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