mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
ed935aa5be
which has been developed by Takashi Kokubun <takashikkbn@gmail> as YARV-MJIT. Many of its bugs are fixed by wanabe <s.wanabe@gmail.com>. This JIT compiler is designed to be a safe migration path to introduce JIT compiler to MRI. So this commit does not include any bytecode changes or dynamic instruction modifications, which are done in original MJIT. This commit even strips off some aggressive optimizations from YARV-MJIT, and thus it's slower than YARV-MJIT too. But it's still fairly faster than Ruby 2.5 in some benchmarks (attached below). Note that this JIT compiler passes `make test`, `make test-all`, `make test-spec` without JIT, and even with JIT. Not only it's perfectly safe with JIT disabled because it does not replace VM instructions unlike MJIT, but also with JIT enabled it stably runs Ruby applications including Rails applications. I'm expecting this version as just "initial" JIT compiler. I have many optimization ideas which are skipped for initial merging, and you may easily replace this JIT compiler with a faster one by just replacing mjit_compile.c. `mjit_compile` interface is designed for the purpose. common.mk: update dependencies for mjit_compile.c. internal.h: declare `rb_vm_insn_addr2insn` for MJIT. vm.c: exclude some definitions if `-DMJIT_HEADER` is provided to compiler. This avoids to include some functions which take a long time to compile, e.g. vm_exec_core. Some of the purpose is achieved in transform_mjit_header.rb (see `IGNORED_FUNCTIONS`) but others are manually resolved for now. Load mjit_helper.h for MJIT header. mjit_helper.h: New. This is a file used only by JIT-ed code. I'll refactor `mjit_call_cfunc` later. vm_eval.c: add some #ifdef switches to skip compiling some functions like Init_vm_eval. win32/mkexports.rb: export thread/ec functions, which are used by MJIT. include/ruby/defines.h: add MJIT_FUNC_EXPORTED macro alis to clarify that a function is exported only for MJIT. array.c: export a function used by MJIT. bignum.c: ditto. class.c: ditto. compile.c: ditto. error.c: ditto. gc.c: ditto. hash.c: ditto. iseq.c: ditto. numeric.c: ditto. object.c: ditto. proc.c: ditto. re.c: ditto. st.c: ditto. string.c: ditto. thread.c: ditto. variable.c: ditto. vm_backtrace.c: ditto. vm_insnhelper.c: ditto. vm_method.c: ditto. I would like to improve maintainability of function exports, but I believe this way is acceptable as initial merging if we clarify the new exports are for MJIT (so that we can use them as TODO list to fix) and add unit tests to detect unresolved symbols. I'll add unit tests of JIT compilations in succeeding commits. Author: Takashi Kokubun <takashikkbn@gmail.com> Contributor: wanabe <s.wanabe@gmail.com> Part of [Feature #14235] --- * Known issues * Code generated by gcc is faster than clang. The benchmark may be worse in macOS. Following benchmark result is provided by gcc w/ Linux. * Performance is decreased when Google Chrome is running * JIT can work on MinGW, but it doesn't improve performance at least in short running benchmark. * Currently it doesn't perform well with Rails. We'll try to fix this before release. --- * Benchmark reslts Benchmarked with: Intel 4.0GHz i7-4790K with 16GB memory under x86-64 Ubuntu 8 Cores - 2.0.0-p0: Ruby 2.0.0-p0 - r62186: Ruby trunk (early 2.6.0), before MJIT changes - JIT off: On this commit, but without `--jit` option - JIT on: On this commit, and with `--jit` option ** Optcarrot fps Benchmark: https://github.com/mame/optcarrot | |2.0.0-p0 |r62186 |JIT off |JIT on | |:--------|:--------|:--------|:--------|:--------| |fps |37.32 |51.46 |51.31 |58.88 | |vs 2.0.0 |1.00x |1.38x |1.37x |1.58x | ** MJIT benchmarks Benchmark: https://github.com/benchmark-driver/mjit-benchmarks (Original: https://github.com/vnmakarov/ruby/tree/rtl_mjit_branch/MJIT-benchmarks) | |2.0.0-p0 |r62186 |JIT off |JIT on | |:----------|:--------|:--------|:--------|:--------| |aread |1.00 |1.09 |1.07 |2.19 | |aref |1.00 |1.13 |1.11 |2.22 | |aset |1.00 |1.50 |1.45 |2.64 | |awrite |1.00 |1.17 |1.13 |2.20 | |call |1.00 |1.29 |1.26 |2.02 | |const2 |1.00 |1.10 |1.10 |2.19 | |const |1.00 |1.11 |1.10 |2.19 | |fannk |1.00 |1.04 |1.02 |1.00 | |fib |1.00 |1.32 |1.31 |1.84 | |ivread |1.00 |1.13 |1.12 |2.43 | |ivwrite |1.00 |1.23 |1.21 |2.40 | |mandelbrot |1.00 |1.13 |1.16 |1.28 | |meteor |1.00 |2.97 |2.92 |3.17 | |nbody |1.00 |1.17 |1.15 |1.49 | |nest-ntimes|1.00 |1.22 |1.20 |1.39 | |nest-while |1.00 |1.10 |1.10 |1.37 | |norm |1.00 |1.18 |1.16 |1.24 | |nsvb |1.00 |1.16 |1.16 |1.17 | |red-black |1.00 |1.02 |0.99 |1.12 | |sieve |1.00 |1.30 |1.28 |1.62 | |trees |1.00 |1.14 |1.13 |1.19 | |while |1.00 |1.12 |1.11 |2.41 | ** Discourse's script/bench.rb Benchmark: https://github.com/discourse/discourse/blob/v1.8.7/script/bench.rb NOTE: Rails performance was somehow a little degraded with JIT for now. We should fix this. (At least I know opt_aref is performing badly in JIT and I have an idea to fix it. Please wait for the fix.) *** JIT off Your Results: (note for timings- percentile is first, duration is second in millisecs) categories_admin: 50: 17 75: 18 90: 22 99: 29 home_admin: 50: 21 75: 21 90: 27 99: 40 topic_admin: 50: 17 75: 18 90: 22 99: 32 categories: 50: 35 75: 41 90: 43 99: 77 home: 50: 39 75: 46 90: 49 99: 95 topic: 50: 46 75: 52 90: 56 99: 101 *** JIT on Your Results: (note for timings- percentile is first, duration is second in millisecs) categories_admin: 50: 19 75: 21 90: 25 99: 33 home_admin: 50: 24 75: 26 90: 30 99: 35 topic_admin: 50: 19 75: 20 90: 25 99: 30 categories: 50: 40 75: 44 90: 48 99: 76 home: 50: 42 75: 48 90: 51 99: 89 topic: 50: 49 75: 55 90: 58 99: 99 git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@62197 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
630 lines
19 KiB
Ruby
Executable file
630 lines
19 KiB
Ruby
Executable file
#!/usr/bin/ruby
|
|
|
|
# tool/update-deps verify makefile dependencies.
|
|
|
|
# Requirements:
|
|
# gcc 4.5 (for -save-temps=obj option)
|
|
# GNU make (for -p option)
|
|
#
|
|
# Warning: ccache (and similar tools) must be disabled for
|
|
# -save-temps=obj to work properly.
|
|
#
|
|
# Usage:
|
|
# 1. Compile ruby with -save-temps=obj option.
|
|
# Ex. ./configure debugflags='-save-temps=obj -g' && make all golf
|
|
# 2. Run tool/update-deps to show dependency problems.
|
|
# Ex. ruby tool/update-deps
|
|
# 3. Use --fix to fix makefiles.
|
|
# Ex. ruby tool/update-deps --fix
|
|
#
|
|
# Other usages:
|
|
# * Fix makefiles using previously detected dependency problems
|
|
# Ex. ruby tool/update-deps --actual-fix [file]
|
|
# "ruby tool/update-deps --fix" is same as "ruby tool/update-deps | ruby tool/update-deps --actual-fix".
|
|
|
|
require 'optparse'
|
|
require 'stringio'
|
|
require 'pathname'
|
|
require 'open3'
|
|
require 'pp'
|
|
|
|
# When out-of-place build, files may be built in source directory or
|
|
# build directory.
|
|
# Some files are always built in the source directory.
|
|
# Some files are always built in the build directory.
|
|
# Some files are built in the source directory for tarball but build directory for repository (svn).
|
|
|
|
=begin
|
|
How to build test directories.
|
|
|
|
VER=2.2.0
|
|
REV=48577
|
|
tar xf ruby-$VER-r$REV.tar.xz
|
|
cp -a ruby-$VER-r$REV tarball_source_dir_original
|
|
mv ruby-$VER-r$REV tarball_source_dir_after_build
|
|
svn co -q -r$REV http://svn.ruby-lang.org/repos/ruby/trunk ruby
|
|
(cd ruby; autoconf)
|
|
cp -a ruby repo_source_dir_original
|
|
mv ruby repo_source_dir_after_build
|
|
mkdir tarball_build_dir repo_build_dir tarball_install_dir repo_install_dir
|
|
(cd tarball_build_dir; ../tarball_source_dir_after_build/configure --prefix=$(cd ../tarball_install_dir; pwd) && make all golf install) > tarball.log 2>&1
|
|
(cd repo_build_dir; ../repo_source_dir_after_build/configure --prefix=$(cd ../repo_install_dir; pwd) && make all golf install) > repo.log 2>&1
|
|
ruby -rpp -rfind -e '
|
|
ds = %w[
|
|
repo_source_dir_original
|
|
repo_source_dir_after_build
|
|
repo_build_dir
|
|
tarball_source_dir_original
|
|
tarball_source_dir_after_build
|
|
tarball_build_dir
|
|
]
|
|
files = {}
|
|
ds.each {|d|
|
|
files[d] = {}
|
|
Dir.chdir(d) { Find.find(".") {|f| files[d][f] = true if %r{\.(c|h|inc|dmyh)\z} =~ f } }
|
|
}
|
|
result = {}
|
|
files_union = files.values.map {|h| h.keys }.flatten.uniq.sort
|
|
files_union.each {|f|
|
|
k = files.map {|d,h| h[f] ? d : nil }.compact.sort
|
|
next if k == %w[repo_source_dir_after_build repo_source_dir_original tarball_source_dir_after_build tarball_source_dir_original]
|
|
next if k == %w[repo_build_dir tarball_build_dir] && File.basename(f) == "extconf.h"
|
|
result[k] ||= []
|
|
result[k] << f
|
|
}
|
|
result.each {|k,v|
|
|
k.each {|d|
|
|
puts d
|
|
}
|
|
v.each {|f|
|
|
puts " " + f.sub(%r{\A\./}, "")
|
|
}
|
|
puts
|
|
}
|
|
' | tee compare.log
|
|
=end
|
|
|
|
# Files built in the source directory.
|
|
# They can be referenced as $(top_srcdir)/filename.
|
|
# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts((g("repo_source_dir_after_build") - g("repo_source_dir_original")).sort)'
|
|
FILES_IN_SOURCE_DIRECTORY = %w[
|
|
revision.h
|
|
]
|
|
|
|
# Files built in the build directory (except extconf.h).
|
|
# They can be referenced as $(topdir)/filename.
|
|
# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts(g("tarball_build_dir").reject {|f| %r{/extconf.h\z} =~ f }.sort)'
|
|
FILES_IN_BUILD_DIRECTORY = %w[
|
|
encdb.h
|
|
ext/etc/constdefs.h
|
|
ext/socket/constdefs.c
|
|
ext/socket/constdefs.h
|
|
probes.h
|
|
transdb.h
|
|
verconf.h
|
|
]
|
|
|
|
# They are built in the build directory if the source is obtained from the repository.
|
|
# However they are pre-built for tarball and they exist in the source directory extracted from the tarball.
|
|
# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts((g("repo_build_dir") & g("tarball_source_dir_original")).sort)'
|
|
FILES_NEED_VPATH = %w[
|
|
ext/rbconfig/sizeof/sizes.c
|
|
ext/ripper/eventids1.c
|
|
ext/ripper/eventids2table.c
|
|
ext/ripper/ripper.c
|
|
golf_prelude.c
|
|
id.c
|
|
id.h
|
|
insns.inc
|
|
insns_info.inc
|
|
known_errors.inc
|
|
lex.c
|
|
miniprelude.c
|
|
mjit_compile.inc
|
|
newline.c
|
|
node_name.inc
|
|
opt_sc.inc
|
|
optinsn.inc
|
|
optunifs.inc
|
|
parse.c
|
|
parse.h
|
|
prelude.c
|
|
probes.dmyh
|
|
vm.inc
|
|
vmtc.inc
|
|
|
|
enc/trans/big5.c
|
|
enc/trans/chinese.c
|
|
enc/trans/emoji.c
|
|
enc/trans/emoji_iso2022_kddi.c
|
|
enc/trans/emoji_sjis_docomo.c
|
|
enc/trans/emoji_sjis_kddi.c
|
|
enc/trans/emoji_sjis_softbank.c
|
|
enc/trans/escape.c
|
|
enc/trans/gb18030.c
|
|
enc/trans/gbk.c
|
|
enc/trans/iso2022.c
|
|
enc/trans/japanese.c
|
|
enc/trans/japanese_euc.c
|
|
enc/trans/japanese_sjis.c
|
|
enc/trans/korean.c
|
|
enc/trans/single_byte.c
|
|
enc/trans/utf8_mac.c
|
|
enc/trans/utf_16_32.c
|
|
]
|
|
|
|
# Multiple files with same filename.
|
|
# It is not good idea to refer them using VPATH.
|
|
# Files in FILES_SAME_NAME_INC is referenced using $(hdrdir).
|
|
# Files in FILES_SAME_NAME_TOP is referenced using $(top_srcdir).
|
|
# include/ruby.h is referenced using $(top_srcdir) because mkmf.rb replaces
|
|
# $(hdrdir)/ruby.h to $(hdrdir)/ruby/ruby.h
|
|
|
|
FILES_SAME_NAME_INC = %w[
|
|
include/ruby/ruby.h
|
|
include/ruby/version.h
|
|
]
|
|
|
|
FILES_SAME_NAME_TOP = %w[
|
|
include/ruby.h
|
|
version.h
|
|
]
|
|
|
|
# Other source files exist in the source directory.
|
|
|
|
def in_makefile(target, source)
|
|
target = target.to_s
|
|
source = source.to_s
|
|
case target
|
|
when %r{\A[^/]*\z}
|
|
target2 = "#{target.sub(/\.o\z/, '.$(OBJEXT)')}"
|
|
case source
|
|
when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
|
|
when *FILES_IN_BUILD_DIRECTORY then source2 = "{$(VPATH)}#{source}" # VPATH is not used now but it may changed in future.
|
|
when *FILES_NEED_VPATH then source2 = "{$(VPATH)}#{source}"
|
|
when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
|
|
when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
|
|
when 'thread_pthread.c' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).c'
|
|
when 'thread_pthread.h' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).h'
|
|
when %r{\A[^/]*\z} then source2 = "{$(VPATH)}#{File.basename source}"
|
|
when %r{\A\.ext/include/[^/]+/ruby/} then source2 = "{$(VPATH)}#{$'}"
|
|
when %r{\Ainclude/ruby/} then source2 = "{$(VPATH)}#{$'}"
|
|
when %r{\Aenc/} then source2 = "{$(VPATH)}#{$'}"
|
|
when %r{\Amissing/} then source2 = "{$(VPATH)}#{$'}"
|
|
when %r{\Accan/} then source2 = "$(CCAN_DIR)/#{$'}"
|
|
when %r{\Adefs/} then source2 = "{$(VPATH)}#{source}"
|
|
else source2 = "$(top_srcdir)/#{source}"
|
|
end
|
|
["common.mk", target2, source2]
|
|
when %r{\Aenc/}
|
|
target2 = "#{target.sub(/\.o\z/, '.$(OBJEXT)')}"
|
|
case source
|
|
when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
|
|
when *FILES_IN_BUILD_DIRECTORY then source2 = source
|
|
when *FILES_NEED_VPATH then source2 = source
|
|
when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
|
|
when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
|
|
when %r{\A\.ext/include/[^/]+/ruby/} then source2 = $'
|
|
when %r{\Ainclude/ruby/} then source2 = $'
|
|
when %r{\Aenc/} then source2 = source
|
|
else source2 = "$(top_srcdir)/#{source}"
|
|
end
|
|
["enc/depend", target2, source2]
|
|
when %r{\Aext/}
|
|
unless File.exist?("#{File.dirname(target)}/extconf.rb")
|
|
warn "warning: not found: #{File.dirname(target)}/extconf.rb"
|
|
end
|
|
target2 = File.basename(target)
|
|
relpath = Pathname(source).relative_path_from(Pathname(target).dirname).to_s
|
|
case source
|
|
when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
|
|
when *FILES_IN_BUILD_DIRECTORY then source2 = relpath
|
|
when *FILES_NEED_VPATH then source2 = "{$(VPATH)}#{File.basename source}"
|
|
when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
|
|
when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
|
|
when %r{\A\.ext/include/[^/]+/ruby/} then source2 = "$(arch_hdrdir)/ruby/#{$'}"
|
|
when %r{\Ainclude/} then source2 = "$(hdrdir)/#{$'}"
|
|
when %r{\A#{Regexp.escape File.dirname(target)}/extconf\.h\z} then source2 = "$(RUBY_EXTCONF_H)"
|
|
when %r{\A#{Regexp.escape File.dirname(target)}/} then source2 = $'
|
|
else source2 = "$(top_srcdir)/#{source}"
|
|
end
|
|
["#{File.dirname(target)}/depend", target2, source2]
|
|
else
|
|
raise "unexpected target: #{target}"
|
|
end
|
|
end
|
|
|
|
DEPENDENCIES_SECTION_START_MARK = "\# AUTOGENERATED DEPENDENCIES START\n"
|
|
DEPENDENCIES_SECTION_END_MARK = "\# AUTOGENERATED DEPENDENCIES END\n"
|
|
|
|
def init_global
|
|
ENV['LC_ALL'] = 'C'
|
|
if mkflag = ENV['GNUMAKEFLAGS'] and mkflag.sub!(/(\A|\s+)-j\d*(?=\s+|\z)/, '')
|
|
mkflag.strip!
|
|
ENV['GNUMAKEFLAGS'] = mkflag
|
|
end
|
|
|
|
$opt_fix = false
|
|
$opt_a = false
|
|
$opt_actual_fix = false
|
|
$i_not_found = false
|
|
end
|
|
|
|
def optionparser
|
|
op = OptionParser.new
|
|
op.banner = 'Usage: ruby tool/update-deps'
|
|
op.def_option('-a', 'show valid dependencies') { $opt_a = true }
|
|
op.def_option('--fix') { $opt_fix = true }
|
|
op.def_option('--actual-fix') { $opt_actual_fix = true }
|
|
op
|
|
end
|
|
|
|
def read_make_deps(cwd)
|
|
dependencies = {}
|
|
make_p, make_p_stderr, make_p_status = Open3.capture3("make -p all miniruby ruby golf")
|
|
File.open('update-deps.make.out.log', 'w') {|f| f.print make_p }
|
|
File.open('update-deps.make.err.log', 'w') {|f| f.print make_p_stderr }
|
|
if !make_p_status.success?
|
|
puts make_p_stderr
|
|
raise "make failed"
|
|
end
|
|
dirstack = [cwd]
|
|
curdir = nil
|
|
make_p.scan(%r{Entering\ directory\ ['`](.*)'|
|
|
^\#\ (GNU\ Make)\ |
|
|
^CURDIR\ :=\ (.*)|
|
|
^([/0-9a-zA-Z._-]+):(.*)\n((?:\#.*\n)*)|
|
|
^\#\ (Finished\ Make\ data\ base\ on)\ |
|
|
Leaving\ directory\ ['`](.*)'}x) {
|
|
directory_enter = $1
|
|
data_base_start = $2
|
|
data_base_curdir = $3
|
|
rule_target = $4
|
|
rule_sources = $5
|
|
rule_desc = $6
|
|
data_base_end = $7
|
|
directory_leave = $8
|
|
#p $~
|
|
if directory_enter
|
|
enter_dir = Pathname(directory_enter)
|
|
#p [:enter, enter_dir]
|
|
dirstack.push enter_dir
|
|
elsif data_base_start
|
|
curdir = nil
|
|
elsif data_base_curdir
|
|
curdir = Pathname(data_base_curdir)
|
|
elsif rule_target && rule_sources && rule_desc &&
|
|
/Modification time never checked/ !~ rule_desc # This pattern match eliminates rules which VPATH is not expanded.
|
|
target = rule_target
|
|
deps = rule_sources
|
|
deps = deps.scan(%r{[/0-9a-zA-Z._-]+})
|
|
next if /\.o\z/ !~ target.to_s
|
|
next if /\A\./ =~ target.to_s # skip rules such as ".c.o"
|
|
#p [curdir, target, deps]
|
|
dir = curdir || dirstack.last
|
|
dependencies[dir + target] ||= []
|
|
dependencies[dir + target] |= deps.map {|dep| dir + dep }
|
|
elsif data_base_end
|
|
curdir = nil
|
|
elsif directory_leave
|
|
leave_dir = Pathname(directory_leave)
|
|
#p [:leave, leave_dir]
|
|
if leave_dir != dirstack.last
|
|
warn "unexpected leave_dir : #{dirstack.last.inspect} != #{leave_dir.inspect}"
|
|
end
|
|
dirstack.pop
|
|
end
|
|
}
|
|
dependencies
|
|
end
|
|
|
|
#def guess_compiler_wd(filename, hint0)
|
|
# hint = hint0
|
|
# begin
|
|
# guess = hint + filename
|
|
# if guess.file?
|
|
# return hint
|
|
# end
|
|
# hint = hint.parent
|
|
# end while hint.to_s != '.'
|
|
# raise ArgumentError, "can not find #{filename} (hint: #{hint0})"
|
|
#end
|
|
|
|
def read_single_cc_deps(path_i, cwd)
|
|
files = {}
|
|
compiler_wd = nil
|
|
path_i.each_line {|line|
|
|
next if /\A\# \d+ "(.*)"/ !~ line
|
|
dep = $1
|
|
next if %r{\A<.*>\z} =~ dep # omit <command-line>, etc.
|
|
compiler_wd ||= dep
|
|
files[dep] = true
|
|
}
|
|
# gcc emits {# 1 "/absolute/directory/of/the/source/file//"} at 2nd line.
|
|
if %r{\A/.*//\z} =~ compiler_wd
|
|
files.delete compiler_wd
|
|
compiler_wd = Pathname(compiler_wd.sub(%r{//\z}, ''))
|
|
elsif !(compiler_wd = yield)
|
|
raise "compiler working directory not found: #{path_i}"
|
|
end
|
|
deps = []
|
|
files.each_key {|dep|
|
|
dep = Pathname(dep)
|
|
if dep.relative?
|
|
dep = compiler_wd + dep
|
|
end
|
|
if !dep.file?
|
|
warn "warning: file not found: #{dep}"
|
|
next
|
|
end
|
|
next if !dep.to_s.start_with?(cwd.to_s) # omit system headers.
|
|
deps << dep
|
|
}
|
|
deps
|
|
end
|
|
|
|
def read_cc_deps(cwd)
|
|
deps = {}
|
|
Pathname.glob('**/*.o').sort.each {|fn_o|
|
|
fn_i = fn_o.sub_ext('.i')
|
|
if !fn_i.exist?
|
|
warn "warning: not found: #{fn_i}"
|
|
$i_not_found = true
|
|
next
|
|
end
|
|
path_o = cwd + fn_o
|
|
path_i = cwd + fn_i
|
|
deps[path_o] = read_single_cc_deps(path_i, cwd) do
|
|
if fn_o.to_s.start_with?("enc/")
|
|
cwd
|
|
else
|
|
path_i.parent
|
|
end
|
|
end
|
|
}
|
|
deps
|
|
end
|
|
|
|
def concentrate(dependencies, cwd)
|
|
deps = {}
|
|
dependencies.keys.sort.each {|target|
|
|
sources = dependencies[target]
|
|
target = target.relative_path_from(cwd)
|
|
sources = sources.map {|s|
|
|
rel = s.relative_path_from(cwd)
|
|
rel
|
|
}
|
|
if %r{\A\.\.(/|\z)} =~ target.to_s
|
|
warn "warning: out of tree target: #{target}"
|
|
next
|
|
end
|
|
sources = sources.reject {|s|
|
|
if %r{\A\.\.(/|\z)} =~ s.to_s
|
|
warn "warning: out of tree source: #{s}"
|
|
true
|
|
elsif %r{/\.time\z} =~ s.to_s
|
|
true
|
|
else
|
|
false
|
|
end
|
|
}
|
|
deps[target] = sources
|
|
}
|
|
deps
|
|
end
|
|
|
|
def sort_paths(paths)
|
|
paths.sort_by {|t|
|
|
ary = t.to_s.split(%r{/})
|
|
ary.map.with_index {|e, i| i == ary.length-1 ? [0, e] : [1, e] } # regular file first, directories last.
|
|
}
|
|
end
|
|
|
|
def show_deps(tag, deps)
|
|
targets = sort_paths(deps.keys)
|
|
targets.each {|t|
|
|
sources = sort_paths(deps[t])
|
|
sources.each {|s|
|
|
puts "#{tag} #{t}: #{s}"
|
|
}
|
|
}
|
|
end
|
|
|
|
def detect_dependencies(out=$stdout)
|
|
cwd = Pathname.pwd
|
|
make_deps = read_make_deps(cwd)
|
|
#pp make_deps
|
|
make_deps = concentrate(make_deps, cwd)
|
|
#pp make_deps
|
|
cc_deps = read_cc_deps(cwd)
|
|
#pp cc_deps
|
|
cc_deps = concentrate(cc_deps, cwd)
|
|
#pp cc_deps
|
|
return make_deps, cc_deps
|
|
end
|
|
|
|
def compare_deps(make_deps, cc_deps, out=$stdout)
|
|
targets = make_deps.keys | cc_deps.keys
|
|
|
|
makefiles = {}
|
|
|
|
make_lines_hash = {}
|
|
make_deps.each {|t, sources|
|
|
sources.each {|s|
|
|
makefile, t2, s2 = in_makefile(t, s)
|
|
makefiles[makefile] = true
|
|
make_lines_hash[makefile] ||= Hash.new(false)
|
|
make_lines_hash[makefile]["#{t2}: #{s2}"] = true
|
|
}
|
|
}
|
|
|
|
cc_lines_hash = {}
|
|
cc_deps.each {|t, sources|
|
|
sources.each {|s|
|
|
makefile, t2, s2 = in_makefile(t, s)
|
|
makefiles[makefile] = true
|
|
cc_lines_hash[makefile] ||= Hash.new(false)
|
|
cc_lines_hash[makefile]["#{t2}: #{s2}"] = true
|
|
}
|
|
}
|
|
|
|
makefiles.keys.sort.each {|makefile|
|
|
cc_lines = cc_lines_hash[makefile] || Hash.new(false)
|
|
make_lines = make_lines_hash[makefile] || Hash.new(false)
|
|
content = begin
|
|
File.read(makefile)
|
|
rescue Errno::ENOENT
|
|
''
|
|
end
|
|
if /^#{Regexp.escape DEPENDENCIES_SECTION_START_MARK}
|
|
((?:.*\n)*)
|
|
#{Regexp.escape DEPENDENCIES_SECTION_END_MARK}/x =~ content
|
|
pre_post_part = [$`, $']
|
|
current_lines = Hash.new(false)
|
|
$1.each_line {|line| current_lines[line.chomp] = true }
|
|
(cc_lines.keys | current_lines.keys | make_lines.keys).sort.each {|line|
|
|
status = [cc_lines[line], current_lines[line], make_lines[line]]
|
|
case status
|
|
when [true, true, true]
|
|
# no problem
|
|
when [true, true, false]
|
|
out.puts "warning #{makefile} : #{line} (make doesn't detect written dependency)"
|
|
when [true, false, true]
|
|
out.puts "add_auto #{makefile} : #{line} (harmless)" # This is automatically updatable.
|
|
when [true, false, false]
|
|
out.puts "add_auto #{makefile} : #{line} (harmful)" # This is automatically updatable.
|
|
when [false, true, true]
|
|
out.puts "del_cc #{makefile} : #{line}" # Not automatically updatable because build on other OS may need the dependency.
|
|
when [false, true, false]
|
|
out.puts "del_cc #{makefile} : #{line} (Curious. make doesn't detect this dependency.)" # Not automatically updatable because build on other OS may need the dependency.
|
|
when [false, false, true]
|
|
out.puts "del_make #{makefile} : #{line}" # Not automatically updatable because the dependency is written manually.
|
|
else
|
|
raise "unexpected status: #{status.inspect}"
|
|
end
|
|
}
|
|
else
|
|
(cc_lines.keys | make_lines.keys).sort.each {|line|
|
|
status = [cc_lines[line], make_lines[line]]
|
|
case status
|
|
when [true, true]
|
|
# no problem
|
|
when [true, false]
|
|
out.puts "add_manual #{makefile} : #{line}" # Not automatically updatable because makefile has no section to update automatically.
|
|
when [false, true]
|
|
out.puts "del_manual #{makefile} : #{line}" # Not automatically updatable because makefile has no section to update automatically.
|
|
else
|
|
raise "unexpected status: #{status.inspect}"
|
|
end
|
|
}
|
|
end
|
|
}
|
|
end
|
|
|
|
def main_show(out=$stdout)
|
|
make_deps, cc_deps = detect_dependencies(out)
|
|
compare_deps(make_deps, cc_deps, out)
|
|
end
|
|
|
|
def extract_deplines(problems)
|
|
adds = {}
|
|
others = {}
|
|
problems.each_line {|line|
|
|
case line
|
|
when /\Aadd_auto (\S+) : ((\S+): (\S+))/
|
|
(adds[$1] ||= []) << [line, "#{$2}\n"]
|
|
when /\A(?:del_cc|del_make|add_manual|del_manual|warning) (\S+) : /
|
|
(others[$1] ||= []) << line
|
|
else
|
|
raise "unexpected line: #{line.inspect}"
|
|
end
|
|
}
|
|
return adds, others
|
|
end
|
|
|
|
def main_actual_fix(problems)
|
|
adds, others = extract_deplines(problems)
|
|
(adds.keys | others.keys).sort.each {|makefile|
|
|
content = begin
|
|
File.read(makefile)
|
|
rescue Errno::ENOENT
|
|
nil
|
|
end
|
|
|
|
if content &&
|
|
/^#{Regexp.escape DEPENDENCIES_SECTION_START_MARK}
|
|
((?:.*\n)*)
|
|
#{Regexp.escape DEPENDENCIES_SECTION_END_MARK}/x =~ content
|
|
pre_dep_post = [$`, $1, $']
|
|
else
|
|
pre_dep_post = nil
|
|
end
|
|
|
|
if pre_dep_post && adds[makefile]
|
|
pre_lines, dep_lines, post_lines = pre_dep_post
|
|
dep_lines = dep_lines.lines.to_a
|
|
add_lines = adds[makefile].map(&:last)
|
|
new_lines = (dep_lines | add_lines).sort.uniq
|
|
new_content = [
|
|
pre_lines,
|
|
DEPENDENCIES_SECTION_START_MARK,
|
|
*new_lines,
|
|
DEPENDENCIES_SECTION_END_MARK,
|
|
post_lines
|
|
].join
|
|
if content != new_content
|
|
puts "modified: #{makefile}"
|
|
tmp_makefile = "#{makefile}.new.#{$$}"
|
|
File.write(tmp_makefile, new_content)
|
|
File.rename tmp_makefile, makefile
|
|
(add_lines - dep_lines).each {|line| puts " added #{line}" }
|
|
else
|
|
puts "not modified: #{makefile}"
|
|
end
|
|
if others[makefile]
|
|
others[makefile].each {|line| puts " #{line}" }
|
|
end
|
|
else
|
|
if pre_dep_post
|
|
puts "no additional lines: #{makefile}"
|
|
elsif content
|
|
puts "no dependencies section: #{makefile}"
|
|
else
|
|
puts "no makefile: #{makefile}"
|
|
end
|
|
if adds[makefile]
|
|
puts " warning: dependencies section was exist at previous phase."
|
|
end
|
|
if adds[makefile]
|
|
adds[makefile].map(&:first).each {|line| puts " #{line}" }
|
|
end
|
|
if others[makefile]
|
|
others[makefile].each {|line| puts " #{line}" }
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
def main_fix
|
|
problems = StringIO.new
|
|
main_show(problems)
|
|
main_actual_fix(problems.string)
|
|
end
|
|
|
|
def run
|
|
op = optionparser
|
|
op.parse!(ARGV)
|
|
if $opt_actual_fix
|
|
main_actual_fix(ARGF.read)
|
|
elsif $opt_fix
|
|
main_fix
|
|
else
|
|
main_show
|
|
end
|
|
end
|
|
|
|
init_global
|
|
run
|
|
if $i_not_found
|
|
warn "warning: missing *.i files, see help in #$0 and ensure ccache is disabled"
|
|
end
|