661 lines
18 KiB
Ruby
Raw Normal View History

require "keg_relocate"
require "language/python"
2017-05-22 03:23:50 +02:00
require "lock_file"
require "ostruct"
2014-06-26 19:06:31 -05:00
class Keg
class AlreadyLinkedError < RuntimeError
def initialize(keg)
2017-10-15 02:28:32 +02:00
super <<~EOS
2014-06-24 19:04:52 -05:00
Cannot link #{keg.name}
Another version is already linked: #{keg.linked_keg_record.resolved_path}
2018-06-06 23:34:19 -04:00
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
2017-10-15 02:28:32 +02:00
<<~EOS
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
2017-10-15 02:28:32 +02:00
s << <<~EOS
To force the link and overwrite all conflicting files:
2014-06-24 19:04:52 -05:00
brew link --overwrite #{keg.name}
To list all files that would be deleted:
2014-06-24 19:04:52 -05:00
brew link --overwrite --dry-run #{keg.name}
2018-06-06 23:34:19 -04:00
EOS
s.join("\n")
end
end
class DirectoryNotWritableError < LinkError
2018-01-17 10:42:43 +00:00
def to_s
<<~EOS
Could not symlink #{src}
#{dst.dirname} is not writable.
EOS
end
end
# locale-specific directories have the form language[_territory][.codeset][@modifier]
2016-09-21 14:44:22 +02:00
LOCALEDIR_RX = %r{(locale|man)/([a-z]{2}|C|POSIX)(_[A-Z]{2})?(\.[a-zA-Z\-0-9]+(@.+)?)?}
INFOFILE_RX = %r{info/([^.].*?\.info|dir)$}
KEG_LINK_DIRECTORIES = %w[
bin etc include lib sbin share var Frameworks
].freeze
# TODO: remove when brew-test-bot no longer uses this
TOP_LEVEL_DIRECTORIES = KEG_LINK_DIRECTORIES
PRUNEABLE_DIRECTORIES = (
KEG_LINK_DIRECTORIES - %w[var] + %w[var/homebrew/linked]
).map { |dir| HOMEBREW_PREFIX/dir }
# Keep relatively in sync with
# https://github.com/Homebrew/install/blob/master/install
MUST_EXIST_DIRECTORIES = (
(KEG_LINK_DIRECTORIES + %w[
opt
]).map { |dir| HOMEBREW_PREFIX/dir }
).freeze
MUST_BE_WRITABLE_DIRECTORIES = (
(KEG_LINK_DIRECTORIES + %w[
opt
etc/bash_completion.d lib/pkgconfig
share/aclocal share/doc share/info share/locale share/man
share/man/man1 share/man/man2 share/man/man3 share/man/man4
share/man/man5 share/man/man6 share/man/man7 share/man/man8
share/zsh share/zsh/site-functions
var/log
]).map { |dir| HOMEBREW_PREFIX/dir } + [
HOMEBREW_CACHE,
HOMEBREW_CELLAR,
HOMEBREW_LOCKS,
HOMEBREW_LOGS,
HOMEBREW_REPOSITORY,
Language::Python.homebrew_site_packages,
]
).freeze
2014-03-27 17:13:39 -05:00
# 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
2014-03-27 17:13:39 -05:00
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
2015-11-06 17:32:14 +08:00
mime-info pixmaps sounds postgresql
].freeze
2014-03-27 17:13:39 -05:00
# Given an array of kegs, this method will try to find some other kegs
# that depend on them.
#
# If it does, it returns:
# - some kegs in the passed array that have installed dependents
# - some installed dependents of those kegs.
#
# If it doesn't, it returns nil.
#
# Note that nil will be returned if the only installed dependents
# in the passed kegs are other kegs in the array.
#
# For efficiency, we don't bother trying to get complete data.
def self.find_some_installed_dependents(kegs)
2018-03-25 12:48:57 +01:00
keg_names = kegs.select(&:optlinked?).map(&:name)
keg_formulae = []
kegs_by_source = kegs.group_by do |keg|
begin
# First, attempt to resolve the keg to a formula
# to get up-to-date name and tap information.
f = keg.to_formula
2018-03-25 12:48:57 +01:00
keg_formulae << f
[f.name, f.tap]
rescue FormulaUnavailableError
# If the formula for the keg can't be found,
# fall back to the information in the tab.
2018-03-25 12:48:57 +01:00
[keg.name, keg.tab.tap]
end
end
2018-03-25 12:48:57 +01:00
all_required_kegs = Set.new
all_dependents = []
2018-03-25 12:48:57 +01:00
# Don't include dependencies of kegs that were in the given array.
formulae_to_check = Formula.installed - keg_formulae
formulae_to_check.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.max_by(&:version)
end.compact
2018-03-25 12:48:57 +01:00
next if required_kegs.empty?
2018-03-25 12:48:57 +01:00
all_required_kegs += required_kegs
all_dependents << dependent.to_s
end
2018-03-25 12:48:57 +01:00
return if all_required_kegs.empty?
return if all_dependents.empty?
[all_required_kegs.to_a, all_dependents.sort]
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
2014-06-26 19:06:31 -05:00
protected :path
extend Forwardable
def_delegators :path,
:to_s, :hash, :abv, :disk_usage, :file_count, :directory?, :exist?, :/,
:join, :rename, :find
def initialize(path)
path = path.resolved_path if path.to_s.start_with?("#{HOMEBREW_PREFIX}/opt/")
2014-06-26 19:06:31 -05:00
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}"
@require_relocation = false
end
2015-05-17 20:34:31 +08:00
def rack
path.parent
end
alias to_path to_s
2014-06-26 19:06:31 -05:00
def inspect
"#<#{self.class.name}:#{path}>"
end
def ==(other)
instance_of?(other.class) && path == other.path
end
2016-09-23 18:13:48 +02:00
alias eql? ==
2014-06-26 19:06:31 -05:00
def empty_installation?
Pathname.glob("#{path}/*") do |file|
return false if file.directory? && !file.children.reject(&:ds_store?).empty?
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 require_relocation?
@require_relocation
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_old_aliases
opt = opt_record.parent
tap = begin
to_formula.tap
rescue FormulaUnavailableError, TapFormulaAmbiguityError,
TapFormulaWithOldnameAmbiguityError
# If the formula can't be found, just ignore aliases for now.
nil
end
if tap
bad_tap_opt = opt/tap.user
if !bad_tap_opt.symlink? && bad_tap_opt.directory?
FileUtils.rm_rf bad_tap_opt
end
end
aliases.each do |a|
# versioned aliases are handled below
next if a =~ /.+@./
alias_symlink = opt/a
if alias_symlink.symlink? && alias_symlink.exist?
alias_symlink.delete if alias_symlink.realpath == opt_record.realpath
elsif alias_symlink.symlink? || alias_symlink.exist?
alias_symlink.delete
end
end
Pathname.glob("#{opt_record}@*").each do |a|
a = a.basename.to_s
next if aliases.include?(a)
alias_symlink = opt/a
if alias_symlink.symlink? && alias_symlink.exist?
next if rack != alias_symlink.realpath.parent
end
alias_symlink.delete
end
end
def remove_opt_record
opt_record.unlink
opt_record.parent.rmdir_if_possible
end
def uninstall
CacheStoreDatabase.use(:linkage) do |db|
break unless db.created?
LinkageCacheStore.new(path, db).flush_cache!
end
2014-06-26 19:06:31 -05:00
path.rmtree
path.parent.rmdir_if_possible
remove_opt_record if optlinked?
remove_old_aliases
remove_oldname_opt_record
end
def unlink(mode = OpenStruct.new)
ObserverPathnameExtension.reset_counts!
dirs = []
KEG_LINK_DIRECTORIES.map { |d| path/d }.each do |dir|
2013-02-17 22:54:43 -06:00
next unless dir.exist?
dir.find do |src|
2014-06-26 19:06:31 -05:00
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
remove_old_aliases
Find.prune if src.directory?
2012-03-25 13:08:58 +01:00
end
end
unless mode.dry_run
remove_linked_keg_record if linked?
dirs.reverse_each(&:rmdir_if_possible)
end
ObserverPathnameExtension.n
end
def lock
2015-08-18 19:33:24 +08:00
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
2017-06-01 16:06:51 +02:00
when :bash then path/"etc/bash_completion.d"
when :zsh
dir = path/"share/zsh/site-functions"
dir if dir.directory? && dir.children.any? { |f| f.basename.to_s.start_with?("_") }
when :fish then path/"share/fish/vendor_completions.d"
2016-09-21 08:32:57 +02:00
end
2017-09-24 19:24:46 +01:00
dir&.directory? && !dir.children.empty?
end
def functions_installed?(shell)
case shell
when :fish
dir = path/"share/fish/vendor_functions.d"
dir.directory? && !dir.children.empty?
when :zsh
# Check for non completion functions (i.e. files not started with an underscore),
# since those can be checked separately
dir = path/"share/zsh/site-functions"
dir.directory? && dir.children.any? { |f| !f.basename.to_s.start_with?("_") }
end
end
def plist_installed?
!Dir["#{path}/*.plist"].empty?
end
def python_site_packages_installed?
2017-06-01 16:06:51 +02:00
(path/"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 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"
2014-06-26 19:06:31 -05:00
PkgVersion.parse(path.basename.to_s)
end
def to_formula
Formulary.from_keg(self)
end
def oldname_opt_record
@oldname_opt_record ||= if (opt_dir = HOMEBREW_PREFIX/"opt").directory?
opt_dir.subdirs.find 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
2016-09-21 14:44:22 +02:00
when %r{^icons/.*/icon-theme\.cache$} then :skip_file
# all icons subfolders should also mkpath
2016-09-21 14:44:22 +02:00
when %r{^icons/} then :mkpath
when /^zsh/ then :mkpath
when /^fish/ then :mkpath
# Lua, Lua51, Lua53 all need the same handling.
2016-09-21 14:44:22 +02:00
when %r{^lua/} then :mkpath
when %r{^guile/} then :mkpath
when *SHARE_PATHS then :mkpath
else :link
end
end
2009-07-29 00:56:22 +01:00
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
2015-08-22 23:25:17 +08:00
when /^gio/ then :mkpath
when "lua" then :mkpath
2015-08-27 16:13:24 +08:00
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
2010-08-21 10:51:43 -07:00
# 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.
2016-09-21 14:44:22 +02:00
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
2014-04-21 09:40:24 -05:00
rescue LinkError
unlink
raise
2014-04-21 09:40:24 -05:00
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 tab
Tab.for_keg(self)
end
def runtime_dependencies
tab.runtime_dependencies
end
def aliases
tab.aliases || []
end
def optlink(mode = OpenStruct.new)
opt_record.delete if opt_record.symlink? || opt_record.exist?
make_relative_symlink(opt_record, path, mode)
aliases.each do |a|
alias_opt_record = opt_record.parent/a
alias_opt_record.delete if alias_opt_record.symlink? || alias_opt_record.exist?
make_relative_symlink(alias_opt_record, path, mode)
end
2016-09-23 22:02:23 +02:00
return unless oldname_opt_record
oldname_opt_record.delete
make_relative_symlink(oldname_opt_record, path, mode)
end
2014-03-13 09:05:40 +00:00
def delete_pyc_files!
find { |pn| pn.delete if %w[.pyc .pyo].include?(pn.extname) }
2018-03-18 18:48:43 +00:00
find { |pn| FileUtils.rm_rf pn if pn.basename.to_s == "__pycache__" }
2014-03-13 09:05:40 +00:00
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
2014-07-12 20:15:57 -05:00
# 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
2016-09-23 22:02:23 +02:00
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"
2014-10-20 17:07:54 -05:00
end
2016-09-23 22:02:23 +02:00
return
end
2016-09-23 22:02:23 +02:00
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
2014-03-27 17:37:38 -05:00
dst.delete if mode.overwrite && (dst.exist? || dst.symlink?)
dst.make_relative_symlink(src)
rescue Errno::EEXIST => e
2016-09-23 22:02:23 +02:00
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
2014-06-26 19:06:31 -05:00
# symlinks the contents of path+relative_dir recursively into #{HOMEBREW_PREFIX}/relative_dir
def link_dir(relative_dir, mode)
2017-06-01 16:06:51 +02:00
root = path/relative_dir
return unless root.exist?
root.find do |src|
next if src == root
2014-06-26 19:06:31 -05:00
dst = HOMEBREW_PREFIX + src.relative_path_from(path)
dst.extend ObserverPathnameExtension
2014-07-12 19:56:58 -05:00
if src.symlink? || src.file?
Find.prune if File.basename(src) == ".DS_Store"
Find.prune if src.resolved_path == 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