Skip to main content

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