
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.
613 lines
17 KiB
Ruby
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
|