#!/usr/bin/env ruby # -*- ruby -*- # vim: set sts=2 sw=2 ts=8 et: # # Copyright (c) 2000-2004 Akinori MUSHA # Copyright (c) 2005,2006 KOMATSU Shinichiro # Copyright (c) 2006-2008 Sergey Matveychuk <[email protected]> # Copyright (c) 2009-2012 Stanislav Sedov <[email protected]> # Copyright (c) 2012 Bryan Drewery <[email protected]> # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # MYNAME = File.basename($0) require "optparse" require "pkgtools" COLUMNSIZE = 30 NEXTLINE = "\n%*s" % [5 + COLUMNSIZE, ''] def init_global $fix_db = false $force = false $automatic = false $check_lost = false; $interactive = false; $noconfig = false $omit_check = false; $quiet = false $quieter = false #$sanity_check = true $temp_dir = "" $update_db = false $origins = Hash.new end def main(argv) usage = <<-"EOF" usage: #{MYNAME} [-hafFfiLOQQquv] [-c pkgname] [-o pkgname] [-s /old_pkgname/new_pkgname/] [file ...] EOF banner = <<-"EOF" #{MYNAME} #{Version} (#{PkgTools::DATE}) #{usage} EOF dry_parse = true OptionParser.new(banner, COLUMNSIZE) do |opts| opts.def_option("-h", "--help", "Show this message") { print opts exit 0 } opts.def_option("-c", "--collate=PKGNAME", "Show files installed by the given packge#{NEXTLINE}that have been overwritten by other packages") { |pkgname| pkgname = $pkgdb.strip(pkgname, true) begin pkg = PkgInfo.new(pkgname) pkg.files.each do |path| owners = $pkgdb.which_m(path) or raise PkgDB::DBError, "The file #{path} is not properly recorded as installed" i = owners.index(pkgname) or raise PkgDB::DBError, "The file #{path} is not properly recorded as installed by #{pkgname}" if i != owners.size - 1 print "#{path}: " print "overwritten by: " if $verbose puts owners[(i + 1)..-1].join(' ') end end rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport STDERR.puts e.message end unless dry_parse } opts.def_option("-f", "--force", "Force;#{NEXTLINE}Specified with -u, update database#{NEXTLINE}regardless of timestamps#{NEXTLINE}Specified with -F, fix held packages too") { |v| $force = v } opts.def_option("-F", "--fix", "Fix the package database interactively") { |v| $fix_db = v $interactive = true if ! $automatic } opts.def_option("-a", "--auto", "Turn on automatic mode when -F is also specified") { |v| $automatic = v $interactive = false if $fix_db } opts.def_option("--autofix", "Shorthand of --auto --fix (-aF)") { $automatic = $fix_db = true } opts.def_option("-i", "--interactive", "Turn on interactive mode") { |v| $interactive = v } opts.def_option("-L", "--fix-lost", "Check and restore lost dependencies#{NEXTLINE}against the ports tree") { |v| $check_lost = v } opts.def_option("-o", "--origin=PKGNAME[=ORIGIN]", "Look up or change the origin of the given#{NEXTLINE}package") { |arg| pkgname, origin = arg.split('=', 2) spkgname = $pkgdb.strip(pkgname, true) unless dry_parse if spkgname print spkgname, ": " if $verbose print $pkgdb.origin(spkgname) || '?' if origin print " -> #{origin}\n" modify_origin(spkgname, origin) else print "\n" end else print pkgname, ": " if $verbose puts '?' end end } opts.def_option("-O", "--omit-check", "Specified with -F, turn off checks#{NEXTLINE}dependencies against the ports tree. Useful#{NEXTLINE}if you need a speed-up") { |v| $omit_check = v } opts.def_option("-Q", "--quiet", "Do not write anything to stdout;#{NEXTLINE}Specified twice, stderr neither") { if !$quiet STDOUT.reopen(open('/dev/null', 'w')) unless dry_parse $quiet = true elsif !$quieter STDERR.reopen(open('/dev/null', 'w')) unless dry_parse $quieter = true end } opts.def_option("-q", "--noconfig", "Do not read pkgtools.conf") { |v| $noconfig = v } opts.def_option("-s", "--substitute=/OLD/NEW/", "Substitute all the dependencies recorded#{NEXTLINE}as OLD with NEW") { |expr| if expr.empty? warning_message "Illegal expression: " + expr print opts exit 64 end break if dry_parse sep = expr.slice!(0,1) oldpkgname, newpkgname = expr.split(sep) if $verbose progress_message "Replacing dependencies: #{oldpkgname} -> #{newpkgname}" end if newpkgname.nil? || newpkgname.empty? || oldpkgname.nil? || oldpkgname.empty? raise OptionParser::ParseError, "requires non-empty pkgnames" end update_pkgdep(oldpkgname, newpkgname) automatic = $automatic $automatic = true fix_db_init() fix_db_phase2() $automatic = automatic } opts.def_option("-u", "--update", "Update the package database") { |v| $update_db = v } opts.def_option("-v", "--verbose", "Be verbose") { |v| $verbose = v } opts.def_tail_option ' Environment Variables [default]: PKGTOOLS_CONF configuration file [$PREFIX/etc/pkgtools.conf] PKG_DBDIR packages DB directory [/var/db/pkg] PORTSDIR ports directory [/usr/ports] PORTS_DBDIR ports db directory [$PORTSDIR] PORTS_INDEX ports index file [$PORTSDIR/INDEX]' if argv.empty? print opts return 0 end begin init_global init_pkgtools_global dry_parse = false rest = opts.order(*argv) unless $noconfig init_global load_config else argv = rest end opts.order!(argv) if $update_db progress_message "Updating the pkgdb" $pkgdb.update_db($force) $pkgdb.open_db # let it check the DB version end check_lost_deps() if $check_lost fix_db() if $fix_db list = [] opts.order(*argv) do |arg| if arg[0,1] == '/' || File.exist?(arg) path = arg else path = `which #{arg}`.chomp if not File.exist?(path) STDERR.puts "#{arg}: not found" next end end print "#{path}: " if $verbose if owners = $pkgdb.which_m(path) puts owners.join(' ') else puts '?' end end rescue OptionParser::ParseError => e STDERR.puts "#{MYNAME}: #{e}", usage exit 64 rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport STDERR.puts e.message exit 1 end end 0 end def get_real_run_deps(pkgname) unless $origins.has_key?(pkgname) origin = PkgInfo::new(pkgname).origin make_env = config_make_env(origin, pkgname) make_args = config_make_args(origin, pkgname) make_env.unshift("env") if !make_env.empty? puts "Disclose depends for #{pkgname}" if $verbose $origins[pkgname] = Hash.new `cd #{$portsdb.portdir(origin)} && #{shelljoin(*make_env)} make #{shelljoin(*make_args)} package-depends-list`.each_line { |line| next if line.empty? a = line.chomp.split(/ /) $origins[pkgname].store(a[0], a[2]) unless a[0].nil? or a[1].nil? } end $origins[pkgname] end def check_lost_deps() # pkg check -dn raise NeedsPkgNGSupport, "PKGNG support needed: #{__FILE__}:#{__LINE__}" if $pkgdb.with_pkgng? fix_db_init() stty_sane if $interactive puts "Look for lost dependencies:" $pkgnames.each do |pkgname| # Ignore bsdpan- pseudo ports if /^bsdpan-/ =~ pkgname puts "#{pkgname}: ignored" next end deps = $pkgdb.pkgdep(pkgname, true) or return real_deps = get_real_run_deps(pkgname) print "#{pkgname}: " lost = real_deps.values - deps.values # Remove dependencies which exist in ALT_PKGDEP lost.delete_if { |origin| alt_dep('', origin) } if ! lost.empty? puts "found" lost.each do |origin| puts " #{origin}" end if $interactive next if not prompt_yesno("Fix?", true) end dep = "" lost.each do |origin| real_deps.each do |d,o| if o == origin dep = d break end end modify_pkgdep(pkgname, dep, :add, origin) end puts "-> Fixed." else puts "ok" end end end def fix_db if $pkgdb.with_pkgng? # FIXME: pkgng STDERR.puts "pkgdb -F not supported with PKGNG yet. Use 'pkg check' directly." if !$quiet $pkgdb.unmark_fixme return end progress_message "Checking the package registry database" if ! File.owned?($pkgdb_dir) && Process.uid > 0 if $force warning_message "You do not own #{$pkgdb_dir}. (proceeding anyway)" else warning_message "You do not own #{$pkgdb_dir}. (use -f to force or run as root)" exit 1 end end stty_sane if $interactive fix_db_init() fix_db_phase1() fix_db_phase2() $pkgdb.unmark_fixme end def fix_db_init() $pkgnames = $pkgdb.installed_pkgs $req_hash = {} # a hash of pkgname => { dependent1 => true , ... } pairs $fix_hash = {} # a hash of pkgname => ans pairs $all_list = [] # an array of pkgnames, with which a user answered "all" end def fix_db_phase1() # fix missing or stale origins org_hash = {} # a hash of origin => [pkg1, pkg2, ...] pairs deleted = [] $pkgnames.each do |pkgname| puts "Checking the origin of #{pkgname}" if $verbose pkg = PkgInfo.new(pkgname) case origin = fix_origin(pkg) when nil deleted << pkgname when false # skipped else if org_hash.key?(origin) org_hash[origin] << pkg else org_hash[origin] = [pkg] end end end $pkgnames -= deleted unless $automatic # fix origin duplicates puts "Checking for origin duplicates" if $verbose fix_duplicates(org_hash).each do |pkg| $pkgnames.delete(pkg.fullname) end end $pkgnames.each do |pkgname| puts "Checking #{pkgname}" if $verbose # check and fix dependencies fix_dependencies(pkgname) end end def fix_db_phase2() if $pkgdb.with_pkgng? # FIXME: pkgng return end # reconstruct all the +REQUIRED_BY files puts "Regenerating +REQUIRED_BY files" if $verbose tsort = PkgTSort.new indep_pkgnames = [] $pkgnames.each do |pkgname| req_file = $pkgdb.pkg_required_by(pkgname) if $req_hash.key?(pkgname) req_by = $req_hash[pkgname].keys req_by.each { |req| tsort.add(req, pkgname) } File.open(req_file, "w") do |f| f.puts(*req_by.sort) end else indep_pkgnames << pkgname File.unlink(req_file) if File.exist?(req_file) end end # unlink cyclic dependencies puts "Checking for cyclic dependencies" if $verbose fix_cycles(tsort) end def fix_origin(pkg) pkgname = pkg.fullname origin = pkg.origin if origin if $portsdb.exist?(origin, true) return origin end puts "Stale origin: '#{origin}': perhaps moved or obsoleted." else puts "Missing origin: #{pkgname}" end special_guess = nil if !origin && /^bsdpan-(.*)/ =~ pkg.name and ports = $portsdb.glob("p5-#{$1}") and !ports.empty? special_guess = ports.first.origin end if origin and trace = $portsdb.moved.trace(origin) trace_element = trace.shift printf "-> The port '%s' was %s on %s because:\n\t\"%s\"\n", origin, trace_element.to ? "moved to '#{trace_element.to}'" : "removed", trace_element.date, trace_element.why trace.each do |trace_element| printf " then %s on %s because:\n\t\"%s\"\n", trace_element.to ? "to '#{trace_element.to}'" : "removed", trace_element.date, trace_element.why end special_guess = trace_element.to || :delete end if special_guess origin = special_guess else if config_held?(pkg) && !$force puts "-> Ignored. (the package is held; specify -f to force)" return false end if prompt_yesno("Skip this for now?", true) if !$automatic || $verbose puts "To skip it without asking in future, please list it in HOLD_PKGS." end return false end if origin && prompt_yesno("Browse CVSweb for the port's history?", false) Dir.chdir($ports_dir) { xsystem(PkgDB::command(:portcvsweb), File.join(origin, "Makefile")) } end guess = guess_origin(pkg) confirm_port(guess) and origin = guess end origin ||= input_port('New origin?') case origin when :abort puts "Abort." exit when :skip puts "Skipped." return false when :delete if $automatic puts "Skipped. (running in non-interactive mode; specify -i to ask)" return false end if pkg.required? puts "-> Hint: #{pkgname} is required by the following package(s):" pkg.required_by.each do |req| puts "\t#{req}" end else puts "-> Hint: #{pkgname} is not required by any other package" end puts "-> Hint: checking for overwritten files..." possible_successors = [] pkg.files.each do |path| owners = $pkgdb.which_m(path) or raise PkgDB::DBError, "#{path} is not properly recorded as installed - please run pkgdb -fu" i = owners.index(pkgname) or raise PkgDB::DBError, "#{path} is not properly recorded as installed by #{pkgname} - please run pkgdb -fu" if i != owners.size - 1 overwriters = owners[(i + 1)..-1] puts "\t#{path}: overwritten by: #{overwriters.join(' ')}" # possible_successors |= overwriters end end if possible_successors.empty? puts " -> No files installed by #{pkgname} have been overwritten by other packages." else puts " -> The package may have been succeeded by some of the following package(s):" possible_successors.each do |s| puts "\t#{s}" end if prompt_yesno("Unregister #{pkgname} keeping the installed files intact?", false) puts "--> Unregistering #{pkgname}" if xsystem('/bin/rm', '-rf', pkg.pkgdir) $pkgdb.update_db puts "--> Done." return nil else puts "--> Failed." return false end end end if prompt_yesno("Deinstall #{pkgname} ?", false) # pkg_deinstall will update the pkgdb $pkgdb.close_db if xsystem(PkgDB::command(:pkg_deinstall), pkgname) puts "--> Done." return nil else puts "--> Failed." return false end end else begin modify_origin(pkgname, origin) puts "Fixed. (-> #{origin})" rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport puts e.message end end origin end def guess_origin(pkg) pkgname = pkg.fullname print "Guessing... " STDOUT.flush guess = $portsdb.glob(pkg.name).max { |a, b| matchlen(pkgname, a.pkgname.to_s) <=> matchlen(pkgname, b.pkgname.to_s) } if guess puts '' return guess.origin else puts "no idea." end nil rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport puts e.message nil end def fix_dependencies(pkgname) deps = $pkgdb.pkgdep(pkgname, true) or return deps.each do |dep, origin| unless $pkgnames.include?(dep) puts "Stale dependency: #{pkgname} -> #{dep} (#{origin}):" if !$omit_check && !get_real_run_deps(pkgname).values.include?(origin) # Ignore dependencies which exist in ALG_PKGDEP unless alt_dep(dep, origin) puts "-> Deleted. (irrelevant)" modify_pkgdep(pkgname, dep, :delete) next end end if config_held?(pkgname) && !$force puts "-> Ignored. (the package is held; specify -f to force)" next end fix = $fix_hash[dep] fix_score = nil if fix.nil? fix, fix_score = guess_dep(dep, origin) end fix = query_dep_fix(dep, fix, fix_score) next if fix == :skip if fix == :install xsystem(PkgDB::command(:portinstall), origin) fix = dep end begin modify_pkgdep(pkgname, dep, fix, origin) case fix when :delete puts "Deleted." else puts "Fixed. (-> #{fix})" end $fix_hash[dep] = fix unless fix_score == 100 dep = fix rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport puts e.message end end if fix != :delete ($req_hash[dep] ||= {})[pkgname] = true end end end def tracing_deorigin(origin) total = $pkgdb.deorigin(origin) || [] if trace = $portsdb.moved.trace(origin) trace.each do |trace_element| pkgnames = $pkgdb.deorigin(trace_element.to) and total |= pkgnames end end if total.empty? nil else total end end def guess_dep(dep, origin) if origin pkgnames = tracing_deorigin(origin) || alt_dep(dep, origin) || $pkgnames else pkgnames = alt_dep(dep) || $pkgnames end if pkgnames.size == 1 return pkgnames.first, 100 end pkg = PkgInfo.new(dep) pkgname = pkg.fullname prefixes = PortsDB::LANGUAGE_SPECIFIC_CATEGORIES.values prefix_re = prefixes.join('|') pkgname_re = /^(#{prefix_re})?(.+?)((?:\+[^+\-]+)+)?(-[^\-]+)$/ pkg_prefix, pkg_base, pkg_suffix, pkg_version = pkgname_re.match(pkgname)[1..-1] calc_score = proc { |name| score = 0 name_prefix, name_base, name_suffix, name_version = pkgname_re.match(name)[1..-1] if name_prefix != pkg_prefix score -= 1 elsif pkg_prefix score += 5 end if name_suffix != pkg_suffix score -= 1 if name_suffix && pkg_suffix n = [pkg_suffix.size, name_suffix.size].max score += 20 * matchlen(name_suffix, pkg_suffix) / n end elsif pkg_suffix score += 20 end if name_base == pkg_base score += 50 score += 5 * matchlen(name_version, pkg_version) else n = matchlen(name_base, pkg_base) if n >= 3 score += 5 * n else score = 0 end end if score < 0 score = 0 end score } full = calc_score.call(pkgname) score, name = pkgnames.map { |name| score = calc_score.call(name) * 100 / full [score, name] }.max if score.nonzero? return name, score else return nil, nil end rescue => e raise e if e.class == PkgDB::NeedsPkgNGSupport puts e.message return nil, nil end def query_dep_fix(dep, fix, fix_score) if fix if fix_score && fix_score >= 100 return fix end if $automatic puts "Skipped. (running in non-interactive mode; specify -i to ask)" return :skip end skip = (fix == :skip) if $all_list.include?(dep) if skip puts "Skipped." return :skip else return fix end end default_ans = true case fix when :skip prompt = "Skip this?" when :delete prompt = "Delete this?" else if fix_score prompt = "#{fix} (score:#{fix_score}%) ?" default_ans = fix_score >= 80 else prompt = "#{fix} ?" end end ans = prompt_yesnoall(prompt, default_ans) if ans == :all $all_list << dep end if ans if skip return :skip else return fix end end else if $automatic puts "Skipped. (running in non-interactive mode; specify -i to ask)" return :skip end end install_stale = prompt_yesnoall('Install stale dependency?', true) if install_stale return :install end fix = input_pkg('New dependency?', true) case fix when :abort puts "Abort." exit when :skip puts "Skipped." $fix_hash[dep] = :skip return :skip when :skip_all $fix_hash[dep] = :skip $all_list << dep return :skip when :delete_all $all_list << dep return :delete end return fix end def fix_cycles(tsort) skip_all = false tsort.tsort! do |cycle| puts "Cyclic dependencies: #{cycle.join(' -> ')} -> (#{cycle[0]})" if $automatic puts "Skipped. (running in non-interactive mode; specify -i to ask)" next end i = nil loop do ans = skip_all || \ cycle.size == 1 ? cycle[0] : input_pkg('Unlink which dependency?', false, cycle) case ans when :abort puts "Abort." exit else i = cycle.index(ans) if cycle[i + 1].nil? a, b = cycle.last, cycle.first else a, b = cycle[i], cycle[i + 1] end if prompt_yesno("Unlink #{a} -> #{b} ?", true) file = $pkgdb.pkg_contents(a) next if not File.exist?(file) File.open(file, "r+") do |f| lines = [] pkgdeps = { b => true } deporigin = nil f.each do |line| case line when /^@pkgdep\s+(\S+)/ deporigin = :keep pkgdep = $1 if pkgdeps.key?(pkgdep) # remove duplicates deporigin = :delete next end pkgdeps[pkgdep] = true lines << line when /^@comment\s+DEPORIGIN:(\S+)/ case deporigin when :keep lines << line else # :delete, nil # no output end deporigin = nil else lines << line deporigin = nil end end f.rewind f.print(lines.join()) f.truncate f.pos end file = $pkgdb.pkg_required_by(b) filter_file(shelljoin('grep', '-v', "^#{Regexp.quote(a)}$"), file) puts 'Done.' break end end end i end end def fix_duplicates(org_hash) all_deleted = [] $pkgdb.close_db org_hash.each do |origin, pkgs| next if pkgs.size < 2 pkgs.sort! n = pkgs.size puts "Duplicated origin: #{origin} - " + pkgs.collect { |pkg| pkg.fullname }.join(' ') prompt_yesno("Unregister any of them?", false) or next deleted = [] pkgs.each do |pkg| pkgname = pkg.fullname if n == 1 # automatically keep one package record at least puts " -> #{pkgname} is kept." break end prompt_yesno(" Unregister #{pkgname} keeping the installed files intact?", false) or next deleted << pkg n -= 1 end unless deleted.empty? biggest = (pkgs - deleted)[-1] deleted.each do |pkg| oldpkgdir = pkg.pkgdir oldpkgname = pkg.fullname newpkgdir = biggest.pkgdir newpkgname = biggest.fullname contents = $pkgdb.pkg_contents(oldpkgname) backup = $pkgdb.pkg_contents(newpkgname) + '.' + oldpkgname puts " --> Saving the #{oldpkgname}'s +CONTENTS file as #{backup}" xsystem('/bin/cp', '-pf', contents, backup) or next puts " --> Unregistering #{oldpkgname}" xsystem('/bin/rm', '-rf', oldpkgdir) or next puts " --> Done." all_deleted << pkg end end end $pkgdb.update_db unless all_deleted.empty? all_deleted end def input_pkg(message = 'Which package?', fullspec = false, pkgnames = $pkgnames) flags = OPTIONS_HISTORY | (fullspec ? OPTIONS_SKIP | OPTIONS_DELETE | OPTIONS_ALL : OPTIONS_NONE) choose_from_options(message, pkgnames, flags) end def input_port(message = 'Which port?') loop do input = input_file(message + ' (? to help): ', $ports_dir, true) if input.nil? print "\n" return :delete end input.strip! case input when '.' return :abort when '?' print <<-EOF [Enter] to skip, [Ctrl]+[D] to unregister or deinstall, [.][Enter] to abort, [Tab] to complete EOF next when '' if prompt_yesno("Skip this?", true) return :skip end next else input = $portsdb.strip(input) confirm_port(input) and return input end end # not reached end def confirm_port(origin) if origin pkgname = $portsdb.exist?(origin) if !pkgname return prompt_yesno("#{origin}: Not found. Force it?", false) end return prompt_yesno("#{origin} (#{pkgname}): Change the origin to this?", true) end puts "Not in due form <category/portname>: #{origin}" false end if $0 == __FILE__ set_signal_handlers exit(main(ARGV) || 1) end