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