``` " = note: some arguments are omitted. use `--verbose` to show all linker arguments\n" + " = note: error: unexpected argument '-W' found\n" + " \n" + " tip: to pass '-W' as a value, use '-- -W'\n" + " \n" + " Usage: sccache [OPTIONS] <--dist-auth|--debug-preprocessor-cache|--dist-status|--show-stats|--show-adv-stats|--start-server|--stop-server|--zero-stats|--package-toolchain <EXE> <OUT>|CMD>\n" + " \n" + " For more information, try '--help'.\n" + " \n" + ``` https://github.com/rubygems/rubygems/commit/45e688ae62
351 lines
10 KiB
Ruby
351 lines
10 KiB
Ruby
# 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)
|
|
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/rubygems/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, but with the
|
|
# --format-version 1 option the output is compatible with YAML, so we can
|
|
# avoid the json dependency
|
|
metadata = Gem::SafeYAML.safe_load(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
|