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 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}