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