1use crate::{
2 build::utils::format_path,
3 config::{tree::RockLayoutConfig, Config},
4 lockfile::{LocalPackage, LocalPackageId, Lockfile, LockfileError, OptState, ReadOnly},
5 lua_version::LuaVersion,
6 package::{PackageName, PackageReq},
7 variables::{GetVariableError, HasVariables},
8};
9use std::{collections::HashMap, io, path::PathBuf};
10
11use itertools::Itertools;
12use miette::Diagnostic;
13use nonempty::NonEmpty;
14use thiserror::Error;
15mod dist;
16mod list;
17
18pub use dist::*;
19
20const LOCKFILE_NAME: &str = "lux.lock";
21
22pub trait InstallTree {
33 fn version(&self) -> &LuaVersion;
35 fn root(&self) -> PathBuf;
37 fn root_for(&self, package: &LocalPackage) -> PathBuf;
39 fn bin(&self) -> PathBuf;
41 fn unwrapped_bin(&self) -> PathBuf;
43 fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout>;
45 fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout>;
47 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError>;
49 fn lockfile_path(&self) -> PathBuf;
51 fn build_tree(&self, config: &Config) -> Result<Tree, TreeError>;
53 fn test_tree(&self, config: &Config) -> Result<Tree, TreeError>;
55 fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError>;
57 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError>;
59 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError>;
61}
62
63#[derive(Clone, Debug)]
66pub struct Tree {
67 version: LuaVersion,
69 root_parent: PathBuf,
71 entrypoint_layout: RockLayoutConfig,
73 test_tree_dir: PathBuf,
75 build_tree_dir: PathBuf,
77}
78
79#[derive(Debug, Error, Diagnostic)]
80pub enum TreeError {
81 #[error("unable to create directory {0}:\n{1}")]
82 CreateDir(String, io::Error),
83 #[error("unable to write to {0}:\n{1}")]
84 WriteFile(String, io::Error),
85 #[error(transparent)]
86 #[diagnostic(transparent)]
87 Lockfile(#[from] LockfileError),
88}
89
90#[derive(Debug, PartialEq)]
92pub struct RockLayout {
93 pub rock_path: PathBuf,
97 pub etc: PathBuf,
99 pub lib: PathBuf,
103 pub src: PathBuf,
107 pub bin: PathBuf,
112 pub conf: PathBuf,
116 pub doc: PathBuf,
120}
121
122impl RockLayout {
123 pub fn rockspec_path(&self) -> PathBuf {
124 self.rock_path.join("package.rockspec")
125 }
126}
127
128impl HasVariables for RockLayout {
129 #[tracing::instrument(level = "trace")]
130 fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
131 Ok(match var {
132 "PREFIX" => Some(format_path(&self.rock_path)),
133 "LIBDIR" => Some(format_path(&self.lib)),
134 "LUADIR" => Some(format_path(&self.src)),
135 "BINDIR" => Some(format_path(&self.bin)),
136 "CONFDIR" => Some(format_path(&self.conf)),
137 "DOCDIR" => Some(format_path(&self.doc)),
138 _ => None,
139 })
140 }
141}
142
143impl Tree {
144 pub(crate) fn new(
147 root: PathBuf,
148 version: LuaVersion,
149 config: &Config,
150 ) -> Result<Self, TreeError> {
151 let version_dir = root.join(version.to_string());
152 let test_tree_dir = version_dir.join("test_dependencies");
153 let build_tree_dir = version_dir.join("build_dependencies");
154 Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
155 }
156
157 fn new_with_paths(
158 root: PathBuf,
159 test_tree_dir: PathBuf,
160 build_tree_dir: PathBuf,
161 version: LuaVersion,
162 config: &Config,
163 ) -> Result<Self, TreeError> {
164 let path_with_version = root.join(version.to_string());
165
166 std::fs::create_dir_all(&path_with_version).map_err(|err| {
168 TreeError::CreateDir(path_with_version.to_string_lossy().to_string(), err)
169 })?;
170
171 let gitignore_file = root.join(".gitignore");
173 std::fs::write(&gitignore_file, "*").map_err(|err| {
174 TreeError::WriteFile(gitignore_file.to_string_lossy().to_string(), err)
175 })?;
176
177 let bin_dir = path_with_version.join("bin");
179 std::fs::create_dir_all(&bin_dir)
180 .map_err(|err| TreeError::CreateDir(bin_dir.to_string_lossy().to_string(), err))?;
181
182 let lockfile_path = root.join(LOCKFILE_NAME);
183 let rock_layout_config = if lockfile_path.is_file() {
184 let lockfile = Lockfile::load(lockfile_path, None)?;
185 lockfile.entrypoint_layout
186 } else {
187 config.entrypoint_layout().clone()
188 };
189 Ok(Self {
190 root_parent: root,
191 version,
192 entrypoint_layout: rock_layout_config,
193 test_tree_dir,
194 build_tree_dir,
195 })
196 }
197
198 pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
199 where
200 F: Fn(&LocalPackage) -> bool,
201 {
202 match self.list()?.get(req.name()) {
203 Some(packages) => {
204 let found_packages = packages
205 .iter()
206 .rev()
207 .filter(|package| {
208 req.version_req().matches(package.version()) && filter(package)
209 })
210 .map(|package| package.id())
211 .collect_vec();
212
213 Ok(match NonEmpty::try_from(found_packages) {
214 Ok(found_packages) => {
215 if found_packages.len() == 1 {
216 RockMatches::Single(found_packages.last().clone())
217 } else {
218 RockMatches::Many(found_packages)
219 }
220 }
221 Err(_) => RockMatches::NotFound(req.clone()),
222 })
223 }
224 None => Ok(RockMatches::NotFound(req.clone())),
225 }
226 }
227
228 pub(crate) fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
230 mk_rock_layout("src", "lib", self, package, &self.entrypoint_layout)
231 }
232
233 fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
235 mk_rock_layout("src", "lib", self, package, &RockLayoutConfig::default())
236 }
237}
238
239impl InstallTree for Tree {
240 fn version(&self) -> &LuaVersion {
241 &self.version
242 }
243
244 fn root(&self) -> PathBuf {
245 self.root_parent.join(self.version.to_string())
246 }
247
248 fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout> {
249 let rock_layout = self.entrypoint_layout(package);
250 std::fs::create_dir_all(&rock_layout.lib)?;
251 std::fs::create_dir_all(&rock_layout.src)?;
252 Ok(rock_layout)
253 }
254
255 fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout> {
256 let rock_layout = self.dependency_layout(package);
257 std::fs::create_dir_all(&rock_layout.lib)?;
258 std::fs::create_dir_all(&rock_layout.src)?;
259 Ok(rock_layout)
260 }
261
262 fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
263 Ok(Lockfile::new(
264 self.lockfile_path(),
265 self.entrypoint_layout.clone(),
266 )?)
267 }
268
269 fn lockfile_path(&self) -> PathBuf {
270 self.root().join(LOCKFILE_NAME)
271 }
272
273 fn root_for(&self, package: &LocalPackage) -> PathBuf {
274 self.root().join(format!(
275 "{}-{}@{}",
276 package.id(),
277 package.name(),
278 package.version()
279 ))
280 }
281
282 fn bin(&self) -> PathBuf {
283 self.root().join("bin")
284 }
285
286 fn unwrapped_bin(&self) -> PathBuf {
287 self.bin().join("unwrapped")
288 }
289
290 fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
291 let test_tree_dir = self.test_tree_dir.clone();
292 let build_tree_dir = self.build_tree_dir.clone();
293 Self::new_with_paths(
294 test_tree_dir.clone(),
295 test_tree_dir,
296 build_tree_dir,
297 self.version.clone(),
298 config,
299 )
300 }
301
302 fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
303 let test_tree_dir = self.test_tree_dir.clone();
304 let build_tree_dir = self.build_tree_dir.clone();
305 Self::new_with_paths(
306 build_tree_dir.clone(),
307 test_tree_dir,
308 build_tree_dir,
309 self.version.clone(),
310 config,
311 )
312 }
313
314 fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError> {
316 let lockfile = self.lockfile()?;
317 if lockfile.is_entrypoint(&package.id()) {
318 Ok(self.entrypoint_layout(package))
319 } else {
320 Ok(self.dependency_layout(package))
321 }
322 }
323
324 fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError> {
325 Ok(self.lockfile()?.list())
326 }
327
328 fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
329 let found_packages = self.lockfile()?.find_rocks(req);
330 Ok(match NonEmpty::try_from(found_packages) {
331 Ok(found_packages) => {
332 if found_packages.len() == 1 {
333 RockMatches::Single(found_packages.last().clone())
334 } else {
335 RockMatches::Many(found_packages)
336 }
337 }
338 Err(_) => RockMatches::NotFound(req.clone()),
339 })
340 }
341}
342
343#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
344pub enum EntryType {
345 Entrypoint,
346 DependencyOnly,
347}
348
349impl EntryType {
350 pub fn is_entrypoint(&self) -> bool {
351 matches!(self, Self::Entrypoint)
352 }
353}
354
355#[derive(Clone, Debug)]
356pub enum RockMatches {
357 NotFound(PackageReq),
358 Single(LocalPackageId),
359 Many(NonEmpty<LocalPackageId>),
360}
361
362impl RockMatches {
364 pub fn is_found(&self) -> bool {
365 matches!(self, Self::Single(_) | Self::Many(_))
366 }
367}
368
369fn mk_rock_layout(
371 src_dir_name: &str,
372 lib_dir_name: &str,
373 tree: &impl InstallTree,
374 package: &LocalPackage,
375 layout_config: &RockLayoutConfig,
376) -> RockLayout {
377 let rock_path = tree.root_for(package);
378 let bin = tree.bin();
379 let etc_root = match layout_config.etc_root {
380 Some(ref etc_root) => tree.root().join(etc_root),
381 None => rock_path.clone(),
382 };
383 let mut etc = match package.spec.opt {
384 OptState::Required => etc_root.join(&layout_config.etc),
385 OptState::Optional => etc_root.join(&layout_config.opt_etc),
386 };
387 if layout_config.etc_root.is_some() {
388 etc = etc.join(format!("{}", package.name()));
389 }
390 let lib = rock_path.join(lib_dir_name);
391 let src = rock_path.join(src_dir_name);
392 let conf = etc.join(&layout_config.conf);
393 let doc = etc.join(&layout_config.doc);
394
395 RockLayout {
396 rock_path,
397 etc,
398 lib,
399 src,
400 bin,
401 conf,
402 doc,
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use assert_fs::prelude::PathCopy;
409 use itertools::Itertools;
410 use std::path::PathBuf;
411
412 use insta::assert_yaml_snapshot;
413
414 use crate::{
415 config::ConfigBuilder,
416 lockfile::{LocalPackage, LocalPackageHashes, LockConstraint},
417 lua_version::LuaVersion,
418 package::{PackageName, PackageSpec, PackageVersion},
419 remote_package_source::RemotePackageSource,
420 rockspec::RockBinaries,
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 = LocalPackageHashes {
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 = LocalPackage::from(
451 &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
452 LockConstraint::Unconstrained,
453 RockBinaries::default(),
454 RemotePackageSource::Test,
455 None,
456 mock_hashes.clone(),
457 );
458
459 let id = package.id();
460
461 let neorg = tree.dependency(&package).unwrap();
462
463 assert_eq!(
464 neorg,
465 RockLayout {
466 bin: tree_path.join("5.1/bin"),
467 rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
468 etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
469 lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
470 src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
471 conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
472 doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
473 }
474 );
475
476 let package = LocalPackage::from(
477 &PackageSpec::parse("lua-cjson".into(), "2.1.0-1".into()).unwrap(),
478 LockConstraint::Unconstrained,
479 RockBinaries::default(),
480 RemotePackageSource::Test,
481 None,
482 mock_hashes.clone(),
483 );
484
485 let id = package.id();
486
487 let lua_cjson = tree.dependency(&package).unwrap();
488
489 assert_eq!(
490 lua_cjson,
491 RockLayout {
492 bin: tree_path.join("5.1/bin"),
493 rock_path: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1")),
494 etc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc")),
495 lib: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/lib")),
496 src: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/src")),
497 conf: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/conf")),
498 doc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/doc")),
499 }
500 );
501 }
502
503 #[test]
504 fn tree_list() {
505 let tree_path =
506 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
507
508 let temp = assert_fs::TempDir::new().unwrap();
509 temp.copy_from(&tree_path, &["**"]).unwrap();
510 let tree_path = temp.to_path_buf();
511
512 let config = ConfigBuilder::new()
513 .unwrap()
514 .user_tree(Some(tree_path.clone()))
515 .build()
516 .unwrap();
517 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
518 let result = tree.list().unwrap();
519 let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
521 .into_iter()
522 .sorted()
523 .map(|(name, package)| {
524 (
525 name,
526 package
527 .into_iter()
528 .map(|package| package.spec.version)
529 .sorted()
530 .collect_vec(),
531 )
532 })
533 .collect_vec();
534
535 assert_yaml_snapshot!(sorted_result)
536 }
537
538 #[test]
539 fn rock_layout_substitute() {
540 let tree_path =
541 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
542
543 let temp = assert_fs::TempDir::new().unwrap();
544 temp.copy_from(&tree_path, &["**"]).unwrap();
545 let tree_path = temp.to_path_buf();
546
547 let config = ConfigBuilder::new()
548 .unwrap()
549 .user_tree(Some(tree_path.clone()))
550 .build()
551 .unwrap();
552 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
553
554 let mock_hashes = LocalPackageHashes {
555 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
556 .parse()
557 .unwrap(),
558 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
559 .parse()
560 .unwrap(),
561 };
562
563 let neorg = tree
564 .dependency(&LocalPackage::from(
565 &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
566 LockConstraint::Unconstrained,
567 RockBinaries::default(),
568 RemotePackageSource::Test,
569 None,
570 mock_hashes.clone(),
571 ))
572 .unwrap();
573 let build_variables = vec![
574 "$(PREFIX)",
575 "$(LIBDIR)",
576 "$(LUADIR)",
577 "$(BINDIR)",
578 "$(CONFDIR)",
579 "$(DOCDIR)",
580 ];
581 let result: Vec<String> = build_variables
582 .into_iter()
583 .map(|var| variables::substitute(&[&neorg], var))
584 .try_collect()
585 .unwrap();
586 assert_eq!(
587 result,
588 vec![
589 neorg.rock_path.to_string_lossy().to_string(),
590 neorg.lib.to_string_lossy().to_string(),
591 neorg.src.to_string_lossy().to_string(),
592 neorg.bin.to_string_lossy().to_string(),
593 neorg.conf.to_string_lossy().to_string(),
594 neorg.doc.to_string_lossy().to_string(),
595 ]
596 );
597 }
598}