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