Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ pub static __MISE_DIFF: Lazy<EnvDiff> = Lazy::new(get_env_diff);
pub static __MISE_ORIG_PATH: Lazy<Option<String>> = Lazy::new(|| var("__MISE_ORIG_PATH").ok());
pub static __MISE_ZSH_PRECMD_RUN: Lazy<bool> = Lazy::new(|| !var_is_false("__MISE_ZSH_PRECMD_RUN"));
pub static LINUX_DISTRO: Lazy<Option<String>> = Lazy::new(linux_distro);
/// Detected glibc version on Linux as (major, minor), e.g. (2, 17).
/// Returns None on non-Linux or if detection fails.
pub static LINUX_GLIBC_VERSION: Lazy<Option<(u32, u32)>> = Lazy::new(linux_glibc_version);
pub static PREFER_OFFLINE: Lazy<AtomicBool> =
Lazy::new(|| prefer_offline(&ARGS.read().unwrap()).into());
pub static OFFLINE: Lazy<bool> = Lazy::new(|| offline(&ARGS.read().unwrap()));
Expand Down Expand Up @@ -652,6 +655,33 @@ fn linux_distro() -> Option<String> {
}
}

#[cfg(target_os = "linux")]
fn linux_glibc_version() -> Option<(u32, u32)> {
let output = std::process::Command::new("ldd")
.arg("--version")
.output()
.ok()?;
// ldd --version prints to stdout on glibc, stderr on some systems
let text = String::from_utf8_lossy(&output.stdout);
let text = if text.is_empty() {
String::from_utf8_lossy(&output.stderr)
} else {
text
};
let first_line = text.lines().next()?;
let version_str = first_line.rsplit(' ').next()?;
let mut parts = version_str.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
debug!("detected glibc version: {}.{}", major, minor);
Some((major, minor))
}

#[cfg(not(target_os = "linux"))]
fn linux_glibc_version() -> Option<(u32, u32)> {
None
}

fn filename(path: &str) -> &str {
path.rsplit_once(path::MAIN_SEPARATOR_STR)
.map(|(_, file)| file)
Expand Down
65 changes: 53 additions & 12 deletions src/plugins/core/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,31 +466,66 @@ impl RubyPlugin {
.replace("{arch}", arch)
}

/// Find precompiled asset from a GitHub repo's releases
/// Check if the system needs the no-YJIT variant (glibc < 2.35 on Linux).
/// YJIT builds from jdx/ruby require glibc 2.35+.
fn needs_no_yjit() -> bool {
match *crate::env::LINUX_GLIBC_VERSION {
Some((major, minor)) => major < 2 || (major == 2 && minor < 35),
None => false, // non-Linux or can't detect, assume modern system
}
}

/// Find precompiled asset from a GitHub repo's releases.
/// On Linux with glibc < 2.35, prefers the no-YJIT variant (.no_yjit.) which
/// targets glibc 2.17. Falls back to the standard build if no variant is found.
async fn find_precompiled_asset_in_repo(
&self,
repo: &str,
version: &str,
platform: &str,
prefer_no_yjit: bool,
) -> Result<Option<(String, Option<String>)>> {
let releases = github::list_releases(repo).await?;
let expected_name = format!("ruby-{}.{}.tar.gz", version, platform);
let standard_name = format!("ruby-{}.{}.tar.gz", version, platform);
let no_yjit_name = format!("ruby-{}.{}.no_yjit.tar.gz", version, platform);

for release in releases {
for asset in release.assets {
if asset.name == expected_name {
return Ok(Some((asset.browser_download_url, asset.digest)));
if prefer_no_yjit {
debug!("glibc < 2.35 detected, preferring no-YJIT Ruby variant");
}

let mut standard_asset = None;
let mut no_yjit_asset = None;

for release in &releases {
for asset in &release.assets {
if no_yjit_asset.is_none() && asset.name == no_yjit_name {
no_yjit_asset =
Some((asset.browser_download_url.clone(), asset.digest.clone()));
} else if standard_asset.is_none() && asset.name == standard_name {
standard_asset =
Some((asset.browser_download_url.clone(), asset.digest.clone()));
}
}
if no_yjit_asset.is_some() && standard_asset.is_some() {
break;
}
}
Comment on lines +499 to 512
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop can be slightly optimized. By using if/else if, we avoid a second string comparison for each asset. Also, we can break out of the loop once both no_yjit_asset and standard_asset have been found, which avoids iterating through all remaining releases unnecessarily.

        for release in &releases {
            for asset in &release.assets {
                if no_yjit_asset.is_none() && asset.name == no_yjit_name {
                    no_yjit_asset =
                        Some((asset.browser_download_url.clone(), asset.digest.clone()));
                } else if standard_asset.is_none() && asset.name == standard_name {
                    standard_asset =
                        Some((asset.browser_download_url.clone(), asset.digest.clone()));
                }
            }
            if no_yjit_asset.is_some() && standard_asset.is_some() {
                break;
            }
        }

Ok(None)

if prefer_no_yjit {
if no_yjit_asset.is_some() {
return Ok(no_yjit_asset);
}
debug!("no-YJIT variant not found, falling back to standard build");
}
Ok(standard_asset)
}

/// Resolve precompiled binary URL and checksum for a given version and platform
async fn resolve_precompiled_url(
&self,
version: &str,
platform: &str,
prefer_no_yjit: bool,
) -> Result<Option<(String, Option<String>)>> {
let settings = Settings::get();
let source = &settings.ruby.precompiled_url;
Expand All @@ -503,7 +538,7 @@ impl RubyPlugin {
)))
} else {
// GitHub repo shorthand (default: "jdx/ruby")
self.find_precompiled_asset_in_repo(source, version, platform)
self.find_precompiled_asset_in_repo(source, version, platform, prefer_no_yjit)
.await
}
}
Expand Down Expand Up @@ -557,12 +592,17 @@ impl RubyPlugin {
return Ok(None);
};

let Some((url, checksum)) = self.resolve_precompiled_url(&tv.version, &platform).await?
let Some((url, checksum)) = self
.resolve_precompiled_url(&tv.version, &platform, Self::needs_no_yjit())
.await?
else {
return Ok(None);
};

let filename = format!("ruby-{}.{}.tar.gz", tv.version, platform);
let filename = match url.rsplit('/').next() {
Some(name) if !name.is_empty() => name.to_string(),
_ => format!("ruby-{}.{}.tar.gz", tv.version, platform),
};
let tarball_path = tv.download_path().join(&filename);

ctx.pr.set_message(format!("download {}", filename));
Expand Down Expand Up @@ -845,8 +885,9 @@ impl Backend for RubyPlugin {
// Precompiled binary info if enabled
if self.should_try_precompiled()
&& let Some(platform) = self.precompiled_platform_for_target(target)
&& let Some((url, checksum)) =
self.resolve_precompiled_url(&tv.version, &platform).await?
&& let Some((url, checksum)) = self
.resolve_precompiled_url(&tv.version, &platform, false)
.await?
{
return Ok(PlatformInfo {
url: Some(url),
Expand Down
Loading