]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/setup.rs
Allow automatically creating vscode `settings.json` from bootstrap
[rust.git] / src / bootstrap / setup.rs
1 use crate::builder::{Builder, RunConfig, ShouldRun, Step};
2 use crate::Config;
3 use crate::{t, VERSION};
4 use sha2::Digest;
5 use std::env::consts::EXE_SUFFIX;
6 use std::fmt::Write as _;
7 use std::fs::File;
8 use std::io::Write;
9 use std::path::{Path, PathBuf, MAIN_SEPARATOR};
10 use std::process::Command;
11 use std::str::FromStr;
12 use std::{fmt, fs, io};
13
14 #[cfg(test)]
15 mod tests;
16
17 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
18 pub enum Profile {
19     Compiler,
20     Codegen,
21     Library,
22     Tools,
23     User,
24 }
25
26 /// A list of historical hashes of `src/etc/vscode_settings.json`.
27 /// New entries should be appended whenever this is updated so we can detected
28 /// outdated vs. user-modified settings files.
29 static SETTINGS_HASHES: &[&str] =
30     &["ea67e259dedf60d4429b6c349a564ffcd1563cf41c920a856d1f5b16b4701ac8"];
31 static VSCODE_SETTINGS: &str = include_str!("../etc/vscode_settings.json");
32
33 impl Profile {
34     fn include_path(&self, src_path: &Path) -> PathBuf {
35         PathBuf::from(format!("{}/src/bootstrap/defaults/config.{}.toml", src_path.display(), self))
36     }
37
38     pub fn all() -> impl Iterator<Item = Self> {
39         use Profile::*;
40         // N.B. these are ordered by how they are displayed, not alphabetically
41         [Library, Compiler, Codegen, Tools, User].iter().copied()
42     }
43
44     pub fn purpose(&self) -> String {
45         use Profile::*;
46         match self {
47             Library => "Contribute to the standard library",
48             Compiler => "Contribute to the compiler itself",
49             Codegen => "Contribute to the compiler, and also modify LLVM or codegen",
50             Tools => "Contribute to tools which depend on the compiler, but do not modify it directly (e.g. rustdoc, clippy, miri)",
51             User => "Install Rust from source",
52         }
53         .to_string()
54     }
55
56     pub fn all_for_help(indent: &str) -> String {
57         let mut out = String::new();
58         for choice in Profile::all() {
59             writeln!(&mut out, "{}{}: {}", indent, choice, choice.purpose()).unwrap();
60         }
61         out
62     }
63
64     pub fn as_str(&self) -> &'static str {
65         match self {
66             Profile::Compiler => "compiler",
67             Profile::Codegen => "codegen",
68             Profile::Library => "library",
69             Profile::Tools => "tools",
70             Profile::User => "user",
71         }
72     }
73 }
74
75 impl FromStr for Profile {
76     type Err = String;
77
78     fn from_str(s: &str) -> Result<Self, Self::Err> {
79         match s {
80             "lib" | "library" => Ok(Profile::Library),
81             "compiler" => Ok(Profile::Compiler),
82             "llvm" | "codegen" => Ok(Profile::Codegen),
83             "maintainer" | "user" => Ok(Profile::User),
84             "tools" | "tool" | "rustdoc" | "clippy" | "miri" | "rustfmt" | "rls" => {
85                 Ok(Profile::Tools)
86             }
87             _ => Err(format!("unknown profile: '{}'", s)),
88         }
89     }
90 }
91
92 impl fmt::Display for Profile {
93     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94         f.write_str(self.as_str())
95     }
96 }
97
98 impl Step for Profile {
99     type Output = ();
100     const DEFAULT: bool = true;
101
102     fn should_run(mut run: ShouldRun<'_>) -> ShouldRun<'_> {
103         for choice in Profile::all() {
104             run = run.alias(choice.as_str());
105         }
106         run
107     }
108
109     fn make_run(run: RunConfig<'_>) {
110         if run.builder.config.dry_run() {
111             return;
112         }
113
114         // for Profile, `run.paths` will have 1 and only 1 element
115         // this is because we only accept at most 1 path from user input.
116         // If user calls `x.py setup` without arguments, the interactive TUI
117         // will guide user to provide one.
118         let profile = if run.paths.len() > 1 {
119             // HACK: `builder` runs this step with all paths if no path was passed.
120             t!(interactive_path())
121         } else {
122             run.paths
123                 .first()
124                 .unwrap()
125                 .assert_single_path()
126                 .path
127                 .as_path()
128                 .as_os_str()
129                 .to_str()
130                 .unwrap()
131                 .parse()
132                 .unwrap()
133         };
134
135         run.builder.ensure(profile);
136     }
137
138     fn run(self, builder: &Builder<'_>) {
139         setup(&builder.build.config, self)
140     }
141 }
142
143 pub fn setup(config: &Config, profile: Profile) {
144     let stage_path =
145         ["build", config.build.rustc_target_arg(), "stage1"].join(&MAIN_SEPARATOR.to_string());
146
147     if !rustup_installed() && profile != Profile::User {
148         eprintln!("`rustup` is not installed; cannot link `stage1` toolchain");
149     } else if stage_dir_exists(&stage_path[..]) && !config.dry_run() {
150         attempt_toolchain_link(&stage_path[..]);
151     }
152
153     let suggestions = match profile {
154         Profile::Codegen | Profile::Compiler => &["check", "build", "test"][..],
155         Profile::Tools => &[
156             "check",
157             "build",
158             "test tests/rustdoc*",
159             "test src/tools/clippy",
160             "test src/tools/miri",
161             "test src/tools/rustfmt",
162         ],
163         Profile::Library => &["check", "build", "test library/std", "doc"],
164         Profile::User => &["dist", "build"],
165     };
166
167     if !config.dry_run() {
168         t!(install_git_hook_maybe(&config));
169         t!(create_vscode_settings_maybe(&config));
170     }
171
172     println!();
173
174     println!("To get started, try one of the following commands:");
175     for cmd in suggestions {
176         println!("- `x.py {}`", cmd);
177     }
178
179     if profile != Profile::User {
180         println!(
181             "For more suggestions, see https://rustc-dev-guide.rust-lang.org/building/suggested.html"
182         );
183     }
184
185     let path = &config.config.clone().unwrap_or(PathBuf::from("config.toml"));
186     setup_config_toml(path, profile, config);
187 }
188
189 fn setup_config_toml(path: &PathBuf, profile: Profile, config: &Config) {
190     if path.exists() {
191         eprintln!();
192         eprintln!(
193             "error: you asked `x.py` to setup a new config file, but one already exists at `{}`",
194             path.display()
195         );
196         eprintln!("help: try adding `profile = \"{}\"` at the top of {}", profile, path.display());
197         eprintln!(
198             "note: this will use the configuration in {}",
199             profile.include_path(&config.src).display()
200         );
201         crate::detail_exit(1);
202     }
203
204     let settings = format!(
205         "# Includes one of the default files in src/bootstrap/defaults\n\
206     profile = \"{}\"\n\
207     changelog-seen = {}\n",
208         profile, VERSION
209     );
210
211     t!(fs::write(path, settings));
212
213     let include_path = profile.include_path(&config.src);
214     println!("`x.py` will now use the configuration at {}", include_path.display());
215 }
216
217 fn rustup_installed() -> bool {
218     Command::new("rustup")
219         .arg("--version")
220         .stdout(std::process::Stdio::null())
221         .output()
222         .map_or(false, |output| output.status.success())
223 }
224
225 fn stage_dir_exists(stage_path: &str) -> bool {
226     match fs::create_dir(&stage_path) {
227         Ok(_) => true,
228         Err(_) => Path::new(&stage_path).exists(),
229     }
230 }
231
232 fn attempt_toolchain_link(stage_path: &str) {
233     if toolchain_is_linked() {
234         return;
235     }
236
237     if !ensure_stage1_toolchain_placeholder_exists(stage_path) {
238         eprintln!(
239             "Failed to create a template for stage 1 toolchain or confirm that it already exists"
240         );
241         return;
242     }
243
244     if try_link_toolchain(&stage_path) {
245         println!(
246             "Added `stage1` rustup toolchain; try `cargo +stage1 build` on a separate rust project to run a newly-built toolchain"
247         );
248     } else {
249         eprintln!("`rustup` failed to link stage 1 build to `stage1` toolchain");
250         eprintln!(
251             "To manually link stage 1 build to `stage1` toolchain, run:\n
252             `rustup toolchain link stage1 {}`",
253             &stage_path
254         );
255     }
256 }
257
258 fn toolchain_is_linked() -> bool {
259     match Command::new("rustup")
260         .args(&["toolchain", "list"])
261         .stdout(std::process::Stdio::piped())
262         .output()
263     {
264         Ok(toolchain_list) => {
265             if !String::from_utf8_lossy(&toolchain_list.stdout).contains("stage1") {
266                 return false;
267             }
268             // The toolchain has already been linked.
269             println!(
270                 "`stage1` toolchain already linked; not attempting to link `stage1` toolchain"
271             );
272         }
273         Err(_) => {
274             // In this case, we don't know if the `stage1` toolchain has been linked;
275             // but `rustup` failed, so let's not go any further.
276             println!(
277                 "`rustup` failed to list current toolchains; not attempting to link `stage1` toolchain"
278             );
279         }
280     }
281     true
282 }
283
284 fn try_link_toolchain(stage_path: &str) -> bool {
285     Command::new("rustup")
286         .stdout(std::process::Stdio::null())
287         .args(&["toolchain", "link", "stage1", &stage_path])
288         .output()
289         .map_or(false, |output| output.status.success())
290 }
291
292 fn ensure_stage1_toolchain_placeholder_exists(stage_path: &str) -> bool {
293     let pathbuf = PathBuf::from(stage_path);
294
295     if fs::create_dir_all(pathbuf.join("lib")).is_err() {
296         return false;
297     };
298
299     let pathbuf = pathbuf.join("bin");
300     if fs::create_dir_all(&pathbuf).is_err() {
301         return false;
302     };
303
304     let pathbuf = pathbuf.join(format!("rustc{}", EXE_SUFFIX));
305
306     if pathbuf.exists() {
307         return true;
308     }
309
310     // Take care not to overwrite the file
311     let result = File::options().append(true).create(true).open(&pathbuf);
312     if result.is_err() {
313         return false;
314     }
315
316     return true;
317 }
318
319 // Used to get the path for `Subcommand::Setup`
320 pub fn interactive_path() -> io::Result<Profile> {
321     fn abbrev_all() -> impl Iterator<Item = ((String, String), Profile)> {
322         ('a'..)
323             .zip(1..)
324             .map(|(letter, number)| (letter.to_string(), number.to_string()))
325             .zip(Profile::all())
326     }
327
328     fn parse_with_abbrev(input: &str) -> Result<Profile, String> {
329         let input = input.trim().to_lowercase();
330         for ((letter, number), profile) in abbrev_all() {
331             if input == letter || input == number {
332                 return Ok(profile);
333             }
334         }
335         input.parse()
336     }
337
338     println!("Welcome to the Rust project! What do you want to do with x.py?");
339     for ((letter, _), profile) in abbrev_all() {
340         println!("{}) {}: {}", letter, profile, profile.purpose());
341     }
342     let template = loop {
343         print!(
344             "Please choose one ({}): ",
345             abbrev_all().map(|((l, _), _)| l).collect::<Vec<_>>().join("/")
346         );
347         io::stdout().flush()?;
348         let mut input = String::new();
349         io::stdin().read_line(&mut input)?;
350         if input.is_empty() {
351             eprintln!("EOF on stdin, when expecting answer to question.  Giving up.");
352             crate::detail_exit(1);
353         }
354         break match parse_with_abbrev(&input) {
355             Ok(profile) => profile,
356             Err(err) => {
357                 eprintln!("error: {}", err);
358                 eprintln!("note: press Ctrl+C to exit");
359                 continue;
360             }
361         };
362     };
363     Ok(template)
364 }
365
366 #[derive(PartialEq)]
367 enum PromptResult {
368     Yes,   // y/Y/yes
369     No,    // n/N/no
370     Print, // p/P/print
371 }
372
373 /// Prompt a user for a answer, looping until they enter an accepted input or nothing
374 fn prompt_user(prompt: &str) -> io::Result<Option<PromptResult>> {
375     let mut input = String::new();
376     loop {
377         print!("{prompt} ");
378         io::stdout().flush()?;
379         input.clear();
380         io::stdin().read_line(&mut input)?;
381         match input.trim().to_lowercase().as_str() {
382             "y" | "yes" => return Ok(Some(PromptResult::Yes)),
383             "n" | "no" => return Ok(Some(PromptResult::No)),
384             "p" | "print" => return Ok(Some(PromptResult::Print)),
385             "" => return Ok(None),
386             _ => {
387                 eprintln!("error: unrecognized option '{}'", input.trim());
388                 eprintln!("note: press Ctrl+C to exit");
389             }
390         };
391     }
392 }
393
394 // install a git hook to automatically run tidy, if they want
395 fn install_git_hook_maybe(config: &Config) -> io::Result<()> {
396     let git = t!(config.git().args(&["rev-parse", "--git-common-dir"]).output().map(|output| {
397         assert!(output.status.success(), "failed to run `git`");
398         PathBuf::from(t!(String::from_utf8(output.stdout)).trim())
399     }));
400     let dst = git.join("hooks").join("pre-push");
401     if dst.exists() {
402         // The git hook has already been set up, or the user already has a custom hook.
403         return Ok(());
404     }
405
406     println!(
407         "\nRust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
408 If you'd like, x.py can install a git hook for you that will automatically run `test tidy` before
409 pushing your code to ensure your code is up to par. If you decide later that this behavior is
410 undesirable, simply delete the `pre-push` file from .git/hooks."
411     );
412
413     if prompt_user("Would you like to install the git hook?: [y/N]")? != Some(PromptResult::Yes) {
414         println!("Ok, skipping installation!");
415         return Ok(());
416     }
417     let src = config.src.join("src").join("etc").join("pre-push.sh");
418     match fs::hard_link(src, &dst) {
419         Err(e) => {
420             eprintln!(
421                 "error: could not create hook {}: do you already have the git hook installed?\n{}",
422                 dst.display(),
423                 e
424             );
425             return Err(e);
426         }
427         Ok(_) => println!("Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`"),
428     };
429     Ok(())
430 }
431
432 /// Create a `.vscode/settings.json` file for rustc development, or just print it
433 fn create_vscode_settings_maybe(config: &Config) -> io::Result<()> {
434     let (current_hash, historical_hashes) = SETTINGS_HASHES.split_last().unwrap();
435     let vscode_settings = config.src.join(".vscode").join("settings.json");
436     // If None, no settings.json exists
437     // If Some(true), is a previous version of settings.json
438     // If Some(false), is not a previous version (i.e. user modified)
439     // If it's up to date we can just skip this
440     let mut mismatched_settings = None;
441     if let Ok(current) = fs::read_to_string(&vscode_settings) {
442         let mut hasher = sha2::Sha256::new();
443         hasher.update(&current);
444         let hash = hex::encode(hasher.finalize().as_slice());
445         if hash == *current_hash {
446             return Ok(());
447         } else if historical_hashes.contains(&hash.as_str()) {
448             mismatched_settings = Some(true);
449         } else {
450             mismatched_settings = Some(false);
451         }
452     }
453     println!(
454         "\nx.py can automatically install the recommended `.vscode/settings.json` file for rustc development"
455     );
456     match mismatched_settings {
457         Some(true) => eprintln!(
458             "warning: existing `.vscode/settings.json` is out of date, x.py will update it"
459         ),
460         Some(false) => eprintln!(
461             "warning: existing `.vscode/settings.json` has been modified by user, x.py will back it up and replace it"
462         ),
463         _ => (),
464     }
465     let should_create = match prompt_user(
466         "Would you like to create/update `settings.json`, or only print suggested settings?: [y/p/N]",
467     )? {
468         Some(PromptResult::Yes) => true,
469         Some(PromptResult::Print) => false,
470         _ => {
471             println!("Ok, skipping settings!");
472             return Ok(());
473         }
474     };
475     if should_create {
476         let path = config.src.join(".vscode");
477         if !path.exists() {
478             fs::create_dir(&path)?;
479         }
480         let verb = match mismatched_settings {
481             // exists but outdated, we can replace this
482             Some(true) => "Updated",
483             // exists but user modified, back it up
484             Some(false) => {
485                 // exists and is not current version or outdated, so back it up
486                 let mut backup = vscode_settings.clone();
487                 backup.set_extension("bak");
488                 eprintln!("warning: copying `settings.json` to `settings.json.bak`");
489                 fs::copy(&vscode_settings, &backup)?;
490                 "Updated"
491             }
492             _ => "Created",
493         };
494         fs::write(&vscode_settings, &VSCODE_SETTINGS)?;
495         println!("{verb} `.vscode/settings.json`");
496     } else {
497         println!("\n{VSCODE_SETTINGS}");
498     }
499     Ok(())
500 }