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#[derive(Clone, Debug)]
30pub struct Tree {
31 version: LuaVersion,
33 root_parent: PathBuf,
35 entrypoint_layout: RockLayoutConfig,
37 test_tree_dir: PathBuf,
39 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#[derive(Debug, PartialEq)]
53pub struct RockLayout {
54 pub rock_path: PathBuf,
58 pub etc: PathBuf,
60 pub lib: PathBuf,
64 pub src: PathBuf,
68 pub bin: PathBuf,
73 pub conf: PathBuf,
77 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 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 std::fs::create_dir_all(&path_with_version)?;
141
142 std::fs::write(root.join(".gitignore"), "*")?;
144
145 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 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 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 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 pub fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
237 self.mk_rock_layout(package, &self.entrypoint_layout)
238 }
239
240 pub fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
242 self.mk_rock_layout(package, &RockLayoutConfig::default())
243 }
244
245 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 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 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 pub fn lockfile_path(&self) -> PathBuf {
305 self.root().join(LOCKFILE_NAME)
306 }
307
308 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 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
385impl 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 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}