Merge pull request #11859 from Rylan12/cask-json
Allow casks to be installed using the `cask-source` API
This commit is contained in:
		
						commit
						8690d661fd
					
				@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								Library/Homebrew/api/cask-source.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Library/Homebrew/api/cask-source.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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?)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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?,
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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")
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user