homebrew-cask-versions/lib/cask/installer.rb
Roland Walker bf99c6b0a3 Store metadata with installed Casks.
Step 2: Snapshot the Cask file used at install-time, using
the previously-merged metadata directory support.

Using a simple filesystem-is-a-database approach, we set up a
`.metadata` directory for each installed Cask in which we can
record any information, starting with a copy of the Cask
definition which was used at install-time.

This should be useful for various cases such as:
- a fallback when the Cask was renamed/removed.  We
  currently cannot recover any uninstall info in that
  scenario
- installation of multiple versions of the same software

There might be a smarter way to discover the source filename
for the Cask through introspection or some deep Ruby voodoo.
All I could come up with was to pass the filename in on the
constructor, which seems perfectly reasonable if voodoo is
not available.  The existing code was taking the title as an
argument to the constructor, which is dispensable.

This PR contains no code to actually make use of the metadata,
but only takes care of the relevant book-keeping: creation,
destruction, as well as organization of metadata according to
software version number and timestamp.
2014-12-10 18:24:43 -05:00

334 lines
11 KiB
Ruby

require 'rubygems'
require 'cask/staged'
class Cask::Installer
# todo: it is unwise for Cask::Staged to be a module, when we are
# dealing with both staged and unstaged Casks here. This should
# either be a class which is only sometimes instantiated, or there
# should be explicit checks on whether staged state is valid in
# every method.
include Cask::Staged
PERSISTENT_METADATA_SUBDIRS = [ 'gpg' ]
def initialize(cask, command=Cask::SystemCommand)
@cask = cask
@command = command
end
def self.print_caveats(cask)
odebug "Printing caveats"
unless cask.caveats.empty?
output = capture_output do
cask.caveats.each do |caveat|
if caveat.respond_to?(:eval_and_print)
caveat.eval_and_print(cask)
else
puts caveat
end
end
end
unless output.empty?
ohai "Caveats"
puts output
end
end
end
def self.capture_output(&block)
old_stdout = $stdout
$stdout = Buffer.new($stdout.tty?)
block.call
output = $stdout.string
$stdout = old_stdout
output
end
def install(force=false)
odebug "Cask::Installer.install"
if @cask.installed? && !force
raise CaskAlreadyInstalledError.new(@cask)
end
print_caveats
begin
satisfy_dependencies
download
extract_primary_container
install_artifacts
save_caskfile force
enable_accessibility_access
rescue StandardError => e
purge_versioned_files
raise e
end
puts summary
end
def summary
s = if MacOS.version >= :lion and not ENV['HOMEBREW_NO_EMOJI']
(ENV['HOMEBREW_INSTALL_BADGE'] || "\xf0\x9f\x8d\xba") + ' '
else
"#{Tty.blue}==>#{Tty.white} Success!#{Tty.reset} "
end
s << "#{@cask} staged at '#{@cask.staged_path}' (#{@cask.staged_path.cabv})"
end
def download
odebug "Downloading"
download = Cask::Download.new(@cask)
@downloaded_path = download.perform
odebug "Downloaded to -> #{@downloaded_path}"
@downloaded_path
end
def extract_primary_container
odebug "Extracting primary container"
FileUtils.mkdir_p @cask.staged_path
container = if @cask.container and @cask.container.type
Cask::Container.from_type(@cask.container.type)
else
Cask::Container.for_path(@downloaded_path, @command)
end
unless container
raise CaskError.new "Uh oh, could not identify primary container for '#{@downloaded_path}'"
end
odebug "Using container class #{container} for #{@downloaded_path}"
container.new(@cask, @downloaded_path, @command).extract
end
def install_artifacts
odebug "Installing artifacts"
artifacts = Cask::Artifact.for_cask(@cask)
odebug "#{artifacts.length} artifact/s defined", artifacts
artifacts.each do |artifact|
odebug "Installing artifact of class #{artifact}"
artifact.new(@cask, @command).install_phase
end
end
# todo move dependencies to a separate class
# dependencies should also apply for "brew cask stage"
# override dependencies with --force or perhaps --force-deps
def satisfy_dependencies
if @cask.depends_on
ohai 'Satisfying dependencies'
macos_dependencies
arch_dependencies
x11_dependencies
formula_dependencies
puts 'complete'
end
end
def macos_dependencies
return unless @cask.depends_on.macos
if @cask.depends_on.macos.first.is_a?(Array)
operator, release = @cask.depends_on.macos.first
unless MacOS.version.send(operator, release)
raise CaskError.new "Cask #{@cask} depends on OS X release #{operator} #{release}, but you are running release #{MacOS.version}."
end
elsif @cask.depends_on.macos.length > 1
unless @cask.depends_on.macos.include?(Gem::Version.new(MacOS.version.to_s))
raise CaskError.new "Cask #{@cask} depends on OS X release being one of: #{@cask.depends_on.macos(&:to_s).inspect}, but you are running release #{MacOS.version}."
end
else
unless MacOS.version == @cask.depends_on.macos.first
raise CaskError.new "Cask #{@cask} depends on OS X release #{@cask.depends_on.macos.first}, but you are running release #{MacOS.version}."
end
end
end
def arch_dependencies
return unless @cask.depends_on.arch
@current_arch ||= [
Hardware::CPU.type,
Hardware::CPU.is_32_bit? ?
(Hardware::CPU.intel? ? :i386 : :ppc_7400) :
(Hardware::CPU.intel? ? :x86_64 : :ppc_64)
]
return unless Array(@cask.depends_on.arch & @current_arch).empty?
raise CaskError.new "Cask #{@cask} depends on hardware architecture being one of #{@cask.depends_on.arch.inspect}, but you are running #{@current_arch.inspect}"
end
def x11_dependencies
return unless @cask.depends_on.x11
raise CaskX11DependencyError.new(@cask.token) unless Cask.x11_executable.exist?
end
def formula_dependencies
return unless @cask.depends_on.formula and not @cask.depends_on.formula.empty?
ohai 'Installing Formula dependencies from Homebrew'
@cask.depends_on.formula.each do |dep_name|
print "#{dep_name} ... "
installed = @command.run(HOMEBREW_BREW_FILE,
:args => ['list', '--versions', dep_name],
:print_stderr => false).stdout.include?(dep_name)
if installed
puts "already installed"
else
@command.run!(HOMEBREW_BREW_FILE,
:args => ['install', dep_name])
puts "done"
end
end
end
def print_caveats
self.class.print_caveats(@cask)
end
# todo: logically could be in a separate class
def enable_accessibility_access
return unless @cask.accessibility_access
ohai 'Enabling accessibility access'
if MacOS.version >= :mavericks
@command.run!('/usr/bin/sqlite3',
:args => [
Cask.tcc_db,
"INSERT INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL);",
],
:sudo => true)
else
@command.run!('/usr/bin/touch',
:args => [Cask.pre_mavericks_accessibility_dotfile],
:sudo => true)
end
end
def disable_accessibility_access
return unless @cask.accessibility_access
if MacOS.version >= :mavericks
ohai 'Disabling accessibility access'
@command.run!('/usr/bin/sqlite3',
:args => [
Cask.tcc_db,
"DELETE FROM access WHERE client='#{bundle_identifier}';",
],
:sudo => true)
else
opoo <<-EOS.undent
Accessibility access was enabled for #{@cask}, but it is not safe to disable
automatically on this version of OS X. See System Preferences.
EOS
end
end
def save_caskfile(force=false)
timestamp = :now
create = true
savedir = @cask.metadata_subdir('Casks', timestamp, create)
if Dir.entries(savedir).size > 2
# should not happen
if force
savedir.rmtree
FileUtils.mkdir_p savedir
else
raise CaskAlreadyInstalledError.new(@cask)
end
end
FileUtils.copy(@cask.sourcefile_path, savedir) if @cask.sourcefile_path
end
def uninstall(force=false)
odebug "Cask::Installer.uninstall"
disable_accessibility_access
uninstall_artifacts
purge_versioned_files
purge_caskroom_path if force
end
def uninstall_artifacts
odebug "Un-installing artifacts"
artifacts = Cask::Artifact.for_cask(@cask)
odebug "#{artifacts.length} artifact/s defined", artifacts
artifacts.each do |artifact|
odebug "Un-installing artifact of class #{artifact}"
artifact.new(@cask, @command).uninstall_phase
end
end
def zap
ohai %Q{Implied "brew cask uninstall #{@cask}"}
uninstall_artifacts
if Cask::Artifact::Zap.me?(@cask)
ohai "Dispatching zap stanza"
Cask::Artifact::Zap.new(@cask, @command).zap_phase
else
opoo "No zap stanza present for Cask '#{@cask}'"
end
ohai %Q{Removing all staged versions of Cask '#{@cask}'}
purge_caskroom_path
end
# this feels like a class method, but uses @command
def permissions_rmtree(path)
if path.respond_to?(:rmtree) and path.exist?
tried_permissions = false
tried_ownership = false
begin
path.rmtree
rescue StandardError => e
# in case of permissions problems
if path.exist? and !tried_permissions
begin
# todo Better handling for the case where path is a symlink.
# The -h and -R flags cannot be combined, and behavior is
# dependent on whether the file argument has a trailing
# slash. This should do the right thing, but is fragile.
@command.run!('/usr/bin/chflags', :args => ['-R', '--', '000', path])
@command.run!('/bin/chmod', :args => ['-R', '--', 'u+rwx', path])
@command.run!('/bin/chmod', :args => ['-R', '-N', path])
rescue StandardError => e
unless tried_ownership
# in case of ownership problems
# todo Further examine files to see if ownership is the problem
# before using sudo+chown
ohai "Using sudo to gain ownership of path '#{path}'"
current_user = Etc.getpwuid(Process.euid).name
@command.run('/usr/sbin/chown', :args => ['-R', '--', current_user, path],
:sudo => true)
tried_ownership = true
retry # permissions
end
end
tried_permissions = true
retry # rmtree
end
end
end
end
def purge_versioned_files
odebug "Purging files for version #{@cask.version} of Cask #{@cask}"
# versioned staged distribution
permissions_rmtree(@cask.staged_path)
# Homebrew-cask metadata
if @cask.metadata_versioned_container_path.respond_to?(:children) and
@cask.metadata_versioned_container_path.exist?
@cask.metadata_versioned_container_path.children.each do |subdir|
permissions_rmtree subdir unless PERSISTENT_METADATA_SUBDIRS.include?(subdir.basename)
end
end
if @cask.metadata_versioned_container_path.respond_to?(:rmdir_if_possible)
@cask.metadata_versioned_container_path.rmdir_if_possible
end
if @cask.metadata_master_container_path.respond_to?(:rmdir_if_possible)
@cask.metadata_master_container_path.rmdir_if_possible
end
# toplevel staged distribution
@cask.caskroom_path.rmdir_if_possible
end
def purge_caskroom_path
odebug "Purging all staged versions of Cask #{@cask}"
permissions_rmtree(@cask.caskroom_path)
end
end