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::{
524 io::{Cursor, Write},
525 path::PathBuf,
526 };
527 use zip::write::SimpleFileOptions;
528
529 use assert_fs::{
530 assert::PathAssert,
531 prelude::{PathChild, PathCopy},
532 };
533
534 use is_executable::IsExecutable;
535
536 use crate::pipeline::build_local::Build;
537 use crate::{
538 config::ConfigBuilder,
539 lockfile::{LockConstraint, LockedPackageHashes, LockedPackageSpec, OptState, PinnedState},
540 lua_installation::{detect_installed_lua_version, LuaInstallation},
541 lua_rockspec::RemoteLuaRockspec,
542 lua_version::LuaVersion,
543 package::PackageSpec,
544 project::Project,
545 remote_package_source::RemotePackageSource,
546 };
547 #[cfg(feature = "impure_tests")]
548 use crate::{
549 operations::{unpack_rockspec, DownloadedPackedRockBytes},
550 tree::Tree,
551 };
552
553 #[tokio::test]
554 async fn test_builtin_build() {
555 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
556 let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
557 .join("resources/test/sample-projects/no-build-spec/");
558 let tree_dir = assert_fs::TempDir::new().unwrap();
559 let config = ConfigBuilder::new()
560 .unwrap()
561 .lua_version(lua_version)
562 .user_tree(Some(tree_dir.to_path_buf()))
563 .build()
564 .unwrap();
565 let build_dir = assert_fs::TempDir::new().unwrap();
566 build_dir.copy_from(&project_root, &["**"]).unwrap();
567 let tree = config
568 .user_tree(config.lua_version().cloned().unwrap())
569 .unwrap();
570 let lua_version = config.lua_version().unwrap_or(&LuaVersion::Lua51);
571 let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
572 let project = Project::from_exact(&project_root).unwrap().unwrap();
573 let rockspec = project.toml().into_remote(None).unwrap();
574 let package = LockedPackage::from(
575 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
576 LockConstraint::Unconstrained,
577 RemotePackageSource::Test,
578 None,
579 LockedPackageHashes {
580 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
581 .parse()
582 .unwrap(),
583 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
584 .parse()
585 .unwrap(),
586 },
587 );
588 tree.prepare(&package.spec).unwrap();
589 let src_dir = tree.layout_for(&package.spec).src;
590 run_build(
591 &rockspec,
592 RunBuildArgs::new()
593 .package(&package.spec)
594 .no_install(false)
595 .lua(&lua)
596 .external_dependencies(&HashMap::default())
597 .deploy(rockspec.deploy().current_platform())
598 .config(&config)
599 .tree(&tree)
600 .build_dir(&build_dir)
601 .build(),
602 )
603 .await
604 .unwrap();
605 let foo_dir = src_dir.join("foo");
606 assert!(foo_dir.is_dir());
607 let foo_init = foo_dir.join("init.lua");
608 assert!(foo_init.is_file());
609 assert!(std::fs::read_to_string(&foo_init)
610 .unwrap()
611 .contains("return true"));
612 let foo_bar_dir = foo_dir.join("bar");
613 assert!(foo_bar_dir.is_dir());
614 let foo_bar_init = foo_bar_dir.join("init.lua");
615 assert!(foo_bar_init.is_file());
616 assert!(std::fs::read_to_string(&foo_bar_init)
617 .unwrap()
618 .contains("return true"));
619 let foo_bar_baz = foo_bar_dir.join("baz.lua");
620 assert!(foo_bar_baz.is_file());
621 assert!(std::fs::read_to_string(&foo_bar_baz)
622 .unwrap()
623 .contains("return true"));
624 let bin_file = tree_dir
625 .child(lua_version.to_string())
626 .child("bin")
627 .child("hello");
628 bin_file.assert(predicate::path::is_file());
629 bin_file.assert(predicate::str::contains("#!/usr/bin/env bash"));
630 bin_file.assert(predicate::str::contains("echo \"Hello\""));
631 }
632
633 #[cfg(unix)]
634 #[tokio::test]
635 async fn test_install_bin_sets_executable_permissions() {
636 let rockspec = RemoteLuaRockspec::new(
637 r#"
638rockspec_format = "3.0"
639package = "test-builtin-bin"
640version = "1.0-1"
641source = { url = "https://github.com/lumen-oss/luarocks-stub" }
642build = {
643 type = "builtin",
644 install = { bin = { mytool = "mytool" } },
645}
646deploy = { wrap_bin_scripts = false }
647"#,
648 )
649 .unwrap();
650 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
651 let tree_dir = assert_fs::TempDir::new().unwrap();
652 let build_dir = assert_fs::TempDir::new().unwrap();
653 let config = ConfigBuilder::new()
654 .unwrap()
655 .lua_version(lua_version)
656 .user_tree(Some(tree_dir.to_path_buf()))
657 .build()
658 .unwrap();
659 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
660 let tree = config
661 .user_tree(config.lua_version().cloned().unwrap())
662 .unwrap();
663 let package = LockedPackageSpec::new(
664 rockspec.package(),
665 rockspec.version(),
666 LockConstraint::Unconstrained,
667 Vec::new(),
668 Vec::new(),
669 &PinnedState::default(),
670 &OptState::default(),
671 );
672 tree.prepare(&package).unwrap();
673 crate::fs::tokio::write(build_dir.path().join("mytool"), "#!/bin/sh\necho hi\n")
674 .await
675 .unwrap();
676 install(
677 &rockspec,
678 &tree,
679 &package,
680 &lua,
681 build_dir.path(),
682 &EntryType::Entrypoint,
683 &config,
684 )
685 .await
686 .unwrap();
687 assert!(tree.bin().join("mytool").is_executable());
688 }
689
690 #[tokio::test]
691 async fn git_src_rock_with_repo_directory_builds() {
692 let dir = assert_fs::TempDir::new().unwrap();
693 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
694 let config = ConfigBuilder::new()
695 .unwrap()
696 .user_tree(Some(dir.to_path_buf()))
697 .lua_version(lua_version)
698 .build()
699 .unwrap();
700 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
701 let tree = config.user_tree(lua.version.clone()).unwrap();
702
703 let rockspec_content = r#"
704package = "git-src-rock"
705version = "1.0.0-1"
706source = { url = "git+https://example.com/example/git-src-rock.git" }
707dependencies = { "lua >= 5.1" }
708build = {
709 type = "builtin",
710 modules = { ["git_src_rock"] = "lua/git_src_rock.lua" },
711}
712"#;
713 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
714
715 let mut archive = Cursor::new(Vec::new());
716 {
717 let mut zip = zip::ZipWriter::new(&mut archive);
718 zip.start_file(
719 "git-src-rock-1.0.0-1.rockspec",
720 SimpleFileOptions::default(),
721 )
722 .unwrap();
723 zip.write_all(rockspec_content.as_bytes()).unwrap();
724 zip.start_file(
725 "git-src-rock/lua/git_src_rock.lua",
726 SimpleFileOptions::default(),
727 )
728 .unwrap();
729 zip.write_all(b"return {}").unwrap();
730 zip.finish().unwrap();
731 }
732 let bytes = Bytes::from(archive.into_inner());
733
734 let package = Build::new()
735 .rockspec(&rockspec)
736 .lua(&lua)
737 .tree(&tree)
738 .entry_type(EntryType::Entrypoint)
739 .config(&config)
740 .behaviour(BuildBehaviour::Force)
741 .source_spec(RemotePackageSourceSpec::SrcRock(SrcRockSource {
742 bytes,
743 source_url: RemotePackageSourceUrl::Url {
744 url: "https://example.org/git-src-rock-1.0.0-1.src.rock"
745 .parse()
746 .unwrap(),
747 },
748 }))
749 .build()
750 .await
751 .unwrap();
752
753 assert!(tree
754 .layout_for(package.spec())
755 .src
756 .join("git_src_rock.lua")
757 .is_file());
758 }
759
760 #[tokio::test]
761 async fn test_install_conf_directory_target() {
762 let rockspec = RemoteLuaRockspec::new(
763 r#"
764rockspec_format = "3.0"
765package = "test-install-conf"
766version = "1.0-1"
767source = { url = "https://github.com/lumen-oss/luarocks-stub" }
768build = {
769 type = "builtin",
770 install = {
771 conf = {
772 [".."] = "shell.nix",
773 ["plain.conf"] = "plain.conf",
774 },
775 },
776}
777"#,
778 )
779 .unwrap();
780 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
781 let tree_dir = assert_fs::TempDir::new().unwrap();
782 let build_dir = assert_fs::TempDir::new().unwrap();
783 let config = ConfigBuilder::new()
784 .unwrap()
785 .lua_version(lua_version)
786 .user_tree(Some(tree_dir.to_path_buf()))
787 .build()
788 .unwrap();
789 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
790 let tree = config
791 .user_tree(config.lua_version().cloned().unwrap())
792 .unwrap();
793 let package = LockedPackageSpec::new(
794 rockspec.package(),
795 rockspec.version(),
796 LockConstraint::Unconstrained,
797 Vec::new(),
798 Vec::new(),
799 &PinnedState::default(),
800 &OptState::default(),
801 );
802 tree.prepare(&package).unwrap();
803 for file in ["shell.nix", "plain.conf"] {
804 crate::fs::tokio::write(build_dir.path().join(file), "return {}")
805 .await
806 .unwrap();
807 }
808 install(
809 &rockspec,
810 &tree,
811 &package,
812 &lua,
813 build_dir.path(),
814 &EntryType::Entrypoint,
815 &config,
816 )
817 .await
818 .unwrap();
819 let layout = tree.layout_for(&package);
820 assert!(layout.etc.join("shell.nix").is_file());
821 assert!(layout.conf.join("plain.conf").is_file());
822 }
823
824 #[cfg(feature = "impure_tests")]
825 const LUATEST_SRC_ROCK: &str = "resources/test/luatest-0.2-1.src.rock";
826 #[cfg(feature = "impure_tests")]
827 const LUATEST_SRC_ROCK_SOURCE_HASH: &str =
828 "sha256-2jS0XOq0iIVhsZJ3BVqXSlKsx2vsqCAaaYyqcBEb7RI=";
829 #[cfg(feature = "impure_tests")]
830 const LUATEST_ROCKSPEC_HASH: &str = "sha256-NljJ20A+VadUyhhBjrRnojeQjSqRpQy7FWcgjUt2Fdc=";
831
832 #[cfg(feature = "impure_tests")]
833 fn luatest_src_rock_bytes() -> Bytes {
834 Bytes::copy_from_slice(
835 &std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(LUATEST_SRC_ROCK))
836 .unwrap(),
837 )
838 }
839
840 #[cfg(feature = "impure_tests")]
841 fn luatest_config(dir: &assert_fs::TempDir) -> Config {
842 ConfigBuilder::new()
843 .unwrap()
844 .user_tree(Some(dir.to_path_buf()))
845 .lua_version(Some(LuaVersion::Lua51))
846 .build()
847 .unwrap()
848 }
849
850 #[cfg(feature = "impure_tests")]
851 async fn build_luatest(
852 config: &Config,
853 tree: &Tree,
854 entry_type: EntryType,
855 behaviour: BuildBehaviour,
856 ) -> LockedPackage {
857 let bytes = luatest_src_rock_bytes();
858 let rock = DownloadedPackedRockBytes {
859 name: "luatest".into(),
860 version: "0.2-1".parse().unwrap(),
861 bytes: bytes.clone(),
862 file_name: "luatest-0.2-1.src.rock".into(),
863 url: "https://example.org/luatest-0.2-1.src.rock"
864 .parse()
865 .unwrap(),
866 };
867 let rockspec = unpack_rockspec(&rock).await.unwrap();
868 let lua = LuaInstallation::new_from_config(config).await.unwrap();
869 Build::new()
870 .rockspec(&rockspec)
871 .lua(&lua)
872 .tree(tree)
873 .entry_type(entry_type)
874 .config(config)
875 .behaviour(behaviour)
876 .source_spec(RemotePackageSourceSpec::SrcRock(SrcRockSource {
877 bytes,
878 source_url: RemotePackageSourceUrl::Url {
879 url: "https://example.org/luatest-0.2-1.src.rock"
880 .parse()
881 .unwrap(),
882 },
883 }))
884 .build()
885 .await
886 .unwrap()
887 }
888
889 #[cfg(feature = "impure_tests")]
890 #[tokio::test]
891 async fn luatest_src_rock_produces_expected_hashes() {
892 let dir = assert_fs::TempDir::new().unwrap();
893 let config = luatest_config(&dir);
894 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
895
896 let package =
897 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
898
899 assert_eq!(
900 package.hashes().source,
901 LUATEST_SRC_ROCK_SOURCE_HASH.parse().unwrap()
902 );
903 assert_eq!(
904 package.hashes().rockspec,
905 LUATEST_ROCKSPEC_HASH.parse().unwrap()
906 );
907 assert_eq!(
908 package.hashes().source,
909 luatest_src_rock_bytes().hash().await.unwrap()
910 );
911 assert!(tree.bin().join("luatest").is_file());
912 }
913
914 #[cfg(feature = "impure_tests")]
915 #[tokio::test]
916 async fn force_build_is_deterministic() {
917 let dir1 = assert_fs::TempDir::new().unwrap();
918 let config1 = luatest_config(&dir1);
919 let tree1 = config1.user_tree(LuaVersion::Lua51).unwrap();
920
921 let dir2 = assert_fs::TempDir::new().unwrap();
922 let config2 = luatest_config(&dir2);
923 let tree2 = config2.user_tree(LuaVersion::Lua51).unwrap();
924
925 let package1 = build_luatest(
926 &config1,
927 &tree1,
928 EntryType::Entrypoint,
929 BuildBehaviour::Force,
930 )
931 .await;
932 let package2 = build_luatest(
933 &config2,
934 &tree2,
935 EntryType::Entrypoint,
936 BuildBehaviour::Force,
937 )
938 .await;
939
940 assert_eq!(package1, package2);
941 assert_eq!(package1.hashes(), package2.hashes());
942 }
943
944 #[cfg(feature = "impure_tests")]
945 #[tokio::test]
946 async fn noforce_skips_rebuild_when_package_is_locked() {
947 let dir = assert_fs::TempDir::new().unwrap();
948 let config = luatest_config(&dir);
949 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
950
951 let package =
952 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
953 let bin = tree.bin().join("luatest");
954 assert!(bin.is_file());
955
956 {
958 let mut lockfile = tree.lockfile().unwrap().write_guard();
959 lockfile.add_entrypoint(&package);
960 }
961
962 std::fs::remove_file(&bin).unwrap();
964
965 let skipped = build_luatest(
966 &config,
967 &tree,
968 EntryType::Entrypoint,
969 BuildBehaviour::Ignore,
970 )
971 .await;
972 assert_eq!(skipped.id(), package.id());
973 assert!(!bin.is_file());
974
975 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
976 assert!(bin.is_file());
977 }
978}