diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 38d2748db5..53b0bf1dd7 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -4,6 +4,7 @@ require "api/analytics" require "api/bottle" require "api/cask" +require "api/cask-source" require "api/formula" require "api/versions" require "extend/cachable" @@ -21,15 +22,19 @@ module Homebrew API_DOMAIN = "https://formulae.brew.sh/api" - sig { params(endpoint: String).returns(T.any(String, Hash)) } - def fetch(endpoint) + sig { params(endpoint: String, json: T::Boolean).returns(T.any(String, Hash)) } + def fetch(endpoint, json: true) return cache[endpoint] if cache.present? && cache.key?(endpoint) api_url = "#{API_DOMAIN}/#{endpoint}" output = Utils::Curl.curl_output("--fail", "--max-time", "5", api_url) raise ArgumentError, "No file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success? - cache[endpoint] = JSON.parse(output.stdout) + cache[endpoint] = if json + JSON.parse(output.stdout) + else + output.stdout + end rescue JSON::ParserError raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}" end diff --git a/Library/Homebrew/api/cask-source.rb b/Library/Homebrew/api/cask-source.rb new file mode 100644 index 0000000000..86e380e252 --- /dev/null +++ b/Library/Homebrew/api/cask-source.rb @@ -0,0 +1,28 @@ +# typed: false +# frozen_string_literal: true + +module Homebrew + module API + # Helper functions for using the cask source API. + # + # @api private + module CaskSource + class << self + extend T::Sig + + sig { params(token: String).returns(Hash) } + def fetch(token) + Homebrew::API.fetch "cask-source/#{token}.rb", json: false + end + + sig { params(token: String).returns(T::Boolean) } + def available?(token) + fetch token + true + rescue ArgumentError + false + end + end + end + end +end diff --git a/Library/Homebrew/api/versions.rb b/Library/Homebrew/api/versions.rb index 0bfc51c505..3c18824348 100644 --- a/Library/Homebrew/api/versions.rb +++ b/Library/Homebrew/api/versions.rb @@ -44,7 +44,12 @@ module Homebrew def latest_cask_version(token) return unless casks.key? token - Version.new(casks[token]["version"]) + version = if casks[token]["versions"].key? MacOS.version.to_sym.to_s + casks[token]["versions"][MacOS.version.to_sym.to_s] + else + casks[token]["version"] + end + Version.new(version) end end end diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index d7f811d263..2575b04530 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -6,6 +6,7 @@ require "cask/config" require "cask/dsl" require "cask/metadata" require "searchable" +require "api" module Cask # An instance of a cask. @@ -19,7 +20,7 @@ module Cask extend Searchable include Metadata - attr_reader :token, :sourcefile_path, :config, :default_config + attr_reader :token, :sourcefile_path, :source, :config, :default_config def self.each(&block) return to_enum unless block @@ -37,9 +38,10 @@ module Cask @tap end - def initialize(token, sourcefile_path: nil, tap: nil, config: nil, &block) + def initialize(token, sourcefile_path: nil, source: nil, tap: nil, config: nil, &block) @token = token @sourcefile_path = sourcefile_path + @source = source @tap = tap @block = block @@ -147,14 +149,21 @@ module Cask return [] end + latest_version = if ENV["HOMEBREW_JSON_CORE"].present? && + (latest_cask_version = Homebrew::API::Versions.latest_cask_version(token)) + DSL::Version.new latest_cask_version.to_s + else + version + end + installed = versions current = installed.last # not outdated unless there is a different version on tap - return [] if current == version + return [] if current == latest_version # collect all installed versions that are different than tap version and return them - installed.reject { |v| v == version } + installed.reject { |v| v == latest_version } end def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates) diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index fa7eed16ae..0b231fcbc9 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -40,7 +40,7 @@ module Cask private def cask(header_token, **options, &block) - Cask.new(header_token, **options, config: @config, &block) + Cask.new(header_token, source: content, **options, config: @config, &block) end end diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index ec1a004058..9377f89958 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -399,10 +399,10 @@ module Cask def save_caskfile old_savedir = @cask.metadata_timestamped_path - return unless @cask.sourcefile_path + return if @cask.source.blank? savedir = @cask.metadata_subdir("Casks", timestamp: :now, create: true) - FileUtils.copy @cask.sourcefile_path, savedir + (savedir/"#{@cask.token}.rb").write @cask.source old_savedir&.rmtree end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 79c7fd822d..7ef5b5a695 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -45,18 +45,18 @@ module Homebrew # the formula and prints a warning unless `only` is specified. sig { params( - only: T.nilable(Symbol), - ignore_unavailable: T.nilable(T::Boolean), - method: T.nilable(Symbol), - uniq: T::Boolean, - prefer_loading_from_json: T::Boolean, + only: T.nilable(Symbol), + ignore_unavailable: T.nilable(T::Boolean), + method: T.nilable(Symbol), + uniq: T::Boolean, + prefer_loading_from_api: T::Boolean, ).returns(T::Array[T.any(Formula, Keg, Cask::Cask)]) } def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true, - prefer_loading_from_json: false) + prefer_loading_from_api: false) @to_formulae_and_casks ||= {} @to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name| - load_formula_or_cask(name, only: only, method: method, prefer_loading_from_json: prefer_loading_from_json) + load_formula_or_cask(name, only: only, method: method, prefer_loading_from_api: prefer_loading_from_api) rescue FormulaUnreadableError, FormulaClassUnavailableError, TapFormulaUnreadableError, TapFormulaClassUnavailableError, Cask::CaskUnreadableError @@ -90,11 +90,11 @@ module Homebrew end.uniq.freeze end - def load_formula_or_cask(name, only: nil, method: nil, prefer_loading_from_json: false) + def load_formula_or_cask(name, only: nil, method: nil, prefer_loading_from_api: false) unreadable_error = nil if only != :cask - if prefer_loading_from_json && ENV["HOMEBREW_JSON_CORE"].present? && + if prefer_loading_from_api && ENV["HOMEBREW_JSON_CORE"].present? && Homebrew::API::Bottle.available?(name) Homebrew::API::Bottle.fetch_bottles(name) end @@ -133,9 +133,14 @@ module Homebrew end if only != :formula + if prefer_loading_from_api && ENV["HOMEBREW_JSON_CORE"].present? && + Homebrew::API::CaskSource.available?(name) + contents = Homebrew::API::CaskSource.fetch(name) + end + begin config = Cask::Config.from_args(@parent) if @cask_options - cask = Cask::CaskLoader.load(name, config: config) + cask = Cask::CaskLoader.load(contents || name, config: config) if unreadable_error.present? onoe <<~EOS diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index e04ced8c99..7c102bb598 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -155,7 +155,7 @@ module Homebrew end begin - formulae, casks = args.named.to_formulae_and_casks(prefer_loading_from_json: true) + formulae, casks = args.named.to_formulae_and_casks(prefer_loading_from_api: true) .partition { |formula_or_cask| formula_or_cask.is_a?(Formula) } rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?) diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index c95693917e..791b299ed3 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -85,6 +85,9 @@ module Homebrew def reinstall args = reinstall_args.parse + # We need to use the bottle API instead of just using the formula file + # from an installed keg because it will not contain bottle information. + # As a consequence, `brew reinstall` will also upgrade outdated formulae if ENV["HOMEBREW_JSON_CORE"].present? args.named.each do |name| formula = Formulary.factory(name) diff --git a/Library/Homebrew/cmd/update.sh b/Library/Homebrew/cmd/update.sh index cc4ae0d1c2..4c7918ab39 100644 --- a/Library/Homebrew/cmd/update.sh +++ b/Library/Homebrew/cmd/update.sh @@ -648,7 +648,8 @@ EOS # HOMEBREW_UPDATE_PREINSTALL wasn't modified in subshell. # shellcheck disable=SC2031 if [[ -n "${HOMEBREW_JSON_CORE}" ]] && [[ -n "${HOMEBREW_UPDATE_PREINSTALL}" ]] && - [[ "${DIR}" = "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-core" ]] + [[ "${DIR}" = "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-core" || + "${DIR}" = "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask" ]] then continue fi diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index cbc10fb6b1..c673975f63 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -225,6 +225,15 @@ module Homebrew def upgrade_outdated_casks(casks, args:) return false if args.formula? + if ENV["HOMEBREW_JSON_CORE"].present? + casks = casks.map do |cask| + next cask if cask.tap.present? && cask.tap != "homebrew/cask" + next cask unless Homebrew::API::CaskSource.available?(cask.token) + + Cask::CaskLoader.load Homebrew::API::CaskSource.fetch(cask.token) + end + end + Cask::Cmd::Upgrade.upgrade_casks( *casks, force: args.force?, diff --git a/Library/Homebrew/extend/os/mac/tap.rb b/Library/Homebrew/extend/os/mac/tap.rb index ca2701238a..d2e5b89212 100644 --- a/Library/Homebrew/extend/os/mac/tap.rb +++ b/Library/Homebrew/extend/os/mac/tap.rb @@ -4,7 +4,7 @@ class Tap def self.install_default_cask_tap_if_necessary(force: false) return false if default_cask_tap.installed? - + return false if ENV["HOMEBREW_JSON_CORE"].present? return false if !force && Tap.untapped_official_taps.include?(default_cask_tap.name) default_cask_tap.install diff --git a/Library/Homebrew/test/api/versions_spec.rb b/Library/Homebrew/test/api/versions_spec.rb index 28b66d65f6..c27ec833dd 100644 --- a/Library/Homebrew/test/api/versions_spec.rb +++ b/Library/Homebrew/test/api/versions_spec.rb @@ -4,22 +4,21 @@ require "api" describe Homebrew::API::Versions do - let(:versions_formulae_json) { - <<~EOS - { - "foo":{"version":"1.2.3","revision":0}, - "bar":{"version":"1.2","revision":4} - } - EOS - } - let(:versions_casks_json) { '{"foo":{"version":"1.2.3"}}' } - def mock_curl_output(stdout: "", success: true) curl_output = OpenStruct.new(stdout: stdout, success?: success) allow(Utils::Curl).to receive(:curl_output).and_return curl_output end describe "::latest_formula_version" do + let(:versions_formulae_json) { + <<~EOS + { + "foo":{"version":"1.2.3","revision":0}, + "bar":{"version":"1.2","revision":4} + } + EOS + } + it "returns the expected `PkgVersion` when the revision is 0" do mock_curl_output stdout: versions_formulae_json pkg_version = described_class.latest_formula_version("foo") @@ -39,16 +38,34 @@ describe Homebrew::API::Versions do end end - describe "::latest_cask_version" do + describe "::latest_cask_version", :needs_macos do + let(:versions_casks_json) { + <<~EOS + { + "foo":{"version":"1.2.3","versions":{}}, + "bar":{"version":"1.2.3","versions":{"#{MacOS.version.to_sym}":"1.2.0"}}, + "baz":{"version":"1.2.3","versions":{"test_os":"1.2.0"}} + } + EOS + } + it "returns the expected `Version`" do mock_curl_output stdout: versions_casks_json version = described_class.latest_cask_version("foo") expect(version.to_s).to eq "1.2.3" end - it "returns `nil` when the cask is not in the JSON file" do + it "returns the expected `Version` for an OS with a non-default version" do mock_curl_output stdout: versions_casks_json version = described_class.latest_cask_version("bar") + expect(version.to_s).to eq "1.2.0" + version = described_class.latest_cask_version("baz") + expect(version.to_s).to eq "1.2.3" + end + + it "returns `nil` when the cask is not in the JSON file" do + mock_curl_output stdout: versions_casks_json + version = described_class.latest_cask_version("doesnotexist") expect(version).to be_nil end end diff --git a/Library/Homebrew/test/api_spec.rb b/Library/Homebrew/test/api_spec.rb index 68a01074fc..6d70ef6626 100644 --- a/Library/Homebrew/test/api_spec.rb +++ b/Library/Homebrew/test/api_spec.rb @@ -14,6 +14,12 @@ describe Homebrew::API do end describe "::fetch" do + it "fetches a text file" do + mock_curl_output stdout: text + fetched_text = described_class.fetch("foo.txt", json: false) + expect(fetched_text).to eq text + end + it "fetches a JSON file" do mock_curl_output stdout: json fetched_json = described_class.fetch("foo.json")