class Gem::Ext::CargoBuilder

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.

Attributes

spec[R]

Public Class Methods

new(spec) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 9
def initialize(spec)
  @spec = spec
end

Public Instance Methods

build(_extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 13
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 Instance Methods

build_crate(dest_path, results, args, cargo_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 26
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
cargo_crate_name() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 122
def cargo_crate_name
  spec.metadata.fetch('cargo_crate_name', spec.name).tr('-', '_')
end
cargo_rustc_args(dest_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 51
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
darwin_target?() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 172
def darwin_target?
  makefile_config("target_os").include?("darwin")
end
debug_flags() click to toggle source

Good balance between binary size and debugability

# File lib/rubygems/ext/cargo_builder.rb, line 243
def debug_flags
  ["-C", "debuginfo=1"]
end
finalize_directory(ext_path, dest_path, lib_dir, extension_dir) click to toggle source

Copied from ExtConfBuilder

# File lib/rubygems/ext/cargo_builder.rb, line 248
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
get_relative_path(path, base) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 285
def get_relative_path(path, base)
  path[0..base.length - 1] = "." if path.start_with?(base)
  path
end
libruby_args(dest_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 92
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
linker_args() click to toggle source

We want to use the same linker that Ruby uses, so that the linker flags from mkmf work properly.

# File lib/rubygems/ext/cargo_builder.rb, line 83
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
makefile_config(var_name) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 234
def makefile_config(var_name)
  val = RbConfig::MAKEFILE_CONFIG[var_name]

  return unless val

  RbConfig.expand(val.dup)
end
maybe_resolve_ldflag_variable(input_arg, dest_dir) click to toggle source

Intepolate substition vars in the arg (i.e. $(DEFFILE))

# File lib/rubygems/ext/cargo_builder.rb, line 186
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
mingw_target?() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 176
def mingw_target?
  makefile_config("target_os").include?("mingw")
end
mkmf_libpath() click to toggle source

Corresponds to $(LIBPATH) in mkmf

# File lib/rubygems/ext/cargo_builder.rb, line 230
def mkmf_libpath
  ["-L", "native=#{makefile_config("libdir")}"]
end
msvc_target?() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 168
def msvc_target?
  makefile_config("target_os").include?("msvc")
end
platform_specific_rustc_args(dest_dir, flags = []) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 62
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
rename_cdylib_for_ruby_compatibility(dest_path) click to toggle source

Ruby expects the dylib to follow a file name convention for loading

# File lib/rubygems/ext/cargo_builder.rb, line 105
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
ruby_static?() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 98
def ruby_static?
  return true if %w[1 true].include?(ENV["RUBY_STATIC"])

  makefile_config("ENABLE_SHARED") == "no"
end
rustc_dynamic_linker_flags(dest_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 126
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
rustc_lib_flags(dest_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 133
def rustc_lib_flags(dest_dir)
  split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg, dest_dir) }
end
so_ext() click to toggle source

We have to basically reimplement RbConfig::CONFIG here to support Ruby < 2.5

@see github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185

# File lib/rubygems/ext/cargo_builder.rb, line 217
def so_ext
  return RbConfig::CONFIG["SOEXT"] if RbConfig::CONFIG.key?("SOEXT")

  if win_target?
    "dll"
  elsif darwin_target?
    "dylib"
  else
    "so"
  end
end
split_flags(var) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 137
def split_flags(var)
  Shellwords.split(RbConfig::CONFIG.fetch(var, ""))
end
validate_cargo_build!(dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 113
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
win_target?() click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 180
def win_target?
  target_platform = RbConfig::CONFIG["target_os"]
  !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
end
write_deffile(dest_dir) click to toggle source
# File lib/rubygems/ext/cargo_builder.rb, line 201
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