diff options
author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-05-02 19:42:15 +0900 |
---|---|---|
committer | nagachika <nagachika@ruby-lang.org> | 2022-05-18 10:02:42 +0900 |
commit | 8ba584ff3c085251865c11c5d7deef0ddfc6d0ff (patch) | |
tree | 7574439a57c7ad4f9ee7decf7f8272f48e7fc343 /lib/rubygems | |
parent | ec5dae0d816261719b0b2f9595c072701c7036e0 (diff) |
Merge RubyGems-3.3.11 and Bundler-2.3.11
Diffstat (limited to 'lib/rubygems')
-rw-r--r-- | lib/rubygems/commands/setup_command.rb | 2 | ||||
-rw-r--r-- | lib/rubygems/ext.rb | 1 | ||||
-rw-r--r-- | lib/rubygems/ext/builder.rb | 3 | ||||
-rw-r--r-- | lib/rubygems/ext/cargo_builder.rb | 305 | ||||
-rw-r--r-- | lib/rubygems/ext/ext_conf_builder.rb | 2 | ||||
-rw-r--r-- | lib/rubygems/gemcutter_utilities.rb | 39 | ||||
-rw-r--r-- | lib/rubygems/request.rb | 2 | ||||
-rw-r--r-- | lib/rubygems/source/git.rb | 1 |
8 files changed, 347 insertions, 8 deletions
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 01714f0342..7b6890013c 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -341,7 +341,7 @@ By default, this RubyGems will install gem as: fake_spec = Gem::Specification.new 'rubygems', Gem::VERSION def fake_spec.full_gem_path - File.expand_path '../../../..', __FILE__ + File.expand_path '../../..', __dir__ end generate_ri = options[:document].include? 'ri' diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb index bdd5bd9d82..59fd830437 100644 --- a/lib/rubygems/ext.rb +++ b/lib/rubygems/ext.rb @@ -16,3 +16,4 @@ require_relative 'ext/configure_builder' require_relative 'ext/ext_conf_builder' require_relative 'ext/rake_builder' require_relative 'ext/cmake_builder' +require_relative 'ext/cargo_builder' diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 6d32be6515..d9aa68c067 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -123,6 +123,9 @@ class Gem::Ext::Builder Gem::Ext::RakeBuilder when /CMakeLists.txt/ then Gem::Ext::CmakeBuilder + when /Cargo.toml/ then + # We use the spec name here to ensure we invoke the correct init function later + Gem::Ext::CargoBuilder.new(@spec) else build_error("No builder for extension '#{extension}'") end diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb new file mode 100644 index 0000000000..4c16063224 --- /dev/null +++ b/lib/rubygems/ext/cargo_builder.rb @@ -0,0 +1,305 @@ +# 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_reader :spec + + def initialize(spec) + @spec = spec + end + + def build(_extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + require "rubygems/command" + require "fileutils" + require "shellwords" + + build_crate(dest_path, results, args, cargo_dir) + ext_path = rename_cdylib_for_ruby_compatibility(dest_path) + finalize_directory(ext_path, dest_path, lib_dir, cargo_dir) + results + end + + private + + def build_crate(dest_path, results, args, cargo_dir) + manifest = File.join(cargo_dir, "Cargo.toml") + + given_ruby_static = ENV["RUBY_STATIC"] + + ENV["RUBY_STATIC"] = "true" if ruby_static? && !given_ruby_static + + cargo = ENV.fetch("CARGO", "cargo") + + cmd = [] + cmd += [cargo, "rustc"] + cmd += ["--target-dir", dest_path] + cmd += ["--manifest-path", manifest] + cmd += ["--lib", "--release", "--locked"] + cmd += ["--"] + cmd += [*cargo_rustc_args(dest_path)] + cmd += Gem::Command.build_args + cmd += args + + self.class.run cmd, results, self.class.class_name, cargo_dir + results + ensure + ENV["RUBY_STATIC"] = given_ruby_static + end + + def cargo_rustc_args(dest_dir) + [ + *linker_args, + *mkmf_libpath, + *rustc_dynamic_linker_flags(dest_dir), + *rustc_lib_flags(dest_dir), + *platform_specific_rustc_args(dest_dir), + *debug_flags, + ] + 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"] + 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 + # Have to handle CC="cl /nologo" on mswin + cc_flag = Shellwords.split(makefile_config("CC")) + linker = cc_flag.shift + link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] } + + ["-C", "linker=#{linker}", *link_args] + end + + def libruby_args(dest_dir) + libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED") + raw_libs = Shellwords.split(libs) + raw_libs.flat_map {|l| ldflag_to_link_modifier(l, dest_dir) } + end + + def ruby_static? + return true if %w[1 true].include?(ENV["RUBY_STATIC"]) + + makefile_config("ENABLE_SHARED") == "no" + end + + # Ruby expects the dylib to follow a file name convention for loading + def rename_cdylib_for_ruby_compatibility(dest_path) + dylib_path = validate_cargo_build!(dest_path) + dlext_name = "#{spec.name}.#{makefile_config("DLEXT")}" + new_name = dylib_path.gsub(File.basename(dylib_path), dlext_name) + FileUtils.cp(dylib_path, new_name) + new_name + end + + def validate_cargo_build!(dir) + prefix = so_ext == "dll" ? "" : "lib" + dylib_path = File.join(dir, "release", "#{prefix}#{cargo_crate_name}.#{so_ext}") + + raise DylibNotFoundError, dir unless File.exist?(dylib_path) + + dylib_path + end + + def cargo_crate_name + spec.metadata.fetch('cargo_crate_name', spec.name).tr('-', '_') + end + + def rustc_dynamic_linker_flags(dest_dir) + split_flags("DLDFLAGS") + .map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir) } + .compact + .flat_map {|arg| ldflag_to_link_modifier(arg, dest_dir) } + end + + def rustc_lib_flags(dest_dir) + split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg, dest_dir) } + end + + def split_flags(var) + Shellwords.split(RbConfig::CONFIG.fetch(var, "")) + end + + def ldflag_to_link_modifier(arg, dest_dir) + flag = arg[0..1] + val = arg[2..-1] + + case flag + when "-L" then ["-L", "native=#{val}"] + when "-l" then ["-l", val.to_s] + when "-F" then ["-l", "framework=#{val}"] + else ["-C", "link_arg=#{arg}"] + end + end + + def link_flag(link_name) + # These are provided by the CRT with MSVC + # @see https://github.com/rust-lang/pkg-config-rs/blob/49a4ac189aafa365167c72e8e503565a7c2697c2/src/lib.rs#L622 + return [] if msvc_target? && ["m", "c", "pthread"].include?(link_name) + + if link_name.include?("ruby") + # Specify the lib kind and give it the name "ruby" for linking + kind = ruby_static? ? "static" : "dylib" + + ["-l", "#{kind}=ruby:#{link_name}"] + else + ["-l", link_name] + end + 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 + + # Intepolate substition vars in the arg (i.e. $(DEFFILE)) + def maybe_resolve_ldflag_variable(input_arg, dest_dir) + str = input_arg.gsub(/\$\((\w+)\)/) do |var_name| + case var_name + # On windows, it is assumed that mkmf has setup an exports file for the + # extension, so we have to to create one ourselves. + when "DEFFILE" + write_deffile(dest_dir) + else + RbConfig::CONFIG[var_name] + end + end.strip + + str == "" ? nil : str + end + + def write_deffile(dest_dir) + deffile_path = File.join(dest_dir, "#{spec.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_#{spec.name}" + end + + deffile_path + end + + # We have to basically reimplement RbConfig::CONFIG['SOEXT'] here to support + # Ruby < 2.5 + # + # @see https://github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185 + def so_ext + return RbConfig::CONFIG["SOEXT"] if RbConfig::CONFIG.key?("SOEXT") + + if win_target? + "dll" + elsif darwin_target? + "dylib" + else + "so" + end + 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 + + # Good balance between binary size and debugability + def debug_flags + ["-C", "debuginfo=1"] + end + + # Copied from ExtConfBuilder + def finalize_directory(ext_path, dest_path, lib_dir, extension_dir) + require "fileutils" + require "tempfile" + + begin + 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. + # + # Details: https://github.com/rubygems/rubygems/issues/977#issuecomment-171544940 + tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir) + + if tmp_dest_relative + full_tmp_dest = File.join(extension_dir, tmp_dest_relative) + + # TODO: remove in RubyGems 3 + if Gem.install_extension_in_lib && lib_dir + FileUtils.mkdir_p lib_dir + FileUtils.cp_r ext_path, 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 + end + ensure + FileUtils.rm_rf tmp_dest if tmp_dest + end + end + + def get_relative_path(path, base) + path[0..base.length - 1] = "." if path.start_with?(base) + path + 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/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 3ca3463615..3e8aa950c9 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -39,7 +39,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder # workaround for https://github.com/oracle/truffleruby/issues/2115 siteconf_path = RUBY_ENGINE == "truffleruby" ? siteconf.path.dup : siteconf.path require "shellwords" - cmd = Gem.ruby.shellsplit << "-I" << File.expand_path("../../..", __FILE__) << + cmd = Gem.ruby.shellsplit << "-I" << File.expand_path('../..', __dir__) << "-r" << get_relative_path(siteconf_path, extension_dir) << File.basename(extension) cmd.push(*args) diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 0968e1a6f9..7cb5fe9448 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -163,12 +163,14 @@ module Gem::GemcutterUtilities key_name = get_key_name(scope) scope_params = get_scope_params(scope) + mfa_params = get_mfa_params(email, password) + all_params = scope_params.merge(mfa_params) response = rubygems_api_request(:post, "api/v1/api_key", sign_in_host, scope: scope) do |request| request.basic_auth email, password request["OTP"] = otp if otp - request.body = URI.encode_www_form({ name: key_name }.merge(scope_params)) + request.body = URI.encode_www_form({ name: key_name }.merge(all_params)) end with_response response do |resp| @@ -219,7 +221,7 @@ module Gem::GemcutterUtilities # +response+ text and no otp provided by options. def set_api_key(host, key) - if host == Gem::DEFAULT_HOST + if default_host? Gem.configuration.rubygems_api_key = key else Gem.configuration.set_api_key host, key @@ -243,7 +245,7 @@ module Gem::GemcutterUtilities end def pretty_host(host) - if Gem::DEFAULT_HOST == host + if default_host? 'RubyGems.org' else host @@ -258,8 +260,8 @@ module Gem::GemcutterUtilities else say "Please select scopes you want to enable for the API key (y/n)" API_SCOPES.each do |scope| - selected = ask "#{scope} [y/N]: " - scope_params[scope] = true if selected =~ /^[yY](es)?$/ + selected = ask_yes_no("#{scope}", false) + scope_params[scope] = true if selected end say "\n" end @@ -267,6 +269,33 @@ module Gem::GemcutterUtilities scope_params end + def default_host? + self.host == Gem::DEFAULT_HOST + end + + def get_mfa_params(email, password) + return {} unless default_host? + + mfa_level = get_user_mfa_level(email, password) + params = {} + if mfa_level == "ui_only" || mfa_level == "ui_and_gem_signin" + selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)") + params["mfa"] = true if selected + end + params + end + + def get_user_mfa_level(email, password) + response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request| + request.basic_auth email, password + end + + with_response response do |resp| + body = Gem::SafeYAML.load clean_text(resp.body) + body["mfa"] + end + end + def get_key_name(scope) hostname = Socket.gethostname || "unknown-host" user = ENV["USER"] || ENV["USERNAME"] || "unknown-user" diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index d6100c914b..136b154bee 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -39,7 +39,7 @@ class Gem::Request def cert_files; @connection_pool.cert_files; end def self.get_cert_files - pattern = File.expand_path("./ssl_certs/*/*.pem", File.dirname(__FILE__)) + pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__) Dir.glob(pattern) end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index cda5aa8073..8d5dc1f69d 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -102,6 +102,7 @@ class Gem::Source::Git < Gem::Source success = system @git, 'reset', '--quiet', '--hard', rev_parse if @need_submodules + require "open3" _, status = Open3.capture2e(@git, 'submodule', 'update', '--quiet', '--init', '--recursive') success &&= status.success? |