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