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