# 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

profile [RW]
runner [RW]
spec [RW]

### Public Class Methods

new ()
Source

    # 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

### Public Instance Methods

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

    # File lib/rubygems/ext/cargo_builder.rb, line 19
    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

build_env ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 73
    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_command (cargo_toml, dest_path, args = [], crate_name = nil)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 81
    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 Instance Methods

cargo ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 100
    def cargo
      ENV.fetch("CARGO", "cargo")
    end

cargo_crate_name (cargo_dir, manifest_path, results)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 200
      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

cargo_dylib_path (dest_path, crate_name)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 191
    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

cargo_rustc_args (dest_dir, crate_name)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 127
    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

darwin_target? ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 275
    def darwin_target?
      makefile_config("target_os").include?("darwin")
    end

extension_nesting (extension)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 106
    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

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

ldflag_to_link_modifier (arg)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 267
    def ldflag_to_link_modifier(arg)
      LinkFlagConverter.convert(arg)
    end

libruby_args (dest_dir)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 179
    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 ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 161
    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

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

makefile_config (var_name)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 325
    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)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 289
    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

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

mingw_target? ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 279
    def mingw_target?
      makefile_config("target_os").include?("mingw")
    end

mkmf_libpath ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 321
    def mkmf_libpath
      ["-L", "native=#{makefile_config("libdir")}"]
    end

Corresponds to $(LIBPATH) in mkmf

msvc_target? ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 271
    def msvc_target?
      makefile_config("target_os").include?("msvc")
    end

mswin_link_args ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 171
    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

normalize_path (path)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 247
    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 = [])
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 137
    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 ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 121
    def rb_config_env
      result = {}
      RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v }
      result
    end

ruby_static? ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 185
    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, crate_name)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 253
    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

rustc_lib_flags (dest_dir)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 259
    def rustc_lib_flags(dest_dir)
      split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) }
    end

split_flags (var)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 263
    def split_flags(var)
      Shellwords.split(RbConfig::CONFIG.fetch(var, ""))
    end

win_target? ()
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 283
    def win_target?
      target_platform = RbConfig::CONFIG["target_os"]
      !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
    end

write_deffile (dest_dir, crate_name)
Source

    # File lib/rubygems/ext/cargo_builder.rb, line 308
    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