442 lines
12 KiB
Ruby
Raw Normal View History

require "extend/pathname"
require "keg_fix_install_names"
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]
PRUNEABLE_DIRECTORIES = %w[bin etc include lib sbin share Frameworks LinkedKegs].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 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
]
# if path is a file in a keg then this will return the containing Keg object
def self.for path
path = path.realpath
while not 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
2014-06-24 19:04:52 -05:00
@linked_keg_record = HOMEBREW_LIBRARY.join("LinkedKegs", name)
@opt_record = HOMEBREW_PREFIX.join("opt", name)
end
2014-06-26 16:10:15 -05:00
def fname
opoo "Keg#fname is a deprecated alias for Keg#name and will be removed soon"
name
end
2014-06-26 19:06:31 -05:00
def to_s
path.to_s
end
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 directory?
path.directory?
end
2014-06-26 19:06:31 -05:00
def exist?
path.exist?
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
2014-06-26 19:06:31 -05:00
path.rmtree
path.parent.rmdir_if_possible
remove_opt_record if optlinked?
end
def unlink
ObserverPathnameExtension.reset_counts!
dirs = []
2014-06-26 19:06:31 -05:00
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
if dst.symlink? && src == dst.resolved_path
dst.uninstall_info if dst.to_s =~ INFOFILE_RX
dst.unlink
Find.prune if src.directory?
end
2012-03-25 13:08:58 +01:00
end
end
remove_linked_keg_record if linked?
dirs.reverse_each(&:rmdir_if_possible)
ObserverPathnameExtension.total
end
def lock
2014-06-24 19:04:52 -05:00
FormulaLock.new(name).with_lock { yield }
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")
end
dir && dir.directory? && dir.children.any?
end
def plist_installed?
2014-06-26 19:06:31 -05:00
Dir["#{path}/*.plist"].any?
end
def python_site_packages_installed?
2014-06-26 19:06:31 -05:00
path.join("lib", "python2.7", "site-packages").directory?
end
def app_installed?
2014-06-26 19:06:31 -05:00
Dir["#{path}/{,libexec/}*.app"].any?
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 link mode=OpenStruct.new
raise AlreadyLinkedError.new(self) if linked_keg_record.directory?
ObserverPathnameExtension.reset_counts!
# 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 |path|
case path.to_s
when 'locale/locale.alias' then :skip_file
when INFOFILE_RX then :info
when LOCALEDIR_RX then :mkpath
2014-03-27 17:13:39 -05:00
when *SHARE_PATHS then :mkpath
when /^icons\/.*\/icon-theme\.cache$/ then :skip_file
# all icons subfolders should also mkpath
when /^icons\// then :mkpath
when /^zsh/ then :mkpath
else :link
end
end
2009-07-29 00:56:22 +01:00
link_dir('lib', mode) do |path|
case path.to_s
when 'charset.alias' then :skip_file
# pkg-config database gets explicitly created
when 'pkgconfig' then :mkpath
# lib/language folders also get explicitly created
when 'dtrace' then :mkpath
when /^gdk-pixbuf/ then :mkpath
when 'ghc' then :mkpath
when 'lua' 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 '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 |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 path.to_s =~ /[^\/]*\.framework(\/Versions)?$/
:mkpath
else
:link
end
end
unless mode.dry_run
2014-06-26 19:06:31 -05:00
make_relative_symlink(linked_keg_record, path, mode)
optlink(mode)
end
2014-04-21 09:40:24 -05:00
rescue LinkError
unlink
raise
2014-04-21 09:40:24 -05:00
else
ObserverPathnameExtension.total
end
def optlink(mode=OpenStruct.new)
opt_record.delete if opt_record.symlink? || opt_record.exist?
make_relative_symlink(opt_record, path, mode)
end
2014-03-13 09:05:40 +00:00
def delete_pyc_files!
find { |pn| pn.delete if pn.extname == ".pyc" }
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?
keg = Keg.for(src)
dst.unlink unless mode.dry_run
keg.link_dir(src, mode) { :mkpath }
return true
end
rescue NotAKegError
puts "Won't resolve conflicts for symlink #{dst} as it doesn't resolve into the Cellar" if ARGV.verbose?
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 and 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'
# Don't link pyc files because Python overwrites these cached object
# files and next time brew wants to link, the pyc file is in the way.
2013-12-14 09:35:57 -06:00
if src.extname == '.pyc' && src.to_s =~ /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? and not 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
2013-12-14 09:35:57 -06:00
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