|  | use std::collections::HashMap; | 
|  | use std::ffi::OsStr; | 
|  | use std::path::{Path, PathBuf}; | 
|  | use std::{env as std_env, fs}; | 
|  |  | 
|  | use boml::Toml; | 
|  | use boml::types::TomlValue; | 
|  |  | 
|  | use crate::utils::{ | 
|  | create_dir, create_symlink, get_os_name, get_sysroot_dir, run_command_with_output, | 
|  | rustc_version_info, split_args, | 
|  | }; | 
|  |  | 
|  | #[derive(Default, PartialEq, Eq, Clone, Copy, Debug)] | 
|  | pub enum Channel { | 
|  | #[default] | 
|  | Debug, | 
|  | Release, | 
|  | } | 
|  |  | 
|  | impl Channel { | 
|  | pub fn as_str(self) -> &'static str { | 
|  | match self { | 
|  | Self::Debug => "debug", | 
|  | Self::Release => "release", | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | fn failed_config_parsing(config_file: &Path, err: &str) -> Result<ConfigFile, String> { | 
|  | Err(format!("Failed to parse `{}`: {}", config_file.display(), err)) | 
|  | } | 
|  |  | 
|  | #[derive(Default)] | 
|  | pub struct ConfigFile { | 
|  | gcc_path: Option<String>, | 
|  | download_gccjit: Option<bool>, | 
|  | } | 
|  |  | 
|  | impl ConfigFile { | 
|  | pub fn new(config_file: &Path) -> Result<Self, String> { | 
|  | let content = fs::read_to_string(config_file).map_err(|_| { | 
|  | format!( | 
|  | "Failed to read `{}`. Take a look at `Readme.md` to see how to set up the project", | 
|  | config_file.display(), | 
|  | ) | 
|  | })?; | 
|  | let toml = Toml::parse(&content).map_err(|err| { | 
|  | format!("Error occurred around `{}`: {:?}", &content[err.start..=err.end], err.kind) | 
|  | })?; | 
|  | let mut config = Self::default(); | 
|  | for (key, value) in toml.iter() { | 
|  | match (key, value) { | 
|  | ("gcc-path", TomlValue::String(value)) => { | 
|  | config.gcc_path = Some(value.as_str().to_string()) | 
|  | } | 
|  | ("gcc-path", _) => { | 
|  | return failed_config_parsing(config_file, "Expected a string for `gcc-path`"); | 
|  | } | 
|  | ("download-gccjit", TomlValue::Boolean(value)) => { | 
|  | config.download_gccjit = Some(*value) | 
|  | } | 
|  | ("download-gccjit", _) => { | 
|  | return failed_config_parsing( | 
|  | config_file, | 
|  | "Expected a boolean for `download-gccjit`", | 
|  | ); | 
|  | } | 
|  | _ => return failed_config_parsing(config_file, &format!("Unknown key `{key}`")), | 
|  | } | 
|  | } | 
|  | match (config.gcc_path.as_mut(), config.download_gccjit) { | 
|  | (None, None | Some(false)) => { | 
|  | return failed_config_parsing( | 
|  | config_file, | 
|  | "At least one of `gcc-path` or `download-gccjit` value must be set", | 
|  | ); | 
|  | } | 
|  | (Some(_), Some(true)) => { | 
|  | println!( | 
|  | "WARNING: both `gcc-path` and `download-gccjit` arguments are used, \ | 
|  | ignoring `gcc-path`" | 
|  | ); | 
|  | } | 
|  | (Some(gcc_path), _) => { | 
|  | let path = Path::new(gcc_path); | 
|  | *gcc_path = path | 
|  | .canonicalize() | 
|  | .map_err(|err| format!("Failed to get absolute path of `{gcc_path}`: {err:?}"))? | 
|  | .display() | 
|  | .to_string(); | 
|  | } | 
|  | _ => {} | 
|  | } | 
|  | Ok(config) | 
|  | } | 
|  | } | 
|  |  | 
|  | #[derive(Default, Debug, Clone)] | 
|  | pub struct ConfigInfo { | 
|  | pub target: String, | 
|  | pub target_triple: String, | 
|  | pub host_triple: String, | 
|  | pub rustc_command: Vec<String>, | 
|  | pub run_in_vm: bool, | 
|  | pub cargo_target_dir: String, | 
|  | pub dylib_ext: String, | 
|  | pub sysroot_release_channel: bool, | 
|  | pub channel: Channel, | 
|  | pub sysroot_panic_abort: bool, | 
|  | pub cg_backend_path: String, | 
|  | pub sysroot_path: String, | 
|  | pub gcc_path: Option<String>, | 
|  | config_file: Option<String>, | 
|  | // This is used in particular in rust compiler bootstrap because it doesn't run at the root | 
|  | // of the `cg_gcc` folder, making it complicated for us to get access to local files we need | 
|  | // like `libgccjit.version` or `config.toml`. | 
|  | cg_gcc_path: Option<PathBuf>, | 
|  | // Needed for the `info` command which doesn't want to actually download the lib if needed, | 
|  | // just to set the `gcc_path` field to display it. | 
|  | pub no_download: bool, | 
|  | pub no_default_features: bool, | 
|  | pub backend: Option<String>, | 
|  | pub features: Vec<String>, | 
|  | } | 
|  |  | 
|  | impl ConfigInfo { | 
|  | /// Returns `true` if the argument was taken into account. | 
|  | pub fn parse_argument( | 
|  | &mut self, | 
|  | arg: &str, | 
|  | args: &mut impl Iterator<Item = String>, | 
|  | ) -> Result<bool, String> { | 
|  | match arg { | 
|  | "--features" => { | 
|  | if let Some(arg) = args.next() { | 
|  | self.features.push(arg); | 
|  | } else { | 
|  | return Err("Expected a value after `--features`, found nothing".to_string()); | 
|  | } | 
|  | } | 
|  | "--target" => { | 
|  | if let Some(arg) = args.next() { | 
|  | self.target = arg; | 
|  | } else { | 
|  | return Err("Expected a value after `--target`, found nothing".to_string()); | 
|  | } | 
|  | } | 
|  | "--target-triple" => match args.next() { | 
|  | Some(arg) if !arg.is_empty() => self.target_triple = arg.to_string(), | 
|  | _ => { | 
|  | return Err( | 
|  | "Expected a value after `--target-triple`, found nothing".to_string() | 
|  | ); | 
|  | } | 
|  | }, | 
|  | "--out-dir" => match args.next() { | 
|  | Some(arg) if !arg.is_empty() => { | 
|  | self.cargo_target_dir = arg.to_string(); | 
|  | } | 
|  | _ => return Err("Expected a value after `--out-dir`, found nothing".to_string()), | 
|  | }, | 
|  | "--config-file" => match args.next() { | 
|  | Some(arg) if !arg.is_empty() => { | 
|  | self.config_file = Some(arg.to_string()); | 
|  | } | 
|  | _ => { | 
|  | return Err("Expected a value after `--config-file`, found nothing".to_string()); | 
|  | } | 
|  | }, | 
|  | "--release-sysroot" => self.sysroot_release_channel = true, | 
|  | "--release" => self.channel = Channel::Release, | 
|  | "--sysroot-panic-abort" => self.sysroot_panic_abort = true, | 
|  | "--gcc-path" => match args.next() { | 
|  | Some(arg) if !arg.is_empty() => { | 
|  | self.gcc_path = Some(arg); | 
|  | } | 
|  | _ => { | 
|  | return Err("Expected a value after `--gcc-path`, found nothing".to_string()); | 
|  | } | 
|  | }, | 
|  | "--cg_gcc-path" => match args.next() { | 
|  | Some(arg) if !arg.is_empty() => { | 
|  | self.cg_gcc_path = Some(arg.into()); | 
|  | } | 
|  | _ => { | 
|  | return Err("Expected a value after `--cg_gcc-path`, found nothing".to_string()); | 
|  | } | 
|  | }, | 
|  | "--use-backend" => match args.next() { | 
|  | Some(backend) if !backend.is_empty() => self.backend = Some(backend), | 
|  | _ => return Err("Expected an argument after `--use-backend`, found nothing".into()), | 
|  | }, | 
|  | "--no-default-features" => self.no_default_features = true, | 
|  | _ => return Ok(false), | 
|  | } | 
|  | Ok(true) | 
|  | } | 
|  |  | 
|  | pub fn rustc_command_vec(&self) -> Vec<&dyn AsRef<OsStr>> { | 
|  | let mut command: Vec<&dyn AsRef<OsStr>> = Vec::with_capacity(self.rustc_command.len()); | 
|  | for arg in self.rustc_command.iter() { | 
|  | command.push(arg); | 
|  | } | 
|  | command | 
|  | } | 
|  |  | 
|  | pub fn get_gcc_commit(&self) -> Result<String, String> { | 
|  | let commit_hash_file = self.compute_path("libgccjit.version"); | 
|  | let content = fs::read_to_string(&commit_hash_file).map_err(|_| { | 
|  | format!( | 
|  | "Failed to read `{}`. Take a look at `Readme.md` to see how to set up the project", | 
|  | commit_hash_file.display(), | 
|  | ) | 
|  | })?; | 
|  | let commit = content.trim(); | 
|  | // This is a very simple check to ensure this is not a path. For the rest, it'll just fail | 
|  | // when trying to download the file so we should be fine. | 
|  | if commit.contains('/') || commit.contains('\\') { | 
|  | return Err(format!( | 
|  | "{}: invalid commit hash `{}`", | 
|  | commit_hash_file.display(), | 
|  | commit, | 
|  | )); | 
|  | } | 
|  | Ok(commit.to_string()) | 
|  | } | 
|  |  | 
|  | fn download_gccjit_if_needed(&mut self) -> Result<(), String> { | 
|  | let output_dir = Path::new(crate::BUILD_DIR).join("libgccjit"); | 
|  | let commit = self.get_gcc_commit()?; | 
|  |  | 
|  | let output_dir = output_dir.join(&commit); | 
|  | if !output_dir.is_dir() { | 
|  | create_dir(&output_dir)?; | 
|  | } | 
|  | let output_dir = output_dir.canonicalize().map_err(|err| { | 
|  | format!("Failed to get absolute path of `{}`: {:?}", output_dir.display(), err) | 
|  | })?; | 
|  |  | 
|  | let libgccjit_so_name = "libgccjit.so"; | 
|  | let libgccjit_so = output_dir.join(libgccjit_so_name); | 
|  | if !libgccjit_so.is_file() && !self.no_download { | 
|  | // Download time! | 
|  | let tempfile_name = format!("{libgccjit_so_name}.download"); | 
|  | let tempfile = output_dir.join(&tempfile_name); | 
|  | let is_in_ci = std::env::var("GITHUB_ACTIONS").is_ok(); | 
|  |  | 
|  | download_gccjit(&commit, &output_dir, tempfile_name, !is_in_ci)?; | 
|  |  | 
|  | let libgccjit_so = output_dir.join(libgccjit_so_name); | 
|  | // If we reach this point, it means the file was correctly downloaded, so let's | 
|  | // rename it! | 
|  | std::fs::rename(&tempfile, &libgccjit_so).map_err(|err| { | 
|  | format!( | 
|  | "Failed to rename `{}` into `{}`: {:?}", | 
|  | tempfile.display(), | 
|  | libgccjit_so.display(), | 
|  | err, | 
|  | ) | 
|  | })?; | 
|  |  | 
|  | println!("Downloaded libgccjit.so version {commit} successfully!"); | 
|  | // We need to create a link named `libgccjit.so.0` because that's what the linker is | 
|  | // looking for. | 
|  | create_symlink(&libgccjit_so, output_dir.join(format!("{libgccjit_so_name}.0")))?; | 
|  | } | 
|  |  | 
|  | let gcc_path = output_dir.display().to_string(); | 
|  | println!("Using `{gcc_path}` as path for libgccjit"); | 
|  | self.gcc_path = Some(gcc_path); | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | pub fn compute_path<P: AsRef<Path>>(&self, other: P) -> PathBuf { | 
|  | match self.cg_gcc_path { | 
|  | Some(ref path) => path.join(other), | 
|  | None => PathBuf::new().join(other), | 
|  | } | 
|  | } | 
|  |  | 
|  | pub fn setup_gcc_path(&mut self) -> Result<(), String> { | 
|  | // If the user used the `--gcc-path` option, no need to look at `config.toml` content | 
|  | // since we already have everything we need. | 
|  | if let Some(gcc_path) = &self.gcc_path { | 
|  | println!( | 
|  | "`--gcc-path` was provided, ignoring config file. Using `{gcc_path}` as path for libgccjit" | 
|  | ); | 
|  | return Ok(()); | 
|  | } | 
|  | let config_file = match self.config_file.as_deref() { | 
|  | Some(config_file) => config_file.into(), | 
|  | None => self.compute_path("config.toml"), | 
|  | }; | 
|  | let ConfigFile { gcc_path, download_gccjit } = ConfigFile::new(&config_file)?; | 
|  |  | 
|  | if let Some(true) = download_gccjit { | 
|  | self.download_gccjit_if_needed()?; | 
|  | return Ok(()); | 
|  | } | 
|  | let Some(gcc_path) = gcc_path else { | 
|  | return Err(format!("missing `gcc-path` value from `{}`", config_file.display())); | 
|  | }; | 
|  | println!( | 
|  | "GCC path retrieved from `{}`. Using `{}` as path for libgccjit", | 
|  | config_file.display(), | 
|  | gcc_path | 
|  | ); | 
|  | self.gcc_path = Some(gcc_path); | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | pub fn setup( | 
|  | &mut self, | 
|  | env: &mut HashMap<String, String>, | 
|  | use_system_gcc: bool, | 
|  | ) -> Result<(), String> { | 
|  | env.insert("CARGO_INCREMENTAL".to_string(), "0".to_string()); | 
|  |  | 
|  | let gcc_path = if !use_system_gcc { | 
|  | if self.gcc_path.is_none() { | 
|  | self.setup_gcc_path()?; | 
|  | } | 
|  | self.gcc_path.clone().expect( | 
|  | "The config module should have emitted an error if the GCC path wasn't provided", | 
|  | ) | 
|  | } else { | 
|  | String::new() | 
|  | }; | 
|  | env.insert("GCC_PATH".to_string(), gcc_path.clone()); | 
|  |  | 
|  | if self.cargo_target_dir.is_empty() { | 
|  | match env.get("CARGO_TARGET_DIR").filter(|dir| !dir.is_empty()) { | 
|  | Some(cargo_target_dir) => self.cargo_target_dir = cargo_target_dir.clone(), | 
|  | None => self.cargo_target_dir = "target/out".to_string(), | 
|  | } | 
|  | } | 
|  |  | 
|  | let os_name = get_os_name()?; | 
|  | self.dylib_ext = match os_name.as_str() { | 
|  | "Linux" => "so", | 
|  | "Darwin" => "dylib", | 
|  | os => return Err(format!("unsupported OS `{os}`")), | 
|  | } | 
|  | .to_string(); | 
|  | let rustc = match env.get("RUSTC") { | 
|  | Some(r) if !r.is_empty() => r.to_string(), | 
|  | _ => "rustc".to_string(), | 
|  | }; | 
|  | self.host_triple = match rustc_version_info(Some(&rustc))?.host { | 
|  | Some(host) => host, | 
|  | None => return Err("no host found".to_string()), | 
|  | }; | 
|  |  | 
|  | if self.target_triple.is_empty() { | 
|  | self.target_triple = self.host_triple.clone(); | 
|  | } | 
|  | if self.target.is_empty() && !self.target_triple.is_empty() { | 
|  | self.target = self.target_triple.clone(); | 
|  | } | 
|  |  | 
|  | let mut linker = None; | 
|  |  | 
|  | if self.host_triple != self.target_triple { | 
|  | if self.target_triple.is_empty() { | 
|  | return Err("Unknown non-native platform".to_string()); | 
|  | } | 
|  | linker = Some(format!("-Clinker={}-gcc", self.target_triple)); | 
|  | self.run_in_vm = true; | 
|  | } | 
|  |  | 
|  | let current_dir = | 
|  | std_env::current_dir().map_err(|error| format!("`current_dir` failed: {error:?}"))?; | 
|  | let channel = if self.channel == Channel::Release { | 
|  | "release" | 
|  | } else if let Some(channel) = env.get("CHANNEL") { | 
|  | channel.as_str() | 
|  | } else { | 
|  | "debug" | 
|  | }; | 
|  |  | 
|  | let mut rustflags = Vec::new(); | 
|  | self.cg_backend_path = current_dir | 
|  | .join("target") | 
|  | .join(channel) | 
|  | .join(format!("librustc_codegen_gcc.{}", self.dylib_ext)) | 
|  | .display() | 
|  | .to_string(); | 
|  | self.sysroot_path = | 
|  | current_dir.join(get_sysroot_dir()).join("sysroot").display().to_string(); | 
|  | if let Some(backend) = &self.backend { | 
|  | // This option is only used in the rust compiler testsuite. The sysroot is handled | 
|  | // by its build system directly so no need to set it ourselves. | 
|  | rustflags.push(format!("-Zcodegen-backend={backend}")); | 
|  | } else { | 
|  | rustflags.extend_from_slice(&[ | 
|  | "--sysroot".to_string(), | 
|  | self.sysroot_path.clone(), | 
|  | format!("-Zcodegen-backend={}", self.cg_backend_path), | 
|  | ]); | 
|  | } | 
|  |  | 
|  | // This environment variable is useful in case we want to change options of rustc commands. | 
|  | // We have a different environment variable than RUSTFLAGS to make sure those flags are | 
|  | // only sent to rustc_codegen_gcc and not the LLVM backend. | 
|  | if let Some(cg_rustflags) = env.get("CG_RUSTFLAGS") { | 
|  | rustflags.extend_from_slice(&split_args(cg_rustflags)?); | 
|  | } | 
|  | if let Some(test_flags) = env.get("TEST_FLAGS") { | 
|  | rustflags.extend_from_slice(&split_args(test_flags)?); | 
|  | } | 
|  |  | 
|  | if let Some(linker) = linker { | 
|  | rustflags.push(linker.to_string()); | 
|  | } | 
|  |  | 
|  | if self.no_default_features { | 
|  | rustflags.push("-Csymbol-mangling-version=v0".to_string()); | 
|  | } | 
|  |  | 
|  | // FIXME(antoyo): remove once the atomic shim is gone | 
|  | if os_name == "Darwin" { | 
|  | rustflags.extend_from_slice(&[ | 
|  | "-Clink-arg=-undefined".to_string(), | 
|  | "-Clink-arg=dynamic_lookup".to_string(), | 
|  | ]); | 
|  | } | 
|  | env.insert("RUSTFLAGS".to_string(), rustflags.join(" ")); | 
|  | // display metadata load errors | 
|  | env.insert("RUSTC_LOG".to_string(), "warn".to_string()); | 
|  |  | 
|  | let sysroot = current_dir | 
|  | .join(get_sysroot_dir()) | 
|  | .join(format!("sysroot/lib/rustlib/{}/lib", self.target_triple)); | 
|  | let ld_library_path = format!( | 
|  | "{target}:{sysroot}:{gcc_path}", | 
|  | target = self.cargo_target_dir, | 
|  | sysroot = sysroot.display(), | 
|  | gcc_path = gcc_path, | 
|  | ); | 
|  | env.insert("LIBRARY_PATH".to_string(), ld_library_path.clone()); | 
|  | env.insert("LD_LIBRARY_PATH".to_string(), ld_library_path.clone()); | 
|  | env.insert("DYLD_LIBRARY_PATH".to_string(), ld_library_path); | 
|  |  | 
|  | // NOTE: To avoid the -fno-inline errors, use /opt/gcc/bin/gcc instead of cc. | 
|  | // To do so, add a symlink for cc to /opt/gcc/bin/gcc in our PATH. | 
|  | // Another option would be to add the following Rust flag: -Clinker=/opt/gcc/bin/gcc | 
|  | let path = std::env::var("PATH").unwrap_or_default(); | 
|  | env.insert( | 
|  | "PATH".to_string(), | 
|  | format!( | 
|  | "/opt/gcc/bin:/opt/m68k-unknown-linux-gnu/bin{}{}", | 
|  | if path.is_empty() { "" } else { ":" }, | 
|  | path | 
|  | ), | 
|  | ); | 
|  |  | 
|  | self.rustc_command = vec![rustc]; | 
|  | self.rustc_command.extend_from_slice(&rustflags); | 
|  | self.rustc_command.extend_from_slice(&[ | 
|  | "-L".to_string(), | 
|  | format!("crate={}", self.cargo_target_dir), | 
|  | "--out-dir".to_string(), | 
|  | self.cargo_target_dir.clone(), | 
|  | ]); | 
|  |  | 
|  | if !env.contains_key("RUSTC_LOG") { | 
|  | env.insert("RUSTC_LOG".to_string(), "warn".to_string()); | 
|  | } | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | pub fn show_usage() { | 
|  | println!( | 
|  | "\ | 
|  | --features [arg]       : Add a new feature [arg] | 
|  | --target-triple [arg]  : Set the target triple to [arg] | 
|  | --target [arg]         : Set the target to [arg] | 
|  | --out-dir              : Location where the files will be generated | 
|  | --release              : Build in release mode | 
|  | --release-sysroot      : Build sysroot in release mode | 
|  | --sysroot-panic-abort  : Build the sysroot without unwinding support | 
|  | --config-file          : Location of the config file to be used | 
|  | --gcc-path             : Location of the GCC root folder | 
|  | --cg_gcc-path          : Location of the rustc_codegen_gcc root folder (used | 
|  | when ran from another directory) | 
|  | --no-default-features  : Add `--no-default-features` flag to cargo commands | 
|  | --use-backend          : Useful only for rustc testsuite" | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | fn download_gccjit( | 
|  | commit: &str, | 
|  | output_dir: &Path, | 
|  | tempfile_name: String, | 
|  | with_progress_bar: bool, | 
|  | ) -> Result<(), String> { | 
|  | let url = if std::env::consts::OS == "linux" && std::env::consts::ARCH == "x86_64" { | 
|  | format!("https://github.com/rust-lang/gcc/releases/download/master-{commit}/libgccjit.so") | 
|  | } else { | 
|  | eprintln!( | 
|  | "\ | 
|  | Pre-compiled libgccjit.so not available for this os or architecture. | 
|  | Please compile it yourself and update the `config.toml` file | 
|  | to `download-gccjit = false` and set `gcc-path` to the appropriate directory." | 
|  | ); | 
|  | return Err(String::from( | 
|  | "no appropriate pre-compiled libgccjit.so available for download", | 
|  | )); | 
|  | }; | 
|  |  | 
|  | println!("Downloading `{url}`..."); | 
|  |  | 
|  | // Try curl. If that fails and we are on windows, fallback to PowerShell. | 
|  | let mut ret = run_command_with_output( | 
|  | &[ | 
|  | &"curl", | 
|  | &"--speed-time", | 
|  | &"30", | 
|  | &"--speed-limit", | 
|  | &"10", // timeout if speed is < 10 bytes/sec for > 30 seconds | 
|  | &"--connect-timeout", | 
|  | &"30", // timeout if cannot connect within 30 seconds | 
|  | &"-o", | 
|  | &tempfile_name, | 
|  | &"--retry", | 
|  | &"3", | 
|  | &"-SRfL", | 
|  | if with_progress_bar { &"--progress-bar" } else { &"-s" }, | 
|  | &url.as_str(), | 
|  | ], | 
|  | Some(output_dir), | 
|  | ); | 
|  | if ret.is_err() && cfg!(windows) { | 
|  | eprintln!("Fallback to PowerShell"); | 
|  | ret = run_command_with_output( | 
|  | &[ | 
|  | &"PowerShell.exe", | 
|  | &"/nologo", | 
|  | &"-Command", | 
|  | &"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;", | 
|  | &format!( | 
|  | "(New-Object System.Net.WebClient).DownloadFile('{url}', '{tempfile_name}')", | 
|  | ) | 
|  | .as_str(), | 
|  | ], | 
|  | Some(output_dir), | 
|  | ); | 
|  | } | 
|  | ret | 
|  | } |