1use cargo_metadata::{camino::Utf8PathBuf, DependencyKind};
2use cargo_platform::Cfg;
3use color_eyre::eyre::{bail, eyre, Result};
4use 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
13use crate::{
14 build_aux, status_emitter::StatusEmitter, Config, Errored, Mode, OutputConflictHandling,
15};
16
17#[derive(Default, Debug)]
18pub 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
26fn 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.
48pub(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)]
208pub enum Build {
209 /// Build the dependencies.
210 Dependencies,
211 /// Build an aux-build.
212 Aux { aux_file: PathBuf },
213}
214impl 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
223pub 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
229impl<'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