1//! Use `cargo` to build dependencies and make them available in your tests
2
3use crate::{
4 build_manager::{Build, BuildManager},
5 custom_flags::Flag,
6 per_test_config::TestConfig,
7 test_result::Errored,
8 CommandBuilder, Config,
9};
10use bstr::ByteSlice;
11use cargo_metadata::{camino::Utf8PathBuf, BuildScript, DependencyKind};
12use cargo_platform::Cfg;
13use std::{
14 collections::{HashMap, HashSet},
15 ffi::OsString,
16 path::PathBuf,
17 process::Command,
18 str::FromStr,
19};
20
21#[derive(Default, Debug)]
22/// Describes where to find the binaries built for the dependencies
23pub struct Dependencies {
24 /// All paths that must be imported with `-L dependency=`. This is for
25 /// finding proc macros run on the host and dependencies for the target.
26 pub import_paths: Vec<PathBuf>,
27 /// Unnamed dependencies that build scripts asked us to link
28 pub import_libs: Vec<PathBuf>,
29 /// The name as chosen in the `Cargo.toml` and its corresponding rmeta file.
30 pub dependencies: Vec<(String, Vec<Utf8PathBuf>)>,
31}
32
33fn cfgs(config: &Config) -> Result<Vec<Cfg>, Errored> {
34 let Some(cfg) = &config.program.cfg_flag else {
35 return Ok(vec![]);
36 };
37 let mut cmd = config.program.build(&config.out_dir);
38 cmd.arg(cfg);
39 cmd.arg("--target").arg(config.target.as_ref().unwrap());
40 let output = config.run_command(&mut cmd)?;
41
42 if !output.status.success() {
43 return Err(Errored {
44 command: format!("{cmd:?}"),
45 stderr: output.stderr,
46 stdout: output.stdout,
47 errors: vec![],
48 });
49 }
50 let mut cfgs = vec![];
51
52 let stdout = String::from_utf8(output.stdout).map_err(|e| Errored {
53 command: "processing cfg information from rustc as utf8".into(),
54 errors: vec![],
55 stderr: e.to_string().into_bytes(),
56 stdout: vec![],
57 })?;
58 for line in stdout.lines() {
59 cfgs.push(Cfg::from_str(line).map_err(|e| Errored {
60 command: "parsing cfgs from rustc output".into(),
61 errors: vec![],
62 stderr: e.to_string().into_bytes(),
63 stdout: vec![],
64 })?);
65 }
66
67 Ok(cfgs)
68}
69
70/// Compiles dependencies and returns the crate names and corresponding rmeta files.
71fn build_dependencies_inner(
72 config: &Config,
73 info: &DependencyBuilder,
74) -> Result<Dependencies, Errored> {
75 let mut build = info.program.build(&config.out_dir);
76 build.arg(&info.crate_manifest_path);
77
78 if let Some(target) = &config.target {
79 build.arg(format!("--target={target}"));
80 }
81
82 if let Some(packages) = &info.build_std {
83 if packages.is_empty() {
84 build.arg("-Zbuild-std");
85 } else {
86 build.arg(format!("-Zbuild-std={packages}"));
87 }
88 }
89
90 // Reusable closure for setting up the environment both for artifact generation and `cargo_metadata`
91 let set_locking = |cmd: &mut Command| {
92 if !info.bless_lockfile {
93 cmd.arg("--locked");
94 }
95 };
96
97 build.arg("--message-format=json");
98
99 let output = config.run_command(&mut build)?;
100
101 if !output.status.success() {
102 let stdout = output
103 .stdout
104 .lines()
105 .flat_map(
106 |line| match serde_json::from_slice::<cargo_metadata::Message>(line) {
107 Ok(cargo_metadata::Message::CompilerArtifact(artifact)) => {
108 format!("{artifact:?}\n").into_bytes()
109 }
110 Ok(cargo_metadata::Message::BuildFinished(bf)) => {
111 format!("{bf:?}\n").into_bytes()
112 }
113 Ok(cargo_metadata::Message::BuildScriptExecuted(be)) => {
114 format!("{be:?}\n").into_bytes()
115 }
116 Ok(cargo_metadata::Message::TextLine(s)) => s.into_bytes(),
117 Ok(cargo_metadata::Message::CompilerMessage(msg)) => msg
118 .target
119 .src_path
120 .as_str()
121 .as_bytes()
122 .iter()
123 .copied()
124 .chain([b'\n'])
125 .chain(msg.message.rendered.unwrap_or_default().into_bytes())
126 .collect(),
127 Ok(_) => vec![],
128 Err(_) => line.iter().copied().chain([b'\n']).collect(),
129 },
130 )
131 .collect();
132 return Err(Errored {
133 command: format!("{build:?}"),
134 stderr: output.stderr,
135 stdout,
136 errors: vec![],
137 });
138 }
139
140 // Collect all artifacts generated
141 let artifact_output = output.stdout;
142 let mut import_paths: HashSet<PathBuf> = HashSet::new();
143 let mut import_libs: HashSet<PathBuf> = HashSet::new();
144 let mut artifacts = HashMap::new();
145 for line in artifact_output.lines() {
146 let Ok(message) = serde_json::from_slice::<cargo_metadata::Message>(line) else {
147 continue;
148 };
149 match message {
150 cargo_metadata::Message::CompilerArtifact(artifact) => {
151 if artifact
152 .target
153 .crate_types
154 .iter()
155 .all(|ctype| !matches!(ctype.as_str(), "proc-macro" | "lib" | "rlib"))
156 {
157 continue;
158 }
159 for filename in &artifact.filenames {
160 import_paths.insert(filename.parent().unwrap().into());
161 }
162 let package_id = artifact.package_id;
163 if let Some(prev) = artifacts.insert(
164 package_id.clone(),
165 Ok((artifact.target.name, artifact.filenames)),
166 ) {
167 artifacts.insert(
168 package_id.clone(),
169 Err(format!(
170 "{prev:#?} vs {:#?} ({:?})",
171 artifacts[&package_id], artifact.target.crate_types
172 )),
173 );
174 }
175 }
176 cargo_metadata::Message::BuildScriptExecuted(BuildScript {
177 linked_libs,
178 linked_paths,
179 ..
180 }) => {
181 import_paths.extend(linked_paths.into_iter().map(Into::into));
182 import_libs.extend(linked_libs.into_iter().map(Into::into));
183 }
184 _ => {}
185 }
186 }
187
188 // Check which crates are mentioned in the crate itself
189 let mut metadata = cargo_metadata::MetadataCommand::new().cargo_command();
190 metadata
191 .arg("--manifest-path")
192 .arg(&info.crate_manifest_path);
193 info.program.apply_env(&mut metadata);
194 set_locking(&mut metadata);
195 let output = config.run_command(&mut metadata)?;
196
197 if !output.status.success() {
198 return Err(Errored {
199 command: format!("{metadata:?}"),
200 stderr: output.stderr,
201 stdout: output.stdout,
202 errors: vec![],
203 });
204 }
205
206 let output = output.stdout;
207
208 let cfg = cfgs(config)?;
209
210 for line in output.lines() {
211 if !line.starts_with(b"{") {
212 continue;
213 }
214 let metadata: cargo_metadata::Metadata =
215 serde_json::from_slice(line).map_err(|err| Errored {
216 command: "decoding cargo metadata json".into(),
217 errors: vec![],
218 stderr: err.to_string().into_bytes(),
219 stdout: vec![],
220 })?;
221 // Only take artifacts that are defined in the Cargo.toml
222
223 // First, find the root artifact
224 let root = metadata
225 .packages
226 .iter()
227 .find(|package| {
228 package.manifest_path.as_std_path().canonicalize().unwrap()
229 == info.crate_manifest_path.canonicalize().unwrap()
230 })
231 .unwrap();
232
233 // Then go over all of its dependencies
234 let mut dependencies = root
235 .dependencies
236 .iter()
237 .filter(|dep| matches!(dep.kind, DependencyKind::Normal))
238 // Only consider dependencies that are enabled on the current target
239 .filter(|dep| match &dep.target {
240 Some(platform) => platform.matches(config.target.as_ref().unwrap(), &cfg),
241 None => true,
242 })
243 .map(|dep| {
244 for p in &metadata.packages {
245 if p.name != dep.name {
246 continue;
247 }
248 if dep
249 .path
250 .as_ref()
251 .is_some_and(|path| p.manifest_path.parent().unwrap() == path)
252 || dep.req.matches(&p.version)
253 {
254 return (p, dep.rename.clone().unwrap_or_else(|| p.name.clone()));
255 }
256 }
257 panic!("dep not found: {dep:#?}")
258 })
259 // Also expose the root crate
260 .chain(std::iter::once((root, root.name.clone())))
261 .filter_map(|(package, name)| {
262 // Get the id for the package matching the version requirement of the dep
263 let id = &package.id;
264 // Return the name chosen in `Cargo.toml` and the path to the corresponding artifact
265 match artifacts.remove(id) {
266 Some(Ok((_, artifacts))) => Some(Ok((name.replace('-', "_"), artifacts))),
267 Some(Err(what)) => Some(Err(Errored {
268 command: what,
269 errors: vec![],
270 stderr: id.to_string().into_bytes(),
271 stdout: "`ui_test` does not support crates that appear as both build-dependencies and core dependencies".as_bytes().into(),
272 })),
273 None => {
274 if name == root.name {
275 // If there are no artifacts, this is the root crate and it is being built as a binary/test
276 // instead of a library. We simply add no artifacts, meaning you can't depend on functions
277 // and types declared in the root crate.
278 None
279 } else {
280 panic!("no artifact found for `{name}`(`{id}`):`\n{}", artifact_output.to_str().unwrap())
281 }
282 }
283 }
284 })
285 .collect::<Result<Vec<_>, Errored>>()?;
286 let import_paths = import_paths.into_iter().collect();
287 let import_libs = import_libs.into_iter().collect();
288
289 if info.build_std.is_some() {
290 let mut build_std_crates = HashSet::new();
291 build_std_crates.insert("core");
292 build_std_crates.insert("alloc");
293 build_std_crates.insert("proc_macro");
294 build_std_crates.insert("panic_unwind");
295 build_std_crates.insert("compiler_builtins");
296 build_std_crates.insert("std");
297 build_std_crates.insert("test");
298 build_std_crates.insert("panic_abort");
299
300 for (name, artifacts) in artifacts
301 .into_iter()
302 .filter_map(|(_, artifacts)| artifacts.ok())
303 {
304 if build_std_crates.remove(name.as_str()) {
305 dependencies.push((format!("noprelude:{name}"), artifacts));
306 }
307 }
308 }
309
310 return Ok(Dependencies {
311 dependencies,
312 import_paths,
313 import_libs,
314 });
315 }
316
317 Err(Errored {
318 command: "looking for json in cargo-metadata output".into(),
319 errors: vec![],
320 stderr: vec![],
321 stdout: vec![],
322 })
323}
324
325/// Build the dependencies.
326#[derive(Debug, Clone)]
327pub struct DependencyBuilder {
328 /// Path to a `Cargo.toml` that describes which dependencies the tests can access.
329 pub crate_manifest_path: PathBuf,
330 /// The command to run can be changed from `cargo` to any custom command to build the
331 /// dependencies in `crate_manifest_path`.
332 pub program: CommandBuilder,
333 /// Build with [`build-std`](https://doc.rust-lang.org/1.78.0/cargo/reference/unstable.html#build-std),
334 /// which requires the nightly toolchain. The [`String`] can contain the standard library crates to build.
335 pub build_std: Option<String>,
336 /// Whether the lockfile can be overwritten
337 pub bless_lockfile: bool,
338}
339
340impl Default for DependencyBuilder {
341 fn default() -> Self {
342 Self {
343 crate_manifest_path: PathBuf::from("Cargo.toml"),
344 program: CommandBuilder::cargo(),
345 build_std: None,
346 bless_lockfile: false,
347 }
348 }
349}
350
351impl Flag for DependencyBuilder {
352 fn must_be_unique(&self) -> bool {
353 true
354 }
355 fn clone_inner(&self) -> Box<dyn Flag> {
356 Box::new(self.clone())
357 }
358 fn apply(
359 &self,
360 cmd: &mut Command,
361 config: &TestConfig,
362 build_manager: &BuildManager,
363 ) -> Result<(), Errored> {
364 let extra_args: Vec = build_manager.build(self.clone(), &config.status)?;
365 cmd.args(extra_args);
366 Ok(())
367 }
368}
369
370impl Build for DependencyBuilder {
371 fn build(&self, build_manager: &BuildManager) -> Result<Vec<OsString>, Errored> {
372 build_dependencies(build_manager.config(), self)
373 }
374
375 fn description(&self) -> String {
376 "Building dependencies".into()
377 }
378}
379
380/// Compile dependencies and return the right flags
381/// to find the dependencies.
382pub fn build_dependencies(
383 config: &Config,
384 info: &DependencyBuilder,
385) -> Result<Vec<OsString>, Errored> {
386 let dependencies = build_dependencies_inner(config, info)?;
387 let mut args = vec![];
388
389 if info.build_std.is_some() {
390 args.push("-Zunstable-options".into());
391 }
392
393 for (name, artifacts) in dependencies.dependencies {
394 for dependency in artifacts {
395 args.push("--extern".into());
396 let mut dep = OsString::from(&name);
397 dep.push("=");
398 dep.push(dependency);
399 args.push(dep);
400 }
401 }
402 for import_path in dependencies.import_paths {
403 args.push("-L".into());
404 args.push(import_path.into());
405 }
406 for import_path in dependencies.import_libs {
407 args.push("-l".into());
408 args.push(import_path.into());
409 }
410 Ok(args)
411}
412

Provided by KDAB

Privacy Policy
Learn Rust with the experts
Find out more