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