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