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