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>
642 lines
18 KiB
Ruby
642 lines
18 KiB
Ruby
require "mruby/core_ext"
|
|
require "mruby/build/load_gems"
|
|
require "mruby/build/command"
|
|
autoload :Find, "find"
|
|
|
|
module MRuby
|
|
autoload :Gem, "mruby/gem"
|
|
autoload :Lockfile, "mruby/lockfile"
|
|
autoload :Presym, "mruby/presym"
|
|
|
|
INSTALL_PREFIX = ENV['PREFIX'] || ENV['INSTALL_PREFIX'] || '/usr/local'
|
|
INSTALL_DESTDIR = ENV['DESTDIR'] || ''
|
|
|
|
class << self
|
|
def targets
|
|
@targets ||= {}
|
|
end
|
|
|
|
def each_target(&block)
|
|
return to_enum(:each_target) if block.nil?
|
|
@targets.each do |key, target|
|
|
target.instance_eval(&block)
|
|
end
|
|
end
|
|
end
|
|
|
|
class Toolchain
|
|
class << self
|
|
attr_accessor :toolchains
|
|
|
|
def guess
|
|
if cc = ENV["CC"] || ENV["CXX"]
|
|
return "clang" if cc.include?("clang")
|
|
else
|
|
return "clang" if RUBY_PLATFORM =~ /darwin|(?:free|open)bsd/
|
|
return "gcc" if RUBY_PLATFORM.include?("cygwin")
|
|
return "visualcpp" if ENV.include?("VisualStudioVersion")
|
|
return "visualcpp" if ENV.include?("VSINSTALLDIR")
|
|
end
|
|
"gcc"
|
|
end
|
|
end
|
|
|
|
def initialize(name, &block)
|
|
@name, @initializer = name.to_s, block
|
|
MRuby::Toolchain.toolchains[@name] = self
|
|
end
|
|
|
|
def setup(conf, params={})
|
|
conf.instance_exec(conf, params, &@initializer)
|
|
end
|
|
|
|
self.toolchains = {}
|
|
end
|
|
|
|
class Build
|
|
class << self
|
|
attr_accessor :current
|
|
|
|
def mruby_config_path
|
|
path = ENV['MRUBY_CONFIG'] || ENV['CONFIG']
|
|
if path.nil? || path.empty?
|
|
path = if Dir.pwd != MRUBY_ROOT && File.file?("./build_config.rb")
|
|
"./build_config.rb"
|
|
else
|
|
"#{MRUBY_ROOT}/build_config/default.rb"
|
|
end
|
|
elsif !File.file?(path) && !Pathname.new(path).absolute?
|
|
f = "#{MRUBY_ROOT}/build_config/#{path}.rb"
|
|
path = File.exist?(f) ? f : File.extname(path).empty? ? f : path
|
|
end
|
|
path
|
|
end
|
|
|
|
def install_dir
|
|
@install_dir ||= ENV['INSTALL_DIR'] || "#{MRUBY_ROOT}/bin"
|
|
end
|
|
end
|
|
|
|
include Rake::DSL
|
|
include LoadGems
|
|
attr_accessor :name, :bins, :exts, :file_separator, :build_dir, :gem_clone_dir, :defines, :libdir_name
|
|
attr_reader :products, :libmruby_core_objs, :libmruby_objs, :gems, :toolchains, :presym, :mrbc_build, :gem_dir_to_repo_url
|
|
attr_reader :install_excludes, :port_names
|
|
|
|
alias libmruby libmruby_objs
|
|
|
|
COMPILERS = %w(cc cxx objc asm)
|
|
COMMANDS = COMPILERS + %w(linker archiver yacc gperf git exts mrbc)
|
|
attr_block MRuby::Build::COMMANDS
|
|
|
|
Exts = Struct.new(:object, :executable, :library, :presym_preprocessed)
|
|
|
|
def initialize(name='host', build_dir=nil, internal: false, &block)
|
|
@name = name.to_s
|
|
|
|
unless current = MRuby.targets[@name]
|
|
if ENV['OS'] == 'Windows_NT'
|
|
@exts = Exts.new('.o', '.exe', '.a', '.pi')
|
|
else
|
|
@exts = Exts.new('.o', '', '.a', '.pi')
|
|
end
|
|
|
|
build_dir = build_dir || ENV['MRUBY_BUILD_DIR'] || "#{MRUBY_ROOT}/build"
|
|
|
|
@file_separator = '/'
|
|
@build_dir = "#{build_dir}/#{@name}"
|
|
@gem_clone_dir = "#{build_dir}/repos/#{@name}"
|
|
@libdir_name = (self.kind_of?(MRuby::CrossBuild) ? nil : ENV["MRUBY_SYSTEM_LIBDIR_NAME"]) || "lib"
|
|
@install_prefix = nil
|
|
@install_excludes = []
|
|
@defines = []
|
|
@cc = Command::Compiler.new(self, %w(.c), label: "CC")
|
|
@cxx = Command::Compiler.new(self, %w(.cc .cxx .cpp), label: "CXX")
|
|
@objc = Command::Compiler.new(self, %w(.m), label: "OBJC")
|
|
@asm = Command::Compiler.new(self, %w(.S .asm .s), label: "ASM")
|
|
@linker = Command::Linker.new(self)
|
|
@archiver = Command::Archiver.new(self)
|
|
@yacc = Command::Yacc.new(self)
|
|
@gperf = Command::Gperf.new(self)
|
|
@git = Command::Git.new(self)
|
|
@mrbc = Command::Mrbc.new(self)
|
|
|
|
@products = []
|
|
@bins = []
|
|
@gems = MRuby::Gem::List.new
|
|
@libmruby_core_objs = []
|
|
@libmruby_objs = [@libmruby_core_objs]
|
|
@enable_libmruby = true
|
|
@build_mrbtest_lib_only = false
|
|
@cxx_exception_enabled = false
|
|
@cxx_exception_disabled = false
|
|
@cxx_abi_enabled = false
|
|
@enable_bintest = false
|
|
@enable_test = false
|
|
@enable_lock = true
|
|
@enable_benchmark = true
|
|
@mrbcfile_external = false
|
|
@internal = internal
|
|
@toolchains = []
|
|
@port_names = nil
|
|
@gem_dir_to_repo_url = {}
|
|
|
|
# Add lambda instead of string because libdir_name or lib may be changed by user configuration
|
|
libmruby_core_name = nil
|
|
@install_excludes << ->(file) {
|
|
libmruby_core_name ||= File.join(libdir_name, libfile("libmruby_core"))
|
|
file == libmruby_core_name
|
|
}
|
|
|
|
MRuby.targets[@name] = current = self
|
|
end
|
|
|
|
MRuby::Build.current = current
|
|
begin
|
|
current.instance_eval(&block)
|
|
ensure
|
|
if current.libmruby_enabled? && !current.mrbcfile_external?
|
|
current.create_mrbc_build if current.host? || current.gems["mruby-bin-mrbc"]
|
|
end
|
|
current.presym = Presym.new(current)
|
|
end
|
|
end
|
|
|
|
def libmruby_enabled?
|
|
@enable_libmruby
|
|
end
|
|
|
|
def disable_libmruby
|
|
@enable_libmruby = false
|
|
end
|
|
|
|
def debug_enabled?
|
|
@enable_debug
|
|
end
|
|
|
|
def enable_debug
|
|
compilers.each do |c|
|
|
c.defines += %w(MRB_DEBUG)
|
|
c.setup_debug(self)
|
|
end
|
|
@mrbc.compile_options += ' -g'
|
|
|
|
@enable_debug = true
|
|
end
|
|
|
|
# Set target port names for this build.
|
|
# Each gem compiles the first matching ports/<name>/ directory;
|
|
# later names in the list act as fallbacks for gems that don't
|
|
# ship a port for the earlier names.
|
|
# conf.ports :esp32
|
|
# conf.ports :rp2040, :posix # use rp2040 if available, else posix
|
|
def ports(*names)
|
|
@port_names = names.map { |n| n.to_s }
|
|
end
|
|
|
|
# Returns the effective port names for this build.
|
|
# If not explicitly set, auto-detects :posix or :win for host builds.
|
|
def effective_ports
|
|
return @port_names if @port_names
|
|
if kind_of?(MRuby::CrossBuild)
|
|
[]
|
|
elsif ENV['OS'] == 'Windows_NT' ||
|
|
('A'..'Z').any? { |v| Dir.exist?("#{v}:") }
|
|
['win']
|
|
else
|
|
['posix']
|
|
end
|
|
end
|
|
|
|
def disable_lock
|
|
@enable_lock = false
|
|
end
|
|
|
|
def lock_enabled?
|
|
Lockfile.enabled? && @enable_lock
|
|
end
|
|
|
|
def disable_cxx_exception
|
|
if @cxx_exception_enabled or @cxx_abi_enabled
|
|
raise "cxx_exception already enabled"
|
|
end
|
|
@cxx_exception_disabled = true
|
|
end
|
|
|
|
def enable_cxx_exception
|
|
return if @cxx_exception_enabled
|
|
return if @cxx_abi_enabled
|
|
if @cxx_exception_disabled
|
|
raise "cxx_exception disabled"
|
|
end
|
|
@cxx_exception_enabled = true
|
|
compilers.each { |c|
|
|
c.defines += %w(MRB_USE_CXX_EXCEPTION)
|
|
c.flags << c.cxx_exception_flag
|
|
}
|
|
linker.command = cxx.command if toolchains.find { |v| v == 'gcc' }
|
|
end
|
|
|
|
def cxx_exception_enabled?
|
|
@cxx_exception_enabled
|
|
end
|
|
|
|
def cxx_abi_enabled?
|
|
@cxx_abi_enabled
|
|
end
|
|
|
|
def enable_cxx_abi
|
|
return if @cxx_abi_enabled
|
|
if @cxx_exception_enabled
|
|
raise "cxx_exception already enabled"
|
|
end
|
|
compilers.each { |c|
|
|
c.defines += %w(MRB_USE_CXX_EXCEPTION MRB_USE_CXX_ABI)
|
|
c.flags << c.cxx_compile_flag
|
|
c.flags = c.flags.flatten - c.cxx_invalid_flags.flatten
|
|
}
|
|
linker.command = cxx.command if toolchains.find { |v| v == 'gcc' }
|
|
@cxx_abi_enabled = true
|
|
end
|
|
|
|
def benchmark_enabled?
|
|
@enable_benchmark
|
|
end
|
|
|
|
def disable_benchmark
|
|
@enable_benchmark = false
|
|
end
|
|
|
|
def compile_as_cxx(src, cxx_src = nil, obj = nil, includes = [])
|
|
#
|
|
# If `cxx_src` is specified, this method behaves the same as before as
|
|
# compatibility mode, but `.d` file is not read.
|
|
#
|
|
# If `cxx_src` is omitted, `.d` file is read by using mruby standard
|
|
# Rake rule (C++ source name is also changed).
|
|
#
|
|
if cxx_src
|
|
obj ||= cxx_src + @exts.object
|
|
dsts = [obj]
|
|
dsts << (cxx_src + @exts.presym_preprocessed)
|
|
defines = []
|
|
include_paths = ["#{MRUBY_ROOT}/src", *includes]
|
|
dsts.each do |dst|
|
|
file dst => cxx_src do |t|
|
|
cxx.run t.name, t.prerequisites.first, defines, include_paths
|
|
end
|
|
end
|
|
else
|
|
cxx_src = "#{build_dir}/#{src.relative_path.to_s.remove_leading_parents}".ext << "-cxx.cxx"
|
|
obj = cxx_src.ext(@exts.object)
|
|
end
|
|
|
|
file cxx_src => [src, __FILE__] do |t|
|
|
mkdir_p File.dirname t.name
|
|
IO.write t.name, <<EOS
|
|
#define __STDC_CONSTANT_MACROS
|
|
#define __STDC_LIMIT_MACROS
|
|
|
|
#ifndef MRB_USE_CXX_ABI
|
|
extern "C" {
|
|
#endif
|
|
#include "#{File.absolute_path src}"
|
|
#ifndef MRB_USE_CXX_ABI
|
|
}
|
|
#endif
|
|
EOS
|
|
end
|
|
|
|
obj
|
|
end
|
|
|
|
def enable_bintest
|
|
@enable_bintest = true
|
|
end
|
|
|
|
def bintest_enabled?
|
|
@enable_bintest
|
|
end
|
|
|
|
def toolchain(name=Toolchain.guess, params={})
|
|
name = name.to_s
|
|
tc = Toolchain.toolchains[name] || begin
|
|
path = "#{MRUBY_ROOT}/tasks/toolchains/#{name}.rake"
|
|
fail "Unknown #{name} toolchain" unless File.exist?(path)
|
|
load path
|
|
Toolchain.toolchains[name]
|
|
end
|
|
tc.setup(self, params)
|
|
@toolchains.unshift name
|
|
end
|
|
|
|
def primary_toolchain
|
|
@toolchains.first
|
|
end
|
|
|
|
def root
|
|
MRUBY_ROOT
|
|
end
|
|
|
|
def enable_test
|
|
@enable_test = true
|
|
end
|
|
alias build_mrbtest enable_test
|
|
|
|
def test_enabled?
|
|
@enable_test
|
|
end
|
|
|
|
def build_mrbc_exec
|
|
gem :core => 'mruby-bin-mrbc' unless @gems['mruby-bin-mrbc']
|
|
end
|
|
|
|
def locks
|
|
Lockfile.build(@name)
|
|
end
|
|
|
|
def mrbcfile
|
|
return @mrbcfile if @mrbcfile
|
|
|
|
gem_name = "mruby-bin-mrbc"
|
|
if (gem = @gems[gem_name])
|
|
@mrbcfile = exefile("#{gem.build.build_dir}/bin/mrbc")
|
|
elsif !host? && (host = MRuby.targets["host"])
|
|
if (gem = host.gems[gem_name])
|
|
@mrbcfile = exefile("#{gem.build.build_dir}/bin/mrbc")
|
|
elsif host.mrbcfile_external?
|
|
@mrbcfile = host.mrbcfile
|
|
end
|
|
end
|
|
@mrbcfile || fail("external mrbc or mruby-bin-mrbc gem in current('#{@name}') or 'host' build is required")
|
|
end
|
|
|
|
def mrbcfile=(path)
|
|
@mrbcfile = path
|
|
@mrbcfile_external = true
|
|
end
|
|
|
|
def mrbcfile_external?
|
|
@mrbcfile_external
|
|
end
|
|
|
|
def compilers
|
|
COMPILERS.map do |c|
|
|
instance_variable_get("@#{c}")
|
|
end
|
|
end
|
|
|
|
def define_rules
|
|
compilers.each do |compiler|
|
|
compiler.defines << "MRB_NO_GEMS" unless enable_gems? && libmruby_enabled?
|
|
end
|
|
[@cc, *(@cxx if cxx_exception_enabled?)].each do |compiler|
|
|
compiler.define_rules(@build_dir, MRUBY_ROOT, @exts.object)
|
|
compiler.define_rules(@build_dir, MRUBY_ROOT, @exts.presym_preprocessed)
|
|
end
|
|
end
|
|
|
|
def define_installer_outline(src, dst)
|
|
file dst => src do
|
|
_pp "GEN", src.relative_path, dst.relative_path
|
|
mkdir_p(File.dirname(dst))
|
|
yield dst
|
|
end
|
|
dst
|
|
end
|
|
|
|
if ENV['OS'] == 'Windows_NT'
|
|
def define_installer(src)
|
|
dst = "#{self.class.install_dir}/#{File.basename(src)}".pathmap("%X.bat")
|
|
define_installer_outline(src, dst) do
|
|
File.write dst, <<~BATCHFILE
|
|
@echo off
|
|
call "#{File.expand_path(src)}" %*
|
|
BATCHFILE
|
|
end
|
|
end
|
|
else
|
|
def define_installer(src)
|
|
dst = "#{self.class.install_dir}/#{File.basename(src)}"
|
|
define_installer_outline(src, dst) do
|
|
File.unlink(dst) rescue nil
|
|
File.symlink(src.relative_path_from(self.class.install_dir), dst)
|
|
end
|
|
end
|
|
end
|
|
|
|
def define_installer_if_needed(bin)
|
|
exe = exefile("#{build_dir}/bin/#{bin}")
|
|
host? ? define_installer(exe) : exe
|
|
end
|
|
|
|
def filename(name)
|
|
if name.is_a?(Array)
|
|
name.flatten.map { |n| filename(n) }
|
|
else
|
|
name.gsub('/', file_separator)
|
|
end
|
|
end
|
|
|
|
def exefile(name)
|
|
if name.is_a?(Array)
|
|
name.flatten.map { |n| exefile(n) }
|
|
elsif File.extname(name).empty?
|
|
"#{name}#{exts.executable}"
|
|
else
|
|
# `name` sometimes have (non-standard) extension (e.g. `.bat`).
|
|
name
|
|
end
|
|
end
|
|
|
|
def objfile(name)
|
|
if name.is_a?(Array)
|
|
name.flatten.map { |n| objfile(n) }
|
|
else
|
|
"#{name}#{exts.object}"
|
|
end
|
|
end
|
|
|
|
def libfile(name)
|
|
if name.is_a?(Array)
|
|
name.flatten.map { |n| libfile(n) }
|
|
else
|
|
"#{name}#{exts.library}"
|
|
end
|
|
end
|
|
|
|
def build_mrbtest_lib_only
|
|
@build_mrbtest_lib_only = true
|
|
end
|
|
|
|
def build_mrbtest_lib_only?
|
|
@build_mrbtest_lib_only
|
|
end
|
|
|
|
def verbose_flag
|
|
Rake.verbose ? ' -v' : ''
|
|
end
|
|
|
|
def run_test
|
|
puts ">>> Test #{name} <<<"
|
|
mrbtest = exefile("#{build_dir}/bin/mrbtest")
|
|
sh "#{filename mrbtest.relative_path}#{verbose_flag}"
|
|
puts
|
|
end
|
|
|
|
def run_bintest
|
|
puts ">>> Bintest #{name} <<<"
|
|
targets = @gems.select { |v| File.directory? "#{v.dir}/bintest" }.map { |v| filename v.dir }
|
|
mrbc = @gems["mruby-bin-mrbc"] ? exefile("#{@build_dir}/bin/mrbc") : mrbcfile
|
|
env = {"BUILD_DIR" => @build_dir, "MRBCFILE" => mrbc}
|
|
bintest = File.join(MRUBY_ROOT, "test/bintest.rb")
|
|
sh env, "ruby #{bintest}#{verbose_flag} #{targets.join ' '}"
|
|
end
|
|
|
|
def print_build_summary
|
|
puts "================================================"
|
|
puts " Config Name: #{@name}"
|
|
puts " Output Directory: #{self.build_dir.relative_path}"
|
|
puts " Binaries: #{@bins.join(', ')}" unless @bins.empty?
|
|
unless @gems.empty?
|
|
puts " Included Gems:"
|
|
gems = @gems.sort_by { |gem| gem.name }
|
|
gems.each do |gem|
|
|
gem_version = " - #{gem.version}" if gem.version != '0.0.0'
|
|
gem_summary = " - #{gem.summary}" if gem.summary
|
|
puts " #{gem.name}#{gem_version}#{gem_summary}"
|
|
puts " - Binaries: #{gem.bins.join(', ')}" unless gem.bins.empty?
|
|
end
|
|
end
|
|
puts "================================================"
|
|
puts
|
|
end
|
|
|
|
def libmruby_static
|
|
libfile("#{build_dir}/#{libdir_name}/libmruby")
|
|
end
|
|
|
|
def libmruby_core_static
|
|
libfile("#{build_dir}/#{libdir_name}/libmruby_core")
|
|
end
|
|
|
|
def libraries
|
|
[libmruby_static]
|
|
end
|
|
|
|
def host?
|
|
@name == "host"
|
|
end
|
|
|
|
def internal?
|
|
@internal
|
|
end
|
|
|
|
def each_header_files(&block)
|
|
return to_enum(__method__) unless block
|
|
|
|
basedir = File.join(MRUBY_ROOT, "include")
|
|
Find.find(basedir) do |d|
|
|
next unless File.file? d
|
|
yield d
|
|
end
|
|
|
|
@gems.each { |g| g.each_header_files(&block) }
|
|
|
|
self
|
|
end
|
|
|
|
def install_prefix
|
|
@install_prefix || (self.name == "host" ? MRuby::INSTALL_PREFIX :
|
|
File.join(MRuby::INSTALL_PREFIX, "mruby/#{self.name}"))
|
|
end
|
|
|
|
def install_prefix=(dir)
|
|
@install_prefix = dir&.to_s
|
|
end
|
|
|
|
protected
|
|
|
|
attr_writer :presym
|
|
|
|
def create_mrbc_build
|
|
exclusions = %i[@name @build_dir @gems @enable_test @enable_bintest @internal @install_excludes]
|
|
name = "#{@name}/mrbc"
|
|
MRuby.targets.delete(name)
|
|
build = self.class.new(name, internal: true){}
|
|
build.build_dir = "#{@build_dir}/mrbc"
|
|
instance_variables.each do |n|
|
|
next if exclusions.include?(n)
|
|
v = instance_variable_get(n)
|
|
v = case v
|
|
when nil, true, false, Numeric; v
|
|
when String; v.clone
|
|
when Command; v.clone.tap { |u| u.build = build }
|
|
else Marshal.load(Marshal.dump(v)) # deep clone
|
|
end
|
|
build.instance_variable_set(n, v)
|
|
end
|
|
build.build_mrbc_exec
|
|
build.disable_libmruby
|
|
build.presym = Presym.new(build)
|
|
@mrbc_build = build
|
|
self.mrbcfile = build.mrbcfile
|
|
build
|
|
end
|
|
end # Build
|
|
|
|
class CrossBuild < Build
|
|
attr_block %w(test_runner)
|
|
# cross compiling targets for building native extensions.
|
|
# host - arch of where the built binary will run
|
|
# build - arch of the machine building the binary
|
|
attr_accessor :host_target, :build_target
|
|
|
|
def initialize(name, build_dir=nil, &block)
|
|
@test_runner = Command::CrossTestRunner.new(self)
|
|
super
|
|
unless mrbcfile_external? || MRuby.targets['host']
|
|
# add minimal 'host'
|
|
MRuby::Build.new('host') do |conf|
|
|
conf.toolchain
|
|
conf.build_mrbc_exec
|
|
conf.disable_libmruby
|
|
end
|
|
end
|
|
end
|
|
|
|
def mrbcfile
|
|
mrbcfile_external? ? super : MRuby::targets['host'].mrbcfile
|
|
end
|
|
|
|
def run_test
|
|
@test_runner.runner_options << verbose_flag
|
|
mrbtest = exefile("#{build_dir}/bin/mrbtest")
|
|
if (@test_runner.command == nil)
|
|
puts "You should run #{mrbtest} on target device."
|
|
puts
|
|
else
|
|
@test_runner.run(mrbtest)
|
|
end
|
|
end
|
|
|
|
def run_bintest
|
|
puts ">>> Bintest #{name} <<<"
|
|
targets = @gems.select { |v| File.directory? "#{v.dir}/bintest" }.map { |v| filename v.dir }
|
|
mrbc = @gems["mruby-bin-mrbc"] ? exefile("#{@build_dir}/bin/mrbc") : mrbcfile
|
|
|
|
env = {
|
|
"BUILD_DIR" => @build_dir,
|
|
"MRBCFILE" => mrbc,
|
|
"EMULATOR" => @test_runner.emulator,
|
|
}
|
|
bintest = File.join(MRUBY_ROOT, "test/bintest.rb")
|
|
sh env, "ruby #{bintest}#{verbose_flag} #{targets.join ' '}"
|
|
end
|
|
|
|
protected
|
|
|
|
def create_mrbc_build; end
|
|
end # CrossBuild
|
|
end # MRuby
|