diff --git a/Library/Homebrew/cask/artifact/abstract_uninstall.rb b/Library/Homebrew/cask/artifact/abstract_uninstall.rb index c7db216a86..1806d8c190 100644 --- a/Library/Homebrew/cask/artifact/abstract_uninstall.rb +++ b/Library/Homebrew/cask/artifact/abstract_uninstall.rb @@ -45,6 +45,8 @@ module Cask directives[:signal] = Array(directives[:signal]).flatten.each_slice(2).to_a @directives = directives + # This is already included when loading from the API. + return if cask.loaded_from_api? return unless directives.key?(:kext) cask.caveats do diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index eb968f31d0..684969ac97 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -17,6 +17,7 @@ module Cask extend Forwardable extend Searchable + extend Predicable include Metadata # Needs a leading slash to avoid `File.expand.path` complaining about non-absolute home. @@ -27,10 +28,11 @@ module Cask # TODO: can be removed when API JSON is regenerated with HOMEBREW_PREFIX_PLACEHOLDER. HOMEBREW_OLD_PREFIX_PLACEHOLDER = "$(brew --prefix)" - attr_reader :token, :sourcefile_path, :source, :config, :default_config, :loaded_from_api, :loader - + attr_reader :token, :sourcefile_path, :source, :config, :default_config, :loader attr_accessor :download, :allow_reassignment + attr_predicate :loaded_from_api? + class << self def generating_hash! return if generating_hash? @@ -83,14 +85,14 @@ module Cask @tap end - def initialize(token, sourcefile_path: nil, source: nil, tap: nil, + def initialize(token, sourcefile_path: nil, source: nil, tap: nil, loaded_from_api: false, config: nil, allow_reassignment: false, loader: nil, &block) @token = token @sourcefile_path = sourcefile_path @source = source @tap = tap @allow_reassignment = allow_reassignment - @loaded_from_api = false + @loaded_from_api = loaded_from_api @loader = loader @block = block @@ -278,7 +280,8 @@ module Cask end def populate_from_api!(json_cask) - @loaded_from_api = true + raise ArgumentError, "Expected cask to be loaded from the API" unless loaded_from_api? + @languages = json_cask[:languages] @tap_git_head = json_cask[:tap_git_head] @ruby_source_checksum = json_cask[:ruby_source_checksum].freeze @@ -298,11 +301,6 @@ module Cask alias == eql? def to_h - if loaded_from_api && !Homebrew::EnvConfig.no_install_from_api? - json_cask = Homebrew::API::Cask.all_casks[token] - return api_to_local_hash(Homebrew::API.merge_variations(json_cask)) - end - url_specs = url&.specs.dup case url_specs&.dig(:user_agent) when :default @@ -339,7 +337,7 @@ module Cask end def to_hash_with_variations - if loaded_from_api && !Homebrew::EnvConfig.no_install_from_api? + if loaded_from_api? && !Homebrew::EnvConfig.no_install_from_api? return api_to_local_hash(Homebrew::API::Cask.all_casks[token]) end diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 0828bd9c1f..d8a6bd41c9 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -219,11 +219,17 @@ module Cask def load(config:) json_cask = @from_json || Homebrew::API::Cask.all_casks[token] - cask_source = JSON.pretty_generate(json_cask) + + cask_options = { + loaded_from_api: true, + source: JSON.pretty_generate(json_cask), + config: config, + loader: self, + } json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys.freeze - tap = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/") + cask_options[:tap] = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/") user_agent = json_cask.dig(:url_specs, :user_agent) json_cask[:url_specs][:user_agent] = user_agent[1..].to_sym if user_agent && user_agent[0] == ":" @@ -231,7 +237,7 @@ module Cask json_cask[:url_specs][:using] = using.to_sym end - api_cask = Cask.new(token, tap: tap, source: cask_source, config: config, loader: self) do + api_cask = Cask.new(token, **cask_options) do version json_cask[:version] if json_cask[:sha256] == "no_check" @@ -248,7 +254,7 @@ module Cask desc json_cask[:desc] homepage json_cask[:homepage] - auto_updates json_cask[:auto_updates] if json_cask[:auto_updates].present? + auto_updates json_cask[:auto_updates] unless json_cask[:auto_updates].nil? conflicts_with(**json_cask[:conflicts_with]) if json_cask[:conflicts_with].present? if json_cask[:depends_on].present? @@ -289,7 +295,7 @@ module Cask json_cask[:artifacts].each do |artifact| # convert generic string replacements into actual ones - artifact = cask.loader.from_h_hash_gsubs(artifact, appdir) + artifact = cask.loader.from_h_gsubs(artifact, appdir) key = artifact.keys.first if artifact[key].nil? # for artifacts with blocks that can't be loaded from the API @@ -328,18 +334,17 @@ module Cask hash.to_h.transform_values do |value| from_h_gsubs(value, appdir) end - rescue TypeError - from_h_array_gsubs(hash, appdir) end def from_h_gsubs(value, appdir) return value if value.blank? - if value.respond_to? :to_h + case value + when Hash from_h_hash_gsubs(value, appdir) - elsif value.respond_to? :to_a + when Array from_h_array_gsubs(value, appdir) - elsif value.is_a? String + when String from_h_string_gsubs(value, appdir) else value diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index ed36e8843c..03e49a9744 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -64,7 +64,7 @@ module Cask def fetch(quiet: nil, timeout: nil) odebug "Cask::Installer#fetch" - load_cask_from_source_api! if @cask.loaded_from_api && @cask.caskfile_only? + load_cask_from_source_api! if @cask.loaded_from_api? && @cask.caskfile_only? verify_has_sha if require_sha? && !force? @@ -382,7 +382,7 @@ module Cask return if @cask.source.blank? - extension = @cask.loaded_from_api ? "json" : "rb" + extension = @cask.loaded_from_api? ? "json" : "rb" (metadata_subdir/"#{@cask.token}.#{extension}").write @cask.source old_savedir&.rmtree end @@ -559,7 +559,7 @@ module Cask end end - load_cask_from_source_api! if @cask.loaded_from_api && @cask.caskfile_only? + load_cask_from_source_api! if @cask.loaded_from_api? && @cask.caskfile_only? # otherwise we default to the current cask end diff --git a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb index a6e9afddf0..358016c9db 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb @@ -66,7 +66,7 @@ describe Cask::CaskLoader::FromAPILoader, :cask do it "loads from JSON API" do expect(cask_from_api).to be_a(Cask::Cask) expect(cask_from_api.token).to eq(cask_token) - expect(cask_from_api.loaded_from_api).to be(true) + expect(cask_from_api.loaded_from_api?).to be(true) expect(cask_from_api.caskfile_only?).to be(caskfile_only) end end diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 6927a656ca..eb90f5090c 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -41,6 +41,12 @@ describe Cask::Cask, :cask do expect(c.token).to eq("local-caffeine") end + it "returns an instance of the Cask from a JSON file" do + c = Cask::CaskLoader.load("#{tap_path}/caffeine.json") + expect(c).to be_a(described_class) + expect(c.token).to eq("caffeine") + end + it "returns an instance of the Cask from a URL" do c = Cask::CaskLoader.load("file://#{tap_path}/Casks/local-caffeine.rb") expect(c).to be_a(described_class) @@ -49,9 +55,7 @@ describe Cask::Cask, :cask do it "raises an error when failing to download a Cask from a URL" do expect { - url = "file://#{tap_path}/Casks/notacask.rb" - - Cask::CaskLoader.load(url) + Cask::CaskLoader.load("file://#{tap_path}/Casks/notacask.rb") }.to raise_error(Cask::CaskUnavailableError) end @@ -212,6 +216,33 @@ describe Cask::Cask, :cask do end end + describe "#to_h" do + let(:json_file) { "#{TEST_FIXTURE_DIR}/cask/everything.json" } + let(:expected_json) { File.read(json_file).strip } + + context "when loaded from cask file" do + it "returns expected hash" do + hash = Cask::CaskLoader.load("everything").to_h + + expect(hash).to be_a(Hash) + expect(JSON.pretty_generate(hash)).to eq(expected_json) + end + end + + context "when loaded from json file" do + it "returns expected hash" do + expect(Homebrew::API::Cask).not_to receive(:fetch_source) + hash = Cask::CaskLoader::FromAPILoader + .new("everything", from_json: JSON.parse(expected_json)) + .load(config: nil) + .to_h + + expect(hash).to be_a(Hash) + expect(JSON.pretty_generate(hash)).to eq(expected_json) + end + end + end + describe "#to_hash_with_variations" do let!(:original_macos_version) { MacOS.full_version.to_s } let(:expected_versions_variations) { diff --git a/Library/Homebrew/test/cask/installer_spec.rb b/Library/Homebrew/test/cask/installer_spec.rb index b120ef9ad4..90dfddf437 100644 --- a/Library/Homebrew/test/cask/installer_spec.rb +++ b/Library/Homebrew/test/cask/installer_spec.rb @@ -244,7 +244,7 @@ describe Cask::Installer, :cask do expect(Homebrew::API::Cask).to receive(:fetch_source).once.and_return(content) caffeine = Cask::CaskLoader.load(path) - expect(caffeine).to receive(:loaded_from_api).once.and_return(true) + expect(caffeine).to receive(:loaded_from_api?).once.and_return(true) expect(caffeine).to receive(:caskfile_only?).once.and_return(true) described_class.new(caffeine).install @@ -299,7 +299,7 @@ describe Cask::Installer, :cask do expect(Homebrew::API::Cask).to receive(:fetch_source).twice.and_return(content) caffeine = Cask::CaskLoader.load(path) - expect(caffeine).to receive(:loaded_from_api).twice.and_return(true) + expect(caffeine).to receive(:loaded_from_api?).twice.and_return(true) expect(caffeine).to receive(:caskfile_only?).twice.and_return(true) expect(caffeine).to receive(:installed_caskfile).once.and_return(invalid_path) diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/everything.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/everything.rb new file mode 100644 index 0000000000..b253c3d787 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/everything.rb @@ -0,0 +1,48 @@ +# Used to test cask hash generation. +cask "everything" do + version "1.2.3" + + language "en", default: true do + sha256 "c64c05bdc0be845505d6e55e69e696a7f50d40846e76155f0c85d5ff5e7bbb84" + "en-US" + end + language "eo" do + sha256 "e8ffa07370a7fb7e1696b04c269e01d3459725965a32facdd54629a95d148908" + "eo" + end + + url "https://cachefly.everything.app/releases/Everything_#{version}.zip", + user_agent: :fake, + cookies: { "ALL" => "1234" } + name "Everything" + desc "Little bit of everything" + homepage "https://www.everything.app/" + + auto_updates true + conflicts_with formula: "nothing" + depends_on cask: "something" + container type: :naked + + app "Everything.app" + installer script: { + executable: "~/just/another/path/install.sh", + args: ["--mode=silent"], + sudo: true, + print_stderr: false, + } + + uninstall launchctl: "com.every.thing.agent", + delete: ["/Library/EverythingHelperTools"], + kext: "com.every.thing.driver", + signal: [ + ["TERM", "com.every.thing.controller#{version.major}"], + ["TERM", "com.every.thing.bin"], + ] + + zap trash: [ + "~/.everything", + "~/Library/Everything", + ] + + caveats "Installing everything might take a while..." +end diff --git a/Library/Homebrew/test/support/fixtures/cask/caffeine.json b/Library/Homebrew/test/support/fixtures/cask/caffeine.json new file mode 100644 index 0000000000..0907739b63 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/caffeine.json @@ -0,0 +1,42 @@ +{ + "token": "caffeine", + "full_token": "caffeine", + "tap": null, + "name": [ + + ], + "desc": null, + "homepage": "https://brew.sh/", + "url": "https://www.example.com/cask/caffeine.zip", + "appcast": null, + "version": "1.2.3", + "versions": { + }, + "installed": null, + "outdated": false, + "sha256": "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94", + "artifacts": [ + { + "app": [ + "Caffeine.app" + ] + }, + { + "zap": [ + { + "trash": "/$HOME/support/fixtures/cask/caffeine/org.example.caffeine.plist" + } + ] + } + ], + "caveats": null, + "depends_on": { + }, + "conflicts_with": null, + "container": null, + "auto_updates": null, + "tap_git_head": null, + "languages": [ + + ] +} diff --git a/Library/Homebrew/test/support/fixtures/cask/everything.json b/Library/Homebrew/test/support/fixtures/cask/everything.json new file mode 100644 index 0000000000..9ac6b3d522 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/everything.json @@ -0,0 +1,99 @@ +{ + "token": "everything", + "full_token": "everything", + "tap": "homebrew/cask", + "name": [ + "Everything" + ], + "desc": "Little bit of everything", + "homepage": "https://www.everything.app/", + "url": "https://cachefly.everything.app/releases/Everything_1.2.3.zip", + "url_specs": { + "cookies": { + "ALL": "1234" + }, + "user_agent": ":fake" + }, + "appcast": null, + "version": "1.2.3", + "versions": { + }, + "installed": null, + "outdated": false, + "sha256": "c64c05bdc0be845505d6e55e69e696a7f50d40846e76155f0c85d5ff5e7bbb84", + "artifacts": [ + { + "uninstall": [ + { + "launchctl": "com.every.thing.agent", + "delete": [ + "/Library/EverythingHelperTools" + ], + "kext": "com.every.thing.driver", + "signal": [ + [ + "TERM", + "com.every.thing.controller1" + ], + [ + "TERM", + "com.every.thing.bin" + ] + ] + } + ] + }, + { + "installer": [ + { + "script": { + "executable": "~/just/another/path/install.sh", + "args": [ + "--mode=silent" + ], + "sudo": true, + "print_stderr": false + } + } + ] + }, + { + "app": [ + "Everything.app" + ] + }, + { + "zap": [ + { + "trash": [ + "~/.everything", + "~/Library/Everything" + ] + } + ] + } + ], + "caveats": "Installing everything might take a while...\n\neverything requires a kernel extension to work.\nIf the installation fails, retry after you enable it in:\n System Preferences → Security & Privacy → General\n\nFor more information, refer to vendor documentation or this Apple Technical Note:\n https://developer.apple.com/library/content/technotes/tn2459/_index.html\n", + "depends_on": { + "cask": [ + "something" + ] + }, + "conflicts_with": { + "formula": [ + "nothing" + ] + }, + "container": { + "type": "naked" + }, + "auto_updates": true, + "tap_git_head": null, + "languages": [ + "en", + "eo" + ], + "ruby_source_checksum": { + "sha256": "b2707d1952f02c3fa566b7ad2a707a847a959d36f51d3dee642dbe5deec12f27" + } +}