Skip to content

Commit

Permalink
Refactor formula, cask and Ruby source downloads to use shared code
Browse files Browse the repository at this point in the history
  • Loading branch information
Bo98 committed Apr 27, 2023
1 parent 7386d4e commit 44f058e
Show file tree
Hide file tree
Showing 34 changed files with 765 additions and 538 deletions.
56 changes: 12 additions & 44 deletions Library/Homebrew/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module API
extend Cachable

HOMEBREW_CACHE_API = (HOMEBREW_CACHE/"api").freeze
HOMEBREW_CACHE_API_SOURCE = (HOMEBREW_CACHE/"api-source").freeze

sig { params(endpoint: String).returns(Hash) }
def self.fetch(endpoint)
Expand Down Expand Up @@ -114,50 +115,6 @@ def self.fetch_json_api_file(endpoint, target:)
end
end

sig {
params(name: String, path: T.any(Pathname, String), git_head: String,
sha256: T.nilable(String)).returns(String)
}
def self.fetch_homebrew_cask_source(name, path:, git_head:, sha256: nil)
# TODO: unify with formula logic (https://github.com/Homebrew/brew/issues/14746)
raw_endpoint = "#{git_head}/#{path}"
return cache[raw_endpoint] if cache.present? && cache.key?(raw_endpoint)

# This API sometimes returns random 404s so needs a fallback at formulae.brew.sh.
raw_source_url = "https://raw.githubusercontent.com/Homebrew/homebrew-cask/#{raw_endpoint}"
api_source_url = "#{HOMEBREW_API_DEFAULT_DOMAIN}/cask-source/#{name}.rb"

url = raw_source_url
output = Utils::Curl.curl_output("--fail", url)

if !output.success? || output.blank?
url = api_source_url
output = Utils::Curl.curl_output("--fail", url)
if !output.success? || output.blank?
raise ArgumentError, <<~EOS
No valid file found at either of:
#{Tty.underline}#{raw_source_url}#{Tty.reset}
#{Tty.underline}#{api_source_url}#{Tty.reset}
EOS
end
end

cask_source = output.stdout
actual_sha256 = Digest::SHA256.hexdigest(cask_source)
if sha256 && actual_sha256 != sha256
raise ArgumentError, <<~EOS
SHA256 mismatch
Expected: #{Formatter.success(sha256.to_s)}
Actual: #{Formatter.error(actual_sha256.to_s)}
URL: #{url}
Check if you can access the URL in your browser.
Regardless, try again in a few minutes.
EOS
end

cache[raw_endpoint] = cask_source
end

sig { params(json: Hash).returns(Hash) }
def self.merge_variations(json)
bottle_tag = ::Utils::Bottles::Tag.new(system: Homebrew::SimulateSystem.current_os,
Expand Down Expand Up @@ -207,5 +164,16 @@ def self.write_names_file(names, type, regenerate:)

[true, JSON.parse(json_data["payload"])]
end

sig { params(path: Pathname).returns(T.nilable(Tap)) }
def self.tap_from_source_download(path)
source_relative_path = path.relative_path_from(Homebrew::API::HOMEBREW_CACHE_API_SOURCE)
return if source_relative_path.to_s.start_with?("../")

org, repo = source_relative_path.each_filename.first(2)

Check warning on line 173 in Library/Homebrew/api.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api.rb#L173

Added line #L173 was not covered by tests
return if org.blank? || repo.blank?

Tap.fetch(org, repo)

Check warning on line 176 in Library/Homebrew/api.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api.rb#L176

Added line #L176 was not covered by tests
end
end
end
26 changes: 20 additions & 6 deletions Library/Homebrew/api/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "extend/cachable"
require "api/download"

module Homebrew
module API
Expand All @@ -19,12 +20,25 @@ def fetch(token)
Homebrew::API.fetch "cask/#{token}.json"
end

sig {
params(token: String, path: T.any(String, Pathname), git_head: String,
sha256: T.nilable(String)).returns(String)
}
def fetch_source(token, path:, git_head:, sha256: nil)
Homebrew::API.fetch_homebrew_cask_source token, path: path, git_head: git_head, sha256: sha256
sig { params(cask: ::Cask::Cask).returns(::Cask::Cask) }
def source_download(cask)
path = cask.ruby_source_path.to_s || "Casks/#{cask.token}.rb"
sha256 = cask.ruby_source_checksum[:sha256]

Check warning on line 26 in Library/Homebrew/api/cask.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/cask.rb#L25-L26

Added lines #L25 - L26 were not covered by tests
checksum = Checksum.new(sha256) if sha256
git_head = cask.tap_git_head || "HEAD"

Check warning on line 28 in Library/Homebrew/api/cask.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/cask.rb#L28

Added line #L28 was not covered by tests
tap = cask.tap&.full_name || "Homebrew/homebrew-cask"

download = Homebrew::API::Download.new(

Check warning on line 31 in Library/Homebrew/api/cask.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/cask.rb#L31

Added line #L31 was not covered by tests
"https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}",
checksum,
mirrors: [
"#{HOMEBREW_API_DEFAULT_DOMAIN}/cask-source/#{File.basename(path)}",
],
cache: HOMEBREW_CACHE_API_SOURCE/"#{tap}/#{git_head}/Cask",
)
download.fetch
::Cask::CaskLoader::FromPathLoader.new(download.symlink_location)

Check warning on line 40 in Library/Homebrew/api/cask.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/cask.rb#L39-L40

Added lines #L39 - L40 were not covered by tests
.load(config: cask.config)
end

sig { returns(T::Boolean) }
Expand Down
45 changes: 45 additions & 0 deletions Library/Homebrew/api/download.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# typed: true
# frozen_string_literal: true

require "downloadable"

module Homebrew
module API
# @api private
class DownloadStrategy < CurlDownloadStrategy
sig { override.returns(Pathname) }
def symlink_location
cache/name

Check warning on line 12 in Library/Homebrew/api/download.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/download.rb#L12

Added line #L12 was not covered by tests
end
end

# @api private
class Download < Downloadable
sig {
params(

Check warning on line 19 in Library/Homebrew/api/download.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/download.rb#L19

Added line #L19 was not covered by tests
url: String,
checksum: T.nilable(Checksum),
mirrors: T::Array[String],
cache: T.nilable(Pathname),
).void
}
def initialize(url, checksum, mirrors: [], cache: nil)
super()
@url = URL.new(url, using: API::DownloadStrategy)
@checksum = checksum
@mirrors = mirrors
@cache = cache

Check warning on line 31 in Library/Homebrew/api/download.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/download.rb#L27-L31

Added lines #L27 - L31 were not covered by tests
end

sig { override.returns(Pathname) }
def cache
@cache || super

Check warning on line 36 in Library/Homebrew/api/download.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/download.rb#L36

Added line #L36 was not covered by tests
end

sig { returns(Pathname) }
def symlink_location
T.cast(downloader, API::DownloadStrategy).symlink_location

Check warning on line 41 in Library/Homebrew/api/download.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/download.rb#L41

Added line #L41 was not covered by tests
end
end
end
end
19 changes: 19 additions & 0 deletions Library/Homebrew/api/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "extend/cachable"
require "api/download"

module Homebrew
module API
Expand All @@ -19,6 +20,24 @@ def fetch(name)
Homebrew::API.fetch "formula/#{name}.json"
end

sig { params(formula: ::Formula).returns(::Formula) }
def source_download(formula)
path = formula.ruby_source_path || "Formula/#{formula.name}.rb"
git_head = formula.tap_git_head || "HEAD"

Check warning on line 26 in Library/Homebrew/api/formula.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/formula.rb#L25-L26

Added lines #L25 - L26 were not covered by tests
tap = formula.tap&.full_name || "Homebrew/homebrew-core"

download = Homebrew::API::Download.new(

Check warning on line 29 in Library/Homebrew/api/formula.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/formula.rb#L29

Added line #L29 was not covered by tests
"https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}",
formula.ruby_source_checksum,
cache: HOMEBREW_CACHE_API_SOURCE/"#{tap}/#{git_head}/Formula",
)
download.fetch
Formulary.factory(download.symlink_location,

Check warning on line 35 in Library/Homebrew/api/formula.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/api/formula.rb#L34-L35

Added lines #L34 - L35 were not covered by tests
formula.active_spec_sym,
alias_path: formula.alias_path,
flags: formula.class.build_flags)
end

sig { returns(T::Boolean) }
def download_and_cache_data!
json_formulae, updated = Homebrew::API.fetch_json_api_file "formula.jws.json",
Expand Down
51 changes: 37 additions & 14 deletions Library/Homebrew/cask/cask_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,33 @@ def load(config:); end
end

# Loads a cask from a string.
class FromContentLoader
class AbstractContentLoader
include ILoader
attr_reader :content, :tap
extend T::Helpers
abstract!

sig { returns(String) }
attr_reader :content

sig { returns(T.nilable(Tap)) }
attr_reader :tap

private

sig {
overridable.params(
header_token: String,
options: T.untyped,
block: T.nilable(T.proc.bind(DSL).void),
).returns(Cask)
}
def cask(header_token, **options, &block)
Cask.new(header_token, source: content, tap: tap, **options, config: @config, &block)
end
end

# Loads a cask from a string.
class FromContentLoader < AbstractContentLoader
def self.can_load?(ref)
return false unless ref.respond_to?(:to_str)

Expand All @@ -42,6 +65,8 @@ def self.can_load?(ref)
end

def initialize(content, tap: nil)
super()

@content = content.force_encoding("UTF-8")
@tap = tap
end
Expand All @@ -51,28 +76,26 @@ def load(config:)

instance_eval(content, __FILE__, __LINE__)
end

private

def cask(header_token, **options, &block)
Cask.new(header_token, source: content, tap: tap, **options, config: @config, &block)
end
end

# Loads a cask from a path.
class FromPathLoader < FromContentLoader
class FromPathLoader < AbstractContentLoader
def self.can_load?(ref)
path = Pathname(ref)
%w[.rb .json].include?(path.extname) && path.expand_path.exist?
end

attr_reader :token, :path

def initialize(path) # rubocop:disable Lint/MissingSuper
def initialize(path, token: nil)
super()

path = Pathname(path).expand_path

@token = path.basename(path.extname).to_s

@path = path
@tap = Homebrew::API.tap_from_source_download(path)
end

def load(config:)
Expand Down Expand Up @@ -153,8 +176,8 @@ def self.can_load?(ref)
end

def initialize(path)
@tap = Tap.from_path(path)
super(path)
@tap = Tap.from_path(path)
end
end

Expand All @@ -172,7 +195,7 @@ def initialize(tapped_name)
end

def load(config:)
raise TapCaskUnavailableError.new(tap, token) unless tap.installed?
raise TapCaskUnavailableError.new(tap, token) unless T.must(tap).installed?

super
end
Expand Down Expand Up @@ -215,12 +238,12 @@ def self.can_load?(ref)
return false unless ref.is_a?(String)
return false unless ref.match?(HOMEBREW_MAIN_TAP_CASK_REGEX)

token = ref.delete_prefix("homebrew/cask/")
token = ref.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "")
Homebrew::API::Cask.all_casks.key?(token)
end

def initialize(token, from_json: nil)
@token = token.delete_prefix("homebrew/cask/")
@token = token.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "")
@path = CaskLoader.default_path(token)
@from_json = from_json
end
Expand Down

0 comments on commit 44f058e

Please sign in to comment.