Created
July 18, 2025 15:51
-
-
Save keithrbennett/bf004c325c0a863a046e80ecd0917703 to your computer and use it in GitHub Desktop.
Updates `rvm head`, optionally installs any new C Ruby or JRuby rubies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'open3' | |
class RvmUpdater | |
def initialize | |
@installed_rubies = [] | |
@available_rubies = [] | |
end | |
def run | |
puts "Starting RVM Ruby updater..." | |
update_rvm | |
fetch_installed_rubies | |
fetch_available_rubies | |
install_newer_rubies | |
puts "RVM Ruby updater completed!" | |
end | |
private | |
def update_rvm | |
puts "\n=== Updating RVM ===" | |
stdout, stderr, status = Open3.capture3('rvm get head') | |
if status.success? | |
puts "RVM updated successfully" | |
puts stdout unless stdout.empty? | |
else | |
puts "Error updating RVM:" | |
puts stderr | |
exit 1 | |
end | |
end | |
def fetch_installed_rubies | |
puts "\n=== Fetching installed rubies ===" | |
stdout, stderr, status = Open3.capture3('rvm list') | |
unless status.success? | |
puts "Error fetching installed rubies:" | |
puts stderr | |
exit 1 | |
end | |
@installed_rubies = parse_installed_rubies(stdout) | |
puts "Installed rubies: #{@installed_rubies.join(', ')}" | |
end | |
def fetch_available_rubies | |
puts "\n=== Fetching available rubies ===" | |
stdout, stderr, status = Open3.capture3('rvm list known') | |
unless status.success? | |
puts "Error fetching available rubies:" | |
puts stderr | |
exit 1 | |
end | |
@available_rubies = parse_available_rubies(stdout) | |
puts "Found #{@available_rubies.length} available ruby versions" | |
end | |
def parse_installed_rubies(output) | |
output.lines.map(&:strip) | |
.map { |line| line.sub(/^=>?\s*/, '').sub(/\s*\[.*\].*$/, '') } | |
.select { |line| line.start_with?('ruby-') || line.start_with?('jruby-') } | |
.map { |line| line.split(/\s+/).first } | |
.compact | |
.uniq | |
end | |
def parse_available_rubies(output) | |
output.lines.each_with_object([]) do |line, rubies| | |
line = line.strip | |
# Check for section headers like [ruby-3.0.0] or [jruby-9.4.0.0] | |
if line.start_with?('[') && line.end_with?(']') | |
section_name = line[1...-1] # Remove [ and ] | |
@current_section = if section_name.start_with?('ruby-') || section_name.start_with?('jruby-') | |
section_name.start_with?('ruby-') ? 'ruby' : 'jruby' | |
else | |
nil | |
end | |
next | |
end | |
# If we're in a ruby or jruby section, collect versions | |
if @current_section && !line.empty? && !line.start_with?('[') | |
# Split by whitespace and collect valid versions | |
versions = line.split(/\s+/).reject(&:empty?) | |
.select { |version| version.match(/^\d+\.\d+/) } | |
rubies.concat(versions) | |
end | |
end.uniq | |
end | |
def install_newer_rubies | |
puts "\n=== Checking for newer rubies to install ===" | |
# Get latest standard Ruby versions (filter out very old ones) | |
standard_rubies = @available_rubies.select { |v| v.match(/^\d+\.\d+/) && modern_ruby_version?(v) } | |
jruby_versions = @available_rubies.select { |v| v.match(/^jruby-/) } | |
newer_standard = find_newer_versions(standard_rubies, 'ruby') | |
newer_jruby = find_newer_versions(jruby_versions, 'jruby') | |
to_install = newer_standard + newer_jruby | |
if to_install.empty? | |
puts "No newer rubies to install. You're up to date!" | |
return | |
end | |
puts "Found newer rubies available:" | |
to_install.each do |ruby_version| | |
installed_version = get_current_installed_version(ruby_version) | |
if installed_version | |
puts " #{ruby_version} (you have #{installed_version})" | |
else | |
puts " #{ruby_version} (new)" | |
end | |
end | |
puts "\nDo you want to install these newer rubies?" | |
to_install.each do |ruby_version| | |
print "Install #{ruby_version}? (y/n): " | |
response = gets.chomp.downcase | |
if response == 'y' || response == 'yes' | |
install_ruby(ruby_version) | |
else | |
puts "Skipping #{ruby_version}" | |
end | |
end | |
end | |
def find_newer_versions(available_versions, type) | |
# Get installed versions of this type | |
installed_of_type = @installed_rubies.select { |v| v.start_with?("#{type}-") } | |
.map { |v| v.sub("#{type}-", '') } | |
# Find the highest installed version for each major.minor series | |
installed_versions_by_series = installed_of_type.each_with_object({}) do |version, series_map| | |
version_parts = version.split('.') | |
next if version_parts.length < 2 | |
series = "#{version_parts[0]}.#{version_parts[1]}" | |
current_best = series_map[series] | |
if current_best.nil? || Gem::Version.new(version) > Gem::Version.new(current_best) | |
series_map[series] = version | |
end | |
end | |
# Group available versions by series | |
available_by_series = available_versions.each_with_object({}) do |version, series_map| | |
clean_version = version.start_with?('jruby-') ? version.sub('jruby-', '') : version | |
version_parts = clean_version.split('.') | |
next if version_parts.length < 2 | |
series = "#{version_parts[0]}.#{version_parts[1]}" | |
series_map[series] ||= [] | |
series_map[series] << clean_version | |
end | |
# Find newer versions | |
available_by_series.each_with_object([]) do |(series, versions), newer_versions| | |
latest_available = versions.max_by { |v| Gem::Version.new(v) } | |
latest_installed = installed_versions_by_series[series] | |
if latest_installed.nil? || Gem::Version.new(latest_available) > Gem::Version.new(latest_installed) | |
newer_versions << "#{type}-#{latest_available}" | |
end | |
end | |
end | |
def install_ruby(ruby_version) | |
puts "\n--- Installing #{ruby_version} ---" | |
# Use popen3 to get real-time output | |
Open3.popen3("rvm install #{ruby_version}") do |stdin, stdout, stderr, wait_thr| | |
stdin.close | |
# Read output in real-time | |
threads = [] | |
threads << Thread.new do | |
stdout.each_line { |line| puts line } | |
end | |
threads << Thread.new do | |
stderr.each_line { |line| puts line } | |
end | |
threads.each(&:join) | |
if wait_thr.value.success? | |
puts "Successfully installed #{ruby_version}" | |
else | |
puts "Failed to install #{ruby_version}" | |
end | |
end | |
end | |
def get_current_installed_version(newer_version) | |
# Extract the series (e.g., "ruby-3.1" from "ruby-3.1.4") | |
prefix, version_part = if newer_version.start_with?('ruby-') | |
['ruby-', newer_version.sub('ruby-', '')] | |
elsif newer_version.start_with?('jruby-') | |
['jruby-', newer_version.sub('jruby-', '')] | |
else | |
return nil | |
end | |
# Extract major.minor series | |
version_parts = version_part.split('.') | |
return nil if version_parts.length < 2 | |
series = "#{version_parts[0]}.#{version_parts[1]}" | |
# Find installed versions in this series | |
installed_in_series = @installed_rubies.select do |installed| | |
next false unless installed.start_with?(prefix) | |
installed_version = installed.sub(prefix, '') | |
installed_parts = installed_version.split('.') | |
next false if installed_parts.length < 2 | |
installed_series = "#{installed_parts[0]}.#{installed_parts[1]}" | |
installed_series == series | |
end | |
installed_in_series.max_by { |v| Gem::Version.new(v.sub(prefix, '')) } | |
end | |
def modern_ruby_version?(version) | |
# Only consider Ruby 3.x as "modern" | |
parts = version.split('.') | |
return false if parts.length < 2 | |
major = parts[0].to_i | |
# Ruby 3.x only | |
major >= 3 | |
end | |
end | |
# Run the updater | |
if __FILE__ == $0 | |
updater = RvmUpdater.new | |
updater.run | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment