diff options
Diffstat (limited to 'lib/rubygems/ext')
| -rw-r--r-- | lib/rubygems/ext/build_error.rb | 9 | ||||
| -rw-r--r-- | lib/rubygems/ext/builder.rb | 271 | ||||
| -rw-r--r-- | lib/rubygems/ext/cargo_builder.rb | 349 | ||||
| -rw-r--r-- | lib/rubygems/ext/cargo_builder/link_flag_converter.rb | 27 | ||||
| -rw-r--r-- | lib/rubygems/ext/cmake_builder.rb | 110 | ||||
| -rw-r--r-- | lib/rubygems/ext/configure_builder.rb | 26 | ||||
| -rw-r--r-- | lib/rubygems/ext/ext_conf_builder.rb | 81 | ||||
| -rw-r--r-- | lib/rubygems/ext/rake_builder.rb | 37 |
8 files changed, 910 insertions, 0 deletions
diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb new file mode 100644 index 0000000000..0329c1eec3 --- /dev/null +++ b/lib/rubygems/ext/build_error.rb @@ -0,0 +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 new file mode 100644 index 0000000000..e00cf159da --- /dev/null +++ b/lib/rubygems/ext/builder.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require_relative "../user_interaction" + +class Gem::Ext::Builder + include Gem::UserInteraction + + class NoMakefileError < Gem::InstallError + end + + attr_accessor :build_args # :nodoc: + + def self.class_name + name =~ /Ext::(.*)Builder/ + $1.downcase + end + + 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 + 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 + + env = [destdir] + + 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, + *env, + target, + ].reject(&:empty?) + begin + run(cmd, results, "make #{target}".rstrip, make_dir) + rescue Gem::InstallError + raise unless target == "clean" # ignore clean failure + end + end + end + + 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, dir = Dir.pwd, env = {}) + verbose = Gem.configuration.really_verbose + + begin + rubygems_gemdeps = ENV["RUBYGEMS_GEMDEPS"] + ENV["RUBYGEMS_GEMDEPS"] = nil + if verbose + 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 + end + + 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 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, target_rbconfig = Gem.target_rbconfig, build_jobs = nil) + @spec = spec + @build_args = build_args + @gem_dir = spec.full_gem_path + @target_rbconfig = target_rbconfig + @build_jobs = build_jobs + end + + ## + # Chooses the extension builder class for +extension+ + + 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 + Gem::Ext::RakeBuilder + when /CMakeLists.txt/ then + Gem::Ext::CmakeBuilder.new + when /Cargo.toml/ then + Gem::Ext::CargoBuilder.new + else + build_error("No builder for extension '#{extension}'") + end + end + + ## + # Logs the build +output+, then raises Gem::Ext::BuildError. + + def build_error(output, backtrace = nil) # :nodoc: + gem_make_out = write_gem_make_out output + + message = <<-EOF +ERROR: Failed to build gem native extension. + + #{output} + +Gem files will remain installed in #{@gem_dir} for inspection. +Results logged to #{gem_make_out} +EOF + + raise Gem::Ext::BuildError, message, backtrace + end + + def build_extension(extension, dest_path) # :nodoc: + results = [] + + builder = builder_for(extension) + + extension_dir = + File.expand_path File.join(@gem_dir, File.dirname(extension)) + lib_dir = File.join @spec.full_gem_path, @spec.raw_require_paths.first + + begin + FileUtils.mkdir_p dest_path + + results = builder.build(extension, dest_path, + results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs) + + verbose { results.join("\n") } + + write_gem_make_out results.join "\n" + rescue StandardError => e + results << e.message + build_error(results.join("\n"), $@) + end + end + + ## + # Builds extensions. Valid types of extensions are extconf.rb files, + # configure scripts and rakefiles or mkrf_conf files. + + def build_extensions + return if @spec.extensions.empty? + + if @build_args.empty? + say "Building native extensions. This could take a while..." + else + 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 + + @spec.extensions.each do |extension| + build_extension extension, dest_path + end + + FileUtils.touch @spec.gem_build_complete_path + end + + ## + # 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" + + FileUtils.mkdir_p @spec.extension_dir + + 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 new file mode 100644 index 0000000000..e660ed558b --- /dev/null +++ b/lib/rubygems/ext/cmake_builder.rb @@ -0,0 +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 + 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, n_jobs: nil) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" + end + + # 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 new file mode 100644 index 0000000000..230b214b3c --- /dev/null +++ b/lib/rubygems/ext/configure_builder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +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 + + unless File.exist?(File.join(configure_dir, "Makefile")) + cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args] + + run cmd, results, class_name, configure_dir + end + + 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 new file mode 100644 index 0000000000..822454355d --- /dev/null +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -0,0 +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. +#++ + +class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder + 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" + + tmp_dest = Dir.mktmpdir(".gem.", extension_dir) + + # Some versions of `mktmpdir` return absolute paths, which will break make + # if the paths contain spaces. + # + # 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 + FileUtils.mv mkmf_log, dest_path + end + end + + ENV["DESTDIR"] = nil + + make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs + + full_tmp_dest = File.join(extension_dir, tmp_dest_relative) + + 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 + + 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 new file mode 100644 index 0000000000..d702d7f339 --- /dev/null +++ b/lib/rubygems/ext/rake_builder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +class Gem::Ext::RakeBuilder < Gem::Ext::Builder + 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 + + if /mkrf_conf/i.match?(File.basename(extension)) + run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir) + end + + rake = ENV["rake"] + + 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 + + rake_args = ["RUBYARCHDIR=#{dest_path}", "RUBYLIBDIR=#{dest_path}", *args] + run(rake + rake_args, results, class_name, extension_dir) + + results + end +end |
