1use crate::{
2 build::utils::format_path,
3 config::{tree::RockLayoutConfig, Config},
4 fs,
5 lockfile::{LocalPackage, LocalPackageId, Lockfile, LockfileError, OptState, ReadOnly},
6 lua_version::LuaVersion,
7 package::{PackageName, PackageReq},
8 variables::{GetVariableError, HasVariables},
9};
10use std::{
11 collections::HashMap,
12 io,
13 path::{Path, PathBuf},
14};
15
16use itertools::Itertools;
17use miette::Diagnostic;
18use nonempty::NonEmpty;
19use thiserror::Error;
20mod dist;
21mod list;
22
23pub use dist::*;
24
25const LOCKFILE_NAME: &str = "lux.lock";
26
27pub trait InstallTree {
38 fn version(&self) -> &LuaVersion;
40 fn root(&self) -> PathBuf;
42 fn root_for(&self, package: &LocalPackage) -> PathBuf;
44 fn bin(&self) -> PathBuf;
46 fn unwrapped_bin(&self) -> PathBuf;
48 fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout>;
50 fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout>;
52 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError>;
54 fn lockfile_path(&self) -> PathBuf;
56 fn build_tree(&self, config: &Config) -> Result<Tree, TreeError>;
58 fn test_tree(&self, config: &Config) -> Result<Tree, TreeError>;
60 fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError>;
62 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError>;
64 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError>;
66}
67
68#[derive(Clone, Debug)]
71pub struct Tree {
72 version: LuaVersion,
74 root_parent: PathBuf,
76 entrypoint_layout: RockLayoutConfig,
78 test_tree_dir: PathBuf,
80 build_tree_dir: PathBuf,
82}
83
84#[derive(Debug, Error, Diagnostic)]
85pub enum TreeError {
86 #[error(transparent)]
87 #[diagnostic(transparent)]
88 Fs(#[from] fs::FsError),
89 #[error(transparent)]
90 #[diagnostic(transparent)]
91 Lockfile(#[from] LockfileError),
92}
93
94#[derive(Debug, PartialEq)]
96pub struct RockLayout {
97 pub rock_path: PathBuf,
101 pub etc: PathBuf,
103 pub lib: PathBuf,
107 pub src: PathBuf,
111 pub bin: PathBuf,
116 pub conf: PathBuf,
120 pub doc: PathBuf,
124}
125
126impl RockLayout {
127 pub fn rockspec_path(&self) -> PathBuf {
128 self.rock_path.join("package.rockspec")
129 }
130}
131
132impl HasVariables for RockLayout {
133 #[tracing::instrument(level = "trace")]
134 fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
135 Ok(match var {
136 "PREFIX" => Some(format_path(&self.rock_path)),
137 "LIBDIR" => Some(format_path(&self.lib)),
138 "LUADIR" => Some(format_path(&self.src)),
139 "BINDIR" => Some(format_path(&self.bin)),
140 "CONFDIR" => Some(format_path(&self.conf)),
141 "DOCDIR" => Some(format_path(&self.doc)),
142 _ => None,
143 })
144 }
145}
146
147impl Tree {
148 pub(crate) fn new(
151 root: PathBuf,
152 version: LuaVersion,
153 config: &Config,
154 ) -> Result<Self, TreeError> {
155 let version_dir = root.join(version.to_string());
156 let test_tree_dir = version_dir.join("test_dependencies");
157 let build_tree_dir = version_dir.join("build_dependencies");
158 Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
159 }
160
161 fn new_with_paths(
162 root: PathBuf,
163 test_tree_dir: PathBuf,
164 build_tree_dir: PathBuf,
165 version: LuaVersion,
166 config: &Config,
167 ) -> Result<Self, TreeError> {
168 let path_with_version = root.join(version.to_string());
169
170 fs::sync::create_dir_all(&path_with_version)?;
172
173 let gitignore_file = root.join(".gitignore");
175 fs::sync::write(&gitignore_file, "*")?;
176
177 let bin_dir = path_with_version.join("bin");
179 fs::sync::create_dir_all(&bin_dir)?;
180
181 let lockfile_path = root.join(LOCKFILE_NAME);
182 let rock_layout_config = if lockfile_path.is_file() {
183 let lockfile = Lockfile::load(lockfile_path, None)?;
184 lockfile.entrypoint_layout
185 } else {
186 config.entrypoint_layout().clone()
187 };
188 Ok(Self {
189 root_parent: root,
190 version,
191 entrypoint_layout: rock_layout_config,
192 test_tree_dir,
193 build_tree_dir,
194 })
195 }
196
197 pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
198 where
199 F: Fn(&LocalPackage) -> bool,
200 {
201 match self.list()?.get(req.name()) {
202 Some(packages) => {
203 let found_packages = packages
204 .iter()
205 .rev()
206 .filter(|package| {
207 req.version_req().matches(package.version()) && filter(package)
208 })
209 .map(|package| package.id())
210 .collect_vec();
211
212 Ok(match NonEmpty::try_from(found_packages) {
213 Ok(found_packages) => {
214 if found_packages.len() == 1 {
215 RockMatches::Single(found_packages.last().clone())
216 } else {
217 RockMatches::Many(found_packages)
218 }
219 }
220 Err(_) => RockMatches::NotFound(req.clone()),
221 })
222 }
223 None => Ok(RockMatches::NotFound(req.clone())),
224 }
225 }
226
227 pub(crate) fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
229 mk_rock_layout(
230 &self.root(),
231 &self.bin(),
232 &self.root_for(package),
233 package,
234 &self.entrypoint_layout,
235 )
236 }
237
238 fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
240 mk_rock_layout(
241 &self.root(),
242 &self.bin(),
243 &self.root_for(package),
244 package,
245 &RockLayoutConfig::default(),
246 )
247 }
248}
249
250impl InstallTree for Tree {
251 fn version(&self) -> &LuaVersion {
252 &self.version
253 }
254
255 fn root(&self) -> PathBuf {
256 self.root_parent.join(self.version.to_string())
257 }
258
259 fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout> {
260 let rock_layout = self.entrypoint_layout(package);
261 fs::sync::create_dir_all(&rock_layout.rock_path).map_err(io::Error::other)?;
262 fs::sync::create_dir_all(&rock_layout.lib).map_err(io::Error::other)?;
263 fs::sync::create_dir_all(&rock_layout.src).map_err(io::Error::other)?;
264 Ok(rock_layout)
265 }
266
267 fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout> {
268 let rock_layout = self.dependency_layout(package);
269 fs::sync::create_dir_all(&rock_layout.rock_path).map_err(io::Error::other)?;
270 fs::sync::create_dir_all(&rock_layout.lib).map_err(io::Error::other)?;
271 fs::sync::create_dir_all(&rock_layout.src).map_err(io::Error::other)?;
272 Ok(rock_layout)
273 }
274
275 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
276 Ok(Lockfile::new(
277 self.lockfile_path(),
278 self.entrypoint_layout.clone(),
279 )?)
280 }
281
282 fn lockfile_path(&self) -> PathBuf {
283 self.root().join(LOCKFILE_NAME)
284 }
285
286 fn root_for(&self, package: &LocalPackage) -> PathBuf {
287 self.root().join(format!(
288 "{}-{}@{}",
289 package.id(),
290 package.name(),
291 package.version()
292 ))
293 }
294
295 fn bin(&self) -> PathBuf {
296 self.root().join("bin")
297 }
298
299 fn unwrapped_bin(&self) -> PathBuf {
300 self.bin().join("unwrapped")
301 }
302
303 fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
304 let test_tree_dir = self.test_tree_dir.clone();
305 let build_tree_dir = self.build_tree_dir.clone();
306 Self::new_with_paths(
307 test_tree_dir.clone(),
308 test_tree_dir,
309 build_tree_dir,
310 self.version.clone(),
311 config,
312 )
313 }
314
315 fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
316 let test_tree_dir = self.test_tree_dir.clone();
317 let build_tree_dir = self.build_tree_dir.clone();
318 Self::new_with_paths(
319 build_tree_dir.clone(),
320 test_tree_dir,
321 build_tree_dir,
322 self.version.clone(),
323 config,
324 )
325 }
326
327 fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError> {
329 let lockfile = self.lockfile()?;
330 if lockfile.is_entrypoint(&package.id()) {
331 Ok(self.entrypoint_layout(package))
332 } else {
333 Ok(self.dependency_layout(package))
334 }
335 }
336
337 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError> {
338 Ok(self.lockfile()?.list())
339 }
340
341 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
342 let found_packages = self.lockfile()?.find_rocks(req);
343 Ok(match NonEmpty::try_from(found_packages) {
344 Ok(found_packages) => {
345 if found_packages.len() == 1 {
346 RockMatches::Single(found_packages.last().clone())
347 } else {
348 RockMatches::Many(found_packages)
349 }
350 }
351 Err(_) => RockMatches::NotFound(req.clone()),
352 })
353 }
354}
355
356#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
357pub enum EntryType {
358 Entrypoint,
359 DependencyOnly,
360}
361
362impl EntryType {
363 pub fn is_entrypoint(&self) -> bool {
364 matches!(self, Self::Entrypoint)
365 }
366}
367
368#[derive(Clone, Debug)]
369pub enum RockMatches {
370 NotFound(PackageReq),
371 Single(LocalPackageId),
372 Many(NonEmpty<LocalPackageId>),
373}
374
375impl RockMatches {
377 pub fn is_found(&self) -> bool {
378 matches!(self, Self::Single(_) | Self::Many(_))
379 }
380}
381
382pub fn mk_rock_layout(
384 tree_root: &Path,
385 bin: &Path,
386 rock_path: &Path,
387 package: &LocalPackage,
388 layout_config: &RockLayoutConfig,
389) -> RockLayout {
390 let (etc, lib, src) = if let Some(ref root) = layout_config.root {
391 let base = tree_root.join(root);
392
393 let etc = match package.spec.opt {
394 OptState::Required => base.join(&layout_config.etc),
395 OptState::Optional => base.join(&layout_config.opt_etc),
396 }
397 .join(format!("{}", package.name()));
398
399 let lib = etc.join(&layout_config.lib);
400 let src = etc.join(&layout_config.src);
401
402 (etc, lib, src)
403 } else {
404 let etc = rock_path.join(&layout_config.etc);
405 let lib = rock_path.join(&layout_config.lib);
406 let src = rock_path.join(&layout_config.src);
407
408 (etc, lib, src)
409 };
410 let conf = etc.join(&layout_config.conf);
411 let doc = etc.join(&layout_config.doc);
412
413 RockLayout {
414 rock_path: rock_path.to_path_buf(),
415 etc,
416 lib,
417 src,
418 bin: bin.to_path_buf(),
419 conf,
420 doc,
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use assert_fs::prelude::PathCopy;
427 use itertools::Itertools;
428 use std::path::PathBuf;
429
430 use insta::assert_yaml_snapshot;
431
432 use crate::{
433 config::{tree::RockLayoutConfig, ConfigBuilder},
434 lockfile::{LocalPackage, LocalPackageHashes, LockConstraint},
435 lua_version::LuaVersion,
436 package::{PackageName, PackageSpec, PackageVersion},
437 remote_package_source::RemotePackageSource,
438 rockspec::RockBinaries,
439 tree::{InstallTree, RockLayout},
440 variables,
441 };
442
443 #[test]
444 fn rock_layout() {
445 let tree_path =
446 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
447
448 let temp = assert_fs::TempDir::new().unwrap();
449 temp.copy_from(&tree_path, &["**"]).unwrap();
450 let tree_path = temp.to_path_buf();
451
452 let config = ConfigBuilder::new()
453 .unwrap()
454 .user_tree(Some(tree_path.clone()))
455 .build()
456 .unwrap();
457 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
458
459 let mock_hashes = LocalPackageHashes {
460 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
461 .parse()
462 .unwrap(),
463 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
464 .parse()
465 .unwrap(),
466 };
467
468 let package = LocalPackage::from(
469 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
470 LockConstraint::Unconstrained,
471 RockBinaries::default(),
472 RemotePackageSource::Test,
473 None,
474 mock_hashes.clone(),
475 );
476
477 let id = package.id();
478
479 let neorg = tree.dependency(&package).unwrap();
480
481 assert_eq!(
482 neorg,
483 RockLayout {
484 bin: tree_path.join("5.1/bin"),
485 rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
486 etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
487 lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
488 src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
489 conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
490 doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
491 }
492 );
493
494 let package = LocalPackage::from(
495 &PackageSpec::parse("lua-cjson".into(), "2.1.0-1".into()).unwrap(),
496 LockConstraint::Unconstrained,
497 RockBinaries::default(),
498 RemotePackageSource::Test,
499 None,
500 mock_hashes.clone(),
501 );
502
503 let id = package.id();
504
505 let lua_cjson = tree.dependency(&package).unwrap();
506
507 assert_eq!(
508 lua_cjson,
509 RockLayout {
510 bin: tree_path.join("5.1/bin"),
511 rock_path: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1")),
512 etc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc")),
513 lib: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/lib")),
514 src: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/src")),
515 conf: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/conf")),
516 doc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/doc")),
517 }
518 );
519 }
520
521 #[test]
522 fn tree_list() {
523 let tree_path =
524 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
525
526 let temp = assert_fs::TempDir::new().unwrap();
527 temp.copy_from(&tree_path, &["**"]).unwrap();
528 let tree_path = temp.to_path_buf();
529
530 let config = ConfigBuilder::new()
531 .unwrap()
532 .user_tree(Some(tree_path.clone()))
533 .build()
534 .unwrap();
535 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
536 let result = tree.list().unwrap();
537 let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
539 .into_iter()
540 .sorted()
541 .map(|(name, package)| {
542 (
543 name,
544 package
545 .into_iter()
546 .map(|package| package.spec.version)
547 .sorted()
548 .collect_vec(),
549 )
550 })
551 .collect_vec();
552
553 assert_yaml_snapshot!(sorted_result)
554 }
555
556 #[test]
557 fn rock_layout_nvim() {
558 let tree_path =
559 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
560
561 let temp = assert_fs::TempDir::new().unwrap();
562 temp.copy_from(&tree_path, &["**"]).unwrap();
563
564 let tree_path = temp.to_path_buf();
565
566 let config = ConfigBuilder::new()
567 .unwrap()
568 .user_tree(Some(tree_path.clone()))
569 .entrypoint_layout(RockLayoutConfig::new_nvim_layout())
570 .build()
571 .unwrap();
572
573 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
574
575 let mock_hashes = LocalPackageHashes {
576 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
577 .parse()
578 .unwrap(),
579 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
580 .parse()
581 .unwrap(),
582 };
583
584 let package = LocalPackage::from(
585 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
586 LockConstraint::Unconstrained,
587 RockBinaries::default(),
588 RemotePackageSource::Test,
589 None,
590 mock_hashes,
591 );
592
593 let id = package.id();
594 let neorg = tree.entrypoint(&package).unwrap();
595
596 assert_eq!(
597 neorg,
598 RockLayout {
599 bin: tree_path.join("5.1/bin"),
600 rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
601 etc: tree_path.join("5.1/site/pack/lux/start/neorg"),
602 lib: tree_path.join("5.1/site/pack/lux/start/neorg/lib"),
603 src: tree_path.join("5.1/site/pack/lux/start/neorg/lua"),
604 conf: tree_path.join("5.1/site/pack/lux/start/neorg/conf"),
605 doc: tree_path.join("5.1/site/pack/lux/start/neorg/doc"),
606 }
607 );
608 }
609
610 #[test]
611 fn rock_layout_substitute() {
612 let tree_path =
613 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
614
615 let temp = assert_fs::TempDir::new().unwrap();
616 temp.copy_from(&tree_path, &["**"]).unwrap();
617 let tree_path = temp.to_path_buf();
618
619 let config = ConfigBuilder::new()
620 .unwrap()
621 .user_tree(Some(tree_path.clone()))
622 .build()
623 .unwrap();
624 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
625
626 let mock_hashes = LocalPackageHashes {
627 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
628 .parse()
629 .unwrap(),
630 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
631 .parse()
632 .unwrap(),
633 };
634
635 let neorg = tree
636 .dependency(&LocalPackage::from(
637 &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
638 LockConstraint::Unconstrained,
639 RockBinaries::default(),
640 RemotePackageSource::Test,
641 None,
642 mock_hashes.clone(),
643 ))
644 .unwrap();
645 let build_variables = vec![
646 "$(PREFIX)",
647 "$(LIBDIR)",
648 "$(LUADIR)",
649 "$(BINDIR)",
650 "$(CONFDIR)",
651 "$(DOCDIR)",
652 ];
653 let result: Vec<String> = build_variables
654 .into_iter()
655 .map(|var| variables::substitute(&[&neorg], var))
656 .try_collect()
657 .unwrap();
658 assert_eq!(
659 result,
660 vec![
661 neorg.rock_path.to_string_lossy().to_string(),
662 neorg.lib.to_string_lossy().to_string(),
663 neorg.src.to_string_lossy().to_string(),
664 neorg.bin.to_string_lossy().to_string(),
665 neorg.conf.to_string_lossy().to_string(),
666 neorg.doc.to_string_lossy().to_string(),
667 ]
668 );
669 }
670}