Alyssa Ross e5d7e13c63 keg: installed dependencies of unknown formulae
Previously, trying to resolve the dependencies of a keg would raise an
exception if the formulae for any of the dependencies could not be found
(e.g. if it had been moved to another tap).

This commit updates the dependency finding logic to catch these
exceptions, and fall back to comparing names and taps of formulae, which
should give the correct behaviour.

Fixes #1586.
2016-12-27 18:03:05 +00:00

613 lines
17 KiB
Ruby

require "extend/pathname"
require "keg_relocate"
require "formula_lock"
require "ostruct"
class Keg
class AlreadyLinkedError < RuntimeError
def initialize(keg)
super <<-EOS.undent
Cannot link #{keg.name}
Another version is already linked: #{keg.linked_keg_record.resolved_path}
EOS
end
end
class LinkError < RuntimeError
attr_reader :keg, :src, :dst
def initialize(keg, src, dst, cause)
@src = src
@dst = dst
@keg = keg
@cause = cause
super(cause.message)
set_backtrace(cause.backtrace)
end
end
class ConflictError < LinkError
def suggestion
conflict = Keg.for(dst)
rescue NotAKegError, Errno::ENOENT
"already exists. You may want to remove it:\n rm '#{dst}'\n"
else
<<-EOS.undent
is a symlink belonging to #{conflict.name}. You can unlink it:
brew unlink #{conflict.name}
EOS
end
def to_s
s = []
s << "Could not symlink #{src}"
s << "Target #{dst}" << suggestion
s << <<-EOS.undent
To force the link and overwrite all conflicting files:
brew link --overwrite #{keg.name}
To list all files that would be deleted:
brew link --overwrite --dry-run #{keg.name}
EOS
s.join("\n")
end
end
class DirectoryNotWritableError < LinkError
def to_s; <<-EOS.undent
Could not symlink #{src}
#{dst.dirname} is not writable.
EOS
end
end
# locale-specific directories have the form language[_territory][.codeset][@modifier]
LOCALEDIR_RX = %r{(locale|man)/([a-z]{2}|C|POSIX)(_[A-Z]{2})?(\.[a-zA-Z\-0-9]+(@.+)?)?}
INFOFILE_RX = %r{info/([^.].*?\.info|dir)$}
TOP_LEVEL_DIRECTORIES = %w[bin etc include lib sbin share var Frameworks].freeze
ALL_TOP_LEVEL_DIRECTORIES = (TOP_LEVEL_DIRECTORIES + %w[lib/pkgconfig share/locale share/man opt]).freeze
PRUNEABLE_DIRECTORIES = %w[bin etc include lib sbin share opt Frameworks LinkedKegs var/homebrew/linked].map do |dir|
case dir
when "LinkedKegs"
HOMEBREW_LIBRARY/dir
else
HOMEBREW_PREFIX/dir
end
end
# These paths relative to the keg's share directory should always be real
# directories in the prefix, never symlinks.
SHARE_PATHS = %w[
aclocal doc info java locale man
man/man1 man/man2 man/man3 man/man4
man/man5 man/man6 man/man7 man/man8
man/cat1 man/cat2 man/cat3 man/cat4
man/cat5 man/cat6 man/cat7 man/cat8
applications gnome gnome/help icons
mime-info pixmaps sounds postgresql
].freeze
# Will return some kegs, and some dependencies, if they're present.
# For efficiency, we don't bother trying to get complete data.
def self.find_some_installed_dependents(kegs)
# First, check in the tabs of installed Formulae.
kegs.each do |keg|
dependents = keg.installed_dependents - kegs
dependents.map! { |d| "#{d.name} #{d.version}" }
return [keg], dependents if dependents.any?
end
# Some kegs won't have modern Tabs with the dependencies listed.
# In this case, fall back to Formula#missing_dependencies.
# Find formulae that didn't have dependencies saved in all of their kegs,
# so need them to be calculated now.
#
# This happens after the initial dependency check because it's sloooow.
remaining_formulae = Formula.installed.select do |f|
f.installed_kegs.any? { |k| Tab.for_keg(k).runtime_dependencies.nil? }
end
keg_names = kegs.map(&:name)
kegs_by_source = kegs.group_by { |k| [k.name, Tab.for_keg(k).tap] }
remaining_formulae.each do |dependent|
required = dependent.missing_dependencies(hide: keg_names)
required_kegs = required.map do |f|
f_kegs = kegs_by_source[[f.name, f.tap]]
next unless f_kegs
f_kegs.sort_by(&:version).last
end
next unless required_kegs.any?
return required_kegs, [dependent.to_s]
end
nil
end
# if path is a file in a keg then this will return the containing Keg object
def self.for(path)
path = path.realpath
until path.root?
return Keg.new(path) if path.parent.parent == HOMEBREW_CELLAR.realpath
path = path.parent.realpath # realpath() prevents root? failing
end
raise NotAKegError, "#{path} is not inside a keg"
end
def self.all
Formula.racks.flat_map(&:subdirs).map { |d| new(d) }
end
attr_reader :path, :name, :linked_keg_record, :opt_record
protected :path
def initialize(path)
raise "#{path} is not a valid keg" unless path.parent.parent.realpath == HOMEBREW_CELLAR.realpath
raise "#{path} is not a directory" unless path.directory?
@path = path
@name = path.parent.basename.to_s
@linked_keg_record = HOMEBREW_LINKED_KEGS/name
@opt_record = HOMEBREW_PREFIX/"opt/#{name}"
end
def to_s
path.to_s
end
def rack
path.parent
end
if Pathname.method_defined?(:to_path)
alias to_path to_s
else
alias to_str to_s
end
def inspect
"#<#{self.class.name}:#{path}>"
end
def ==(other)
instance_of?(other.class) && path == other.path
end
alias eql? ==
def hash
path.hash
end
def abv
path.abv
end
def disk_usage
path.disk_usage
end
def file_count
path.file_count
end
def directory?
path.directory?
end
def exist?
path.exist?
end
def empty_installation?
Pathname.glob("#{path}/**/*") do |file|
next if file.directory?
basename = file.basename.to_s
next if Metafiles.copy?(basename)
next if %w[.DS_Store INSTALL_RECEIPT.json].include?(basename)
return false
end
true
end
def /(other)
path / other
end
def join(*args)
path.join(*args)
end
def rename(*args)
path.rename(*args)
end
def linked?
linked_keg_record.symlink? &&
linked_keg_record.directory? &&
path == linked_keg_record.resolved_path
end
def remove_linked_keg_record
linked_keg_record.unlink
linked_keg_record.parent.rmdir_if_possible
end
def optlinked?
opt_record.symlink? && path == opt_record.resolved_path
end
def remove_opt_record
opt_record.unlink
opt_record.parent.rmdir_if_possible
end
def uninstall
path.rmtree
path.parent.rmdir_if_possible
remove_opt_record if optlinked?
remove_oldname_opt_record
end
def unlink(mode = OpenStruct.new)
ObserverPathnameExtension.reset_counts!
dirs = []
TOP_LEVEL_DIRECTORIES.map { |d| path.join(d) }.each do |dir|
next unless dir.exist?
dir.find do |src|
dst = HOMEBREW_PREFIX + src.relative_path_from(path)
dst.extend(ObserverPathnameExtension)
dirs << dst if dst.directory? && !dst.symlink?
# check whether the file to be unlinked is from the current keg first
next unless dst.symlink? && src == dst.resolved_path
if mode.dry_run
puts dst
Find.prune if src.directory?
next
end
dst.uninstall_info if dst.to_s =~ INFOFILE_RX
dst.unlink
Find.prune if src.directory?
end
end
unless mode.dry_run
remove_linked_keg_record if linked?
dirs.reverse_each(&:rmdir_if_possible)
end
ObserverPathnameExtension.n
end
def lock
FormulaLock.new(name).with_lock do
if oldname_opt_record
FormulaLock.new(oldname_opt_record.basename.to_s).with_lock { yield }
else
yield
end
end
end
def completion_installed?(shell)
dir = case shell
when :bash then path.join("etc", "bash_completion.d")
when :zsh
dir = path.join("share", "zsh", "site-functions")
dir if dir && dir.directory? && dir.children.any? { |f| f.basename.to_s.start_with?("_") }
when :fish then path.join("share", "fish", "vendor_completions.d")
end
dir && dir.directory? && !dir.children.empty?
end
def zsh_functions_installed?
# Check for non completion functions (i.e. files not started with an underscore),
# since those can be checked separately
dir = path.join("share", "zsh", "site-functions")
dir && dir.directory? && dir.children.any? { |f| !f.basename.to_s.start_with?("_") }
end
def fish_functions_installed?
dir = path.join("share", "fish", "vendor_functions.d")
dir && dir.directory? && !dir.children.empty?
end
def plist_installed?
!Dir["#{path}/*.plist"].empty?
end
def python_site_packages_installed?
path.join("lib", "python2.7", "site-packages").directory?
end
def python_pth_files_installed?
!Dir["#{path}/lib/python2.7/site-packages/*.pth"].empty?
end
def apps
app_prefix = optlinked? ? opt_record : path
Pathname.glob("#{app_prefix}/{,libexec/}*.app")
end
def app_installed?
!apps.empty?
end
def elisp_installed?
return false unless (path/"share/emacs/site-lisp"/name).exist?
(path/"share/emacs/site-lisp"/name).children.any? { |f| %w[.el .elc].include? f.extname }
end
def version
require "pkg_version"
PkgVersion.parse(path.basename.to_s)
end
def to_formula
Formulary.from_keg(self)
end
def installed_dependents
my_tab = Tab.for_keg(self)
Keg.all.select do |keg|
tab = Tab.for_keg(keg)
next if tab.runtime_dependencies.nil? # no dependency information saved.
tab.runtime_dependencies.any? do |dep|
# Resolve formula rather than directly comparing names
# in case of conflicts between formulae from different taps.
begin
dep_formula = Formulary.factory(dep["full_name"])
next false unless dep_formula == to_formula
rescue FormulaUnavailableError
next false unless my_tab["full_name"] = dep["full_name"]
end
dep["version"] == version.to_s
end
end
end
def find(*args, &block)
path.find(*args, &block)
end
def oldname_opt_record
@oldname_opt_record ||= if (opt_dir = HOMEBREW_PREFIX/"opt").directory?
opt_dir.subdirs.detect do |dir|
dir.symlink? && dir != opt_record && path.parent == dir.resolved_path.parent
end
end
end
def link(mode = OpenStruct.new)
raise AlreadyLinkedError, self if linked_keg_record.directory?
ObserverPathnameExtension.reset_counts!
optlink(mode) unless mode.dry_run
# yeah indeed, you have to force anything you need in the main tree into
# these dirs REMEMBER that *NOT* everything needs to be in the main tree
link_dir("etc", mode) { :mkpath }
link_dir("bin", mode) { :skip_dir }
link_dir("sbin", mode) { :skip_dir }
link_dir("include", mode) { :link }
link_dir("share", mode) do |relative_path|
case relative_path.to_s
when "locale/locale.alias" then :skip_file
when INFOFILE_RX then :info
when LOCALEDIR_RX then :mkpath
when %r{^icons/.*/icon-theme\.cache$} then :skip_file
# all icons subfolders should also mkpath
when %r{^icons/} then :mkpath
when /^zsh/ then :mkpath
when /^fish/ then :mkpath
# Lua, Lua51, Lua53 all need the same handling.
when %r{^lua/} then :mkpath
when %r{^guile/} then :mkpath
when *SHARE_PATHS then :mkpath
else :link
end
end
link_dir("lib", mode) do |relative_path|
case relative_path.to_s
when "charset.alias" then :skip_file
# pkg-config database gets explicitly created
when "pkgconfig" then :mkpath
# cmake database gets explicitly created
when "cmake" then :mkpath
# lib/language folders also get explicitly created
when "dtrace" then :mkpath
when /^gdk-pixbuf/ then :mkpath
when "ghc" then :mkpath
when /^gio/ then :mkpath
when "lua" then :mkpath
when /^mecab/ then :mkpath
when /^node/ then :mkpath
when /^ocaml/ then :mkpath
when /^perl5/ then :mkpath
when "php" then :mkpath
when /^python[23]\.\d/ then :mkpath
when /^R/ then :mkpath
when /^ruby/ then :mkpath
# Everything else is symlinked to the cellar
else :link
end
end
link_dir("Frameworks", mode) do |relative_path|
# Frameworks contain symlinks pointing into a subdir, so we have to use
# the :link strategy. However, for Foo.framework and
# Foo.framework/Versions we have to use :mkpath so that multiple formulae
# can link their versions into it and `brew [un]link` works.
if relative_path.to_s =~ %r{[^/]*\.framework(/Versions)?$}
:mkpath
else
:link
end
end
make_relative_symlink(linked_keg_record, path, mode) unless mode.dry_run
rescue LinkError
unlink
raise
else
ObserverPathnameExtension.n
end
def remove_oldname_opt_record
return unless oldname_opt_record
return unless oldname_opt_record.resolved_path == path
@oldname_opt_record.unlink
@oldname_opt_record.parent.rmdir_if_possible
@oldname_opt_record = nil
end
def optlink(mode = OpenStruct.new)
opt_record.delete if opt_record.symlink? || opt_record.exist?
make_relative_symlink(opt_record, path, mode)
return unless oldname_opt_record
oldname_opt_record.delete
make_relative_symlink(oldname_opt_record, path, mode)
end
def delete_pyc_files!
find { |pn| pn.delete if %w[.pyc .pyo].include?(pn.extname) }
end
private
def resolve_any_conflicts(dst, mode)
return unless dst.symlink?
src = dst.resolved_path
# src itself may be a symlink, so check lstat to ensure we are dealing with
# a directory, and not a symlink pointing at a directory (which needs to be
# treated as a file). In other words, we only want to resolve one symlink.
begin
stat = src.lstat
rescue Errno::ENOENT
# dst is a broken symlink, so remove it.
dst.unlink unless mode.dry_run
return
end
return unless stat.directory?
begin
keg = Keg.for(src)
rescue NotAKegError
if ARGV.verbose?
puts "Won't resolve conflicts for symlink #{dst} as it doesn't resolve into the Cellar"
end
return
end
dst.unlink unless mode.dry_run
keg.link_dir(src, mode) { :mkpath }
true
end
def make_relative_symlink(dst, src, mode)
if dst.symlink? && src == dst.resolved_path
puts "Skipping; link already exists: #{dst}" if ARGV.verbose?
return
end
# cf. git-clean -n: list files to delete, don't really link or delete
if mode.dry_run && mode.overwrite
if dst.symlink?
puts "#{dst} -> #{dst.resolved_path}"
elsif dst.exist?
puts dst
end
return
end
# list all link targets
if mode.dry_run
puts dst
return
end
dst.delete if mode.overwrite && (dst.exist? || dst.symlink?)
dst.make_relative_symlink(src)
rescue Errno::EEXIST => e
raise ConflictError.new(self, src.relative_path_from(path), dst, e) if dst.exist?
if dst.symlink?
dst.unlink
retry
end
rescue Errno::EACCES => e
raise DirectoryNotWritableError.new(self, src.relative_path_from(path), dst, e)
rescue SystemCallError => e
raise LinkError.new(self, src.relative_path_from(path), dst, e)
end
protected
# symlinks the contents of path+relative_dir recursively into #{HOMEBREW_PREFIX}/relative_dir
def link_dir(relative_dir, mode)
root = path+relative_dir
return unless root.exist?
root.find do |src|
next if src == root
dst = HOMEBREW_PREFIX + src.relative_path_from(path)
dst.extend ObserverPathnameExtension
if src.symlink? || src.file?
Find.prune if File.basename(src) == ".DS_Store"
Find.prune if src.realpath == dst
# Don't link pyc or pyo files because Python overwrites these
# cached object files and next time brew wants to link, the
# file is in the way.
if %w[.pyc .pyo].include?(src.extname) && src.to_s.include?("/site-packages/")
Find.prune
end
case yield src.relative_path_from(root)
when :skip_file, nil
Find.prune
when :info
next if File.basename(src) == "dir" # skip historical local 'dir' files
make_relative_symlink dst, src, mode
dst.install_info
else
make_relative_symlink dst, src, mode
end
elsif src.directory?
# if the dst dir already exists, then great! walk the rest of the tree tho
next if dst.directory? && !dst.symlink?
# no need to put .app bundles in the path, the user can just use
# spotlight, or the open command and actual mac apps use an equivalent
Find.prune if src.extname == ".app"
case yield src.relative_path_from(root)
when :skip_dir
Find.prune
when :mkpath
dst.mkpath unless resolve_any_conflicts(dst, mode)
else
unless resolve_any_conflicts(dst, mode)
make_relative_symlink dst, src, mode
Find.prune
end
end
end
end
end
end