rb-sys 0.9.127

Rust bindings for the CRuby API
Documentation
use rb_sys_build::{RbConfig, RubyEngine};

use crate::{
    features::is_env_variable_defined,
    version::{Version, MIN_SUPPORTED_STABLE_VERSION},
};
use std::{
    convert::TryFrom,
    error::Error,
    path::{Path, PathBuf},
};

/// Get the path to the Rust implementation file for the given Ruby version.
fn rust_impl_path(version: Version) -> PathBuf {
    let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    crate_dir.join("src").join("stable_api").join(format!(
        "ruby_{}_{}.rs",
        version.major(),
        version.minor()
    ))
}

/// Check if a Rust implementation file exists for the given Ruby version.
fn has_rust_impl(version: Version) -> bool {
    let path = rust_impl_path(version);
    path.exists()
}

pub fn setup(rb_config: &RbConfig) -> Result<(), Box<dyn Error>> {
    let ruby_version = Version::current(rb_config);
    let ruby_engine = rb_config.ruby_engine();

    // Ensure we rebuild if the file is added or removed
    let rust_impl_path = rust_impl_path(ruby_version);
    println!("cargo:rerun-if-changed={}", rust_impl_path.display());
    let strategy = Strategy::try_from((ruby_engine, ruby_version))?;

    strategy.apply()?;

    Ok(())
}

#[derive(Debug)]
enum Strategy {
    RustOnly(Version),
    CompiledOnly,
    RustThenCompiled(Version),
    Testing(Version),
}

impl TryFrom<(RubyEngine, Version)> for Strategy {
    type Error = Box<dyn Error>;

    fn try_from(
        (engine, current_ruby_version): (RubyEngine, Version),
    ) -> Result<Self, Self::Error> {
        let mut strategy = None;

        match engine {
            RubyEngine::TruffleRuby => {
                return Ok(Strategy::CompiledOnly);
            }
            RubyEngine::JRuby => {
                return Err("JRuby is not supported".into());
            }
            RubyEngine::Mri => {}
        }

        if has_rust_impl(current_ruby_version) {
            strategy = Some(Strategy::RustOnly(current_ruby_version));
        } else {
            maybe_warn_old_ruby_version(current_ruby_version);
        }

        if is_fallback_enabled() {
            strategy = Some(Strategy::RustThenCompiled(current_ruby_version));
        }

        if is_testing() {
            strategy = Some(Strategy::Testing(current_ruby_version));
        }

        if is_force_enabled() {
            strategy = Some(Strategy::CompiledOnly);
        }

        if let Some(strategy) = strategy {
            return Ok(strategy);
        }

        Err("Stable API is needed but could not find a candidate. Try enabling the `stable-api-compiled-fallback` feature in rb-sys.".into())
    }
}

impl Strategy {
    fn apply(self) -> Result<(), Box<dyn Error>> {
        println!("cargo:rustc-check-cfg=cfg(stable_api_include_rust_impl)");
        println!("cargo:rustc-check-cfg=cfg(stable_api_enable_compiled_mod)");
        println!("cargo:rustc-check-cfg=cfg(stable_api_export_compiled_as_api)");
        println!("cargo:rustc-check-cfg=cfg(stable_api_has_rust_impl)");
        match self {
            Strategy::RustOnly(current_ruby_version) => {
                if has_rust_impl(current_ruby_version) {
                    println!("cargo:rustc-cfg=stable_api_include_rust_impl");
                } else {
                    return Err(format!("No Rust stable API implementation found for Ruby {}. If you are using a stable version of Ruby, try upgrading rb-sys. Otherwise if you are testing against ruby-head or Ruby < {}, enable the `stable-api-compiled-fallback` feature in rb-sys.", current_ruby_version, MIN_SUPPORTED_STABLE_VERSION).into());
                }
            }
            Strategy::CompiledOnly => {
                compile()?;
                println!("cargo:rustc-cfg=stable_api_enable_compiled_mod");
                println!("cargo:rustc-cfg=stable_api_export_compiled_as_api");
            }
            Strategy::RustThenCompiled(current_ruby_version) => {
                if has_rust_impl(current_ruby_version) {
                    println!("cargo:rustc-cfg=stable_api_has_rust_impl");
                    println!("cargo:rustc-cfg=stable_api_include_rust_impl");
                } else {
                    compile()?;
                    println!("cargo:rustc-cfg=stable_api_enable_compiled_mod");
                    println!("cargo:rustc-cfg=stable_api_export_compiled_as_api");
                }
            }
            Strategy::Testing(current_ruby_version) => {
                compile()?;

                println!("cargo:rustc-cfg=stable_api_enable_compiled_mod");

                if has_rust_impl(current_ruby_version) {
                    println!("cargo:rustc-cfg=stable_api_include_rust_impl");
                } else {
                    println!("cargo:rustc-cfg=stable_api_export_compiled_as_api");
                }
            }
        };

        Ok(())
    }
}

fn is_fallback_enabled() -> bool {
    println!("cargo:rerun-if-env-changed=RB_SYS_STABLE_API_COMPILED_FALLBACK");

    is_env_variable_defined("CARGO_FEATURE_STABLE_API_COMPILED_FALLBACK")
        || cfg!(rb_sys_use_stable_api_compiled_fallback)
        || is_env_variable_defined("RB_SYS_STABLE_API_COMPILED_FALLBACK")
}

fn is_force_enabled() -> bool {
    println!("cargo:rerun-if-env-changed=RB_SYS_STABLE_API_COMPILED_FORCE");

    is_env_variable_defined("CARGO_FEATURE_STABLE_API_COMPILED_FORCE")
        || cfg!(rb_sys_force_stable_api_compiled)
        || is_env_variable_defined("RB_SYS_STABLE_API_COMPILED_FORCE")
}

fn is_testing() -> bool {
    is_env_variable_defined("CARGO_FEATURE_STABLE_API_COMPILED_TESTING")
}

fn maybe_warn_old_ruby_version(current_ruby_version: Version) {
    if current_ruby_version < MIN_SUPPORTED_STABLE_VERSION {
        println!(
            "cargo:warning=Support for Ruby {} will be removed in a future release.",
            current_ruby_version
        );
    }
}

fn compile() -> Result<(), Box<dyn Error>> {
    eprintln!("INFO: Compiling the stable API compiled module");
    let mut build = rb_sys_build::cc::Build::new();
    let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let path = crate_dir.join("src").join("stable_api").join("compiled.c");
    eprintln!("cargo:rerun-if-changed={}", path.display());

    build.file(path);
    build.try_compile("compiled")
}