Skip to main content

bootstrap/
cli_main.rs

1//! bootstrap, the Rust build system
2//!
3//! This is the entry point for the build system used to compile the `rustc`
4//! compiler. Lots of documentation can be found in the `README.md` file in the
5//! parent directory, and otherwise documentation can be found throughout the `build`
6//! directory in each respective module.
7
8use std::fs::{self, OpenOptions, TryLockError};
9use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write};
10use std::path::Path;
11use std::str::FromStr;
12use std::sync::Once;
13use std::time::Instant;
14use std::{env, process};
15
16use crate::core::builder::StepStack;
17use crate::core::config::flags::Flags;
18use crate::core::config::{ChangeId, Config, Subcommand};
19use crate::utils::change_tracker::{
20    CONFIG_CHANGE_HISTORY, find_recent_config_change_ids, human_readable_changes,
21};
22use crate::utils::helpers::t;
23use crate::{Build, debug};
24
25fn is_tracing_enabled() -> bool {
26    cfg!(feature = "tracing")
27}
28
29pub fn main() {
30    #[cfg(feature = "tracing")]
31    let guard = crate::utils::tracing::setup_tracing("BOOTSTRAP_TRACING");
32
33    let _start_time = Instant::now();
34
35    let default_panic_hook = std::panic::take_hook();
36    std::panic::set_hook(Box::new(move |info| {
37        static BACKTRACE_LOCK: Once = Once::new();
38
39        // Always print backtraces to provide richer errors, to help debug hard-to-reproduce panics
40        // when the user didn't specify RUST_BACKTRACE
41        // Note that we only override this variable in the panic handler, because bootstrap might
42        // manually capture backtraces when a command is executed, and in that case we do not want
43        // to always force backtraces.
44        BACKTRACE_LOCK.call_once(|| {
45            if std::env::var("RUST_BACKTRACE").is_err() {
46                unsafe {
47                    std::env::set_var("RUST_BACKTRACE", "1");
48                }
49            }
50        });
51
52        default_panic_hook(info);
53        StepStack::with_current(|stack| {
54            eprintln!("\nBootstrap has panicked, currently active steps:");
55            for step in stack.get_active_steps() {
56                eprintln!("{} at {}", step.info, step.location);
57            }
58        });
59    }));
60
61    let args = env::args().skip(1).collect::<Vec<_>>();
62
63    if Flags::try_parse_verbose_help(&args) {
64        return;
65    }
66
67    debug!("parsing flags");
68    let flags = Flags::parse(&args);
69    debug!("parsing config based on flags");
70    let config = Config::parse(flags);
71
72    let mut build_lock;
73
74    if !config.bypass_bootstrap_lock {
75        // Display PID of process holding the lock
76        // PID will be stored in a lock file
77        let lock_path = config.out.join("lock");
78        build_lock = t!(fs::OpenOptions::new()
79            .read(true)
80            .write(true)
81            .create(true)
82            .truncate(false)
83            .open(&lock_path));
84        t!(build_lock.try_lock().or_else(|e| {
85            if let TryLockError::Error(e) = e {
86                return Err(e);
87            }
88            let mut pid = String::new();
89            t!(build_lock.read_to_string(&mut pid));
90            // #135972: We can reach this point when the lock has been taken,
91            // but the locker has not yet written its PID to the file
92            if !pid.is_empty() {
93                println!("WARNING: build directory locked by process {pid}, waiting for lock");
94            } else {
95                println!("WARNING: build directory locked, waiting for lock");
96            }
97            build_lock.lock()
98        }));
99        t!(build_lock.set_len(0));
100        t!(build_lock.write_all(process::id().to_string().as_bytes()));
101    }
102
103    // check_version warnings are not printed during setup, or during CI
104    let changelog_suggestion = if matches!(config.cmd, Subcommand::Setup { .. })
105        || config.is_running_on_ci()
106        || config.dry_run()
107    {
108        None
109    } else {
110        check_version(&config)
111    };
112
113    // NOTE: Since `./configure` generates a `bootstrap.toml`, distro maintainers will see the
114    // changelog warning, not the `x.py setup` message.
115    let suggest_setup = config.config.is_none() && !matches!(config.cmd, Subcommand::Setup { .. });
116    if suggest_setup {
117        println!("WARNING: you have not made a `bootstrap.toml`");
118        println!(
119            "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \
120            `cp bootstrap.example.toml bootstrap.toml`"
121        );
122    } else if let Some(suggestion) = &changelog_suggestion {
123        println!("{suggestion}");
124    }
125
126    let pre_commit = config.src.join(".git").join("hooks").join("pre-commit");
127    let dump_bootstrap_shims = config.dump_bootstrap_shims;
128    let out_dir = config.out.clone();
129
130    let tracing_enabled = is_tracing_enabled();
131
132    // Prepare a directory for tracing output
133    // Also store a symlink named "latest" to point to the latest tracing directory.
134    let tracing_dir = out_dir.join("bootstrap-trace").join(std::process::id().to_string());
135    let latest_trace_dir = tracing_dir.parent().unwrap().join("latest");
136    if tracing_enabled {
137        let _ = std::fs::remove_dir_all(&tracing_dir);
138        std::fs::create_dir_all(&tracing_dir).unwrap();
139
140        #[cfg(windows)]
141        let _ = std::fs::remove_dir(&latest_trace_dir);
142        #[cfg(not(windows))]
143        let _ = std::fs::remove_file(&latest_trace_dir);
144
145        #[cfg(not(windows))]
146        fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> {
147            use std::os::unix::fs;
148            fs::symlink(original, link)
149        }
150
151        #[cfg(windows)]
152        fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> {
153            junction::create(target, junction)
154        }
155
156        t!(symlink_dir_inner(&tracing_dir, &latest_trace_dir));
157    }
158
159    debug!("creating new build based on config");
160    let mut build = Build::new(config);
161    build.build();
162
163    if suggest_setup {
164        println!("WARNING: you have not made a `bootstrap.toml`");
165        println!(
166            "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \
167            `cp bootstrap.example.toml bootstrap.toml`"
168        );
169    } else if let Some(suggestion) = &changelog_suggestion {
170        println!("{suggestion}");
171    }
172
173    // Give a warning if the pre-commit script is in pre-commit and not pre-push.
174    // HACK: Since the commit script uses hard links, we can't actually tell if it was installed by x.py setup or not.
175    // We could see if it's identical to src/etc/pre-push.sh, but pre-push may have been modified in the meantime.
176    // Instead, look for this comment, which is almost certainly not in any custom hook.
177    if fs::read_to_string(pre_commit).is_ok_and(|contents| {
178        contents.contains("https://github.com/rust-lang/rust/issues/77620#issuecomment-705144570")
179    }) {
180        println!(
181            "WARNING: You have the pre-push script installed to .git/hooks/pre-commit. \
182                  Consider moving it to .git/hooks/pre-push instead, which runs less often."
183        );
184    }
185
186    if suggest_setup || changelog_suggestion.is_some() {
187        println!("NOTE: this message was printed twice to make it more likely to be seen");
188    }
189
190    if dump_bootstrap_shims {
191        let dump_dir = out_dir.join("bootstrap-shims-dump");
192        assert!(dump_dir.exists());
193
194        for entry in walkdir::WalkDir::new(&dump_dir) {
195            let entry = t!(entry);
196
197            if !entry.file_type().is_file() {
198                continue;
199            }
200
201            let file = t!(fs::File::open(entry.path()));
202
203            // To ensure deterministic results we must sort the dump lines.
204            // This is necessary because the order of rustc invocations different
205            // almost all the time.
206            let mut lines: Vec<String> = t!(BufReader::new(&file).lines().collect());
207            lines.sort_by_key(|t| t.to_lowercase());
208            let mut file = t!(OpenOptions::new().write(true).truncate(true).open(entry.path()));
209            t!(file.write_all(lines.join("\n").as_bytes()));
210        }
211    }
212
213    #[cfg(feature = "tracing")]
214    {
215        build.report_summary(&tracing_dir.join("command-stats.txt"), _start_time);
216        build.report_step_graph(&tracing_dir);
217        guard.copy_to_dir(&tracing_dir);
218        eprintln!("Tracing/profiling output has been written to {}", latest_trace_dir.display());
219    }
220}
221
222fn check_version(config: &Config) -> Option<String> {
223    let mut msg = String::new();
224
225    let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id;
226    let warned_id_path = config.out.join("bootstrap").join(".last-warned-change-id");
227
228    let mut id = match config.change_id {
229        Some(ChangeId::Id(id)) if id == latest_change_id => return None,
230        Some(ChangeId::Ignore) => return None,
231        Some(ChangeId::Id(id)) => id,
232        None => {
233            msg.push_str("WARNING: The `change-id` is missing in the `bootstrap.toml`. This means that you will not be able to track the major changes made to the bootstrap configurations.\n");
234            msg.push_str("NOTE: to silence this warning, ");
235            msg.push_str(&format!(
236                "add `change-id = {latest_change_id}` or `change-id = \"ignore\"` at the top of `bootstrap.toml`"
237            ));
238            return Some(msg);
239        }
240    };
241
242    // Always try to use `change-id` from .last-warned-change-id first. If it doesn't exist,
243    // then use the one from the bootstrap.toml. This way we never show the same warnings
244    // more than once.
245    if let Ok(t) = fs::read_to_string(&warned_id_path) {
246        let last_warned_id = usize::from_str(&t)
247            .unwrap_or_else(|_| panic!("{} is corrupted.", warned_id_path.display()));
248
249        // We only use the last_warned_id if it exists in `CONFIG_CHANGE_HISTORY`.
250        // Otherwise, we may retrieve all the changes if it's not the highest value.
251        // For better understanding, refer to `change_tracker::find_recent_config_change_ids`.
252        if CONFIG_CHANGE_HISTORY.iter().any(|config| config.change_id == last_warned_id) {
253            id = last_warned_id;
254        }
255    };
256
257    let changes = find_recent_config_change_ids(id);
258
259    if changes.is_empty() {
260        return None;
261    }
262
263    msg.push_str("There have been changes to x.py since you last updated:\n");
264    msg.push_str(&human_readable_changes(changes));
265
266    msg.push_str("NOTE: to silence this warning, ");
267    msg.push_str(&format!(
268        "update `bootstrap.toml` to use `change-id = {latest_change_id}` or `change-id = \"ignore\"` instead"
269    ));
270
271    if io::stdout().is_terminal() {
272        t!(fs::write(warned_id_path, latest_change_id.to_string()));
273    }
274
275    Some(msg)
276}