Skip to main content

lux_lib/build/
command.rs

1use miette::Diagnostic;
2use std::{
3    collections::HashMap,
4    io,
5    path::Path,
6    process::{ExitStatus, Output, Stdio},
7};
8use thiserror::Error;
9
10use tracing::{info_span, Instrument};
11use which::which;
12
13use crate::{
14    build::backend::{self, BuildBackend, BuildInfo, RunBuildArgs},
15    config::Config,
16    lua_installation::LuaInstallation,
17    lua_rockspec::CommandBuildSpec,
18    path::{self, Paths},
19    tree::{self, InstallTree, RockLayout},
20    variables::VariableSubstitutionError,
21};
22
23use super::external_dependency::ExternalDependencyInfo;
24
25#[derive(Error, Debug, Diagnostic)]
26pub enum Error {
27    #[error(transparent)]
28    #[diagnostic(transparent)]
29    Tree(#[from] tree::Error),
30    #[error(transparent)]
31    #[diagnostic(transparent)]
32    Paths(#[from] path::Error),
33    #[error("'build_command' and 'install_command' cannot be empty.")]
34    #[diagnostic(
35        help("set a command in the `build.build_command` or `build.install_command` field"),
36        url("https://lux.lumen-labs.org/reference/lux-toml#command")
37    )]
38    EmptyCommand,
39    #[error("error parsing command:\n{command}\n\nerror: {err}")]
40    #[diagnostic(
41        help("put build/install command arguments in quotes if they contain spaces"),
42        url("https://lux.lumen-labs.org/reference/lux-toml/#command")
43    )]
44    ParseError {
45        err: shell_words::ParseError,
46        command: String,
47    },
48    #[error("cannot find a shell to execute the command")]
49    ShellNotFoundError(#[from] which::Error),
50    #[error("error executing command:\n{command}\n\nerror: {err}")]
51    Io { err: io::Error, command: String },
52    #[error(
53        "failed to execute command:\n{command}\n\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}"
54    )]
55    CommandFailure {
56        command: String,
57        status: ExitStatus,
58        stdout: String,
59        stderr: String,
60    },
61    #[error(transparent)]
62    #[diagnostic(transparent)]
63    VariableSubstitution(#[from] VariableSubstitutionError),
64}
65pub(crate) fn trace_output(output: &Output) {
66    if !output.stderr.is_empty() {
67        for line in String::from_utf8_lossy(&output.stderr).lines() {
68            tracing::debug!("{}", line);
69        }
70    }
71    if !output.stdout.is_empty() {
72        for line in String::from_utf8_lossy(&output.stdout).lines() {
73            tracing::debug!("{}", line);
74        }
75    }
76}
77
78impl BuildBackend for CommandBuildSpec {
79    type Err = Error;
80
81    #[tracing::instrument(name = "command::run", skip_all, level = "debug")]
82    async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
83    where
84        T: InstallTree,
85    {
86        let package = args.package;
87        let no_install = args.no_install;
88        let lua = args.lua;
89        let external_dependencies = args.external_dependencies;
90        let config = args.config;
91        let tree = args.tree;
92        let layout = tree.layout_for(package);
93        let build_dir = args.build_dir;
94
95        let build_tree = tree.build_tree(config)?;
96        let build_paths = Paths::new(&build_tree)?;
97
98        if let Some(build_command) = &self.build_command {
99            run_command(
100                build_command,
101                &layout,
102                lua,
103                external_dependencies,
104                config,
105                build_dir,
106                &build_paths,
107            )
108            .await?;
109        }
110        if !no_install {
111            if let Some(install_command) = &self.install_command {
112                run_command(
113                    install_command,
114                    &layout,
115                    lua,
116                    external_dependencies,
117                    config,
118                    build_dir,
119                    &build_paths,
120                )
121                .await?;
122            }
123        }
124        Ok(BuildInfo::default())
125    }
126}
127
128#[tracing::instrument(level = "trace", skip(config, output_paths))]
129async fn run_command(
130    command: &str,
131    output_paths: &RockLayout,
132    lua: &LuaInstallation,
133    external_dependencies: &HashMap<String, ExternalDependencyInfo>,
134    config: &Config,
135    build_dir: &Path,
136    build_paths: &Paths,
137) -> Result<(), Error> {
138    let lua_path = build_paths.package_path_prepended().joined();
139    let lua_cpath = build_paths.package_cpath_prepended().joined();
140    let bin_path = build_paths.path_prepended().joined();
141    let substituted_cmd =
142        backend::substitute_variables(command, output_paths, lua, external_dependencies, config)?;
143
144    if substituted_cmd.is_empty() {
145        return Err(Error::EmptyCommand);
146    }
147
148    #[cfg(target_env = "msvc")]
149    let (shell, shell_arg) = (which("cmd.exe")?, "/C");
150    #[cfg(not(target_env = "msvc"))]
151    let (shell, shell_arg) = (which("sh")?, "-c");
152
153    match config
154        .wrapped_command(shell, [shell_arg, &substituted_cmd])
155        .current_dir(build_dir)
156        .stdout(Stdio::piped())
157        .stderr(Stdio::piped())
158        .env("PATH", &bin_path)
159        .env("LUA_PATH", &lua_path)
160        .env("LUA_CPATH", &lua_cpath)
161        .spawn()
162    {
163        Err(err) => {
164            return Err(Error::Io {
165                err,
166                command: substituted_cmd,
167            });
168        }
169        Ok(child) => match child
170            .wait_with_output()
171            .instrument(info_span!(
172                "Running build command",
173                command = substituted_cmd
174            ))
175            .await
176        {
177            Ok(output) if output.status.success() => trace_output(&output),
178            Ok(output) => {
179                return Err(Error::CommandFailure {
180                    command: substituted_cmd,
181                    status: output.status,
182                    stdout: String::from_utf8_lossy(&output.stdout).into(),
183                    stderr: String::from_utf8_lossy(&output.stderr).into(),
184                });
185            }
186            Err(err) => {
187                return Err(Error::Io {
188                    err,
189                    command: substituted_cmd,
190                });
191            }
192        },
193    }
194    Ok(())
195}