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