lux_lib/build/
cmake.rs

1use itertools::Itertools;
2use std::{
3    collections::HashMap,
4    env, io,
5    path::Path,
6    process::{ExitStatus, Stdio},
7};
8use thiserror::Error;
9use tokio::process::Command;
10
11use crate::{
12    build::utils,
13    config::Config,
14    lua_installation::LuaInstallation,
15    lua_rockspec::{Build, BuildInfo, CMakeBuildSpec},
16    progress::{Progress, ProgressBar},
17    tree::{RockLayout, Tree},
18    variables::{self, HasVariables, VariableSubstitutionError},
19};
20
21use super::external_dependency::ExternalDependencyInfo;
22
23const CMAKE_BUILD_FILE: &str = "build.lux";
24
25#[derive(Error, Debug)]
26pub enum CMakeError {
27    #[error("{name} step failed.\n\n{status}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}")]
28    CommandFailure {
29        name: String,
30        status: ExitStatus,
31        stdout: String,
32        stderr: String,
33    },
34    #[error("failed to run `cmake` step: {0}")]
35    Io(io::Error),
36    #[error("failed to write CMakeLists.txt: {0}")]
37    WriteCmakeListsError(io::Error),
38    #[error("failed to run `cmake` step: `{0}` command not found!")]
39    CommandNotFound(String),
40    #[error(transparent)]
41    VariableSubstitutionError(#[from] VariableSubstitutionError),
42}
43
44struct CMakeVariables;
45
46impl HasVariables for CMakeVariables {
47    fn get_variable(&self, input: &str) -> Option<String> {
48        match input {
49            "CMAKE_MODULE_PATH" => Some(env::var("CMAKE_MODULE_PATH").unwrap_or("".into())),
50            "CMAKE_LIBRARY_PATH" => Some(env::var("CMAKE_LIBRARY_PATH").unwrap_or("".into())),
51            "CMAKE_INCLUDE_PATH" => Some(env::var("CMAKE_INCLUDE_PATH").unwrap_or("".into())),
52            _ => None,
53        }
54    }
55}
56
57impl Build for CMakeBuildSpec {
58    type Err = CMakeError;
59
60    async fn run(
61        self,
62        output_paths: &RockLayout,
63        no_install: bool,
64        lua: &LuaInstallation,
65        external_dependencies: &HashMap<String, ExternalDependencyInfo>,
66        config: &Config,
67        _tree: &Tree,
68        build_dir: &Path,
69        _progress: &Progress<ProgressBar>,
70    ) -> Result<BuildInfo, Self::Err> {
71        let mut args = Vec::new();
72        if let Some(content) = self.cmake_lists_content {
73            let cmakelists = build_dir.join("CMakeLists.txt");
74            std::fs::write(&cmakelists, content).map_err(CMakeError::WriteCmakeListsError)?;
75            args.push(format!("-G\"{}\"", cmakelists.display()));
76        } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
77            // With msvc and x64, CMake does not select it by default so we need to be explicit.
78            args.push("-DCMAKE_GENERATOR_PLATFORM=x64".into());
79        }
80        self.variables
81            .into_iter()
82            .map(|(key, value)| {
83                let substituted_value = utils::substitute_variables(
84                    &value,
85                    output_paths,
86                    lua,
87                    external_dependencies,
88                    config,
89                )?;
90                let substituted_value =
91                    variables::substitute(&[&CMakeVariables], &substituted_value)?;
92                Ok::<_, Self::Err>(format!("{key}={substituted_value}"))
93            })
94            .fold_ok((), |(), variable| args.push(format!("-D{}", variable)))?;
95
96        spawn_cmake_cmd(
97            Command::new(config.cmake_cmd())
98                .current_dir(build_dir)
99                .arg("-H.")
100                .arg(format!("-B{}", CMAKE_BUILD_FILE))
101                .args(args),
102            config,
103        )
104        .await?;
105
106        if self.build_pass {
107            spawn_cmake_cmd(
108                Command::new(config.cmake_cmd())
109                    .current_dir(build_dir)
110                    .arg("--build")
111                    .arg(CMAKE_BUILD_FILE)
112                    .arg("--config")
113                    .arg("Release"),
114                config,
115            )
116            .await?
117        }
118
119        if self.install_pass && !no_install {
120            spawn_cmake_cmd(
121                Command::new(config.cmake_cmd())
122                    .current_dir(build_dir)
123                    .arg("--build")
124                    .arg(CMAKE_BUILD_FILE)
125                    .arg("--target")
126                    .arg("install")
127                    .arg("--config")
128                    .arg("Release"),
129                config,
130            )
131            .await?;
132        }
133
134        Ok(BuildInfo::default())
135    }
136}
137
138async fn spawn_cmake_cmd(cmd: &mut Command, config: &Config) -> Result<(), CMakeError> {
139    match cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
140        Ok(child) => match child.wait_with_output().await {
141            Ok(output) if output.status.success() => {}
142            Ok(output) => {
143                return Err(CMakeError::CommandFailure {
144                    name: config.cmake_cmd().clone(),
145                    status: output.status,
146                    stdout: String::from_utf8_lossy(&output.stdout).into(),
147                    stderr: String::from_utf8_lossy(&output.stderr).into(),
148                });
149            }
150            Err(err) => return Err(CMakeError::Io(err)),
151        },
152        Err(_) => return Err(CMakeError::CommandNotFound(config.cmake_cmd().clone())),
153    }
154    Ok(())
155}