lib/rb_sys/extensiontask.rb



require_relative "cargo/metadata"
require_relative "error"

begin
  require "rake/extensiontask"
rescue LoadError
  abort "Please install rake-compiler to use this feature"
end

module RbSys
  # ExtensionTask is a Rake::ExtensionTask subclass that is used to tailored for
  # Rust extensions. It has the same options a `Rake::ExtensionTask`.
  #
  # @see https://www.rubydoc.info/gems/rake-compiler/Rake/ExtensionTask
  #
  # @example
  #   RbSys::ExtensionTask.new("my-crate", my_gemspec) do |ext|
  #     ext.lib_dir = "lib/my-crate"
  #   end
  #
  # @param name [String] the crate name to build
  # @param gem_spec [Gem::Specification] the gem specification to build (needed for cross-compiling)
  # @return [Rake::ExtensionTask]
  class ExtensionTask < Rake::ExtensionTask
    def initialize(name = nil, gem_spec = :undefined)
      super
    end

    def init(name = nil, gem_spec = nil)
      super(name, lint_gem_spec(name, gem_spec))

      @orginal_ext_dir = @ext_dir
      @ext_dir = cargo_metadata.manifest_directory
      @source_pattern = nil
      @compiled_pattern = "*.{obj,so,bundle,dSYM}"
      @cross_compile = ENV.key?("RUBY_TARGET")
      @cross_platform = [ENV["RUBY_TARGET"]].compact
      @cross_compiling_blocks = []
      @cross_compiling_blocks << proc do |gemspec|
        warn "Removing unneeded dependencies from native gemspec"
        gemspec.dependencies.reject! { |d| d.name == "rb_sys" }
      end
      @cross_compiling_blocks << proc do |gemspec|
        warn "Removing source files from native gemspec"
        gemspec.files.reject! { |f| f.end_with?(".rs") }
        gemspec.files.reject! { |f| f.match?(/Cargo.(toml|lock)$/) }
        gemspec.files.reject! { |f| extconf.end_with?(f) }
        gemspec.extensions.reject! { |f| f.end_with?("Cargo.toml") }
      end
    end

    def define
      super
      define_env_tasks

      CLEAN.include(target_directory) if defined?(CLEAN)
    end

    def cargo_metadata
      @cargo_metadata ||= Cargo::Metadata.new(@name)
    end

    def extconf
      File.join(cargo_metadata.manifest_directory, "extconf.rb")
    end

    def binary(_platf)
      super.tr("-", "_")
    end

    # I'm not sure why this is necessary, can it be removed?
    def source_files
      list = FileList[
        "#{ext_dir}/**/*.{rs,rb,c,h,toml}",
        "**/Cargo.{toml,lock}",
        "**/.cargo/**/*",
        "#{ext_dir}/lib/**/*"
      ]
      list.include("#{ext_dir}/#{@source_pattern}") if @source_pattern
      list.exclude(File.join(target_directory, "**/*"))
      list
    end

    def cross_compiling(&block)
      @cross_compiling_blocks << block if block
    end

    def target_directory
      cargo_metadata.target_directory
    end

    def define_native_tasks(for_platform = nil, ruby_ver = RUBY_VERSION, callback = nil)
      cb = proc do |gemspec|
        callback&.call(gemspec)

        @cross_compiling_blocks.each do |block|
          block.call(gemspec)
        end
      end

      super(for_platform, ruby_ver, cb)
    end

    def define_env_tasks
      task "rb_sys:env:default" do
        ENV["RB_SYS_CARGO_TARGET_DIR"] ||= target_directory
        ENV["RB_SYS_CARGO_MANIFEST_DIR"] ||= cargo_metadata.manifest_directory
        ENV["RB_SYS_CARGO_PROFILE"] ||= "release"
      end

      desc "Use the debug profile for building native Rust extensions"
      task "rb_sys:env:dev" do
        ENV["RB_SYS_CARGO_PROFILE"] = "dev"
      end

      desc "Use the release profile for building native Rust extensions"
      task "rb_sys:env:release" do
        ENV["RB_SYS_CARGO_PROFILE"] = "release"
      end

      file extconf => "rb_sys:env:default"

      desc 'Compile the native Rust extension with the "dev" profile'
      task "compile:dev" => ["rb_sys:env:dev", "compile"]

      desc 'Compile the native Rust extension with the "release" profile'
      task "compile:release" => ["rb_sys:env:release", "compile"]
    end

    private

    def lint_gem_spec(name, gs)
      gem_spec = case gs
      when :undefined
        return
      when Gem::Specification
        gs
      when String
        Gem::Specification.load(gem_spec) || raise(ArgumentError, "Unable to load gemspec from file #{gs.inspect}")
      else
        raise ArgumentError, "gem_spec must be a Gem::Specification, got #{gs.class}"
      end

      gem_spec.files.each do |f|
        if /\.(dll|so|dylib|lib|bundle)$/.match?(f)
          warn "⚠️ gemspec includes native artifact (#{f}), please remove it."
        end
      end

      if (gem_crate_name = gem_spec.metadata["cargo_crate_name"])
        if name != gem_crate_name
          warn "⚠️ cargo_crate_name (#{gem_crate_name}) does not match extension task crate name (#{name})"
        end
      end

      gem_spec
    end
  end
end