Skip to main content

lux_lib/tree/
mod.rs

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