1use crate::{
2 build::utils::format_path,
3 config::Config,
4 fs,
5 lockfile::{LocalPackage, LocalPackageId, Lockfile, LockfileError, ReadOnly},
6 lua_version::LuaVersion,
7 package::{PackageName, PackageReq},
8 variables::{GetVariableError, HasVariables},
9};
10use std::{collections::HashMap, io, path::PathBuf, sync::Arc};
11
12use itertools::Itertools;
13use miette::Diagnostic;
14use nonempty::NonEmpty;
15use thiserror::Error;
16mod dist;
17mod layout;
18mod list;
19
20pub use dist::*;
21pub use layout::*;
22
23const LOCKFILE_NAME: &str = "lux.lock";
24
25pub trait InstallTree {
36 fn version(&self) -> &LuaVersion;
38 fn root(&self) -> PathBuf;
40 fn bin(&self) -> PathBuf;
42 fn unwrapped_bin(&self) -> PathBuf;
44 fn layout_for(&self, package: &LocalPackage) -> RockLayout;
46 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError>;
48 fn lockfile_path(&self) -> PathBuf;
50 fn build_tree(&self, config: &Config) -> Result<Tree, TreeError>;
52 fn test_tree(&self, config: &Config) -> Result<Tree, TreeError>;
54 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError>;
56 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError>;
58 fn prepare(&self, package: &LocalPackage) -> Result<(), TreeError>;
60 fn finalize(&self, _package: &LocalPackage, _entry_type: EntryType) -> Result<(), TreeError> {
65 Ok(())
66 }
67 fn cleanup(&self, package: &LocalPackage, entry_type: EntryType) -> Result<(), TreeError>;
71}
72
73#[derive(Debug, PartialEq, Eq, Clone)]
75pub struct RockLayout {
76 pub root: PathBuf,
80 pub etc: PathBuf,
82 pub lib: PathBuf,
86 pub src: PathBuf,
90 pub bin: PathBuf,
95 pub conf: PathBuf,
99 pub doc: PathBuf,
103}
104
105impl RockLayout {
106 pub fn new(root: PathBuf, bin: PathBuf) -> Self {
108 let etc = root.join("etc");
109 let lib = root.join("lib");
110 let src = root.join("src");
111 let conf = etc.join("conf");
112 let doc = etc.join("doc");
113 Self {
114 root,
115 etc,
116 lib,
117 src,
118 bin,
119 conf,
120 doc,
121 }
122 }
123
124 pub fn rockspec_path(&self) -> PathBuf {
126 self.root.join("package.rockspec")
127 }
128}
129
130#[derive(Clone, Debug)]
133pub struct Tree {
134 version: LuaVersion,
136 root_parent: PathBuf,
138 test_tree_dir: PathBuf,
140 build_tree_dir: PathBuf,
142 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
144}
145
146#[derive(Debug, Error, Diagnostic)]
147pub enum TreeError {
148 #[error(transparent)]
149 #[diagnostic(transparent)]
150 Fs(#[from] fs::FsError),
151 #[error(transparent)]
152 #[diagnostic(transparent)]
153 Lockfile(#[from] LockfileError),
154 #[error(transparent)]
155 Io(#[from] io::Error),
156}
157
158impl HasVariables for RockLayout {
159 fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
160 Ok(match var {
161 "PREFIX" => Some(format_path(&self.root)),
162 "LIBDIR" => Some(format_path(&self.lib)),
163 "LUADIR" => Some(format_path(&self.src)),
164 "BINDIR" => Some(format_path(&self.bin)),
165 "CONFDIR" => Some(format_path(&self.conf)),
166 "DOCDIR" => Some(format_path(&self.doc)),
167 _ => None,
168 })
169 }
170}
171
172impl Tree {
173 pub(crate) fn new(
176 root: PathBuf,
177 version: LuaVersion,
178 config: &Config,
179 ) -> Result<Self, TreeError> {
180 let version_dir = root.join(version.to_string());
181 let test_tree_dir = version_dir.join("test_dependencies");
182 let build_tree_dir = version_dir.join("build_dependencies");
183 Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
184 }
185
186 fn new_with_paths(
187 root: PathBuf,
188 test_tree_dir: PathBuf,
189 build_tree_dir: PathBuf,
190 version: LuaVersion,
191 config: &Config,
192 ) -> Result<Self, TreeError> {
193 let path_with_version = root.join(version.to_string());
194
195 fs::sync::create_dir_all(&path_with_version)?;
197
198 let gitignore_file = root.join(".gitignore");
200 fs::sync::write(&gitignore_file, "*")?;
201
202 let bin_dir = path_with_version.join("bin");
204 fs::sync::create_dir_all(&bin_dir)?;
205
206 Ok(Self {
207 root_parent: root,
208 version,
209 test_tree_dir,
210 build_tree_dir,
211 entrypoint_layout: config.entrypoint_layout().cloned(),
212 })
213 }
214
215 pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
216 where
217 F: Fn(&LocalPackage) -> bool,
218 {
219 match self.list()?.get(req.name()) {
220 Some(packages) => {
221 let found_packages = packages
222 .iter()
223 .rev()
224 .filter(|package| {
225 req.version_req().matches(package.version()) && filter(package)
226 })
227 .map(|package| package.id())
228 .collect_vec();
229
230 Ok(match NonEmpty::try_from(found_packages) {
231 Ok(found_packages) => {
232 if found_packages.len() == 1 {
233 RockMatches::Single(found_packages.last().clone())
234 } else {
235 RockMatches::Many(found_packages)
236 }
237 }
238 Err(_) => RockMatches::NotFound(req.clone()),
239 })
240 }
241 None => Ok(RockMatches::NotFound(req.clone())),
242 }
243 }
244}
245
246impl InstallTree for Tree {
247 fn version(&self) -> &LuaVersion {
248 &self.version
249 }
250
251 fn root(&self) -> PathBuf {
252 self.root_parent.join(self.version.to_string())
253 }
254
255 fn prepare(&self, package: &LocalPackage) -> Result<(), TreeError> {
256 let layout = self.layout_for(package);
257 fs::sync::create_dir_all(&layout.root)?;
258 fs::sync::create_dir_all(&layout.lib)?;
259 fs::sync::create_dir_all(&layout.src)?;
260 fs::sync::create_dir_all(&layout.etc)?;
261
262 Ok(())
263 }
264
265 fn finalize(&self, package: &LocalPackage, entry_type: EntryType) -> Result<(), TreeError> {
266 if entry_type.is_entrypoint() {
267 if let Some(custom_layout) = &self.entrypoint_layout {
268 custom_layout.make_symlinks(self, package)?;
269 }
270 }
271
272 Ok(())
273 }
274
275 fn cleanup(&self, package: &LocalPackage, entry_type: EntryType) -> Result<(), TreeError> {
276 if entry_type.is_entrypoint() {
277 if let Some(layout) = &self.entrypoint_layout {
278 layout.remove_symlinks(self, package)?;
279 }
280 }
281
282 let layout = self.layout_for(package);
283 fs::sync::remove_dir_all(&layout.etc)?;
284 fs::sync::remove_dir_all(&layout.root)?;
285
286 for relative_binary_path in package.spec.binaries() {
287 if let Some(binary_file_name) = relative_binary_path.file_name() {
288 let binary_path = self.bin().join(binary_file_name);
289 if binary_path.is_file() {
290 fs::sync::remove_file(binary_path)?;
291 }
292
293 let unwrapped_binary_path = self.unwrapped_bin().join(binary_file_name);
294 if unwrapped_binary_path.is_file() {
295 fs::sync::remove_file(unwrapped_binary_path)?;
296 }
297 }
298 }
299
300 Ok(())
301 }
302
303 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
304 Ok(Lockfile::new(self.lockfile_path())?)
305 }
306
307 fn lockfile_path(&self) -> PathBuf {
308 self.root().join(LOCKFILE_NAME)
309 }
310
311 fn layout_for(&self, package: &LocalPackage) -> RockLayout {
312 RockLayout::new(
313 self.root().join(format!(
314 "{}-{}@{}",
315 package.id(),
316 package.name(),
317 package.version()
318 )),
319 self.bin(),
320 )
321 }
322
323 fn bin(&self) -> PathBuf {
324 self.root().join("bin")
325 }
326
327 fn unwrapped_bin(&self) -> PathBuf {
328 self.bin().join("unwrapped")
329 }
330
331 fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
332 let test_tree_dir = self.test_tree_dir.clone();
333 let build_tree_dir = self.build_tree_dir.clone();
334 Self::new_with_paths(
335 test_tree_dir.clone(),
336 test_tree_dir,
337 build_tree_dir,
338 self.version.clone(),
339 config,
340 )
341 }
342
343 fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
344 let test_tree_dir = self.test_tree_dir.clone();
345 let build_tree_dir = self.build_tree_dir.clone();
346 Self::new_with_paths(
347 build_tree_dir.clone(),
348 test_tree_dir,
349 build_tree_dir,
350 self.version.clone(),
351 config,
352 )
353 }
354
355 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError> {
356 Ok(self.lockfile()?.list())
357 }
358
359 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
360 let found_packages = self.lockfile()?.find_rocks(req);
361 Ok(match NonEmpty::try_from(found_packages) {
362 Ok(found_packages) => {
363 if found_packages.len() == 1 {
364 RockMatches::Single(found_packages.last().clone())
365 } else {
366 RockMatches::Many(found_packages)
367 }
368 }
369 Err(_) => RockMatches::NotFound(req.clone()),
370 })
371 }
372}
373
374#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
375pub enum EntryType {
376 Entrypoint,
377 DependencyOnly,
378}
379
380impl EntryType {
381 pub fn is_entrypoint(&self) -> bool {
382 matches!(self, Self::Entrypoint)
383 }
384}
385
386#[derive(Clone, Debug)]
387pub enum RockMatches {
388 NotFound(PackageReq),
389 Single(LocalPackageId),
390 Many(NonEmpty<LocalPackageId>),
391}
392
393impl RockMatches {
395 pub fn is_found(&self) -> bool {
396 matches!(self, Self::Single(_) | Self::Many(_))
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use assert_fs::prelude::PathCopy;
403 use itertools::Itertools;
404 use std::path::PathBuf;
405
406 use insta::assert_yaml_snapshot;
407
408 use crate::{
409 config::ConfigBuilder,
410 lockfile::{LocalPackage, LocalPackageHashes, LockConstraint},
411 lua_version::LuaVersion,
412 package::{PackageName, PackageSpec, PackageVersion},
413 remote_package_source::RemotePackageSource,
414 rockspec::RockBinaries,
415 tree::{InstallTree, RockLayout},
416 variables,
417 };
418
419 #[test]
420 fn rock_layout() {
421 let tree_path =
422 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
423
424 let temp = assert_fs::TempDir::new().unwrap();
425 temp.copy_from(&tree_path, &["**"]).unwrap();
426 let tree_path = temp.to_path_buf();
427
428 let config = ConfigBuilder::new()
429 .unwrap()
430 .user_tree(Some(tree_path.clone()))
431 .build()
432 .unwrap();
433 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
434
435 let mock_hashes = LocalPackageHashes {
436 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
437 .parse()
438 .unwrap(),
439 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
440 .parse()
441 .unwrap(),
442 };
443
444 let package = LocalPackage::from(
445 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
446 LockConstraint::Unconstrained,
447 RockBinaries::default(),
448 RemotePackageSource::Test,
449 None,
450 mock_hashes.clone(),
451 );
452
453 let id = package.id();
454 tree.prepare(&package).unwrap();
455
456 assert_eq!(
457 tree.layout_for(&package),
458 RockLayout {
459 bin: tree_path.join("5.1/bin"),
460 root: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
461 etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
462 lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
463 src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
464 conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
465 doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
466 }
467 );
468 }
469
470 #[test]
471 fn tree_list() {
472 let tree_path =
473 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
474
475 let temp = assert_fs::TempDir::new().unwrap();
476 temp.copy_from(&tree_path, &["**"]).unwrap();
477 let tree_path = temp.to_path_buf();
478
479 let config = ConfigBuilder::new()
480 .unwrap()
481 .user_tree(Some(tree_path.clone()))
482 .build()
483 .unwrap();
484 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
485 let result = tree.list().unwrap();
486 let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
488 .into_iter()
489 .sorted()
490 .map(|(name, package)| {
491 (
492 name,
493 package
494 .into_iter()
495 .map(|package| package.spec.version)
496 .sorted()
497 .collect_vec(),
498 )
499 })
500 .collect_vec();
501
502 assert_yaml_snapshot!(sorted_result)
503 }
504
505 #[test]
506 fn rock_layout_substitute() {
507 let tree_path =
508 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
509
510 let temp = assert_fs::TempDir::new().unwrap();
511 temp.copy_from(&tree_path, &["**"]).unwrap();
512 let tree_path = temp.to_path_buf();
513
514 let config = ConfigBuilder::new()
515 .unwrap()
516 .user_tree(Some(tree_path.clone()))
517 .build()
518 .unwrap();
519 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
520
521 let mock_hashes = LocalPackageHashes {
522 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
523 .parse()
524 .unwrap(),
525 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
526 .parse()
527 .unwrap(),
528 };
529
530 let package = LocalPackage::from(
531 &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
532 LockConstraint::Unconstrained,
533 RockBinaries::default(),
534 RemotePackageSource::Test,
535 None,
536 mock_hashes.clone(),
537 );
538 let layout = tree.layout_for(&package);
539 let build_variables = vec![
540 "$(PREFIX)",
541 "$(LIBDIR)",
542 "$(LUADIR)",
543 "$(BINDIR)",
544 "$(CONFDIR)",
545 "$(DOCDIR)",
546 ];
547 let result: Vec<String> = build_variables
548 .into_iter()
549 .map(|var| variables::substitute(&[&layout], var))
550 .try_collect()
551 .unwrap();
552 assert_eq!(
553 result,
554 vec![
555 layout.root.to_string_lossy().to_string(),
556 layout.lib.to_string_lossy().to_string(),
557 layout.src.to_string_lossy().to_string(),
558 layout.bin.to_string_lossy().to_string(),
559 layout.conf.to_string_lossy().to_string(),
560 layout.doc.to_string_lossy().to_string(),
561 ]
562 );
563 }
564}