diff options
| author | AnastasiaSulyagina | 2016-08-18 22:11:42 +0300 |
|---|---|---|
| committer | AnastasiaSulyagina | 2016-08-19 14:50:14 +0300 |
| commit | e81f4ab7deeb40308f240be5ea00091fc8786d7a (patch) | |
| tree | b5418f9149de71c0f05f90cb2b39ab47f46e27b4 /Library/Homebrew/cask/lib | |
| parent | 5c7c9de669025bbe4cad9829be39c5cf3b31ad25 (diff) | |
| download | brew-e81f4ab7deeb40308f240be5ea00091fc8786d7a.tar.bz2 | |
init
Diffstat (limited to 'Library/Homebrew/cask/lib')
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 |
