Skip to main content

cargo_zigbuild/zig/
wrapper.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};
9
10use anyhow::{Context, Result, bail};
11use fs_err as fs;
12#[cfg(not(target_family = "unix"))]
13use path_slash::PathBufExt;
14use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
15
16use super::cli_config::CliConfig;
17use super::locate::cache_dir;
18use super::{Zig, get_dlltool_name, has_system_dlltool};
19
20/// zig wrapper paths, and the zig target they were prepared for
21#[derive(Debug, Clone)]
22pub struct ZigWrapper {
23    pub cc: PathBuf,
24    pub cxx: PathBuf,
25    pub ar: PathBuf,
26    pub ranlib: PathBuf,
27    pub lib: PathBuf,
28    pub target: String,
29}
30
31#[derive(Debug, Clone, Default, PartialEq)]
32struct TargetFlags {
33    pub target_cpu: String,
34    pub target_feature: String,
35}
36
37impl TargetFlags {
38    pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
39        let mut parsed = Self::default();
40
41        let f = rustflags::from_encoded(encoded);
42        for flag in f {
43            if let rustflags::Flag::Codegen { opt, value } = flag {
44                let key = opt.replace('-', "_");
45                match key.as_str() {
46                    "target_cpu" => {
47                        if let Some(value) = value {
48                            parsed.target_cpu = value;
49                        }
50                    }
51                    "target_feature" => {
52                        // See https://github.com/rust-lang/rust/blob/7e3ba5b8b7556073ab69822cc36b93d6e74cd8c9/compiler/rustc_session/src/options.rs#L1233
53                        if let Some(value) = value {
54                            if !parsed.target_feature.is_empty() {
55                                parsed.target_feature.push(',');
56                            }
57                            parsed.target_feature.push_str(&value);
58                        }
59                    }
60                    _ => {}
61                }
62            }
63        }
64        Ok(parsed)
65    }
66}
67
68/// Prepare wrapper scripts for `zig cc` and `zig c++` and returns their paths
69///
70/// We want to use `zig cc` as linker and c compiler. We want to call `python -m ziglang cc`, but
71/// cargo only accepts a path to an executable as linker, so we add a wrapper script. We then also
72/// use the wrapper script to pass arguments and substitute an unsupported argument.
73///
74/// We create different files for different args because otherwise cargo might skip recompiling even
75/// if the linker target changed
76#[allow(clippy::blocks_in_conditions)]
77pub fn prepare_zig_linker(
78    target: &str,
79    cargo_config: &cargo_config2::Config,
80) -> Result<ZigWrapper> {
81    prepare_zig_linker_with_cli_config(target, cargo_config, &[], None)
82}
83
84/// Like [`prepare_zig_linker`], but additionally honors rustflags passed via
85/// cargo's `--config` CLI option (`config_args`) when deriving the `-mcpu`
86/// passed to `zig cc`.
87pub fn prepare_zig_linker_with_cli_config(
88    target: &str,
89    cargo_config: &cargo_config2::Config,
90    config_args: &[String],
91    macosx_deployment_target: Option<&str>,
92) -> Result<ZigWrapper> {
93    let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
94    let abi_suffix = if abi_suffix.is_empty() {
95        String::new()
96    } else {
97        if abi_suffix
98            .split_once('.')
99            .filter(|(x, y)| {
100                !x.is_empty()
101                    && x.chars().all(|c| c.is_ascii_digit())
102                    && !y.is_empty()
103                    && y.chars().all(|c| c.is_ascii_digit())
104            })
105            .is_none()
106        {
107            bail!("Malformed zig target abi suffix.")
108        }
109        format!(".{abi_suffix}")
110    };
111    let triple: Triple = rust_target
112        .parse()
113        .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
114    let abi_suffix = if matches!(
115        triple.operating_system,
116        OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)
117    ) {
118        if !abi_suffix.is_empty() {
119            bail!(
120                "Version suffixes are not supported for macOS targets, set MACOSX_DEPLOYMENT_TARGET instead."
121            )
122        }
123        let env_version = env::var("MACOSX_DEPLOYMENT_TARGET").ok();
124        match macosx_deployment_target.or(env_version.as_deref()) {
125            Some(version) if !version.is_empty() => normalize_macos_version_suffix(version)?,
126            _ => String::new(),
127        }
128    } else {
129        abi_suffix
130    };
131    let arch = triple.architecture.to_string();
132    let target_env = zig_target_env(&triple);
133    let file_ext = if cfg!(windows) { "bat" } else { "sh" };
134    let file_target = target.trim_end_matches('.');
135
136    let mut cc_args = vec![
137        // prevent stripping
138        "-g".to_owned(),
139        // disable sanitizers
140        "-fno-sanitize=all".to_owned(),
141    ];
142
143    // TODO: Maybe better to assign mcpu according to:
144    // rustc --target <target> -Z unstable-options --print target-spec-json
145    let zig_mcpu_default = match triple.operating_system {
146        OperatingSystem::Linux => {
147            match arch.as_str() {
148                // zig uses _ instead of - in cpu features
149                "arm" => match target_env {
150                    Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
151                    Environment::Gnueabihf | Environment::Musleabihf => {
152                        "generic+v6+strict_align+vfp2-d32"
153                    }
154                    _ => "",
155                },
156                "armv5te" => "generic+v5te+soft_float+strict_align",
157                "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
158                "thumbv7neon" => "generic+v7a+vfp3+thumb2+neon",
159                arch_str @ ("i586" | "i686") => {
160                    if arch_str == "i586" {
161                        "pentium"
162                    } else {
163                        "pentium4"
164                    }
165                }
166                "riscv64gc" => "generic_rv64+m+a+f+d+c",
167                "riscv64a23" => "generic_rv64+rva23u64",
168                "s390x" => "z10-vector",
169                _ => "",
170            }
171        }
172        _ => "",
173    };
174
175    // Override mcpu from RUSTFLAGS if provided. The override happens when
176    // commands like `cargo-zigbuild build` are invoked.
177    // Currently we only override according to target_cpu.
178    let zig_mcpu_override = {
179        let cli_config = CliConfig::parse(config_args)?;
180        let rust_flags = cli_config
181            .rustflags(cargo_config, rust_target)?
182            .unwrap_or_default();
183        let encoded_rust_flags = rust_flags.encode()?;
184        let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
185        // Note: zig uses _ instead of - for target_cpu and target_feature
186        // target_cpu may be empty string, which means target_cpu is not specified.
187        target_flags.target_cpu.replace('-', "_")
188    };
189
190    if !zig_mcpu_override.is_empty() {
191        cc_args.push(format!("-mcpu={zig_mcpu_override}"));
192    } else if !zig_mcpu_default.is_empty() {
193        cc_args.push(format!("-mcpu={zig_mcpu_default}"));
194    }
195
196    let zig_target = zig_target_triple(
197        rust_target,
198        &triple,
199        target_env,
200        &abi_suffix,
201        &Zig::zig_version()?,
202    )?;
203    cc_args.push("-target".to_string());
204    cc_args.push(zig_target.clone());
205
206    let zig_linker_dir = cache_dir();
207    fs::create_dir_all(&zig_linker_dir)?;
208
209    if triple.operating_system == OperatingSystem::Linux {
210        if matches!(
211            triple.environment,
212            Environment::Gnu
213                | Environment::Gnuspe
214                | Environment::Gnux32
215                | Environment::Gnueabi
216                | Environment::Gnuabi64
217                | Environment::GnuIlp32
218                | Environment::Gnueabihf
219        ) {
220            let glibc_version = if abi_suffix.is_empty() {
221                (2, 17)
222            } else {
223                let mut parts = abi_suffix[1..].split('.');
224                let major: usize = parts.next().unwrap().parse()?;
225                let minor: usize = parts.next().unwrap().parse()?;
226                (major, minor)
227            };
228            // See https://github.com/ziglang/zig/issues/9485
229            if glibc_version < (2, 28) {
230                use crate::linux::{FCNTL_H, FCNTL_MAP};
231
232                let zig_version = Zig::zig_version()?;
233                if zig_version.major == 0 && zig_version.minor < 11 {
234                    let fcntl_map = zig_linker_dir.join("fcntl.map");
235                    let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
236                    if existing_content != FCNTL_MAP {
237                        fs::write(&fcntl_map, FCNTL_MAP)?;
238                    }
239                    let fcntl_h = zig_linker_dir.join("fcntl.h");
240                    let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
241                    if existing_content != FCNTL_H {
242                        fs::write(&fcntl_h, FCNTL_H)?;
243                    }
244
245                    cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
246                    cc_args.push("-include".to_string());
247                    cc_args.push(fcntl_h.display().to_string());
248                }
249            }
250        } else if matches!(
251            triple.environment,
252            Environment::Musl
253                | Environment::Muslabi64
254                | Environment::Musleabi
255                | Environment::Musleabihf
256        ) {
257            use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
258
259            let zig_version = Zig::zig_version()?;
260            let rustc_version = rustc_version::version_meta()?.semver;
261
262            // as zig 0.11.0 is released, its musl has been upgraded to 1.2.4 with break changes
263            // but rust is still with musl 1.2.3
264            // we need this workaround before rust 1.72
265            // https://github.com/ziglang/zig/pull/16098
266            if (zig_version.major, zig_version.minor) >= (0, 11)
267                && (rustc_version.major, rustc_version.minor) < (1, 72)
268            {
269                let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
270                fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
271
272                cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
273            }
274        }
275    }
276
277    // Use platform-specific quoting: shell_words for Unix (single quotes),
278    // custom quoting for Windows batch files (double quotes)
279    let cc_args_str = join_args_for_script(&cc_args);
280
281    // Put all generated wrappers and symlinks in a per-exe subdirectory so
282    // that parallel builds driven by different binaries (e.g. multiple maturin
283    // instances in separate temp venvs) never clobber each other.
284    // See https://github.com/rust-cross/cargo-zigbuild/issues/318
285    let current_exe = resolve_current_exe()?;
286    let exe_hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC)
287        .checksum(current_exe.as_os_str().as_encoded_bytes());
288    let wrapper_dir = zig_linker_dir
289        .join("wrappers")
290        .join(format!("{:x}", exe_hash));
291    fs::create_dir_all(&wrapper_dir)?;
292
293    let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
294    let zig_cc = wrapper_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
295    let zig_cxx = wrapper_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
296    let zig_ranlib = wrapper_dir.join(format!("zigranlib.{file_ext}"));
297    let zig_version = Zig::zig_version()?;
298    let zig_command = Zig::find_zig()?;
299    write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version, &zig_command)?;
300    write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version, &zig_command)?;
301    write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version, &zig_command)?;
302
303    let exe_ext = if cfg!(windows) { ".exe" } else { "" };
304    let zig_ar = wrapper_dir.join(format!("ar{exe_ext}"));
305    symlink_wrapper(&zig_ar)?;
306    let zig_lib = wrapper_dir.join(format!("lib{exe_ext}"));
307    symlink_wrapper(&zig_lib)?;
308
309    // Create dlltool symlinks for Windows GNU targets, but only if no system dlltool exists
310    // On Windows hosts, rustc looks for "dlltool.exe"
311    // On non-Windows hosts, rustc looks for architecture-specific names
312    //
313    // See https://github.com/rust-lang/rust/blob/a18e6d9d1473d9b25581dd04bef6c7577999631c/compiler/rustc_codegen_ssa/src/back/archive.rs#L275-L309
314    if matches!(triple.operating_system, OperatingSystem::Windows)
315        && matches!(triple.environment, Environment::Gnu)
316    {
317        // Only create zig dlltool wrapper if no system dlltool is found
318        // System dlltool (from mingw-w64) handles raw-dylib better than zig's dlltool
319        if !has_system_dlltool(&triple.architecture) {
320            let dlltool_name = get_dlltool_name(&triple.architecture);
321            let zig_dlltool = wrapper_dir.join(format!("{dlltool_name}{exe_ext}"));
322            symlink_wrapper(&zig_dlltool)?;
323        }
324    }
325
326    Ok(ZigWrapper {
327        cc: zig_cc,
328        cxx: zig_cxx,
329        ar: zig_ar,
330        ranlib: zig_ranlib,
331        lib: zig_lib,
332        target: zig_target,
333    })
334}
335
336/// Resolve the current executable path, preferring the test override env var.
337fn resolve_current_exe() -> Result<PathBuf> {
338    if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
339        Ok(PathBuf::from(exe))
340    } else {
341        Ok(env::current_exe()?)
342    }
343}
344
345pub(crate) fn symlink_wrapper(target: &Path) -> Result<()> {
346    let current_exe = resolve_current_exe()?;
347    #[cfg(windows)]
348    {
349        if !target.exists() {
350            // symlink on Windows requires admin privileges so we use hardlink instead
351            if std::fs::hard_link(&current_exe, target).is_err() {
352                // hard_link doesn't support cross-device links so we fallback to copy
353                std::fs::copy(&current_exe, target)?;
354            }
355        }
356    }
357
358    #[cfg(unix)]
359    {
360        if !target.exists() {
361            if fs::read_link(target).is_ok() {
362                // remove broken symlink
363                fs::remove_file(target)?;
364            }
365            std::os::unix::fs::symlink(current_exe, target)?;
366        }
367    }
368    Ok(())
369}
370
371/// Join arguments for Unix shell script using shell_words (single quotes)
372#[cfg(target_family = "unix")]
373fn join_args_for_script<I, S>(args: I) -> String
374where
375    I: IntoIterator<Item = S>,
376    S: AsRef<str>,
377{
378    shell_words::join(args)
379}
380
381/// Quote a string for Windows batch file (cmd.exe)
382///
383/// - `%` expands even inside quotes, so we escape it as `%%`.
384/// - We disable delayed expansion in the wrapper script, so `!` should not expand.
385/// - Internal `"` are escaped by doubling them (`""`).
386#[cfg(any(not(target_family = "unix"), test))]
387fn quote_for_batch(s: &str) -> String {
388    let needs_quoting_or_escaping = s.is_empty()
389        || s.contains(|c: char| {
390            matches!(
391                c,
392                ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
393            )
394        });
395
396    if !needs_quoting_or_escaping {
397        return s.to_string();
398    }
399
400    let mut out = String::with_capacity(s.len() + 8);
401    out.push('"');
402    for c in s.chars() {
403        match c {
404            '"' => out.push_str("\"\""),
405            '%' => out.push_str("%%"),
406            _ => out.push(c),
407        }
408    }
409    out.push('"');
410    out
411}
412
413/// Join arguments for Windows batch file using double quotes
414#[cfg(not(target_family = "unix"))]
415fn join_args_for_script<I, S>(args: I) -> String
416where
417    I: IntoIterator<Item = S>,
418    S: AsRef<str>,
419{
420    args.into_iter()
421        .map(|s| quote_for_batch(s.as_ref()))
422        .collect::<Vec<_>>()
423        .join(" ")
424}
425
426/// Write a zig cc wrapper batch script for unix
427#[cfg(target_family = "unix")]
428fn write_linker_wrapper(
429    path: &Path,
430    command: &str,
431    args: &str,
432    zig_version: &semver::Version,
433    zig_command: &(PathBuf, Vec<String>),
434) -> Result<()> {
435    let mut buf = Vec::<u8>::new();
436    let current_exe = resolve_current_exe()?;
437    writeln!(&mut buf, "#!/bin/sh")?;
438
439    // Export zig version to avoid spawning `zig version` subprocess
440    writeln!(
441        &mut buf,
442        "export CARGO_ZIGBUILD_ZIG_VERSION={}",
443        zig_version
444    )?;
445    // Export the resolved zig command to avoid re-probing for
446    // `python -m ziglang` / `zig` on every compiler invocation
447    writeln!(
448        &mut buf,
449        "export CARGO_ZIGBUILD_ZIG_COMMAND={}",
450        shell_words::quote(&zig_command.0.to_string_lossy())
451    )?;
452    if !zig_command.1.is_empty() {
453        writeln!(
454            &mut buf,
455            "export CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}",
456            shell_words::quote(&zig_command.1.join(" "))
457        )?;
458    }
459
460    // Pass through SDKROOT if it exists at runtime
461    writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
462
463    writeln!(
464        &mut buf,
465        "exec \"{}\" zig {} -- {} \"$@\"",
466        current_exe.display(),
467        command,
468        args
469    )?;
470
471    // Try not to write the file again if it's already the same.
472    // This is more friendly for cache systems like ccache, which by default
473    // uses mtime to determine if a recompilation is needed.
474    let existing_content = fs::read(path).unwrap_or_default();
475    if existing_content != buf {
476        OpenOptions::new()
477            .create(true)
478            .write(true)
479            .truncate(true)
480            .mode(0o700)
481            .open(path)?
482            .write_all(&buf)?;
483    }
484    Ok(())
485}
486
487/// Write a zig cc wrapper batch script for windows
488#[cfg(not(target_family = "unix"))]
489fn write_linker_wrapper(
490    path: &Path,
491    command: &str,
492    args: &str,
493    zig_version: &semver::Version,
494    zig_command: &(PathBuf, Vec<String>),
495) -> Result<()> {
496    let mut buf = Vec::<u8>::new();
497    let current_exe = resolve_current_exe()?;
498    let current_exe = if is_mingw_shell() {
499        current_exe.to_slash_lossy().to_string()
500    } else {
501        current_exe.display().to_string()
502    };
503    writeln!(&mut buf, "@echo off")?;
504    // Prevent `!VAR!` expansion surprises (delayed expansion) in user-controlled args.
505    writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
506    // Set zig version to avoid spawning `zig version` subprocess
507    writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
508    // Set the resolved zig command to avoid re-probing for
509    // `python -m ziglang` / `zig` on every compiler invocation
510    writeln!(
511        &mut buf,
512        "set \"CARGO_ZIGBUILD_ZIG_COMMAND={}\"",
513        zig_command.0.display()
514    )?;
515    if !zig_command.1.is_empty() {
516        writeln!(
517            &mut buf,
518            "set \"CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}\"",
519            zig_command.1.join(" ")
520        )?;
521    }
522    writeln!(
523        &mut buf,
524        "\"{}\" zig {} -- {} %*",
525        adjust_canonicalization(current_exe),
526        command,
527        args
528    )?;
529
530    let existing_content = fs::read(path).unwrap_or_default();
531    if existing_content != buf {
532        fs::write(path, buf)?;
533    }
534    Ok(())
535}
536
537pub(crate) fn is_mingw_shell() -> bool {
538    env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
539}
540
541// https://stackoverflow.com/a/50323079/3549270
542#[cfg(target_os = "windows")]
543pub fn adjust_canonicalization(p: String) -> String {
544    const VERBATIM_PREFIX: &str = r#"\\?\"#;
545    if p.starts_with(VERBATIM_PREFIX) {
546        p[VERBATIM_PREFIX.len()..].to_string()
547    } else {
548        p
549    }
550}
551
552/// Maps a Rust target's environment to the one zig names it by.
553fn zig_target_env(triple: &Triple) -> Environment {
554    match (triple.architecture, triple.environment) {
555        (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
556        (Architecture::Mips32(..), Environment::Musl) => Environment::Musleabi,
557        (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
558        (_, Environment::GnuLlvm) => Environment::Gnu,
559        (_, environment) => environment,
560    }
561}
562
563/// Turn a `MACOSX_DEPLOYMENT_TARGET` value into a zig target version suffix,
564/// e.g. `11` -> `.11.0` and `10.15.7` -> `.10.15.7`, as zig requires at least
565/// `major.minor`.
566fn normalize_macos_version_suffix(version: &str) -> Result<String> {
567    let mut parts = 0;
568    for part in version.split('.') {
569        parts += 1;
570        if parts > 3 || part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
571            bail!("Malformed MACOSX_DEPLOYMENT_TARGET '{version}'.")
572        }
573    }
574    if parts == 1 {
575        Ok(format!(".{version}.0"))
576    } else {
577        Ok(format!(".{version}"))
578    }
579}
580
581/// Build the target triple to pass to `zig cc -target`, where `abi_suffix` is
582/// the glibc or macOS version including its leading dot (`.2.36`, `.13.0`)
583/// or an empty string.
584fn zig_target_triple(
585    rust_target: &str,
586    triple: &Triple,
587    target_env: Environment,
588    abi_suffix: &str,
589    zig_version: &semver::Version,
590) -> Result<String> {
591    let arch = triple.architecture.to_string();
592    let zig_target = match triple.operating_system {
593        OperatingSystem::Linux => {
594            let zig_arch = match arch.as_str() {
595                // zig uses _ instead of - in cpu features
596                "arm" => "arm",
597                "armv5te" => "arm",
598                "armv7" => "arm",
599                "thumbv7neon" => "arm",
600                "i586" | "i686" => {
601                    if zig_version.major == 0 && zig_version.minor >= 11 {
602                        "x86"
603                    } else {
604                        "i386"
605                    }
606                }
607                "riscv64gc" | "riscv64a23" => "riscv64",
608                "s390x" => "s390x",
609                _ => arch.as_str(),
610            };
611            let mut zig_target_env = target_env.to_string();
612
613            // Since Zig 0.15.0, arm-linux-ohos changed to arm-linux-ohoseabi
614            // We need to follow the change but target_lexicon follow the LLVM target(https://github.com/bytecodealliance/target-lexicon/pull/123).
615            // So we use string directly.
616            if *zig_version >= semver::Version::new(0, 15, 0)
617                && arch.as_str() == "armv7"
618                && target_env == Environment::Ohos
619            {
620                zig_target_env = "ohoseabi".to_string();
621            }
622
623            format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}")
624        }
625        OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
626            // Zig 0.10.0 switched macOS ABI to none
627            // see https://github.com/ziglang/zig/pull/11684
628            if *zig_version > semver::Version::new(0, 9, 1) {
629                format!("{arch}-macos{abi_suffix}-none")
630            } else {
631                format!("{arch}-macos{abi_suffix}-gnu")
632            }
633        }
634        OperatingSystem::Windows => {
635            let zig_arch = match arch.as_str() {
636                "i686" => {
637                    if zig_version.major == 0 && zig_version.minor >= 11 {
638                        "x86"
639                    } else {
640                        "i386"
641                    }
642                }
643                arch => arch,
644            };
645            format!("{zig_arch}-windows-{target_env}{abi_suffix}")
646        }
647        OperatingSystem::Emscripten => {
648            format!("{arch}-emscripten{abi_suffix}")
649        }
650        OperatingSystem::Wasi => {
651            format!("{arch}-wasi{abi_suffix}")
652        }
653        OperatingSystem::WasiP1 => {
654            format!("{arch}-wasi.0.1.0{abi_suffix}")
655        }
656        OperatingSystem::IOS(_) if triple.environment == Environment::Macabi => {
657            // Mac Catalyst (aarch64-apple-ios-macabi / x86_64-apple-ios-macabi)
658            // maps to zig's maccatalyst target
659            format!("{arch}-maccatalyst-none{abi_suffix}")
660        }
661        OperatingSystem::Freebsd => {
662            let zig_arch = match arch.as_str() {
663                "i686" => {
664                    if zig_version.major == 0 && zig_version.minor >= 11 {
665                        "x86"
666                    } else {
667                        "i386"
668                    }
669                }
670                arch => arch,
671            };
672            format!("{zig_arch}-freebsd")
673        }
674        OperatingSystem::Openbsd => {
675            format!("{arch}-openbsd")
676        }
677        OperatingSystem::Unknown => {
678            if triple.architecture == Architecture::Wasm32
679                || triple.architecture == Architecture::Wasm64
680            {
681                format!("{arch}-freestanding{abi_suffix}")
682            } else {
683                bail!("unsupported target '{rust_target}'")
684            }
685        }
686        _ => bail!(format!("unsupported target '{rust_target}'")),
687    };
688    Ok(zig_target)
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694
695    #[test]
696    fn test_target_flags() {
697        let cases = [
698            // Input, TargetCPU, TargetFeature
699            ("-C target-feature=-crt-static", "", "-crt-static"),
700            ("-C target-cpu=native", "native", ""),
701            (
702                "--deny warnings --codegen target-feature=+crt-static",
703                "",
704                "+crt-static",
705            ),
706            ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
707            ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
708            (
709                "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
710                "native",
711                "-avx512bf16,-avx512bitalg",
712            ),
713            (
714                "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
715                "x86-64",
716                "",
717            ),
718            (
719                "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
720                "",
721                "+crt-static,+avx",
722            ),
723        ];
724
725        for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
726            let args = cargo_config2::Flags::from_space_separated(input);
727            let encoded_rust_flags = args.encode().unwrap();
728            let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
729            assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
730            assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
731        }
732    }
733
734    #[test]
735    fn test_join_args_for_script() {
736        // Test basic arguments without special characters
737        let args = vec!["-target", "x86_64-linux-gnu"];
738        let result = join_args_for_script(&args);
739        assert!(result.contains("-target"));
740        assert!(result.contains("x86_64-linux-gnu"));
741    }
742
743    #[test]
744    fn test_quote_for_batch() {
745        // Simple argument without special characters - no quoting needed
746        assert_eq!(quote_for_batch("-target"), "-target");
747        assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
748
749        // Arguments with spaces need quoting
750        assert_eq!(
751            quote_for_batch("C:\\Users\\John Doe\\path"),
752            "\"C:\\Users\\John Doe\\path\""
753        );
754
755        // Empty string needs quoting
756        assert_eq!(quote_for_batch(""), "\"\"");
757
758        // Arguments with special batch characters need quoting
759        assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
760        assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
761        assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
762        assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
763        assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
764
765        // Percent signs must be doubled even inside quotes in a batch file.
766        assert_eq!(quote_for_batch("foo%bar"), "\"foo%%bar\"");
767        assert_eq!(quote_for_batch("%PATH%"), "\"%%PATH%%\"");
768        assert_eq!(quote_for_batch("%1"), "\"%%1\"");
769
770        // Internal double quotes are escaped by doubling
771        assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
772    }
773
774    #[test]
775    #[cfg(not(target_family = "unix"))]
776    fn test_join_args_for_script_windows() {
777        // Test with path containing spaces
778        let args = vec![
779            "-target",
780            "x86_64-linux-gnu",
781            "-L",
782            "C:\\Users\\John Doe\\path",
783        ];
784        let result = join_args_for_script(&args);
785        // The path with space should be quoted
786        assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
787        // Simple args should not be quoted
788        assert!(result.contains("-target"));
789        assert!(!result.contains("\"-target\""));
790    }
791
792    /// Mirrors the prologue of [`prepare_zig_linker_with_cli_config`].
793    fn zig_target(target: &str, zig_version: &str) -> Result<String> {
794        let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
795        let abi_suffix = if abi_suffix.is_empty() {
796            String::new()
797        } else {
798            format!(".{abi_suffix}")
799        };
800        let triple: Triple = rust_target.parse()?;
801        let target_env = zig_target_env(&triple);
802        zig_target_triple(
803            rust_target,
804            &triple,
805            target_env,
806            &abi_suffix,
807            &semver::Version::parse(zig_version)?,
808        )
809    }
810
811    #[test]
812    fn test_zig_target_triple() {
813        let cases = [
814            // Rust target (with optional glibc suffix), zig version, zig target
815            ("x86_64-unknown-linux-gnu", "0.15.2", "x86_64-linux-gnu"),
816            (
817                "x86_64-unknown-linux-gnu.2.36",
818                "0.15.2",
819                "x86_64-linux-gnu.2.36",
820            ),
821            ("aarch64-unknown-linux-musl", "0.15.2", "aarch64-linux-musl"),
822            (
823                "armv7-unknown-linux-gnueabihf",
824                "0.15.2",
825                "arm-linux-gnueabihf",
826            ),
827            (
828                "arm-unknown-linux-gnueabihf",
829                "0.15.2",
830                "arm-linux-gnueabihf",
831            ),
832            (
833                "thumbv7neon-unknown-linux-gnueabihf",
834                "0.15.2",
835                "arm-linux-gnueabihf",
836            ),
837            ("riscv64gc-unknown-linux-gnu", "0.15.2", "riscv64-linux-gnu"),
838            (
839                "riscv64a23-unknown-linux-gnu",
840                "0.15.2",
841                "riscv64-linux-gnu",
842            ),
843            ("s390x-unknown-linux-gnu", "0.15.2", "s390x-linux-gnu"),
844            // zig 0.11 renamed the 32-bit x86 arch from i386 to x86
845            ("i686-unknown-linux-gnu", "0.15.2", "x86-linux-gnu"),
846            ("i686-unknown-linux-gnu", "0.10.1", "i386-linux-gnu"),
847            // zig 0.15 renamed arm-linux-ohos to arm-linux-ohoseabi
848            ("armv7-unknown-linux-ohos", "0.15.2", "arm-linux-ohoseabi"),
849            ("armv7-unknown-linux-ohos", "0.14.1", "arm-linux-ohos"),
850            (
851                "powerpc-unknown-linux-gnu",
852                "0.15.2",
853                "powerpc-linux-gnueabihf",
854            ),
855            // zig 0.10 switched the macOS abi to none
856            ("aarch64-apple-darwin", "0.15.2", "aarch64-macos-none"),
857            ("aarch64-apple-darwin", "0.9.1", "aarch64-macos-gnu"),
858            (
859                "aarch64-apple-ios-macabi",
860                "0.15.2",
861                "aarch64-maccatalyst-none",
862            ),
863            ("x86_64-pc-windows-gnu", "0.15.2", "x86_64-windows-gnu"),
864            ("i686-pc-windows-gnu", "0.15.2", "x86-windows-gnu"),
865            ("i686-pc-windows-gnu", "0.10.1", "i386-windows-gnu"),
866            ("wasm32-wasip1", "0.15.2", "wasm32-wasi.0.1.0"),
867            ("wasm32-wasi", "0.15.2", "wasm32-wasi"),
868            ("wasm32-unknown-unknown", "0.15.2", "wasm32-freestanding"),
869            ("wasm32-unknown-emscripten", "0.15.2", "wasm32-emscripten"),
870            ("x86_64-unknown-freebsd", "0.15.2", "x86_64-freebsd"),
871            ("x86_64-unknown-openbsd", "0.15.2", "x86_64-openbsd"),
872        ];
873
874        for (target, zig_version, expected) in cases {
875            assert_eq!(
876                zig_target(target, zig_version).unwrap(),
877                expected,
878                "{target} with zig {zig_version}"
879            );
880        }
881    }
882
883    #[test]
884    fn test_zig_target_triple_macos_version() {
885        let cases = [
886            (
887                "aarch64-apple-darwin",
888                "13.0",
889                "0.15.2",
890                "aarch64-macos.13.0-none",
891            ),
892            (
893                "aarch64-apple-darwin",
894                "11",
895                "0.15.2",
896                "aarch64-macos.11.0-none",
897            ),
898            (
899                "x86_64-apple-darwin",
900                "10.15.7",
901                "0.15.2",
902                "x86_64-macos.10.15.7-none",
903            ),
904            (
905                "x86_64-apple-darwin",
906                "10.15",
907                "0.9.1",
908                "x86_64-macos.10.15-gnu",
909            ),
910        ];
911
912        for (target, version, zig_version, expected) in cases {
913            let triple: Triple = target.parse().unwrap();
914            let zig_target = zig_target_triple(
915                target,
916                &triple,
917                zig_target_env(&triple),
918                &normalize_macos_version_suffix(version).unwrap(),
919                &semver::Version::parse(zig_version).unwrap(),
920            )
921            .unwrap();
922            assert_eq!(zig_target, expected, "{target} with {version}");
923        }
924    }
925
926    #[test]
927    fn test_normalize_macos_version_suffix_malformed() {
928        for version in ["", "abc", "13.", ".13", "13.0.1.2", "13.x"] {
929            assert!(
930                normalize_macos_version_suffix(version).is_err(),
931                "{version}"
932            );
933        }
934    }
935
936    #[test]
937    fn test_zig_target_triple_unsupported() {
938        let err = zig_target("x86_64-unknown-redox", "0.15.2").unwrap_err();
939        assert!(
940            err.to_string().contains("unsupported target"),
941            "unexpected error: {err}"
942        );
943    }
944}