1use std::{
2 collections::{hash_map::Keys, HashMap},
3 env,
4 path::PathBuf,
5 process::Command,
6};
7
8use regex::Regex;
9mod flags;
10mod library;
11mod search_path;
12
13use library::*;
14use search_path::*;
15use std::ffi::OsString;
16
17use crate::{
18 debug_log, memoize,
19 utils::{is_msvc, shellsplit},
20};
21
22use self::flags::Flags;
23
24#[derive(Debug, PartialEq, Eq)]
27pub struct RbConfig {
28 pub search_paths: Vec<SearchPath>,
29 pub libs: Vec<Library>,
30 pub link_args: Vec<String>,
31 pub cflags: Vec<String>,
32 pub blocklist_lib: Vec<String>,
33 pub blocklist_link_arg: Vec<String>,
34 use_rpath: bool,
35 value_map: HashMap<String, String>,
36}
37
38impl Default for RbConfig {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl RbConfig {
45 pub(crate) fn new() -> RbConfig {
47 RbConfig {
48 blocklist_lib: vec![],
49 blocklist_link_arg: vec![],
50 search_paths: Vec::new(),
51 libs: Vec::new(),
52 link_args: Vec::new(),
53 cflags: Vec::new(),
54 value_map: HashMap::new(),
55 use_rpath: false,
56 }
57 }
58
59 pub fn all_keys(&self) -> Keys<'_, String, String> {
61 self.value_map.keys()
62 }
63
64 pub fn current() -> RbConfig {
66 println!("cargo:rerun-if-env-changed=RUBY");
67
68 let mut rbconfig = RbConfig::new();
69
70 let parsed = if rbconfig.is_cross_compiling() {
73 HashMap::new()
74 } else {
75 let output = memoize!(String: {
76 let ruby = env::var_os("RUBY").unwrap_or_else(|| OsString::from("ruby"));
77
78 let config = Command::new(ruby)
79 .arg("--disable-gems")
80 .arg("-rrbconfig")
81 .arg("-e")
82 .arg("print RbConfig::CONFIG.map {|kv| kv.join(\"\x1F\")}.join(\"\x1E\")")
83 .env_remove("RUBYOPT")
84 .output()
85 .unwrap_or_else(|e| panic!("ruby not found: {}", e));
86 if !config.status.success() {
87 panic!("non-zero exit status while dumping RbConfig: {:?}", config);
88 }
89 String::from_utf8(config.stdout).expect("RbConfig value not UTF-8!")
90 });
91
92 let mut parsed = HashMap::new();
93 for line in output.split('\x1E') {
94 let mut parts = line.splitn(2, '\x1F');
95 if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
96 parsed.insert(key.to_owned(), val.to_owned());
97 }
98 }
99 parsed
100 };
101
102 parsed.get("cflags").map(|f| rbconfig.push_cflags(f));
103 parsed.get("DLDFLAGS").map(|f| rbconfig.push_dldflags(f));
104
105 rbconfig.value_map = parsed;
106
107 rbconfig
108 }
109
110 pub fn link_ruby(&mut self, is_static: bool) -> &mut Self {
112 let Some(libdir) = self.get("libdir") else {
113 return self;
114 };
115
116 self.push_search_path(libdir.as_str());
117 self.push_dldflags(&format!("-L{}", libdir));
118
119 let librubyarg = if is_static {
120 self.get("LIBRUBYARG_STATIC")
121 } else {
122 self.get("LIBRUBYARG_SHARED")
123 };
124
125 let librubyarg = match librubyarg {
126 Some(lib) => lib,
127 None => {
128 debug_log!("WARN: LIBRUBYARG not found in RbConfig, skipping linking Ruby");
129 return self;
130 }
131 };
132
133 if is_msvc() {
134 for lib in librubyarg.split_whitespace() {
135 self.push_library(lib);
136 }
137
138 let mut to_link: Vec<String> = vec![];
139
140 if let Some(libs) = self.get("LIBS") {
141 to_link.extend(libs.split_whitespace().map(|s| s.to_string()));
142 }
143
144 if let Some(libs) = self.get("LOCAL_LIBS") {
145 to_link.extend(libs.split_whitespace().map(|s| s.to_string()));
146 }
147
148 for lib in to_link {
149 self.push_library(lib);
150 }
151 } else {
152 self.push_dldflags(&librubyarg);
153
154 if cfg!(unix) {
155 self.use_rpath();
156 }
157 }
158
159 self
160 }
161
162 pub fn libruby_static_name(&self) -> String {
164 let Some(lib) = self.get("LIBRUBY_A") else {
165 return format!("{}-static", self.libruby_so_name());
166 };
167
168 lib.trim_start_matches("lib")
169 .trim_end_matches(".a")
170 .to_string()
171 }
172
173 pub fn libruby_so_name(&self) -> String {
175 self.get("RUBY_SO_NAME")
176 .unwrap_or_else(|| "ruby".to_string())
177 }
178
179 pub fn platform(&self) -> String {
181 self.get("platform")
182 .unwrap_or_else(|| self.get("arch").expect("arch not found"))
183 }
184
185 pub fn blocklist_lib(&mut self, name: &str) -> &mut RbConfig {
187 self.blocklist_lib.push(name.to_string());
188 self
189 }
190
191 pub fn blocklist_link_arg(&mut self, name: &str) -> &mut RbConfig {
193 self.blocklist_link_arg.push(name.to_string());
194 self
195 }
196
197 pub fn ruby_version_slug(&self) -> String {
199 let ver = if let Some(progv) = self.get("RUBY_PROGRAM_VERSION") {
200 progv
201 } else if let Some(major_minor) = self.major_minor() {
202 format!(
203 "{}.{}.{}",
204 major_minor.0,
205 major_minor.1,
206 self.get("TEENY").unwrap_or_else(|| "0".to_string())
207 )
208 } else if let Some(fallback) = self.get("ruby_version") {
209 fallback
210 } else {
211 panic!("RUBY_PROGRAM_VERSION not found")
212 };
213
214 format!("{}-{}-{}", self.ruby_engine(), self.platform(), ver)
215 }
216
217 pub fn cppflags(&self) -> Vec<String> {
219 if let Some(cppflags) = self.get("CPPFLAGS") {
220 let flags = self.subst_shell_variables(&cppflags);
221 shellsplit(flags)
222 } else {
223 vec![]
224 }
225 }
226
227 pub fn is_cross_compiling(&self) -> bool {
229 if let Some(cross) = self.get("CROSS_COMPILING") {
230 cross == "yes" || cross == "1"
231 } else {
232 false
233 }
234 }
235
236 pub fn validate_cargo_target(&self) -> Result<(), String> {
242 let Ok(cargo_target) = env::var("TARGET") else {
243 return Ok(());
244 };
245 let Some(ruby_target_os) = self.get("target_os") else {
246 return Ok(());
247 };
248
249 validate_target_compatibility(&ruby_target_os, &cargo_target)
250 }
251
252 pub fn get(&self, key: &str) -> Option<String> {
255 self.try_rbconfig_env(key)
256 .or_else(|| self.try_value_map(key))
257 }
258
259 pub fn use_rpath(&mut self) -> &mut RbConfig {
261 self.use_rpath = true;
262 self
263 }
264
265 pub fn push_cflags(&mut self, cflags: &str) -> &mut Self {
267 for flag in shellsplit(cflags) {
268 if !self.cflags.contains(&flag) {
269 self.cflags.push(flag.to_string());
270 }
271 }
272
273 self
274 }
275
276 pub fn major_minor(&self) -> Option<(u32, u32)> {
278 let major = self.get("MAJOR").map(|v| v.parse::<u32>())?.ok()?;
279 let minor = self.get("MINOR").map(|v| v.parse::<u32>())?.ok()?;
280 Some((major, minor))
281 }
282
283 pub fn cargo_args(&self) -> Vec<String> {
285 let mut result = vec![];
286
287 let mut search_paths = vec![];
288
289 for search_path in &self.search_paths {
290 result.push(format!("cargo:rustc-link-search={}", search_path));
291 search_paths.push(search_path.name.as_str());
292 }
293
294 for lib in &self.libs {
295 if !self.blocklist_lib.iter().any(|b| lib.name.contains(b)) {
296 result.push(format!("cargo:rustc-link-lib={}", lib));
297 }
298
299 if self.use_rpath && !lib.is_static() {
300 result.push(format!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib));
301 }
302 }
303
304 for link_arg in &self.link_args {
305 if !self.blocklist_link_arg.iter().any(|b| link_arg == b) {
306 result.push(format!("cargo:rustc-link-arg={}", link_arg));
307 }
308 }
309
310 result
311 }
312
313 pub fn print_cargo_args(&self) {
315 let cargo_args = self.cargo_args();
316
317 for arg in &cargo_args {
318 println!("{}", arg);
319 }
320
321 debug_log!("INFO: printing cargo args ({:?})", cargo_args);
322
323 let encoded_cargo_args = cargo_args.join("\x1E");
324 let encoded_cargo_args = encoded_cargo_args.replace('\n', "\x1F");
325
326 println!("cargo:encoded_cargo_args={}", encoded_cargo_args);
327 }
328
329 pub fn push_dldflags(&mut self, input: &str) -> &mut Self {
331 let input = self.subst_shell_variables(input);
332 let split_args = Flags::new(input.as_str());
333
334 let search_path_regex = Regex::new(r"^-L\s*(?P<name>.*)$").unwrap();
335 let lib_regex_short = Regex::new(r"^-l\s*(?P<name>\w+\S+)$").unwrap();
336 let lib_regex_long = Regex::new(r"^--library=(?P<name>\w+\S+)$").unwrap();
337 let dynamic_lib_regex = Regex::new(r"^-l\s*:lib(?P<name>\S+).(so|dylib|dll)$").unwrap();
338 let framework_regex_short = Regex::new(r"^-F\s*(?P<name>.*)$").unwrap();
339 let framework_regex_long = Regex::new(r"^-framework\s*(?P<name>.*)$").unwrap();
340
341 for arg in split_args {
342 let arg = arg.trim().to_owned();
343
344 if let Some(name) = capture_name(&search_path_regex, &arg) {
345 self.push_search_path(name.as_str());
346 } else if let Some(name) = capture_name(&lib_regex_long, &arg) {
347 self.push_library(name);
348 } else if let Some(name) = capture_name(&lib_regex_short, &arg) {
349 if name.contains("ruby") && name.contains("-static") {
350 self.push_library((LibraryKind::Static, name));
351 } else {
352 self.push_library(name);
353 }
354 } else if let Some(name) = capture_name(&dynamic_lib_regex, &arg) {
355 self.push_library((LibraryKind::Dylib, name));
356 } else if let Some(name) = capture_name(&framework_regex_short, &arg) {
357 self.push_search_path((SearchPathKind::Framework, name));
358 } else if let Some(name) = capture_name(&framework_regex_long, &arg) {
359 self.push_library((LibraryKind::Framework, name));
360 } else {
361 self.push_link_arg(arg);
362 }
363 }
364
365 self
366 }
367
368 pub fn set_value_for_key(&mut self, key: &str, value: String) {
370 self.value_map.insert(key.to_owned(), value);
371 }
372
373 pub fn has_ruby_dln_check_abi(&self) -> bool {
375 let Some((major, minor)) = self.major_minor() else {
376 return false;
377 };
378
379 let patchlevel = self
380 .get("PATCHLEVEL")
381 .and_then(|v| v.parse::<i32>().ok())
382 .unwrap_or(-1);
383
384 (major > 3 || (major == 3 && minor >= 2))
387 && patchlevel == -1
388 && !cfg!(target_family = "windows")
389 }
390
391 pub fn ruby_engine(&self) -> RubyEngine {
393 if let Some(engine) = self.get("ruby_install_name") {
394 match engine.as_str() {
395 "ruby" => RubyEngine::Mri,
396 "jruby" => RubyEngine::JRuby,
397 "truffleruby" => RubyEngine::TruffleRuby,
398 _ => RubyEngine::Mri, }
400 } else {
401 RubyEngine::Mri
402 }
403 }
404
405 fn subst_shell_variables(&self, input: &str) -> String {
407 let mut result = String::new();
408 let mut chars = input.chars().enumerate();
409
410 while let Some((_, c)) = chars.next() {
411 if c == '$' {
412 if let Some((i, c)) = chars.next() {
413 if c == '(' {
414 let start = i + 1;
415 let mut end = start;
416
417 for (i, c) in chars.by_ref() {
418 if c == ')' {
419 end = i;
420 break;
421 }
422 }
423
424 let key = &input[start..end];
425
426 if let Some(val) = self.get(key) {
427 result.push_str(&val);
428 } else if let Some(val) = env::var_os(key) {
429 result.push_str(&val.to_string_lossy());
430 } else {
431 chars.next();
433 }
434 } else {
435 result.push(c);
436 }
437 }
438 } else {
439 result.push(c);
440 }
441 }
442
443 result
444 }
445
446 pub fn have_ruby_header<T: AsRef<str>>(&self, header: T) -> bool {
447 let Some(ruby_include_dir) = self.get("rubyhdrdir") else {
448 return false;
449 };
450 PathBuf::from(ruby_include_dir)
451 .join(header.as_ref())
452 .exists()
453 }
454
455 fn push_search_path<T: Into<SearchPath>>(&mut self, path: T) -> &mut Self {
456 let path = path.into();
457
458 if !self.search_paths.contains(&path) {
459 self.search_paths.push(path);
460 }
461
462 self
463 }
464
465 fn push_library<T: Into<Library>>(&mut self, lib: T) -> &mut Self {
466 let lib = lib.into();
467
468 if !self.libs.contains(&lib) {
469 self.libs.push(lib);
470 }
471
472 self
473 }
474
475 fn push_link_arg<T: Into<String>>(&mut self, arg: T) -> &mut Self {
476 let arg = arg.into();
477
478 if !self.link_args.contains(&arg) {
479 self.link_args.push(arg);
480 }
481
482 self
483 }
484
485 fn try_value_map(&self, key: &str) -> Option<String> {
486 self.value_map
487 .get(key)
488 .map(|val| val.trim_matches('\n').to_owned())
489 }
490
491 fn try_rbconfig_env(&self, key: &str) -> Option<String> {
492 let key = format!("RBCONFIG_{}", key);
493 println!("cargo:rerun-if-env-changed={}", key);
494 env::var(key).map(|v| v.trim_matches('\n').to_owned()).ok()
495 }
496}
497
498#[derive(Debug, PartialEq, Eq)]
499enum TargetFamily {
500 WindowsGnu,
501 WindowsMsvc,
502 Os(&'static str),
503}
504
505fn ruby_target_family(target_os: &str) -> Option<TargetFamily> {
506 let target_os = target_os.to_ascii_lowercase();
507
508 if target_os.contains("mingw") {
509 return Some(TargetFamily::WindowsGnu);
510 }
511 if target_os.contains("mswin") {
512 return Some(TargetFamily::WindowsMsvc);
513 }
514
515 target_os_family(&target_os)
516}
517
518fn cargo_target_family(target: &str) -> Option<TargetFamily> {
519 let target = target.to_ascii_lowercase();
520
521 if target.contains("windows-gnu") || target.contains("windows-gnullvm") {
522 return Some(TargetFamily::WindowsGnu);
523 }
524 if target.contains("windows-msvc") {
525 return Some(TargetFamily::WindowsMsvc);
526 }
527
528 target_os_family(&target)
529}
530
531fn target_os_family(target: &str) -> Option<TargetFamily> {
532 const OPERATING_SYSTEMS: &[&str] = &[
533 "android",
534 "darwin",
535 "dragonfly",
536 "emscripten",
537 "freebsd",
538 "haiku",
539 "illumos",
540 "ios",
541 "linux",
542 "netbsd",
543 "openbsd",
544 "solaris",
545 "wasi",
546 ];
547
548 OPERATING_SYSTEMS
549 .iter()
550 .find(|operating_system| target.contains(*operating_system))
551 .map(|operating_system| TargetFamily::Os(operating_system))
552}
553
554fn validate_target_compatibility(ruby_target_os: &str, cargo_target: &str) -> Result<(), String> {
555 let Some(ruby_family) = ruby_target_family(ruby_target_os) else {
556 return Ok(());
557 };
558 let Some(cargo_family) = cargo_target_family(cargo_target) else {
559 return Ok(());
560 };
561
562 if ruby_family == cargo_family {
563 return Ok(());
564 }
565
566 let mut message = format!(
567 "Ruby was built for target_os={ruby_target_os:?}, but Cargo is compiling for \
568 {cargo_target:?}. rb-sys cannot generate bindings from Ruby headers for a different \
569 operating-system ABI."
570 );
571
572 if ruby_family == TargetFamily::WindowsGnu && cargo_family == TargetFamily::WindowsMsvc {
573 message.push_str(
574 " RubyInstaller uses MinGW; select the matching Rust GNU target (for 64-bit \
575 RubyInstaller, set RUST_TARGET=x86_64-pc-windows-gnu or pass \
576 --target x86_64-pc-windows-gnu to Cargo).",
577 );
578 } else {
579 message.push_str(
580 " When cross-compiling with rb-sys-dock, use `rb-sys-dock --build` or the \
581 `rake native:$RUBY_TARGET` task so rake-compiler supplies the target Ruby's \
582 RbConfig instead of the container's host Ruby config.",
583 );
584 }
585
586 Err(message)
587}
588
589#[derive(Debug, PartialEq, Eq, Clone, Copy)]
590pub enum RubyEngine {
591 Mri,
592 TruffleRuby,
593 JRuby,
594}
595
596impl std::fmt::Display for RubyEngine {
597 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598 match self {
599 RubyEngine::Mri => write!(f, "mri"),
600 RubyEngine::TruffleRuby => write!(f, "truffleruby"),
601 RubyEngine::JRuby => write!(f, "jruby"),
602 }
603 }
604}
605
606fn capture_name(regex: &Regex, arg: &str) -> Option<String> {
607 regex
608 .captures(arg)
609 .map(|cap| cap.name("name").unwrap().as_str().trim().to_owned())
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use std::{sync::Mutex, vec};
616
617 lazy_static::lazy_static! {
618 static ref ENV_LOCK: Mutex<()> = Mutex::new(());
619 }
620
621 fn with_locked_env<F, T>(f: F) -> T
622 where
623 F: FnOnce() -> T,
624 {
625 let _guard = ENV_LOCK.lock().unwrap();
626 f()
627 }
628
629 #[test]
630 fn test_extract_lib_search_paths() {
631 let mut rb_config = RbConfig::new();
632 rb_config.push_dldflags("-L/usr/local/lib -L/usr/lib");
633 assert_eq!(
634 rb_config.search_paths,
635 vec!["/usr/local/lib".into(), "/usr/lib".into()]
636 );
637 }
638
639 #[test]
640 fn test_search_path_basic() {
641 let mut rb_config = RbConfig::new();
642 rb_config.push_dldflags("-L/usr/local/lib");
643
644 assert_eq!(rb_config.search_paths, vec!["native=/usr/local/lib".into()]);
645 }
646
647 #[test]
648 fn test_search_path_space() {
649 let mut rb_config = RbConfig::new();
650 rb_config.push_dldflags("-L /usr/local/lib");
651
652 assert_eq!(rb_config.search_paths, vec!["/usr/local/lib".into()]);
653 }
654
655 #[test]
656 fn test_search_path_space_in_path() {
657 let mut rb_config = RbConfig::new();
658 rb_config.push_dldflags("-L/usr/local/my lib");
659
660 assert_eq!(
661 rb_config.search_paths,
662 vec!["native=/usr/local/my lib".into()]
663 );
664 }
665
666 #[test]
667 fn test_simple_lib() {
668 let mut rb_config = RbConfig::new();
669 rb_config.push_dldflags("-lfoo");
670
671 assert_eq!(rb_config.libs, ["foo".into()]);
672 }
673
674 #[test]
675 fn test_lib_with_nonascii() {
676 let mut rb_config = RbConfig::new();
677 rb_config.push_dldflags("-lws2_32");
678
679 assert_eq!(rb_config.libs, ["ws2_32".into()]);
680 }
681
682 #[test]
683 fn test_simple_lib_space() {
684 let mut rb_config = RbConfig::new();
685 rb_config.push_dldflags("-l foo");
686
687 assert_eq!(rb_config.libs, ["foo".into()]);
688 }
689
690 #[test]
691 fn test_verbose_lib_space() {
692 let mut rb_config = RbConfig::new();
693 rb_config.push_dldflags("--library=foo");
694
695 assert_eq!(rb_config.libs, ["foo".into()]);
696 }
697
698 #[test]
699 fn test_dylib_with_colon_space() {
700 let mut rb_config = RbConfig::new();
701 rb_config.push_dldflags("-l :libssp.dylib");
702
703 assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
704 }
705
706 #[test]
707 fn test_so_with_colon_space() {
708 let mut rb_config = RbConfig::new();
709 rb_config.push_dldflags("-l :libssp.so");
710
711 assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
712 }
713
714 #[test]
715 fn test_dll_with_colon_space() {
716 let mut rb_config = RbConfig::new();
717 rb_config.push_dldflags("-l :libssp.dll");
718
719 assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
720 }
721
722 #[test]
723 fn test_framework() {
724 let mut rb_config = RbConfig::new();
725 rb_config.push_dldflags("-F/some/path");
726
727 assert_eq!(rb_config.search_paths, ["framework=/some/path".into()]);
728 }
729
730 #[test]
731 fn test_framework_space() {
732 let mut rb_config = RbConfig::new();
733 rb_config.push_dldflags("-F /some/path");
734
735 assert_eq!(
736 rb_config.search_paths,
737 [SearchPath {
738 kind: SearchPathKind::Framework,
739 name: "/some/path".into(),
740 }]
741 );
742 }
743
744 #[test]
745 fn test_framework_arg_real() {
746 let mut rb_config = RbConfig::new();
747 rb_config.push_dldflags("-framework CoreFoundation");
748
749 assert_eq!(
750 rb_config.libs,
751 [Library {
752 kind: LibraryKind::Framework,
753 name: "CoreFoundation".into(),
754 }]
755 );
756 }
757
758 #[test]
759 fn test_libruby_static() {
760 let mut rb_config = RbConfig::new();
761 rb_config.push_dldflags("-lruby.3.1-static");
762
763 assert_eq!(
764 rb_config.cargo_args(),
765 ["cargo:rustc-link-lib=static=ruby.3.1-static"]
766 );
767 }
768
769 #[test]
770 fn test_libruby_dynamic() {
771 let mut rb_config = RbConfig::new();
772 rb_config.push_dldflags("-lruby.3.1");
773
774 assert_eq!(rb_config.cargo_args(), ["cargo:rustc-link-lib=ruby.3.1"]);
775 }
776
777 #[test]
778 fn test_non_lib_dash_l() {
779 let mut rb_config = RbConfig::new();
780 rb_config.push_dldflags("test_rubygems_20220413-976-lemgf9/prefix");
781
782 assert_eq!(
783 rb_config.link_args,
784 vec!["test_rubygems_20220413-976-lemgf9/prefix"]
785 );
786 }
787
788 #[test]
789 fn test_real_dldflags() {
790 let mut rb_config = RbConfig::new();
791 rb_config.push_dldflags("-L/Users/ianks/.asdf/installs/ruby/3.1.1/lib -L/opt/homebrew/opt/openssl@1.1/lib -Wl,-undefined,dynamic_lookup -Wl,-multiply_defined,suppress");
792
793 assert_eq!(
794 rb_config.link_args,
795 vec![
796 "-Wl,-undefined,dynamic_lookup",
797 "-Wl,-multiply_defined,suppress"
798 ]
799 );
800 assert_eq!(
801 rb_config.search_paths,
802 vec![
803 SearchPath {
804 kind: SearchPathKind::Native,
805 name: "/Users/ianks/.asdf/installs/ruby/3.1.1/lib".to_string()
806 },
807 SearchPath {
808 kind: SearchPathKind::Native,
809 name: "/opt/homebrew/opt/openssl@1.1/lib".to_string()
810 },
811 ]
812 );
813 }
814
815 #[test]
816 fn test_crazy_cases() {
817 let mut rb_config = RbConfig::new();
818 rb_config.push_dldflags("-F /something -l:libssp.a -static-libgcc ");
819
820 assert_eq!(rb_config.link_args, vec!["-l:libssp.a", "-static-libgcc"]);
821 assert_eq!(
822 rb_config.search_paths,
823 vec![SearchPath {
824 kind: SearchPathKind::Framework,
825 name: "/something".to_string()
826 },]
827 );
828 }
829
830 #[test]
831 fn test_printing_cargo_args() {
832 let mut rb_config = RbConfig::new();
833 rb_config.push_dldflags("-L/Users/ianks/.asdf/installs/ruby/3.1.1/lib");
834 rb_config.push_dldflags("-lfoo");
835 rb_config.push_dldflags("-static-libgcc");
836 let result = rb_config.cargo_args();
837
838 assert_eq!(
839 vec![
840 "cargo:rustc-link-search=native=/Users/ianks/.asdf/installs/ruby/3.1.1/lib",
841 "cargo:rustc-link-lib=foo",
842 "cargo:rustc-link-arg=-static-libgcc"
843 ],
844 result
845 );
846 }
847
848 #[test]
849 fn test_use_rpath() {
850 let mut rb_config = RbConfig::new();
851 rb_config.push_dldflags("-lfoo");
852
853 assert_eq!(vec!["cargo:rustc-link-lib=foo"], rb_config.cargo_args());
854
855 rb_config.use_rpath();
856
857 assert_eq!(
858 vec![
859 "cargo:rustc-link-lib=foo",
860 "cargo:rustc-link-arg=-Wl,-rpath,foo"
861 ],
862 rb_config.cargo_args()
863 );
864 }
865
866 #[test]
867 fn test_link_mswin() {
868 with_locked_env(|| {
869 let old_var = env::var("TARGET").ok();
870 env::set_var("TARGET", "x86_64-pc-windows-msvc");
871
872 let mut rb_config = RbConfig::new();
873 rb_config.set_value_for_key("LIBRUBYARG_SHARED", "x64-vcruntime140-ruby320.lib".into());
874 rb_config.set_value_for_key("libdir", "D:/ruby-mswin/lib".into());
875 rb_config.set_value_for_key("LIBS", "user32.lib".into());
876 rb_config.link_ruby(false);
877
878 assert_eq!(
879 vec![
880 "cargo:rustc-link-search=native=D:/ruby-mswin/lib",
881 "cargo:rustc-link-lib=x64-vcruntime140-ruby320",
882 "cargo:rustc-link-lib=user32",
883 ],
884 rb_config.cargo_args()
885 );
886
887 if let Some(old_var) = old_var {
888 env::set_var("TARGET", old_var);
889 } else {
890 env::remove_var("TARGET");
891 }
892 })
893 }
894
895 #[test]
896 fn test_link_static() {
897 with_locked_env(|| {
898 let mut rb_config = RbConfig::new();
899 rb_config.set_value_for_key("LIBRUBYARG_STATIC", "-lruby-static".into());
900 rb_config.set_value_for_key("libdir", "/opt/ruby".into());
901
902 rb_config.link_ruby(true);
903
904 assert_eq!(
905 vec![
906 "cargo:rustc-link-search=native=/opt/ruby",
907 "cargo:rustc-link-lib=static=ruby-static",
908 ],
909 rb_config.cargo_args()
910 );
911 });
912 }
913
914 #[test]
915 fn test_prioritizes_rbconfig_env() {
916 with_locked_env(|| {
917 env::set_var("RBCONFIG_libdir", "/foo");
918 let rb_config = RbConfig::new();
919
920 assert_eq!(rb_config.get("libdir"), Some("/foo".into()));
921
922 env::remove_var("RBCONFIG_libdir");
923 });
924 }
925
926 #[test]
927 fn test_never_loads_shell_rbconfig_if_cross_compiling() {
928 with_locked_env(|| {
929 env::set_var("RBCONFIG_CROSS_COMPILING", "yes");
930
931 let rb_config = RbConfig::current();
932
933 assert!(rb_config.value_map.is_empty());
934 });
935 }
936
937 #[test]
938 fn test_loads_shell_rbconfig_if_not_cross_compiling() {
939 with_locked_env(|| {
940 env::set_var("RBCONFIG_CROSS_COMPILING", "no");
941
942 let rb_config = RbConfig::current();
943
944 assert!(!rb_config.value_map.is_empty());
945 });
946 }
947
948 #[test]
949 fn test_accepts_matching_ruby_and_cargo_targets() {
950 assert_eq!(
951 validate_target_compatibility("mingw32", "x86_64-pc-windows-gnu"),
952 Ok(())
953 );
954 assert_eq!(
955 validate_target_compatibility("mingw-ucrt", "aarch64-pc-windows-gnullvm"),
956 Ok(())
957 );
958 assert_eq!(
959 validate_target_compatibility("linux-gnu", "aarch64-unknown-linux-gnu"),
960 Ok(())
961 );
962 assert_eq!(
963 validate_target_compatibility("darwin24", "aarch64-apple-darwin"),
964 Ok(())
965 );
966 }
967
968 #[test]
969 fn test_rejects_mingw_ruby_with_msvc_cargo_target() {
970 let error = validate_target_compatibility("mingw32", "x86_64-pc-windows-msvc").unwrap_err();
971
972 assert!(error.contains("RubyInstaller uses MinGW"));
973 assert!(error.contains("RUST_TARGET=x86_64-pc-windows-gnu"));
974 }
975
976 #[test]
977 fn test_rejects_host_ruby_config_for_rb_sys_dock_target() {
978 let error =
979 validate_target_compatibility("linux-gnu", "x86_64-pc-windows-gnu").unwrap_err();
980
981 assert!(error.contains("rb-sys-dock --build"));
982 assert!(error.contains("rake native:$RUBY_TARGET"));
983 assert!(error.contains("host Ruby config"));
984 }
985
986 #[test]
987 fn test_allows_unknown_targets_for_backwards_compatibility() {
988 assert_eq!(
989 validate_target_compatibility("new-ruby-os", "x86_64-new-rust-os"),
990 Ok(())
991 );
992 }
993
994 #[test]
995 fn test_libstatic() {
996 let mut rb_config = RbConfig::new();
997 rb_config.push_dldflags("-l:libssp.a");
998
999 assert_eq!(rb_config.link_args, ["-l:libssp.a".to_string()]);
1000 }
1001
1002 #[test]
1003 fn test_link_arg_blocklist() {
1004 let mut rb_config = RbConfig::new();
1005 rb_config.blocklist_link_arg("-Wl,--compress-debug-sections=zlib");
1006 rb_config.blocklist_link_arg("-s");
1007 rb_config.push_dldflags(
1008 "-lfoo -Wl,--compress-debug-sections=zlib -s -somethingthatshouldnotbeblocked",
1009 );
1010
1011 assert_eq!(
1012 vec![
1013 "cargo:rustc-link-lib=foo",
1014 "cargo:rustc-link-arg=-somethingthatshouldnotbeblocked"
1015 ],
1016 rb_config.cargo_args()
1017 );
1018 }
1019
1020 #[test]
1021 fn test_has_ruby_dln_check_abi() {
1022 fn make_config(major: &str, minor: &str, patchlevel: &str) -> RbConfig {
1024 let mut rb_config = RbConfig::new();
1025 rb_config.set_value_for_key("MAJOR", major.into());
1026 rb_config.set_value_for_key("MINOR", minor.into());
1027 rb_config.set_value_for_key("PATCHLEVEL", patchlevel.into());
1028 rb_config
1029 }
1030
1031 assert!(!make_config("3", "1", "-1").has_ruby_dln_check_abi());
1033 assert!(!make_config("3", "1", "0").has_ruby_dln_check_abi());
1034
1035 #[cfg(not(target_family = "windows"))]
1037 assert!(make_config("3", "2", "-1").has_ruby_dln_check_abi());
1038
1039 assert!(!make_config("3", "2", "0").has_ruby_dln_check_abi());
1041
1042 #[cfg(not(target_family = "windows"))]
1044 assert!(make_config("3", "3", "-1").has_ruby_dln_check_abi());
1045
1046 #[cfg(not(target_family = "windows"))]
1048 assert!(make_config("4", "0", "-1").has_ruby_dln_check_abi());
1049
1050 assert!(!make_config("4", "0", "0").has_ruby_dln_check_abi());
1052
1053 #[cfg(not(target_family = "windows"))]
1055 assert!(make_config("4", "1", "-1").has_ruby_dln_check_abi());
1056
1057 assert!(!make_config("2", "7", "-1").has_ruby_dln_check_abi());
1059 }
1060}