use std::env;
use std::ffi::OsStr;
#[cfg(target_family = "unix")]
use std::fs::OpenOptions;
use std::io::Write;
#[cfg(target_family = "unix")]
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::str;
use anyhow::{anyhow, bail, Context, Result};
use fs_err as fs;
use path_slash::PathBufExt;
use serde::Deserialize;
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
use crate::linux::ARM_FEATURES_H;
use crate::macos::{LIBCHARSET_TBD, LIBICONV_TBD};
#[derive(Clone, Debug, clap::Subcommand)]
pub enum Zig {
#[command(name = "cc")]
Cc {
#[arg(num_args = 1.., trailing_var_arg = true)]
args: Vec<String>,
},
#[command(name = "c++")]
Cxx {
#[arg(num_args = 1.., trailing_var_arg = true)]
args: Vec<String>,
},
#[command(name = "ar")]
Ar {
#[arg(num_args = 1.., trailing_var_arg = true)]
args: Vec<String>,
},
#[command(name = "ranlib")]
Ranlib {
#[arg(num_args = 1.., trailing_var_arg = true)]
args: Vec<String>,
},
}
struct TargetInfo {
target: Option<String>,
is_musl: bool,
is_windows_gnu: bool,
is_windows_msvc: bool,
is_arm: bool,
is_i386: bool,
is_riscv64: bool,
is_mips32: bool,
is_macos: bool,
is_ohos: bool,
}
impl TargetInfo {
fn new(target: Option<&String>) -> Self {
Self {
target: target.cloned(),
is_musl: target.map(|x| x.contains("musl")).unwrap_or_default(),
is_windows_gnu: target
.map(|x| x.contains("windows-gnu"))
.unwrap_or_default(),
is_windows_msvc: target
.map(|x| x.contains("windows-msvc"))
.unwrap_or_default(),
is_arm: target.map(|x| x.starts_with("arm")).unwrap_or_default(),
is_i386: target.map(|x| x.starts_with("i386")).unwrap_or_default(),
is_riscv64: target.map(|x| x.starts_with("riscv64")).unwrap_or_default(),
is_mips32: target
.map(|x| x.starts_with("mips") && !x.starts_with("mips64"))
.unwrap_or_default(),
is_macos: target.map(|x| x.contains("macos")).unwrap_or_default(),
is_ohos: target.map(|x| x.contains("ohos")).unwrap_or_default(),
}
}
}
impl Zig {
pub fn execute(&self) -> Result<()> {
match self {
Zig::Cc { args } => self.execute_compiler("cc", args),
Zig::Cxx { args } => self.execute_compiler("c++", args),
Zig::Ar { args } => self.execute_tool("ar", args),
Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
}
}
pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
let target = cmd_args
.iter()
.position(|x| x == "-target")
.and_then(|index| cmd_args.get(index + 1));
let target_info = TargetInfo::new(target);
let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
Ok(version) => version.parse()?,
Err(_) => rustc_version::version()?,
};
let zig_version = Zig::zig_version()?;
let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
let mut skip_next_arg = false;
for arg in cmd_args {
if skip_next_arg {
skip_next_arg = false;
continue;
}
let arg = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
Some(self.process_linker_response_file(
arg,
&rustc_ver,
&zig_version,
&target_info,
)?)
} else {
self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info)
};
if let Some(arg) = arg {
if arg == "-Wl,-exported_symbols_list" {
skip_next_arg = true;
} else {
new_cmd_args.push(arg);
}
}
}
if target_info.is_mips32 {
new_cmd_args.push("-Wl,-z,notext".to_string());
}
if self.has_undefined_dynamic_lookup(cmd_args) {
new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
}
if target_info.is_macos {
if self.should_add_libcharset(cmd_args, &zig_version) {
new_cmd_args.push("-lcharset".to_string());
}
self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
}
let mut child = Self::command()?
.arg(cmd)
.args(new_cmd_args)
.spawn()
.with_context(|| format!("Failed to run `zig {cmd}`"))?;
let status = child.wait().expect("Failed to wait on zig child process");
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
Ok(())
}
fn process_linker_response_file(
&self,
arg: &str,
rustc_ver: &rustc_version::Version,
zig_version: &semver::Version,
target_info: &TargetInfo,
) -> Result<String> {
let content_bytes = fs::read(arg.trim_start_matches('@'))?;
let content = if target_info.is_windows_msvc {
if content_bytes[0..2] != [255, 254] {
bail!(
"linker response file `{}` didn't start with a utf16 BOM",
&arg
);
}
let content_utf16: Vec<u16> = content_bytes[2..]
.chunks_exact(2)
.map(|a| u16::from_ne_bytes([a[0], a[1]]))
.collect();
String::from_utf16(&content_utf16).with_context(|| {
format!(
"linker response file `{}` didn't contain valid utf16 content",
&arg
)
})?
} else {
String::from_utf8(content_bytes).with_context(|| {
format!(
"linker response file `{}` didn't contain valid utf8 content",
&arg
)
})?
};
let mut link_args: Vec<_> = content
.split('\n')
.filter_map(|arg| self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info))
.collect();
if self.has_undefined_dynamic_lookup(&link_args) {
link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
}
if target_info.is_macos && self.should_add_libcharset(&link_args, &zig_version) {
link_args.push("-lcharset".to_string());
}
if target_info.is_windows_msvc {
let new_content = link_args.join("\n");
let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
out.push(c as u8);
out.push((c >> 8) as u8);
}
fs::write(arg.trim_start_matches('@'), out)?;
} else {
fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
}
Ok(arg.to_string())
}
fn filter_linker_arg(
&self,
arg: &str,
rustc_ver: &rustc_version::Version,
zig_version: &semver::Version,
target_info: &TargetInfo,
) -> Option<String> {
if arg == "-lgcc_s" {
return Some("-lunwind".to_string());
} else if arg.starts_with("--target=") {
return None;
}
if (target_info.is_arm || target_info.is_windows_gnu)
&& arg.ends_with(".rlib")
&& arg.contains("libcompiler_builtins-")
{
return None;
}
if target_info.is_windows_gnu {
#[allow(clippy::if_same_then_else)]
if arg == "-lgcc_eh" {
return Some("-lc++".to_string());
} else if arg == "-Wl,-Bdynamic" && (zig_version.major, zig_version.minor) >= (0, 11) {
return Some("-Wl,-search_paths_first".to_owned());
} else if arg == "-lwindows" || arg == "-l:libpthread.a" || arg == "-lgcc" {
return None;
} else if arg == "-Wl,--disable-auto-image-base"
|| arg == "-Wl,--dynamicbase"
|| arg == "-Wl,--large-address-aware"
|| (arg.starts_with("-Wl,")
&& (arg.ends_with("/list.def") || arg.ends_with("\\list.def")))
{
return None;
} else if arg == "-lmsvcrt" {
return None;
}
} else if arg == "-Wl,--no-undefined-version" {
return None;
}
if target_info.is_musl || target_info.is_ohos {
if arg.ends_with(".o") && arg.contains("self-contained") && arg.contains("crt") {
return None;
} else if arg == "-Wl,-melf_i386" {
return None;
}
if rustc_ver.major == 1
&& rustc_ver.minor < 59
&& arg.ends_with(".rlib")
&& arg.contains("liblibc-")
{
return None;
}
if arg == "-lc" {
return None;
}
}
if arg.starts_with("-march=") {
if target_info.is_arm || target_info.is_i386 {
return None;
} else if target_info.is_riscv64 {
return Some("-march=generic_rv64".to_string());
} else if arg.starts_with("-march=armv8-a") {
if target_info
.target
.as_ref()
.map(|x| x.starts_with("aarch64-macos"))
.unwrap_or_default()
{
return Some(arg.replace("armv8-a", "apple_m1"));
} else if target_info
.target
.as_ref()
.map(|x| x.starts_with("aarch64-linux"))
.unwrap_or_default()
{
return Some(
arg.replace("armv8-a", "generic+v8a")
.replace("simd", "neon"),
);
}
}
}
if target_info.is_macos {
if arg.starts_with("-Wl,-exported_symbols_list,") {
return None;
}
if arg == "-Wl,-dylib" {
return None;
}
}
Some(arg.to_string())
}
fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
let undefined = args
.iter()
.position(|x| x == "-undefined")
.and_then(|i| args.get(i + 1));
matches!(undefined, Some(x) if x == "dynamic_lookup")
}
fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
if (zig_version.major, zig_version.minor) >= (0, 12) {
args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
} else {
false
}
}
fn add_macos_specific_args(
&self,
new_cmd_args: &mut Vec<String>,
zig_version: &semver::Version,
) -> Result<()> {
let sdkroot = Self::macos_sdk_root();
if (zig_version.major, zig_version.minor) >= (0, 12) {
if let Some(ref sdkroot) = sdkroot {
new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
}
}
if let Some(ref sdkroot) = sdkroot {
new_cmd_args.extend_from_slice(&[
"-isystem".to_string(),
format!("{}", sdkroot.join("usr").join("include").display()),
format!("-L{}", sdkroot.join("usr").join("lib").display()),
format!(
"-F{}",
sdkroot
.join("System")
.join("Library")
.join("Frameworks")
.display()
),
"-DTARGET_OS_IPHONE=0".to_string(),
]);
}
let cache_dir = cache_dir();
let deps_dir = cache_dir.join("deps");
fs::create_dir_all(&deps_dir)?;
write_tbd_files(&deps_dir)?;
new_cmd_args.push("-L".to_string());
new_cmd_args.push(format!("{}", deps_dir.display()));
Ok(())
}
pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
let mut child = Self::command()?
.arg(cmd)
.args(cmd_args)
.spawn()
.with_context(|| format!("Failed to run `zig {cmd}`"))?;
let status = child.wait().expect("Failed to wait on zig child process");
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
Ok(())
}
pub fn command() -> Result<Command> {
let (zig, zig_args) = Self::find_zig()?;
let mut cmd = Command::new(zig);
cmd.args(zig_args);
Ok(cmd)
}
fn zig_version() -> Result<semver::Version> {
let output = Self::command()?.arg("version").output()?;
let version_str =
str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
let version = semver::Version::parse(version_str.trim())?;
Ok(version)
}
pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
Self::find_zig_python()
.or_else(|_| Self::find_zig_bin())
.context("Failed to find zig")
}
fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
let zig_path = zig_path()?;
let output = Command::new(&zig_path).arg("version").output()?;
let version_str = str::from_utf8(&output.stdout).with_context(|| {
format!("`{} version` didn't return utf8 output", zig_path.display())
})?;
Self::validate_zig_version(version_str)?;
Ok((zig_path, Vec::new()))
}
fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
let python_path = python_path()?;
let output = Command::new(&python_path)
.args(["-m", "ziglang", "version"])
.output()?;
let version_str = str::from_utf8(&output.stdout).with_context(|| {
format!(
"`{} -m ziglang version` didn't return utf8 output",
python_path.display()
)
})?;
Self::validate_zig_version(version_str)?;
Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
}
fn validate_zig_version(version: &str) -> Result<()> {
let min_ver = semver::Version::new(0, 9, 0);
let version = semver::Version::parse(version.trim())?;
if version >= min_ver {
Ok(())
} else {
bail!(
"zig version {} is too old, need at least {}",
version,
min_ver
)
}
}
pub fn lib_dir() -> Result<PathBuf> {
let (zig, zig_args) = Self::find_zig()?;
let output = Command::new(zig).args(zig_args).arg("env").output()?;
let zig_env: ZigEnv = serde_json::from_slice(&output.stdout)?;
Ok(PathBuf::from(zig_env.lib_dir))
}
fn add_env_if_missing<K, V>(command: &mut Command, name: K, value: V)
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let command_env_contains_no_key =
|name: &K| !command.get_envs().any(|(key, _)| name.as_ref() == key);
if command_env_contains_no_key(&name) && env::var_os(&name).is_none() {
command.env(name, value);
}
}
pub(crate) fn apply_command_env(
manifest_path: Option<&Path>,
release: bool,
cargo: &cargo_options::CommonOptions,
cmd: &mut Command,
enable_zig_ar: bool,
) -> Result<()> {
let rust_targets = cargo
.target
.iter()
.map(|target| target.split_once('.').map(|(t, _)| t).unwrap_or(target))
.collect::<Vec<&str>>();
let rustc_meta = rustc_version::version_meta()?;
Self::add_env_if_missing(
cmd,
"CARGO_ZIGBUILD_RUSTC_VERSION",
rustc_meta.semver.to_string(),
);
let host_target = &rustc_meta.host;
for (parsed_target, raw_target) in rust_targets.iter().zip(&cargo.target) {
let env_target = parsed_target.replace('-', "_");
let zig_wrapper = prepare_zig_linker(raw_target)?;
if is_mingw_shell() {
let zig_cc = zig_wrapper.cc.to_slash_lossy();
let zig_cxx = zig_wrapper.cxx.to_slash_lossy();
Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &*zig_cc);
Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &*zig_cxx);
if !parsed_target.contains("wasm") {
Self::add_env_if_missing(
cmd,
format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
&*zig_cc,
);
}
} else {
Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &zig_wrapper.cc);
Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &zig_wrapper.cxx);
if !parsed_target.contains("wasm") {
Self::add_env_if_missing(
cmd,
format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
&zig_wrapper.cc,
);
}
}
Self::add_env_if_missing(cmd, format!("RANLIB_{env_target}"), &zig_wrapper.ranlib);
if enable_zig_ar {
Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.ar);
}
Self::setup_os_deps(manifest_path, release, cargo)?;
let cmake_toolchain_file_env = format!("CMAKE_TOOLCHAIN_FILE_{env_target}");
if env::var_os(&cmake_toolchain_file_env).is_none()
&& env::var_os(format!("CMAKE_TOOLCHAIN_FILE_{parsed_target}")).is_none()
&& env::var_os("TARGET_CMAKE_TOOLCHAIN_FILE").is_none()
&& env::var_os("CMAKE_TOOLCHAIN_FILE").is_none()
{
if let Ok(cmake_toolchain_file) =
Self::setup_cmake_toolchain(parsed_target, &zig_wrapper, enable_zig_ar)
{
cmd.env(cmake_toolchain_file_env, cmake_toolchain_file);
}
}
if raw_target.contains("windows-gnu") {
cmd.env("WINAPI_NO_BUNDLED_LIBRARIES", "1");
}
if raw_target.contains("apple-darwin") {
if let Some(sdkroot) = Self::macos_sdk_root() {
if env::var_os("PKG_CONFIG_SYSROOT_DIR").is_none() {
cmd.env("PKG_CONFIG_SYSROOT_DIR", sdkroot);
}
}
}
if host_target == parsed_target {
if !matches!(rustc_meta.channel, rustc_version::Channel::Nightly) {
cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
}
cmd.env("CARGO_UNSTABLE_TARGET_APPLIES_TO_HOST", "true");
cmd.env("CARGO_TARGET_APPLIES_TO_HOST", "false");
}
if let Ok(mut options) = Self::collect_zig_cc_options(&zig_wrapper, raw_target) {
if raw_target.contains("apple-darwin") {
options.push("-DTARGET_OS_IPHONE=0".to_string());
}
let escaped_options = shlex::try_join(options.iter().map(|s| &s[..]))?;
let bindgen_env = "BINDGEN_EXTRA_CLANG_ARGS";
let fallback_value = env::var(bindgen_env);
for target in [&env_target[..], parsed_target] {
let name = format!("{bindgen_env}_{target}");
if let Ok(mut value) = env::var(&name).or(fallback_value.clone()) {
if shlex::split(&value).is_none() {
value = shlex::try_quote(&value)?.into_owned();
}
if !value.is_empty() {
value.push(' ');
}
value.push_str(&escaped_options);
env::set_var(name, value);
} else {
env::set_var(name, escaped_options.clone());
}
}
}
}
Ok(())
}
fn collect_zig_cc_options(zig_wrapper: &ZigWrapper, raw_target: &str) -> Result<Vec<String>> {
#[derive(Debug, PartialEq, Eq)]
enum Kind {
Normal,
Framework,
}
#[derive(Debug)]
struct PerLanguageOptions {
glibc_minor_ver: Option<u32>,
include_paths: Vec<(Kind, String)>,
}
fn collect_per_language_options(
program: &Path,
ext: &str,
raw_target: &str,
) -> Result<PerLanguageOptions> {
let empty_file_path = cache_dir().join(format!(".intentionally-empty-file.{ext}"));
if !empty_file_path.exists() {
fs::write(&empty_file_path, "")?;
}
let output = Command::new(program)
.arg("-E")
.arg(&empty_file_path)
.arg("-v")
.output()?;
let stderr = String::from_utf8(output.stderr)?;
if !output.status.success() {
bail!(
"Failed to run `zig cc -v` with status {}: {}",
output.status,
stderr.trim(),
);
}
let glibc_minor_ver = if let Some(start) = stderr.find("__GLIBC_MINOR__=") {
let stderr = &stderr[start + 16..];
let end = stderr
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(stderr.len());
stderr[..end].parse().ok()
} else {
None
};
let start = stderr
.find("#include <...> search starts here:")
.ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?
+ 34;
let end = stderr
.find("End of search list.")
.ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?;
let mut include_paths = Vec::new();
for mut line in stderr[start..end].lines() {
line = line.trim();
let mut kind = Kind::Normal;
if line.ends_with(" (framework directory)") {
line = line[..line.len() - 22].trim();
kind = Kind::Framework;
} else if line.ends_with(" (headermap)") {
bail!("C/C++ search path includes header maps, which are not supported");
}
if !line.is_empty() {
include_paths.push((kind, line.to_owned()));
}
}
if raw_target.contains("ohos") {
let ndk = env::var("OHOS_NDK_HOME").expect("Can't get NDK path");
include_paths.push((Kind::Normal, format!("{}/native/sysroot/usr/include", ndk)));
}
Ok(PerLanguageOptions {
include_paths,
glibc_minor_ver,
})
}
let c_opts = collect_per_language_options(&zig_wrapper.cc, "c", raw_target)?;
let cpp_opts = collect_per_language_options(&zig_wrapper.cxx, "cpp", raw_target)?;
if c_opts.glibc_minor_ver != cpp_opts.glibc_minor_ver {
bail!(
"`zig cc` gives a different glibc minor version for C ({:?}) and C++ ({:?})",
c_opts.glibc_minor_ver,
cpp_opts.glibc_minor_ver,
);
}
let c_paths = c_opts.include_paths;
let mut cpp_paths = cpp_opts.include_paths;
let (cpp_pre_len, cpp_post_len) = if cpp_paths.starts_with(&c_paths) {
(0, cpp_paths.len() - c_paths.len())
} else if cpp_paths.ends_with(&c_paths) {
(cpp_paths.len() - c_paths.len(), 0)
} else {
bail!("C++ search path used by `zig cc` is unexpectedly different from C search path!");
};
if !cpp_paths[..cpp_pre_len]
.iter()
.chain(cpp_paths[cpp_paths.len() - cpp_post_len..].iter())
.all(|(kind, _)| *kind == Kind::Normal)
{
bail!("C++ search path used by `zig cc` contains additional special search paths!");
}
let mut args = Vec::new();
args.push("-nostdinc".to_owned());
if raw_target.contains("musl") || raw_target.contains("ohos") {
args.push("-D_LIBCPP_HAS_MUSL_LIBC".to_owned());
args.push("-D_LARGEFILE64_SOURCE".to_owned());
}
args.push("-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS".to_owned());
args.push("-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS".to_owned());
args.push("-D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS".to_owned());
args.push("-D_LIBCPP_PSTL_CPU_BACKEND_SERIAL".to_owned());
args.push("-D_LIBCPP_ABI_VERSION=1".to_owned());
args.push("-D_LIBCPP_ABI_NAMESPACE=__1".to_owned());
args.push("-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST".to_owned());
if let Some(ver) = c_opts.glibc_minor_ver {
args.push(format!("-D__GLIBC_MINOR__={ver}"));
}
for (kind, path) in cpp_paths.drain(..cpp_pre_len) {
assert!(kind == Kind::Normal);
args.push("-cxx-isystem".to_owned());
args.push(path);
}
for (kind, path) in c_paths {
match kind {
Kind::Normal => {
args.push("-Xclang".to_owned());
args.push("-c-isystem".to_owned());
args.push("-Xclang".to_owned());
args.push(path.clone());
args.push("-cxx-isystem".to_owned());
args.push(path);
}
Kind::Framework => {
args.push("-iframework".to_owned());
args.push(path);
}
}
}
for (kind, path) in cpp_paths.drain(cpp_paths.len() - cpp_post_len..) {
assert!(kind == Kind::Normal);
args.push("-cxx-isystem".to_owned());
args.push(path);
}
Ok(args)
}
fn setup_os_deps(
manifest_path: Option<&Path>,
release: bool,
cargo: &cargo_options::CommonOptions,
) -> Result<()> {
for target in &cargo.target {
if target.contains("apple") {
let target_dir = if let Some(target_dir) = cargo.target_dir.clone() {
target_dir.join(target)
} else {
let manifest_path = manifest_path.unwrap_or_else(|| Path::new("Cargo.toml"));
if !manifest_path.exists() {
continue;
}
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(manifest_path)
.no_deps()
.exec()?;
metadata.target_directory.into_std_path_buf().join(target)
};
let profile = match cargo.profile.as_deref() {
Some("dev" | "test") => "debug",
Some("release" | "bench") => "release",
Some(profile) => profile,
None => {
if release {
"release"
} else {
"debug"
}
}
};
let deps_dir = target_dir.join(profile).join("deps");
fs::create_dir_all(&deps_dir)?;
if !target_dir.join("CACHEDIR.TAG").is_file() {
let _ = write_file(
&target_dir.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
",
);
}
write_tbd_files(&deps_dir)?;
} else if target.contains("arm") && target.contains("linux") {
if let Ok(lib_dir) = Zig::lib_dir() {
let arm_features_h = lib_dir
.join("libc")
.join("glibc")
.join("sysdeps")
.join("arm")
.join("arm-features.h");
if !arm_features_h.is_file() {
fs::write(arm_features_h, ARM_FEATURES_H)?;
}
}
} else if target.contains("windows-gnu") {
if let Ok(lib_dir) = Zig::lib_dir() {
let lib_common = lib_dir.join("libc").join("mingw").join("lib-common");
let synchronization_def = lib_common.join("synchronization.def");
if !synchronization_def.is_file() {
let api_ms_win_core_synch_l1_2_0_def =
lib_common.join("api-ms-win-core-synch-l1-2-0.def");
fs::copy(api_ms_win_core_synch_l1_2_0_def, synchronization_def).ok();
}
}
}
}
Ok(())
}
fn setup_cmake_toolchain(
target: &str,
zig_wrapper: &ZigWrapper,
enable_zig_ar: bool,
) -> Result<PathBuf> {
let cmake = cache_dir().join("cmake");
fs::create_dir_all(&cmake)?;
let toolchain_file = cmake.join(format!("{target}-toolchain.cmake"));
let triple: Triple = target.parse()?;
let os = triple.operating_system.to_string();
let arch = triple.architecture.to_string();
let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
("darwin", "x86_64") => ("Darwin", "x86_64"),
("darwin", "aarch64") => ("Darwin", "arm64"),
("linux", arch) => {
let cmake_arch = match arch {
"powerpc" => "ppc",
"powerpc64" => "ppc64",
"powerpc64le" => "ppc64le",
_ => arch,
};
("Linux", cmake_arch)
}
("windows", "x86_64") => ("Windows", "AMD64"),
("windows", "i686") => ("Windows", "X86"),
("windows", "aarch64") => ("Windows", "ARM64"),
(os, arch) => (os, arch),
};
let mut content = format!(
r#"
set(CMAKE_SYSTEM_NAME {system_name})
set(CMAKE_SYSTEM_PROCESSOR {system_processor})
set(CMAKE_C_COMPILER {cc})
set(CMAKE_CXX_COMPILER {cxx})
set(CMAKE_RANLIB {ranlib})"#,
system_name = system_name,
system_processor = system_processor,
cc = zig_wrapper.cc.to_slash_lossy(),
cxx = zig_wrapper.cxx.to_slash_lossy(),
ranlib = zig_wrapper.ranlib.to_slash_lossy(),
);
if enable_zig_ar {
content.push_str(&format!(
"\nset(CMAKE_AR {})\n",
zig_wrapper.ar.to_slash_lossy()
));
}
write_file(&toolchain_file, &content)?;
Ok(toolchain_file)
}
#[cfg(target_os = "macos")]
fn macos_sdk_root() -> Option<PathBuf> {
match env::var_os("SDKROOT") {
Some(sdkroot) => {
if !sdkroot.is_empty() {
Some(sdkroot.into())
} else {
None
}
}
None => {
let output = Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output();
if let Ok(output) = output {
if output.status.success() {
if let Ok(stdout) = String::from_utf8(output.stdout) {
let stdout = stdout.trim();
if !stdout.is_empty() {
return Some(stdout.into());
}
}
}
}
None
}
}
}
#[cfg(not(target_os = "macos"))]
fn macos_sdk_root() -> Option<PathBuf> {
match env::var_os("SDKROOT") {
Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
_ => None,
}
}
}
fn write_file(path: &Path, content: &str) -> Result<(), anyhow::Error> {
let existing_content = fs::read_to_string(path).unwrap_or_default();
if existing_content != content {
fs::write(path, content)?;
}
Ok(())
}
fn write_tbd_files(deps_dir: &Path) -> Result<(), anyhow::Error> {
write_file(&deps_dir.join("libiconv.tbd"), LIBICONV_TBD)?;
write_file(&deps_dir.join("libcharset.1.tbd"), LIBCHARSET_TBD)?;
write_file(&deps_dir.join("libcharset.tbd"), LIBCHARSET_TBD)?;
Ok(())
}
fn cache_dir() -> PathBuf {
env::var("CARGO_ZIGBUILD_CACHE_DIR")
.ok()
.map(|s| s.into())
.or_else(dirs::cache_dir)
.unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
.join(env!("CARGO_PKG_NAME"))
.join(env!("CARGO_PKG_VERSION"))
}
#[derive(Debug, Deserialize)]
struct ZigEnv {
lib_dir: String,
}
#[derive(Debug, Clone)]
pub struct ZigWrapper {
pub cc: PathBuf,
pub cxx: PathBuf,
pub ar: PathBuf,
pub ranlib: PathBuf,
}
#[derive(Debug, Clone, Default, PartialEq)]
struct TargetFlags {
pub target_cpu: String,
pub target_feature: String,
}
impl TargetFlags {
pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
let mut parsed = Self::default();
let f = rustflags::from_encoded(encoded);
for flag in f {
if let rustflags::Flag::Codegen { opt, value } = flag {
let key = opt.replace('-', "_");
match key.as_str() {
"target_cpu" => {
if let Some(value) = value {
parsed.target_cpu = value;
}
}
"target_feature" => {
if let Some(value) = value {
if !parsed.target_feature.is_empty() {
parsed.target_feature.push(',');
}
parsed.target_feature.push_str(&value);
}
}
_ => {}
}
}
}
Ok(parsed)
}
}
#[allow(clippy::blocks_in_conditions)]
pub fn prepare_zig_linker(target: &str) -> Result<ZigWrapper> {
let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
let abi_suffix = if abi_suffix.is_empty() {
String::new()
} else {
if abi_suffix
.split_once('.')
.filter(|(x, y)| {
!x.is_empty()
&& x.chars().all(|c| c.is_ascii_digit())
&& !y.is_empty()
&& y.chars().all(|c| c.is_ascii_digit())
})
.is_none()
{
bail!("Malformed zig target abi suffix.")
}
format!(".{abi_suffix}")
};
let triple: Triple = rust_target
.parse()
.with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
let arch = triple.architecture.to_string();
let target_env = match (triple.architecture, triple.environment) {
(Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
(Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
(_, Environment::GnuLlvm) => Environment::Gnu,
(_, environment) => environment,
};
let file_ext = if cfg!(windows) { "bat" } else { "sh" };
let file_target = target.trim_end_matches('.');
let mut cc_args = vec!["-g".to_owned()]; let zig_mcpu_default = match triple.operating_system {
OperatingSystem::Linux => {
match arch.as_str() {
"arm" => match target_env {
Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
Environment::Gnueabihf | Environment::Musleabihf => {
"generic+v6+strict_align+vfp2-d32"
}
_ => "",
},
"armv5te" => "generic+soft_float+strict_align",
"armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
arch_str @ ("i586" | "i686") => {
if arch_str == "i586" {
"pentium"
} else {
"pentium4"
}
}
"riscv64gc" => "generic_rv64+m+a+f+d+c",
"s390x" => "z10-vector",
_ => "",
}
}
_ => "",
};
let zig_mcpu_override = {
let cargo_config = cargo_config2::Config::load()?;
let rust_flags = cargo_config.rustflags(rust_target)?.unwrap_or_default();
let encoded_rust_flags = rust_flags.encode()?;
let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
target_flags.target_cpu.replace('-', "_")
};
if !zig_mcpu_override.is_empty() {
cc_args.push(format!("-mcpu={zig_mcpu_override}"));
} else if !zig_mcpu_default.is_empty() {
cc_args.push(format!("-mcpu={zig_mcpu_default}"));
}
match triple.operating_system {
OperatingSystem::Linux => {
let zig_arch = match arch.as_str() {
"arm" => "arm",
"armv5te" => "arm",
"armv7" => "arm",
"i586" | "i686" => {
let zig_version = Zig::zig_version()?;
if zig_version.major == 0 && zig_version.minor >= 11 {
"x86"
} else {
"i386"
}
}
"riscv64gc" => "riscv64",
"s390x" => "s390x",
_ => arch.as_str(),
};
cc_args.push(format!("-target {zig_arch}-linux-{target_env}{abi_suffix}"));
}
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
let zig_version = Zig::zig_version()?;
if zig_version > semver::Version::new(0, 9, 1) {
cc_args.push(format!("-target {arch}-macos-none{abi_suffix}"));
} else {
cc_args.push(format!("-target {arch}-macos-gnu{abi_suffix}"));
}
}
OperatingSystem::Windows { .. } => {
let zig_arch = match arch.as_str() {
"i686" => {
let zig_version = Zig::zig_version()?;
if zig_version.major == 0 && zig_version.minor >= 11 {
"x86"
} else {
"i386"
}
}
arch => arch,
};
cc_args.push(format!(
"-target {zig_arch}-windows-{target_env}{abi_suffix}"
));
}
OperatingSystem::Emscripten => {
cc_args.push(format!("-target {arch}-emscripten{abi_suffix}"));
}
OperatingSystem::Wasi => {
cc_args.push(format!("-target {arch}-wasi{abi_suffix}"));
}
OperatingSystem::WasiP1 => {
cc_args.push(format!("-target {arch}-wasi.0.1.0{abi_suffix}"));
}
OperatingSystem::Unknown => {
if triple.architecture == Architecture::Wasm32
|| triple.architecture == Architecture::Wasm64
{
cc_args.push(format!("-target {arch}-freestanding{abi_suffix}"));
} else {
bail!("unsupported target '{rust_target}'")
}
}
_ => bail!(format!("unsupported target '{rust_target}'")),
};
let zig_linker_dir = cache_dir();
fs::create_dir_all(&zig_linker_dir)?;
if triple.operating_system == OperatingSystem::Linux {
if matches!(
triple.environment,
Environment::Gnu
| Environment::Gnuspe
| Environment::Gnux32
| Environment::Gnueabi
| Environment::Gnuabi64
| Environment::GnuIlp32
| Environment::Gnueabihf
) {
let glibc_version = if abi_suffix.is_empty() {
(2, 17)
} else {
let mut parts = abi_suffix[1..].split('.');
let major: usize = parts.next().unwrap().parse()?;
let minor: usize = parts.next().unwrap().parse()?;
(major, minor)
};
if glibc_version < (2, 28) {
use crate::linux::{FCNTL_H, FCNTL_MAP};
let zig_version = Zig::zig_version()?;
if zig_version.major == 0 && zig_version.minor < 11 {
let fcntl_map = zig_linker_dir.join("fcntl.map");
let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
if existing_content != FCNTL_MAP {
fs::write(&fcntl_map, FCNTL_MAP)?;
}
let fcntl_h = zig_linker_dir.join("fcntl.h");
let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
if existing_content != FCNTL_H {
fs::write(&fcntl_h, FCNTL_H)?;
}
cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
cc_args.push(format!("-include {}", fcntl_h.display()));
}
}
} else if matches!(
triple.environment,
Environment::Musl
| Environment::Muslabi64
| Environment::Musleabi
| Environment::Musleabihf
) {
use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
let zig_version = Zig::zig_version()?;
let rustc_version = rustc_version::version_meta()?.semver;
if (zig_version.major, zig_version.minor) >= (0, 11)
&& (rustc_version.major, rustc_version.minor) < (1, 72)
{
let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
}
}
}
let cc_args_str = cc_args.join(" ");
let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
let zig_cc = zig_linker_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
let zig_cxx = zig_linker_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
let zig_ranlib = zig_linker_dir.join(format!("zigranlib.{file_ext}"));
write_linker_wrapper(&zig_cc, "cc", &cc_args_str)?;
write_linker_wrapper(&zig_cxx, "c++", &cc_args_str)?;
write_linker_wrapper(&zig_ranlib, "ranlib", "")?;
let exe_ext = if cfg!(windows) { ".exe" } else { "" };
let zig_ar = zig_linker_dir.join(format!("ar{exe_ext}"));
symlink_wrapper(&zig_ar)?;
Ok(ZigWrapper {
cc: zig_cc,
cxx: zig_cxx,
ar: zig_ar,
ranlib: zig_ranlib,
})
}
fn symlink_wrapper(target: &Path) -> Result<()> {
let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
PathBuf::from(exe)
} else {
env::current_exe()?
};
#[cfg(windows)]
{
if !target.exists() {
if std::fs::hard_link(¤t_exe, target).is_err() {
std::fs::copy(¤t_exe, target)?;
}
}
}
#[cfg(unix)]
{
if !target.exists() {
if fs::read_link(target).is_ok() {
fs::remove_file(target)?;
}
std::os::unix::fs::symlink(current_exe, target)?;
}
}
Ok(())
}
#[cfg(target_family = "unix")]
fn write_linker_wrapper(path: &Path, command: &str, args: &str) -> Result<()> {
let mut buf = Vec::<u8>::new();
let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
PathBuf::from(exe)
} else {
env::current_exe()?
};
writeln!(&mut buf, "#!/bin/sh")?;
writeln!(
&mut buf,
"exec \"{}\" zig {} -- {} \"$@\"",
current_exe.display(),
command,
args
)?;
let existing_content = fs::read(path).unwrap_or_default();
if existing_content != buf {
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o700)
.open(path)?
.write_all(&buf)?;
}
Ok(())
}
#[cfg(not(target_family = "unix"))]
fn write_linker_wrapper(path: &Path, command: &str, args: &str) -> Result<()> {
let mut buf = Vec::<u8>::new();
let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
PathBuf::from(exe)
} else {
env::current_exe()?
};
let current_exe = if is_mingw_shell() {
current_exe.to_slash_lossy().to_string()
} else {
current_exe.display().to_string()
};
writeln!(
&mut buf,
"\"{}\" zig {} -- {} %*",
adjust_canonicalization(current_exe),
command,
args
)?;
let existing_content = fs::read(path).unwrap_or_default();
if existing_content != buf {
fs::write(path, buf)?;
}
Ok(())
}
pub(crate) fn is_mingw_shell() -> bool {
env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
}
#[cfg(target_os = "windows")]
pub fn adjust_canonicalization(p: String) -> String {
const VERBATIM_PREFIX: &str = r#"\\?\"#;
if p.starts_with(VERBATIM_PREFIX) {
p[VERBATIM_PREFIX.len()..].to_string()
} else {
p
}
}
fn python_path() -> Result<PathBuf> {
let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
Ok(which::which(python)?)
}
fn zig_path() -> Result<PathBuf> {
let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
Ok(which::which(zig)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_target_flags() {
let cases = [
("-C target-feature=-crt-static", "", "-crt-static"),
("-C target-cpu=native", "native", ""),
(
"--deny warnings --codegen target-feature=+crt-static",
"",
"+crt-static",
),
("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
(
"-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
"native",
"-avx512bf16,-avx512bitalg",
),
(
"--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
"x86-64",
"",
),
(
"-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
"",
"+crt-static,+avx",
),
];
for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
let args = cargo_config2::Flags::from_space_separated(input);
let encoded_rust_flags = args.encode().unwrap();
let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
}
}
}