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
|
||||
|
||||
|
||||
@ -49,14 +49,14 @@ module Homebrew
|
||||
ignore_unavailable: T.nilable(T::Boolean),
|
||||
method: T.nilable(Symbol),
|
||||
uniq: T::Boolean,
|
||||
prefer_loading_from_json: 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,6 +4,12 @@
|
||||
require "api"
|
||||
|
||||
describe Homebrew::API::Versions do
|
||||
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
|
||||
{
|
||||
@ -12,14 +18,7 @@ describe Homebrew::API::Versions do
|
||||
}
|
||||
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
|
||||
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