diff --git a/README.md b/README.md index 2484b26..8001bd6 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,44 @@ Other environment variables can be set or passed through to the container like t RakeCompilerDock.sh "rake cross native gem OPENSSL_VERSION=#{ENV['OPENSSL_VERSION']}" ``` +### Choosing specific Ruby versions to support + +If you only want to precompile for certain Ruby versions, you can specify those versions by overwriting the `RUBY_CC_VERSION` environment variable. + +For example, if you wanted to only support Ruby 3.4 and 3.3, you might set this variable to: + +``` ruby +ENV["RUBY_CC_VERSION"] = "3.4.1:3.3.7" +``` + +In practice, though, hardcoding these versions is brittle because the patch versions in the container may very from release to release. + +A more robust way to do this is to use `RakeCompilerDock.ruby_cc_version` which accepts an array of Ruby minor versions or patch version requirements. + +``` ruby +RakeCompilerDock.ruby_cc_version("3.3", "3.4") +# => "3.4.1:3.3.7" + +RakeCompilerDock.ruby_cc_version("~> 3.4.0", "~> 3.3.0") +# => "3.4.1:3.3.7" + +RakeCompilerDock.ruby_cc_version("~> 3.3") +# => "3.4.1:3.3.7" +``` + +So you can either set the environment variable directly: + +``` ruby +ENV["RUBY_CC_VERSION"] = RakeCompilerDock.ruby_cc_version("~> 3.1") +``` + +or do the same thing using the `set_ruby_cc_version` convenience method: + +``` ruby +RakeCompilerDock.set_ruby_cc_version("~> 3.1") # equivalent to the above assignment +``` + + ## More information diff --git a/lib/rake_compiler_dock.rb b/lib/rake_compiler_dock.rb index f73d7e2..4b5ef2d 100644 --- a/lib/rake_compiler_dock.rb +++ b/lib/rake_compiler_dock.rb @@ -73,5 +73,86 @@ def exec(*args, &block) Starter.exec(*args, &block) end - module_function :exec, :sh, :image_name + # Retrieve the cross-rubies that are available in the docker image. This can be used to construct + # a custom `RUBY_CC_VERSION` string that is valid. + # + # Returns a Hash corresponding_patch_version> + # + # For example: + # + # RakeCompilerDock.cross_rubies + # # => { + # # "3.4" => "3.4.1", + # # "3.3" => "3.3.5", + # # "3.2" => "3.2.6", + # # "3.1" => "3.1.6", + # # "3.0" => "3.0.7", + # # "2.7" => "2.7.8", + # # "2.6" => "2.6.10", + # # "2.5" => "2.5.9", + # # "2.4" => "2.4.10", + # # } + # + def cross_rubies + { + "3.4" => "3.4.1", + "3.3" => "3.3.7", + "3.2" => "3.2.6", + "3.1" => "3.1.6", + "3.0" => "3.0.7", + "2.7" => "2.7.8", + "2.6" => "2.6.10", + "2.5" => "2.5.9", + "2.4" => "2.4.10", + } + end + + # Returns a valid RUBY_CC_VERSION string for the given requirements, + # where each `requirement` may be: + # + # - a String that matches the minor version exactly + # - a String that can be used as a Gem::Requirement constructor argument + # - a Gem::Requirement object + # + # Note that the returned string will contain versions sorted in descending order. + # + # For example: + # RakeCompilerDock.ruby_cc_version("2.7", "3.4") + # # => "3.4.1:2.7.8" + # + # RakeCompilerDock.ruby_cc_version("~> 3.2") + # # => "3.4.1:3.3.7:3.2.6" + # + # RakeCompilerDock.ruby_cc_version(Gem::Requirement.new("~> 3.2")) + # # => "3.4.1:3.3.7:3.2.6" + # + def ruby_cc_version(*requirements) + cross = cross_rubies + output = [] + + if requirements.empty? + output += cross.values + else + requirements.each do |requirement| + if cross[requirement] + output << cross[requirement] + else + requirement = Gem::Requirement.new(requirement) unless requirement.is_a?(Gem::Requirement) + versions = cross.values.find_all { |v| requirement.satisfied_by?(Gem::Version.new(v)) } + raise("No matching ruby version for requirement: #{requirement.inspect}") if versions.empty? + output += versions + end + end + end + + output.uniq.sort.reverse.join(":") + end + + # Set the environment variable `RUBY_CC_VERSION` to the value returned by `ruby_cc_version`, + # for the given requirements. + def set_ruby_cc_version(*requirements) + ENV["RUBY_CC_VERSION"] = ruby_cc_version(*requirements) + end + + module_function :exec, :sh, :image_name, :cross_rubies, :ruby_cc_version, :set_ruby_cc_version end diff --git a/test/test_environment_variables.rb b/test/test_environment_variables.rb index 1e72001..d6a18ee 100644 --- a/test/test_environment_variables.rb +++ b/test/test_environment_variables.rb @@ -34,6 +34,8 @@ def test_RUBY_CC_VERSION df = File.read(File.expand_path("../../Dockerfile.mri.erb", __FILE__)) df =~ /^ENV RUBY_CC_VERSION=(.*)$/ assert_equal $1, rcd_env['RUBY_CC_VERSION'] + + assert_equal RakeCompilerDock.ruby_cc_version, rcd_env['RUBY_CC_VERSION'] end def test_RAKE_EXTENSION_TASK_NO_NATIVE diff --git a/test/test_versions.rb b/test/test_versions.rb new file mode 100644 index 0000000..4e6e77c --- /dev/null +++ b/test/test_versions.rb @@ -0,0 +1,82 @@ +require 'rake_compiler_dock' +require 'rbconfig' +require 'test/unit' + +class TestVersions < Test::Unit::TestCase + def test_cross_rubies + cross = RakeCompilerDock.cross_rubies + assert_operator(cross, :is_a?, Hash) + cross.each do |minor, patch| + assert_match(/^\d+\.\d+$/, minor) + assert_match(/^\d+\.\d+\.\d+$/, patch) + end + end + + def test_ruby_cc_versions_no_args + cross = RakeCompilerDock.cross_rubies + expected = cross.values.sort.reverse.join(":") + + assert_equal(expected, RakeCompilerDock.ruby_cc_version) + end + + def test_ruby_cc_versions_strings + cross = RakeCompilerDock.cross_rubies + + expected = cross["3.4"] + assert_equal(expected, RakeCompilerDock.ruby_cc_version("3.4")) + + expected = [cross["3.4"], cross["3.2"]].join(":") + assert_equal(expected, RakeCompilerDock.ruby_cc_version("3.4", "3.2")) + + expected = [cross["3.4"], cross["3.2"]].join(":") + assert_equal(expected, RakeCompilerDock.ruby_cc_version("3.2", "3.4")) + + assert_raises do + RakeCompilerDock.ruby_cc_version("9.8") + end + + assert_raises do + RakeCompilerDock.ruby_cc_version("foo") + end + end + + def test_ruby_cc_versions_requirements + cross = RakeCompilerDock.cross_rubies + + expected = cross["3.4"] + assert_equal(expected, RakeCompilerDock.ruby_cc_version("~> 3.4")) + assert_equal(expected, RakeCompilerDock.ruby_cc_version(Gem::Requirement.new("~> 3.4"))) + + expected = [cross["3.4"], cross["3.3"], cross["3.2"]].join(":") + assert_equal(expected, RakeCompilerDock.ruby_cc_version("~> 3.2")) + assert_equal(expected, RakeCompilerDock.ruby_cc_version(Gem::Requirement.new("~> 3.2"))) + + expected = [cross["3.4"], cross["3.2"]].join(":") + assert_equal(expected, RakeCompilerDock.ruby_cc_version("~> 3.2.0", "~> 3.4.0")) + assert_equal(expected, RakeCompilerDock.ruby_cc_version(Gem::Requirement.new("~> 3.2.0"), Gem::Requirement.new("~> 3.4.0"))) + + expected = [cross["3.4"], cross["3.3"], cross["3.2"]].join(":") + assert_equal(expected, RakeCompilerDock.ruby_cc_version(">= 3.2")) + assert_equal(expected, RakeCompilerDock.ruby_cc_version(Gem::Requirement.new(">= 3.2"))) + + assert_raises do + RakeCompilerDock.ruby_cc_version(Gem::Requirement.new("> 9.8")) + end + end + + def test_set_ruby_cc_versions + original_ruby_cc_versions = ENV["RUBY_CC_VERSION"] + cross = RakeCompilerDock.cross_rubies + + RakeCompilerDock.set_ruby_cc_version(Gem::Requirement.new("~> 3.2.0"), Gem::Requirement.new("~> 3.4.0")) + assert_equal([cross["3.4"], cross["3.2"]].join(":"), ENV["RUBY_CC_VERSION"]) + + RakeCompilerDock.set_ruby_cc_version("~> 3.2.0", "~> 3.4.0") + assert_equal([cross["3.4"], cross["3.2"]].join(":"), ENV["RUBY_CC_VERSION"]) + + RakeCompilerDock.set_ruby_cc_version("~> 3.1") + assert_equal([cross["3.4"], cross["3.3"], cross["3.2"], cross["3.1"]].join(":"), ENV["RUBY_CC_VERSION"]) + ensure + ENV["RUBY_CC_VERSION"] = original_ruby_cc_versions + end +end