1use crate::package;
2use crate::{
3 config::Config,
4 fs,
5 lockfile::{self, LockedPackage, LockedPackageId, LockedPackageSpec, Lockfile, ReadOnly},
6 lua_version::LuaVersion,
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<package::Name, Vec<LockedPackage>>, Error>;
56 fn match_rocks(&self, req: &package::Req) -> 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: &package::Req, 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<package::Name, Vec<LockedPackage>>, Error> {
357 Ok(self.lockfile()?.list())
358 }
359
360 fn match_rocks(&self, req: &package::Req) -> 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(package::Req),
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 crate::package;
408
409 use insta::assert_yaml_snapshot;
410
411 use crate::{
412 config::ConfigBuilder,
413 lockfile::{LockConstraint, LockedPackage, LockedPackageHashes},
414 lua_version::LuaVersion,
415 remote_package_source::RemotePackageSource,
416 tree::{InstallTree, RockLayout},
417 variables,
418 };
419
420 #[test]
421 fn rock_layout() {
422 let tree_path =
423 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
424
425 let temp = assert_fs::TempDir::new().unwrap();
426 temp.copy_from(&tree_path, &["**"]).unwrap();
427 let tree_path = temp.to_path_buf();
428
429 let config = ConfigBuilder::new()
430 .unwrap()
431 .user_tree(Some(tree_path.clone()))
432 .build()
433 .unwrap();
434 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
435
436 let mock_hashes = LockedPackageHashes {
437 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
438 .parse()
439 .unwrap(),
440 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
441 .parse()
442 .unwrap(),
443 };
444
445 let package = LockedPackage::from(
446 &package::Spec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
447 LockConstraint::Unconstrained,
448 RemotePackageSource::Test,
449 None,
450 mock_hashes.clone(),
451 );
452
453 let id = package.id();
454 tree.prepare(&package.spec).unwrap();
455
456 assert_eq!(
457 tree.layout_for(&package.spec),
458 RockLayout {
459 bin: tree_path.join("5.1/bin"),
460 root: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
461 etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
462 lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
463 src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
464 conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
465 doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
466 }
467 );
468 }
469
470 #[test]
471 fn tree_list() {
472 let tree_path =
473 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
474
475 let temp = assert_fs::TempDir::new().unwrap();
476 temp.copy_from(&tree_path, &["**"]).unwrap();
477 let tree_path = temp.to_path_buf();
478
479 let config = ConfigBuilder::new()
480 .unwrap()
481 .user_tree(Some(tree_path.clone()))
482 .build()
483 .unwrap();
484 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
485 let result = tree.list().unwrap();
486 let sorted_result: Vec<(package::Name, Vec<package::Version>)> = result
488 .into_iter()
489 .sorted()
490 .map(|(name, package)| {
491 (
492 name,
493 package
494 .into_iter()
495 .map(|package| package.version().clone())
496 .sorted()
497 .collect_vec(),
498 )
499 })
500 .collect_vec();
501
502 assert_yaml_snapshot!(sorted_result)
503 }
504
505 #[test]
506 fn rock_layout_substitute() {
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
521 let mock_hashes = LockedPackageHashes {
522 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
523 .parse()
524 .unwrap(),
525 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
526 .parse()
527 .unwrap(),
528 };
529
530 let package = LockedPackage::from(
531 &package::Spec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
532 LockConstraint::Unconstrained,
533 RemotePackageSource::Test,
534 None,
535 mock_hashes.clone(),
536 );
537 let layout = tree.layout_for(&package.spec);
538 let build_variables = vec![
539 "$(PREFIX)",
540 "$(LIBDIR)",
541 "$(LUADIR)",
542 "$(BINDIR)",
543 "$(CONFDIR)",
544 "$(DOCDIR)",
545 ];
546 let result: Vec<String> = build_variables
547 .into_iter()
548 .map(|var| variables::substitute(&[&layout], var))
549 .try_collect()
550 .unwrap();
551 assert_eq!(
552 result,
553 vec![
554 layout.root.to_string_lossy().to_string(),
555 layout.lib.to_string_lossy().to_string(),
556 layout.src.to_string_lossy().to_string(),
557 layout.bin.to_string_lossy().to_string(),
558 layout.conf.to_string_lossy().to_string(),
559 layout.doc.to_string_lossy().to_string(),
560 ]
561 );
562 }
563}