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::fmt::{Display, Formatter};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::OnceLock;
15
16use crate::core::builder::{Builder, Cargo, Kind, RunConfig, ShouldRun, Step};
17use crate::core::config::TargetSelection;
18use crate::utils::build_stamp::{BuildStamp, generate_smart_stamp_hash};
19use crate::utils::exec::command;
20use crate::utils::helpers::{self, t};
21
22/// GCC cannot cross-compile from a single binary to multiple targets.
23/// So we need to have a separate GCC dylib for each (host, target) pair.
24/// We represent this explicitly using this struct.
25#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
26pub struct GccTargetPair {
27    /// Target on which the libgccjit.so dylib will be executed.
28    host: TargetSelection,
29    /// Target for which the libgccjit.so dylib will generate assembly.
30    target: TargetSelection,
31}
32
33impl GccTargetPair {
34    /// Create a target pair for a GCC that will run on `target` and generate assembly for `target`.
35    pub fn for_native_build(target: TargetSelection) -> Self {
36        Self { host: target, target }
37    }
38
39    /// Create a target pair for a GCC that will run on `host` and generate assembly for `target`.
40    /// This may be cross-compilation if `host != target`.
41    pub fn for_target_pair(host: TargetSelection, target: TargetSelection) -> Self {
42        Self { host, target }
43    }
44
45    pub fn host(&self) -> TargetSelection {
46        self.host
47    }
48
49    pub fn target(&self) -> TargetSelection {
50        self.target
51    }
52}
53
54impl Display for GccTargetPair {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{} -> {}", self.host, self.target)
57    }
58}
59
60#[derive(Debug, Clone, Hash, PartialEq, Eq)]
61pub struct Gcc {
62    pub target_pair: GccTargetPair,
63}
64
65#[derive(Clone)]
66pub struct GccOutput {
67    /// Path to a built or downloaded libgccjit.
68    libgccjit: PathBuf,
69}
70
71impl GccOutput {
72    pub fn libgccjit(&self) -> &Path {
73        &self.libgccjit
74    }
75}
76
77impl Step for Gcc {
78    type Output = GccOutput;
79
80    const IS_HOST: bool = true;
81
82    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
83        run.path("src/gcc").alias("gcc")
84    }
85
86    fn make_run(run: RunConfig<'_>) {
87        // By default, we build libgccjit that can do native compilation (no cross-compilation)
88        // on a given target.
89        run.builder
90            .ensure(Gcc { target_pair: GccTargetPair { host: run.target, target: run.target } });
91    }
92
93    /// Compile GCC (specifically `libgccjit`) for `target`.
94    fn run(self, builder: &Builder<'_>) -> Self::Output {
95        let target_pair = self.target_pair;
96
97        // If GCC has already been built, we avoid building it again.
98        let metadata = match get_gcc_build_status(builder, target_pair) {
99            GccBuildStatus::AlreadyBuilt(path) => return GccOutput { libgccjit: path },
100            GccBuildStatus::ShouldBuild(m) => m,
101        };
102
103        let action = Kind::Build.description();
104        let msg = format!("{action} GCC for {target_pair}");
105        let _guard = builder.group(&msg);
106        t!(metadata.stamp.remove());
107        let _time = helpers::timeit(builder);
108
109        let libgccjit_path = libgccjit_built_path(&metadata.install_dir);
110        if builder.config.dry_run() {
111            return GccOutput { libgccjit: libgccjit_path };
112        }
113
114        build_gcc(&metadata, builder, target_pair);
115
116        t!(metadata.stamp.write());
117
118        GccOutput { libgccjit: libgccjit_path }
119    }
120}
121
122pub struct Meta {
123    stamp: BuildStamp,
124    out_dir: PathBuf,
125    install_dir: PathBuf,
126    root: PathBuf,
127}
128
129pub enum GccBuildStatus {
130    /// libgccjit is already built at this path
131    AlreadyBuilt(PathBuf),
132    ShouldBuild(Meta),
133}
134
135/// Tries to download GCC from CI if it is enabled and GCC artifacts
136/// are available for the given target.
137/// Returns a path to the libgccjit.so file.
138#[cfg(not(test))]
139fn try_download_gcc(builder: &Builder<'_>, target_pair: GccTargetPair) -> Option<PathBuf> {
140    use build_helper::git::PathFreshness;
141
142    // Try to download GCC from CI if configured and available
143    if !matches!(builder.config.gcc_ci_mode, crate::core::config::GccCiMode::DownloadFromCi) {
144        return None;
145    }
146
147    // We currently do not support downloading CI GCC if the host/target pair doesn't match.
148    if target_pair.host != target_pair.target {
149        eprintln!(
150            "GCC CI download is not available when the host ({}) does not equal the compilation target ({}).",
151            target_pair.host, target_pair.target
152        );
153        return None;
154    }
155
156    if target_pair.host != "x86_64-unknown-linux-gnu" {
157        eprintln!(
158            "GCC CI download is only available for the `x86_64-unknown-linux-gnu` host/target"
159        );
160        return None;
161    }
162    let source = detect_gcc_freshness(
163        &builder.config,
164        builder.config.rust_info.is_managed_git_subrepository(),
165    );
166    builder.do_if_verbose(|| {
167        eprintln!("GCC freshness: {source:?}");
168    });
169    match source {
170        PathFreshness::LastModifiedUpstream { upstream } => {
171            // Download from upstream CI
172            let root = ci_gcc_root(&builder.config, target_pair.target);
173            let gcc_stamp = BuildStamp::new(&root).with_prefix("gcc").add_stamp(&upstream);
174            if !gcc_stamp.is_up_to_date() && !builder.config.dry_run() {
175                builder.config.download_ci_gcc(&upstream, &root);
176                t!(gcc_stamp.write());
177            }
178
179            let libgccjit = root.join("lib").join("libgccjit.so");
180            Some(libgccjit)
181        }
182        PathFreshness::HasLocalModifications { .. } => {
183            // We have local modifications, rebuild GCC.
184            eprintln!("Found local GCC modifications, GCC will *not* be downloaded");
185            None
186        }
187        PathFreshness::MissingUpstream => {
188            eprintln!("error: could not find commit hash for downloading GCC");
189            eprintln!("HELP: maybe your repository history is too shallow?");
190            eprintln!("HELP: consider disabling `download-ci-gcc`");
191            eprintln!("HELP: or fetch enough history to include one upstream commit");
192            None
193        }
194    }
195}
196
197#[cfg(test)]
198fn try_download_gcc(_builder: &Builder<'_>, _target_pair: GccTargetPair) -> Option<PathBuf> {
199    None
200}
201
202/// This returns information about whether GCC should be built or if it's already built.
203/// It transparently handles downloading GCC from CI if needed.
204///
205/// It's used to avoid busting caches during x.py check -- if we've already built
206/// GCC, it's fine for us to not try to avoid doing so.
207pub fn get_gcc_build_status(builder: &Builder<'_>, target_pair: GccTargetPair) -> GccBuildStatus {
208    // Prefer taking externally provided prebuilt libgccjit dylib
209    if let Some(dir) = &builder.config.libgccjit_libs_dir {
210        // The dir structure should be <root>/<host>/<target>/libgccjit.so
211        let host_dir = dir.join(target_pair.host);
212        let path = host_dir.join(target_pair.target).join("libgccjit.so");
213        if path.exists() {
214            return GccBuildStatus::AlreadyBuilt(path);
215        } else {
216            builder.info(&format!(
217                "libgccjit.so for `{target_pair}` was not found at `{}`",
218                path.display()
219            ));
220
221            if target_pair.host != target_pair.target || target_pair.host != builder.host_target {
222                eprintln!(
223                    "info: libgccjit.so for `{target_pair}` was not found at `{}`",
224                    path.display()
225                );
226                eprintln!("error: we do not support downloading or building a GCC cross-compiler");
227                std::process::exit(1);
228            }
229        }
230    }
231
232    // If not available, try to download from CI
233    if let Some(path) = try_download_gcc(builder, target_pair) {
234        return GccBuildStatus::AlreadyBuilt(path);
235    }
236
237    // If not available, try to build (or use already built libgccjit from disk)
238    static STAMP_HASH_MEMO: OnceLock<String> = OnceLock::new();
239    let smart_stamp_hash = STAMP_HASH_MEMO.get_or_init(|| {
240        generate_smart_stamp_hash(
241            builder,
242            &builder.config.src.join("src/gcc"),
243            builder.in_tree_gcc_info.sha().unwrap_or_default(),
244        )
245    });
246
247    // Initialize the gcc submodule if not initialized already.
248    builder.config.update_submodule("src/gcc");
249
250    let root = builder.src.join("src/gcc");
251    let out_dir = gcc_out(builder, target_pair).join("build");
252    let install_dir = gcc_out(builder, target_pair).join("install");
253
254    let stamp = BuildStamp::new(&out_dir).with_prefix("gcc").add_stamp(smart_stamp_hash);
255
256    if stamp.is_up_to_date() {
257        if stamp.stamp().is_empty() {
258            builder.info(
259                "Could not determine the GCC submodule commit hash. \
260                     Assuming that an GCC rebuild is not necessary.",
261            );
262            builder.info(&format!(
263                "To force GCC to rebuild, remove the file `{}`",
264                stamp.path().display()
265            ));
266        }
267        let path = libgccjit_built_path(&install_dir);
268        if path.is_file() {
269            return GccBuildStatus::AlreadyBuilt(path);
270        } else {
271            builder.info(&format!(
272                "GCC stamp is up-to-date, but the libgccjit.so file was not found at `{}`",
273                path.display(),
274            ));
275        }
276    }
277
278    GccBuildStatus::ShouldBuild(Meta { stamp, out_dir, install_dir, root })
279}
280
281fn gcc_out(builder: &Builder<'_>, pair: GccTargetPair) -> PathBuf {
282    builder.out.join(pair.host).join("gcc").join(pair.target)
283}
284
285/// Returns the path to a libgccjit.so file in the install directory of GCC.
286fn libgccjit_built_path(install_dir: &Path) -> PathBuf {
287    install_dir.join("lib/libgccjit.so")
288}
289
290fn build_gcc(metadata: &Meta, builder: &Builder<'_>, target_pair: GccTargetPair) {
291    // Target on which libgccjit.so will be executed. Here we will generate a dylib with
292    // instructions for that target.
293    let host = target_pair.host;
294    if builder.build.cc_tool(host).is_like_clang() || builder.build.cxx_tool(host).is_like_clang() {
295        panic!(
296            "Attempting to build GCC using Clang, which is known to misbehave. Please use GCC as the host C/C++ compiler. "
297        );
298    }
299
300    let Meta { stamp: _, out_dir, install_dir, root } = metadata;
301
302    t!(fs::create_dir_all(out_dir));
303    t!(fs::create_dir_all(install_dir));
304
305    // GCC creates files (e.g. symlinks to the downloaded dependencies)
306    // in the source directory, which does not work with our CI/Docker setup, where we mount
307    // source directories as read-only on Linux.
308    // And in general, we shouldn't be modifying the source directories if possible, even for local
309    // builds.
310    // Therefore, we first copy the whole source directory to the build directory, and perform the
311    // build from there.
312    let src_dir = gcc_out(builder, target_pair).join("src");
313    if src_dir.exists() {
314        builder.remove_dir(&src_dir);
315    }
316    builder.create_dir(&src_dir);
317    builder.cp_link_r(root, &src_dir);
318
319    command(src_dir.join("contrib/download_prerequisites")).current_dir(&src_dir).run(builder);
320    let mut configure_cmd = command(src_dir.join("configure"));
321    configure_cmd
322        .current_dir(out_dir)
323        .arg("--enable-host-shared")
324        .arg("--enable-languages=c,jit,lto")
325        .arg("--enable-checking=release")
326        .arg("--disable-bootstrap")
327        .arg("--disable-multilib")
328        .arg(format!("--prefix={}", install_dir.display()));
329
330    let cc = builder.build.cc(host).display().to_string();
331    let cc = builder
332        .build
333        .config
334        .ccache
335        .as_ref()
336        .map_or_else(|| cc.clone(), |ccache| format!("{ccache} {cc}"));
337    configure_cmd.env("CC", cc);
338
339    if let Ok(ref cxx) = builder.build.cxx(host) {
340        let cxx = cxx.display().to_string();
341        let cxx = builder
342            .build
343            .config
344            .ccache
345            .as_ref()
346            .map_or_else(|| cxx.clone(), |ccache| format!("{ccache} {cxx}"));
347        configure_cmd.env("CXX", cxx);
348    }
349    configure_cmd.run(builder);
350
351    command("make")
352        .current_dir(out_dir)
353        .arg("--silent")
354        .arg(format!("-j{}", builder.jobs()))
355        .run_capture_stdout(builder);
356    command("make").current_dir(out_dir).arg("--silent").arg("install").run_capture_stdout(builder);
357}
358
359/// Configures a Cargo invocation so that it can build the GCC codegen backend.
360pub fn add_cg_gcc_cargo_flags(cargo: &mut Cargo, gcc: &GccOutput) {
361    // Add the path to libgccjit.so to the linker search paths.
362    cargo.rustflag(&format!("-L{}", gcc.libgccjit.parent().unwrap().to_str().unwrap()));
363}
364
365/// The absolute path to the downloaded GCC artifacts.
366#[cfg(not(test))]
367fn ci_gcc_root(config: &crate::Config, target: TargetSelection) -> PathBuf {
368    config.out.join(target).join("ci-gcc")
369}
370
371/// Detect whether GCC sources have been modified locally or not.
372#[cfg(not(test))]
373fn detect_gcc_freshness(config: &crate::Config, is_git: bool) -> build_helper::git::PathFreshness {
374    use build_helper::git::PathFreshness;
375
376    if is_git {
377        config.check_path_modifications(&["src/gcc", "src/bootstrap/download-ci-gcc-stamp"])
378    } else if let Some(info) = crate::utils::channel::read_commit_info_file(&config.src) {
379        PathFreshness::LastModifiedUpstream { upstream: info.sha.trim().to_owned() }
380    } else {
381        PathFreshness::MissingUpstream
382    }
383}