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}