From 5ced99dddb384762a79c9aa07de130e460479f06 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 5 Jun 2025 23:30:22 -0700 Subject: [PATCH 1/5] [rubygems/rubygems] :Revamp CmakeBuilder to fix the issues described in #8572. Specifically: * Correctly pass command line arguments to CMake * Call CMake twice - once to configure a project and a second time to build (which is the standard way to use CMake). This fixes the previously incorrect assumption that CMake generates a Make file. * Update the tests to specify a CMake minimum version of 3.26 (which is already two years old). 3.26 is a bit arbritary but it aligns with Rice, and updates from the ancient 3.5 version being used (which CMake generates a warning message saying stop using it!) * Update the CMake call to use CMAKE_RUNTIME_OUTPUT_DIRECTORY and CMAKE_LIBRARY_OUTPUT_DIRECTORY to tell CMake to copy compiled binaries to the a Gem's lib directory. Note the updated builder took inspiration from the Cargo Builder, meaning you first create an instance of CmakeBuilder versus just calling class methods. https://github.com/rubygems/rubygems/commit/9e248d4679 --- lib/rubygems/ext/builder.rb | 2 +- lib/rubygems/ext/cmake_builder.rb | 103 ++++++++++++++++++-- test/rubygems/test_gem_ext_cmake_builder.rb | 95 ++++++++++++++---- 3 files changed, 175 insertions(+), 25 deletions(-) diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index b47996d0920bb3..600a6a5ff675ac 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -169,7 +169,7 @@ def builder_for(extension) # :nodoc: @ran_rake = true Gem::Ext::RakeBuilder when /CMakeLists.txt/ then - Gem::Ext::CmakeBuilder + Gem::Ext::CmakeBuilder.new when /Cargo.toml/ then Gem::Ext::CargoBuilder.new else diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index c7bfbb8a57ad27..2915568b39d0c1 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -1,21 +1,110 @@ # frozen_string_literal: true +# This builder creates extensions defined using CMake. Its is invoked if a Gem's spec file +# sets the `extension` property to a string that contains `CMakeLists.txt`. +# +# In general, CMake projects are built in two steps: +# +# * configure +# * build +# +# The builder follow this convention. First it runs a configuration step and then it runs a build step. +# +# CMake projects can be quite configurable - it is likely you will want to specify options when +# installing a gem. To pass options to CMake specify them after `--` in the gem install command. For example: +# +# gem install -- --preset +# +# Note that options are ONLY sent to the configure step - it is not currently possible to specify +# options for the build step. If this becomes and issue then the CMake builder can be updated to +# support build options. +# +# Useful options to know are: +# +# -G to specify a generator (-G Ninja is recommended) +# -D to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release) +# --preset to use a preset +# +# If the Gem author provides presets, via CMakePresets.json file, you will likely want to use one of them. +# If not, you may wish to specify a generator. Ninja is recommended because it can build projects in parallel +# and thus much faster than building them serially like Make does. + class Gem::Ext::CmakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd, + attr_accessor :runner, :profile + def initialize + @runner = self.class.method(:run) + @profile = :release + end + + def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd, target_rbconfig = Gem.target_rbconfig) if target_rbconfig.path warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" end - unless File.exist?(File.join(cmake_dir, "Makefile")) - require_relative "../command" - cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args] + # Figure the build dir + build_dir = File.join(cmake_dir, "build") - run cmd, results, class_name, cmake_dir - end + # Check if the gem defined presets + check_presets(cmake_dir, args, results) + + # Configure + configure(cmake_dir, build_dir, dest_path, args, results) - make dest_path, results, cmake_dir, target_rbconfig: target_rbconfig + # Compile + compile(cmake_dir, build_dir, args, results) results end + + def configure(cmake_dir, build_dir, install_dir, args, results) + cmd = ["cmake", + cmake_dir, + "-B", + build_dir, + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=#{install_dir}", # Windows + "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=#{install_dir}", # Not Windows + *Gem::Command.build_args, + *args] + + runner.call(cmd, results, "cmake_configure", cmake_dir) + end + + def compile(cmake_dir, build_dir, args, results) + cmd = ["cmake", + "--build", + build_dir.to_s, + "--config", + @profile.to_s] + + runner.call(cmd, results, "cmake_compile", cmake_dir) + end + + private + + def check_presets(cmake_dir, args, results) + # Return if the user specified a preset + return unless args.grep(/--preset/i).empty? + + cmd = ["cmake", + "--list-presets"] + + presets = Array.new + begin + runner.call(cmd, presets, "cmake_presets", cmake_dir) + + # Remove the first two lines of the array which is the current_directory and the command + # that was run + presets = presets[2..].join + results << <<~EOS + The gem author provided a list of presets that can be used to build the gem. To use a preset specify it on the command line: + + gem install -- --preset + + #{presets} + EOS + rescue Gem::InstallError + # Do nothing, CMakePresets.json was not included in the Gem + end + end end diff --git a/test/rubygems/test_gem_ext_cmake_builder.rb b/test/rubygems/test_gem_ext_cmake_builder.rb index b4cf8a8443c99b..e2bdedc710546d 100644 --- a/test/rubygems/test_gem_ext_cmake_builder.rb +++ b/test/rubygems/test_gem_ext_cmake_builder.rb @@ -29,7 +29,7 @@ def setup def test_self_build File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| cmakelists.write <<-EO_CMAKE -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.26) project(self_build NONE) install (FILES test.txt DESTINATION bin) EO_CMAKE @@ -39,46 +39,107 @@ def test_self_build output = [] - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext output = output.join "\n" - assert_match(/^cmake \. -DCMAKE_INSTALL_PREFIX\\=#{Regexp.escape @dest_path}/, output) + assert_match(/^current directory: #{Regexp.escape @ext}/, output) + assert_match(/cmake.*-DCMAKE_RUNTIME_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/cmake.*-DCMAKE_LIBRARY_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/#{Regexp.escape @ext}/, output) + end + + def test_self_build_presets + File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| + cmakelists.write <<-EO_CMAKE +cmake_minimum_required(VERSION 3.26) +project(self_build NONE) +install (FILES test.txt DESTINATION bin) + EO_CMAKE + end + + File.open File.join(@ext, "CMakePresets.json"), "w" do |presets| + presets.write <<-EO_CMAKE +{ + "version": 6, + "configurePresets": [ + { + "name": "debug", + "displayName": "Debug", + "generator": "Ninja", + "binaryDir": "build/debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "displayName": "Release", + "generator": "Ninja", + "binaryDir": "build/release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ] +} + EO_CMAKE + end + + FileUtils.touch File.join(@ext, "test.txt") + + output = [] + + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext + + output = output.join "\n" + + assert_match(/The gem author provided a list of presets that can be used to build the gem./, output) + assert_match(/Available configure presets/, output) + assert_match(/\"debug\" - Debug/, output) + assert_match(/\"release\" - Release/, output) + assert_match(/^current directory: #{Regexp.escape @ext}/, output) + assert_match(/cmake.*-DCMAKE_RUNTIME_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/cmake.*-DCMAKE_LIBRARY_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) assert_match(/#{Regexp.escape @ext}/, output) - assert_contains_make_command "", output - assert_contains_make_command "install", output - assert_match(/test\.txt/, output) end def test_self_build_fail output = [] + builder = Gem::Ext::CmakeBuilder.new error = assert_raise Gem::InstallError do - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder.build nil, @dest_path, output, [], @dest_path, @ext end - output = output.join "\n" + assert_match "cmake_configure failed", error.message shell_error_msg = /(CMake Error: .*)/ - - assert_match "cmake failed", error.message - - assert_match(/^cmake . -DCMAKE_INSTALL_PREFIX\\=#{Regexp.escape @dest_path}/, output) + output = output.join "\n" assert_match(/#{shell_error_msg}/, output) + assert_match(/CMake Error: The source directory .* does not appear to contain CMakeLists.txt./, output) end def test_self_build_has_makefile - File.open File.join(@ext, "Makefile"), "w" do |makefile| - makefile.puts "all:\n\t@echo ok\ninstall:\n\t@echo ok" + File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| + cmakelists.write <<-EO_CMAKE +cmake_minimum_required(VERSION 3.26) +project(self_build NONE) +install (FILES test.txt DESTINATION bin) + EO_CMAKE end output = [] - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext output = output.join "\n" - assert_contains_make_command "", output - assert_contains_make_command "install", output + # The default generator will create a Makefile in the build directory + makefile = File.join(@ext, "build", "Makefile") + assert(File.exist?(makefile)) end end From 943b5f66f7b2e90c349b1751b489b9881f6c26bc Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Thu, 9 Oct 2025 15:52:04 +0100 Subject: [PATCH 2/5] CI: Launchable: Fix errors at actions/setup-python on ppc64le/s390x The following errors happened at the actions/setup-python step. https://github.com/ruby/ruby/actions/runs/18229870239 > The version '3.x' with architecture 's390x' was not found for Ubuntu 24.04. > The list of all available versions can be found here: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json > The version '3.x' with architecture 'ppc64' was not found for Ubuntu 24.04. > The list of all available versions can be found here: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json After skipping the actions/setup-python step, the following errors also happened at the actions/setup-java step. https://github.com/ruby/ruby/actions/runs/18355975425?pr=14721 > make-ibm (check, ubuntu-24.04-ppc64le) > Could not find satisfied version for SemVer '17'. > make-ibm (check, ubuntu-24.04-s390x) > The process '/usr/bin/bash' failed with exit code 1 > make-ibm (check, ubuntu-24.04-s390x) > Process completed with exit code 127. To fix the errors, I started using the Java distribution semeru (IBM Semeru Runtime Open Edition) on the ppc64le/s390x cases. You can see the following page for the details of the Java distribution semeru. https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions https://github.com/actions/setup-java/blob/ead9eaa3cfe0b0fc2fa749519ae09c3d4f4080b0/src/distributions/semeru/installer.ts#L20-L27 --- .github/actions/launchable/setup/action.yml | 36 +++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/actions/launchable/setup/action.yml b/.github/actions/launchable/setup/action.yml index 54f1abd97a35a0..16af8fc3fd74d6 100644 --- a/.github/actions/launchable/setup/action.yml +++ b/.github/actions/launchable/setup/action.yml @@ -92,14 +92,38 @@ runs: uses: actions/setup-python@871daa956ca9ea99f3c3e30acb424b7960676734 # v5.0.0 with: python-version: "3.x" - if: steps.enable-launchable.outputs.enable-launchable + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && !endsWith(inputs.os, 'ppc64le') && !endsWith(inputs.os, 's390x') }} - name: Set up Java uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 with: distribution: 'temurin' java-version: '17' - if: steps.enable-launchable.outputs.enable-launchable + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && !endsWith(inputs.os, 'ppc64le') && !endsWith(inputs.os, 's390x') }} + + - name: Set up Java ppc64le + uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 + with: + distribution: 'semeru' + architecture: 'ppc64le' + java-version: '17' + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && endsWith(inputs.os, 'ppc64le') }} + + - name: Set up Java s390x + uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 + with: + distribution: 'semeru' + architecture: 's390x' + java-version: '17' + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && endsWith(inputs.os, 's390x') }} - name: Set global vars id: global @@ -142,7 +166,13 @@ runs: # Since updated PATH variable will be available in only subsequent actions, we need to add the path beforehand. # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path run: echo "$(python -msite --user-base)/bin" >> $GITHUB_PATH - if: steps.enable-launchable.outputs.enable-launchable && startsWith(inputs.os, 'macos') + if: >- + ${{ + steps.enable-launchable.outputs.enable-launchable + && (startsWith(inputs.os, 'macos') + || endsWith(inputs.os, 'ppc64le') + || endsWith(inputs.os, 's390x')) + }} - name: Set up Launchable id: setup-launchable From 837947ac7fb52c0492eb148e3cb17946aefaadc4 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 17 Oct 2025 17:34:59 +0900 Subject: [PATCH 3/5] Compatibility with test-unit-ruby-core --- tool/lib/core_assertions.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb index 1288b8c9aae132..fbf2c1a8bb3174 100644 --- a/tool/lib/core_assertions.rb +++ b/tool/lib/core_assertions.rb @@ -74,7 +74,19 @@ def message msg = nil, ending = nil, &default module CoreAssertions require_relative 'envutil' require 'pp' - require '-test-/sanitizers' + begin + require '-test-/sanitizers' + rescue LoadError + # in test-unit-ruby-core gem + def sanitizers + nil + end + else + def sanitizers + Test::Sanitizers + end + end + module_function :sanitizers nil.pretty_inspect @@ -159,7 +171,7 @@ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: fal pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # ASAN has the same problem - its shadow memory greatly increases memory usage # (plus asan has better ways to detect memory leaks than this assertion) - pend 'assert_no_memory_leak may consider ASAN memory usage as leak' if Test::Sanitizers.asan_enabled? + pend 'assert_no_memory_leak may consider ASAN memory usage as leak' if sanitizers&.asan_enabled? require_relative 'memory_status' raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) @@ -329,7 +341,7 @@ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **o args << "--debug" if RUBY_ENGINE == 'jruby' # warning: tracing (e.g. set_trace_func) will not capture all events without --debug flag stdout, stderr, status = EnvUtil.invoke_ruby(args, src, capture_stdout, true, **opt) - if Test::Sanitizers.lsan_enabled? + if sanitizers&.lsan_enabled? # LSAN may output messages like the following line into stderr. We should ignore it. # ==276855==Running thread 276851 was not suspended. False leaks are possible. # See https://github.com/google/sanitizers/issues/1479 From fb72e188ef2401c5399d855c198783a256a524c0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 17 Oct 2025 21:26:35 +0900 Subject: [PATCH 4/5] Update repository urls for rubygems and bundler --- doc/maintainers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/maintainers.md b/doc/maintainers.md index 7d217a166534a9..353909fe152baa 100644 --- a/doc/maintainers.md +++ b/doc/maintainers.md @@ -52,7 +52,7 @@ have commit right, others don't. * Eric Hodel ([drbrain]) * Hiroshi SHIBATA ([hsbt]) -* https://github.com/rubygems/rubygems +* https://github.com/ruby/rubygems #### lib/unicode_normalize.rb, lib/unicode_normalize/* @@ -104,7 +104,7 @@ have commit right, others don't. #### lib/bundler.rb, lib/bundler/* * Hiroshi SHIBATA ([hsbt]) -* https://github.com/rubygems/rubygems +* https://github.com/ruby/rubygems * https://rubygems.org/gems/bundler #### lib/cgi/escape.rb From 5298e979547a5859e84a70da88df0d7e4d308877 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 17 Oct 2025 20:00:55 +0900 Subject: [PATCH 5/5] [rubygems/rubygems] Postpone to remove legacy mingw platform https://github.com/rubygems/rubygems/commit/9b3a5a8ae9 --- lib/bundler/lockfile_parser.rb | 2 +- spec/bundler/other/major_deprecation_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index 07b5bd75147cf9..ac0ce1ef3d0aaf 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -142,7 +142,7 @@ def initialize(lockfile, strict: false) end if @platforms.include?(Gem::Platform::X64_MINGW_LEGACY) - SharedHelpers.feature_removed!("Found x64-mingw32 in lockfile, which is no longer supported as of Bundler 4.0.") + SharedHelpers.feature_deprecated!("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.") end @most_specific_locked_platform = @platforms.min_by do |bundle_platform| diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb index 1b04af76325795..947800be22bad1 100644 --- a/spec/bundler/other/major_deprecation_spec.rb +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -601,7 +601,7 @@ it "raises a helpful error" do bundle "install", raise_on_error: false - expect(err).to include("Found x64-mingw32 in lockfile, which is no longer supported as of Bundler 4.0.") + expect(err).to include("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.") end end