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