bootstrap/core/build_steps/
gcc.rs

1//! Compilation of native dependencies like GCC.
2//!
3//! Native projects like GCC unfortunately aren't suited just yet for
4//! compilation in build scripts that Cargo has. This is because the
5//! compilation takes a *very* long time but also because we don't want to
6//! compile GCC 3 times as part of a normal bootstrap (we want it cached).
7//!
8//! GCC and compiler-rt are essentially just wired up to everything else to
9//! ensure that they're always in place if needed.
10
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::OnceLock;
14
15use crate::core::builder::{Builder, Cargo, Kind, RunConfig, ShouldRun, Step};
16use crate::core::config::TargetSelection;
17use crate::utils::build_stamp::{BuildStamp, generate_smart_stamp_hash};
18use crate::utils::exec::command;
19use crate::utils::helpers::{self, t};
20
21#[derive(Debug, Clone, Hash, PartialEq, Eq)]
22pub struct Gcc {
23    pub target: TargetSelection,
24}
25
26#[derive(Clone)]
27pub struct GccOutput {
28    pub libgccjit: PathBuf,
29}
30
31impl Step for Gcc {
32    type Output = GccOutput;
33
34    const ONLY_HOSTS: bool = true;
35
36    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
37        run.path("src/gcc").alias("gcc")
38    }
39
40    fn make_run(run: RunConfig<'_>) {
41        run.builder.ensure(Gcc { target: run.target });
42    }
43
44    /// Compile GCC (specifically `libgccjit`) for `target`.
45    fn run(self, builder: &Builder<'_>) -> Self::Output {
46        let target = self.target;
47
48        // If GCC has already been built, we avoid building it again.
49        let metadata = match get_gcc_build_status(builder, target) {
50            GccBuildStatus::AlreadyBuilt(path) => return GccOutput { libgccjit: path },
51            GccBuildStatus::ShouldBuild(m) => m,
52        };
53
54        let _guard = builder.msg_unstaged(Kind::Build, "GCC", target);
55        t!(metadata.stamp.remove());
56        let _time = helpers::timeit(builder);
57
58        let libgccjit_path = libgccjit_built_path(&metadata.install_dir);
59        if builder.config.dry_run() {
60            return GccOutput { libgccjit: libgccjit_path };
61        }
62
63        build_gcc(&metadata, builder, target);
64        create_lib_alias(builder, &libgccjit_path);
65
66        t!(metadata.stamp.write());
67
68        GccOutput { libgccjit: libgccjit_path }
69    }
70}
71
72/// Creates a libgccjit.so.0 alias next to libgccjit.so if it does not
73/// already exist
74fn create_lib_alias(builder: &Builder<'_>, libgccjit: &PathBuf) {
75    let lib_alias = libgccjit.parent().unwrap().join("libgccjit.so.0");
76    if !lib_alias.exists() {
77        t!(builder.symlink_file(libgccjit, lib_alias));
78    }
79}
80
81pub struct Meta {
82    stamp: BuildStamp,
83    out_dir: PathBuf,
84    install_dir: PathBuf,
85    root: PathBuf,
86}
87
88pub enum GccBuildStatus {
89    /// libgccjit is already built at this path
90    AlreadyBuilt(PathBuf),
91    ShouldBuild(Meta),
92}
93
94/// Tries to download GCC from CI if it is enabled and GCC artifacts
95/// are available for the given target.
96/// Returns a path to the libgccjit.so file.
97#[cfg(not(test))]
98fn try_download_gcc(builder: &Builder<'_>, target: TargetSelection) -> Option<PathBuf> {
99    use build_helper::git::PathFreshness;
100
101    // Try to download GCC from CI if configured and available
102    if !matches!(builder.config.gcc_ci_mode, crate::core::config::GccCiMode::DownloadFromCi) {
103        return None;
104    }
105    if target != "x86_64-unknown-linux-gnu" {
106        eprintln!("GCC CI download is only available for the `x86_64-unknown-linux-gnu` target");
107        return None;
108    }
109    let source = detect_gcc_freshness(
110        &builder.config,
111        builder.config.rust_info.is_managed_git_subrepository(),
112    );
113    builder.verbose(|| {
114        eprintln!("GCC freshness: {source:?}");
115    });
116    match source {
117        PathFreshness::LastModifiedUpstream { upstream } => {
118            // Download from upstream CI
119            let root = ci_gcc_root(&builder.config);
120            let gcc_stamp = BuildStamp::new(&root).with_prefix("gcc").add_stamp(&upstream);
121            if !gcc_stamp.is_up_to_date() && !builder.config.dry_run() {
122                builder.config.download_ci_gcc(&upstream, &root);
123                t!(gcc_stamp.write());
124            }
125
126            let libgccjit = root.join("lib").join("libgccjit.so");
127            create_lib_alias(builder, &libgccjit);
128            Some(libgccjit)
129        }
130        PathFreshness::HasLocalModifications { .. } => {
131            // We have local modifications, rebuild GCC.
132            eprintln!("Found local GCC modifications, GCC will *not* be downloaded");
133            None
134        }
135        PathFreshness::MissingUpstream => {
136            eprintln!("error: could not find commit hash for downloading GCC");
137            eprintln!("HELP: maybe your repository history is too shallow?");
138            eprintln!("HELP: consider disabling `download-ci-gcc`");
139            eprintln!("HELP: or fetch enough history to include one upstream commit");
140            None
141        }
142    }
143}
144
145#[cfg(test)]
146fn try_download_gcc(_builder: &Builder<'_>, _target: TargetSelection) -> Option<PathBuf> {
147    None
148}
149
150/// This returns information about whether GCC should be built or if it's already built.
151/// It transparently handles downloading GCC from CI if needed.
152///
153/// It's used to avoid busting caches during x.py check -- if we've already built
154/// GCC, it's fine for us to not try to avoid doing so.
155pub fn get_gcc_build_status(builder: &Builder<'_>, target: TargetSelection) -> GccBuildStatus {
156    if let Some(path) = try_download_gcc(builder, target) {
157        return GccBuildStatus::AlreadyBuilt(path);
158    }
159
160    static STAMP_HASH_MEMO: OnceLock<String> = OnceLock::new();
161    let smart_stamp_hash = STAMP_HASH_MEMO.get_or_init(|| {
162        generate_smart_stamp_hash(
163            builder,
164            &builder.config.src.join("src/gcc"),
165            builder.in_tree_gcc_info.sha().unwrap_or_default(),
166        )
167    });
168
169    // Initialize the gcc submodule if not initialized already.
170    builder.config.update_submodule("src/gcc");
171
172    let root = builder.src.join("src/gcc");
173    let out_dir = builder.gcc_out(target).join("build");
174    let install_dir = builder.gcc_out(target).join("install");
175
176    let stamp = BuildStamp::new(&out_dir).with_prefix("gcc").add_stamp(smart_stamp_hash);
177
178    if stamp.is_up_to_date() {
179        if stamp.stamp().is_empty() {
180            builder.info(
181                "Could not determine the GCC submodule commit hash. \
182                     Assuming that an GCC rebuild is not necessary.",
183            );
184            builder.info(&format!(
185                "To force GCC to rebuild, remove the file `{}`",
186                stamp.path().display()
187            ));
188        }
189        let path = libgccjit_built_path(&install_dir);
190        if path.is_file() {
191            return GccBuildStatus::AlreadyBuilt(path);
192        } else {
193            builder.info(&format!(
194                "GCC stamp is up-to-date, but the libgccjit.so file was not found at `{}`",
195                path.display(),
196            ));
197        }
198    }
199
200    GccBuildStatus::ShouldBuild(Meta { stamp, out_dir, install_dir, root })
201}
202
203/// Returns the path to a libgccjit.so file in the install directory of GCC.
204fn libgccjit_built_path(install_dir: &Path) -> PathBuf {
205    install_dir.join("lib/libgccjit.so")
206}
207
208fn build_gcc(metadata: &Meta, builder: &Builder<'_>, target: TargetSelection) {
209    if builder.build.cc_tool(target).is_like_clang()
210        || builder.build.cxx_tool(target).is_like_clang()
211    {
212        panic!(
213            "Attempting to build GCC using Clang, which is known to misbehave. Please use GCC as the host C/C++ compiler. "
214        );
215    }
216
217    let Meta { stamp: _, out_dir, install_dir, root } = metadata;
218
219    t!(fs::create_dir_all(out_dir));
220    t!(fs::create_dir_all(install_dir));
221
222    // GCC creates files (e.g. symlinks to the downloaded dependencies)
223    // in the source directory, which does not work with our CI setup, where we mount
224    // source directories as read-only on Linux.
225    // Therefore, as a part of the build in CI, we first copy the whole source directory
226    // to the build directory, and perform the build from there.
227    let src_dir = if builder.config.is_running_on_ci {
228        let src_dir = builder.gcc_out(target).join("src");
229        if src_dir.exists() {
230            builder.remove_dir(&src_dir);
231        }
232        builder.create_dir(&src_dir);
233        builder.cp_link_r(root, &src_dir);
234        src_dir
235    } else {
236        root.clone()
237    };
238
239    command(src_dir.join("contrib/download_prerequisites")).current_dir(&src_dir).run(builder);
240    let mut configure_cmd = command(src_dir.join("configure"));
241    configure_cmd
242        .current_dir(out_dir)
243        .arg("--enable-host-shared")
244        .arg("--enable-languages=c,jit,lto")
245        .arg("--enable-checking=release")
246        .arg("--disable-bootstrap")
247        .arg("--disable-multilib")
248        .arg(format!("--prefix={}", install_dir.display()));
249
250    let cc = builder.build.cc(target).display().to_string();
251    let cc = builder
252        .build
253        .config
254        .ccache
255        .as_ref()
256        .map_or_else(|| cc.clone(), |ccache| format!("{ccache} {cc}"));
257    configure_cmd.env("CC", cc);
258
259    if let Ok(ref cxx) = builder.build.cxx(target) {
260        let cxx = cxx.display().to_string();
261        let cxx = builder
262            .build
263            .config
264            .ccache
265            .as_ref()
266            .map_or_else(|| cxx.clone(), |ccache| format!("{ccache} {cxx}"));
267        configure_cmd.env("CXX", cxx);
268    }
269    configure_cmd.run(builder);
270
271    command("make")
272        .current_dir(out_dir)
273        .arg("--silent")
274        .arg(format!("-j{}", builder.jobs()))
275        .run_capture_stdout(builder);
276    command("make").current_dir(out_dir).arg("--silent").arg("install").run_capture_stdout(builder);
277}
278
279/// Configures a Cargo invocation so that it can build the GCC codegen backend.
280pub fn add_cg_gcc_cargo_flags(cargo: &mut Cargo, gcc: &GccOutput) {
281    // Add the path to libgccjit.so to the linker search paths.
282    cargo.rustflag(&format!("-L{}", gcc.libgccjit.parent().unwrap().to_str().unwrap()));
283}
284
285/// The absolute path to the downloaded GCC artifacts.
286#[cfg(not(test))]
287fn ci_gcc_root(config: &crate::Config) -> PathBuf {
288    config.out.join(config.build).join("ci-gcc")
289}
290
291/// Detect whether GCC sources have been modified locally or not.
292#[cfg(not(test))]
293fn detect_gcc_freshness(config: &crate::Config, is_git: bool) -> build_helper::git::PathFreshness {
294    use build_helper::git::PathFreshness;
295
296    if is_git {
297        config.check_path_modifications(&["src/gcc", "src/bootstrap/download-ci-gcc-stamp"])
298    } else if let Some(info) = crate::utils::channel::read_commit_info_file(&config.src) {
299        PathFreshness::LastModifiedUpstream { upstream: info.sha.trim().to_owned() }
300    } else {
301        PathFreshness::MissingUpstream
302    }
303}