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