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