From 1bf41d924cdbaebc47213bf23f0c941c25177f54 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Tue, 1 Sep 2026 17:00:07 +0100 Subject: [PATCH] Add support for packaging single-ABI gems --- .github/actions/cibuildgem/action.yml | 6 ++- lib/cibuildgem/cli.rb | 13 ++++-- lib/cibuildgem/compilation_tasks.rb | 11 +++-- test/cli_test.rb | 66 +++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/.github/actions/cibuildgem/action.yml b/.github/actions/cibuildgem/action.yml index ceaf5e7f7..acef2e798 100644 --- a/.github/actions/cibuildgem/action.yml +++ b/.github/actions/cibuildgem/action.yml @@ -9,6 +9,10 @@ inputs: step: description: "The step to run" required: true + include-single-abi: + description: "Whether to also compile a set of single-ABI gems." + required: false + default: false test-command: description: "The command to run the test suite. By default cibuildgem will run either `bundle exec rake test` or `bundle exec rake spec` depending on the test framework used." required: false @@ -41,7 +45,7 @@ runs: if: "${{ inputs.step == 'compile' }}" working-directory: ${{ inputs.working-directory }} shell: bash - run: cibuildgem package + run: cibuildgem package --include-single-abi "${{ inputs.include-single-abi }}" - name: Upload artifacts if: "${{ inputs.step == 'compile' }}" uses: actions/upload-artifact@v4 diff --git a/lib/cibuildgem/cli.rb b/lib/cibuildgem/cli.rb index e76fb0b3e..66b39e05f 100644 --- a/lib/cibuildgem/cli.rb +++ b/lib/cibuildgem/cli.rb @@ -32,7 +32,7 @@ def compile run_rake_tasks!("cibuildgem:setup", :compile) end - desc "package", "Compile and package a 'fat gem'.", hide: true + desc "package", "Compile and package a 'multi-ABI gem'.", hide: true long_desc <<~MSG This command should normally run on CI, using the cibuildgem workflow. It will not work locally unless the environment is properly setup. @@ -42,13 +42,20 @@ def compile - A gem without precompiled binary (Ruby platform). The gem with precompiled binaries will be packaged with multiple binaries compatible for different - Ruby ABI (depending on what Ruby version the gem supports). + Ruby ABI (depending on what Ruby version the gem supports). If `--include-single-abi` is passed, + cibuildgem will also compile a set of single-ABI gems per Ruby version supported by the gem. MSG method_option "gemspec", type: "string", required: false, desc: "The gemspec to use. Defaults to the gemspec from the current working directory." + method_option "include-single-abi", type: :boolean, required: false, default: false, desc: "Whether to also compile a set of single-ABI gems. Defaults to false." def package ENV["RUBY_CC_VERSION"] ||= compilation_task.ruby_cc_version - run_rake_tasks!("cibuildgem:setup", :cross, :native, :gem) + if options["include-single-abi"] == true + compilation_task.ruby_versions.each do |ruby_version| + ENV["RUBY_CC_VERSION"] = ruby_version.to_s + run_rake_tasks!("cibuildgem:setup", :cross, :native, :gem) + end + end end desc "test", "Run the test suites of the target gem" diff --git a/lib/cibuildgem/compilation_tasks.rb b/lib/cibuildgem/compilation_tasks.rb index e19d6ecd7..c072867ba 100644 --- a/lib/cibuildgem/compilation_tasks.rb +++ b/lib/cibuildgem/compilation_tasks.rb @@ -25,11 +25,13 @@ def setup setup_packaging if create_packaging_task end - def ruby_cc_version + def ruby_versions required_ruby_version = @gemspec.required_ruby_version - selected_rubies = RubySeries.versions_to_compile_against(required_ruby_version) + RubySeries.versions_to_compile_against(required_ruby_version) + end - selected_rubies.map(&:to_s).join(":") + def ruby_cc_version + ruby_versions.map(&:to_s).join(":") end def normalized_platform @@ -45,6 +47,9 @@ def normalized_platform private def setup_packaging + # Creates a Gem::PackageTask for the ordinary Ruby gem. + # Elsewhere in rake-compiler we also create one for the compiled gem. + # Internally in PackageTask, we define the :gem rake task which calls Gem::Package.build. Gem::PackageTask.new(gemspec) do |pkg| pkg.need_zip = true pkg.need_tar = true diff --git a/test/cli_test.rb b/test/cli_test.rb index 7a1feebbb..2fab5f890 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -303,6 +303,72 @@ def test_package_when_a_rakefile_defines_an_extension_task assert_predicate($CHILD_STATUS, :success?) end + def test_package_single_abi_runs_rake_tasks_for_each_ruby_version + cli = CLI.new([], { "include-single-abi" => true }) + compilation_task = Struct.new(:ruby_cc_version, :ruby_versions).new("3.3.8:3.2.8", [ + Gem::Version.new("3.3.8"), + Gem::Version.new("3.2.8"), + ]) + rake_calls = [] + run_rake_tasks = proc do |*tasks| + rake_calls << [ENV.fetch("RUBY_CC_VERSION"), tasks] + end + + cli.stub(:compilation_task, compilation_task) do + cli.stub(:run_rake_tasks!, run_rake_tasks) do + cli.package + end + end + + expected_tasks = ["cibuildgem:setup", :cross, :native, :gem] + assert_equal( + [ + ["3.3.8:3.2.8", expected_tasks], + ["3.3.8", expected_tasks], + ["3.2.8", expected_tasks], + ], + rake_calls, + ) + end + + def test_package_defaults_to_multi_abi + cli = CLI.new + compilation_task = Struct.new(:ruby_cc_version).new("3.3.8:3.2.8") + rake_calls = [] + run_rake_tasks = proc do |*tasks| + rake_calls << [ENV.fetch("RUBY_CC_VERSION"), tasks] + end + + cli.stub(:compilation_task, compilation_task) do + cli.stub(:run_rake_tasks!, run_rake_tasks) do + cli.package + end + end + + assert_equal( + [["3.3.8:3.2.8", ["cibuildgem:setup", :cross, :native, :gem]]], + rake_calls, + ) + end + + def test_package_multi_abi_preserves_ruby_cc_version_from_environment + ENV["RUBY_CC_VERSION"] = "3.1.6" + cli = CLI.new + compilation_task = Struct.new(:ruby_cc_version).new("3.3.8:3.2.8") + ruby_cc_versions_used = [] + run_rake_tasks = proc do |*| + ruby_cc_versions_used << ENV.fetch("RUBY_CC_VERSION") + end + + cli.stub(:compilation_task, compilation_task) do + cli.stub(:run_rake_tasks!, run_rake_tasks) do + cli.package + end + end + + assert_equal(["3.1.6"], ruby_cc_versions_used) + end + def test_keep_the_extension_task_config_defined_by_the_gem Dir.chdir("test/fixtures/with_configured_ext") do cli = CLI.new