]> git.lizzy.rs Git - rust.git/blob - src/tools/bump-stage0/src/main.rs
Download rustc component for rustfmt toolchain as well
[rust.git] / src / tools / bump-stage0 / src / main.rs
1 use anyhow::{Context, Error};
2 use curl::easy::Easy;
3 use indexmap::IndexMap;
4 use std::collections::HashMap;
5 use std::convert::TryInto;
6
7 const PATH: &str = "src/stage0.json";
8 const COMPILER_COMPONENTS: &[&str] = &["rustc", "rust-std", "cargo"];
9 const RUSTFMT_COMPONENTS: &[&str] = &["rustfmt-preview", "rustc"];
10
11 struct Tool {
12     config: Config,
13     comments: Vec<String>,
14
15     channel: Channel,
16     date: Option<String>,
17     version: [u16; 3],
18     checksums: IndexMap<String, String>,
19 }
20
21 impl Tool {
22     fn new(date: Option<String>) -> Result<Self, Error> {
23         let channel = match std::fs::read_to_string("src/ci/channel")?.trim() {
24             "stable" => Channel::Stable,
25             "beta" => Channel::Beta,
26             "nightly" => Channel::Nightly,
27             other => anyhow::bail!("unsupported channel: {}", other),
28         };
29
30         // Split "1.42.0" into [1, 42, 0]
31         let version = std::fs::read_to_string("src/version")?
32             .trim()
33             .split('.')
34             .map(|val| val.parse())
35             .collect::<Result<Vec<_>, _>>()?
36             .try_into()
37             .map_err(|_| anyhow::anyhow!("failed to parse version"))?;
38
39         let existing: Stage0 = serde_json::from_slice(&std::fs::read(PATH)?)?;
40
41         Ok(Self {
42             channel,
43             version,
44             date,
45             config: existing.config,
46             comments: existing.comments,
47             checksums: IndexMap::new(),
48         })
49     }
50
51     fn update_json(mut self) -> Result<(), Error> {
52         std::fs::write(
53             PATH,
54             format!(
55                 "{}\n",
56                 serde_json::to_string_pretty(&Stage0 {
57                     compiler: self.detect_compiler()?,
58                     rustfmt: self.detect_rustfmt()?,
59                     checksums_sha256: {
60                         // Keys are sorted here instead of beforehand because values in this map
61                         // are added while filling the other struct fields just above this block.
62                         self.checksums.sort_keys();
63                         self.checksums
64                     },
65                     config: self.config,
66                     comments: self.comments,
67                 })?
68             ),
69         )?;
70         Ok(())
71     }
72
73     // Currently Rust always bootstraps from the previous stable release, and in our train model
74     // this means that the master branch bootstraps from beta, beta bootstraps from current stable,
75     // and stable bootstraps from the previous stable release.
76     //
77     // On the master branch the compiler version is configured to `beta` whereas if you're looking
78     // at the beta or stable channel you'll likely see `1.x.0` as the version, with the previous
79     // release's version number.
80     fn detect_compiler(&mut self) -> Result<Stage0Toolchain, Error> {
81         let channel = match self.channel {
82             Channel::Stable | Channel::Beta => {
83                 // The 1.XX manifest points to the latest point release of that minor release.
84                 format!("{}.{}", self.version[0], self.version[1] - 1)
85             }
86             Channel::Nightly => "beta".to_string(),
87         };
88
89         let manifest = fetch_manifest(&self.config, &channel, self.date.as_deref())?;
90         self.collect_checksums(&manifest, COMPILER_COMPONENTS)?;
91         Ok(Stage0Toolchain {
92             date: manifest.date,
93             version: if self.channel == Channel::Nightly {
94                 "beta".to_string()
95             } else {
96                 // The version field is like "1.42.0 (abcdef1234 1970-01-01)"
97                 manifest.pkg["rust"]
98                     .version
99                     .split_once(' ')
100                     .expect("invalid version field")
101                     .0
102                     .to_string()
103             },
104         })
105     }
106
107     /// We use a nightly rustfmt to format the source because it solves some bootstrapping issues
108     /// with use of new syntax in this repo. For the beta/stable channels rustfmt is not provided,
109     /// as we don't want to depend on rustfmt from nightly there.
110     fn detect_rustfmt(&mut self) -> Result<Option<Stage0Toolchain>, Error> {
111         if self.channel != Channel::Nightly {
112             return Ok(None);
113         }
114
115         let manifest = fetch_manifest(&self.config, "nightly", self.date.as_deref())?;
116         self.collect_checksums(&manifest, RUSTFMT_COMPONENTS)?;
117         Ok(Some(Stage0Toolchain { date: manifest.date, version: "nightly".into() }))
118     }
119
120     fn collect_checksums(&mut self, manifest: &Manifest, components: &[&str]) -> Result<(), Error> {
121         let prefix = format!("{}/", self.config.dist_server);
122         for component in components {
123             let pkg = manifest
124                 .pkg
125                 .get(*component)
126                 .ok_or_else(|| anyhow::anyhow!("missing component from manifest: {}", component))?;
127             for target in pkg.target.values() {
128                 for pair in &[(&target.url, &target.hash), (&target.xz_url, &target.xz_hash)] {
129                     if let (Some(url), Some(sha256)) = pair {
130                         let url = url
131                             .strip_prefix(&prefix)
132                             .ok_or_else(|| {
133                                 anyhow::anyhow!("url doesn't start with dist server base: {}", url)
134                             })?
135                             .to_string();
136                         self.checksums.insert(url, sha256.clone());
137                     }
138                 }
139             }
140         }
141         Ok(())
142     }
143 }
144
145 fn main() -> Result<(), Error> {
146     let tool = Tool::new(std::env::args().nth(1))?;
147     tool.update_json()?;
148     Ok(())
149 }
150
151 fn fetch_manifest(config: &Config, channel: &str, date: Option<&str>) -> Result<Manifest, Error> {
152     let url = if let Some(date) = date {
153         format!("{}/dist/{}/channel-rust-{}.toml", config.dist_server, date, channel)
154     } else {
155         format!("{}/dist/channel-rust-{}.toml", config.dist_server, channel)
156     };
157
158     Ok(toml::from_slice(&http_get(&url)?)?)
159 }
160
161 fn http_get(url: &str) -> Result<Vec<u8>, Error> {
162     let mut data = Vec::new();
163     let mut handle = Easy::new();
164     handle.fail_on_error(true)?;
165     handle.url(url)?;
166     {
167         let mut transfer = handle.transfer();
168         transfer.write_function(|new_data| {
169             data.extend_from_slice(new_data);
170             Ok(new_data.len())
171         })?;
172         transfer.perform().context(format!("failed to fetch {url}"))?;
173     }
174     Ok(data)
175 }
176
177 #[derive(Debug, PartialEq, Eq)]
178 enum Channel {
179     Stable,
180     Beta,
181     Nightly,
182 }
183
184 #[derive(Debug, serde::Serialize, serde::Deserialize)]
185 struct Stage0 {
186     config: Config,
187     // Comments are explicitly below the config, do not move them above.
188     //
189     // Downstream forks of the compiler codebase can change the configuration values defined above,
190     // but doing so would risk merge conflicts whenever they import new changes that include a
191     // bootstrap compiler bump.
192     //
193     // To lessen the pain, a big block of comments is placed between the configuration and the
194     // auto-generated parts of the file, preventing git diffs of the config to include parts of the
195     // auto-generated content and vice versa. This should prevent merge conflicts.
196     #[serde(rename = "__comments")]
197     comments: Vec<String>,
198     compiler: Stage0Toolchain,
199     rustfmt: Option<Stage0Toolchain>,
200     checksums_sha256: IndexMap<String, String>,
201 }
202
203 #[derive(Debug, serde::Serialize, serde::Deserialize)]
204 struct Config {
205     dist_server: String,
206     // There are other fields in the configuration, which will be read by src/bootstrap or other
207     // tools consuming stage0.json. To avoid the need to update bump-stage0 every time a new field
208     // is added, we collect all the fields in an untyped Value and serialize them back with the
209     // same order and structure they were deserialized in.
210     #[serde(flatten)]
211     other: serde_json::Value,
212 }
213
214 #[derive(Debug, serde::Serialize, serde::Deserialize)]
215 struct Stage0Toolchain {
216     date: String,
217     version: String,
218 }
219
220 #[derive(Debug, serde::Serialize, serde::Deserialize)]
221 struct Manifest {
222     date: String,
223     pkg: HashMap<String, ManifestPackage>,
224 }
225
226 #[derive(Debug, serde::Serialize, serde::Deserialize)]
227 struct ManifestPackage {
228     version: String,
229     target: HashMap<String, ManifestTargetPackage>,
230 }
231
232 #[derive(Debug, serde::Serialize, serde::Deserialize)]
233 struct ManifestTargetPackage {
234     url: Option<String>,
235     hash: Option<String>,
236     xz_url: Option<String>,
237     xz_hash: Option<String>,
238 }