1use crate::{
2 build::utils::format_path,
3 config::Config,
4 fs,
5 lockfile::{
6 LockedPackage, LockedPackageId, LockedPackageSpec, Lockfile, LockfileError, ReadOnly,
7 },
8 lua_version::LuaVersion,
9 package::{PackageName, PackageReq},
10 variables::{GetVariableError, HasVariables},
11};
12use std::{collections::HashMap, io, path::PathBuf, sync::Arc};
13
14use itertools::Itertools;
15use miette::Diagnostic;
16use nonempty::NonEmpty;
17use thiserror::Error;
18mod dist;
19mod layout;
20mod list;
21
22pub use dist::*;
23pub use layout::*;
24
25const LOCKFILE_NAME: &str = "lux.lock";
26
27pub trait InstallTree {
38 fn version(&self) -> &LuaVersion;
40 fn root(&self) -> PathBuf;
42 fn bin(&self) -> PathBuf;
44 fn unwrapped_bin(&self) -> PathBuf;
46 fn layout_for(&self, package: &LockedPackageSpec) -> RockLayout;
48 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError>;
50 fn lockfile_path(&self) -> PathBuf;
52 fn build_tree(&self, config: &Config) -> Result<Tree, TreeError>;
54 fn test_tree(&self, config: &Config) -> Result<Tree, TreeError>;
56 fn list(&self) -> Result<HashMap<PackageName, Vec<LockedPackage>>, TreeError>;
58 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError>;
60 fn prepare(&self, package: &LockedPackageSpec) -> Result<(), TreeError>;
62 fn finalize(&self, _package: &LockedPackage, _entry_type: EntryType) -> Result<(), TreeError> {
67 Ok(())
68 }
69 fn cleanup(&self, package: &LockedPackage, entry_type: EntryType) -> Result<(), TreeError>;
73}
74
75#[derive(Debug, PartialEq, Eq, Clone)]
77pub struct RockLayout {
78 pub root: PathBuf,
82 pub etc: PathBuf,
84 pub lib: PathBuf,
88 pub src: PathBuf,
92 pub bin: PathBuf,
97 pub conf: PathBuf,
101 pub doc: PathBuf,
105}
106
107impl RockLayout {
108 pub fn new(root: PathBuf, bin: PathBuf) -> Self {
110 let etc = root.join("etc");
111 let lib = root.join("lib");
112 let src = root.join("src");
113 let conf = etc.join("conf");
114 let doc = etc.join("doc");
115 Self {
116 root,
117 etc,
118 lib,
119 src,
120 bin,
121 conf,
122 doc,
123 }
124 }
125
126 pub fn rockspec_path(&self) -> PathBuf {
128 self.root.join("package.rockspec")
129 }
130}
131
132#[derive(Clone, Debug)]
135pub struct Tree {
136 version: LuaVersion,
138 root_parent: PathBuf,
140 test_tree_dir: PathBuf,
142 build_tree_dir: PathBuf,
144 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
146}
147
148#[derive(Debug, Error, Diagnostic)]
149pub enum TreeError {
150 #[error(transparent)]
151 #[diagnostic(transparent)]
152 Fs(#[from] fs::FsError),
153 #[error(transparent)]
154 #[diagnostic(transparent)]
155 Lockfile(#[from] LockfileError),
156 #[error(transparent)]
157 Io(#[from] io::Error),
158}
159
160impl HasVariables for RockLayout {
161 fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
162 Ok(match var {
163 "PREFIX" => Some(format_path(&self.root)),
164 "LIBDIR" => Some(format_path(&self.lib)),
165 "LUADIR" => Some(format_path(&self.src)),
166 "BINDIR" => Some(format_path(&self.bin)),
167 "CONFDIR" => Some(format_path(&self.conf)),
168 "DOCDIR" => Some(format_path(&self.doc)),
169 _ => None,
170 })
171 }
172}
173
174impl Tree {
175 pub(crate) fn new(
178 root: PathBuf,
179 version: LuaVersion,
180 config: &Config,
181 ) -> Result<Self, TreeError> {
182 let version_dir = root.join(version.to_string());
183 let test_tree_dir = version_dir.join("test_dependencies");
184 let build_tree_dir = version_dir.join("build_dependencies");
185 Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
186 }
187
188 fn new_with_paths(
189 root: PathBuf,
190 test_tree_dir: PathBuf,
191 build_tree_dir: PathBuf,
192 version: LuaVersion,
193 config: &Config,
194 ) -> Result<Self, TreeError> {
195 let path_with_version = root.join(version.to_string());
196
197 fs::sync::create_dir_all(&path_with_version)?;
199
200 let gitignore_file = root.join(".gitignore");
202 fs::sync::write(&gitignore_file, "*")?;
203
204 let bin_dir = path_with_version.join("bin");
206 fs::sync::create_dir_all(&bin_dir)?;
207
208 Ok(Self {
209 root_parent: root,
210 version,
211 test_tree_dir,
212 build_tree_dir,
213 entrypoint_layout: config.entrypoint_layout().cloned(),
214 })
215 }
216
217 pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
218 where
219 F: Fn(&LockedPackage) -> bool,
220 {
221 match self.list()?.get(req.name()) {
222 Some(packages) => {
223 let found_packages = packages
224 .iter()
225 .rev()
226 .filter(|package| {
227 req.version_req().matches(package.version()) && filter(package)
228 })
229 .map(|package| package.id())
230 .collect_vec();
231
232 Ok(match NonEmpty::try_from(found_packages) {
233 Ok(found_packages) => {
234 if found_packages.len() == 1 {
235 RockMatches::Single(found_packages.last().clone())
236 } else {
237 RockMatches::Many(found_packages)
238 }
239 }
240 Err(_) => RockMatches::NotFound(req.clone()),
241 })
242 }
243 None => Ok(RockMatches::NotFound(req.clone())),
244 }
245 }
246}
247
248impl InstallTree for Tree {
249 fn version(&self) -> &LuaVersion {
250 &self.version
251 }
252
253 fn root(&self) -> PathBuf {
254 self.root_parent.join(self.version.to_string())
255 }
256
257 fn prepare(&self, package: &LockedPackageSpec) -> Result<(), TreeError> {
258 let layout = self.layout_for(package);
259 fs::sync::create_dir_all(&layout.root)?;
260 fs::sync::create_dir_all(&layout.lib)?;
261 fs::sync::create_dir_all(&layout.src)?;
262 fs::sync::create_dir_all(&layout.etc)?;
263
264 Ok(())
265 }
266
267 fn finalize(&self, package: &LockedPackage, entry_type: EntryType) -> Result<(), TreeError> {
268 if let Some(custom_layout) = &self.entrypoint_layout {
269 if entry_type.is_entrypoint() || custom_layout.layout_dependencies() {
270 custom_layout.apply_layout(self, &package.spec)?;
271 }
272 }
273
274 Ok(())
275 }
276
277 fn cleanup(&self, package: &LockedPackage, entry_type: EntryType) -> Result<(), TreeError> {
278 if let Some(layout) = &self.entrypoint_layout {
279 if entry_type.is_entrypoint() || layout.layout_dependencies() {
280 layout.remove_layout(self, &package.spec)?;
281 }
282 }
283
284 let layout = self.layout_for(&package.spec);
285 fs::sync::remove_dir_all(&layout.etc)?;
286 fs::sync::remove_dir_all(&layout.root)?;
287
288 let lockfile = self.lockfile()?;
289 for relative_binary_path in lockfile
290 .binaries(&package.id())
291 .into_iter()
292 .flat_map(|binaries| binaries.iter())
293 {
294 if let Some(binary_file_name) = relative_binary_path.file_name() {
295 let binary_path = self.bin().join(binary_file_name);
296 if binary_path.is_file() {
297 fs::sync::remove_file(binary_path)?;
298 }
299
300 let unwrapped_binary_path = self.unwrapped_bin().join(binary_file_name);
301 if unwrapped_binary_path.is_file() {
302 fs::sync::remove_file(unwrapped_binary_path)?;
303 }
304 }
305 }
306
307 Ok(())
308 }
309
310 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
311 Ok(Lockfile::new(self.lockfile_path())?)
312 }
313
314 fn lockfile_path(&self) -> PathBuf {
315 self.root().join(LOCKFILE_NAME)
316 }
317
318 fn layout_for(&self, package: &LockedPackageSpec) -> RockLayout {
319 RockLayout::new(
320 self.root().join(format!(
321 "{}-{}@{}",
322 package.id(),
323 package.name(),
324 package.version()
325 )),
326 self.bin(),
327 )
328 }
329
330 fn bin(&self) -> PathBuf {
331 self.root().join("bin")
332 }
333
334 fn unwrapped_bin(&self) -> PathBuf {
335 self.bin().join("unwrapped")
336 }
337
338 fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
339 let test_tree_dir = self.test_tree_dir.clone();
340 let build_tree_dir = self.build_tree_dir.clone();
341 Self::new_with_paths(
342 test_tree_dir.clone(),
343 test_tree_dir,
344 build_tree_dir,
345 self.version.clone(),
346 config,
347 )
348 }
349
350 fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
351 let test_tree_dir = self.test_tree_dir.clone();
352 let build_tree_dir = self.build_tree_dir.clone();
353 Self::new_with_paths(
354 build_tree_dir.clone(),
355 test_tree_dir,
356 build_tree_dir,
357 self.version.clone(),
358 config,
359 )
360 }
361
362 fn list(&self) -> Result<HashMap<PackageName, Vec<LockedPackage>>, TreeError> {
363 Ok(self.lockfile()?.list())
364 }
365
366 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
367 let found_packages = self.lockfile()?.find_rocks(req);
368 Ok(match NonEmpty::try_from(found_packages) {
369 Ok(found_packages) => {
370 if found_packages.len() == 1 {
371 RockMatches::Single(found_packages.last().clone())
372 } else {
373 RockMatches::Many(found_packages)
374 }
375 }
376 Err(_) => RockMatches::NotFound(req.clone()),
377 })
378 }
379}
380
381#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
382pub enum EntryType {
383 Entrypoint,
384 DependencyOnly,
385}
386
387impl EntryType {
388 pub fn is_entrypoint(&self) -> bool {
389 matches!(self, Self::Entrypoint)
390 }
391}
392
393#[derive(Clone, Debug)]
394pub enum RockMatches {
395 NotFound(PackageReq),
396 Single(LockedPackageId),
397 Many(NonEmpty<LockedPackageId>),
398}
399
400impl RockMatches {
402 pub fn is_found(&self) -> bool {
403 matches!(self, Self::Single(_) | Self::Many(_))
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use assert_fs::prelude::PathCopy;
410 use itertools::Itertools;
411 use std::path::PathBuf;
412
413 use insta::assert_yaml_snapshot;
414
415 use crate::{
416 config::ConfigBuilder,
417 lockfile::{LockConstraint, LockedPackage, LockedPackageHashes},
418 lua_version::LuaVersion,
419 package::{PackageName, PackageSpec, PackageVersion},
420 remote_package_source::RemotePackageSource,
421 tree::{InstallTree, RockLayout},
422 variables,
423 };
424
425 #[test]
426 fn rock_layout() {
427 let tree_path =
428 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
429
430 let temp = assert_fs::TempDir::new().unwrap();
431 temp.copy_from(&tree_path, &["**"]).unwrap();
432 let tree_path = temp.to_path_buf();
433
434 let config = ConfigBuilder::new()
435 .unwrap()
436 .user_tree(Some(tree_path.clone()))
437 .build()
438 .unwrap();
439 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
440
441 let mock_hashes = LockedPackageHashes {
442 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
443 .parse()
444 .unwrap(),
445 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
446 .parse()
447 .unwrap(),
448 };
449
450 let package = LockedPackage::from(
451 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
452 LockConstraint::Unconstrained,
453 RemotePackageSource::Test,
454 None,
455 mock_hashes.clone(),
456 );
457
458 let id = package.id();
459 tree.prepare(&package.spec).unwrap();
460
461 assert_eq!(
462 tree.layout_for(&package.spec),
463 RockLayout {
464 bin: tree_path.join("5.1/bin"),
465 root: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
466 etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
467 lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
468 src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
469 conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
470 doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
471 }
472 );
473 }
474
475 #[test]
476 fn tree_list() {
477 let tree_path =
478 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
479
480 let temp = assert_fs::TempDir::new().unwrap();
481 temp.copy_from(&tree_path, &["**"]).unwrap();
482 let tree_path = temp.to_path_buf();
483
484 let config = ConfigBuilder::new()
485 .unwrap()
486 .user_tree(Some(tree_path.clone()))
487 .build()
488 .unwrap();
489 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
490 let result = tree.list().unwrap();
491 let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
493 .into_iter()
494 .sorted()
495 .map(|(name, package)| {
496 (
497 name,
498 package
499 .into_iter()
500 .map(|package| package.version().clone())
501 .sorted()
502 .collect_vec(),
503 )
504 })
505 .collect_vec();
506
507 assert_yaml_snapshot!(sorted_result)
508 }
509
510 #[test]
511 fn rock_layout_substitute() {
512 let tree_path =
513 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
514
515 let temp = assert_fs::TempDir::new().unwrap();
516 temp.copy_from(&tree_path, &["**"]).unwrap();
517 let tree_path = temp.to_path_buf();
518
519 let config = ConfigBuilder::new()
520 .unwrap()
521 .user_tree(Some(tree_path.clone()))
522 .build()
523 .unwrap();
524 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
525
526 let mock_hashes = LockedPackageHashes {
527 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
528 .parse()
529 .unwrap(),
530 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
531 .parse()
532 .unwrap(),
533 };
534
535 let package = LockedPackage::from(
536 &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
537 LockConstraint::Unconstrained,
538 RemotePackageSource::Test,
539 None,
540 mock_hashes.clone(),
541 );
542 let layout = tree.layout_for(&package.spec);
543 let build_variables = vec![
544 "$(PREFIX)",
545 "$(LIBDIR)",
546 "$(LUADIR)",
547 "$(BINDIR)",
548 "$(CONFDIR)",
549 "$(DOCDIR)",
550 ];
551 let result: Vec<String> = build_variables
552 .into_iter()
553 .map(|var| variables::substitute(&[&layout], var))
554 .try_collect()
555 .unwrap();
556 assert_eq!(
557 result,
558 vec![
559 layout.root.to_string_lossy().to_string(),
560 layout.lib.to_string_lossy().to_string(),
561 layout.src.to_string_lossy().to_string(),
562 layout.bin.to_string_lossy().to_string(),
563 layout.conf.to_string_lossy().to_string(),
564 layout.doc.to_string_lossy().to_string(),
565 ]
566 );
567 }
568}