1use crate::build::backend::{BuildBackend, BuildInfo, RunBuildArgs};
2use crate::lockfile::{LockfileError, OptState, RemotePackageSourceUrl};
3use crate::lua_installation::LuaInstallationError;
4use crate::lua_rockspec::LuaVersionError;
5use crate::operations::{RemotePackageSourceMetadata, UnpackError};
6use crate::rockspec::{LuaVersionCompatibility, Rockspec};
7use crate::tree::{self, EntryType, TreeError};
8use bytes::Bytes;
9use std::collections::HashMap;
10use std::fs::DirEntry;
11use std::io::Cursor;
12use std::path::PathBuf;
13use std::{io, path::Path};
14
15use crate::{
16 config::Config,
17 hash::HasIntegrity,
18 lockfile::{LocalPackage, LocalPackageHashes, LockConstraint, PinnedState},
19 lua_installation::LuaInstallation,
20 lua_rockspec::BuildBackendSpec,
21 operations::{self, FetchSrcError},
22 package::PackageSpec,
23 progress::{Progress, ProgressBar},
24 remote_package_source::RemotePackageSource,
25 tree::{RockLayout, Tree},
26};
27use bon::Builder;
28use builtin::BuiltinBuildError;
29use cmake::CMakeError;
30use command::CommandError;
31use external_dependency::{ExternalDependencyError, ExternalDependencyInfo};
32
33use indicatif::style::TemplateError;
34use itertools::Itertools;
35use luarocks::LuarocksBuildError;
36use make::MakeError;
37use mlua::FromLua;
38use patch::{Patch, PatchError};
39use rust_mlua::RustError;
40use source::SourceBuildError;
41use ssri::Integrity;
42use thiserror::Error;
43use treesitter_parser::TreesitterBuildError;
44use utils::{recursive_copy_dir, CompileCFilesError, InstallBinaryError};
45
46mod builtin;
47mod cmake;
48mod command;
49mod luarocks;
50mod make;
51mod patch;
52mod rust_mlua;
53mod source;
54mod treesitter_parser;
55
56pub(crate) mod backend;
57pub(crate) mod utils;
58
59pub mod external_dependency;
60
61#[derive(Builder)]
64#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
65pub struct Build<'a, R: Rockspec + HasIntegrity> {
66 rockspec: &'a R,
67 tree: &'a Tree,
68 entry_type: tree::EntryType,
69 config: &'a Config,
70 progress: &'a Progress<ProgressBar>,
71 lua: &'a LuaInstallation,
72
73 #[builder(default)]
74 pin: PinnedState,
75 #[builder(default)]
76 opt: OptState,
77 #[builder(default)]
78 constraint: LockConstraint,
79 #[builder(default)]
80 behaviour: BuildBehaviour,
81
82 #[builder(setters(vis = "pub(crate)"))]
83 source_spec: Option<RemotePackageSourceSpec>,
84
85 #[builder(setters(vis = "pub(crate)"))]
87 source: Option<RemotePackageSource>,
88}
89
90#[derive(Debug)]
91pub(crate) enum RemotePackageSourceSpec {
92 RockSpec(Option<RemotePackageSourceUrl>),
93 SrcRock(SrcRockSource),
94}
95
96#[derive(Debug)]
98pub(crate) struct SrcRockSource {
99 pub bytes: Bytes,
100 pub source_url: RemotePackageSourceUrl,
101}
102
103impl<R: Rockspec + HasIntegrity, State> BuildBuilder<'_, R, State>
105where
106 State: build_builder::State + build_builder::IsComplete,
107{
108 pub async fn build(self) -> Result<LocalPackage, BuildError> {
109 do_build(self._build()).await
110 }
111}
112
113#[derive(Error, Debug)]
114pub enum BuildError {
115 #[error("builtin build failed: {0}")]
116 Builtin(#[from] BuiltinBuildError),
117 #[error("cmake build failed: {0}")]
118 CMake(#[from] CMakeError),
119 #[error("make build failed: {0}")]
120 Make(#[from] MakeError),
121 #[error("command build failed: {0}")]
122 Command(#[from] CommandError),
123 #[error("rust-mlua build failed: {0}")]
124 Rust(#[from] RustError),
125 #[error("treesitter-parser build failed: {0}")]
126 TreesitterBuild(#[from] TreesitterBuildError),
127 #[error("luarocks build failed: {0}")]
128 LuarocksBuild(#[from] LuarocksBuildError),
129 #[error("building from rock source failed: {0}")]
130 SourceBuild(#[from] SourceBuildError),
131 #[error("IO operation failed: {0}")]
132 Io(#[from] io::Error),
133 #[error(transparent)]
134 Lockfile(#[from] LockfileError),
135 #[error(transparent)]
136 Tree(#[from] TreeError),
137 #[error("failed to create spinner: {0}")]
138 SpinnerFailure(#[from] TemplateError),
139 #[error(transparent)]
140 ExternalDependencyError(#[from] ExternalDependencyError),
141 #[error(transparent)]
142 PatchError(#[from] PatchError),
143 #[error(transparent)]
144 CompileCFiles(#[from] CompileCFilesError),
145 #[error(transparent)]
146 LuaVersion(#[from] LuaVersionError),
147 #[error("source integrity mismatch.\nExpected: {expected},\nbut got: {actual}")]
148 SourceIntegrityMismatch {
149 expected: Integrity,
150 actual: Integrity,
151 },
152 #[error("failed to unpack src.rock:\n{0}")]
153 UnpackSrcRock(UnpackError),
154 #[error("failed to fetch rock source:\n{0}")]
155 FetchSrcError(#[from] FetchSrcError),
156 #[error("failed to install binary {0}:\n{1}")]
157 InstallBinary(String, InstallBinaryError),
158 #[error(transparent)]
159 LuaInstallation(#[from] LuaInstallationError),
160}
161
162#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
163pub enum BuildBehaviour {
164 #[default]
166 NoForce,
167 Force,
169}
170
171impl FromLua for BuildBehaviour {
172 fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
173 Ok(bool::from_lua(value, lua)?.into())
174 }
175}
176
177impl From<bool> for BuildBehaviour {
178 fn from(value: bool) -> Self {
179 if value {
180 Self::Force
181 } else {
182 Self::NoForce
183 }
184 }
185}
186
187async fn run_build<R: Rockspec + HasIntegrity>(
188 rockspec: &R,
189 args: RunBuildArgs<'_>,
190) -> Result<BuildInfo, BuildError> {
191 let progress = args.progress;
192 progress.map(|p| p.set_message("🛠️ Building..."));
193
194 Ok(
195 match rockspec.build().current_platform().build_backend.to_owned() {
196 Some(BuildBackendSpec::Builtin(build_spec)) => build_spec.run(args).await?,
197 Some(BuildBackendSpec::Make(make_spec)) => make_spec.run(args).await?,
198 Some(BuildBackendSpec::CMake(cmake_spec)) => cmake_spec.run(args).await?,
199 Some(BuildBackendSpec::Command(command_spec)) => command_spec.run(args).await?,
200 Some(BuildBackendSpec::RustMlua(rust_mlua_spec)) => rust_mlua_spec.run(args).await?,
201 Some(BuildBackendSpec::TreesitterParser(treesitter_parser_spec)) => {
202 treesitter_parser_spec.run(args).await?
203 }
204 Some(BuildBackendSpec::LuaRock(_)) => luarocks::build(rockspec, args).await?,
205 Some(BuildBackendSpec::Source) => source::build(args).await?,
206 None => BuildInfo::default(),
207 },
208 )
209}
210
211#[allow(clippy::too_many_arguments)]
212async fn install<R: Rockspec + HasIntegrity>(
213 rockspec: &R,
214 tree: &Tree,
215 output_paths: &RockLayout,
216 lua: &LuaInstallation,
217 build_dir: &Path,
218 entry_type: &EntryType,
219 progress: &Progress<ProgressBar>,
220 config: &Config,
221) -> Result<(), BuildError> {
222 progress.map(|p| {
223 p.set_message(format!(
224 "💻 Installing {} {}",
225 rockspec.package(),
226 rockspec.version()
227 ))
228 });
229
230 let install_spec = &rockspec.build().current_platform().install;
231 let lua_len = install_spec.lua.len();
232 let lib_len = install_spec.lib.len();
233 let bin_len = install_spec.bin.len();
234 let conf_len = install_spec.conf.len();
235 let total_len = lua_len + lib_len + bin_len + conf_len;
236 progress.map(|p| p.set_position(total_len as u64));
237
238 if lua_len > 0 {
239 progress.map(|p| p.set_message("📋 Copying Lua modules..."));
240 }
241 for (target, source) in &install_spec.lua {
242 let absolute_source = build_dir.join(source);
243 utils::copy_lua_to_module_path(&absolute_source, target, &output_paths.src)?;
244 progress.map(|p| p.set_position(p.position() + 1));
245 }
246 if lib_len > 0 {
247 progress.map(|p| p.set_message("📋 Compiling C libraries..."));
248 }
249 for (target, source) in &install_spec.lib {
250 let absolute_source = build_dir.join(source);
251 let resolved_target = output_paths.lib.join(target);
252 tokio::fs::copy(absolute_source, resolved_target).await?;
253 progress.map(|p| p.set_position(p.position() + 1));
254 }
255 if entry_type.is_entrypoint() {
256 if bin_len > 0 {
257 progress.map(|p| p.set_message("💻 Installing binaries..."));
258 }
259 let deploy_spec = rockspec.deploy().current_platform();
260 for (target, source) in &install_spec.bin {
261 utils::install_binary(
262 &build_dir.join(source),
263 target,
264 tree,
265 lua,
266 deploy_spec,
267 config,
268 )
269 .await
270 .map_err(|err| BuildError::InstallBinary(target.clone(), err))?;
271 progress.map(|p| p.set_position(p.position() + 1));
272 }
273 }
274 if conf_len > 0 {
275 progress.map(|p| p.set_message("📋 Copying configuration files..."));
276 for (target, source) in &install_spec.conf {
277 let absolute_source = build_dir.join(source);
278 let target = output_paths.conf.join(target);
279 if let Some(parent_dir) = target.parent() {
280 tokio::fs::create_dir_all(parent_dir).await?;
281 }
282 tokio::fs::copy(absolute_source, target).await?;
283 progress.map(|p| p.set_position(p.position() + 1));
284 }
285 }
286 Ok(())
287}
288
289async fn do_build<R>(build: Build<'_, R>) -> Result<LocalPackage, BuildError>
290where
291 R: Rockspec + HasIntegrity,
292{
293 let rockspec = build.rockspec;
294
295 build.progress.map(|p| {
296 p.set_message(format!(
297 "🛠️ Building {}@{}...",
298 rockspec.package(),
299 rockspec.version()
300 ))
301 });
302
303 let lua = build.lua;
304
305 rockspec.validate_lua_version(&lua.version)?;
306
307 let tree = build.tree;
308
309 let temp_dir = tempfile::tempdir()?;
310
311 let source_metadata = match build.source_spec {
312 Some(RemotePackageSourceSpec::SrcRock(SrcRockSource { bytes, source_url })) => {
313 let hash = bytes.hash()?;
314 let cursor = Cursor::new(&bytes);
315 operations::unpack_src_rock(cursor, temp_dir.path().to_path_buf(), build.progress)
316 .await
317 .map_err(BuildError::UnpackSrcRock)?;
318 RemotePackageSourceMetadata { hash, source_url }
319 }
320 Some(RemotePackageSourceSpec::RockSpec(source_url)) => {
321 operations::FetchSrc::new(temp_dir.path(), rockspec, build.config, build.progress)
322 .maybe_source_url(source_url)
323 .fetch_internal()
324 .await?
325 }
326 None => {
327 operations::FetchSrc::new(temp_dir.path(), rockspec, build.config, build.progress)
328 .fetch_internal()
329 .await?
330 }
331 };
332
333 let hashes = LocalPackageHashes {
334 rockspec: rockspec.hash()?,
335 source: source_metadata.hash.clone(),
336 };
337
338 let mut package = LocalPackage::from(
339 &PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
340 build.constraint,
341 rockspec.binaries(),
342 build
343 .source
344 .map(Result::Ok)
345 .unwrap_or_else(|| {
346 rockspec
347 .to_lua_remote_rockspec_string()
348 .map(RemotePackageSource::RockspecContent)
349 })
350 .unwrap_or(RemotePackageSource::Local),
351 Some(source_metadata.source_url.clone()),
352 hashes,
353 );
354 package.spec.pinned = build.pin;
355 package.spec.opt = build.opt;
356
357 match tree.lockfile()?.get(&package.id()) {
358 Some(package) if build.behaviour == BuildBehaviour::NoForce => Ok(package.clone()),
359 _ => {
360 let output_paths = match build.entry_type {
361 tree::EntryType::Entrypoint => tree.entrypoint(&package)?,
362 tree::EntryType::DependencyOnly => tree.dependency(&package)?,
363 };
364
365 let rock_source = rockspec.source().current_platform();
366 let build_dir = match &rock_source.unpack_dir {
367 Some(unpack_dir) => temp_dir.path().join(unpack_dir),
368 None => {
369 let has_lua_or_c_sources = std::fs::read_dir(temp_dir.path())?
385 .filter_map(Result::ok)
386 .filter(|f| f.path().is_file())
387 .any(|f| {
388 f.path().extension().is_some_and(|ext| {
389 matches!(ext.to_string_lossy().to_string().as_str(), "lua" | "c")
390 })
391 });
392 if has_lua_or_c_sources {
393 temp_dir.path().into()
394 } else {
395 let dir_entries = std::fs::read_dir(temp_dir.path())?
396 .filter_map(Result::ok)
397 .filter(|f| f.path().is_dir())
398 .collect_vec();
399 if dir_entries.len() == 1
400 && !is_source_or_etc_dir(
401 unsafe { dir_entries.first().unwrap_unchecked() },
402 rockspec,
403 )
404 {
405 unsafe {
406 temp_dir
407 .path()
408 .join(dir_entries.first().unwrap_unchecked().path())
409 }
410 } else {
411 temp_dir.path().into()
412 }
413 }
414 }
415 };
416
417 Patch::new(
418 &build_dir,
419 &rockspec.build().current_platform().patches,
420 build.progress,
421 )
422 .apply()?;
423
424 let external_dependencies = rockspec
425 .external_dependencies()
426 .current_platform()
427 .iter()
428 .map(|(name, dep)| {
429 ExternalDependencyInfo::probe(name, dep, build.config.external_deps())
430 .map(|info| (name.clone(), info))
431 })
432 .try_collect::<_, HashMap<_, _>, _>()?;
433
434 let output = run_build(
435 rockspec,
436 RunBuildArgs::new()
437 .output_paths(&output_paths)
438 .no_install(false)
439 .lua(lua)
440 .external_dependencies(&external_dependencies)
441 .deploy(rockspec.deploy().current_platform())
442 .config(build.config)
443 .tree(tree)
444 .build_dir(&build_dir)
445 .progress(build.progress)
446 .build(),
447 )
448 .await?;
449
450 package.spec.binaries.extend(output.binaries);
451
452 install(
453 rockspec,
454 tree,
455 &output_paths,
456 lua,
457 &build_dir,
458 &build.entry_type,
459 build.progress,
460 build.config,
461 )
462 .await?;
463
464 for directory in rockspec
465 .build()
466 .current_platform()
467 .copy_directories
468 .iter()
469 .filter(|dir| {
470 dir.file_name()
471 .is_some_and(|name| name != "doc" && name != "docs")
472 })
473 {
474 recursive_copy_dir(
475 &build_dir.join(directory),
476 &output_paths.etc.join(directory),
477 )
478 .await?;
479 }
480
481 recursive_copy_doc_dir(&output_paths, &build_dir).await?;
482
483 if let Ok(rockspec_str) = rockspec.to_lua_remote_rockspec_string() {
484 std::fs::write(output_paths.rockspec_path(), rockspec_str)?;
485 }
486
487 Ok(package)
488 }
489 }
490}
491
492fn is_source_or_etc_dir<R>(dir: &DirEntry, rockspec: &R) -> bool
493where
494 R: Rockspec + HasIntegrity,
495{
496 let copy_dirs = &rockspec.build().current_platform().copy_directories;
497 let dir_name = dir.file_name().to_string_lossy().to_string();
498 matches!(dir_name.as_str(), "lua" | "src")
499 || copy_dirs
500 .iter()
501 .any(|copy_dir_name| copy_dir_name == &PathBuf::from(&dir_name))
502}
503
504async fn recursive_copy_doc_dir(
505 output_paths: &RockLayout,
506 build_dir: &Path,
507) -> Result<(), BuildError> {
508 let mut doc_dir = build_dir.join("doc");
509 if !doc_dir.exists() {
510 doc_dir = build_dir.join("docs");
511 }
512 recursive_copy_dir(&doc_dir, &output_paths.doc).await?;
513 Ok(())
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use predicates::prelude::*;
520 use std::path::PathBuf;
521
522 use assert_fs::{
523 assert::PathAssert,
524 prelude::{PathChild, PathCopy},
525 };
526
527 use crate::{
528 config::{ConfigBuilder, LuaVersion},
529 lua_installation::{detect_installed_lua_version, LuaInstallation},
530 progress::MultiProgress,
531 project::Project,
532 tree::RockLayout,
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 dest_dir = assert_fs::TempDir::new().unwrap();
553 let rock_layout = RockLayout {
554 rock_path: dest_dir.to_path_buf(),
555 etc: dest_dir.join("etc"),
556 lib: dest_dir.join("lib"),
557 src: dest_dir.join("src"),
558 bin: tree.bin(),
559 conf: dest_dir.join("conf"),
560 doc: dest_dir.join("doc"),
561 };
562 let lua_version = config.lua_version().unwrap_or(&LuaVersion::Lua51);
563 let progress = MultiProgress::new(&config);
564 let bar = progress.map(MultiProgress::new_bar);
565 let lua = LuaInstallation::new(lua_version, &config, &bar)
566 .await
567 .unwrap();
568 let project = Project::from(&project_root).unwrap().unwrap();
569 let rockspec = project.toml().into_remote(None).unwrap();
570 let progress = MultiProgress::new(&config);
571 run_build(
572 &rockspec,
573 RunBuildArgs::new()
574 .output_paths(&rock_layout)
575 .no_install(false)
576 .lua(&lua)
577 .external_dependencies(&HashMap::default())
578 .deploy(rockspec.deploy().current_platform())
579 .config(&config)
580 .tree(&tree)
581 .build_dir(&build_dir)
582 .progress(&progress.map(|p| p.new_bar()))
583 .build(),
584 )
585 .await
586 .unwrap();
587 let foo_dir = dest_dir.child("src").child("foo");
588 foo_dir.assert(predicate::path::is_dir());
589 let foo_init = foo_dir.child("init.lua");
590 foo_init.assert(predicate::path::is_file());
591 foo_init.assert(predicate::str::contains("return true"));
592 let foo_bar_dir = foo_dir.child("bar");
593 foo_bar_dir.assert(predicate::path::is_dir());
594 let foo_bar_init = foo_bar_dir.child("init.lua");
595 foo_bar_init.assert(predicate::path::is_file());
596 foo_bar_init.assert(predicate::str::contains("return true"));
597 let foo_bar_baz = foo_bar_dir.child("baz.lua");
598 foo_bar_baz.assert(predicate::path::is_file());
599 foo_bar_baz.assert(predicate::str::contains("return true"));
600 let bin_file = tree_dir
601 .child(lua_version.to_string())
602 .child("bin")
603 .child("hello");
604 bin_file.assert(predicate::path::is_file());
605 bin_file.assert(predicate::str::contains("#!/usr/bin/env bash"));
606 bin_file.assert(predicate::str::contains("echo \"Hello\""));
607 }
608}