Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions .github/actions/launchable/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/*

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/lockfile_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/ext/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 96 additions & 7 deletions lib/rubygems/ext/cmake_builder.rb
Original file line number Diff line number Diff line change
@@ -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 <gem_name> -- --preset <preset_name>
#
# 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<CMAKE_VARIABLE> to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release)
# --preset <preset_name> 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 <gem_name> -- --preset <preset_name>

#{presets}
EOS
rescue Gem::InstallError
# Do nothing, CMakePresets.json was not included in the Gem
end
end
end
2 changes: 1 addition & 1 deletion spec/bundler/other/major_deprecation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
95 changes: 78 additions & 17 deletions test/rubygems/test_gem_ext_cmake_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
18 changes: 15 additions & 3 deletions tool/lib/core_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down