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