lux_lib/build/
mod.rs

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/// A rocks package builder, providing fine-grained control
53/// over how a package should be built.
54#[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    // TODO(vhyrro): Remove this and enforce that this is provided at a type level.
81    source: Option<RemotePackageSource>,
82}
83
84// Overwrite the `build()` function to use our own instead.
85impl<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    /// Don't force a rebuild if the package is already installed
142    NoForce,
143    /// Force a rebuild if the package is already installed
144    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                    // Some older rockspecs don't specify a source.dir.
432                    // If there exists a single directory and a rockspec
433                    // after unpacking, we assume it's the source directory.
434                    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}