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#[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 Ignore,
170 Conflict,
173 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#[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 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
417pub(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 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 {
715 let mut lockfile = tree.lockfile().unwrap().write_guard();
716 lockfile.add_entrypoint(&package);
717 }
718
719 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}