class RbSys::CargoBuilder

@api private
A class to build a Ruby gem Cargo. Extracted from ‘rubygems` gem, with some modifications.

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 "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

def build_crate(dest_path, results, args, cargo_dir)

def build_crate(dest_path, results, args, cargo_dir)
  env = build_env
  cmd = cargo_command(dest_path, args)
  runner.call cmd, results, "cargo", cargo_dir, env
  results
end

def build_env

def build_env
  build_env = rb_config_env
  build_env["RUBY_STATIC"] = "true" if ruby_static? && ENV.key?("RUBY_STATIC")
  build_env.merge(env)
end

def cargo_command(dest_path, args = [])

def cargo_command(dest_path, args = [])
  cmd = []
  cmd += if config.use_cargo_build
    ["cargo", "build"]
  else
    ["cargo", "rustc"]
  end
  cmd += ["--target", target] if target
  cmd += ["--target-dir", dest_path]
  cmd += ["--features", features.join(",")] unless features.empty?
  cmd += ["--lib"]
  cmd += ["--profile", profile.to_s]
  cmd += Gem::Command.build_args
  cmd += args
  if !config.use_cargo_build
    cmd += ["--"]
    cmd += [*rustc_args(dest_path)]
    cmd += extra_rustc_args
  end
  cmd
end

def cargo_crate_name

def cargo_crate_name
  spec.metadata.fetch("cargo_crate_name", spec.name).tr("-", "_")
end

def cargo_dylib_path(dest_path)

def cargo_dylib_path(dest_path)
  prefix = so_ext == "dll" ? "" : "lib"
  path_parts = [dest_path]
  path_parts << target if target
  path_parts += [profile_target_directory, "#{prefix}#{cargo_crate_name}.#{so_ext}"]
  File.join(*path_parts)
end

def darwin_target?

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

def final_extension_path(dest_path)

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

def finalize_directory(dest_path, lib_dir, extension_dir)

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. 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

def get_relative_path(path, base)

def get_relative_path(path, base)
  path[0..base.length - 1] = "." if path.start_with?(base)
  path
end

def initialize(spec)

def initialize(spec)
  require "rubygems/command"
  require_relative "cargo_builder/link_flag_converter"
  @spec = spec
  @runner = self.class.method(:run)
  @profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
  @env = {}
  @features = []
  @target = ENV["CARGO_BUILD_TARGET"] || ENV["RUST_TARGET"]
  @extra_rustc_args = []
  @extra_cargo_args = []
  @dry_run = true
  @ext_dir = ""
  @extra_rustflags = []
end

def ldflag_to_link_modifier(arg)

def ldflag_to_link_modifier(arg)
  LinkFlagConverter.convert(arg)
end

def libruby_args(dest_dir)

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

def linker_args

mkmf work properly.
We want to use the same linker that Ruby uses, so that the linker flags from
def linker_args
  cc_flag = Shellwords.split(makefile_config("CC"))
  linker = cc_flag.shift
  if WELL_KNOWN_WRAPPERS.any? { |w| linker.include?(w) }
    linker = cc_flag.shift
  end
  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 makefile_config(var_name)

def makefile_config(var_name)
  val = RbConfig::MAKEFILE_CONFIG[var_name]
  return unless val
  RbConfig.expand(val.dup)
end

def manifest_dir

def manifest_dir
  ext_dir
end

def maybe_resolve_ldflag_variable(input_arg, dest_dir)

Interpolate substition vars in the arg
def maybe_resolve_ldflag_variable(input_arg, dest_dir)
  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
  when "DEFFILE"
    # DEFFILE already generated by cargo
  else
    RbConfig::CONFIG[var_name]
  end
end

def mingw_target?

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

def mkmf_libpath

Corresponds to $(LIBPATH) in mkmf
def mkmf_libpath
  ["-L", "native=#{makefile_config("libdir")}"]
end

def msvc_target?

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

def mswin_link_args

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 musl?

def musl?
  RbConfig::CONFIG["target_os"] == "linux-musl" || RbConfig::CONFIG["CC"]&.include?("musl-gcc")
end

def platform_specific_rustc_args(dest_dir, flags = [])

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?
    # See https://github.com/oxidize-rb/rb-sys/issues/88
    dl_flag = "-Wl,-undefined,dynamic_lookup"
    flags += ["-C", "link-arg=#{dl_flag}"] unless makefile_config("DLDFLAGS")&.include?(dl_flag)
  elsif RUBY_ENGINE == "truffleruby"
    dl_flag = "-Wl,-z,lazy"
    flags += ["-C", "link-arg=#{dl_flag}"] unless makefile_config("DLDFLAGS")&.include?(dl_flag)
    # lazy binding requires RELRO to be off, see https://users.rust-lang.org/t/linux-executable-lazy-loading/65621/2
    flags += ["-C", "relro-level=off"]
  end
  if musl?
    flags += ["-C", "target-feature=-crt-static"]
  end
  flags
end

def profile

def profile
  return :release if rubygems_invoked?
  @profile
end

def profile_target_directory

def profile_target_directory
  case profile.to_sym
  when :release then "release"
  when :dev then "debug"
  else raise "unknown target directory for profile: #{profile}"
  end
end

def rb_config_env

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

def rename_cdylib_for_ruby_compatibility(dest_path)

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
end

def ruby_static?

def ruby_static?
  return true if %w[1 true].include?(ENV["RUBY_STATIC"])
  makefile_config("ENABLE_SHARED") == "no"
end

def rubygems_invoked?

def rubygems_invoked?
  ENV.key?("SOURCE_DATE_EPOCH") && ENV["RB_SYS_TEST"] != "1"
end

def rustc_args(dest_dir)

def 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)
  ]
end

def rustc_dynamic_linker_flags(dest_dir)

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

def rustc_lib_flags(dest_dir)

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

def so_ext

Other tags:
    See: https://github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185 -
def so_ext
  return RbConfig::CONFIG["SOEXT"] if RbConfig::CONFIG.key?("SOEXT")
  if win_target?
    "dll"
  elsif darwin_target?
    "dylib"
  else
    "so"
  end
end

def split_flags(var)

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

def validate_cargo_build!(dir)

def validate_cargo_build!(dir)
  dylib_path = cargo_dylib_path(dir)
  raise DylibNotFoundError, dir unless File.exist?(dylib_path)
  dylib_path
end

def win_target?

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