Skip to main content

lux_lib/build/
make.rs

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        // Build step
78        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        // Install step
137        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}