1 | use cargo_metadata::{camino::Utf8PathBuf, DependencyKind}; |
2 | use cargo_platform::Cfg; |
3 | use color_eyre::eyre::{bail, eyre, Result}; |
4 | use std::{ |
5 | collections::{hash_map::Entry, HashMap, HashSet}, |
6 | ffi::OsString, |
7 | path::PathBuf, |
8 | process::Command, |
9 | str::FromStr, |
10 | sync::{Arc, OnceLock, RwLock}, |
11 | }; |
12 | |
13 | use crate::{ |
14 | build_aux, status_emitter::StatusEmitter, Config, Errored, Mode, OutputConflictHandling, |
15 | }; |
16 | |
17 | #[derive (Default, Debug)] |
18 | pub struct Dependencies { |
19 | /// All paths that must be imported with `-L dependency=`. This is for |
20 | /// finding proc macros run on the host and dependencies for the target. |
21 | pub import_paths: Vec<PathBuf>, |
22 | /// The name as chosen in the `Cargo.toml` and its corresponding rmeta file. |
23 | pub dependencies: Vec<(String, Vec<Utf8PathBuf>)>, |
24 | } |
25 | |
26 | fn cfgs(config: &Config) -> Result<Vec<Cfg>> { |
27 | let mut cmd: Command = config.cfgs.build(&config.out_dir); |
28 | cmd.arg("--target" ).arg(config.target.as_ref().unwrap()); |
29 | let output: Output = cmd.output()?; |
30 | let stdout: String = String::from_utf8(vec:output.stdout)?; |
31 | |
32 | if !output.status.success() { |
33 | let stderr: String = String::from_utf8(vec:output.stderr)?; |
34 | bail!( |
35 | "failed to obtain `cfg` information from {cmd:?}: \nstderr: \n{stderr}\n\nstdout: {stdout}" |
36 | ); |
37 | } |
38 | let mut cfgs: Vec = vec![]; |
39 | |
40 | for line: &str in stdout.lines() { |
41 | cfgs.push(Cfg::from_str(line)?); |
42 | } |
43 | |
44 | Ok(cfgs) |
45 | } |
46 | |
47 | /// Compiles dependencies and returns the crate names and corresponding rmeta files. |
48 | pub(crate) fn build_dependencies(config: &Config) -> Result<Dependencies> { |
49 | let manifest_path = match &config.dependencies_crate_manifest_path { |
50 | Some(path) => path.to_owned(), |
51 | None => return Ok(Default::default()), |
52 | }; |
53 | let manifest_path = &manifest_path; |
54 | let mut build = config.dependency_builder.build(&config.out_dir); |
55 | build.arg(manifest_path); |
56 | |
57 | if let Some(target) = &config.target { |
58 | build.arg(format!("--target= {target}" )); |
59 | } |
60 | |
61 | // Reusable closure for setting up the environment both for artifact generation and `cargo_metadata` |
62 | let set_locking = |cmd: &mut Command| match (&config.output_conflict_handling, &config.mode) { |
63 | (_, Mode::Yolo { .. }) => {} |
64 | (OutputConflictHandling::Error(_), _) => { |
65 | cmd.arg("--locked" ); |
66 | } |
67 | _ => {} |
68 | }; |
69 | |
70 | set_locking(&mut build); |
71 | build.arg("--message-format=json" ); |
72 | |
73 | let output = build.output()?; |
74 | |
75 | if !output.status.success() { |
76 | let stdout = String::from_utf8(output.stdout)?; |
77 | let stderr = String::from_utf8(output.stderr)?; |
78 | bail!("failed to compile dependencies: \ncommand: {build:?}\nstderr: \n{stderr}\n\nstdout: {stdout}" ); |
79 | } |
80 | |
81 | // Collect all artifacts generated |
82 | let artifact_output = output.stdout; |
83 | let artifact_output = String::from_utf8(artifact_output)?; |
84 | let mut import_paths: HashSet<PathBuf> = HashSet::new(); |
85 | let mut artifacts = HashMap::new(); |
86 | for line in artifact_output.lines() { |
87 | let Ok(message) = serde_json::from_str::<cargo_metadata::Message>(line) else { |
88 | continue; |
89 | }; |
90 | if let cargo_metadata::Message::CompilerArtifact(artifact) = message { |
91 | if artifact |
92 | .filenames |
93 | .iter() |
94 | .any(|f| f.ends_with("build-script-build" )) |
95 | { |
96 | continue; |
97 | } |
98 | // Check that we only collect rmeta and rlib crates, not build script crates |
99 | if artifact |
100 | .filenames |
101 | .iter() |
102 | .any(|f| !matches!(f.extension(), Some("rlib" | "rmeta" ))) |
103 | { |
104 | continue; |
105 | } |
106 | for filename in &artifact.filenames { |
107 | import_paths.insert(filename.parent().unwrap().into()); |
108 | } |
109 | let package_id = artifact.package_id; |
110 | if let Some(prev) = artifacts.insert(package_id.clone(), Ok(artifact.filenames)) { |
111 | artifacts.insert( |
112 | package_id.clone(), |
113 | Err(format!(" {prev:#?} vs {:#?}" , artifacts[&package_id])), |
114 | ); |
115 | } |
116 | } |
117 | } |
118 | |
119 | // Check which crates are mentioned in the crate itself |
120 | let mut metadata = cargo_metadata::MetadataCommand::new().cargo_command(); |
121 | metadata.arg("--manifest-path" ).arg(manifest_path); |
122 | config.dependency_builder.apply_env(&mut metadata); |
123 | set_locking(&mut metadata); |
124 | let output = metadata.output()?; |
125 | |
126 | if !output.status.success() { |
127 | let stdout = String::from_utf8(output.stdout)?; |
128 | let stderr = String::from_utf8(output.stderr)?; |
129 | bail!("failed to run cargo-metadata: \nstderr: \n{stderr}\n\nstdout: {stdout}" ); |
130 | } |
131 | |
132 | let output = output.stdout; |
133 | let output = String::from_utf8(output)?; |
134 | |
135 | let cfg = cfgs(config)?; |
136 | |
137 | for line in output.lines() { |
138 | if !line.starts_with('{' ) { |
139 | continue; |
140 | } |
141 | let metadata: cargo_metadata::Metadata = serde_json::from_str(line)?; |
142 | // Only take artifacts that are defined in the Cargo.toml |
143 | |
144 | // First, find the root artifact |
145 | let root = metadata |
146 | .packages |
147 | .iter() |
148 | .find(|package| { |
149 | package.manifest_path.as_std_path().canonicalize().unwrap() |
150 | == manifest_path.canonicalize().unwrap() |
151 | }) |
152 | .unwrap(); |
153 | |
154 | // Then go over all of its dependencies |
155 | let dependencies = root |
156 | .dependencies |
157 | .iter() |
158 | .filter(|dep| matches!(dep.kind, DependencyKind::Normal)) |
159 | // Only consider dependencies that are enabled on the current target |
160 | .filter(|dep| match &dep.target { |
161 | Some(platform) => platform.matches(config.target.as_ref().unwrap(), &cfg), |
162 | None => true, |
163 | }) |
164 | .map(|dep| { |
165 | let package = metadata |
166 | .packages |
167 | .iter() |
168 | .find(|&p| p.name == dep.name && dep.req.matches(&p.version)) |
169 | .expect("dependency does not exist" ); |
170 | ( |
171 | package, |
172 | dep.rename.clone().unwrap_or_else(|| package.name.clone()), |
173 | ) |
174 | }) |
175 | // Also expose the root crate |
176 | .chain(std::iter::once((root, root.name.clone()))) |
177 | .filter_map(|(package, name)| { |
178 | // Get the id for the package matching the version requirement of the dep |
179 | let id = &package.id; |
180 | // Return the name chosen in `Cargo.toml` and the path to the corresponding artifact |
181 | match artifacts.remove(id) { |
182 | Some(Ok(artifacts)) => Some(Ok((name.replace('-' , "_" ), artifacts))), |
183 | Some(Err(what)) => Some(Err(eyre!("`ui_test` does not support crates that appear as both build-dependencies and core dependencies: {id}: {what}" ))), |
184 | None => { |
185 | if name == root.name { |
186 | // If there are no artifacts, this is the root crate and it is being built as a binary/test |
187 | // instead of a library. We simply add no artifacts, meaning you can't depend on functions |
188 | // and types declared in the root crate. |
189 | None |
190 | } else { |
191 | panic!("no artifact found for ` {name}`(` {id}`):` \n{artifact_output}" ) |
192 | } |
193 | } |
194 | } |
195 | }) |
196 | .collect::<Result<Vec<_>>>()?; |
197 | let import_paths = import_paths.into_iter().collect(); |
198 | return Ok(Dependencies { |
199 | dependencies, |
200 | import_paths, |
201 | }); |
202 | } |
203 | |
204 | bail!("no json found in cargo-metadata output" ) |
205 | } |
206 | |
207 | #[derive (PartialEq, Eq, Debug, Hash, Clone)] |
208 | pub enum Build { |
209 | /// Build the dependencies. |
210 | Dependencies, |
211 | /// Build an aux-build. |
212 | Aux { aux_file: PathBuf }, |
213 | } |
214 | impl Build { |
215 | fn description(&self) -> String { |
216 | match self { |
217 | Build::Dependencies => "Building dependencies" .into(), |
218 | Build::Aux { aux_file: &PathBuf } => format!("Building aux file {}" , aux_file.display()), |
219 | } |
220 | } |
221 | } |
222 | |
223 | pub struct BuildManager<'a> { |
224 | #[allow (clippy::type_complexity)] |
225 | cache: RwLock<HashMap<Build, Arc<OnceLock<Result<Vec<OsString>, ()>>>>>, |
226 | status_emitter: &'a dyn StatusEmitter, |
227 | } |
228 | |
229 | impl<'a> BuildManager<'a> { |
230 | pub fn new(status_emitter: &'a dyn StatusEmitter) -> Self { |
231 | Self { |
232 | cache: Default::default(), |
233 | status_emitter, |
234 | } |
235 | } |
236 | /// This function will block until the build is done and then return the arguments |
237 | /// that need to be passed in order to build the dependencies. |
238 | /// The error is only reported once, all follow up invocations of the same build will |
239 | /// have a generic error about a previous build failing. |
240 | pub fn build(&self, what: Build, config: &Config) -> Result<Vec<OsString>, Errored> { |
241 | // Fast path without much contention. |
242 | if let Some(res) = self.cache.read().unwrap().get(&what).and_then(|o| o.get()) { |
243 | return res.clone().map_err(|()| Errored { |
244 | command: Command::new(format!(" {what:?}" )), |
245 | errors: vec![], |
246 | stderr: b"previous build failed" .to_vec(), |
247 | stdout: vec![], |
248 | }); |
249 | } |
250 | let mut lock = self.cache.write().unwrap(); |
251 | let once = match lock.entry(what.clone()) { |
252 | Entry::Occupied(entry) => { |
253 | if let Some(res) = entry.get().get() { |
254 | return res.clone().map_err(|()| Errored { |
255 | command: Command::new(format!(" {what:?}" )), |
256 | errors: vec![], |
257 | stderr: b"previous build failed" .to_vec(), |
258 | stdout: vec![], |
259 | }); |
260 | } |
261 | entry.get().clone() |
262 | } |
263 | Entry::Vacant(entry) => { |
264 | let once = Arc::new(OnceLock::new()); |
265 | entry.insert(once.clone()); |
266 | once |
267 | } |
268 | }; |
269 | drop(lock); |
270 | |
271 | let mut err = None; |
272 | once.get_or_init(|| { |
273 | let build = self |
274 | .status_emitter |
275 | .register_test(what.description().into()) |
276 | .for_revision("" ); |
277 | let res = match &what { |
278 | Build::Dependencies => match config.build_dependencies() { |
279 | Ok(args) => Ok(args), |
280 | Err(e) => { |
281 | err = Some(Errored { |
282 | command: Command::new(format!(" {what:?}" )), |
283 | errors: vec![], |
284 | stderr: format!(" {e:?}" ).into_bytes(), |
285 | stdout: vec![], |
286 | }); |
287 | Err(()) |
288 | } |
289 | }, |
290 | Build::Aux { aux_file } => match build_aux(aux_file, config, self) { |
291 | Ok(args) => Ok(args.iter().map(Into::into).collect()), |
292 | Err(e) => { |
293 | err = Some(e); |
294 | Err(()) |
295 | } |
296 | }, |
297 | }; |
298 | build.done( |
299 | &res.as_ref() |
300 | .map(|_| crate::TestOk::Ok) |
301 | .map_err(|()| Errored { |
302 | command: Command::new(what.description()), |
303 | errors: vec![], |
304 | stderr: vec![], |
305 | stdout: vec![], |
306 | }), |
307 | ); |
308 | res |
309 | }) |
310 | .clone() |
311 | .map_err(|()| { |
312 | err.unwrap_or_else(|| Errored { |
313 | command: Command::new(what.description()), |
314 | errors: vec![], |
315 | stderr: b"previous build failed" .to_vec(), |
316 | stdout: vec![], |
317 | }) |
318 | }) |
319 | } |
320 | } |
321 | |