mirror of
https://github.com/donl/homebrew-cask-versions.git
synced 2026-07-17 06:16:47 -06:00
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.
334 lines
11 KiB
Ruby
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
|