mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
0dc240e782
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@48515 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
464 lines
14 KiB
Ruby
Executable file
464 lines
14 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'
|
|
|
|
ENV['LC_ALL'] = 'C'
|
|
|
|
$opt_fix = false
|
|
$opt_a = false
|
|
$opt_actual_fix = false
|
|
$i_not_found = false
|
|
|
|
DEPENDENCIES_SECTION_START_MARK = "\# AUTOGENERATED DEPENDENCIES START\n"
|
|
DEPENDENCIES_SECTION_END_MARK = "\# AUTOGENERATED DEPENDENCIES END\n"
|
|
|
|
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")
|
|
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 = {}
|
|
path_i.each_line.with_index {|line, lineindex|
|
|
next if /\A\# \d+ "(.*)"/ !~ line
|
|
files[$1] = lineindex
|
|
}
|
|
# gcc emits {# 1 "/absolute/directory/of/the/source/file//"} at 2nd line.
|
|
compiler_wd = files.keys.find {|f| %r{\A/.*//\z} =~ f }
|
|
if compiler_wd
|
|
files.delete compiler_wd
|
|
compiler_wd = Pathname(compiler_wd.sub(%r{//\z}, ''))
|
|
else
|
|
raise "compiler working directory not found"
|
|
end
|
|
deps = []
|
|
files.each_key {|dep|
|
|
next if %r{\A<.*>\z} =~ dep # omit <command-line>, etc.
|
|
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)
|
|
}
|
|
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
|
|
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 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 'newline.c', 'miniprelude.c', 'prelude.c' then source2 = source
|
|
when 'thread_pthread.c' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).c'
|
|
when 'thread_pthread.h' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).h'
|
|
when 'include/ruby.h' then source2 = '$(hdrdir)/ruby.h'
|
|
when 'include/ruby/ruby.h' then source2 = '$(hdrdir)/ruby/ruby.h'
|
|
when 'revision.h' then source2 = '$(srcdir)/revision.h'
|
|
when 'version.h' then source2 = '$(srcdir)/version.h'
|
|
when 'include/ruby/version.h' then source2 = '$(srcdir)/include/ruby/version.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 'include/ruby.h' then source2 = '$(hdrdir)/ruby.h'
|
|
when 'include/ruby/ruby.h' then source2 = '$(hdrdir)/ruby/ruby.h'
|
|
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)
|
|
case source
|
|
when 'include/ruby.h' then source2 = '$(top_srcdir)/include/ruby.h'
|
|
when %r{\Ainclude/} then source2 = "$(hdrdir)/#{$'}"
|
|
when %r{\A\.ext/include/[^/]+/ruby/} then source2 = "$(arch_hdrdir)/ruby/#{$'}"
|
|
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 = $'
|
|
when 'id.h' then source2 = '$(topdir)/id.h'
|
|
when 'parse.h' then source2 = '$(topdir)/parse.h'
|
|
when 'lex.c' then source2 = '$(topdir)/lex.c'
|
|
when 'probes.h' then source2 = '$(topdir)/probes.h'
|
|
else source2 = "$(top_srcdir)/#{source}"
|
|
end
|
|
["#{File.dirname(target)}/depend", target2, source2]
|
|
else
|
|
raise "unexpected target: #{target}"
|
|
end
|
|
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 addtional 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
|
|
|
|
run
|
|
if $i_not_found
|
|
warn "warning: missing *.i files, see help in #$0 and ensure ccache is disabled"
|
|
end
|