Skip to main content

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                    .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    /// Pushes the `LIBRUBYARG` flags so Ruby will be linked.
111    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    /// Get the name for libruby-static (i.e. `ruby.3.1-static`).
163    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    /// Get the name for libruby (i.e. `ruby.3.1`)
174    pub fn libruby_so_name(&self) -> String {
175        self.get("RUBY_SO_NAME")
176            .unwrap_or_else(|| "ruby".to_string())
177    }
178
179    /// Get the platform for the current ruby.
180    pub fn platform(&self) -> String {
181        self.get("platform")
182            .unwrap_or_else(|| self.get("arch").expect("arch not found"))
183    }
184
185    /// Filter the libs, removing the ones that are not needed.
186    pub fn blocklist_lib(&mut self, name: &str) -> &mut RbConfig {
187        self.blocklist_lib.push(name.to_string());
188        self
189    }
190
191    /// Blocklist a link argument.
192    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    /// Returns the current ruby program version.
198    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    /// Get the CPPFLAGS from the RbConfig, making sure to subsitute variables.
218    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    /// Returns true if the current Ruby is cross compiling.
228    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    /// Checks that Ruby's C headers use the same operating-system ABI as Cargo's target.
237    ///
238    /// Bindgen can locate headers from a cross-compilation sysroot, but headers from a Ruby
239    /// built for a different target are not compatible. In particular, RubyInstaller uses
240    /// MinGW headers which cannot be used with Rust's MSVC target.
241    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    /// Returns the value of the given key from the either the matching
253    /// `RBCONFIG_{key}` environment variable or `RbConfig::CONFIG[{key}]` hash.
254    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    /// Enables the use of rpath for linking.
260    pub fn use_rpath(&mut self) -> &mut RbConfig {
261        self.use_rpath = true;
262        self
263    }
264
265    /// Push cflags string
266    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    /// Get major/minor version tuple of Ruby
277    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    /// Get the rb_config output for cargo
284    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    /// Print to rb_config output for cargo
314    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    /// Adds items to the rb_config based on a string from LDFLAGS/DLDFLAGS
330    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    /// Sets a value for a key
369    pub fn set_value_for_key(&mut self, key: &str, value: String) {
370        self.value_map.insert(key.to_owned(), value);
371    }
372
373    // Check if has ABI version
374    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        // Ruby has ABI version on version 3.2 and later only on development
385        // versions
386        (major > 3 || (major == 3 && minor >= 2))
387            && patchlevel == -1
388            && !cfg!(target_family = "windows")
389    }
390
391    /// The RUBY_ENGINE we are building for
392    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, // not sure how stable this is, so default to MRI to avoid breaking things
399            }
400        } else {
401            RubyEngine::Mri
402        }
403    }
404
405    // Examines the string from shell variables and expands them with values in the value_map
406    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                            // Consume whitespace
432                            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        // Helper to create RbConfig with specific version
1023        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        // Ruby 3.1.x (any patchlevel) - too old
1032        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        // Ruby 3.2.0-dev (patchlevel -1) - should have ABI check
1036        #[cfg(not(target_family = "windows"))]
1037        assert!(make_config("3", "2", "-1").has_ruby_dln_check_abi());
1038
1039        // Ruby 3.2.0 release (patchlevel 0) - no ABI check
1040        assert!(!make_config("3", "2", "0").has_ruby_dln_check_abi());
1041
1042        // Ruby 3.3.0-dev - should have ABI check
1043        #[cfg(not(target_family = "windows"))]
1044        assert!(make_config("3", "3", "-1").has_ruby_dln_check_abi());
1045
1046        // Ruby 4.0.0-dev - should have ABI check (this was the bug!)
1047        #[cfg(not(target_family = "windows"))]
1048        assert!(make_config("4", "0", "-1").has_ruby_dln_check_abi());
1049
1050        // Ruby 4.0.0 release - no ABI check
1051        assert!(!make_config("4", "0", "0").has_ruby_dln_check_abi());
1052
1053        // Ruby 4.1.0-dev - should have ABI check
1054        #[cfg(not(target_family = "windows"))]
1055        assert!(make_config("4", "1", "-1").has_ruby_dln_check_abi());
1056
1057        // Ruby 2.7.x - too old
1058        assert!(!make_config("2", "7", "-1").has_ruby_dln_check_abi());
1059    }
1060}