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