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