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