Skip to content

Instantly share code, notes, and snippets.

@keithrbennett
Created July 18, 2025 15:51
Show Gist options
  • Save keithrbennett/bf004c325c0a863a046e80ecd0917703 to your computer and use it in GitHub Desktop.
Save keithrbennett/bf004c325c0a863a046e80ecd0917703 to your computer and use it in GitHub Desktop.
Updates `rvm head`, optionally installs any new C Ruby or JRuby rubies.
#!/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