summaryrefslogtreecommitdiff
path: root/lib/rubygems/ext/cargo_builder.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/ext/cargo_builder.rb')
-rw-r--r--lib/rubygems/ext/cargo_builder.rb266
1 files changed, 152 insertions, 114 deletions
diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb
index e33b07a8a2..86a0e73f28 100644
--- a/lib/rubygems/ext/cargo_builder.rb
+++ b/lib/rubygems/ext/cargo_builder.rb
@@ -1,35 +1,66 @@
# frozen_string_literal: true
+require_relative "../shellwords"
+
# 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(spec)
+ def initialize
require_relative "../command"
require_relative "cargo_builder/link_flag_converter"
- @spec = spec
@runner = self.class.method(:run)
@profile = :release
end
- def build(_extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd)
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd)
+ require "tempfile"
require "fileutils"
- require "shellwords"
- build_crate(dest_path, results, args, cargo_dir)
- validate_cargo_build!(dest_path)
- rename_cdylib_for_ruby_compatibility(dest_path)
- finalize_directory(dest_path, lib_dir, cargo_dir)
- results
- 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)
- def build_crate(dest_path, results, args, cargo_dir)
- env = build_env
- cmd = cargo_command(cargo_dir, dest_path, args)
- runner.call cmd, results, "cargo", cargo_dir, env
+ # 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
@@ -37,44 +68,64 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
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_dir, dest_path, args = [])
- manifest = File.join(cargo_dir, "Cargo.toml")
- cargo = ENV.fetch("CARGO", "cargo")
-
+ 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", manifest]
+ cmd += ["--manifest-path", cargo_toml]
cmd += ["--lib"]
cmd += ["--profile", profile.to_s]
- cmd += ["--locked"] if profile == :release
+ cmd += ["--locked"]
cmd += Gem::Command.build_args
cmd += args
cmd += ["--"]
- cmd += [*cargo_rustc_args(dest_path)]
+ 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)
+ def cargo_rustc_args(dest_dir, crate_name)
[
*linker_args,
*mkmf_libpath,
- *rustc_dynamic_linker_flags(dest_dir),
+ *rustc_dynamic_linker_flags(dest_dir, crate_name),
*rustc_lib_flags(dest_dir),
*platform_specific_rustc_args(dest_dir),
- *debug_flags,
]
end
@@ -92,6 +143,9 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
# 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
@@ -100,14 +154,23 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
# 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}"] }
+ 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 = Shellwords.split(libs)
@@ -120,44 +183,72 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
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)
- new_path = final_extension_path(dest_path)
- FileUtils.cp(cargo_dylib_path(dest_path), new_path)
- new_path
+ def cargo_dylib_path(dest_path, crate_name)
+ 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 validate_cargo_build!(dir)
- dylib_path = cargo_dylib_path(dir)
+ def cargo_crate_name(cargo_dir, manifest_path, results)
+ require "open3"
+ Gem.load_yaml
- raise DylibNotFoundError, dir unless File.exist?(dylib_path)
+ 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
- dylib_path
- end
+ unless status.success?
+ if Gem.configuration.really_verbose
+ puts output
+ else
+ results << output
+ end
- def final_extension_path(dest_path)
- dylib_path = cargo_dylib_path(dest_path)
- dlext_name = "#{spec.name}.#{makefile_config("DLEXT")}"
- dylib_path.gsub(File.basename(dylib_path), dlext_name)
- end
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
- def cargo_dylib_path(dest_path)
- prefix = so_ext == "dll" ? "" : "lib"
- path_parts = [dest_path]
- path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"]
- path_parts += [profile_target_directory, "#{prefix}#{cargo_crate_name}.#{so_ext}"]
- File.join(*path_parts)
+ 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 cargo_crate_name
- spec.metadata.fetch("cargo_crate_name", spec.name).tr("-", "_")
+ 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)
- split_flags("DLDFLAGS")
- .map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir) }
- .compact
- .flat_map {|arg| ldflag_to_link_modifier(arg) }
+ def rustc_dynamic_linker_flags(dest_dir, crate_name)
+ split_flags("DLDFLAGS").
+ map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }.
+ compact.
+ flat_map {|arg| ldflag_to_link_modifier(arg) }
end
def rustc_lib_flags(dest_dir)
@@ -189,8 +280,8 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
!!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
end
- # Interpolate substition vars in the arg (i.e. $(DEFFILE))
- def maybe_resolve_ldflag_variable(input_arg, dest_dir)
+ # 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
@@ -201,27 +292,27 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
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.
+ # extension, so we have to create one ourselves.
when "DEFFILE"
- write_deffile(dest_dir)
+ write_deffile(dest_dir, crate_name)
else
RbConfig::CONFIG[var_name]
end
end
- def write_deffile(dest_dir)
- deffile_path = File.join(dest_dir, "#{spec.name}-#{RbConfig::CONFIG["arch"]}.def")
+ 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_#{spec.name}"
+ f.puts "#{export_prefix.strip}Init_#{crate_name}"
end
deffile_path
end
- # We have to basically reimplement RbConfig::CONFIG['SOEXT'] here to support
+ # We have to basically reimplement <code>RbConfig::CONFIG['SOEXT']</code> here to support
# Ruby < 2.5
#
# @see https://github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185
@@ -250,59 +341,6 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
RbConfig.expand(val.dup)
end
- # Good balance between binary size and debugability
- def debug_flags
- return [] if profile == :dev
-
- ["-C", "debuginfo=1"]
- end
-
- # Copied from ExtConfBuilder
- def finalize_directory(dest_path, lib_dir, extension_dir)
- require "fileutils"
- require "tempfile"
-
- ext_path = final_extension_path(dest_path)
-
- begin
- 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)
-
- full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
-
- # TODO: remove in RubyGems 4
- 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
- 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
-
- def profile_target_directory
- case profile
- when :release then "release"
- when :dev then "debug"
- else raise "unknown target directory for profile: #{profile}"
- end
- end
-
# Error raised when no cdylib artifact was created
class DylibNotFoundError < StandardError
def initialize(dir)