#!/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 bulid, 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 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' $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 = {} 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 , 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 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 init_global run if $i_not_found warn "warning: missing *.i files, see help in #$0 and ensure ccache is disabled" end