1use itertools::Itertools;
2use miette::Diagnostic;
3use path_slash::PathBufExt;
4use std::{
5 io,
6 path::PathBuf,
7 process::{ExitStatus, Stdio},
8};
9use thiserror::Error;
10
11use tracing::{info_span, Instrument};
12
13use crate::{
14 build::{
15 backend::{self, BuildBackend, BuildInfo, RunBuildArgs},
16 command,
17 },
18 lua_rockspec::MakeBuildSpec,
19 path::{self, Paths},
20 tree::{self, InstallTree},
21 variables::VariableSubstitutionError,
22};
23
24#[derive(Error, Debug, Diagnostic)]
25#[non_exhaustive]
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("{name} step failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
34 CommandFailure {
35 name: String,
36 status: ExitStatus,
37 stdout: String,
38 stderr: String,
39 },
40 #[error("failed to run `make` step")]
41 Io(#[source] io::Error),
42 #[error("failed to run `make` step: '{0}' command not found!")]
43 #[diagnostic(help("run `lx debug toolchains` to check available build tools."))]
44 CommandNotFound(String),
45 #[error(transparent)]
46 #[diagnostic(transparent)]
47 VariableSubstitution(#[from] VariableSubstitutionError),
48 #[error("{0} not found")]
49 #[diagnostic(help(
50 "ensure the Makefile exists in the build directory or check the rockspec source layout."
51 ))]
52 MakefileNotFound(PathBuf),
53}
54
55impl BuildBackend for MakeBuildSpec {
56 type Err = Error;
57
58 #[tracing::instrument(name = "make::run", skip_all, level = "debug")]
59 async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
60 where
61 T: InstallTree,
62 {
63 let no_install = args.no_install;
64 let lua = args.lua;
65 let external_dependencies = args.external_dependencies;
66 let config = args.config;
67 let tree = args.tree;
68 let layout = tree.layout_for(args.package);
69 let build_dir = args.build_dir;
70
71 let build_tree = tree.build_tree(config)?;
72 let build_paths = Paths::new(&build_tree)?;
73 let lua_path = build_paths.package_path_prepended().joined();
74 let lua_cpath = build_paths.package_cpath_prepended().joined();
75 let bin_path = build_paths.path_prepended().joined();
76
77 if self.build_pass {
79 let build_args = self
80 .variables
81 .iter()
82 .chain(&self.build_variables)
83 .filter(|(_, value)| !value.is_empty())
84 .map(|(key, value)| {
85 let substituted_value = backend::substitute_variables(
86 value,
87 &layout,
88 lua,
89 external_dependencies,
90 config,
91 )?;
92 Ok(format!("{key}={substituted_value}").trim().to_string())
93 })
94 .try_collect::<_, Vec<_>, Self::Err>()?;
95 let mut make_args: Vec<String> = Vec::new();
96 if let Some(build_target) = &self.build_target {
97 make_args.push(build_target.clone());
98 }
99 make_args.push("-f".into());
100 make_args.push(self.makefile.to_slash_lossy().into());
101 make_args.extend(build_args);
102 let mut cmd = config.wrapped_command(config.make_cmd(), make_args);
103 cmd.current_dir(build_dir)
104 .stdout(Stdio::piped())
105 .stderr(Stdio::piped())
106 .env("PATH", &bin_path)
107 .env("LUA_PATH", &lua_path)
108 .env("LUA_CPATH", &lua_cpath);
109 match cmd.spawn() {
110 Ok(child) => match child
111 .wait_with_output()
112 .instrument(info_span!("Make build pass"))
113 .await
114 {
115 Ok(output) if output.status.success() => command::trace_output(&output),
116 Ok(output) => {
117 return Err(Error::CommandFailure {
118 name: match self.build_target {
119 Some(build_target) => {
120 format!("{} {}", config.make_cmd(), build_target)
121 }
122 None => config.make_cmd(),
123 },
124
125 status: output.status,
126 stdout: String::from_utf8_lossy(&output.stdout).into(),
127 stderr: String::from_utf8_lossy(&output.stderr).into(),
128 });
129 }
130 Err(err) => return Err(Error::Io(err)),
131 },
132 Err(_) => return Err(Error::CommandNotFound(config.make_cmd().clone())),
133 }
134 };
135
136 if self.install_pass && !no_install {
138 let install_args = self
139 .variables
140 .iter()
141 .chain(&self.install_variables)
142 .filter(|(_, value)| !value.is_empty())
143 .map(|(key, value)| {
144 let substituted_value = backend::substitute_variables(
145 value,
146 &layout,
147 lua,
148 external_dependencies,
149 config,
150 )?;
151 Ok(format!("{key}={substituted_value}").trim().to_string())
152 })
153 .try_collect::<_, Vec<_>, Self::Err>()?;
154 let mut install_cmd_args: Vec<String> = Vec::with_capacity(install_args.len() + 3);
155 install_cmd_args.push(self.install_target.clone());
156 install_cmd_args.push("-f".into());
157 install_cmd_args.push(self.makefile.to_slash_lossy().into());
158 install_cmd_args.extend(install_args);
159 match config
160 .wrapped_command(config.make_cmd(), install_cmd_args)
161 .current_dir(build_dir)
162 .env("PATH", &bin_path)
163 .env("LUA_PATH", &lua_path)
164 .env("LUA_CPATH", &lua_cpath)
165 .output()
166 .instrument(info_span!("Make install pass"))
167 .await
168 {
169 Ok(output) if output.status.success() => command::trace_output(&output),
170 Ok(output) => {
171 return Err(Error::CommandFailure {
172 name: format!("{} {}", config.make_cmd(), self.install_target),
173 status: output.status,
174 stdout: String::from_utf8_lossy(&output.stdout).into(),
175 stderr: String::from_utf8_lossy(&output.stderr).into(),
176 });
177 }
178 Err(err) => return Err(Error::Io(err)),
179 }
180 };
181
182 Ok(BuildInfo::default())
183 }
184}