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