1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/tool/update-deps
k0kubun ed935aa5be mjit_compile.c: merge initial JIT compiler
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
2018-02-04 11:22:28 +00:00

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