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