Skip to main content

lux_lib/tree/
mod.rs

1use crate::{
2    build::utils::format_path,
3    config::{tree::RockLayoutConfig, Config},
4    lockfile::{LocalPackage, LocalPackageId, Lockfile, LockfileError, OptState, ReadOnly},
5    lua_version::LuaVersion,
6    package::PackageReq,
7    variables::{GetVariableError, HasVariables},
8};
9use std::{io, path::PathBuf};
10
11use itertools::Itertools;
12use nonempty::NonEmpty;
13use thiserror::Error;
14
15mod list;
16
17const LOCKFILE_NAME: &str = "lux.lock";
18
19/// A tree is a collection of files where installed rocks are located.
20///
21/// `lux` diverges from the traditional hierarchy employed by luarocks.
22/// Instead, we opt for a much simpler approach:
23///
24/// - /rocks/<lua-version> - contains rocks
25/// - /rocks/<lua-version>/<rock>/etc - documentation and supplementary files for the rock
26/// - /rocks/<lua-version>/<rock>/lib - shared libraries (.so files)
27/// - /rocks/<lua-version>/<rock>/src - library code for the rock
28/// - /bin - binary files produced by various rocks
29
30#[derive(Clone, Debug)]
31pub struct Tree {
32    /// The Lua version of the tree.
33    version: LuaVersion,
34    /// The parent of this tree's root directory.
35    root_parent: PathBuf,
36    /// The rock layout config for this tree
37    entrypoint_layout: RockLayoutConfig,
38    /// The root of this tree's test dependency tree.
39    test_tree_dir: PathBuf,
40    /// The root of this tree's build dependency tree.
41    build_tree_dir: PathBuf,
42}
43
44#[derive(Debug, Error)]
45pub enum TreeError {
46    #[error("unable to create directory {0}:\n{1}")]
47    CreateDir(String, io::Error),
48    #[error("unable to write to {0}:\n{1}")]
49    WriteFile(String, io::Error),
50    #[error(transparent)]
51    Lockfile(#[from] LockfileError),
52}
53
54/// Change-agnostic way of referencing various paths for a rock.
55#[derive(Debug, PartialEq)]
56pub struct RockLayout {
57    /// The local installation directory.
58    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
59    /// using `$(PREFIX)`.
60    pub rock_path: PathBuf,
61    /// The `etc` directory, containing resources.
62    pub etc: PathBuf,
63    /// The `lib` directory, containing native libraries.
64    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
65    /// using `$(LIBDIR)`.
66    pub lib: PathBuf,
67    /// The `src` directory, containing Lua sources.
68    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
69    /// using `$(LUADIR)`.
70    pub src: PathBuf,
71    /// The `bin` directory, containing executables.
72    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
73    /// using `$(BINDIR)`.
74    /// This points to a global binary path at the root of the current tree by default.
75    pub bin: PathBuf,
76    /// The `etc/conf` directory, containing configuration files.
77    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
78    /// using `$(CONFDIR)`.
79    pub conf: PathBuf,
80    /// The `etc/doc` directory, containing documentation files.
81    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
82    /// using `$(DOCDIR)`.
83    pub doc: PathBuf,
84}
85
86impl RockLayout {
87    pub fn rockspec_path(&self) -> PathBuf {
88        self.rock_path.join("package.rockspec")
89    }
90}
91
92impl HasVariables for RockLayout {
93    fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
94        Ok(match var {
95            "PREFIX" => Some(format_path(&self.rock_path)),
96            "LIBDIR" => Some(format_path(&self.lib)),
97            "LUADIR" => Some(format_path(&self.src)),
98            "BINDIR" => Some(format_path(&self.bin)),
99            "CONFDIR" => Some(format_path(&self.conf)),
100            "DOCDIR" => Some(format_path(&self.doc)),
101            _ => None,
102        })
103    }
104}
105
106impl Tree {
107    /// NOTE: This is exposed for use by the config module.
108    /// Use `Config::tree()`
109    pub(crate) fn new(
110        root: PathBuf,
111        version: LuaVersion,
112        config: &Config,
113    ) -> Result<Self, TreeError> {
114        let version_dir = root.join(version.to_string());
115        let test_tree_dir = version_dir.join("test_dependencies");
116        let build_tree_dir = version_dir.join("build_dependencies");
117        Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
118    }
119
120    fn new_with_paths(
121        root: PathBuf,
122        test_tree_dir: PathBuf,
123        build_tree_dir: PathBuf,
124        version: LuaVersion,
125        config: &Config,
126    ) -> Result<Self, TreeError> {
127        let path_with_version = root.join(version.to_string());
128
129        // Ensure that the root and the version directory exist.
130        std::fs::create_dir_all(&path_with_version).map_err(|err| {
131            TreeError::CreateDir(path_with_version.to_string_lossy().to_string(), err)
132        })?;
133
134        // In case the tree is in a git repository, we tell git to ignore it.
135        let gitignore_file = root.join(".gitignore");
136        std::fs::write(&gitignore_file, "*").map_err(|err| {
137            TreeError::WriteFile(gitignore_file.to_string_lossy().to_string(), err)
138        })?;
139
140        // Ensure that the bin directory exists.
141        let bin_dir = path_with_version.join("bin");
142        std::fs::create_dir_all(&bin_dir)
143            .map_err(|err| TreeError::CreateDir(bin_dir.to_string_lossy().to_string(), err))?;
144
145        let lockfile_path = root.join(LOCKFILE_NAME);
146        let rock_layout_config = if lockfile_path.is_file() {
147            let lockfile = Lockfile::load(lockfile_path, None)?;
148            lockfile.entrypoint_layout
149        } else {
150            config.entrypoint_layout().clone()
151        };
152        Ok(Self {
153            root_parent: root,
154            version,
155            entrypoint_layout: rock_layout_config,
156            test_tree_dir,
157            build_tree_dir,
158        })
159    }
160
161    /// The root of the tree
162    pub fn root(&self) -> PathBuf {
163        self.root_parent.join(self.version.to_string())
164    }
165
166    pub fn version(&self) -> &LuaVersion {
167        &self.version
168    }
169
170    pub fn root_for(&self, package: &LocalPackage) -> PathBuf {
171        self.root().join(format!(
172            "{}-{}@{}",
173            package.id(),
174            package.name(),
175            package.version()
176        ))
177    }
178
179    pub fn bin(&self) -> PathBuf {
180        self.root().join("bin")
181    }
182
183    /// Directory containing unwrapped Lua scripts
184    /// The wrapped scripts are in `Self::bin()`
185    pub(crate) fn unwrapped_bin(&self) -> PathBuf {
186        self.bin().join("unwrapped")
187    }
188
189    pub fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
190        let found_packages = self.lockfile()?.find_rocks(req);
191        Ok(match NonEmpty::try_from(found_packages) {
192            Ok(found_packages) => {
193                if found_packages.len() == 1 {
194                    RockMatches::Single(found_packages.last().clone())
195                } else {
196                    RockMatches::Many(found_packages)
197                }
198            }
199            Err(_) => RockMatches::NotFound(req.clone()),
200        })
201    }
202
203    pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
204    where
205        F: Fn(&LocalPackage) -> bool,
206    {
207        match self.list()?.get(req.name()) {
208            Some(packages) => {
209                let found_packages = packages
210                    .iter()
211                    .rev()
212                    .filter(|package| {
213                        req.version_req().matches(package.version()) && filter(package)
214                    })
215                    .map(|package| package.id())
216                    .collect_vec();
217
218                Ok(match NonEmpty::try_from(found_packages) {
219                    Ok(found_packages) => {
220                        if found_packages.len() == 1 {
221                            RockMatches::Single(found_packages.last().clone())
222                        } else {
223                            RockMatches::Many(found_packages)
224                        }
225                    }
226                    Err(_) => RockMatches::NotFound(req.clone()),
227                })
228            }
229            None => Ok(RockMatches::NotFound(req.clone())),
230        }
231    }
232
233    /// Get the `RockLayout` for an installed package.
234    pub fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError> {
235        let lockfile = self.lockfile()?;
236        if lockfile.is_entrypoint(&package.id()) {
237            Ok(self.entrypoint_layout(package))
238        } else {
239            Ok(self.dependency_layout(package))
240        }
241    }
242
243    /// Create a `RockLayout` for an entrypoint
244    pub fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
245        self.mk_rock_layout(package, &self.entrypoint_layout)
246    }
247
248    /// Create a `RockLayout` for a dependency
249    pub fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
250        self.mk_rock_layout(package, &RockLayoutConfig::default())
251    }
252
253    /// Create a `RockLayout` for a package.
254    fn mk_rock_layout(
255        &self,
256        package: &LocalPackage,
257        layout_config: &RockLayoutConfig,
258    ) -> RockLayout {
259        let rock_path = self.root_for(package);
260        let bin = self.bin();
261        let etc_root = match layout_config.etc_root {
262            Some(ref etc_root) => self.root().join(etc_root),
263            None => rock_path.clone(),
264        };
265        let mut etc = match package.spec.opt {
266            OptState::Required => etc_root.join(&layout_config.etc),
267            OptState::Optional => etc_root.join(&layout_config.opt_etc),
268        };
269        if layout_config.etc_root.is_some() {
270            etc = etc.join(format!("{}", package.name()));
271        }
272        let lib = rock_path.join("lib");
273        let src = rock_path.join("src");
274        let conf = etc.join(&layout_config.conf);
275        let doc = etc.join(&layout_config.doc);
276
277        RockLayout {
278            rock_path,
279            etc,
280            lib,
281            src,
282            bin,
283            conf,
284            doc,
285        }
286    }
287
288    /// Create a `RockLayout` for an entrypoint package, creating the `lib` and `src` directories.
289    pub fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout> {
290        let rock_layout = self.entrypoint_layout(package);
291        std::fs::create_dir_all(&rock_layout.lib)?;
292        std::fs::create_dir_all(&rock_layout.src)?;
293        Ok(rock_layout)
294    }
295
296    /// Create a `RockLayout` for a dependency package, creating the `lib` and `src` directories.
297    pub fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout> {
298        let rock_layout = self.dependency_layout(package);
299        std::fs::create_dir_all(&rock_layout.lib)?;
300        std::fs::create_dir_all(&rock_layout.src)?;
301        Ok(rock_layout)
302    }
303
304    pub fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
305        Ok(Lockfile::new(
306            self.lockfile_path(),
307            self.entrypoint_layout.clone(),
308        )?)
309    }
310
311    /// Get this tree's lockfile path.
312    pub fn lockfile_path(&self) -> PathBuf {
313        self.root().join(LOCKFILE_NAME)
314    }
315
316    /// The tree in which to install test dependencies
317    pub fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
318        let test_tree_dir = self.test_tree_dir.clone();
319        let build_tree_dir = self.build_tree_dir.clone();
320        Self::new_with_paths(
321            test_tree_dir.clone(),
322            test_tree_dir,
323            build_tree_dir,
324            self.version.clone(),
325            config,
326        )
327    }
328
329    /// The tree in which to install build dependencies
330    pub fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
331        let test_tree_dir = self.test_tree_dir.clone();
332        let build_tree_dir = self.build_tree_dir.clone();
333        Self::new_with_paths(
334            build_tree_dir.clone(),
335            test_tree_dir,
336            build_tree_dir,
337            self.version.clone(),
338            config,
339        )
340    }
341}
342
343#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
344pub enum EntryType {
345    Entrypoint,
346    DependencyOnly,
347}
348
349impl EntryType {
350    pub fn is_entrypoint(&self) -> bool {
351        matches!(self, Self::Entrypoint)
352    }
353}
354
355#[derive(Clone, Debug)]
356pub enum RockMatches {
357    NotFound(PackageReq),
358    Single(LocalPackageId),
359    Many(NonEmpty<LocalPackageId>),
360}
361
362// Loosely mimic the Option<T> functions.
363impl RockMatches {
364    pub fn is_found(&self) -> bool {
365        matches!(self, Self::Single(_) | Self::Many(_))
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use assert_fs::prelude::PathCopy;
372    use itertools::Itertools;
373    use std::path::PathBuf;
374
375    use insta::assert_yaml_snapshot;
376
377    use crate::{
378        config::ConfigBuilder,
379        lockfile::{LocalPackage, LocalPackageHashes, LockConstraint},
380        lua_version::LuaVersion,
381        package::{PackageName, PackageSpec, PackageVersion},
382        remote_package_source::RemotePackageSource,
383        rockspec::RockBinaries,
384        tree::RockLayout,
385        variables,
386    };
387
388    #[test]
389    fn rock_layout() {
390        let tree_path =
391            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
392
393        let temp = assert_fs::TempDir::new().unwrap();
394        temp.copy_from(&tree_path, &["**"]).unwrap();
395        let tree_path = temp.to_path_buf();
396
397        let config = ConfigBuilder::new()
398            .unwrap()
399            .user_tree(Some(tree_path.clone()))
400            .build()
401            .unwrap();
402        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
403
404        let mock_hashes = LocalPackageHashes {
405            rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
406                .parse()
407                .unwrap(),
408            source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
409                .parse()
410                .unwrap(),
411        };
412
413        let package = LocalPackage::from(
414            &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
415            LockConstraint::Unconstrained,
416            RockBinaries::default(),
417            RemotePackageSource::Test,
418            None,
419            mock_hashes.clone(),
420        );
421
422        let id = package.id();
423
424        let neorg = tree.dependency(&package).unwrap();
425
426        assert_eq!(
427            neorg,
428            RockLayout {
429                bin: tree_path.join("5.1/bin"),
430                rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
431                etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
432                lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
433                src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
434                conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
435                doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
436            }
437        );
438
439        let package = LocalPackage::from(
440            &PackageSpec::parse("lua-cjson".into(), "2.1.0-1".into()).unwrap(),
441            LockConstraint::Unconstrained,
442            RockBinaries::default(),
443            RemotePackageSource::Test,
444            None,
445            mock_hashes.clone(),
446        );
447
448        let id = package.id();
449
450        let lua_cjson = tree.dependency(&package).unwrap();
451
452        assert_eq!(
453            lua_cjson,
454            RockLayout {
455                bin: tree_path.join("5.1/bin"),
456                rock_path: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1")),
457                etc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc")),
458                lib: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/lib")),
459                src: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/src")),
460                conf: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/conf")),
461                doc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/doc")),
462            }
463        );
464    }
465
466    #[test]
467    fn tree_list() {
468        let tree_path =
469            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
470
471        let temp = assert_fs::TempDir::new().unwrap();
472        temp.copy_from(&tree_path, &["**"]).unwrap();
473        let tree_path = temp.to_path_buf();
474
475        let config = ConfigBuilder::new()
476            .unwrap()
477            .user_tree(Some(tree_path.clone()))
478            .build()
479            .unwrap();
480        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
481        let result = tree.list().unwrap();
482        // note: sorted_redaction doesn't work because we have a nested Vec
483        let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
484            .into_iter()
485            .sorted()
486            .map(|(name, package)| {
487                (
488                    name,
489                    package
490                        .into_iter()
491                        .map(|package| package.spec.version)
492                        .sorted()
493                        .collect_vec(),
494                )
495            })
496            .collect_vec();
497
498        assert_yaml_snapshot!(sorted_result)
499    }
500
501    #[test]
502    fn rock_layout_substitute() {
503        let tree_path =
504            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
505
506        let temp = assert_fs::TempDir::new().unwrap();
507        temp.copy_from(&tree_path, &["**"]).unwrap();
508        let tree_path = temp.to_path_buf();
509
510        let config = ConfigBuilder::new()
511            .unwrap()
512            .user_tree(Some(tree_path.clone()))
513            .build()
514            .unwrap();
515        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
516
517        let mock_hashes = LocalPackageHashes {
518            rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
519                .parse()
520                .unwrap(),
521            source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
522                .parse()
523                .unwrap(),
524        };
525
526        let neorg = tree
527            .dependency(&LocalPackage::from(
528                &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
529                LockConstraint::Unconstrained,
530                RockBinaries::default(),
531                RemotePackageSource::Test,
532                None,
533                mock_hashes.clone(),
534            ))
535            .unwrap();
536        let build_variables = vec![
537            "$(PREFIX)",
538            "$(LIBDIR)",
539            "$(LUADIR)",
540            "$(BINDIR)",
541            "$(CONFDIR)",
542            "$(DOCDIR)",
543        ];
544        let result: Vec<String> = build_variables
545            .into_iter()
546            .map(|var| variables::substitute(&[&neorg], var))
547            .try_collect()
548            .unwrap();
549        assert_eq!(
550            result,
551            vec![
552                neorg.rock_path.to_string_lossy().to_string(),
553                neorg.lib.to_string_lossy().to_string(),
554                neorg.src.to_string_lossy().to_string(),
555                neorg.bin.to_string_lossy().to_string(),
556                neorg.conf.to_string_lossy().to_string(),
557                neorg.doc.to_string_lossy().to_string(),
558            ]
559        );
560    }
561}