rb_sys_build/
rb_config.rs

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/// Extracts structured information from raw compiler/linker flags to make
25/// compiling Ruby gems easier.
26#[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    /// Creates a new, blank `RbConfig`. You likely want to use `RbConfig::current()` instead.
46    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    /// All keys in the `RbConfig`'s value map.
60    pub fn all_keys(&self) -> Keys<'_, String, String> {
61        self.value_map.keys()
62    }
63
64    /// Instantiates a new `RbConfig` for the current Ruby.
65    pub fn current() -> RbConfig {
66        println!("cargo:rerun-if-env-changed=RUBY");
67
68        let mut rbconfig = RbConfig::new();
69
70        // Never use the current Ruby's RbConfig if we're cross compiling, or
71        // else bad things happen
72        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                    .output()
84                    .unwrap_or_else(|e| panic!("ruby not found: {}", e));
85                if !config.status.success() {
86                    panic!("non-zero exit status while dumping RbConfig: {:?}", config);
87                }
88                String::from_utf8(config.stdout).expect("RbConfig value not UTF-8!")
89            });
90
91            let mut parsed = HashMap::new();
92            for line in output.split('\x1E') {
93                let mut parts = line.splitn(2, '\x1F');
94                if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
95                    parsed.insert(key.to_owned(), val.to_owned());
96                }
97            }
98            parsed
99        };
100
101        parsed.get("cflags").map(|f| rbconfig.push_cflags(f));
102        parsed.get("DLDFLAGS").map(|f| rbconfig.push_dldflags(f));
103
104        rbconfig.value_map = parsed;
105
106        rbconfig
107    }
108
109    /// Pushes the `LIBRUBYARG` flags so Ruby will be linked.
110    pub fn link_ruby(&mut self, is_static: bool) -> &mut Self {
111        let Some(libdir) = self.get("libdir") else {
112            return self;
113        };
114
115        self.push_search_path(libdir.as_str());
116        self.push_dldflags(&format!("-L{}", libdir));
117
118        let librubyarg = if is_static {
119            self.get("LIBRUBYARG_STATIC")
120        } else {
121            self.get("LIBRUBYARG_SHARED")
122        };
123
124        let librubyarg = match librubyarg {
125            Some(lib) => lib,
126            None => {
127                debug_log!("WARN: LIBRUBYARG not found in RbConfig, skipping linking Ruby");
128                return self;
129            }
130        };
131
132        if is_msvc() {
133            for lib in librubyarg.split_whitespace() {
134                self.push_library(lib);
135            }
136
137            let mut to_link: Vec<String> = vec![];
138
139            if let Some(libs) = self.get("LIBS") {
140                to_link.extend(libs.split_whitespace().map(|s| s.to_string()));
141            }
142
143            if let Some(libs) = self.get("LOCAL_LIBS") {
144                to_link.extend(libs.split_whitespace().map(|s| s.to_string()));
145            }
146
147            for lib in to_link {
148                self.push_library(lib);
149            }
150        } else {
151            self.push_dldflags(&librubyarg);
152
153            if cfg!(unix) {
154                self.use_rpath();
155            }
156        }
157
158        self
159    }
160
161    /// Get the name for libruby-static (i.e. `ruby.3.1-static`).
162    pub fn libruby_static_name(&self) -> String {
163        let Some(lib) = self.get("LIBRUBY_A") else {
164            return format!("{}-static", self.libruby_so_name());
165        };
166
167        lib.trim_start_matches("lib")
168            .trim_end_matches(".a")
169            .to_string()
170    }
171
172    /// Get the name for libruby (i.e. `ruby.3.1`)
173    pub fn libruby_so_name(&self) -> String {
174        self.get("RUBY_SO_NAME")
175            .unwrap_or_else(|| "ruby".to_string())
176    }
177
178    /// Get the platform for the current ruby.
179    pub fn platform(&self) -> String {
180        self.get("platform")
181            .unwrap_or_else(|| self.get("arch").expect("arch not found"))
182    }
183
184    /// Filter the libs, removing the ones that are not needed.
185    pub fn blocklist_lib(&mut self, name: &str) -> &mut RbConfig {
186        self.blocklist_lib.push(name.to_string());
187        self
188    }
189
190    /// Blocklist a link argument.
191    pub fn blocklist_link_arg(&mut self, name: &str) -> &mut RbConfig {
192        self.blocklist_link_arg.push(name.to_string());
193        self
194    }
195
196    /// Returns the current ruby program version.
197    pub fn ruby_version_slug(&self) -> String {
198        let ver = if let Some(progv) = self.get("RUBY_PROGRAM_VERSION") {
199            progv
200        } else if let Some(major_minor) = self.major_minor() {
201            format!(
202                "{}.{}.{}",
203                major_minor.0,
204                major_minor.1,
205                self.get("TEENY").unwrap_or_else(|| "0".to_string())
206            )
207        } else if let Some(fallback) = self.get("ruby_version") {
208            fallback
209        } else {
210            panic!("RUBY_PROGRAM_VERSION not found")
211        };
212
213        format!("{}-{}-{}", self.ruby_engine(), self.platform(), ver)
214    }
215
216    /// Get the CPPFLAGS from the RbConfig, making sure to subsitute variables.
217    pub fn cppflags(&self) -> Vec<String> {
218        if let Some(cppflags) = self.get("CPPFLAGS") {
219            let flags = self.subst_shell_variables(&cppflags);
220            shellsplit(flags)
221        } else {
222            vec![]
223        }
224    }
225
226    /// Returns true if the current Ruby is cross compiling.
227    pub fn is_cross_compiling(&self) -> bool {
228        if let Some(cross) = self.get("CROSS_COMPILING") {
229            cross == "yes" || cross == "1"
230        } else {
231            false
232        }
233    }
234
235    /// Returns the value of the given key from the either the matching
236    /// `RBCONFIG_{key}` environment variable or `RbConfig::CONFIG[{key}]` hash.
237    pub fn get(&self, key: &str) -> Option<String> {
238        self.try_rbconfig_env(key)
239            .or_else(|| self.try_value_map(key))
240    }
241
242    /// Enables the use of rpath for linking.
243    pub fn use_rpath(&mut self) -> &mut RbConfig {
244        self.use_rpath = true;
245        self
246    }
247
248    /// Push cflags string
249    pub fn push_cflags(&mut self, cflags: &str) -> &mut Self {
250        for flag in shellsplit(cflags) {
251            if !self.cflags.contains(&flag) {
252                self.cflags.push(flag.to_string());
253            }
254        }
255
256        self
257    }
258
259    /// Get major/minor version tuple of Ruby
260    pub fn major_minor(&self) -> Option<(u32, u32)> {
261        let major = self.get("MAJOR").map(|v| v.parse::<u32>())?.ok()?;
262        let minor = self.get("MINOR").map(|v| v.parse::<u32>())?.ok()?;
263        Some((major, minor))
264    }
265
266    /// Get the rb_config output for cargo
267    pub fn cargo_args(&self) -> Vec<String> {
268        let mut result = vec![];
269
270        let mut search_paths = vec![];
271
272        for search_path in &self.search_paths {
273            result.push(format!("cargo:rustc-link-search={}", search_path));
274            search_paths.push(search_path.name.as_str());
275        }
276
277        for lib in &self.libs {
278            if !self.blocklist_lib.iter().any(|b| lib.name.contains(b)) {
279                result.push(format!("cargo:rustc-link-lib={}", lib));
280            }
281
282            if self.use_rpath && !lib.is_static() {
283                result.push(format!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib));
284            }
285        }
286
287        for link_arg in &self.link_args {
288            if !self.blocklist_link_arg.iter().any(|b| link_arg == b) {
289                result.push(format!("cargo:rustc-link-arg={}", link_arg));
290            }
291        }
292
293        result
294    }
295
296    /// Print to rb_config output for cargo
297    pub fn print_cargo_args(&self) {
298        let cargo_args = self.cargo_args();
299
300        for arg in &cargo_args {
301            println!("{}", arg);
302        }
303
304        debug_log!("INFO: printing cargo args ({:?})", cargo_args);
305
306        let encoded_cargo_args = cargo_args.join("\x1E");
307        let encoded_cargo_args = encoded_cargo_args.replace('\n', "\x1F");
308
309        println!("cargo:encoded_cargo_args={}", encoded_cargo_args);
310    }
311
312    /// Adds items to the rb_config based on a string from LDFLAGS/DLDFLAGS
313    pub fn push_dldflags(&mut self, input: &str) -> &mut Self {
314        let input = self.subst_shell_variables(input);
315        let split_args = Flags::new(input.as_str());
316
317        let search_path_regex = Regex::new(r"^-L\s*(?P<name>.*)$").unwrap();
318        let lib_regex_short = Regex::new(r"^-l\s*(?P<name>\w+\S+)$").unwrap();
319        let lib_regex_long = Regex::new(r"^--library=(?P<name>\w+\S+)$").unwrap();
320        let dynamic_lib_regex = Regex::new(r"^-l\s*:lib(?P<name>\S+).(so|dylib|dll)$").unwrap();
321        let framework_regex_short = Regex::new(r"^-F\s*(?P<name>.*)$").unwrap();
322        let framework_regex_long = Regex::new(r"^-framework\s*(?P<name>.*)$").unwrap();
323
324        for arg in split_args {
325            let arg = arg.trim().to_owned();
326
327            if let Some(name) = capture_name(&search_path_regex, &arg) {
328                self.push_search_path(name.as_str());
329            } else if let Some(name) = capture_name(&lib_regex_long, &arg) {
330                self.push_library(name);
331            } else if let Some(name) = capture_name(&lib_regex_short, &arg) {
332                if name.contains("ruby") && name.contains("-static") {
333                    self.push_library((LibraryKind::Static, name));
334                } else {
335                    self.push_library(name);
336                }
337            } else if let Some(name) = capture_name(&dynamic_lib_regex, &arg) {
338                self.push_library((LibraryKind::Dylib, name));
339            } else if let Some(name) = capture_name(&framework_regex_short, &arg) {
340                self.push_search_path((SearchPathKind::Framework, name));
341            } else if let Some(name) = capture_name(&framework_regex_long, &arg) {
342                self.push_library((LibraryKind::Framework, name));
343            } else {
344                self.push_link_arg(arg);
345            }
346        }
347
348        self
349    }
350
351    /// Sets a value for a key
352    pub fn set_value_for_key(&mut self, key: &str, value: String) {
353        self.value_map.insert(key.to_owned(), value);
354    }
355
356    // Check if has ABI version
357    pub fn has_ruby_dln_check_abi(&self) -> bool {
358        let Some((major, minor)) = self.major_minor() else {
359            return false;
360        };
361
362        let patchlevel = self
363            .get("PATCHLEVEL")
364            .and_then(|v| v.parse::<i32>().ok())
365            .unwrap_or(-1);
366
367        // Ruby has ABI version on version 3.2 and later only on development
368        // versions
369        (major > 3 || (major == 3 && minor >= 2))
370            && patchlevel == -1
371            && !cfg!(target_family = "windows")
372    }
373
374    /// The RUBY_ENGINE we are building for
375    pub fn ruby_engine(&self) -> RubyEngine {
376        if let Some(engine) = self.get("ruby_install_name") {
377            match engine.as_str() {
378                "ruby" => RubyEngine::Mri,
379                "jruby" => RubyEngine::JRuby,
380                "truffleruby" => RubyEngine::TruffleRuby,
381                _ => RubyEngine::Mri, // not sure how stable this is, so default to MRI to avoid breaking things
382            }
383        } else {
384            RubyEngine::Mri
385        }
386    }
387
388    // Examines the string from shell variables and expands them with values in the value_map
389    fn subst_shell_variables(&self, input: &str) -> String {
390        let mut result = String::new();
391        let mut chars = input.chars().enumerate();
392
393        while let Some((_, c)) = chars.next() {
394            if c == '$' {
395                if let Some((i, c)) = chars.next() {
396                    if c == '(' {
397                        let start = i + 1;
398                        let mut end = start;
399
400                        for (i, c) in chars.by_ref() {
401                            if c == ')' {
402                                end = i;
403                                break;
404                            }
405                        }
406
407                        let key = &input[start..end];
408
409                        if let Some(val) = self.get(key) {
410                            result.push_str(&val);
411                        } else if let Some(val) = env::var_os(key) {
412                            result.push_str(&val.to_string_lossy());
413                        } else {
414                            // Consume whitespace
415                            chars.next();
416                        }
417                    } else {
418                        result.push(c);
419                    }
420                }
421            } else {
422                result.push(c);
423            }
424        }
425
426        result
427    }
428
429    pub fn have_ruby_header<T: AsRef<str>>(&self, header: T) -> bool {
430        let Some(ruby_include_dir) = self.get("rubyhdrdir") else {
431            return false;
432        };
433        PathBuf::from(ruby_include_dir)
434            .join(header.as_ref())
435            .exists()
436    }
437
438    fn push_search_path<T: Into<SearchPath>>(&mut self, path: T) -> &mut Self {
439        let path = path.into();
440
441        if !self.search_paths.contains(&path) {
442            self.search_paths.push(path);
443        }
444
445        self
446    }
447
448    fn push_library<T: Into<Library>>(&mut self, lib: T) -> &mut Self {
449        let lib = lib.into();
450
451        if !self.libs.contains(&lib) {
452            self.libs.push(lib);
453        }
454
455        self
456    }
457
458    fn push_link_arg<T: Into<String>>(&mut self, arg: T) -> &mut Self {
459        let arg = arg.into();
460
461        if !self.link_args.contains(&arg) {
462            self.link_args.push(arg);
463        }
464
465        self
466    }
467
468    fn try_value_map(&self, key: &str) -> Option<String> {
469        self.value_map
470            .get(key)
471            .map(|val| val.trim_matches('\n').to_owned())
472    }
473
474    fn try_rbconfig_env(&self, key: &str) -> Option<String> {
475        let key = format!("RBCONFIG_{}", key);
476        println!("cargo:rerun-if-env-changed={}", key);
477        env::var(key).map(|v| v.trim_matches('\n').to_owned()).ok()
478    }
479}
480
481#[derive(Debug, PartialEq, Eq, Clone, Copy)]
482pub enum RubyEngine {
483    Mri,
484    TruffleRuby,
485    JRuby,
486}
487
488impl std::fmt::Display for RubyEngine {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        match self {
491            RubyEngine::Mri => write!(f, "mri"),
492            RubyEngine::TruffleRuby => write!(f, "truffleruby"),
493            RubyEngine::JRuby => write!(f, "jruby"),
494        }
495    }
496}
497
498fn capture_name(regex: &Regex, arg: &str) -> Option<String> {
499    regex
500        .captures(arg)
501        .map(|cap| cap.name("name").unwrap().as_str().trim().to_owned())
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use std::{sync::Mutex, vec};
508
509    lazy_static::lazy_static! {
510        static ref ENV_LOCK: Mutex<()> = Mutex::new(());
511    }
512
513    fn with_locked_env<F, T>(f: F) -> T
514    where
515        F: FnOnce() -> T,
516    {
517        let _guard = ENV_LOCK.lock().unwrap();
518        f()
519    }
520
521    #[test]
522    fn test_extract_lib_search_paths() {
523        let mut rb_config = RbConfig::new();
524        rb_config.push_dldflags("-L/usr/local/lib -L/usr/lib");
525        assert_eq!(
526            rb_config.search_paths,
527            vec!["/usr/local/lib".into(), "/usr/lib".into()]
528        );
529    }
530
531    #[test]
532    fn test_search_path_basic() {
533        let mut rb_config = RbConfig::new();
534        rb_config.push_dldflags("-L/usr/local/lib");
535
536        assert_eq!(rb_config.search_paths, vec!["native=/usr/local/lib".into()]);
537    }
538
539    #[test]
540    fn test_search_path_space() {
541        let mut rb_config = RbConfig::new();
542        rb_config.push_dldflags("-L /usr/local/lib");
543
544        assert_eq!(rb_config.search_paths, vec!["/usr/local/lib".into()]);
545    }
546
547    #[test]
548    fn test_search_path_space_in_path() {
549        let mut rb_config = RbConfig::new();
550        rb_config.push_dldflags("-L/usr/local/my lib");
551
552        assert_eq!(
553            rb_config.search_paths,
554            vec!["native=/usr/local/my lib".into()]
555        );
556    }
557
558    #[test]
559    fn test_simple_lib() {
560        let mut rb_config = RbConfig::new();
561        rb_config.push_dldflags("-lfoo");
562
563        assert_eq!(rb_config.libs, ["foo".into()]);
564    }
565
566    #[test]
567    fn test_lib_with_nonascii() {
568        let mut rb_config = RbConfig::new();
569        rb_config.push_dldflags("-lws2_32");
570
571        assert_eq!(rb_config.libs, ["ws2_32".into()]);
572    }
573
574    #[test]
575    fn test_simple_lib_space() {
576        let mut rb_config = RbConfig::new();
577        rb_config.push_dldflags("-l foo");
578
579        assert_eq!(rb_config.libs, ["foo".into()]);
580    }
581
582    #[test]
583    fn test_verbose_lib_space() {
584        let mut rb_config = RbConfig::new();
585        rb_config.push_dldflags("--library=foo");
586
587        assert_eq!(rb_config.libs, ["foo".into()]);
588    }
589
590    #[test]
591    fn test_dylib_with_colon_space() {
592        let mut rb_config = RbConfig::new();
593        rb_config.push_dldflags("-l :libssp.dylib");
594
595        assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
596    }
597
598    #[test]
599    fn test_so_with_colon_space() {
600        let mut rb_config = RbConfig::new();
601        rb_config.push_dldflags("-l :libssp.so");
602
603        assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
604    }
605
606    #[test]
607    fn test_dll_with_colon_space() {
608        let mut rb_config = RbConfig::new();
609        rb_config.push_dldflags("-l :libssp.dll");
610
611        assert_eq!(rb_config.libs, ["dylib=ssp".into()]);
612    }
613
614    #[test]
615    fn test_framework() {
616        let mut rb_config = RbConfig::new();
617        rb_config.push_dldflags("-F/some/path");
618
619        assert_eq!(rb_config.search_paths, ["framework=/some/path".into()]);
620    }
621
622    #[test]
623    fn test_framework_space() {
624        let mut rb_config = RbConfig::new();
625        rb_config.push_dldflags("-F /some/path");
626
627        assert_eq!(
628            rb_config.search_paths,
629            [SearchPath {
630                kind: SearchPathKind::Framework,
631                name: "/some/path".into(),
632            }]
633        );
634    }
635
636    #[test]
637    fn test_framework_arg_real() {
638        let mut rb_config = RbConfig::new();
639        rb_config.push_dldflags("-framework CoreFoundation");
640
641        assert_eq!(
642            rb_config.libs,
643            [Library {
644                kind: LibraryKind::Framework,
645                name: "CoreFoundation".into(),
646            }]
647        );
648    }
649
650    #[test]
651    fn test_libruby_static() {
652        let mut rb_config = RbConfig::new();
653        rb_config.push_dldflags("-lruby.3.1-static");
654
655        assert_eq!(
656            rb_config.cargo_args(),
657            ["cargo:rustc-link-lib=static=ruby.3.1-static"]
658        );
659    }
660
661    #[test]
662    fn test_libruby_dynamic() {
663        let mut rb_config = RbConfig::new();
664        rb_config.push_dldflags("-lruby.3.1");
665
666        assert_eq!(rb_config.cargo_args(), ["cargo:rustc-link-lib=ruby.3.1"]);
667    }
668
669    #[test]
670    fn test_non_lib_dash_l() {
671        let mut rb_config = RbConfig::new();
672        rb_config.push_dldflags("test_rubygems_20220413-976-lemgf9/prefix");
673
674        assert_eq!(
675            rb_config.link_args,
676            vec!["test_rubygems_20220413-976-lemgf9/prefix"]
677        );
678    }
679
680    #[test]
681    fn test_real_dldflags() {
682        let mut rb_config = RbConfig::new();
683        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");
684
685        assert_eq!(
686            rb_config.link_args,
687            vec![
688                "-Wl,-undefined,dynamic_lookup",
689                "-Wl,-multiply_defined,suppress"
690            ]
691        );
692        assert_eq!(
693            rb_config.search_paths,
694            vec![
695                SearchPath {
696                    kind: SearchPathKind::Native,
697                    name: "/Users/ianks/.asdf/installs/ruby/3.1.1/lib".to_string()
698                },
699                SearchPath {
700                    kind: SearchPathKind::Native,
701                    name: "/opt/homebrew/opt/openssl@1.1/lib".to_string()
702                },
703            ]
704        );
705    }
706
707    #[test]
708    fn test_crazy_cases() {
709        let mut rb_config = RbConfig::new();
710        rb_config.push_dldflags("-F   /something -l:libssp.a -static-libgcc ");
711
712        assert_eq!(rb_config.link_args, vec!["-l:libssp.a", "-static-libgcc"]);
713        assert_eq!(
714            rb_config.search_paths,
715            vec![SearchPath {
716                kind: SearchPathKind::Framework,
717                name: "/something".to_string()
718            },]
719        );
720    }
721
722    #[test]
723    fn test_printing_cargo_args() {
724        let mut rb_config = RbConfig::new();
725        rb_config.push_dldflags("-L/Users/ianks/.asdf/installs/ruby/3.1.1/lib");
726        rb_config.push_dldflags("-lfoo");
727        rb_config.push_dldflags("-static-libgcc");
728        let result = rb_config.cargo_args();
729
730        assert_eq!(
731            vec![
732                "cargo:rustc-link-search=native=/Users/ianks/.asdf/installs/ruby/3.1.1/lib",
733                "cargo:rustc-link-lib=foo",
734                "cargo:rustc-link-arg=-static-libgcc"
735            ],
736            result
737        );
738    }
739
740    #[test]
741    fn test_use_rpath() {
742        let mut rb_config = RbConfig::new();
743        rb_config.push_dldflags("-lfoo");
744
745        assert_eq!(vec!["cargo:rustc-link-lib=foo"], rb_config.cargo_args());
746
747        rb_config.use_rpath();
748
749        assert_eq!(
750            vec![
751                "cargo:rustc-link-lib=foo",
752                "cargo:rustc-link-arg=-Wl,-rpath,foo"
753            ],
754            rb_config.cargo_args()
755        );
756    }
757
758    #[test]
759    fn test_link_mswin() {
760        with_locked_env(|| {
761            let old_var = env::var("TARGET").ok();
762            env::set_var("TARGET", "x86_64-pc-windows-msvc");
763
764            let mut rb_config = RbConfig::new();
765            rb_config.set_value_for_key("LIBRUBYARG_SHARED", "x64-vcruntime140-ruby320.lib".into());
766            rb_config.set_value_for_key("libdir", "D:/ruby-mswin/lib".into());
767            rb_config.set_value_for_key("LIBS", "user32.lib".into());
768            rb_config.link_ruby(false);
769
770            assert_eq!(
771                vec![
772                    "cargo:rustc-link-search=native=D:/ruby-mswin/lib",
773                    "cargo:rustc-link-lib=x64-vcruntime140-ruby320",
774                    "cargo:rustc-link-lib=user32",
775                ],
776                rb_config.cargo_args()
777            );
778
779            if let Some(old_var) = old_var {
780                env::set_var("TARGET", old_var);
781            } else {
782                env::remove_var("TARGET");
783            }
784        })
785    }
786
787    #[test]
788    fn test_link_static() {
789        with_locked_env(|| {
790            let mut rb_config = RbConfig::new();
791            rb_config.set_value_for_key("LIBRUBYARG_STATIC", "-lruby-static".into());
792            rb_config.set_value_for_key("libdir", "/opt/ruby".into());
793
794            rb_config.link_ruby(true);
795
796            assert_eq!(
797                vec![
798                    "cargo:rustc-link-search=native=/opt/ruby",
799                    "cargo:rustc-link-lib=static=ruby-static",
800                ],
801                rb_config.cargo_args()
802            );
803        });
804    }
805
806    #[test]
807    fn test_prioritizes_rbconfig_env() {
808        with_locked_env(|| {
809            env::set_var("RBCONFIG_libdir", "/foo");
810            let rb_config = RbConfig::new();
811
812            assert_eq!(rb_config.get("libdir"), Some("/foo".into()));
813
814            env::remove_var("RBCONFIG_libdir");
815        });
816    }
817
818    #[test]
819    fn test_never_loads_shell_rbconfig_if_cross_compiling() {
820        with_locked_env(|| {
821            env::set_var("RBCONFIG_CROSS_COMPILING", "yes");
822
823            let rb_config = RbConfig::current();
824
825            assert!(rb_config.value_map.is_empty());
826        });
827    }
828
829    #[test]
830    fn test_loads_shell_rbconfig_if_not_cross_compiling() {
831        with_locked_env(|| {
832            env::set_var("RBCONFIG_CROSS_COMPILING", "no");
833
834            let rb_config = RbConfig::current();
835
836            assert!(!rb_config.value_map.is_empty());
837        });
838    }
839
840    #[test]
841    fn test_libstatic() {
842        let mut rb_config = RbConfig::new();
843        rb_config.push_dldflags("-l:libssp.a");
844
845        assert_eq!(rb_config.link_args, ["-l:libssp.a".to_string()]);
846    }
847
848    #[test]
849    fn test_link_arg_blocklist() {
850        let mut rb_config = RbConfig::new();
851        rb_config.blocklist_link_arg("-Wl,--compress-debug-sections=zlib");
852        rb_config.blocklist_link_arg("-s");
853        rb_config.push_dldflags(
854            "-lfoo -Wl,--compress-debug-sections=zlib -s -somethingthatshouldnotbeblocked",
855        );
856
857        assert_eq!(
858            vec![
859                "cargo:rustc-link-lib=foo",
860                "cargo:rustc-link-arg=-somethingthatshouldnotbeblocked"
861            ],
862            rb_config.cargo_args()
863        );
864    }
865
866    #[test]
867    fn test_has_ruby_dln_check_abi() {
868        // Helper to create RbConfig with specific version
869        fn make_config(major: &str, minor: &str, patchlevel: &str) -> RbConfig {
870            let mut rb_config = RbConfig::new();
871            rb_config.set_value_for_key("MAJOR", major.into());
872            rb_config.set_value_for_key("MINOR", minor.into());
873            rb_config.set_value_for_key("PATCHLEVEL", patchlevel.into());
874            rb_config
875        }
876
877        // Ruby 3.1.x (any patchlevel) - too old
878        assert!(!make_config("3", "1", "-1").has_ruby_dln_check_abi());
879        assert!(!make_config("3", "1", "0").has_ruby_dln_check_abi());
880
881        // Ruby 3.2.0-dev (patchlevel -1) - should have ABI check
882        #[cfg(not(target_family = "windows"))]
883        assert!(make_config("3", "2", "-1").has_ruby_dln_check_abi());
884
885        // Ruby 3.2.0 release (patchlevel 0) - no ABI check
886        assert!(!make_config("3", "2", "0").has_ruby_dln_check_abi());
887
888        // Ruby 3.3.0-dev - should have ABI check
889        #[cfg(not(target_family = "windows"))]
890        assert!(make_config("3", "3", "-1").has_ruby_dln_check_abi());
891
892        // Ruby 4.0.0-dev - should have ABI check (this was the bug!)
893        #[cfg(not(target_family = "windows"))]
894        assert!(make_config("4", "0", "-1").has_ruby_dln_check_abi());
895
896        // Ruby 4.0.0 release - no ABI check
897        assert!(!make_config("4", "0", "0").has_ruby_dln_check_abi());
898
899        // Ruby 4.1.0-dev - should have ABI check
900        #[cfg(not(target_family = "windows"))]
901        assert!(make_config("4", "1", "-1").has_ruby_dln_check_abi());
902
903        // Ruby 2.7.x - too old
904        assert!(!make_config("2", "7", "-1").has_ruby_dln_check_abi());
905    }
906}