]>
Commit | Line | Data |
---|---|---|
48663c56 | 1 | use serde::{Deserialize, Serialize}; |
60c5eb7d XL |
2 | use build_helper::t; |
3 | use std::time; | |
4 | use std::fs; | |
5 | use std::io::{Seek, SeekFrom}; | |
6 | use std::collections::HashMap; | |
7 | use crate::builder::{Builder, RunConfig, ShouldRun, Step}; | |
8 | use std::fmt; | |
9 | use std::process::Command; | |
10 | use std::path::PathBuf; | |
11 | use std::env; | |
12 | ||
13 | // Each cycle is 42 days long (6 weeks); the last week is 35..=42 then. | |
14 | const BETA_WEEK_START: u64 = 35; | |
15 | ||
16 | #[cfg(linux)] | |
17 | const OS: Option<&str> = Some("linux"); | |
18 | ||
19 | #[cfg(windows)] | |
20 | const OS: Option<&str> = Some("windows"); | |
21 | ||
22 | #[cfg(all(not(linux), not(windows)))] | |
23 | const OS: Option<&str> = None; | |
24 | ||
25 | type 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 |
30 | pub 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 |
39 | impl 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 |
49 | impl 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. | |
60 | fn 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. | |
69 | static 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. | |
85 | static 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 | ||
91 | fn 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 | ||
104 | fn 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)] | |
139 | pub struct ToolStateCheck; | |
140 | ||
141 | impl 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 | ||
206 | impl 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, ¤t_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. | |
278 | fn 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(¤t_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 | ||
374 | fn 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(¤t_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)] | |
421 | struct RepoState { | |
422 | tool: String, | |
423 | windows: ToolState, | |
424 | linux: ToolState, | |
425 | commit: String, | |
426 | datetime: String, | |
427 | } |