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