525 lines
14 KiB
Ruby
Raw Normal View History

require "extend/pathname"
require "keg_relocate"
require "formula_lock"
require "ostruct"
2014-06-26 19:06:31 -05:00
class Keg
class AlreadyLinkedError < RuntimeError
def initialize(keg)
super <<-EOS.undent
2014-06-24 19:04:52 -05:00
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
2014-06-24 19:04:52 -05:00
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:
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}
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 = /(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 Frameworks LinkedKegs var/homebrew/linked].map do |d|
case d when "LinkedKegs" then HOMEBREW_LIBRARY/d else HOMEBREW_PREFIX/d end
end
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
# 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
attr_reader :path, :name, :linked_keg_record, :opt_record
2014-06-26 19:06:31 -05:00
protected :path
def initialize(path)
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}"
end
2014-06-26 19:06:31 -05:00
def to_s
path.to_s
end
2015-05-17 20:34:31 +08:00
def rack
path.parent
end
2014-06-26 19:06:31 -05:00
if Pathname.method_defined?(:to_path)
alias_method :to_path, :to_s
else
alias_method :to_str, :to_s
end
def inspect
"#<#{self.class.name}:#{path}>"
end
def ==(other)
instance_of?(other.class) && path == other.path
end
alias_method :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
2014-06-26 19:06:31 -05:00
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
2014-06-26 19:06:31 -05:00
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
2014-06-26 19:06:31 -05:00
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|
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
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
2014-06-26 19:06:31 -05:00
when :bash then path.join("etc", "bash_completion.d")
when :zsh then path.join("share", "zsh", "site-functions")
when :fish then path.join("share", "fish", "vendor_completions.d")
2014-06-26 19:06:31 -05:00
end
dir && dir.directory? && !dir.children.empty?
end
def plist_installed?
!Dir["#{path}/*.plist"].empty?
end
def python_site_packages_installed?
2014-06-26 19:06:31 -05:00
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"
2014-06-26 19:06:31 -05:00
PkgVersion.parse(path.basename.to_s)
end
def find(*args, &block)
2014-06-26 19:06:31 -05:00
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 /^icons\/.*\/icon-theme\.cache$/ then :skip_file
# all icons subfolders should also mkpath
when /^icons\// then :mkpath
when /^zsh/ then :mkpath
when /^fish/ then :mkpath
# Lua, Lua51, Lua53 all need the same handling.
when /^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.
if relative_path.to_s =~ /[^\/]*\.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 optlink(mode = OpenStruct.new)
opt_record.delete if opt_record.symlink? || opt_record.exist?
make_relative_symlink(opt_record, path, mode)
if oldname_opt_record
oldname_opt_record.delete
make_relative_symlink(oldname_opt_record, path, mode)
end
end
2014-03-13 09:05:40 +00:00
def delete_pyc_files!
find { |pn| pn.delete if %w[.pyc .pyo].include?(pn.extname) }
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
if stat.directory?
2014-10-20 17:07:54 -05:00
begin
keg = Keg.for(src)
rescue NotAKegError
puts "Won't resolve conflicts for symlink #{dst} as it doesn't resolve into the Cellar" if ARGV.verbose?
return
end
dst.unlink unless mode.dry_run
keg.link_dir(src, mode) { :mkpath }
return true
end
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
if dst.exist?
raise ConflictError.new(self, src.relative_path_from(path), dst, e)
elsif 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)
2014-06-26 19:06:31 -05: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.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