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#[derive(Debug, Clone)]
22pub struct ZigWrapper {
23 pub cc: PathBuf,
24 pub cxx: PathBuf,
25 pub ar: PathBuf,
26 pub ranlib: PathBuf,
27 pub lib: PathBuf,
28 pub target: String,
29}
30
31#[derive(Debug, Clone, Default, PartialEq)]
32struct TargetFlags {
33 pub target_cpu: String,
34 pub target_feature: String,
35}
36
37impl TargetFlags {
38 pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
39 let mut parsed = Self::default();
40
41 let f = rustflags::from_encoded(encoded);
42 for flag in f {
43 if let rustflags::Flag::Codegen { opt, value } = flag {
44 let key = opt.replace('-', "_");
45 match key.as_str() {
46 "target_cpu" => {
47 if let Some(value) = value {
48 parsed.target_cpu = value;
49 }
50 }
51 "target_feature" => {
52 if let Some(value) = value {
54 if !parsed.target_feature.is_empty() {
55 parsed.target_feature.push(',');
56 }
57 parsed.target_feature.push_str(&value);
58 }
59 }
60 _ => {}
61 }
62 }
63 }
64 Ok(parsed)
65 }
66}
67
68#[allow(clippy::blocks_in_conditions)]
77pub fn prepare_zig_linker(
78 target: &str,
79 cargo_config: &cargo_config2::Config,
80) -> Result<ZigWrapper> {
81 prepare_zig_linker_with_cli_config(target, cargo_config, &[], None)
82}
83
84pub fn prepare_zig_linker_with_cli_config(
88 target: &str,
89 cargo_config: &cargo_config2::Config,
90 config_args: &[String],
91 macosx_deployment_target: Option<&str>,
92) -> Result<ZigWrapper> {
93 let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
94 let abi_suffix = if abi_suffix.is_empty() {
95 String::new()
96 } else {
97 if abi_suffix
98 .split_once('.')
99 .filter(|(x, y)| {
100 !x.is_empty()
101 && x.chars().all(|c| c.is_ascii_digit())
102 && !y.is_empty()
103 && y.chars().all(|c| c.is_ascii_digit())
104 })
105 .is_none()
106 {
107 bail!("Malformed zig target abi suffix.")
108 }
109 format!(".{abi_suffix}")
110 };
111 let triple: Triple = rust_target
112 .parse()
113 .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
114 let abi_suffix = if matches!(
115 triple.operating_system,
116 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)
117 ) {
118 if !abi_suffix.is_empty() {
119 bail!(
120 "Version suffixes are not supported for macOS targets, set MACOSX_DEPLOYMENT_TARGET instead."
121 )
122 }
123 let env_version = env::var("MACOSX_DEPLOYMENT_TARGET").ok();
124 match macosx_deployment_target.or(env_version.as_deref()) {
125 Some(version) if !version.is_empty() => normalize_macos_version_suffix(version)?,
126 _ => String::new(),
127 }
128 } else {
129 abi_suffix
130 };
131 let arch = triple.architecture.to_string();
132 let target_env = zig_target_env(&triple);
133 let file_ext = if cfg!(windows) { "bat" } else { "sh" };
134 let file_target = target.trim_end_matches('.');
135
136 let mut cc_args = vec![
137 "-g".to_owned(),
139 "-fno-sanitize=all".to_owned(),
141 ];
142
143 let zig_mcpu_default = match triple.operating_system {
146 OperatingSystem::Linux => {
147 match arch.as_str() {
148 "arm" => match target_env {
150 Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
151 Environment::Gnueabihf | Environment::Musleabihf => {
152 "generic+v6+strict_align+vfp2-d32"
153 }
154 _ => "",
155 },
156 "armv5te" => "generic+v5te+soft_float+strict_align",
157 "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
158 "thumbv7neon" => "generic+v7a+vfp3+thumb2+neon",
159 arch_str @ ("i586" | "i686") => {
160 if arch_str == "i586" {
161 "pentium"
162 } else {
163 "pentium4"
164 }
165 }
166 "riscv64gc" => "generic_rv64+m+a+f+d+c",
167 "riscv64a23" => "generic_rv64+rva23u64",
168 "s390x" => "z10-vector",
169 _ => "",
170 }
171 }
172 _ => "",
173 };
174
175 let zig_mcpu_override = {
179 let cli_config = CliConfig::parse(config_args)?;
180 let rust_flags = cli_config
181 .rustflags(cargo_config, rust_target)?
182 .unwrap_or_default();
183 let encoded_rust_flags = rust_flags.encode()?;
184 let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
185 target_flags.target_cpu.replace('-', "_")
188 };
189
190 if !zig_mcpu_override.is_empty() {
191 cc_args.push(format!("-mcpu={zig_mcpu_override}"));
192 } else if !zig_mcpu_default.is_empty() {
193 cc_args.push(format!("-mcpu={zig_mcpu_default}"));
194 }
195
196 let zig_target = zig_target_triple(
197 rust_target,
198 &triple,
199 target_env,
200 &abi_suffix,
201 &Zig::zig_version()?,
202 )?;
203 cc_args.push("-target".to_string());
204 cc_args.push(zig_target.clone());
205
206 let zig_linker_dir = cache_dir();
207 fs::create_dir_all(&zig_linker_dir)?;
208
209 if triple.operating_system == OperatingSystem::Linux {
210 if matches!(
211 triple.environment,
212 Environment::Gnu
213 | Environment::Gnuspe
214 | Environment::Gnux32
215 | Environment::Gnueabi
216 | Environment::Gnuabi64
217 | Environment::GnuIlp32
218 | Environment::Gnueabihf
219 ) {
220 let glibc_version = if abi_suffix.is_empty() {
221 (2, 17)
222 } else {
223 let mut parts = abi_suffix[1..].split('.');
224 let major: usize = parts.next().unwrap().parse()?;
225 let minor: usize = parts.next().unwrap().parse()?;
226 (major, minor)
227 };
228 if glibc_version < (2, 28) {
230 use crate::linux::{FCNTL_H, FCNTL_MAP};
231
232 let zig_version = Zig::zig_version()?;
233 if zig_version.major == 0 && zig_version.minor < 11 {
234 let fcntl_map = zig_linker_dir.join("fcntl.map");
235 let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
236 if existing_content != FCNTL_MAP {
237 fs::write(&fcntl_map, FCNTL_MAP)?;
238 }
239 let fcntl_h = zig_linker_dir.join("fcntl.h");
240 let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
241 if existing_content != FCNTL_H {
242 fs::write(&fcntl_h, FCNTL_H)?;
243 }
244
245 cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
246 cc_args.push("-include".to_string());
247 cc_args.push(fcntl_h.display().to_string());
248 }
249 }
250 } else if matches!(
251 triple.environment,
252 Environment::Musl
253 | Environment::Muslabi64
254 | Environment::Musleabi
255 | Environment::Musleabihf
256 ) {
257 use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
258
259 let zig_version = Zig::zig_version()?;
260 let rustc_version = rustc_version::version_meta()?.semver;
261
262 if (zig_version.major, zig_version.minor) >= (0, 11)
267 && (rustc_version.major, rustc_version.minor) < (1, 72)
268 {
269 let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
270 fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
271
272 cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
273 }
274 }
275 }
276
277 let cc_args_str = join_args_for_script(&cc_args);
280
281 let current_exe = resolve_current_exe()?;
286 let exe_hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC)
287 .checksum(current_exe.as_os_str().as_encoded_bytes());
288 let wrapper_dir = zig_linker_dir
289 .join("wrappers")
290 .join(format!("{:x}", exe_hash));
291 fs::create_dir_all(&wrapper_dir)?;
292
293 let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
294 let zig_cc = wrapper_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
295 let zig_cxx = wrapper_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
296 let zig_ranlib = wrapper_dir.join(format!("zigranlib.{file_ext}"));
297 let zig_version = Zig::zig_version()?;
298 let zig_command = Zig::find_zig()?;
299 write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version, &zig_command)?;
300 write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version, &zig_command)?;
301 write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version, &zig_command)?;
302
303 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
304 let zig_ar = wrapper_dir.join(format!("ar{exe_ext}"));
305 symlink_wrapper(&zig_ar)?;
306 let zig_lib = wrapper_dir.join(format!("lib{exe_ext}"));
307 symlink_wrapper(&zig_lib)?;
308
309 if matches!(triple.operating_system, OperatingSystem::Windows)
315 && matches!(triple.environment, Environment::Gnu)
316 {
317 if !has_system_dlltool(&triple.architecture) {
320 let dlltool_name = get_dlltool_name(&triple.architecture);
321 let zig_dlltool = wrapper_dir.join(format!("{dlltool_name}{exe_ext}"));
322 symlink_wrapper(&zig_dlltool)?;
323 }
324 }
325
326 Ok(ZigWrapper {
327 cc: zig_cc,
328 cxx: zig_cxx,
329 ar: zig_ar,
330 ranlib: zig_ranlib,
331 lib: zig_lib,
332 target: zig_target,
333 })
334}
335
336fn resolve_current_exe() -> Result<PathBuf> {
338 if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
339 Ok(PathBuf::from(exe))
340 } else {
341 Ok(env::current_exe()?)
342 }
343}
344
345pub(crate) fn symlink_wrapper(target: &Path) -> Result<()> {
346 let current_exe = resolve_current_exe()?;
347 #[cfg(windows)]
348 {
349 if !target.exists() {
350 if std::fs::hard_link(¤t_exe, target).is_err() {
352 std::fs::copy(¤t_exe, target)?;
354 }
355 }
356 }
357
358 #[cfg(unix)]
359 {
360 if !target.exists() {
361 if fs::read_link(target).is_ok() {
362 fs::remove_file(target)?;
364 }
365 std::os::unix::fs::symlink(current_exe, target)?;
366 }
367 }
368 Ok(())
369}
370
371#[cfg(target_family = "unix")]
373fn join_args_for_script<I, S>(args: I) -> String
374where
375 I: IntoIterator<Item = S>,
376 S: AsRef<str>,
377{
378 shell_words::join(args)
379}
380
381#[cfg(any(not(target_family = "unix"), test))]
387fn quote_for_batch(s: &str) -> String {
388 let needs_quoting_or_escaping = s.is_empty()
389 || s.contains(|c: char| {
390 matches!(
391 c,
392 ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
393 )
394 });
395
396 if !needs_quoting_or_escaping {
397 return s.to_string();
398 }
399
400 let mut out = String::with_capacity(s.len() + 8);
401 out.push('"');
402 for c in s.chars() {
403 match c {
404 '"' => out.push_str("\"\""),
405 '%' => out.push_str("%%"),
406 _ => out.push(c),
407 }
408 }
409 out.push('"');
410 out
411}
412
413#[cfg(not(target_family = "unix"))]
415fn join_args_for_script<I, S>(args: I) -> String
416where
417 I: IntoIterator<Item = S>,
418 S: AsRef<str>,
419{
420 args.into_iter()
421 .map(|s| quote_for_batch(s.as_ref()))
422 .collect::<Vec<_>>()
423 .join(" ")
424}
425
426#[cfg(target_family = "unix")]
428fn write_linker_wrapper(
429 path: &Path,
430 command: &str,
431 args: &str,
432 zig_version: &semver::Version,
433 zig_command: &(PathBuf, Vec<String>),
434) -> Result<()> {
435 let mut buf = Vec::<u8>::new();
436 let current_exe = resolve_current_exe()?;
437 writeln!(&mut buf, "#!/bin/sh")?;
438
439 writeln!(
441 &mut buf,
442 "export CARGO_ZIGBUILD_ZIG_VERSION={}",
443 zig_version
444 )?;
445 writeln!(
448 &mut buf,
449 "export CARGO_ZIGBUILD_ZIG_COMMAND={}",
450 shell_words::quote(&zig_command.0.to_string_lossy())
451 )?;
452 if !zig_command.1.is_empty() {
453 writeln!(
454 &mut buf,
455 "export CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}",
456 shell_words::quote(&zig_command.1.join(" "))
457 )?;
458 }
459
460 writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
462
463 writeln!(
464 &mut buf,
465 "exec \"{}\" zig {} -- {} \"$@\"",
466 current_exe.display(),
467 command,
468 args
469 )?;
470
471 let existing_content = fs::read(path).unwrap_or_default();
475 if existing_content != buf {
476 OpenOptions::new()
477 .create(true)
478 .write(true)
479 .truncate(true)
480 .mode(0o700)
481 .open(path)?
482 .write_all(&buf)?;
483 }
484 Ok(())
485}
486
487#[cfg(not(target_family = "unix"))]
489fn write_linker_wrapper(
490 path: &Path,
491 command: &str,
492 args: &str,
493 zig_version: &semver::Version,
494 zig_command: &(PathBuf, Vec<String>),
495) -> Result<()> {
496 let mut buf = Vec::<u8>::new();
497 let current_exe = resolve_current_exe()?;
498 let current_exe = if is_mingw_shell() {
499 current_exe.to_slash_lossy().to_string()
500 } else {
501 current_exe.display().to_string()
502 };
503 writeln!(&mut buf, "@echo off")?;
504 writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
506 writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
508 writeln!(
511 &mut buf,
512 "set \"CARGO_ZIGBUILD_ZIG_COMMAND={}\"",
513 zig_command.0.display()
514 )?;
515 if !zig_command.1.is_empty() {
516 writeln!(
517 &mut buf,
518 "set \"CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}\"",
519 zig_command.1.join(" ")
520 )?;
521 }
522 writeln!(
523 &mut buf,
524 "\"{}\" zig {} -- {} %*",
525 adjust_canonicalization(current_exe),
526 command,
527 args
528 )?;
529
530 let existing_content = fs::read(path).unwrap_or_default();
531 if existing_content != buf {
532 fs::write(path, buf)?;
533 }
534 Ok(())
535}
536
537pub(crate) fn is_mingw_shell() -> bool {
538 env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
539}
540
541#[cfg(target_os = "windows")]
543pub fn adjust_canonicalization(p: String) -> String {
544 const VERBATIM_PREFIX: &str = r#"\\?\"#;
545 if p.starts_with(VERBATIM_PREFIX) {
546 p[VERBATIM_PREFIX.len()..].to_string()
547 } else {
548 p
549 }
550}
551
552fn zig_target_env(triple: &Triple) -> Environment {
554 match (triple.architecture, triple.environment) {
555 (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
556 (Architecture::Mips32(..), Environment::Musl) => Environment::Musleabi,
557 (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
558 (_, Environment::GnuLlvm) => Environment::Gnu,
559 (_, environment) => environment,
560 }
561}
562
563fn normalize_macos_version_suffix(version: &str) -> Result<String> {
567 let mut parts = 0;
568 for part in version.split('.') {
569 parts += 1;
570 if parts > 3 || part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
571 bail!("Malformed MACOSX_DEPLOYMENT_TARGET '{version}'.")
572 }
573 }
574 if parts == 1 {
575 Ok(format!(".{version}.0"))
576 } else {
577 Ok(format!(".{version}"))
578 }
579}
580
581fn zig_target_triple(
585 rust_target: &str,
586 triple: &Triple,
587 target_env: Environment,
588 abi_suffix: &str,
589 zig_version: &semver::Version,
590) -> Result<String> {
591 let arch = triple.architecture.to_string();
592 let zig_target = match triple.operating_system {
593 OperatingSystem::Linux => {
594 let zig_arch = match arch.as_str() {
595 "arm" => "arm",
597 "armv5te" => "arm",
598 "armv7" => "arm",
599 "thumbv7neon" => "arm",
600 "i586" | "i686" => {
601 if zig_version.major == 0 && zig_version.minor >= 11 {
602 "x86"
603 } else {
604 "i386"
605 }
606 }
607 "riscv64gc" | "riscv64a23" => "riscv64",
608 "s390x" => "s390x",
609 _ => arch.as_str(),
610 };
611 let mut zig_target_env = target_env.to_string();
612
613 if *zig_version >= semver::Version::new(0, 15, 0)
617 && arch.as_str() == "armv7"
618 && target_env == Environment::Ohos
619 {
620 zig_target_env = "ohoseabi".to_string();
621 }
622
623 format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}")
624 }
625 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
626 if *zig_version > semver::Version::new(0, 9, 1) {
629 format!("{arch}-macos{abi_suffix}-none")
630 } else {
631 format!("{arch}-macos{abi_suffix}-gnu")
632 }
633 }
634 OperatingSystem::Windows => {
635 let zig_arch = match arch.as_str() {
636 "i686" => {
637 if zig_version.major == 0 && zig_version.minor >= 11 {
638 "x86"
639 } else {
640 "i386"
641 }
642 }
643 arch => arch,
644 };
645 format!("{zig_arch}-windows-{target_env}{abi_suffix}")
646 }
647 OperatingSystem::Emscripten => {
648 format!("{arch}-emscripten{abi_suffix}")
649 }
650 OperatingSystem::Wasi => {
651 format!("{arch}-wasi{abi_suffix}")
652 }
653 OperatingSystem::WasiP1 => {
654 format!("{arch}-wasi.0.1.0{abi_suffix}")
655 }
656 OperatingSystem::IOS(_) if triple.environment == Environment::Macabi => {
657 format!("{arch}-maccatalyst-none{abi_suffix}")
660 }
661 OperatingSystem::Freebsd => {
662 let zig_arch = match arch.as_str() {
663 "i686" => {
664 if zig_version.major == 0 && zig_version.minor >= 11 {
665 "x86"
666 } else {
667 "i386"
668 }
669 }
670 arch => arch,
671 };
672 format!("{zig_arch}-freebsd")
673 }
674 OperatingSystem::Openbsd => {
675 format!("{arch}-openbsd")
676 }
677 OperatingSystem::Unknown => {
678 if triple.architecture == Architecture::Wasm32
679 || triple.architecture == Architecture::Wasm64
680 {
681 format!("{arch}-freestanding{abi_suffix}")
682 } else {
683 bail!("unsupported target '{rust_target}'")
684 }
685 }
686 _ => bail!(format!("unsupported target '{rust_target}'")),
687 };
688 Ok(zig_target)
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn test_target_flags() {
697 let cases = [
698 ("-C target-feature=-crt-static", "", "-crt-static"),
700 ("-C target-cpu=native", "native", ""),
701 (
702 "--deny warnings --codegen target-feature=+crt-static",
703 "",
704 "+crt-static",
705 ),
706 ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
707 ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
708 (
709 "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
710 "native",
711 "-avx512bf16,-avx512bitalg",
712 ),
713 (
714 "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
715 "x86-64",
716 "",
717 ),
718 (
719 "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
720 "",
721 "+crt-static,+avx",
722 ),
723 ];
724
725 for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
726 let args = cargo_config2::Flags::from_space_separated(input);
727 let encoded_rust_flags = args.encode().unwrap();
728 let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
729 assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
730 assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
731 }
732 }
733
734 #[test]
735 fn test_join_args_for_script() {
736 let args = vec!["-target", "x86_64-linux-gnu"];
738 let result = join_args_for_script(&args);
739 assert!(result.contains("-target"));
740 assert!(result.contains("x86_64-linux-gnu"));
741 }
742
743 #[test]
744 fn test_quote_for_batch() {
745 assert_eq!(quote_for_batch("-target"), "-target");
747 assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
748
749 assert_eq!(
751 quote_for_batch("C:\\Users\\John Doe\\path"),
752 "\"C:\\Users\\John Doe\\path\""
753 );
754
755 assert_eq!(quote_for_batch(""), "\"\"");
757
758 assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
760 assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
761 assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
762 assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
763 assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
764
765 assert_eq!(quote_for_batch("foo%bar"), "\"foo%%bar\"");
767 assert_eq!(quote_for_batch("%PATH%"), "\"%%PATH%%\"");
768 assert_eq!(quote_for_batch("%1"), "\"%%1\"");
769
770 assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
772 }
773
774 #[test]
775 #[cfg(not(target_family = "unix"))]
776 fn test_join_args_for_script_windows() {
777 let args = vec![
779 "-target",
780 "x86_64-linux-gnu",
781 "-L",
782 "C:\\Users\\John Doe\\path",
783 ];
784 let result = join_args_for_script(&args);
785 assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
787 assert!(result.contains("-target"));
789 assert!(!result.contains("\"-target\""));
790 }
791
792 fn zig_target(target: &str, zig_version: &str) -> Result<String> {
794 let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
795 let abi_suffix = if abi_suffix.is_empty() {
796 String::new()
797 } else {
798 format!(".{abi_suffix}")
799 };
800 let triple: Triple = rust_target.parse()?;
801 let target_env = zig_target_env(&triple);
802 zig_target_triple(
803 rust_target,
804 &triple,
805 target_env,
806 &abi_suffix,
807 &semver::Version::parse(zig_version)?,
808 )
809 }
810
811 #[test]
812 fn test_zig_target_triple() {
813 let cases = [
814 ("x86_64-unknown-linux-gnu", "0.15.2", "x86_64-linux-gnu"),
816 (
817 "x86_64-unknown-linux-gnu.2.36",
818 "0.15.2",
819 "x86_64-linux-gnu.2.36",
820 ),
821 ("aarch64-unknown-linux-musl", "0.15.2", "aarch64-linux-musl"),
822 (
823 "armv7-unknown-linux-gnueabihf",
824 "0.15.2",
825 "arm-linux-gnueabihf",
826 ),
827 (
828 "arm-unknown-linux-gnueabihf",
829 "0.15.2",
830 "arm-linux-gnueabihf",
831 ),
832 (
833 "thumbv7neon-unknown-linux-gnueabihf",
834 "0.15.2",
835 "arm-linux-gnueabihf",
836 ),
837 ("riscv64gc-unknown-linux-gnu", "0.15.2", "riscv64-linux-gnu"),
838 (
839 "riscv64a23-unknown-linux-gnu",
840 "0.15.2",
841 "riscv64-linux-gnu",
842 ),
843 ("s390x-unknown-linux-gnu", "0.15.2", "s390x-linux-gnu"),
844 ("i686-unknown-linux-gnu", "0.15.2", "x86-linux-gnu"),
846 ("i686-unknown-linux-gnu", "0.10.1", "i386-linux-gnu"),
847 ("armv7-unknown-linux-ohos", "0.15.2", "arm-linux-ohoseabi"),
849 ("armv7-unknown-linux-ohos", "0.14.1", "arm-linux-ohos"),
850 (
851 "powerpc-unknown-linux-gnu",
852 "0.15.2",
853 "powerpc-linux-gnueabihf",
854 ),
855 ("aarch64-apple-darwin", "0.15.2", "aarch64-macos-none"),
857 ("aarch64-apple-darwin", "0.9.1", "aarch64-macos-gnu"),
858 (
859 "aarch64-apple-ios-macabi",
860 "0.15.2",
861 "aarch64-maccatalyst-none",
862 ),
863 ("x86_64-pc-windows-gnu", "0.15.2", "x86_64-windows-gnu"),
864 ("i686-pc-windows-gnu", "0.15.2", "x86-windows-gnu"),
865 ("i686-pc-windows-gnu", "0.10.1", "i386-windows-gnu"),
866 ("wasm32-wasip1", "0.15.2", "wasm32-wasi.0.1.0"),
867 ("wasm32-wasi", "0.15.2", "wasm32-wasi"),
868 ("wasm32-unknown-unknown", "0.15.2", "wasm32-freestanding"),
869 ("wasm32-unknown-emscripten", "0.15.2", "wasm32-emscripten"),
870 ("x86_64-unknown-freebsd", "0.15.2", "x86_64-freebsd"),
871 ("x86_64-unknown-openbsd", "0.15.2", "x86_64-openbsd"),
872 ];
873
874 for (target, zig_version, expected) in cases {
875 assert_eq!(
876 zig_target(target, zig_version).unwrap(),
877 expected,
878 "{target} with zig {zig_version}"
879 );
880 }
881 }
882
883 #[test]
884 fn test_zig_target_triple_macos_version() {
885 let cases = [
886 (
887 "aarch64-apple-darwin",
888 "13.0",
889 "0.15.2",
890 "aarch64-macos.13.0-none",
891 ),
892 (
893 "aarch64-apple-darwin",
894 "11",
895 "0.15.2",
896 "aarch64-macos.11.0-none",
897 ),
898 (
899 "x86_64-apple-darwin",
900 "10.15.7",
901 "0.15.2",
902 "x86_64-macos.10.15.7-none",
903 ),
904 (
905 "x86_64-apple-darwin",
906 "10.15",
907 "0.9.1",
908 "x86_64-macos.10.15-gnu",
909 ),
910 ];
911
912 for (target, version, zig_version, expected) in cases {
913 let triple: Triple = target.parse().unwrap();
914 let zig_target = zig_target_triple(
915 target,
916 &triple,
917 zig_target_env(&triple),
918 &normalize_macos_version_suffix(version).unwrap(),
919 &semver::Version::parse(zig_version).unwrap(),
920 )
921 .unwrap();
922 assert_eq!(zig_target, expected, "{target} with {version}");
923 }
924 }
925
926 #[test]
927 fn test_normalize_macos_version_suffix_malformed() {
928 for version in ["", "abc", "13.", ".13", "13.0.1.2", "13.x"] {
929 assert!(
930 normalize_macos_version_suffix(version).is_err(),
931 "{version}"
932 );
933 }
934 }
935
936 #[test]
937 fn test_zig_target_triple_unsupported() {
938 let err = zig_target("x86_64-unknown-redox", "0.15.2").unwrap_err();
939 assert!(
940 err.to_string().contains("unsupported target"),
941 "unexpected error: {err}"
942 );
943 }
944}