diff --git a/src/bootstrap/src/bin/main.rs b/src/bootstrap/src/bin/main.rs index 1310255de3ca4..171ef1811ca40 100644 --- a/src/bootstrap/src/bin/main.rs +++ b/src/bootstrap/src/bin/main.rs @@ -1,272 +1,8 @@ -//! bootstrap, the Rust build system +//! The `main.rs` for bootstrap is a small stub that delegates to the real +//! entry point within the bootstrap library crate. //! -//! This is the entry point for the build system used to compile the `rustc` -//! compiler. Lots of documentation can be found in the `README.md` file in the -//! parent directory, and otherwise documentation can be found throughout the `build` -//! directory in each respective module. - -use std::fs::{self, OpenOptions, TryLockError}; -use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write}; -use std::path::Path; -use std::str::FromStr; -use std::sync::Once; -use std::time::Instant; -use std::{env, process}; - -use bootstrap::{ - Build, CONFIG_CHANGE_HISTORY, ChangeId, Config, Flags, StepStack, Subcommand, debug, - find_recent_config_change_ids, human_readable_changes, t, -}; - -fn is_tracing_enabled() -> bool { - cfg!(feature = "tracing") -} +//! Don't add more code here! Add it to the inner main instead. fn main() { - #[cfg(feature = "tracing")] - let guard = bootstrap::setup_tracing("BOOTSTRAP_TRACING"); - - let _start_time = Instant::now(); - - let default_panic_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - static BACKTRACE_LOCK: Once = Once::new(); - - // Always print backtraces to provide richer errors, to help debug hard-to-reproduce panics - // when the user didn't specify RUST_BACKTRACE - // Note that we only override this variable in the panic handler, because bootstrap might - // manually capture backtraces when a command is executed, and in that case we do not want - // to always force backtraces. - BACKTRACE_LOCK.call_once(|| { - if std::env::var("RUST_BACKTRACE").is_err() { - unsafe { - std::env::set_var("RUST_BACKTRACE", "1"); - } - } - }); - - default_panic_hook(info); - StepStack::with_current(|stack| { - eprintln!("\nBootstrap has panicked, currently active steps:"); - for step in stack.get_active_steps() { - eprintln!("{} at {}", step.info, step.location); - } - }); - })); - - let args = env::args().skip(1).collect::>(); - - if Flags::try_parse_verbose_help(&args) { - return; - } - - debug!("parsing flags"); - let flags = Flags::parse(&args); - debug!("parsing config based on flags"); - let config = Config::parse(flags); - - let mut build_lock; - - if !config.bypass_bootstrap_lock { - // Display PID of process holding the lock - // PID will be stored in a lock file - let lock_path = config.out.join("lock"); - build_lock = t!(fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(&lock_path)); - t!(build_lock.try_lock().or_else(|e| { - if let TryLockError::Error(e) = e { - return Err(e); - } - let mut pid = String::new(); - t!(build_lock.read_to_string(&mut pid)); - // #135972: We can reach this point when the lock has been taken, - // but the locker has not yet written its PID to the file - if !pid.is_empty() { - println!("WARNING: build directory locked by process {pid}, waiting for lock"); - } else { - println!("WARNING: build directory locked, waiting for lock"); - } - build_lock.lock() - })); - t!(build_lock.set_len(0)); - t!(build_lock.write_all(process::id().to_string().as_bytes())); - } - - // check_version warnings are not printed during setup, or during CI - let changelog_suggestion = if matches!(config.cmd, Subcommand::Setup { .. }) - || config.is_running_on_ci() - || config.dry_run() - { - None - } else { - check_version(&config) - }; - - // NOTE: Since `./configure` generates a `bootstrap.toml`, distro maintainers will see the - // changelog warning, not the `x.py setup` message. - let suggest_setup = config.config.is_none() && !matches!(config.cmd, Subcommand::Setup { .. }); - if suggest_setup { - println!("WARNING: you have not made a `bootstrap.toml`"); - println!( - "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \ - `cp bootstrap.example.toml bootstrap.toml`" - ); - } else if let Some(suggestion) = &changelog_suggestion { - println!("{suggestion}"); - } - - let pre_commit = config.src.join(".git").join("hooks").join("pre-commit"); - let dump_bootstrap_shims = config.dump_bootstrap_shims; - let out_dir = config.out.clone(); - - let tracing_enabled = is_tracing_enabled(); - - // Prepare a directory for tracing output - // Also store a symlink named "latest" to point to the latest tracing directory. - let tracing_dir = out_dir.join("bootstrap-trace").join(std::process::id().to_string()); - let latest_trace_dir = tracing_dir.parent().unwrap().join("latest"); - if tracing_enabled { - let _ = std::fs::remove_dir_all(&tracing_dir); - std::fs::create_dir_all(&tracing_dir).unwrap(); - - #[cfg(windows)] - let _ = std::fs::remove_dir(&latest_trace_dir); - #[cfg(not(windows))] - let _ = std::fs::remove_file(&latest_trace_dir); - - #[cfg(not(windows))] - fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> { - use std::os::unix::fs; - fs::symlink(original, link) - } - - #[cfg(windows)] - fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> { - junction::create(target, junction) - } - - t!(symlink_dir_inner(&tracing_dir, &latest_trace_dir)); - } - - debug!("creating new build based on config"); - let mut build = Build::new(config); - build.build(); - - if suggest_setup { - println!("WARNING: you have not made a `bootstrap.toml`"); - println!( - "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \ - `cp bootstrap.example.toml bootstrap.toml`" - ); - } else if let Some(suggestion) = &changelog_suggestion { - println!("{suggestion}"); - } - - // Give a warning if the pre-commit script is in pre-commit and not pre-push. - // HACK: Since the commit script uses hard links, we can't actually tell if it was installed by x.py setup or not. - // We could see if it's identical to src/etc/pre-push.sh, but pre-push may have been modified in the meantime. - // Instead, look for this comment, which is almost certainly not in any custom hook. - if fs::read_to_string(pre_commit).is_ok_and(|contents| { - contents.contains("https://github.com/rust-lang/rust/issues/77620#issuecomment-705144570") - }) { - println!( - "WARNING: You have the pre-push script installed to .git/hooks/pre-commit. \ - Consider moving it to .git/hooks/pre-push instead, which runs less often." - ); - } - - if suggest_setup || changelog_suggestion.is_some() { - println!("NOTE: this message was printed twice to make it more likely to be seen"); - } - - if dump_bootstrap_shims { - let dump_dir = out_dir.join("bootstrap-shims-dump"); - assert!(dump_dir.exists()); - - for entry in walkdir::WalkDir::new(&dump_dir) { - let entry = t!(entry); - - if !entry.file_type().is_file() { - continue; - } - - let file = t!(fs::File::open(entry.path())); - - // To ensure deterministic results we must sort the dump lines. - // This is necessary because the order of rustc invocations different - // almost all the time. - let mut lines: Vec = t!(BufReader::new(&file).lines().collect()); - lines.sort_by_key(|t| t.to_lowercase()); - let mut file = t!(OpenOptions::new().write(true).truncate(true).open(entry.path())); - t!(file.write_all(lines.join("\n").as_bytes())); - } - } - - #[cfg(feature = "tracing")] - { - build.report_summary(&tracing_dir.join("command-stats.txt"), _start_time); - build.report_step_graph(&tracing_dir); - guard.copy_to_dir(&tracing_dir); - eprintln!("Tracing/profiling output has been written to {}", latest_trace_dir.display()); - } -} - -fn check_version(config: &Config) -> Option { - let mut msg = String::new(); - - let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id; - let warned_id_path = config.out.join("bootstrap").join(".last-warned-change-id"); - - let mut id = match config.change_id { - Some(ChangeId::Id(id)) if id == latest_change_id => return None, - Some(ChangeId::Ignore) => return None, - Some(ChangeId::Id(id)) => id, - None => { - 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"); - msg.push_str("NOTE: to silence this warning, "); - msg.push_str(&format!( - "add `change-id = {latest_change_id}` or `change-id = \"ignore\"` at the top of `bootstrap.toml`" - )); - return Some(msg); - } - }; - - // Always try to use `change-id` from .last-warned-change-id first. If it doesn't exist, - // then use the one from the bootstrap.toml. This way we never show the same warnings - // more than once. - if let Ok(t) = fs::read_to_string(&warned_id_path) { - let last_warned_id = usize::from_str(&t) - .unwrap_or_else(|_| panic!("{} is corrupted.", warned_id_path.display())); - - // We only use the last_warned_id if it exists in `CONFIG_CHANGE_HISTORY`. - // Otherwise, we may retrieve all the changes if it's not the highest value. - // For better understanding, refer to `change_tracker::find_recent_config_change_ids`. - if CONFIG_CHANGE_HISTORY.iter().any(|config| config.change_id == last_warned_id) { - id = last_warned_id; - } - }; - - let changes = find_recent_config_change_ids(id); - - if changes.is_empty() { - return None; - } - - msg.push_str("There have been changes to x.py since you last updated:\n"); - msg.push_str(&human_readable_changes(changes)); - - msg.push_str("NOTE: to silence this warning, "); - msg.push_str(&format!( - "update `bootstrap.toml` to use `change-id = {latest_change_id}` or `change-id = \"ignore\"` instead" - )); - - if io::stdout().is_terminal() { - t!(fs::write(warned_id_path, latest_change_id.to_string())); - } - - Some(msg) + bootstrap::cli_main::main(); } diff --git a/src/bootstrap/src/cli_main.rs b/src/bootstrap/src/cli_main.rs new file mode 100644 index 0000000000000..879cf7159fb6b --- /dev/null +++ b/src/bootstrap/src/cli_main.rs @@ -0,0 +1,272 @@ +//! bootstrap, the Rust build system +//! +//! This is the entry point for the build system used to compile the `rustc` +//! compiler. Lots of documentation can be found in the `README.md` file in the +//! parent directory, and otherwise documentation can be found throughout the `build` +//! directory in each respective module. + +use std::fs::{self, OpenOptions, TryLockError}; +use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write}; +use std::path::Path; +use std::str::FromStr; +use std::sync::Once; +use std::time::Instant; +use std::{env, process}; + +use crate::{ + Build, CONFIG_CHANGE_HISTORY, ChangeId, Config, Flags, StepStack, Subcommand, debug, + find_recent_config_change_ids, human_readable_changes, t, +}; + +fn is_tracing_enabled() -> bool { + cfg!(feature = "tracing") +} + +pub fn main() { + #[cfg(feature = "tracing")] + let guard = crate::utils::tracing::setup_tracing("BOOTSTRAP_TRACING"); + + let _start_time = Instant::now(); + + let default_panic_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + static BACKTRACE_LOCK: Once = Once::new(); + + // Always print backtraces to provide richer errors, to help debug hard-to-reproduce panics + // when the user didn't specify RUST_BACKTRACE + // Note that we only override this variable in the panic handler, because bootstrap might + // manually capture backtraces when a command is executed, and in that case we do not want + // to always force backtraces. + BACKTRACE_LOCK.call_once(|| { + if std::env::var("RUST_BACKTRACE").is_err() { + unsafe { + std::env::set_var("RUST_BACKTRACE", "1"); + } + } + }); + + default_panic_hook(info); + StepStack::with_current(|stack| { + eprintln!("\nBootstrap has panicked, currently active steps:"); + for step in stack.get_active_steps() { + eprintln!("{} at {}", step.info, step.location); + } + }); + })); + + let args = env::args().skip(1).collect::>(); + + if Flags::try_parse_verbose_help(&args) { + return; + } + + debug!("parsing flags"); + let flags = Flags::parse(&args); + debug!("parsing config based on flags"); + let config = Config::parse(flags); + + let mut build_lock; + + if !config.bypass_bootstrap_lock { + // Display PID of process holding the lock + // PID will be stored in a lock file + let lock_path = config.out.join("lock"); + build_lock = t!(fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&lock_path)); + t!(build_lock.try_lock().or_else(|e| { + if let TryLockError::Error(e) = e { + return Err(e); + } + let mut pid = String::new(); + t!(build_lock.read_to_string(&mut pid)); + // #135972: We can reach this point when the lock has been taken, + // but the locker has not yet written its PID to the file + if !pid.is_empty() { + println!("WARNING: build directory locked by process {pid}, waiting for lock"); + } else { + println!("WARNING: build directory locked, waiting for lock"); + } + build_lock.lock() + })); + t!(build_lock.set_len(0)); + t!(build_lock.write_all(process::id().to_string().as_bytes())); + } + + // check_version warnings are not printed during setup, or during CI + let changelog_suggestion = if matches!(config.cmd, Subcommand::Setup { .. }) + || config.is_running_on_ci() + || config.dry_run() + { + None + } else { + check_version(&config) + }; + + // NOTE: Since `./configure` generates a `bootstrap.toml`, distro maintainers will see the + // changelog warning, not the `x.py setup` message. + let suggest_setup = config.config.is_none() && !matches!(config.cmd, Subcommand::Setup { .. }); + if suggest_setup { + println!("WARNING: you have not made a `bootstrap.toml`"); + println!( + "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \ + `cp bootstrap.example.toml bootstrap.toml`" + ); + } else if let Some(suggestion) = &changelog_suggestion { + println!("{suggestion}"); + } + + let pre_commit = config.src.join(".git").join("hooks").join("pre-commit"); + let dump_bootstrap_shims = config.dump_bootstrap_shims; + let out_dir = config.out.clone(); + + let tracing_enabled = is_tracing_enabled(); + + // Prepare a directory for tracing output + // Also store a symlink named "latest" to point to the latest tracing directory. + let tracing_dir = out_dir.join("bootstrap-trace").join(std::process::id().to_string()); + let latest_trace_dir = tracing_dir.parent().unwrap().join("latest"); + if tracing_enabled { + let _ = std::fs::remove_dir_all(&tracing_dir); + std::fs::create_dir_all(&tracing_dir).unwrap(); + + #[cfg(windows)] + let _ = std::fs::remove_dir(&latest_trace_dir); + #[cfg(not(windows))] + let _ = std::fs::remove_file(&latest_trace_dir); + + #[cfg(not(windows))] + fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> { + use std::os::unix::fs; + fs::symlink(original, link) + } + + #[cfg(windows)] + fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> { + junction::create(target, junction) + } + + t!(symlink_dir_inner(&tracing_dir, &latest_trace_dir)); + } + + debug!("creating new build based on config"); + let mut build = Build::new(config); + build.build(); + + if suggest_setup { + println!("WARNING: you have not made a `bootstrap.toml`"); + println!( + "HELP: consider running `./x.py setup` or copying `bootstrap.example.toml` by running \ + `cp bootstrap.example.toml bootstrap.toml`" + ); + } else if let Some(suggestion) = &changelog_suggestion { + println!("{suggestion}"); + } + + // Give a warning if the pre-commit script is in pre-commit and not pre-push. + // HACK: Since the commit script uses hard links, we can't actually tell if it was installed by x.py setup or not. + // We could see if it's identical to src/etc/pre-push.sh, but pre-push may have been modified in the meantime. + // Instead, look for this comment, which is almost certainly not in any custom hook. + if fs::read_to_string(pre_commit).is_ok_and(|contents| { + contents.contains("https://github.com/rust-lang/rust/issues/77620#issuecomment-705144570") + }) { + println!( + "WARNING: You have the pre-push script installed to .git/hooks/pre-commit. \ + Consider moving it to .git/hooks/pre-push instead, which runs less often." + ); + } + + if suggest_setup || changelog_suggestion.is_some() { + println!("NOTE: this message was printed twice to make it more likely to be seen"); + } + + if dump_bootstrap_shims { + let dump_dir = out_dir.join("bootstrap-shims-dump"); + assert!(dump_dir.exists()); + + for entry in walkdir::WalkDir::new(&dump_dir) { + let entry = t!(entry); + + if !entry.file_type().is_file() { + continue; + } + + let file = t!(fs::File::open(entry.path())); + + // To ensure deterministic results we must sort the dump lines. + // This is necessary because the order of rustc invocations different + // almost all the time. + let mut lines: Vec = t!(BufReader::new(&file).lines().collect()); + lines.sort_by_key(|t| t.to_lowercase()); + let mut file = t!(OpenOptions::new().write(true).truncate(true).open(entry.path())); + t!(file.write_all(lines.join("\n").as_bytes())); + } + } + + #[cfg(feature = "tracing")] + { + build.report_summary(&tracing_dir.join("command-stats.txt"), _start_time); + build.report_step_graph(&tracing_dir); + guard.copy_to_dir(&tracing_dir); + eprintln!("Tracing/profiling output has been written to {}", latest_trace_dir.display()); + } +} + +fn check_version(config: &Config) -> Option { + let mut msg = String::new(); + + let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id; + let warned_id_path = config.out.join("bootstrap").join(".last-warned-change-id"); + + let mut id = match config.change_id { + Some(ChangeId::Id(id)) if id == latest_change_id => return None, + Some(ChangeId::Ignore) => return None, + Some(ChangeId::Id(id)) => id, + None => { + 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"); + msg.push_str("NOTE: to silence this warning, "); + msg.push_str(&format!( + "add `change-id = {latest_change_id}` or `change-id = \"ignore\"` at the top of `bootstrap.toml`" + )); + return Some(msg); + } + }; + + // Always try to use `change-id` from .last-warned-change-id first. If it doesn't exist, + // then use the one from the bootstrap.toml. This way we never show the same warnings + // more than once. + if let Ok(t) = fs::read_to_string(&warned_id_path) { + let last_warned_id = usize::from_str(&t) + .unwrap_or_else(|_| panic!("{} is corrupted.", warned_id_path.display())); + + // We only use the last_warned_id if it exists in `CONFIG_CHANGE_HISTORY`. + // Otherwise, we may retrieve all the changes if it's not the highest value. + // For better understanding, refer to `change_tracker::find_recent_config_change_ids`. + if CONFIG_CHANGE_HISTORY.iter().any(|config| config.change_id == last_warned_id) { + id = last_warned_id; + } + }; + + let changes = find_recent_config_change_ids(id); + + if changes.is_empty() { + return None; + } + + msg.push_str("There have been changes to x.py since you last updated:\n"); + msg.push_str(&human_readable_changes(changes)); + + msg.push_str("NOTE: to silence this warning, "); + msg.push_str(&format!( + "update `bootstrap.toml` to use `change-id = {latest_change_id}` or `change-id = \"ignore\"` instead" + )); + + if io::stdout().is_terminal() { + t!(fs::write(warned_id_path, latest_change_id.to_string())); + } + + Some(msg) +} diff --git a/src/bootstrap/src/lib.rs b/src/bootstrap/src/lib.rs index a276f0378f450..f9b29a155eab1 100644 --- a/src/bootstrap/src/lib.rs +++ b/src/bootstrap/src/lib.rs @@ -28,36 +28,31 @@ use std::{env, fs, io, str}; use build_helper::ci::gha; use cc::Tool; use termcolor::{ColorChoice, StandardStream, WriteColor}; -use utils::build_stamp::BuildStamp; -use utils::channel::GitInfo; -use utils::exec::ExecutionContext; - -use crate::core::build_steps::format::InternalRustfmt; -use crate::core::builder; -use crate::core::builder::Kind; -use crate::core::config::{BootstrapOverrideLld, DryRun, LlvmLibunwind, TargetSelection, flags}; -use crate::utils::exec::{BootstrapCommand, command}; -use crate::utils::helpers::{self, dir_is_empty, exe, libdir, set_file_times, split_debuginfo}; - -mod core; -mod utils; - #[cfg(feature = "tracing")] -pub use core::builder::STEP_SPAN_TARGET; -pub use core::builder::{PathSet, StepStack}; -pub use core::config::flags::{Flags, Subcommand}; -pub use core::config::{ChangeId, Config}; +use tracing::{instrument, span}; +use crate::core::build_steps::format::InternalRustfmt; +use crate::core::build_steps::vendor::VENDOR_DIR; #[cfg(feature = "tracing")] -use tracing::{instrument, span}; -pub use utils::change_tracker::{ +use crate::core::builder::STEP_SPAN_TARGET; +use crate::core::builder::{self, Kind, StepStack}; +use crate::core::config::flags::{Flags, Subcommand}; +use crate::core::config::{ + BootstrapOverrideLld, ChangeId, Config, DryRun, LlvmLibunwind, TargetSelection, flags, +}; +use crate::utils::build_stamp::BuildStamp; +use crate::utils::change_tracker::{ CONFIG_CHANGE_HISTORY, find_recent_config_change_ids, human_readable_changes, }; -pub use utils::helpers::{PanicTracker, symlink_dir}; -#[cfg(feature = "tracing")] -pub use utils::tracing::setup_tracing; +use crate::utils::channel::GitInfo; +use crate::utils::exec::{BootstrapCommand, ExecutionContext, command}; +use crate::utils::helpers::{ + self, PanicTracker, dir_is_empty, exe, libdir, set_file_times, split_debuginfo, symlink_dir, +}; -use crate::core::build_steps::vendor::VENDOR_DIR; +pub mod cli_main; +mod core; +mod utils; const LLVM_TOOLS: &[&str] = &[ "llvm-cov", // used to generate coverage report