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