cargo_zigbuild/
zig.rs

1use std::env;
2use std::ffi::OsStr;
3#[cfg(target_family = "unix")]
4use std::fs::OpenOptions;
5use std::io::Write;
6#[cfg(target_family = "unix")]
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::{Path, PathBuf};
9use std::process::{self, Command};
10use std::str;
11
12use anyhow::{anyhow, bail, Context, Result};
13use fs_err as fs;
14use path_slash::PathBufExt;
15use serde::Deserialize;
16use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
17
18use crate::linux::ARM_FEATURES_H;
19use crate::macos::{LIBCHARSET_TBD, LIBICONV_TBD};
20
21/// Zig linker wrapper
22#[derive(Clone, Debug, clap::Subcommand)]
23pub enum Zig {
24    /// `zig cc` wrapper
25    #[command(name = "cc")]
26    Cc {
27        /// `zig cc` arguments
28        #[arg(num_args = 1.., trailing_var_arg = true)]
29        args: Vec<String>,
30    },
31    /// `zig c++` wrapper
32    #[command(name = "c++")]
33    Cxx {
34        /// `zig c++` arguments
35        #[arg(num_args = 1.., trailing_var_arg = true)]
36        args: Vec<String>,
37    },
38    /// `zig ar` wrapper
39    #[command(name = "ar")]
40    Ar {
41        /// `zig ar` arguments
42        #[arg(num_args = 1.., trailing_var_arg = true)]
43        args: Vec<String>,
44    },
45    /// `zig ranlib` wrapper
46    #[command(name = "ranlib")]
47    Ranlib {
48        /// `zig ranlib` arguments
49        #[arg(num_args = 1.., trailing_var_arg = true)]
50        args: Vec<String>,
51    },
52    /// `zig lib` wrapper
53    #[command(name = "lib")]
54    Lib {
55        /// `zig lib` arguments
56        #[arg(num_args = 1.., trailing_var_arg = true)]
57        args: Vec<String>,
58    },
59}
60
61struct TargetInfo {
62    target: Option<String>,
63    is_musl: bool,
64    is_windows_gnu: bool,
65    is_windows_msvc: bool,
66    is_arm: bool,
67    is_i386: bool,
68    is_riscv64: bool,
69    is_mips32: bool,
70    is_macos: bool,
71    is_ohos: bool,
72}
73
74impl TargetInfo {
75    fn new(target: Option<&String>) -> Self {
76        Self {
77            target: target.cloned(),
78            is_musl: target.map(|x| x.contains("musl")).unwrap_or_default(),
79            is_windows_gnu: target
80                .map(|x| x.contains("windows-gnu"))
81                .unwrap_or_default(),
82            is_windows_msvc: target
83                .map(|x| x.contains("windows-msvc"))
84                .unwrap_or_default(),
85            is_arm: target.map(|x| x.starts_with("arm")).unwrap_or_default(),
86            is_i386: target.map(|x| x.starts_with("i386")).unwrap_or_default(),
87            is_riscv64: target.map(|x| x.starts_with("riscv64")).unwrap_or_default(),
88            is_mips32: target
89                .map(|x| x.starts_with("mips") && !x.starts_with("mips64"))
90                .unwrap_or_default(),
91            is_macos: target.map(|x| x.contains("macos")).unwrap_or_default(),
92            is_ohos: target.map(|x| x.contains("ohos")).unwrap_or_default(),
93        }
94    }
95}
96
97impl Zig {
98    /// Execute the underlying zig command
99    pub fn execute(&self) -> Result<()> {
100        match self {
101            Zig::Cc { args } => self.execute_compiler("cc", args),
102            Zig::Cxx { args } => self.execute_compiler("c++", args),
103            Zig::Ar { args } => self.execute_tool("ar", args),
104            Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
105            Zig::Lib { args } => self.execute_compiler("lib", args),
106        }
107    }
108
109    /// Execute zig cc/c++ command
110    pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
111        let target = cmd_args
112            .iter()
113            .position(|x| x == "-target")
114            .and_then(|index| cmd_args.get(index + 1));
115        let target_info = TargetInfo::new(target);
116
117        let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
118            Ok(version) => version.parse()?,
119            Err(_) => rustc_version::version()?,
120        };
121        let zig_version = Zig::zig_version()?;
122
123        let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
124        let mut skip_next_arg = false;
125        for arg in cmd_args {
126            if skip_next_arg {
127                skip_next_arg = false;
128                continue;
129            }
130            let args = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
131                vec![self.process_linker_response_file(
132                    arg,
133                    &rustc_ver,
134                    &zig_version,
135                    &target_info,
136                )?]
137            } else {
138                self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info)
139            };
140            for arg in args {
141                if arg == "-Wl,-exported_symbols_list" {
142                    // Filter out this and the next argument
143                    skip_next_arg = true;
144                } else {
145                    new_cmd_args.push(arg);
146                }
147            }
148        }
149
150        if target_info.is_mips32 {
151            // See https://github.com/ziglang/zig/issues/4925#issuecomment-1499823425
152            new_cmd_args.push("-Wl,-z,notext".to_string());
153        }
154
155        if self.has_undefined_dynamic_lookup(cmd_args) {
156            new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
157        }
158        if target_info.is_macos {
159            if self.should_add_libcharset(cmd_args, &zig_version) {
160                new_cmd_args.push("-lcharset".to_string());
161            }
162            self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
163        }
164
165        let mut child = Self::command()?
166            .arg(cmd)
167            .args(new_cmd_args)
168            .spawn()
169            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
170        let status = child.wait().expect("Failed to wait on zig child process");
171        if !status.success() {
172            process::exit(status.code().unwrap_or(1));
173        }
174        Ok(())
175    }
176
177    fn process_linker_response_file(
178        &self,
179        arg: &str,
180        rustc_ver: &rustc_version::Version,
181        zig_version: &semver::Version,
182        target_info: &TargetInfo,
183    ) -> Result<String> {
184        // rustc passes arguments to linker via an @-file when arguments are too long
185        // See https://github.com/rust-lang/rust/issues/41190
186        // and https://github.com/rust-lang/rust/blob/87937d3b6c302dfedfa5c4b94d0a30985d46298d/compiler/rustc_codegen_ssa/src/back/link.rs#L1373-L1382
187        let content_bytes = fs::read(arg.trim_start_matches('@'))?;
188        let content = if target_info.is_windows_msvc {
189            if content_bytes[0..2] != [255, 254] {
190                bail!(
191                    "linker response file `{}` didn't start with a utf16 BOM",
192                    &arg
193                );
194            }
195            let content_utf16: Vec<u16> = content_bytes[2..]
196                .chunks_exact(2)
197                .map(|a| u16::from_ne_bytes([a[0], a[1]]))
198                .collect();
199            String::from_utf16(&content_utf16).with_context(|| {
200                format!(
201                    "linker response file `{}` didn't contain valid utf16 content",
202                    &arg
203                )
204            })?
205        } else {
206            String::from_utf8(content_bytes).with_context(|| {
207                format!(
208                    "linker response file `{}` didn't contain valid utf8 content",
209                    &arg
210                )
211            })?
212        };
213        let mut link_args: Vec<_> = content
214            .split('\n')
215            .flat_map(|arg| self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info))
216            .collect();
217        if self.has_undefined_dynamic_lookup(&link_args) {
218            link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
219        }
220        if target_info.is_macos && self.should_add_libcharset(&link_args, &zig_version) {
221            link_args.push("-lcharset".to_string());
222        }
223        if target_info.is_windows_msvc {
224            let new_content = link_args.join("\n");
225            let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
226            // start the stream with a UTF-16 BOM
227            for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
228                // encode in little endian
229                out.push(c as u8);
230                out.push((c >> 8) as u8);
231            }
232            fs::write(arg.trim_start_matches('@'), out)?;
233        } else {
234            fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
235        }
236        Ok(arg.to_string())
237    }
238
239    fn filter_linker_arg(
240        &self,
241        arg: &str,
242        rustc_ver: &rustc_version::Version,
243        zig_version: &semver::Version,
244        target_info: &TargetInfo,
245    ) -> Vec<String> {
246        if arg == "-lgcc_s" {
247            // Replace libgcc_s with libunwind
248            return vec!["-lunwind".to_string()];
249        } else if arg.starts_with("--target=") {
250            // We have already passed target via `-target`
251            return vec![];
252        }
253        if (target_info.is_arm || target_info.is_windows_gnu)
254            && arg.ends_with(".rlib")
255            && arg.contains("libcompiler_builtins-")
256        {
257            // compiler-builtins is duplicated with zig's compiler-rt
258            return vec![];
259        }
260        if target_info.is_windows_gnu {
261            #[allow(clippy::if_same_then_else)]
262            if arg == "-lgcc_eh" {
263                // zig doesn't provide gcc_eh alternative
264                // We use libc++ to replace it on windows gnu targets
265                return vec!["-lc++".to_string()];
266            } else if arg == "-Wl,-Bdynamic" && (zig_version.major, zig_version.minor) >= (0, 11) {
267                // https://github.com/ziglang/zig/pull/16058
268                // zig changes the linker behavior, -Bdynamic won't search *.a for mingw, but this may be fixed in the later version
269                // here is a workaround to replace the linker switch with -search_paths_first, which will search for *.dll,*lib first,
270                // then fallback to *.a
271                return vec!["-Wl,-search_paths_first".to_owned()];
272            } else if arg == "-lwindows" || arg == "-l:libpthread.a" || arg == "-lgcc" {
273                return vec![];
274            } else if arg == "-Wl,--disable-auto-image-base"
275                || arg == "-Wl,--dynamicbase"
276                || arg == "-Wl,--large-address-aware"
277                || (arg.starts_with("-Wl,")
278                    && (arg.ends_with("/list.def") || arg.ends_with("\\list.def")))
279            {
280                // https://github.com/rust-lang/rust/blob/f0bc76ac41a0a832c9ee621e31aaf1f515d3d6a5/compiler/rustc_target/src/spec/windows_gnu_base.rs#L23
281                // https://github.com/rust-lang/rust/blob/2fb0e8d162a021f8a795fb603f5d8c0017855160/compiler/rustc_target/src/spec/windows_gnu_base.rs#L22
282                // https://github.com/rust-lang/rust/blob/f0bc76ac41a0a832c9ee621e31aaf1f515d3d6a5/compiler/rustc_target/src/spec/i686_pc_windows_gnu.rs#L16
283                // zig doesn't support --disable-auto-image-base, --dynamicbase and --large-address-aware
284                return vec![];
285            } else if arg == "-lmsvcrt" {
286                return vec![];
287            }
288        } else if arg == "-Wl,--no-undefined-version" {
289            // https://github.com/rust-lang/rust/blob/542ed2bf72b232b245ece058fc11aebb1ca507d7/compiler/rustc_codegen_ssa/src/back/linker.rs#L723
290            // zig doesn't support --no-undefined-version
291            return vec![];
292        } else if arg == "-Wl,-znostart-stop-gc" {
293            // https://github.com/rust-lang/rust/blob/c580c498a1fe144d7c5b2dfc7faab1a229aa288b/compiler/rustc_codegen_ssa/src/back/link.rs#L3371
294            // zig doesn't support -znostart-stop-gc
295            return vec![];
296        }
297        if target_info.is_musl || target_info.is_ohos {
298            // Avoids duplicated symbols with both zig musl libc and the libc crate
299            if arg.ends_with(".o") && arg.contains("self-contained") && arg.contains("crt") {
300                return vec![];
301            } else if arg == "-Wl,-melf_i386" {
302                // unsupported linker arg: -melf_i386
303                return vec![];
304            }
305            if rustc_ver.major == 1
306                && rustc_ver.minor < 59
307                && arg.ends_with(".rlib")
308                && arg.contains("liblibc-")
309            {
310                // Rust distributes standalone libc.a in self-contained for musl since 1.59.0
311                // See https://github.com/rust-lang/rust/pull/90527
312                return vec![];
313            }
314            if arg == "-lc" {
315                return vec![];
316            }
317        }
318        if arg.starts_with("-march=") {
319            // Ignore `-march` option for arm* targets, we use `generic` + cpu features instead
320            if target_info.is_arm || target_info.is_i386 {
321                return vec![];
322            } else if target_info.is_riscv64 {
323                return vec!["-march=generic_rv64".to_string()];
324            } else if arg.starts_with("-march=armv8-a") {
325                let mut args_march = if target_info
326                    .target
327                    .as_ref()
328                    .map(|x| x.starts_with("aarch64-macos"))
329                    .unwrap_or_default()
330                {
331                    vec![arg.replace("armv8-a", "apple_m1")]
332                } else if target_info
333                    .target
334                    .as_ref()
335                    .map(|x| x.starts_with("aarch64-linux"))
336                    .unwrap_or_default()
337                {
338                    vec![arg
339                        .replace("armv8-a", "generic+v8a")
340                        .replace("simd", "neon")]
341                } else {
342                    vec![arg.to_string()]
343                };
344                if arg == "-march=armv8-a+crypto" {
345                    // Workaround for building sha1-asm on aarch64
346                    // See:
347                    // https://github.com/rust-cross/cargo-zigbuild/issues/149
348                    // https://github.com/RustCrypto/asm-hashes/blob/master/sha1/build.rs#L17-L19
349                    // https://github.com/ziglang/zig/issues/10411
350                    args_march.append(&mut vec![
351                        "-Xassembler".to_owned(),
352                        "-march=armv8-a+crypto".to_owned(),
353                    ]);
354                }
355                return args_march;
356            }
357        }
358        if target_info.is_macos {
359            if arg.starts_with("-Wl,-exported_symbols_list,") {
360                // zig doesn't support -exported_symbols_list arg
361                // https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-exported_symbols_list
362                return vec![];
363            }
364            if arg == "-Wl,-dylib" {
365                // zig doesn't support -dylib
366                return vec![];
367            }
368        }
369        vec![arg.to_string()]
370    }
371
372    fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
373        let undefined = args
374            .iter()
375            .position(|x| x == "-undefined")
376            .and_then(|i| args.get(i + 1));
377        matches!(undefined, Some(x) if x == "dynamic_lookup")
378    }
379
380    fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
381        // See https://github.com/apple-oss-distributions/libiconv/blob/a167071feb7a83a01b27ec8d238590c14eb6faff/xcodeconfig/libiconv.xcconfig
382        if (zig_version.major, zig_version.minor) >= (0, 12) {
383            args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
384        } else {
385            false
386        }
387    }
388
389    fn add_macos_specific_args(
390        &self,
391        new_cmd_args: &mut Vec<String>,
392        zig_version: &semver::Version,
393    ) -> Result<()> {
394        let sdkroot = Self::macos_sdk_root();
395        if (zig_version.major, zig_version.minor) >= (0, 12) {
396            // Zig 0.12.0+ requires passing `--sysroot`
397            if let Some(ref sdkroot) = sdkroot {
398                new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
399            }
400        }
401        if let Some(ref sdkroot) = sdkroot {
402            let include_prefix = if (zig_version.major, zig_version.minor) < (0, 14) {
403                sdkroot
404            } else {
405                Path::new("/")
406            };
407            new_cmd_args.extend_from_slice(&[
408                "-isystem".to_string(),
409                format!("{}", include_prefix.join("usr").join("include").display()),
410                format!("-L{}", include_prefix.join("usr").join("lib").display()),
411                format!(
412                    "-F{}",
413                    include_prefix
414                        .join("System")
415                        .join("Library")
416                        .join("Frameworks")
417                        .display()
418                ),
419                "-DTARGET_OS_IPHONE=0".to_string(),
420            ]);
421        }
422
423        // Add the deps directory that contains `.tbd` files to the library search path
424        let cache_dir = cache_dir();
425        let deps_dir = cache_dir.join("deps");
426        fs::create_dir_all(&deps_dir)?;
427        write_tbd_files(&deps_dir)?;
428        new_cmd_args.push("-L".to_string());
429        new_cmd_args.push(format!("{}", deps_dir.display()));
430        Ok(())
431    }
432
433    /// Execute zig ar/ranlib command
434    pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
435        let mut child = Self::command()?
436            .arg(cmd)
437            .args(cmd_args)
438            .spawn()
439            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
440        let status = child.wait().expect("Failed to wait on zig child process");
441        if !status.success() {
442            process::exit(status.code().unwrap_or(1));
443        }
444        Ok(())
445    }
446
447    /// Build the zig command line
448    pub fn command() -> Result<Command> {
449        let (zig, zig_args) = Self::find_zig()?;
450        let mut cmd = Command::new(zig);
451        cmd.args(zig_args);
452        Ok(cmd)
453    }
454
455    fn zig_version() -> Result<semver::Version> {
456        let output = Self::command()?.arg("version").output()?;
457        let version_str =
458            str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
459        let version = semver::Version::parse(version_str.trim())?;
460        Ok(version)
461    }
462
463    /// Search for `python -m ziglang` first and for `zig` second.
464    pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
465        Self::find_zig_python()
466            .or_else(|_| Self::find_zig_bin())
467            .context("Failed to find zig")
468    }
469
470    /// Detect the plain zig binary
471    fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
472        let zig_path = zig_path()?;
473        let output = Command::new(&zig_path).arg("version").output()?;
474
475        let version_str = str::from_utf8(&output.stdout).with_context(|| {
476            format!("`{} version` didn't return utf8 output", zig_path.display())
477        })?;
478        Self::validate_zig_version(version_str)?;
479        Ok((zig_path, Vec::new()))
480    }
481
482    /// Detect the Python ziglang package
483    fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
484        let python_path = python_path()?;
485        let output = Command::new(&python_path)
486            .args(["-m", "ziglang", "version"])
487            .output()?;
488
489        let version_str = str::from_utf8(&output.stdout).with_context(|| {
490            format!(
491                "`{} -m ziglang version` didn't return utf8 output",
492                python_path.display()
493            )
494        })?;
495        Self::validate_zig_version(version_str)?;
496        Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
497    }
498
499    fn validate_zig_version(version: &str) -> Result<()> {
500        let min_ver = semver::Version::new(0, 9, 0);
501        let version = semver::Version::parse(version.trim())?;
502        if version >= min_ver {
503            Ok(())
504        } else {
505            bail!(
506                "zig version {} is too old, need at least {}",
507                version,
508                min_ver
509            )
510        }
511    }
512
513    /// Find zig lib directory
514    pub fn lib_dir() -> Result<PathBuf> {
515        let (zig, zig_args) = Self::find_zig()?;
516        let output = Command::new(zig).args(zig_args).arg("env").output()?;
517        let zig_env: ZigEnv = serde_json::from_slice(&output.stdout)?;
518        Ok(PathBuf::from(zig_env.lib_dir))
519    }
520
521    fn add_env_if_missing<K, V>(command: &mut Command, name: K, value: V)
522    where
523        K: AsRef<OsStr>,
524        V: AsRef<OsStr>,
525    {
526        let command_env_contains_no_key =
527            |name: &K| !command.get_envs().any(|(key, _)| name.as_ref() == key);
528
529        if command_env_contains_no_key(&name) && env::var_os(&name).is_none() {
530            command.env(name, value);
531        }
532    }
533
534    pub(crate) fn apply_command_env(
535        manifest_path: Option<&Path>,
536        release: bool,
537        cargo: &cargo_options::CommonOptions,
538        cmd: &mut Command,
539        enable_zig_ar: bool,
540    ) -> Result<()> {
541        // setup zig as linker
542        let rust_targets = cargo
543            .target
544            .iter()
545            .map(|target| target.split_once('.').map(|(t, _)| t).unwrap_or(target))
546            .collect::<Vec<&str>>();
547        let rustc_meta = rustc_version::version_meta()?;
548        Self::add_env_if_missing(
549            cmd,
550            "CARGO_ZIGBUILD_RUSTC_VERSION",
551            rustc_meta.semver.to_string(),
552        );
553        let host_target = &rustc_meta.host;
554        for (parsed_target, raw_target) in rust_targets.iter().zip(&cargo.target) {
555            let env_target = parsed_target.replace('-', "_");
556            let zig_wrapper = prepare_zig_linker(raw_target)?;
557
558            if is_mingw_shell() {
559                let zig_cc = zig_wrapper.cc.to_slash_lossy();
560                let zig_cxx = zig_wrapper.cxx.to_slash_lossy();
561                Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &*zig_cc);
562                Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &*zig_cxx);
563                if !parsed_target.contains("wasm") {
564                    Self::add_env_if_missing(
565                        cmd,
566                        format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
567                        &*zig_cc,
568                    );
569                }
570            } else {
571                Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &zig_wrapper.cc);
572                Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &zig_wrapper.cxx);
573                if !parsed_target.contains("wasm") {
574                    Self::add_env_if_missing(
575                        cmd,
576                        format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
577                        &zig_wrapper.cc,
578                    );
579                }
580            }
581
582            Self::add_env_if_missing(cmd, format!("RANLIB_{env_target}"), &zig_wrapper.ranlib);
583            // Only setup AR when explicitly asked to
584            // because it need special executable name handling, see src/bin/cargo-zigbuild.rs
585            if enable_zig_ar {
586                if parsed_target.contains("msvc") {
587                    Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.lib);
588                } else {
589                    Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.ar);
590                }
591            }
592
593            Self::setup_os_deps(manifest_path, release, cargo)?;
594
595            let cmake_toolchain_file_env = format!("CMAKE_TOOLCHAIN_FILE_{env_target}");
596            if env::var_os(&cmake_toolchain_file_env).is_none()
597                && env::var_os(format!("CMAKE_TOOLCHAIN_FILE_{parsed_target}")).is_none()
598                && env::var_os("TARGET_CMAKE_TOOLCHAIN_FILE").is_none()
599                && env::var_os("CMAKE_TOOLCHAIN_FILE").is_none()
600            {
601                if let Ok(cmake_toolchain_file) =
602                    Self::setup_cmake_toolchain(parsed_target, &zig_wrapper, enable_zig_ar)
603                {
604                    cmd.env(cmake_toolchain_file_env, cmake_toolchain_file);
605                }
606            }
607
608            if raw_target.contains("windows-gnu") {
609                cmd.env("WINAPI_NO_BUNDLED_LIBRARIES", "1");
610            }
611
612            if raw_target.contains("apple-darwin") {
613                if let Some(sdkroot) = Self::macos_sdk_root() {
614                    if env::var_os("PKG_CONFIG_SYSROOT_DIR").is_none() {
615                        // Set PKG_CONFIG_SYSROOT_DIR for pkg-config crate
616                        cmd.env("PKG_CONFIG_SYSROOT_DIR", sdkroot);
617                    }
618                }
619            }
620
621            // Enable unstable `target-applies-to-host` option automatically
622            // when target is the same as host but may have specified glibc version
623            if host_target == parsed_target {
624                if !matches!(rustc_meta.channel, rustc_version::Channel::Nightly) {
625                    // Hack to use the unstable feature on stable Rust
626                    // https://github.com/rust-lang/cargo/pull/9753#issuecomment-1022919343
627                    cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
628                }
629                cmd.env("CARGO_UNSTABLE_TARGET_APPLIES_TO_HOST", "true");
630                cmd.env("CARGO_TARGET_APPLIES_TO_HOST", "false");
631            }
632
633            // Pass options used by zig cc down to bindgen, if possible
634            let mut options = Self::collect_zig_cc_options(&zig_wrapper, raw_target)
635                .context("Failed to collect `zig cc` options")?;
636            if raw_target.contains("apple-darwin") {
637                // everyone seems to miss `#import <TargetConditionals.h>`...
638                options.push("-DTARGET_OS_IPHONE=0".to_string());
639            }
640            let escaped_options = shlex::try_join(options.iter().map(|s| &s[..]))?;
641            let bindgen_env = "BINDGEN_EXTRA_CLANG_ARGS";
642            let fallback_value = env::var(bindgen_env);
643            for target in [&env_target[..], parsed_target] {
644                let name = format!("{bindgen_env}_{target}");
645                if let Ok(mut value) = env::var(&name).or(fallback_value.clone()) {
646                    if shlex::split(&value).is_none() {
647                        // bindgen treats the whole string as a single argument if split fails
648                        value = shlex::try_quote(&value)?.into_owned();
649                    }
650                    if !value.is_empty() {
651                        value.push(' ');
652                    }
653                    value.push_str(&escaped_options);
654                    env::set_var(name, value);
655                } else {
656                    env::set_var(name, escaped_options.clone());
657                }
658            }
659        }
660        Ok(())
661    }
662
663    /// Collects compiler options used by `zig cc` for given target.
664    /// Used for the case where `zig cc` cannot be used but underlying options should be retained,
665    /// for example, as in bindgen (which requires libclang.so and thus is independent from zig).
666    fn collect_zig_cc_options(zig_wrapper: &ZigWrapper, raw_target: &str) -> Result<Vec<String>> {
667        #[derive(Debug, PartialEq, Eq)]
668        enum Kind {
669            Normal,
670            Framework,
671        }
672
673        #[derive(Debug)]
674        struct PerLanguageOptions {
675            glibc_minor_ver: Option<u32>,
676            include_paths: Vec<(Kind, String)>,
677        }
678
679        fn collect_per_language_options(
680            program: &Path,
681            ext: &str,
682            raw_target: &str,
683        ) -> Result<PerLanguageOptions> {
684            // We can't use `-x c` or `-x c++` because pre-0.11 Zig doesn't handle them
685            let empty_file_path = cache_dir().join(format!(".intentionally-empty-file.{ext}"));
686            if !empty_file_path.exists() {
687                fs::write(&empty_file_path, "")?;
688            }
689
690            let output = Command::new(program)
691                .arg("-E")
692                .arg(&empty_file_path)
693                .arg("-v")
694                .output()?;
695            // Clang always generates UTF-8 regardless of locale, so this is okay.
696            let stderr = String::from_utf8(output.stderr)?;
697            if !output.status.success() {
698                bail!(
699                    "Failed to run `zig cc -v` with status {}: {}",
700                    output.status,
701                    stderr.trim(),
702                );
703            }
704
705            // Collect some macro definitions from cc1 options. We can't directly use
706            // them though, as we can't distinguish options added by zig from options
707            // added by clang driver (e.g. `__GCC_HAVE_DWARF2_CFI_ASM`).
708            let glibc_minor_ver = if let Some(start) = stderr.find("__GLIBC_MINOR__=") {
709                let stderr = &stderr[start + 16..];
710                let end = stderr
711                    .find(|c: char| !c.is_ascii_digit())
712                    .unwrap_or(stderr.len());
713                stderr[..end].parse().ok()
714            } else {
715                None
716            };
717
718            let start = stderr
719                .find("#include <...> search starts here:")
720                .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?
721                + 34;
722            let end = stderr
723                .find("End of search list.")
724                .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?;
725
726            let mut include_paths = Vec::new();
727            for mut line in stderr[start..end].lines() {
728                line = line.trim();
729                let mut kind = Kind::Normal;
730                if line.ends_with(" (framework directory)") {
731                    line = line[..line.len() - 22].trim();
732                    kind = Kind::Framework;
733                } else if line.ends_with(" (headermap)") {
734                    bail!("C/C++ search path includes header maps, which are not supported");
735                }
736                if !line.is_empty() {
737                    include_paths.push((kind, line.to_owned()));
738                }
739            }
740
741            // In openharmony, we should add search header path by default which is useful for bindgen.
742            if raw_target.contains("ohos") {
743                let ndk = env::var("OHOS_NDK_HOME").expect("Can't get NDK path");
744                include_paths.push((Kind::Normal, format!("{}/native/sysroot/usr/include", ndk)));
745            }
746
747            Ok(PerLanguageOptions {
748                include_paths,
749                glibc_minor_ver,
750            })
751        }
752
753        let c_opts = collect_per_language_options(&zig_wrapper.cc, "c", raw_target)?;
754        let cpp_opts = collect_per_language_options(&zig_wrapper.cxx, "cpp", raw_target)?;
755
756        // Ensure that `c_opts` and `cpp_opts` are almost identical in the way we expect.
757        if c_opts.glibc_minor_ver != cpp_opts.glibc_minor_ver {
758            bail!(
759                "`zig cc` gives a different glibc minor version for C ({:?}) and C++ ({:?})",
760                c_opts.glibc_minor_ver,
761                cpp_opts.glibc_minor_ver,
762            );
763        }
764        let c_paths = c_opts.include_paths;
765        let mut cpp_paths = cpp_opts.include_paths;
766        let cpp_pre_len = cpp_paths
767            .iter()
768            .position(|p| {
769                p == c_paths
770                    .iter()
771                    .filter(|(kind, _)| *kind == Kind::Normal)
772                    .next()
773                    .unwrap()
774            })
775            .unwrap_or_default();
776        let cpp_post_len = cpp_paths.len()
777            - cpp_paths
778                .iter()
779                .position(|p| p == c_paths.last().unwrap())
780                .unwrap_or_default()
781            - 1;
782
783        // <digression>
784        //
785        // So, why we do need all of these?
786        //
787        // Bindgen wouldn't look at our `zig cc` (which doesn't contain `libclang.so` anyway),
788        // but it does collect include paths from the local clang and feed them to `libclang.so`.
789        // We want those include paths to come from our `zig cc` instead of the local clang.
790        // There are three main mechanisms possible:
791        //
792        // 1. Replace the local clang with our version.
793        //
794        //    Bindgen, internally via clang-sys, recognizes `CLANG_PATH` and `PATH`.
795        //    They are unfortunately a global namespace and simply setting them may break
796        //    existing build scripts, so we can't confidently override them.
797        //
798        //    Clang-sys can also look at target-prefixed clang if arguments contain `-target`.
799        //    Unfortunately clang-sys can only recognize `-target xxx`, which very slightly
800        //    differs from what bindgen would pass (`-target=xxx`), so this is not yet possible.
801        //
802        //    It should be also noted that we need to collect not only include paths
803        //    but macro definitions added by Zig, for example `-D__GLIBC_MINOR__`.
804        //    Clang-sys can't do this yet, so this option seems less robust than we want.
805        //
806        // 2. Set the environment variable `BINDGEN_EXTRA_CLANG_ARGS` and let bindgen to
807        //    append them to arguments passed to `libclang.so`.
808        //
809        //    This unfortunately means that we have the same set of arguments for C and C++.
810        //    Also we have to support older versions of clang, as old as clang 5 (2017).
811        //    We do have options like `-c-isystem` (cc1 only) and `-cxx-isystem`,
812        //    but we need to be aware of other options may affect our added options
813        //    and this requires a nitty gritty of clang driver and cc1---really annoying.
814        //
815        // 3. Fix either bindgen or clang-sys or Zig to ease our jobs.
816        //
817        //    This is not the option for now because, even after fixes, we have to support
818        //    older versions of bindgen or Zig which won't have those fixes anyway.
819        //    But it seems that minor changes to bindgen can indeed fix lots of issues
820        //    we face, so we are looking for them in the future.
821        //
822        // For this reason, we chose the option 2 and overrode `BINDGEN_EXTRA_CLANG_ARGS`.
823        // The following therefore assumes some understanding about clang option handling,
824        // including what the heck is cc1 (see the clang FAQ) and how driver options get
825        // translated to cc1 options (no documentation at all, as it's supposedly unstable).
826        // Fortunately for us, most (but not all) `-i...` options are passed through cc1.
827        //
828        // If you do experience weird compilation errors during bindgen, there's a chance
829        // that this code has overlooked some edge cases. You can put `.clang_arg("-###")`
830        // to print the final cc1 options, which would give a lot of information about
831        // how it got screwed up and help a lot when we fix the issue.
832        //
833        // </digression>
834
835        let mut args = Vec::new();
836
837        // Never include default include directories,
838        // otherwise `__has_include` will be totally confused.
839        args.push("-nostdinc".to_owned());
840
841        // Add various options for libc++ and glibc.
842        // Should match what `Compilation.zig` internally does:
843        //
844        // https://github.com/ziglang/zig/blob/0.9.0/src/Compilation.zig#L3390-L3427
845        // https://github.com/ziglang/zig/blob/0.9.1/src/Compilation.zig#L3408-L3445
846        // https://github.com/ziglang/zig/blob/0.10.0/src/Compilation.zig#L4163-L4211
847        // https://github.com/ziglang/zig/blob/0.10.1/src/Compilation.zig#L4240-L4288
848        if raw_target.contains("musl") || raw_target.contains("ohos") {
849            args.push("-D_LIBCPP_HAS_MUSL_LIBC".to_owned());
850            // for musl or openharmony
851            // https://github.com/ziglang/zig/pull/16098
852            args.push("-D_LARGEFILE64_SOURCE".to_owned());
853        }
854        args.extend(
855            [
856                "-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
857                "-D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS",
858                "-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
859                "-D_LIBCPP_PSTL_CPU_BACKEND_SERIAL",
860                "-D_LIBCPP_ABI_VERSION=1",
861                "-D_LIBCPP_ABI_NAMESPACE=__1",
862                "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST",
863            ]
864            .into_iter()
865            .map(ToString::to_string),
866        );
867        if let Some(ver) = c_opts.glibc_minor_ver {
868            // Handled separately because we have no way to infer this without Zig
869            args.push(format!("-D__GLIBC_MINOR__={ver}"));
870        }
871
872        for (kind, path) in cpp_paths.drain(..cpp_pre_len) {
873            if kind != Kind::Normal {
874                // may also be Kind::Framework on macOS
875                continue;
876            }
877            // Ideally this should be `-stdlib++-isystem`, which can be disabled by
878            // passing `-nostdinc++`, but it is fairly new: https://reviews.llvm.org/D64089
879            //
880            // (Also note that `-stdlib++-isystem` is a driver-only option,
881            // so it will be moved relative to other `-isystem` options against our will.)
882            args.push("-cxx-isystem".to_owned());
883            args.push(path);
884        }
885
886        for (kind, path) in c_paths {
887            match kind {
888                Kind::Normal => {
889                    // A normal `-isystem` is preferred over `-cxx-isystem` by cc1...
890                    args.push("-Xclang".to_owned());
891                    args.push("-c-isystem".to_owned());
892                    args.push("-Xclang".to_owned());
893                    args.push(path.clone());
894                    args.push("-cxx-isystem".to_owned());
895                    args.push(path);
896                }
897                Kind::Framework => {
898                    args.push("-iframework".to_owned());
899                    args.push(path);
900                }
901            }
902        }
903
904        for (kind, path) in cpp_paths.drain(cpp_paths.len() - cpp_post_len..) {
905            assert!(kind == Kind::Normal);
906            args.push("-cxx-isystem".to_owned());
907            args.push(path);
908        }
909
910        Ok(args)
911    }
912
913    fn setup_os_deps(
914        manifest_path: Option<&Path>,
915        release: bool,
916        cargo: &cargo_options::CommonOptions,
917    ) -> Result<()> {
918        for target in &cargo.target {
919            if target.contains("apple") {
920                let target_dir = if let Some(target_dir) = cargo.target_dir.clone() {
921                    target_dir.join(target)
922                } else {
923                    let manifest_path = manifest_path.unwrap_or_else(|| Path::new("Cargo.toml"));
924                    if !manifest_path.exists() {
925                        // cargo install doesn't pass a manifest path so `Cargo.toml` in cwd may not exist
926                        continue;
927                    }
928                    let metadata = cargo_metadata::MetadataCommand::new()
929                        .manifest_path(manifest_path)
930                        .no_deps()
931                        .exec()?;
932                    metadata.target_directory.into_std_path_buf().join(target)
933                };
934                let profile = match cargo.profile.as_deref() {
935                    Some("dev" | "test") => "debug",
936                    Some("release" | "bench") => "release",
937                    Some(profile) => profile,
938                    None => {
939                        if release {
940                            "release"
941                        } else {
942                            "debug"
943                        }
944                    }
945                };
946                let deps_dir = target_dir.join(profile).join("deps");
947                fs::create_dir_all(&deps_dir)?;
948                if !target_dir.join("CACHEDIR.TAG").is_file() {
949                    // Create a CACHEDIR.TAG file to exclude target directory from backup
950                    let _ = write_file(
951                        &target_dir.join("CACHEDIR.TAG"),
952                        "Signature: 8a477f597d28d172789f06886806bc55
953# This file is a cache directory tag created by cargo.
954# For information about cache directory tags see https://bford.info/cachedir/
955",
956                    );
957                }
958                write_tbd_files(&deps_dir)?;
959            } else if target.contains("arm") && target.contains("linux") {
960                // See https://github.com/ziglang/zig/issues/3287
961                if let Ok(lib_dir) = Zig::lib_dir() {
962                    let arm_features_h = lib_dir
963                        .join("libc")
964                        .join("glibc")
965                        .join("sysdeps")
966                        .join("arm")
967                        .join("arm-features.h");
968                    if !arm_features_h.is_file() {
969                        fs::write(arm_features_h, ARM_FEATURES_H)?;
970                    }
971                }
972            } else if target.contains("windows-gnu") {
973                if let Ok(lib_dir) = Zig::lib_dir() {
974                    let lib_common = lib_dir.join("libc").join("mingw").join("lib-common");
975                    let synchronization_def = lib_common.join("synchronization.def");
976                    if !synchronization_def.is_file() {
977                        let api_ms_win_core_synch_l1_2_0_def =
978                            lib_common.join("api-ms-win-core-synch-l1-2-0.def");
979                        // Ignore error
980                        fs::copy(api_ms_win_core_synch_l1_2_0_def, synchronization_def).ok();
981                    }
982                }
983            }
984        }
985        Ok(())
986    }
987
988    fn setup_cmake_toolchain(
989        target: &str,
990        zig_wrapper: &ZigWrapper,
991        enable_zig_ar: bool,
992    ) -> Result<PathBuf> {
993        let cmake = cache_dir().join("cmake");
994        fs::create_dir_all(&cmake)?;
995
996        let toolchain_file = cmake.join(format!("{target}-toolchain.cmake"));
997        let triple: Triple = target.parse()?;
998        let os = triple.operating_system.to_string();
999        let arch = triple.architecture.to_string();
1000        let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
1001            ("darwin", "x86_64") => ("Darwin", "x86_64"),
1002            ("darwin", "aarch64") => ("Darwin", "arm64"),
1003            ("linux", arch) => {
1004                let cmake_arch = match arch {
1005                    "powerpc" => "ppc",
1006                    "powerpc64" => "ppc64",
1007                    "powerpc64le" => "ppc64le",
1008                    _ => arch,
1009                };
1010                ("Linux", cmake_arch)
1011            }
1012            ("windows", "x86_64") => ("Windows", "AMD64"),
1013            ("windows", "i686") => ("Windows", "X86"),
1014            ("windows", "aarch64") => ("Windows", "ARM64"),
1015            (os, arch) => (os, arch),
1016        };
1017        let mut content = format!(
1018            r#"
1019set(CMAKE_SYSTEM_NAME {system_name})
1020set(CMAKE_SYSTEM_PROCESSOR {system_processor})
1021set(CMAKE_C_COMPILER {cc})
1022set(CMAKE_CXX_COMPILER {cxx})
1023set(CMAKE_RANLIB {ranlib})
1024set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE)
1025set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE)"#,
1026            system_name = system_name,
1027            system_processor = system_processor,
1028            cc = zig_wrapper.cc.to_slash_lossy(),
1029            cxx = zig_wrapper.cxx.to_slash_lossy(),
1030            ranlib = zig_wrapper.ranlib.to_slash_lossy(),
1031        );
1032        if enable_zig_ar {
1033            content.push_str(&format!(
1034                "\nset(CMAKE_AR {})\n",
1035                zig_wrapper.ar.to_slash_lossy()
1036            ));
1037        }
1038        write_file(&toolchain_file, &content)?;
1039        Ok(toolchain_file)
1040    }
1041
1042    #[cfg(target_os = "macos")]
1043    fn macos_sdk_root() -> Option<PathBuf> {
1044        match env::var_os("SDKROOT") {
1045            Some(sdkroot) => {
1046                if !sdkroot.is_empty() {
1047                    Some(sdkroot.into())
1048                } else {
1049                    None
1050                }
1051            }
1052            None => {
1053                let output = Command::new("xcrun")
1054                    .args(["--sdk", "macosx", "--show-sdk-path"])
1055                    .output();
1056                if let Ok(output) = output {
1057                    if output.status.success() {
1058                        if let Ok(stdout) = String::from_utf8(output.stdout) {
1059                            let stdout = stdout.trim();
1060                            if !stdout.is_empty() {
1061                                return Some(stdout.into());
1062                            }
1063                        }
1064                    }
1065                }
1066                None
1067            }
1068        }
1069    }
1070
1071    #[cfg(not(target_os = "macos"))]
1072    fn macos_sdk_root() -> Option<PathBuf> {
1073        match env::var_os("SDKROOT") {
1074            Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1075            _ => None,
1076        }
1077    }
1078}
1079
1080fn write_file(path: &Path, content: &str) -> Result<(), anyhow::Error> {
1081    let existing_content = fs::read_to_string(path).unwrap_or_default();
1082    if existing_content != content {
1083        fs::write(path, content)?;
1084    }
1085    Ok(())
1086}
1087
1088fn write_tbd_files(deps_dir: &Path) -> Result<(), anyhow::Error> {
1089    write_file(&deps_dir.join("libiconv.tbd"), LIBICONV_TBD)?;
1090    write_file(&deps_dir.join("libcharset.1.tbd"), LIBCHARSET_TBD)?;
1091    write_file(&deps_dir.join("libcharset.tbd"), LIBCHARSET_TBD)?;
1092    Ok(())
1093}
1094
1095fn cache_dir() -> PathBuf {
1096    env::var("CARGO_ZIGBUILD_CACHE_DIR")
1097        .ok()
1098        .map(|s| s.into())
1099        .or_else(dirs::cache_dir)
1100        // If the really is no cache dir, cwd will also do
1101        .unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
1102        .join(env!("CARGO_PKG_NAME"))
1103        .join(env!("CARGO_PKG_VERSION"))
1104}
1105
1106#[derive(Debug, Deserialize)]
1107struct ZigEnv {
1108    lib_dir: String,
1109}
1110
1111/// zig wrapper paths
1112#[derive(Debug, Clone)]
1113pub struct ZigWrapper {
1114    pub cc: PathBuf,
1115    pub cxx: PathBuf,
1116    pub ar: PathBuf,
1117    pub ranlib: PathBuf,
1118    pub lib: PathBuf,
1119}
1120
1121#[derive(Debug, Clone, Default, PartialEq)]
1122struct TargetFlags {
1123    pub target_cpu: String,
1124    pub target_feature: String,
1125}
1126
1127impl TargetFlags {
1128    pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
1129        let mut parsed = Self::default();
1130
1131        let f = rustflags::from_encoded(encoded);
1132        for flag in f {
1133            if let rustflags::Flag::Codegen { opt, value } = flag {
1134                let key = opt.replace('-', "_");
1135                match key.as_str() {
1136                    "target_cpu" => {
1137                        if let Some(value) = value {
1138                            parsed.target_cpu = value;
1139                        }
1140                    }
1141                    "target_feature" => {
1142                        // See https://github.com/rust-lang/rust/blob/7e3ba5b8b7556073ab69822cc36b93d6e74cd8c9/compiler/rustc_session/src/options.rs#L1233
1143                        if let Some(value) = value {
1144                            if !parsed.target_feature.is_empty() {
1145                                parsed.target_feature.push(',');
1146                            }
1147                            parsed.target_feature.push_str(&value);
1148                        }
1149                    }
1150                    _ => {}
1151                }
1152            }
1153        }
1154        Ok(parsed)
1155    }
1156}
1157
1158/// Prepare wrapper scripts for `zig cc` and `zig c++` and returns their paths
1159///
1160/// We want to use `zig cc` as linker and c compiler. We want to call `python -m ziglang cc`, but
1161/// cargo only accepts a path to an executable as linker, so we add a wrapper script. We then also
1162/// use the wrapper script to pass arguments and substitute an unsupported argument.
1163///
1164/// We create different files for different args because otherwise cargo might skip recompiling even
1165/// if the linker target changed
1166#[allow(clippy::blocks_in_conditions)]
1167pub fn prepare_zig_linker(target: &str) -> Result<ZigWrapper> {
1168    let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
1169    let abi_suffix = if abi_suffix.is_empty() {
1170        String::new()
1171    } else {
1172        if abi_suffix
1173            .split_once('.')
1174            .filter(|(x, y)| {
1175                !x.is_empty()
1176                    && x.chars().all(|c| c.is_ascii_digit())
1177                    && !y.is_empty()
1178                    && y.chars().all(|c| c.is_ascii_digit())
1179            })
1180            .is_none()
1181        {
1182            bail!("Malformed zig target abi suffix.")
1183        }
1184        format!(".{abi_suffix}")
1185    };
1186    let triple: Triple = rust_target
1187        .parse()
1188        .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
1189    let arch = triple.architecture.to_string();
1190    let target_env = match (triple.architecture, triple.environment) {
1191        (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
1192        (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
1193        (_, Environment::GnuLlvm) => Environment::Gnu,
1194        (_, environment) => environment,
1195    };
1196    let file_ext = if cfg!(windows) { "bat" } else { "sh" };
1197    let file_target = target.trim_end_matches('.');
1198
1199    let mut cc_args = vec![
1200        // prevent stripping
1201        "-g".to_owned(),
1202        // disable sanitizers
1203        "-fno-sanitize=all".to_owned(),
1204    ];
1205
1206    // TODO: Maybe better to assign mcpu according to:
1207    // rustc --target <target> -Z unstable-options --print target-spec-json
1208    let zig_mcpu_default = match triple.operating_system {
1209        OperatingSystem::Linux => {
1210            match arch.as_str() {
1211                // zig uses _ instead of - in cpu features
1212                "arm" => match target_env {
1213                    Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
1214                    Environment::Gnueabihf | Environment::Musleabihf => {
1215                        "generic+v6+strict_align+vfp2-d32"
1216                    }
1217                    _ => "",
1218                },
1219                "armv5te" => "generic+soft_float+strict_align",
1220                "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
1221                arch_str @ ("i586" | "i686") => {
1222                    if arch_str == "i586" {
1223                        "pentium"
1224                    } else {
1225                        "pentium4"
1226                    }
1227                }
1228                "riscv64gc" => "generic_rv64+m+a+f+d+c",
1229                "s390x" => "z10-vector",
1230                _ => "",
1231            }
1232        }
1233        _ => "",
1234    };
1235
1236    // Override mcpu from RUSTFLAGS if provided. The override happens when
1237    // commands like `cargo-zigbuild build` are invoked.
1238    // Currently we only override according to target_cpu.
1239    let zig_mcpu_override = {
1240        let cargo_config = cargo_config2::Config::load()?;
1241        let rust_flags = cargo_config.rustflags(rust_target)?.unwrap_or_default();
1242        let encoded_rust_flags = rust_flags.encode()?;
1243        let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
1244        // Note: zig uses _ instead of - for target_cpu and target_feature
1245        // target_cpu may be empty string, which means target_cpu is not specified.
1246        target_flags.target_cpu.replace('-', "_")
1247    };
1248
1249    if !zig_mcpu_override.is_empty() {
1250        cc_args.push(format!("-mcpu={zig_mcpu_override}"));
1251    } else if !zig_mcpu_default.is_empty() {
1252        cc_args.push(format!("-mcpu={zig_mcpu_default}"));
1253    }
1254
1255    match triple.operating_system {
1256        OperatingSystem::Linux => {
1257            let zig_arch = match arch.as_str() {
1258                // zig uses _ instead of - in cpu features
1259                "arm" => "arm",
1260                "armv5te" => "arm",
1261                "armv7" => "arm",
1262                "i586" | "i686" => {
1263                    let zig_version = Zig::zig_version()?;
1264                    if zig_version.major == 0 && zig_version.minor >= 11 {
1265                        "x86"
1266                    } else {
1267                        "i386"
1268                    }
1269                }
1270                "riscv64gc" => "riscv64",
1271                "s390x" => "s390x",
1272                _ => arch.as_str(),
1273            };
1274            cc_args.push(format!("-target {zig_arch}-linux-{target_env}{abi_suffix}"));
1275        }
1276        OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
1277            let zig_version = Zig::zig_version()?;
1278            // Zig 0.10.0 switched macOS ABI to none
1279            // see https://github.com/ziglang/zig/pull/11684
1280            if zig_version > semver::Version::new(0, 9, 1) {
1281                cc_args.push(format!("-target {arch}-macos-none{abi_suffix}"));
1282            } else {
1283                cc_args.push(format!("-target {arch}-macos-gnu{abi_suffix}"));
1284            }
1285        }
1286        OperatingSystem::Windows { .. } => {
1287            let zig_arch = match arch.as_str() {
1288                "i686" => {
1289                    let zig_version = Zig::zig_version()?;
1290                    if zig_version.major == 0 && zig_version.minor >= 11 {
1291                        "x86"
1292                    } else {
1293                        "i386"
1294                    }
1295                }
1296                arch => arch,
1297            };
1298            cc_args.push(format!(
1299                "-target {zig_arch}-windows-{target_env}{abi_suffix}"
1300            ));
1301        }
1302        OperatingSystem::Emscripten => {
1303            cc_args.push(format!("-target {arch}-emscripten{abi_suffix}"));
1304        }
1305        OperatingSystem::Wasi => {
1306            cc_args.push(format!("-target {arch}-wasi{abi_suffix}"));
1307        }
1308        OperatingSystem::WasiP1 => {
1309            cc_args.push(format!("-target {arch}-wasi.0.1.0{abi_suffix}"));
1310        }
1311        OperatingSystem::Unknown => {
1312            if triple.architecture == Architecture::Wasm32
1313                || triple.architecture == Architecture::Wasm64
1314            {
1315                cc_args.push(format!("-target {arch}-freestanding{abi_suffix}"));
1316            } else {
1317                bail!("unsupported target '{rust_target}'")
1318            }
1319        }
1320        _ => bail!(format!("unsupported target '{rust_target}'")),
1321    };
1322
1323    let zig_linker_dir = cache_dir();
1324    fs::create_dir_all(&zig_linker_dir)?;
1325
1326    if triple.operating_system == OperatingSystem::Linux {
1327        if matches!(
1328            triple.environment,
1329            Environment::Gnu
1330                | Environment::Gnuspe
1331                | Environment::Gnux32
1332                | Environment::Gnueabi
1333                | Environment::Gnuabi64
1334                | Environment::GnuIlp32
1335                | Environment::Gnueabihf
1336        ) {
1337            let glibc_version = if abi_suffix.is_empty() {
1338                (2, 17)
1339            } else {
1340                let mut parts = abi_suffix[1..].split('.');
1341                let major: usize = parts.next().unwrap().parse()?;
1342                let minor: usize = parts.next().unwrap().parse()?;
1343                (major, minor)
1344            };
1345            // See https://github.com/ziglang/zig/issues/9485
1346            if glibc_version < (2, 28) {
1347                use crate::linux::{FCNTL_H, FCNTL_MAP};
1348
1349                let zig_version = Zig::zig_version()?;
1350                if zig_version.major == 0 && zig_version.minor < 11 {
1351                    let fcntl_map = zig_linker_dir.join("fcntl.map");
1352                    let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
1353                    if existing_content != FCNTL_MAP {
1354                        fs::write(&fcntl_map, FCNTL_MAP)?;
1355                    }
1356                    let fcntl_h = zig_linker_dir.join("fcntl.h");
1357                    let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
1358                    if existing_content != FCNTL_H {
1359                        fs::write(&fcntl_h, FCNTL_H)?;
1360                    }
1361
1362                    cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
1363                    cc_args.push(format!("-include {}", fcntl_h.display()));
1364                }
1365            }
1366        } else if matches!(
1367            triple.environment,
1368            Environment::Musl
1369                | Environment::Muslabi64
1370                | Environment::Musleabi
1371                | Environment::Musleabihf
1372        ) {
1373            use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
1374
1375            let zig_version = Zig::zig_version()?;
1376            let rustc_version = rustc_version::version_meta()?.semver;
1377
1378            // as zig 0.11.0 is released, its musl has been upgraded to 1.2.4 with break changes
1379            // but rust is still with musl 1.2.3
1380            // we need this workaround before rust 1.72
1381            // https://github.com/ziglang/zig/pull/16098
1382            if (zig_version.major, zig_version.minor) >= (0, 11)
1383                && (rustc_version.major, rustc_version.minor) < (1, 72)
1384            {
1385                let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
1386                fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
1387
1388                cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
1389            }
1390        }
1391    }
1392
1393    let cc_args_str = cc_args.join(" ");
1394    let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
1395    let zig_cc = zig_linker_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
1396    let zig_cxx = zig_linker_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
1397    let zig_ranlib = zig_linker_dir.join(format!("zigranlib.{file_ext}"));
1398    write_linker_wrapper(&zig_cc, "cc", &cc_args_str)?;
1399    write_linker_wrapper(&zig_cxx, "c++", &cc_args_str)?;
1400    write_linker_wrapper(&zig_ranlib, "ranlib", "")?;
1401
1402    let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1403    let zig_ar = zig_linker_dir.join(format!("ar{exe_ext}"));
1404    symlink_wrapper(&zig_ar)?;
1405    let zig_lib = zig_linker_dir.join(format!("lib{exe_ext}"));
1406    symlink_wrapper(&zig_lib)?;
1407
1408    Ok(ZigWrapper {
1409        cc: zig_cc,
1410        cxx: zig_cxx,
1411        ar: zig_ar,
1412        ranlib: zig_ranlib,
1413        lib: zig_lib,
1414    })
1415}
1416
1417fn symlink_wrapper(target: &Path) -> Result<()> {
1418    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1419        PathBuf::from(exe)
1420    } else {
1421        env::current_exe()?
1422    };
1423    #[cfg(windows)]
1424    {
1425        if !target.exists() {
1426            // symlink on Windows requires admin privileges so we use hardlink instead
1427            if std::fs::hard_link(&current_exe, target).is_err() {
1428                // hard_link doesn't support cross-device links so we fallback to copy
1429                std::fs::copy(&current_exe, target)?;
1430            }
1431        }
1432    }
1433
1434    #[cfg(unix)]
1435    {
1436        if !target.exists() {
1437            if fs::read_link(target).is_ok() {
1438                // remove broken symlink
1439                fs::remove_file(target)?;
1440            }
1441            std::os::unix::fs::symlink(current_exe, target)?;
1442        }
1443    }
1444    Ok(())
1445}
1446
1447/// Write a zig cc wrapper batch script for unix
1448#[cfg(target_family = "unix")]
1449fn write_linker_wrapper(path: &Path, command: &str, args: &str) -> Result<()> {
1450    let mut buf = Vec::<u8>::new();
1451    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1452        PathBuf::from(exe)
1453    } else {
1454        env::current_exe()?
1455    };
1456    writeln!(&mut buf, "#!/bin/sh")?;
1457    writeln!(
1458        &mut buf,
1459        "exec \"{}\" zig {} -- {} \"$@\"",
1460        current_exe.display(),
1461        command,
1462        args
1463    )?;
1464
1465    // Try not to write the file again if it's already the same.
1466    // This is more friendly for cache systems like ccache, which by default
1467    // uses mtime to determine if a recompilation is needed.
1468    let existing_content = fs::read(path).unwrap_or_default();
1469    if existing_content != buf {
1470        OpenOptions::new()
1471            .create(true)
1472            .write(true)
1473            .truncate(true)
1474            .mode(0o700)
1475            .open(path)?
1476            .write_all(&buf)?;
1477    }
1478    Ok(())
1479}
1480
1481/// Write a zig cc wrapper batch script for windows
1482#[cfg(not(target_family = "unix"))]
1483fn write_linker_wrapper(path: &Path, command: &str, args: &str) -> Result<()> {
1484    let mut buf = Vec::<u8>::new();
1485    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1486        PathBuf::from(exe)
1487    } else {
1488        env::current_exe()?
1489    };
1490    let current_exe = if is_mingw_shell() {
1491        current_exe.to_slash_lossy().to_string()
1492    } else {
1493        current_exe.display().to_string()
1494    };
1495    writeln!(
1496        &mut buf,
1497        "\"{}\" zig {} -- {} %*",
1498        adjust_canonicalization(current_exe),
1499        command,
1500        args
1501    )?;
1502
1503    let existing_content = fs::read(path).unwrap_or_default();
1504    if existing_content != buf {
1505        fs::write(path, buf)?;
1506    }
1507    Ok(())
1508}
1509
1510pub(crate) fn is_mingw_shell() -> bool {
1511    env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
1512}
1513
1514// https://stackoverflow.com/a/50323079/3549270
1515#[cfg(target_os = "windows")]
1516pub fn adjust_canonicalization(p: String) -> String {
1517    const VERBATIM_PREFIX: &str = r#"\\?\"#;
1518    if p.starts_with(VERBATIM_PREFIX) {
1519        p[VERBATIM_PREFIX.len()..].to_string()
1520    } else {
1521        p
1522    }
1523}
1524
1525fn python_path() -> Result<PathBuf> {
1526    let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
1527    Ok(which::which(python)?)
1528}
1529
1530fn zig_path() -> Result<PathBuf> {
1531    let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
1532    Ok(which::which(zig)?)
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537    use super::*;
1538
1539    #[test]
1540    fn test_target_flags() {
1541        let cases = [
1542            // Input, TargetCPU, TargetFeature
1543            ("-C target-feature=-crt-static", "", "-crt-static"),
1544            ("-C target-cpu=native", "native", ""),
1545            (
1546                "--deny warnings --codegen target-feature=+crt-static",
1547                "",
1548                "+crt-static",
1549            ),
1550            ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
1551            ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
1552            (
1553                "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
1554                "native",
1555                "-avx512bf16,-avx512bitalg",
1556            ),
1557            (
1558                "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
1559                "x86-64",
1560                "",
1561            ),
1562            (
1563                "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
1564                "",
1565                "+crt-static,+avx",
1566            ),
1567        ];
1568
1569        for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
1570            let args = cargo_config2::Flags::from_space_separated(input);
1571            let encoded_rust_flags = args.encode().unwrap();
1572            let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
1573            assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
1574            assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
1575        }
1576    }
1577}