Skip to content

Commit faebcc1

Browse files
committed
rustbuild: Fail the build if we build Cargo twice
This commit updates the `ToolBuild` step to stream Cargo's JSON messages, parse them, and record all libraries built. If we build anything twice (aka Cargo) it'll most likely happen due to dependencies being recompiled which is caught by this check.
1 parent ab8b961 commit faebcc1

File tree

8 files changed

+349
-240
lines changed

8 files changed

+349
-240
lines changed

Diff for: src/Cargo.lock

+190-189
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/bootstrap/compile.rs

+73-48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//! compiler. This module is also responsible for assembling the sysroot as it
1717
//! goes along from the output of the previous stage.
1818
19+
use std::borrow::Cow;
1920
use std::env;
2021
use std::fs::{self, File};
2122
use std::io::BufReader;
@@ -996,24 +997,6 @@ fn stderr_isatty() -> bool {
996997
pub fn run_cargo(build: &Build, cargo: &mut Command, stamp: &Path, is_check: bool)
997998
-> Vec<PathBuf>
998999
{
999-
// Instruct Cargo to give us json messages on stdout, critically leaving
1000-
// stderr as piped so we can get those pretty colors.
1001-
cargo.arg("--message-format").arg("json")
1002-
.stdout(Stdio::piped());
1003-
1004-
if stderr_isatty() && build.ci_env == CiEnv::None {
1005-
// since we pass message-format=json to cargo, we need to tell the rustc
1006-
// wrapper to give us colored output if necessary. This is because we
1007-
// only want Cargo's JSON output, not rustcs.
1008-
cargo.env("RUSTC_COLOR", "1");
1009-
}
1010-
1011-
build.verbose(&format!("running: {:?}", cargo));
1012-
let mut child = match cargo.spawn() {
1013-
Ok(child) => child,
1014-
Err(e) => panic!("failed to execute command: {:?}\nerror: {}", cargo, e),
1015-
};
1016-
10171000
// `target_root_dir` looks like $dir/$target/release
10181001
let target_root_dir = stamp.parent().unwrap();
10191002
// `target_deps_dir` looks like $dir/$target/release/deps
@@ -1028,46 +1011,33 @@ pub fn run_cargo(build: &Build, cargo: &mut Command, stamp: &Path, is_check: boo
10281011
// files we need to probe for later.
10291012
let mut deps = Vec::new();
10301013
let mut toplevel = Vec::new();
1031-
let stdout = BufReader::new(child.stdout.take().unwrap());
1032-
for line in stdout.lines() {
1033-
let line = t!(line);
1034-
let json: serde_json::Value = if line.starts_with("{") {
1035-
t!(serde_json::from_str(&line))
1036-
} else {
1037-
// If this was informational, just print it out and continue
1038-
println!("{}", line);
1039-
continue
1014+
let ok = stream_cargo(build, cargo, &mut |msg| {
1015+
let filenames = match msg {
1016+
CargoMessage::CompilerArtifact { filenames, .. } => filenames,
1017+
_ => return,
10401018
};
1041-
if json["reason"].as_str() != Some("compiler-artifact") {
1042-
if build.config.rustc_error_format.as_ref().map_or(false, |e| e == "json") {
1043-
// most likely not a cargo message, so let's send it out as well
1044-
println!("{}", line);
1045-
}
1046-
continue
1047-
}
1048-
for filename in json["filenames"].as_array().unwrap() {
1049-
let filename = filename.as_str().unwrap();
1019+
for filename in filenames {
10501020
// Skip files like executables
10511021
if !filename.ends_with(".rlib") &&
10521022
!filename.ends_with(".lib") &&
10531023
!is_dylib(&filename) &&
10541024
!(is_check && filename.ends_with(".rmeta")) {
1055-
continue
1025+
return;
10561026
}
10571027

1058-
let filename = Path::new(filename);
1028+
let filename = Path::new(&*filename);
10591029

10601030
// If this was an output file in the "host dir" we don't actually
10611031
// worry about it, it's not relevant for us.
10621032
if filename.starts_with(&host_root_dir) {
1063-
continue;
1033+
return;
10641034
}
10651035

10661036
// If this was output in the `deps` dir then this is a precise file
10671037
// name (hash included) so we start tracking it.
10681038
if filename.starts_with(&target_deps_dir) {
10691039
deps.push(filename.to_path_buf());
1070-
continue;
1040+
return;
10711041
}
10721042

10731043
// Otherwise this was a "top level artifact" which right now doesn't
@@ -1088,15 +1058,10 @@ pub fn run_cargo(build: &Build, cargo: &mut Command, stamp: &Path, is_check: boo
10881058

10891059
toplevel.push((file_stem, extension, expected_len));
10901060
}
1091-
}
1061+
});
10921062

1093-
// Make sure Cargo actually succeeded after we read all of its stdout.
1094-
let status = t!(child.wait());
1095-
if !status.success() {
1096-
panic!("command did not execute successfully: {:?}\n\
1097-
expected success, got: {}",
1098-
cargo,
1099-
status);
1063+
if !ok {
1064+
panic!("cargo must succeed");
11001065
}
11011066

11021067
// Ok now we need to actually find all the files listed in `toplevel`. We've
@@ -1167,3 +1132,63 @@ pub fn run_cargo(build: &Build, cargo: &mut Command, stamp: &Path, is_check: boo
11671132
t!(t!(File::create(stamp)).write_all(&new_contents));
11681133
deps
11691134
}
1135+
1136+
pub fn stream_cargo(
1137+
build: &Build,
1138+
cargo: &mut Command,
1139+
cb: &mut FnMut(CargoMessage),
1140+
) -> bool {
1141+
// Instruct Cargo to give us json messages on stdout, critically leaving
1142+
// stderr as piped so we can get those pretty colors.
1143+
cargo.arg("--message-format").arg("json")
1144+
.stdout(Stdio::piped());
1145+
1146+
if stderr_isatty() && build.ci_env == CiEnv::None {
1147+
// since we pass message-format=json to cargo, we need to tell the rustc
1148+
// wrapper to give us colored output if necessary. This is because we
1149+
// only want Cargo's JSON output, not rustcs.
1150+
cargo.env("RUSTC_COLOR", "1");
1151+
}
1152+
1153+
build.verbose(&format!("running: {:?}", cargo));
1154+
let mut child = match cargo.spawn() {
1155+
Ok(child) => child,
1156+
Err(e) => panic!("failed to execute command: {:?}\nerror: {}", cargo, e),
1157+
};
1158+
1159+
// Spawn Cargo slurping up its JSON output. We'll start building up the
1160+
// `deps` array of all files it generated along with a `toplevel` array of
1161+
// files we need to probe for later.
1162+
let stdout = BufReader::new(child.stdout.take().unwrap());
1163+
for line in stdout.lines() {
1164+
let line = t!(line);
1165+
match serde_json::from_str::<CargoMessage>(&line) {
1166+
Ok(msg) => cb(msg),
1167+
// If this was informational, just print it out and continue
1168+
Err(_) => println!("{}", line)
1169+
}
1170+
}
1171+
1172+
// Make sure Cargo actually succeeded after we read all of its stdout.
1173+
let status = t!(child.wait());
1174+
if !status.success() {
1175+
println!("command did not execute successfully: {:?}\n\
1176+
expected success, got: {}",
1177+
cargo,
1178+
status);
1179+
}
1180+
status.success()
1181+
}
1182+
1183+
#[derive(Deserialize)]
1184+
#[serde(tag = "reason", rename_all = "kebab-case")]
1185+
pub enum CargoMessage<'a> {
1186+
CompilerArtifact {
1187+
package_id: Cow<'a, str>,
1188+
features: Vec<Cow<'a, str>>,
1189+
filenames: Vec<Cow<'a, str>>,
1190+
},
1191+
BuildScriptExecuted {
1192+
package_id: Cow<'a, str>,
1193+
}
1194+
}

Diff for: src/bootstrap/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ pub struct Build {
254254
ci_env: CiEnv,
255255
delayed_failures: RefCell<Vec<String>>,
256256
prerelease_version: Cell<Option<u32>>,
257+
tool_artifacts: RefCell<HashMap<
258+
Interned<String>,
259+
HashMap<String, (&'static str, PathBuf, Vec<String>)>
260+
>>,
257261
}
258262

259263
#[derive(Debug)]
@@ -353,6 +357,7 @@ impl Build {
353357
ci_env: CiEnv::current(),
354358
delayed_failures: RefCell::new(Vec::new()),
355359
prerelease_version: Cell::new(None),
360+
tool_artifacts: Default::default(),
356361
}
357362
}
358363

Diff for: src/bootstrap/tool.rs

+74-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,80 @@ impl Step for ToolBuild {
117117

118118
let _folder = build.fold_output(|| format!("stage{}-{}", compiler.stage, tool));
119119
println!("Building stage{} tool {} ({})", compiler.stage, tool, target);
120-
let is_expected = build.try_run(&mut cargo);
120+
let mut duplicates = Vec::new();
121+
let is_expected = compile::stream_cargo(build, &mut cargo, &mut |msg| {
122+
// Only care about big things like the RLS/Cargo for now
123+
if tool != "rls" && tool != "cargo" {
124+
return
125+
}
126+
let (id, features, filenames) = match msg {
127+
compile::CargoMessage::CompilerArtifact {
128+
package_id,
129+
features,
130+
filenames
131+
} => {
132+
(package_id, features, filenames)
133+
}
134+
_ => return,
135+
};
136+
let features = features.iter().map(|s| s.to_string()).collect::<Vec<_>>();
137+
138+
for path in filenames {
139+
let val = (tool, PathBuf::from(&*path), features.clone());
140+
// we're only interested in deduplicating rlibs for now
141+
if val.1.extension().and_then(|s| s.to_str()) != Some("rlib") {
142+
continue
143+
}
144+
145+
// Don't worry about libs that turn out to be host dependencies
146+
// or build scripts, we only care about target dependencies that
147+
// are in `deps`.
148+
if let Some(maybe_target) = val.1
149+
.parent() // chop off file name
150+
.and_then(|p| p.parent()) // chop off `deps`
151+
.and_then(|p| p.parent()) // chop off `release`
152+
.and_then(|p| p.file_name())
153+
.and_then(|p| p.to_str())
154+
{
155+
if maybe_target != &*target {
156+
continue
157+
}
158+
}
159+
160+
let mut artifacts = build.tool_artifacts.borrow_mut();
161+
let prev_artifacts = artifacts
162+
.entry(target)
163+
.or_insert_with(Default::default);
164+
if let Some(prev) = prev_artifacts.get(&*id) {
165+
if prev.1 != val.1 {
166+
duplicates.push((
167+
id.to_string(),
168+
val,
169+
prev.clone(),
170+
));
171+
}
172+
return
173+
}
174+
prev_artifacts.insert(id.to_string(), val);
175+
}
176+
});
177+
178+
if is_expected && duplicates.len() != 0 {
179+
println!("duplicate artfacts found when compiling a tool, this \
180+
typically means that something was recompiled because \
181+
a transitive dependency has different features activated \
182+
than in a previous build:\n");
183+
for (id, cur, prev) in duplicates {
184+
println!(" {}", id);
185+
println!(" `{}` enabled features {:?} at {:?}",
186+
cur.0, cur.2, cur.1);
187+
println!(" `{}` enabled features {:?} at {:?}",
188+
prev.0, prev.2, prev.1);
189+
}
190+
println!("");
191+
panic!("tools should not compile multiple copies of the same crate");
192+
}
193+
121194
build.save_toolstate(tool, if is_expected {
122195
ToolState::TestFail
123196
} else {

Diff for: src/librustc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ byteorder = { version = "1.1", features = ["i128"]}
5656
# later crate stop compiling. If you can remove this and everything
5757
# compiles, then please feel free to do so!
5858
flate2 = "1.0"
59+
tempdir = "0.3"

Diff for: src/tools/rls

Submodule rls updated from 974c515 to f5a0c91

Diff for: src/tools/unstable-book-gen/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ license = "MIT/Apache-2.0"
77

88
[dependencies]
99
tidy = { path = "../tidy" }
10+
11+
# not actually needed but required for now to unify the feature selection of
12+
# `num-traits` between this and `rustbook`
13+
num-traits = "0.2"

0 commit comments

Comments
 (0)