123456789_123456789_123456789_123456789_123456789_

Class: Gem::Ext::CargoBuilder

Relationships & Source Files
Namespace Children
Classes:
Exceptions:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Builder
Instance Chain:
Inherits: Gem::Ext::Builder
Defined in: lib/rubygems/ext/cargo_builder.rb,
lib/rubygems/ext/cargo_builder/link_flag_converter.rb

Overview

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 Method Summary

Builder - Inherited

.class_name, .make,
.new

Creates a new extension builder for #spec.

.ruby, .run

Instance Attribute Summary

Instance Method Summary

Builder - Inherited

#build_extensions

Builds extensions.

#build_error

Logs the build output, then raises BuildError.

#build_extension,
#builder_for

Chooses the extension builder class for extension

#write_gem_make_out

Writes output to gem_make.out in the extension install directory.

::Gem::UserInteraction - Included

#alert

Displays an alert statement.

#alert_error

Displays an error statement to the error output location.

#alert_warning

Displays a warning statement to the warning output location.

#ask

Asks a question and returns the answer.

#ask_for_password

Asks for a password with a prompt

#ask_yes_no

Asks a yes or no question.

#choose_from_list

Asks the user to answer question with an answer from the given list.

#say

Displays the given statement on the standard output (or equivalent).

#terminate_interaction

Terminates the RubyGems process with the given exit_code

#verbose

Calls say with msg or the results of the block if really_verbose is true.

::Gem::DefaultUserInteraction - Included

::Gem::Text - Included

#clean_text

Remove any non-printable characters and make the text suitable for printing.

#format_text

Wraps text to wrap characters and optionally indents by indent characters.

#levenshtein_distance

Returns a value representing the “cost” of transforming str1 into str2 Vendored version of DidYouMean::Levenshtein.distance from the ruby/did_you_mean gem @ 1.4.0 github.com/ruby/did_you_mean/blob/2ddf39b874808685965dbc47d344cf6c7651807c/lib/did_you_mean/levenshtein.rb#L7-L37.

#truncate_text, #min3

Constructor Details

.newCargoBuilder

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 11

def initialize
  require_relative "../command"
  require_relative "cargo_builder/link_flag_converter"

  @runner = self.class.method(:run)
  @profile = :release
end

Instance Attribute Details

#darwin_target?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 270

def darwin_target?
  makefile_config("target_os").include?("darwin")
end

#mingw_target?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 274

def mingw_target?
  makefile_config("target_os").include?("mingw")
end

#msvc_target?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 266

def msvc_target?
  makefile_config("target_os").include?("msvc")
end

#profile (rw)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 9

attr_accessor :spec, :runner, :profile

#ruby_static?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 180

def ruby_static?
  return true if %w[1 true].include?(ENV["RUBY_STATIC"])

  makefile_config("ENABLE_SHARED") == "no"
end

#runner (rw)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 9

attr_accessor :spec, :runner, :profile

#spec (rw)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 9

attr_accessor :spec, :runner, :profile

#win_target?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 278

def win_target?
  target_platform = RbConfig::CONFIG["target_os"]
  !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
end

Instance Method Details

#build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 19

def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd)
  require "tempfile"
  require "fileutils"

  # 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

#build_env

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 68

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

#cargo (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 95

def cargo
  ENV.fetch("CARGO", "cargo")
end

#cargo_command(cargo_toml, dest_path, args = [], crate_name = nil)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 76

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

#cargo_crate_name(cargo_dir, manifest_path, results) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 194

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
   = Gem::SafeYAML.safe_load(output)
  package = ["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path }
  unless package
    found = ["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

#cargo_dylib_path(dest_path, crate_name) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 186

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

#cargo_rustc_args(dest_dir, crate_name) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 122

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

#extension_nesting(extension) (private)

returns the directory nesting of the extension, ignoring the first part, so “ext/foo/bar/Cargo.toml” becomes “foo/bar”

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 101

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

#libruby_args(dest_dir) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 174

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) }
end

#linker_args (private)

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

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 156

def linker_args
  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

#makefile_config(var_name) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 336

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, crate_name) (private)

Interpolate substitution vars in the arg (i.e. $(DEFFILE))

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 284

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

#mkmf_libpath (private)

Corresponds to $(LIBPATH) in mkmf

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 332

def mkmf_libpath
  ["-L", "native=#{makefile_config("libdir")}"]
end

#normalize_path(path) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 241

def normalize_path(path)
  return path unless File::ALT_SEPARATOR

  path.tr(File::ALT_SEPARATOR, File::SEPARATOR)
end

#platform_specific_rustc_args(dest_dir, flags = []) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 132

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

#rb_config_env (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 116

def rb_config_env
  result = {}
  RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v }
  result
end

#rustc_dynamic_linker_flags(dest_dir, crate_name) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 247

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

#rustc_lib_flags(dest_dir) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 254

def rustc_lib_flags(dest_dir)
  split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) }
end

#so_ext (private)

We have to basically reimplement RbConfig::CONFIG['SOEXT'] here to support Ruby < 2.5

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 319

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) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 258

def split_flags(var)
  Shellwords.split(RbConfig::CONFIG.fetch(var, ""))
end

#write_deffile(dest_dir, crate_name) (private)

[ GitHub ]

  
# File 'lib/rubygems/ext/cargo_builder.rb', line 303

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