Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow casks to be installed using the cask-source API #11859

Merged
merged 6 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions Library/Homebrew/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,15 +22,19 @@ module API

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
Expand Down
28 changes: 28 additions & 0 deletions Library/Homebrew/api/cask-source.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 13 additions & 4 deletions Library/Homebrew/cask/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "cask/dsl"
require "cask/metadata"
require "searchable"
require "api"

module Cask
# An instance of a cask.
Expand All @@ -19,7 +20,7 @@ class 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
Expand All @@ -37,9 +38,10 @@ def tap
@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

Expand Down Expand Up @@ -130,14 +132,21 @@ def outdated_versions(greedy: false, greedy_latest: false, greedy_auto_updates:
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)
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/cask/cask_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def load(config:)
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

Expand Down
4 changes: 2 additions & 2 deletions Library/Homebrew/cask/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,10 @@ def caveats
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

Expand Down
25 changes: 15 additions & 10 deletions Library/Homebrew/cli/named_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,18 @@ def to_formulae
# 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
Expand Down Expand Up @@ -90,11 +90,11 @@ def to_formulae_and_casks_and_unavailable(only: parent&.only_formula_or_cask, me
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
Expand Down Expand Up @@ -133,9 +133,14 @@ def load_formula_or_cask(name, only: nil, method: nil, prefer_loading_from_json:
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
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/cmd/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def install
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?)
Expand Down
3 changes: 3 additions & 0 deletions Library/Homebrew/cmd/reinstall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def reinstall_args
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)
Expand Down
3 changes: 2 additions & 1 deletion Library/Homebrew/cmd/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Library/Homebrew/cmd/upgrade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ def upgrade_outdated_formulae(formulae, args:)
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?,
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/extend/os/mac/tap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Library/Homebrew/test/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ def mock_curl_output(stdout: "", success: true)
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")
Expand Down