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 fs::tokio::copy(&absolute_source, &resolved_target)
261 .instrument(tracing::trace_span!("copying target"))
262 .await?;
263 utils::make_writable(&resolved_target).await?;
264 }
265 }
266 if entry_type.is_entrypoint() {
267 let span = tracing::info_span!("Installing binaries");
268 let _enter = span.enter();
269 let deploy_spec = rockspec.deploy().current_platform();
270 for (target, source) in &install_spec.bin {
271 utils::install_binary(
272 &build_dir.join(source),
273 target,
274 tree,
275 lua,
276 deploy_spec,
277 config,
278 )
279 .instrument(tracing::trace_span!("installing binary"))
280 .await
281 .map_err(|err| BuildError::InstallBinary {
282 file_name: target.clone(),
283 source: err,
284 })?;
285 }
286 }
287 if !install_spec.conf.is_empty() {
288 let span = tracing::info_span!("Copying configuration files");
289 let _enter = span.enter();
290 for (target, source) in &install_spec.conf {
291 let absolute_source = build_dir.join(source);
292 let target = layout.conf.join(target);
293 if let Some(parent_dir) = target.parent() {
294 fs::tokio::create_dir_all(parent_dir)
295 .instrument(tracing::trace_span!("creating configuration directory"))
296 .await?;
297 }
298 fs::tokio::copy(&absolute_source, &target)
299 .instrument(tracing::trace_span!("copying configuration file"))
300 .await?;
301 utils::make_writable(&target).await?;
302 }
303 }
304 Ok(())
305}
306
307#[derive(Builder)]
309#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
310pub(crate) struct Deploy<'a, R: Rockspec + HasIntegrity, T: InstallTree + Sync> {
311 #[builder(start_fn)]
312 rockspec: &'a R,
313 #[builder(start_fn)]
314 tree: &'a T,
315 package: LockedPackage,
316 lua: &'a LuaInstallation,
317 source_root: &'a Path,
318 entry_type: EntryType,
319 config: &'a Config,
320 behaviour: BuildBehaviour,
321}
322
323impl<R, T, State> DeployBuilder<'_, R, T, State>
324where
325 R: Rockspec + HasIntegrity,
326 T: InstallTree + Sync,
327 State: deploy_builder::State + deploy_builder::IsComplete,
328{
329 pub(crate) async fn deploy(self) -> Result<LockedPackage, BuildError> {
330 do_deploy(self._build()).await
331 }
332}
333
334#[tracing::instrument(level = "trace", skip_all, fields(package = deploy.rockspec.package().to_string()))]
335async fn do_deploy<R, T>(deploy: Deploy<'_, R, T>) -> Result<LockedPackage, BuildError>
336where
337 R: Rockspec + HasIntegrity,
338 T: InstallTree + Sync,
339{
340 let Deploy {
341 rockspec,
342 tree,
343 package,
344 lua,
345 source_root,
346 entry_type,
347 config,
348 behaviour,
349 } = deploy;
350
351 if matches!(behaviour, BuildBehaviour::Ignore | BuildBehaviour::Conflict) {
352 if let Some(existing) = tree.lockfile()?.get(&package.id()) {
353 return Ok(existing.clone());
354 }
355 }
356
357 tree.prepare(&package.spec)?;
360 let layout = tree.layout_for(&package.spec);
361
362 let rock_source = rockspec.source().current_platform();
363 let build_dir = resolve_source_dir(
364 source_root,
365 rock_source.unpack_dir.as_deref(),
366 &rockspec.build().current_platform().copy_directories,
367 )?;
368
369 Patch::new(&build_dir, &rockspec.build().current_platform().patches).apply()?;
370
371 let external_dependencies = rockspec
372 .external_dependencies()
373 .current_platform()
374 .iter()
375 .map(|(name, dep)| {
376 ExternalDependencyInfo::probe(name, dep, config.external_deps())
377 .map(|info| (name.clone(), info))
378 })
379 .try_collect::<_, HashMap<_, _>, _>()?;
380
381 let output = run_build(
382 rockspec,
383 RunBuildArgs::new()
384 .package(&package.spec)
385 .no_install(false)
386 .lua(lua)
387 .external_dependencies(&external_dependencies)
388 .deploy(rockspec.deploy().current_platform())
389 .config(config)
390 .tree(tree)
391 .build_dir(&build_dir)
392 .build(),
393 )
394 .await?;
395
396 let mut binaries = rockspec.binaries();
397 binaries.extend(output.binaries);
398 tree.lockfile()?
399 .write_guard()
400 .set_binaries(&package, binaries);
401
402 install(
403 rockspec,
404 tree,
405 &package.spec,
406 lua,
407 &build_dir,
408 &entry_type,
409 config,
410 )
411 .await?;
412
413 for directory in rockspec
414 .build()
415 .current_platform()
416 .copy_directories
417 .iter()
418 .filter(|dir| {
419 dir.file_name()
420 .is_some_and(|name| name != "doc" && name != "docs")
421 })
422 {
423 recursive_copy_dir(&build_dir.join(directory), &layout.etc.join(directory)).await?;
424 }
425
426 recursive_copy_doc_dir(&layout.doc, &build_dir).await?;
427
428 if let Ok(rockspec_str) = rockspec.to_lua_remote_rockspec_string() {
429 fs::sync::write(layout.rockspec_path(), rockspec_str)?;
430 }
431
432 tree.finalize(&package, entry_type)?;
433
434 Ok(package)
435}
436
437fn is_source_or_etc_dir(dir: &DirEntry, copy_dirs: &[PathBuf]) -> bool {
438 let dir_name = dir.file_name().to_string_lossy().to_string();
439 matches!(dir_name.as_str(), "lua" | "src")
440 || copy_dirs
441 .iter()
442 .any(|copy_dir_name| copy_dir_name == &PathBuf::from(&dir_name))
443}
444
445pub(crate) fn resolve_source_dir(
448 source_root: &Path,
449 unpack_dir: Option<&Path>,
450 copy_dirs: &[PathBuf],
451) -> Result<PathBuf, fs::FsError> {
452 if let Some(unpack_dir) = unpack_dir {
453 return Ok(source_root.join(unpack_dir));
454 }
455 let has_lua_or_c_sources = fs::sync::read_dir(source_root)?
471 .filter_map(Result::ok)
472 .filter(|entry| entry.path().is_file())
473 .any(|entry| {
474 entry.path().extension().is_some_and(|ext| {
475 matches!(
476 ext.to_string_lossy().to_string().as_str(),
477 "lua" | "luau" | "c"
478 )
479 })
480 });
481 if has_lua_or_c_sources {
482 return Ok(source_root.to_path_buf());
483 }
484 let dir_entries = fs::sync::read_dir(source_root)?
485 .filter_map(Result::ok)
486 .filter(|entry| entry.path().is_dir())
487 .collect_vec();
488 if dir_entries.len() == 1 && !is_source_or_etc_dir(&dir_entries[0], copy_dirs) {
489 Ok(dir_entries[0].path())
490 } else {
491 Ok(source_root.to_path_buf())
492 }
493}
494
495#[tracing::instrument(level = "trace")]
496async fn recursive_copy_doc_dir(target_doc_dir: &Path, build_dir: &Path) -> Result<(), BuildError> {
497 let mut doc_dir = build_dir.join("doc");
498 if !doc_dir.exists() {
499 doc_dir = build_dir.join("docs");
500 }
501 recursive_copy_dir(&doc_dir, target_doc_dir).await?;
502 Ok(())
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use predicates::prelude::*;
509 use std::path::PathBuf;
510
511 use assert_fs::{
512 assert::PathAssert,
513 prelude::{PathChild, PathCopy},
514 };
515
516 use is_executable::IsExecutable;
517
518 use crate::{
519 config::ConfigBuilder,
520 lockfile::{LockConstraint, LockedPackageHashes, LockedPackageSpec, OptState, PinnedState},
521 lua_installation::{detect_installed_lua_version, LuaInstallation},
522 lua_rockspec::RemoteLuaRockspec,
523 lua_version::LuaVersion,
524 package::PackageSpec,
525 project::Project,
526 remote_package_source::RemotePackageSource,
527 };
528 #[cfg(feature = "impure_tests")]
529 use crate::{
530 operations::{unpack_rockspec, DownloadedPackedRockBytes},
531 pipeline::build_local::Build,
532 tree::Tree,
533 };
534
535 #[tokio::test]
536 async fn test_builtin_build() {
537 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
538 let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
539 .join("resources/test/sample-projects/no-build-spec/");
540 let tree_dir = assert_fs::TempDir::new().unwrap();
541 let config = ConfigBuilder::new()
542 .unwrap()
543 .lua_version(lua_version)
544 .user_tree(Some(tree_dir.to_path_buf()))
545 .build()
546 .unwrap();
547 let build_dir = assert_fs::TempDir::new().unwrap();
548 build_dir.copy_from(&project_root, &["**"]).unwrap();
549 let tree = config
550 .user_tree(config.lua_version().cloned().unwrap())
551 .unwrap();
552 let lua_version = config.lua_version().unwrap_or(&LuaVersion::Lua51);
553 let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
554 let project = Project::from_exact(&project_root).unwrap().unwrap();
555 let rockspec = project.toml().into_remote(None).unwrap();
556 let package = LockedPackage::from(
557 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
558 LockConstraint::Unconstrained,
559 RemotePackageSource::Test,
560 None,
561 LockedPackageHashes {
562 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
563 .parse()
564 .unwrap(),
565 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
566 .parse()
567 .unwrap(),
568 },
569 );
570 tree.prepare(&package.spec).unwrap();
571 let src_dir = tree.layout_for(&package.spec).src;
572 run_build(
573 &rockspec,
574 RunBuildArgs::new()
575 .package(&package.spec)
576 .no_install(false)
577 .lua(&lua)
578 .external_dependencies(&HashMap::default())
579 .deploy(rockspec.deploy().current_platform())
580 .config(&config)
581 .tree(&tree)
582 .build_dir(&build_dir)
583 .build(),
584 )
585 .await
586 .unwrap();
587 let foo_dir = src_dir.join("foo");
588 assert!(foo_dir.is_dir());
589 let foo_init = foo_dir.join("init.lua");
590 assert!(foo_init.is_file());
591 assert!(std::fs::read_to_string(&foo_init)
592 .unwrap()
593 .contains("return true"));
594 let foo_bar_dir = foo_dir.join("bar");
595 assert!(foo_bar_dir.is_dir());
596 let foo_bar_init = foo_bar_dir.join("init.lua");
597 assert!(foo_bar_init.is_file());
598 assert!(std::fs::read_to_string(&foo_bar_init)
599 .unwrap()
600 .contains("return true"));
601 let foo_bar_baz = foo_bar_dir.join("baz.lua");
602 assert!(foo_bar_baz.is_file());
603 assert!(std::fs::read_to_string(&foo_bar_baz)
604 .unwrap()
605 .contains("return true"));
606 let bin_file = tree_dir
607 .child(lua_version.to_string())
608 .child("bin")
609 .child("hello");
610 bin_file.assert(predicate::path::is_file());
611 bin_file.assert(predicate::str::contains("#!/usr/bin/env bash"));
612 bin_file.assert(predicate::str::contains("echo \"Hello\""));
613 }
614
615 #[cfg(unix)]
616 #[tokio::test]
617 async fn test_install_bin_sets_executable_permissions() {
618 let rockspec = RemoteLuaRockspec::new(
619 r#"
620rockspec_format = "3.0"
621package = "test-builtin-bin"
622version = "1.0-1"
623source = { url = "https://github.com/lumen-oss/luarocks-stub" }
624build = {
625 type = "builtin",
626 install = { bin = { mytool = "mytool" } },
627}
628deploy = { wrap_bin_scripts = false }
629"#,
630 )
631 .unwrap();
632 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
633 let tree_dir = assert_fs::TempDir::new().unwrap();
634 let build_dir = assert_fs::TempDir::new().unwrap();
635 let config = ConfigBuilder::new()
636 .unwrap()
637 .lua_version(lua_version)
638 .user_tree(Some(tree_dir.to_path_buf()))
639 .build()
640 .unwrap();
641 let lua = LuaInstallation::new_from_config(&config).await.unwrap();
642 let tree = config
643 .user_tree(config.lua_version().cloned().unwrap())
644 .unwrap();
645 let package = LockedPackageSpec::new(
646 rockspec.package(),
647 rockspec.version(),
648 LockConstraint::Unconstrained,
649 Vec::new(),
650 Vec::new(),
651 &PinnedState::default(),
652 &OptState::default(),
653 );
654 tree.prepare(&package).unwrap();
655 crate::fs::tokio::write(build_dir.path().join("mytool"), "#!/bin/sh\necho hi\n")
656 .await
657 .unwrap();
658 install(
659 &rockspec,
660 &tree,
661 &package,
662 &lua,
663 build_dir.path(),
664 &EntryType::Entrypoint,
665 &config,
666 )
667 .await
668 .unwrap();
669 assert!(tree.bin().join("mytool").is_executable());
670 }
671
672 #[cfg(feature = "impure_tests")]
673 const LUATEST_SRC_ROCK: &str = "resources/test/luatest-0.2-1.src.rock";
674 #[cfg(feature = "impure_tests")]
675 const LUATEST_SRC_ROCK_SOURCE_HASH: &str =
676 "sha256-2jS0XOq0iIVhsZJ3BVqXSlKsx2vsqCAaaYyqcBEb7RI=";
677 #[cfg(feature = "impure_tests")]
678 const LUATEST_ROCKSPEC_HASH: &str = "sha256-NljJ20A+VadUyhhBjrRnojeQjSqRpQy7FWcgjUt2Fdc=";
679
680 #[cfg(feature = "impure_tests")]
681 fn luatest_src_rock_bytes() -> Bytes {
682 Bytes::copy_from_slice(
683 &std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(LUATEST_SRC_ROCK))
684 .unwrap(),
685 )
686 }
687
688 #[cfg(feature = "impure_tests")]
689 fn luatest_config(dir: &assert_fs::TempDir) -> Config {
690 ConfigBuilder::new()
691 .unwrap()
692 .user_tree(Some(dir.to_path_buf()))
693 .lua_version(Some(LuaVersion::Lua51))
694 .build()
695 .unwrap()
696 }
697
698 #[cfg(feature = "impure_tests")]
699 async fn build_luatest(
700 config: &Config,
701 tree: &Tree,
702 entry_type: EntryType,
703 behaviour: BuildBehaviour,
704 ) -> LockedPackage {
705 let bytes = luatest_src_rock_bytes();
706 let rock = DownloadedPackedRockBytes {
707 name: "luatest".into(),
708 version: "0.2-1".parse().unwrap(),
709 bytes: bytes.clone(),
710 file_name: "luatest-0.2-1.src.rock".into(),
711 url: "https://example.org/luatest-0.2-1.src.rock"
712 .parse()
713 .unwrap(),
714 };
715 let rockspec = unpack_rockspec(&rock).await.unwrap();
716 let lua = LuaInstallation::new_from_config(config).await.unwrap();
717 Build::new()
718 .rockspec(&rockspec)
719 .lua(&lua)
720 .tree(tree)
721 .entry_type(entry_type)
722 .config(config)
723 .behaviour(behaviour)
724 .source_spec(RemotePackageSourceSpec::SrcRock(SrcRockSource {
725 bytes,
726 source_url: RemotePackageSourceUrl::Url {
727 url: "https://example.org/luatest-0.2-1.src.rock"
728 .parse()
729 .unwrap(),
730 },
731 }))
732 .build()
733 .await
734 .unwrap()
735 }
736
737 #[cfg(feature = "impure_tests")]
738 #[tokio::test]
739 async fn luatest_src_rock_produces_expected_hashes() {
740 let dir = assert_fs::TempDir::new().unwrap();
741 let config = luatest_config(&dir);
742 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
743
744 let package =
745 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
746
747 assert_eq!(
748 package.hashes().source,
749 LUATEST_SRC_ROCK_SOURCE_HASH.parse().unwrap()
750 );
751 assert_eq!(
752 package.hashes().rockspec,
753 LUATEST_ROCKSPEC_HASH.parse().unwrap()
754 );
755 assert_eq!(
756 package.hashes().source,
757 luatest_src_rock_bytes().hash().await.unwrap()
758 );
759 assert!(tree.bin().join("luatest").is_file());
760 }
761
762 #[cfg(feature = "impure_tests")]
763 #[tokio::test]
764 async fn force_build_is_deterministic() {
765 let dir1 = assert_fs::TempDir::new().unwrap();
766 let config1 = luatest_config(&dir1);
767 let tree1 = config1.user_tree(LuaVersion::Lua51).unwrap();
768
769 let dir2 = assert_fs::TempDir::new().unwrap();
770 let config2 = luatest_config(&dir2);
771 let tree2 = config2.user_tree(LuaVersion::Lua51).unwrap();
772
773 let package1 = build_luatest(
774 &config1,
775 &tree1,
776 EntryType::Entrypoint,
777 BuildBehaviour::Force,
778 )
779 .await;
780 let package2 = build_luatest(
781 &config2,
782 &tree2,
783 EntryType::Entrypoint,
784 BuildBehaviour::Force,
785 )
786 .await;
787
788 assert_eq!(package1, package2);
789 assert_eq!(package1.hashes(), package2.hashes());
790 }
791
792 #[cfg(feature = "impure_tests")]
793 #[tokio::test]
794 async fn noforce_skips_rebuild_when_package_is_locked() {
795 let dir = assert_fs::TempDir::new().unwrap();
796 let config = luatest_config(&dir);
797 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
798
799 let package =
800 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
801 let bin = tree.bin().join("luatest");
802 assert!(bin.is_file());
803
804 {
806 let mut lockfile = tree.lockfile().unwrap().write_guard();
807 lockfile.add_entrypoint(&package);
808 }
809
810 std::fs::remove_file(&bin).unwrap();
812
813 let skipped = build_luatest(
814 &config,
815 &tree,
816 EntryType::Entrypoint,
817 BuildBehaviour::Ignore,
818 )
819 .await;
820 assert_eq!(skipped.id(), package.id());
821 assert!(!bin.is_file());
822
823 build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
824 assert!(bin.is_file());
825 }
826}