1 use serde
::{Deserialize, Serialize}
;
5 use std
::io
::{Seek, SeekFrom}
;
6 use std
::collections
::HashMap
;
7 use crate::builder
::{Builder, RunConfig, ShouldRun, Step}
;
9 use std
::process
::Command
;
10 use std
::path
::PathBuf
;
13 // Each cycle is 42 days long (6 weeks); the last week is 35..=42 then.
14 const BETA_WEEK_START
: u64 = 35;
17 const OS
: Option
<&str> = Some("linux");
20 const OS
: Option
<&str> = Some("windows");
22 #[cfg(all(not(linux), not(windows)))]
23 const OS
: Option
<&str> = None
;
25 type ToolstateData
= HashMap
<Box
<str>, ToolState
>;
27 #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28 #[serde(rename_all = "kebab-case")]
29 /// Whether a tool can be compiled, tested or neither
31 /// The tool compiles successfully, but the test suite fails
33 /// The tool compiles successfully and its test suite passes
35 /// The tool can't even be compiled
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",
49 impl Default
for ToolState
{
50 fn default() -> Self {
51 // err on the safe side
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
65 // These tools must test-pass on the beta/stable channels.
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"),
80 // These tools are permitted to not build on the beta/stable channels.
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
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"),
91 fn print_error(tool
: &str, submodule
: &str) {
93 eprintln
!("We detected that this PR updated '{}', but its tests failed.", tool
);
95 eprintln
!("If you do intend to update '{}', please check the error messages above and", tool
);
96 eprintln
!("commit another update.");
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);
104 fn check_changed_files(toolstates
: &HashMap
<Box
<str>, ToolState
>) {
106 let output
= std
::process
::Command
::new("git")
108 .arg("--name-status")
112 let output
= match output
{
115 eprintln
!("Failed to get changed files: {:?}", e
);
116 std
::process
::exit(1);
120 let output
= t
!(String
::from_utf8(output
.stdout
));
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
)
126 eprintln
!("Verifying status of {}...", tool
);
131 eprintln
!("This PR updated '{}', verifying if status is 'test-pass'...", submodule
);
132 if toolstates
[*tool
] != ToolState
::TestPass
{
133 print_error(tool
, submodule
);
138 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
139 pub struct ToolStateCheck
;
141 impl Step
for ToolStateCheck
{
144 /// Runs the `linkchecker` tool as compiled in `stage` by the `host` compiler.
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
{
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();
158 let mut did_error
= false;
160 for (tool
, _
) in STABLE_TOOLS
.iter().chain(NIGHTLY_TOOLS
.iter()) {
161 if !toolstates
.contains_key(*tool
) {
163 eprintln
!("error: Tool `{}` was not recorded in tool state.", tool
);
168 std
::process
::exit(1);
171 check_changed_files(&toolstates
);
173 for (tool
, _
) in STABLE_TOOLS
.iter() {
174 let state
= toolstates
[*tool
];
176 if state
!= ToolState
::TestPass
{
179 eprintln
!("error: Tool `{}` should be test-pass but is {}", tool
, state
);
180 } else if in_beta_week
{
182 eprintln
!("error: Tool `{}` should be test-pass but is {} during beta week.",
189 std
::process
::exit(1);
192 if builder
.config
.channel
== "nightly" && env
::var_os("TOOLSTATE_PUBLISH").is_some() {
193 commit_toolstate_change(&toolstates
, in_beta_week
);
197 fn should_run(run
: ShouldRun
<'_
>) -> ShouldRun
<'_
> {
198 run
.path("check-tools")
201 fn make_run(run
: RunConfig
<'_
>) {
202 run
.builder
.ensure(ToolStateCheck
);
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
));
213 let mut file
= t
!(fs
::OpenOptions
::new()
219 serde_json
::from_reader(&mut file
).unwrap_or_default()
225 /// Updates the actual toolstate of a tool.
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
));
236 let mut file
= t
!(fs
::OpenOptions
::new()
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)));
247 t
!(serde_json
::to_writer(file
, ¤t_toolstates
));
252 /// This function `commit_toolstate_change` provides functionality for pushing a change
253 /// to the `rust-toolstate` repository.
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
260 /// 1. Generate a new Personal access token:
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
270 /// 2. Update the variable group in Azure Pipelines
272 /// * Ping a member of the infrastructure team to do this.
274 /// 4. Replace the email address below if the bot account identity is changed
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
,
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(),
289 panic
!("git config key={} value={} successful (status: {:?})", key
, value
, status
);
293 // If changing anything here, then please check that src/ci/publish_toolstate.sh is up to date
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");
299 let credential
= format
!(
300 "https://{}:x-oauth-basic@github.com\n",
301 t
!(env
::var("TOOLSTATE_REPO_ACCESS_TOKEN")),
303 let git_credential_path
= PathBuf
::from(t
!(env
::var("HOME"))).join(".git-credentials");
304 t
!(fs
::write(&git_credential_path
, credential
));
306 let status
= Command
::new("git").arg("clone")
308 .arg(t
!(env
::var("TOOLSTATE_REPO")))
310 let success
= match status
{
311 Ok(s
) => s
.success(),
315 panic
!("git clone successful (status: {:?})", status
);
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
));
321 let message
= format
!("({} CI update)", OS
.expect("linux/windows only"));
322 let mut success
= false;
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
);
327 // `git commit` failing means nothing to commit.
328 let status
= t
!(Command
::new("git")
329 .current_dir("rust-toolstate")
335 if !status
.success() {
340 let status
= t
!(Command
::new("git")
341 .current_dir("rust-toolstate")
346 // If we successfully push, exit.
347 if status
.success() {
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")
359 assert
!(status
.success());
360 let status
= t
!(Command
::new("git")
361 .current_dir("rust-toolstate")
364 .arg("origin/master")
366 assert
!(status
.success());
370 panic
!("Failed to update toolstate repository with new data");
375 current_toolstate
: &ToolstateData
,
376 old_toolstate
: &[RepoState
],
379 let mut regressed
= false;
380 for repo_state
in old_toolstate
{
381 let tool
= &repo_state
.tool
;
382 let state
= if cfg
!(linux
) {
384 } else if cfg
!(windows
) {
389 let new_state
= current_toolstate
[tool
.as_str()];
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()) {
401 if regressed
&& in_beta_week
{
402 std
::process
::exit(1);
405 let commit
= t
!(std
::process
::Command
::new("git")
409 let commit
= t
!(String
::from_utf8(commit
.stdout
));
411 let toolstate_serialized
= t
!(serde_json
::to_string(¤t_toolstate
));
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
));
420 #[derive(Debug, Serialize, Deserialize)]