bootstrap/core/build_steps/
setup.rs1use std::collections::BTreeMap;
9use std::env::consts::EXE_SUFFIX;
10use std::fmt::Write as _;
11use std::fs::File;
12use std::io::Write;
13use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf};
14use std::str::FromStr;
15use std::sync::LazyLock;
16use std::{fmt, fs, io};
17
18use serde_derive::{Deserialize, Serialize};
19use sha2::Digest;
20
21use crate::core::build_steps::format;
22use crate::core::builder::{Builder, CommandLineStep, RunConfig, ShouldRun};
23use crate::core::config::Config;
24use crate::utils::change_tracker::CONFIG_CHANGE_HISTORY;
25use crate::utils::exec::command;
26use crate::utils::helpers::{self, hex_encode, t};
27
28#[cfg(test)]
29mod tests;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
32pub enum Profile {
33 Compiler,
34 Library,
35 Tools,
36 Dist,
37 None,
38}
39
40static PROFILE_DIR: &str = "src/bootstrap/defaults";
41
42impl Profile {
43 fn include_path(&self, src_path: &Path) -> PathBuf {
44 PathBuf::from(format!("{}/{PROFILE_DIR}/bootstrap.{}.toml", src_path.display(), self))
45 }
46
47 pub fn all() -> impl Iterator<Item = Self> {
48 use Profile::*;
49 [Library, Compiler, Tools, Dist, None].iter().copied()
51 }
52
53 pub fn purpose(&self) -> String {
54 use Profile::*;
55 match self {
56 Library => "Contribute to the standard library",
57 Compiler => "Contribute to the compiler itself",
58 Tools => "Contribute to tools which depend on the compiler, but do not modify it directly (e.g. rustdoc, clippy, miri)",
59 Dist => "Install Rust from source",
60 None => "Do not modify `bootstrap.toml`"
61 }
62 .to_string()
63 }
64
65 pub fn all_for_help(indent: &str) -> String {
66 let mut out = String::new();
67 for choice in Profile::all() {
68 writeln!(&mut out, "{}{}: {}", indent, choice, choice.purpose()).unwrap();
69 }
70 out
71 }
72
73 pub fn as_str(&self) -> &'static str {
74 match self {
75 Profile::Compiler => "compiler",
76 Profile::Library => "library",
77 Profile::Tools => "tools",
78 Profile::Dist => "dist",
79 Profile::None => "none",
80 }
81 }
82}
83
84impl FromStr for Profile {
85 type Err = String;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 match s {
89 "lib" | "library" => Ok(Profile::Library),
90 "compiler" => Ok(Profile::Compiler),
91 "maintainer" | "dist" | "user" => Ok(Profile::Dist),
92 "tools" | "tool" | "rustdoc" | "clippy" | "miri" | "rustfmt" => Ok(Profile::Tools),
93 "none" => Ok(Profile::None),
94 "llvm" | "codegen" => Err("the \"llvm\" and \"codegen\" profiles have been removed,\
95 use \"compiler\" instead which has the same functionality"
96 .to_string()),
97 _ => Err(format!("unknown profile: '{s}'")),
98 }
99 }
100}
101
102impl fmt::Display for Profile {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(self.as_str())
105 }
106}
107
108impl CommandLineStep for Profile {
109 type Output = ();
110
111 fn should_run(mut run: ShouldRun<'_>) -> ShouldRun<'_> {
112 for choice in Profile::all() {
113 run = run.alias_without_assert(choice.as_str());
117 }
118 run
119 }
120
121 fn is_default_step(_builder: &Builder<'_>) -> bool {
122 true
123 }
124
125 fn make_run(run: RunConfig<'_>) {
126 if run.builder.config.dry_run() {
127 return;
128 }
129
130 let path = &run.builder.config.config.clone().unwrap_or(PathBuf::from("bootstrap.toml"));
131 if path.exists() {
132 eprintln!();
133 eprintln!(
134 "ERROR: you asked for a new config file, but one already exists at `{}`",
135 t!(path.canonicalize()).display()
136 );
137
138 match prompt_user(
139 "Do you wish to override the existing configuration (which will allow the setup process to continue)?: [y/N]",
140 ) {
141 Ok(Some(PromptResult::Yes)) => {
142 t!(fs::remove_file(path));
143 }
144 _ => {
145 println!("Exiting.");
146 helpers::exit_process(1);
147 }
148 }
149 }
150
151 let profile = if run.paths.len() > 1 {
156 t!(interactive_path())
158 } else {
159 run.paths
160 .first()
161 .unwrap()
162 .assert_single_path()
163 .path
164 .as_path()
165 .as_os_str()
166 .to_str()
167 .unwrap()
168 .parse()
169 .unwrap()
170 };
171
172 run.builder.ensure(profile);
173 }
174
175 fn run(self, builder: &Builder<'_>) {
176 setup(&builder.sess.config, self);
177 }
178}
179
180pub fn setup(config: &Config, profile: Profile) {
181 let suggestions: &[&str] = match profile {
182 Profile::Compiler | Profile::None => &["check", "build", "test"],
183 Profile::Tools => &[
184 "check",
185 "build",
186 "test tests/rustdoc*",
187 "test src/tools/clippy",
188 "test src/tools/miri",
189 "test src/tools/rustfmt",
190 ],
191 Profile::Library => &["check", "build", "test library/std", "doc"],
192 Profile::Dist => &["dist", "build"],
193 };
194
195 println!();
196
197 println!("To get started, try one of the following commands:");
198 for cmd in suggestions {
199 println!("- `x.py {cmd}`");
200 }
201
202 if profile != Profile::Dist {
203 println!(
204 "For more suggestions, see https://rustc-dev-guide.rust-lang.org/building/suggested.html"
205 );
206 }
207
208 if profile == Profile::Tools {
209 eprintln!();
210 eprintln!(
211 "NOTE: the `tools` profile sets up the `stage2` toolchain (use \
212 `rustup toolchain link 'name' build/host/stage2` to use rustc)"
213 )
214 }
215
216 let path = &config.config.clone().unwrap_or(PathBuf::from("bootstrap.toml"));
217 setup_config_toml(path, profile, config);
218}
219
220fn setup_config_toml(path: &Path, profile: Profile, config: &Config) {
221 if profile == Profile::None {
222 return;
223 }
224
225 let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id;
226 let settings = format!(
227 "# See bootstrap.example.toml for documentation of available options\n\
228 #\n\
229 profile = \"{profile}\" # Includes one of the default files in {PROFILE_DIR}\n\
230 change-id = {latest_change_id}\n"
231 );
232
233 t!(fs::write(path, settings));
234
235 let include_path = profile.include_path(&config.src);
236 println!("`x.py` will now use the configuration at {}", include_path.display());
237}
238
239#[derive(Clone, Debug, Eq, PartialEq, Hash)]
241pub struct Link;
242impl CommandLineStep for Link {
243 type Output = ();
244
245 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
246 run.alias("link")
247 }
248
249 fn is_default_step(_builder: &Builder<'_>) -> bool {
250 true
251 }
252
253 fn make_run(run: RunConfig<'_>) {
254 if run.builder.config.dry_run() {
255 return;
256 }
257 if let [cmd] = &run.paths[..]
258 && cmd.assert_single_path().path.as_path().as_os_str() == "link"
259 {
260 run.builder.ensure(Link);
261 }
262 }
263 fn run(self, builder: &Builder<'_>) -> Self::Output {
264 let config = &builder.config;
265
266 if config.dry_run() {
267 return;
268 }
269
270 if !rustup_installed(builder) {
271 println!("WARNING: `rustup` is not installed; Skipping `stage1` toolchain linking.");
272 return;
273 }
274
275 let stage_path =
276 ["build", config.host_target.rustc_target_arg(), "stage1"].join(MAIN_SEPARATOR_STR);
277
278 if stage_dir_exists(&stage_path[..]) && !config.dry_run() {
279 attempt_toolchain_link(builder, &stage_path[..]);
280 }
281 }
282}
283
284fn rustup_installed(builder: &Builder<'_>) -> bool {
285 let mut rustup = command("rustup");
286 rustup.arg("--version");
287
288 rustup.allow_failure().run_in_dry_run().run_capture_stdout(builder).is_success()
289}
290
291fn stage_dir_exists(stage_path: &str) -> bool {
292 match fs::create_dir(stage_path) {
293 Ok(_) => true,
294 Err(_) => Path::new(&stage_path).exists(),
295 }
296}
297
298fn attempt_toolchain_link(builder: &Builder<'_>, stage_path: &str) {
299 if toolchain_is_linked(builder) {
300 return;
301 }
302
303 if !ensure_stage1_toolchain_placeholder_exists(stage_path) {
304 eprintln!(
305 "Failed to create a template for stage 1 toolchain or confirm that it already exists"
306 );
307 return;
308 }
309
310 if try_link_toolchain(builder, stage_path) {
311 println!(
312 "Added `stage1` rustup toolchain; try `cargo +stage1 build` on a separate rust project to run a newly-built toolchain"
313 );
314 } else {
315 eprintln!("`rustup` failed to link stage 1 build to `stage1` toolchain");
316 eprintln!(
317 "To manually link stage 1 build to `stage1` toolchain, run:\n
318 `rustup toolchain link stage1 {}`",
319 stage_path
320 );
321 }
322}
323
324fn toolchain_is_linked(builder: &Builder<'_>) -> bool {
325 match command("rustup")
326 .allow_failure()
327 .args(["toolchain", "list"])
328 .run_capture_stdout(builder)
329 .stdout_if_ok()
330 {
331 Some(toolchain_list) => {
332 if !toolchain_list.contains("stage1") {
333 return false;
334 }
335 println!(
337 "`stage1` toolchain already linked; not attempting to link `stage1` toolchain"
338 );
339 }
340 None => {
341 println!(
344 "`rustup` failed to list current toolchains; not attempting to link `stage1` toolchain"
345 );
346 }
347 }
348 true
349}
350
351fn try_link_toolchain(builder: &Builder<'_>, stage_path: &str) -> bool {
352 command("rustup")
353 .args(["toolchain", "link", "stage1", stage_path])
354 .run_capture_stdout(builder)
355 .is_success()
356}
357
358fn ensure_stage1_toolchain_placeholder_exists(stage_path: &str) -> bool {
359 let pathbuf = PathBuf::from(stage_path);
360
361 if fs::create_dir_all(pathbuf.join("lib")).is_err() {
362 return false;
363 };
364
365 let pathbuf = pathbuf.join("bin");
366 if fs::create_dir_all(&pathbuf).is_err() {
367 return false;
368 };
369
370 let pathbuf = pathbuf.join(format!("rustc{EXE_SUFFIX}"));
371
372 if pathbuf.exists() {
373 return true;
374 }
375
376 let result = File::options().append(true).create(true).open(&pathbuf);
378 result.is_ok()
379}
380
381pub fn interactive_path() -> io::Result<Profile> {
383 fn abbrev_all() -> impl Iterator<Item = ((String, String), Profile)> {
384 ('a'..)
385 .zip(1..)
386 .map(|(letter, number)| (letter.to_string(), number.to_string()))
387 .zip(Profile::all())
388 }
389
390 fn parse_with_abbrev(input: &str) -> Result<Profile, String> {
391 let input = input.trim().to_lowercase();
392 for ((letter, number), profile) in abbrev_all() {
393 if input == letter || input == number {
394 return Ok(profile);
395 }
396 }
397 input.parse()
398 }
399
400 println!("Welcome to the Rust project! What do you want to do with x.py?");
401 for ((letter, _), profile) in abbrev_all() {
402 println!("{}) {}: {}", letter, profile, profile.purpose());
403 }
404 let template = loop {
405 print!(
406 "Please choose one ({}): ",
407 abbrev_all().map(|((l, _), _)| l).collect::<Vec<_>>().join("/")
408 );
409 io::stdout().flush()?;
410 let mut input = String::new();
411 io::stdin().read_line(&mut input)?;
412 if input.is_empty() {
413 eprintln!("EOF on stdin, when expecting answer to question. Giving up.");
414 helpers::exit_process(1);
415 }
416 break match parse_with_abbrev(&input) {
417 Ok(profile) => profile,
418 Err(err) => {
419 eprintln!("ERROR: {err}");
420 eprintln!("NOTE: press Ctrl+C to exit");
421 continue;
422 }
423 };
424 };
425 Ok(template)
426}
427
428#[derive(PartialEq)]
429enum PromptResult {
430 Yes, No, Print, }
434
435fn prompt_user(prompt: &str) -> io::Result<Option<PromptResult>> {
437 let mut input = String::new();
438 loop {
439 print!("{prompt} ");
440 io::stdout().flush()?;
441 input.clear();
442 io::stdin().read_line(&mut input)?;
443 match input.trim().to_lowercase().as_str() {
444 "y" | "yes" => return Ok(Some(PromptResult::Yes)),
445 "n" | "no" => return Ok(Some(PromptResult::No)),
446 "p" | "print" => return Ok(Some(PromptResult::Print)),
447 "" => return Ok(None),
448 _ => {
449 eprintln!("ERROR: unrecognized option '{}'", input.trim());
450 eprintln!("NOTE: press Ctrl+C to exit");
451 }
452 };
453 }
454}
455
456#[derive(Clone, Debug, Eq, PartialEq, Hash)]
458pub struct Hook;
459
460impl CommandLineStep for Hook {
461 type Output = ();
462
463 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
464 run.alias("hook")
465 }
466
467 fn is_default_step(_builder: &Builder<'_>) -> bool {
468 true
469 }
470
471 fn make_run(run: RunConfig<'_>) {
472 if let [cmd] = &run.paths[..]
473 && cmd.assert_single_path().path.as_path().as_os_str() == "hook"
474 {
475 run.builder.ensure(Hook);
476 }
477 }
478
479 fn run(self, builder: &Builder<'_>) -> Self::Output {
480 let config = &builder.config;
481
482 if config.dry_run() || !config.rust_info.is_managed_git_subrepository() {
483 return;
484 }
485
486 t!(install_git_hook_maybe(builder, config));
487 }
488}
489
490fn install_git_hook_maybe(builder: &Builder<'_>, config: &Config) -> io::Result<()> {
492 let git = helpers::git(Some(&config.src))
493 .args(["rev-parse", "--git-common-dir"])
494 .run_capture(builder)
495 .stdout();
496 let git = PathBuf::from(git.trim());
497 let hooks_dir = git.join("hooks");
498 let dst = hooks_dir.join("pre-push");
499 if dst.exists() {
500 return Ok(());
502 }
503
504 println!(
505 "\nRust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
506If you'd like, x.py can install a git hook for you that will automatically run `test tidy` before
507pushing your code to ensure your code is up to par. If you decide later that this behavior is
508undesirable, simply delete the `pre-push` file from .git/hooks."
509 );
510
511 if prompt_user("Would you like to install the git hook?: [y/N]")? != Some(PromptResult::Yes) {
512 println!("Ok, skipping installation!");
513 return Ok(());
514 }
515 if !hooks_dir.exists() {
516 let _ = fs::create_dir(hooks_dir);
518 }
519 let src = config.src.join("src").join("etc").join("pre-push.sh");
520 match fs::hard_link(src, &dst) {
521 Err(e) => {
522 eprintln!(
523 "ERROR: could not create hook {}: do you already have the git hook installed?\n{}",
524 dst.display(),
525 e
526 );
527 return Err(e);
528 }
529 Ok(_) => println!("Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`"),
530 };
531 Ok(())
532}
533
534#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
536#[serde(rename_all = "lowercase")]
537enum EditorKind {
538 Emacs,
539 Helix,
540 Vim,
541 VsCode,
542 Zed,
543}
544
545static PARSED_HASHES: LazyLock<BTreeMap<EditorKind, Vec<&'static str>>> = LazyLock::new(|| {
546 const ALL_HASHES: &str = include_str!("setup/hashes.json");
547 let mut map: BTreeMap<_, Vec<_>> = serde_json::from_str(ALL_HASHES).unwrap();
548 map.insert(EditorKind::Vim, map.get(&EditorKind::VsCode).unwrap().clone());
549 map
550});
551
552impl EditorKind {
553 #[cfg(test)]
555 pub const ALL: &[EditorKind] = &[
556 EditorKind::Emacs,
557 EditorKind::Helix,
558 EditorKind::Vim,
559 EditorKind::VsCode,
560 EditorKind::Zed,
561 ];
562
563 fn prompt_user() -> io::Result<Option<EditorKind>> {
564 let prompt_str = "Available editors:
5651. Emacs
5662. Helix
5673. Vim
5684. VS Code
5695. Zed
570
571Select which editor you would like to set up [default: None]: ";
572
573 let mut input = String::new();
574 loop {
575 print!("{prompt_str}");
576 io::stdout().flush()?;
577 io::stdin().read_line(&mut input)?;
578
579 let mut modified_input = input.to_lowercase();
580 modified_input.retain(|ch| !ch.is_whitespace());
581 match modified_input.as_str() {
582 "1" | "emacs" => return Ok(Some(EditorKind::Emacs)),
583 "2" | "helix" => return Ok(Some(EditorKind::Helix)),
584 "3" | "vim" => return Ok(Some(EditorKind::Vim)),
585 "4" | "vscode" => return Ok(Some(EditorKind::VsCode)),
586 "5" | "zed" => return Ok(Some(EditorKind::Zed)),
587 "" | "none" => return Ok(None),
588 _ => {
589 eprintln!("ERROR: unrecognized option '{}'", input.trim());
590 eprintln!("NOTE: press Ctrl+C to exit");
591 }
592 }
593
594 input.clear();
595 }
596 }
597
598 fn hashes(&self) -> &'static [&'static str] {
602 PARSED_HASHES.get(self).unwrap()
603 }
604
605 fn settings_path(&self, config: &Config) -> PathBuf {
606 config.src.join(self.settings_short_path())
607 }
608
609 fn settings_short_path(&self) -> PathBuf {
610 self.settings_folder().join(match self {
611 EditorKind::Emacs => ".dir-locals.el",
612 EditorKind::Helix => "languages.toml",
613 EditorKind::Vim => "coc-settings.json",
614 EditorKind::VsCode | EditorKind::Zed => "settings.json",
615 })
616 }
617
618 fn settings_folder(&self) -> PathBuf {
619 match self {
620 EditorKind::Emacs => PathBuf::new(),
621 EditorKind::Helix => PathBuf::from(".helix"),
622 EditorKind::Vim => PathBuf::from(".vim"),
623 EditorKind::VsCode => PathBuf::from(".vscode"),
624 EditorKind::Zed => PathBuf::from(".zed"),
625 }
626 }
627
628 fn settings_template(&self) -> &'static str {
629 match self {
630 EditorKind::Emacs => include_str!("../../../../etc/rust_analyzer_eglot.el"),
631 EditorKind::Helix => include_str!("../../../../etc/rust_analyzer_helix.toml"),
632 EditorKind::Vim | EditorKind::VsCode => {
633 include_str!("../../../../etc/rust_analyzer_settings.json")
634 }
635 EditorKind::Zed => include_str!("../../../../etc/rust_analyzer_zed.json"),
636 }
637 }
638
639 fn backup_extension(&self) -> String {
640 format!("{}.bak", self.settings_short_path().extension().unwrap().to_str().unwrap())
641 }
642}
643
644#[derive(Clone, Debug, Eq, PartialEq, Hash)]
646pub struct Editor;
647
648impl CommandLineStep for Editor {
649 type Output = ();
650
651 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
652 run.alias("editor")
653 }
654
655 fn is_default_step(_builder: &Builder<'_>) -> bool {
656 true
657 }
658
659 fn make_run(run: RunConfig<'_>) {
660 if run.builder.config.dry_run() {
661 return;
662 }
663 if let [cmd] = &run.paths[..]
664 && cmd.assert_single_path().path.as_path().as_os_str() == "editor"
665 {
666 run.builder.ensure(Editor);
667 }
668 }
669
670 fn run(self, builder: &Builder<'_>) -> Self::Output {
671 let config = &builder.config;
672 if config.dry_run() {
673 return;
674 }
675 match EditorKind::prompt_user() {
676 Ok(editor_kind) => {
677 if let Some(editor_kind) = editor_kind {
678 while !t!(create_editor_settings_maybe(config, &editor_kind)) {}
679
680 builder.ensure(format::InternalRustfmt);
683 } else {
684 println!("Ok, skipping editor setup!");
685 }
686 }
687 Err(e) => eprintln!("Could not determine the editor: {e}"),
688 }
689 }
690}
691
692fn create_editor_settings_maybe(config: &Config, editor: &EditorKind) -> io::Result<bool> {
695 let hashes = editor.hashes();
696 let (current_hash, historical_hashes) = hashes.split_last().unwrap();
697 let settings_path = editor.settings_path(config);
698 let settings_short_path = editor.settings_short_path();
699 let settings_filename = settings_short_path.to_str().unwrap();
700 let mut mismatched_settings = None;
705 if let Ok(current) = fs::read_to_string(&settings_path) {
706 let mut hasher = sha2::Sha256::new();
707 hasher.update(¤t);
708 let hash = hex_encode(hasher.finalize().as_slice());
709 if hash == *current_hash {
710 return Ok(true);
711 } else if historical_hashes.contains(&hash.as_str()) {
712 mismatched_settings = Some(true);
713 } else {
714 mismatched_settings = Some(false);
715 }
716 }
717 println!(
718 "\nx.py can automatically install the recommended `{settings_filename}` file for rustc development"
719 );
720
721 match mismatched_settings {
722 Some(true) => {
723 eprintln!("WARNING: existing `{settings_filename}` is out of date, x.py will update it")
724 }
725 Some(false) => eprintln!(
726 "WARNING: existing `{settings_filename}` has been modified by user, x.py will back it up and replace it"
727 ),
728 _ => (),
729 }
730 let should_create = match prompt_user(&format!(
731 "Would you like to create/update `{settings_filename}`? (Press 'p' to preview values): [y/N]"
732 ))? {
733 Some(PromptResult::Yes) => true,
734 Some(PromptResult::Print) => false,
735 _ => {
736 println!("Ok, skipping settings!");
737 return Ok(true);
738 }
739 };
740 if should_create {
741 let settings_folder_path = config.src.join(editor.settings_folder());
742 if !settings_folder_path.exists() {
743 fs::create_dir(settings_folder_path)?;
744 }
745 let verb = match mismatched_settings {
746 Some(true) => "Updated",
748 Some(false) => {
750 let backup = settings_path.with_extension(editor.backup_extension());
752 eprintln!(
753 "WARNING: copying `{}` to `{}`",
754 settings_path.file_name().unwrap().to_str().unwrap(),
755 backup.file_name().unwrap().to_str().unwrap(),
756 );
757 fs::copy(&settings_path, &backup)?;
758 "Updated"
759 }
760 _ => "Created",
761 };
762 fs::write(&settings_path, editor.settings_template())?;
763 println!("{verb} `{settings_filename}`");
764 } else {
765 println!("\n{}", editor.settings_template());
766 }
767 Ok(should_create)
768}