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