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