Skip to main content

cargo_zigbuild/
zig.rs

1use std::env;
2use std::ffi::OsStr;
3#[cfg(target_family = "unix")]
4use std::fs::OpenOptions;
5use std::io::Write;
6#[cfg(target_family = "unix")]
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::{Path, PathBuf};
9use std::process::{self, Command};
10use std::str;
11use std::sync::OnceLock;
12
13use anyhow::{anyhow, bail, Context, Result};
14use fs_err as fs;
15use path_slash::PathBufExt;
16use serde::Deserialize;
17use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
18
19use crate::linux::ARM_FEATURES_H;
20use crate::macos::{LIBCHARSET_TBD, LIBICONV_TBD};
21
22/// Zig linker wrapper
23#[derive(Clone, Debug, clap::Subcommand)]
24pub enum Zig {
25    /// `zig cc` wrapper
26    #[command(name = "cc")]
27    Cc {
28        /// `zig cc` arguments
29        #[arg(num_args = 1.., trailing_var_arg = true)]
30        args: Vec<String>,
31    },
32    /// `zig c++` wrapper
33    #[command(name = "c++")]
34    Cxx {
35        /// `zig c++` arguments
36        #[arg(num_args = 1.., trailing_var_arg = true)]
37        args: Vec<String>,
38    },
39    /// `zig ar` wrapper
40    #[command(name = "ar")]
41    Ar {
42        /// `zig ar` arguments
43        #[arg(num_args = 1.., trailing_var_arg = true)]
44        args: Vec<String>,
45    },
46    /// `zig ranlib` wrapper
47    #[command(name = "ranlib")]
48    Ranlib {
49        /// `zig ranlib` arguments
50        #[arg(num_args = 1.., trailing_var_arg = true)]
51        args: Vec<String>,
52    },
53    /// `zig lib` wrapper
54    #[command(name = "lib")]
55    Lib {
56        /// `zig lib` arguments
57        #[arg(num_args = 1.., trailing_var_arg = true)]
58        args: Vec<String>,
59    },
60    /// `zig dlltool` wrapper
61    #[command(name = "dlltool")]
62    Dlltool {
63        /// `zig dlltool` arguments
64        #[arg(num_args = 1.., trailing_var_arg = true)]
65        args: Vec<String>,
66    },
67}
68
69struct TargetInfo {
70    target: Option<String>,
71    is_musl: bool,
72    is_windows_gnu: bool,
73    is_windows_msvc: bool,
74    is_arm: bool,
75    is_i386: bool,
76    is_riscv64: bool,
77    is_mips32: bool,
78    is_macos: bool,
79    is_ohos: bool,
80    is_freebsd: bool,
81}
82
83impl TargetInfo {
84    fn new(target: Option<&String>) -> Self {
85        Self {
86            target: target.cloned(),
87            is_musl: target.map(|x| x.contains("musl")).unwrap_or_default(),
88            is_windows_gnu: target
89                .map(|x| x.contains("windows-gnu"))
90                .unwrap_or_default(),
91            is_windows_msvc: target
92                .map(|x| x.contains("windows-msvc"))
93                .unwrap_or_default(),
94            is_arm: target.map(|x| x.starts_with("arm")).unwrap_or_default(),
95            is_i386: target.map(|x| x.starts_with("i386")).unwrap_or_default(),
96            is_riscv64: target.map(|x| x.starts_with("riscv64")).unwrap_or_default(),
97            is_mips32: target
98                .map(|x| x.starts_with("mips") && !x.starts_with("mips64"))
99                .unwrap_or_default(),
100            is_macos: target.map(|x| x.contains("macos")).unwrap_or_default(),
101            is_ohos: target.map(|x| x.contains("ohos")).unwrap_or_default(),
102            is_freebsd: target.map(|x| x.contains("freebsd")).unwrap_or_default(),
103        }
104    }
105}
106
107impl Zig {
108    /// Execute the underlying zig command
109    pub fn execute(&self) -> Result<()> {
110        match self {
111            Zig::Cc { args } => self.execute_compiler("cc", args),
112            Zig::Cxx { args } => self.execute_compiler("c++", args),
113            Zig::Ar { args } => self.execute_tool("ar", args),
114            Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
115            Zig::Lib { args } => self.execute_compiler("lib", args),
116            Zig::Dlltool { args } => self.execute_tool("dlltool", args),
117        }
118    }
119
120    /// Execute zig cc/c++ command
121    pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
122        let target = cmd_args
123            .iter()
124            .position(|x| x == "-target")
125            .and_then(|index| cmd_args.get(index + 1));
126        let target_info = TargetInfo::new(target);
127
128        let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
129            Ok(version) => version.parse()?,
130            Err(_) => rustc_version::version()?,
131        };
132        let zig_version = Zig::zig_version()?;
133
134        let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
135        let mut skip_next_arg = false;
136        for arg in cmd_args {
137            if skip_next_arg {
138                skip_next_arg = false;
139                continue;
140            }
141            let args = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
142                vec![self.process_linker_response_file(
143                    arg,
144                    &rustc_ver,
145                    &zig_version,
146                    &target_info,
147                )?]
148            } else {
149                self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info)
150            };
151            for arg in args {
152                if arg == "-Wl,-exported_symbols_list" || arg == "-Wl,--dynamic-list" {
153                    // Filter out this and the next argument
154                    // zig cc doesn't pass these through to lld even though lld supports them
155                    // See https://github.com/rust-cross/cargo-zigbuild/issues/355
156                    skip_next_arg = true;
157                } else {
158                    new_cmd_args.push(arg);
159                }
160            }
161        }
162
163        if target_info.is_mips32 {
164            // See https://github.com/ziglang/zig/issues/4925#issuecomment-1499823425
165            new_cmd_args.push("-Wl,-z,notext".to_string());
166        }
167
168        if self.has_undefined_dynamic_lookup(cmd_args) {
169            new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
170        }
171        if target_info.is_macos {
172            if self.should_add_libcharset(cmd_args, &zig_version) {
173                new_cmd_args.push("-lcharset".to_string());
174            }
175            self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
176        }
177
178        // For Zig >= 0.15 with macOS, set SDKROOT environment variable
179        // if it exists, instead of passing --sysroot
180        let mut command = Self::command()?;
181        if (zig_version.major, zig_version.minor) >= (0, 15) {
182            if let Some(sdkroot) = Self::macos_sdk_root() {
183                command.env("SDKROOT", sdkroot);
184            }
185        }
186
187        let mut child = command
188            .arg(cmd)
189            .args(new_cmd_args)
190            .spawn()
191            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
192        let status = child.wait().expect("Failed to wait on zig child process");
193        if !status.success() {
194            process::exit(status.code().unwrap_or(1));
195        }
196        Ok(())
197    }
198
199    fn process_linker_response_file(
200        &self,
201        arg: &str,
202        rustc_ver: &rustc_version::Version,
203        zig_version: &semver::Version,
204        target_info: &TargetInfo,
205    ) -> Result<String> {
206        // rustc passes arguments to linker via an @-file when arguments are too long
207        // See https://github.com/rust-lang/rust/issues/41190
208        // and https://github.com/rust-lang/rust/blob/87937d3b6c302dfedfa5c4b94d0a30985d46298d/compiler/rustc_codegen_ssa/src/back/link.rs#L1373-L1382
209        let content_bytes = fs::read(arg.trim_start_matches('@'))?;
210        let content = if target_info.is_windows_msvc {
211            if content_bytes[0..2] != [255, 254] {
212                bail!(
213                    "linker response file `{}` didn't start with a utf16 BOM",
214                    &arg
215                );
216            }
217            let content_utf16: Vec<u16> = content_bytes[2..]
218                .chunks_exact(2)
219                .map(|a| u16::from_ne_bytes([a[0], a[1]]))
220                .collect();
221            String::from_utf16(&content_utf16).with_context(|| {
222                format!(
223                    "linker response file `{}` didn't contain valid utf16 content",
224                    &arg
225                )
226            })?
227        } else {
228            String::from_utf8(content_bytes).with_context(|| {
229                format!(
230                    "linker response file `{}` didn't contain valid utf8 content",
231                    &arg
232                )
233            })?
234        };
235        let mut link_args: Vec<_> = content
236            .split('\n')
237            .flat_map(|arg| self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info))
238            .collect();
239        if self.has_undefined_dynamic_lookup(&link_args) {
240            link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
241        }
242        if target_info.is_macos && self.should_add_libcharset(&link_args, &zig_version) {
243            link_args.push("-lcharset".to_string());
244        }
245        if target_info.is_windows_msvc {
246            let new_content = link_args.join("\n");
247            let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
248            // start the stream with a UTF-16 BOM
249            for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
250                // encode in little endian
251                out.push(c as u8);
252                out.push((c >> 8) as u8);
253            }
254            fs::write(arg.trim_start_matches('@'), out)?;
255        } else {
256            fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
257        }
258        Ok(arg.to_string())
259    }
260
261    fn filter_linker_arg(
262        &self,
263        arg: &str,
264        rustc_ver: &rustc_version::Version,
265        zig_version: &semver::Version,
266        target_info: &TargetInfo,
267    ) -> Vec<String> {
268        if arg == "-lgcc_s" {
269            // Replace libgcc_s with libunwind
270            return vec!["-lunwind".to_string()];
271        } else if arg.starts_with("--target=") {
272            // We have already passed target via `-target`
273            return vec![];
274        } else if arg.starts_with("-e") && arg.len() > 2 && !arg.starts_with("-export") {
275            // GCC accepts -e<entry> (no space) but zig/clang requires -e <entry> (with space)
276            // Transform to -Wl,--entry=<entry> to pass directly to the linker
277            // See https://github.com/rust-cross/cargo-zigbuild/issues/378
278            let entry = &arg[2..];
279            return vec![format!("-Wl,--entry={}", entry)];
280        }
281        if (target_info.is_arm || target_info.is_windows_gnu)
282            && arg.ends_with(".rlib")
283            && arg.contains("libcompiler_builtins-")
284        {
285            // compiler-builtins is duplicated with zig's compiler-rt
286            return vec![];
287        }
288        if target_info.is_windows_gnu {
289            #[allow(clippy::if_same_then_else)]
290            if arg == "-lgcc_eh" {
291                // zig doesn't provide gcc_eh alternative
292                // We use libc++ to replace it on windows gnu targets
293                return vec!["-lc++".to_string()];
294            } else if arg == "-Wl,-Bdynamic" && (zig_version.major, zig_version.minor) >= (0, 11) {
295                // https://github.com/ziglang/zig/pull/16058
296                // zig changes the linker behavior, -Bdynamic won't search *.a for mingw, but this may be fixed in the later version
297                // here is a workaround to replace the linker switch with -search_paths_first, which will search for *.dll,*lib first,
298                // then fallback to *.a
299                return vec!["-Wl,-search_paths_first".to_owned()];
300            } else if arg == "-lwindows" || arg == "-l:libpthread.a" || arg == "-lgcc" {
301                return vec![];
302            } else if arg == "-Wl,--disable-auto-image-base"
303                || arg == "-Wl,--dynamicbase"
304                || arg == "-Wl,--large-address-aware"
305                || (arg.starts_with("-Wl,")
306                    && (arg.ends_with("/list.def") || arg.ends_with("\\list.def")))
307            {
308                // https://github.com/rust-lang/rust/blob/f0bc76ac41a0a832c9ee621e31aaf1f515d3d6a5/compiler/rustc_target/src/spec/windows_gnu_base.rs#L23
309                // https://github.com/rust-lang/rust/blob/2fb0e8d162a021f8a795fb603f5d8c0017855160/compiler/rustc_target/src/spec/windows_gnu_base.rs#L22
310                // https://github.com/rust-lang/rust/blob/f0bc76ac41a0a832c9ee621e31aaf1f515d3d6a5/compiler/rustc_target/src/spec/i686_pc_windows_gnu.rs#L16
311                // zig doesn't support --disable-auto-image-base, --dynamicbase and --large-address-aware
312                return vec![];
313            } else if arg == "-lmsvcrt" {
314                return vec![];
315            }
316        } else if arg == "-Wl,--no-undefined-version" {
317            // https://github.com/rust-lang/rust/blob/542ed2bf72b232b245ece058fc11aebb1ca507d7/compiler/rustc_codegen_ssa/src/back/linker.rs#L723
318            // zig doesn't support --no-undefined-version
319            return vec![];
320        } else if arg == "-Wl,-znostart-stop-gc" {
321            // https://github.com/rust-lang/rust/blob/c580c498a1fe144d7c5b2dfc7faab1a229aa288b/compiler/rustc_codegen_ssa/src/back/link.rs#L3371
322            // zig doesn't support -znostart-stop-gc
323            return vec![];
324        } else if arg.starts_with("-Wl,-plugin-opt") {
325            // https://github.com/rust-cross/cargo-zigbuild/issues/369
326            // zig doesn't support -plugin-opt (used for cross-lang LTO with LLVM gold plugin)
327            // Since zig cc is already LLVM-based, ignoring this is fine
328            return vec![];
329        }
330        if target_info.is_musl || target_info.is_ohos {
331            // Avoids duplicated symbols with both zig musl libc and the libc crate
332            if arg.ends_with(".o") && arg.contains("self-contained") && arg.contains("crt") {
333                return vec![];
334            } else if arg == "-Wl,-melf_i386" {
335                // unsupported linker arg: -melf_i386
336                return vec![];
337            }
338            if rustc_ver.major == 1
339                && rustc_ver.minor < 59
340                && arg.ends_with(".rlib")
341                && arg.contains("liblibc-")
342            {
343                // Rust distributes standalone libc.a in self-contained for musl since 1.59.0
344                // See https://github.com/rust-lang/rust/pull/90527
345                return vec![];
346            }
347            if arg == "-lc" {
348                return vec![];
349            }
350        }
351        if arg.starts_with("-march=") {
352            // Ignore `-march` option for arm* targets, we use `generic` + cpu features instead
353            if target_info.is_arm || target_info.is_i386 {
354                return vec![];
355            } else if target_info.is_riscv64 {
356                return vec!["-march=generic_rv64".to_string()];
357            } else if arg.starts_with("-march=armv8-a") {
358                let mut args_march = if target_info
359                    .target
360                    .as_ref()
361                    .map(|x| x.starts_with("aarch64-macos"))
362                    .unwrap_or_default()
363                {
364                    vec![arg.replace("armv8-a", "apple_m1")]
365                } else if target_info
366                    .target
367                    .as_ref()
368                    .map(|x| x.starts_with("aarch64-linux"))
369                    .unwrap_or_default()
370                {
371                    vec![arg
372                        .replace("armv8-a", "generic+v8a")
373                        .replace("simd", "neon")]
374                } else {
375                    vec![arg.to_string()]
376                };
377                if arg == "-march=armv8-a+crypto" {
378                    // Workaround for building sha1-asm on aarch64
379                    // See:
380                    // https://github.com/rust-cross/cargo-zigbuild/issues/149
381                    // https://github.com/RustCrypto/asm-hashes/blob/master/sha1/build.rs#L17-L19
382                    // https://github.com/ziglang/zig/issues/10411
383                    args_march.append(&mut vec![
384                        "-Xassembler".to_owned(),
385                        "-march=armv8-a+crypto".to_owned(),
386                    ]);
387                }
388                return args_march;
389            }
390        }
391        if target_info.is_macos {
392            if arg.starts_with("-Wl,-exported_symbols_list,") {
393                // zig doesn't support -exported_symbols_list arg
394                // https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-exported_symbols_list
395                return vec![];
396            }
397            if arg == "-Wl,-dylib" {
398                // zig doesn't support -dylib
399                return vec![];
400            }
401        }
402        if target_info.is_freebsd {
403            let ignored_libs = ["-lkvm", "-lmemstat", "-lprocstat", "-ldevstat"];
404            if ignored_libs.contains(&arg) {
405                return vec![];
406            }
407        }
408        vec![arg.to_string()]
409    }
410
411    fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
412        let undefined = args
413            .iter()
414            .position(|x| x == "-undefined")
415            .and_then(|i| args.get(i + 1));
416        matches!(undefined, Some(x) if x == "dynamic_lookup")
417    }
418
419    fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
420        // See https://github.com/apple-oss-distributions/libiconv/blob/a167071feb7a83a01b27ec8d238590c14eb6faff/xcodeconfig/libiconv.xcconfig
421        if (zig_version.major, zig_version.minor) >= (0, 12) {
422            args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
423        } else {
424            false
425        }
426    }
427
428    fn add_macos_specific_args(
429        &self,
430        new_cmd_args: &mut Vec<String>,
431        zig_version: &semver::Version,
432    ) -> Result<()> {
433        let sdkroot = Self::macos_sdk_root();
434        if (zig_version.major, zig_version.minor) >= (0, 12) {
435            // Zig 0.12.0+ requires passing `--sysroot`
436            // However, for Zig 0.15+, we should use SDKROOT environment variable instead
437            // to avoid issues with library paths being interpreted relative to sysroot
438            if let Some(ref sdkroot) = sdkroot {
439                if (zig_version.major, zig_version.minor) < (0, 15) {
440                    new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
441                }
442                // For Zig >= 0.15, SDKROOT will be set as environment variable
443            }
444        }
445        if let Some(ref sdkroot) = sdkroot {
446            let include_prefix = if (zig_version.major, zig_version.minor) < (0, 15) {
447                sdkroot
448            } else {
449                Path::new("/")
450            };
451            new_cmd_args.extend_from_slice(&[
452                "-isystem".to_string(),
453                format!("{}", include_prefix.join("usr").join("include").display()),
454                format!("-L{}", include_prefix.join("usr").join("lib").display()),
455                format!(
456                    "-F{}",
457                    include_prefix
458                        .join("System")
459                        .join("Library")
460                        .join("Frameworks")
461                        .display()
462                ),
463                "-DTARGET_OS_IPHONE=0".to_string(),
464            ]);
465        }
466
467        // Add the deps directory that contains `.tbd` files to the library search path
468        let cache_dir = cache_dir();
469        let deps_dir = cache_dir.join("deps");
470        fs::create_dir_all(&deps_dir)?;
471        write_tbd_files(&deps_dir)?;
472        new_cmd_args.push("-L".to_string());
473        new_cmd_args.push(format!("{}", deps_dir.display()));
474        Ok(())
475    }
476
477    /// Execute zig ar/ranlib command
478    pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
479        let mut child = Self::command()?
480            .arg(cmd)
481            .args(cmd_args)
482            .spawn()
483            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
484        let status = child.wait().expect("Failed to wait on zig child process");
485        if !status.success() {
486            process::exit(status.code().unwrap_or(1));
487        }
488        Ok(())
489    }
490
491    /// Build the zig command line
492    pub fn command() -> Result<Command> {
493        let (zig, zig_args) = Self::find_zig()?;
494        let mut cmd = Command::new(zig);
495        cmd.args(zig_args);
496        Ok(cmd)
497    }
498
499    fn zig_version() -> Result<semver::Version> {
500        static ZIG_VERSION: OnceLock<semver::Version> = OnceLock::new();
501
502        if let Some(version) = ZIG_VERSION.get() {
503            return Ok(version.clone());
504        }
505        // Check for cached version from environment variable first
506        if let Ok(version_str) = env::var("CARGO_ZIGBUILD_ZIG_VERSION") {
507            if let Ok(version) = semver::Version::parse(&version_str) {
508                return Ok(ZIG_VERSION.get_or_init(|| version).clone());
509            }
510        }
511        let output = Self::command()?.arg("version").output()?;
512        let version_str =
513            str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
514        let version = semver::Version::parse(version_str.trim())?;
515        Ok(ZIG_VERSION.get_or_init(|| version).clone())
516    }
517
518    /// Search for `python -m ziglang` first and for `zig` second.
519    pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
520        static ZIG_PATH: OnceLock<(PathBuf, Vec<String>)> = OnceLock::new();
521
522        if let Some(cached) = ZIG_PATH.get() {
523            return Ok(cached.clone());
524        }
525        let result = Self::find_zig_python()
526            .or_else(|_| Self::find_zig_bin())
527            .context("Failed to find zig")?;
528        Ok(ZIG_PATH.get_or_init(|| result).clone())
529    }
530
531    /// Detect the plain zig binary
532    fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
533        let zig_path = zig_path()?;
534        let output = Command::new(&zig_path).arg("version").output()?;
535
536        let version_str = str::from_utf8(&output.stdout).with_context(|| {
537            format!("`{} version` didn't return utf8 output", zig_path.display())
538        })?;
539        Self::validate_zig_version(version_str)?;
540        Ok((zig_path, Vec::new()))
541    }
542
543    /// Detect the Python ziglang package
544    fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
545        let python_path = python_path()?;
546        let output = Command::new(&python_path)
547            .args(["-m", "ziglang", "version"])
548            .output()?;
549
550        let version_str = str::from_utf8(&output.stdout).with_context(|| {
551            format!(
552                "`{} -m ziglang version` didn't return utf8 output",
553                python_path.display()
554            )
555        })?;
556        Self::validate_zig_version(version_str)?;
557        Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
558    }
559
560    fn validate_zig_version(version: &str) -> Result<()> {
561        let min_ver = semver::Version::new(0, 9, 0);
562        let version = semver::Version::parse(version.trim())?;
563        if version >= min_ver {
564            Ok(())
565        } else {
566            bail!(
567                "zig version {} is too old, need at least {}",
568                version,
569                min_ver
570            )
571        }
572    }
573
574    /// Find zig lib directory
575    pub fn lib_dir() -> Result<PathBuf> {
576        static LIB_DIR: OnceLock<PathBuf> = OnceLock::new();
577
578        if let Some(cached) = LIB_DIR.get() {
579            return Ok(cached.clone());
580        }
581        let (zig, zig_args) = Self::find_zig()?;
582        let output = Command::new(zig).args(zig_args).arg("env").output()?;
583        let zig_env: ZigEnv = serde_json::from_slice(&output.stdout)?;
584        Ok(LIB_DIR
585            .get_or_init(|| PathBuf::from(zig_env.lib_dir))
586            .clone())
587    }
588
589    fn add_env_if_missing<K, V>(command: &mut Command, name: K, value: V)
590    where
591        K: AsRef<OsStr>,
592        V: AsRef<OsStr>,
593    {
594        let command_env_contains_no_key =
595            |name: &K| !command.get_envs().any(|(key, _)| name.as_ref() == key);
596
597        if command_env_contains_no_key(&name) && env::var_os(&name).is_none() {
598            command.env(name, value);
599        }
600    }
601
602    pub(crate) fn apply_command_env(
603        manifest_path: Option<&Path>,
604        release: bool,
605        cargo: &cargo_options::CommonOptions,
606        cmd: &mut Command,
607        enable_zig_ar: bool,
608    ) -> Result<()> {
609        // setup zig as linker
610        let rust_targets = cargo
611            .target
612            .iter()
613            .map(|target| target.split_once('.').map(|(t, _)| t).unwrap_or(target))
614            .collect::<Vec<&str>>();
615        let rustc_meta = rustc_version::version_meta()?;
616        Self::add_env_if_missing(
617            cmd,
618            "CARGO_ZIGBUILD_RUSTC_VERSION",
619            rustc_meta.semver.to_string(),
620        );
621        let host_target = &rustc_meta.host;
622        for (parsed_target, raw_target) in rust_targets.iter().zip(&cargo.target) {
623            let env_target = parsed_target.replace('-', "_");
624            let zig_wrapper = prepare_zig_linker(raw_target)?;
625
626            if is_mingw_shell() {
627                let zig_cc = zig_wrapper.cc.to_slash_lossy();
628                let zig_cxx = zig_wrapper.cxx.to_slash_lossy();
629                Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &*zig_cc);
630                Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &*zig_cxx);
631                if !parsed_target.contains("wasm") {
632                    Self::add_env_if_missing(
633                        cmd,
634                        format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
635                        &*zig_cc,
636                    );
637                }
638            } else {
639                Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &zig_wrapper.cc);
640                Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &zig_wrapper.cxx);
641                if !parsed_target.contains("wasm") {
642                    Self::add_env_if_missing(
643                        cmd,
644                        format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
645                        &zig_wrapper.cc,
646                    );
647                }
648            }
649
650            Self::add_env_if_missing(cmd, format!("RANLIB_{env_target}"), &zig_wrapper.ranlib);
651            // Only setup AR when explicitly asked to
652            // because it need special executable name handling, see src/bin/cargo-zigbuild.rs
653            if enable_zig_ar {
654                if parsed_target.contains("msvc") {
655                    Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.lib);
656                } else {
657                    Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.ar);
658                }
659            }
660
661            Self::setup_os_deps(manifest_path, release, cargo)?;
662
663            let cmake_toolchain_file_env = format!("CMAKE_TOOLCHAIN_FILE_{env_target}");
664            if env::var_os(&cmake_toolchain_file_env).is_none()
665                && env::var_os(format!("CMAKE_TOOLCHAIN_FILE_{parsed_target}")).is_none()
666                && env::var_os("TARGET_CMAKE_TOOLCHAIN_FILE").is_none()
667                && env::var_os("CMAKE_TOOLCHAIN_FILE").is_none()
668            {
669                if let Ok(cmake_toolchain_file) =
670                    Self::setup_cmake_toolchain(parsed_target, &zig_wrapper, enable_zig_ar)
671                {
672                    cmd.env(cmake_toolchain_file_env, cmake_toolchain_file);
673                }
674            }
675
676            if raw_target.contains("windows-gnu") {
677                cmd.env("WINAPI_NO_BUNDLED_LIBRARIES", "1");
678                // Add the cache directory to PATH so rustc can find architecture-specific dlltool
679                // (e.g., x86_64-w64-mingw32-dlltool)
680                let cache_dir = cache_dir();
681                let existing_path = env::var_os("PATH").unwrap_or_default();
682                let paths = std::iter::once(cache_dir).chain(env::split_paths(&existing_path));
683                if let Ok(new_path) = env::join_paths(paths) {
684                    cmd.env("PATH", new_path);
685                }
686            }
687
688            if raw_target.contains("apple-darwin") {
689                if let Some(sdkroot) = Self::macos_sdk_root() {
690                    if env::var_os("PKG_CONFIG_SYSROOT_DIR").is_none() {
691                        // Set PKG_CONFIG_SYSROOT_DIR for pkg-config crate
692                        cmd.env("PKG_CONFIG_SYSROOT_DIR", sdkroot);
693                    }
694                }
695            }
696
697            // Enable unstable `target-applies-to-host` option automatically
698            // when target is the same as host but may have specified glibc version
699            if host_target == parsed_target {
700                if !matches!(rustc_meta.channel, rustc_version::Channel::Nightly) {
701                    // Hack to use the unstable feature on stable Rust
702                    // https://github.com/rust-lang/cargo/pull/9753#issuecomment-1022919343
703                    cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
704                }
705                cmd.env("CARGO_UNSTABLE_TARGET_APPLIES_TO_HOST", "true");
706                cmd.env("CARGO_TARGET_APPLIES_TO_HOST", "false");
707            }
708
709            // Pass options used by zig cc down to bindgen, if possible
710            let mut options = Self::collect_zig_cc_options(&zig_wrapper, raw_target)
711                .context("Failed to collect `zig cc` options")?;
712            if raw_target.contains("apple-darwin") {
713                // everyone seems to miss `#import <TargetConditionals.h>`...
714                options.push("-DTARGET_OS_IPHONE=0".to_string());
715            }
716            let escaped_options = shell_words::join(options.iter().map(|s| &s[..]));
717            let bindgen_env = "BINDGEN_EXTRA_CLANG_ARGS";
718            let fallback_value = env::var(bindgen_env);
719            for target in [&env_target[..], parsed_target] {
720                let name = format!("{bindgen_env}_{target}");
721                if let Ok(mut value) = env::var(&name).or(fallback_value.clone()) {
722                    if shell_words::split(&value).is_err() {
723                        // bindgen treats the whole string as a single argument if split fails
724                        value = shell_words::quote(&value).into_owned();
725                    }
726                    if !value.is_empty() {
727                        value.push(' ');
728                    }
729                    value.push_str(&escaped_options);
730                    env::set_var(name, value);
731                } else {
732                    env::set_var(name, escaped_options.clone());
733                }
734            }
735        }
736        Ok(())
737    }
738
739    /// Collects compiler options used by `zig cc` for given target.
740    /// Used for the case where `zig cc` cannot be used but underlying options should be retained,
741    /// for example, as in bindgen (which requires libclang.so and thus is independent from zig).
742    fn collect_zig_cc_options(zig_wrapper: &ZigWrapper, raw_target: &str) -> Result<Vec<String>> {
743        #[derive(Debug, PartialEq, Eq)]
744        enum Kind {
745            Normal,
746            Framework,
747        }
748
749        #[derive(Debug)]
750        struct PerLanguageOptions {
751            glibc_minor_ver: Option<u32>,
752            include_paths: Vec<(Kind, String)>,
753        }
754
755        fn collect_per_language_options(
756            program: &Path,
757            ext: &str,
758            raw_target: &str,
759        ) -> Result<PerLanguageOptions> {
760            // We can't use `-x c` or `-x c++` because pre-0.11 Zig doesn't handle them
761            let empty_file_path = cache_dir().join(format!(".intentionally-empty-file.{ext}"));
762            if !empty_file_path.exists() {
763                fs::write(&empty_file_path, "")?;
764            }
765
766            let output = Command::new(program)
767                .arg("-E")
768                .arg(&empty_file_path)
769                .arg("-v")
770                .output()?;
771            // Clang always generates UTF-8 regardless of locale, so this is okay.
772            let stderr = String::from_utf8(output.stderr)?;
773            if !output.status.success() {
774                bail!(
775                    "Failed to run `zig cc -v` with status {}: {}",
776                    output.status,
777                    stderr.trim(),
778                );
779            }
780
781            // Collect some macro definitions from cc1 options. We can't directly use
782            // them though, as we can't distinguish options added by zig from options
783            // added by clang driver (e.g. `__GCC_HAVE_DWARF2_CFI_ASM`).
784            let glibc_minor_ver = if let Some(start) = stderr.find("__GLIBC_MINOR__=") {
785                let stderr = &stderr[start + 16..];
786                let end = stderr
787                    .find(|c: char| !c.is_ascii_digit())
788                    .unwrap_or(stderr.len());
789                stderr[..end].parse().ok()
790            } else {
791                None
792            };
793
794            let start = stderr
795                .find("#include <...> search starts here:")
796                .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?
797                + 34;
798            let end = stderr
799                .find("End of search list.")
800                .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?;
801
802            let mut include_paths = Vec::new();
803            for mut line in stderr[start..end].lines() {
804                line = line.trim();
805                let mut kind = Kind::Normal;
806                if line.ends_with(" (framework directory)") {
807                    line = line[..line.len() - 22].trim();
808                    kind = Kind::Framework;
809                } else if line.ends_with(" (headermap)") {
810                    bail!("C/C++ search path includes header maps, which are not supported");
811                }
812                if !line.is_empty() {
813                    include_paths.push((kind, line.to_owned()));
814                }
815            }
816
817            // In openharmony, we should add search header path by default which is useful for bindgen.
818            if raw_target.contains("ohos") {
819                let ndk = env::var("OHOS_NDK_HOME").expect("Can't get NDK path");
820                include_paths.push((Kind::Normal, format!("{}/native/sysroot/usr/include", ndk)));
821            }
822
823            Ok(PerLanguageOptions {
824                include_paths,
825                glibc_minor_ver,
826            })
827        }
828
829        let c_opts = collect_per_language_options(&zig_wrapper.cc, "c", raw_target)?;
830        let cpp_opts = collect_per_language_options(&zig_wrapper.cxx, "cpp", raw_target)?;
831
832        // Ensure that `c_opts` and `cpp_opts` are almost identical in the way we expect.
833        if c_opts.glibc_minor_ver != cpp_opts.glibc_minor_ver {
834            bail!(
835                "`zig cc` gives a different glibc minor version for C ({:?}) and C++ ({:?})",
836                c_opts.glibc_minor_ver,
837                cpp_opts.glibc_minor_ver,
838            );
839        }
840        let c_paths = c_opts.include_paths;
841        let mut cpp_paths = cpp_opts.include_paths;
842        let cpp_pre_len = cpp_paths
843            .iter()
844            .position(|p| {
845                p == c_paths
846                    .iter()
847                    .filter(|(kind, _)| *kind == Kind::Normal)
848                    .next()
849                    .unwrap()
850            })
851            .unwrap_or_default();
852        let cpp_post_len = cpp_paths.len()
853            - cpp_paths
854                .iter()
855                .position(|p| p == c_paths.last().unwrap())
856                .unwrap_or_default()
857            - 1;
858
859        // <digression>
860        //
861        // So, why we do need all of these?
862        //
863        // Bindgen wouldn't look at our `zig cc` (which doesn't contain `libclang.so` anyway),
864        // but it does collect include paths from the local clang and feed them to `libclang.so`.
865        // We want those include paths to come from our `zig cc` instead of the local clang.
866        // There are three main mechanisms possible:
867        //
868        // 1. Replace the local clang with our version.
869        //
870        //    Bindgen, internally via clang-sys, recognizes `CLANG_PATH` and `PATH`.
871        //    They are unfortunately a global namespace and simply setting them may break
872        //    existing build scripts, so we can't confidently override them.
873        //
874        //    Clang-sys can also look at target-prefixed clang if arguments contain `-target`.
875        //    Unfortunately clang-sys can only recognize `-target xxx`, which very slightly
876        //    differs from what bindgen would pass (`-target=xxx`), so this is not yet possible.
877        //
878        //    It should be also noted that we need to collect not only include paths
879        //    but macro definitions added by Zig, for example `-D__GLIBC_MINOR__`.
880        //    Clang-sys can't do this yet, so this option seems less robust than we want.
881        //
882        // 2. Set the environment variable `BINDGEN_EXTRA_CLANG_ARGS` and let bindgen to
883        //    append them to arguments passed to `libclang.so`.
884        //
885        //    This unfortunately means that we have the same set of arguments for C and C++.
886        //    Also we have to support older versions of clang, as old as clang 5 (2017).
887        //    We do have options like `-c-isystem` (cc1 only) and `-cxx-isystem`,
888        //    but we need to be aware of other options may affect our added options
889        //    and this requires a nitty gritty of clang driver and cc1---really annoying.
890        //
891        // 3. Fix either bindgen or clang-sys or Zig to ease our jobs.
892        //
893        //    This is not the option for now because, even after fixes, we have to support
894        //    older versions of bindgen or Zig which won't have those fixes anyway.
895        //    But it seems that minor changes to bindgen can indeed fix lots of issues
896        //    we face, so we are looking for them in the future.
897        //
898        // For this reason, we chose the option 2 and overrode `BINDGEN_EXTRA_CLANG_ARGS`.
899        // The following therefore assumes some understanding about clang option handling,
900        // including what the heck is cc1 (see the clang FAQ) and how driver options get
901        // translated to cc1 options (no documentation at all, as it's supposedly unstable).
902        // Fortunately for us, most (but not all) `-i...` options are passed through cc1.
903        //
904        // If you do experience weird compilation errors during bindgen, there's a chance
905        // that this code has overlooked some edge cases. You can put `.clang_arg("-###")`
906        // to print the final cc1 options, which would give a lot of information about
907        // how it got screwed up and help a lot when we fix the issue.
908        //
909        // </digression>
910
911        let mut args = Vec::new();
912
913        // Never include default include directories,
914        // otherwise `__has_include` will be totally confused.
915        args.push("-nostdinc".to_owned());
916
917        // Add various options for libc++ and glibc.
918        // Should match what `Compilation.zig` internally does:
919        //
920        // https://github.com/ziglang/zig/blob/0.9.0/src/Compilation.zig#L3390-L3427
921        // https://github.com/ziglang/zig/blob/0.9.1/src/Compilation.zig#L3408-L3445
922        // https://github.com/ziglang/zig/blob/0.10.0/src/Compilation.zig#L4163-L4211
923        // https://github.com/ziglang/zig/blob/0.10.1/src/Compilation.zig#L4240-L4288
924        if raw_target.contains("musl") || raw_target.contains("ohos") {
925            args.push("-D_LIBCPP_HAS_MUSL_LIBC".to_owned());
926            // for musl or openharmony
927            // https://github.com/ziglang/zig/pull/16098
928            args.push("-D_LARGEFILE64_SOURCE".to_owned());
929        }
930        args.extend(
931            [
932                "-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
933                "-D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS",
934                "-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
935                "-D_LIBCPP_PSTL_CPU_BACKEND_SERIAL",
936                "-D_LIBCPP_ABI_VERSION=1",
937                "-D_LIBCPP_ABI_NAMESPACE=__1",
938                "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST",
939            ]
940            .into_iter()
941            .map(ToString::to_string),
942        );
943        if let Some(ver) = c_opts.glibc_minor_ver {
944            // Handled separately because we have no way to infer this without Zig
945            args.push(format!("-D__GLIBC_MINOR__={ver}"));
946        }
947
948        for (kind, path) in cpp_paths.drain(..cpp_pre_len) {
949            if kind != Kind::Normal {
950                // may also be Kind::Framework on macOS
951                continue;
952            }
953            // Ideally this should be `-stdlib++-isystem`, which can be disabled by
954            // passing `-nostdinc++`, but it is fairly new: https://reviews.llvm.org/D64089
955            //
956            // (Also note that `-stdlib++-isystem` is a driver-only option,
957            // so it will be moved relative to other `-isystem` options against our will.)
958            args.push("-cxx-isystem".to_owned());
959            args.push(path);
960        }
961
962        for (kind, path) in c_paths {
963            match kind {
964                Kind::Normal => {
965                    // A normal `-isystem` is preferred over `-cxx-isystem` by cc1...
966                    args.push("-Xclang".to_owned());
967                    args.push("-c-isystem".to_owned());
968                    args.push("-Xclang".to_owned());
969                    args.push(path.clone());
970                    args.push("-cxx-isystem".to_owned());
971                    args.push(path);
972                }
973                Kind::Framework => {
974                    args.push("-iframework".to_owned());
975                    args.push(path);
976                }
977            }
978        }
979
980        for (kind, path) in cpp_paths.drain(cpp_paths.len() - cpp_post_len..) {
981            assert!(kind == Kind::Normal);
982            args.push("-cxx-isystem".to_owned());
983            args.push(path);
984        }
985
986        Ok(args)
987    }
988
989    fn setup_os_deps(
990        manifest_path: Option<&Path>,
991        release: bool,
992        cargo: &cargo_options::CommonOptions,
993    ) -> Result<()> {
994        for target in &cargo.target {
995            if target.contains("apple") {
996                let target_dir = if let Some(target_dir) = cargo.target_dir.clone() {
997                    target_dir.join(target)
998                } else {
999                    let manifest_path = manifest_path.unwrap_or_else(|| Path::new("Cargo.toml"));
1000                    if !manifest_path.exists() {
1001                        // cargo install doesn't pass a manifest path so `Cargo.toml` in cwd may not exist
1002                        continue;
1003                    }
1004                    let metadata = cargo_metadata::MetadataCommand::new()
1005                        .manifest_path(manifest_path)
1006                        .no_deps()
1007                        .exec()?;
1008                    metadata.target_directory.into_std_path_buf().join(target)
1009                };
1010                let profile = match cargo.profile.as_deref() {
1011                    Some("dev" | "test") => "debug",
1012                    Some("release" | "bench") => "release",
1013                    Some(profile) => profile,
1014                    None => {
1015                        if release {
1016                            "release"
1017                        } else {
1018                            "debug"
1019                        }
1020                    }
1021                };
1022                let deps_dir = target_dir.join(profile).join("deps");
1023                fs::create_dir_all(&deps_dir)?;
1024                if !target_dir.join("CACHEDIR.TAG").is_file() {
1025                    // Create a CACHEDIR.TAG file to exclude target directory from backup
1026                    let _ = write_file(
1027                        &target_dir.join("CACHEDIR.TAG"),
1028                        "Signature: 8a477f597d28d172789f06886806bc55
1029# This file is a cache directory tag created by cargo.
1030# For information about cache directory tags see https://bford.info/cachedir/
1031",
1032                    );
1033                }
1034                write_tbd_files(&deps_dir)?;
1035            } else if target.contains("arm") && target.contains("linux") {
1036                // See https://github.com/ziglang/zig/issues/3287
1037                if let Ok(lib_dir) = Zig::lib_dir() {
1038                    let arm_features_h = lib_dir
1039                        .join("libc")
1040                        .join("glibc")
1041                        .join("sysdeps")
1042                        .join("arm")
1043                        .join("arm-features.h");
1044                    if !arm_features_h.is_file() {
1045                        fs::write(arm_features_h, ARM_FEATURES_H)?;
1046                    }
1047                }
1048            } else if target.contains("windows-gnu") {
1049                if let Ok(lib_dir) = Zig::lib_dir() {
1050                    let lib_common = lib_dir.join("libc").join("mingw").join("lib-common");
1051                    let synchronization_def = lib_common.join("synchronization.def");
1052                    if !synchronization_def.is_file() {
1053                        let api_ms_win_core_synch_l1_2_0_def =
1054                            lib_common.join("api-ms-win-core-synch-l1-2-0.def");
1055                        // Ignore error
1056                        fs::copy(api_ms_win_core_synch_l1_2_0_def, synchronization_def).ok();
1057                    }
1058                }
1059            }
1060        }
1061        Ok(())
1062    }
1063
1064    fn setup_cmake_toolchain(
1065        target: &str,
1066        zig_wrapper: &ZigWrapper,
1067        enable_zig_ar: bool,
1068    ) -> Result<PathBuf> {
1069        let cmake = cache_dir().join("cmake");
1070        fs::create_dir_all(&cmake)?;
1071
1072        let toolchain_file = cmake.join(format!("{target}-toolchain.cmake"));
1073        let triple: Triple = target.parse()?;
1074        let os = triple.operating_system.to_string();
1075        let arch = triple.architecture.to_string();
1076        let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
1077            ("darwin", "x86_64") => ("Darwin", "x86_64"),
1078            ("darwin", "aarch64") => ("Darwin", "arm64"),
1079            ("linux", arch) => {
1080                let cmake_arch = match arch {
1081                    "powerpc" => "ppc",
1082                    "powerpc64" => "ppc64",
1083                    "powerpc64le" => "ppc64le",
1084                    _ => arch,
1085                };
1086                ("Linux", cmake_arch)
1087            }
1088            ("windows", "x86_64") => ("Windows", "AMD64"),
1089            ("windows", "i686") => ("Windows", "X86"),
1090            ("windows", "aarch64") => ("Windows", "ARM64"),
1091            (os, arch) => (os, arch),
1092        };
1093        let mut content = format!(
1094            r#"
1095set(CMAKE_SYSTEM_NAME {system_name})
1096set(CMAKE_SYSTEM_PROCESSOR {system_processor})
1097set(CMAKE_C_COMPILER {cc})
1098set(CMAKE_CXX_COMPILER {cxx})
1099set(CMAKE_RANLIB {ranlib})
1100set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE)
1101set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE)"#,
1102            system_name = system_name,
1103            system_processor = system_processor,
1104            cc = zig_wrapper.cc.to_slash_lossy(),
1105            cxx = zig_wrapper.cxx.to_slash_lossy(),
1106            ranlib = zig_wrapper.ranlib.to_slash_lossy(),
1107        );
1108        if enable_zig_ar {
1109            content.push_str(&format!(
1110                "\nset(CMAKE_AR {})\n",
1111                zig_wrapper.ar.to_slash_lossy()
1112            ));
1113        }
1114        write_file(&toolchain_file, &content)?;
1115        Ok(toolchain_file)
1116    }
1117
1118    #[cfg(target_os = "macos")]
1119    fn macos_sdk_root() -> Option<PathBuf> {
1120        static SDK_ROOT: OnceLock<Option<PathBuf>> = OnceLock::new();
1121
1122        SDK_ROOT
1123            .get_or_init(|| match env::var_os("SDKROOT") {
1124                Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1125                _ => {
1126                    let output = Command::new("xcrun")
1127                        .args(["--sdk", "macosx", "--show-sdk-path"])
1128                        .output()
1129                        .ok()?;
1130                    if output.status.success() {
1131                        let stdout = String::from_utf8(output.stdout).ok()?;
1132                        let stdout = stdout.trim();
1133                        if !stdout.is_empty() {
1134                            return Some(stdout.into());
1135                        }
1136                    }
1137                    None
1138                }
1139            })
1140            .clone()
1141    }
1142
1143    #[cfg(not(target_os = "macos"))]
1144    fn macos_sdk_root() -> Option<PathBuf> {
1145        match env::var_os("SDKROOT") {
1146            Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1147            _ => None,
1148        }
1149    }
1150}
1151
1152fn write_file(path: &Path, content: &str) -> Result<(), anyhow::Error> {
1153    let existing_content = fs::read_to_string(path).unwrap_or_default();
1154    if existing_content != content {
1155        fs::write(path, content)?;
1156    }
1157    Ok(())
1158}
1159
1160fn write_tbd_files(deps_dir: &Path) -> Result<(), anyhow::Error> {
1161    write_file(&deps_dir.join("libiconv.tbd"), LIBICONV_TBD)?;
1162    write_file(&deps_dir.join("libcharset.1.tbd"), LIBCHARSET_TBD)?;
1163    write_file(&deps_dir.join("libcharset.tbd"), LIBCHARSET_TBD)?;
1164    Ok(())
1165}
1166
1167fn cache_dir() -> PathBuf {
1168    env::var("CARGO_ZIGBUILD_CACHE_DIR")
1169        .ok()
1170        .map(|s| s.into())
1171        .or_else(dirs::cache_dir)
1172        // If the really is no cache dir, cwd will also do
1173        .unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
1174        .join(env!("CARGO_PKG_NAME"))
1175        .join(env!("CARGO_PKG_VERSION"))
1176}
1177
1178#[derive(Debug, Deserialize)]
1179struct ZigEnv {
1180    lib_dir: String,
1181}
1182
1183/// zig wrapper paths
1184#[derive(Debug, Clone)]
1185pub struct ZigWrapper {
1186    pub cc: PathBuf,
1187    pub cxx: PathBuf,
1188    pub ar: PathBuf,
1189    pub ranlib: PathBuf,
1190    pub lib: PathBuf,
1191}
1192
1193#[derive(Debug, Clone, Default, PartialEq)]
1194struct TargetFlags {
1195    pub target_cpu: String,
1196    pub target_feature: String,
1197}
1198
1199impl TargetFlags {
1200    pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
1201        let mut parsed = Self::default();
1202
1203        let f = rustflags::from_encoded(encoded);
1204        for flag in f {
1205            if let rustflags::Flag::Codegen { opt, value } = flag {
1206                let key = opt.replace('-', "_");
1207                match key.as_str() {
1208                    "target_cpu" => {
1209                        if let Some(value) = value {
1210                            parsed.target_cpu = value;
1211                        }
1212                    }
1213                    "target_feature" => {
1214                        // See https://github.com/rust-lang/rust/blob/7e3ba5b8b7556073ab69822cc36b93d6e74cd8c9/compiler/rustc_session/src/options.rs#L1233
1215                        if let Some(value) = value {
1216                            if !parsed.target_feature.is_empty() {
1217                                parsed.target_feature.push(',');
1218                            }
1219                            parsed.target_feature.push_str(&value);
1220                        }
1221                    }
1222                    _ => {}
1223                }
1224            }
1225        }
1226        Ok(parsed)
1227    }
1228}
1229
1230/// Prepare wrapper scripts for `zig cc` and `zig c++` and returns their paths
1231///
1232/// We want to use `zig cc` as linker and c compiler. We want to call `python -m ziglang cc`, but
1233/// cargo only accepts a path to an executable as linker, so we add a wrapper script. We then also
1234/// use the wrapper script to pass arguments and substitute an unsupported argument.
1235///
1236/// We create different files for different args because otherwise cargo might skip recompiling even
1237/// if the linker target changed
1238#[allow(clippy::blocks_in_conditions)]
1239pub fn prepare_zig_linker(target: &str) -> Result<ZigWrapper> {
1240    let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
1241    let abi_suffix = if abi_suffix.is_empty() {
1242        String::new()
1243    } else {
1244        if abi_suffix
1245            .split_once('.')
1246            .filter(|(x, y)| {
1247                !x.is_empty()
1248                    && x.chars().all(|c| c.is_ascii_digit())
1249                    && !y.is_empty()
1250                    && y.chars().all(|c| c.is_ascii_digit())
1251            })
1252            .is_none()
1253        {
1254            bail!("Malformed zig target abi suffix.")
1255        }
1256        format!(".{abi_suffix}")
1257    };
1258    let triple: Triple = rust_target
1259        .parse()
1260        .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
1261    let arch = triple.architecture.to_string();
1262    let target_env = match (triple.architecture, triple.environment) {
1263        (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
1264        (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
1265        (_, Environment::GnuLlvm) => Environment::Gnu,
1266        (_, environment) => environment,
1267    };
1268    let file_ext = if cfg!(windows) { "bat" } else { "sh" };
1269    let file_target = target.trim_end_matches('.');
1270
1271    let mut cc_args = vec![
1272        // prevent stripping
1273        "-g".to_owned(),
1274        // disable sanitizers
1275        "-fno-sanitize=all".to_owned(),
1276    ];
1277
1278    // TODO: Maybe better to assign mcpu according to:
1279    // rustc --target <target> -Z unstable-options --print target-spec-json
1280    let zig_mcpu_default = match triple.operating_system {
1281        OperatingSystem::Linux => {
1282            match arch.as_str() {
1283                // zig uses _ instead of - in cpu features
1284                "arm" => match target_env {
1285                    Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
1286                    Environment::Gnueabihf | Environment::Musleabihf => {
1287                        "generic+v6+strict_align+vfp2-d32"
1288                    }
1289                    _ => "",
1290                },
1291                "armv5te" => "generic+soft_float+strict_align",
1292                "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
1293                arch_str @ ("i586" | "i686") => {
1294                    if arch_str == "i586" {
1295                        "pentium"
1296                    } else {
1297                        "pentium4"
1298                    }
1299                }
1300                "riscv64gc" => "generic_rv64+m+a+f+d+c",
1301                "s390x" => "z10-vector",
1302                _ => "",
1303            }
1304        }
1305        _ => "",
1306    };
1307
1308    // Override mcpu from RUSTFLAGS if provided. The override happens when
1309    // commands like `cargo-zigbuild build` are invoked.
1310    // Currently we only override according to target_cpu.
1311    let zig_mcpu_override = {
1312        let cargo_config = cargo_config2::Config::load()?;
1313        let rust_flags = cargo_config.rustflags(rust_target)?.unwrap_or_default();
1314        let encoded_rust_flags = rust_flags.encode()?;
1315        let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
1316        // Note: zig uses _ instead of - for target_cpu and target_feature
1317        // target_cpu may be empty string, which means target_cpu is not specified.
1318        target_flags.target_cpu.replace('-', "_")
1319    };
1320
1321    if !zig_mcpu_override.is_empty() {
1322        cc_args.push(format!("-mcpu={zig_mcpu_override}"));
1323    } else if !zig_mcpu_default.is_empty() {
1324        cc_args.push(format!("-mcpu={zig_mcpu_default}"));
1325    }
1326
1327    match triple.operating_system {
1328        OperatingSystem::Linux => {
1329            let zig_arch = match arch.as_str() {
1330                // zig uses _ instead of - in cpu features
1331                "arm" => "arm",
1332                "armv5te" => "arm",
1333                "armv7" => "arm",
1334                "i586" | "i686" => {
1335                    let zig_version = Zig::zig_version()?;
1336                    if zig_version.major == 0 && zig_version.minor >= 11 {
1337                        "x86"
1338                    } else {
1339                        "i386"
1340                    }
1341                }
1342                "riscv64gc" => "riscv64",
1343                "s390x" => "s390x",
1344                _ => arch.as_str(),
1345            };
1346            let mut zig_target_env = target_env.to_string();
1347
1348            let zig_version = Zig::zig_version()?;
1349
1350            // Since Zig 0.15.0, arm-linux-ohos changed to arm-linux-ohoseabi
1351            // We need to follow the change but target_lexicon follow the LLVM target(https://github.com/bytecodealliance/target-lexicon/pull/123).
1352            // So we use string directly.
1353            if zig_version >= semver::Version::new(0, 15, 0)
1354                && arch.as_str() == "armv7"
1355                && target_env == Environment::Ohos
1356            {
1357                zig_target_env = "ohoseabi".to_string();
1358            }
1359
1360            cc_args.push("-target".to_string());
1361            cc_args.push(format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}"));
1362        }
1363        OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
1364            let zig_version = Zig::zig_version()?;
1365            // Zig 0.10.0 switched macOS ABI to none
1366            // see https://github.com/ziglang/zig/pull/11684
1367            if zig_version > semver::Version::new(0, 9, 1) {
1368                cc_args.push("-target".to_string());
1369                cc_args.push(format!("{arch}-macos-none{abi_suffix}"));
1370            } else {
1371                cc_args.push("-target".to_string());
1372                cc_args.push(format!("{arch}-macos-gnu{abi_suffix}"));
1373            }
1374        }
1375        OperatingSystem::Windows { .. } => {
1376            let zig_arch = match arch.as_str() {
1377                "i686" => {
1378                    let zig_version = Zig::zig_version()?;
1379                    if zig_version.major == 0 && zig_version.minor >= 11 {
1380                        "x86"
1381                    } else {
1382                        "i386"
1383                    }
1384                }
1385                arch => arch,
1386            };
1387            cc_args.push("-target".to_string());
1388            cc_args.push(format!("{zig_arch}-windows-{target_env}{abi_suffix}"));
1389        }
1390        OperatingSystem::Emscripten => {
1391            cc_args.push("-target".to_string());
1392            cc_args.push(format!("{arch}-emscripten{abi_suffix}"));
1393        }
1394        OperatingSystem::Wasi => {
1395            cc_args.push("-target".to_string());
1396            cc_args.push(format!("{arch}-wasi{abi_suffix}"));
1397        }
1398        OperatingSystem::WasiP1 => {
1399            cc_args.push("-target".to_string());
1400            cc_args.push(format!("{arch}-wasi.0.1.0{abi_suffix}"));
1401        }
1402        OperatingSystem::Freebsd => {
1403            let zig_arch = match arch.as_str() {
1404                "i686" => {
1405                    let zig_version = Zig::zig_version()?;
1406                    if zig_version.major == 0 && zig_version.minor >= 11 {
1407                        "x86"
1408                    } else {
1409                        "i386"
1410                    }
1411                }
1412                arch => arch,
1413            };
1414            cc_args.push("-target".to_string());
1415            cc_args.push(format!("{zig_arch}-freebsd"));
1416        }
1417        OperatingSystem::Unknown => {
1418            if triple.architecture == Architecture::Wasm32
1419                || triple.architecture == Architecture::Wasm64
1420            {
1421                cc_args.push("-target".to_string());
1422                cc_args.push(format!("{arch}-freestanding{abi_suffix}"));
1423            } else {
1424                bail!("unsupported target '{rust_target}'")
1425            }
1426        }
1427        _ => bail!(format!("unsupported target '{rust_target}'")),
1428    };
1429
1430    let zig_linker_dir = cache_dir();
1431    fs::create_dir_all(&zig_linker_dir)?;
1432
1433    if triple.operating_system == OperatingSystem::Linux {
1434        if matches!(
1435            triple.environment,
1436            Environment::Gnu
1437                | Environment::Gnuspe
1438                | Environment::Gnux32
1439                | Environment::Gnueabi
1440                | Environment::Gnuabi64
1441                | Environment::GnuIlp32
1442                | Environment::Gnueabihf
1443        ) {
1444            let glibc_version = if abi_suffix.is_empty() {
1445                (2, 17)
1446            } else {
1447                let mut parts = abi_suffix[1..].split('.');
1448                let major: usize = parts.next().unwrap().parse()?;
1449                let minor: usize = parts.next().unwrap().parse()?;
1450                (major, minor)
1451            };
1452            // See https://github.com/ziglang/zig/issues/9485
1453            if glibc_version < (2, 28) {
1454                use crate::linux::{FCNTL_H, FCNTL_MAP};
1455
1456                let zig_version = Zig::zig_version()?;
1457                if zig_version.major == 0 && zig_version.minor < 11 {
1458                    let fcntl_map = zig_linker_dir.join("fcntl.map");
1459                    let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
1460                    if existing_content != FCNTL_MAP {
1461                        fs::write(&fcntl_map, FCNTL_MAP)?;
1462                    }
1463                    let fcntl_h = zig_linker_dir.join("fcntl.h");
1464                    let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
1465                    if existing_content != FCNTL_H {
1466                        fs::write(&fcntl_h, FCNTL_H)?;
1467                    }
1468
1469                    cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
1470                    cc_args.push("-include".to_string());
1471                    cc_args.push(fcntl_h.display().to_string());
1472                }
1473            }
1474        } else if matches!(
1475            triple.environment,
1476            Environment::Musl
1477                | Environment::Muslabi64
1478                | Environment::Musleabi
1479                | Environment::Musleabihf
1480        ) {
1481            use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
1482
1483            let zig_version = Zig::zig_version()?;
1484            let rustc_version = rustc_version::version_meta()?.semver;
1485
1486            // as zig 0.11.0 is released, its musl has been upgraded to 1.2.4 with break changes
1487            // but rust is still with musl 1.2.3
1488            // we need this workaround before rust 1.72
1489            // https://github.com/ziglang/zig/pull/16098
1490            if (zig_version.major, zig_version.minor) >= (0, 11)
1491                && (rustc_version.major, rustc_version.minor) < (1, 72)
1492            {
1493                let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
1494                fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
1495
1496                cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
1497            }
1498        }
1499    }
1500
1501    // Use platform-specific quoting: shell_words for Unix (single quotes),
1502    // custom quoting for Windows batch files (double quotes)
1503    let cc_args_str = join_args_for_script(&cc_args);
1504    let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
1505    let zig_cc = zig_linker_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
1506    let zig_cxx = zig_linker_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
1507    let zig_ranlib = zig_linker_dir.join(format!("zigranlib.{file_ext}"));
1508    let zig_version = Zig::zig_version()?;
1509    write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version)?;
1510    write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version)?;
1511    write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version)?;
1512
1513    let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1514    let zig_ar = zig_linker_dir.join(format!("ar{exe_ext}"));
1515    symlink_wrapper(&zig_ar)?;
1516    let zig_lib = zig_linker_dir.join(format!("lib{exe_ext}"));
1517    symlink_wrapper(&zig_lib)?;
1518
1519    // Create dlltool symlinks for Windows GNU targets
1520    // On Windows hosts, rustc looks for "dlltool.exe"
1521    // On non-Windows hosts, rustc looks for architecture-specific names
1522    //
1523    // See https://github.com/rust-lang/rust/blob/a18e6d9d1473d9b25581dd04bef6c7577999631c/compiler/rustc_codegen_ssa/src/back/archive.rs#L275-L309
1524    if matches!(triple.operating_system, OperatingSystem::Windows)
1525        && matches!(triple.environment, Environment::Gnu)
1526    {
1527        let dlltool_name: &str = if cfg!(windows) {
1528            "dlltool"
1529        } else {
1530            match triple.architecture {
1531                Architecture::X86_64 => "x86_64-w64-mingw32-dlltool",
1532                Architecture::X86_32(_) => "i686-w64-mingw32-dlltool",
1533                Architecture::Aarch64(_) => "aarch64-w64-mingw32-dlltool",
1534                _ => "dlltool",
1535            }
1536        };
1537        let zig_dlltool = zig_linker_dir.join(format!("{dlltool_name}{exe_ext}"));
1538        symlink_wrapper(&zig_dlltool)?;
1539    }
1540
1541    Ok(ZigWrapper {
1542        cc: zig_cc,
1543        cxx: zig_cxx,
1544        ar: zig_ar,
1545        ranlib: zig_ranlib,
1546        lib: zig_lib,
1547    })
1548}
1549
1550fn symlink_wrapper(target: &Path) -> Result<()> {
1551    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1552        PathBuf::from(exe)
1553    } else {
1554        env::current_exe()?
1555    };
1556    #[cfg(windows)]
1557    {
1558        if !target.exists() {
1559            // symlink on Windows requires admin privileges so we use hardlink instead
1560            if std::fs::hard_link(&current_exe, target).is_err() {
1561                // hard_link doesn't support cross-device links so we fallback to copy
1562                std::fs::copy(&current_exe, target)?;
1563            }
1564        }
1565    }
1566
1567    #[cfg(unix)]
1568    {
1569        if !target.exists() {
1570            if fs::read_link(target).is_ok() {
1571                // remove broken symlink
1572                fs::remove_file(target)?;
1573            }
1574            std::os::unix::fs::symlink(current_exe, target)?;
1575        }
1576    }
1577    Ok(())
1578}
1579
1580/// Join arguments for Unix shell script using shell_words (single quotes)
1581#[cfg(target_family = "unix")]
1582fn join_args_for_script<I, S>(args: I) -> String
1583where
1584    I: IntoIterator<Item = S>,
1585    S: AsRef<str>,
1586{
1587    shell_words::join(args)
1588}
1589
1590/// Quote a string for Windows batch file (cmd.exe)
1591///
1592/// - `%` expands even inside quotes, so we escape it as `%%`.
1593/// - We disable delayed expansion in the wrapper script, so `!` should not expand.
1594/// - Internal `"` are escaped by doubling them (`""`).
1595#[cfg(not(target_family = "unix"))]
1596fn quote_for_batch(s: &str) -> String {
1597    let needs_quoting_or_escaping = s.is_empty()
1598        || s.contains(|c: char| {
1599            matches!(
1600                c,
1601                ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
1602            )
1603        });
1604
1605    if !needs_quoting_or_escaping {
1606        return s.to_string();
1607    }
1608
1609    let mut out = String::with_capacity(s.len() + 8);
1610    out.push('"');
1611    for c in s.chars() {
1612        match c {
1613            '"' => out.push_str("\"\""),
1614            '%' => out.push_str("%%"),
1615            _ => out.push(c),
1616        }
1617    }
1618    out.push('"');
1619    out
1620}
1621
1622/// Join arguments for Windows batch file using double quotes
1623#[cfg(not(target_family = "unix"))]
1624fn join_args_for_script<I, S>(args: I) -> String
1625where
1626    I: IntoIterator<Item = S>,
1627    S: AsRef<str>,
1628{
1629    args.into_iter()
1630        .map(|s| quote_for_batch(s.as_ref()))
1631        .collect::<Vec<_>>()
1632        .join(" ")
1633}
1634
1635/// Write a zig cc wrapper batch script for unix
1636#[cfg(target_family = "unix")]
1637fn write_linker_wrapper(
1638    path: &Path,
1639    command: &str,
1640    args: &str,
1641    zig_version: &semver::Version,
1642) -> Result<()> {
1643    let mut buf = Vec::<u8>::new();
1644    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1645        PathBuf::from(exe)
1646    } else {
1647        env::current_exe()?
1648    };
1649    writeln!(&mut buf, "#!/bin/sh")?;
1650
1651    // Export zig version to avoid spawning `zig version` subprocess
1652    writeln!(
1653        &mut buf,
1654        "export CARGO_ZIGBUILD_ZIG_VERSION={}",
1655        zig_version
1656    )?;
1657
1658    // For Zig >= 0.14 with macOS, pass through SDKROOT environment variable
1659    // if it exists at runtime (instead of passing --sysroot)
1660    // This avoids issues with library paths being interpreted relative to sysroot
1661    writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
1662
1663    writeln!(
1664        &mut buf,
1665        "exec \"{}\" zig {} -- {} \"$@\"",
1666        current_exe.display(),
1667        command,
1668        args
1669    )?;
1670
1671    // Try not to write the file again if it's already the same.
1672    // This is more friendly for cache systems like ccache, which by default
1673    // uses mtime to determine if a recompilation is needed.
1674    let existing_content = fs::read(path).unwrap_or_default();
1675    if existing_content != buf {
1676        OpenOptions::new()
1677            .create(true)
1678            .write(true)
1679            .truncate(true)
1680            .mode(0o700)
1681            .open(path)?
1682            .write_all(&buf)?;
1683    }
1684    Ok(())
1685}
1686
1687/// Write a zig cc wrapper batch script for windows
1688#[cfg(not(target_family = "unix"))]
1689fn write_linker_wrapper(
1690    path: &Path,
1691    command: &str,
1692    args: &str,
1693    zig_version: &semver::Version,
1694) -> Result<()> {
1695    let mut buf = Vec::<u8>::new();
1696    let current_exe = if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1697        PathBuf::from(exe)
1698    } else {
1699        env::current_exe()?
1700    };
1701    let current_exe = if is_mingw_shell() {
1702        current_exe.to_slash_lossy().to_string()
1703    } else {
1704        current_exe.display().to_string()
1705    };
1706    writeln!(&mut buf, "@echo off")?;
1707    // Prevent `!VAR!` expansion surprises (delayed expansion) in user-controlled args.
1708    writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
1709    // Set zig version to avoid spawning `zig version` subprocess
1710    writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
1711    writeln!(
1712        &mut buf,
1713        "\"{}\" zig {} -- {} %*",
1714        adjust_canonicalization(current_exe),
1715        command,
1716        args
1717    )?;
1718
1719    let existing_content = fs::read(path).unwrap_or_default();
1720    if existing_content != buf {
1721        fs::write(path, buf)?;
1722    }
1723    Ok(())
1724}
1725
1726pub(crate) fn is_mingw_shell() -> bool {
1727    env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
1728}
1729
1730// https://stackoverflow.com/a/50323079/3549270
1731#[cfg(target_os = "windows")]
1732pub fn adjust_canonicalization(p: String) -> String {
1733    const VERBATIM_PREFIX: &str = r#"\\?\"#;
1734    if p.starts_with(VERBATIM_PREFIX) {
1735        p[VERBATIM_PREFIX.len()..].to_string()
1736    } else {
1737        p
1738    }
1739}
1740
1741fn python_path() -> Result<PathBuf> {
1742    let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
1743    Ok(which::which(python)?)
1744}
1745
1746fn zig_path() -> Result<PathBuf> {
1747    let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
1748    Ok(which::which(zig)?)
1749}
1750
1751#[cfg(test)]
1752mod tests {
1753    use super::*;
1754
1755    #[test]
1756    fn test_target_flags() {
1757        let cases = [
1758            // Input, TargetCPU, TargetFeature
1759            ("-C target-feature=-crt-static", "", "-crt-static"),
1760            ("-C target-cpu=native", "native", ""),
1761            (
1762                "--deny warnings --codegen target-feature=+crt-static",
1763                "",
1764                "+crt-static",
1765            ),
1766            ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
1767            ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
1768            (
1769                "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
1770                "native",
1771                "-avx512bf16,-avx512bitalg",
1772            ),
1773            (
1774                "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
1775                "x86-64",
1776                "",
1777            ),
1778            (
1779                "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
1780                "",
1781                "+crt-static,+avx",
1782            ),
1783        ];
1784
1785        for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
1786            let args = cargo_config2::Flags::from_space_separated(input);
1787            let encoded_rust_flags = args.encode().unwrap();
1788            let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
1789            assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
1790            assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
1791        }
1792    }
1793
1794    #[test]
1795    fn test_join_args_for_script() {
1796        // Test basic arguments without special characters
1797        let args = vec!["-target", "x86_64-linux-gnu"];
1798        let result = join_args_for_script(&args);
1799        assert!(result.contains("-target"));
1800        assert!(result.contains("x86_64-linux-gnu"));
1801    }
1802
1803    #[test]
1804    #[cfg(not(target_family = "unix"))]
1805    fn test_quote_for_batch() {
1806        // Simple argument without special characters - no quoting needed
1807        assert_eq!(quote_for_batch("-target"), "-target");
1808        assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
1809
1810        // Arguments with spaces need quoting
1811        assert_eq!(
1812            quote_for_batch("C:\\Users\\John Doe\\path"),
1813            "\"C:\\Users\\John Doe\\path\""
1814        );
1815
1816        // Empty string needs quoting
1817        assert_eq!(quote_for_batch(""), "\"\"");
1818
1819        // Arguments with special batch characters need quoting
1820        assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
1821        assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
1822        assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
1823        assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
1824        assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
1825        assert_eq!(quote_for_batch("foo%bar"), "\"foo%bar\"");
1826
1827        // Internal double quotes are escaped by doubling
1828        assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
1829    }
1830
1831    #[test]
1832    #[cfg(not(target_family = "unix"))]
1833    fn test_join_args_for_script_windows() {
1834        // Test with path containing spaces
1835        let args = vec![
1836            "-target",
1837            "x86_64-linux-gnu",
1838            "-L",
1839            "C:\\Users\\John Doe\\path",
1840        ];
1841        let result = join_args_for_script(&args);
1842        // The path with space should be quoted
1843        assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
1844        // Simple args should not be quoted
1845        assert!(result.contains("-target"));
1846        assert!(!result.contains("\"-target\""));
1847    }
1848}