]> git.proxmox.com Git - rustc.git/blame - src/bootstrap/toolstate.rs
New upstream version 1.41.1+dfsg1
[rustc.git] / src / bootstrap / toolstate.rs
CommitLineData
48663c56 1use serde::{Deserialize, Serialize};
60c5eb7d
XL
2use build_helper::t;
3use std::time;
4use std::fs;
5use std::io::{Seek, SeekFrom};
6use std::collections::HashMap;
7use crate::builder::{Builder, RunConfig, ShouldRun, Step};
8use std::fmt;
9use std::process::Command;
10use std::path::PathBuf;
11use std::env;
12
13// Each cycle is 42 days long (6 weeks); the last week is 35..=42 then.
14const BETA_WEEK_START: u64 = 35;
15
16#[cfg(linux)]
17const OS: Option<&str> = Some("linux");
18
19#[cfg(windows)]
20const OS: Option<&str> = Some("windows");
21
22#[cfg(all(not(linux), not(windows)))]
23const OS: Option<&str> = None;
24
25type ToolstateData = HashMap<Box<str>, ToolState>;
48663c56 26
ff7c6d11
XL
27#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28#[serde(rename_all = "kebab-case")]
ea8adc8c
XL
29/// Whether a tool can be compiled, tested or neither
30pub enum ToolState {
31 /// The tool compiles successfully, but the test suite fails
ff7c6d11 32 TestFail = 1,
ea8adc8c 33 /// The tool compiles successfully and its test suite passes
ff7c6d11 34 TestPass = 2,
ea8adc8c 35 /// The tool can't even be compiled
ff7c6d11 36 BuildFail = 0,
ea8adc8c
XL
37}
38
60c5eb7d
XL
39impl fmt::Display for ToolState {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(f, "{}", match self {
42 ToolState::TestFail => "test-fail",
43 ToolState::TestPass => "test-pass",
44 ToolState::BuildFail => "build-fail",
45 })
46 }
47}
48
ea8adc8c
XL
49impl Default for ToolState {
50 fn default() -> Self {
51 // err on the safe side
ff7c6d11 52 ToolState::BuildFail
ea8adc8c
XL
53 }
54}
60c5eb7d
XL
55
56/// Number of days after the last promotion of beta.
57/// Its value is 41 on the Tuesday where "Promote master to beta (T-2)" happens.
58/// The Wednesday after this has value 0.
59/// We track this value to prevent regressing tools in the last week of the 6-week cycle.
60fn days_since_beta_promotion() -> u64 {
61 let since_epoch = t!(time::SystemTime::UNIX_EPOCH.elapsed());
62 (since_epoch.as_secs() / 86400 - 20) % 42
63}
64
65// These tools must test-pass on the beta/stable channels.
66//
67// On the nightly channel, their build step must be attempted, but they may not
68// be able to build successfully.
69static STABLE_TOOLS: &[(&str, &str)] = &[
70 ("book", "src/doc/book"),
71 ("nomicon", "src/doc/nomicon"),
72 ("reference", "src/doc/reference"),
73 ("rust-by-example", "src/doc/rust-by-example"),
74 ("edition-guide", "src/doc/edition-guide"),
75 ("rls", "src/tools/rls"),
76 ("rustfmt", "src/tools/rustfmt"),
77 ("clippy-driver", "src/tools/clippy"),
78];
79
80// These tools are permitted to not build on the beta/stable channels.
81//
82// We do require that we checked whether they build or not on the tools builder,
83// though, as otherwise we will be unable to file an issue if they start
84// failing.
85static NIGHTLY_TOOLS: &[(&str, &str)] = &[
86 ("miri", "src/tools/miri"),
87 ("embedded-book", "src/doc/embedded-book"),
88 ("rustc-guide", "src/doc/rustc-guide"),
89];
90
91fn print_error(tool: &str, submodule: &str) {
92 eprintln!("");
93 eprintln!("We detected that this PR updated '{}', but its tests failed.", tool);
94 eprintln!("");
95 eprintln!("If you do intend to update '{}', please check the error messages above and", tool);
96 eprintln!("commit another update.");
97 eprintln!("");
98 eprintln!("If you do NOT intend to update '{}', please ensure you did not accidentally", tool);
99 eprintln!("change the submodule at '{}'. You may ask your reviewer for the", submodule);
100 eprintln!("proper steps.");
101 std::process::exit(3);
102}
103
104fn check_changed_files(toolstates: &HashMap<Box<str>, ToolState>) {
105 // Changed files
106 let output = std::process::Command::new("git")
107 .arg("diff")
108 .arg("--name-status")
109 .arg("HEAD")
110 .arg("HEAD^")
111 .output();
112 let output = match output {
113 Ok(o) => o,
114 Err(e) => {
115 eprintln!("Failed to get changed files: {:?}", e);
116 std::process::exit(1);
117 }
118 };
119
120 let output = t!(String::from_utf8(output.stdout));
121
122 for (tool, submodule) in STABLE_TOOLS.iter().chain(NIGHTLY_TOOLS.iter()) {
123 let changed = output.lines().any(|l| {
124 l.starts_with("M") && l.ends_with(submodule)
125 });
126 eprintln!("Verifying status of {}...", tool);
127 if !changed {
128 continue;
129 }
130
131 eprintln!("This PR updated '{}', verifying if status is 'test-pass'...", submodule);
132 if toolstates[*tool] != ToolState::TestPass {
133 print_error(tool, submodule);
134 }
135 }
136}
137
138#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
139pub struct ToolStateCheck;
140
141impl Step for ToolStateCheck {
142 type Output = ();
143
144 /// Runs the `linkchecker` tool as compiled in `stage` by the `host` compiler.
145 ///
146 /// This tool in `src/tools` will verify the validity of all our links in the
147 /// documentation to ensure we don't have a bunch of dead ones.
148 fn run(self, builder: &Builder<'_>) {
149 if builder.config.dry_run {
150 return;
151 }
152
153 let days_since_beta_promotion = days_since_beta_promotion();
154 let in_beta_week = days_since_beta_promotion >= BETA_WEEK_START;
155 let is_nightly = !(builder.config.channel == "beta" || builder.config.channel == "stable");
156 let toolstates = builder.toolstates();
157
158 let mut did_error = false;
159
160 for (tool, _) in STABLE_TOOLS.iter().chain(NIGHTLY_TOOLS.iter()) {
161 if !toolstates.contains_key(*tool) {
162 did_error = true;
163 eprintln!("error: Tool `{}` was not recorded in tool state.", tool);
164 }
165 }
166
167 if did_error {
168 std::process::exit(1);
169 }
170
171 check_changed_files(&toolstates);
172
173 for (tool, _) in STABLE_TOOLS.iter() {
174 let state = toolstates[*tool];
175
176 if state != ToolState::TestPass {
177 if !is_nightly {
178 did_error = true;
179 eprintln!("error: Tool `{}` should be test-pass but is {}", tool, state);
180 } else if in_beta_week {
181 did_error = true;
182 eprintln!("error: Tool `{}` should be test-pass but is {} during beta week.",
183 tool, state);
184 }
185 }
186 }
187
188 if did_error {
189 std::process::exit(1);
190 }
191
192 if builder.config.channel == "nightly" && env::var_os("TOOLSTATE_PUBLISH").is_some() {
193 commit_toolstate_change(&toolstates, in_beta_week);
194 }
195 }
196
197 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
198 run.path("check-tools")
199 }
200
201 fn make_run(run: RunConfig<'_>) {
202 run.builder.ensure(ToolStateCheck);
203 }
204}
205
206impl Builder<'_> {
207 fn toolstates(&self) -> HashMap<Box<str>, ToolState> {
208 if let Some(ref path) = self.config.save_toolstates {
209 if let Some(parent) = path.parent() {
210 // Ensure the parent directory always exists
211 t!(std::fs::create_dir_all(parent));
212 }
213 let mut file = t!(fs::OpenOptions::new()
214 .create(true)
215 .write(true)
216 .read(true)
217 .open(path));
218
219 serde_json::from_reader(&mut file).unwrap_or_default()
220 } else {
221 Default::default()
222 }
223 }
224
225 /// Updates the actual toolstate of a tool.
226 ///
227 /// The toolstates are saved to the file specified by the key
228 /// `rust.save-toolstates` in `config.toml`. If unspecified, nothing will be
229 /// done. The file is updated immediately after this function completes.
230 pub fn save_toolstate(&self, tool: &str, state: ToolState) {
231 if let Some(ref path) = self.config.save_toolstates {
232 if let Some(parent) = path.parent() {
233 // Ensure the parent directory always exists
234 t!(std::fs::create_dir_all(parent));
235 }
236 let mut file = t!(fs::OpenOptions::new()
237 .create(true)
238 .read(true)
239 .write(true)
240 .open(path));
241
242 let mut current_toolstates: HashMap<Box<str>, ToolState> =
243 serde_json::from_reader(&mut file).unwrap_or_default();
244 current_toolstates.insert(tool.into(), state);
245 t!(file.seek(SeekFrom::Start(0)));
246 t!(file.set_len(0));
247 t!(serde_json::to_writer(file, &current_toolstates));
248 }
249 }
250}
251
252/// This function `commit_toolstate_change` provides functionality for pushing a change
253/// to the `rust-toolstate` repository.
254///
255/// The function relies on a GitHub bot user, which should have a Personal access
256/// token defined in the environment variable $TOOLSTATE_REPO_ACCESS_TOKEN. If for
257/// some reason you need to change the token, please update the Azure Pipelines
258/// variable group.
259///
260/// 1. Generate a new Personal access token:
261///
262/// * Login to the bot account, and go to Settings -> Developer settings ->
263/// Personal access tokens
264/// * Click "Generate new token"
265/// * Enable the "public_repo" permission, then click "Generate token"
266/// * Copy the generated token (should be a 40-digit hexadecimal number).
267/// Save it somewhere secure, as the token would be gone once you leave
268/// the page.
269///
270/// 2. Update the variable group in Azure Pipelines
271///
272/// * Ping a member of the infrastructure team to do this.
273///
274/// 4. Replace the email address below if the bot account identity is changed
275///
276/// * See <https://help.github.com/articles/about-commit-email-addresses/>
277/// if a private email by GitHub is wanted.
278fn commit_toolstate_change(
279 current_toolstate: &ToolstateData,
280 in_beta_week: bool,
281) {
282 fn git_config(key: &str, value: &str) {
283 let status = Command::new("git").arg("config").arg("--global").arg(key).arg(value).status();
284 let success = match status {
285 Ok(s) => s.success(),
286 Err(_) => false,
287 };
288 if !success {
289 panic!("git config key={} value={} successful (status: {:?})", key, value, status);
290 }
291 }
292
293 // If changing anything here, then please check that src/ci/publish_toolstate.sh is up to date
294 // as well.
295 git_config("user.email", "7378925+rust-toolstate-update@users.noreply.github.com");
296 git_config("user.name", "Rust Toolstate Update");
297 git_config("credential.helper", "store");
298
299 let credential = format!(
300 "https://{}:x-oauth-basic@github.com\n",
301 t!(env::var("TOOLSTATE_REPO_ACCESS_TOKEN")),
302 );
303 let git_credential_path = PathBuf::from(t!(env::var("HOME"))).join(".git-credentials");
304 t!(fs::write(&git_credential_path, credential));
305
306 let status = Command::new("git").arg("clone")
307 .arg("--depth=1")
308 .arg(t!(env::var("TOOLSTATE_REPO")))
309 .status();
310 let success = match status {
311 Ok(s) => s.success(),
312 Err(_) => false,
313 };
314 if !success {
315 panic!("git clone successful (status: {:?})", status);
316 }
317
318 let old_toolstate = t!(fs::read("rust-toolstate/_data/latest.json"));
319 let old_toolstate: Vec<RepoState> = t!(serde_json::from_slice(&old_toolstate));
320
321 let message = format!("({} CI update)", OS.expect("linux/windows only"));
322 let mut success = false;
323 for _ in 1..=5 {
324 // Update the toolstate results (the new commit-to-toolstate mapping) in the toolstate repo.
325 change_toolstate(&current_toolstate, &old_toolstate, in_beta_week);
326
327 // `git commit` failing means nothing to commit.
328 let status = t!(Command::new("git")
329 .current_dir("rust-toolstate")
330 .arg("commit")
331 .arg("-a")
332 .arg("-m")
333 .arg(&message)
334 .status());
335 if !status.success() {
336 success = true;
337 break;
338 }
339
340 let status = t!(Command::new("git")
341 .current_dir("rust-toolstate")
342 .arg("push")
343 .arg("origin")
344 .arg("master")
345 .status());
346 // If we successfully push, exit.
347 if status.success() {
348 success = true;
349 break;
350 }
351 eprintln!("Sleeping for 3 seconds before retrying push");
352 std::thread::sleep(std::time::Duration::from_secs(3));
353 let status = t!(Command::new("git")
354 .current_dir("rust-toolstate")
355 .arg("fetch")
356 .arg("origin")
357 .arg("master")
358 .status());
359 assert!(status.success());
360 let status = t!(Command::new("git")
361 .current_dir("rust-toolstate")
362 .arg("reset")
363 .arg("--hard")
364 .arg("origin/master")
365 .status());
366 assert!(status.success());
367 }
368
369 if !success {
370 panic!("Failed to update toolstate repository with new data");
371 }
372}
373
374fn change_toolstate(
375 current_toolstate: &ToolstateData,
376 old_toolstate: &[RepoState],
377 in_beta_week: bool,
378) {
379 let mut regressed = false;
380 for repo_state in old_toolstate {
381 let tool = &repo_state.tool;
382 let state = if cfg!(linux) {
383 &repo_state.linux
384 } else if cfg!(windows) {
385 &repo_state.windows
386 } else {
387 unimplemented!()
388 };
389 let new_state = current_toolstate[tool.as_str()];
390
391 if new_state != *state {
392 eprintln!("The state of `{}` has changed from `{}` to `{}`", tool, state, new_state);
393 if (new_state as u8) < (*state as u8) {
394 if !["rustc-guide", "miri", "embedded-book"].contains(&tool.as_str()) {
395 regressed = true;
396 }
397 }
398 }
399 }
400
401 if regressed && in_beta_week {
402 std::process::exit(1);
403 }
404
405 let commit = t!(std::process::Command::new("git")
406 .arg("rev-parse")
407 .arg("HEAD")
408 .output());
409 let commit = t!(String::from_utf8(commit.stdout));
410
411 let toolstate_serialized = t!(serde_json::to_string(&current_toolstate));
412
413 let history_path = format!("rust-toolstate/history/{}.tsv", OS.expect("linux/windows only"));
414 let mut file = t!(fs::read_to_string(&history_path));
415 let end_of_first_line = file.find('\n').unwrap();
416 file.insert_str(end_of_first_line, &format!("{}\t{}\n", commit, toolstate_serialized));
417 t!(fs::write(&history_path, file));
418}
419
420#[derive(Debug, Serialize, Deserialize)]
421struct RepoState {
422 tool: String,
423 windows: ToolState,
424 linux: ToolState,
425 commit: String,
426 datetime: String,
427}