aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cask/lib
diff options
context:
space:
mode:
authorAnastasiaSulyagina2016-08-18 22:11:42 +0300
committerAnastasiaSulyagina2016-08-19 14:50:14 +0300
commite81f4ab7deeb40308f240be5ea00091fc8786d7a (patch)
treeb5418f9149de71c0f05f90cb2b39ab47f46e27b4 /Library/Homebrew/cask/lib
parent5c7c9de669025bbe4cad9829be39c5cf3b31ad25 (diff)
downloadbrew-e81f4ab7deeb40308f240be5ea00091fc8786d7a.tar.bz2
init
Diffstat (limited to 'Library/Homebrew/cask/lib')
-rw-r--r--Library/Homebrew/cask/lib/hbc.rb60
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact.rb65
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb36
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/app.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/artifact.rb20
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/base.rb79
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/binary.rb7
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/font.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/input_method.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/installer.rb41
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/moved.rb88
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb24
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/pkg.rb53
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb7
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb21
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/relocated.rb53
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/service.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb15
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/suite.rb11
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb65
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb249
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb4
-rw-r--r--Library/Homebrew/cask/lib/hbc/artifact/zap.rb16
-rw-r--r--Library/Homebrew/cask/lib/hbc/audit.rb216
-rw-r--r--Library/Homebrew/cask/lib/hbc/auditor.rb10
-rw-r--r--Library/Homebrew/cask/lib/hbc/cache.rb34
-rw-r--r--Library/Homebrew/cask/lib/hbc/cask.rb114
-rw-r--r--Library/Homebrew/cask/lib/hbc/cask_dependencies.rb33
-rw-r--r--Library/Homebrew/cask/lib/hbc/caskroom.rb28
-rw-r--r--Library/Homebrew/cask/lib/hbc/caveats.rb12
-rw-r--r--Library/Homebrew/cask/lib/hbc/checkable.rb51
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli.rb274
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/audit.rb52
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/base.rb21
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/cat.rb15
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/cleanup.rb88
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/create.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/doctor.rb213
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/edit.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/fetch.rb19
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/home.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/info.rb66
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/install.rb60
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb135
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb15
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb30
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_help.rb19
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb127
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb9
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/list.rb82
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/search.rb56
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/style.rb69
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/uninstall.rb40
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/update.rb16
-rw-r--r--Library/Homebrew/cask/lib/hbc/cli/zap.rb15
-rw-r--r--Library/Homebrew/cask/lib/hbc/container.rb68
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/air.rb33
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/base.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/bzip2.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/cab.rb26
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/criteria.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/dmg.rb125
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/generic_unar.rb26
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/gzip.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/lzma.rb23
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/naked.rb19
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/otf.rb7
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/pkg.rb9
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/rar.rb8
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/seven_zip.rb9
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/sit.rb8
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/tar.rb18
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/ttf.rb10
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/xar.rb16
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/xip.rb25
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/xz.rb23
-rw-r--r--Library/Homebrew/cask/lib/hbc/container/zip.rb15
-rw-r--r--Library/Homebrew/cask/lib/hbc/download.rb43
-rw-r--r--Library/Homebrew/cask/lib/hbc/download_strategy.rb332
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl.rb283
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/appcast.rb17
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/base.rb21
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/caveats.rb112
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb30
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/container.rb26
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb124
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/gpg.rb43
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/installer.rb28
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/license.rb66
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/postflight.rb9
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/preflight.rb3
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb2
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb5
-rw-r--r--Library/Homebrew/cask/lib/hbc/dsl/version.rb111
-rw-r--r--Library/Homebrew/cask/lib/hbc/exceptions.rb146
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend.rb6
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend/hash.rb7
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend/io.rb10
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend/optparse.rb6
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend/pathname.rb19
-rw-r--r--Library/Homebrew/cask/lib/hbc/extend/string.rb5
-rw-r--r--Library/Homebrew/cask/lib/hbc/fetcher.rb22
-rw-r--r--Library/Homebrew/cask/lib/hbc/installer.rb343
-rw-r--r--Library/Homebrew/cask/lib/hbc/locations.rb196
-rw-r--r--Library/Homebrew/cask/lib/hbc/macos.rb378
-rw-r--r--Library/Homebrew/cask/lib/hbc/options.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/pkg.rb113
-rw-r--r--Library/Homebrew/cask/lib/hbc/qualified_token.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/scopes.rb59
-rw-r--r--Library/Homebrew/cask/lib/hbc/source.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/gone.rb19
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/path_base.rb65
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb8
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb8
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/tapped.rb35
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb12
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb11
-rw-r--r--Library/Homebrew/cask/lib/hbc/source/uri.rb28
-rw-r--r--Library/Homebrew/cask/lib/hbc/staged.rb48
-rw-r--r--Library/Homebrew/cask/lib/hbc/system_command.rb173
-rw-r--r--Library/Homebrew/cask/lib/hbc/topological_hash.rb12
-rw-r--r--Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb26
-rw-r--r--Library/Homebrew/cask/lib/hbc/url.rb37
-rw-r--r--Library/Homebrew/cask/lib/hbc/url_checker.rb75
-rw-r--r--Library/Homebrew/cask/lib/hbc/utils.rb198
-rw-r--r--Library/Homebrew/cask/lib/hbc/utils/file.rb12
-rw-r--r--Library/Homebrew/cask/lib/hbc/utils/tty.rb125
-rw-r--r--Library/Homebrew/cask/lib/hbc/verify.rb33
-rw-r--r--Library/Homebrew/cask/lib/hbc/verify/checksum.rb43
-rw-r--r--Library/Homebrew/cask/lib/hbc/verify/gpg.rb60
-rw-r--r--Library/Homebrew/cask/lib/hbc/version.rb13
-rw-r--r--Library/Homebrew/cask/lib/hbc/without_source.rb15
-rw-r--r--Library/Homebrew/cask/lib/vendor/plist.rb234
142 files changed, 7516 insertions, 0 deletions
diff --git a/Library/Homebrew/cask/lib/hbc.rb b/Library/Homebrew/cask/lib/hbc.rb
new file mode 100644
index 000000000..a9a23f997
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc.rb
@@ -0,0 +1,60 @@
+module Hbc; end
+
+require "hardware"
+require "hbc/extend"
+require "hbc/artifact"
+require "hbc/audit"
+require "hbc/auditor"
+require "hbc/cache"
+require "hbc/cask"
+require "hbc/without_source"
+require "hbc/caskroom"
+require "hbc/checkable"
+require "hbc/cli"
+require "hbc/cask_dependencies"
+require "hbc/caveats"
+require "hbc/container"
+require "hbc/download"
+require "hbc/download_strategy"
+require "hbc/exceptions"
+require "hbc/fetcher"
+require "hbc/installer"
+require "hbc/locations"
+require "hbc/macos"
+require "hbc/options"
+require "hbc/pkg"
+require "hbc/qualified_token"
+require "hbc/scopes"
+require "hbc/source"
+require "hbc/staged"
+require "hbc/system_command"
+require "hbc/topological_hash"
+require "hbc/underscore_supporting_uri"
+require "hbc/url"
+require "hbc/url_checker"
+require "hbc/utils"
+require "hbc/verify"
+require "hbc/version"
+
+require "vendor/plist"
+
+module Hbc
+ include Hbc::Locations
+ include Hbc::Scopes
+ include Hbc::Options
+ include Hbc::Utils
+
+ def self.init
+ Hbc::Cache.ensure_cache_exists
+ Hbc::Cache.migrate_legacy_cache
+
+ Hbc::Caskroom.ensure_caskroom_exists
+ end
+
+ def self.load(query)
+ odebug "Loading Cask definitions"
+ cask = Hbc::Source.for_query(query).load
+ cask.dumpcask
+ cask
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact.rb b/Library/Homebrew/cask/lib/hbc/artifact.rb
new file mode 100644
index 000000000..73bd582a5
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact.rb
@@ -0,0 +1,65 @@
+module Hbc::Artifact; end
+
+require "hbc/artifact/app"
+require "hbc/artifact/artifact" # generic 'artifact' stanza
+require "hbc/artifact/binary"
+require "hbc/artifact/colorpicker"
+require "hbc/artifact/font"
+require "hbc/artifact/input_method"
+require "hbc/artifact/installer"
+require "hbc/artifact/internet_plugin"
+require "hbc/artifact/audio_unit_plugin"
+require "hbc/artifact/vst_plugin"
+require "hbc/artifact/vst3_plugin"
+require "hbc/artifact/nested_container"
+require "hbc/artifact/pkg"
+require "hbc/artifact/postflight_block"
+require "hbc/artifact/preflight_block"
+require "hbc/artifact/prefpane"
+require "hbc/artifact/qlplugin"
+require "hbc/artifact/screen_saver"
+require "hbc/artifact/service"
+require "hbc/artifact/stage_only"
+require "hbc/artifact/suite"
+require "hbc/artifact/uninstall"
+require "hbc/artifact/zap"
+
+module Hbc::Artifact
+ # NOTE: order is important here, since we want to extract nested containers
+ # before we handle any other artifacts
+ def self.artifacts
+ [
+ Hbc::Artifact::PreflightBlock,
+ Hbc::Artifact::NestedContainer,
+ Hbc::Artifact::Installer,
+ Hbc::Artifact::App,
+ Hbc::Artifact::Suite,
+ Hbc::Artifact::Artifact, # generic 'artifact' stanza
+ Hbc::Artifact::Colorpicker,
+ Hbc::Artifact::Pkg,
+ Hbc::Artifact::Prefpane,
+ Hbc::Artifact::Qlplugin,
+ Hbc::Artifact::Font,
+ Hbc::Artifact::Service,
+ Hbc::Artifact::StageOnly,
+ Hbc::Artifact::Binary,
+ Hbc::Artifact::InputMethod,
+ Hbc::Artifact::InternetPlugin,
+ Hbc::Artifact::AudioUnitPlugin,
+ Hbc::Artifact::VstPlugin,
+ Hbc::Artifact::Vst3Plugin,
+ Hbc::Artifact::ScreenSaver,
+ Hbc::Artifact::Uninstall,
+ Hbc::Artifact::PostflightBlock,
+ Hbc::Artifact::Zap,
+ ]
+ end
+
+ def self.for_cask(cask)
+ odebug "Determining which artifacts are present in Cask #{cask}"
+ artifacts.select do |artifact|
+ odebug "Checking for artifact class #{artifact}"
+ artifact.me?(cask)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb
new file mode 100644
index 000000000..fcf98d7ad
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb
@@ -0,0 +1,36 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::AbstractFlightBlock < Hbc::Artifact::Base
+ def self.artifact_dsl_key
+ super.to_s.sub(%r{_block$}, "").to_sym
+ end
+
+ def self.uninstall_artifact_dsl_key
+ artifact_dsl_key.to_s.prepend("uninstall_").to_sym
+ end
+
+ def self.class_for_dsl_key(dsl_key)
+ Object.const_get("Hbc::DSL::#{dsl_key.to_s.split('_').collect(&:capitalize).join}")
+ end
+
+ def self.me?(cask)
+ cask.artifacts[artifact_dsl_key].any? ||
+ cask.artifacts[uninstall_artifact_dsl_key].any?
+ end
+
+ def install_phase
+ abstract_phase(self.class.artifact_dsl_key)
+ end
+
+ def uninstall_phase
+ abstract_phase(self.class.uninstall_artifact_dsl_key)
+ end
+
+ private
+
+ def abstract_phase(dsl_key)
+ @cask.artifacts[dsl_key].each do |block|
+ self.class.class_for_dsl_key(dsl_key).new(@cask).instance_eval(&block)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/app.rb b/Library/Homebrew/cask/lib/hbc/artifact/app.rb
new file mode 100644
index 000000000..bbda16f74
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/app.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::App < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb b/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb
new file mode 100644
index 000000000..e2c06eb70
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb
@@ -0,0 +1,20 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Artifact < Hbc::Artifact::Moved
+ def self.artifact_english_name
+ "Generic Artifact"
+ end
+
+ def self.artifact_dirmethod
+ :appdir
+ end
+
+ def load_specification(artifact_spec)
+ source_string, target_hash = artifact_spec
+ raise Hbc::CaskInvalidError.new(@cask.token, "no source given for artifact") if source_string.nil?
+ @source = @cask.staged_path.join(source_string)
+ raise Hbc::CaskInvalidError.new(@cask.token, "target required for generic artifact #{source_string}") unless target_hash.is_a?(Hash)
+ target_hash.assert_valid_keys(:target)
+ @target = Pathname.new(target_hash[:target])
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb
new file mode 100644
index 000000000..7f3999306
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::AudioUnitPlugin < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/base.rb b/Library/Homebrew/cask/lib/hbc/artifact/base.rb
new file mode 100644
index 000000000..9a07cc906
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/base.rb
@@ -0,0 +1,79 @@
+class Hbc::Artifact::Base
+ def self.artifact_name
+ @artifact_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1_\2').downcase
+ end
+
+ def self.artifact_english_name
+ @artifact_english_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1 \2')
+ end
+
+ def self.artifact_english_article
+ @artifact_english_article ||= artifact_english_name =~ %r{^[aeiou]}i ? "an" : "a"
+ end
+
+ def self.artifact_dsl_key
+ @artifact_dsl_key ||= artifact_name.to_sym
+ end
+
+ def self.artifact_dirmethod
+ @artifact_dirmethod ||= "#{artifact_name}dir".to_sym
+ end
+
+ def self.me?(cask)
+ cask.artifacts[artifact_dsl_key].any?
+ end
+
+ attr_reader :force
+
+ def zap_phase
+ odebug "Nothing to do. The #{self.class.artifact_name} artifact has no zap phase."
+ end
+
+ # TODO: this sort of logic would make more sense in dsl.rb, or a
+ # constructor called from dsl.rb, so long as that isn't slow.
+ def self.read_script_arguments(arguments, stanza, default_arguments = {}, override_arguments = {}, key = nil)
+ # TODO: when stanza names are harmonized with class names,
+ # stanza may not be needed as an explicit argument
+ description = stanza.to_s
+ if key
+ arguments = arguments[key]
+ description.concat(" #{key.inspect}")
+ end
+
+ # backward-compatible string value
+ arguments = { executable: arguments } if arguments.is_a?(String)
+
+ # key sanity
+ permitted_keys = [:args, :input, :executable, :must_succeed, :sudo, :bsexec, :print_stdout, :print_stderr]
+ unknown_keys = arguments.keys - permitted_keys
+ unless unknown_keys.empty?
+ opoo %Q{Unknown arguments to #{description} -- #{unknown_keys.inspect} (ignored). Running "brew update; brew cleanup; brew cask cleanup" will likely fix it.}
+ end
+ arguments.reject! { |k| !permitted_keys.include?(k) }
+
+ # key warnings
+ override_keys = override_arguments.keys
+ ignored_keys = arguments.keys & override_keys
+ unless ignored_keys.empty?
+ onoe "Some arguments to #{description} will be ignored -- :#{unknown_keys.inspect} (overridden)."
+ end
+
+ # extract executable
+ executable = arguments.key?(:executable) ? arguments.delete(:executable) : nil
+
+ arguments = default_arguments.merge arguments
+ arguments.merge! override_arguments
+
+ [executable, arguments]
+ end
+
+ def summary
+ {}
+ end
+
+ def initialize(cask, command: Hbc::SystemCommand, force: false)
+ @cask = cask
+ @command = command
+ @force = force
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/binary.rb b/Library/Homebrew/cask/lib/hbc/artifact/binary.rb
new file mode 100644
index 000000000..ccaebe0c8
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/binary.rb
@@ -0,0 +1,7 @@
+require "hbc/artifact/symlinked"
+
+class Hbc::Artifact::Binary < Hbc::Artifact::Symlinked
+ def install_phase
+ super unless Hbc.no_binaries
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb b/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb
new file mode 100644
index 000000000..7b56d0ffc
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Colorpicker < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/font.rb b/Library/Homebrew/cask/lib/hbc/artifact/font.rb
new file mode 100644
index 000000000..9697d9e13
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/font.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Font < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb b/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb
new file mode 100644
index 000000000..3c7f3d990
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::InputMethod < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/installer.rb b/Library/Homebrew/cask/lib/hbc/artifact/installer.rb
new file mode 100644
index 000000000..2f66397e9
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/installer.rb
@@ -0,0 +1,41 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::Installer < Hbc::Artifact::Base
+ # TODO: for backward compatibility, removeme
+ def install
+ install_phase
+ end
+
+ # TODO: for backward compatibility, removeme
+ def uninstall
+ uninstall_phase
+ end
+
+ def install_phase
+ @cask.artifacts[self.class.artifact_dsl_key].each do |artifact|
+ if artifact.manual
+ puts <<-EOS.undent
+ To complete the installation of Cask #{@cask}, you must also
+ run the installer at
+
+ '#{@cask.staged_path.join(artifact.manual)}'
+
+ EOS
+ else
+ executable, script_arguments = self.class.read_script_arguments(artifact.script,
+ self.class.artifact_dsl_key.to_s,
+ { must_succeed: true, sudo: true },
+ print_stdout: true)
+ ohai "Running #{self.class.artifact_dsl_key} script #{executable}"
+ raise Hbc::CaskInvalidError.new(@cask, "#{self.class.artifact_dsl_key} missing executable") if executable.nil?
+ executable_path = @cask.staged_path.join(executable)
+ @command.run("/bin/chmod", args: ["--", "+x", executable_path]) if File.exist?(executable_path)
+ @command.run(executable_path, script_arguments)
+ end
+ end
+ end
+
+ def uninstall_phase
+ odebug "Nothing to do. The #{self.class.artifact_dsl_key} artifact has no uninstall phase."
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb
new file mode 100644
index 000000000..a44418274
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::InternetPlugin < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb
new file mode 100644
index 000000000..c6b52f30f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb
@@ -0,0 +1,88 @@
+require "hbc/artifact/relocated"
+
+class Hbc::Artifact::Moved < Hbc::Artifact::Relocated
+ def self.english_description
+ "#{artifact_english_name}s"
+ end
+
+ def install_phase
+ each_artifact do |artifact|
+ load_specification(artifact)
+ next unless preflight_checks
+ delete if Hbc::Utils.path_occupied?(target) && force
+ move
+ end
+ end
+
+ def uninstall_phase
+ each_artifact do |artifact|
+ load_specification(artifact)
+ next unless File.exist?(target)
+ delete
+ end
+ end
+
+ private
+
+ def each_artifact
+ # the sort is for predictability between Ruby versions
+ @cask.artifacts[self.class.artifact_dsl_key].sort.each do |artifact|
+ yield artifact
+ end
+ end
+
+ def move
+ ohai "Moving #{self.class.artifact_english_name} '#{source.basename}' to '#{target}'"
+ target.dirname.mkpath
+ FileUtils.move(source, target)
+ add_altname_metadata target, source.basename.to_s
+ end
+
+ def preflight_checks
+ if Hbc::Utils.path_occupied?(target)
+ if force
+ ohai(warning_target_exists { |s| s << "overwriting." })
+ else
+ ohai(warning_target_exists { |s| s << "not moving." })
+ return false
+ end
+ end
+ unless source.exist?
+ message = "It seems the #{self.class.artifact_english_name} source is not there: '#{source}'"
+ raise Hbc::CaskError, message
+ end
+ true
+ end
+
+ def warning_target_exists
+ message_parts = [
+ "It seems there is already #{self.class.artifact_english_article} #{self.class.artifact_english_name} at '#{target}'",
+ ]
+ yield(message_parts) if block_given?
+ message_parts.join("; ")
+ end
+
+ def delete
+ ohai "Removing #{self.class.artifact_english_name}: '#{target}'"
+ if MacOS.undeletable?(target)
+ raise Hbc::CaskError, "Cannot remove undeletable #{self.class.artifact_english_name}"
+ elsif force
+ Hbc::Utils.gain_permissions_remove(target, command: @command)
+ else
+ target.rmtree
+ end
+ end
+
+ def summarize_artifact(artifact_spec)
+ load_specification artifact_spec
+
+ if target.exist?
+ target_abv = " (#{target.abv})"
+ else
+ warning = "Missing #{self.class.artifact_english_name}"
+ warning = "#{Hbc::Utils::Tty.red.underline}#{warning}#{Hbc::Utils::Tty.reset}: "
+ end
+
+ "#{warning}#{printable_target}#{target_abv}"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb b/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb
new file mode 100644
index 000000000..68e4a552c
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb
@@ -0,0 +1,24 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::NestedContainer < Hbc::Artifact::Base
+ def install_phase
+ @cask.artifacts[:nested_container].each { |container| extract(container) }
+ end
+
+ def uninstall_phase
+ # no need to take action; is removed after extraction
+ end
+
+ def extract(container_relative_path)
+ source = @cask.staged_path.join(container_relative_path)
+ container = Hbc::Container.for_path(source, @command)
+
+ unless container
+ raise Hbc::CaskError, "Aw dang, could not identify nested container at '#{source}'"
+ end
+
+ ohai "Extracting nested container #{source.basename}"
+ container.new(@cask, source, @command).extract
+ FileUtils.remove_entry_secure(source)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb b/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb
new file mode 100644
index 000000000..fb27308d7
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb
@@ -0,0 +1,53 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::Pkg < Hbc::Artifact::Base
+ attr_reader :pkg_relative_path
+
+ def self.artifact_dsl_key
+ :pkg
+ end
+
+ def load_pkg_description(pkg_description)
+ @pkg_relative_path = pkg_description.shift
+ @pkg_install_opts = pkg_description.shift
+ begin
+ if @pkg_install_opts.respond_to?(:keys)
+ @pkg_install_opts.assert_valid_keys(:allow_untrusted)
+ elsif @pkg_install_opts
+ raise
+ end
+ raise if pkg_description.nil?
+ rescue StandardError
+ raise Hbc::CaskInvalidError.new(@cask, "Bad pkg stanza")
+ end
+ end
+
+ def pkg_install_opts(opt)
+ @pkg_install_opts[opt] if @pkg_install_opts.respond_to?(:keys)
+ end
+
+ def install_phase
+ @cask.artifacts[:pkg].each { |pkg_description| run_installer(pkg_description) }
+ end
+
+ def uninstall_phase
+ # Do nothing. Must be handled explicitly by a separate :uninstall stanza.
+ end
+
+ def run_installer(pkg_description)
+ load_pkg_description pkg_description
+ ohai "Running installer for #{@cask}; your password may be necessary."
+ ohai "Package installers may write to any location; options such as --appdir are ignored."
+ source = @cask.staged_path.join(pkg_relative_path)
+ unless source.exist?
+ raise Hbc::CaskError, "pkg source file not found: '#{source}'"
+ end
+ args = [
+ "-pkg", source,
+ "-target", "/"
+ ]
+ args << "-verboseR" if Hbc.verbose
+ args << "-allowUntrusted" if pkg_install_opts :allow_untrusted
+ @command.run!("/usr/sbin/installer", sudo: true, args: args, print_stdout: true)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb
new file mode 100644
index 000000000..92b21a83f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/abstract_flight_block"
+
+class Hbc::Artifact::PostflightBlock < Hbc::Artifact::AbstractFlightBlock
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb
new file mode 100644
index 000000000..772a88016
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/abstract_flight_block"
+
+class Hbc::Artifact::PreflightBlock < Hbc::Artifact::AbstractFlightBlock
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb b/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb
new file mode 100644
index 000000000..e45cc0b19
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb
@@ -0,0 +1,7 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Prefpane < Hbc::Artifact::Moved
+ def self.artifact_english_name
+ "Preference Pane"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb
new file mode 100644
index 000000000..6702aa5ef
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb
@@ -0,0 +1,21 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Qlplugin < Hbc::Artifact::Moved
+ def self.artifact_english_name
+ "QuickLook Plugin"
+ end
+
+ def install_phase
+ super
+ reload_quicklook
+ end
+
+ def uninstall_phase
+ super
+ reload_quicklook
+ end
+
+ def reload_quicklook
+ @command.run!("/usr/bin/qlmanage", args: ["-r"])
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb b/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb
new file mode 100644
index 000000000..cd0054188
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb
@@ -0,0 +1,53 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::Relocated < Hbc::Artifact::Base
+ def summary
+ {
+ english_description: self.class.english_description,
+ contents: @cask.artifacts[self.class.artifact_dsl_key].map(&method(:summarize_artifact)).compact,
+ }
+ end
+
+ attr_reader :source, :target
+
+ def printable_target
+ target.to_s.sub(%r{^#{ENV['HOME']}(#{File::SEPARATOR}|$)}, "~/")
+ end
+
+ ALT_NAME_ATTRIBUTE = "com.apple.metadata:kMDItemAlternateNames".freeze
+
+ # Try to make the asset searchable under the target name. Spotlight
+ # respects this attribute for many filetypes, but ignores it for App
+ # bundles. Alfred 2.2 respects it even for App bundles.
+ def add_altname_metadata(file, altname)
+ return if altname.casecmp(file.basename).zero?
+ odebug "Adding #{ALT_NAME_ATTRIBUTE} metadata"
+ altnames = @command.run("/usr/bin/xattr",
+ args: ["-p", ALT_NAME_ATTRIBUTE, file.to_s],
+ print_stderr: false).stdout.sub(%r{\A\((.*)\)\Z}, '\1')
+ odebug "Existing metadata is: '#{altnames}'"
+ altnames.concat(", ") unless altnames.empty?
+ altnames.concat(%Q{"#{altname}"})
+ altnames = "(#{altnames})"
+
+ # Some packges are shipped as u=rx (e.g. Bitcoin Core)
+ @command.run!("/bin/chmod", args: ["--", "u=rwx", file.to_s, file.realpath.to_s])
+
+ @command.run!("/usr/bin/xattr",
+ args: ["-w", ALT_NAME_ATTRIBUTE, altnames, file.to_s],
+ print_stderr: false)
+ end
+
+ def load_specification(artifact_spec)
+ source_string, target_hash = artifact_spec
+ raise Hbc::CaskInvalidError if source_string.nil?
+ @source = @cask.staged_path.join(source_string)
+ if target_hash
+ raise Hbc::CaskInvalidError unless target_hash.respond_to?(:keys)
+ target_hash.assert_valid_keys(:target)
+ @target = Hbc.send(self.class.artifact_dirmethod).join(target_hash[:target])
+ else
+ @target = Hbc.send(self.class.artifact_dirmethod).join(source.basename)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb b/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb
new file mode 100644
index 000000000..bbd929152
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::ScreenSaver < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/service.rb b/Library/Homebrew/cask/lib/hbc/artifact/service.rb
new file mode 100644
index 000000000..d5a00e4fe
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/service.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Service < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb b/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb
new file mode 100644
index 000000000..7a48b19aa
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb
@@ -0,0 +1,15 @@
+require "hbc/artifact/base"
+
+class Hbc::Artifact::StageOnly < Hbc::Artifact::Base
+ def self.artifact_dsl_key
+ :stage_only
+ end
+
+ def install_phase
+ # do nothing
+ end
+
+ def uninstall_phase
+ # do nothing
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/suite.rb b/Library/Homebrew/cask/lib/hbc/artifact/suite.rb
new file mode 100644
index 000000000..cdfb757dd
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/suite.rb
@@ -0,0 +1,11 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Suite < Hbc::Artifact::Moved
+ def self.artifact_english_name
+ "App Suite"
+ end
+
+ def self.artifact_dirmethod
+ :appdir
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb b/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb
new file mode 100644
index 000000000..749b0b98b
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb
@@ -0,0 +1,65 @@
+require "hbc/artifact/relocated"
+
+class Hbc::Artifact::Symlinked < Hbc::Artifact::Relocated
+ def self.link_type_english_name
+ "Symlink"
+ end
+
+ def self.english_description
+ "#{artifact_english_name} #{link_type_english_name}s"
+ end
+
+ def self.islink?(path)
+ path.symlink?
+ end
+
+ def link(artifact_spec)
+ load_specification artifact_spec
+ return unless preflight_checks(source, target)
+ ohai "#{self.class.link_type_english_name}ing #{self.class.artifact_english_name} '#{source.basename}' to '#{target}'"
+ create_filesystem_link(source, target)
+ end
+
+ def unlink(artifact_spec)
+ load_specification artifact_spec
+ return unless self.class.islink?(target)
+ ohai "Removing #{self.class.artifact_english_name} #{self.class.link_type_english_name.downcase}: '#{target}'"
+ target.delete
+ end
+
+ def install_phase
+ @cask.artifacts[self.class.artifact_dsl_key].each(&method(:link))
+ end
+
+ def uninstall_phase
+ @cask.artifacts[self.class.artifact_dsl_key].each(&method(:unlink))
+ end
+
+ def preflight_checks(source, target)
+ if target.exist? && !self.class.islink?(target)
+ ohai "It seems there is already #{self.class.artifact_english_article} #{self.class.artifact_english_name} at '#{target}'; not linking."
+ return false
+ end
+ unless source.exist?
+ raise Hbc::CaskError, "It seems the #{self.class.link_type_english_name.downcase} source is not there: '#{source}'"
+ end
+ true
+ end
+
+ def create_filesystem_link(source, target)
+ Pathname.new(target).dirname.mkpath
+ @command.run!("/bin/ln", args: ["-hfs", "--", source, target])
+ add_altname_metadata source, target.basename.to_s
+ end
+
+ def summarize_artifact(artifact_spec)
+ load_specification artifact_spec
+
+ return unless self.class.islink?(target)
+
+ link_description = "#{Hbc::Utils::Tty.red.underline}Broken Link#{Hbc::Utils::Tty.reset}: " unless target.exist?
+ target_readlink_abv = " (#{target.readlink.abv})" if target.readlink.exist?
+
+ "#{link_description}#{printable_target} -> #{target.readlink}#{target_readlink_abv}"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb
new file mode 100644
index 000000000..12010aeb8
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/uninstall_base"
+
+class Hbc::Artifact::Uninstall < Hbc::Artifact::UninstallBase
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb
new file mode 100644
index 000000000..f92e09a89
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb
@@ -0,0 +1,249 @@
+require "pathname"
+
+require "hbc/artifact/base"
+
+class Hbc::Artifact::UninstallBase < Hbc::Artifact::Base
+ # TODO: 500 is also hardcoded in cask/pkg.rb, but much of
+ # that logic is probably in the wrong location
+
+ PATH_ARG_SLICE_SIZE = 500
+
+ ORDERED_DIRECTIVES = [
+ :early_script,
+ :launchctl,
+ :quit,
+ :signal,
+ :login_item,
+ :kext,
+ :script,
+ :pkgutil,
+ :delete,
+ :trash,
+ :rmdir,
+ ].freeze
+
+ # TODO: these methods were consolidated here from separate
+ # sources and should be refactored for consistency
+
+ def self.expand_path_strings(path_strings)
+ path_strings.map { |path_string|
+ path_string.start_with?("~") ? Pathname.new(path_string).expand_path : Pathname.new(path_string)
+ }
+ end
+
+ def self.remove_relative_path_strings(action, path_strings)
+ relative = path_strings.map { |path_string|
+ path_string if %r{/\.\.(?:/|\Z)}.match(path_string) || !%r{\A/}.match(path_string)
+ }.compact
+ relative.each do |path_string|
+ opoo "Skipping #{action} for relative path #{path_string}"
+ end
+ path_strings - relative
+ end
+
+ def self.remove_undeletable_path_strings(action, path_strings)
+ undeletable = path_strings.map { |path_string|
+ path_string if MacOS.undeletable?(Pathname.new(path_string))
+ }.compact
+ undeletable.each do |path_string|
+ opoo "Skipping #{action} for undeletable path #{path_string}"
+ end
+ path_strings - undeletable
+ end
+
+ def install_phase
+ odebug "Nothing to do. The uninstall artifact has no install phase."
+ end
+
+ def uninstall_phase
+ dispatch_uninstall_directives
+ end
+
+ def dispatch_uninstall_directives(expand_tilde = true)
+ directives_set = @cask.artifacts[stanza]
+ ohai "Running #{stanza} process for #{@cask}; your password may be necessary"
+
+ directives_set.each do |directives|
+ warn_for_unknown_directives(directives)
+ end
+
+ ORDERED_DIRECTIVES.each do |directive_sym|
+ directives_set.select { |h| h.key?(directive_sym) }.each do |directives|
+ args = [directives]
+ args << expand_tilde if [:delete, :trash, :rmdir].include?(directive_sym)
+ send("uninstall_#{directive_sym}", *args)
+ end
+ end
+ end
+
+ private
+
+ def stanza
+ self.class.artifact_dsl_key
+ end
+
+ def warn_for_unknown_directives(directives)
+ unknown_keys = directives.keys - ORDERED_DIRECTIVES
+ return if unknown_keys.empty?
+ opoo %Q{Unknown arguments to #{stanza} -- #{unknown_keys.inspect}. Running "brew update; brew cleanup; brew cask cleanup" will likely fix it.}
+ end
+
+ # Preserve prior functionality of script which runs first. Should rarely be needed.
+ # :early_script should not delete files, better defer that to :script.
+ # If Cask writers never need :early_script it may be removed in the future.
+ def uninstall_early_script(directives)
+ uninstall_script(directives, directive_name: :early_script)
+ end
+
+ # :launchctl must come before :quit/:signal for cases where app would instantly re-launch
+ def uninstall_launchctl(directives)
+ Array(directives[:launchctl]).each do |service|
+ ohai "Removing launchctl service #{service}"
+ [false, true].each do |with_sudo|
+ plist_status = @command.run("/bin/launchctl", args: ["list", service], sudo: with_sudo, print_stderr: false).stdout
+ if plist_status =~ %r{^\{}
+ @command.run!("/bin/launchctl", args: ["remove", service], sudo: with_sudo)
+ sleep 1
+ end
+ paths = ["/Library/LaunchAgents/#{service}.plist",
+ "/Library/LaunchDaemons/#{service}.plist"]
+ paths.each { |elt| elt.prepend(ENV["HOME"]) } unless with_sudo
+ paths = paths.map { |elt| Pathname(elt) }.select(&:exist?)
+ paths.each do |path|
+ @command.run!("/bin/rm", args: ["-f", "--", path], sudo: with_sudo)
+ end
+ # undocumented and untested: pass a path to uninstall :launchctl
+ next unless Pathname(service).exist?
+ @command.run!("/bin/launchctl", args: ["unload", "-w", "--", service], sudo: with_sudo)
+ @command.run!("/bin/rm", args: ["-f", "--", service], sudo: with_sudo)
+ sleep 1
+ end
+ end
+ end
+
+ # :quit/:signal must come before :kext so the kext will not be in use by a running process
+ def uninstall_quit(directives)
+ Array(directives[:quit]).each do |id|
+ ohai "Quitting application ID #{id}"
+ num_running = count_running_processes(id)
+ next unless num_running > 0
+ @command.run!("/usr/bin/osascript", args: ["-e", %Q{tell application id "#{id}" to quit}], sudo: true)
+ sleep 3
+ end
+ end
+
+ # :signal should come after :quit so it can be used as a backup when :quit fails
+ def uninstall_signal(directives)
+ Array(directives[:signal]).flatten.each_slice(2) do |pair|
+ raise Hbc::CaskInvalidError.new(@cask, "Each #{stanza} :signal must have 2 elements.") unless pair.length == 2
+ signal, id = pair
+ ohai "Signalling '#{signal}' to application ID '#{id}'"
+ pids = get_unix_pids(id)
+ next unless pids.any?
+ # Note that unlike :quit, signals are sent from the current user (not
+ # upgraded to the superuser). This is a todo item for the future, but
+ # there should be some additional thought/safety checks about that, as a
+ # misapplied "kill" by root could bring down the system. The fact that we
+ # learned the pid from AppleScript is already some degree of protection,
+ # though indirect.
+ odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{id}"
+ Process.kill(signal, *pids)
+ sleep 3
+ end
+ end
+
+ def count_running_processes(bundle_id)
+ @command.run!("/usr/bin/osascript",
+ args: ["-e", %Q{tell application "System Events" to count processes whose bundle identifier is "#{bundle_id}"}],
+ sudo: true).stdout.to_i
+ end
+
+ def get_unix_pids(bundle_id)
+ pid_string = @command.run!("/usr/bin/osascript",
+ args: ["-e", %Q{tell application "System Events" to get the unix id of every process whose bundle identifier is "#{bundle_id}"}],
+ sudo: true).stdout.chomp
+ return [] unless pid_string =~ %r{\A\d+(?:\s*,\s*\d+)*\Z} # sanity check
+ pid_string.split(%r{\s*,\s*}).map(&:strip).map(&:to_i)
+ end
+
+ def uninstall_login_item(directives)
+ Array(directives[:login_item]).each do |name|
+ ohai "Removing login item #{name}"
+ @command.run!("/usr/bin/osascript",
+ args: ["-e", %Q{tell application "System Events" to delete every login item whose name is "#{name}"}],
+ sudo: false)
+ sleep 1
+ end
+ end
+
+ # :kext should be unloaded before attempting to delete the relevant file
+ def uninstall_kext(directives)
+ Array(directives[:kext]).each do |kext|
+ ohai "Unloading kernel extension #{kext}"
+ is_loaded = @command.run!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout
+ if is_loaded.length > 1
+ @command.run!("/sbin/kextunload", args: ["-b", kext], sudo: true)
+ sleep 1
+ end
+ end
+ end
+
+ # :script must come before :pkgutil, :delete, or :trash so that the script file is not already deleted
+ def uninstall_script(directives, directive_name: :script)
+ executable, script_arguments = self.class.read_script_arguments(directives,
+ "uninstall",
+ { must_succeed: true, sudo: true },
+ { print_stdout: true },
+ directive_name)
+ ohai "Running uninstall script #{executable}"
+ raise Hbc::CaskInvalidError.new(@cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil?
+ executable_path = @cask.staged_path.join(executable)
+ @command.run("/bin/chmod", args: ["--", "+x", executable_path]) if File.exist?(executable_path)
+ @command.run(executable_path, script_arguments)
+ sleep 1
+ end
+
+ def uninstall_pkgutil(directives)
+ ohai "Removing files from pkgutil Bill-of-Materials"
+ Array(directives[:pkgutil]).each do |regexp|
+ pkgs = Hbc::Pkg.all_matching(regexp, @command)
+ pkgs.each(&:uninstall)
+ end
+ end
+
+ def uninstall_delete(directives, expand_tilde = true)
+ Array(directives[:delete]).concat(Array(directives[:trash])).flatten.each_slice(PATH_ARG_SLICE_SIZE) do |path_slice|
+ ohai "Removing files: #{path_slice.utf8_inspect}"
+ path_slice = self.class.expand_path_strings(path_slice) if expand_tilde
+ path_slice = self.class.remove_relative_path_strings(:delete, path_slice)
+ path_slice = self.class.remove_undeletable_path_strings(:delete, path_slice)
+ @command.run!("/bin/rm", args: path_slice.unshift("-rf", "--"), sudo: true)
+ end
+ end
+
+ # :trash functionality is stubbed as a synonym for :delete
+ # TODO: make :trash work differently, moving files to the Trash
+ def uninstall_trash(directives, expand_tilde = true)
+ uninstall_delete(directives, expand_tilde)
+ end
+
+ def uninstall_rmdir(directives, expand_tilde = true)
+ Array(directives[:rmdir]).flatten.each do |directory|
+ directory = self.class.expand_path_strings([directory]).first if expand_tilde
+ directory = self.class.remove_relative_path_strings(:rmdir, [directory]).first
+ directory = self.class.remove_undeletable_path_strings(:rmdir, [directory]).first
+ next if directory.to_s.empty?
+ ohai "Removing directory if empty: #{directory.to_s.utf8_inspect}"
+ directory = Pathname.new(directory)
+ next unless directory.exist?
+ @command.run!("/bin/rm",
+ args: ["-f", "--", directory.join(".DS_Store")],
+ sudo: true,
+ print_stderr: false)
+ @command.run("/bin/rmdir",
+ args: ["--", directory],
+ sudo: true,
+ print_stderr: false)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb
new file mode 100644
index 000000000..243884435
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::Vst3Plugin < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb
new file mode 100644
index 000000000..8d0546480
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb
@@ -0,0 +1,4 @@
+require "hbc/artifact/moved"
+
+class Hbc::Artifact::VstPlugin < Hbc::Artifact::Moved
+end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb
new file mode 100644
index 000000000..8bd8da63b
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb
@@ -0,0 +1,16 @@
+require "hbc/artifact/uninstall_base"
+
+class Hbc::Artifact::Zap < Hbc::Artifact::UninstallBase
+ def install_phase
+ odebug "Nothing to do. The zap artifact has no install phase."
+ end
+
+ def uninstall_phase
+ odebug "Nothing to do. The zap artifact has no uninstall phase."
+ end
+
+ def zap_phase
+ expand_tilde = true
+ dispatch_uninstall_directives(expand_tilde)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/audit.rb b/Library/Homebrew/cask/lib/hbc/audit.rb
new file mode 100644
index 000000000..98f09ffa4
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/audit.rb
@@ -0,0 +1,216 @@
+require "hbc/checkable"
+require "hbc/download"
+require "digest"
+
+class Hbc::Audit
+ include Hbc::Checkable
+
+ attr_reader :cask, :download
+
+ def initialize(cask, download: false, check_token_conflicts: false, command: Hbc::SystemCommand)
+ @cask = cask
+ @download = download
+ @check_token_conflicts = check_token_conflicts
+ @command = command
+ end
+
+ def check_token_conflicts?
+ @check_token_conflicts
+ end
+
+ def run!
+ check_required_stanzas
+ check_version
+ check_sha256
+ check_appcast
+ check_url
+ check_generic_artifacts
+ check_token_conflicts
+ check_download
+ self
+ rescue StandardError => e
+ odebug "#{e.message}\n#{e.backtrace.join("\n")}"
+ add_error "exception while auditing #{cask}: #{e.message}"
+ self
+ end
+
+ def success?
+ !(errors? || warnings?)
+ end
+
+ def summary_header
+ "audit for #{cask}"
+ end
+
+ private
+
+ def check_required_stanzas
+ odebug "Auditing required stanzas"
+ %i{version sha256 url homepage}.each do |sym|
+ add_error "a #{sym} stanza is required" unless cask.send(sym)
+ end
+ add_error "a license stanza is required (:unknown is OK)" unless cask.license
+ add_error "at least one name stanza is required" if cask.name.empty?
+ # TODO: specific DSL knowledge should not be spread around in various files like this
+ # TODO: nested_container should not still be a pseudo-artifact at this point
+ installable_artifacts = cask.artifacts.reject { |k| [:uninstall, :zap, :nested_container].include?(k) }
+ add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
+ end
+
+ def check_version
+ return unless cask.version
+ check_no_string_version_latest
+ end
+
+ def check_no_string_version_latest
+ odebug "Verifying version :latest does not appear as a string ('latest')"
+ return unless cask.version.raw_version == "latest"
+ add_error "you should use version :latest instead of version 'latest'"
+ end
+
+ def check_sha256
+ return unless cask.sha256
+ check_sha256_no_check_if_latest
+ check_sha256_actually_256
+ check_sha256_invalid
+ end
+
+ def check_sha256_no_check_if_latest
+ odebug "Verifying sha256 :no_check with version :latest"
+ return unless cask.version.latest? && cask.sha256 != :no_check
+ add_error "you should use sha256 :no_check when version is :latest"
+ end
+
+ def check_sha256_actually_256(sha256: cask.sha256, stanza: "sha256")
+ odebug "Verifying #{stanza} string is a legal SHA-256 digest"
+ return unless sha256.is_a?(String)
+ return if sha256.length == 64 && sha256[%r{^[0-9a-f]+$}i]
+ add_error "#{stanza} string must be of 64 hexadecimal characters"
+ end
+
+ def check_sha256_invalid(sha256: cask.sha256, stanza: "sha256")
+ odebug "Verifying #{stanza} is not a known invalid value"
+ empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ return unless sha256 == empty_sha256
+ add_error "cannot use the sha256 for an empty string in #{stanza}: #{empty_sha256}"
+ end
+
+ def check_appcast
+ return unless cask.appcast
+ odebug "Auditing appcast"
+ check_appcast_has_checkpoint
+ return unless cask.appcast.checkpoint
+ check_sha256_actually_256(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint")
+ check_sha256_invalid(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint")
+ return unless download
+ check_appcast_http_code
+ check_appcast_checkpoint_accuracy
+ end
+
+ def check_appcast_has_checkpoint
+ odebug "Verifying appcast has :checkpoint key"
+ add_error "a checkpoint sha256 is required for appcast" unless cask.appcast.checkpoint
+ end
+
+ def check_appcast_http_code
+ odebug "Verifying appcast returns 200 HTTP response code"
+ result = @command.run("/usr/bin/curl", args: ["--compressed", "--location", "--user-agent", Hbc::URL::FAKE_USER_AGENT, "--output", "/dev/null", "--write-out", "%{http_code}", cask.appcast], print_stderr: false)
+ if result.success?
+ http_code = result.stdout.chomp
+ add_warning "unexpected HTTP response code retrieving appcast: #{http_code}" unless http_code == "200"
+ else
+ add_warning "error retrieving appcast: #{result.stderr}"
+ end
+ end
+
+ def check_appcast_checkpoint_accuracy
+ odebug "Verifying appcast checkpoint is accurate"
+ result = @command.run("/usr/bin/curl", args: ["--compressed", "--location", "--user-agent", Hbc::URL::FAKE_USER_AGENT, cask.appcast], print_stderr: false)
+ if result.success?
+ processed_appcast_text = result.stdout.gsub(%r{<pubDate>[^<]*</pubDate>}, "")
+ # This step is necessary to replicate running `sed` from the command line
+ processed_appcast_text << "\n" unless processed_appcast_text.end_with?("\n")
+ expected = cask.appcast.checkpoint
+ actual = Digest::SHA2.hexdigest(processed_appcast_text)
+ add_warning <<-EOS.undent unless expected == actual
+ appcast checkpoint mismatch
+ Expected: #{expected}
+ Actual: #{actual}
+ EOS
+ else
+ add_warning "error retrieving appcast: #{result.stderr}"
+ end
+ end
+
+ def check_url
+ return unless cask.url
+ check_download_url_format
+ end
+
+ def check_download_url_format
+ odebug "Auditing URL format"
+ if bad_sourceforge_url?
+ add_warning "SourceForge URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
+ elsif bad_osdn_url?
+ add_warning "OSDN URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
+ end
+ end
+
+ def bad_url_format?(regex, valid_formats_array)
+ return false unless cask.url.to_s =~ regex
+ valid_formats_array.none? { |format| cask.url.to_s =~ format }
+ end
+
+ def bad_sourceforge_url?
+ bad_url_format?(%r{sourceforge},
+ [
+ %r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z},
+ %r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)\/)},
+ # special cases: cannot find canonical format URL
+ %r{\Ahttps?://brushviewer\.sourceforge\.net/brushviewql\.zip\Z},
+ %r{\Ahttps?://doublecommand\.sourceforge\.net/files/},
+ %r{\Ahttps?://excalibur\.sourceforge\.net/get\.php\?id=},
+ ])
+ end
+
+ def bad_osdn_url?
+ bad_url_format?(%r{osd}, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}])
+ end
+
+ def check_generic_artifacts
+ cask.artifacts[:artifact].each do |source, target_hash|
+ unless target_hash.is_a?(Hash) && target_hash[:target]
+ add_error "target required for generic artifact #{source}"
+ next
+ end
+ add_error "target must be absolute path for generic artifact #{source}" unless Pathname.new(target_hash[:target]).absolute?
+ end
+ end
+
+ def check_token_conflicts
+ return unless check_token_conflicts?
+ return unless core_formula_names.include?(cask.token)
+ add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}"
+ end
+
+ def core_tap
+ @core_tap ||= CoreTap.instance
+ end
+
+ def core_formula_names
+ core_tap.formula_names
+ end
+
+ def core_formula_url
+ "#{core_tap.default_remote}/blob/master/Formula/#{cask.token}.rb"
+ end
+
+ def check_download
+ return unless download && cask.url
+ odebug "Auditing download"
+ downloaded_path = download.perform
+ Hbc::Verify.all(cask, downloaded_path)
+ rescue => e
+ add_error "download not possible: #{e.message}"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/auditor.rb b/Library/Homebrew/cask/lib/hbc/auditor.rb
new file mode 100644
index 000000000..89947c1aa
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/auditor.rb
@@ -0,0 +1,10 @@
+class Hbc::Auditor
+ def self.audit(cask, audit_download: false, check_token_conflicts: false)
+ download = audit_download && Hbc::Download.new(cask)
+ audit = Hbc::Audit.new(cask, download: download,
+ check_token_conflicts: check_token_conflicts)
+ audit.run!
+ puts audit.summary
+ audit.success?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cache.rb b/Library/Homebrew/cask/lib/hbc/cache.rb
new file mode 100644
index 000000000..9fc5fe0f3
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cache.rb
@@ -0,0 +1,34 @@
+module Hbc::Cache
+ module_function
+
+ def ensure_cache_exists
+ return if Hbc.cache.exist?
+ odebug "Creating Cache at #{Hbc.cache}"
+ Hbc.cache.mkpath
+ end
+
+ def migrate_legacy_cache
+ if Hbc.legacy_cache.exist?
+ ohai "Migrating cached files to #{Hbc.cache}..."
+
+ Hbc.legacy_cache.children.select(&:symlink?).each do |symlink|
+ file = symlink.readlink
+
+ new_name = file.basename
+ .sub(%r{\-((?:(\d|#{Hbc::DSL::Version::DIVIDER_REGEX})*\-\2*)*[^\-]+)$}x,
+ '--\1')
+
+ renamed_file = Hbc.cache.join(new_name)
+
+ if file.exist?
+ puts "#{file} -> #{renamed_file}"
+ FileUtils.mv(file, renamed_file)
+ end
+
+ FileUtils.rm(symlink)
+ end
+
+ FileUtils.remove_entry_secure(Hbc.legacy_cache)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cask.rb b/Library/Homebrew/cask/lib/hbc/cask.rb
new file mode 100644
index 000000000..fd13a6fe7
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cask.rb
@@ -0,0 +1,114 @@
+require "forwardable"
+
+require "hbc/dsl"
+
+class Hbc::Cask
+ extend Forwardable
+
+ attr_reader :token, :sourcefile_path
+ def initialize(token, sourcefile_path: nil, dsl: nil, &block)
+ @token = token
+ @sourcefile_path = sourcefile_path
+ @dsl = dsl || Hbc::DSL.new(@token)
+ @dsl.instance_eval(&block) if block_given?
+ end
+
+ Hbc::DSL::DSL_METHODS.each do |method_name|
+ define_method(method_name) { @dsl.send(method_name) }
+ end
+
+ METADATA_SUBDIR = ".metadata".freeze
+
+ def metadata_master_container_path
+ @metadata_master_container_path ||= caskroom_path.join(METADATA_SUBDIR)
+ end
+
+ def metadata_versioned_container_path
+ cask_version = version ? version : :unknown
+ metadata_master_container_path.join(cask_version.to_s)
+ end
+
+ def metadata_path(timestamp = :latest, create = false)
+ return nil unless metadata_versioned_container_path.respond_to?(:join)
+ if create && timestamp == :latest
+ raise Hbc::CaskError, "Cannot create metadata path when timestamp is :latest"
+ end
+ path = if timestamp == :latest
+ Pathname.glob(metadata_versioned_container_path.join("*")).sort.last
+ elsif timestamp == :now
+ Hbc::Utils.nowstamp_metadata_path(metadata_versioned_container_path)
+ else
+ metadata_versioned_container_path.join(timestamp)
+ end
+ if create
+ odebug "Creating metadata directory #{path}"
+ FileUtils.mkdir_p path
+ end
+ path
+ end
+
+ def metadata_subdir(leaf, timestamp = :latest, create = false)
+ if create && timestamp == :latest
+ raise Hbc::CaskError, "Cannot create metadata subdir when timestamp is :latest"
+ end
+ unless leaf.respond_to?(:length) && !leaf.empty?
+ raise Hbc::CaskError, "Cannot create metadata subdir for empty leaf"
+ end
+ parent = metadata_path(timestamp, create)
+ return nil unless parent.respond_to?(:join)
+ subdir = parent.join(leaf)
+ if create
+ odebug "Creating metadata subdirectory #{subdir}"
+ FileUtils.mkdir_p subdir
+ end
+ subdir
+ end
+
+ def timestamped_versions
+ Pathname.glob(metadata_master_container_path.join("*", "*"))
+ .map { |p| p.relative_path_from(metadata_master_container_path) }
+ .sort_by(&:basename) # sort by timestamp
+ .map(&:split)
+ end
+
+ def versions
+ timestamped_versions.map(&:first)
+ .reverse
+ .uniq
+ .reverse
+ end
+
+ def installed?
+ !versions.empty?
+ end
+
+ def to_s
+ @token
+ end
+
+ def dumpcask
+ if Hbc.respond_to?(:debug) && Hbc.debug
+ odebug "Cask instance dumps in YAML:"
+ odebug "Cask instance toplevel:", to_yaml
+ [
+ :name,
+ :homepage,
+ :url,
+ :appcast,
+ :version,
+ :license,
+ :sha256,
+ :artifacts,
+ :caveats,
+ :depends_on,
+ :conflicts_with,
+ :container,
+ :gpg,
+ :accessibility_access,
+ :auto_updates,
+ ].each do |method|
+ odebug "Cask instance method '#{method}':", send(method).to_yaml
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb b/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb
new file mode 100644
index 000000000..6cbfd05af
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb
@@ -0,0 +1,33 @@
+require "hbc/topological_hash"
+
+class Hbc::CaskDependencies
+ attr_reader :cask, :graph, :sorted
+
+ def initialize(cask)
+ @cask = cask
+ @graph = graph_dependencies
+ @sorted = sort
+ end
+
+ def graph_dependencies
+ deps_in = ->(csk) { csk.depends_on ? csk.depends_on.cask || [] : [] }
+ walk = lambda { |acc, deps|
+ deps.each do |dep|
+ next if acc.key?(dep)
+ succs = deps_in.call Hbc.load(dep)
+ acc[dep] = succs
+ walk.call(acc, succs)
+ end
+ acc
+ }
+
+ graphed = walk.call({}, @cask.depends_on.cask)
+ Hbc::TopologicalHash[graphed]
+ end
+
+ def sort
+ @graph.tsort
+ rescue TSort::Cyclic
+ raise Hbc::CaskCyclicCaskDependencyError, @cask.token
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/caskroom.rb b/Library/Homebrew/cask/lib/hbc/caskroom.rb
new file mode 100644
index 000000000..cb471a125
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/caskroom.rb
@@ -0,0 +1,28 @@
+module Hbc::Caskroom
+ module_function
+
+ def ensure_caskroom_exists
+ unless Hbc.caskroom.exist?
+ ohai "Creating Caskroom at #{Hbc.caskroom}"
+
+ if Hbc.caskroom.parent.writable?
+ Hbc.caskroom.mkpath
+ else
+ ohai "We'll set permissions properly so we won't need sudo in the future"
+ toplevel_dir = Hbc.caskroom
+ toplevel_dir = toplevel_dir.parent until toplevel_dir.parent.root?
+ unless toplevel_dir.directory?
+ # If a toplevel dir such as '/opt' must be created, enforce standard permissions.
+ # sudo in system is rude.
+ system "/usr/bin/sudo", "--", "/bin/mkdir", "--", toplevel_dir
+ system "/usr/bin/sudo", "--", "/bin/chmod", "--", "0775", toplevel_dir
+ end
+ # sudo in system is rude.
+ system "/usr/bin/sudo", "--", "/bin/mkdir", "-p", "--", Hbc.caskroom
+ unless Hbc.caskroom.parent == toplevel_dir
+ system "/usr/bin/sudo", "--", "/usr/sbin/chown", "-R", "--", "#{Hbc::Utils.current_user}:staff", Hbc.caskroom.parent.to_s
+ end
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/caveats.rb b/Library/Homebrew/cask/lib/hbc/caveats.rb
new file mode 100644
index 000000000..04bbcf218
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/caveats.rb
@@ -0,0 +1,12 @@
+class Hbc::Caveats
+ def initialize(block)
+ @block = block
+ end
+
+ def eval_and_print(cask)
+ dsl = Hbc::DSL::Caveats.new(cask)
+ retval = dsl.instance_eval(&@block)
+ return if retval.nil?
+ puts retval.to_s.sub(%r{[\r\n \t]*\Z}, "\n\n")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/checkable.rb b/Library/Homebrew/cask/lib/hbc/checkable.rb
new file mode 100644
index 000000000..630a3f063
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/checkable.rb
@@ -0,0 +1,51 @@
+module Hbc::Checkable
+ def errors
+ Array(@errors)
+ end
+
+ def warnings
+ Array(@warnings)
+ end
+
+ def add_error(message)
+ @errors ||= []
+ @errors << message
+ end
+
+ def add_warning(message)
+ @warnings ||= []
+ @warnings << message
+ end
+
+ def errors?
+ Array(@errors).any?
+ end
+
+ def warnings?
+ Array(@warnings).any?
+ end
+
+ def result
+ if errors?
+ "#{Hbc::Utils::Tty.red.underline}failed#{Hbc::Utils::Tty.reset}"
+ elsif warnings?
+ "#{Hbc::Utils::Tty.yellow.underline}warning#{Hbc::Utils::Tty.reset}"
+ else
+ "#{Hbc::Utils::Tty.green}passed#{Hbc::Utils::Tty.reset}"
+ end
+ end
+
+ def summary
+ summary = ["#{summary_header}: #{result}"]
+
+ errors.each do |error|
+ summary << " #{Hbc::Utils::Tty.red}-#{Hbc::Utils::Tty.reset} #{error}"
+ end
+
+ warnings.each do |warning|
+ summary << " #{Hbc::Utils::Tty.yellow}-#{Hbc::Utils::Tty.reset} #{warning}"
+ end
+
+ summary.join("\n")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli.rb b/Library/Homebrew/cask/lib/hbc/cli.rb
new file mode 100644
index 000000000..be40ce11b
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli.rb
@@ -0,0 +1,274 @@
+class Hbc::CLI; end
+
+require "optparse"
+require "shellwords"
+
+require "hbc/cli/base"
+require "hbc/cli/audit"
+require "hbc/cli/cat"
+require "hbc/cli/cleanup"
+require "hbc/cli/create"
+require "hbc/cli/doctor"
+require "hbc/cli/edit"
+require "hbc/cli/fetch"
+require "hbc/cli/home"
+require "hbc/cli/info"
+require "hbc/cli/install"
+require "hbc/cli/list"
+require "hbc/cli/search"
+require "hbc/cli/style"
+require "hbc/cli/uninstall"
+require "hbc/cli/update"
+require "hbc/cli/zap"
+
+require "hbc/cli/internal_use_base"
+require "hbc/cli/internal_audit_modified_casks"
+require "hbc/cli/internal_checkurl"
+require "hbc/cli/internal_dump"
+require "hbc/cli/internal_help"
+require "hbc/cli/internal_stanza"
+
+class Hbc::CLI
+ ALIASES = {
+ "ls" => "list",
+ "homepage" => "home",
+ "-S" => "search", # verb starting with "-" is questionable
+ "up" => "update",
+ "instal" => "install", # gem does the same
+ "rm" => "uninstall",
+ "remove" => "uninstall",
+ "abv" => "info",
+ "dr" => "doctor",
+ # aliases from Homebrew that we don't (yet) support
+ # 'ln' => 'link',
+ # 'configure' => 'diy',
+ # '--repo' => '--repository',
+ # 'environment' => '--env',
+ # '-c1' => '--config',
+ }.freeze
+
+ OPTIONS = {
+ "--caskroom=" => :caskroom=,
+ "--appdir=" => :appdir=,
+ "--colorpickerdir=" => :colorpickerdir=,
+ "--prefpanedir=" => :prefpanedir=,
+ "--qlplugindir=" => :qlplugindir=,
+ "--fontdir=" => :fontdir=,
+ "--servicedir=" => :servicedir=,
+ "--input_methoddir=" => :input_methoddir=,
+ "--internet_plugindir=" => :internet_plugindir=,
+ "--audio_unit_plugindir=" => :audio_unit_plugindir=,
+ "--vst_plugindir=" => :vst_plugindir=,
+ "--vst3_plugindir=" => :vst3_plugindir=,
+ "--screen_saverdir=" => :screen_saverdir=,
+ }.freeze
+
+ FLAGS = {
+ "--no-binaries" => :no_binaries=,
+ "--debug" => :debug=,
+ "--verbose" => :verbose=,
+ "--outdated" => :cleanup_outdated=,
+ "--help" => :help=,
+ }.freeze
+
+ def self.command_classes
+ @command_classes ||= Hbc::CLI.constants
+ .map(&Hbc::CLI.method(:const_get))
+ .select { |sym| sym.respond_to?(:run) }
+ end
+
+ def self.commands
+ @commands ||= command_classes.map(&:command_name)
+ end
+
+ def self.lookup_command(command_string)
+ @lookup ||= Hash[commands.zip(command_classes)]
+ command_string = ALIASES.fetch(command_string, command_string)
+ @lookup.fetch(command_string, command_string)
+ end
+
+ # modified from Homebrew
+ def self.require?(path)
+ require path
+ true # OK if already loaded
+ rescue LoadError => e
+ # HACK: :( because we should raise on syntax errors
+ # but not if the file doesn't exist.
+ # TODO: make robust!
+ raise unless e.to_s.include? path
+ end
+
+ def self.should_init?(command)
+ (command.is_a? Class) && (command < Hbc::CLI::Base) && command.needs_init?
+ end
+
+ def self.run_command(command, *rest)
+ if command.respond_to?(:run)
+ # usual case: built-in command verb
+ command.run(*rest)
+ elsif require? Hbc::Utils.which("brewcask-#{command}.rb").to_s
+ # external command as Ruby library on PATH, Homebrew-style
+ elsif command.to_s.include?("/") && require?(command.to_s)
+ # external command as Ruby library with literal path, useful
+ # for development and troubleshooting
+ sym = Pathname.new(command.to_s).basename(".rb").to_s.capitalize
+ klass = begin
+ Hbc::CLI.const_get(sym)
+ rescue NameError
+ nil
+ end
+ if klass.respond_to?(:run)
+ # invoke "run" on a Ruby library which follows our coding conventions
+ klass.run(*rest)
+ else
+ # other Ruby libraries must do everything via "require"
+ end
+ elsif Hbc::Utils.which "brewcask-#{command}"
+ # arbitrary external executable on PATH, Homebrew-style
+ exec "brewcask-#{command}", *ARGV[1..-1]
+ elsif Pathname.new(command.to_s).executable? &&
+ command.to_s.include?("/") &&
+ !command.to_s.match(%r{\.rb$})
+ # arbitrary external executable with literal path, useful
+ # for development and troubleshooting
+ exec command, *ARGV[1..-1]
+ else
+ # failure
+ Hbc::CLI::NullCommand.new(command).run
+ end
+ end
+
+ def self.process(arguments)
+ command_string, *rest = *arguments
+ rest = process_options(rest)
+ command = Hbc.help ? "help" : lookup_command(command_string)
+ Hbc.init if should_init?(command)
+ run_command(command, *rest)
+ rescue Hbc::CaskError, Hbc::CaskSha256MismatchError => e
+ msg = e.message
+ msg << e.backtrace.join("\n") if Hbc.debug
+ onoe msg
+ exit 1
+ rescue StandardError, ScriptError, NoMemoryError => e
+ msg = e.message
+ msg << Hbc::Utils.error_message_with_suggestions
+ msg << e.backtrace.join("\n")
+ onoe msg
+ exit 1
+ end
+
+ def self.nice_listing(cask_list)
+ cask_taps = {}
+ cask_list.each do |c|
+ user, repo, token = c.split "/"
+ repo.sub!(%r{^homebrew-}i, "")
+ cask_taps[token] ||= []
+ cask_taps[token].push "#{user}/#{repo}"
+ end
+ list = []
+ cask_taps.each do |token, taps|
+ if taps.length == 1
+ list.push token
+ else
+ taps.each { |r| list.push [r, token].join "/" }
+ end
+ end
+ list.sort
+ end
+
+ def self.parser
+ # If you modify these arguments, please update USAGE.md
+ @parser ||= OptionParser.new do |opts|
+ OPTIONS.each do |option, method|
+ opts.on("#{option}" "PATH", Pathname) do |path|
+ Hbc.public_send(method, path)
+ end
+ end
+
+ opts.on("--binarydir=PATH") do
+ opoo <<-EOF.undent
+ Option --binarydir is obsolete!
+ Homebrew-Cask now uses the same location as your Homebrew installation for executable links.
+ EOF
+ end
+
+ FLAGS.each do |flag, method|
+ opts.on(flag) do
+ Hbc.public_send(method, true)
+ end
+ end
+
+ opts.on("--version") do
+ raise OptionParser::InvalidOption # override default handling of --version
+ end
+ end
+ end
+
+ def self.process_options(args)
+ all_args = Shellwords.shellsplit(ENV["HOMEBREW_CASK_OPTS"] || "") + args
+ remaining = []
+ until all_args.empty?
+ begin
+ head = all_args.shift
+ remaining.concat(parser.parse([head]))
+ rescue OptionParser::InvalidOption
+ remaining << head
+ retry
+ rescue OptionParser::MissingArgument
+ raise Hbc::CaskError, "The option '#{head}' requires an argument"
+ rescue OptionParser::AmbiguousOption
+ raise Hbc::CaskError, "There is more than one possible option that starts with '#{head}'"
+ end
+ end
+
+ # for compat with Homebrew, not certain if this is desirable
+ Hbc.verbose = true if !ENV["VERBOSE"].nil? || !ENV["HOMEBREW_VERBOSE"].nil?
+
+ remaining
+ end
+
+ class NullCommand
+ def initialize(attempted_verb)
+ @attempted_verb = attempted_verb
+ end
+
+ def run(*args)
+ if args.include?("--version") || @attempted_verb == "--version"
+ puts Hbc.full_version
+ else
+ purpose
+ usage
+ unless @attempted_verb.to_s.strip.empty? || @attempted_verb == "help"
+ raise Hbc::CaskError, "Unknown command: #{@attempted_verb}"
+ end
+ end
+ end
+
+ def purpose
+ puts <<-PURPOSE.undent
+ brew-cask provides a friendly homebrew-style CLI workflow for the
+ administration of macOS applications distributed as binaries.
+
+ PURPOSE
+ end
+
+ def usage
+ max_command_len = Hbc::CLI.commands.map(&:length).max
+
+ puts "Commands:\n\n"
+ Hbc::CLI.command_classes.each do |klass|
+ next unless klass.visible
+ puts " #{klass.command_name.ljust(max_command_len)} #{_help_for(klass)}"
+ end
+ puts %Q{\nSee also "man brew-cask"}
+ end
+
+ def help
+ ""
+ end
+
+ def _help_for(klass)
+ klass.respond_to?(:help) ? klass.help : nil
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/audit.rb b/Library/Homebrew/cask/lib/hbc/cli/audit.rb
new file mode 100644
index 000000000..289547b44
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/audit.rb
@@ -0,0 +1,52 @@
+class Hbc::CLI::Audit < Hbc::CLI::Base
+ def self.help
+ "verifies installability of Casks"
+ end
+
+ def self.run(*args)
+ failed_casks = new(args, Hbc::Auditor).run
+ return if failed_casks.empty?
+ raise Hbc::CaskError, "audit failed for casks: #{failed_casks.join(' ')}"
+ end
+
+ def initialize(args, auditor)
+ @args = args
+ @auditor = auditor
+ end
+
+ def run
+ casks_to_audit.each_with_object([]) do |cask, failed|
+ failed << cask unless audit(cask)
+ end
+ end
+
+ def audit(cask)
+ odebug "Auditing Cask #{cask}"
+ @auditor.audit(cask, audit_download: audit_download?,
+ check_token_conflicts: check_token_conflicts?)
+ end
+
+ def audit_download?
+ @args.include?("--download")
+ end
+
+ def check_token_conflicts?
+ @args.include?("--token-conflicts")
+ end
+
+ def casks_to_audit
+ if cask_tokens.empty?
+ Hbc.all
+ else
+ cask_tokens.map { |token| Hbc.load(token) }
+ end
+ end
+
+ def cask_tokens
+ @cask_tokens ||= self.class.cask_tokens_from(@args)
+ end
+
+ def self.needs_init?
+ true
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/base.rb b/Library/Homebrew/cask/lib/hbc/cli/base.rb
new file mode 100644
index 000000000..af03969af
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/base.rb
@@ -0,0 +1,21 @@
+class Hbc::CLI::Base
+ def self.command_name
+ @command_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1_\2').downcase
+ end
+
+ def self.visible
+ true
+ end
+
+ def self.cask_tokens_from(args)
+ args.reject { |a| a.empty? || a.chars.first == "-" }
+ end
+
+ def self.help
+ "No help available for the #{command_name} command"
+ end
+
+ def self.needs_init?
+ false
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/cat.rb b/Library/Homebrew/cask/lib/hbc/cli/cat.rb
new file mode 100644
index 000000000..d6d545c3b
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/cat.rb
@@ -0,0 +1,15 @@
+class Hbc::CLI::Cat < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ # only respects the first argument
+ cask_token = cask_tokens.first.sub(%r{\.rb$}i, "")
+ cask_path = Hbc.path(cask_token)
+ raise Hbc::CaskUnavailableError, cask_token.to_s unless cask_path.exist?
+ puts File.open(cask_path, &:read)
+ end
+
+ def self.help
+ "dump raw source of the given Cask to the standard output"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb b/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb
new file mode 100644
index 000000000..b098a243d
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb
@@ -0,0 +1,88 @@
+class Hbc::CLI::Cleanup < Hbc::CLI::Base
+ OUTDATED_DAYS = 10
+ OUTDATED_TIMESTAMP = Time.now - (60 * 60 * 24 * OUTDATED_DAYS)
+
+ def self.help
+ "cleans up cached downloads and tracker symlinks"
+ end
+
+ def self.needs_init?
+ true
+ end
+
+ def self.run(*_ignored)
+ default.cleanup!
+ end
+
+ def self.default
+ @default ||= new(Hbc.cache, Hbc.cleanup_outdated)
+ end
+
+ attr_reader :cache_location, :outdated_only
+ def initialize(cache_location, outdated_only)
+ @cache_location = Pathname.new(cache_location)
+ @outdated_only = outdated_only
+ end
+
+ def cleanup!
+ remove_all_cache_files
+ end
+
+ def cache_files
+ return [] unless cache_location.exist?
+ cache_location.children
+ .map(&method(:Pathname))
+ .reject(&method(:outdated?))
+ end
+
+ def outdated?(file)
+ outdated_only && file && file.stat.mtime > OUTDATED_TIMESTAMP
+ end
+
+ def incomplete?(file)
+ file.extname == ".incomplete"
+ end
+
+ def cache_incompletes
+ cache_files.select(&method(:incomplete?))
+ end
+
+ def cache_completes
+ cache_files.reject(&method(:incomplete?))
+ end
+
+ def disk_cleanup_size
+ Hbc::Utils.size_in_bytes(cache_files)
+ end
+
+ def remove_all_cache_files
+ message = "Removing cached downloads"
+ message.concat " older than #{OUTDATED_DAYS} days old" if outdated_only
+ ohai message
+ delete_paths(cache_files)
+ end
+
+ def delete_paths(paths)
+ cleanup_size = 0
+ processed_files = 0
+ paths.each do |item|
+ next unless item.exist?
+ processed_files += 1
+ if Hbc::Utils.file_locked?(item)
+ puts "skipping: #{item} is locked"
+ next
+ end
+ puts item
+ item_size = File.size?(item)
+ cleanup_size += item_size unless item_size.nil?
+ item.unlink
+ end
+
+ if processed_files.zero?
+ puts "Nothing to do"
+ else
+ disk_space = disk_usage_readable(cleanup_size)
+ ohai "This operation has freed approximately #{disk_space} of disk space."
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/create.rb b/Library/Homebrew/cask/lib/hbc/cli/create.rb
new file mode 100644
index 000000000..3c1ac76ed
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/create.rb
@@ -0,0 +1,37 @@
+class Hbc::CLI::Create < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ cask_token = cask_tokens.first.sub(%r{\.rb$}i, "")
+ cask_path = Hbc.path(cask_token)
+ odebug "Creating Cask #{cask_token}"
+
+ raise Hbc::CaskAlreadyCreatedError, cask_token if cask_path.exist?
+
+ File.open(cask_path, "w") do |f|
+ f.write template(cask_token)
+ end
+
+ exec_editor cask_path
+ end
+
+ def self.template(cask_token)
+ <<-EOS.undent
+ cask '#{cask_token}' do
+ version ''
+ sha256 ''
+
+ url 'https://'
+ name ''
+ homepage ''
+ license :unknown # TODO: change license and remove this comment; ':unknown' is a machine-generated placeholder
+
+ app ''
+ end
+ EOS
+ end
+
+ def self.help
+ "creates the given Cask and opens it in an editor"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/doctor.rb b/Library/Homebrew/cask/lib/hbc/cli/doctor.rb
new file mode 100644
index 000000000..d2feb1e06
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/doctor.rb
@@ -0,0 +1,213 @@
+class Hbc::CLI::Doctor < Hbc::CLI::Base
+ def self.run
+ ohai "macOS Release:", render_with_none_as_error(MacOS.full_version)
+ ohai "Hardware Architecture:", render_with_none_as_error("#{Hardware::CPU.type}-#{Hardware::CPU.bits}")
+ ohai "Ruby Version:", render_with_none_as_error("#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}")
+ ohai "Ruby Path:", render_with_none_as_error(RbConfig.ruby)
+ # TODO: consider removing most Homebrew constants from doctor output
+ ohai "Homebrew Version:", render_with_none_as_error(homebrew_version)
+ ohai "Homebrew Executable Path:", render_with_none_as_error(Hbc.homebrew_executable)
+ ohai "Homebrew Cellar Path:", render_with_none_as_error(homebrew_cellar)
+ ohai "Homebrew Repository Path:", render_with_none_as_error(homebrew_repository)
+ ohai "Homebrew Origin:", render_with_none_as_error(homebrew_origin)
+ ohai "Homebrew-Cask Version:", render_with_none_as_error(Hbc.full_version)
+ ohai "Homebrew-Cask Install Location:", render_install_location
+ ohai "Homebrew-Cask Staging Location:", render_staging_location(Hbc.caskroom)
+ ohai "Homebrew-Cask Cached Downloads:", render_cached_downloads
+ ohai "Homebrew-Cask Default Tap Path:", render_tap_paths(Hbc.default_tap.path)
+ ohai "Homebrew-Cask Alternate Cask Taps:", render_tap_paths(alt_taps)
+ ohai "Homebrew-Cask Default Tap Cask Count:", render_with_none_as_error(default_cask_count)
+ ohai "Contents of $LOAD_PATH:", render_load_path($LOAD_PATH)
+ ohai "Contents of $RUBYLIB Environment Variable:", render_env_var("RUBYLIB")
+ ohai "Contents of $RUBYOPT Environment Variable:", render_env_var("RUBYOPT")
+ ohai "Contents of $RUBYPATH Environment Variable:", render_env_var("RUBYPATH")
+ ohai "Contents of $RBENV_VERSION Environment Variable:", render_env_var("RBENV_VERSION")
+ ohai "Contents of $CHRUBY_VERSION Environment Variable:", render_env_var("CHRUBY_VERSION")
+ ohai "Contents of $GEM_HOME Environment Variable:", render_env_var("GEM_HOME")
+ ohai "Contents of $GEM_PATH Environment Variable:", render_env_var("GEM_PATH")
+ ohai "Contents of $BUNDLE_PATH Environment Variable:", render_env_var("BUNDLE_PATH")
+ ohai "Contents of $PATH Environment Variable:", render_env_var("PATH")
+ ohai "Contents of $SHELL Environment Variable:", render_env_var("SHELL")
+ ohai "Contents of Locale Environment Variables:", render_with_none(locale_variables)
+ ohai "Running As Privileged User:", render_with_none_as_error(privileged_uid)
+ end
+
+ def self.alt_taps
+ Tap.select { |t| t.cask_dir.directory? && t != Hbc.default_tap }
+ .map(&:path)
+ end
+
+ def self.default_cask_count
+ default_cask_count = notfound_string
+ begin
+ default_cask_count = Hbc.default_tap.cask_dir.children.count(&:file?)
+ rescue StandardError
+ default_cask_count = "0 #{error_string "Error reading #{Hbc.default_tap.path}"}"
+ end
+ default_cask_count
+ end
+
+ def self.homebrew_origin
+ homebrew_origin = notfound_string
+ begin
+ Dir.chdir(homebrew_repository) do
+ homebrew_origin = Hbc::SystemCommand.run("/usr/bin/git",
+ args: %w[config --get remote.origin.url],
+ print_stderr: false).stdout.strip
+ end
+ if homebrew_origin !~ %r{\S}
+ homebrew_origin = "#{none_string} #{error_string}"
+ elsif homebrew_origin !~ %r{(mxcl|Homebrew)/(home)?brew(\.git)?\Z}
+ homebrew_origin.concat " #{error_string 'warning: nonstandard origin'}"
+ end
+ rescue StandardError
+ homebrew_origin = error_string "Not Found - Error running git"
+ end
+ homebrew_origin
+ end
+
+ def self.homebrew_repository
+ homebrew_constants("repository")
+ end
+
+ def self.homebrew_cellar
+ homebrew_constants("cellar")
+ end
+
+ def self.homebrew_version
+ homebrew_constants("version")
+ end
+
+ def self.homebrew_taps
+ @homebrew_taps ||= if homebrew_repository.respond_to?(:join)
+ homebrew_repository.join("Library", "Taps")
+ end
+ end
+
+ def self.homebrew_constants(name)
+ @homebrew_constants ||= {}
+ return @homebrew_constants[name] if @homebrew_constants.key?(name)
+ @homebrew_constants[name] = notfound_string
+ begin
+ @homebrew_constants[name] = Hbc::SystemCommand.run!(Hbc.homebrew_executable,
+ args: ["--#{name}"],
+ print_stderr: false)
+ .stdout
+ .strip
+ if @homebrew_constants[name] !~ %r{\S}
+ @homebrew_constants[name] = "#{none_string} #{error_string}"
+ end
+ path = Pathname.new(@homebrew_constants[name])
+ @homebrew_constants[name] = path if path.exist?
+ rescue StandardError
+ @homebrew_constants[name] = error_string "Not Found - Error running brew"
+ end
+ @homebrew_constants[name]
+ end
+
+ def self.locale_variables
+ ENV.keys.grep(%r{^(?:LC_\S+|LANG|LANGUAGE)\Z}).collect { |v| %Q{#{v}="#{ENV[v]}"} }.sort.join("\n")
+ end
+
+ def self.privileged_uid
+ Process.euid == 0 ? "Yes #{error_string 'warning: not recommended'}" : "No"
+ rescue StandardError
+ notfound_string
+ end
+
+ def self.none_string
+ "<NONE>"
+ end
+
+ def self.legacy_tap_pattern
+ %r{phinze}
+ end
+
+ def self.notfound_string
+ "#{Hbc::Utils::Tty.red.underline}Not Found - Unknown Error#{Hbc::Utils::Tty.reset}"
+ end
+
+ def self.error_string(string = "Error")
+ "#{Hbc::Utils::Tty.red.underline}(#{string})#{Hbc::Utils::Tty.reset}"
+ end
+
+ def self.render_with_none(string)
+ return string if !string.nil? && string.respond_to?(:to_s) && !string.to_s.empty?
+ none_string
+ end
+
+ def self.render_with_none_as_error(string)
+ return string if !string.nil? && string.respond_to?(:to_s) && !string.to_s.empty?
+ "#{none_string} #{error_string}"
+ end
+
+ def self.render_tap_paths(paths)
+ paths = [paths] unless paths.respond_to?(:each)
+ paths.collect do |dir|
+ if dir.nil? || dir.to_s.empty?
+ none_string
+ elsif dir.to_s.match(legacy_tap_pattern)
+ dir.to_s.concat(" #{error_string 'Warning: legacy tap path'}")
+ else
+ dir.to_s
+ end
+ end
+ end
+
+ def self.render_env_var(var)
+ if ENV.key?(var)
+ %Q{#{var}="#{ENV[var]}"}
+ else
+ none_string
+ end
+ end
+
+ # This could be done by calling into Homebrew, but the situation
+ # where "doctor" is needed is precisely the situation where such
+ # things are less dependable.
+ def self.render_install_location
+ locations = Dir.glob(homebrew_cellar.join("brew-cask", "*")).reverse
+ if locations.empty?
+ none_string
+ else
+ locations.collect do |l|
+ "#{l} #{error_string 'error: legacy install. Run "brew uninstall --force brew-cask".'}"
+ end
+ end
+ end
+
+ def self.render_staging_location(path)
+ path = Pathname.new(path)
+ if !path.exist?
+ "#{path} #{error_string 'error: path does not exist'}}"
+ elsif !path.writable?
+ "#{path} #{error_string 'error: not writable by current user'}"
+ else
+ path
+ end
+ end
+
+ def self.render_load_path(paths)
+ return "#{none_string} #{error_string}" if paths.nil? || paths.empty?
+ copy = Array.new(paths)
+ unless Hbc::Utils.file_is_descendant(copy[0], homebrew_taps)
+ copy[0] = "#{copy[0]} #{error_string 'error: should be descendant of Homebrew taps directory'}"
+ end
+ copy
+ end
+
+ def self.render_cached_downloads
+ cleanup = Hbc::CLI::Cleanup.default
+ files = cleanup.cache_files
+ count = files.count
+ size = cleanup.disk_cleanup_size
+ size_msg = "#{number_readable(count)} files, #{disk_usage_readable(size)}"
+ warn_msg = error_string('warning: run "brew cask cleanup"')
+ size_msg << " #{warn_msg}" if count > 0
+ [Hbc.cache, size_msg]
+ end
+
+ def self.help
+ "checks for configuration issues"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/edit.rb b/Library/Homebrew/cask/lib/hbc/cli/edit.rb
new file mode 100644
index 000000000..b2d4a9156
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/edit.rb
@@ -0,0 +1,18 @@
+class Hbc::CLI::Edit < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ # only respects the first argument
+ cask_token = cask_tokens.first.sub(%r{\.rb$}i, "")
+ cask_path = Hbc.path(cask_token)
+ odebug "Opening editor for Cask #{cask_token}"
+ unless cask_path.exist?
+ raise Hbc::CaskUnavailableError, %Q{#{cask_token}, run "brew cask create #{cask_token}" to create a new Cask}
+ end
+ exec_editor cask_path
+ end
+
+ def self.help
+ "edits the given Cask"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/fetch.rb b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb
new file mode 100644
index 000000000..647f2af2c
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb
@@ -0,0 +1,19 @@
+class Hbc::CLI::Fetch < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ force = args.include? "--force"
+
+ cask_tokens.each do |cask_token|
+ ohai "Downloading external files for Cask #{cask_token}"
+ cask = Hbc.load(cask_token)
+ downloaded_path = Hbc::Download.new(cask, force: force).perform
+ Hbc::Verify.all(cask, downloaded_path)
+ ohai "Success! Downloaded to -> #{downloaded_path}"
+ end
+ end
+
+ def self.help
+ "downloads remote application files to local cache"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/home.rb b/Library/Homebrew/cask/lib/hbc/cli/home.rb
new file mode 100644
index 000000000..9c8c0a0e4
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/home.rb
@@ -0,0 +1,18 @@
+class Hbc::CLI::Home < Hbc::CLI::Base
+ def self.run(*cask_tokens)
+ if cask_tokens.empty?
+ odebug "Opening project homepage"
+ system "/usr/bin/open", "--", "http://caskroom.io/"
+ else
+ cask_tokens.each do |cask_token|
+ odebug "Opening homepage for Cask #{cask_token}"
+ cask = Hbc.load(cask_token)
+ system "/usr/bin/open", "--", cask.homepage
+ end
+ end
+ end
+
+ def self.help
+ "opens the homepage of the given Cask"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/info.rb b/Library/Homebrew/cask/lib/hbc/cli/info.rb
new file mode 100644
index 000000000..dda405705
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/info.rb
@@ -0,0 +1,66 @@
+class Hbc::CLI::Info < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ cask_tokens.each do |cask_token|
+ odebug "Getting info for Cask #{cask_token}"
+ cask = Hbc.load(cask_token)
+
+ info(cask)
+ end
+ end
+
+ def self.help
+ "displays information about the given Cask"
+ end
+
+ def self.info(cask)
+ puts "#{cask.token}: #{cask.version}"
+ puts formatted_url(cask.homepage) if cask.homepage
+ installation_info(cask)
+ puts "From: #{formatted_url(github_info(cask))}" if github_info(cask)
+ name_info(cask)
+ artifact_info(cask)
+ Hbc::Installer.print_caveats(cask)
+ end
+
+ def self.formatted_url(url)
+ "#{Hbc::Utils::Tty.underline}#{url}#{Hbc::Utils::Tty.reset}"
+ end
+
+ def self.installation_info(cask)
+ if cask.installed?
+ cask.versions.each do |version|
+ versioned_staged_path = cask.caskroom_path.join(version)
+
+ puts versioned_staged_path.to_s
+ .concat(" (")
+ .concat(versioned_staged_path.exist? ? versioned_staged_path.abv : "#{Hbc::Utils::Tty.red}does not exist#{Hbc::Utils::Tty.reset}")
+ .concat(")")
+ end
+ else
+ puts "Not installed"
+ end
+ end
+
+ def self.name_info(cask)
+ ohai cask.name.size > 1 ? "Names" : "Name"
+ puts cask.name.empty? ? "#{Hbc::Utils::Tty.red}None#{Hbc::Utils::Tty.reset}" : cask.name
+ end
+
+ def self.github_info(cask)
+ user, repo, token = Hbc::QualifiedToken.parse(Hbc.all_tokens.detect { |t| t.split("/").last == cask.token })
+ "#{Tap.fetch(user, repo).default_remote}/blob/master/Casks/#{token}.rb"
+ end
+
+ def self.artifact_info(cask)
+ ohai "Artifacts"
+ Hbc::DSL::ORDINARY_ARTIFACT_TYPES.each do |type|
+ next if cask.artifacts[type].empty?
+ cask.artifacts[type].each do |artifact|
+ activatable_item = type == :stage_only ? "<none>" : artifact.first
+ puts "#{activatable_item} (#{type})"
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/install.rb b/Library/Homebrew/cask/lib/hbc/cli/install.rb
new file mode 100644
index 000000000..43eab9f3d
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/install.rb
@@ -0,0 +1,60 @@
+
+class Hbc::CLI::Install < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ force = args.include? "--force"
+ skip_cask_deps = args.include? "--skip-cask-deps"
+ require_sha = args.include? "--require-sha"
+ retval = install_casks cask_tokens, force, skip_cask_deps, require_sha
+ # retval is ternary: true/false/nil
+
+ raise Hbc::CaskError, "nothing to install" if retval.nil?
+ raise Hbc::CaskError, "install incomplete" unless retval
+ end
+
+ def self.install_casks(cask_tokens, force, skip_cask_deps, require_sha)
+ count = 0
+ cask_tokens.each do |cask_token|
+ begin
+ cask = Hbc.load(cask_token)
+ Hbc::Installer.new(cask,
+ force: force,
+ skip_cask_deps: skip_cask_deps,
+ require_sha: require_sha).install
+ count += 1
+ rescue Hbc::CaskAlreadyInstalledError => e
+ opoo e.message
+ count += 1
+ rescue Hbc::CaskAutoUpdatesError => e
+ opoo e.message
+ count += 1
+ rescue Hbc::CaskUnavailableError => e
+ warn_unavailable_with_suggestion cask_token, e
+ rescue Hbc::CaskNoShasumError => e
+ opoo e.message
+ count += 1
+ end
+ end
+ count == 0 ? nil : count == cask_tokens.length
+ end
+
+ def self.warn_unavailable_with_suggestion(cask_token, e)
+ exact_match, partial_matches = Hbc::CLI::Search.search(cask_token)
+ errmsg = e.message
+ if exact_match
+ errmsg.concat(". Did you mean:\n#{exact_match}")
+ elsif !partial_matches.empty?
+ errmsg.concat(". Did you mean one of:\n#{puts_columns(partial_matches.take(20))}\n")
+ end
+ onoe errmsg
+ end
+
+ def self.help
+ "installs the given Cask"
+ end
+
+ def self.needs_init?
+ true
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb
new file mode 100644
index 000000000..f05dbe803
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb
@@ -0,0 +1,135 @@
+class Hbc::CLI::InternalAuditModifiedCasks < Hbc::CLI::InternalUseBase
+ RELEVANT_STANZAS = %i{version sha256 url appcast}.freeze
+
+ class << self
+ def needs_init?
+ true
+ end
+
+ def run(*args)
+ commit_range = commit_range(args)
+ cleanup = args.any? { |a| a =~ %r{^-+c(leanup)?$}i }
+ new(commit_range, cleanup: cleanup).run
+ end
+
+ def commit_range(args)
+ posargs = args.reject { |a| a.empty? || a.chars.first == "-" }
+ odie usage unless posargs.size == 1
+ posargs.first
+ end
+
+ def posargs(args)
+ args.reject { |a| a.empty? || a.chars.first == "-" }
+ end
+
+ def usage
+ <<-EOS.undent
+ Usage: brew cask _audit_modified_casks [options...] <commit range>
+
+ Given a range of Git commits, find any Casks that were modified and run `brew
+ cask audit' on them. If the `url', `version', or `sha256' stanzas were modified,
+ run with the `--download' flag to verify the hash.
+
+ Options:
+ -c, --cleanup
+ Remove all cached downloads. Use with care.
+ EOS
+ end
+ end
+
+ def initialize(commit_range, cleanup: false)
+ @commit_range = commit_range
+ @cleanup = cleanup
+ end
+
+ attr_reader :commit_range
+
+ def cleanup?
+ @cleanup
+ end
+
+ def run
+ at_exit do
+ cleanup
+ end
+
+ Dir.chdir git_root do
+ modified_cask_files.zip(modified_casks).each do |cask_file, cask|
+ audit(cask, cask_file)
+ end
+ end
+ report_failures
+ end
+
+ def git_root
+ @git_root ||= git(*%w[rev-parse --show-toplevel])
+ end
+
+ def modified_cask_files
+ @modified_cask_files ||= git_filter_cask_files("AM")
+ end
+
+ def added_cask_files
+ @added_cask_files ||= git_filter_cask_files("A")
+ end
+
+ def git_filter_cask_files(filter)
+ git("diff", "--name-only", "--diff-filter=#{filter}", commit_range,
+ "--", Pathname.new(git_root).join("Casks", "*.rb").to_s).split("\n")
+ end
+
+ def modified_casks
+ return @modified_casks if defined? @modified_casks
+ @modified_casks = modified_cask_files.map { |f| Hbc.load(f) }
+ if @modified_casks.any?
+ num_modified = @modified_casks.size
+ ohai "#{num_modified} modified #{pluralize('cask', num_modified)}: " \
+ "#{@modified_casks.join(' ')}"
+ end
+ @modified_casks
+ end
+
+ def audit(cask, cask_file)
+ audit_download = audit_download?(cask, cask_file)
+ check_token_conflicts = added_cask_files.include?(cask_file)
+ success = Hbc::Auditor.audit(cask, audit_download: audit_download,
+ check_token_conflicts: check_token_conflicts)
+ failed_casks << cask unless success
+ end
+
+ def failed_casks
+ @failed_casks ||= []
+ end
+
+ def audit_download?(cask, cask_file)
+ cask.sha256 != :no_check && relevant_stanza_modified?(cask_file)
+ end
+
+ def relevant_stanza_modified?(cask_file)
+ out = git("diff", commit_range, "--", cask_file)
+ out =~ %r{^\+\s*(#{RELEVANT_STANZAS.join('|')})}
+ end
+
+ def git(*args)
+ odebug ["git", *args].join(" ")
+ out, err, status = Open3.capture3("git", *args)
+ return out.chomp if status.success?
+ odie err.chomp
+ end
+
+ def report_failures
+ return if failed_casks.empty?
+ num_failed = failed_casks.size
+ cask_pluralized = pluralize("cask", num_failed)
+ odie "audit failed for #{num_failed} #{cask_pluralized}: " \
+ "#{failed_casks.join(' ')}"
+ end
+
+ def pluralize(str, num)
+ num == 1 ? str : "#{str}s"
+ end
+
+ def cleanup
+ Hbc::CLI::Cleanup.run if cleanup?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb
new file mode 100644
index 000000000..d53f420e2
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb
@@ -0,0 +1,15 @@
+class Hbc::CLI::InternalCheckurl < Hbc::CLI::InternalUseBase
+ def self.run(*args)
+ casks_to_check = args.empty? ? Hbc.all : args.map { |arg| Hbc.load(arg) }
+ casks_to_check.each do |cask|
+ odebug "Checking URL for Cask #{cask}"
+ checker = Hbc::UrlChecker.new(cask)
+ checker.run
+ puts checker.summary
+ end
+ end
+
+ def self.help
+ "checks for bad Cask URLs"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb
new file mode 100644
index 000000000..d1cfe8d63
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb
@@ -0,0 +1,30 @@
+class Hbc::CLI::InternalDump < Hbc::CLI::InternalUseBase
+ def self.run(*arguments)
+ cask_tokens = cask_tokens_from(arguments)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ retval = dump_casks(*cask_tokens)
+ # retval is ternary: true/false/nil
+
+ raise Hbc::CaskError, "nothing to dump" if retval.nil?
+ raise Hbc::CaskError, "dump incomplete" unless retval
+ end
+
+ def self.dump_casks(*cask_tokens)
+ Hbc.debug = true # Yuck. At the moment this is the only way to make dumps visible
+ count = 0
+ cask_tokens.each do |cask_token|
+ begin
+ cask = Hbc.load(cask_token)
+ count += 1
+ cask.dumpcask
+ rescue StandardError => e
+ opoo "#{cask_token} was not found or would not load: #{e}"
+ end
+ end
+ count == 0 ? nil : count == cask_tokens.length
+ end
+
+ def self.help
+ "Dump the given Cask in YAML format"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb
new file mode 100644
index 000000000..81d7ee673
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb
@@ -0,0 +1,19 @@
+class Hbc::CLI::InternalHelp < Hbc::CLI::InternalUseBase
+ def self.run(*_ignored)
+ max_command_len = Hbc::CLI.commands.map(&:length).max
+ puts "Unstable Internal-use Commands:\n\n"
+ Hbc::CLI.command_classes.each do |klass|
+ next if klass.visible
+ puts " #{klass.command_name.ljust(max_command_len)} #{help_for(klass)}"
+ end
+ puts "\n"
+ end
+
+ def self.help_for(klass)
+ klass.respond_to?(:help) ? klass.help : nil
+ end
+
+ def self.help
+ "Print help strings for unstable internal-use commands"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb
new file mode 100644
index 000000000..651a9ae37
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb
@@ -0,0 +1,127 @@
+class Hbc::CLI::InternalStanza < Hbc::CLI::InternalUseBase
+ # Syntax
+ #
+ # brew cask _stanza <stanza_name> [ --table | --yaml | --inspect | --quiet ] [ <cask_token> ... ]
+ #
+ # If no tokens are given, then data for all Casks is returned.
+ #
+ # The pseudo-stanza "artifacts" is available.
+ #
+ # On failure, a blank line is returned on the standard output.
+ #
+ # Examples
+ #
+ # brew cask _stanza appcast --table
+ # brew cask _stanza app --table alfred google-chrome adium voicemac logisim vagrant
+ # brew cask _stanza url --table alfred google-chrome adium voicemac logisim vagrant
+ # brew cask _stanza version --table alfred google-chrome adium voicemac logisim vagrant
+ # brew cask _stanza artifacts --table --inspect alfred google-chrome adium voicemac logisim vagrant
+ # brew cask _stanza artifacts --table --yaml alfred google-chrome adium voicemac logisim vagrant
+ #
+
+ # TODO: this should be retrievable from Hbc::DSL
+ ARTIFACTS = Set.new [
+ :app,
+ :suite,
+ :artifact,
+ :prefpane,
+ :qlplugin,
+ :font,
+ :service,
+ :colorpicker,
+ :binary,
+ :input_method,
+ :internet_plugin,
+ :audio_unit_plugin,
+ :vst_plugin,
+ :vst3_plugin,
+ :screen_saver,
+ :pkg,
+ :installer,
+ :stage_only,
+ :nested_container,
+ :uninstall,
+ :postflight,
+ :uninstall_postflight,
+ :preflight,
+ :uninstall_postflight,
+ ]
+
+ def self.run(*arguments)
+ table = arguments.include? "--table"
+ quiet = arguments.include? "--quiet"
+ format = :to_yaml if arguments.include? "--yaml"
+ format = :inspect if arguments.include? "--inspect"
+ cask_tokens = arguments.reject { |arg| arg.chars.first == "-" }
+ stanza = cask_tokens.shift.to_sym
+ cask_tokens = Hbc.all_tokens if cask_tokens.empty?
+
+ retval = print_stanzas(stanza, format, table, quiet, *cask_tokens)
+
+ # retval is ternary: true/false/nil
+ if retval.nil?
+ exit 1 if quiet
+ raise Hbc::CaskError, "nothing to print"
+ elsif !retval
+ exit 1 if quiet
+ raise Hbc::CaskError, "print incomplete"
+ end
+ end
+
+ def self.print_stanzas(stanza, format = nil, table = nil, quiet = nil, *cask_tokens)
+ count = 0
+ if ARTIFACTS.include?(stanza)
+ artifact_name = stanza
+ stanza = :artifacts
+ end
+
+ cask_tokens.each do |cask_token|
+ print "#{cask_token}\t" if table
+
+ begin
+ cask = Hbc.load(cask_token)
+ rescue StandardError
+ opoo "Cask '#{cask_token}' was not found" unless quiet
+ puts ""
+ next
+ end
+
+ unless cask.respond_to?(stanza)
+ opoo "no such stanza '#{stanza}' on Cask '#{cask_token}'" unless quiet
+ puts ""
+ next
+ end
+
+ begin
+ value = cask.send(stanza)
+ rescue StandardError
+ opoo "failure calling '#{stanza}' on Cask '#{cask_token}'" unless quiet
+ puts ""
+ next
+ end
+
+ if artifact_name && !value.key?(artifact_name)
+ opoo "no such stanza '#{artifact_name}' on Cask '#{cask_token}'" unless quiet
+ puts ""
+ next
+ end
+
+ value = value.fetch(artifact_name).to_a.flatten if artifact_name
+
+ if format
+ puts value.send(format)
+ elsif artifact_name || value.is_a?(Symbol)
+ puts value.inspect
+ else
+ puts value.to_s
+ end
+
+ count += 1
+ end
+ count == 0 ? nil : count == cask_tokens.length
+ end
+
+ def self.help
+ "Extract and render a specific stanza for the given Casks"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb
new file mode 100644
index 000000000..6a4359ea1
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb
@@ -0,0 +1,9 @@
+class Hbc::CLI::InternalUseBase < Hbc::CLI::Base
+ def self.command_name
+ super.sub(%r{^internal_}i, "_")
+ end
+
+ def self.visible
+ false
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/list.rb b/Library/Homebrew/cask/lib/hbc/cli/list.rb
new file mode 100644
index 000000000..ce507a827
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/list.rb
@@ -0,0 +1,82 @@
+class Hbc::CLI::List < Hbc::CLI::Base
+ def self.run(*arguments)
+ @options = {}
+ @options[:one] = true if arguments.delete("-1")
+ @options[:versions] = true if arguments.delete("--versions")
+
+ if arguments.delete("-l")
+ @options[:one] = true
+ opoo "Option -l is obsolete! Implying option -1."
+ end
+
+ retval = arguments.any? ? list(*arguments) : list_installed
+ # retval is ternary: true/false/nil
+ if retval.nil? && !arguments.any?
+ opoo "nothing to list" # special case: avoid exit code
+ elsif retval.nil?
+ raise Hbc::CaskError, "nothing to list"
+ elsif !retval
+ raise Hbc::CaskError, "listing incomplete"
+ end
+ end
+
+ def self.list(*cask_tokens)
+ count = 0
+
+ cask_tokens.each do |cask_token|
+ odebug "Listing files for Cask #{cask_token}"
+ begin
+ cask = Hbc.load(cask_token)
+
+ if cask.installed?
+ if @options[:one]
+ puts cask.token
+ elsif @options[:versions]
+ puts format_versioned(cask)
+ else
+ installed_caskfile = cask.metadata_master_container_path.join(*cask.timestamped_versions.last, "Casks", "#{cask_token}.rb")
+ cask = Hbc.load(installed_caskfile)
+ list_artifacts(cask)
+ end
+
+ count += 1
+ else
+ opoo "#{cask} is not installed"
+ end
+ rescue Hbc::CaskUnavailableError => e
+ onoe e
+ end
+ end
+
+ count == 0 ? nil : count == cask_tokens.length
+ end
+
+ def self.list_artifacts(cask)
+ Hbc::Artifact.for_cask(cask).each do |artifact|
+ summary = artifact.new(cask).summary
+ ohai summary[:english_description], summary[:contents] unless summary.empty?
+ end
+ end
+
+ def self.list_installed
+ installed_casks = Hbc.installed
+
+ if @options[:one]
+ puts installed_casks.map(&:to_s)
+ elsif @options[:versions]
+ puts installed_casks.map(&method(:format_versioned))
+ else
+ puts_columns installed_casks.map(&:to_s)
+ end
+
+ installed_casks.empty? ? nil : true
+ end
+
+ def self.format_versioned(cask)
+ cask.to_s.concat(cask.versions.map(&:to_s).join(" ").prepend(" "))
+ end
+
+ def self.help
+ "with no args, lists installed Casks; given installed Casks, lists staged files"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/search.rb b/Library/Homebrew/cask/lib/hbc/cli/search.rb
new file mode 100644
index 000000000..c356128a6
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/search.rb
@@ -0,0 +1,56 @@
+class Hbc::CLI::Search < Hbc::CLI::Base
+ def self.run(*arguments)
+ render_results(*search(*arguments))
+ end
+
+ def self.extract_regexp(string)
+ if string =~ %r{^/(.*)/$}
+ Regexp.last_match[1]
+ else
+ false
+ end
+ end
+
+ def self.search(*arguments)
+ exact_match = nil
+ partial_matches = []
+ search_term = arguments.join(" ")
+ search_regexp = extract_regexp arguments.first
+ if search_regexp
+ search_term = arguments.first
+ partial_matches = Hbc::CLI.nice_listing(Hbc.all_tokens).grep(%r{#{search_regexp}}i)
+ else
+ # suppressing search of the font Tap is a quick hack until behavior can be made configurable
+ all_tokens = Hbc::CLI.nice_listing Hbc.all_tokens.reject { |t| %r{^caskroom/homebrew-fonts/}.match(t) }
+ simplified_tokens = all_tokens.map { |t| t.sub(%r{^.*\/}, "").gsub(%r{[^a-z0-9]+}i, "") }
+ simplified_search_term = search_term.sub(%r{\.rb$}i, "").gsub(%r{[^a-z0-9]+}i, "")
+ exact_match = simplified_tokens.grep(%r{^#{simplified_search_term}$}i) { |t| all_tokens[simplified_tokens.index(t)] }.first
+ partial_matches = simplified_tokens.grep(%r{#{simplified_search_term}}i) { |t| all_tokens[simplified_tokens.index(t)] }
+ partial_matches.delete(exact_match)
+ end
+ [exact_match, partial_matches, search_term]
+ end
+
+ def self.render_results(exact_match, partial_matches, search_term)
+ if !exact_match && partial_matches.empty?
+ puts "No Cask found for \"#{search_term}\"."
+ return
+ end
+ if exact_match
+ ohai "Exact match"
+ puts exact_match
+ end
+ unless partial_matches.empty?
+ if extract_regexp search_term
+ ohai "Regexp matches"
+ else
+ ohai "Partial matches"
+ end
+ puts_columns partial_matches
+ end
+ end
+
+ def self.help
+ "searches all known Casks"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/style.rb b/Library/Homebrew/cask/lib/hbc/cli/style.rb
new file mode 100644
index 000000000..ac7cbfb44
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/style.rb
@@ -0,0 +1,69 @@
+require "English"
+
+class Hbc::CLI::Style < Hbc::CLI::Base
+ def self.help
+ "checks Cask style using RuboCop"
+ end
+
+ def self.run(*args)
+ retval = new(args).run
+ raise Hbc::CaskError, "style check failed" unless retval
+ end
+
+ attr_reader :args
+ def initialize(args)
+ @args = args
+ end
+
+ def run
+ install_rubocop
+ system "rubocop", *rubocop_args, "--", *cask_paths
+ $CHILD_STATUS.success?
+ end
+
+ RUBOCOP_CASK_VERSION = "~> 0.8.3".freeze
+
+ def install_rubocop
+ Hbc::Utils.capture_stderr do
+ begin
+ Homebrew.install_gem_setup_path! "rubocop-cask", RUBOCOP_CASK_VERSION, "rubocop"
+ rescue SystemExit
+ raise Hbc::CaskError, $stderr.string.chomp.sub("#{::Tty.red}Error#{::Tty.reset}: ", "")
+ end
+ end
+ end
+
+ def cask_paths
+ @cask_paths ||= if cask_tokens.empty?
+ Hbc.all_tapped_cask_dirs
+ elsif cask_tokens.any? { |file| File.exist?(file) }
+ cask_tokens
+ else
+ cask_tokens.map { |token| Hbc.path(token) }
+ end
+ end
+
+ def cask_tokens
+ @cask_tokens ||= self.class.cask_tokens_from(args)
+ end
+
+ def rubocop_args
+ fix? ? autocorrect_args : default_args
+ end
+
+ def default_args
+ ["--format", "simple", "--force-exclusion", "--config", rubocop_config]
+ end
+
+ def autocorrect_args
+ default_args + ["--auto-correct"]
+ end
+
+ def rubocop_config
+ Hbc.default_tap.cask_dir.join(".rubocop.yml")
+ end
+
+ def fix?
+ args.any? { |arg| arg =~ %r{--(fix|(auto-?)?correct)} }
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb b/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb
new file mode 100644
index 000000000..cd98b6e61
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb
@@ -0,0 +1,40 @@
+class Hbc::CLI::Uninstall < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ force = args.include? "--force"
+
+ cask_tokens.each do |cask_token|
+ odebug "Uninstalling Cask #{cask_token}"
+ cask = Hbc.load(cask_token)
+
+ raise Hbc::CaskNotInstalledError, cask unless cask.installed? || force
+
+ latest_installed_version = cask.timestamped_versions.last
+
+ unless latest_installed_version.nil?
+ latest_installed_cask_file = cask.metadata_master_container_path
+ .join(latest_installed_version.join(File::Separator),
+ "Casks", "#{cask_token}.rb")
+
+ # use the same cask file that was used for installation, if possible
+ cask = Hbc.load(latest_installed_cask_file) if latest_installed_cask_file.exist?
+ end
+
+ Hbc::Installer.new(cask, force: force).uninstall
+
+ next if (versions = cask.versions).empty?
+
+ single = versions.count == 1
+
+ puts <<-EOF.undent
+ #{cask_token} #{versions.join(', ')} #{single ? 'is' : 'are'} still installed.
+ Remove #{single ? 'it' : 'them all'} with `brew cask uninstall --force #{cask_token}`.
+ EOF
+ end
+ end
+
+ def self.help
+ "uninstalls the given Cask"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/update.rb b/Library/Homebrew/cask/lib/hbc/cli/update.rb
new file mode 100644
index 000000000..ceb947544
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/update.rb
@@ -0,0 +1,16 @@
+class Hbc::CLI::Update < Hbc::CLI::Base
+ def self.run(*_ignored)
+ result = Hbc::SystemCommand.run(Hbc.homebrew_executable,
+ args: %w[update])
+ # TODO: separating stderr/stdout is undesirable here.
+ # Hbc::SystemCommand should have an option for plain
+ # unbuffered output.
+ print result.stdout
+ $stderr.print result.stderr
+ exit result.exit_status
+ end
+
+ def self.help
+ "a synonym for 'brew update'"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/cli/zap.rb b/Library/Homebrew/cask/lib/hbc/cli/zap.rb
new file mode 100644
index 000000000..081378330
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/cli/zap.rb
@@ -0,0 +1,15 @@
+class Hbc::CLI::Zap < Hbc::CLI::Base
+ def self.run(*args)
+ cask_tokens = cask_tokens_from(args)
+ raise Hbc::CaskUnspecifiedError if cask_tokens.empty?
+ cask_tokens.each do |cask_token|
+ odebug "Zapping Cask #{cask_token}"
+ cask = Hbc.load(cask_token)
+ Hbc::Installer.new(cask).zap
+ end
+ end
+
+ def self.help
+ "zaps all files associated with the given Cask"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container.rb b/Library/Homebrew/cask/lib/hbc/container.rb
new file mode 100644
index 000000000..e2b21a3ef
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container.rb
@@ -0,0 +1,68 @@
+class Hbc::Container; end
+
+require "hbc/container/base"
+require "hbc/container/air"
+require "hbc/container/bzip2"
+require "hbc/container/cab"
+require "hbc/container/criteria"
+require "hbc/container/dmg"
+require "hbc/container/generic_unar"
+require "hbc/container/gzip"
+require "hbc/container/lzma"
+require "hbc/container/naked"
+require "hbc/container/otf"
+require "hbc/container/pkg"
+require "hbc/container/seven_zip"
+require "hbc/container/sit"
+require "hbc/container/tar"
+require "hbc/container/ttf"
+require "hbc/container/rar"
+require "hbc/container/xar"
+require "hbc/container/xip"
+require "hbc/container/xz"
+require "hbc/container/zip"
+
+class Hbc::Container
+ def self.autodetect_containers
+ [
+ Hbc::Container::Pkg,
+ Hbc::Container::Ttf,
+ Hbc::Container::Otf,
+ Hbc::Container::Air,
+ Hbc::Container::Cab,
+ Hbc::Container::Dmg,
+ Hbc::Container::SevenZip,
+ Hbc::Container::Sit,
+ Hbc::Container::Rar,
+ Hbc::Container::Zip,
+ Hbc::Container::Xip, # needs to be before xar as this is a cpio inside a gzip inside a xar
+ Hbc::Container::Xar, # need to be before tar as tar can also list xar
+ Hbc::Container::Tar, # or compressed tar (bzip2/gzip/lzma/xz)
+ Hbc::Container::Bzip2, # pure bzip2
+ Hbc::Container::Gzip, # pure gzip
+ Hbc::Container::Lzma, # pure lzma
+ Hbc::Container::Xz, # pure xz
+ ]
+ # for explicit use only (never autodetected):
+ # Hbc::Container::Naked
+ # Hbc::Container::GenericUnar
+ end
+
+ def self.for_path(path, command)
+ odebug "Determining which containers to use based on filetype"
+ criteria = Hbc::Container::Criteria.new(path, command)
+ autodetect_containers.find do |c|
+ odebug "Checking container class #{c}"
+ c.me?(criteria)
+ end
+ end
+
+ def self.from_type(type)
+ odebug "Determining which containers to use based on 'container :type'"
+ begin
+ Hbc::Container.const_get(type.to_s.split("_").map(&:capitalize).join)
+ rescue NameError
+ false
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/air.rb b/Library/Homebrew/cask/lib/hbc/container/air.rb
new file mode 100644
index 000000000..e82b677e9
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/air.rb
@@ -0,0 +1,33 @@
+require "hbc/container/base"
+
+class Hbc::Container::Air < Hbc::Container::Base
+ INSTALLER_PATHNAME =
+ Pathname("/Applications/Utilities/Adobe AIR Application Installer.app" \
+ "/Contents/MacOS/Adobe AIR Application Installer")
+
+ def self.me?(criteria)
+ %w[.air].include?(criteria.path.extname)
+ end
+
+ def self.installer_cmd
+ return @installer_cmd ||= INSTALLER_PATHNAME if installer_exist?
+ raise Hbc::CaskError, <<-ERRMSG.undent
+ Adobe AIR runtime not present, try installing it via
+
+ brew cask install adobe-air
+
+ ERRMSG
+ end
+
+ def self.installer_exist?
+ INSTALLER_PATHNAME.exist?
+ end
+
+ def extract
+ install = @command.run(self.class.installer_cmd,
+ args: ["-silent", "-location", @cask.staged_path, Pathname.new(@path).realpath])
+
+ return unless install.exit_status == 9
+ raise Hbc::CaskError, "Adobe AIR application #{@cask} already exists on the system, and cannot be reinstalled."
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/base.rb b/Library/Homebrew/cask/lib/hbc/container/base.rb
new file mode 100644
index 000000000..42331df31
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/base.rb
@@ -0,0 +1,37 @@
+class Hbc::Container::Base
+ def initialize(cask, path, command, nested: false)
+ @cask = cask
+ @path = path
+ @command = command
+ @nested = nested
+ end
+
+ def extract_nested_inside(dir)
+ children = Pathname.new(dir).children
+
+ nested_container = children[0]
+
+ unless children.count == 1 &&
+ !nested_container.directory? &&
+ @cask.artifacts[:nested_container].empty? &&
+ extract_nested_container(nested_container)
+
+ children.each do |src|
+ dest = @cask.staged_path.join(src.basename)
+ FileUtils.rm_r(dest) if dest.exist?
+ FileUtils.mv(src, dest)
+ end
+ end
+ end
+
+ def extract_nested_container(source)
+ container = Hbc::Container.for_path(source, @command)
+
+ return false unless container
+
+ ohai "Extracting nested container #{source.basename}"
+ container.new(@cask, source, @command, nested: true).extract
+
+ true
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/bzip2.rb b/Library/Homebrew/cask/lib/hbc/container/bzip2.rb
new file mode 100644
index 000000000..617c68b32
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/bzip2.rb
@@ -0,0 +1,18 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Bzip2 < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^BZh}n)
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir])
+ @command.run!("/usr/bin/bunzip2", args: ["--quiet", "--", Pathname.new(unpack_dir).join(@path.basename)])
+
+ extract_nested_inside(unpack_dir)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/cab.rb b/Library/Homebrew/cask/lib/hbc/container/cab.rb
new file mode 100644
index 000000000..28000a5a3
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/cab.rb
@@ -0,0 +1,26 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Cab < Hbc::Container::Base
+ def self.me?(criteria)
+ cabextract = Hbc.homebrew_prefix.join("bin", "cabextract")
+
+ criteria.magic_number(%r{^MSCF}n) &&
+ cabextract.exist? &&
+ criteria.command.run(cabextract, args: ["-t", "--", criteria.path.to_s]).stderr.empty?
+ end
+
+ def extract
+ cabextract = Hbc.homebrew_prefix.join("bin", "cabextract")
+
+ unless cabextract.exist?
+ raise Hbc::CaskError, "Expected to find cabextract executable. Cask '#{@cask}' must add: depends_on formula: 'cabextract'"
+ end
+
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!(cabextract, args: ["-d", unpack_dir, "--", @path])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/criteria.rb b/Library/Homebrew/cask/lib/hbc/container/criteria.rb
new file mode 100644
index 000000000..2ebb9d6fa
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/criteria.rb
@@ -0,0 +1,18 @@
+class Hbc::Container::Criteria
+ attr_reader :path, :command
+
+ def initialize(path, command)
+ @path = path
+ @command = command
+ end
+
+ def extension(regex)
+ path.extname.sub(%r{^\.}, "") =~ Regexp.new(regex.source, regex.options | Regexp::IGNORECASE)
+ end
+
+ def magic_number(regex)
+ # 262: length of the longest regex (currently: Hbc::Container::Tar)
+ @magic_number ||= File.open(@path, "rb") { |f| f.read(262) }
+ @magic_number =~ regex
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/dmg.rb b/Library/Homebrew/cask/lib/hbc/container/dmg.rb
new file mode 100644
index 000000000..7e4b9340d
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/dmg.rb
@@ -0,0 +1,125 @@
+require "set"
+require "tempfile"
+
+require "hbc/container/base"
+
+class Hbc::Container::Dmg < Hbc::Container::Base
+ def self.me?(criteria)
+ !criteria.command.run("/usr/bin/hdiutil",
+ # realpath is a failsafe against unusual filenames
+ args: ["imageinfo", Pathname.new(criteria.path).realpath],
+ print_stderr: false).stdout.empty?
+ end
+
+ attr_reader :mounts
+ def initialize(*args)
+ super(*args)
+ @mounts = []
+ end
+
+ def extract
+ mount!
+ assert_mounts_found
+ extract_mounts
+ ensure
+ eject!
+ end
+
+ def mount!
+ plist = @command.run!("/usr/bin/hdiutil",
+ # realpath is a failsafe against unusual filenames
+ args: %w[mount -plist -nobrowse -readonly -noidme -mountrandom /tmp] + [Pathname.new(@path).realpath],
+ input: %w[y])
+ .plist
+ @mounts = mounts_from_plist(plist)
+ end
+
+ def eject!
+ @mounts.each do |mount|
+ # realpath is a failsafe against unusual filenames
+ mountpath = Pathname.new(mount).realpath
+ next unless mountpath.exist?
+
+ begin
+ tries ||= 2
+ @command.run("/usr/sbin/diskutil",
+ args: ["eject", mountpath],
+ print_stderr: false)
+
+ raise Hbc::CaskError, "Failed to eject #{mountpath}" if mountpath.exist?
+ rescue Hbc::CaskError => e
+ raise e if (tries -= 1).zero?
+ sleep 1
+ retry
+ end
+ end
+ end
+
+ private
+
+ def extract_mounts
+ @mounts.each(&method(:extract_mount))
+ end
+
+ def extract_mount(mount)
+ Tempfile.open(["", ".bom"]) do |bomfile|
+ bomfile.close
+
+ Tempfile.open(["", ".list"]) do |filelist|
+ filelist.write(bom_filelist_from_path(mount))
+ filelist.close
+
+ @command.run!("/usr/bin/mkbom", args: ["-s", "-i", filelist.path, "--", bomfile.path])
+ @command.run!("/usr/bin/ditto", args: ["--bom", bomfile.path, "--", mount, @cask.staged_path])
+ end
+ end
+ end
+
+ def bom_filelist_from_path(mount)
+ Dir.chdir(mount) {
+ Dir.glob("**/*", File::FNM_DOTMATCH).map { |path|
+ next if skip_path?(Pathname(path))
+ path == "." ? path : path.prepend("./")
+ }.compact.join("\n").concat("\n")
+ }
+ end
+
+ def skip_path?(path)
+ dmg_metadata?(path) || system_dir_symlink?(path)
+ end
+
+ # unnecessary DMG metadata
+ DMG_METADATA_FILES = %w[
+ .background
+ .com.apple.timemachine.donotpresent
+ .DocumentRevisions-V100
+ .DS_Store
+ .fseventsd
+ .MobileBackups
+ .Spotlight-V100
+ .TemporaryItems
+ .Trashes
+ .VolumeIcon.icns
+ ].to_set.freeze
+
+ def dmg_metadata?(path)
+ relative_root = path.sub(%r{/.*}, "")
+ DMG_METADATA_FILES.include?(relative_root.basename.to_s)
+ end
+
+ def system_dir_symlink?(path)
+ # symlinks to system directories (commonly to /Applications)
+ path.symlink? && MacOS.system_dir?(path.readlink)
+ end
+
+ def mounts_from_plist(plist)
+ return [] unless plist.respond_to?(:fetch)
+ plist.fetch("system-entities", []).map { |entity|
+ entity["mount-point"]
+ }.compact
+ end
+
+ def assert_mounts_found
+ raise Hbc::CaskError, "No mounts found in '#{@path}'; perhaps it is a bad DMG?" if @mounts.empty?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb b/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb
new file mode 100644
index 000000000..1dcc0997a
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb
@@ -0,0 +1,26 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::GenericUnar < Hbc::Container::Base
+ def self.me?(criteria)
+ lsar = Hbc.homebrew_prefix.join("bin", "lsar")
+ lsar.exist? &&
+ criteria.command.run(lsar,
+ args: ["-l", "-t", "--", criteria.path],
+ print_stderr: false).stdout.chomp.end_with?("passed, 0 failed.")
+ end
+
+ def extract
+ unar = Hbc.homebrew_prefix.join("bin", "unar")
+
+ unless unar.exist?
+ raise Hbc::CaskError, "Expected to find unar executable. Cask #{@cask} must add: depends_on formula: 'unar'"
+ end
+
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!(unar, args: ["-force-overwrite", "-quiet", "-no-directory", "-output-directory", unpack_dir, "--", @path])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/gzip.rb b/Library/Homebrew/cask/lib/hbc/container/gzip.rb
new file mode 100644
index 000000000..1d2cc1f37
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/gzip.rb
@@ -0,0 +1,18 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Gzip < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^\037\213}n)
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir])
+ @command.run!("/usr/bin/gunzip", args: ["--quiet", "--", Pathname.new(unpack_dir).join(@path.basename)])
+
+ extract_nested_inside(unpack_dir)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/lzma.rb b/Library/Homebrew/cask/lib/hbc/container/lzma.rb
new file mode 100644
index 000000000..e538b3779
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/lzma.rb
@@ -0,0 +1,23 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Lzma < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^\]\000\000\200\000}n)
+ end
+
+ def extract
+ unlzma = Hbc.homebrew_prefix.join("bin", "unlzma")
+
+ unless unlzma.exist?
+ raise Hbc::CaskError, "Expected to find unlzma executable. Cask '#{@cask}' must add: depends_on formula: 'lzma'"
+ end
+
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir])
+ @command.run!(unlzma, args: ["-q", "--", Pathname(unpack_dir).join(@path.basename)])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/naked.rb b/Library/Homebrew/cask/lib/hbc/container/naked.rb
new file mode 100644
index 000000000..596f50789
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/naked.rb
@@ -0,0 +1,19 @@
+require "hbc/container/base"
+
+class Hbc::Container::Naked < Hbc::Container::Base
+ # Either inherit from this class and override with self.me?(criteria),
+ # or use this class directly as "container type: :naked",
+ # in which case self.me? is not called.
+ def self.me?(*)
+ false
+ end
+
+ def extract
+ @command.run!("/usr/bin/ditto", args: ["--", @path, @cask.staged_path.join(target_file)])
+ end
+
+ def target_file
+ return @path.basename if @nested
+ URI.decode(File.basename(@cask.url.path))
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/otf.rb b/Library/Homebrew/cask/lib/hbc/container/otf.rb
new file mode 100644
index 000000000..f9a25e1ed
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/otf.rb
@@ -0,0 +1,7 @@
+require "hbc/container/naked"
+
+class Hbc::Container::Otf < Hbc::Container::Naked
+ def self.me?(criteria)
+ criteria.magic_number(%r{^OTTO}n)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/pkg.rb b/Library/Homebrew/cask/lib/hbc/container/pkg.rb
new file mode 100644
index 000000000..5d2282d0f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/pkg.rb
@@ -0,0 +1,9 @@
+require "hbc/container/naked"
+
+class Hbc::Container::Pkg < Hbc::Container::Naked
+ def self.me?(criteria)
+ criteria.extension(%r{m?pkg$}) &&
+ (criteria.path.directory? ||
+ criteria.magic_number(%r{^xar!}n))
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/rar.rb b/Library/Homebrew/cask/lib/hbc/container/rar.rb
new file mode 100644
index 000000000..9c144006f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/rar.rb
@@ -0,0 +1,8 @@
+require "hbc/container/generic_unar"
+
+class Hbc::Container::Rar < Hbc::Container::GenericUnar
+ def self.me?(criteria)
+ criteria.magic_number(%r{^Rar!}n) &&
+ super
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb b/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb
new file mode 100644
index 000000000..f0d183064
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb
@@ -0,0 +1,9 @@
+require "hbc/container/generic_unar"
+
+class Hbc::Container::SevenZip < Hbc::Container::GenericUnar
+ def self.me?(criteria)
+ # TODO: cover self-extracting archives
+ criteria.magic_number(%r{^7z}n) &&
+ super
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/sit.rb b/Library/Homebrew/cask/lib/hbc/container/sit.rb
new file mode 100644
index 000000000..155b93f3f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/sit.rb
@@ -0,0 +1,8 @@
+require "hbc/container/generic_unar"
+
+class Hbc::Container::Sit < Hbc::Container::GenericUnar
+ def self.me?(criteria)
+ criteria.magic_number(%r{^StuffIt}n) &&
+ super
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/tar.rb b/Library/Homebrew/cask/lib/hbc/container/tar.rb
new file mode 100644
index 000000000..8bc7c5f64
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/tar.rb
@@ -0,0 +1,18 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Tar < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^.{257}ustar}n) ||
+ # or compressed tar (bzip2/gzip/lzma/xz)
+ IO.popen(["/usr/bin/tar", "-t", "-f", criteria.path.to_s], err: "/dev/null") { |io| !io.read(1).nil? }
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/tar", args: ["-x", "-f", @path, "-C", unpack_dir])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/ttf.rb b/Library/Homebrew/cask/lib/hbc/container/ttf.rb
new file mode 100644
index 000000000..8d787f360
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/ttf.rb
@@ -0,0 +1,10 @@
+require "hbc/container/naked"
+
+class Hbc::Container::Ttf < Hbc::Container::Naked
+ def self.me?(criteria)
+ # TrueType Font
+ criteria.magic_number(%r{^\000\001\000\000\000}n) ||
+ # Truetype Font Collection
+ criteria.magic_number(%r{^ttcf}n)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/xar.rb b/Library/Homebrew/cask/lib/hbc/container/xar.rb
new file mode 100644
index 000000000..5afc78bc5
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/xar.rb
@@ -0,0 +1,16 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Xar < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^xar!}n)
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/xar", args: ["-x", "-f", @path, "-C", unpack_dir])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/xip.rb b/Library/Homebrew/cask/lib/hbc/container/xip.rb
new file mode 100644
index 000000000..579f28fe0
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/xip.rb
@@ -0,0 +1,25 @@
+require "tmpdir"
+
+class Hbc::Container::Xip < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^xar!}n) &&
+ IO.popen(["/usr/bin/xar", "-t", "-f", criteria.path.to_s], err: "/dev/null") { |io| io.read =~ %r{\AContent\nMetadata\n\Z} }
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ begin
+ ohai "Verifying signature for #{@path.basename}"
+ @command.run!("/usr/sbin/pkgutil", args: ["--check-signature", @path])
+ rescue
+ raise "Signature check failed."
+ end
+
+ @command.run!("/usr/bin/xar", args: ["-x", "-f", @path, "Content", "-C", unpack_dir])
+
+ Dir.chdir(@cask.staged_path) do
+ @command.run!("/usr/bin/cpio", args: ["--quiet", "-i", "-I", Pathname(unpack_dir).join("Content")])
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/xz.rb b/Library/Homebrew/cask/lib/hbc/container/xz.rb
new file mode 100644
index 000000000..228532943
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/xz.rb
@@ -0,0 +1,23 @@
+require "tmpdir"
+
+require "hbc/container/base"
+
+class Hbc::Container::Xz < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^\xFD7zXZ\x00}n)
+ end
+
+ def extract
+ unxz = Hbc.homebrew_prefix.join("bin", "unxz")
+
+ unless unxz.exist?
+ raise Hbc::CaskError, "Expected to find unxz executable. Cask '#{@cask}' must add: depends_on formula: 'xz'"
+ end
+
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir])
+ @command.run!(unxz, args: ["-q", "--", Pathname(unpack_dir).join(@path.basename)])
+ @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path])
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/container/zip.rb b/Library/Homebrew/cask/lib/hbc/container/zip.rb
new file mode 100644
index 000000000..c6702fbb5
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/container/zip.rb
@@ -0,0 +1,15 @@
+require "hbc/container/base"
+
+class Hbc::Container::Zip < Hbc::Container::Base
+ def self.me?(criteria)
+ criteria.magic_number(%r{^PK(\003\004|\005\006)}n)
+ end
+
+ def extract
+ Dir.mktmpdir do |unpack_dir|
+ @command.run!("/usr/bin/ditto", args: ["-x", "-k", "--", @path, unpack_dir])
+
+ extract_nested_inside(unpack_dir)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/download.rb b/Library/Homebrew/cask/lib/hbc/download.rb
new file mode 100644
index 000000000..18dd7fe44
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/download.rb
@@ -0,0 +1,43 @@
+require "fileutils"
+require "hbc/verify"
+
+class Hbc::Download
+ attr_reader :cask
+
+ def initialize(cask, force: false)
+ @cask = cask
+ @force = force
+ end
+
+ def perform
+ clear_cache
+ fetch
+ downloaded_path
+ end
+
+ private
+
+ attr_reader :force
+ attr_accessor :downloaded_path
+
+ def downloader
+ @downloader ||= case cask.url.using
+ when :svn
+ Hbc::SubversionDownloadStrategy.new(cask)
+ when :post
+ Hbc::CurlPostDownloadStrategy.new(cask)
+ else
+ Hbc::CurlDownloadStrategy.new(cask)
+ end
+ end
+
+ def clear_cache
+ downloader.clear_cache if force || cask.version.latest?
+ end
+
+ def fetch
+ self.downloaded_path = downloader.fetch
+ rescue StandardError => e
+ raise Hbc::CaskError, "Download failed on Cask '#{cask}' with message: #{e}"
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/download_strategy.rb b/Library/Homebrew/cask/lib/hbc/download_strategy.rb
new file mode 100644
index 000000000..88ffb5050
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/download_strategy.rb
@@ -0,0 +1,332 @@
+require "cgi"
+
+# We abuse Homebrew's download strategies considerably here.
+# * Our downloader instances only invoke the fetch and
+# clear_cache methods, ignoring stage
+# * Our overridden fetch methods are expected to return
+# a value: the successfully downloaded file.
+
+class Hbc::AbstractDownloadStrategy
+ attr_reader :cask, :name, :url, :uri_object, :version
+
+ def initialize(cask, command = Hbc::SystemCommand)
+ @cask = cask
+ @command = command
+ # TODO: this excess of attributes is a function of integrating
+ # with Homebrew's classes. Later we should be able to remove
+ # these in favor of @cask
+ @name = cask.token
+ @url = cask.url.to_s
+ @uri_object = cask.url
+ @version = cask.version
+ end
+
+ # All download strategies are expected to implement these methods
+ def fetch; end
+
+ def cached_location; end
+
+ def clear_cache; end
+end
+
+class Hbc::HbVCSDownloadStrategy < Hbc::AbstractDownloadStrategy
+ REF_TYPES = [:branch, :revision, :revisions, :tag].freeze
+
+ def initialize(cask, command = Hbc::SystemCommand)
+ super
+ @ref_type, @ref = extract_ref
+ @clone = Hbc.cache.join(cache_filename)
+ end
+
+ def extract_ref
+ key = REF_TYPES.find { |type|
+ uri_object.respond_to?(type) && uri_object.send(type)
+ }
+ [key, key ? uri_object.send(key) : nil]
+ end
+
+ def cache_filename
+ "#{name}--#{cache_tag}"
+ end
+
+ def cache_tag
+ "__UNKNOWN__"
+ end
+
+ def cached_location
+ @clone
+ end
+
+ def clear_cache
+ cached_location.rmtree if cached_location.exist?
+ end
+end
+
+class Hbc::CurlDownloadStrategy < Hbc::AbstractDownloadStrategy
+ # TODO: should be part of url object
+ def mirrors
+ @mirrors ||= []
+ end
+
+ def tarball_path
+ @tarball_path ||= Hbc.cache.join("#{name}--#{version}#{ext}")
+ end
+
+ def temporary_path
+ @temporary_path ||= tarball_path.sub(%r{$}, ".incomplete")
+ end
+
+ def cached_location
+ tarball_path
+ end
+
+ def clear_cache
+ [cached_location, temporary_path].each do |f|
+ next unless f.exist?
+ raise CurlDownloadStrategyError, "#{f} is in use by another process" if Hbc::Utils.file_locked?(f)
+ f.unlink
+ end
+ end
+
+ def downloaded_size
+ temporary_path.size? || 0
+ end
+
+ def _fetch
+ odebug "Calling curl with args #{cask_curl_args.utf8_inspect}"
+ curl(*cask_curl_args)
+ end
+
+ def fetch
+ ohai "Downloading #{@url}"
+ if tarball_path.exist?
+ puts "Already downloaded: #{tarball_path}"
+ else
+ had_incomplete_download = temporary_path.exist?
+ begin
+ File.open(temporary_path, "w+") do |f|
+ f.flock(File::LOCK_EX)
+ _fetch
+ f.flock(File::LOCK_UN)
+ end
+ rescue ErrorDuringExecution
+ # 33 == range not supported
+ # try wiping the incomplete download and retrying once
+ if $CHILD_STATUS.exitstatus == 33 && had_incomplete_download
+ ohai "Trying a full download"
+ temporary_path.unlink
+ had_incomplete_download = false
+ retry
+ end
+
+ msg = @url
+ msg.concat("\nThe incomplete download is cached at #{temporary_path}") if temporary_path.exist?
+ raise CurlDownloadStrategyError, msg
+ end
+ ignore_interrupts { temporary_path.rename(tarball_path) }
+ end
+ tarball_path
+ rescue CurlDownloadStrategyError
+ raise if mirrors.empty?
+ puts "Trying a mirror..."
+ @url = mirrors.shift
+ retry
+ end
+
+ private
+
+ def cask_curl_args
+ default_curl_args.tap do |args|
+ args.concat(user_agent_args)
+ args.concat(cookies_args)
+ args.concat(referer_args)
+ end
+ end
+
+ def default_curl_args
+ [url, "-C", downloaded_size, "-o", temporary_path]
+ end
+
+ def user_agent_args
+ if uri_object.user_agent
+ ["-A", uri_object.user_agent]
+ else
+ []
+ end
+ end
+
+ def cookies_args
+ if uri_object.cookies
+ [
+ "-b",
+ # sort_by is for predictability between Ruby versions
+ uri_object
+ .cookies
+ .sort_by(&:to_s)
+ .map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }
+ .join(";"),
+ ]
+ else
+ []
+ end
+ end
+
+ def referer_args
+ if uri_object.referer
+ ["-e", uri_object.referer]
+ else
+ []
+ end
+ end
+
+ def ext
+ Pathname.new(@url).extname
+ end
+end
+
+class Hbc::CurlPostDownloadStrategy < Hbc::CurlDownloadStrategy
+ def cask_curl_args
+ super
+ default_curl_args.concat(post_args)
+ end
+
+ def post_args
+ if uri_object.data
+ # sort_by is for predictability between Ruby versions
+ uri_object
+ .data
+ .sort_by(&:to_s)
+ .map { |key, value| ["-d", "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"] }
+ .flatten
+ else
+ ["-X", "POST"]
+ end
+ end
+end
+
+class Hbc::SubversionDownloadStrategy < Hbc::HbVCSDownloadStrategy
+ def cache_tag
+ # TODO: pass versions as symbols, support :head here
+ version == "head" ? "svn-HEAD" : "svn"
+ end
+
+ def repo_valid?
+ @clone.join(".svn").directory?
+ end
+
+ def repo_url
+ `svn info '#{@clone}' 2>/dev/null`.strip[%r{^URL: (.+)$}, 1]
+ end
+
+ # super does not provide checks for already-existing downloads
+ def fetch
+ if tarball_path.exist?
+ puts "Already downloaded: #{tarball_path}"
+ else
+ @url = @url.sub(%r{^svn\+}, "") if @url =~ %r{^svn\+http://}
+ ohai "Checking out #{@url}"
+
+ clear_cache unless @url.chomp("/") == repo_url || quiet_system("svn", "switch", @url, @clone)
+
+ if @clone.exist? && !repo_valid?
+ puts "Removing invalid SVN repo from cache"
+ clear_cache
+ end
+
+ case @ref_type
+ when :revision
+ fetch_repo @clone, @url, @ref
+ when :revisions
+ # nil is OK for main_revision, as fetch_repo will then get latest
+ main_revision = @ref[:trunk]
+ fetch_repo @clone, @url, main_revision, true
+
+ fetch_externals do |external_name, external_url|
+ fetch_repo @clone + external_name, external_url, @ref[external_name], true
+ end
+ else
+ fetch_repo @clone, @url
+ end
+ compress
+ end
+ tarball_path
+ end
+
+ # This primary reason for redefining this method is the trust_cert
+ # option, controllable from the Cask definition. We also force
+ # consistent timestamps. The rest of this method is similar to
+ # Homebrew's, but translated to local idiom.
+ def fetch_repo(target, url, revision = uri_object.revision, ignore_externals = false)
+ # Use "svn up" when the repository already exists locally.
+ # This saves on bandwidth and will have a similar effect to verifying the
+ # cache as it will make any changes to get the right revision.
+ svncommand = target.directory? ? "up" : "checkout"
+ args = [svncommand]
+
+ # SVN shipped with XCode 3.1.4 can't force a checkout.
+ args << "--force" unless MacOS.version == :leopard
+
+ # make timestamps consistent for checksumming
+ args.concat(%w[--config-option config:miscellany:use-commit-times=yes])
+
+ if uri_object.trust_cert
+ args << "--trust-server-cert"
+ args << "--non-interactive"
+ end
+
+ args << url unless target.directory?
+ args << target
+ args << "-r" << revision if revision
+ args << "--ignore-externals" if ignore_externals
+ @command.run!("/usr/bin/svn",
+ args: args,
+ print_stderr: false)
+ end
+
+ def tarball_path
+ @tarball_path ||= cached_location.dirname.join(cached_location.basename.to_s + "-#{@cask.version}.tar")
+ end
+
+ def shell_quote(str)
+ # Oh god escaping shell args.
+ # See http://notetoself.vrensk.com/2008/08/escaping-single-quotes-in-ruby-harder-than-expected/
+ str.gsub(%r{\\|'}) { |c| "\\#{c}" }
+ end
+
+ def fetch_externals
+ `svn propget svn:externals '#{shell_quote(@url)}'`.chomp.each_line do |line|
+ name, url = line.split(%r{\s+})
+ yield name, url
+ end
+ end
+
+ private
+
+ # TODO/UPDATE: the tar approach explained below is fragile
+ # against challenges such as case-sensitive filesystems,
+ # and must be re-implemented.
+ #
+ # Seems nutty: we "download" the contents into a tape archive.
+ # Why?
+ # * A single file is tractable to the rest of the Cask toolchain,
+ # * An alternative would be to create a Directory container type.
+ # However, some type of file-serialization trick would still be
+ # needed in order to enable calculating a single checksum over
+ # a directory. So, in that alternative implementation, the
+ # special cases would propagate outside this class, including
+ # the use of tar or equivalent.
+ # * SubversionDownloadStrategy.cached_location is not versioned
+ # * tarball_path provides a needed return value for our overridden
+ # fetch method.
+ # * We can also take this private opportunity to strip files from
+ # the download which are protocol-specific.
+
+ def compress
+ Dir.chdir(cached_location) do
+ @command.run!("/usr/bin/tar",
+ args: ['-s/^\.//', "--exclude", ".svn", "-cf", Pathname.new(tarball_path), "--", "."],
+ print_stderr: false)
+ end
+ clear_cache
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl.rb b/Library/Homebrew/cask/lib/hbc/dsl.rb
new file mode 100644
index 000000000..f39012542
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl.rb
@@ -0,0 +1,283 @@
+require "set"
+
+class Hbc::DSL; end
+
+require "hbc/dsl/appcast"
+require "hbc/dsl/base"
+require "hbc/dsl/caveats"
+require "hbc/dsl/conflicts_with"
+require "hbc/dsl/container"
+require "hbc/dsl/depends_on"
+require "hbc/dsl/gpg"
+require "hbc/dsl/installer"
+require "hbc/dsl/license"
+require "hbc/dsl/postflight"
+require "hbc/dsl/preflight"
+require "hbc/dsl/stanza_proxy"
+require "hbc/dsl/uninstall_postflight"
+require "hbc/dsl/uninstall_preflight"
+require "hbc/dsl/version"
+
+class Hbc::DSL
+ ORDINARY_ARTIFACT_TYPES = [
+ :app,
+ :artifact,
+ :audio_unit_plugin,
+ :binary,
+ :colorpicker,
+ :font,
+ :input_method,
+ :internet_plugin,
+ :pkg,
+ :prefpane,
+ :qlplugin,
+ :screen_saver,
+ :service,
+ :stage_only,
+ :suite,
+ :vst_plugin,
+ :vst3_plugin,
+ ].freeze
+
+ ACTIVATABLE_ARTIFACT_TYPES = ([:installer, *ORDINARY_ARTIFACT_TYPES] - [:stage_only]).freeze
+
+ SPECIAL_ARTIFACT_TYPES = [
+ :uninstall,
+ :zap,
+ ].freeze
+
+ ARTIFACT_BLOCK_TYPES = [
+ :preflight,
+ :postflight,
+ :uninstall_preflight,
+ :uninstall_postflight,
+ ].freeze
+
+ DSL_METHODS = Set.new [
+ :accessibility_access,
+ :appcast,
+ :artifacts,
+ :auto_updates,
+ :caskroom_path,
+ :caveats,
+ :conflicts_with,
+ :container,
+ :depends_on,
+ :gpg,
+ :homepage,
+ :license,
+ :name,
+ :sha256,
+ :staged_path,
+ :url,
+ :version,
+ :appdir,
+ *ORDINARY_ARTIFACT_TYPES,
+ *ACTIVATABLE_ARTIFACT_TYPES,
+ *SPECIAL_ARTIFACT_TYPES,
+ *ARTIFACT_BLOCK_TYPES,
+ ].freeze
+
+ attr_reader :token
+ def initialize(token)
+ @token = token
+ end
+
+ def name(*args)
+ @name ||= []
+ return @name if args.empty?
+ @name.concat(args.flatten)
+ end
+
+ def assert_only_one_stanza_allowed(stanza, arg_given)
+ return unless instance_variable_defined?("@#{stanza}") && arg_given
+ raise Hbc::CaskInvalidError.new(token, "'#{stanza}' stanza may only appear once")
+ end
+
+ def homepage(homepage = nil)
+ assert_only_one_stanza_allowed :homepage, !homepage.nil?
+ @homepage ||= homepage
+ end
+
+ def url(*args, &block)
+ url_given = !args.empty? || block_given?
+ return @url unless url_given
+ assert_only_one_stanza_allowed :url, url_given
+ @url ||= begin
+ Hbc::URL.from(*args, &block)
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, "'url' stanza failed with: #{e}")
+ end
+ end
+
+ def appcast(*args)
+ return @appcast if args.empty?
+ assert_only_one_stanza_allowed :appcast, !args.empty?
+ @appcast ||= begin
+ Hbc::DSL::Appcast.new(*args) unless args.empty?
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ end
+
+ def gpg(*args)
+ return @gpg if args.empty?
+ assert_only_one_stanza_allowed :gpg, !args.empty?
+ @gpg ||= begin
+ Hbc::DSL::Gpg.new(*args) unless args.empty?
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ end
+
+ def container(*args)
+ return @container if args.empty?
+ # TODO: remove this constraint, and instead merge multiple container stanzas
+ assert_only_one_stanza_allowed :container, !args.empty?
+ @container ||= begin
+ Hbc::DSL::Container.new(*args) unless args.empty?
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ # TODO: remove this backward-compatibility section after removing nested_container
+ if @container && @container.nested
+ artifacts[:nested_container] << @container.nested
+ end
+ @container
+ end
+
+ SYMBOLIC_VERSIONS = Set.new [
+ :latest,
+ ]
+
+ def version(arg = nil)
+ return @version if arg.nil?
+ assert_only_one_stanza_allowed :version, !arg.nil?
+ raise Hbc::CaskInvalidError.new(token, "invalid 'version' value: '#{arg.inspect}'") if !arg.is_a?(String) && !SYMBOLIC_VERSIONS.include?(arg)
+ @version ||= Hbc::DSL::Version.new(arg)
+ end
+
+ SYMBOLIC_SHA256S = Set.new [
+ :no_check,
+ ]
+
+ def sha256(arg = nil)
+ return @sha256 if arg.nil?
+ assert_only_one_stanza_allowed :sha256, !arg.nil?
+ raise Hbc::CaskInvalidError.new(token, "invalid 'sha256' value: '#{arg.inspect}'") if !arg.is_a?(String) && !SYMBOLIC_SHA256S.include?(arg)
+ @sha256 ||= arg
+ end
+
+ def license(arg = nil)
+ return @license if arg.nil?
+ assert_only_one_stanza_allowed :license, !arg.nil?
+ @license ||= begin
+ Hbc::DSL::License.new(arg) unless arg.nil?
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ end
+
+ # depends_on uses a load method so that multiple stanzas can be merged
+ def depends_on(*args)
+ return @depends_on if args.empty?
+ @depends_on ||= Hbc::DSL::DependsOn.new
+ begin
+ @depends_on.load(*args) unless args.empty?
+ rescue RuntimeError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ @depends_on
+ end
+
+ def conflicts_with(*args)
+ return @conflicts_with if args.empty?
+ # TODO: remove this constraint, and instead merge multiple conflicts_with stanzas
+ assert_only_one_stanza_allowed :conflicts_with, !args.empty?
+ @conflicts_with ||= begin
+ Hbc::DSL::ConflictsWith.new(*args) unless args.empty?
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+ end
+
+ def artifacts
+ @artifacts ||= Hash.new { |hash, key| hash[key] = Set.new }
+ end
+
+ def caskroom_path
+ @caskroom_path ||= Hbc.caskroom.join(token)
+ end
+
+ def staged_path
+ return @staged_path if @staged_path
+ cask_version = version || :unknown
+ @staged_path = caskroom_path.join(cask_version.to_s)
+ end
+
+ def caveats(*string, &block)
+ @caveats ||= []
+ if block_given?
+ @caveats << Hbc::Caveats.new(block)
+ elsif string.any?
+ @caveats << string.map { |s| s.to_s.sub(%r{[\r\n \t]*\Z}, "\n\n") }
+ end
+ @caveats
+ end
+
+ def accessibility_access(accessibility_access = nil)
+ assert_only_one_stanza_allowed :accessibility_access, !accessibility_access.nil?
+ @accessibility_access ||= accessibility_access
+ end
+
+ def auto_updates(auto_updates = nil)
+ assert_only_one_stanza_allowed :auto_updates, !auto_updates.nil?
+ @auto_updates ||= auto_updates
+ end
+
+ ORDINARY_ARTIFACT_TYPES.each do |type|
+ define_method(type) do |*args|
+ if type == :stage_only && args != [true]
+ raise Hbc::CaskInvalidError.new(token, "'stage_only' takes a single argument: true")
+ end
+ artifacts[type] << args
+ if artifacts.key?(:stage_only) && artifacts.keys.count > 1 &&
+ !(artifacts.keys & ACTIVATABLE_ARTIFACT_TYPES).empty?
+ raise Hbc::CaskInvalidError.new(token, "'stage_only' must be the only activatable artifact")
+ end
+ end
+ end
+
+ def installer(*args)
+ return artifacts[:installer] if args.empty?
+ artifacts[:installer] << Hbc::DSL::Installer.new(*args)
+ raise "'stage_only' must be the only activatable artifact" if artifacts.key?(:stage_only)
+ rescue StandardError => e
+ raise Hbc::CaskInvalidError.new(token, e)
+ end
+
+ SPECIAL_ARTIFACT_TYPES.each do |type|
+ define_method(type) do |*args|
+ artifacts[type].merge(args)
+ end
+ end
+
+ ARTIFACT_BLOCK_TYPES.each do |type|
+ define_method(type) do |&block|
+ artifacts[type] << block
+ end
+ end
+
+ def method_missing(method, *)
+ Hbc::Utils.method_missing_message(method, token)
+ nil
+ end
+
+ def appdir
+ self.class.appdir
+ end
+
+ def self.appdir
+ Hbc.appdir.sub(%r{\/$}, "")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb b/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb
new file mode 100644
index 000000000..b02616cfe
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb
@@ -0,0 +1,17 @@
+class Hbc::DSL::Appcast
+ attr_reader :parameters, :checkpoint
+
+ def initialize(uri, parameters = {})
+ @parameters = parameters
+ @uri = Hbc::UnderscoreSupportingURI.parse(uri)
+ @checkpoint = @parameters[:checkpoint]
+ end
+
+ def to_yaml
+ [@uri, @parameters].to_yaml
+ end
+
+ def to_s
+ @uri.to_s
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/base.rb b/Library/Homebrew/cask/lib/hbc/dsl/base.rb
new file mode 100644
index 000000000..4bf62014e
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/base.rb
@@ -0,0 +1,21 @@
+class Hbc::DSL::Base
+ extend Forwardable
+
+ def initialize(cask, command = Hbc::SystemCommand)
+ @cask = cask
+ @command = command
+ end
+
+ def_delegators :@cask, :token, :version, :caskroom_path, :staged_path, :appdir
+
+ def system_command(executable, options = {})
+ @command.run!(executable, options)
+ end
+
+ def method_missing(method, *)
+ underscored_class = self.class.name.gsub(%r{([[:lower:]])([[:upper:]][[:lower:]])}, '\1_\2').downcase
+ section = underscored_class.downcase.split("::").last
+ Hbc::Utils.method_missing_message(method, @cask.to_s, section)
+ nil
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb b/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb
new file mode 100644
index 000000000..d872f49cb
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb
@@ -0,0 +1,112 @@
+# Caveats DSL. Each method should handle output, following the
+# convention of at least one trailing blank line so that the user
+# can distinguish separate caveats.
+#
+# ( The return value of the last method in the block is also sent
+# to the output by the caller, but that feature is only for the
+# convenience of Cask authors. )
+class Hbc::DSL::Caveats < Hbc::DSL::Base
+ def path_environment_variable(path)
+ puts <<-EOS.undent
+ To use #{@cask}, you may need to add the #{path} directory
+ to your PATH environment variable, eg (for bash shell):
+
+ export PATH=#{path}:"$PATH"
+
+ EOS
+ end
+
+ def zsh_path_helper(path)
+ puts <<-EOS.undent
+ To use #{@cask}, zsh users may need to add the following line to their
+ ~/.zprofile. (Among other effects, #{path} will be added to the
+ PATH environment variable):
+
+ eval `/usr/libexec/path_helper -s`
+
+ EOS
+ end
+
+ def files_in_usr_local
+ localpath = "/usr/local"
+ return unless Hbc.homebrew_prefix.to_s.downcase.start_with?(localpath)
+ puts <<-EOS.undent
+ Cask #{@cask} installs files under "#{localpath}". The presence of such
+ files can cause warnings when running "brew doctor", which is considered
+ to be a bug in Homebrew-Cask.
+
+ EOS
+ end
+
+ def depends_on_java(java_version = "any")
+ if java_version == "any"
+ puts <<-EOS.undent
+ #{@cask} requires Java. You can install the latest version with
+
+ brew cask install java
+
+ EOS
+ elsif java_version.include?("8") || java_version.include?("+")
+ puts <<-EOS.undent
+ #{@cask} requires Java #{java_version}. You can install the latest version with
+
+ brew cask install java
+
+ EOS
+ else
+ puts <<-EOS.undent
+ #{@cask} requires Java #{java_version}. You can install it with
+
+ brew cask install caskroom/versions/java#{java_version}
+
+ EOS
+ end
+ end
+
+ def logout
+ puts <<-EOS.undent
+ You must log out and log back in for the installation of #{@cask}
+ to take effect.
+
+ EOS
+ end
+
+ def reboot
+ puts <<-EOS.undent
+ You must reboot for the installation of #{@cask} to take effect.
+
+ EOS
+ end
+
+ def discontinued
+ puts <<-EOS.undent
+ #{@cask} has been officially discontinued upstream.
+ It may stop working correctly (or at all) in recent versions of macOS.
+
+ EOS
+ end
+
+ def free_license(web_page)
+ puts <<-EOS.undent
+ The vendor offers a free license for #{@cask} at
+ #{web_page}
+
+ EOS
+ end
+
+ def malware(radar_number)
+ puts <<-EOS.undent
+ #{@cask} has been reported to bundle malware. Like with any app, use at your own risk.
+
+ A report has been made to Apple about this app. Their certificate will hopefully be revoked.
+ See the public report at
+ https://openradar.appspot.com/#{radar_number}
+
+ If this report is accurate, please duplicate it at
+ https://bugreport.apple.com/
+ If this report is a mistake, please let us know by opening an issue at
+ https://github.com/caskroom/homebrew-cask/issues/new
+
+ EOS
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb b/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb
new file mode 100644
index 000000000..b2de2cd45
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb
@@ -0,0 +1,30 @@
+class Hbc::DSL::ConflictsWith
+ VALID_KEYS = Set.new [
+ :formula,
+ :cask,
+ :macos,
+ :arch,
+ :x11,
+ :java,
+ ]
+
+ attr_accessor(*VALID_KEYS)
+ attr_accessor :pairs
+
+ def initialize(pairs = {})
+ @pairs = pairs
+ pairs.each do |key, value|
+ raise "invalid conflicts_with key: '#{key.inspect}'" unless VALID_KEYS.include?(key)
+ writer_method = "#{key}=".to_sym
+ send(writer_method, value)
+ end
+ end
+
+ def to_yaml
+ @pairs.to_yaml
+ end
+
+ def to_s
+ @pairs.inspect
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/container.rb b/Library/Homebrew/cask/lib/hbc/dsl/container.rb
new file mode 100644
index 000000000..39f156668
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/container.rb
@@ -0,0 +1,26 @@
+class Hbc::DSL::Container
+ VALID_KEYS = Set.new [
+ :type,
+ :nested,
+ ]
+
+ attr_accessor(*VALID_KEYS)
+ attr_accessor :pairs
+
+ def initialize(pairs = {})
+ @pairs = pairs
+ pairs.each do |key, value|
+ raise "invalid container key: '#{key.inspect}'" unless VALID_KEYS.include?(key)
+ writer_method = "#{key}=".to_sym
+ send(writer_method, value)
+ end
+ end
+
+ def to_yaml
+ @pairs.to_yaml
+ end
+
+ def to_s
+ @pairs.inspect
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb b/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb
new file mode 100644
index 000000000..a7dba3643
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb
@@ -0,0 +1,124 @@
+require "rubygems"
+
+class Hbc::DSL::DependsOn
+ VALID_KEYS = Set.new [
+ :formula,
+ :cask,
+ :macos,
+ :arch,
+ :x11,
+ :java,
+ ].freeze
+
+ VALID_ARCHES = {
+ intel: { type: :intel, bits: [32, 64] },
+ ppc: { type: :ppc, bits: [32, 64] },
+ # specific
+ i386: { type: :intel, bits: 32 },
+ x86_64: { type: :intel, bits: 64 },
+ ppc_7400: { type: :ppc, bits: 32 },
+ ppc_64: { type: :ppc, bits: 64 },
+ }.freeze
+
+ # Intentionally undocumented: catch variant spellings.
+ ARCH_SYNONYMS = {
+ x86_32: :i386,
+ x8632: :i386,
+ x8664: :x86_64,
+ intel_32: :i386,
+ intel32: :i386,
+ intel_64: :x86_64,
+ intel64: :x86_64,
+ amd_64: :x86_64,
+ amd64: :x86_64,
+ ppc7400: :ppc_7400,
+ ppc_32: :ppc_7400,
+ ppc32: :ppc_7400,
+ ppc64: :ppc_64,
+ }.freeze
+
+ attr_accessor :java
+ attr_accessor :pairs
+ attr_reader :arch, :cask, :formula, :macos, :x11
+
+ def initialize
+ @pairs ||= {}
+ end
+
+ def load(pairs = {})
+ pairs.each do |key, value|
+ raise "invalid depends_on key: '#{key.inspect}'" unless VALID_KEYS.include?(key)
+ writer_method = "#{key}=".to_sym
+ @pairs[key] = send(writer_method, value)
+ end
+ end
+
+ def self.coerce_os_release(arg)
+ @macos_symbols ||= MacOS::Version::SYMBOLS
+ @inverted_macos_symbols ||= @macos_symbols.invert
+
+ begin
+ if arg.is_a?(Symbol)
+ Gem::Version.new(@macos_symbols.fetch(arg))
+ elsif arg =~ %r{^\s*:?([a-z]\S+)\s*$}i
+ Gem::Version.new(@macos_symbols.fetch(Regexp.last_match[1].downcase.to_sym))
+ elsif @inverted_macos_symbols.key?(arg)
+ Gem::Version.new(arg)
+ else
+ raise
+ end
+ rescue StandardError
+ raise "invalid 'depends_on macos' value: #{arg.inspect}"
+ end
+ end
+
+ def formula=(*arg)
+ @formula ||= []
+ @formula.concat(Array(*arg))
+ end
+
+ def cask=(*arg)
+ @cask ||= []
+ @cask.concat(Array(*arg))
+ end
+
+ def macos=(*arg)
+ @macos ||= []
+ macos = if arg.count == 1 && arg.first =~ %r{^\s*(<|>|[=<>]=)\s*(\S+)\s*$}
+ raise "'depends_on macos' comparison expressions cannot be combined" unless @macos.empty?
+ operator = Regexp.last_match[1].to_sym
+ release = self.class.coerce_os_release(Regexp.last_match[2])
+ [[operator, release]]
+ else
+ raise "'depends_on macos' comparison expressions cannot be combined" if @macos.first.is_a?(Symbol)
+ Array(*arg).map { |elt|
+ self.class.coerce_os_release(elt)
+ }.sort
+ end
+ @macos.concat(macos)
+ end
+
+ def arch=(*arg)
+ @arch ||= []
+ arches = Array(*arg).map { |elt|
+ elt = elt.to_s.downcase.sub(%r{^:}, "").tr("-", "_").to_sym
+ ARCH_SYNONYMS.key?(elt) ? ARCH_SYNONYMS[elt] : elt
+ }
+ invalid_arches = arches - VALID_ARCHES.keys
+ raise "invalid 'depends_on arch' values: #{invalid_arches.inspect}" unless invalid_arches.empty?
+ @arch.concat(arches.map { |arch| VALID_ARCHES[arch] })
+ end
+
+ def x11=(arg)
+ raise "invalid 'depends_on x11' value: #{arg.inspect}" unless [true, false].include?(arg)
+ @x11 = arg
+ end
+
+ def to_yaml
+ @pairs.to_yaml
+ end
+
+ def to_s
+ @pairs.inspect
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb b/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb
new file mode 100644
index 000000000..9496a8c05
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb
@@ -0,0 +1,43 @@
+class Hbc::DSL::Gpg
+ KEY_PARAMETERS = Set.new [
+ :key_id,
+ :key_url,
+ ]
+
+ VALID_PARAMETERS = Set.new []
+ VALID_PARAMETERS.merge KEY_PARAMETERS
+
+ attr_accessor(*VALID_PARAMETERS)
+ attr_accessor :signature
+
+ def initialize(signature, parameters = {})
+ @parameters = parameters
+ @signature = Hbc::UnderscoreSupportingURI.parse(signature)
+ parameters.each do |hkey, hvalue|
+ raise "invalid 'gpg' parameter: '#{hkey.inspect}'" unless VALID_PARAMETERS.include?(hkey)
+ writer_method = "#{hkey}=".to_sym
+ hvalue = Hbc::UnderscoreSupportingURI.parse(hvalue) if hkey == :key_url
+ valid_id?(hvalue) if hkey == :key_id
+ send(writer_method, hvalue)
+ end
+ return if KEY_PARAMETERS.intersection(parameters.keys).length == 1
+ raise "'gpg' stanza must include exactly one of: '#{KEY_PARAMETERS.to_a}'"
+ end
+
+ def valid_id?(id)
+ legal_lengths = Set.new [8, 16, 40]
+ is_valid = id.is_a?(String) && legal_lengths.include?(id.length) && id[%r{^[0-9a-f]+$}i]
+ raise "invalid ':key_id' value: '#{id.inspect}'" unless is_valid
+
+ is_valid
+ end
+
+ def to_yaml
+ # bug, :key_url value is not represented as an instance of Hbc::UnderscoreSupportingURI
+ [@signature, @parameters].to_yaml
+ end
+
+ def to_s
+ @signature.to_s
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/installer.rb b/Library/Homebrew/cask/lib/hbc/dsl/installer.rb
new file mode 100644
index 000000000..74b4b3a91
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/installer.rb
@@ -0,0 +1,28 @@
+class Hbc::DSL::Installer
+ VALID_KEYS = Set.new [
+ :manual,
+ :script,
+ ]
+
+ attr_accessor(*VALID_KEYS)
+
+ def initialize(*parameters)
+ raise Hbc::CaskInvalidError.new(token, "'installer' stanza requires an argument") if parameters.empty?
+ parameters = {}.merge(*parameters)
+ if parameters.key?(:script) && !parameters[:script].respond_to?(:key?)
+ if parameters.key?(:executable)
+ raise Hbc::CaskInvalidError.new(token, "'installer' stanza gave arguments for both :script and :executable")
+ end
+ parameters[:executable] = parameters[:script]
+ parameters.delete(:script)
+ parameters = { script: parameters }
+ end
+ unless parameters.keys.length == 1
+ raise "invalid 'installer' stanza: only one of #{VALID_KEYS.inspect} is permitted"
+ end
+ key = parameters.keys.first
+ raise "invalid 'installer' stanza key: '#{key.inspect}'" unless VALID_KEYS.include?(key)
+ writer_method = "#{key}=".to_sym
+ send(writer_method, parameters[key])
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/license.rb b/Library/Homebrew/cask/lib/hbc/dsl/license.rb
new file mode 100644
index 000000000..5f607c268
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/license.rb
@@ -0,0 +1,66 @@
+class Hbc::DSL::License
+ # a generic category can always be given as a license, so
+ # category names should be given as both key and value
+ VALID_LICENSES = {
+ # license category
+ unknown: :unknown,
+
+ other: :other,
+
+ closed: :closed,
+ commercial: :closed,
+ gratis: :closed,
+ freemium: :closed,
+
+ oss: :oss,
+ affero: :oss,
+ apache: :oss,
+ arphic: :oss,
+ artistic: :oss,
+ bsd: :oss,
+ cc: :oss,
+ eclipse: :oss,
+ gpl: :oss,
+ isc: :oss,
+ lppl: :oss,
+ ncsa: :oss,
+ mit: :oss,
+ mpl: :oss,
+ ofl: :oss,
+ public_domain: :oss,
+ ubuntu_font: :oss,
+ x11: :oss,
+ }.freeze
+
+ DEFAULT_LICENSE = :unknown
+ DEFAULT_CATEGORY = VALID_LICENSES[DEFAULT_LICENSE]
+
+ attr_reader :value
+
+ def self.check_constants
+ categories = Set.new(VALID_LICENSES.values)
+ categories.each do |cat|
+ next if VALID_LICENSES.key?(cat)
+ raise "license category is not a value: '#{@cat.inspect}'"
+ end
+ end
+
+ def self.category(license)
+ VALID_LICENSES.fetch(license, DEFAULT_CATEGORY)
+ end
+
+ def initialize(arg)
+ @value = arg
+ @value = DEFAULT_LICENSE if @value.nil?
+ return if VALID_LICENSES.key?(@value)
+ raise "invalid license value: '#{@value.inspect}'"
+ end
+
+ def category
+ self.class.category(@value)
+ end
+
+ def to_s
+ @value.inspect
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb
new file mode 100644
index 000000000..321c7e81a
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb
@@ -0,0 +1,9 @@
+require "hbc/staged"
+
+class Hbc::DSL::Postflight < Hbc::DSL::Base
+ include Hbc::Staged
+
+ def suppress_move_to_applications(options = {})
+ # TODO: Remove from all casks because it is no longer needed
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb
new file mode 100644
index 000000000..a0d53c69c
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb
@@ -0,0 +1,3 @@
+class Hbc::DSL::Preflight < Hbc::DSL::Base
+ include Hbc::Staged
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb b/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb
new file mode 100644
index 000000000..02c76fb27
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb
@@ -0,0 +1,37 @@
+class Hbc::DSL::StanzaProxy
+ attr_reader :type
+
+ def self.once(type)
+ resolved = nil
+ new(type) { resolved ||= yield }
+ end
+
+ def initialize(type, &resolver)
+ @type = type
+ @resolver = resolver
+ end
+
+ def proxy?
+ true
+ end
+
+ def to_s
+ @resolver.call.to_s
+ end
+
+ # Serialization for dumpcask
+ def encode_with(coder)
+ coder["type"] = type
+ coder["resolved"] = @resolver.call
+ end
+
+ def respond_to?(symbol, include_private = false)
+ return true if %i{encode_with proxy? to_s type}.include?(symbol)
+ return false if symbol == :to_ary
+ @resolver.call.respond_to?(symbol, include_private)
+ end
+
+ def method_missing(symbol, *args)
+ @resolver.call.send(symbol, *args)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb
new file mode 100644
index 000000000..bd8777ca7
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb
@@ -0,0 +1,2 @@
+class Hbc::DSL::UninstallPostflight < Hbc::DSL::Base
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb
new file mode 100644
index 000000000..994151c25
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb
@@ -0,0 +1,5 @@
+require "hbc/staged"
+
+class Hbc::DSL::UninstallPreflight < Hbc::DSL::Base
+ include Hbc::Staged
+end
diff --git a/Library/Homebrew/cask/lib/hbc/dsl/version.rb b/Library/Homebrew/cask/lib/hbc/dsl/version.rb
new file mode 100644
index 000000000..e01e67ea2
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/dsl/version.rb
@@ -0,0 +1,111 @@
+class Hbc::DSL::Version < ::String
+ DIVIDERS = {
+ "." => :dots,
+ "-" => :hyphens,
+ "_" => :underscores,
+ "/" => :slashes,
+ }.freeze
+
+ DIVIDER_REGEX = %r{(#{DIVIDERS.keys.map { |v| Regexp.quote(v) }.join('|')})}
+
+ MAJOR_MINOR_PATCH_REGEX = %r{^(\d+)(?:\.(\d+)(?:\.(\d+))?)?}
+
+ class << self
+ private
+
+ def define_divider_methods(divider)
+ define_divider_deletion_method(divider)
+ define_divider_conversion_methods(divider)
+ end
+
+ def define_divider_deletion_method(divider)
+ method_name = deletion_method_name(divider)
+ define_method(method_name) do
+ version { delete(divider) }
+ end
+ end
+
+ def deletion_method_name(divider)
+ "no_#{DIVIDERS[divider]}"
+ end
+
+ def define_divider_conversion_methods(left_divider)
+ (DIVIDERS.keys - [left_divider]).each do |right_divider|
+ define_divider_conversion_method(left_divider, right_divider)
+ end
+ end
+
+ def define_divider_conversion_method(left_divider, right_divider)
+ method_name = conversion_method_name(left_divider, right_divider)
+ define_method(method_name) do
+ version { gsub(left_divider, right_divider) }
+ end
+ end
+
+ def conversion_method_name(left_divider, right_divider)
+ "#{DIVIDERS[left_divider]}_to_#{DIVIDERS[right_divider]}"
+ end
+ end
+
+ DIVIDERS.keys.each do |divider|
+ define_divider_methods(divider)
+ end
+
+ attr_reader :raw_version
+
+ def initialize(raw_version)
+ @raw_version = raw_version
+ super(raw_version.to_s)
+ end
+
+ def latest?
+ to_s == "latest"
+ end
+
+ def major
+ version { slice(MAJOR_MINOR_PATCH_REGEX, 1) }
+ end
+
+ def minor
+ version { slice(MAJOR_MINOR_PATCH_REGEX, 2) }
+ end
+
+ def patch
+ version { slice(MAJOR_MINOR_PATCH_REGEX, 3) }
+ end
+
+ def major_minor
+ version { [major, minor].reject(&:empty?).join(".") }
+ end
+
+ def major_minor_patch
+ version { [major, minor, patch].reject(&:empty?).join(".") }
+ end
+
+ def before_comma
+ version { split(",", 2)[0] }
+ end
+
+ def after_comma
+ version { split(",", 2)[1] }
+ end
+
+ def before_colon
+ version { split(":", 2)[0] }
+ end
+
+ def after_colon
+ version { split(":", 2)[1] }
+ end
+
+ def no_dividers
+ version { gsub(DIVIDER_REGEX, "") }
+ end
+
+ private
+
+ def version
+ return self if empty? || latest?
+ self.class.new(yield)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/exceptions.rb b/Library/Homebrew/cask/lib/hbc/exceptions.rb
new file mode 100644
index 000000000..8813aaedf
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/exceptions.rb
@@ -0,0 +1,146 @@
+class Hbc::CaskError < RuntimeError; end
+
+class Hbc::AbstractCaskErrorWithToken < Hbc::CaskError
+ attr_reader :token
+
+ def initialize(token)
+ @token = token
+ end
+end
+
+class Hbc::CaskNotInstalledError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ "#{token} is not installed"
+ end
+end
+
+class Hbc::CaskUnavailableError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ "No available Cask for #{token}"
+ end
+end
+
+class Hbc::CaskAlreadyCreatedError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ %Q{A Cask for #{token} already exists. Run "brew cask cat #{token}" to see it.}
+ end
+end
+
+class Hbc::CaskAlreadyInstalledError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ %Q{A Cask for #{token} is already installed. Add the "--force" option to force re-install.}
+ end
+end
+
+class Hbc::CaskAutoUpdatesError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ %Q{A Cask for #{token} is already installed and using auto-updates. Add the "--force" option to force re-install.}
+ end
+end
+
+class Hbc::CaskCommandFailedError < Hbc::CaskError
+ def initialize(cmd, stdout, stderr, status)
+ @cmd = cmd
+ @stdout = stdout
+ @stderr = stderr
+ @status = status
+ end
+
+ def to_s
+ <<-EOS
+Command failed to execute!
+
+==> Failed command:
+#{@cmd}
+
+==> Standard Output of failed command:
+#{@stdout}
+
+==> Standard Error of failed command:
+#{@stderr}
+
+==> Exit status of failed command:
+#{@status.inspect}
+ EOS
+ end
+end
+
+class Hbc::CaskX11DependencyError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ <<-EOS.undent
+ #{token} requires XQuartz/X11, which can be installed via homebrew-cask by
+
+ brew cask install xquartz
+
+ or manually, by downloading the package from
+
+ https://www.xquartz.org/
+ EOS
+ end
+end
+
+class Hbc::CaskCyclicCaskDependencyError < Hbc::AbstractCaskErrorWithToken
+ def to_s
+ "Cask '#{token}' includes cyclic dependencies on other Casks and could not be installed."
+ end
+end
+
+class Hbc::CaskUnspecifiedError < Hbc::CaskError
+ def to_s
+ "This command requires a Cask token"
+ end
+end
+
+class Hbc::CaskInvalidError < Hbc::AbstractCaskErrorWithToken
+ attr_reader :submsg
+ def initialize(token, *submsg)
+ super(token)
+ @submsg = submsg.join(" ")
+ end
+
+ def to_s
+ "Cask '#{token}' definition is invalid" + (!submsg.empty? ? ": #{submsg}" : "")
+ end
+end
+
+class Hbc::CaskTokenDoesNotMatchError < Hbc::CaskInvalidError
+ def initialize(token, header_token)
+ super(token, "Bad header line: '#{header_token}' does not match file name")
+ end
+end
+
+class Hbc::CaskSha256MissingError < ArgumentError
+end
+
+class Hbc::CaskSha256MismatchError < RuntimeError
+ attr_reader :path, :expected, :actual
+ def initialize(path, expected, actual)
+ @path = path
+ @expected = expected
+ @actual = actual
+ end
+
+ def to_s
+ <<-EOS.undent
+ sha256 mismatch
+ Expected: #{expected}
+ Actual: #{actual}
+ File: #{path}
+ To retry an incomplete download, remove the file above.
+ EOS
+ end
+end
+
+class Hbc::CaskNoShasumError < Hbc::CaskError
+ attr_reader :token
+ def initialize(token)
+ @token = token
+ end
+
+ def to_s
+ <<-EOS.undent
+ Cask '#{token}' does not have a sha256 checksum defined and was not installed.
+ This means you have the "--require-sha" option set, perhaps in your HOMEBREW_CASK_OPTS.
+ EOS
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/extend.rb b/Library/Homebrew/cask/lib/hbc/extend.rb
new file mode 100644
index 000000000..629c53468
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend.rb
@@ -0,0 +1,6 @@
+# monkeypatching
+require "hbc/extend/hash"
+require "hbc/extend/io"
+require "hbc/extend/optparse"
+require "hbc/extend/pathname"
+require "hbc/extend/string"
diff --git a/Library/Homebrew/cask/lib/hbc/extend/hash.rb b/Library/Homebrew/cask/lib/hbc/extend/hash.rb
new file mode 100644
index 000000000..dc28cfb29
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend/hash.rb
@@ -0,0 +1,7 @@
+class Hash
+ def assert_valid_keys(*valid_keys)
+ unknown_keys = keys - valid_keys
+ return if unknown_keys.empty?
+ raise Hbc::CaskError, %Q{Unknown keys: #{unknown_keys.inspect}. Running "#{UPDATE_CMD}" will likely fix it.}
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/extend/io.rb b/Library/Homebrew/cask/lib/hbc/extend/io.rb
new file mode 100644
index 000000000..1357293cd
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend/io.rb
@@ -0,0 +1,10 @@
+class IO
+ def readline_nonblock(sep = $INPUT_RECORD_SEPARATOR)
+ buffer = ""
+ buffer.concat(read_nonblock(1)) while buffer[-1] != sep
+ buffer
+ rescue IO::WaitReadable, EOFError => e
+ raise e if buffer.empty?
+ buffer
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/extend/optparse.rb b/Library/Homebrew/cask/lib/hbc/extend/optparse.rb
new file mode 100644
index 000000000..784d6d699
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend/optparse.rb
@@ -0,0 +1,6 @@
+require "optparse"
+require "pathname"
+
+OptionParser.accept Pathname do |path|
+ Pathname(path).expand_path if path
+end
diff --git a/Library/Homebrew/cask/lib/hbc/extend/pathname.rb b/Library/Homebrew/cask/lib/hbc/extend/pathname.rb
new file mode 100644
index 000000000..598a99cd2
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend/pathname.rb
@@ -0,0 +1,19 @@
+require "pathname"
+
+class Pathname
+ # extended to support common double extensions
+ def extname(path = to_s)
+ %r{(\.(dmg|tar|cpio|pax)\.(gz|bz2|lz|xz|Z|zip))$} =~ path
+ return Regexp.last_match(1) if Regexp.last_match(1)
+ File.extname(path)
+ end
+
+ # https://bugs.ruby-lang.org/issues/9915
+ if RUBY_VERSION == "2.0.0"
+ prepend Module.new {
+ def inspect
+ super.force_encoding(@path.encoding)
+ end
+ }
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/extend/string.rb b/Library/Homebrew/cask/lib/hbc/extend/string.rb
new file mode 100644
index 000000000..38c284194
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/extend/string.rb
@@ -0,0 +1,5 @@
+class String
+ def undent
+ gsub(%r{^.{#{(slice(%r{^ +}) || '').length}}}, "")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/fetcher.rb b/Library/Homebrew/cask/lib/hbc/fetcher.rb
new file mode 100644
index 000000000..44a898ce0
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/fetcher.rb
@@ -0,0 +1,22 @@
+require "open3"
+
+class Hbc::Fetcher
+ TIMEOUT = 10
+
+ def self.head(url)
+ if url.to_s =~ %r{googlecode}
+ googlecode_fake_head(url)
+ else
+ Hbc::SystemCommand.run("/usr/bin/curl",
+ args: ["--max-time", TIMEOUT, "--silent", "--location", "--head", url]).stdout
+ end
+ end
+
+ # google code does not properly respond to HTTP HEAD requests, like a jerk
+ # this fakes a HEAD by doing a GET, taking the first 20 lines, then running away
+ def self.googlecode_fake_head(url)
+ command = "curl --max-time #{TIMEOUT} --verbose --location '#{url}' | head -n 20 > /dev/null"
+ stderr = Open3.capture3(command)[1]
+ stderr.split("\n").grep(%r{^< }).map { |line| line.sub(%r{^< }, "") }.join("\n")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/installer.rb b/Library/Homebrew/cask/lib/hbc/installer.rb
new file mode 100644
index 000000000..8e55b8a99
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/installer.rb
@@ -0,0 +1,343 @@
+require "rubygems"
+
+require "extend/pathname"
+require "hbc/cask_dependencies"
+require "hbc/staged"
+require "hbc/verify"
+
+class Hbc::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 Hbc::Staged
+ include Hbc::Verify
+
+ attr_reader :force, :skip_cask_deps
+
+ PERSISTENT_METADATA_SUBDIRS = ["gpg"].freeze
+
+ def initialize(cask, command: Hbc::SystemCommand, force: false, skip_cask_deps: false, require_sha: false)
+ @cask = cask
+ @command = command
+ @force = force
+ @skip_cask_deps = skip_cask_deps
+ @require_sha = require_sha
+ 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
+ odebug "Hbc::Installer.install"
+
+ if @cask.installed? && @cask.auto_updates && !force
+ raise Hbc::CaskAutoUpdatesError, @cask
+ end
+
+ raise Hbc::CaskAlreadyInstalledError, @cask if @cask.installed? && !force
+
+ print_caveats
+
+ begin
+ satisfy_dependencies
+ verify_has_sha if @require_sha && !@force
+ download
+ verify
+ extract_primary_container
+ install_artifacts
+ save_caskfile
+ enable_accessibility_access
+ rescue StandardError => e
+ purge_versioned_files
+ raise e
+ end
+
+ puts summary
+ end
+
+ def summary
+ s = if MacOS.version >= :lion && !ENV["HOMEBREW_NO_EMOJI"]
+ (ENV["HOMEBREW_INSTALL_BADGE"] || "\xf0\x9f\x8d\xba") + " "
+ else
+ "#{Hbc::Utils::Tty.blue.bold}==>#{Hbc::Utils::Tty.reset.bold} Success!#{Hbc::Utils::Tty.reset} "
+ end
+ s << "#{@cask} was successfully installed!"
+ end
+
+ def download
+ odebug "Downloading"
+ download = Hbc::Download.new(@cask, force: false)
+ @downloaded_path = download.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 Hbc::CaskNoShasumError, @cask
+ end
+
+ def verify
+ Hbc::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
+ Hbc::Container.from_type(@cask.container.type)
+ else
+ Hbc::Container.for_path(@downloaded_path, @command)
+ end
+ unless container
+ raise Hbc::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
+ odebug "Installing artifacts"
+ artifacts = Hbc::Artifact.for_cask(@cask)
+ odebug "#{artifacts.length} artifact/s defined", artifacts
+ artifacts.each do |artifact|
+ odebug "Installing artifact of class #{artifact}"
+ options = { command: @command, force: force }
+ artifact.new(@cask, options).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
+ cask_dependencies unless skip_cask_deps
+ 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 Hbc::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 Hbc::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 Hbc::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? { |arch|
+ arch[:type] == @current_arch[:type] &&
+ Array(arch[:bits]).include?(@current_arch[:bits])
+ }
+ raise Hbc::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 Hbc::CaskX11DependencyError, @cask.token if Hbc.x11_libpng.select(&:exist?).empty?
+ 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(Hbc.homebrew_executable,
+ args: ["list", "--versions", dep_name],
+ print_stderr: false).stdout.include?(dep_name)
+ if installed
+ puts "already installed"
+ else
+ @command.run!(Hbc.homebrew_executable,
+ 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 = Hbc::CaskDependencies.new(@cask)
+ deps.sorted.each do |dep_token|
+ puts "#{dep_token} ..."
+ dep = Hbc.load(dep_token)
+ if dep.installed?
+ puts "already installed"
+ else
+ Hbc::Installer.new(dep, force: false, 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)
+ else
+ @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)
+ 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: [
+ Hbc.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 macOS. See System Preferences.
+ EOS
+ end
+ end
+
+ def save_caskfile
+ timestamp = :now
+ create = true
+ savedir = @cask.metadata_subdir("Casks", timestamp, create)
+ if Dir.entries(savedir).size > 2
+ # should not happen
+ raise Hbc::CaskAlreadyInstalledError, @cask unless force
+ savedir.rmtree
+ FileUtils.mkdir_p savedir
+ end
+ FileUtils.copy(@cask.sourcefile_path, savedir) if @cask.sourcefile_path
+ end
+
+ def uninstall
+ odebug "Hbc::Installer.uninstall"
+ disable_accessibility_access
+ uninstall_artifacts
+ purge_versioned_files
+ purge_caskroom_path if force
+ end
+
+ def uninstall_artifacts
+ odebug "Un-installing artifacts"
+ artifacts = Hbc::Artifact.for_cask(@cask)
+ odebug "#{artifacts.length} artifact/s defined", artifacts
+ artifacts.each do |artifact|
+ odebug "Un-installing artifact of class #{artifact}"
+ options = { command: @command, force: force }
+ artifact.new(@cask, options).uninstall_phase
+ end
+ end
+
+ def zap
+ ohai %Q{Implied "brew cask uninstall #{@cask}"}
+ uninstall_artifacts
+ if Hbc::Artifact::Zap.me?(@cask)
+ ohai "Dispatching zap stanza"
+ Hbc::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)
+ Hbc::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_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
+ end
+ end
+ @cask.metadata_versioned_container_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
diff --git a/Library/Homebrew/cask/lib/hbc/locations.rb b/Library/Homebrew/cask/lib/hbc/locations.rb
new file mode 100644
index 000000000..e4d88f318
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/locations.rb
@@ -0,0 +1,196 @@
+module Hbc::Locations
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def legacy_caskroom
+ @legacy_caskroom ||= Pathname.new("/opt/homebrew-cask/Caskroom")
+ end
+
+ def default_caskroom
+ @default_caskroom ||= homebrew_repository.join("Caskroom")
+ end
+
+ def caskroom
+ @caskroom ||= begin
+ if Hbc::Utils.path_occupied?(legacy_caskroom)
+ opoo <<-EOS.undent
+ The default Caskroom location has moved to #{default_caskroom}.
+
+ Please migrate your Casks to the new location and delete #{legacy_caskroom},
+ or if you would like to keep your Caskroom at #{legacy_caskroom}, add the
+ following to your HOMEBREW_CASK_OPTS:
+
+ --caskroom=#{legacy_caskroom}
+
+ For more details on each of those options, see https://github.com/caskroom/homebrew-cask/issues/21913.
+ EOS
+ legacy_caskroom
+ else
+ default_caskroom
+ end
+ end
+ end
+
+ def caskroom=(caskroom)
+ @caskroom = caskroom
+ end
+
+ def legacy_cache
+ @legacy_cache ||= homebrew_cache.join("Casks")
+ end
+
+ def cache
+ @cache ||= homebrew_cache.join("Cask")
+ end
+
+ attr_writer :appdir
+
+ def appdir
+ @appdir ||= Pathname.new("/Applications").expand_path
+ end
+
+ attr_writer :prefpanedir
+
+ def prefpanedir
+ @prefpanedir ||= Pathname.new("~/Library/PreferencePanes").expand_path
+ end
+
+ attr_writer :qlplugindir
+
+ def qlplugindir
+ @qlplugindir ||= Pathname.new("~/Library/QuickLook").expand_path
+ end
+
+ attr_writer :fontdir
+
+ def fontdir
+ @fontdir ||= Pathname.new("~/Library/Fonts").expand_path
+ end
+
+ attr_writer :colorpickerdir
+
+ def colorpickerdir
+ @colorpickerdir ||= Pathname.new("~/Library/ColorPickers").expand_path
+ end
+
+ attr_writer :servicedir
+
+ def servicedir
+ @servicedir ||= Pathname.new("~/Library/Services").expand_path
+ end
+
+ attr_writer :binarydir
+
+ def binarydir
+ @binarydir ||= homebrew_prefix.join("bin")
+ end
+
+ attr_writer :input_methoddir
+
+ def input_methoddir
+ @input_methoddir ||= Pathname.new("~/Library/Input Methods").expand_path
+ end
+
+ attr_writer :internet_plugindir
+
+ def internet_plugindir
+ @internet_plugindir ||= Pathname.new("~/Library/Internet Plug-Ins").expand_path
+ end
+
+ attr_writer :audio_unit_plugindir
+
+ def audio_unit_plugindir
+ @audio_unit_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/Components").expand_path
+ end
+
+ attr_writer :vst_plugindir
+
+ def vst_plugindir
+ @vst_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/VST").expand_path
+ end
+
+ attr_writer :vst3_plugindir
+
+ def vst3_plugindir
+ @vst3_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/VST3").expand_path
+ end
+
+ attr_writer :screen_saverdir
+
+ def screen_saverdir
+ @screen_saverdir ||= Pathname.new("~/Library/Screen Savers").expand_path
+ end
+
+ attr_writer :default_tap
+
+ def default_tap
+ @default_tap ||= Tap.fetch("caskroom/homebrew-cask")
+ end
+
+ def path(query)
+ query = query.sub(%r{\.rb$}i, "")
+ token_with_tap = if query.include?("/")
+ query
+ else
+ all_tokens.detect do |tap_and_token|
+ tap_and_token.split("/")[2] == query
+ end
+ end
+
+ if token_with_tap
+ user, repo, token = token_with_tap.split("/")
+ Tap.fetch(user, repo).cask_dir.join("#{token}.rb")
+ else
+ default_tap.cask_dir.join("#{query}.rb")
+ end
+ end
+
+ def tcc_db
+ @tcc_db ||= Pathname.new("/Library/Application Support/com.apple.TCC/TCC.db")
+ end
+
+ def pre_mavericks_accessibility_dotfile
+ @pre_mavericks_accessibility_dotfile ||= Pathname.new("/private/var/db/.AccessibilityAPIEnabled")
+ end
+
+ def x11_executable
+ @x11_executable ||= Pathname.new("/usr/X11/bin/X")
+ end
+
+ def x11_libpng
+ @x11_libpng ||= [Pathname.new("/opt/X11/lib/libpng.dylib"), Pathname.new("/usr/X11/lib/libpng.dylib")]
+ end
+
+ def homebrew_cache
+ @homebrew_cache ||= HOMEBREW_CACHE
+ end
+
+ def homebrew_cache=(path)
+ @homebrew_cache = path ? Pathname.new(path) : path
+ end
+
+ def homebrew_executable
+ @homebrew_executable ||= HOMEBREW_BREW_FILE
+ end
+
+ def homebrew_prefix
+ # where Homebrew links
+ @homebrew_prefix ||= HOMEBREW_PREFIX
+ end
+
+ def homebrew_prefix=(path)
+ @homebrew_prefix = path ? Pathname.new(path) : path
+ end
+
+ def homebrew_repository
+ # where Homebrew's .git dir is found
+ @homebrew_repository ||= HOMEBREW_REPOSITORY
+ end
+
+ def homebrew_repository=(path)
+ @homebrew_repository = path ? Pathname.new(path) : path
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/macos.rb b/Library/Homebrew/cask/lib/hbc/macos.rb
new file mode 100644
index 000000000..46047f413
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/macos.rb
@@ -0,0 +1,378 @@
+require "set"
+
+require "os/mac/version"
+
+module OS::Mac
+ SYSTEM_DIRS = [
+ "/",
+ "/Applications",
+ "/Applications/Utilities",
+ "/Incompatible Software",
+ "/Library",
+ "/Library/Application Support",
+ "/Library/Audio",
+ "/Library/Caches",
+ "/Library/ColorPickers",
+ "/Library/ColorSync",
+ "/Library/Components",
+ "/Library/Compositions",
+ "/Library/Contextual Menu Items",
+ "/Library/CoreMediaIO",
+ "/Library/Desktop Pictures",
+ "/Library/Developer",
+ "/Library/Dictionaries",
+ "/Library/DirectoryServices",
+ "/Library/Documentation",
+ "/Library/Extensions",
+ "/Library/Filesystems",
+ "/Library/Fonts",
+ "/Library/Frameworks",
+ "/Library/Graphics",
+ "/Library/Image Capture",
+ "/Library/Input Methods",
+ "/Library/Internet Plug-Ins",
+ "/Library/Java",
+ "/Library/Keyboard Layouts",
+ "/Library/Keychains",
+ "/Library/LaunchAgents",
+ "/Library/LaunchDaemons",
+ "/Library/Logs",
+ "/Library/Messages",
+ "/Library/Modem Scripts",
+ "/Library/OpenDirectory",
+ "/Library/PDF Services",
+ "/Library/Perl",
+ "/Library/PreferencePanes",
+ "/Library/Preferences",
+ "/Library/Printers",
+ "/Library/PrivilegedHelperTools",
+ "/Library/Python",
+ "/Library/QuickLook",
+ "/Library/QuickTime",
+ "/Library/Receipts",
+ "/Library/Ruby",
+ "/Library/Sandbox",
+ "/Library/Screen Savers",
+ "/Library/ScriptingAdditions",
+ "/Library/Scripts",
+ "/Library/Security",
+ "/Library/Speech",
+ "/Library/Spelling",
+ "/Library/Spotlight",
+ "/Library/StartupItems",
+ "/Library/SystemProfiler",
+ "/Library/Updates",
+ "/Library/User Pictures",
+ "/Library/Video",
+ "/Library/WebServer",
+ "/Library/Widgets",
+ "/Library/iTunes",
+ "/Network",
+ "/System",
+ "/System/Library",
+ "/System/Library/Accessibility",
+ "/System/Library/Accounts",
+ "/System/Library/Address Book Plug-Ins",
+ "/System/Library/Assistant",
+ "/System/Library/Automator",
+ "/System/Library/BridgeSupport",
+ "/System/Library/Caches",
+ "/System/Library/ColorPickers",
+ "/System/Library/ColorSync",
+ "/System/Library/Colors",
+ "/System/Library/Components",
+ "/System/Library/Compositions",
+ "/System/Library/CoreServices",
+ "/System/Library/DTDs",
+ "/System/Library/DirectoryServices",
+ "/System/Library/Displays",
+ "/System/Library/Extensions",
+ "/System/Library/Filesystems",
+ "/System/Library/Filters",
+ "/System/Library/Fonts",
+ "/System/Library/Frameworks",
+ "/System/Library/Graphics",
+ "/System/Library/IdentityServices",
+ "/System/Library/Image Capture",
+ "/System/Library/Input Methods",
+ "/System/Library/InternetAccounts",
+ "/System/Library/Java",
+ "/System/Library/KerberosPlugins",
+ "/System/Library/Keyboard Layouts",
+ "/System/Library/Keychains",
+ "/System/Library/LaunchAgents",
+ "/System/Library/LaunchDaemons",
+ "/System/Library/LinguisticData",
+ "/System/Library/LocationBundles",
+ "/System/Library/LoginPlugins",
+ "/System/Library/Messages",
+ "/System/Library/Metadata",
+ "/System/Library/MonitorPanels",
+ "/System/Library/OpenDirectory",
+ "/System/Library/OpenSSL",
+ "/System/Library/Password Server Filters",
+ "/System/Library/PerformanceMetrics",
+ "/System/Library/Perl",
+ "/System/Library/PreferencePanes",
+ "/System/Library/Printers",
+ "/System/Library/PrivateFrameworks",
+ "/System/Library/QuickLook",
+ "/System/Library/QuickTime",
+ "/System/Library/QuickTimeJava",
+ "/System/Library/Recents",
+ "/System/Library/SDKSettingsPlist",
+ "/System/Library/Sandbox",
+ "/System/Library/Screen Savers",
+ "/System/Library/ScreenReader",
+ "/System/Library/ScriptingAdditions",
+ "/System/Library/ScriptingDefinitions",
+ "/System/Library/Security",
+ "/System/Library/Services",
+ "/System/Library/Sounds",
+ "/System/Library/Speech",
+ "/System/Library/Spelling",
+ "/System/Library/Spotlight",
+ "/System/Library/StartupItems",
+ "/System/Library/SyncServices",
+ "/System/Library/SystemConfiguration",
+ "/System/Library/SystemProfiler",
+ "/System/Library/Tcl",
+ "/System/Library/TextEncodings",
+ "/System/Library/User Template",
+ "/System/Library/UserEventPlugins",
+ "/System/Library/Video",
+ "/System/Library/WidgetResources",
+ "/User Information",
+ "/Users",
+ "/Volumes",
+ "/bin",
+ "/boot",
+ "/cores",
+ "/dev",
+ "/etc",
+ "/etc/X11",
+ "/etc/opt",
+ "/etc/sgml",
+ "/etc/xml",
+ "/home",
+ "/libexec",
+ "/lost+found",
+ "/media",
+ "/mnt",
+ "/net",
+ "/opt",
+ "/private",
+ "/private/etc",
+ "/private/tftpboot",
+ "/private/tmp",
+ "/private/var",
+ "/proc",
+ "/root",
+ "/sbin",
+ "/srv",
+ "/tmp",
+ "/usr",
+ "/usr/X11R6",
+ "/usr/bin",
+ "/usr/etc",
+ "/usr/include",
+ "/usr/lib",
+ "/usr/libexec",
+ "/usr/local",
+ "/usr/local/Cellar",
+ "/usr/local/Frameworks",
+ "/usr/local/Library",
+ "/usr/local/bin",
+ "/usr/local/etc",
+ "/usr/local/include",
+ "/usr/local/lib",
+ "/usr/local/libexec",
+ "/usr/local/opt",
+ "/usr/local/share",
+ "/usr/local/share/man",
+ "/usr/local/share/man/man1",
+ "/usr/local/share/man/man2",
+ "/usr/local/share/man/man3",
+ "/usr/local/share/man/man4",
+ "/usr/local/share/man/man5",
+ "/usr/local/share/man/man6",
+ "/usr/local/share/man/man7",
+ "/usr/local/share/man/man8",
+ "/usr/local/share/man/man9",
+ "/usr/local/share/man/mann",
+ "/usr/local/var",
+ "/usr/local/var/lib",
+ "/usr/local/var/lock",
+ "/usr/local/var/run",
+ "/usr/sbin",
+ "/usr/share",
+ "/usr/share/man",
+ "/usr/share/man/man1",
+ "/usr/share/man/man2",
+ "/usr/share/man/man3",
+ "/usr/share/man/man4",
+ "/usr/share/man/man5",
+ "/usr/share/man/man6",
+ "/usr/share/man/man7",
+ "/usr/share/man/man8",
+ "/usr/share/man/man9",
+ "/usr/share/man/mann",
+ "/usr/src",
+ "/var",
+ "/var/cache",
+ "/var/lib",
+ "/var/lock",
+ "/var/log",
+ "/var/mail",
+ "/var/run",
+ "/var/spool",
+ "/var/spool/mail",
+ "/var/tmp",
+ ]
+ .map(&method(:Pathname))
+ .to_set
+ .freeze
+
+ # TODO: There should be a way to specify a containing
+ # directory under which nothing can be deleted.
+ UNDELETABLE_DIRS = [
+ "~/",
+ "~/Applications",
+ "~/Desktop",
+ "~/Documents",
+ "~/Downloads",
+ "~/Mail",
+ "~/Movies",
+ "~/Music",
+ "~/Music/iTunes",
+ "~/Music/iTunes/iTunes Music",
+ "~/Music/iTunes/Album Artwork",
+ "~/News",
+ "~/Pictures",
+ "~/Pictures/Desktops",
+ "~/Pictures/Photo Booth",
+ "~/Pictures/iChat Icons",
+ "~/Pictures/iPhoto Library",
+ "~/Public",
+ "~/Sites",
+ "~/Library",
+ "~/Library/.localized",
+ "~/Library/Accessibility",
+ "~/Library/Accounts",
+ "~/Library/Address Book Plug-Ins",
+ "~/Library/Application Scripts",
+ "~/Library/Application Support",
+ "~/Library/Application Support/Apple",
+ "~/Library/Application Support/com.apple.AssistiveControl",
+ "~/Library/Application Support/com.apple.QuickLook",
+ "~/Library/Application Support/com.apple.TCC",
+ "~/Library/Assistants",
+ "~/Library/Audio",
+ "~/Library/Automator",
+ "~/Library/Autosave Information",
+ "~/Library/Caches",
+ "~/Library/Calendars",
+ "~/Library/ColorPickers",
+ "~/Library/ColorSync",
+ "~/Library/Colors",
+ "~/Library/Components",
+ "~/Library/Compositions",
+ "~/Library/Containers",
+ "~/Library/Contextual Menu Items",
+ "~/Library/Cookies",
+ "~/Library/DTDs",
+ "~/Library/Desktop Pictures",
+ "~/Library/Developer",
+ "~/Library/Dictionaries",
+ "~/Library/DirectoryServices",
+ "~/Library/Displays",
+ "~/Library/Documentation",
+ "~/Library/Extensions",
+ "~/Library/Favorites",
+ "~/Library/FileSync",
+ "~/Library/Filesystems",
+ "~/Library/Filters",
+ "~/Library/FontCollections",
+ "~/Library/Fonts",
+ "~/Library/Frameworks",
+ "~/Library/GameKit",
+ "~/Library/Graphics",
+ "~/Library/Group Containers",
+ "~/Library/Icons",
+ "~/Library/IdentityServices",
+ "~/Library/Image Capture",
+ "~/Library/Images",
+ "~/Library/Input Methods",
+ "~/Library/Internet Plug-Ins",
+ "~/Library/InternetAccounts",
+ "~/Library/iTunes",
+ "~/Library/KeyBindings",
+ "~/Library/Keyboard Layouts",
+ "~/Library/Keychains",
+ "~/Library/LaunchAgents",
+ "~/Library/LaunchDaemons",
+ "~/Library/LocationBundles",
+ "~/Library/LoginPlugins",
+ "~/Library/Logs",
+ "~/Library/Mail",
+ "~/Library/Mail Downloads",
+ "~/Library/Messages",
+ "~/Library/Metadata",
+ "~/Library/Mobile Documents",
+ "~/Library/MonitorPanels",
+ "~/Library/OpenDirectory",
+ "~/Library/PDF Services",
+ "~/Library/PhonePlugins",
+ "~/Library/Phones",
+ "~/Library/PreferencePanes",
+ "~/Library/Preferences",
+ "~/Library/Printers",
+ "~/Library/PrivateFrameworks",
+ "~/Library/PubSub",
+ "~/Library/QuickLook",
+ "~/Library/QuickTime",
+ "~/Library/Receipts",
+ "~/Library/Recent Servers",
+ "~/Library/Recents",
+ "~/Library/Safari",
+ "~/Library/Saved Application State",
+ "~/Library/Screen Savers",
+ "~/Library/ScreenReader",
+ "~/Library/ScriptingAdditions",
+ "~/Library/ScriptingDefinitions",
+ "~/Library/Scripts",
+ "~/Library/Security",
+ "~/Library/Services",
+ "~/Library/Sounds",
+ "~/Library/Speech",
+ "~/Library/Spelling",
+ "~/Library/Spotlight",
+ "~/Library/StartupItems",
+ "~/Library/StickiesDatabase",
+ "~/Library/Sync Services",
+ "~/Library/SyncServices",
+ "~/Library/SyncedPreferences",
+ "~/Library/TextEncodings",
+ "~/Library/User Pictures",
+ "~/Library/Video",
+ "~/Library/Voices",
+ "~/Library/WebKit",
+ "~/Library/WidgetResources",
+ "~/Library/Widgets",
+ "~/Library/Workflows",
+ ]
+ .map { |x| Pathname(x).expand_path }
+ .to_set
+ .union(SYSTEM_DIRS)
+ .freeze
+
+ def system_dir?(dir)
+ SYSTEM_DIRS.any? { |u| File.identical?(u, dir) }
+ end
+
+ def undeletable?(dir)
+ UNDELETABLE_DIRS.any? { |u| File.identical?(u, dir) }
+ end
+
+ alias release version
+end
diff --git a/Library/Homebrew/cask/lib/hbc/options.rb b/Library/Homebrew/cask/lib/hbc/options.rb
new file mode 100644
index 000000000..c0e3e2ed0
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/options.rb
@@ -0,0 +1,37 @@
+module Hbc::Options
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ attr_writer :no_binaries
+
+ def no_binaries
+ @no_binaries ||= false
+ end
+
+ attr_writer :debug
+
+ def debug
+ @debug ||= false
+ end
+
+ attr_writer :verbose
+
+ def verbose
+ @verbose ||= false
+ end
+
+ attr_writer :cleanup_outdated
+
+ def cleanup_outdated
+ @cleanup_outdated ||= false
+ end
+
+ attr_writer :help
+
+ def help
+ @help ||= false
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/pkg.rb b/Library/Homebrew/cask/lib/hbc/pkg.rb
new file mode 100644
index 000000000..6f8d28c24
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/pkg.rb
@@ -0,0 +1,113 @@
+class Hbc::Pkg
+ def self.all_matching(regexp, command)
+ command.run("/usr/sbin/pkgutil", args: ["--pkgs=#{regexp}"]).stdout.split("\n").map { |package_id|
+ new(package_id.chomp, command)
+ }
+ end
+
+ attr_reader :package_id
+
+ def initialize(package_id, command = Hbc::SystemCommand)
+ @package_id = package_id
+ @command = command
+ end
+
+ def uninstall
+ odebug "Deleting pkg files"
+ pkgutil_bom_files.each_slice(500) do |file_slice|
+ @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true)
+ end
+ odebug "Deleting pkg symlinks and special files"
+ pkgutil_bom_specials.each_slice(500) do |file_slice|
+ @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true)
+ end
+ odebug "Deleting pkg directories"
+ _deepest_path_first(pkgutil_bom_dirs).each do |dir|
+ next unless dir.exist? && !MacOS.undeletable?(dir)
+ _with_full_permissions(dir) do
+ _clean_broken_symlinks(dir)
+ _clean_ds_store(dir)
+ _rmdir(dir)
+ end
+ end
+ forget
+ end
+
+ def forget
+ odebug "Unregistering pkg receipt (aka forgetting)"
+ @command.run!("/usr/sbin/pkgutil", args: ["--forget", package_id], sudo: true)
+ end
+
+ def pkgutil_bom(*type)
+ @command.run!("/usr/sbin/pkgutil", args: [*type, "--files", package_id].compact)
+ .stdout
+ .split("\n")
+ .map { |path| root.join(path) }
+ end
+
+ def pkgutil_bom_files
+ @pkgutil_bom_files ||= pkgutil_bom("--only-files")
+ end
+
+ def pkgutil_bom_dirs
+ @pkgutil_bom_dirs ||= pkgutil_bom("--only-dirs")
+ end
+
+ def pkgutil_bom_all
+ @pkgutil_bom_all ||= pkgutil_bom
+ end
+
+ def pkgutil_bom_specials
+ pkgutil_bom_all - pkgutil_bom_files - pkgutil_bom_dirs
+ end
+
+ def root
+ @root ||= Pathname(info.fetch("volume")).join(info.fetch("install-location"))
+ end
+
+ def info
+ @command.run!("/usr/sbin/pkgutil", args: ["--pkg-info-plist", package_id])
+ .plist
+ end
+
+ def _rmdir(path)
+ @command.run!("/bin/rmdir", args: ["--", path], sudo: true) if path.children.empty?
+ end
+
+ def _with_full_permissions(path)
+ original_mode = (path.stat.mode % 0o1000).to_s(8)
+ # TODO: similarly read and restore macOS flags (cf man chflags)
+ @command.run!("/bin/chmod", args: ["--", "777", path], sudo: true)
+ yield
+ ensure
+ if path.exist? # block may have removed dir
+ @command.run!("/bin/chmod", args: ["--", original_mode, path], sudo: true)
+ end
+ end
+
+ def _deepest_path_first(paths)
+ paths.sort do |path_a, path_b|
+ path_b.to_s.split("/").count <=> path_a.to_s.split("/").count
+ end
+ end
+
+ # Some pkgs leave broken symlinks hanging around; we clean them out before
+ # attempting to rmdir to prevent extra cruft from lying around after
+ # uninstall
+ def _clean_broken_symlinks(dir)
+ dir.children.each do |child|
+ if _broken_symlink?(child)
+ @command.run!("/bin/rm", args: ["--", child], sudo: true)
+ end
+ end
+ end
+
+ def _clean_ds_store(dir)
+ ds_store = dir.join(".DS_Store")
+ @command.run!("/bin/rm", args: ["--", ds_store], sudo: true) if ds_store.exist?
+ end
+
+ def _broken_symlink?(path)
+ path.symlink? && !path.exist?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/qualified_token.rb b/Library/Homebrew/cask/lib/hbc/qualified_token.rb
new file mode 100644
index 000000000..635e1cb3d
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/qualified_token.rb
@@ -0,0 +1,37 @@
+module Hbc::QualifiedToken
+ REPO_PREFIX = "homebrew-".freeze
+
+ # per https://github.com/Homebrew/homebrew/blob/4c7bc9ec3bca729c898ee347b6135ba692ee0274/Library/Homebrew/cmd/tap.rb#L121
+ USER_REGEX = %r{[a-z_\-]+}
+
+ # per https://github.com/Homebrew/homebrew/blob/4c7bc9ec3bca729c898ee347b6135ba692ee0274/Library/Homebrew/cmd/tap.rb#L121
+ REPO_REGEX = %r{(?:#{REPO_PREFIX})?\w+}
+
+ # per https://github.com/caskroom/homebrew-cask/blob/master/CONTRIBUTING.md#generating-a-token-for-the-cask
+ TOKEN_REGEX = %r{[a-z0-9\-]+}
+
+ TAP_REGEX = %r{#{USER_REGEX}[/\-]#{REPO_REGEX}}
+
+ QUALIFIED_TOKEN_REGEX ||= %r{#{TAP_REGEX}/#{TOKEN_REGEX}}
+
+ def self.parse(arg)
+ return nil unless arg.is_a?(String) && arg.downcase =~ %r{^#{QUALIFIED_TOKEN_REGEX}$}
+ path_elements = arg.downcase.split("/")
+ if path_elements.count == 2
+ # eg phinze-cask/google-chrome.
+ # Not certain this form is needed, but it was supported in the past.
+ token = path_elements[1]
+ dash_elements = path_elements[0].split("-")
+ repo = dash_elements.pop
+ dash_elements.pop if dash_elements.count > 1 && dash_elements[-1] + "-" == REPO_PREFIX
+ user = dash_elements.join("-")
+ else
+ # eg caskroom/cask/google-chrome
+ # per https://github.com/Homebrew/homebrew/wiki/brew-tap
+ user, repo, token = path_elements
+ end
+ repo.sub!(%r{^#{REPO_PREFIX}}, "")
+ odebug "[user, repo, token] might be [#{user}, #{repo}, #{token}]"
+ [user, repo, token]
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/scopes.rb b/Library/Homebrew/cask/lib/hbc/scopes.rb
new file mode 100644
index 000000000..3fbb59d26
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/scopes.rb
@@ -0,0 +1,59 @@
+module Hbc::Scopes
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def all
+ @all_casks ||= {}
+ all_tokens.map { |t| @all_casks[t] ||= load(t) }
+ end
+
+ def all_tapped_cask_dirs
+ @all_tapped_cask_dirs ||= Tap.names.map(&Tap.method(:fetch)).map(&:cask_dir)
+ .unshift(default_tap.cask_dir) # optimization: place the default Tap first
+ .uniq
+ end
+
+ def reset_all_tapped_cask_dirs
+ # The memoized value should be reset when a Tap is added/removed
+ # (which is a rare event in our codebase).
+ @all_tapped_cask_dirs = nil
+ end
+
+ def all_tokens
+ cask_tokens = all_tapped_cask_dirs.map { |d| Dir.glob d.join("*.rb") }.flatten
+ cask_tokens.map { |c|
+ # => "/usr/local/Library/Taps/caskroom/example-tap/Casks/example.rb"
+ c.sub!(%r{\.rb$}, "")
+ # => ".../example"
+ c = c.split("/").last 4
+ # => ["caskroom", "example-tap", "Casks", "example"]
+ c.delete_at(-2)
+ # => ["caskroom", "example-tap", "example"]
+ c.join "/"
+ }
+ end
+
+ def installed
+ # Hbc.load has some DWIM which is slow. Optimize here
+ # by spoon-feeding Hbc.load fully-qualified paths.
+ # TODO: speed up Hbc::Source::Tapped (main perf drag is calling Hbc.all_tokens repeatedly)
+ # TODO: ability to specify expected source when calling Hbc.load (minor perf benefit)
+ Pathname.glob(caskroom.join("*"))
+ .map { |caskroom_path|
+ token = caskroom_path.basename.to_s
+
+ path_to_cask = all_tapped_cask_dirs.find { |tap_dir|
+ tap_dir.join("#{token}.rb").exist?
+ }
+
+ if path_to_cask
+ Hbc.load(path_to_cask.join("#{token}.rb"))
+ else
+ Hbc.load(token)
+ end
+ }
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source.rb b/Library/Homebrew/cask/lib/hbc/source.rb
new file mode 100644
index 000000000..af298108a
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source.rb
@@ -0,0 +1,37 @@
+module Hbc::Source; end
+
+require "hbc/source/gone"
+require "hbc/source/path_slash_required"
+require "hbc/source/path_slash_optional"
+require "hbc/source/tapped_qualified"
+require "hbc/source/untapped_qualified"
+require "hbc/source/tapped"
+require "hbc/source/uri"
+
+module Hbc::Source
+ def self.sources
+ [
+ Hbc::Source::URI,
+ Hbc::Source::PathSlashRequired,
+ Hbc::Source::TappedQualified,
+ Hbc::Source::UntappedQualified,
+ Hbc::Source::Tapped,
+ Hbc::Source::PathSlashOptional,
+ Hbc::Source::Gone,
+ ]
+ end
+
+ def self.for_query(query)
+ odebug "Translating '#{query}' into a valid Cask source"
+ raise Hbc::CaskUnavailableError, query if query.to_s =~ %r{^\s*$}
+ source = sources.find { |s|
+ odebug "Testing source class #{s}"
+ s.me?(query)
+ }
+ raise Hbc::CaskUnavailableError, query unless source
+ odebug "Success! Using source class #{source}"
+ resolved_cask_source = source.new(query)
+ odebug "Resolved Cask URI or file source to '#{resolved_cask_source}'"
+ resolved_cask_source
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/gone.rb b/Library/Homebrew/cask/lib/hbc/source/gone.rb
new file mode 100644
index 000000000..2b9f2b5f2
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/gone.rb
@@ -0,0 +1,19 @@
+class Hbc::Source::Gone
+ def self.me?(query)
+ Hbc::WithoutSource.new(query).installed?
+ end
+
+ attr_reader :query
+
+ def initialize(query)
+ @query = query
+ end
+
+ def load
+ Hbc::WithoutSource.new(query)
+ end
+
+ def to_s
+ ""
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/path_base.rb b/Library/Homebrew/cask/lib/hbc/source/path_base.rb
new file mode 100644
index 000000000..bbb413fd3
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/path_base.rb
@@ -0,0 +1,65 @@
+require "rubygems"
+
+class Hbc::Source::PathBase
+ # derived classes must define method self.me?
+
+ def self.path_for_query(query)
+ query_string = query.to_s
+ Pathname.new(query_string.end_with?(".rb") ? query_string : query_string + ".rb")
+ end
+
+ attr_reader :path
+
+ def initialize(path)
+ @path = Pathname(path).expand_path
+ end
+
+ def load
+ raise Hbc::CaskError, "File '#{path}' does not exist" unless path.exist?
+ raise Hbc::CaskError, "File '#{path}' is not readable" unless path.readable?
+ raise Hbc::CaskError, "File '#{path}' is not a plain file" unless path.file?
+ load_cask
+ end
+
+ def to_s
+ # stringify to fully-resolved location
+ path.to_s
+ end
+
+ private
+
+ def load_cask
+ instance_eval(cask_contents, __FILE__, __LINE__)
+ rescue Hbc::CaskError, StandardError, ScriptError => e
+ # bug: e.message.concat doesn't work with Hbc::CaskError exceptions
+ raise e, e.message.concat(" while loading '#{path}'")
+ end
+
+ def cask_contents
+ File.open(path, "rb") do |handle|
+ contents = handle.read
+ if defined?(Encoding)
+ contents.force_encoding("UTF-8")
+ else
+ contents
+ end
+ end
+ end
+
+ def cask(header_token, &block)
+ build_cask(Hbc::Cask, header_token, &block)
+ end
+
+ def test_cask(header_token, &block)
+ build_cask(Hbc::TestCask, header_token, &block)
+ end
+
+ def build_cask(cask_class, header_token, &block)
+ raise Hbc::CaskTokenDoesNotMatchError.new(cask_token, header_token) unless cask_token == header_token
+ cask_class.new(cask_token, sourcefile_path: path, &block)
+ end
+
+ def cask_token
+ path.basename.to_s.sub(%r{\.rb}, "")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb b/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb
new file mode 100644
index 000000000..fb34c481a
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb
@@ -0,0 +1,8 @@
+require "hbc/source/path_base"
+
+class Hbc::Source::PathSlashOptional < Hbc::Source::PathBase
+ def self.me?(query)
+ path = path_for_query(query)
+ path.exist?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb b/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb
new file mode 100644
index 000000000..0c533a8a5
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb
@@ -0,0 +1,8 @@
+require "hbc/source/path_base"
+
+class Hbc::Source::PathSlashRequired < Hbc::Source::PathBase
+ def self.me?(query)
+ path = path_for_query(query)
+ path.to_s.include?("/") && path.exist?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/tapped.rb b/Library/Homebrew/cask/lib/hbc/source/tapped.rb
new file mode 100644
index 000000000..da9366840
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/tapped.rb
@@ -0,0 +1,35 @@
+class Hbc::Source::Tapped
+ def self.me?(query)
+ path_for_query(query).exist?
+ end
+
+ def self.path_for_query(query)
+ # Repeating Hbc.all_tokens is very slow for operations such as
+ # brew cask list, but memoizing the value might cause breakage
+ # elsewhere, given that installation and tap status is permitted
+ # to change during the course of an invocation.
+ token_with_tap = Hbc.all_tokens.find { |t| t.split("/").last == query.sub(%r{\.rb$}i, "") }
+ if token_with_tap
+ user, repo, token = token_with_tap.split("/")
+ Tap.fetch(user, repo).cask_dir.join("#{token}.rb")
+ else
+ Hbc.default_tap.cask_dir.join(query.sub(%r{(\.rb)?$}i, ".rb"))
+ end
+ end
+
+ attr_reader :token
+
+ def initialize(token)
+ @token = token
+ end
+
+ def load
+ path = self.class.path_for_query(token)
+ Hbc::Source::PathSlashOptional.new(path).load
+ end
+
+ def to_s
+ # stringify to fully-resolved location
+ self.class.path_for_query(token).expand_path.to_s
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb b/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb
new file mode 100644
index 000000000..48f8501e5
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb
@@ -0,0 +1,12 @@
+require "hbc/source/tapped"
+
+class Hbc::Source::TappedQualified < Hbc::Source::Tapped
+ def self.me?(query)
+ !Hbc::QualifiedToken.parse(query).nil? && path_for_query(query).exist?
+ end
+
+ def self.path_for_query(query)
+ user, repo, token = Hbc::QualifiedToken.parse(query)
+ Tap.new(user, repo).cask_dir.join(token.sub(%r{(\.rb)?$}i, ".rb"))
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb b/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb
new file mode 100644
index 000000000..361919bb3
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb
@@ -0,0 +1,11 @@
+require "hbc/source/tapped_qualified"
+
+class Hbc::Source::UntappedQualified < Hbc::Source::TappedQualified
+ def self.path_for_query(query)
+ user, repo, token = Hbc::QualifiedToken.parse(query)
+
+ tap = Tap.fetch(user, repo)
+ tap.install unless tap.installed?
+ tap.cask_dir.join(token.sub(%r{(\.rb)?$}i, ".rb"))
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/source/uri.rb b/Library/Homebrew/cask/lib/hbc/source/uri.rb
new file mode 100644
index 000000000..99bc50688
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/source/uri.rb
@@ -0,0 +1,28 @@
+class Hbc::Source::URI
+ def self.me?(query)
+ !(query.to_s =~ URI.regexp).nil?
+ end
+
+ attr_reader :uri
+
+ def initialize(uri)
+ @uri = uri
+ end
+
+ def load
+ Hbc.cache.mkpath
+ path = Hbc.cache.join(File.basename(uri))
+ ohai "Downloading #{uri}"
+ odebug "Download target -> #{path}"
+ begin
+ curl(uri, "-o", path.to_s)
+ rescue ErrorDuringExecution
+ raise Hbc::CaskUnavailableError, uri
+ end
+ Hbc::Source::PathSlashOptional.new(path).load
+ end
+
+ def to_s
+ uri.to_s
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/staged.rb b/Library/Homebrew/cask/lib/hbc/staged.rb
new file mode 100644
index 000000000..7e2c93541
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/staged.rb
@@ -0,0 +1,48 @@
+module Hbc::Staged
+ def info_plist_file(index = 0)
+ index = 0 if index == :first
+ index = 1 if index == :second
+ index = -1 if index == :last
+ Hbc.appdir.join(@cask.artifacts[:app].to_a.at(index).first, "Contents", "Info.plist")
+ end
+
+ def plist_exec(cmd)
+ @command.run!("/usr/libexec/PlistBuddy", args: ["-c", cmd, info_plist_file])
+ end
+
+ def plist_set(key, value)
+ plist_exec("Set #{key} #{value}")
+ rescue StandardError => e
+ raise Hbc::CaskError, "#{@cask.token}: 'plist_set' failed with: #{e}"
+ end
+
+ def bundle_identifier
+ plist_exec("Print CFBundleIdentifier").stdout.chomp
+ rescue StandardError => e
+ raise Hbc::CaskError, "#{@cask.token}: 'bundle_identifier' failed with: #{e}"
+ end
+
+ def set_permissions(paths, permissions_str)
+ full_paths = remove_nonexistent(paths)
+ return if full_paths.empty?
+ @command.run!("/bin/chmod", args: ["-R", "--", permissions_str] + full_paths,
+ sudo: true)
+ end
+
+ def set_ownership(paths, user: current_user, group: "staff")
+ full_paths = remove_nonexistent(paths)
+ return if full_paths.empty?
+ @command.run!("/usr/sbin/chown", args: ["-R", "--", "#{user}:#{group}"] + full_paths,
+ sudo: true)
+ end
+
+ def current_user
+ Hbc::Utils.current_user
+ end
+
+ private
+
+ def remove_nonexistent(paths)
+ Array(paths).map { |p| Pathname(p).expand_path }.select(&:exist?)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/system_command.rb b/Library/Homebrew/cask/lib/hbc/system_command.rb
new file mode 100644
index 000000000..6fa8a901f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/system_command.rb
@@ -0,0 +1,173 @@
+require "open3"
+require "shellwords"
+
+class Hbc::SystemCommand
+ attr_reader :command
+
+ def self.run(executable, options = {})
+ new(executable, options).run!
+ end
+
+ def self.run!(command, options = {})
+ run(command, options.merge(must_succeed: true))
+ end
+
+ def run!
+ @processed_output = { stdout: "", stderr: "" }
+ odebug "Executing: #{expanded_command.utf8_inspect}"
+
+ each_output_line do |type, line|
+ case type
+ when :stdout
+ processed_output[:stdout] << line
+ ohai line.chomp if options[:print_stdout]
+ when :stderr
+ processed_output[:stderr] << line
+ ohai line.chomp if options[:print_stderr]
+ end
+ end
+
+ assert_success if options[:must_succeed]
+ result
+ end
+
+ def initialize(executable, options)
+ @executable = executable
+ @options = options
+ process_options!
+ end
+
+ private
+
+ attr_reader :executable, :options, :processed_output, :processed_status
+
+ def process_options!
+ options.assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo, :bsexec
+ sudo_prefix = %w[/usr/bin/sudo -E --]
+ bsexec_prefix = ["/bin/launchctl", "bsexec", options[:bsexec] == :startup ? "/" : options[:bsexec]]
+ @command = [executable]
+ options[:print_stderr] = true unless options.key?(:print_stderr)
+ @command.unshift(*bsexec_prefix) if options[:bsexec]
+ @command.unshift(*sudo_prefix) if options[:sudo]
+ @command.concat(options[:args]) if options.key?(:args) && !options[:args].empty?
+ @command[0] = Shellwords.shellescape(@command[0]) if @command.size == 1
+ nil
+ end
+
+ def assert_success
+ return if processed_status && processed_status.success?
+ raise Hbc::CaskCommandFailedError.new(command.utf8_inspect, processed_output[:stdout], processed_output[:stderr], processed_status)
+ end
+
+ def expanded_command
+ @expanded_command ||= command.map { |arg|
+ if arg.respond_to?(:to_path)
+ File.absolute_path(arg)
+ else
+ String(arg)
+ end
+ }
+ end
+
+ def each_output_line(&b)
+ raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
+ Open3.popen3(*expanded_command)
+
+ write_input_to(raw_stdin) if options[:input]
+ raw_stdin.close_write
+ each_line_from [raw_stdout, raw_stderr], &b
+
+ @processed_status = raw_wait_thr.value
+ end
+
+ def write_input_to(raw_stdin)
+ Array(options[:input]).each { |line| raw_stdin.puts line }
+ end
+
+ def each_line_from(sources)
+ loop do
+ readable_sources = IO.select(sources)[0]
+ readable_sources.delete_if(&:eof?).first(1).each do |source|
+ type = (source == sources[0] ? :stdout : :stderr)
+ begin
+ yield(type, source.readline_nonblock || "")
+ rescue IO::WaitReadable, EOFError
+ next
+ end
+ end
+ break if readable_sources.empty?
+ end
+ sources.each(&:close_read)
+ end
+
+ def result
+ Hbc::SystemCommand::Result.new(command,
+ processed_output[:stdout],
+ processed_output[:stderr],
+ processed_status.exitstatus)
+ end
+end
+
+class Hbc::SystemCommand::Result
+ attr_accessor :command, :stdout, :stderr, :exit_status
+
+ def initialize(command, stdout, stderr, exit_status)
+ @command = command
+ @stdout = stdout
+ @stderr = stderr
+ @exit_status = exit_status
+ end
+
+ def plist
+ @plist ||= self.class._parse_plist(@command, @stdout.dup)
+ end
+
+ def success?
+ @exit_status == 0
+ end
+
+ def merged_output
+ @merged_output ||= @stdout + @stderr
+ end
+
+ def to_s
+ @stdout
+ end
+
+ def self._warn_plist_garbage(command, garbage)
+ return true unless garbage =~ %r{\S}
+ external = File.basename(command.first)
+ lines = garbage.strip.split("\n")
+ opoo "Non-XML stdout from #{external}:"
+ $stderr.puts lines.map { |l| " #{l}" }
+ end
+
+ def self._parse_plist(command, output)
+ raise Hbc::CaskError, "Empty plist input" unless output =~ %r{\S}
+ output.sub!(%r{\A(.*?)(<\?\s*xml)}m, '\2')
+ _warn_plist_garbage(command, Regexp.last_match[1]) if Hbc.debug
+ output.sub!(%r{(<\s*/\s*plist\s*>)(.*?)\Z}m, '\1')
+ _warn_plist_garbage(command, Regexp.last_match[2])
+ xml = Plist.parse_xml(output)
+ unless xml.respond_to?(:keys) && !xml.keys.empty?
+ raise Hbc::CaskError, <<-ERRMSG
+Empty result parsing plist output from command.
+ command was:
+ #{command.utf8_inspect}
+ output we attempted to parse:
+ #{output}
+ ERRMSG
+ end
+ xml
+ rescue Plist::ParseError => e
+ raise Hbc::CaskError, <<-ERRMSG
+Error parsing plist output from command.
+ command was:
+ #{command.utf8_inspect}
+ error was:
+ #{e}
+ output we attempted to parse:
+ #{output}
+ ERRMSG
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/topological_hash.rb b/Library/Homebrew/cask/lib/hbc/topological_hash.rb
new file mode 100644
index 000000000..bbad1bb4d
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/topological_hash.rb
@@ -0,0 +1,12 @@
+require "tsort"
+
+# a basic topologically sortable hashmap
+class Hbc::TopologicalHash < Hash
+ include TSort
+
+ alias tsort_each_node each_key
+
+ def tsort_each_child(node, &block)
+ fetch(node).each(&block)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb b/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb
new file mode 100644
index 000000000..34bfea387
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb
@@ -0,0 +1,26 @@
+require "uri"
+
+module Hbc::UnderscoreSupportingURI
+ def self.parse(maybe_uri)
+ return nil if maybe_uri.nil?
+ URI.parse(maybe_uri)
+ rescue URI::InvalidURIError => e
+ scheme, host, path = simple_parse(maybe_uri)
+ raise e unless path && host.include?("_")
+ URI.parse(without_host_underscores(scheme, host, path)).tap do |uri|
+ uri.instance_variable_set("@host", host)
+ end
+ end
+
+ def self.simple_parse(maybe_uri)
+ scheme, host_and_path = maybe_uri.split("://")
+ host, path = host_and_path.split("/", 2)
+ [scheme, host, path]
+ rescue StandardError
+ nil
+ end
+
+ def self.without_host_underscores(scheme, host, path)
+ ["#{scheme}:/", host.tr("_", "-"), path].join("/")
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/url.rb b/Library/Homebrew/cask/lib/hbc/url.rb
new file mode 100644
index 000000000..5f763ca8a
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/url.rb
@@ -0,0 +1,37 @@
+require "forwardable"
+
+class Hbc::URL
+ FAKE_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10) http://caskroom.io".freeze
+
+ attr_reader :using, :revision, :trust_cert, :uri, :cookies, :referer, :data
+
+ extend Forwardable
+ def_delegators :uri, :path, :scheme, :to_s
+
+ def self.from(*args, &block)
+ if block_given?
+ Hbc::DSL::StanzaProxy.once(self) { new(*block.call) }
+ else
+ new(*args)
+ end
+ end
+
+ def initialize(uri, options = {})
+ @uri = Hbc::UnderscoreSupportingURI.parse(uri)
+ @user_agent = options[:user_agent]
+ @cookies = options[:cookies]
+ @referer = options[:referer]
+ @using = options[:using]
+ @revision = options[:revision]
+ @trust_cert = options[:trust_cert]
+ @data = options[:data]
+ end
+
+ def user_agent
+ if @user_agent == :fake
+ FAKE_USER_AGENT
+ else
+ @user_agent
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/url_checker.rb b/Library/Homebrew/cask/lib/hbc/url_checker.rb
new file mode 100644
index 000000000..8737903df
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/url_checker.rb
@@ -0,0 +1,75 @@
+require "hbc/checkable"
+
+class Hbc::UrlChecker
+ attr_accessor :cask, :response_status, :headers
+
+ include Hbc::Checkable
+
+ def initialize(cask, fetcher = Hbc::Fetcher)
+ @cask = cask
+ @fetcher = fetcher
+ @headers = {}
+ end
+
+ def summary_header
+ "url check result for #{cask}"
+ end
+
+ def run
+ _get_data_from_request
+ return if errors?
+ _check_response_status
+ end
+
+ HTTP_RESPONSES = [
+ "HTTP/1.0 200 OK",
+ "HTTP/1.1 200 OK",
+ "HTTP/1.1 302 Found",
+ ].freeze
+
+ OK_RESPONSES = {
+ "http" => HTTP_RESPONSES,
+ "https" => HTTP_RESPONSES,
+ "ftp" => ["OK"],
+ }.freeze
+
+ def _check_response_status
+ ok = OK_RESPONSES[cask.url.scheme]
+ return if ok.include?(@response_status)
+ add_error "unexpected http response, expecting #{ok.map(&:utf8_inspect).join(' or ')}, got #{@response_status.utf8_inspect}"
+ end
+
+ def _get_data_from_request
+ response = @fetcher.head(cask.url)
+
+ if response.empty?
+ add_error "timeout while requesting #{cask.url}"
+ return
+ end
+
+ response_lines = response.split("\n").map(&:chomp)
+
+ case cask.url.scheme
+ when "http", "https" then
+ @response_status = response_lines.grep(%r{^HTTP}).last
+ if @response_status.respond_to?(:strip)
+ @response_status.strip!
+ unless response_lines.index(@response_status).nil?
+ http_headers = response_lines[(response_lines.index(@response_status) + 1)..-1]
+ http_headers.each do |line|
+ header_name, header_value = line.split(": ")
+ @headers[header_name] = header_value
+ end
+ end
+ end
+ when "ftp" then
+ @response_status = "OK"
+ response_lines.each do |line|
+ header_name, header_value = line.split(": ")
+ @headers[header_name] = header_value
+ end
+ else
+ add_error "unknown scheme for #{cask.url}"
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/utils.rb b/Library/Homebrew/cask/lib/hbc/utils.rb
new file mode 100644
index 000000000..6fc52cc93
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/utils.rb
@@ -0,0 +1,198 @@
+module Hbc::Utils; end
+
+require "yaml"
+require "open3"
+require "stringio"
+
+require "hbc/utils/file"
+require "hbc/utils/tty"
+
+UPDATE_CMD = "brew uninstall --force brew-cask; brew untap phinze/cask; brew untap caskroom/cask; brew update; brew cleanup; brew cask cleanup".freeze
+ISSUES_URL = "https://github.com/caskroom/homebrew-cask#reporting-bugs".freeze
+
+# monkeypatch Object - not a great idea
+class Object
+ def utf8_inspect
+ return inspect unless defined?(Encoding)
+ return map(&:utf8_inspect) if respond_to?(:map)
+ inspect.force_encoding("UTF-8").sub(%r{\A"(.*)"\Z}, '\1')
+ end
+end
+
+class Buffer < StringIO
+ def initialize(tty = false)
+ super()
+ @tty = tty
+ end
+
+ def tty?
+ @tty
+ end
+end
+
+# global methods
+
+def odebug(title, *sput)
+ if Hbc.respond_to?(:debug) && Hbc.debug
+ width = Hbc::Utils::Tty.width * 4 - 6
+ if $stdout.tty? && title.to_s.length > width
+ title = title.to_s[0, width - 3] + "..."
+ end
+ puts "#{Hbc::Utils::Tty.magenta.bold}==>#{Hbc::Utils::Tty.reset.bold} #{title}#{Hbc::Utils::Tty.reset}"
+ puts sput unless sput.empty?
+ end
+end
+
+module Hbc::Utils
+ def self.which(cmd, path = ENV["PATH"])
+ unless File.basename(cmd) == cmd.to_s
+ # cmd contains a directory element
+ cmd_pn = Pathname(cmd)
+ return nil unless cmd_pn.absolute?
+ return resolve_executable(cmd_pn)
+ end
+ path.split(File::PATH_SEPARATOR).each do |elt|
+ fq_cmd = Pathname(elt).expand_path.join(cmd)
+ resolved = resolve_executable fq_cmd
+ return resolved if resolved
+ end
+ nil
+ end
+
+ def self.resolve_executable(cmd)
+ cmd_pn = Pathname(cmd)
+ return nil unless cmd_pn.exist?
+ return nil unless cmd_pn.executable?
+ begin
+ cmd_pn = Pathname(cmd_pn.realpath)
+ rescue RuntimeError
+ return nil
+ end
+ return nil unless cmd_pn.file?
+ cmd_pn
+ end
+
+ def self.gain_permissions_remove(path, command: Hbc::SystemCommand)
+ if path.respond_to?(:rmtree) && path.exist?
+ gain_permissions(path, ["-R"], command, &:rmtree)
+ elsif File.symlink?(path)
+ gain_permissions(path, ["-h"], command, &FileUtils.method(:rm_f))
+ end
+ end
+
+ def self.gain_permissions(path, command_args, command)
+ tried_permissions = false
+ tried_ownership = false
+ begin
+ yield path
+ rescue StandardError
+ # in case of permissions problems
+ unless tried_permissions
+ # 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",
+ must_succeed: false,
+ args: command_args + ["--", "000", path])
+ command.run("/bin/chmod",
+ must_succeed: false,
+ args: command_args + ["--", "u+rwx", path])
+ command.run("/bin/chmod",
+ must_succeed: false,
+ args: command_args + ["-N", path])
+ tried_permissions = true
+ retry # rmtree
+ end
+ 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}'"
+ command.run("/usr/sbin/chown",
+ args: command_args + ["--", current_user, path],
+ sudo: true)
+ tried_ownership = true
+ # retry chflags/chmod after chown
+ tried_permissions = false
+ retry # rmtree
+ end
+ end
+ end
+
+ def self.current_user
+ Etc.getpwuid(Process.euid).name
+ end
+
+ # paths that "look" descendant (textually) will still
+ # return false unless both the given paths exist
+ def self.file_is_descendant(file, dir)
+ file = Pathname.new(file)
+ dir = Pathname.new(dir)
+ return false unless file.exist? && dir.exist?
+ unless dir.directory?
+ onoe "Argument must be a directory: '#{dir}'"
+ return false
+ end
+ unless file.absolute? && dir.absolute?
+ onoe "Both arguments must be absolute: '#{file}', '#{dir}'"
+ return false
+ end
+ while file.parent != file
+ return true if File.identical?(file, dir)
+ file = file.parent
+ end
+ false
+ end
+
+ def self.path_occupied?(path)
+ File.exist?(path) || File.symlink?(path)
+ end
+
+ def self.error_message_with_suggestions
+ <<-EOS.undent
+ #{Hbc::Utils::Tty.reset.bold}
+ Most likely, this means you have an outdated version of Homebrew-Cask. Please run:
+
+ #{Hbc::Utils::Tty.green.normal}#{UPDATE_CMD}
+
+ #{Hbc::Utils::Tty.reset.bold}If this doesn’t fix the problem, please report this bug:
+
+ #{Hbc::Utils::Tty.underline}#{ISSUES_URL}#{Hbc::Utils::Tty.reset}
+
+ EOS
+ end
+
+ def self.method_missing_message(method, token, section = nil)
+ poo = []
+ poo << "Unexpected method '#{method}' called"
+ poo << "during #{section}" if section
+ poo << "on Cask #{token}."
+
+ opoo(poo.join(" ") + "\n" + error_message_with_suggestions)
+ end
+
+ def self.nowstamp_metadata_path(container_path)
+ @timenow ||= Time.now.gmtime
+ if container_path.respond_to?(:join)
+ precision = 3
+ timestamp = @timenow.strftime("%Y%m%d%H%M%S")
+ fraction = format("%.#{precision}f", @timenow.to_f - @timenow.to_i)[1..-1]
+ timestamp.concat(fraction)
+ container_path.join(timestamp)
+ end
+ end
+
+ def self.size_in_bytes(files)
+ Array(files).reduce(0) { |a, e| a + (File.size?(e) || 0) }
+ end
+
+ def self.capture_stderr
+ previous_stderr = $stderr
+ $stderr = StringIO.new
+ yield
+ $stderr.string
+ ensure
+ $stderr = previous_stderr
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/utils/file.rb b/Library/Homebrew/cask/lib/hbc/utils/file.rb
new file mode 100644
index 000000000..967c6834f
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/utils/file.rb
@@ -0,0 +1,12 @@
+module Hbc::Utils
+ module_function
+
+ def file_locked?(file)
+ unlocked = File.open(file).flock(File::LOCK_EX | File::LOCK_NB)
+ # revert lock if file was unlocked before check
+ File.open(file).flock(File::LOCK_UN) if unlocked
+ !unlocked
+ rescue
+ true
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/utils/tty.rb b/Library/Homebrew/cask/lib/hbc/utils/tty.rb
new file mode 100644
index 000000000..c383df828
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/utils/tty.rb
@@ -0,0 +1,125 @@
+# originally from Homebrew utils.rb
+
+class Hbc::Utils::Tty
+ COLORS = {
+ black: 0,
+ red: 1,
+ green: 2,
+ yellow: 3,
+ blue: 4,
+ magenta: 5,
+ cyan: 6,
+ white: 7,
+ default: 9,
+ }.freeze
+
+ ATTRIBUTES = {
+ reset: 0,
+ bold: 1,
+ dim: 2,
+ italic: 3,
+ underline: 4,
+ blink: 5,
+ inverse: 7,
+ invisible: 8,
+ strikethrough: 9,
+ normal: 22,
+ }.freeze
+
+ @sequence = []
+
+ class << self
+ COLORS.keys.each do |sym|
+ define_method(sym) do
+ foreground(COLORS[sym])
+ end
+ define_method("fg_#{sym}".to_sym) do
+ foreground(COLORS[sym])
+ end
+ define_method("bg_#{sym}".to_sym) do
+ background(COLORS[sym])
+ end
+ end
+
+ ATTRIBUTES.keys.each do |sym|
+ define_method(sym) do
+ deferred_emit(ATTRIBUTES[sym])
+ end
+ end
+
+ def width
+ `/usr/bin/tput cols`.strip.to_i
+ end
+
+ def truncate(str)
+ str.to_s[0, width - 4]
+ end
+
+ private
+
+ def foreground(color)
+ deferred_emit(to_foreground_code(color))
+ end
+
+ def background(color)
+ deferred_emit(to_background_code(color))
+ end
+
+ def to_color_code(space, color)
+ return unless (num = to_color_number(color))
+ return space + num if num < space
+ return space + 9 if num > space
+ num
+ end
+
+ def to_foreground_code(color)
+ to_color_code(30, color)
+ end
+
+ def to_background_code(color)
+ to_color_code(40, color)
+ end
+
+ def to_color_number(color)
+ COLORS[color] || color.is_a?(Integer) ? color : nil
+ end
+
+ def to_attribute_number(attribute)
+ ATTRIBUTES[attribute] || attribute.is_a?(Integer) ? attribute : nil
+ end
+
+ def sanitize_integer(arg)
+ return arg.to_i if arg.is_a?(Integer)
+ return 0 if arg.to_s =~ %r{^0+$}
+ if arg.respond_to?(:to_i) && (int = arg.to_i) > 0
+ return int
+ end
+ $stderr.puts "Warning: bad Tty code #{arg}"
+ ATTRIBUTES[:reset]
+ end
+
+ def deferred_emit(*codes)
+ @sequence.concat Array(*codes).map(&method(:sanitize_integer))
+ Hbc::Utils::Tty
+ end
+
+ def to_s
+ sequence = @sequence
+ @sequence = []
+ return "" unless $stdout.tty?
+ if sequence.empty?
+ $stderr.puts "Warning: empty Tty sequence"
+ sequence = [ATTRIBUTES[:reset]]
+ end
+ "#{initiate}#{sequence.join(';')}#{terminate}"
+ end
+
+ def initiate
+ "\033["
+ end
+
+ def terminate
+ "m"
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/verify.rb b/Library/Homebrew/cask/lib/hbc/verify.rb
new file mode 100644
index 000000000..d3c2713e7
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/verify.rb
@@ -0,0 +1,33 @@
+module Hbc::Verify; end
+
+require "hbc/verify/checksum"
+require "hbc/verify/gpg"
+
+module Hbc::Verify
+ module_function
+
+ def verifications
+ [
+ Hbc::Verify::Checksum
+ # TODO: Hbc::Verify::Gpg
+ ]
+ end
+
+ def all(cask, downloaded_path)
+ odebug "Verifying download"
+ verifications = for_cask(cask)
+ odebug "#{verifications.size} verifications defined", verifications
+ verifications.each do |verification|
+ odebug "Running verification of class #{verification}"
+ verification.new(cask, downloaded_path).verify
+ end
+ end
+
+ def for_cask(cask)
+ odebug "Determining which verifications to run for Cask #{cask}"
+ verifications.select do |verification|
+ odebug "Checking for verification class #{verification}"
+ verification.me?(cask)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/verify/checksum.rb b/Library/Homebrew/cask/lib/hbc/verify/checksum.rb
new file mode 100644
index 000000000..3af6f1667
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/verify/checksum.rb
@@ -0,0 +1,43 @@
+require "digest"
+
+class Hbc::Verify::Checksum
+ def self.me?(cask)
+ return true unless cask.sha256 == :no_check
+ ohai "No checksum defined for Cask #{cask}, skipping verification"
+ false
+ end
+
+ attr_reader :cask, :downloaded_path
+
+ def initialize(cask, downloaded_path)
+ @cask = cask
+ @downloaded_path = downloaded_path
+ end
+
+ def verify
+ return unless self.class.me?(cask)
+ ohai "Verifying checksum for Cask #{cask}"
+ verify_checksum
+ end
+
+ private
+
+ def expected
+ @expected ||= cask.sha256
+ end
+
+ def computed
+ @computed ||= Digest::SHA2.file(downloaded_path).hexdigest
+ end
+
+ def verify_checksum
+ raise Hbc::CaskSha256MissingError, "sha256 required: sha256 '#{computed}'" if expected.nil? || expected.empty?
+
+ if expected == computed
+ odebug "SHA256 checksums match"
+ else
+ ohai 'Note: running "brew update" may fix sha256 checksum errors'
+ raise Hbc::CaskSha256MismatchError.new(downloaded_path, expected, computed)
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/verify/gpg.rb b/Library/Homebrew/cask/lib/hbc/verify/gpg.rb
new file mode 100644
index 000000000..6190f67d1
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/verify/gpg.rb
@@ -0,0 +1,60 @@
+class Hbc::Verify::Gpg
+ def self.me?(cask)
+ cask.gpg
+ end
+
+ attr_reader :cask, :downloaded_path
+
+ def initialize(cask, downloaded_path, command = Hbc::SystemCommand)
+ @command = command
+ @cask = cask
+ @downloaded_path = downloaded_path
+ end
+
+ def available?
+ return @available unless @available.nil?
+ @available = self.class.me?(cask) && installed?
+ end
+
+ def installed?
+ cmd = @command.run("/usr/bin/type",
+ args: ["-p", "gpg"])
+
+ # if `gpg` is found, return its absolute path
+ cmd.success? ? cmd.stdout : false
+ end
+
+ def fetch_sig(force = false)
+ unversioned_cask = cask.version.is_a?(Symbol)
+ cached = cask.metadata_subdir("gpg") unless unversioned_cask
+
+ meta_dir = cached || cask.metadata_subdir("gpg", :now, true)
+ sig_path = meta_dir.join("signature.asc")
+
+ curl(cask.gpg.signature, "-o", sig_path.to_s) unless cached || force
+
+ sig_path
+ end
+
+ def import_key
+ args = if cask.gpg.key_id
+ ["--recv-keys", cask.gpg.key_id]
+ elsif cask.gpg.key_url
+ ["--fetch-key", cask.gpg.key_url.to_s]
+ end
+
+ @command.run!("gpg", args: args)
+ end
+
+ def verify
+ return unless available?
+ import_key
+ sig = fetch_sig
+
+ ohai "Verifying GPG signature for #{cask}"
+
+ @command.run!("gpg",
+ args: ["--verify", sig, downloaded_path],
+ print_stdout: true)
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/version.rb b/Library/Homebrew/cask/lib/hbc/version.rb
new file mode 100644
index 000000000..471fd1999
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/version.rb
@@ -0,0 +1,13 @@
+HBC_VERSION = "0.60.0".freeze
+
+module Hbc
+ def self.full_version
+ @full_version ||= begin
+ revision, commit = Dir.chdir(Hbc.default_tap.path) do
+ [`git rev-parse --short=4 --verify -q HEAD 2>/dev/null`.chomp,
+ `git show -s --format="%cr" HEAD 2>/dev/null`.chomp]
+ end
+ "#{HBC_VERSION} (git revision #{revision}; last commit #{commit})"
+ end
+ end
+end
diff --git a/Library/Homebrew/cask/lib/hbc/without_source.rb b/Library/Homebrew/cask/lib/hbc/without_source.rb
new file mode 100644
index 000000000..6ed826e41
--- /dev/null
+++ b/Library/Homebrew/cask/lib/hbc/without_source.rb
@@ -0,0 +1,15 @@
+class Hbc::WithoutSource < Hbc::Cask
+ # Override from `Hbc::DSL` because we don't have a cask source file to work
+ # with, so we don't know the cask's `version`.
+ def staged_path
+ (caskroom_path.children - [metadata_master_container_path]).first
+ end
+
+ def to_s
+ "#{token} (!)"
+ end
+
+ def installed?
+ caskroom_path.exist?
+ end
+end
diff --git a/Library/Homebrew/cask/lib/vendor/plist.rb b/Library/Homebrew/cask/lib/vendor/plist.rb
new file mode 100644
index 000000000..9e469a069
--- /dev/null
+++ b/Library/Homebrew/cask/lib/vendor/plist.rb
@@ -0,0 +1,234 @@
+#
+# = plist
+#
+# Copyright 2006-2010 Ben Bleything and Patrick May
+# Distributed under the MIT License
+#
+
+# Plist parses macOS xml property list files into ruby data structures.
+#
+# === Load a plist file
+# This is the main point of the library:
+#
+# r = Plist::parse_xml( filename_or_xml )
+module Plist
+# Note that I don't use these two elements much:
+#
+# + Date elements are returned as DateTime objects.
+# + Data elements are implemented as Tempfiles
+#
+# Plist::parse_xml will blow up if it encounters a Date element.
+# If you encounter such an error, or if you have a Date element which
+# can't be parsed into a Time object, please send your plist file to
+# plist@hexane.org so that I can implement the proper support.
+ def Plist::parse_xml( filename_or_xml )
+ listener = Listener.new
+ #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
+ parser = StreamParser.new(filename_or_xml, listener)
+ parser.parse
+ listener.result
+ end
+
+ class Listener
+ #include REXML::StreamListener
+
+ attr_accessor :result, :open
+
+ def initialize
+ @result = nil
+ @open = Array.new
+ end
+
+
+ def tag_start(name, attributes)
+ @open.push PTag::mappings[name].new
+ end
+
+ def text( contents )
+ @open.last.text = contents if @open.last
+ end
+
+ def tag_end(name)
+ last = @open.pop
+ if @open.empty?
+ @result = last.to_ruby
+ else
+ @open.last.children.push last
+ end
+ end
+ end
+
+ class StreamParser
+ def initialize( plist_data_or_file, listener )
+ if plist_data_or_file.respond_to? :read
+ @xml = plist_data_or_file.read
+ elsif File.exists? plist_data_or_file
+ @xml = File.read( plist_data_or_file )
+ else
+ @xml = plist_data_or_file
+ end
+
+ trim_to_xml_start!
+
+ @listener = listener
+ end
+
+ def trim_to_xml_start!
+ _, xml_tag, rest = @xml.partition(/^<\?xml/)
+ @xml = [xml_tag, rest].join
+ end
+
+ TEXT = /([^<]+)/
+ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um
+ DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
+ COMMENT_START = /\A<!--/u
+ COMMENT_END = /.*?-->/um
+
+
+ def parse
+ plist_tags = PTag::mappings.keys.join('|')
+ start_tag = /<(#{plist_tags})([^>]*)>/i
+ end_tag = /<\/(#{plist_tags})[^>]*>/i
+
+ require 'strscan'
+
+ @scanner = StringScanner.new( @xml )
+ until @scanner.eos?
+ if @scanner.scan(COMMENT_START)
+ @scanner.scan(COMMENT_END)
+ elsif @scanner.scan(XMLDECL_PATTERN)
+ elsif @scanner.scan(DOCTYPE_PATTERN)
+ elsif @scanner.scan(start_tag)
+ @listener.tag_start(@scanner[1], nil)
+ if (@scanner[2] =~ /\/$/)
+ @listener.tag_end(@scanner[1])
+ end
+ elsif @scanner.scan(TEXT)
+ @listener.text(@scanner[1])
+ elsif @scanner.scan(end_tag)
+ @listener.tag_end(@scanner[1])
+ else
+ raise ParseError.new("Unimplemented element #{@xml}")
+ end
+ end
+ end
+ end
+
+ class PTag
+ @@mappings = { }
+ def PTag::mappings
+ @@mappings
+ end
+
+ def PTag::inherited( sub_class )
+ key = sub_class.to_s.downcase
+ key.gsub!(/^plist::/, '' )
+ key.gsub!(/^p/, '') unless key == "plist"
+
+ @@mappings[key] = sub_class
+ end
+
+ attr_accessor :text, :children
+ def initialize
+ @children = Array.new
+ end
+
+ def to_ruby
+ raise ParseError.new("Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}")
+ end
+ end
+
+ class PList < PTag
+ def to_ruby
+ children.first.to_ruby if children.first
+ end
+ end
+
+ class PDict < PTag
+ def to_ruby
+ dict = Hash.new
+ key = nil
+
+ children.each do |c|
+ if key.nil?
+ key = c.to_ruby
+ else
+ dict[key] = c.to_ruby
+ key = nil
+ end
+ end
+
+ dict
+ end
+ end
+
+ require 'cgi'
+ class PKey < PTag
+ def to_ruby
+ CGI::unescapeHTML(text || '')
+ end
+ end
+
+ class PString < PTag
+ def to_ruby
+ CGI::unescapeHTML(text || '')
+ end
+ end
+
+ class PArray < PTag
+ def to_ruby
+ children.collect do |c|
+ c.to_ruby
+ end
+ end
+ end
+
+ class PInteger < PTag
+ def to_ruby
+ text.to_i
+ end
+ end
+
+ class PTrue < PTag
+ def to_ruby
+ true
+ end
+ end
+
+ class PFalse < PTag
+ def to_ruby
+ false
+ end
+ end
+
+ class PReal < PTag
+ def to_ruby
+ text.to_f
+ end
+ end
+
+ require 'date'
+ class PDate < PTag
+ def to_ruby
+ DateTime.parse(text)
+ end
+ end
+
+ require 'base64'
+ class PData < PTag
+ def to_ruby
+ data = Base64.decode64(text.gsub(/\s+/, ''))
+
+ begin
+ return Marshal.load(data)
+ rescue Exception => e
+ io = StringIO.new
+ io.write data
+ io.rewind
+ return io
+ end
+ end
+ end
+
+ class ParseError < RuntimeError; end
+end