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