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