brew/Library/Homebrew/cask/lib/hbc/installer.rb

396 lines
12 KiB
Ruby
Raw Normal View History

2016-08-18 22:11:42 +03:00
require "rubygems"
require "hbc/cask_dependencies"
require "hbc/staged"
require "hbc/verify"
2016-09-24 13:52:43 +02:00
module Hbc
class Installer
# TODO: it is unwise for Hbc::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 Staged
include Verify
attr_reader :force, :skip_cask_deps
PERSISTENT_METADATA_SUBDIRS = ["gpg"].freeze
def initialize(cask, command: SystemCommand, force: false, skip_cask_deps: false, require_sha: false, reinstall: false)
2016-09-24 13:52:43 +02:00
@cask = cask
@command = command
@force = force
@skip_cask_deps = skip_cask_deps
@require_sha = require_sha
@reinstall = reinstall
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def self.print_caveats(cask)
odebug "Printing caveats"
2016-10-24 17:07:57 +02:00
return if 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
2016-08-18 22:11:42 +03:00
end
end
end
2016-10-24 17:07:57 +02:00
return if output.empty?
ohai "Caveats"
puts output
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def self.capture_output(&block)
old_stdout = $stdout
$stdout = Buffer.new($stdout.tty?)
block.call
output = $stdout.string
$stdout = old_stdout
output
end
2016-08-18 22:11:42 +03:00
def fetch
odebug "Hbc::Installer#fetch"
satisfy_dependencies
verify_has_sha if @require_sha && !@force
download
verify
end
def stage
odebug "Hbc::Installer#stage"
extract_primary_container
save_caskfile
rescue StandardError => e
purge_versioned_files
raise e
end
2016-09-24 13:52:43 +02:00
def install
odebug "Hbc::Installer#install"
2016-08-18 22:11:42 +03:00
if @cask.installed? && !force && !@reinstall
raise CaskAlreadyInstalledAutoUpdatesError, @cask if @cask.auto_updates
raise CaskAlreadyInstalledError, @cask
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
print_caveats
fetch
uninstall_if_neccessary
oh1 "Installing Cask #{@cask}"
stage
install_artifacts
enable_accessibility_access
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
puts summary
end
2016-08-18 22:11:42 +03:00
def uninstall_if_neccessary
return unless @cask.installed? && @reinstall
installed_cask = @cask
# use the same cask file that was used for installation, if possible
if (installed_caskfile = installed_cask.installed_caskfile).exist?
installed_cask = CaskLoader.load_from_file(installed_caskfile)
end
# Always force uninstallation, ignore method parameter
Installer.new(installed_cask, force: true).uninstall
end
2016-09-24 13:52:43 +02:00
def summary
s = ""
s << "#{Emoji.install_badge} " if Emoji.enabled?
2016-09-24 13:52:43 +02:00
s << "#{@cask} was successfully installed!"
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def download
odebug "Downloading"
@downloaded_path = Download.new(@cask, force: false).perform
2016-09-24 13:52:43 +02:00
odebug "Downloaded to -> #{@downloaded_path}"
@downloaded_path
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def verify_has_sha
odebug "Checking cask has checksum"
return unless @cask.sha256 == :no_check
raise CaskNoShasumError, @cask
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def verify
Verify.all(@cask, @downloaded_path)
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def extract_primary_container
odebug "Extracting primary container"
2016-09-24 13:52:43 +02:00
FileUtils.mkdir_p @cask.staged_path
container = if @cask.container && @cask.container.type
Container.from_type(@cask.container.type)
else
Container.for_path(@downloaded_path, @command)
end
2016-09-24 13:52:43 +02:00
unless container
raise CaskError, "Uh oh, could not figure out how to unpack '#{@downloaded_path}'"
end
2016-09-24 13:52:43 +02:00
odebug "Using container class #{container} for #{@downloaded_path}"
container.new(@cask, @downloaded_path, @command).extract
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def install_artifacts
already_installed_artifacts = []
2016-09-24 13:52:43 +02:00
odebug "Installing artifacts"
artifacts = Artifact.for_cask(@cask, command: @command, force: force)
2016-09-24 13:52:43 +02:00
odebug "#{artifacts.length} artifact/s defined", artifacts
2016-09-24 13:52:43 +02:00
artifacts.each do |artifact|
2017-02-05 07:47:54 +01:00
next unless artifact.respond_to?(:install_phase)
odebug "Installing artifact of class #{artifact.class}"
2017-02-05 07:47:54 +01:00
artifact.install_phase
already_installed_artifacts.unshift(artifact)
2016-09-24 13:52:43 +02:00
end
rescue StandardError => e
begin
already_installed_artifacts.each do |artifact|
2017-02-05 07:47:54 +01:00
next unless artifact.respond_to?(:uninstall_phase)
odebug "Reverting installation of artifact of class #{artifact.class}"
2017-02-05 07:47:54 +01:00
artifact.uninstall_phase
end
ensure
purge_versioned_files
raise e
end
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# 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
return unless @cask.depends_on
ohai "Satisfying dependencies"
macos_dependencies
arch_dependencies
x11_dependencies
formula_dependencies
cask_dependencies unless skip_cask_deps
puts "complete"
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
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, "Cask #{@cask} depends on macOS 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, "Cask #{@cask} depends on macOS release being one of [#{@cask.depends_on.macos.map(&:to_s).join(", ")}], but you are running release #{MacOS.version}."
end
else
unless MacOS.version == @cask.depends_on.macos.first
raise CaskError, "Cask #{@cask} depends on macOS release #{@cask.depends_on.macos.first}, but you are running release #{MacOS.version}."
end
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def arch_dependencies
return if @cask.depends_on.arch.nil?
@current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits }
2016-10-23 14:44:14 +02:00
return if @cask.depends_on.arch.any? do |arch|
2016-09-24 13:52:43 +02:00
arch[:type] == @current_arch[:type] &&
Array(arch[:bits]).include?(@current_arch[:bits])
2016-10-23 14:44:14 +02:00
end
2016-09-24 13:52:43 +02:00
raise CaskError, "Cask #{@cask} depends on hardware architecture being one of [#{@cask.depends_on.arch.map(&:to_s).join(", ")}], but you are running #{@current_arch}"
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def x11_dependencies
return unless @cask.depends_on.x11
raise CaskX11DependencyError, @cask.token unless MacOS::X11.installed?
2016-09-24 13:52:43 +02:00
end
def formula_dependencies
return unless @cask.depends_on.formula && !@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,
2016-09-24 13:52:43 +02:00
args: ["list", "--versions", dep_name],
print_stderr: false).stdout.include?(dep_name)
if installed
puts "already installed"
else
@command.run!(HOMEBREW_BREW_FILE,
2016-09-24 13:52:43 +02:00
args: ["install", dep_name])
puts "done"
end
2016-08-18 22:11:42 +03:00
end
end
2016-09-24 13:52:43 +02:00
def cask_dependencies
return unless @cask.depends_on.cask && !@cask.depends_on.cask.empty?
ohai "Installing Cask dependencies: #{@cask.depends_on.cask.join(", ")}"
deps = CaskDependencies.new(@cask)
deps.sorted.each do |dep_token|
puts "#{dep_token} ..."
dep = CaskLoader.load(dep_token)
2016-09-24 13:52:43 +02:00
if dep.installed?
puts "already installed"
else
Installer.new(dep, force: false, skip_cask_deps: true).install
puts "done"
end
end
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def print_caveats
self.class.print_caveats(@cask)
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# TODO: logically could be in a separate class
def enable_accessibility_access
return unless @cask.accessibility_access
ohai "Enabling accessibility access"
if MacOS.version <= :mountain_lion
@command.run!("/usr/bin/touch",
args: [Hbc.pre_mavericks_accessibility_dotfile],
sudo: true)
elsif MacOS.version <= :yosemite
@command.run!("/usr/bin/sqlite3",
args: [
2016-10-14 20:33:16 +02:00
Hbc.tcc_db,
"INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL);",
],
2016-09-24 13:52:43 +02:00
sudo: true)
elsif MacOS.version <= :el_capitan
@command.run!("/usr/bin/sqlite3",
args: [
2016-10-14 20:33:16 +02:00
Hbc.tcc_db,
"INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL,NULL);",
],
2016-09-24 13:52:43 +02:00
sudo: true)
2016-08-18 22:11:42 +03:00
else
2016-09-24 13:52:43 +02:00
opoo <<-EOS.undent
Accessibility access cannot be enabled automatically on this version of macOS.
See System Preferences to enable it manually.
EOS
2016-08-18 22:11:42 +03:00
end
rescue StandardError => e
purge_versioned_files
raise e
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def disable_accessibility_access
return unless @cask.accessibility_access
if MacOS.version >= :mavericks && MacOS.version <= :el_capitan
ohai "Disabling accessibility access"
@command.run!("/usr/bin/sqlite3",
args: [
2016-10-14 20:33:16 +02:00
Hbc.tcc_db,
"DELETE FROM access WHERE client='#{bundle_identifier}';",
],
2016-09-24 13:52:43 +02:00
sudo: true)
2016-08-18 22:11:42 +03:00
else
2016-09-24 13:52:43 +02:00
opoo <<-EOS.undent
Accessibility access cannot be disabled automatically on this version of macOS.
See System Preferences to disable it manually.
EOS
2016-08-18 22:11:42 +03:00
end
end
2016-09-24 13:52:43 +02:00
def save_caskfile
unless (old_savedirs = Pathname.glob(@cask.metadata_path("*"))).empty?
old_savedirs.each(&:rmtree)
2016-09-24 13:52:43 +02:00
end
return unless @cask.sourcefile_path
savedir = @cask.metadata_subdir("Casks", :now, true)
savedir.mkpath
FileUtils.copy @cask.sourcefile_path, savedir
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def uninstall
oh1 "Uninstalling Cask #{@cask}"
2016-09-24 13:52:43 +02:00
disable_accessibility_access
uninstall_artifacts
purge_versioned_files
purge_caskroom_path if force
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def uninstall_artifacts
odebug "Un-installing artifacts"
artifacts = Artifact.for_cask(@cask, command: @command, force: force)
# Make sure the `uninstall` stanza is run first, as it
# may depend on other artifacts still being installed.
artifacts = artifacts.sort_by { |a| a.is_a?(Artifact::Uninstall) ? -1 : 1 }
2016-09-24 13:52:43 +02:00
odebug "#{artifacts.length} artifact/s defined", artifacts
2016-09-24 13:52:43 +02:00
artifacts.each do |artifact|
2017-02-05 07:47:54 +01:00
next unless artifact.respond_to?(:uninstall_phase)
odebug "Un-installing artifact of class #{artifact.class}"
2017-02-05 07:47:54 +01:00
artifact.uninstall_phase
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def zap
ohai %Q(Implied "brew cask uninstall #{@cask}")
2016-09-24 13:52:43 +02:00
uninstall_artifacts
if Artifact::Zap.me?(@cask)
ohai "Dispatching zap stanza"
2017-02-10 07:49:21 +01:00
Artifact::Zap.new(@cask, command: @command).zap_phase
2016-09-24 13:52:43 +02:00
else
opoo "No zap stanza present for Cask '#{@cask}'"
end
ohai "Removing all staged versions of Cask '#{@cask}'"
purge_caskroom_path
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def gain_permissions_remove(path)
Utils.gain_permissions_remove(path, command: @command)
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def purge_versioned_files
odebug "Purging files for version #{@cask.version} of Cask #{@cask}"
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# versioned staged distribution
gain_permissions_remove(@cask.staged_path) if !@cask.staged_path.nil? && @cask.staged_path.exist?
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# Homebrew-Cask metadata
if @cask.metadata_versioned_container_path.respond_to?(:children) &&
@cask.metadata_versioned_container_path.exist?
@cask.metadata_versioned_container_path.children.each do |subdir|
unless PERSISTENT_METADATA_SUBDIRS.include?(subdir.basename)
gain_permissions_remove(subdir)
end
2016-08-18 22:11:42 +03:00
end
end
2016-09-24 13:52:43 +02:00
@cask.metadata_versioned_container_path.rmdir_if_possible
@cask.metadata_master_container_path.rmdir_if_possible
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# toplevel staged distribution
@cask.caskroom_path.rmdir_if_possible
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def purge_caskroom_path
odebug "Purging all staged versions of Cask #{@cask}"
gain_permissions_remove(@cask.caskroom_path)
end
2016-08-18 22:11:42 +03:00
end
end