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#[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) -> 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 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 std::fs::create_dir_all(&path_with_version)?;
140
141 std::fs::write(root.join(".gitignore"), "*")?;
143
144 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 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 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 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 pub fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
236 self.mk_rock_layout(package, &self.entrypoint_layout)
237 }
238
239 pub fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
241 self.mk_rock_layout(package, &RockLayoutConfig::default())
242 }
243
244 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 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 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 pub fn lockfile_path(&self) -> PathBuf {
304 self.root().join(LOCKFILE_NAME)
305 }
306
307 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 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
384impl 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 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}