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::{Context, Result, anyhow, bail};
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#[derive(Clone, Debug, clap::Subcommand)]
24pub enum Zig {
25 #[command(name = "cc")]
27 Cc {
28 #[arg(num_args = 1.., trailing_var_arg = true)]
30 args: Vec<String>,
31 },
32 #[command(name = "c++")]
34 Cxx {
35 #[arg(num_args = 1.., trailing_var_arg = true)]
37 args: Vec<String>,
38 },
39 #[command(name = "ar")]
41 Ar {
42 #[arg(num_args = 1.., trailing_var_arg = true)]
44 args: Vec<String>,
45 },
46 #[command(name = "ranlib")]
48 Ranlib {
49 #[arg(num_args = 1.., trailing_var_arg = true)]
51 args: Vec<String>,
52 },
53 #[command(name = "lib")]
55 Lib {
56 #[arg(num_args = 1.., trailing_var_arg = true)]
58 args: Vec<String>,
59 },
60 #[command(name = "dlltool")]
62 Dlltool {
63 #[arg(num_args = 1.., trailing_var_arg = true)]
65 args: Vec<String>,
66 },
67}
68
69struct TargetInfo {
70 target: Option<String>,
71}
72
73impl TargetInfo {
74 fn new(target: Option<&String>) -> Self {
75 Self {
76 target: target.cloned(),
77 }
78 }
79
80 fn is_arm(&self) -> bool {
82 self.target
83 .as_ref()
84 .map(|x| x.starts_with("arm"))
85 .unwrap_or_default()
86 }
87
88 fn is_aarch64(&self) -> bool {
89 self.target
90 .as_ref()
91 .map(|x| x.starts_with("aarch64"))
92 .unwrap_or_default()
93 }
94
95 fn is_aarch64_be(&self) -> bool {
96 self.target
97 .as_ref()
98 .map(|x| x.starts_with("aarch64_be"))
99 .unwrap_or_default()
100 }
101
102 fn is_i386(&self) -> bool {
103 self.target
104 .as_ref()
105 .map(|x| x.starts_with("i386"))
106 .unwrap_or_default()
107 }
108
109 fn is_i686(&self) -> bool {
110 self.target
111 .as_ref()
112 .map(|x| x.starts_with("i686") || x.starts_with("x86-"))
113 .unwrap_or_default()
114 }
115
116 fn is_riscv64(&self) -> bool {
117 self.target
118 .as_ref()
119 .map(|x| x.starts_with("riscv64"))
120 .unwrap_or_default()
121 }
122
123 fn is_riscv32(&self) -> bool {
124 self.target
125 .as_ref()
126 .map(|x| x.starts_with("riscv32"))
127 .unwrap_or_default()
128 }
129
130 fn is_mips32(&self) -> bool {
131 self.target
132 .as_ref()
133 .map(|x| x.starts_with("mips") && !x.starts_with("mips64"))
134 .unwrap_or_default()
135 }
136
137 fn is_musl(&self) -> bool {
139 self.target
140 .as_ref()
141 .map(|x| x.contains("musl"))
142 .unwrap_or_default()
143 }
144
145 fn is_macos(&self) -> bool {
147 self.target
148 .as_ref()
149 .map(|x| x.contains("macos") || x.contains("maccatalyst"))
150 .unwrap_or_default()
151 }
152
153 fn is_darwin(&self) -> bool {
154 self.target
155 .as_ref()
156 .map(|x| x.contains("darwin"))
157 .unwrap_or_default()
158 }
159
160 fn is_apple_platform(&self) -> bool {
161 self.target
162 .as_ref()
163 .map(|x| {
164 x.contains("macos")
165 || x.contains("darwin")
166 || x.contains("ios")
167 || x.contains("tvos")
168 || x.contains("watchos")
169 || x.contains("visionos")
170 || x.contains("maccatalyst")
171 })
172 .unwrap_or_default()
173 }
174
175 fn is_ios(&self) -> bool {
176 self.target
177 .as_ref()
178 .map(|x| x.contains("ios") && !x.contains("visionos"))
179 .unwrap_or_default()
180 }
181
182 fn is_tvos(&self) -> bool {
183 self.target
184 .as_ref()
185 .map(|x| x.contains("tvos"))
186 .unwrap_or_default()
187 }
188
189 fn is_watchos(&self) -> bool {
190 self.target
191 .as_ref()
192 .map(|x| x.contains("watchos"))
193 .unwrap_or_default()
194 }
195
196 fn is_visionos(&self) -> bool {
197 self.target
198 .as_ref()
199 .map(|x| x.contains("visionos"))
200 .unwrap_or_default()
201 }
202
203 fn apple_cpu(&self) -> &'static str {
205 if self.is_macos() || self.is_darwin() {
206 "apple_m1" } else if self.is_visionos() {
208 "apple_m2" } else if self.is_watchos() {
210 "apple_s5" } else if self.is_ios() || self.is_tvos() {
212 "apple_a14" } else {
214 "generic"
215 }
216 }
217
218 fn is_freebsd(&self) -> bool {
219 self.target
220 .as_ref()
221 .map(|x| x.contains("freebsd"))
222 .unwrap_or_default()
223 }
224
225 fn is_windows_gnu(&self) -> bool {
226 self.target
227 .as_ref()
228 .map(|x| x.contains("windows-gnu"))
229 .unwrap_or_default()
230 }
231
232 fn is_windows_msvc(&self) -> bool {
233 self.target
234 .as_ref()
235 .map(|x| x.contains("windows-msvc"))
236 .unwrap_or_default()
237 }
238
239 fn is_ohos(&self) -> bool {
240 self.target
241 .as_ref()
242 .map(|x| x.contains("ohos"))
243 .unwrap_or_default()
244 }
245}
246
247impl Zig {
248 pub fn execute(&self) -> Result<()> {
250 match self {
251 Zig::Cc { args } => self.execute_compiler("cc", args),
252 Zig::Cxx { args } => self.execute_compiler("c++", args),
253 Zig::Ar { args } => self.execute_tool("ar", args),
254 Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
255 Zig::Lib { args } => self.execute_compiler("lib", args),
256 Zig::Dlltool { args } => self.execute_dlltool(args),
257 }
258 }
259
260 pub fn execute_dlltool(&self, cmd_args: &[String]) -> Result<()> {
263 let zig_version = Zig::zig_version()?;
264 let needs_filtering = zig_version.major == 0 && zig_version.minor < 12;
265
266 if !needs_filtering {
267 return self.execute_tool("dlltool", cmd_args);
268 }
269
270 let mut filtered_args = Vec::with_capacity(cmd_args.len());
273 let mut skip_next = false;
274 for arg in cmd_args {
275 if skip_next {
276 skip_next = false;
277 continue;
278 }
279 if arg == "--no-leading-underscore" {
280 continue;
281 }
282 if arg == "--temp-prefix" || arg == "-t" {
283 skip_next = true;
285 continue;
286 }
287 if arg.starts_with("--temp-prefix=") || arg.starts_with("-t=") {
289 continue;
290 }
291 filtered_args.push(arg.clone());
292 }
293
294 self.execute_tool("dlltool", &filtered_args)
295 }
296
297 pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
299 let target = cmd_args
300 .iter()
301 .position(|x| x == "-target")
302 .and_then(|index| cmd_args.get(index + 1));
303 let target_info = TargetInfo::new(target);
304
305 let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
306 Ok(version) => version.parse()?,
307 Err(_) => rustc_version::version()?,
308 };
309 let zig_version = Zig::zig_version()?;
310
311 let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
312 let mut skip_next_arg = false;
313 let mut seen_target = false;
314 for arg in cmd_args {
315 if skip_next_arg {
316 skip_next_arg = false;
317 continue;
318 }
319 if arg == "-target" {
323 if seen_target {
324 skip_next_arg = true;
325 continue;
326 }
327 seen_target = true;
328 }
329 let args = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
330 vec![self.process_linker_response_file(
331 arg,
332 &rustc_ver,
333 &zig_version,
334 &target_info,
335 )?]
336 } else {
337 match self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info) {
338 FilteredArg::Keep(filtered) => filtered,
339 FilteredArg::Skip => continue,
340 FilteredArg::SkipWithNext => {
341 skip_next_arg = true;
342 continue;
343 }
344 }
345 };
346 new_cmd_args.extend(args);
347 }
348
349 if target_info.is_mips32() {
350 new_cmd_args.push("-Wl,-z,notext".to_string());
352 }
353
354 if self.has_undefined_dynamic_lookup(cmd_args) {
355 new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
356 }
357 if target_info.is_macos() {
358 if self.should_add_libcharset(cmd_args, &zig_version) {
359 new_cmd_args.push("-lcharset".to_string());
360 }
361 self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
362 }
363
364 let mut command = Self::command()?;
367 if (zig_version.major, zig_version.minor) >= (0, 15)
368 && let Some(sdkroot) = Self::macos_sdk_root()
369 {
370 command.env("SDKROOT", sdkroot);
371 }
372
373 let mut child = command
374 .arg(cmd)
375 .args(new_cmd_args)
376 .spawn()
377 .with_context(|| format!("Failed to run `zig {cmd}`"))?;
378 let status = child.wait().expect("Failed to wait on zig child process");
379 if !status.success() {
380 process::exit(status.code().unwrap_or(1));
381 }
382 Ok(())
383 }
384
385 fn process_linker_response_file(
386 &self,
387 arg: &str,
388 rustc_ver: &rustc_version::Version,
389 zig_version: &semver::Version,
390 target_info: &TargetInfo,
391 ) -> Result<String> {
392 let content_bytes = fs::read(arg.trim_start_matches('@'))?;
396 let content = if target_info.is_windows_msvc() {
397 if content_bytes[0..2] != [255, 254] {
398 bail!(
399 "linker response file `{}` didn't start with a utf16 BOM",
400 &arg
401 );
402 }
403 let content_utf16: Vec<u16> = content_bytes[2..]
404 .chunks_exact(2)
405 .map(|a| u16::from_ne_bytes([a[0], a[1]]))
406 .collect();
407 String::from_utf16(&content_utf16).with_context(|| {
408 format!(
409 "linker response file `{}` didn't contain valid utf16 content",
410 &arg
411 )
412 })?
413 } else {
414 String::from_utf8(content_bytes).with_context(|| {
415 format!(
416 "linker response file `{}` didn't contain valid utf8 content",
417 &arg
418 )
419 })?
420 };
421 let mut link_args: Vec<_> = filter_linker_args(
422 content.split('\n').map(|s| s.to_string()),
423 rustc_ver,
424 zig_version,
425 target_info,
426 );
427 if self.has_undefined_dynamic_lookup(&link_args) {
428 link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
429 }
430 if target_info.is_macos() && self.should_add_libcharset(&link_args, zig_version) {
431 link_args.push("-lcharset".to_string());
432 }
433 if target_info.is_windows_msvc() {
434 let new_content = link_args.join("\n");
435 let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
436 for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
438 out.push(c as u8);
440 out.push((c >> 8) as u8);
441 }
442 fs::write(arg.trim_start_matches('@'), out)?;
443 } else {
444 fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
445 }
446 Ok(arg.to_string())
447 }
448
449 fn filter_linker_arg(
450 &self,
451 arg: &str,
452 rustc_ver: &rustc_version::Version,
453 zig_version: &semver::Version,
454 target_info: &TargetInfo,
455 ) -> FilteredArg {
456 filter_linker_arg(arg, rustc_ver, zig_version, target_info)
457 }
458}
459
460enum FilteredArg {
461 Keep(Vec<String>),
462 Skip,
463 SkipWithNext,
464}
465
466fn filter_linker_args(
467 args: impl IntoIterator<Item = String>,
468 rustc_ver: &rustc_version::Version,
469 zig_version: &semver::Version,
470 target_info: &TargetInfo,
471) -> Vec<String> {
472 let mut result = Vec::new();
473 let mut skip_next = false;
474 for arg in args {
475 if skip_next {
476 skip_next = false;
477 continue;
478 }
479 match filter_linker_arg(&arg, rustc_ver, zig_version, target_info) {
480 FilteredArg::Keep(filtered) => result.extend(filtered),
481 FilteredArg::Skip => {}
482 FilteredArg::SkipWithNext => {
483 skip_next = true;
484 }
485 }
486 }
487 result
488}
489
490fn filter_linker_arg(
491 arg: &str,
492 rustc_ver: &rustc_version::Version,
493 zig_version: &semver::Version,
494 target_info: &TargetInfo,
495) -> FilteredArg {
496 if arg == "-lgcc_s" {
497 return FilteredArg::Keep(vec!["-lunwind".to_string()]);
498 } else if arg.starts_with("--target=") {
499 return FilteredArg::Skip;
500 } else if arg.starts_with("-e") && arg.len() > 2 && !arg.starts_with("-export") {
501 let entry = &arg[2..];
502 return FilteredArg::Keep(vec![format!("-Wl,--entry={}", entry)]);
503 }
504 if (target_info.is_arm() || target_info.is_windows_gnu())
505 && arg.ends_with(".rlib")
506 && arg.contains("libcompiler_builtins-")
507 {
508 return FilteredArg::Skip;
509 }
510 if target_info.is_windows_gnu() {
511 #[allow(clippy::if_same_then_else)]
512 if arg == "-lgcc_eh"
513 && ((zig_version.major, zig_version.minor) < (0, 14) || target_info.is_i686())
514 {
515 return FilteredArg::Keep(vec!["-lc++".to_string()]);
516 } else if arg.ends_with("rsbegin.o") || arg.ends_with("rsend.o") {
517 if target_info.is_i686() {
518 return FilteredArg::Skip;
519 }
520 } else if arg == "-Wl,-Bdynamic" && (zig_version.major, zig_version.minor) >= (0, 11) {
521 return FilteredArg::Keep(vec!["-Wl,-search_paths_first".to_owned()]);
522 } else if arg == "-lwindows" || arg == "-l:libpthread.a" || arg == "-lgcc" {
523 return FilteredArg::Skip;
524 } else if arg == "-Wl,--disable-auto-image-base"
525 || arg == "-Wl,--dynamicbase"
526 || arg == "-Wl,--large-address-aware"
527 || (arg.starts_with("-Wl,")
528 && (arg.ends_with("/list.def") || arg.ends_with("\\list.def")))
529 {
530 return FilteredArg::Skip;
531 } else if arg == "-lmsvcrt" {
532 return FilteredArg::Skip;
533 }
534 } else if arg == "-Wl,--no-undefined-version"
535 || arg == "-Wl,-znostart-stop-gc"
536 || arg.starts_with("-Wl,-plugin-opt")
537 {
538 return FilteredArg::Skip;
539 }
540 if target_info.is_musl() || target_info.is_ohos() {
541 if (arg.ends_with(".o") && arg.contains("self-contained") && arg.contains("crt"))
542 || arg == "-Wl,-melf_i386"
543 {
544 return FilteredArg::Skip;
545 }
546 if rustc_ver.major == 1
547 && rustc_ver.minor < 59
548 && arg.ends_with(".rlib")
549 && arg.contains("liblibc-")
550 {
551 return FilteredArg::Skip;
552 }
553 if arg == "-lc" {
554 return FilteredArg::Skip;
555 }
556 }
557 if arg.starts_with("-Wp,")
561 && !arg.starts_with("-Wp,-MD")
562 && !arg.starts_with("-Wp,-MMD")
563 && !arg.starts_with("-Wp,-MT")
564 {
565 return FilteredArg::Skip;
566 }
567 if arg.starts_with("-march=") {
568 if target_info.is_arm() || target_info.is_i386() {
569 return FilteredArg::Skip;
570 } else if target_info.is_riscv64() {
571 return FilteredArg::Keep(vec!["-march=generic_rv64".to_string()]);
572 } else if target_info.is_riscv32() {
573 return FilteredArg::Keep(vec!["-march=generic_rv32".to_string()]);
574 } else if arg.starts_with("-march=armv")
575 && (target_info.is_aarch64() || target_info.is_aarch64_be())
576 {
577 let march_value = arg.strip_prefix("-march=").unwrap();
578 let features = if let Some(pos) = march_value.find('+') {
579 &march_value[pos..]
580 } else {
581 ""
582 };
583 let base_cpu = if target_info.is_apple_platform() {
584 target_info.apple_cpu()
585 } else {
586 "generic"
587 };
588 let mut result = vec![format!("-mcpu={}{}", base_cpu, features)];
589 if features.contains("+crypto") {
590 result.append(&mut vec!["-Xassembler".to_owned(), arg.to_string()]);
591 }
592 return FilteredArg::Keep(result);
593 }
594 }
595 if target_info.is_apple_platform() {
596 if (zig_version.major, zig_version.minor) < (0, 16) {
597 if arg.starts_with("-Wl,-exported_symbols_list,") {
598 return FilteredArg::Skip;
599 }
600 if arg == "-Wl,-exported_symbols_list" {
601 return FilteredArg::SkipWithNext;
602 }
603 }
604 if arg == "-Wl,-dylib" {
605 return FilteredArg::Skip;
606 }
607 }
608 if (zig_version.major, zig_version.minor) < (0, 16) {
610 if arg == "-Wl,-exported_symbols_list" || arg == "-Wl,--dynamic-list" {
611 return FilteredArg::SkipWithNext;
612 }
613 if arg.starts_with("-Wl,-exported_symbols_list,") || arg.starts_with("-Wl,--dynamic-list,")
614 {
615 return FilteredArg::Skip;
616 }
617 }
618 if target_info.is_freebsd() {
619 let ignored_libs = ["-lkvm", "-lmemstat", "-lprocstat", "-ldevstat"];
620 if ignored_libs.contains(&arg) {
621 return FilteredArg::Skip;
622 }
623 }
624 FilteredArg::Keep(vec![arg.to_string()])
625}
626
627impl Zig {
628 fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
629 let undefined = args
630 .iter()
631 .position(|x| x == "-undefined")
632 .and_then(|i| args.get(i + 1));
633 matches!(undefined, Some(x) if x == "dynamic_lookup")
634 }
635
636 fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
637 if (zig_version.major, zig_version.minor) >= (0, 12) {
639 args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
640 } else {
641 false
642 }
643 }
644
645 fn add_macos_specific_args(
646 &self,
647 new_cmd_args: &mut Vec<String>,
648 zig_version: &semver::Version,
649 ) -> Result<()> {
650 let sdkroot = Self::macos_sdk_root();
651 if (zig_version.major, zig_version.minor) >= (0, 12) {
652 if let Some(ref sdkroot) = sdkroot
656 && (zig_version.major, zig_version.minor) < (0, 15)
657 {
658 new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
659 }
660 }
662 if let Some(ref sdkroot) = sdkroot {
663 if (zig_version.major, zig_version.minor) < (0, 15) {
664 new_cmd_args.extend_from_slice(&[
666 "-isystem".to_string(),
667 format!("{}", sdkroot.join("usr").join("include").display()),
668 format!("-L{}", sdkroot.join("usr").join("lib").display()),
669 format!(
670 "-F{}",
671 sdkroot
672 .join("System")
673 .join("Library")
674 .join("Frameworks")
675 .display()
676 ),
677 "-DTARGET_OS_IPHONE=0".to_string(),
678 ]);
679 } else {
680 new_cmd_args.extend_from_slice(&[
683 "-isystem".to_string(),
684 format!("{}", sdkroot.join("usr").join("include").display()),
685 format!("-L{}", sdkroot.join("usr").join("lib").display()),
686 format!(
687 "-F{}",
688 sdkroot
689 .join("System")
690 .join("Library")
691 .join("Frameworks")
692 .display()
693 ),
694 "-iframework".to_string(),
696 format!(
697 "{}",
698 sdkroot
699 .join("System")
700 .join("Library")
701 .join("Frameworks")
702 .display()
703 ),
704 "-DTARGET_OS_IPHONE=0".to_string(),
705 ]);
706 }
707 }
708
709 let cache_dir = cache_dir();
711 let deps_dir = cache_dir.join("deps");
712 fs::create_dir_all(&deps_dir)?;
713 write_tbd_files(&deps_dir)?;
714 new_cmd_args.push("-L".to_string());
715 new_cmd_args.push(format!("{}", deps_dir.display()));
716 Ok(())
717 }
718
719 pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
721 let mut child = Self::command()?
722 .arg(cmd)
723 .args(cmd_args)
724 .spawn()
725 .with_context(|| format!("Failed to run `zig {cmd}`"))?;
726 let status = child.wait().expect("Failed to wait on zig child process");
727 if !status.success() {
728 process::exit(status.code().unwrap_or(1));
729 }
730 Ok(())
731 }
732
733 pub fn command() -> Result<Command> {
735 let (zig, zig_args) = Self::find_zig()?;
736 let mut cmd = Command::new(zig);
737 cmd.args(zig_args);
738 Ok(cmd)
739 }
740
741 fn zig_version() -> Result<semver::Version> {
742 static ZIG_VERSION: OnceLock<semver::Version> = OnceLock::new();
743
744 if let Some(version) = ZIG_VERSION.get() {
745 return Ok(version.clone());
746 }
747 if let Ok(version_str) = env::var("CARGO_ZIGBUILD_ZIG_VERSION")
749 && let Ok(version) = semver::Version::parse(&version_str)
750 {
751 return Ok(ZIG_VERSION.get_or_init(|| version).clone());
752 }
753 let output = Self::command()?.arg("version").output()?;
754 let version_str =
755 str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
756 let version = semver::Version::parse(version_str.trim())?;
757 Ok(ZIG_VERSION.get_or_init(|| version).clone())
758 }
759
760 pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
762 static ZIG_PATH: OnceLock<(PathBuf, Vec<String>)> = OnceLock::new();
763
764 if let Some(cached) = ZIG_PATH.get() {
765 return Ok(cached.clone());
766 }
767 let result = Self::find_zig_python()
768 .or_else(|_| Self::find_zig_bin())
769 .context("Failed to find zig")?;
770 Ok(ZIG_PATH.get_or_init(|| result).clone())
771 }
772
773 fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
775 let zig_path = zig_path()?;
776 let output = Command::new(&zig_path).arg("version").output()?;
777
778 let version_str = str::from_utf8(&output.stdout).with_context(|| {
779 format!("`{} version` didn't return utf8 output", zig_path.display())
780 })?;
781 Self::validate_zig_version(version_str)?;
782 Ok((zig_path, Vec::new()))
783 }
784
785 fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
787 let python_path = python_path()?;
788 let output = Command::new(&python_path)
789 .args(["-m", "ziglang", "version"])
790 .output()?;
791
792 let version_str = str::from_utf8(&output.stdout).with_context(|| {
793 format!(
794 "`{} -m ziglang version` didn't return utf8 output",
795 python_path.display()
796 )
797 })?;
798 Self::validate_zig_version(version_str)?;
799 Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
800 }
801
802 fn validate_zig_version(version: &str) -> Result<()> {
803 let min_ver = semver::Version::new(0, 9, 0);
804 let version = semver::Version::parse(version.trim())?;
805 if version >= min_ver {
806 Ok(())
807 } else {
808 bail!(
809 "zig version {} is too old, need at least {}",
810 version,
811 min_ver
812 )
813 }
814 }
815
816 pub fn lib_dir() -> Result<PathBuf> {
818 static LIB_DIR: OnceLock<PathBuf> = OnceLock::new();
819
820 if let Some(cached) = LIB_DIR.get() {
821 return Ok(cached.clone());
822 }
823 let (zig, zig_args) = Self::find_zig()?;
824 let zig_version = Self::zig_version()?;
825 let output = Command::new(zig).args(zig_args).arg("env").output()?;
826 let parse_zon_lib_dir = || -> Result<PathBuf> {
827 let output_str =
828 str::from_utf8(&output.stdout).context("`zig env` didn't return utf8 output")?;
829 let lib_dir = output_str
830 .find(".lib_dir")
831 .and_then(|idx| {
832 let bytes = output_str.as_bytes();
833 let mut start = idx;
834 while start < bytes.len() && bytes[start] != b'"' {
835 start += 1;
836 }
837 if start >= bytes.len() {
838 return None;
839 }
840 let mut end = start + 1;
841 while end < bytes.len() && bytes[end] != b'"' {
842 end += 1;
843 }
844 if end >= bytes.len() {
845 return None;
846 }
847 Some(&output_str[start + 1..end])
848 })
849 .context("Failed to parse lib_dir from `zig env` ZON output")?;
850 Ok(PathBuf::from(lib_dir))
851 };
852 let lib_dir = if zig_version >= semver::Version::new(0, 15, 0) {
853 parse_zon_lib_dir()?
854 } else {
855 serde_json::from_slice::<ZigEnv>(&output.stdout)
856 .map(|zig_env| PathBuf::from(zig_env.lib_dir))
857 .or_else(|_| parse_zon_lib_dir())?
858 };
859 Ok(LIB_DIR.get_or_init(|| lib_dir).clone())
860 }
861
862 fn add_env_if_missing<K, V>(command: &mut Command, name: K, value: V)
863 where
864 K: AsRef<OsStr>,
865 V: AsRef<OsStr>,
866 {
867 let command_env_contains_no_key =
868 |name: &K| !command.get_envs().any(|(key, _)| name.as_ref() == key);
869
870 if command_env_contains_no_key(&name) && env::var_os(&name).is_none() {
871 command.env(name, value);
872 }
873 }
874
875 pub(crate) fn apply_command_env(
876 manifest_path: Option<&Path>,
877 release: bool,
878 cargo: &cargo_options::CommonOptions,
879 cmd: &mut Command,
880 enable_zig_ar: bool,
881 ) -> Result<()> {
882 let cargo_config = cargo_config2::Config::load()?;
884 let config_targets;
886 let raw_targets: &[String] = if cargo.target.is_empty() {
887 if let Some(targets) = &cargo_config.build.target {
888 config_targets = targets
889 .iter()
890 .map(|t| t.triple().to_string())
891 .collect::<Vec<_>>();
892 &config_targets
893 } else {
894 &cargo.target
895 }
896 } else {
897 &cargo.target
898 };
899 let rust_targets = raw_targets
900 .iter()
901 .map(|target| target.split_once('.').map(|(t, _)| t).unwrap_or(target))
902 .collect::<Vec<&str>>();
903 let rustc_meta = rustc_version::version_meta()?;
904 Self::add_env_if_missing(
905 cmd,
906 "CARGO_ZIGBUILD_RUSTC_VERSION",
907 rustc_meta.semver.to_string(),
908 );
909 let host_target = &rustc_meta.host;
910 for (parsed_target, raw_target) in rust_targets.iter().zip(raw_targets) {
911 let env_target = parsed_target.replace('-', "_");
912 let zig_wrapper = prepare_zig_linker(raw_target, &cargo_config)?;
913
914 if is_mingw_shell() {
915 let zig_cc = zig_wrapper.cc.to_slash_lossy();
916 let zig_cxx = zig_wrapper.cxx.to_slash_lossy();
917 Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &*zig_cc);
918 Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &*zig_cxx);
919 if !parsed_target.contains("wasm") {
920 Self::add_env_if_missing(
921 cmd,
922 format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
923 &*zig_cc,
924 );
925 }
926 } else {
927 Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &zig_wrapper.cc);
928 Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &zig_wrapper.cxx);
929 if !parsed_target.contains("wasm") {
930 Self::add_env_if_missing(
931 cmd,
932 format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
933 &zig_wrapper.cc,
934 );
935 }
936 }
937
938 Self::add_env_if_missing(cmd, format!("RANLIB_{env_target}"), &zig_wrapper.ranlib);
939 if enable_zig_ar {
942 if parsed_target.contains("msvc") {
943 Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.lib);
944 } else {
945 Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.ar);
946 }
947 }
948
949 Self::setup_os_deps(manifest_path, release, cargo)?;
950
951 let cmake_toolchain_file_env = format!("CMAKE_TOOLCHAIN_FILE_{env_target}");
952 if env::var_os(&cmake_toolchain_file_env).is_none()
953 && env::var_os(format!("CMAKE_TOOLCHAIN_FILE_{parsed_target}")).is_none()
954 && env::var_os("TARGET_CMAKE_TOOLCHAIN_FILE").is_none()
955 && env::var_os("CMAKE_TOOLCHAIN_FILE").is_none()
956 && let Ok(cmake_toolchain_file) =
957 Self::setup_cmake_toolchain(parsed_target, &zig_wrapper, enable_zig_ar)
958 {
959 cmd.env(cmake_toolchain_file_env, cmake_toolchain_file);
960 }
961
962 if cfg!(target_os = "windows")
967 && env::var_os("CMAKE_GENERATOR").is_none()
968 && which::which("ninja").is_ok()
969 {
970 cmd.env("CMAKE_GENERATOR", "Ninja");
971 }
972
973 if raw_target.contains("windows-gnu") {
974 cmd.env("WINAPI_NO_BUNDLED_LIBRARIES", "1");
975 let triple: Triple = parsed_target.parse().unwrap_or_else(|_| Triple::unknown());
979 if !has_system_dlltool(&triple.architecture) {
980 let wrapper_dir = zig_wrapper.ar.parent().unwrap();
982 let existing_path = env::var_os("PATH").unwrap_or_default();
983 let paths = std::iter::once(wrapper_dir.to_path_buf())
984 .chain(env::split_paths(&existing_path));
985 if let Ok(new_path) = env::join_paths(paths) {
986 cmd.env("PATH", new_path);
987 }
988 }
989 }
990
991 if raw_target.contains("apple-darwin")
992 && let Some(sdkroot) = Self::macos_sdk_root()
993 && env::var_os("PKG_CONFIG_SYSROOT_DIR").is_none()
994 {
995 cmd.env("PKG_CONFIG_SYSROOT_DIR", sdkroot);
997 }
998
999 if host_target == parsed_target {
1002 if !matches!(rustc_meta.channel, rustc_version::Channel::Nightly) {
1003 cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
1006 }
1007 cmd.env("CARGO_UNSTABLE_TARGET_APPLIES_TO_HOST", "true");
1008 cmd.env("CARGO_TARGET_APPLIES_TO_HOST", "false");
1009 }
1010
1011 let mut options = Self::collect_zig_cc_options(&zig_wrapper, raw_target)
1013 .context("Failed to collect `zig cc` options")?;
1014 if raw_target.contains("apple-darwin") {
1015 options.push("-DTARGET_OS_IPHONE=0".to_string());
1017 }
1018 let escaped_options = shell_words::join(options.iter().map(|s| &s[..]));
1019 let bindgen_env = "BINDGEN_EXTRA_CLANG_ARGS";
1020 let fallback_value = env::var(bindgen_env);
1021 for target in [&env_target[..], parsed_target] {
1022 let name = format!("{bindgen_env}_{target}");
1023 if let Ok(mut value) = env::var(&name).or(fallback_value.clone()) {
1024 if shell_words::split(&value).is_err() {
1025 value = shell_words::quote(&value).into_owned();
1027 }
1028 if !value.is_empty() {
1029 value.push(' ');
1030 }
1031 value.push_str(&escaped_options);
1032 unsafe { env::set_var(name, value) };
1033 } else {
1034 unsafe { env::set_var(name, escaped_options.clone()) };
1035 }
1036 }
1037 }
1038 Ok(())
1039 }
1040
1041 fn collect_zig_cc_options(zig_wrapper: &ZigWrapper, raw_target: &str) -> Result<Vec<String>> {
1045 #[derive(Debug, PartialEq, Eq)]
1046 enum Kind {
1047 Normal,
1048 Framework,
1049 }
1050
1051 #[derive(Debug)]
1052 struct PerLanguageOptions {
1053 glibc_minor_ver: Option<u32>,
1054 include_paths: Vec<(Kind, String)>,
1055 }
1056
1057 fn collect_per_language_options(
1058 program: &Path,
1059 ext: &str,
1060 raw_target: &str,
1061 ) -> Result<PerLanguageOptions> {
1062 let empty_file_path = cache_dir().join(format!(".intentionally-empty-file.{ext}"));
1064 if !empty_file_path.exists() {
1065 fs::write(&empty_file_path, "")?;
1066 }
1067
1068 let output = Command::new(program)
1069 .arg("-E")
1070 .arg(&empty_file_path)
1071 .arg("-v")
1072 .output()?;
1073 let stderr = String::from_utf8(output.stderr)?;
1075 if !output.status.success() {
1076 bail!(
1077 "Failed to run `zig cc -v` with status {}: {}",
1078 output.status,
1079 stderr.trim(),
1080 );
1081 }
1082
1083 let glibc_minor_ver = if let Some(start) = stderr.find("__GLIBC_MINOR__=") {
1087 let stderr = &stderr[start + 16..];
1088 let end = stderr
1089 .find(|c: char| !c.is_ascii_digit())
1090 .unwrap_or(stderr.len());
1091 stderr[..end].parse().ok()
1092 } else {
1093 None
1094 };
1095
1096 let start = stderr
1097 .find("#include <...> search starts here:")
1098 .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?
1099 + 34;
1100 let end = stderr
1101 .find("End of search list.")
1102 .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?;
1103
1104 let mut include_paths = Vec::new();
1105 for mut line in stderr[start..end].lines() {
1106 line = line.trim();
1107 let mut kind = Kind::Normal;
1108 if line.ends_with(" (framework directory)") {
1109 line = line[..line.len() - 22].trim();
1110 kind = Kind::Framework;
1111 } else if line.ends_with(" (headermap)") {
1112 bail!("C/C++ search path includes header maps, which are not supported");
1113 }
1114 if !line.is_empty() {
1115 include_paths.push((kind, line.to_owned()));
1116 }
1117 }
1118
1119 if raw_target.contains("ohos") {
1121 let ndk = env::var("OHOS_NDK_HOME").expect("Can't get NDK path");
1122 include_paths.push((Kind::Normal, format!("{}/native/sysroot/usr/include", ndk)));
1123 }
1124
1125 Ok(PerLanguageOptions {
1126 include_paths,
1127 glibc_minor_ver,
1128 })
1129 }
1130
1131 let c_opts = collect_per_language_options(&zig_wrapper.cc, "c", raw_target)?;
1132 let cpp_opts = collect_per_language_options(&zig_wrapper.cxx, "cpp", raw_target)?;
1133
1134 if c_opts.glibc_minor_ver != cpp_opts.glibc_minor_ver {
1136 bail!(
1137 "`zig cc` gives a different glibc minor version for C ({:?}) and C++ ({:?})",
1138 c_opts.glibc_minor_ver,
1139 cpp_opts.glibc_minor_ver,
1140 );
1141 }
1142 let c_paths = c_opts.include_paths;
1143 let mut cpp_paths = cpp_opts.include_paths;
1144 let cpp_pre_len = cpp_paths
1145 .iter()
1146 .position(|p| {
1147 p == c_paths
1148 .iter()
1149 .find(|(kind, _)| *kind == Kind::Normal)
1150 .unwrap()
1151 })
1152 .unwrap_or_default();
1153 let cpp_post_len = cpp_paths.len()
1154 - cpp_paths
1155 .iter()
1156 .position(|p| p == c_paths.last().unwrap())
1157 .unwrap_or_default()
1158 - 1;
1159
1160 let mut args = Vec::new();
1213
1214 args.push("-nostdinc".to_owned());
1217
1218 if raw_target.contains("musl") || raw_target.contains("ohos") {
1226 args.push("-D_LIBCPP_HAS_MUSL_LIBC".to_owned());
1227 args.push("-D_LARGEFILE64_SOURCE".to_owned());
1230 }
1231 args.extend(
1232 [
1233 "-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
1234 "-D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS",
1235 "-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
1236 "-D_LIBCPP_PSTL_CPU_BACKEND_SERIAL",
1237 "-D_LIBCPP_ABI_VERSION=1",
1238 "-D_LIBCPP_ABI_NAMESPACE=__1",
1239 "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST",
1240 "-D_LIBCPP_HAS_LOCALIZATION=1",
1242 "-D_LIBCPP_HAS_WIDE_CHARACTERS=1",
1243 "-D_LIBCPP_HAS_UNICODE=1",
1244 "-D_LIBCPP_HAS_THREADS=1",
1245 "-D_LIBCPP_HAS_MONOTONIC_CLOCK",
1246 ]
1247 .into_iter()
1248 .map(ToString::to_string),
1249 );
1250 if let Some(ver) = c_opts.glibc_minor_ver {
1251 args.push(format!("-D__GLIBC_MINOR__={ver}"));
1253 }
1254
1255 for (kind, path) in cpp_paths.drain(..cpp_pre_len) {
1256 if kind != Kind::Normal {
1257 continue;
1259 }
1260 args.push("-cxx-isystem".to_owned());
1266 args.push(path);
1267 }
1268
1269 for (kind, path) in c_paths {
1270 match kind {
1271 Kind::Normal => {
1272 args.push("-Xclang".to_owned());
1274 args.push("-c-isystem".to_owned());
1275 args.push("-Xclang".to_owned());
1276 args.push(path.clone());
1277 args.push("-cxx-isystem".to_owned());
1278 args.push(path);
1279 }
1280 Kind::Framework => {
1281 args.push("-iframework".to_owned());
1282 args.push(path);
1283 }
1284 }
1285 }
1286
1287 for (kind, path) in cpp_paths.drain(cpp_paths.len() - cpp_post_len..) {
1288 assert!(kind == Kind::Normal);
1289 args.push("-cxx-isystem".to_owned());
1290 args.push(path);
1291 }
1292
1293 Ok(args)
1294 }
1295
1296 fn setup_os_deps(
1297 manifest_path: Option<&Path>,
1298 release: bool,
1299 cargo: &cargo_options::CommonOptions,
1300 ) -> Result<()> {
1301 for target in &cargo.target {
1302 if target.contains("apple") {
1303 let target_dir = if let Some(target_dir) = cargo.target_dir.clone() {
1304 target_dir.join(target)
1305 } else {
1306 let manifest_path = manifest_path.unwrap_or_else(|| Path::new("Cargo.toml"));
1307 if !manifest_path.exists() {
1308 continue;
1310 }
1311 let metadata = cargo_metadata::MetadataCommand::new()
1312 .manifest_path(manifest_path)
1313 .no_deps()
1314 .exec()?;
1315 metadata.target_directory.into_std_path_buf().join(target)
1316 };
1317 let profile = match cargo.profile.as_deref() {
1318 Some("dev" | "test") => "debug",
1319 Some("release" | "bench") => "release",
1320 Some(profile) => profile,
1321 None => {
1322 if release {
1323 "release"
1324 } else {
1325 "debug"
1326 }
1327 }
1328 };
1329 let deps_dir = target_dir.join(profile).join("deps");
1330 fs::create_dir_all(&deps_dir)?;
1331 if !target_dir.join("CACHEDIR.TAG").is_file() {
1332 let _ = write_file(
1334 &target_dir.join("CACHEDIR.TAG"),
1335 "Signature: 8a477f597d28d172789f06886806bc55
1336# This file is a cache directory tag created by cargo.
1337# For information about cache directory tags see https://bford.info/cachedir/
1338",
1339 );
1340 }
1341 write_tbd_files(&deps_dir)?;
1342 } else if target.contains("arm") && target.contains("linux") {
1343 if let Ok(lib_dir) = Zig::lib_dir() {
1345 let arm_features_h = lib_dir
1346 .join("libc")
1347 .join("glibc")
1348 .join("sysdeps")
1349 .join("arm")
1350 .join("arm-features.h");
1351 if !arm_features_h.is_file() {
1352 fs::write(arm_features_h, ARM_FEATURES_H)?;
1353 }
1354 }
1355 } else if target.contains("windows-gnu")
1356 && let Ok(lib_dir) = Zig::lib_dir()
1357 {
1358 let lib_common = lib_dir.join("libc").join("mingw").join("lib-common");
1359 let synchronization_def = lib_common.join("synchronization.def");
1360 if !synchronization_def.is_file() {
1361 let api_ms_win_core_synch_l1_2_0_def =
1362 lib_common.join("api-ms-win-core-synch-l1-2-0.def");
1363 fs::copy(api_ms_win_core_synch_l1_2_0_def, synchronization_def).ok();
1365 }
1366 }
1367 }
1368 Ok(())
1369 }
1370
1371 fn setup_cmake_toolchain(
1372 target: &str,
1373 zig_wrapper: &ZigWrapper,
1374 enable_zig_ar: bool,
1375 ) -> Result<PathBuf> {
1376 let wrapper_dir = zig_wrapper.cc.parent().unwrap();
1379 let cmake = wrapper_dir.join("cmake");
1380 fs::create_dir_all(&cmake)?;
1381
1382 let toolchain_file = cmake.join(format!("{target}-toolchain.cmake"));
1383 let triple: Triple = target.parse()?;
1384 let os = triple.operating_system.to_string();
1385 let arch = triple.architecture.to_string();
1386 let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
1387 ("darwin", "x86_64") => ("Darwin", "x86_64"),
1388 ("darwin", "aarch64") => ("Darwin", "arm64"),
1389 ("linux", arch) => {
1390 let cmake_arch = match arch {
1391 "powerpc" => "ppc",
1392 "powerpc64" => "ppc64",
1393 "powerpc64le" => "ppc64le",
1394 _ => arch,
1395 };
1396 ("Linux", cmake_arch)
1397 }
1398 ("windows", "x86_64") => ("Windows", "AMD64"),
1399 ("windows", "i686") => ("Windows", "X86"),
1400 ("windows", "aarch64") => ("Windows", "ARM64"),
1401 (os, arch) => (os, arch),
1402 };
1403 let mut content = format!(
1404 r#"
1405set(CMAKE_SYSTEM_NAME {system_name})
1406set(CMAKE_SYSTEM_PROCESSOR {system_processor})
1407set(CMAKE_C_COMPILER {cc})
1408set(CMAKE_CXX_COMPILER {cxx})
1409set(CMAKE_RANLIB {ranlib})
1410set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE)
1411set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE)"#,
1412 system_name = system_name,
1413 system_processor = system_processor,
1414 cc = zig_wrapper.cc.to_slash_lossy(),
1415 cxx = zig_wrapper.cxx.to_slash_lossy(),
1416 ranlib = zig_wrapper.ranlib.to_slash_lossy(),
1417 );
1418 if enable_zig_ar {
1419 content.push_str(&format!(
1420 "\nset(CMAKE_AR {})\n",
1421 zig_wrapper.ar.to_slash_lossy()
1422 ));
1423 }
1424 if system_name == "Darwin" && !cfg!(target_os = "macos") {
1429 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1430 let install_name_tool = wrapper_dir.join(format!("install_name_tool{exe_ext}"));
1431 symlink_wrapper(&install_name_tool)?;
1432 content.push_str(&format!(
1433 "\nset(CMAKE_INSTALL_NAME_TOOL {})",
1434 install_name_tool.to_slash_lossy()
1435 ));
1436
1437 if which::which("otool").is_err() {
1438 let script_ext = if cfg!(windows) { "bat" } else { "sh" };
1439 let otool = cmake.join(format!("otool.{script_ext}"));
1440 write_noop_script(&otool)?;
1441 content.push_str(&format!("\nset(CMAKE_OTOOL {})", otool.to_slash_lossy()));
1442 }
1443 }
1444 content.push_str(
1448 r#"
1449set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
1450set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
1451set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
1452set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)"#,
1453 );
1454 write_file(&toolchain_file, &content)?;
1455 Ok(toolchain_file)
1456 }
1457
1458 #[cfg(target_os = "macos")]
1459 fn macos_sdk_root() -> Option<PathBuf> {
1460 static SDK_ROOT: OnceLock<Option<PathBuf>> = OnceLock::new();
1461
1462 SDK_ROOT
1463 .get_or_init(|| match env::var_os("SDKROOT") {
1464 Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1465 _ => {
1466 let output = Command::new("xcrun")
1467 .args(["--sdk", "macosx", "--show-sdk-path"])
1468 .output()
1469 .ok()?;
1470 if output.status.success() {
1471 let stdout = String::from_utf8(output.stdout).ok()?;
1472 let stdout = stdout.trim();
1473 if !stdout.is_empty() {
1474 return Some(stdout.into());
1475 }
1476 }
1477 None
1478 }
1479 })
1480 .clone()
1481 }
1482
1483 #[cfg(not(target_os = "macos"))]
1484 fn macos_sdk_root() -> Option<PathBuf> {
1485 match env::var_os("SDKROOT") {
1486 Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1487 _ => None,
1488 }
1489 }
1490}
1491
1492fn write_file(path: &Path, content: &str) -> Result<(), anyhow::Error> {
1493 let existing_content = fs::read_to_string(path).unwrap_or_default();
1494 if existing_content != content {
1495 fs::write(path, content)?;
1496 }
1497 Ok(())
1498}
1499
1500#[cfg(target_family = "unix")]
1504fn write_noop_script(path: &Path) -> Result<()> {
1505 let content = "#!/bin/sh\nexit 0\n";
1506 let existing = fs::read_to_string(path).unwrap_or_default();
1507 if existing != content {
1508 OpenOptions::new()
1509 .create(true)
1510 .write(true)
1511 .truncate(true)
1512 .mode(0o700)
1513 .open(path)?
1514 .write_all(content.as_bytes())?;
1515 }
1516 Ok(())
1517}
1518
1519#[cfg(not(target_family = "unix"))]
1520fn write_noop_script(path: &Path) -> Result<()> {
1521 let content = "@echo off\r\nexit /b 0\r\n";
1522 let existing = fs::read_to_string(path).unwrap_or_default();
1523 if existing != content {
1524 fs::write(path, content)?;
1525 }
1526 Ok(())
1527}
1528
1529fn write_tbd_files(deps_dir: &Path) -> Result<(), anyhow::Error> {
1530 write_file(&deps_dir.join("libiconv.tbd"), LIBICONV_TBD)?;
1531 write_file(&deps_dir.join("libcharset.1.tbd"), LIBCHARSET_TBD)?;
1532 write_file(&deps_dir.join("libcharset.tbd"), LIBCHARSET_TBD)?;
1533 Ok(())
1534}
1535
1536fn cache_dir() -> PathBuf {
1537 env::var("CARGO_ZIGBUILD_CACHE_DIR")
1538 .ok()
1539 .map(|s| s.into())
1540 .or_else(dirs::cache_dir)
1541 .unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
1543 .join(env!("CARGO_PKG_NAME"))
1544 .join(env!("CARGO_PKG_VERSION"))
1545}
1546
1547#[derive(Debug, Deserialize)]
1548struct ZigEnv {
1549 lib_dir: String,
1550}
1551
1552#[derive(Debug, Clone)]
1554pub struct ZigWrapper {
1555 pub cc: PathBuf,
1556 pub cxx: PathBuf,
1557 pub ar: PathBuf,
1558 pub ranlib: PathBuf,
1559 pub lib: PathBuf,
1560}
1561
1562#[derive(Debug, Clone, Default, PartialEq)]
1563struct TargetFlags {
1564 pub target_cpu: String,
1565 pub target_feature: String,
1566}
1567
1568impl TargetFlags {
1569 pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
1570 let mut parsed = Self::default();
1571
1572 let f = rustflags::from_encoded(encoded);
1573 for flag in f {
1574 if let rustflags::Flag::Codegen { opt, value } = flag {
1575 let key = opt.replace('-', "_");
1576 match key.as_str() {
1577 "target_cpu" => {
1578 if let Some(value) = value {
1579 parsed.target_cpu = value;
1580 }
1581 }
1582 "target_feature" => {
1583 if let Some(value) = value {
1585 if !parsed.target_feature.is_empty() {
1586 parsed.target_feature.push(',');
1587 }
1588 parsed.target_feature.push_str(&value);
1589 }
1590 }
1591 _ => {}
1592 }
1593 }
1594 }
1595 Ok(parsed)
1596 }
1597}
1598
1599#[allow(clippy::blocks_in_conditions)]
1608pub fn prepare_zig_linker(
1609 target: &str,
1610 cargo_config: &cargo_config2::Config,
1611) -> Result<ZigWrapper> {
1612 let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
1613 let abi_suffix = if abi_suffix.is_empty() {
1614 String::new()
1615 } else {
1616 if abi_suffix
1617 .split_once('.')
1618 .filter(|(x, y)| {
1619 !x.is_empty()
1620 && x.chars().all(|c| c.is_ascii_digit())
1621 && !y.is_empty()
1622 && y.chars().all(|c| c.is_ascii_digit())
1623 })
1624 .is_none()
1625 {
1626 bail!("Malformed zig target abi suffix.")
1627 }
1628 format!(".{abi_suffix}")
1629 };
1630 let triple: Triple = rust_target
1631 .parse()
1632 .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
1633 let arch = triple.architecture.to_string();
1634 let target_env = match (triple.architecture, triple.environment) {
1635 (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
1636 (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
1637 (_, Environment::GnuLlvm) => Environment::Gnu,
1638 (_, environment) => environment,
1639 };
1640 let file_ext = if cfg!(windows) { "bat" } else { "sh" };
1641 let file_target = target.trim_end_matches('.');
1642
1643 let mut cc_args = vec![
1644 "-g".to_owned(),
1646 "-fno-sanitize=all".to_owned(),
1648 ];
1649
1650 let zig_mcpu_default = match triple.operating_system {
1653 OperatingSystem::Linux => {
1654 match arch.as_str() {
1655 "arm" => match target_env {
1657 Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
1658 Environment::Gnueabihf | Environment::Musleabihf => {
1659 "generic+v6+strict_align+vfp2-d32"
1660 }
1661 _ => "",
1662 },
1663 "armv5te" => "generic+soft_float+strict_align",
1664 "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
1665 arch_str @ ("i586" | "i686") => {
1666 if arch_str == "i586" {
1667 "pentium"
1668 } else {
1669 "pentium4"
1670 }
1671 }
1672 "riscv64gc" => "generic_rv64+m+a+f+d+c",
1673 "s390x" => "z10-vector",
1674 _ => "",
1675 }
1676 }
1677 _ => "",
1678 };
1679
1680 let zig_mcpu_override = {
1684 let rust_flags = cargo_config.rustflags(rust_target)?.unwrap_or_default();
1685 let encoded_rust_flags = rust_flags.encode()?;
1686 let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
1687 target_flags.target_cpu.replace('-', "_")
1690 };
1691
1692 if !zig_mcpu_override.is_empty() {
1693 cc_args.push(format!("-mcpu={zig_mcpu_override}"));
1694 } else if !zig_mcpu_default.is_empty() {
1695 cc_args.push(format!("-mcpu={zig_mcpu_default}"));
1696 }
1697
1698 match triple.operating_system {
1699 OperatingSystem::Linux => {
1700 let zig_arch = match arch.as_str() {
1701 "arm" => "arm",
1703 "armv5te" => "arm",
1704 "armv7" => "arm",
1705 "i586" | "i686" => {
1706 let zig_version = Zig::zig_version()?;
1707 if zig_version.major == 0 && zig_version.minor >= 11 {
1708 "x86"
1709 } else {
1710 "i386"
1711 }
1712 }
1713 "riscv64gc" => "riscv64",
1714 "s390x" => "s390x",
1715 _ => arch.as_str(),
1716 };
1717 let mut zig_target_env = target_env.to_string();
1718
1719 let zig_version = Zig::zig_version()?;
1720
1721 if zig_version >= semver::Version::new(0, 15, 0)
1725 && arch.as_str() == "armv7"
1726 && target_env == Environment::Ohos
1727 {
1728 zig_target_env = "ohoseabi".to_string();
1729 }
1730
1731 cc_args.push("-target".to_string());
1732 cc_args.push(format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}"));
1733 }
1734 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
1735 let zig_version = Zig::zig_version()?;
1736 if zig_version > semver::Version::new(0, 9, 1) {
1739 cc_args.push("-target".to_string());
1740 cc_args.push(format!("{arch}-macos-none{abi_suffix}"));
1741 } else {
1742 cc_args.push("-target".to_string());
1743 cc_args.push(format!("{arch}-macos-gnu{abi_suffix}"));
1744 }
1745 }
1746 OperatingSystem::Windows => {
1747 let zig_arch = match arch.as_str() {
1748 "i686" => {
1749 let zig_version = Zig::zig_version()?;
1750 if zig_version.major == 0 && zig_version.minor >= 11 {
1751 "x86"
1752 } else {
1753 "i386"
1754 }
1755 }
1756 arch => arch,
1757 };
1758 cc_args.push("-target".to_string());
1759 cc_args.push(format!("{zig_arch}-windows-{target_env}{abi_suffix}"));
1760 }
1761 OperatingSystem::Emscripten => {
1762 cc_args.push("-target".to_string());
1763 cc_args.push(format!("{arch}-emscripten{abi_suffix}"));
1764 }
1765 OperatingSystem::Wasi => {
1766 cc_args.push("-target".to_string());
1767 cc_args.push(format!("{arch}-wasi{abi_suffix}"));
1768 }
1769 OperatingSystem::WasiP1 => {
1770 cc_args.push("-target".to_string());
1771 cc_args.push(format!("{arch}-wasi.0.1.0{abi_suffix}"));
1772 }
1773 OperatingSystem::IOS(_) if triple.environment == Environment::Macabi => {
1774 cc_args.push("-target".to_string());
1777 cc_args.push(format!("{arch}-maccatalyst-none{abi_suffix}"));
1778 }
1779 OperatingSystem::Freebsd => {
1780 let zig_arch = match arch.as_str() {
1781 "i686" => {
1782 let zig_version = Zig::zig_version()?;
1783 if zig_version.major == 0 && zig_version.minor >= 11 {
1784 "x86"
1785 } else {
1786 "i386"
1787 }
1788 }
1789 arch => arch,
1790 };
1791 cc_args.push("-target".to_string());
1792 cc_args.push(format!("{zig_arch}-freebsd"));
1793 }
1794 OperatingSystem::Openbsd => {
1795 cc_args.push("-target".to_string());
1796 cc_args.push(format!("{arch}-openbsd"));
1797 }
1798 OperatingSystem::Unknown => {
1799 if triple.architecture == Architecture::Wasm32
1800 || triple.architecture == Architecture::Wasm64
1801 {
1802 cc_args.push("-target".to_string());
1803 cc_args.push(format!("{arch}-freestanding{abi_suffix}"));
1804 } else {
1805 bail!("unsupported target '{rust_target}'")
1806 }
1807 }
1808 _ => bail!(format!("unsupported target '{rust_target}'")),
1809 };
1810
1811 let zig_linker_dir = cache_dir();
1812 fs::create_dir_all(&zig_linker_dir)?;
1813
1814 if triple.operating_system == OperatingSystem::Linux {
1815 if matches!(
1816 triple.environment,
1817 Environment::Gnu
1818 | Environment::Gnuspe
1819 | Environment::Gnux32
1820 | Environment::Gnueabi
1821 | Environment::Gnuabi64
1822 | Environment::GnuIlp32
1823 | Environment::Gnueabihf
1824 ) {
1825 let glibc_version = if abi_suffix.is_empty() {
1826 (2, 17)
1827 } else {
1828 let mut parts = abi_suffix[1..].split('.');
1829 let major: usize = parts.next().unwrap().parse()?;
1830 let minor: usize = parts.next().unwrap().parse()?;
1831 (major, minor)
1832 };
1833 if glibc_version < (2, 28) {
1835 use crate::linux::{FCNTL_H, FCNTL_MAP};
1836
1837 let zig_version = Zig::zig_version()?;
1838 if zig_version.major == 0 && zig_version.minor < 11 {
1839 let fcntl_map = zig_linker_dir.join("fcntl.map");
1840 let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
1841 if existing_content != FCNTL_MAP {
1842 fs::write(&fcntl_map, FCNTL_MAP)?;
1843 }
1844 let fcntl_h = zig_linker_dir.join("fcntl.h");
1845 let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
1846 if existing_content != FCNTL_H {
1847 fs::write(&fcntl_h, FCNTL_H)?;
1848 }
1849
1850 cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
1851 cc_args.push("-include".to_string());
1852 cc_args.push(fcntl_h.display().to_string());
1853 }
1854 }
1855 } else if matches!(
1856 triple.environment,
1857 Environment::Musl
1858 | Environment::Muslabi64
1859 | Environment::Musleabi
1860 | Environment::Musleabihf
1861 ) {
1862 use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
1863
1864 let zig_version = Zig::zig_version()?;
1865 let rustc_version = rustc_version::version_meta()?.semver;
1866
1867 if (zig_version.major, zig_version.minor) >= (0, 11)
1872 && (rustc_version.major, rustc_version.minor) < (1, 72)
1873 {
1874 let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
1875 fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
1876
1877 cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
1878 }
1879 }
1880 }
1881
1882 let cc_args_str = join_args_for_script(&cc_args);
1885
1886 let current_exe = resolve_current_exe()?;
1891 let exe_hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC)
1892 .checksum(current_exe.as_os_str().as_encoded_bytes());
1893 let wrapper_dir = zig_linker_dir
1894 .join("wrappers")
1895 .join(format!("{:x}", exe_hash));
1896 fs::create_dir_all(&wrapper_dir)?;
1897
1898 let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
1899 let zig_cc = wrapper_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
1900 let zig_cxx = wrapper_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
1901 let zig_ranlib = wrapper_dir.join(format!("zigranlib.{file_ext}"));
1902 let zig_version = Zig::zig_version()?;
1903 write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version)?;
1904 write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version)?;
1905 write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version)?;
1906
1907 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1908 let zig_ar = wrapper_dir.join(format!("ar{exe_ext}"));
1909 symlink_wrapper(&zig_ar)?;
1910 let zig_lib = wrapper_dir.join(format!("lib{exe_ext}"));
1911 symlink_wrapper(&zig_lib)?;
1912
1913 if matches!(triple.operating_system, OperatingSystem::Windows)
1919 && matches!(triple.environment, Environment::Gnu)
1920 {
1921 if !has_system_dlltool(&triple.architecture) {
1924 let dlltool_name = get_dlltool_name(&triple.architecture);
1925 let zig_dlltool = wrapper_dir.join(format!("{dlltool_name}{exe_ext}"));
1926 symlink_wrapper(&zig_dlltool)?;
1927 }
1928 }
1929
1930 Ok(ZigWrapper {
1931 cc: zig_cc,
1932 cxx: zig_cxx,
1933 ar: zig_ar,
1934 ranlib: zig_ranlib,
1935 lib: zig_lib,
1936 })
1937}
1938
1939fn resolve_current_exe() -> Result<PathBuf> {
1941 if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1942 Ok(PathBuf::from(exe))
1943 } else {
1944 Ok(env::current_exe()?)
1945 }
1946}
1947
1948fn symlink_wrapper(target: &Path) -> Result<()> {
1949 let current_exe = resolve_current_exe()?;
1950 #[cfg(windows)]
1951 {
1952 if !target.exists() {
1953 if std::fs::hard_link(¤t_exe, target).is_err() {
1955 std::fs::copy(¤t_exe, target)?;
1957 }
1958 }
1959 }
1960
1961 #[cfg(unix)]
1962 {
1963 if !target.exists() {
1964 if fs::read_link(target).is_ok() {
1965 fs::remove_file(target)?;
1967 }
1968 std::os::unix::fs::symlink(current_exe, target)?;
1969 }
1970 }
1971 Ok(())
1972}
1973
1974#[cfg(target_family = "unix")]
1976fn join_args_for_script<I, S>(args: I) -> String
1977where
1978 I: IntoIterator<Item = S>,
1979 S: AsRef<str>,
1980{
1981 shell_words::join(args)
1982}
1983
1984#[cfg(not(target_family = "unix"))]
1990fn quote_for_batch(s: &str) -> String {
1991 let needs_quoting_or_escaping = s.is_empty()
1992 || s.contains(|c: char| {
1993 matches!(
1994 c,
1995 ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
1996 )
1997 });
1998
1999 if !needs_quoting_or_escaping {
2000 return s.to_string();
2001 }
2002
2003 let mut out = String::with_capacity(s.len() + 8);
2004 out.push('"');
2005 for c in s.chars() {
2006 match c {
2007 '"' => out.push_str("\"\""),
2008 '%' => out.push_str("%%"),
2009 _ => out.push(c),
2010 }
2011 }
2012 out.push('"');
2013 out
2014}
2015
2016#[cfg(not(target_family = "unix"))]
2018fn join_args_for_script<I, S>(args: I) -> String
2019where
2020 I: IntoIterator<Item = S>,
2021 S: AsRef<str>,
2022{
2023 args.into_iter()
2024 .map(|s| quote_for_batch(s.as_ref()))
2025 .collect::<Vec<_>>()
2026 .join(" ")
2027}
2028
2029#[cfg(target_family = "unix")]
2031fn write_linker_wrapper(
2032 path: &Path,
2033 command: &str,
2034 args: &str,
2035 zig_version: &semver::Version,
2036) -> Result<()> {
2037 let mut buf = Vec::<u8>::new();
2038 let current_exe = resolve_current_exe()?;
2039 writeln!(&mut buf, "#!/bin/sh")?;
2040
2041 writeln!(
2043 &mut buf,
2044 "export CARGO_ZIGBUILD_ZIG_VERSION={}",
2045 zig_version
2046 )?;
2047
2048 writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
2050
2051 writeln!(
2052 &mut buf,
2053 "exec \"{}\" zig {} -- {} \"$@\"",
2054 current_exe.display(),
2055 command,
2056 args
2057 )?;
2058
2059 let existing_content = fs::read(path).unwrap_or_default();
2063 if existing_content != buf {
2064 OpenOptions::new()
2065 .create(true)
2066 .write(true)
2067 .truncate(true)
2068 .mode(0o700)
2069 .open(path)?
2070 .write_all(&buf)?;
2071 }
2072 Ok(())
2073}
2074
2075#[cfg(not(target_family = "unix"))]
2077fn write_linker_wrapper(
2078 path: &Path,
2079 command: &str,
2080 args: &str,
2081 zig_version: &semver::Version,
2082) -> Result<()> {
2083 let mut buf = Vec::<u8>::new();
2084 let current_exe = resolve_current_exe()?;
2085 let current_exe = if is_mingw_shell() {
2086 current_exe.to_slash_lossy().to_string()
2087 } else {
2088 current_exe.display().to_string()
2089 };
2090 writeln!(&mut buf, "@echo off")?;
2091 writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
2093 writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
2095 writeln!(
2096 &mut buf,
2097 "\"{}\" zig {} -- {} %*",
2098 adjust_canonicalization(current_exe),
2099 command,
2100 args
2101 )?;
2102
2103 let existing_content = fs::read(path).unwrap_or_default();
2104 if existing_content != buf {
2105 fs::write(path, buf)?;
2106 }
2107 Ok(())
2108}
2109
2110pub(crate) fn is_mingw_shell() -> bool {
2111 env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
2112}
2113
2114#[cfg(target_os = "windows")]
2116pub fn adjust_canonicalization(p: String) -> String {
2117 const VERBATIM_PREFIX: &str = r#"\\?\"#;
2118 if p.starts_with(VERBATIM_PREFIX) {
2119 p[VERBATIM_PREFIX.len()..].to_string()
2120 } else {
2121 p
2122 }
2123}
2124
2125fn python_path() -> Result<PathBuf> {
2126 let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
2127 Ok(which::which(python)?)
2128}
2129
2130fn zig_path() -> Result<PathBuf> {
2131 let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
2132 Ok(which::which(zig)?)
2133}
2134
2135fn get_dlltool_name(arch: &Architecture) -> &'static str {
2139 if cfg!(windows) {
2140 "dlltool"
2141 } else {
2142 match arch {
2143 Architecture::X86_64 => "x86_64-w64-mingw32-dlltool",
2144 Architecture::X86_32(_) => "i686-w64-mingw32-dlltool",
2145 Architecture::Aarch64(_) => "aarch64-w64-mingw32-dlltool",
2146 _ => "dlltool",
2147 }
2148 }
2149}
2150
2151fn has_system_dlltool(arch: &Architecture) -> bool {
2154 which::which(get_dlltool_name(arch)).is_ok()
2155}
2156
2157#[cfg(test)]
2158mod tests {
2159 use super::*;
2160
2161 #[test]
2162 fn test_target_flags() {
2163 let cases = [
2164 ("-C target-feature=-crt-static", "", "-crt-static"),
2166 ("-C target-cpu=native", "native", ""),
2167 (
2168 "--deny warnings --codegen target-feature=+crt-static",
2169 "",
2170 "+crt-static",
2171 ),
2172 ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
2173 ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
2174 (
2175 "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
2176 "native",
2177 "-avx512bf16,-avx512bitalg",
2178 ),
2179 (
2180 "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
2181 "x86-64",
2182 "",
2183 ),
2184 (
2185 "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
2186 "",
2187 "+crt-static,+avx",
2188 ),
2189 ];
2190
2191 for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
2192 let args = cargo_config2::Flags::from_space_separated(input);
2193 let encoded_rust_flags = args.encode().unwrap();
2194 let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
2195 assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
2196 assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
2197 }
2198 }
2199
2200 #[test]
2201 fn test_join_args_for_script() {
2202 let args = vec!["-target", "x86_64-linux-gnu"];
2204 let result = join_args_for_script(&args);
2205 assert!(result.contains("-target"));
2206 assert!(result.contains("x86_64-linux-gnu"));
2207 }
2208
2209 #[test]
2210 #[cfg(not(target_family = "unix"))]
2211 fn test_quote_for_batch() {
2212 assert_eq!(quote_for_batch("-target"), "-target");
2214 assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
2215
2216 assert_eq!(
2218 quote_for_batch("C:\\Users\\John Doe\\path"),
2219 "\"C:\\Users\\John Doe\\path\""
2220 );
2221
2222 assert_eq!(quote_for_batch(""), "\"\"");
2224
2225 assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
2227 assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
2228 assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
2229 assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
2230 assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
2231 assert_eq!(quote_for_batch("foo%bar"), "\"foo%bar\"");
2232
2233 assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
2235 }
2236
2237 #[test]
2238 #[cfg(not(target_family = "unix"))]
2239 fn test_join_args_for_script_windows() {
2240 let args = vec![
2242 "-target",
2243 "x86_64-linux-gnu",
2244 "-L",
2245 "C:\\Users\\John Doe\\path",
2246 ];
2247 let result = join_args_for_script(&args);
2248 assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
2250 assert!(result.contains("-target"));
2252 assert!(!result.contains("\"-target\""));
2253 }
2254
2255 fn make_rustc_ver(major: u64, minor: u64, patch: u64) -> rustc_version::Version {
2256 rustc_version::Version::new(major, minor, patch)
2257 }
2258
2259 fn make_zig_ver(major: u64, minor: u64, patch: u64) -> semver::Version {
2260 semver::Version::new(major, minor, patch)
2261 }
2262
2263 fn run_filter(args: &[&str], target: Option<&str>, zig_ver: (u64, u64)) -> Vec<String> {
2264 let rustc_ver = make_rustc_ver(1, 80, 0);
2265 let zig_version = make_zig_ver(0, zig_ver.0, zig_ver.1);
2266 let target_info = TargetInfo::new(target.map(|s| s.to_string()).as_ref());
2267 filter_linker_args(
2268 args.iter().map(|s| s.to_string()),
2269 &rustc_ver,
2270 &zig_version,
2271 &target_info,
2272 )
2273 }
2274
2275 fn run_filter_one(arg: &str, target: Option<&str>, zig_ver: (u64, u64)) -> Vec<String> {
2276 run_filter(&[arg], target, zig_ver)
2277 }
2278
2279 fn run_filter_one_rustc(
2280 arg: &str,
2281 target: Option<&str>,
2282 zig_ver: (u64, u64),
2283 rustc_minor: u64,
2284 ) -> Vec<String> {
2285 let rustc_ver = make_rustc_ver(1, rustc_minor, 0);
2286 let zig_version = make_zig_ver(0, zig_ver.0, zig_ver.1);
2287 let target_info = TargetInfo::new(target.map(|s| s.to_string()).as_ref());
2288 filter_linker_args(
2289 std::iter::once(arg.to_string()),
2290 &rustc_ver,
2291 &zig_version,
2292 &target_info,
2293 )
2294 }
2295
2296 #[test]
2297 fn test_filter_common_replacements() {
2298 let linux = Some("x86_64-unknown-linux-gnu");
2299 assert_eq!(run_filter_one("-lgcc_s", linux, (13, 0)), vec!["-lunwind"]);
2301 assert!(run_filter_one("--target=x86_64-unknown-linux-gnu", linux, (13, 0)).is_empty());
2303 assert_eq!(
2305 run_filter_one("-emain", linux, (13, 0)),
2306 vec!["-Wl,--entry=main"]
2307 );
2308 assert_eq!(
2310 run_filter_one("-export-dynamic", linux, (13, 0)),
2311 vec!["-export-dynamic"]
2312 );
2313 }
2314
2315 #[test]
2316 fn test_filter_compiler_builtins_removed() {
2317 for target in &["armv7-unknown-linux-gnueabihf", "x86_64-pc-windows-gnu"] {
2318 let result = run_filter_one(
2319 "/path/to/libcompiler_builtins-abc123.rlib",
2320 Some(target),
2321 (13, 0),
2322 );
2323 assert!(
2324 result.is_empty(),
2325 "compiler_builtins should be removed for {target}"
2326 );
2327 }
2328 }
2329
2330 #[test]
2331 fn test_filter_windows_gnu_args() {
2332 let gnu = Some("x86_64-pc-windows-gnu");
2333 let removed: &[&str] = &[
2335 "-lwindows",
2336 "-l:libpthread.a",
2337 "-lgcc",
2338 "-Wl,--disable-auto-image-base",
2339 "-Wl,--dynamicbase",
2340 "-Wl,--large-address-aware",
2341 "-Wl,/path/to/list.def",
2342 "-Wl,C:\\path\\to\\list.def",
2343 "-lmsvcrt",
2344 ];
2345 for arg in removed {
2346 let result = run_filter_one(arg, gnu, (13, 0));
2347 assert!(result.is_empty(), "{arg} should be removed for windows-gnu");
2348 }
2349 let replaced: &[(&str, (u64, u64), &str)] = &[
2351 ("-lgcc_eh", (13, 0), "-lc++"),
2352 ("-Wl,-Bdynamic", (13, 0), "-Wl,-search_paths_first"),
2353 ];
2354 for (arg, zig_ver, expected) in replaced {
2355 let result = run_filter_one(arg, gnu, *zig_ver);
2356 assert_eq!(result, vec![*expected], "filter({arg})");
2357 }
2358 let result = run_filter_one("-lgcc_eh", gnu, (14, 0));
2360 assert_eq!(result, vec!["-lgcc_eh"]);
2361 }
2362
2363 #[test]
2364 fn test_filter_windows_gnu_rsbegin() {
2365 let result = run_filter_one("/path/to/rsbegin.o", Some("i686-pc-windows-gnu"), (13, 0));
2367 assert!(result.is_empty());
2368 let result = run_filter_one("/path/to/rsbegin.o", Some("x86_64-pc-windows-gnu"), (13, 0));
2370 assert_eq!(result, vec!["/path/to/rsbegin.o"]);
2371 }
2372
2373 #[test]
2374 fn test_filter_unsupported_linker_args() {
2375 let linux = Some("x86_64-unknown-linux-gnu");
2376 let removed: &[&str] = &[
2377 "-Wl,--no-undefined-version",
2378 "-Wl,-znostart-stop-gc",
2379 "-Wl,-plugin-opt=O2",
2380 ];
2381 for arg in removed {
2382 let result = run_filter_one(arg, linux, (13, 0));
2383 assert!(result.is_empty(), "{arg} should be removed");
2384 }
2385 }
2386
2387 #[test]
2388 fn test_filter_wp_args() {
2389 let linux = Some("x86_64-unknown-linux-gnu");
2390 for arg in &[
2392 "-Wp,-U_FORTIFY_SOURCE",
2393 "-Wp,-DFOO=1",
2394 "-Wp,-MF,/tmp/t.d",
2395 "-Wp,-MQ,foo",
2396 "-Wp,-MP",
2397 ] {
2398 let result = run_filter_one(arg, linux, (13, 0));
2399 assert!(result.is_empty(), "{arg} should be removed");
2400 }
2401 for arg in &["-Wp,-MD,/tmp/test.d", "-Wp,-MMD,/tmp/test.d", "-Wp,-MT,foo"] {
2403 let result = run_filter_one(arg, linux, (13, 0));
2404 assert_eq!(result, vec![*arg], "{arg} should be kept");
2405 }
2406 let result = run_filter_one("-U_FORTIFY_SOURCE", linux, (13, 0));
2408 assert_eq!(result, vec!["-U_FORTIFY_SOURCE"]);
2409 let result = run_filter_one("-DFOO=1", linux, (13, 0));
2410 assert_eq!(result, vec!["-DFOO=1"]);
2411 }
2412
2413 #[test]
2414 fn test_filter_musl_args() {
2415 let musl = Some("x86_64-unknown-linux-musl");
2416 let removed: &[&str] = &["/path/self-contained/crt1.o", "-lc"];
2417 for arg in removed {
2418 let result = run_filter_one(arg, musl, (13, 0));
2419 assert!(result.is_empty(), "{arg} should be removed for musl");
2420 }
2421 let result = run_filter_one("-Wl,-melf_i386", Some("i686-unknown-linux-musl"), (13, 0));
2423 assert!(result.is_empty());
2424 let result = run_filter_one_rustc("/path/to/liblibc-abc123.rlib", musl, (13, 0), 58);
2426 assert!(result.is_empty());
2427 let result = run_filter_one_rustc("/path/to/liblibc-abc123.rlib", musl, (13, 0), 59);
2428 assert_eq!(result, vec!["/path/to/liblibc-abc123.rlib"]);
2429 }
2430
2431 #[test]
2432 fn test_filter_march_args() {
2433 let cases: &[(&str, &str, &[&str])] = &[
2435 ("-march=armv7-a", "armv7-unknown-linux-gnueabihf", &[]),
2437 (
2439 "-march=rv64gc",
2440 "riscv64gc-unknown-linux-gnu",
2441 &["-march=generic_rv64"],
2442 ),
2443 (
2445 "-march=rv32imac",
2446 "riscv32imac-unknown-none-elf",
2447 &["-march=generic_rv32"],
2448 ),
2449 (
2451 "-march=armv8.4-a",
2452 "aarch64-unknown-linux-gnu",
2453 &["-mcpu=generic"],
2454 ),
2455 (
2457 "-march=armv8.4-a+crypto",
2458 "aarch64-unknown-linux-gnu",
2459 &[
2460 "-mcpu=generic+crypto",
2461 "-Xassembler",
2462 "-march=armv8.4-a+crypto",
2463 ],
2464 ),
2465 (
2467 "-march=armv8.4-a",
2468 "aarch64-apple-darwin",
2469 &["-mcpu=apple_m1"],
2470 ),
2471 ];
2472 for (input, target, expected) in cases {
2473 let result = run_filter_one(input, Some(target), (13, 0));
2474 assert_eq!(&result, expected, "filter({input}, {target})");
2475 }
2476 }
2477
2478 #[test]
2479 fn test_filter_apple_args() {
2480 let darwin = Some("aarch64-apple-darwin");
2481 let result = run_filter_one("-Wl,-dylib", darwin, (13, 0));
2482 assert!(result.is_empty());
2483 }
2484
2485 #[test]
2486 fn test_filter_freebsd_libs_removed() {
2487 for lib in &["-lkvm", "-lmemstat", "-lprocstat", "-ldevstat"] {
2488 let result = run_filter_one(lib, Some("x86_64-unknown-freebsd"), (13, 0));
2489 assert!(result.is_empty(), "{lib} should be removed for freebsd");
2490 }
2491 }
2492
2493 #[test]
2494 fn test_filter_exported_symbols_list_two_arg_apple() {
2495 let result = run_filter(
2496 &[
2497 "-arch",
2498 "arm64",
2499 "-Wl,-exported_symbols_list",
2500 "-Wl,/tmp/rustcXXX/list",
2501 "-o",
2502 "output.dylib",
2503 ],
2504 Some("aarch64-apple-darwin"),
2505 (13, 0),
2506 );
2507 assert_eq!(result, vec!["-arch", "arm64", "-o", "output.dylib"]);
2508 }
2509
2510 #[test]
2511 fn test_filter_exported_symbols_list_two_arg_cross_platform() {
2512 let result = run_filter(
2513 &[
2514 "-arch",
2515 "arm64",
2516 "-Wl,-exported_symbols_list",
2517 "-Wl,C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\rustcXXX\\list",
2518 "-o",
2519 "output.dylib",
2520 ],
2521 None,
2522 (13, 0),
2523 );
2524 assert_eq!(result, vec!["-arch", "arm64", "-o", "output.dylib"]);
2525 }
2526
2527 #[test]
2528 fn test_filter_exported_symbols_list_single_arg_comma() {
2529 let result = run_filter(
2530 &[
2531 "-Wl,-exported_symbols_list,/tmp/rustcXXX/list",
2532 "-o",
2533 "output.dylib",
2534 ],
2535 Some("aarch64-apple-darwin"),
2536 (13, 0),
2537 );
2538 assert_eq!(result, vec!["-o", "output.dylib"]);
2539 }
2540
2541 #[test]
2542 fn test_filter_exported_symbols_list_not_filtered_zig_016() {
2543 let result = run_filter(
2544 &[
2545 "-Wl,-exported_symbols_list",
2546 "-Wl,/tmp/rustcXXX/list",
2547 "-o",
2548 "output.dylib",
2549 ],
2550 Some("aarch64-apple-darwin"),
2551 (16, 0),
2552 );
2553 assert_eq!(
2554 result,
2555 vec![
2556 "-Wl,-exported_symbols_list",
2557 "-Wl,/tmp/rustcXXX/list",
2558 "-o",
2559 "output.dylib"
2560 ]
2561 );
2562 }
2563
2564 #[test]
2565 fn test_filter_dynamic_list_two_arg() {
2566 let result = run_filter(
2567 &[
2568 "-Wl,--dynamic-list",
2569 "-Wl,/tmp/rustcXXX/list",
2570 "-o",
2571 "output.so",
2572 ],
2573 Some("x86_64-unknown-linux-gnu"),
2574 (13, 0),
2575 );
2576 assert_eq!(result, vec!["-o", "output.so"]);
2577 }
2578
2579 #[test]
2580 fn test_filter_dynamic_list_single_arg_comma() {
2581 let result = run_filter(
2582 &["-Wl,--dynamic-list,/tmp/rustcXXX/list", "-o", "output.so"],
2583 Some("x86_64-unknown-linux-gnu"),
2584 (13, 0),
2585 );
2586 assert_eq!(result, vec!["-o", "output.so"]);
2587 }
2588
2589 #[test]
2590 fn test_filter_preserves_normal_args() {
2591 let result = run_filter(
2592 &["-arch", "arm64", "-lSystem", "-lc", "-o", "output"],
2593 Some("aarch64-apple-darwin"),
2594 (13, 0),
2595 );
2596 assert_eq!(
2597 result,
2598 vec!["-arch", "arm64", "-lSystem", "-lc", "-o", "output"]
2599 );
2600 }
2601
2602 #[test]
2603 fn test_filter_skip_next_at_end_of_args() {
2604 let result = run_filter(
2605 &["-o", "output", "-Wl,-exported_symbols_list"],
2606 Some("aarch64-apple-darwin"),
2607 (13, 0),
2608 );
2609 assert_eq!(result, vec!["-o", "output"]);
2610 }
2611}