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::utils::change_tracker::CONFIG_CHANGE_HISTORY;
24use crate::utils::exec::command;
25use crate::utils::helpers::{self, hex_encode};
26use crate::{Config, 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 crate::exit!(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.build.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 if result.is_err() {
379 return false;
380 }
381
382 true
383}
384
385pub fn interactive_path() -> io::Result<Profile> {
387 fn abbrev_all() -> impl Iterator<Item = ((String, String), Profile)> {
388 ('a'..)
389 .zip(1..)
390 .map(|(letter, number)| (letter.to_string(), number.to_string()))
391 .zip(Profile::all())
392 }
393
394 fn parse_with_abbrev(input: &str) -> Result<Profile, String> {
395 let input = input.trim().to_lowercase();
396 for ((letter, number), profile) in abbrev_all() {
397 if input == letter || input == number {
398 return Ok(profile);
399 }
400 }
401 input.parse()
402 }
403
404 println!("Welcome to the Rust project! What do you want to do with x.py?");
405 for ((letter, _), profile) in abbrev_all() {
406 println!("{}) {}: {}", letter, profile, profile.purpose());
407 }
408 let template = loop {
409 print!(
410 "Please choose one ({}): ",
411 abbrev_all().map(|((l, _), _)| l).collect::<Vec<_>>().join("/")
412 );
413 io::stdout().flush()?;
414 let mut input = String::new();
415 io::stdin().read_line(&mut input)?;
416 if input.is_empty() {
417 eprintln!("EOF on stdin, when expecting answer to question. Giving up.");
418 crate::exit!(1);
419 }
420 break match parse_with_abbrev(&input) {
421 Ok(profile) => profile,
422 Err(err) => {
423 eprintln!("ERROR: {err}");
424 eprintln!("NOTE: press Ctrl+C to exit");
425 continue;
426 }
427 };
428 };
429 Ok(template)
430}
431
432#[derive(PartialEq)]
433enum PromptResult {
434 Yes, No, Print, }
438
439fn prompt_user(prompt: &str) -> io::Result<Option<PromptResult>> {
441 let mut input = String::new();
442 loop {
443 print!("{prompt} ");
444 io::stdout().flush()?;
445 input.clear();
446 io::stdin().read_line(&mut input)?;
447 match input.trim().to_lowercase().as_str() {
448 "y" | "yes" => return Ok(Some(PromptResult::Yes)),
449 "n" | "no" => return Ok(Some(PromptResult::No)),
450 "p" | "print" => return Ok(Some(PromptResult::Print)),
451 "" => return Ok(None),
452 _ => {
453 eprintln!("ERROR: unrecognized option '{}'", input.trim());
454 eprintln!("NOTE: press Ctrl+C to exit");
455 }
456 };
457 }
458}
459
460#[derive(Clone, Debug, Eq, PartialEq, Hash)]
462pub struct Hook;
463
464impl CommandLineStep for Hook {
465 type Output = ();
466
467 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
468 run.alias("hook")
469 }
470
471 fn is_default_step(_builder: &Builder<'_>) -> bool {
472 true
473 }
474
475 fn make_run(run: RunConfig<'_>) {
476 if let [cmd] = &run.paths[..]
477 && cmd.assert_single_path().path.as_path().as_os_str() == "hook"
478 {
479 run.builder.ensure(Hook);
480 }
481 }
482
483 fn run(self, builder: &Builder<'_>) -> Self::Output {
484 let config = &builder.config;
485
486 if config.dry_run() || !config.rust_info.is_managed_git_subrepository() {
487 return;
488 }
489
490 t!(install_git_hook_maybe(builder, config));
491 }
492}
493
494fn install_git_hook_maybe(builder: &Builder<'_>, config: &Config) -> io::Result<()> {
496 let git = helpers::git(Some(&config.src))
497 .args(["rev-parse", "--git-common-dir"])
498 .run_capture(builder)
499 .stdout();
500 let git = PathBuf::from(git.trim());
501 let hooks_dir = git.join("hooks");
502 let dst = hooks_dir.join("pre-push");
503 if dst.exists() {
504 return Ok(());
506 }
507
508 println!(
509 "\nRust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
510If you'd like, x.py can install a git hook for you that will automatically run `test tidy` before
511pushing your code to ensure your code is up to par. If you decide later that this behavior is
512undesirable, simply delete the `pre-push` file from .git/hooks."
513 );
514
515 if prompt_user("Would you like to install the git hook?: [y/N]")? != Some(PromptResult::Yes) {
516 println!("Ok, skipping installation!");
517 return Ok(());
518 }
519 if !hooks_dir.exists() {
520 let _ = fs::create_dir(hooks_dir);
522 }
523 let src = config.src.join("src").join("etc").join("pre-push.sh");
524 match fs::hard_link(src, &dst) {
525 Err(e) => {
526 eprintln!(
527 "ERROR: could not create hook {}: do you already have the git hook installed?\n{}",
528 dst.display(),
529 e
530 );
531 return Err(e);
532 }
533 Ok(_) => println!("Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`"),
534 };
535 Ok(())
536}
537
538#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
540#[serde(rename_all = "lowercase")]
541enum EditorKind {
542 Emacs,
543 Helix,
544 Vim,
545 VsCode,
546 Zed,
547}
548
549static PARSED_HASHES: LazyLock<BTreeMap<EditorKind, Vec<&'static str>>> = LazyLock::new(|| {
550 const ALL_HASHES: &str = include_str!("setup/hashes.json");
551 let mut map: BTreeMap<_, Vec<_>> = serde_json::from_str(ALL_HASHES).unwrap();
552 map.insert(EditorKind::Vim, map.get(&EditorKind::VsCode).unwrap().clone());
553 map
554});
555
556impl EditorKind {
557 #[cfg(test)]
559 pub const ALL: &[EditorKind] = &[
560 EditorKind::Emacs,
561 EditorKind::Helix,
562 EditorKind::Vim,
563 EditorKind::VsCode,
564 EditorKind::Zed,
565 ];
566
567 fn prompt_user() -> io::Result<Option<EditorKind>> {
568 let prompt_str = "Available editors:
5691. Emacs
5702. Helix
5713. Vim
5724. VS Code
5735. Zed
574
575Select which editor you would like to set up [default: None]: ";
576
577 let mut input = String::new();
578 loop {
579 print!("{prompt_str}");
580 io::stdout().flush()?;
581 io::stdin().read_line(&mut input)?;
582
583 let mut modified_input = input.to_lowercase();
584 modified_input.retain(|ch| !ch.is_whitespace());
585 match modified_input.as_str() {
586 "1" | "emacs" => return Ok(Some(EditorKind::Emacs)),
587 "2" | "helix" => return Ok(Some(EditorKind::Helix)),
588 "3" | "vim" => return Ok(Some(EditorKind::Vim)),
589 "4" | "vscode" => return Ok(Some(EditorKind::VsCode)),
590 "5" | "zed" => return Ok(Some(EditorKind::Zed)),
591 "" | "none" => return Ok(None),
592 _ => {
593 eprintln!("ERROR: unrecognized option '{}'", input.trim());
594 eprintln!("NOTE: press Ctrl+C to exit");
595 }
596 }
597
598 input.clear();
599 }
600 }
601
602 fn hashes(&self) -> &'static [&'static str] {
606 PARSED_HASHES.get(self).unwrap()
607 }
608
609 fn settings_path(&self, config: &Config) -> PathBuf {
610 config.src.join(self.settings_short_path())
611 }
612
613 fn settings_short_path(&self) -> PathBuf {
614 self.settings_folder().join(match self {
615 EditorKind::Emacs => ".dir-locals.el",
616 EditorKind::Helix => "languages.toml",
617 EditorKind::Vim => "coc-settings.json",
618 EditorKind::VsCode | EditorKind::Zed => "settings.json",
619 })
620 }
621
622 fn settings_folder(&self) -> PathBuf {
623 match self {
624 EditorKind::Emacs => PathBuf::new(),
625 EditorKind::Helix => PathBuf::from(".helix"),
626 EditorKind::Vim => PathBuf::from(".vim"),
627 EditorKind::VsCode => PathBuf::from(".vscode"),
628 EditorKind::Zed => PathBuf::from(".zed"),
629 }
630 }
631
632 fn settings_template(&self) -> &'static str {
633 match self {
634 EditorKind::Emacs => include_str!("../../../../etc/rust_analyzer_eglot.el"),
635 EditorKind::Helix => include_str!("../../../../etc/rust_analyzer_helix.toml"),
636 EditorKind::Vim | EditorKind::VsCode => {
637 include_str!("../../../../etc/rust_analyzer_settings.json")
638 }
639 EditorKind::Zed => include_str!("../../../../etc/rust_analyzer_zed.json"),
640 }
641 }
642
643 fn backup_extension(&self) -> String {
644 format!("{}.bak", self.settings_short_path().extension().unwrap().to_str().unwrap())
645 }
646}
647
648#[derive(Clone, Debug, Eq, PartialEq, Hash)]
650pub struct Editor;
651
652impl CommandLineStep for Editor {
653 type Output = ();
654
655 fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
656 run.alias("editor")
657 }
658
659 fn is_default_step(_builder: &Builder<'_>) -> bool {
660 true
661 }
662
663 fn make_run(run: RunConfig<'_>) {
664 if run.builder.config.dry_run() {
665 return;
666 }
667 if let [cmd] = &run.paths[..]
668 && cmd.assert_single_path().path.as_path().as_os_str() == "editor"
669 {
670 run.builder.ensure(Editor);
671 }
672 }
673
674 fn run(self, builder: &Builder<'_>) -> Self::Output {
675 let config = &builder.config;
676 if config.dry_run() {
677 return;
678 }
679 match EditorKind::prompt_user() {
680 Ok(editor_kind) => {
681 if let Some(editor_kind) = editor_kind {
682 while !t!(create_editor_settings_maybe(config, &editor_kind)) {}
683
684 builder.ensure(format::InternalRustfmt);
687 } else {
688 println!("Ok, skipping editor setup!");
689 }
690 }
691 Err(e) => eprintln!("Could not determine the editor: {e}"),
692 }
693 }
694}
695
696fn create_editor_settings_maybe(config: &Config, editor: &EditorKind) -> io::Result<bool> {
699 let hashes = editor.hashes();
700 let (current_hash, historical_hashes) = hashes.split_last().unwrap();
701 let settings_path = editor.settings_path(config);
702 let settings_short_path = editor.settings_short_path();
703 let settings_filename = settings_short_path.to_str().unwrap();
704 let mut mismatched_settings = None;
709 if let Ok(current) = fs::read_to_string(&settings_path) {
710 let mut hasher = sha2::Sha256::new();
711 hasher.update(¤t);
712 let hash = hex_encode(hasher.finalize().as_slice());
713 if hash == *current_hash {
714 return Ok(true);
715 } else if historical_hashes.contains(&hash.as_str()) {
716 mismatched_settings = Some(true);
717 } else {
718 mismatched_settings = Some(false);
719 }
720 }
721 println!(
722 "\nx.py can automatically install the recommended `{settings_filename}` file for rustc development"
723 );
724
725 match mismatched_settings {
726 Some(true) => {
727 eprintln!("WARNING: existing `{settings_filename}` is out of date, x.py will update it")
728 }
729 Some(false) => eprintln!(
730 "WARNING: existing `{settings_filename}` has been modified by user, x.py will back it up and replace it"
731 ),
732 _ => (),
733 }
734 let should_create = match prompt_user(&format!(
735 "Would you like to create/update `{settings_filename}`? (Press 'p' to preview values): [y/N]"
736 ))? {
737 Some(PromptResult::Yes) => true,
738 Some(PromptResult::Print) => false,
739 _ => {
740 println!("Ok, skipping settings!");
741 return Ok(true);
742 }
743 };
744 if should_create {
745 let settings_folder_path = config.src.join(editor.settings_folder());
746 if !settings_folder_path.exists() {
747 fs::create_dir(settings_folder_path)?;
748 }
749 let verb = match mismatched_settings {
750 Some(true) => "Updated",
752 Some(false) => {
754 let backup = settings_path.with_extension(editor.backup_extension());
756 eprintln!(
757 "WARNING: copying `{}` to `{}`",
758 settings_path.file_name().unwrap().to_str().unwrap(),
759 backup.file_name().unwrap().to_str().unwrap(),
760 );
761 fs::copy(&settings_path, &backup)?;
762 "Updated"
763 }
764 _ => "Created",
765 };
766 fs::write(&settings_path, editor.settings_template())?;
767 println!("{verb} `{settings_filename}`");
768 } else {
769 println!("\n{}", editor.settings_template());
770 }
771 Ok(should_create)
772}