mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
f22a991065
When `conf.ports` was given multiple names, every matching ports/ directory for each gem was compiled -- fine for single-name builds and for gems that have at most one matching port, but a footgun once HAL-using gems start providing multiple alternative ports (e.g. mruby-task with both posix and an alternative runloop). Iterate the chain and break on the first match. Existing builds that pass at most one name per gem keep the same behavior; `conf.ports :rp2040, :posix` now reads as "use rp2040 if a gem has that port, otherwise posix" rather than "compile both". ref #6825 Co-authored-by: Claude <noreply@anthropic.com>
614 lines
18 KiB
Ruby
614 lines
18 KiB
Ruby
require 'forwardable'
|
|
autoload :TSort, 'tsort'
|
|
autoload :Shellwords, 'shellwords'
|
|
|
|
module MRuby
|
|
module Gem
|
|
class << self
|
|
attr_accessor :current
|
|
end
|
|
|
|
class Specification
|
|
include Rake::DSL
|
|
extend Forwardable
|
|
def_delegators :@build, :filename, :objfile, :libfile, :exefile
|
|
|
|
attr_accessor :name, :dir, :build
|
|
alias mruby build
|
|
attr_accessor :build_config_initializer
|
|
|
|
attr_accessor :version
|
|
attr_accessor :description, :summary
|
|
attr_accessor :homepage
|
|
attr_accessor :licenses, :authors
|
|
alias :license= :licenses=
|
|
alias :author= :authors=
|
|
|
|
attr_accessor :rbfiles, :objs
|
|
attr_writer :test_objs, :test_rbfiles
|
|
attr_accessor :test_args, :test_preload
|
|
|
|
attr_accessor :bins
|
|
|
|
attr_accessor :requirements
|
|
attr_reader :dependencies, :conflicts
|
|
|
|
attr_accessor :export_include_paths
|
|
|
|
attr_reader :generate_functions
|
|
attr_writer :skip_test
|
|
|
|
attr_block MRuby::Build::COMMANDS
|
|
|
|
def initialize(name, &block)
|
|
@name = name
|
|
@initializer = block
|
|
@post_user_config = nil
|
|
@version = "0.0.0"
|
|
@dependencies = []
|
|
@conflicts = []
|
|
MRuby::Gem.current = self
|
|
end
|
|
|
|
def setup
|
|
return if defined?(@bins) # return if already set up
|
|
|
|
MRuby::Gem.current = self
|
|
reset_commands # for backward compatibility, reset the commands from the beginning.
|
|
@build_settings = nil
|
|
|
|
@rbfiles = Dir.glob("#{@dir}/mrblib/**/*.rb").sort
|
|
@objs = srcs_to_objs("src")
|
|
|
|
# Add platform-specific sources from the first matching
|
|
# ports/<name>/ directory. effective_ports is a fallback
|
|
# chain: later names act as defaults for gems that don't ship
|
|
# a port for the earlier names.
|
|
build.effective_ports.each do |port|
|
|
port_dir = "#{@dir}/ports/#{port}"
|
|
if File.directory?(port_dir)
|
|
@objs += srcs_to_objs("ports/#{port}")
|
|
break
|
|
end
|
|
end
|
|
|
|
@test_preload = nil # 'test/assert.rb'
|
|
@test_args = {}
|
|
@skip_test = false
|
|
|
|
@bins = []
|
|
@cdump = true
|
|
|
|
@requirements = []
|
|
@export_include_paths = []
|
|
# Headers in include/ are for inter-gem use only
|
|
# Headers in include/export/ are exported to external users via mruby-config
|
|
export_dir = "#{dir}/include/export"
|
|
@export_include_paths << export_dir if File.directory?(export_dir)
|
|
|
|
instance_eval(&@initializer)
|
|
|
|
@generate_functions = !(@rbfiles.empty? && @objs.empty?)
|
|
@objs << objfile("#{build_dir}/gem_init") if @generate_functions
|
|
|
|
if !name || !licenses || !authors
|
|
fail "#{name || dir} required to set name, license(s) and author(s)"
|
|
end
|
|
|
|
build.libmruby_objs << @objs
|
|
|
|
instance_eval(&@build_config_initializer) if @build_config_initializer
|
|
instance_eval(&@post_user_config) if @post_user_config
|
|
|
|
repo_url = build.gem_dir_to_repo_url[dir]
|
|
build.locks[repo_url]['version'] = version if repo_url
|
|
end
|
|
|
|
def skip_test?
|
|
@skip_test
|
|
end
|
|
|
|
def setup_compilers
|
|
(core? ? [@cc, *(@cxx if build.cxx_exception_enabled?)] : compilers).each do |compiler|
|
|
compiler.define_rules build_dir, @dir, @build.exts.presym_preprocessed
|
|
compiler.define_rules build_dir, @dir, @build.exts.object
|
|
compiler.defines << %Q[MRBGEM_#{funcname.upcase}_VERSION=#{version}]
|
|
compiler.include_paths << "#{@dir}/include" if File.directory? "#{@dir}/include"
|
|
end
|
|
|
|
define_gem_init_builder if @generate_functions
|
|
end
|
|
|
|
def for_windows?
|
|
if build.kind_of?(MRuby::CrossBuild)
|
|
return %w(x86_64-w64-mingw32 i686-w64-mingw32).include?(build.host_target)
|
|
elsif build.kind_of?(MRuby::Build)
|
|
return ('A'..'Z').to_a.any? { |vol| Dir.exist?("#{vol}:") } ||
|
|
('a'..'z').to_a.any? { |vol| Dir.exist?("/#{vol}/") }
|
|
end
|
|
return false
|
|
end
|
|
|
|
def disable_cdump
|
|
@cdump = false
|
|
end
|
|
|
|
def cdump?
|
|
@cdump
|
|
end
|
|
|
|
def core?
|
|
@dir.start_with?("#{MRUBY_ROOT}/mrbgems/")
|
|
end
|
|
|
|
def bin?
|
|
@bins.size > 0
|
|
end
|
|
|
|
def add_dependency(name, *requirements)
|
|
default_gem = requirements.last.kind_of?(Hash) ? requirements.pop : nil
|
|
requirements = ['>= 0.0.0'] if requirements.empty?
|
|
requirements.flatten!
|
|
@dependencies << {:gem => name, :requirements => requirements, :default => default_gem}
|
|
end
|
|
|
|
def add_test_dependency(*args)
|
|
add_dependency(*args) if build.test_enabled? || build.bintest_enabled?
|
|
end
|
|
|
|
def add_conflict(name, *req)
|
|
@conflicts << {:gem => name, :requirements => req.empty? ? nil : req}
|
|
end
|
|
|
|
def build_dir
|
|
"#{build.build_dir}/mrbgems/#{name}"
|
|
end
|
|
|
|
def test_rbireps
|
|
"#{build_dir}/gem_test.c"
|
|
end
|
|
|
|
def test_objs
|
|
@test_objs ||= srcs_to_objs("test")
|
|
end
|
|
|
|
def test_rbfiles
|
|
@test_rbfiles ||= Dir["#{@dir}/test/**/*.rb"].sort!
|
|
end
|
|
|
|
def search_package(name, version_query=nil)
|
|
package_query = name
|
|
package_query += " #{version_query}" if version_query
|
|
_pp "PKG-CONFIG", package_query
|
|
escaped_package_query = Shellwords.escape(package_query)
|
|
if system("pkg-config --exists #{escaped_package_query}")
|
|
cc.flags += [`pkg-config --cflags #{escaped_package_query}`.strip]
|
|
cxx.flags += [`pkg-config --cflags #{escaped_package_query}`.strip]
|
|
linker.flags_before_libraries += [`pkg-config --libs #{escaped_package_query}`.strip]
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def funcname
|
|
@funcname ||= @name.tr('-', '_')
|
|
end
|
|
|
|
def compilers
|
|
MRuby::Build::COMPILERS.map do |c|
|
|
instance_variable_get("@#{c}")
|
|
end
|
|
end
|
|
|
|
def srcs_to_objs(src_dir_from_gem_dir)
|
|
exts = compilers.flat_map{|c| c.source_exts} * ","
|
|
Dir["#{@dir}/#{src_dir_from_gem_dir}/*{#{exts}}"].map do |f|
|
|
objfile(f.relative_path_from(@dir).to_s.pathmap("#{build_dir}/%X"))
|
|
end
|
|
end
|
|
|
|
# Register a block that runs after the user's `build.gem` block has
|
|
# been processed. Intended for gem authors to fill in defaults that
|
|
# depend on user-supplied configuration (e.g. auto-detect a library
|
|
# only if the user didn't specify which to use).
|
|
#
|
|
# Initialization order:
|
|
# 1. block in `MRuby::Gem::Specification.new` (gem author)
|
|
# 2. block in `build.gem` (user's build_config)
|
|
# 3. block in `post_user_config` (gem author, this hook)
|
|
def post_user_config(&block)
|
|
@post_user_config = block
|
|
end
|
|
|
|
def build_settings(&blk)
|
|
@build_settings = blk
|
|
end
|
|
|
|
def setup_build
|
|
if @build_settings
|
|
# by this point, build.cc or other commands may have been modified.
|
|
# therefore, reset the commands again before calling build_settings.
|
|
reset_commands
|
|
@build_settings.call(self)
|
|
end
|
|
end
|
|
|
|
def define_gem_init_builder
|
|
file "#{build_dir}/gem_init.c" => [build.mrbcfile, __FILE__] + [rbfiles].flatten do |t|
|
|
mkdir_p build_dir
|
|
generate_gem_init("#{build_dir}/gem_init.c")
|
|
end
|
|
end
|
|
|
|
def generate_gem_init(fname)
|
|
_pp "GEN", fname.relative_path
|
|
open(fname, 'w') do |f|
|
|
print_gem_init_header f
|
|
unless rbfiles.empty?
|
|
opts = {cdump: cdump?, static: true}
|
|
if cdump?
|
|
build.mrbc.run f, rbfiles, "gem_mrblib_#{funcname}_proc", **opts
|
|
else
|
|
build.mrbc.run f, rbfiles, "gem_mrblib_irep_#{funcname}", **opts
|
|
end
|
|
end
|
|
f.puts %Q[void mrb_#{funcname}_gem_init(mrb_state *mrb);]
|
|
f.puts %Q[void mrb_#{funcname}_gem_final(mrb_state *mrb);]
|
|
f.puts %Q[]
|
|
f.puts %Q[void GENERATED_TMP_mrb_#{funcname}_gem_init(mrb_state *mrb) {]
|
|
f.puts %Q[ gem_mrblib_#{funcname}_proc_init_syms(mrb);] if !rbfiles.empty? && cdump?
|
|
f.puts %Q[ mrb_#{funcname}_gem_init(mrb);] if objs != [objfile("#{build_dir}/gem_init")]
|
|
unless rbfiles.empty?
|
|
if cdump?
|
|
f.puts %Q[ mrb_load_proc(mrb, gem_mrblib_#{funcname}_proc);]
|
|
else
|
|
f.puts %Q[ mrb_load_irep(mrb, gem_mrblib_irep_#{funcname});]
|
|
end
|
|
end
|
|
f.puts %Q[}]
|
|
f.puts %Q[]
|
|
f.puts %Q[void GENERATED_TMP_mrb_#{funcname}_gem_final(mrb_state *mrb) {]
|
|
f.puts %Q[ mrb_#{funcname}_gem_final(mrb);] if objs != [objfile("#{build_dir}/gem_init")]
|
|
f.puts %Q[}]
|
|
end
|
|
end # generate_gem_init
|
|
|
|
def print_gem_comment(f)
|
|
f.puts %Q[/*]
|
|
f.puts %Q[ * This file is loading the irep]
|
|
f.puts %Q[ * Ruby GEM code.]
|
|
f.puts %Q[ *]
|
|
f.puts %Q[ * This file was generated by mruby/#{__FILE__.relative_path_from(MRUBY_ROOT)}.]
|
|
f.puts %Q[ *]
|
|
f.puts %Q[ * IMPORTANT:]
|
|
f.puts %Q[ * This file was generated!]
|
|
f.puts %Q[ * All manual changes will get lost.]
|
|
f.puts %Q[ */]
|
|
end
|
|
|
|
def print_gem_init_header(f)
|
|
print_gem_comment(f)
|
|
if rbfiles.empty?
|
|
f.puts %Q[#include <mruby.h>]
|
|
else
|
|
f.puts %Q[#include <stdlib.h>]
|
|
unless cdump?
|
|
f.puts %Q[#include <mruby.h>]
|
|
f.puts %Q[#include <mruby/proc.h>]
|
|
end
|
|
end
|
|
end
|
|
|
|
def print_gem_test_header(f)
|
|
print_gem_comment(f)
|
|
f.puts %Q[#include <stdio.h>]
|
|
f.puts %Q[#include <stdlib.h>]
|
|
f.puts %Q[#include <mruby.h>]
|
|
f.puts %Q[#include <mruby/irep.h>]
|
|
f.puts %Q[#include <mruby/variable.h>]
|
|
f.puts %Q[#include <mruby/hash.h>] unless test_args.empty?
|
|
end
|
|
|
|
def custom_test_init?
|
|
!test_objs.empty?
|
|
end
|
|
|
|
def version_ok?(req_versions)
|
|
req_versions.map do |req|
|
|
cmp, ver = req.split
|
|
cmp_result = Version.new(version) <=> Version.new(ver)
|
|
case cmp
|
|
when '=' then cmp_result == 0
|
|
when '!=' then cmp_result != 0
|
|
when '>' then cmp_result == 1
|
|
when '<' then cmp_result == -1
|
|
when '>=' then cmp_result >= 0
|
|
when '<=' then cmp_result <= 0
|
|
when '~>'
|
|
Version.new(version).twiddle_wakka_ok?(Version.new(ver))
|
|
else
|
|
fail "Comparison not possible with '#{cmp}'"
|
|
end
|
|
end.all?
|
|
end
|
|
|
|
def each_header_files(&block)
|
|
return to_enum(__method__) unless block
|
|
|
|
self.export_include_paths.flatten.uniq.compact.each do |dir|
|
|
Find.find(dir) do |d|
|
|
next unless File.file? d
|
|
yield d
|
|
end
|
|
end
|
|
|
|
self
|
|
end
|
|
|
|
private def reset_commands
|
|
MRuby::Build::COMMANDS.each do |command|
|
|
instance_variable_set("@#{command}", @build.send(command).clone)
|
|
end
|
|
@linker.run_attrs.each(&:clear)
|
|
end
|
|
end # Specification
|
|
|
|
class Version
|
|
include Comparable
|
|
include Enumerable
|
|
|
|
def <=>(other)
|
|
ret = 0
|
|
own = to_enum
|
|
|
|
other.each do |oth|
|
|
begin
|
|
ret = own.next <=> oth
|
|
rescue StopIteration
|
|
ret = 0 <=> oth
|
|
end
|
|
|
|
break unless ret == 0
|
|
end
|
|
|
|
ret
|
|
end
|
|
|
|
# ~> compare algorithm
|
|
#
|
|
# Example:
|
|
# ~> 2 means >= 2.0.0 and < 3.0.0
|
|
# ~> 2.2 means >= 2.2.0 and < 3.0.0
|
|
# ~> 2.2.2 means >= 2.2.2 and < 2.3.0
|
|
def twiddle_wakka_ok?(other)
|
|
gr_or_eql = (self <=> other) >= 0
|
|
still_major_or_minor = (self <=> other.skip_major_or_minor) < 0
|
|
gr_or_eql and still_major_or_minor
|
|
end
|
|
|
|
def skip_major_or_minor
|
|
a = @ary.dup
|
|
a << 0 if a.size == 1 # ~> 2 can also be represented as ~> 2.0
|
|
a.slice!(-1)
|
|
a[-1] = a[-1].succ
|
|
a
|
|
end
|
|
|
|
def initialize(str)
|
|
@str = str
|
|
@ary = @str.split('.').map(&:to_i)
|
|
end
|
|
|
|
def each(&block); @ary.each(&block); end
|
|
def [](index); @ary[index]; end
|
|
def []=(index, value)
|
|
@ary[index] = value
|
|
@str = @ary.join('.')
|
|
end
|
|
def slice!(index)
|
|
@ary.slice!(index)
|
|
@str = @ary.join('.')
|
|
end
|
|
end # Version
|
|
|
|
class List
|
|
include Enumerable
|
|
|
|
def initialize
|
|
@ary = []
|
|
end
|
|
|
|
def each(&b)
|
|
@ary.each(&b)
|
|
self
|
|
end
|
|
|
|
def [](name)
|
|
@ary.detect {|g| g.name == name}
|
|
end
|
|
|
|
def <<(gem)
|
|
unless @ary.detect {|g| g.dir == gem.dir }
|
|
@ary << gem
|
|
else
|
|
# GEM was already added to this list
|
|
end
|
|
self
|
|
end
|
|
|
|
def empty?
|
|
@ary.empty?
|
|
end
|
|
|
|
def default_gem_params dep
|
|
if dep[:default]; dep
|
|
elsif File.exist? "#{MRUBY_ROOT}/mrbgems/#{dep[:gem]}" # check core
|
|
{ :gem => dep[:gem], :default => { :core => dep[:gem] } }
|
|
else # fallback to mgem-list
|
|
{ :gem => dep[:gem], :default => { :mgem => dep[:gem] } }
|
|
end
|
|
end
|
|
|
|
def setup(build)
|
|
gemset = nil
|
|
begin
|
|
gemset_prev = gemset
|
|
self.each(&:setup)
|
|
gemset = self.setup_dependencies(build).keys.sort
|
|
end until gemset == gemset_prev
|
|
end
|
|
|
|
def setup_build
|
|
each(&:setup_build)
|
|
self
|
|
end
|
|
|
|
def setup_dependencies(build)
|
|
gem_table = each_with_object({}) { |spec, h| h[spec.name] = spec }
|
|
|
|
default_gems = {}
|
|
each do |g|
|
|
g.dependencies.each do |dep|
|
|
default_gems[dep[:gem]] ||= default_gem_params(dep)
|
|
end
|
|
end
|
|
|
|
until default_gems.empty?
|
|
def_name, def_gem = default_gems.shift
|
|
next if gem_table[def_name]
|
|
|
|
spec = gem_table[def_name] = build.gem(def_gem[:default])
|
|
fail "Invalid gem name: #{spec.name} (Expected: #{def_name})" if spec.name != def_name
|
|
spec.setup
|
|
|
|
spec.dependencies.each do |dep|
|
|
default_gems[dep[:gem]] ||= default_gem_params(dep)
|
|
end
|
|
end
|
|
|
|
gem_table
|
|
end
|
|
|
|
def generate_gem_table(build)
|
|
gem_table = setup_dependencies(build)
|
|
|
|
each do |g|
|
|
g.dependencies.each do |dep|
|
|
name = dep[:gem]
|
|
req_versions = dep[:requirements]
|
|
dep_g = gem_table[name]
|
|
|
|
# check each GEM dependency against all available GEMs
|
|
if dep_g.nil?
|
|
fail "The GEM '#{g.name}' depends on the GEM '#{name}' but it could not be found"
|
|
end
|
|
unless dep_g.version_ok? req_versions
|
|
fail "#{name} version should be #{req_versions.join(' and ')} but was '#{dep_g.version}'"
|
|
end
|
|
end
|
|
|
|
cfls = g.conflicts.select { |c|
|
|
cfl_g = gem_table[c[:gem]]
|
|
cfl_g and cfl_g.version_ok?(c[:requirements] || ['>= 0.0.0'])
|
|
}.map { |c| "#{c[:gem]}(#{gem_table[c[:gem]].version})" }
|
|
fail "Conflicts of gem `#{g.name}` found: #{cfls.join ', '}" unless cfls.empty?
|
|
end
|
|
|
|
gem_table
|
|
end
|
|
|
|
def tsort_dependencies ary, table, all_dependency_listed = false
|
|
unless all_dependency_listed
|
|
left = ary.dup
|
|
until left.empty?
|
|
v = left.pop
|
|
table[v].dependencies.each do |dep|
|
|
left.push dep[:gem]
|
|
ary.push dep[:gem]
|
|
end
|
|
end
|
|
end
|
|
|
|
ary.uniq!
|
|
table.instance_variable_set :@root_gems, ary
|
|
class << table
|
|
include TSort
|
|
def tsort_each_node(&b)
|
|
@root_gems.each(&b)
|
|
end
|
|
|
|
def tsort_each_child(n, &b)
|
|
fetch(n).dependencies.each do |v|
|
|
b.call v[:gem]
|
|
end
|
|
end
|
|
end
|
|
|
|
begin
|
|
table.tsort.map { |v| table[v] }
|
|
rescue TSort::Cyclic => e
|
|
fail "Circular mrbgem dependency found: #{e.message}"
|
|
end
|
|
end
|
|
|
|
def check(build)
|
|
gem_table = generate_gem_table build
|
|
|
|
@ary = tsort_dependencies gem_table.keys, gem_table, true
|
|
|
|
each(&:setup_compilers)
|
|
|
|
each do |g|
|
|
import_include_paths(g)
|
|
end
|
|
end
|
|
|
|
def import_include_paths(g)
|
|
gem_table = each_with_object({}) { |spec, h| h[spec.name] = spec }
|
|
|
|
g.dependencies.each do |dep|
|
|
dep_g = gem_table[dep[:gem]]
|
|
# We can do recursive call safely
|
|
# as circular dependency has already detected in the caller.
|
|
import_include_paths(dep_g)
|
|
|
|
# Add dependency's include/ to compiler paths (for inter-gem use)
|
|
dep_include = "#{dep_g.dir}/include"
|
|
if File.directory?(dep_include)
|
|
g.compilers.each do |compiler|
|
|
compiler.include_paths << dep_include
|
|
compiler.include_paths.uniq!
|
|
end
|
|
end
|
|
|
|
# Propagate any explicitly set export_include_paths
|
|
dep_g.export_include_paths.uniq!
|
|
g.compilers.each do |compiler|
|
|
compiler.include_paths += dep_g.export_include_paths
|
|
g.export_include_paths += dep_g.export_include_paths
|
|
compiler.include_paths.uniq!
|
|
g.export_include_paths.uniq!
|
|
end
|
|
end
|
|
end
|
|
|
|
def linker_attrs(gem=nil)
|
|
gems = self.reject{|g| g.bin?} # library gems
|
|
gems << gem unless gem.nil?
|
|
gems.map{|g| g.linker.run_attrs}.transpose
|
|
end
|
|
end # List
|
|
end # Gem
|
|
|
|
GemBox = Object.new
|
|
class << GemBox
|
|
attr_accessor :path
|
|
|
|
def new(&block); block.call(self); end
|
|
def config=(obj); @config = obj; end
|
|
def gem(gemdir, &block); @config.gem(gemdir, &block); end
|
|
def gembox(gemfile); @config.gembox(gemfile); end
|
|
end # GemBox
|
|
end # MRuby
|