mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
408 lines
13 KiB
Ruby
408 lines
13 KiB
Ruby
require "rubygems"
|
|
|
|
require "hbc/cask_dependencies"
|
|
require "hbc/staged"
|
|
require "hbc/verify"
|
|
|
|
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, binaries: true, require_sha: false)
|
|
@cask = cask
|
|
@command = command
|
|
@force = force
|
|
@skip_cask_deps = skip_cask_deps
|
|
@binaries = binaries
|
|
@require_sha = require_sha
|
|
@reinstall = false
|
|
end
|
|
|
|
def binaries?
|
|
@binaries
|
|
end
|
|
|
|
def self.print_caveats(cask)
|
|
odebug "Printing caveats"
|
|
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
|
|
end
|
|
end
|
|
end
|
|
|
|
return if output.empty?
|
|
ohai "Caveats"
|
|
puts output
|
|
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 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
|
|
|
|
def install
|
|
odebug "Hbc::Installer#install"
|
|
|
|
if @cask.installed? && !force && !@reinstall
|
|
raise CaskAlreadyInstalledAutoUpdatesError, @cask if @cask.auto_updates
|
|
raise CaskAlreadyInstalledError, @cask
|
|
end
|
|
|
|
print_caveats
|
|
fetch
|
|
uninstall_existing_cask if @reinstall
|
|
|
|
oh1 "Installing Cask #{@cask}"
|
|
stage
|
|
install_artifacts
|
|
enable_accessibility_access
|
|
|
|
puts summary
|
|
end
|
|
|
|
def reinstall
|
|
odebug "Hbc::Installer#reinstall"
|
|
@reinstall = true
|
|
install
|
|
end
|
|
|
|
def uninstall_existing_cask
|
|
return unless @cask.installed?
|
|
|
|
# use the same cask file that was used for installation, if possible
|
|
installed_caskfile = @cask.installed_caskfile
|
|
installed_cask = installed_caskfile.exist? ? CaskLoader.load_from_file(installed_caskfile) : @cask
|
|
|
|
# Always force uninstallation, ignore method parameter
|
|
Installer.new(installed_cask, binaries: binaries?, force: true).uninstall
|
|
end
|
|
|
|
def summary
|
|
s = ""
|
|
s << "#{Emoji.install_badge} " if Emoji.enabled?
|
|
s << "#{@cask} was successfully installed!"
|
|
end
|
|
|
|
def download
|
|
odebug "Downloading"
|
|
@downloaded_path = Download.new(@cask, force: false).perform
|
|
odebug "Downloaded to -> #{@downloaded_path}"
|
|
@downloaded_path
|
|
end
|
|
|
|
def verify_has_sha
|
|
odebug "Checking cask has checksum"
|
|
return unless @cask.sha256 == :no_check
|
|
raise CaskNoShasumError, @cask
|
|
end
|
|
|
|
def verify
|
|
Verify.all(@cask, @downloaded_path)
|
|
end
|
|
|
|
def extract_primary_container
|
|
odebug "Extracting primary container"
|
|
|
|
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
|
|
|
|
unless container
|
|
raise CaskError, "Uh oh, could not figure out how to unpack '#{@downloaded_path}'"
|
|
end
|
|
|
|
odebug "Using container class #{container} for #{@downloaded_path}"
|
|
container.new(@cask, @downloaded_path, @command).extract
|
|
end
|
|
|
|
def install_artifacts
|
|
already_installed_artifacts = []
|
|
|
|
odebug "Installing artifacts"
|
|
artifacts = Artifact.for_cask(@cask, command: @command, force: force)
|
|
odebug "#{artifacts.length} artifact/s defined", artifacts
|
|
|
|
artifacts.each do |artifact|
|
|
next unless artifact.respond_to?(:install_phase)
|
|
odebug "Installing artifact of class #{artifact.class}"
|
|
|
|
if artifact.is_a?(Artifact::Binary)
|
|
next unless binaries?
|
|
end
|
|
|
|
artifact.install_phase
|
|
already_installed_artifacts.unshift(artifact)
|
|
end
|
|
rescue StandardError => e
|
|
begin
|
|
already_installed_artifacts.each do |artifact|
|
|
next unless artifact.respond_to?(:uninstall_phase)
|
|
odebug "Reverting installation of artifact of class #{artifact.class}"
|
|
artifact.uninstall_phase
|
|
end
|
|
ensure
|
|
purge_versioned_files
|
|
raise e
|
|
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
|
|
return unless @cask.depends_on
|
|
|
|
ohai "Satisfying dependencies"
|
|
macos_dependencies
|
|
arch_dependencies
|
|
x11_dependencies
|
|
formula_dependencies
|
|
cask_dependencies unless skip_cask_deps
|
|
puts "complete"
|
|
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, "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
|
|
end
|
|
|
|
def arch_dependencies
|
|
return if @cask.depends_on.arch.nil?
|
|
@current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits }
|
|
return if @cask.depends_on.arch.any? do |arch|
|
|
arch[:type] == @current_arch[:type] &&
|
|
Array(arch[:bits]).include?(@current_arch[:bits])
|
|
end
|
|
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
|
|
|
|
def x11_dependencies
|
|
return unless @cask.depends_on.x11
|
|
raise CaskX11DependencyError, @cask.token unless MacOS::X11.installed?
|
|
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,
|
|
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 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)
|
|
if dep.installed?
|
|
puts "already installed"
|
|
else
|
|
Installer.new(dep, force: false, binaries: binaries?, skip_cask_deps: true).install
|
|
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 <= :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: [
|
|
Hbc.tcc_db,
|
|
"INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL);",
|
|
],
|
|
sudo: true)
|
|
elsif MacOS.version <= :el_capitan
|
|
@command.run!("/usr/bin/sqlite3",
|
|
args: [
|
|
Hbc.tcc_db,
|
|
"INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL,NULL);",
|
|
],
|
|
sudo: true)
|
|
else
|
|
opoo <<-EOS.undent
|
|
Accessibility access cannot be enabled automatically on this version of macOS.
|
|
See System Preferences to enable it manually.
|
|
EOS
|
|
end
|
|
rescue StandardError => e
|
|
purge_versioned_files
|
|
raise e
|
|
end
|
|
|
|
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: [
|
|
Hbc.tcc_db,
|
|
"DELETE FROM access WHERE client='#{bundle_identifier}';",
|
|
],
|
|
sudo: true)
|
|
else
|
|
opoo <<-EOS.undent
|
|
Accessibility access cannot be disabled automatically on this version of macOS.
|
|
See System Preferences to disable it manually.
|
|
EOS
|
|
end
|
|
end
|
|
|
|
def save_caskfile
|
|
old_savedir = @cask.metadata_timestamped_path
|
|
|
|
return unless @cask.sourcefile_path
|
|
|
|
savedir = @cask.metadata_subdir("Casks", timestamp: :now, create: true)
|
|
FileUtils.copy @cask.sourcefile_path, savedir
|
|
old_savedir.rmtree unless old_savedir.nil?
|
|
end
|
|
|
|
def uninstall
|
|
oh1 "Uninstalling Cask #{@cask}"
|
|
disable_accessibility_access
|
|
uninstall_artifacts
|
|
purge_versioned_files
|
|
purge_caskroom_path if force
|
|
end
|
|
|
|
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 }
|
|
|
|
odebug "#{artifacts.length} artifact/s defined", artifacts
|
|
|
|
artifacts.each do |artifact|
|
|
next unless artifact.respond_to?(:uninstall_phase)
|
|
odebug "Un-installing artifact of class #{artifact.class}"
|
|
artifact.uninstall_phase
|
|
end
|
|
end
|
|
|
|
def zap
|
|
ohai %Q(Implied "brew cask uninstall #{@cask}")
|
|
uninstall_artifacts
|
|
if Artifact::Zap.me?(@cask)
|
|
ohai "Dispatching zap stanza"
|
|
Artifact::Zap.new(@cask, command: @command).zap_phase
|
|
else
|
|
opoo "No zap stanza present for Cask '#{@cask}'"
|
|
end
|
|
ohai "Removing all staged versions of Cask '#{@cask}'"
|
|
purge_caskroom_path
|
|
end
|
|
|
|
def gain_permissions_remove(path)
|
|
Utils.gain_permissions_remove(path, command: @command)
|
|
end
|
|
|
|
def purge_versioned_files
|
|
odebug "Purging files for version #{@cask.version} of Cask #{@cask}"
|
|
|
|
# versioned staged distribution
|
|
gain_permissions_remove(@cask.staged_path) if !@cask.staged_path.nil? && @cask.staged_path.exist?
|
|
|
|
# Homebrew-Cask metadata
|
|
if @cask.metadata_versioned_path.respond_to?(:children) &&
|
|
@cask.metadata_versioned_path.exist?
|
|
@cask.metadata_versioned_path.children.each do |subdir|
|
|
unless PERSISTENT_METADATA_SUBDIRS.include?(subdir.basename)
|
|
gain_permissions_remove(subdir)
|
|
end
|
|
end
|
|
end
|
|
@cask.metadata_versioned_path.rmdir_if_possible
|
|
@cask.metadata_master_container_path.rmdir_if_possible
|
|
|
|
# toplevel staged distribution
|
|
@cask.caskroom_path.rmdir_if_possible
|
|
end
|
|
|
|
def purge_caskroom_path
|
|
odebug "Purging all staged versions of Cask #{@cask}"
|
|
gain_permissions_remove(@cask.caskroom_path)
|
|
end
|
|
end
|
|
end
|