Skip to content

Add automatic Ruby download support using rv binaries#1668

Open
is-alnilam wants to merge 8 commits intoj178:masterfrom
is-alnilam:ruby_download
Open

Add automatic Ruby download support using rv binaries#1668
is-alnilam wants to merge 8 commits intoj178:masterfrom
is-alnilam:ruby_download

Conversation

@is-alnilam
Copy link
Contributor

@is-alnilam is-alnilam commented Feb 19, 2026

Current Ruby support can only use Ruby interpreters which are already installed on the system, although it goes to great lengths to find interpreters installed by a variety of Ruby managers. This change adds support for installing new interpreters using the binaries delivered by the rv team. rv only provide installers for versions of Ruby still actively supported (so they don't offer version 3.1, for example), and only build for a subset of all Ruby-supported platforms. If users need an unsupported version of Ruby or wish to use an unsupported platform, they will be prompted to download and install a version of Ruby manually.

rv bundles are named according to the platform, currently including these components in the filename:

  • x86_64_linux
  • arm64_linux
  • x86_64_linux_musl
  • arm64_linux_musl
  • ventura (used for macOS on x86_64)
  • arm64_sonoma (used for macOS on 64-bit ARM)

If and when upstream rv changes these names, the detection code will need to be updated to match. In particular, this includes the use of macOS codenames, as if rv stop releasing a 'sonoma' package, this will block installing the macOS versions of Ruby. Currently rv seem to be attempting to keep these codenames, as they already rename their x86_64 builds from 'sequoia' (macOS 15) to 'ventura' (macOS 13). Adding a new CPU architecture (such as RISC-V) would also need changes, but wouldn't break existing platform support.

Ruby versions are found by querying the GitHub Releases API, searching the options returned for an installer that matches the platform and version requirements, then, if found, downloading and unpacking into the prek tools folder. The PREK_RUBY_MIRROR environment variable can be used to point to a different source for installers, for example to support mirrors or air-gapped CI environments. Mirrors need to follow the GitHub URL patterns, but note that although the GitHub hostname changes between api.github.com and github.com as needed, any non-GitHub mirror server will not be remapped in this manner. Where Ruby is being downloaded from GitHub (either from the upstream rv or a mirror), this remapping does occur, and any GITHUB_TOKEN will be sent with the requests. This both limits impact of rate limiting, and also allows a private GitHub repository to be used (e.g. for a vetted subset of rv rubies to be mirrored). Note that GitHub tokens will only be sent to mirrors which are hosted on GitHub.

To allow for passing the GITHUB_TOKEN in download requests, the generic download_and_extract function is now a wrapper over a version which takes an extension function, with the default function not extending the request. The Ruby code will add the GitHub token if the request is to GitHub.

Closes #43
Closes #765

Current Ruby support can only use Ruby interpreters which are already
installed on the system, although it goes to great lengths to find
interpreters installed by a variety of Ruby managers. This change adds
support for installing new interpreters using the binaries delivered by
the `rv` team. `rv` only provide installers for versions of Ruby still
actively supported (so they don't offer version 3.1, for example), and
only build for a subset of all Ruby-supported platforms. If users need
an unsupported version of Ruby or wish to use an unsupported platform,
they will be prompted to download and install a version of Ruby
manually.

`rv` bundles are named according to the platform, currently including
these components in the filename:
- x86_64_linux
- arm64_linux
- x86_64_linux_musl
- arm64_linux_musl
- ventura (used for macOS on x86_64)
- arm64_sonoma (used for macOS on 64-bit ARM)

If and when upstream `rv` changes these names, the detection code will
need to be updated to match. In particular, this includes the use of
macOS codenames, as if `rv` stop releasing a 'sonoma' package, this will
block installing the macOS versions of Ruby. Currently `rv` seem to be
attempting to keep these codenames, as they already rename their x86_64
builds from 'sequoia' (macOS 15) to 'ventura' (macOS 13). Adding a new
CPU architecture (such as RISC-V) would also need changes, but wouldn't
break existing platform support.

Ruby versions are found by querying the GitHub Releases API, searching
the options returned for an installer that matches the platform and
version requirements, then, if found, downloading and unpacking into the
`prek` tools folder. The `PREK_RUBY_MIRROR` environment variable can be
used to point to a different source for installers, for example to
support mirrors or air-gapped CI environments. Mirrors need to follow
the GitHub URL patterns, but note that although the GitHub hostname
changes between `api.github.com` and `github.com` as needed, any
non-GitHub mirror server will not be remapped in this manner. Where Ruby
is being downloaded from GitHub (either from the upstream `rv` or a
mirror), this remapping does occur, and any `GITHUB_TOKEN` will be sent
with the requests. This both limits impact of rate limiting, and also
allows a private GitHub repository to be used (e.g. for a vetted subset
of `rv` rubies to be mirrored). Note that GitHub tokens will only be
sent to mirrors which are hosted on GitHub.

To allow for passing the `GITHUB_TOKEN` in download requests, the
generic `download_and_extract` function is now a wrapper over a version
which takes an extension function, with the default function not
extending the request. The Ruby code will add the GitHub token if the
request is to GitHub.
@is-alnilam is-alnilam requested a review from j178 as a code owner February 19, 2026 13:19
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 91.51194% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.62%. Comparing base (85d6dc6) to head (019db85).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
crates/prek/src/languages/ruby/installer.rs 91.19% 31 Missing ⚠️
crates/prek/src/languages/ruby/version.rs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1668      +/-   ##
==========================================
+ Coverage   90.35%   91.62%   +1.27%     
==========================================
  Files          96       96              
  Lines       18661    19077     +416     
==========================================
+ Hits        16861    17480     +619     
+ Misses       1800     1597     -203     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@prek-ci-bot
Copy link

prek-ci-bot bot commented Feb 19, 2026

📦 Cargo Bloat Comparison

Binary size change: +0.42% (23.8 MiB → 23.9 MiB)

Expand for cargo-bloat output

Head Branch Results

 File  .text    Size             Crate Name
 0.3%   0.7% 70.9KiB              prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.3%   0.6% 65.9KiB             prek? <prek::cli::Command as clap_builder::derive::Subcommand>::augment_subcommands
 0.3%   0.6% 65.6KiB              prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.2%   0.5% 51.2KiB annotate_snippets annotate_snippets::renderer::render::render
 0.2%   0.5% 50.5KiB              prek prek::languages::<impl prek::config::Language>::install::{{closure}}
 0.2%   0.4% 41.2KiB              prek prek::cli::run::run::run::{{closure}}
 0.2%   0.4% 39.5KiB              prek prek::run::{{closure}}
 0.1%   0.3% 31.3KiB             prek? <prek::cli::RunArgs as clap_builder::derive::Args>::augment_args
 0.1%   0.3% 28.5KiB      serde_saphyr saphyr_parser_bw::scanner::Scanner<T>::fetch_more_tokens
 0.1%   0.2% 24.7KiB             prek? <prek::config::_::<impl serde_core::de::Deserialize for prek::config::Config>::deserialize::__Visitor as serde_core::de::Visitor>::visit_map
 0.1%   0.2% 22.8KiB      serde_saphyr saphyr_parser_bw::scanner::Scanner<T>::fetch_more_tokens
 0.1%   0.2% 22.7KiB              prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 21.6KiB               std core::ptr::drop_in_place<prek::languages::<impl prek::config::Language>::install::{{closure}}>
 0.1%   0.2% 21.4KiB              prek prek::languages::ruby::installer::RubyInstaller::install::{{closure}}
 0.1%   0.2% 21.1KiB      clap_builder clap_builder::parser::parser::Parser::get_matches_with
 0.1%   0.2% 20.5KiB              prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 20.0KiB   cargo_metadata? <cargo_metadata::_::<impl serde_core::de::Deserialize for cargo_metadata::Package>::deserialize::__Visitor as serde_core::de::Visitor>::visit_map
 0.1%   0.2% 19.5KiB              prek prek::archive::unzip::{{closure}}
 0.1%   0.2% 18.7KiB     serde_saphyr? <serde_saphyr::de::YamlDeserializer as serde_core::de::Deserializer>::deserialize_map
 0.1%   0.2% 18.6KiB     serde_saphyr? <serde_saphyr::de::YamlDeserializer as serde_core::de::Deserializer>::deserialize_map
38.4%  91.8%  9.2MiB                   And 21143 smaller methods. Use -n N to show more.
41.8% 100.0% 10.0MiB                   .text section size, the file size is 23.9MiB

Base Branch Results

 File  .text    Size             Crate Name
 0.3%   0.7% 71.4KiB              prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.3%   0.7% 70.6KiB             prek? <prek::cli::Command as clap_builder::derive::Subcommand>::augment_subcommands
 0.3%   0.6% 65.6KiB              prek prek::languages::<impl prek::config::Language>::run::{{closure}}::{{closure}}
 0.2%   0.5% 51.2KiB annotate_snippets annotate_snippets::renderer::render::render
 0.2%   0.5% 50.5KiB              prek prek::languages::<impl prek::config::Language>::install::{{closure}}
 0.2%   0.4% 41.1KiB              prek prek::cli::run::run::run::{{closure}}
 0.2%   0.4% 38.8KiB              prek prek::run::{{closure}}
 0.1%   0.3% 32.0KiB             prek? <prek::cli::RunArgs as clap_builder::derive::Args>::augment_args
 0.1%   0.3% 28.5KiB      serde_saphyr saphyr_parser_bw::scanner::Scanner<T>::fetch_more_tokens
 0.1%   0.2% 24.7KiB             prek? <prek::config::_::<impl serde_core::de::Deserialize for prek::config::Config>::deserialize::__Visitor as serde_core::de::Visitor>::visit_map
 0.1%   0.2% 22.8KiB      serde_saphyr saphyr_parser_bw::scanner::Scanner<T>::fetch_more_tokens
 0.1%   0.2% 22.7KiB              prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 21.1KiB      clap_builder clap_builder::parser::parser::Parser::get_matches_with
 0.1%   0.2% 20.5KiB              prek prek::hooks::meta_hooks::MetaHooks::run::{{closure}}
 0.1%   0.2% 20.0KiB   cargo_metadata? <cargo_metadata::_::<impl serde_core::de::Deserialize for cargo_metadata::Package>::deserialize::__Visitor as serde_core::de::Visitor>::visit_map
 0.1%   0.2% 19.6KiB              prek prek::archive::unzip::{{closure}}
 0.1%   0.2% 19.3KiB               std core::ptr::drop_in_place<prek::languages::<impl prek::config::Language>::install::{{closure}}>
 0.1%   0.2% 19.2KiB              prek <prek::languages::ruby::ruby::Ruby as prek::languages::LanguageImpl>::install::{{closure}}
 0.1%   0.2% 18.8KiB        serde_json serde_json::de::from_str
 0.1%   0.2% 18.7KiB     serde_saphyr? <serde_saphyr::de::YamlDeserializer as serde_core::de::Deserializer>::deserialize_map
38.3%  91.8%  9.1MiB                   And 21022 smaller methods. Use -n N to show more.
41.7% 100.0%  9.9MiB                   .text section size, the file size is 23.8MiB

@j178
Copy link
Owner

j178 commented Feb 19, 2026

We need to update the Ruby support status on this page:

prek/docs/languages.md

Lines 313 to 317 in 85d6dc6

!!! note "prek-only"
prek does not automatically download Ruby toolchains. It uses system-installed Rubies, including common version managers, and fails if no suitable version matches `language_version`.
Tracking for Ruby toolchain download support: [#43](https://github.com/j178/prek/issues/43)

@is-alnilam
Copy link
Contributor Author

We need to update the Ruby support status on this page:

prek/docs/languages.md

Lines 313 to 317 in 85d6dc6

!!! note "prek-only"
prek does not automatically download Ruby toolchains. It uses system-installed Rubies, including common version managers, and fails if no suitable version matches `language_version`.
Tracking for Ruby toolchain download support: [#43](https://github.com/j178/prek/issues/43)

Good point!

I'm trying to deal with the CI failures, I'll add that as well. It looks like the Mac issues may be simply that the GitHub API is being rate limited from the CI runner (but I'll have to work out a way around that...)

Looking for available Ruby versions via the API is subject to rate
limiting when a GitHub token is not supplied. This change improves the
messaging when GitHub return an error which may suggest that rate
limiting is being applied, hinting to the user that they may need to
supply a GitHub token.
To avoid being rate-limited when running tests, pass the GitHub token in
to the language tests (used by Ruby to download interpreters). The token
has no special permissions (the workflow already includes `permissions:
{}` at the top level), but this is sufficient to raise the rate limit.
@j178 j178 added the enhancement New feature or request label Feb 19, 2026
@j178 j178 self-assigned this Feb 20, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds automatic Ruby interpreter download support to prek using binaries from the rv-ruby project. Previously, prek could only use Ruby interpreters already installed on the system. With this change, prek can automatically download and install Ruby versions when requested versions aren't available locally.

Changes:

  • Implements automatic Ruby download from rv-ruby GitHub releases for supported platforms (macOS and Linux on x86_64 and ARM64)
  • Adds PREK_RUBY_MIRROR environment variable for mirror/air-gapped support with GitHub token authentication
  • Extends download infrastructure with download_and_extract_with to support request customization for authentication headers

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/languages.md Updates Ruby documentation to reflect automatic download support and mirror configuration
crates/prek/tests/languages/ruby.rs Adds comprehensive integration test for auto-download and caching behavior; updates error message snapshots
crates/prek/src/languages/ruby/version.rs Adds Display trait implementation for RubyRequest to improve error messages
crates/prek/src/languages/ruby/ruby.rs Integrates installer with download support by passing allows_download flag and tools directory path
crates/prek/src/languages/ruby/installer.rs Core implementation: GitHub release API querying, platform detection, download logic, system Ruby search (including rv paths), and extensive test coverage
crates/prek/src/languages/mod.rs Adds download_and_extract_with wrapper to support request customization (e.g., auth headers)
crates/prek-consts/src/env_vars.rs Adds PREK_RUBY_MIRROR environment variable constant
.github/workflows/ci.yml Propagates GITHUB_TOKEN to language tests to avoid API rate limiting

Comment on lines +38 to +42
let (base, is_github) = rv_ruby_mirror();
let url = if is_github {
// Rewrite github.com web URL to API URL.
let path = base.strip_prefix("https://github.com").unwrap();
format!("https://api.github.com/repos{path}/releases/latest")
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The unwrap() call here is fragile. While it should always succeed given that is_github is only true when the URL starts with "https://github.com/", this creates tight coupling between is_github_https and rv_ruby_api_url. Consider using expect() with a descriptive message explaining the invariant, or restructure to make the relationship more explicit.

Suggested change
let (base, is_github) = rv_ruby_mirror();
let url = if is_github {
// Rewrite github.com web URL to API URL.
let path = base.strip_prefix("https://github.com").unwrap();
format!("https://api.github.com/repos{path}/releases/latest")
let (base, mut is_github) = rv_ruby_mirror();
let url = if is_github {
// Rewrite github.com web URL to API URL.
if let Some(path) = base.strip_prefix("https://github.com") {
format!("https://api.github.com/repos{path}/releases/latest")
} else {
// Defensive fallback: mirror was marked as GitHub but does not have the
// expected prefix. Treat it as a non-GitHub mirror to avoid panicking
// or sending tokens to an unexpected host.
warn!(
"PREK_RUBY_MIRROR is marked as GitHub but does not start with \
'https://github.com': {base}"
);
is_github = false;
format!("{base}/releases/latest")
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally I'd probably change this unwrap() to an expect()? It should never be hit, after all...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use https://github.com/spinel-coop/rv for ruby toolchain Support ruby language

3 participants