trybuild/
cargo.rs

1use crate::directory::Directory;
2use crate::error::{Error, Result};
3use crate::manifest::Name;
4use crate::run::Project;
5use crate::rustflags;
6use serde_derive::Deserialize;
7use std::fs::File;
8use std::path::PathBuf;
9use std::process::{Command, Output, Stdio};
10use std::{env, io, iter};
11use target_triple::TARGET;
12
13#[derive(Deserialize)]
14pub(crate) struct Metadata {
15    pub target_directory: Directory,
16    pub workspace_root: Directory,
17    pub packages: Vec<PackageMetadata>,
18}
19
20#[derive(Deserialize)]
21pub(crate) struct PackageMetadata {
22    pub name: String,
23    pub targets: Vec<BuildTarget>,
24    pub manifest_path: PathBuf,
25}
26
27#[derive(Deserialize)]
28pub(crate) struct BuildTarget {
29    pub crate_types: Vec<String>,
30}
31
32fn raw_cargo() -> Command {
33    match env::var_os("CARGO") {
34        Some(cargo) => Command::new(cargo),
35        None => Command::new("cargo"),
36    }
37}
38
39fn cargo(project: &Project) -> Command {
40    cargo_with_rustflags(project, &[])
41}
42
43fn cargo_with_rustflags(project: &Project, extra_rustflags: &[&'static str]) -> Command {
44    let mut cmd = raw_cargo();
45    cmd.current_dir(&project.dir);
46    cmd.envs(cargo_target_dir(project));
47    cmd.env_remove("RUSTFLAGS");
48    cmd.env("CARGO_INCREMENTAL", "0");
49    cmd.arg("--offline");
50
51    let rustflags = rustflags::toml(extra_rustflags);
52    cmd.arg(format!("--config=build.rustflags={rustflags}"));
53    cmd.arg(format!("--config=target.{TARGET}.rustflags={rustflags}"));
54
55    cmd
56}
57
58fn cargo_target_dir(project: &Project) -> impl Iterator<Item = (&'static str, PathBuf)> {
59    iter::once((
60        "CARGO_TARGET_DIR",
61        path!(project.target_dir / "tests" / "trybuild"),
62    ))
63}
64
65pub(crate) fn manifest_dir() -> Result<Directory> {
66    if let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR") {
67        return Ok(Directory::from(manifest_dir));
68    }
69    let mut dir = Directory::current()?;
70    loop {
71        if dir.join("Cargo.toml").exists() {
72            return Ok(dir);
73        }
74        dir = dir.parent().ok_or(Error::ProjectDir)?;
75    }
76}
77
78pub(crate) fn build_dependencies(project: &mut Project) -> Result<()> {
79    // Try copying or generating lockfile.
80    match File::open(path!(project.workspace / "Cargo.lock")) {
81        Ok(mut workspace_cargo_lock) => {
82            if let Ok(mut new_cargo_lock) = File::create(path!(project.dir / "Cargo.lock")) {
83                // Not fs::copy in order to avoid producing a read-only destination
84                // file if the source file happens to be read-only.
85                let _ = io::copy(&mut workspace_cargo_lock, &mut new_cargo_lock);
86            }
87        }
88        Err(err) => {
89            if err.kind() == io::ErrorKind::NotFound {
90                let _ = cargo(project).arg("generate-lockfile").status();
91            }
92        }
93    }
94
95    let mut command = cargo(project);
96    command
97        .arg(if project.has_pass { "build" } else { "check" })
98        .args(target())
99        .arg("--bin")
100        .arg(&project.name)
101        .args(features(project));
102
103    let status = command.status().map_err(Error::Cargo)?;
104    if !status.success() {
105        return Err(Error::CargoFail);
106    }
107
108    // Check if this Cargo contains https://github.com/rust-lang/cargo/pull/10383
109    project.keep_going = command
110        .arg("--keep-going")
111        .stdout(Stdio::null())
112        .stderr(Stdio::null())
113        .status()
114        .is_ok_and(|status| status.success());
115
116    Ok(())
117}
118
119pub(crate) fn build_test(project: &Project, name: &Name) -> Result<Output> {
120    let _ = cargo(project)
121        .arg("clean")
122        .arg("--package")
123        .arg(&project.name)
124        .arg("--color=never")
125        .stdout(Stdio::null())
126        .stderr(Stdio::null())
127        .status();
128
129    cargo_with_rustflags(project, &["--diagnostic-width=140"])
130        .arg(if project.has_pass { "build" } else { "check" })
131        .args(target())
132        .arg("--bin")
133        .arg(name)
134        .args(features(project))
135        .arg("--quiet")
136        .arg("--color=never")
137        .arg("--message-format=json")
138        .output()
139        .map_err(Error::Cargo)
140}
141
142pub(crate) fn build_all_tests(project: &Project) -> Result<Output> {
143    let _ = cargo(project)
144        .arg("clean")
145        .arg("--package")
146        .arg(&project.name)
147        .arg("--color=never")
148        .stdout(Stdio::null())
149        .stderr(Stdio::null())
150        .status();
151
152    cargo_with_rustflags(project, &["--diagnostic-width=140"])
153        .arg(if project.has_pass { "build" } else { "check" })
154        .args(target())
155        .arg("--bins")
156        .args(features(project))
157        .arg("--quiet")
158        .arg("--color=never")
159        .arg("--message-format=json")
160        .arg("--keep-going")
161        .output()
162        .map_err(Error::Cargo)
163}
164
165pub(crate) fn run_test(project: &Project, name: &Name) -> Result<Output> {
166    cargo(project)
167        .arg("run")
168        .args(target())
169        .arg("--bin")
170        .arg(name)
171        .args(features(project))
172        .arg("--quiet")
173        .arg("--color=never")
174        .output()
175        .map_err(Error::Cargo)
176}
177
178pub(crate) fn metadata() -> Result<Metadata> {
179    let output = raw_cargo()
180        .arg("metadata")
181        .arg("--no-deps")
182        .arg("--format-version=1")
183        .output()
184        .map_err(Error::Cargo)?;
185
186    serde_json::from_slice(&output.stdout).map_err(|err| {
187        print!("{}", String::from_utf8_lossy(&output.stderr));
188        Error::Metadata(err)
189    })
190}
191
192fn features(project: &Project) -> Vec<String> {
193    match &project.features {
194        Some(features) => vec![
195            "--no-default-features".to_owned(),
196            "--features".to_owned(),
197            features.join(","),
198        ],
199        None => vec![],
200    }
201}
202
203fn target() -> Vec<&'static str> {
204    // When --target flag is passed, cargo does not pass RUSTFLAGS to rustc when
205    // building proc-macro and build script even if the host and target triples
206    // are the same. Therefore, if we always pass --target to cargo, tools such
207    // as coverage that require RUSTFLAGS do not work for tests run by trybuild.
208    //
209    // To avoid that problem, do not pass --target to cargo if we know that it
210    // has not been passed.
211    //
212    // Currently, cargo does not have a way to tell the build script whether
213    // --target has been passed or not, and there is no heuristic that can
214    // handle this well.
215    //
216    // Therefore, expose a cfg to always treat the target as host.
217    if cfg!(trybuild_no_target) {
218        vec![]
219    } else {
220        vec!["--target", TARGET]
221    }
222}