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::builder::{Builder, RunConfig, ShouldRun, Step};
22use crate::utils::change_tracker::CONFIG_CHANGE_HISTORY;
23use crate::utils::exec::command;
24use crate::utils::helpers::{self, hex_encode};
25use crate::{Config, t};
26
27#[cfg(test)]
28mod tests;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
31pub enum Profile {
32 Compiler,
33 Library,
34 Tools,
35 Dist,
36 None,
37}
38
39static PROFILE_DIR: &str = "src/bootstrap/defaults";
40
41impl Profile {
42 fn include_path(&self, src_path: &Path) -> PathBuf {
43 PathBuf::from(format!("{}/{PROFILE_DIR}/bootstrap.{}.toml", src_path.display(), self))
44 }
45
46 pub fn all() -> impl Iterator<Item = Self> {
47 use Profile::*;
48 [Library, Compiler, Tools, Dist, None].iter().copied()
50 }
51
52 pub fn purpose(&self) -> String {
53 use Profile::*;
54 match self {
55 Library => "Contribute to the standard library",
56 Compiler => "Contribute to the compiler itself",
57 Tools => "Contribute to tools which depend on the compiler, but do not modify it directly (e.g. rustdoc, clippy, miri)",
58 Dist => "Install Rust from source",
59 None => "Do not modify `bootstrap.toml`"
60 }
61 .to_string()
62 }
63
64 pub fn all_for_help(indent: &str) -> String {
65 let mut out = String::new();
66 for choice in Profile::all() {
67 writeln!(&mut out, "{}{}: {}", indent, choice, choice.purpose()).unwrap();
68 }
69 out
70 }
71
72 pub fn as_str(&self) -> &'static str {
73 match self {
74 Profile::Compiler => "compiler",
75 Profile::Library => "library",
76 Profile::Tools => "tools",
77 Profile::Dist => "dist",
78 Profile::None => "none",
79 }
80 }
81}
82
83impl FromStr for Profile {
84 type Err = String;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 match s {
88 "lib" | "library" => Ok(Profile::Library),
89 "compiler" => Ok(Profile::Compiler),
90 "maintainer" | "dist" | "user" => Ok(Profile::Dist),
91 "tools" | "tool" | "rustdoc" | "clippy" | "miri" | "rustfmt" => Ok(Profile::Tools),
92 "none" => Ok(Profile::None),
93 "llvm" | "codegen" => Err("the \"llvm\" and \"codegen\" profiles have been removed,\
94 use \"compiler\" instead which has the same functionality"
95 .to_string()),
96 _ => Err(format!("unknown profile: '{s}'")),
97 }
98 }
99}
100
101impl fmt::Display for Profile {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(self.as_str())
104 }
105}
106
107impl Step for Profile {
108 type Output = ();
109
110 fn should_run(mut run: ShouldRun<'_>) -> ShouldRun<'_> {
111 for choice in Profile::all() {
112 run = run.alias(choice.as_str());
113 }
114 run
115 }
116
117 fn is_default_step(_builder: &Builder<'_>) -> bool {
118 true
119 }
120
121 fn make_run(run: RunConfig<'_>) {
122 if run.builder.config.dry_run() {
123 return;
124 }
125
126 let path = &run.builder.config.config.clone().unwrap_or(PathBuf::from("bootstrap.toml"));
127 if path.exists() {
128 eprintln!();
129 eprintln!(
130 "ERROR: you asked for a new config file, but one already exists at `{}`",
131 t!(path.canonicalize()).display()
132 );
133
134 match prompt_user(
135 "Do you wish to override the existing configuration (which will allow the setup process to continue)?: [y/N]",
136 ) {
137 Ok(Some(PromptResult::Yes)) => {
138 t!(fs::remove_file(path));
139 }
140 _ => {
141 println!("Exiting.");
142 crate::exit!(1);
143 }
144 }
145 }
146
147 let profile = if run.paths.len() > 1 {
152 t!(interactive_path())
154 } else {
155 run.paths
156 .first()
157 .unwrap()
158 .assert_single_path()
159 .path
160 .as_path()
161 .as_os_str()
162 .to_str()
163 .unwrap()
164 .parse()
165 .unwrap()
166 };
167
168 run.builder.ensure(profile);
169 }
170
171 fn run(self, builder: &Builder<'_>) {
172 setup(&builder.build.config, self);
173 }
174}
175
176pub fn setup(config: &Config, profile: Profile) {
177 let suggestions: &[&str] = match profile {
178 Profile::Compiler | Profile::None => &["check", "build", "test"],
179 Profile::Tools => &[
180 "check",
181 "build",
182 "test tests/rustdoc*",
183 "test src/tools/clippy",
184 "test src/tools/miri",
185 "test src/tools/rustfmt",
186 ],
187 Profile::Library => &["check", "build", "test library/std", "doc"],
188 Profile::Dist => &["dist", "build"],
189 };
190
191 println!();
192
193 println!("To get started, try one of the following commands:");
194 for cmd in suggestions {
195 println!("- `x.py {cmd}`");
196 }
197
198 if profile != Profile::Dist {
199 println!(
200 "For more suggestions, see https://rustc-dev-guide.rust-lang.org/building/suggested.html"
201 );
202 }
203
204 if profile == Profile::Tools {
205 eprintln!();
206 eprintln!(
207 "NOTE: the `tools` profile sets up the `stage2` toolchain (use \
208 `rustup toolchain link 'name' build/host/stage2` to use rustc)"
209 )
210 }
211
212 let path = &config.config.clone().unwrap_or(PathBuf::from("bootstrap.toml"));
213 setup_config_toml(path, profile, config);
214}
215
216fn setup_config_toml(path: &Path, profile: Profile, config: &Config) {
217 if profile == Profile::None {
218 return;
219 }
220
221 let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id;
222 let settings = format!(
223 "# See bootstrap.example.toml for documentation of available options\n\
224 #\n\
225 profile = \"{profile}\" # Includes one of the default files in {PROFILE_DIR}\n\
226 change-id = {latest_change_id}\n"
227 );
228
229 t!(fs::write(path, settings));
230
231 let include_path = profile.include_path(&config.src);
232 println!("`x.py` will now use the configuration at {}", include_path.display());
233}
234
235#[derive(Clone, Debug, Eq, PartialEq, Hash)]
237pub struct Link;
238impl Step for Link {
239 type Output = ();
240
241 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
242 run.alias("link")
243 }
244
245 fn is_default_step(_builder: &Builder<'_>) -> bool {
246 true
247 }
248
249 fn make_run(run: RunConfig<'_>) {
250 if run.builder.config.dry_run() {
251 return;
252 }
253 if let [cmd] = &run.paths[..]
254 && cmd.assert_single_path().path.as_path().as_os_str() == "link"
255 {
256 run.builder.ensure(Link);
257 }
258 }
259 fn run(self, builder: &Builder<'_>) -> Self::Output {
260 let config = &builder.config;
261
262 if config.dry_run() {
263 return;
264 }
265
266 if !rustup_installed(builder) {
267 println!("WARNING: `rustup` is not installed; Skipping `stage1` toolchain linking.");
268 return;
269 }
270
271 let stage_path =
272 ["build", config.host_target.rustc_target_arg(), "stage1"].join(MAIN_SEPARATOR_STR);
273
274 if stage_dir_exists(&stage_path[..]) && !config.dry_run() {
275 attempt_toolchain_link(builder, &stage_path[..]);
276 }
277 }
278}
279
280fn rustup_installed(builder: &Builder<'_>) -> bool {
281 let mut rustup = command("rustup");
282 rustup.arg("--version");
283
284 rustup.allow_failure().run_in_dry_run().run_capture_stdout(builder).is_success()
285}
286
287fn stage_dir_exists(stage_path: &str) -> bool {
288 match fs::create_dir(stage_path) {
289 Ok(_) => true,
290 Err(_) => Path::new(&stage_path).exists(),
291 }
292}
293
294fn attempt_toolchain_link(builder: &Builder<'_>, stage_path: &str) {
295 if toolchain_is_linked(builder) {
296 return;
297 }
298
299 if !ensure_stage1_toolchain_placeholder_exists(stage_path) {
300 eprintln!(
301 "Failed to create a template for stage 1 toolchain or confirm that it already exists"
302 );
303 return;
304 }
305
306 if try_link_toolchain(builder, stage_path) {
307 println!(
308 "Added `stage1` rustup toolchain; try `cargo +stage1 build` on a separate rust project to run a newly-built toolchain"
309 );
310 } else {
311 eprintln!("`rustup` failed to link stage 1 build to `stage1` toolchain");
312 eprintln!(
313 "To manually link stage 1 build to `stage1` toolchain, run:\n
314 `rustup toolchain link stage1 {}`",
315 &stage_path
316 );
317 }
318}
319
320fn toolchain_is_linked(builder: &Builder<'_>) -> bool {
321 match command("rustup")
322 .allow_failure()
323 .args(["toolchain", "list"])
324 .run_capture_stdout(builder)
325 .stdout_if_ok()
326 {
327 Some(toolchain_list) => {
328 if !toolchain_list.contains("stage1") {
329 return false;
330 }
331 println!(
333 "`stage1` toolchain already linked; not attempting to link `stage1` toolchain"
334 );
335 }
336 None => {
337 println!(
340 "`rustup` failed to list current toolchains; not attempting to link `stage1` toolchain"
341 );
342 }
343 }
344 true
345}
346
347fn try_link_toolchain(builder: &Builder<'_>, stage_path: &str) -> bool {
348 command("rustup")
349 .args(["toolchain", "link", "stage1", stage_path])
350 .run_capture_stdout(builder)
351 .is_success()
352}
353
354fn ensure_stage1_toolchain_placeholder_exists(stage_path: &str) -> bool {
355 let pathbuf = PathBuf::from(stage_path);
356
357 if fs::create_dir_all(pathbuf.join("lib")).is_err() {
358 return false;
359 };
360
361 let pathbuf = pathbuf.join("bin");
362 if fs::create_dir_all(&pathbuf).is_err() {
363 return false;
364 };
365
366 let pathbuf = pathbuf.join(format!("rustc{EXE_SUFFIX}"));
367
368 if pathbuf.exists() {
369 return true;
370 }
371
372 let result = File::options().append(true).create(true).open(&pathbuf);
374 if result.is_err() {
375 return false;
376 }
377
378 true
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 crate::exit!(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 Step 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 Step 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 } else {
680 println!("Ok, skipping editor setup!");
681 }
682 }
683 Err(e) => eprintln!("Could not determine the editor: {e}"),
684 }
685 }
686}
687
688fn create_editor_settings_maybe(config: &Config, editor: &EditorKind) -> io::Result<bool> {
691 let hashes = editor.hashes();
692 let (current_hash, historical_hashes) = hashes.split_last().unwrap();
693 let settings_path = editor.settings_path(config);
694 let settings_short_path = editor.settings_short_path();
695 let settings_filename = settings_short_path.to_str().unwrap();
696 let mut mismatched_settings = None;
701 if let Ok(current) = fs::read_to_string(&settings_path) {
702 let mut hasher = sha2::Sha256::new();
703 hasher.update(¤t);
704 let hash = hex_encode(hasher.finalize().as_slice());
705 if hash == *current_hash {
706 return Ok(true);
707 } else if historical_hashes.contains(&hash.as_str()) {
708 mismatched_settings = Some(true);
709 } else {
710 mismatched_settings = Some(false);
711 }
712 }
713 println!(
714 "\nx.py can automatically install the recommended `{settings_filename}` file for rustc development"
715 );
716
717 match mismatched_settings {
718 Some(true) => {
719 eprintln!("WARNING: existing `{settings_filename}` is out of date, x.py will update it")
720 }
721 Some(false) => eprintln!(
722 "WARNING: existing `{settings_filename}` has been modified by user, x.py will back it up and replace it"
723 ),
724 _ => (),
725 }
726 let should_create = match prompt_user(&format!(
727 "Would you like to create/update `{settings_filename}`? (Press 'p' to preview values): [y/N]"
728 ))? {
729 Some(PromptResult::Yes) => true,
730 Some(PromptResult::Print) => false,
731 _ => {
732 println!("Ok, skipping settings!");
733 return Ok(true);
734 }
735 };
736 if should_create {
737 let settings_folder_path = config.src.join(editor.settings_folder());
738 if !settings_folder_path.exists() {
739 fs::create_dir(settings_folder_path)?;
740 }
741 let verb = match mismatched_settings {
742 Some(true) => "Updated",
744 Some(false) => {
746 let backup = settings_path.with_extension(editor.backup_extension());
748 eprintln!(
749 "WARNING: copying `{}` to `{}`",
750 settings_path.file_name().unwrap().to_str().unwrap(),
751 backup.file_name().unwrap().to_str().unwrap(),
752 );
753 fs::copy(&settings_path, &backup)?;
754 "Updated"
755 }
756 _ => "Created",
757 };
758 fs::write(&settings_path, editor.settings_template())?;
759 println!("{verb} `{settings_filename}`");
760 } else {
761 println!("\n{}", editor.settings_template());
762 }
763 Ok(should_create)
764}