Skip to main content

lux_lib/build/
cmake.rs

1use itertools::Itertools;
2use miette::Diagnostic;
3use std::{
4    env, io,
5    process::{ExitStatus, Stdio},
6};
7use thiserror::Error;
8use tokio::process::Command;
9use tracing::{info_span, Instrument};
10
11use crate::{
12    build::{
13        backend::{self, BuildBackend, BuildInfo, RunBuildArgs},
14        command,
15    },
16    config::Config,
17    fs,
18    lua_rockspec::CMakeBuildSpec,
19    path::{self, Paths},
20    tree::{self, InstallTree},
21    variables::{self, GetVariableError, HasVariables, VariableSubstitutionError},
22};
23
24const CMAKE_BUILD_FILE: &str = "build.lux";
25
26#[derive(Error, Debug, Diagnostic)]
27pub enum Error {
28    #[error(transparent)]
29    #[diagnostic(transparent)]
30    Tree(#[from] tree::Error),
31    #[error(transparent)]
32    #[diagnostic(transparent)]
33    Paths(#[from] path::Error),
34    #[error("{name} step failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
35    CommandFailure {
36        name: String,
37        status: ExitStatus,
38        stdout: String,
39        stderr: String,
40    },
41    #[error("failed to run `cmake` step")]
42    Io { source: io::Error },
43    #[error("failed to run `cmake` step: '{0}' command not found!")]
44    #[diagnostic(help("run `lx debug toolchains` to check available build tools."))]
45    CommandNotFound(String),
46    #[error(transparent)]
47    #[diagnostic(transparent)]
48    VariableSubstitution(#[from] VariableSubstitutionError),
49    #[error(transparent)]
50    #[diagnostic(transparent)]
51    Fs(#[from] fs::Error),
52}
53
54#[derive(Debug)]
55struct CMakeVariables;
56
57impl HasVariables for CMakeVariables {
58    #[tracing::instrument(level = "trace")]
59    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
60        Ok(match input {
61            "CMAKE_MODULE_PATH" => Some(env::var("CMAKE_MODULE_PATH").unwrap_or("".into())),
62            "CMAKE_LIBRARY_PATH" => Some(env::var("CMAKE_LIBRARY_PATH").unwrap_or("".into())),
63            "CMAKE_INCLUDE_PATH" => Some(env::var("CMAKE_INCLUDE_PATH").unwrap_or("".into())),
64            _ => None,
65        })
66    }
67}
68
69impl BuildBackend for CMakeBuildSpec {
70    type Err = Error;
71
72    #[tracing::instrument(name = "cmake::run", skip_all, level = "debug")]
73    async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
74    where
75        T: InstallTree,
76    {
77        let no_install = args.no_install;
78        let lua = args.lua;
79        let external_dependencies = args.external_dependencies;
80        let config = args.config;
81        let tree = args.tree;
82        let layout = tree.layout_for(args.package);
83        let build_dir = args.build_dir;
84
85        let build_tree = tree.build_tree(config)?;
86        let build_paths = Paths::new(&build_tree)?;
87        let lua_path = build_paths.package_path_prepended().joined();
88        let lua_cpath = build_paths.package_cpath_prepended().joined();
89        let bin_path = build_paths.path_prepended().joined();
90
91        let mut args = Vec::new();
92        if let Some(content) = self.cmake_lists_content {
93            let cmakelists = build_dir.join("CMakeLists.txt");
94            fs::tokio::write(&cmakelists, content).await?;
95            args.push(format!("-G\"{}\"", cmakelists.display()));
96        } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
97            // With msvc and x64, CMake does not select it by default so we need to be explicit.
98            args.push("-DCMAKE_GENERATOR_PLATFORM=x64".into());
99        }
100        self.variables
101            .into_iter()
102            .map(|(key, value)| {
103                let substituted_value = backend::substitute_variables(
104                    &value,
105                    &layout,
106                    lua,
107                    external_dependencies,
108                    config,
109                )?;
110                let substituted_value =
111                    variables::substitute(&[&CMakeVariables], &substituted_value)?;
112                Ok::<_, Self::Err>(format!("{key}={substituted_value}"))
113            })
114            .fold_ok((), |(), variable| args.push(format!("-D{variable}")))?;
115
116        let span = info_span!("CMake");
117        spawn_cmake_cmd(
118            config
119                .wrapped_command(
120                    config.cmake_cmd(),
121                    ["-H.".into(), format!("-B{CMAKE_BUILD_FILE}")]
122                        .into_iter()
123                        .chain(args),
124                )
125                .current_dir(build_dir)
126                .env("PATH", &bin_path)
127                .env("LUA_PATH", &lua_path)
128                .env("LUA_CPATH", &lua_cpath),
129            config,
130        )
131        .instrument(span)
132        .await?;
133
134        if self.build_pass {
135            let span = info_span!("CMake build pass");
136            spawn_cmake_cmd(
137                config
138                    .wrapped_command(
139                        config.cmake_cmd(),
140                        ["--build", CMAKE_BUILD_FILE, "--config", "Release"],
141                    )
142                    .current_dir(build_dir)
143                    .env("PATH", &bin_path)
144                    .env("LUA_PATH", &lua_path)
145                    .env("LUA_CPATH", &lua_cpath),
146                config,
147            )
148            .instrument(span)
149            .await?
150        }
151
152        if self.install_pass && !no_install {
153            let span = info_span!("CMake install pass");
154            spawn_cmake_cmd(
155                config
156                    .wrapped_command(
157                        config.cmake_cmd(),
158                        [
159                            "--build",
160                            CMAKE_BUILD_FILE,
161                            "--target",
162                            "install",
163                            "--config",
164                            "Release",
165                        ],
166                    )
167                    .current_dir(build_dir)
168                    .env("PATH", &bin_path)
169                    .env("LUA_PATH", &lua_path)
170                    .env("LUA_CPATH", &lua_cpath),
171                config,
172            )
173            .instrument(span)
174            .await?;
175        }
176
177        Ok(BuildInfo::default())
178    }
179}
180
181#[tracing::instrument(level = "trace", skip(config))]
182async fn spawn_cmake_cmd(cmd: &mut Command, config: &Config) -> Result<(), Error> {
183    match cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
184        Ok(child) => match child.wait_with_output().await {
185            Ok(output) if output.status.success() => command::trace_output(&output),
186            Ok(output) => {
187                return Err(Error::CommandFailure {
188                    name: config.cmake_cmd().clone(),
189                    status: output.status,
190                    stdout: String::from_utf8_lossy(&output.stdout).into(),
191                    stderr: String::from_utf8_lossy(&output.stderr).into(),
192                });
193            }
194            Err(source) => return Err(Error::Io { source }),
195        },
196        Err(_) => return Err(Error::CommandNotFound(config.cmake_cmd().clone())),
197    }
198    Ok(())
199}