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