]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/setup.rs
Rollup merge of #89422 - GuillaumeGomez:doctest-whitespace-name, r=CraftSpider
[rust.git] / src / bootstrap / setup.rs
1 use crate::TargetSelection;
2 use crate::{t, VERSION};
3 use std::fmt::Write as _;
4 use std::path::{Path, PathBuf};
5 use std::process::Command;
6 use std::str::FromStr;
7 use std::{
8     env, fmt, fs,
9     io::{self, Write},
10 };
11
12 #[derive(Clone, Copy, Eq, PartialEq)]
13 pub enum Profile {
14     Compiler,
15     Codegen,
16     Library,
17     Tools,
18     User,
19 }
20
21 impl Profile {
22     fn include_path(&self, src_path: &Path) -> PathBuf {
23         PathBuf::from(format!("{}/src/bootstrap/defaults/config.{}.toml", src_path.display(), self))
24     }
25
26     pub fn all() -> impl Iterator<Item = Self> {
27         use Profile::*;
28         // N.B. these are ordered by how they are displayed, not alphabetically
29         [Library, Compiler, Codegen, Tools, User].iter().copied()
30     }
31
32     pub fn purpose(&self) -> String {
33         use Profile::*;
34         match self {
35             Library => "Contribute to the standard library",
36             Compiler => "Contribute to the compiler itself",
37             Codegen => "Contribute to the compiler, and also modify LLVM or codegen",
38             Tools => "Contribute to tools which depend on the compiler, but do not modify it directly (e.g. rustdoc, clippy, miri)",
39             User => "Install Rust from source",
40         }
41         .to_string()
42     }
43
44     pub fn all_for_help(indent: &str) -> String {
45         let mut out = String::new();
46         for choice in Profile::all() {
47             writeln!(&mut out, "{}{}: {}", indent, choice, choice.purpose()).unwrap();
48         }
49         out
50     }
51 }
52
53 impl FromStr for Profile {
54     type Err = String;
55
56     fn from_str(s: &str) -> Result<Self, Self::Err> {
57         match s {
58             "lib" | "library" => Ok(Profile::Library),
59             "compiler" => Ok(Profile::Compiler),
60             "llvm" | "codegen" => Ok(Profile::Codegen),
61             "maintainer" | "user" => Ok(Profile::User),
62             "tools" | "tool" | "rustdoc" | "clippy" | "miri" | "rustfmt" | "rls" => {
63                 Ok(Profile::Tools)
64             }
65             _ => Err(format!("unknown profile: '{}'", s)),
66         }
67     }
68 }
69
70 impl fmt::Display for Profile {
71     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72         match self {
73             Profile::Compiler => write!(f, "compiler"),
74             Profile::Codegen => write!(f, "codegen"),
75             Profile::Library => write!(f, "library"),
76             Profile::User => write!(f, "user"),
77             Profile::Tools => write!(f, "tools"),
78         }
79     }
80 }
81
82 pub fn setup(src_path: &Path, profile: Profile) {
83     let cfg_file = env::var_os("BOOTSTRAP_CONFIG").map(PathBuf::from);
84
85     if cfg_file.as_ref().map_or(false, |f| f.exists()) {
86         let file = cfg_file.unwrap();
87         println!(
88             "error: you asked `x.py` to setup a new config file, but one already exists at `{}`",
89             file.display()
90         );
91         println!("help: try adding `profile = \"{}\"` at the top of {}", profile, file.display());
92         println!(
93             "note: this will use the configuration in {}",
94             profile.include_path(src_path).display()
95         );
96         std::process::exit(1);
97     }
98
99     let path = cfg_file.unwrap_or_else(|| "config.toml".into());
100     let settings = format!(
101         "# Includes one of the default files in src/bootstrap/defaults\n\
102     profile = \"{}\"\n\
103     changelog-seen = {}\n",
104         profile, VERSION
105     );
106     t!(fs::write(path, settings));
107
108     let include_path = profile.include_path(src_path);
109     println!("`x.py` will now use the configuration at {}", include_path.display());
110
111     let build = TargetSelection::from_user(&env!("BUILD_TRIPLE"));
112     let stage_path = ["build", build.rustc_target_arg(), "stage1"].join("/");
113
114     println!();
115
116     if !rustup_installed() && profile != Profile::User {
117         println!("`rustup` is not installed; cannot link `stage1` toolchain");
118     } else if stage_dir_exists(&stage_path[..]) {
119         attempt_toolchain_link(&stage_path[..]);
120     }
121
122     let suggestions = match profile {
123         Profile::Codegen | Profile::Compiler => &["check", "build", "test"][..],
124         Profile::Tools => &[
125             "check",
126             "build",
127             "test src/test/rustdoc*",
128             "test src/tools/clippy",
129             "test src/tools/miri",
130             "test src/tools/rustfmt",
131         ],
132         Profile::Library => &["check", "build", "test library/std", "doc"],
133         Profile::User => &["dist", "build"],
134     };
135
136     println!();
137
138     t!(install_git_hook_maybe(src_path));
139
140     println!();
141
142     println!("To get started, try one of the following commands:");
143     for cmd in suggestions {
144         println!("- `x.py {}`", cmd);
145     }
146
147     if profile != Profile::User {
148         println!(
149             "For more suggestions, see https://rustc-dev-guide.rust-lang.org/building/suggested.html"
150         );
151     }
152 }
153
154 fn rustup_installed() -> bool {
155     Command::new("rustup")
156         .arg("--version")
157         .stdout(std::process::Stdio::null())
158         .output()
159         .map_or(false, |output| output.status.success())
160 }
161
162 fn stage_dir_exists(stage_path: &str) -> bool {
163     match fs::create_dir(&stage_path[..]) {
164         Ok(_) => true,
165         Err(_) => Path::new(&stage_path[..]).exists(),
166     }
167 }
168
169 fn attempt_toolchain_link(stage_path: &str) {
170     if toolchain_is_linked() {
171         return;
172     }
173
174     if try_link_toolchain(&stage_path[..]) {
175         println!(
176             "Added `stage1` rustup toolchain; try `cargo +stage1 build` on a separate rust project to run a newly-built toolchain"
177         );
178     } else {
179         println!("`rustup` failed to link stage 1 build to `stage1` toolchain");
180         println!(
181             "To manually link stage 1 build to `stage1` toolchain, run:\n
182             `rustup toolchain link stage1 {}`",
183             &stage_path[..]
184         );
185     }
186 }
187
188 fn toolchain_is_linked() -> bool {
189     match Command::new("rustup")
190         .args(&["toolchain", "list"])
191         .stdout(std::process::Stdio::piped())
192         .output()
193     {
194         Ok(toolchain_list) => {
195             if !String::from_utf8_lossy(&toolchain_list.stdout).contains("stage1") {
196                 return false;
197             }
198             // The toolchain has already been linked.
199             println!(
200                 "`stage1` toolchain already linked; not attempting to link `stage1` toolchain"
201             );
202         }
203         Err(_) => {
204             // In this case, we don't know if the `stage1` toolchain has been linked;
205             // but `rustup` failed, so let's not go any further.
206             println!(
207                 "`rustup` failed to list current toolchains; not attempting to link `stage1` toolchain"
208             );
209         }
210     }
211     true
212 }
213
214 fn try_link_toolchain(stage_path: &str) -> bool {
215     Command::new("rustup")
216         .stdout(std::process::Stdio::null())
217         .args(&["toolchain", "link", "stage1", &stage_path[..]])
218         .output()
219         .map_or(false, |output| output.status.success())
220 }
221
222 // Used to get the path for `Subcommand::Setup`
223 pub fn interactive_path() -> io::Result<Profile> {
224     fn abbrev_all() -> impl Iterator<Item = ((String, String), Profile)> {
225         ('a'..)
226             .zip(1..)
227             .map(|(letter, number)| (letter.to_string(), number.to_string()))
228             .zip(Profile::all())
229     }
230
231     fn parse_with_abbrev(input: &str) -> Result<Profile, String> {
232         let input = input.trim().to_lowercase();
233         for ((letter, number), profile) in abbrev_all() {
234             if input == letter || input == number {
235                 return Ok(profile);
236             }
237         }
238         input.parse()
239     }
240
241     println!("Welcome to the Rust project! What do you want to do with x.py?");
242     for ((letter, _), profile) in abbrev_all() {
243         println!("{}) {}: {}", letter, profile, profile.purpose());
244     }
245     let template = loop {
246         print!(
247             "Please choose one ({}): ",
248             abbrev_all().map(|((l, _), _)| l).collect::<Vec<_>>().join("/")
249         );
250         io::stdout().flush()?;
251         let mut input = String::new();
252         io::stdin().read_line(&mut input)?;
253         if input.is_empty() {
254             eprintln!("EOF on stdin, when expecting answer to question.  Giving up.");
255             std::process::exit(1);
256         }
257         break match parse_with_abbrev(&input) {
258             Ok(profile) => profile,
259             Err(err) => {
260                 println!("error: {}", err);
261                 println!("note: press Ctrl+C to exit");
262                 continue;
263             }
264         };
265     };
266     Ok(template)
267 }
268
269 // install a git hook to automatically run tidy --bless, if they want
270 fn install_git_hook_maybe(src_path: &Path) -> io::Result<()> {
271     let mut input = String::new();
272     println!(
273         "Rust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality.
274 If you'd like, x.py can install a git hook for you that will automatically run `tidy --bless` on each commit
275 to ensure your code is up to par. If you decide later that this behavior is undesirable,
276 simply delete the `pre-commit` file from .git/hooks."
277     );
278
279     let should_install = loop {
280         print!("Would you like to install the git hook?: [y/N] ");
281         io::stdout().flush()?;
282         input.clear();
283         io::stdin().read_line(&mut input)?;
284         break match input.trim().to_lowercase().as_str() {
285             "y" | "yes" => true,
286             "n" | "no" | "" => false,
287             _ => {
288                 println!("error: unrecognized option '{}'", input.trim());
289                 println!("note: press Ctrl+C to exit");
290                 continue;
291             }
292         };
293     };
294
295     if should_install {
296         let src = src_path.join("src").join("etc").join("pre-commit.sh");
297         let git = t!(Command::new("git").args(&["rev-parse", "--git-common-dir"]).output().map(
298             |output| {
299                 assert!(output.status.success(), "failed to run `git`");
300                 PathBuf::from(t!(String::from_utf8(output.stdout)).trim())
301             }
302         ));
303         let dst = git.join("hooks").join("pre-commit");
304         match fs::hard_link(src, &dst) {
305             Err(e) => println!(
306                 "error: could not create hook {}: do you already have the git hook installed?\n{}",
307                 dst.display(),
308                 e
309             ),
310             Ok(_) => println!("Linked `src/etc/pre-commit.sh` to `.git/hooks/pre-commit`"),
311         };
312     } else {
313         println!("Ok, skipping installation!");
314     }
315     Ok(())
316 }