1use crate::build::backend::{BuildBackend, BuildInfo, RunBuildArgs};
2use crate::fs;
3use crate::lockfile::{LockfileError, RemotePackageSourceUrl};
4use crate::lua_installation::LuaInstallationError;
5use crate::lua_rockspec::LuaVersionError;
6use crate::lua_version::LuaVersion;
7use crate::operations::UnpackError;
8use crate::rockspec::Rockspec;
9use crate::tree::{EntryType, InstallTree, TreeError};
10use bon::Builder;
11use bytes::Bytes;
12use std::collections::HashMap;
13use std::fs::DirEntry;
14use std::path::PathBuf;
15use std::{io, path::Path};
16use tracing::Instrument;
17
18use crate::{
19 config::Config,
20 hash::HasIntegrity,
21 lockfile::{LockedPackage, LockedPackageSpec},
22 lua_installation::LuaInstallation,
23 lua_rockspec::BuildBackendSpec,
24 operations::FetchSrcError,
25};
26use builtin::BuiltinBuildError;
27use cmake::CMakeError;
28use command::CommandError;
29use external_dependency::{ExternalDependencyError, ExternalDependencyInfo};
30
31use itertools::Itertools;
32use luarocks::LuarocksBuildError;
33use make::MakeError;
34
35use miette::Diagnostic;
36use patch::{Patch, PatchError};
37use rust_binary::RustBinaryError;
38use rust_mlua::RustError;
39use source::SourceBuildError;
40use ssri::Integrity;
41use thiserror::Error;
42use treesitter_parser::TreesitterBuildError;
43use utils::{recursive_copy_dir, CompileCFilesError, InstallBinaryError};
44
45mod builtin;
46mod cmake;
47mod command;
48mod luarocks;
49#[cfg(feature = "luau-transpile")]
50mod luau;
51mod make;
52mod patch;
53mod rust_binary;
54mod rust_mlua;
55mod source;
56mod treesitter_parser;
57
58pub(crate) mod backend;
59pub(crate) mod utils;
60pub(crate) use builtin::autodetect_modules;
61
62pub mod external_dependency;
63
64#[derive(Debug)]
65pub(crate) enum RemotePackageSourceSpec {
66 RockSpec(Option<RemotePackageSourceUrl>),
67 SrcRock(SrcRockSource),
68}
69
70#[derive(Debug)]
72pub(crate) struct SrcRockSource {
73 pub bytes: Bytes,
74 pub source_url: RemotePackageSourceUrl,
75}
76
77#[derive(Error, Debug, Diagnostic)]
78#[non_exhaustive]
79pub enum BuildError {
80 #[error("builtin build failed")]
81 #[diagnostic(forward(0))]
82 Builtin(#[from] BuiltinBuildError),
83 #[error("cmake build failed")]
84 #[diagnostic(forward(0))]
85 CMake(#[from] CMakeError),
86 #[error("make build failed")]
87 #[diagnostic(forward(0))]
88 Make(#[from] MakeError),
89 #[error("command build failed")]
90 #[diagnostic(forward(0))]
91 Command(#[from] CommandError),
92 #[error("rust-mlua build failed")]
93 #[diagnostic(forward(0))]
94 Rust(#[from] RustError),
95 #[error("rust-binary build failed")]
96 #[diagnostic(forward(0))]
97 RustBinary(#[from] RustBinaryError),
98 #[error("treesitter-parser build failed")]
99 #[diagnostic(forward(0))]
100 TreesitterBuild(#[from] TreesitterBuildError),
101 #[error("luarocks build failed")]
102 #[diagnostic(forward(0))]
103 LuarocksBuild(#[from] LuarocksBuildError),
104 #[error("building from rock source failed")]
105 #[diagnostic(forward(0))]
106 SourceBuild(#[from] Box<SourceBuildError>),
107 #[error("unsupported build backend for luau")]
108 #[diagnostic(help("lux can build luau packages with the following build backends: 'builtin', 'source' & 'none'"))]
109 LuauUnsupportedBuildBackend,
110 #[error("luau does not support native dependencies")]
111 #[diagnostic(help("the luau runtime cannot load C libraries. use pure-Luau packages or Lua"))]
112 LuauNativeDepsUnsupported,
113 #[error("IO operation failed")]
114 Io(#[from] io::Error),
115 #[error(transparent)]
116 #[diagnostic(transparent)]
117 Fs(#[from] fs::FsError),
118 #[error(transparent)]
119 #[diagnostic(transparent)]
120 Lockfile(#[from] LockfileError),
121 #[error(transparent)]
122 #[diagnostic(transparent)]
123 Tree(#[from] TreeError),
124
125 #[error(transparent)]
126 #[diagnostic(transparent)]
127 ExternalDependencyError(#[from] ExternalDependencyError),
128 #[error(transparent)]
129 #[diagnostic(transparent)]
130 PatchError(#[from] PatchError),
131 #[error(transparent)]
132 #[diagnostic(transparent)]
133 CompileCFiles(#[from] CompileCFilesError),
134 #[error(transparent)]
135 #[diagnostic(transparent)]
136 LuaVersion(#[from] LuaVersionError),
137 #[error(
138 r#"source integrity mismatch.
139- source: {src}
140- expected: {expected}
141- got: {actual}"#
142 )]
143 #[diagnostic(help(
144 r#"the source may have been modified or a tag may have been moved.
145check the source, then rerun the command with `--no-lock` to update the hash."#
146 ))]
147 SourceIntegrityMismatch {
148 src: String,
149 expected: Integrity,
150 actual: Integrity,
151 },
152 #[error("failed to unpack src.rock")]
153 #[diagnostic(forward(0))]
154 UnpackSrcRock(#[source] UnpackError),
155 #[error("failed to fetch rock source")]
156 #[diagnostic(forward(0))]
157 FetchSrcError(#[from] FetchSrcError),
158 #[error("failed to install binary '{file_name}'")]
159 InstallBinary {
160 file_name: String,
161 #[diagnostic_source]
162 source: InstallBinaryError,
163 },
164 #[error(transparent)]
165 #[diagnostic(transparent)]
166 LuaInstallation(#[from] LuaInstallationError),
167}
168
169impl From<SourceBuildError> for BuildError {
170 fn from(source: SourceBuildError) -> Self {
171 Self::SourceBuild(Box::new(source))
172 }
173}
174
175#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
176pub enum BuildBehaviour {
177 Ignore,
180 Conflict,
183 Force,
185}
186
187#[tracing::instrument(level = "trace", skip_all)]
188async fn run_build<R: Rockspec + HasIntegrity, T: InstallTree + Sync>(
189 rockspec: &R,
190 args: RunBuildArgs<'_, T>,
191) -> Result<BuildInfo, BuildError> {
192 if args.tree.version() == &LuaVersion::Luau
193 && rockspec
194 .build()
195 .current_platform()
196 .build_backend
197 .as_ref()
198 .is_some_and(|backend| {
199 !matches!(
200 backend,
201 BuildBackendSpec::Builtin(_) | BuildBackendSpec::Source
202 )
203 })
204 {
205 Err(BuildError::LuauUnsupportedBuildBackend)
206 } else {
207 Ok(
208 match rockspec.build().current_platform().build_backend.to_owned() {
209 Some(BuildBackendSpec::Builtin(build_spec)) => build_spec.run(args).await?,
210 Some(BuildBackendSpec::Make(make_spec)) => make_spec.run(args).await?,
211 Some(BuildBackendSpec::CMake(cmake_spec)) => cmake_spec.run(args).await?,
212 Some(BuildBackendSpec::Command(command_spec)) => command_spec.run(args).await?,
213 Some(BuildBackendSpec::RustMlua(rust_mlua_spec)) => {
214 rust_mlua_spec.run(args).await?
215 }
216 Some(BuildBackendSpec::RustBinary(rust_binary_spec)) => {
217 rust_binary_spec.run(args).await?
218 }
219 Some(BuildBackendSpec::TreesitterParser(treesitter_parser_spec)) => {
220 treesitter_parser_spec.run(args).await?
221 }
222 Some(BuildBackendSpec::LuaRock(build_backend_name)) => {
223 luarocks::build(&build_backend_name, rockspec, args).await?
224 }
225 Some(BuildBackendSpec::Source) => source::build(args).await?,
226 None => BuildInfo::default(),
227 },
228 )
229 }
230}
231
232#[allow(clippy::too_many_arguments)]
233#[tracing::instrument(level = "trace", skip(rockspec, tree, config))]
234async fn install<R: Rockspec + HasIntegrity, T: InstallTree>(
235 rockspec: &R,
236 tree: &T,
237 package: &LockedPackageSpec,
238 lua: &LuaInstallation,
239 build_dir: &Path,
240 entry_type: &EntryType,
241 config: &Config,
242) -> Result<(), BuildError> {
243 let install_spec = &rockspec.build().current_platform().install;
244 let layout = tree.layout_for(package);
245 {
246 let span = tracing::info_span!("Copying Lua modules");
247 let _enter = span.enter();
248 for (target, source) in &install_spec.lua {
249 let _enter = span.enter();
250 let absolute_source = build_dir.join(source);
251 utils::copy_lua_to_module_path(&absolute_source, target, &layout.src)?;
252 }
253 }
254 {
255 let span = tracing::info_span!("Compiling C libraries");
256 let _enter = span.enter();
257 for (target, source) in &install_spec.lib {
258 let absolute_source = build_dir.join(source);
259 let resolved_target = layout.lib.join(target);
260 if !resolved_target.exists() {
264 if let Some(parent) = resolved_target.parent() {
265 fs::tokio::create_dir_all(&parent).await?;
266 }
267 fs::tokio::copy(&absolute_source, &resolved_target)
268 .instrument(tracing::trace_span!("copying target"))
269 .await?;
270 }
271 utils::make_writable(&resolved_target).await?;
272 }
273 }
274 if entry_type.is_entrypoint() {
275 let span = tracing::info_span!("Installing binaries");
276 let _enter = span.enter();
277 let deploy_spec = rockspec.deploy().current_platform();
278 for (target, source) in &install_spec.bin {
279 utils::install_binary(
280 &build_dir.join(source),
281 target,
282 tree,
283 lua,
284 deploy_spec,
285 config,
286 )
287 .instrument(tracing::trace_span!("installing binary"))
288 .await
289 .map_err(|err| BuildError::InstallBinary {
290 file_name: target.clone(),
291 source: err,
292 })?;
293 }
294 }
295 if !install_spec.conf.is_empty() {
296 let span = tracing::info_span!("Copying configuration files");
297 let _enter = span.enter();
298 for (target, source) in &install_spec.conf {
299 let absolute_source = build_dir.join(source);
300 let target = {
301 let joined = layout.conf.join(target);
302 match joined.file_name() {
303 Some(_) => joined,
304 None => joined.join(absolute_source.file_name().unwrap_or_default()),
305 }
306 };
307 if let Some(parent_dir) = target.parent() {
308 fs::tokio::create_dir_all(parent_dir)
309 .instrument(tracing::trace_span!("creating configuration directory"))
310 .await?;
311 }
312 fs::tokio::copy(&absolute_source, &target)
313 .instrument(tracing::trace_span!("copying configuration file"))
314 .await?;
315 utils::make_writable(&target).await?;
316 }
317 }
318 Ok(())
319}
320
321#[derive(Builder)]
323#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
324pub(crate) struct Deploy<'a, R: Rockspec + HasIntegrity, T: InstallTree + Sync> {
325 #[builder(start_fn)]
326 rockspec: &'a R,
327 #[builder(start_fn)]
328 tree: &'a T,
329 package: LockedPackage,
330 lua: &'a LuaInstallation,
331 source_root: &'a Path,
332 entry_type: EntryType,
333 config: &'a Config,
334 behaviour: BuildBehaviour,
335}
336
337impl<R, T, State> DeployBuilder<'_, R, T, State>
338where
339 R: Rockspec + HasIntegrity,
340 T: InstallTree + Sync,
341 State: deploy_builder::State + deploy_builder::IsComplete,
342{
343 pub(crate) async fn deploy(self) -> Result<LockedPackage, BuildError> {
344 do_deploy(self._build()).await
345 }
346}
347
348#[tracing::instrument(level = "trace", skip_all, fields(package = deploy.rockspec.package().to_string()))]
349async fn do_deploy<R, T>(deploy: Deploy<'_, R, T>) -> Result<LockedPackage, BuildError>
350where
351 R: Rockspec + HasIntegrity,
352 T: InstallTree + Sync,
353{
354 let Deploy {
355 rockspec,
356 tree,
357 package,
358 lua,
359 source_root,
360 entry_type,
361 config,
362 behaviour,
363 } = deploy;
364
365 if matches!(behaviour, BuildBehaviour::Ignore | BuildBehaviour::Conflict) {
366 if let Some(existing) = tree.lockfile()?.get(&package.id()) {
367 return Ok(existing.clone());
368 }
369 }
370
371 tree.prepare(&package.spec)?;
374 let layout = tree.layout_for(&package.spec);
375
376 let rock_source = rockspec.source().current_platform();
377 let build_dir = resolve_source_dir(
378 source_root,
379 rock_source.unpack_dir.as_deref(),
380 &rockspec.build().current_platform().copy_directories,
381 )?;
382
383 Patch::new(&build_dir, &rockspec.build().current_platform().patches).apply()?;
384
385 let external_dependencies = rockspec
386 .external_dependencies()
387 .current_platform()
388 .iter()
389 .map(|(name, dep)| {
390 ExternalDependencyInfo::probe(name, dep, config.external_deps())
391 .map(|info| (name.clone(), info))
392 })
393 .try_collect::<_, HashMap<_, _>, _>()?;
394
395 let output = run_build(
396 rockspec,
397 RunBuildArgs::new()
398 .package(&package.spec)
399 .no_install(false)
400 .lua(lua)
401 .external_dependencies(&external_dependencies)
402 .deploy(rockspec.deploy().current_platform())
403 .config(config)
404 .tree(tree)
405 .build_dir(&build_dir)
406 .build(),
407 )
408 .await?;
409
410 let mut binaries = rockspec.binaries();
411 binaries.extend(output.binaries);
412 tree.lockfile()?
413 .write_guard()
414 .set_binaries(&package, binaries);
415
416 install(
417 rockspec,
418 tree,
419 &package.spec,
420 lua,
421 &build_dir,
422 &entry_type,
423 config,
424 )
425 .await?;
426
427 for directory in rockspec
428 .build()
429 .current_platform()
430 .copy_directories
431 .iter()
432 .filter(|dir| {
433 dir.file_name()
434 .is_some_and(|name| name != "doc" && name != "docs")
435 })
436 {
437 recursive_copy_dir(&build_dir.join(directory), &layout.etc.join(directory)).await?;
438 }
439
440 recursive_copy_doc_dir(&layout.doc, &build_dir).await?;
441
442 if let Ok(rockspec_str) = rockspec.to_lua_remote_rockspec_string() {
443 fs::sync::write(layout.rockspec_path(), rockspec_str)?;
444 }
445
446 tree.finalize(&package, entry_type)?;
447
448 Ok(package)
449}
450
451fn is_source_or_etc_dir(dir: &DirEntry, copy_dirs: &[PathBuf]) -> bool {
452 let dir_name = dir.file_name().to_string_lossy().to_string();
453 matches!(dir_name.as_str(), "lua" | "src")
454 || copy_dirs
455 .iter()
456 .any(|copy_dir_name| copy_dir_name == &PathBuf::from(&dir_name))
457}
458
459pub(crate) fn resolve_source_dir(
462 source_root: &Path,
463 unpack_dir: Option<&Path>,
464 copy_dirs: &[PathBuf],
465) -> Result<PathBuf, fs::FsError> {
466 if let Some(unpack_dir) = unpack_dir {
467 return Ok(source_root.join(unpack_dir));
468 }
469 let has_lua_or_c_sources = fs::sync::read_dir(source_root)?
485 .filter_map(Result::ok)
486 .filter(|entry| entry.path().is_file())
487 .any(|entry| {
488 entry.path().extension().is_some_and(|ext| {
489 matches!(
490 ext.to_string_lossy().to_string().as_str(),
491 "lua" | "luau" | "c"
492 )
493 })
494 });
495 if has_lua_or_c_sources {
496 return Ok(source_root.to_path_buf());
497 }
498 let dir_entries = fs::sync::read_dir(source_root)?
499 .filter_map(Result::ok)
500 .filter(|entry| entry.path().is_dir())
501 .collect_vec();
502 if dir_entries.len() == 1 && !is_source_or_etc_dir(&dir_entries[0], copy_dirs) {
503 Ok(dir_entries[0].path())
504 } else {
505 Ok(source_root.to_path_buf())
506 }
507}
508
509#[tracing::instrument(level = "trace")]
510async fn recursive_copy_doc_dir(target_doc_dir: &Path, build_dir: &Path) -> Result<(), BuildError> {
511 let mut doc_dir = build_dir.join("doc");
512 if !doc_dir.exists() {
513 doc_dir = build_dir.join("docs");
514 }
515 recursive_copy_dir(&doc_dir, target_doc_dir).await?;
516 Ok(())
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use predicates::prelude::*;
523 use std::path::PathBuf;
524
525 use assert_fs::{
526 assert::PathAssert,
527 prelude::{PathChild, PathCopy},
528 };
529
530 use is_executable::IsExecutable;
531
532 use crate::{
533 config::ConfigBuilder,
534 lockfile::{LockConstraint, LockedPackageHashes, LockedPackageSpec, OptState, PinnedState},
535 lua_installation::{detect_installed_lua_version, LuaInstallation},
536 lua_rockspec::RemoteLuaRockspec,
537 lua_version::LuaVersion,
538 package::PackageSpec,
539 project::Project,
540 remote_package_source::RemotePackageSource,
541 };
542 #[cfg(feature = "impure_tests")]
543 use crate::{
544 operations::{unpack_rockspec, DownloadedPackedRockBytes},
545 pipeline::build_local::Build,
546 tree::Tree,
547 };
548
549 #[tokio::test]
550 async fn test_builtin_build() {
551 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
552 let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
553 .join("resources/test/sample-projects/no-build-spec/");
554 let tree_dir = assert_fs::TempDir::new().unwrap();
555 let config = ConfigBuilder::new()
556 .unwrap()
557 .lua_version(lua_version)
558 .user_tree(Some(tree_dir.to_path_buf()))
559 .build()
560 .unwrap();
561 let build_dir = assert_fs::TempDir::new().unwrap();
562 build_dir.copy_from(&project_root, &["**"]).unwrap();
563 let tree = config
564 .user_tree(config.lua_version().cloned().unwrap())
565 .unwrap();
566 let lua_version = config.lua_version().unwrap_or(&LuaVersion::Lua51);
567 let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
568 let project = Project::from_exact(&project_root).unwrap().unwrap();
569 let rockspec = project.toml().into_remote(None).unwrap();
570 let package = LockedPackage::from(
571 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
572 LockConstraint::Unconstrained,
573 RemotePackageSource::Test,
574 None,
575 LockedPackageHashes {
576 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
577 .parse()
578 .unwrap(),
579 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
580 .parse()
581 .unwrap(),
582 },
583 );
584 tree.prepare(&package.spec).unwrap();
585 let src_dir = tree.layout_for(&package.spec).src;
586 run_build(
587 &rockspec,
588 RunBuildArgs::new()
589 .package(&package.spec)
590 .no_install(false)
591 .lua(&lua)
592 .external_dependencies(&HashMap::default())
593 .deploy(rockspec.deploy().current_platform())
594 .config(&config)
595 .tree(&tree)
596 .build_dir(&build_dir)
597 .build(),
598 )
599 .await
600 .unwrap();
601 let foo_dir = src_dir.join("foo");
602 assert!(foo_dir.is_dir());
603 let foo_init = foo_dir.join("init.lua");
604 assert!(foo_init.is_file());
605 assert!(std::fs::read_to_string(&foo_init)
606 .unwrap()
607 .contains("return true"));
608 let foo_bar_dir = foo_dir.join("bar");
609 assert!(foo_bar_dir.is_dir());
610 let foo_bar_init = foo_bar_dir.join("init.lua");
611 assert!(foo_bar_init.is_file());
612 assert!(std::fs::read_to_string(&foo_bar_init)
613 .unwrap()
614 .contains("return true"));
615 let foo_bar_baz = foo_bar_dir.join("baz.lua");
616 assert!(foo_bar_baz.is_file());
617 assert!(std::fs::read_to_string(&foo_bar_baz)
618 .unwrap()
619 .contains("return true"));
620 let bin_file = tree_dir
621 .child(lua_version.to_string())
622 .child("bin")
623 .child("hello");
624 bin_file.assert(predicate::path::is_file());
625 bin_file.assert(predicate::str::contains("#!/usr/bin/env bash"));
626 bin_file.assert(predicate::str::contains("echo \"Hello\""));
627 }
628
629 #[cfg(unix)]
630 #[tokio::test]
631 async fn test_install_bin_sets_executable_permissions() {
632 let rockspec = RemoteLuaRockspec::new(
633 r#"
634rockspec_format = "3.0"
635package = "test-builtin-bin"
636version = "1.0-1"
637source = { url = "https://github.com/lumen-oss/luarocks-stub" }
638build = {
639 type = "builtin",
640 install = { bin = { mytool = "mytool" } },
641}
642deploy = { wrap_bin_scripts = false }
643"#,
644 )
645 .unwrap();
646 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
647 let tree_dir = assert_fs::TempDir::new().unwrap();
648 let build_dir = assert_fs::TempDir::new().unwrap();
649 let config = ConfigBuilder::new()
650 .unwrap()
651 .lua_version(lua_version)
652 .user_tree(Some(tree_dir.to_path_buf()))
653 .build()
654 .unwrap();
655 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
656 let tree = config
657 .user_tree(config.lua_version().cloned().unwrap())
658 .unwrap();
659 let package = LockedPackageSpec::new(
660 rockspec.package(),
661 rockspec.version(),
662 LockConstraint::Unconstrained,
663 Vec::new(),
664 Vec::new(),
665 &PinnedState::default(),
666 &OptState::default(),
667 );
668 tree.prepare(&package).unwrap();
669 crate::fs::tokio::write(build_dir.path().join("mytool"), "#!/bin/sh\necho hi\n")
670 .await
671 .unwrap();
672 install(
673 &rockspec,
674 &tree,
675 &package,
676 &lua,
677 build_dir.path(),
678 &EntryType::Entrypoint,
679 &config,
680 )
681 .await
682 .unwrap();
683 assert!(tree.bin().join("mytool").is_executable());
684 }
685
686 #[tokio::test]
687 async fn test_install_conf_directory_target() {
688 let rockspec = RemoteLuaRockspec::new(
689 r#"
690rockspec_format = "3.0"
691package = "test-install-conf"
692version = "1.0-1"
693source = { url = "https://github.com/lumen-oss/luarocks-stub" }
694build = {
695 type = "builtin",
696 install = {
697 conf = {
698 [".."] = "shell.nix",
699 ["plain.conf"] = "plain.conf",
700 },
701 },
702}
703"#,
704 )
705 .unwrap();
706 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
707 let tree_dir = assert_fs::TempDir::new().unwrap();
708 let build_dir = assert_fs::TempDir::new().unwrap();
709 let config = ConfigBuilder::new()
710 .unwrap()
711 .lua_version(lua_version)
712 .user_tree(Some(tree_dir.to_path_buf()))
713 .build()
714 .unwrap();
715 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
716 let tree = config
717 .user_tree(config.lua_version().cloned().unwrap())
718 .unwrap();
719 let package = LockedPackageSpec::new(
720 rockspec.package(),
721 rockspec.version(),
722 LockConstraint::Unconstrained,
723 Vec::new(),
724 Vec::new(),
725 &PinnedState::default(),
726 &OptState::default(),
727 );
728 tree.prepare(&package).unwrap();
729 for file in ["shell.nix", "plain.conf"] {
730 crate::fs::tokio::write(build_dir.path().join(file), "return {}")
731 .await
732 .unwrap();
733 }
734 install(
735 &rockspec,
736 &tree,
737 &package,
738 &lua,
739 build_dir.path(),
740 &EntryType::Entrypoint,
741 &config,
742 )
743 .await
744 .unwrap();
745 let layout = tree.layout_for(&package);
746 assert!(layout.etc.join("shell.nix").is_file());
747 assert!(layout.conf.join("plain.conf").is_file());
748 }
749
750 #[cfg(feature = "impure_tests")]
751 const LUATEST_SRC_ROCK: &str = "resources/test/luatest-0.2-1.src.rock";
752 #[cfg(feature = "impure_tests")]
753 const LUATEST_SRC_ROCK_SOURCE_HASH: &str =
754 "sha256-2jS0XOq0iIVhsZJ3BVqXSlKsx2vsqCAaaYyqcBEb7RI=";
755 #[cfg(feature = "impure_tests")]
756 const LUATEST_ROCKSPEC_HASH: &str = "sha256-NljJ20A+VadUyhhBjrRnojeQjSqRpQy7FWcgjUt2Fdc=";
757
758 #[cfg(feature = "impure_tests")]
759 fn luatest_src_rock_bytes() -> Bytes {
760 Bytes::copy_from_slice(
761 &std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(LUATEST_SRC_ROCK))
762 .unwrap(),
763 )
764 }
765
766 #[cfg(feature = "impure_tests")]
767 fn luatest_config(dir: &assert_fs::TempDir) -> Config {
768 ConfigBuilder::new()
769 .unwrap()
770 .user_tree(Some(dir.to_path_buf()))
771 .lua_version(Some(LuaVersion::Lua51))
772 .build()
773 .unwrap()
774 }
775
776 #[cfg(feature = "impure_tests")]
777 async fn build_luatest(
778 config: &Config,
779 tree: &Tree,
780 entry_type: EntryType,
781 behaviour: BuildBehaviour,
782 ) -> LockedPackage {
783 let bytes = luatest_src_rock_bytes();
784 let rock = DownloadedPackedRockBytes {
785 name: "luatest".into(),
786 version: "0.2-1".parse().unwrap(),
787 bytes: bytes.clone(),
788 file_name: "luatest-0.2-1.src.rock".into(),
789 url: "https://example.org/luatest-0.2-1.src.rock"
790 .parse()
791 .unwrap(),
792 };
793 let rockspec = unpack_rockspec(&rock).await.unwrap();
794 let lua = LuaInstallation::new_from_config(config).await.unwrap();
795 Build::new()
796 .rockspec(&rockspec)
797 .lua(&lua)
798 .tree(tree)
799 .entry_type(entry_type)
800 .config(config)
801 .behaviour(behaviour)
802 .source_spec(RemotePackageSourceSpec::SrcRock(SrcRockSource {
803 bytes,
804 source_url: RemotePackageSourceUrl::Url {
805 url: "https://example.org/luatest-0.2-1.src.rock"
806 .parse()
807 .unwrap(),
808 },
809 }))
810 .build()
811 .await
812 .unwrap()
813 }
814
815 #[cfg(feature = "impure_tests")]
816 #[tokio::test]
817 async fn luatest_src_rock_produces_expected_hashes() {
818 let dir = assert_fs::TempDir::new().unwrap();
819 let config = luatest_config(&dir);
820 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
821
822 let package =
823 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
824
825 assert_eq!(
826 package.hashes().source,
827 LUATEST_SRC_ROCK_SOURCE_HASH.parse().unwrap()
828 );
829 assert_eq!(
830 package.hashes().rockspec,
831 LUATEST_ROCKSPEC_HASH.parse().unwrap()
832 );
833 assert_eq!(
834 package.hashes().source,
835 luatest_src_rock_bytes().hash().await.unwrap()
836 );
837 assert!(tree.bin().join("luatest").is_file());
838 }
839
840 #[cfg(feature = "impure_tests")]
841 #[tokio::test]
842 async fn force_build_is_deterministic() {
843 let dir1 = assert_fs::TempDir::new().unwrap();
844 let config1 = luatest_config(&dir1);
845 let tree1 = config1.user_tree(LuaVersion::Lua51).unwrap();
846
847 let dir2 = assert_fs::TempDir::new().unwrap();
848 let config2 = luatest_config(&dir2);
849 let tree2 = config2.user_tree(LuaVersion::Lua51).unwrap();
850
851 let package1 = build_luatest(
852 &config1,
853 &tree1,
854 EntryType::Entrypoint,
855 BuildBehaviour::Force,
856 )
857 .await;
858 let package2 = build_luatest(
859 &config2,
860 &tree2,
861 EntryType::Entrypoint,
862 BuildBehaviour::Force,
863 )
864 .await;
865
866 assert_eq!(package1, package2);
867 assert_eq!(package1.hashes(), package2.hashes());
868 }
869
870 #[cfg(feature = "impure_tests")]
871 #[tokio::test]
872 async fn noforce_skips_rebuild_when_package_is_locked() {
873 let dir = assert_fs::TempDir::new().unwrap();
874 let config = luatest_config(&dir);
875 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
876
877 let package =
878 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
879 let bin = tree.bin().join("luatest");
880 assert!(bin.is_file());
881
882 {
884 let mut lockfile = tree.lockfile().unwrap().write_guard();
885 lockfile.add_entrypoint(&package);
886 }
887
888 std::fs::remove_file(&bin).unwrap();
890
891 let skipped = build_luatest(
892 &config,
893 &tree,
894 EntryType::Entrypoint,
895 BuildBehaviour::Ignore,
896 )
897 .await;
898 assert_eq!(skipped.id(), package.id());
899 assert!(!bin.is_file());
900
901 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
902 assert!(bin.is_file());
903 }
904}