lib/rubygems/compiler.rb
# frozen_string_literal: true
require "fileutils"
require "open3"
require "rbconfig"
require "tmpdir"
require "rubygems/installer"
require "rubygems/package"
class Gem::Compiler
include Gem::UserInteraction
# raise when there is a error
class CompilerError < Gem::InstallError; end
attr_reader :target_dir, :options
def initialize(gemfile, _options = {})
@gemfile = gemfile
@output_dir = _options.delete(:output)
@options = _options
end
def compile
unpack
build_extensions
artifacts = collect_artifacts
strip_artifacts artifacts
if shared_dir = options[:include_shared_dir]
shared_libs = collect_shared(shared_dir)
artifacts.concat shared_libs
end
# build a new gemspec from the original one
gemspec = installer.spec.dup
adjust_gemspec_files gemspec, artifacts
# generate new gem and return new path to it
repackage gemspec
ensure
cleanup
end
private
def adjust_abi_lock(gemspec)
abi_lock = @options[:abi_lock] || :ruby
case abi_lock
when :ruby
ruby_abi = RbConfig::CONFIG["ruby_version"]
gemspec.required_ruby_version = "~> #{ruby_abi}"
when :strict
cfg = RbConfig::CONFIG
ruby_abi = "#{cfg["MAJOR"]}.#{cfg["MINOR"]}.#{cfg["TEENY"]}.0"
gemspec.required_ruby_version = "~> #{ruby_abi}"
end
end
def adjust_gemspec_files(gemspec, artifacts)
# remove any non-existing files
if @options[:prune]
gemspec.files.reject! { |f| !File.exist?("#{target_dir}/#{f}") }
end
# add discovered artifacts
artifacts.each do |path|
# path needs to be relative to target_dir
file = path.sub("#{target_dir}/", "")
debug "Adding '#{file}' to gemspec"
gemspec.files.push file
end
end
def build_extensions
# run pre_install hooks
if installer.respond_to?(:run_pre_install_hooks)
installer.run_pre_install_hooks
end
installer.build_extensions
end
def cleanup
FileUtils.rm_rf tmp_dir
end
def collect_artifacts
# determine build artifacts from require_paths
dlext = RbConfig::CONFIG["DLEXT"]
lib_dirs = installer.spec.require_paths.join(",")
Dir.glob("#{target_dir}/{#{lib_dirs}}/**/*.#{dlext}")
end
def collect_shared(shared_dir)
libext = platform_shared_ext
Dir.glob("#{target_dir}/#{shared_dir}/**/*.#{libext}")
end
def ensure_ruby_version_met(spec)
if rrv = spec.required_ruby_version
ruby_version = Gem.ruby_version
unless rrv.satisfied_by? ruby_version
raise Gem::RuntimeRequirementNotMetError,
"#{spec.full_name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}."
end
end
end
def ensure_rubygems_version_met(spec)
if rrgv = spec.required_rubygems_version
unless rrgv.satisfied_by? Gem.rubygems_version
rg_version = Gem::VERSION
raise Gem::RuntimeRequirementNotMetError,
"#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " +
"Try 'gem update --system' to update RubyGems itself."
end
end
end
def info(msg)
say msg if Gem.configuration.verbose
end
def debug(msg)
say msg if Gem.configuration.really_verbose
end
def installer
@installer ||= prepare_installer
end
def platform_shared_ext
platform = Gem::Platform.local
case platform.os
when /darwin/
"dylib"
when /linux|bsd|solaris/
"so"
when /mingw|mswin|cygwin|msys/
"dll"
else
"so"
end
end
def prepare_installer
installer = Gem::Installer.at(@gemfile, options.dup.merge(unpack: true))
installer.spec.full_gem_path = @target_dir
installer.spec.extension_dir = File.join(@target_dir, "lib")
# Ensure Ruby version is met
ensure_ruby_version_met(installer.spec)
# Check version of RubyGems (just in case)
ensure_rubygems_version_met(installer.spec)
# Hmm, gem already compiled?
if installer.spec.platform != Gem::Platform::RUBY
info "The gem file seems to be compiled already. Skipping."
cleanup
terminate_interaction
end
# Hmm, no extensions?
if installer.spec.extensions.empty?
info "There are no extensions to build on this gem file. Skipping."
cleanup
terminate_interaction
end
installer
end
def repackage(gemspec)
# clear out extensions from gemspec
gemspec.extensions.clear
# adjust platform
gemspec.platform = Gem::Platform::CURRENT
# adjust gem version
if build_number = options[:build_number]
gemspec.version = Gem::Version.create("#{gemspec.version}.#{build_number}")
end
# adjust version of Ruby
adjust_abi_lock(gemspec)
# build new gem
output_gem = nil
Dir.chdir target_dir do
output_gem = Gem::Package.build(gemspec)
end
unless output_gem
raise CompilerError,
"There was a problem building the gem."
end
# move the built gem to the original output directory
FileUtils.mv File.join(target_dir, output_gem), @output_dir
# return the path of the gem
output_gem
end
def simple_run(command, command_name)
begin
output, status = Open3.capture2e(*command)
rescue => error
raise Gem::CompilerError, "#{command_name} failed#{error.message}"
end
yield(status, output) if block_given?
unless status.success?
exit_reason =
if status.exited?
", exit code #{status.exitstatus}"
elsif status.signaled?
", uncaught signal #{status.termsig}"
end
raise Gem::CompilerError, "#{command_name} failed#{exit_reason}"
end
end
def strip_artifacts(artifacts)
return unless options[:strip]
strip_cmd = options[:strip]
info "Stripping symbols from extensions (using '#{strip_cmd}')..."
artifacts.each do |artifact|
cmd = [strip_cmd, artifact].join(' ').rstrip
simple_run(cmd, "strip #{File.basename(artifact)}") do |status, output|
if status.success?
debug "Stripped #{File.basename(artifact)}"
end
end
end
end
def tmp_dir
@tmp_dir ||= Dir.glob(Dir.mktmpdir).first
end
def unpack
basename = File.basename(@gemfile, ".gem")
@target_dir = File.join(tmp_dir, basename)
# unpack gem sources into target_dir
# We need the basename to keep the unpack happy
info "Unpacking gem: '#{basename}' in temporary directory..."
# RubyGems >= 3.1.x
if installer.respond_to?(:package)
package = installer.package
else
package = Gem::Package.new(@gemfile)
end
package.extract_files(@target_dir)
end
end