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