Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions lib/mruby/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def install_dir

Exts = Struct.new(:object, :executable, :library, :presym_preprocessed)

# `rake -m` resolves the dependencies of several outputs at once, and each
# of them compares its own record, so the report is guarded to print once.
FLAGS_CHANGE_LOCK = Mutex.new

def initialize(name='host', build_dir=nil, internal: false, &block)
@name = name.to_s

Expand All @@ -111,6 +115,7 @@ def initialize(name='host', build_dir=nil, internal: false, &block)
@install_excludes = []
@defines = []
@defines_final = false
@flags_change_reported = false
@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")
Expand Down Expand Up @@ -515,6 +520,34 @@ def run_bintest
sh env, "ruby #{bintest}#{verbose_flag} #{targets.join ' '}"
end

# Report that this build directory holds output produced by another
# configuration, once for the whole directory: the command line is
# recorded per output, so every output that follows carries the same
# change and would report it again.
#
# `recorded` is nil when there is no record at all, which is what a
# directory built before this check looks like.
def report_flags_change(recorded, current)
FLAGS_CHANGE_LOCK.synchronize do
return if @flags_change_reported
@flags_change_reported = true

unless recorded
warn "#{build_dir}: output here has no record of what built it, rebuilding it"
return
end

warn "#{build_dir}: output here was built by another configuration, rebuilding it"
recorded.lines.zip(current.lines).each do |before, after|
next if before == after
field = (after || before)[/\A[^:]+/]
before, after = [before, after].map {|line| line.to_s.split(": ", 2)[1].to_s.split}
warn " #{field} added: #{(after - before).join(" ")}" unless (after - before).empty?
warn " #{field} removed: #{(before - after).join(" ")}" unless (before - after).empty?
end
end
end

def print_build_summary
puts "================================================"
puts " Config Name: #{@name}"
Expand Down
112 changes: 86 additions & 26 deletions lib/mruby/build/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,19 @@ def all_flags(_defines=[], _include_paths=[], _flags=[])

def run(outfile, infile, _defines=[], _include_paths=[], _flags=[])
mkdir_p File.dirname(outfile)
flags = all_flags(_defines, _include_paths, _flags)
flags = compile_flags(outfile, _defines, _include_paths, _flags)
if object_ext?(outfile)
label = @label
opts = compile_options
else
label = "CPP"
opts = preprocess_options
flags << " -DMRB_PRESYM_SCANNING"
end
_pp label, infile.relative_path, outfile.relative_path
_run opts, flags: flags, infile: filename(infile), outfile: filename(outfile)
# Recorded after the compile, so that a compile that failed leaves
# nothing claiming a configuration its output was not built with.
File.write(flags_file(outfile), flags_record(opts, flags))
end

def define_rules(build_dir, source_dir='', out_ext=build.exts.object)
Expand All @@ -130,26 +132,16 @@ def define_rules(build_dir, source_dir='', out_ext=build.exts.object)
generated_file_matcher = Regexp.new("^#{Regexp.escape build_dir}/(?!mrbc/|mrbgems/.+/)(.*)#{Regexp.escape out_ext}$")
end
source_exts.each do |ext|
rule generated_file_matcher => [
proc { |file|
file.sub(generated_file_matcher, "#{source_dir}/\\1#{ext}")
},
proc { |file|
get_dependencies(file) + rakedep
}
] do |t|
run t.name, t.prerequisites.first
end

rule generated_file_matcher => [
proc { |file|
file.sub(generated_file_matcher, "#{build_dir}/\\1#{ext}")
},
proc { |file|
get_dependencies(file) + rakedep
}
] do |t|
run t.name, t.prerequisites.first
# The source is looked for beside the sources first and among the
# generated files second.
[source_dir, build_dir].each do |dir|
source_of = proc { |file| file.sub(generated_file_matcher, "#{dir}/\\1#{ext}") }
rule generated_file_matcher => [
source_of,
proc { |file| get_dependencies(file, source_of.call(file)) + rakedep }
] do |t|
run t.name, t.prerequisites.first
end
end
end
end
Expand Down Expand Up @@ -183,11 +175,13 @@ def setup_debug(conf)
#
# /src/value_array.h:
#
def get_dependencies(file)
def get_dependencies(file, source)
discard_foreign_output(file) if rule_applies?(source)
deps = [MRUBY_CONFIG]
dep_file = file.ext(".d")
return [MRUBY_CONFIG] unless object_ext?(file) && File.exist?(dep_file)
return deps unless object_ext?(file) && File.exist?(dep_file)

deps = File.read(dep_file).gsub("\\\n ", "").split("\n").map do |dep_line|
header_deps = File.read(dep_file).gsub("\\\n ", "").split("\n").map do |dep_line|
# dep_line:
# - "/build/host/src/array.o: /src/array.c /include/mruby/common.h ..."
# - ""
Expand All @@ -197,7 +191,73 @@ def get_dependencies(file)
# []
# []
end.flatten.uniq
deps << MRUBY_CONFIG
deps.concat(header_deps)
end

#
# === Example of +.flags+ file
#
# command: gcc
# options: -MMD -c %{flags} -o "%{outfile}" "%{infile}"
# flags: -std=gnu99 -g -O3 -Wall -DMRB_NO_FLOAT -I"/mruby/include"
#
def flags_record(options, flags)
"command: #{build.filename(command)}\noptions: #{options}\nflags: #{flags}\n"
end

# The record sits beside the output under the output's own name, so that
# the object and the presym preprocess of one source keep a record each.
def flags_file(outfile)
"#{outfile}.flags"
end

# Whether the rule this source belongs to is the one that builds the
# output. Rake asks the same of a rule before it applies it, and it asks
# every rule that matches the name: the compilers of a build define a rule
# each for the same output, differing in the extension of the source they
# look for. Only the one that finds its source speaks for the output; the
# others would compare it against flags no compile of it ever used.
#
# Rake accepts one source more than this, one another rule can produce:
# `Rake::TaskManager#attempt_rule` falls back to
# `enhance_with_matching_rule` for a source that is neither a file nor a
# task. The rules here are the only ones in the tree and they match object
# names, so nothing answers that fallback; a generated source is a `file`
# task, which `Rake::Task.task_defined?` already finds.
def rule_applies?(source)
File.exist?(source) || Rake::Task.task_defined?(source)
end

# Remove an output that the command line beside it does not answer for,
# so that the rule that would have found it up to date builds it again.
#
# Nothing else in the build tells the two apart. A +.d+ file lists header
# dependencies only, and the config file is a dependency by path, so its
# mtime does not move when another config takes over the same build
# directory. Left uncompared, the output stays up to date against every
# dependency it has, and the build silently keeps the flags of whichever
# config wrote it first, its defines above all.
#
# An output with no record at all counts as foreign too, since what
# produced it is unknown.
def discard_foreign_output(file)
return unless File.exist?(file)
path = flags_file(file)
options = object_ext?(file) ? compile_options : preprocess_options
record = flags_record(options, compile_flags(file))
recorded = File.read(path) if File.exist?(path)
return if recorded == record
build.report_flags_change(recorded, record)
rm_f file
end

# The flags a compile of `outfile` runs with. The preprocess that feeds
# the presym scan is this compiler with one define more, so the define
# belongs to the flags and to what is recorded of them.
def compile_flags(outfile, _defines=[], _include_paths=[], _flags=[])
flags = all_flags(_defines, _include_paths, _flags)
flags += " -DMRB_PRESYM_SCANNING" unless object_ext?(outfile)
flags
end

def object_ext?(path)
Expand Down
10 changes: 8 additions & 2 deletions tasks/presym.rake
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ all_prerequisites = ->(task_name, prereqs) do
end
end

# Every target first, before the walk below resolves a rule: the products of
# one target reach the objects of another (a build reaches the mrbc build it
# generated), and a rule resolved for those objects must see the same include
# paths as the compile that follows it.
MRuby.each_target do |build|
presym = build.presym

include_dir = "#{build.build_dir}/include"
build.compilers.each{|c| c.include_paths << include_dir}
build.gems.each{|gem| gem.compilers.each{|c| c.include_paths << include_dir}}
end

MRuby.each_target do |build|
presym = build.presym

prereqs = {}
ppps = []
Expand Down
Loading