Skip to main content

lux_lib/build/
mod.rs

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::operations::UnpackError;
7use crate::rockspec::Rockspec;
8use crate::tree::{EntryType, InstallTree, TreeError};
9use bon::Builder;
10use bytes::Bytes;
11use std::collections::HashMap;
12use std::fs::DirEntry;
13use std::path::PathBuf;
14use std::{io, path::Path};
15use tracing::Instrument;
16
17use crate::{
18    config::Config,
19    hash::HasIntegrity,
20    lockfile::{LockedPackage, LockedPackageSpec},
21    lua_installation::LuaInstallation,
22    lua_rockspec::BuildBackendSpec,
23    operations::FetchSrcError,
24};
25use builtin::BuiltinBuildError;
26use cmake::CMakeError;
27use command::CommandError;
28use external_dependency::{ExternalDependencyError, ExternalDependencyInfo};
29
30use itertools::Itertools;
31use luarocks::LuarocksBuildError;
32use make::MakeError;
33
34use miette::Diagnostic;
35use patch::{Patch, PatchError};
36use rust_binary::RustBinaryError;
37use rust_mlua::RustError;
38use source::SourceBuildError;
39use ssri::Integrity;
40use thiserror::Error;
41use treesitter_parser::TreesitterBuildError;
42use utils::{recursive_copy_dir, CompileCFilesError, InstallBinaryError};
43
44mod builtin;
45mod cmake;
46mod command;
47mod luarocks;
48mod make;
49mod patch;
50mod rust_binary;
51mod rust_mlua;
52mod source;
53mod treesitter_parser;
54
55pub(crate) mod backend;
56pub(crate) mod utils;
57
58pub mod external_dependency;
59
60#[derive(Debug)]
61pub(crate) enum RemotePackageSourceSpec {
62    RockSpec(Option<RemotePackageSourceUrl>),
63    SrcRock(SrcRockSource),
64}
65
66/// A packed .src.rock archive.
67#[derive(Debug)]
68pub(crate) struct SrcRockSource {
69    pub bytes: Bytes,
70    pub source_url: RemotePackageSourceUrl,
71}
72
73#[derive(Error, Debug, Diagnostic)]
74#[non_exhaustive]
75pub enum BuildError {
76    #[error("builtin build failed")]
77    #[diagnostic(forward(0))]
78    Builtin(#[from] BuiltinBuildError),
79    #[error("cmake build failed")]
80    #[diagnostic(forward(0))]
81    CMake(#[from] CMakeError),
82    #[error("make build failed")]
83    #[diagnostic(forward(0))]
84    Make(#[from] MakeError),
85    #[error("command build failed")]
86    #[diagnostic(forward(0))]
87    Command(#[from] CommandError),
88    #[error("rust-mlua build failed")]
89    #[diagnostic(forward(0))]
90    Rust(#[from] RustError),
91    #[error("rust-binary build failed")]
92    #[diagnostic(forward(0))]
93    RustBinary(#[from] RustBinaryError),
94    #[error("treesitter-parser build failed")]
95    #[diagnostic(forward(0))]
96    TreesitterBuild(#[from] TreesitterBuildError),
97    #[error("luarocks build failed")]
98    #[diagnostic(forward(0))]
99    LuarocksBuild(#[from] LuarocksBuildError),
100    #[error("building from rock source failed")]
101    #[diagnostic(forward(0))]
102    SourceBuild(#[from] Box<SourceBuildError>),
103    #[error("IO operation failed")]
104    Io(#[from] io::Error),
105    #[error(transparent)]
106    #[diagnostic(transparent)]
107    Fs(#[from] fs::FsError),
108    #[error(transparent)]
109    #[diagnostic(transparent)]
110    Lockfile(#[from] LockfileError),
111    #[error(transparent)]
112    #[diagnostic(transparent)]
113    Tree(#[from] TreeError),
114
115    #[error(transparent)]
116    #[diagnostic(transparent)]
117    ExternalDependencyError(#[from] ExternalDependencyError),
118    #[error(transparent)]
119    #[diagnostic(transparent)]
120    PatchError(#[from] PatchError),
121    #[error(transparent)]
122    #[diagnostic(transparent)]
123    CompileCFiles(#[from] CompileCFilesError),
124    #[error(transparent)]
125    #[diagnostic(transparent)]
126    LuaVersion(#[from] LuaVersionError),
127    #[error(
128        r#"source integrity mismatch.
129- source: {src}
130- expected: {expected}
131- got: {actual}"#
132    )]
133    #[diagnostic(help(
134        r#"the source may have been modified or a tag may have been moved.
135check the source, then rerun the command with `--no-lock` to update the hash."#
136    ))]
137    SourceIntegrityMismatch {
138        src: String,
139        expected: Integrity,
140        actual: Integrity,
141    },
142    #[error("failed to unpack src.rock")]
143    #[diagnostic(forward(0))]
144    UnpackSrcRock(#[source] UnpackError),
145    #[error("failed to fetch rock source")]
146    #[diagnostic(forward(0))]
147    FetchSrcError(#[from] FetchSrcError),
148    #[error("failed to install binary '{file_name}'")]
149    InstallBinary {
150        file_name: String,
151        #[diagnostic_source]
152        source: InstallBinaryError,
153    },
154    #[error(transparent)]
155    #[diagnostic(transparent)]
156    LuaInstallation(#[from] LuaInstallationError),
157}
158
159impl From<SourceBuildError> for BuildError {
160    fn from(source: SourceBuildError) -> Self {
161        Self::SourceBuild(Box::new(source))
162    }
163}
164
165#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
166pub enum BuildBehaviour {
167    /// If a package with the same version is already installed, do nothing.
168    /// Otherwise, behave like [`BuildBehaviour::Conflict`].
169    Ignore,
170    /// Error if an entrypoint with the same name is already installed, even if
171    /// it is the same version.
172    Conflict,
173    /// Remove any installed entrypoint with the same name and rebuild.
174    Force,
175}
176
177#[tracing::instrument(level = "trace", skip_all)]
178async fn run_build<R: Rockspec + HasIntegrity, T: InstallTree + Sync>(
179    rockspec: &R,
180    args: RunBuildArgs<'_, T>,
181) -> Result<BuildInfo, BuildError> {
182    Ok(
183        match rockspec.build().current_platform().build_backend.to_owned() {
184            Some(BuildBackendSpec::Builtin(build_spec)) => build_spec.run(args).await?,
185            Some(BuildBackendSpec::Make(make_spec)) => make_spec.run(args).await?,
186            Some(BuildBackendSpec::CMake(cmake_spec)) => cmake_spec.run(args).await?,
187            Some(BuildBackendSpec::Command(command_spec)) => command_spec.run(args).await?,
188            Some(BuildBackendSpec::RustMlua(rust_mlua_spec)) => rust_mlua_spec.run(args).await?,
189            Some(BuildBackendSpec::RustBinary(rust_binary_spec)) => {
190                rust_binary_spec.run(args).await?
191            }
192            Some(BuildBackendSpec::TreesitterParser(treesitter_parser_spec)) => {
193                treesitter_parser_spec.run(args).await?
194            }
195            Some(BuildBackendSpec::LuaRock(build_backend_name)) => {
196                luarocks::build(&build_backend_name, rockspec, args).await?
197            }
198            Some(BuildBackendSpec::Source) => source::build(args).await?,
199            None => BuildInfo::default(),
200        },
201    )
202}
203
204#[allow(clippy::too_many_arguments)]
205#[tracing::instrument(level = "trace", skip(rockspec, tree, config))]
206async fn install<R: Rockspec + HasIntegrity, T: InstallTree>(
207    rockspec: &R,
208    tree: &T,
209    package: &LockedPackageSpec,
210    lua: &LuaInstallation,
211    build_dir: &Path,
212    entry_type: &EntryType,
213    config: &Config,
214) -> Result<(), BuildError> {
215    let install_spec = &rockspec.build().current_platform().install;
216    let layout = tree.layout_for(package);
217    {
218        let span = tracing::info_span!("Copying Lua modules");
219        let _enter = span.enter();
220        for (target, source) in &install_spec.lua {
221            let _enter = span.enter();
222            let absolute_source = build_dir.join(source);
223            utils::copy_lua_to_module_path(&absolute_source, target, &layout.src)?;
224        }
225    }
226    {
227        let span = tracing::info_span!("Compiling C libraries");
228        let _enter = span.enter();
229        for (target, source) in &install_spec.lib {
230            let absolute_source = build_dir.join(source);
231            let resolved_target = layout.lib.join(target);
232            fs::tokio::copy(&absolute_source, &resolved_target)
233                .instrument(tracing::trace_span!("copying target"))
234                .await?;
235            utils::make_writable(&resolved_target).await?;
236        }
237    }
238    if entry_type.is_entrypoint() {
239        let span = tracing::info_span!("Installing binaries");
240        let _enter = span.enter();
241        let deploy_spec = rockspec.deploy().current_platform();
242        for (target, source) in &install_spec.bin {
243            utils::install_binary(
244                &build_dir.join(source),
245                target,
246                tree,
247                lua,
248                deploy_spec,
249                config,
250            )
251            .instrument(tracing::trace_span!("installing binary"))
252            .await
253            .map_err(|err| BuildError::InstallBinary {
254                file_name: target.clone(),
255                source: err,
256            })?;
257        }
258    }
259    if !install_spec.conf.is_empty() {
260        let span = tracing::info_span!("Copying configuration files");
261        let _enter = span.enter();
262        for (target, source) in &install_spec.conf {
263            let absolute_source = build_dir.join(source);
264            let target = layout.conf.join(target);
265            if let Some(parent_dir) = target.parent() {
266                fs::tokio::create_dir_all(parent_dir)
267                    .instrument(tracing::trace_span!("creating configuration directory"))
268                    .await?;
269            }
270            fs::tokio::copy(&absolute_source, &target)
271                .instrument(tracing::trace_span!("copying configuration file"))
272                .await?;
273            utils::make_writable(&target).await?;
274        }
275    }
276    Ok(())
277}
278
279// TODO(vhyrro): break apart deployment into a separate step (once we implement transactions)
280#[derive(Builder)]
281#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
282pub(crate) struct Deploy<'a, R: Rockspec + HasIntegrity, T: InstallTree + Sync> {
283    #[builder(start_fn)]
284    rockspec: &'a R,
285    #[builder(start_fn)]
286    tree: &'a T,
287    package: LockedPackage,
288    lua: &'a LuaInstallation,
289    source_root: &'a Path,
290    entry_type: EntryType,
291    config: &'a Config,
292    behaviour: BuildBehaviour,
293}
294
295impl<R, T, State> DeployBuilder<'_, R, T, State>
296where
297    R: Rockspec + HasIntegrity,
298    T: InstallTree + Sync,
299    State: deploy_builder::State + deploy_builder::IsComplete,
300{
301    pub(crate) async fn deploy(self) -> Result<LockedPackage, BuildError> {
302        do_deploy(self._build()).await
303    }
304}
305
306#[tracing::instrument(level = "trace", skip_all, fields(package = deploy.rockspec.package().to_string()))]
307async fn do_deploy<R, T>(deploy: Deploy<'_, R, T>) -> Result<LockedPackage, BuildError>
308where
309    R: Rockspec + HasIntegrity,
310    T: InstallTree + Sync,
311{
312    let Deploy {
313        rockspec,
314        tree,
315        package,
316        lua,
317        source_root,
318        entry_type,
319        config,
320        behaviour,
321    } = deploy;
322
323    if matches!(behaviour, BuildBehaviour::Ignore | BuildBehaviour::Conflict) {
324        if let Some(existing) = tree.lockfile()?.get(&package.id()) {
325            return Ok(existing.clone());
326        }
327    }
328
329    // TODO(vhyrro): When we implement transactionality make this a Drop guard
330    // or something else.
331    tree.prepare(&package.spec)?;
332    let layout = tree.layout_for(&package.spec);
333
334    let rock_source = rockspec.source().current_platform();
335    let build_dir = resolve_source_dir(
336        source_root,
337        rock_source.unpack_dir.as_deref(),
338        &rockspec.build().current_platform().copy_directories,
339    )?;
340
341    Patch::new(&build_dir, &rockspec.build().current_platform().patches).apply()?;
342
343    let external_dependencies = rockspec
344        .external_dependencies()
345        .current_platform()
346        .iter()
347        .map(|(name, dep)| {
348            ExternalDependencyInfo::probe(name, dep, config.external_deps())
349                .map(|info| (name.clone(), info))
350        })
351        .try_collect::<_, HashMap<_, _>, _>()?;
352
353    let output = run_build(
354        rockspec,
355        RunBuildArgs::new()
356            .package(&package.spec)
357            .no_install(false)
358            .lua(lua)
359            .external_dependencies(&external_dependencies)
360            .deploy(rockspec.deploy().current_platform())
361            .config(config)
362            .tree(tree)
363            .build_dir(&build_dir)
364            .build(),
365    )
366    .await?;
367
368    let mut binaries = rockspec.binaries();
369    binaries.extend(output.binaries);
370    tree.lockfile()?
371        .write_guard()
372        .set_binaries(&package, binaries);
373
374    install(
375        rockspec,
376        tree,
377        &package.spec,
378        lua,
379        &build_dir,
380        &entry_type,
381        config,
382    )
383    .await?;
384
385    for directory in rockspec
386        .build()
387        .current_platform()
388        .copy_directories
389        .iter()
390        .filter(|dir| {
391            dir.file_name()
392                .is_some_and(|name| name != "doc" && name != "docs")
393        })
394    {
395        recursive_copy_dir(&build_dir.join(directory), &layout.etc.join(directory)).await?;
396    }
397
398    recursive_copy_doc_dir(&layout.doc, &build_dir).await?;
399
400    if let Ok(rockspec_str) = rockspec.to_lua_remote_rockspec_string() {
401        fs::sync::write(layout.rockspec_path(), rockspec_str)?;
402    }
403
404    tree.finalize(&package, entry_type)?;
405
406    Ok(package)
407}
408
409fn is_source_or_etc_dir(dir: &DirEntry, copy_dirs: &[PathBuf]) -> bool {
410    let dir_name = dir.file_name().to_string_lossy().to_string();
411    matches!(dir_name.as_str(), "lua" | "src")
412        || copy_dirs
413            .iter()
414            .any(|copy_dir_name| copy_dir_name == &PathBuf::from(&dir_name))
415}
416
417/// Resolve the directory containing a rock's sources within an extracted source archive.
418/// When the rockspec has no `source.dir`, fall back to the archive's single top-level directory.
419pub(crate) fn resolve_source_dir(
420    source_root: &Path,
421    unpack_dir: Option<&Path>,
422    copy_dirs: &[PathBuf],
423) -> Result<PathBuf, fs::FsError> {
424    if let Some(unpack_dir) = unpack_dir {
425        return Ok(source_root.join(unpack_dir));
426    }
427    // Some older/off-spec rockspecs don't specify a `source.dir`.
428    // After unpacking the archive, if
429    //
430    //   - there exist no Lua or C sources
431    //   - there exists a single subdirectory that is not a source
432    //     or etc directory
433    //
434    // we assume it's the `source.dir`.
435    // Unlike the LuaRocks implementation - which filters when fetching sources -
436    // we only infer `source.dir` if the directory name is not 'src', 'lua'
437    // or one of the `build.copy_directories`.
438    // This allows us to build local projects with only a `src` directory.
439    //
440    // LuaRocks implementation:
441    // https://github.com/luarocks/luarocks/blob/4188fdb235aca66530d274c782374cf6afba09b8/src/luarocks/fetch.tl?plain=1#L526
442    let has_lua_or_c_sources = fs::sync::read_dir(source_root)?
443        .filter_map(Result::ok)
444        .filter(|entry| entry.path().is_file())
445        .any(|entry| {
446            entry.path().extension().is_some_and(|ext| {
447                matches!(ext.to_string_lossy().to_string().as_str(), "lua" | "c")
448            })
449        });
450    if has_lua_or_c_sources {
451        return Ok(source_root.to_path_buf());
452    }
453    let dir_entries = fs::sync::read_dir(source_root)?
454        .filter_map(Result::ok)
455        .filter(|entry| entry.path().is_dir())
456        .collect_vec();
457    if dir_entries.len() == 1 && !is_source_or_etc_dir(&dir_entries[0], copy_dirs) {
458        Ok(dir_entries[0].path())
459    } else {
460        Ok(source_root.to_path_buf())
461    }
462}
463
464#[tracing::instrument(level = "trace")]
465async fn recursive_copy_doc_dir(target_doc_dir: &Path, build_dir: &Path) -> Result<(), BuildError> {
466    let mut doc_dir = build_dir.join("doc");
467    if !doc_dir.exists() {
468        doc_dir = build_dir.join("docs");
469    }
470    recursive_copy_dir(&doc_dir, target_doc_dir).await?;
471    Ok(())
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use predicates::prelude::*;
478    use std::path::PathBuf;
479
480    use assert_fs::{
481        assert::PathAssert,
482        prelude::{PathChild, PathCopy},
483    };
484
485    use crate::{
486        config::ConfigBuilder,
487        lockfile::{LockConstraint, LockedPackageHashes},
488        lua_installation::{detect_installed_lua_version, LuaInstallation},
489        lua_version::LuaVersion,
490        package::PackageSpec,
491        project::Project,
492        remote_package_source::RemotePackageSource,
493    };
494    #[cfg(feature = "impure_tests")]
495    use crate::{
496        operations::{unpack_rockspec, DownloadedPackedRockBytes},
497        pipeline::build_local::Build,
498        tree::Tree,
499    };
500
501    #[tokio::test]
502    async fn test_builtin_build() {
503        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
504        let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
505            .join("resources/test/sample-projects/no-build-spec/");
506        let tree_dir = assert_fs::TempDir::new().unwrap();
507        let config = ConfigBuilder::new()
508            .unwrap()
509            .lua_version(lua_version)
510            .user_tree(Some(tree_dir.to_path_buf()))
511            .build()
512            .unwrap();
513        let build_dir = assert_fs::TempDir::new().unwrap();
514        build_dir.copy_from(&project_root, &["**"]).unwrap();
515        let tree = config
516            .user_tree(config.lua_version().cloned().unwrap())
517            .unwrap();
518        let lua_version = config.lua_version().unwrap_or(&LuaVersion::Lua51);
519        let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
520        let project = Project::from_exact(&project_root).unwrap().unwrap();
521        let rockspec = project.toml().into_remote(None).unwrap();
522        let package = LockedPackage::from(
523            &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
524            LockConstraint::Unconstrained,
525            RemotePackageSource::Test,
526            None,
527            LockedPackageHashes {
528                rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
529                    .parse()
530                    .unwrap(),
531                source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
532                    .parse()
533                    .unwrap(),
534            },
535        );
536        tree.prepare(&package.spec).unwrap();
537        let src_dir = tree.layout_for(&package.spec).src;
538        run_build(
539            &rockspec,
540            RunBuildArgs::new()
541                .package(&package.spec)
542                .no_install(false)
543                .lua(&lua)
544                .external_dependencies(&HashMap::default())
545                .deploy(rockspec.deploy().current_platform())
546                .config(&config)
547                .tree(&tree)
548                .build_dir(&build_dir)
549                .build(),
550        )
551        .await
552        .unwrap();
553        let foo_dir = src_dir.join("foo");
554        assert!(foo_dir.is_dir());
555        let foo_init = foo_dir.join("init.lua");
556        assert!(foo_init.is_file());
557        assert!(std::fs::read_to_string(&foo_init)
558            .unwrap()
559            .contains("return true"));
560        let foo_bar_dir = foo_dir.join("bar");
561        assert!(foo_bar_dir.is_dir());
562        let foo_bar_init = foo_bar_dir.join("init.lua");
563        assert!(foo_bar_init.is_file());
564        assert!(std::fs::read_to_string(&foo_bar_init)
565            .unwrap()
566            .contains("return true"));
567        let foo_bar_baz = foo_bar_dir.join("baz.lua");
568        assert!(foo_bar_baz.is_file());
569        assert!(std::fs::read_to_string(&foo_bar_baz)
570            .unwrap()
571            .contains("return true"));
572        let bin_file = tree_dir
573            .child(lua_version.to_string())
574            .child("bin")
575            .child("hello");
576        bin_file.assert(predicate::path::is_file());
577        bin_file.assert(predicate::str::contains("#!/usr/bin/env bash"));
578        bin_file.assert(predicate::str::contains("echo \"Hello\""));
579    }
580
581    #[cfg(feature = "impure_tests")]
582    const LUATEST_SRC_ROCK: &str = "resources/test/luatest-0.2-1.src.rock";
583    #[cfg(feature = "impure_tests")]
584    const LUATEST_SRC_ROCK_SOURCE_HASH: &str =
585        "sha256-2jS0XOq0iIVhsZJ3BVqXSlKsx2vsqCAaaYyqcBEb7RI=";
586    #[cfg(feature = "impure_tests")]
587    const LUATEST_ROCKSPEC_HASH: &str = "sha256-NljJ20A+VadUyhhBjrRnojeQjSqRpQy7FWcgjUt2Fdc=";
588
589    #[cfg(feature = "impure_tests")]
590    fn luatest_src_rock_bytes() -> Bytes {
591        Bytes::copy_from_slice(
592            &std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(LUATEST_SRC_ROCK))
593                .unwrap(),
594        )
595    }
596
597    #[cfg(feature = "impure_tests")]
598    fn luatest_config(dir: &assert_fs::TempDir) -> Config {
599        ConfigBuilder::new()
600            .unwrap()
601            .user_tree(Some(dir.to_path_buf()))
602            .lua_version(Some(LuaVersion::Lua51))
603            .build()
604            .unwrap()
605    }
606
607    #[cfg(feature = "impure_tests")]
608    async fn build_luatest(
609        config: &Config,
610        tree: &Tree,
611        entry_type: EntryType,
612        behaviour: BuildBehaviour,
613    ) -> LockedPackage {
614        let bytes = luatest_src_rock_bytes();
615        let rock = DownloadedPackedRockBytes {
616            name: "luatest".into(),
617            version: "0.2-1".parse().unwrap(),
618            bytes: bytes.clone(),
619            file_name: "luatest-0.2-1.src.rock".into(),
620            url: "https://example.org/luatest-0.2-1.src.rock"
621                .parse()
622                .unwrap(),
623        };
624        let rockspec = unpack_rockspec(&rock).await.unwrap();
625        let lua = LuaInstallation::new_from_config(config).await.unwrap();
626        Build::new()
627            .rockspec(&rockspec)
628            .lua(&lua)
629            .tree(tree)
630            .entry_type(entry_type)
631            .config(config)
632            .behaviour(behaviour)
633            .source_spec(RemotePackageSourceSpec::SrcRock(SrcRockSource {
634                bytes,
635                source_url: RemotePackageSourceUrl::Url {
636                    url: "https://example.org/luatest-0.2-1.src.rock"
637                        .parse()
638                        .unwrap(),
639                },
640            }))
641            .build()
642            .await
643            .unwrap()
644    }
645
646    #[cfg(feature = "impure_tests")]
647    #[tokio::test]
648    async fn luatest_src_rock_produces_expected_hashes() {
649        let dir = assert_fs::TempDir::new().unwrap();
650        let config = luatest_config(&dir);
651        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
652
653        let package =
654            build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
655
656        assert_eq!(
657            package.hashes().source,
658            LUATEST_SRC_ROCK_SOURCE_HASH.parse().unwrap()
659        );
660        assert_eq!(
661            package.hashes().rockspec,
662            LUATEST_ROCKSPEC_HASH.parse().unwrap()
663        );
664        assert_eq!(
665            package.hashes().source,
666            luatest_src_rock_bytes().hash().await.unwrap()
667        );
668        assert!(tree.bin().join("luatest").is_file());
669    }
670
671    #[cfg(feature = "impure_tests")]
672    #[tokio::test]
673    async fn force_build_is_deterministic() {
674        let dir1 = assert_fs::TempDir::new().unwrap();
675        let config1 = luatest_config(&dir1);
676        let tree1 = config1.user_tree(LuaVersion::Lua51).unwrap();
677
678        let dir2 = assert_fs::TempDir::new().unwrap();
679        let config2 = luatest_config(&dir2);
680        let tree2 = config2.user_tree(LuaVersion::Lua51).unwrap();
681
682        let package1 = build_luatest(
683            &config1,
684            &tree1,
685            EntryType::Entrypoint,
686            BuildBehaviour::Force,
687        )
688        .await;
689        let package2 = build_luatest(
690            &config2,
691            &tree2,
692            EntryType::Entrypoint,
693            BuildBehaviour::Force,
694        )
695        .await;
696
697        assert_eq!(package1, package2);
698        assert_eq!(package1.hashes(), package2.hashes());
699    }
700
701    #[cfg(feature = "impure_tests")]
702    #[tokio::test]
703    async fn noforce_skips_rebuild_when_package_is_locked() {
704        let dir = assert_fs::TempDir::new().unwrap();
705        let config = luatest_config(&dir);
706        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
707
708        let package =
709            build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
710        let bin = tree.bin().join("luatest");
711        assert!(bin.is_file());
712
713        // Simulate a committed install
714        {
715            let mut lockfile = tree.lockfile().unwrap().write_guard();
716            lockfile.add_entrypoint(&package);
717        }
718
719        // Simulate a partially broken install.
720        std::fs::remove_file(&bin).unwrap();
721
722        let skipped = build_luatest(
723            &config,
724            &tree,
725            EntryType::Entrypoint,
726            BuildBehaviour::Ignore,
727        )
728        .await;
729        assert_eq!(skipped.id(), package.id());
730        assert!(!bin.is_file());
731
732        build_luatest(&config, &tree, EntryType::Entrypoint, BuildBehaviour::Force).await;
733        assert!(bin.is_file());
734    }
735}