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