Skip to main content

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