From ed7fc1aad6c613009736717e70dc99450c9ae997 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Wed, 26 Aug 2026 20:59:48 -0400 Subject: [PATCH] Strip the brew formula revision from detected Homebrew Ruby versions Homebrew shipped ruby 4.0.6_1 and detect_homebrew_ruby_version returned the keg dirname verbatim - brew's PKG version, whose "_N" revision suffix (a rebuild of the same upstream Ruby) is not part of RUBY_VERSION and is rejected by Gem::Version. Every dev command in an unpinned repo on such a machine died pre-dispatch with "Malformed version number string 4.0.6_1" (first seen: the snappy CI runner the day its brew updated). Detection now extracts only the numeric version prefix from the dirname; anything else still falls through to asking the ruby binary itself. Co-authored-by: Cursor --- lib/shadowenv_ruby.rb | 10 ++++++--- test/lib/shadowenv_ruby_test.rb | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/shadowenv_ruby.rb b/lib/shadowenv_ruby.rb index d75603b..8cf36a4 100644 --- a/lib/shadowenv_ruby.rb +++ b/lib/shadowenv_ruby.rb @@ -48,10 +48,14 @@ def resolve_ruby_version(explicit_version) def detect_homebrew_ruby_version prefix = brew_prefix_for("ruby") return nil unless prefix - # /opt/homebrew/Cellar/ruby/4.0.1 -> "4.0.1" + # /opt/homebrew/Cellar/ruby/4.0.1 -> "4.0.1" + # /opt/homebrew/Cellar/ruby/4.0.6_1 -> "4.0.6" + # The keg dirname carries brew's PKG version, whose "_N" formula-revision + # suffix (a rebuild of the same upstream Ruby) is not part of RUBY_VERSION + # and is rejected by Gem::Version - so only the numeric prefix may leak out. realpath = File.realpath(prefix) rescue prefix - version = File.basename(realpath) - return version if version.match?(/\A\d+\.\d+/) + version = File.basename(realpath)[/\A\d+(?:\.\d+)+/] + return version if version # Fallback: ask the Homebrew ruby binary directly ruby_bin = File.join(prefix, "bin", "ruby") return nil unless File.executable?(ruby_bin) diff --git a/test/lib/shadowenv_ruby_test.rb b/test/lib/shadowenv_ruby_test.rb index d0cc2a7..117f6c7 100644 --- a/test/lib/shadowenv_ruby_test.rb +++ b/test/lib/shadowenv_ruby_test.rb @@ -57,6 +57,42 @@ class ShadowenvRubyTest < Minitest::Test 1 * Kernel.abort("dev: Resolved Ruby 2.6.0 is below dev's minimum (>= 2.7.0). Pin a newer version in dependencies.rb or run: brew upgrade ruby") end + # --- detect_homebrew_ruby_version --- + + test "detect_homebrew_ruby_version returns the keg version from a plain Cellar path" do + Given "a brew ruby prefix resolving to a plain version keg" + tmpdir = Dir.mktmpdir("shadowenv-detect-test-") + keg = File.join(tmpdir, "Cellar", "ruby", "4.0.6") + FileUtils.mkdir_p(keg) + + When "we detect the Homebrew Ruby version" + result = ShadowenvRuby.detect_homebrew_ruby_version + + Then "the keg's version is returned" + _ * ShadowenvRuby.brew_prefix_for("ruby") >> keg + result == "4.0.6" + + Cleanup + FileUtils.rm_rf(tmpdir) + end + + test "detect_homebrew_ruby_version strips a brew formula revision suffix" do + Given "a brew ruby prefix resolving to a revision keg (a rebuild of the same upstream Ruby)" + tmpdir = Dir.mktmpdir("shadowenv-detect-test-") + keg = File.join(tmpdir, "Cellar", "ruby", "4.0.6_1") + FileUtils.mkdir_p(keg) + + When "we detect the Homebrew Ruby version" + result = ShadowenvRuby.detect_homebrew_ruby_version + + Then "the upstream version is returned - RUBY_VERSION carries no _1, and Gem::Version rejects it" + _ * ShadowenvRuby.brew_prefix_for("ruby") >> keg + result == "4.0.6" + + Cleanup + FileUtils.rm_rf(tmpdir) + end + # --- ensure! --- test "ensure! skips setup! when the project is already provisioned" do