summaryrefslogtreecommitdiff
path: root/lib/rubygems/ext
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/ext')
-rw-r--r--lib/rubygems/ext/build_error.rb4
-rw-r--r--lib/rubygems/ext/builder.rb208
-rw-r--r--lib/rubygems/ext/cargo_builder.rb349
-rw-r--r--lib/rubygems/ext/cargo_builder/link_flag_converter.rb27
-rw-r--r--lib/rubygems/ext/cmake_builder.rb107
-rw-r--r--lib/rubygems/ext/configure_builder.rb18
-rw-r--r--lib/rubygems/ext/ext_conf_builder.rb117
-rw-r--r--lib/rubygems/ext/rake_builder.rb40
8 files changed, 690 insertions, 180 deletions
diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb
index 0b3c17a9a0..0329c1eec3 100644
--- a/lib/rubygems/ext/build_error.rb
+++ b/lib/rubygems/ext/build_error.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
+
##
# Raised when there is an error while building extensions.
+require_relative "../exceptions"
+
class Gem::Ext::BuildError < Gem::InstallError
end
-
diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb
index a1619c97d7..e00cf159da 100644
--- a/lib/rubygems/ext/builder.rb
+++ b/lib/rubygems/ext/builder.rb
@@ -1,24 +1,18 @@
# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/user_interaction'
-require 'thread'
+require_relative "../user_interaction"
class Gem::Ext::Builder
-
include Gem::UserInteraction
- ##
- # The builder shells-out to run various commands after changing the
- # directory. This means multiple installations cannot be allowed to build
- # extensions in parallel as they may change each other's directories leading
- # to broken extensions or failed installations.
-
- CHDIR_MUTEX = Mutex.new # :nodoc:
+ class NoMakefileError < Gem::InstallError
+ end
attr_accessor :build_args # :nodoc:
@@ -27,111 +21,175 @@ class Gem::Ext::Builder
$1.downcase
end
- def self.make(dest_path, results)
- unless File.exist? 'Makefile' then
- raise Gem::InstallError, 'Makefile not found'
+ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"],
+ target_rbconfig: Gem.target_rbconfig, n_jobs: nil)
+ unless File.exist? File.join(make_dir, "Makefile")
+ # No makefile exists, nothing to do.
+ raise NoMakefileError, "No Makefile found in #{make_dir}"
end
# try to find make program from Ruby configure arguments first
- RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/
- make_program = ENV['MAKE'] || ENV['make'] || $1
- unless make_program then
- make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make'
+ target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/
+ make_program_name = ENV["MAKE"] || ENV["make"] || $1
+ make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make"
+ make_program = shellsplit(make_program_name)
+
+ is_nmake = /\bnmake/i.match?(make_program_name)
+ # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform.
+ destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : ""
+
+ # nmake doesn't support parallel build
+ unless is_nmake
+ have_make_arguments = make_program.size > 1
+
+ if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs
+ make_program << "-j#{n_jobs}"
+ end
end
- destdir = '"DESTDIR=%s"' % ENV['DESTDIR'] if RUBY_VERSION > '2.0'
+ env = [destdir]
- ['clean', '', 'install'].each do |target|
+ if sitedir
+ env << format("sitearchdir=%s", sitedir)
+ env << format("sitelibdir=%s", sitedir)
+ end
+
+ targets.each do |target|
# Pass DESTDIR via command line to override what's in MAKEFLAGS
cmd = [
- make_program,
- destdir,
- target
- ].join(' ').rstrip
+ *make_program,
+ *env,
+ target,
+ ].reject(&:empty?)
begin
- run(cmd, results, "make #{target}".rstrip)
+ run(cmd, results, "make #{target}".rstrip, make_dir)
rescue Gem::InstallError
- raise unless target == 'clean' # ignore clean failure
+ raise unless target == "clean" # ignore clean failure
end
end
end
- def self.redirector
- '2>&1'
+ def self.ruby
+ # Gem.ruby is quoted if it contains whitespace
+ cmd = shellsplit(Gem.ruby)
+
+ # This load_path is only needed when running rubygems test without a proper installation.
+ # Prepending it in a normal installation will cause problem with order of $LOAD_PATH.
+ # Therefore only add load_path if it is not present in the default $LOAD_PATH.
+ load_path = File.expand_path("../..", __dir__)
+ case load_path
+ when RbConfig::CONFIG["sitelibdir"], RbConfig::CONFIG["vendorlibdir"], RbConfig::CONFIG["rubylibdir"]
+ cmd
+ else
+ cmd << "-I#{load_path}"
+ end
end
- def self.run(command, results, command_name = nil)
+ def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {})
verbose = Gem.configuration.really_verbose
begin
- # TODO use Process.spawn when ruby 1.8 support is dropped.
- rubygems_gemdeps, ENV['RUBYGEMS_GEMDEPS'] = ENV['RUBYGEMS_GEMDEPS'], nil
+ rubygems_gemdeps = ENV["RUBYGEMS_GEMDEPS"]
+ ENV["RUBYGEMS_GEMDEPS"] = nil
if verbose
- puts("current directory: #{Dir.pwd}")
- puts(command)
- system(command)
- else
- results << "current directory: #{Dir.pwd}"
- results << command
- results << `#{command} #{redirector}`
+ puts("current directory: #{dir}")
+ p(command)
+ end
+ results << "current directory: #{dir}"
+ results << shelljoin(command)
+
+ require "open3"
+ # Set $SOURCE_DATE_EPOCH for the subprocess.
+ build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env)
+ output, status = begin
+ Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread|
+ stdin.close
+ output = String.new
+ while line = stdouterr.gets
+ output << line
+ if verbose
+ print line
+ end
+ end
+ [output, wait_thread.value]
+ end
+ rescue StandardError => error
+ raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}"
+ end
+ unless verbose
+ results << output
end
ensure
- ENV['RUBYGEMS_GEMDEPS'] = rubygems_gemdeps
+ ENV["RUBYGEMS_GEMDEPS"] = rubygems_gemdeps
end
- unless $?.success? then
+ unless status.success?
results << "Building has failed. See above output for more information on the failure." if verbose
+ end
+ yield(status, results) if block_given?
+
+ unless status.success?
exit_reason =
- if $?.exited? then
- ", exit code #{$?.exitstatus}"
- elsif $?.signaled? then
- ", uncaught signal #{$?.termsig}"
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
end
raise Gem::InstallError, "#{command_name || class_name} failed#{exit_reason}"
end
end
+ def self.shellsplit(command)
+ require "shellwords"
+
+ Shellwords.split(command)
+ end
+
+ def self.shelljoin(command)
+ require "shellwords"
+
+ Shellwords.join(command)
+ end
+
##
# Creates a new extension builder for +spec+. If the +spec+ does not yet
# have build arguments, saved, set +build_args+ which is an ARGV-style
# array.
- def initialize spec, build_args = spec.build_args
+ def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil)
@spec = spec
@build_args = build_args
@gem_dir = spec.full_gem_path
-
- @ran_rake = nil
+ @target_rbconfig = target_rbconfig
+ @build_jobs = build_jobs
end
##
# Chooses the extension builder class for +extension+
- def builder_for extension # :nodoc:
+ def builder_for(extension) # :nodoc:
case extension
when /extconf/ then
Gem::Ext::ExtConfBuilder
when /configure/ then
Gem::Ext::ConfigureBuilder
when /rakefile/i, /mkrf_conf/i then
- @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
- extension_dir = File.join @gem_dir, File.dirname(extension)
-
- message = "No builder for extension '#{extension}'"
- build_error extension_dir, message
+ build_error("No builder for extension '#{extension}'")
end
end
##
- # Logs the build +output+ in +build_dir+, then raises Gem::Ext::BuildError.
+ # Logs the build +output+, then raises Gem::Ext::BuildError.
- def build_error build_dir, output, backtrace = nil # :nodoc:
+ def build_error(output, backtrace = nil) # :nodoc:
gem_make_out = write_gem_make_out output
message = <<-EOF
@@ -146,32 +204,27 @@ EOF
raise Gem::Ext::BuildError, message, backtrace
end
- def build_extension extension, dest_path # :nodoc:
+ def build_extension(extension, dest_path) # :nodoc:
results = []
- extension ||= '' # I wish I knew why this line existed
+ builder = builder_for(extension)
+
extension_dir =
- File.expand_path File.join @gem_dir, File.dirname(extension)
+ File.expand_path File.join(@gem_dir, File.dirname(extension))
lib_dir = File.join @spec.full_gem_path, @spec.raw_require_paths.first
- builder = builder_for extension
-
begin
FileUtils.mkdir_p dest_path
- CHDIR_MUTEX.synchronize do
- Dir.chdir extension_dir do
- results = builder.build(extension, @gem_dir, dest_path,
- results, @build_args, lib_dir)
+ results = builder.build(extension, dest_path,
+ results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs)
- verbose { results.join("\n") }
- end
- end
+ verbose { results.join("\n") }
write_gem_make_out results.join "\n"
- rescue => e
+ rescue StandardError => e
results << e.message
- build_error extension_dir, results.join("\n"), $@
+ build_error(results.join("\n"), $@)
end
end
@@ -185,19 +238,16 @@ EOF
if @build_args.empty?
say "Building native extensions. This could take a while..."
else
- say "Building native extensions with: '#{@build_args.join ' '}'"
+ say "Building native extensions with: '#{@build_args.join " "}'"
say "This could take a while..."
end
dest_path = @spec.extension_dir
+ require "fileutils"
FileUtils.rm_f @spec.gem_build_complete_path
- @ran_rake = false # only run rake once
-
@spec.extensions.each do |extension|
- break if @ran_rake
-
build_extension extension, dest_path
end
@@ -207,15 +257,15 @@ EOF
##
# Writes +output+ to gem_make.out in the extension install directory.
- def write_gem_make_out output # :nodoc:
- destination = File.join @spec.extension_dir, 'gem_make.out'
+ def write_gem_make_out(output) # :nodoc:
+ destination = File.join @spec.extension_dir, "gem_make.out"
FileUtils.mkdir_p @spec.extension_dir
- open destination, 'wb' do |io| io.puts output end
+ File.open destination, "wb" do |io|
+ io.puts output
+ end
destination
end
-
end
-
diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb
new file mode 100644
index 0000000000..516459dd60
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder.rb
@@ -0,0 +1,349 @@
+# frozen_string_literal: true
+
+# This class is used by rubygems to build Rust extensions. It is a thin-wrapper
+# over the `cargo rustc` command which takes care of building Rust code in a way
+# that Ruby can use.
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ attr_accessor :spec, :runner, :profile
+
+ def initialize
+ require_relative "../command"
+ require_relative "cargo_builder/link_flag_converter"
+
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
+
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "tempfile"
+ require "fileutils"
+
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring"
+ end
+
+ # Where's the Cargo.toml of the crate we're building
+ cargo_toml = File.join(cargo_dir, "Cargo.toml")
+ # What's the crate's name
+ crate_name = cargo_crate_name(cargo_dir, cargo_toml, results)
+
+ begin
+ # Create a tmp dir to do the build in
+ tmp_dest = Dir.mktmpdir(".gem.", cargo_dir)
+
+ # Run the build
+ cmd = cargo_command(cargo_toml, tmp_dest, args, crate_name)
+ runner.call(cmd, results, "cargo", cargo_dir, build_env)
+
+ # Where do we expect Cargo to write the compiled library
+ dylib_path = cargo_dylib_path(tmp_dest, crate_name)
+
+ # Helpful error if we didn't find the compiled library
+ raise DylibNotFoundError, tmp_dest unless File.exist?(dylib_path)
+
+ # Cargo and Ruby differ on how the library should be named, rename from
+ # what Cargo outputs to what Ruby expects
+ dlext_name = "#{crate_name}.#{makefile_config("DLEXT")}"
+ dlext_path = File.join(File.dirname(dylib_path), dlext_name)
+ FileUtils.cp(dylib_path, dlext_path)
+
+ nesting = extension_nesting(extension)
+
+ if Gem.install_extension_in_lib && lib_dir
+ nested_lib_dir = File.join(lib_dir, nesting)
+ FileUtils.mkdir_p nested_lib_dir
+ FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true
+ end
+
+ # move to final destination
+ nested_dest_path = File.join(dest_path, nesting)
+ FileUtils.mkdir_p nested_dest_path
+ FileUtils.cp_r dlext_path, nested_dest_path, remove_destination: true
+ ensure
+ # clean up intermediary build artifacts
+ FileUtils.rm_rf tmp_dest if tmp_dest
+ end
+
+ results
+ end
+
+ def build_env
+ build_env = rb_config_env
+ build_env["RUBY_STATIC"] = "true" if ruby_static? && ENV.key?("RUBY_STATIC")
+ cfg = "--cfg=rb_sys_gem --cfg=rubygems --cfg=rubygems_#{Gem::VERSION.tr(".", "_")}"
+ build_env["RUSTFLAGS"] = [ENV["RUSTFLAGS"], cfg].compact.join(" ")
+ build_env
+ end
+
+ def cargo_command(cargo_toml, dest_path, args = [], crate_name = nil)
+ cmd = []
+ cmd += [cargo, "rustc"]
+ cmd += ["--crate-type", "cdylib"]
+ cmd += ["--target", ENV["CARGO_BUILD_TARGET"]] if ENV["CARGO_BUILD_TARGET"]
+ cmd += ["--target-dir", dest_path]
+ cmd += ["--manifest-path", cargo_toml]
+ cmd += ["--lib"]
+ cmd += ["--profile", profile.to_s]
+ cmd += ["--locked"]
+ cmd += Gem::Command.build_args
+ cmd += args
+ cmd += ["--"]
+ cmd += [*cargo_rustc_args(dest_path, crate_name)]
+ cmd
+ end
+
+ private
+
+ def cargo
+ ENV.fetch("CARGO", "cargo")
+ end
+
+ # returns the directory nesting of the extension, ignoring the first part, so
+ # "ext/foo/bar/Cargo.toml" becomes "foo/bar"
+ def extension_nesting(extension)
+ parts = extension.to_s.split(Regexp.union([File::SEPARATOR, File::ALT_SEPARATOR].compact))
+
+ parts = parts.each_with_object([]) do |segment, final|
+ next if segment == "."
+ if segment == ".."
+ raise Gem::InstallError, "extension outside of gem root" if final.empty?
+ next final.pop
+ end
+ final << segment
+ end
+
+ File.join(parts[1...-1])
+ end
+
+ def rb_config_env
+ result = {}
+ RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v }
+ result
+ end
+
+ def cargo_rustc_args(dest_dir, crate_name)
+ [
+ *linker_args,
+ *mkmf_libpath,
+ *rustc_dynamic_linker_flags(dest_dir, crate_name),
+ *rustc_lib_flags(dest_dir),
+ *platform_specific_rustc_args(dest_dir),
+ ]
+ end
+
+ def platform_specific_rustc_args(dest_dir, flags = [])
+ if mingw_target?
+ # On mingw platforms, mkmf adds libruby to the linker flags
+ flags += libruby_args(dest_dir)
+
+ # Make sure ALSR is used on mingw
+ # see https://github.com/rust-lang/rust/pull/75406/files
+ flags += ["-C", "link-arg=-Wl,--dynamicbase"]
+ flags += ["-C", "link-arg=-Wl,--disable-auto-image-base"]
+
+ # If the gem is installed on a host with build tools installed, but is
+ # run on one that isn't the missing libraries will cause the extension
+ # to fail on start.
+ flags += ["-C", "link-arg=-static-libgcc"]
+ elsif darwin_target?
+ # Ventura does not always have this flag enabled
+ flags += ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
+ end
+
+ flags
+ end
+
+ # We want to use the same linker that Ruby uses, so that the linker flags from
+ # mkmf work properly.
+ def linker_args
+ cc_flag = self.class.shellsplit(makefile_config("CC"))
+ # Avoid to ccache like tool from Rust build
+ # see https://github.com/ruby/rubygems/pull/8521#issuecomment-2689854359
+ # ex. CC="ccache gcc" or CC="sccache clang --any --args"
+ cc_flag.shift if cc_flag.size >= 2 && !cc_flag[1].start_with?("-")
+ linker = cc_flag.shift
+ link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] }
+
+ return mswin_link_args if linker == "cl"
+
+ ["-C", "linker=#{linker}", *link_args]
+ end
+
+ def mswin_link_args
+ args = []
+ args += ["-l", makefile_config("LIBRUBYARG_SHARED").chomp(".lib")]
+ args += split_flags("LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args += split_flags("LOCAL_LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args
+ end
+
+ def libruby_args(dest_dir)
+ libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED")
+ raw_libs = self.class.shellsplit(libs)
+ raw_libs.flat_map {|l| ldflag_to_link_modifier(l) }
+ end
+
+ def ruby_static?
+ return true if %w[1 true].include?(ENV["RUBY_STATIC"])
+
+ makefile_config("ENABLE_SHARED") == "no"
+ end
+
+ def cargo_dylib_path(dest_path, crate_name)
+ so_ext = RbConfig::CONFIG["SOEXT"]
+ prefix = so_ext == "dll" ? "" : "lib"
+ path_parts = [dest_path]
+ path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"]
+ path_parts += ["release", "#{prefix}#{crate_name}.#{so_ext}"]
+ File.join(*path_parts)
+ end
+
+ def cargo_crate_name(cargo_dir, manifest_path, results)
+ require "open3"
+ Gem.load_yaml
+
+ output, status =
+ begin
+ Open3.capture2e(cargo, "metadata", "--no-deps", "--format-version", "1", chdir: cargo_dir)
+ rescue StandardError => error
+ raise Gem::InstallError, "cargo metadata failed #{error.message}"
+ end
+
+ unless status.success?
+ if Gem.configuration.really_verbose
+ puts output
+ else
+ results << output
+ end
+
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
+
+ raise Gem::InstallError, "cargo metadata failed#{exit_reason}"
+ end
+
+ # cargo metadata output is specified as json
+ require "json"
+ metadata = JSON.parse(output)
+ package = metadata["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path }
+ unless package
+ found = metadata["packages"].map {|md| "#{md["name"]} at #{md["manifest_path"]}" }
+ raise Gem::InstallError, <<-EOF
+failed to determine cargo package name
+
+looking for: #{manifest_path}
+
+found:
+#{found.join("\n")}
+EOF
+ end
+ package["name"].tr("-", "_")
+ end
+
+ def normalize_path(path)
+ return path unless File::ALT_SEPARATOR
+
+ path.tr(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
+
+ def rustc_dynamic_linker_flags(dest_dir, crate_name)
+ split_flags("DLDFLAGS").
+ filter_map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }.
+ flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def rustc_lib_flags(dest_dir)
+ split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def split_flags(var)
+ self.class.shellsplit(RbConfig::CONFIG.fetch(var, ""))
+ end
+
+ def ldflag_to_link_modifier(arg)
+ LinkFlagConverter.convert(arg)
+ end
+
+ def msvc_target?
+ makefile_config("target_os").include?("msvc")
+ end
+
+ def darwin_target?
+ makefile_config("target_os").include?("darwin")
+ end
+
+ def mingw_target?
+ makefile_config("target_os").include?("mingw")
+ end
+
+ def win_target?
+ target_platform = RbConfig::CONFIG["target_os"]
+ !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
+ end
+
+ # Interpolate substitution vars in the arg (i.e. $(DEFFILE))
+ def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name)
+ var_matches = input_arg.match(/\$\((\w+)\)/)
+
+ return input_arg unless var_matches
+
+ var_name = var_matches[1]
+
+ return input_arg if var_name.nil? || var_name.chomp.empty?
+
+ case var_name
+ # On windows, it is assumed that mkmf has setup an exports file for the
+ # extension, so we have to create one ourselves.
+ when "DEFFILE"
+ write_deffile(dest_dir, crate_name)
+ else
+ RbConfig::CONFIG[var_name]
+ end
+ end
+
+ def write_deffile(dest_dir, crate_name)
+ deffile_path = File.join(dest_dir, "#{crate_name}-#{RbConfig::CONFIG["arch"]}.def")
+ export_prefix = makefile_config("EXPORT_PREFIX") || ""
+
+ File.open(deffile_path, "w") do |f|
+ f.puts "EXPORTS"
+ f.puts "#{export_prefix.strip}Init_#{crate_name}"
+ end
+
+ deffile_path
+ end
+
+ # Corresponds to $(LIBPATH) in mkmf
+ def mkmf_libpath
+ ["-L", "native=#{makefile_config("libdir")}"]
+ end
+
+ def makefile_config(var_name)
+ val = RbConfig::MAKEFILE_CONFIG[var_name]
+
+ return unless val
+
+ RbConfig.expand(val.dup)
+ end
+
+ # Error raised when no cdylib artifact was created
+ class DylibNotFoundError < StandardError
+ def initialize(dir)
+ files = Dir.glob(File.join(dir, "**", "*")).map {|f| "- #{f}" }.join "\n"
+
+ super <<~MSG
+ Dynamic library not found for Rust extension (in #{dir})
+
+ Make sure you set "crate-type" in Cargo.toml to "cdylib"
+
+ Found files:
+ #{files}
+ MSG
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cargo_builder/link_flag_converter.rb b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
new file mode 100644
index 0000000000..e4d196cb10
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ # Converts Ruby link flags into something cargo understands
+ class LinkFlagConverter
+ FILTERED_PATTERNS = [
+ /compress-debug-sections/, # Not supported by all linkers, and not required for Rust
+ ].freeze
+
+ def self.convert(arg)
+ return [] if FILTERED_PATTERNS.any? {|p| p.match?(arg) }
+
+ case arg.chomp
+ when /^-L\s*(.+)$/
+ ["-L", "native=#{$1}"]
+ when /^--library=(\w+\S+)$/, /^-l\s*(\w+\S+)$/
+ ["-l", $1]
+ when /^-l\s*([^:\s])+/ # -lfoo, but not -l:libfoo.a
+ ["-l", $1]
+ when /^-F\s*(.*)$/
+ ["-l", "framework=#{$1}"]
+ else
+ ["-C", "link-args=#{arg}"]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb
index efa3bd1d88..e660ed558b 100644
--- a/lib/rubygems/ext/cmake_builder.rb
+++ b/lib/rubygems/ext/cmake_builder.rb
@@ -1,17 +1,110 @@
# frozen_string_literal: true
-require 'rubygems/command'
+
+# 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, directory, dest_path, results, args=[], lib_dir=nil)
- unless File.exist?('Makefile') then
- cmd = "cmake . -DCMAKE_INSTALL_PREFIX=#{dest_path}"
- cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty?
+ attr_accessor :runner, :profile
+ def initialize
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
- run cmd, results
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring"
end
- make dest_path, results
+ # Figure the build dir
+ build_dir = File.join(cmake_dir, "build")
+
+ # Check if the gem defined presets
+ check_presets(cmake_dir, args, results)
+
+ # Configure
+ configure(cmake_dir, build_dir, dest_path, args, results)
+
+ # 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
diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb
index 8b42bf7ee9..230b214b3c 100644
--- a/lib/rubygems/ext/configure_builder.rb
+++ b/lib/rubygems/ext/configure_builder.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
@@ -6,19 +7,20 @@
#++
class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring"
+ end
- def self.build(extension, directory, dest_path, results, args=[], lib_dir=nil)
- unless File.exist?('Makefile') then
- cmd = "sh ./configure --prefix=#{dest_path}"
- cmd << " #{args.join ' '}" unless args.empty?
+ unless File.exist?(File.join(configure_dir, "Makefile"))
+ cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args]
- run cmd, results
+ run cmd, results, class_name, configure_dir
end
- make dest_path, results
+ make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs
results
end
-
end
-
diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb
index 965b728278..822454355d 100644
--- a/lib/rubygems/ext/ext_conf_builder.rb
+++ b/lib/rubygems/ext/ext_conf_builder.rb
@@ -1,94 +1,81 @@
# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'fileutils'
-require 'tempfile'
-
class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
- FileEntry = FileUtils::Entry_ # :nodoc:
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "fileutils"
+ require "tempfile"
- def self.build(extension, directory, dest_path, results, args=[], lib_dir=nil)
- tmp_dest = Dir.mktmpdir(".gem.", ".")
+ tmp_dest = Dir.mktmpdir(".gem.", extension_dir)
# Some versions of `mktmpdir` return absolute paths, which will break make
- # if the paths contain spaces. However, on Ruby 1.9.x on Windows, relative
- # paths cause all C extension builds to fail.
- #
- # As such, we convert to a relative path unless we are using Ruby 1.9.x on
- # Windows. This means that when using Ruby 1.9.x on Windows, paths with
- # spaces do not work.
+ # if the paths contain spaces.
#
- # Details: https://github.com/rubygems/rubygems/issues/977#issuecomment-171544940
- #
- # TODO: Make this unconditional when rubygems no longer supports Ruby 1.9.x.
- tmp_dest = get_relative_path(tmp_dest) unless Gem.win_platform? && RUBY_VERSION <= '2.0'
-
- Tempfile.open %w"siteconf .rb", "." do |siteconf|
- siteconf.puts "require 'rbconfig'"
- siteconf.puts "dest_path = #{tmp_dest.dump}"
- %w[sitearchdir sitelibdir].each do |dir|
- siteconf.puts "RbConfig::MAKEFILE_CONFIG['#{dir}'] = dest_path"
- siteconf.puts "RbConfig::CONFIG['#{dir}'] = dest_path"
- end
-
- siteconf.close
-
- destdir = ENV["DESTDIR"]
-
- begin
- cmd = [Gem.ruby, "-r", get_relative_path(siteconf.path), File.basename(extension), *args].join ' '
-
- begin
- run cmd, results
- ensure
- if File.exist? 'mkmf.log'
- unless $?.success? then
- results << "To see why this extension failed to compile, please check" \
- " the mkmf.log which can be found here:\n"
- results << " " + File.join(dest_path, 'mkmf.log') + "\n"
- end
- FileUtils.mv 'mkmf.log', dest_path
+ # As such, we convert to a relative path.
+ tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir)
+
+ destdir = ENV["DESTDIR"]
+
+ begin
+ cmd = ruby << File.basename(extension)
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path
+ cmd.push(*args)
+
+ run(cmd, results, class_name, extension_dir) do |s, r|
+ mkmf_log = File.join(extension_dir, "mkmf.log")
+ if File.exist? mkmf_log
+ unless s.success?
+ r << "To see why this extension failed to compile, please check" \
+ " the mkmf.log which can be found here:\n"
+ r << " " + File.join(dest_path, "mkmf.log") + "\n"
end
- siteconf.unlink
+ FileUtils.mv mkmf_log, dest_path
end
+ end
- ENV["DESTDIR"] = nil
+ ENV["DESTDIR"] = nil
- make dest_path, results
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
- if tmp_dest
- # TODO remove in RubyGems 3
- if Gem.install_extension_in_lib and lib_dir then
- FileUtils.mkdir_p lib_dir
- entries = Dir.entries(tmp_dest) - %w[. ..]
- entries = entries.map { |entry| File.join tmp_dest, entry }
- FileUtils.cp_r entries, lib_dir, :remove_destination => true
- end
+ full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
- FileEntry.new(tmp_dest).traverse do |ent|
- destent = ent.class.new(dest_path, ent.rel)
- destent.exist? or FileUtils.mv(ent.path, destent.path)
- end
- end
- ensure
- ENV["DESTDIR"] = destdir
- siteconf.close!
+ is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
+ # Do not copy extension libraries by default when cross-compiling
+ # not to conflict with the one already built for the host platform.
+ if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling
+ FileUtils.mkdir_p lib_dir
+ entries = Dir.entries(full_tmp_dest) - %w[. ..]
+ entries = entries.map {|entry| File.join full_tmp_dest, entry }
+ FileUtils.cp_r entries, lib_dir, remove_destination: true
end
+
+ FileUtils::Entry_.new(full_tmp_dest).traverse do |ent|
+ destent = ent.class.new(dest_path, ent.rel)
+ destent.exist? || FileUtils.mv(ent.path, destent.path)
+ end
+
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
+ ensure
+ ENV["DESTDIR"] = destdir
end
results
+ rescue Gem::Ext::Builder::NoMakefileError => error
+ results << error.message
+ results << "Skipping make for #{extension} as no Makefile was found."
+ # We are good, do not re-raise the error.
ensure
FileUtils.rm_rf tmp_dest if tmp_dest
end
- private
- def self.get_relative_path(path)
- path[0..Dir.pwd.length-1] = '.' if path.start_with?(Dir.pwd)
+ def self.get_relative_path(path, base)
+ path[0..base.length - 1] = "." if path.start_with?(base)
path
end
-
end
diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb
index 4b3534bf35..d702d7f339 100644
--- a/lib/rubygems/ext/rake_builder.rb
+++ b/lib/rubygems/ext/rake_builder.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
@@ -6,32 +7,31 @@
#++
class Gem::Ext::RakeBuilder < Gem::Ext::Builder
-
- def self.build(extension, directory, dest_path, results, args=[], lib_dir=nil)
- if File.basename(extension) =~ /mkrf_conf/i then
- cmd = "#{Gem.ruby} #{File.basename extension}".dup
- cmd << " #{args.join " "}" unless args.empty?
- run cmd, results
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring"
end
- # Deal with possible spaces in the path, e.g. C:/Program Files
- dest_path = '"' + dest_path.to_s + '"' if dest_path.to_s.include?(' ')
-
- rake = ENV['rake']
-
- rake ||= begin
- "#{Gem.ruby} -rrubygems #{Gem.bin_path('rake', 'rake')}"
- rescue Gem::Exception
- end
+ if /mkrf_conf/i.match?(File.basename(extension))
+ run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
+ end
- rake ||= Gem.default_exec_format % 'rake'
+ rake = ENV["rake"]
- cmd = "#{rake} RUBYARCHDIR=#{dest_path} RUBYLIBDIR=#{dest_path}" # ENV is frozen
+ if rake
+ rake = shellsplit(rake)
+ else
+ begin
+ rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake")
+ rescue Gem::Exception
+ rake = [Gem.default_exec_format % "rake"]
+ end
+ end
- run cmd, results
+ rake_args = ["RUBYARCHDIR=#{dest_path}", "RUBYLIBDIR=#{dest_path}", *args]
+ run(rake + rake_args, results, class_name, extension_dir)
results
end
-
end
-