diff options
Diffstat (limited to 'src/bootstrap/setup.rs')
-rw-r--r-- | src/bootstrap/setup.rs | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/bootstrap/setup.rs b/src/bootstrap/setup.rs new file mode 100644 index 000000000..a5a39a5a3 --- /dev/null +++ b/src/bootstrap/setup.rs @@ -0,0 +1,350 @@ +use crate::{t, VERSION}; +use crate::{Config, TargetSelection}; +use std::env::consts::EXE_SUFFIX; +use std::fmt::Write as _; +use std::fs::File; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; +use std::process::Command; +use std::str::FromStr; +use std::{ + env, fmt, fs, + io::{self, Write}, +}; + +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Profile { + Compiler, + Codegen, + Library, + Tools, + User, +} + +impl Profile { + fn include_path(&self, src_path: &Path) -> PathBuf { + PathBuf::from(format!("{}/src/bootstrap/defaults/config.{}.toml", src_path.display(), self)) + } + + pub fn all() -> impl Iterator<Item = Self> { + use Profile::*; + // N.B. these are ordered by how they are displayed, not alphabetically + [Library, Compiler, Codegen, Tools, User].iter().copied() + } + + pub fn purpose(&self) -> String { + use Profile::*; + match self { + Library => "Contribute to the standard library", + Compiler => "Contribute to the compiler itself", + Codegen => "Contribute to the compiler, and also modify LLVM or codegen", + Tools => "Contribute to tools which depend on the compiler, but do not modify it directly (e.g. rustdoc, clippy, miri)", + User => "Install Rust from source", + } + .to_string() + } + + pub fn all_for_help(indent: &str) -> String { + let mut out = String::new(); + for choice in Profile::all() { + writeln!(&mut out, "{}{}: {}", indent, choice, choice.purpose()).unwrap(); + } + out + } +} + +impl FromStr for Profile { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "lib" | "library" => Ok(Profile::Library), + "compiler" => Ok(Profile::Compiler), + "llvm" | "codegen" => Ok(Profile::Codegen), + "maintainer" | "user" => Ok(Profile::User), + "tools" | "tool" | "rustdoc" | "clippy" | "miri" | "rustfmt" | "rls" => { + Ok(Profile::Tools) + } + _ => Err(format!("unknown profile: '{}'", s)), + } + } +} + +impl fmt::Display for Profile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Profile::Compiler => write!(f, "compiler"), + Profile::Codegen => write!(f, "codegen"), + Profile::Library => write!(f, "library"), + Profile::User => write!(f, "user"), + Profile::Tools => write!(f, "tools"), + } + } +} + +pub fn setup(config: &Config, profile: Profile) { + let path = &config.config; + + if path.exists() { + eprintln!( + "error: you asked `x.py` to setup a new config file, but one already exists at `{}`", + path.display() + ); + eprintln!("help: try adding `profile = \"{}\"` at the top of {}", profile, path.display()); + eprintln!( + "note: this will use the configuration in {}", + profile.include_path(&config.src).display() + ); + crate::detail_exit(1); + } + + let settings = format!( + "# Includes one of the default files in src/bootstrap/defaults\n\ + profile = \"{}\"\n\ + changelog-seen = {}\n", + profile, VERSION + ); + t!(fs::write(path, settings)); + + let include_path = profile.include_path(&config.src); + println!("`x.py` will now use the configuration at {}", include_path.display()); + + let build = TargetSelection::from_user(&env!("BUILD_TRIPLE")); + let stage_path = + ["build", build.rustc_target_arg(), "stage1"].join(&MAIN_SEPARATOR.to_string()); + + println!(); + + if !rustup_installed() && profile != Profile::User { + eprintln!("`rustup` is not installed; cannot link `stage1` toolchain"); + } else if stage_dir_exists(&stage_path[..]) { + attempt_toolchain_link(&stage_path[..]); + } + + let suggestions = match profile { + Profile::Codegen | Profile::Compiler => &["check", "build", "test"][..], + Profile::Tools => &[ + "check", + "build", + "test src/test/rustdoc*", + "test src/tools/clippy", + "test src/tools/miri", + "test src/tools/rustfmt", + ], + Profile::Library => &["check", "build", "test library/std", "doc"], + Profile::User => &["dist", "build"], + }; + + println!(); + + t!(install_git_hook_maybe(&config)); + + println!(); + + println!("To get started, try one of the following commands:"); + for cmd in suggestions { + println!("- `x.py {}`", cmd); + } + + if profile != Profile::User { + println!( + "For more suggestions, see https://rustc-dev-guide.rust-lang.org/building/suggested.html" + ); + } +} + +fn rustup_installed() -> bool { + Command::new("rustup") + .arg("--version") + .stdout(std::process::Stdio::null()) + .output() + .map_or(false, |output| output.status.success()) +} + +fn stage_dir_exists(stage_path: &str) -> bool { + match fs::create_dir(&stage_path) { + Ok(_) => true, + Err(_) => Path::new(&stage_path).exists(), + } +} + +fn attempt_toolchain_link(stage_path: &str) { + if toolchain_is_linked() { + return; + } + + if !ensure_stage1_toolchain_placeholder_exists(stage_path) { + eprintln!( + "Failed to create a template for stage 1 toolchain or confirm that it already exists" + ); + return; + } + + if try_link_toolchain(&stage_path) { + println!( + "Added `stage1` rustup toolchain; try `cargo +stage1 build` on a separate rust project to run a newly-built toolchain" + ); + } else { + eprintln!("`rustup` failed to link stage 1 build to `stage1` toolchain"); + eprintln!( + "To manually link stage 1 build to `stage1` toolchain, run:\n + `rustup toolchain link stage1 {}`", + &stage_path + ); + } +} + +fn toolchain_is_linked() -> bool { + match Command::new("rustup") + .args(&["toolchain", "list"]) + .stdout(std::process::Stdio::piped()) + .output() + { + Ok(toolchain_list) => { + if !String::from_utf8_lossy(&toolchain_list.stdout).contains("stage1") { + return false; + } + // The toolchain has already been linked. + println!( + "`stage1` toolchain already linked; not attempting to link `stage1` toolchain" + ); + } + Err(_) => { + // In this case, we don't know if the `stage1` toolchain has been linked; + // but `rustup` failed, so let's not go any further. + println!( + "`rustup` failed to list current toolchains; not attempting to link `stage1` toolchain" + ); + } + } + true +} + +fn try_link_toolchain(stage_path: &str) -> bool { + Command::new("rustup") + .stdout(std::process::Stdio::null()) + .args(&["toolchain", "link", "stage1", &stage_path]) + .output() + .map_or(false, |output| output.status.success()) +} + +fn ensure_stage1_toolchain_placeholder_exists(stage_path: &str) -> bool { + let pathbuf = PathBuf::from(stage_path); + + if fs::create_dir_all(pathbuf.join("lib")).is_err() { + return false; + }; + + let pathbuf = pathbuf.join("bin"); + if fs::create_dir_all(&pathbuf).is_err() { + return false; + }; + + let pathbuf = pathbuf.join(format!("rustc{}", EXE_SUFFIX)); + + if pathbuf.exists() { + return true; + } + + // Take care not to overwrite the file + let result = File::options().append(true).create(true).open(&pathbuf); + if result.is_err() { + return false; + } + + return true; +} + +// Used to get the path for `Subcommand::Setup` +pub fn interactive_path() -> io::Result<Profile> { + fn abbrev_all() -> impl Iterator<Item = ((String, String), Profile)> { + ('a'..) + .zip(1..) + .map(|(letter, number)| (letter.to_string(), number.to_string())) + .zip(Profile::all()) + } + + fn parse_with_abbrev(input: &str) -> Result<Profile, String> { + let input = input.trim().to_lowercase(); + for ((letter, number), profile) in abbrev_all() { + if input == letter || input == number { + return Ok(profile); + } + } + input.parse() + } + + println!("Welcome to the Rust project! What do you want to do with x.py?"); + for ((letter, _), profile) in abbrev_all() { + println!("{}) {}: {}", letter, profile, profile.purpose()); + } + let template = loop { + print!( + "Please choose one ({}): ", + abbrev_all().map(|((l, _), _)| l).collect::<Vec<_>>().join("/") + ); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + if input.is_empty() { + eprintln!("EOF on stdin, when expecting answer to question. Giving up."); + crate::detail_exit(1); + } + break match parse_with_abbrev(&input) { + Ok(profile) => profile, + Err(err) => { + eprintln!("error: {}", err); + eprintln!("note: press Ctrl+C to exit"); + continue; + } + }; + }; + Ok(template) +} + +// install a git hook to automatically run tidy --bless, if they want +fn install_git_hook_maybe(config: &Config) -> io::Result<()> { + let mut input = String::new(); + println!( + "Rust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality. +If you'd like, x.py can install a git hook for you that will automatically run `tidy --bless` before +pushing your code to ensure your code is up to par. If you decide later that this behavior is +undesirable, simply delete the `pre-push` file from .git/hooks." + ); + + let should_install = loop { + print!("Would you like to install the git hook?: [y/N] "); + io::stdout().flush()?; + input.clear(); + io::stdin().read_line(&mut input)?; + break match input.trim().to_lowercase().as_str() { + "y" | "yes" => true, + "n" | "no" | "" => false, + _ => { + eprintln!("error: unrecognized option '{}'", input.trim()); + eprintln!("note: press Ctrl+C to exit"); + continue; + } + }; + }; + + if should_install { + let src = config.src.join("src").join("etc").join("pre-push.sh"); + let git = + t!(config.git().args(&["rev-parse", "--git-common-dir"]).output().map(|output| { + assert!(output.status.success(), "failed to run `git`"); + PathBuf::from(t!(String::from_utf8(output.stdout)).trim()) + })); + let dst = git.join("hooks").join("pre-push"); + match fs::hard_link(src, &dst) { + Err(e) => eprintln!( + "error: could not create hook {}: do you already have the git hook installed?\n{}", + dst.display(), + e + ), + Ok(_) => println!("Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`"), + }; + } else { + println!("Ok, skipping installation!"); + } + Ok(()) +} |