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