diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 8e1a3994d0..c09fb2b000 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -68,7 +68,8 @@ module Homebrew if download_queue unless skip_download - download = Homebrew::API::Download.new(url, nil, cache: HOMEBREW_CACHE_API, require_checksum: false) + require "api/json_download" + download = Homebrew::API::JSONDownload.new(endpoint, target:, stale_seconds:) download_queue.enqueue(download) end return [{}, false] diff --git a/Library/Homebrew/api/cask.rb b/Library/Homebrew/api/cask.rb index f5d762b136..cc72ff13ef 100644 --- a/Library/Homebrew/api/cask.rb +++ b/Library/Homebrew/api/cask.rb @@ -3,7 +3,7 @@ require "cachable" require "api" -require "api/download" +require "api/source_download" require "download_queue" module Homebrew @@ -36,7 +36,7 @@ module Homebrew git_head = cask.tap_git_head || "HEAD" tap = cask.tap&.full_name || "Homebrew/homebrew-cask" - download = Homebrew::API::Download.new( + download = Homebrew::API::SourceDownload.new( "https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}", checksum, mirrors: [ diff --git a/Library/Homebrew/api/download.rb b/Library/Homebrew/api/download.rb deleted file mode 100644 index db20a37d0a..0000000000 --- a/Library/Homebrew/api/download.rb +++ /dev/null @@ -1,69 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "downloadable" - -module Homebrew - module API - class DownloadStrategy < CurlDownloadStrategy - sig { override.returns(Pathname) } - def symlink_location - cache/name - end - end - - class Download - include Downloadable - - sig { - params( - url: String, - checksum: T.nilable(Checksum), - mirrors: T::Array[String], - cache: T.nilable(Pathname), - require_checksum: T::Boolean, - ).void - } - def initialize(url, checksum, mirrors: [], cache: nil, require_checksum: true) - super() - @url = T.let(URL.new(url, using: API::DownloadStrategy), URL) - @checksum = checksum - @mirrors = mirrors - @cache = cache - @require_checksum = require_checksum - end - - sig { override.returns(API::DownloadStrategy) } - def downloader - T.cast(super, API::DownloadStrategy) - end - - sig { override.returns(String) } - def name - download_name - end - - sig { override.returns(String) } - def download_type - "API source" - end - - sig { override.returns(Pathname) } - def cache - @cache || super - end - - sig { returns(Pathname) } - def symlink_location - downloader.symlink_location - end - - private - - sig { override.returns(T::Boolean) } - def silence_checksum_missing_error? - !@require_checksum - end - end - end -end diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index 59682a7a95..ba02f8a086 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -3,7 +3,7 @@ require "cachable" require "api" -require "api/download" +require "api/source_download" require "download_queue" module Homebrew @@ -34,7 +34,7 @@ module Homebrew git_head = formula.tap_git_head || "HEAD" tap = formula.tap&.full_name || "Homebrew/homebrew-core" - download = Homebrew::API::Download.new( + download = Homebrew::API::SourceDownload.new( "https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}", formula.ruby_source_checksum, cache: HOMEBREW_CACHE_API_SOURCE/"#{tap}/#{git_head}/Formula", diff --git a/Library/Homebrew/api/json_download.rb b/Library/Homebrew/api/json_download.rb new file mode 100644 index 0000000000..b2f28252a9 --- /dev/null +++ b/Library/Homebrew/api/json_download.rb @@ -0,0 +1,53 @@ +# typed: strict +# frozen_string_literal: true + +require "downloadable" + +module Homebrew + module API + class JSONDownloadStrategy < AbstractDownloadStrategy + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } + def initialize(url, name, version, **meta) + super + @target = T.let(meta.fetch(:target), Pathname) + @stale_seconds = T.let(meta.fetch(:stale_seconds), Integer) + end + + sig { override.params(timeout: T.nilable(T.any(Integer, Float))).returns(Pathname) } + def fetch(timeout: nil) + with_context quiet: quiet? do + Homebrew::API.fetch_json_api_file(url, target: cached_location, stale_seconds: meta.fetch(:stale_seconds)) + end + cached_location + end + + sig { override.returns(Pathname) } + def cached_location + meta.fetch(:target) + end + end + + class JSONDownload + include Downloadable + + sig { params(url: String, target: Pathname, stale_seconds: Integer).void } + def initialize(url, target:, stale_seconds:) + super() + @url = T.let(URL.new(url, using: API::JSONDownloadStrategy, target:, stale_seconds:), URL) + @target = target + @stale_seconds = stale_seconds + end + + sig { override.returns(API::JSONDownloadStrategy) } + def downloader + T.cast(super, API::JSONDownloadStrategy) + end + + sig { override.returns(String) } + def name = download_name + + sig { override.returns(String) } + def download_type = "JSON API" + end + end +end diff --git a/Library/Homebrew/api/source_download.rb b/Library/Homebrew/api/source_download.rb new file mode 100644 index 0000000000..271375f8c2 --- /dev/null +++ b/Library/Homebrew/api/source_download.rb @@ -0,0 +1,56 @@ +# typed: strict +# frozen_string_literal: true + +require "downloadable" + +module Homebrew + module API + class SourceDownloadStrategy < CurlDownloadStrategy + sig { override.returns(Pathname) } + def symlink_location + cache/name + end + end + + class SourceDownload + include Downloadable + + sig { + params( + url: String, + checksum: T.nilable(Checksum), + mirrors: T::Array[String], + cache: T.nilable(Pathname), + ).void + } + def initialize(url, checksum, mirrors: [], cache: nil) + super() + @url = T.let(URL.new(url, using: API::SourceDownloadStrategy), URL) + @checksum = checksum + @mirrors = mirrors + @cache = cache + end + + sig { override.returns(API::SourceDownloadStrategy) } + def downloader + T.cast(super, API::SourceDownloadStrategy) + end + + sig { override.returns(String) } + def name = download_name + + sig { override.returns(String) } + def download_type = "API Source" + + sig { override.returns(Pathname) } + def cache + @cache || super + end + + sig { returns(Pathname) } + def symlink_location + downloader.symlink_location + end + end + end +end diff --git a/Library/Homebrew/bottle.rb b/Library/Homebrew/bottle.rb index a2351fd89c..07944d8415 100644 --- a/Library/Homebrew/bottle.rb +++ b/Library/Homebrew/bottle.rb @@ -185,6 +185,9 @@ class Bottle end end + sig { override.returns(String) } + def download_type = "Bottle" + private def select_download_strategy(specs) diff --git a/Library/Homebrew/cask/download.rb b/Library/Homebrew/cask/download.rb index 0c73efd33a..7de305ee67 100644 --- a/Library/Homebrew/cask/download.rb +++ b/Library/Homebrew/cask/download.rb @@ -95,14 +95,10 @@ module Cask end sig { override.returns(String) } - def download_name - cask.token - end + def download_name = cask.token sig { override.returns(String) } - def download_type - "cask" - end + def download_type = "Cask" private diff --git a/Library/Homebrew/download_queue.rb b/Library/Homebrew/download_queue.rb index d616352043..4767a3e1b7 100644 --- a/Library/Homebrew/download_queue.rb +++ b/Library/Homebrew/download_queue.rb @@ -21,16 +21,10 @@ module Homebrew sig { params(downloadable: Downloadable).void } def enqueue(downloadable) downloads[downloadable] ||= Concurrent::Promises.future_on( - pool, RetryableDownload.new(downloadable, tries:), force, quiet + pool, RetryableDownload.new(downloadable, tries:, pour:), force, quiet ) do |download, force, quiet| download.clear_cache if force download.fetch(quiet:) - if pour && download.bottle? - UnpackStrategy.detect(download.cached_download, prioritize_extension: true) - .extract_nestedly(to: HOMEBREW_CELLAR) - elsif download.api? - FileUtils.touch(download.cached_download, mtime: Time.now) - end end end @@ -42,7 +36,7 @@ module Homebrew downloads.each do |downloadable, promise| promise.wait! rescue ChecksumMismatchError => e - opoo "#{downloadable.download_type.capitalize} reports different checksum: #{e.expected}" + opoo "#{downloadable.download_type} reports different checksum: #{e.expected}" Homebrew.failed = true if downloadable.is_a?(Resource::Patch) end else @@ -66,13 +60,13 @@ module Homebrew raise future.state.to_s end - message = "#{downloadable.download_type.capitalize} #{downloadable.name}" + message = "#{downloadable.download_type} #{downloadable.name}" $stdout.print "#{status} #{message}#{"\n" unless last}" $stdout.flush if future.rejected? if (e = future.reason).is_a?(ChecksumMismatchError) - opoo "#{downloadable.download_type.capitalize} reports different checksum: #{e.expected}" + opoo "#{downloadable.download_type} reports different checksum: #{e.expected}" Homebrew.failed = true if downloadable.is_a?(Resource::Patch) next 2 else diff --git a/Library/Homebrew/resource.rb b/Library/Homebrew/resource.rb index a774f659df..383a68e825 100644 --- a/Library/Homebrew/resource.rb +++ b/Library/Homebrew/resource.rb @@ -235,6 +235,9 @@ class Resource @url&.specs || {}.freeze end + sig { override.returns(String) } + def download_type = "Resource" + protected def stage_resource(prefix, debug_symbols: false, &block) @@ -339,6 +342,9 @@ class Resource manifest_annotations["sh.brew.bottle.installed_size"]&.to_i end + sig { override.returns(String) } + def download_type = "Bottle Manifest" + private def manifest_annotations @@ -391,6 +397,9 @@ class Resource @directory = val end + + sig { override.returns(String) } + def download_type = "Patch" end end diff --git a/Library/Homebrew/retryable_download.rb b/Library/Homebrew/retryable_download.rb index 6f26ec192c..f28e614893 100644 --- a/Library/Homebrew/retryable_download.rb +++ b/Library/Homebrew/retryable_download.rb @@ -1,6 +1,9 @@ # typed: strict # frozen_string_literal: true +require "bottle" +require "api/json_download" + module Homebrew class RetryableDownload include Downloadable @@ -14,13 +17,14 @@ module Homebrew sig { override.returns(T::Array[String]) } def mirrors = downloadable.mirrors - sig { params(downloadable: Downloadable, tries: Integer).void } - def initialize(downloadable, tries:) + sig { params(downloadable: Downloadable, tries: Integer, pour: T::Boolean).void } + def initialize(downloadable, tries:, pour: false) super() @downloadable = downloadable @try = T.let(0, Integer) @tries = tries + @pour = pour end sig { override.returns(String) } @@ -65,7 +69,15 @@ module Homebrew puts "SHA256: #{download.sha256}" end - downloadable.verify_download_integrity(download) if verify_download_integrity + json_download = downloadable.is_a?(API::JSONDownload) + downloadable.verify_download_integrity(download) if verify_download_integrity && !json_download + + if pour && downloadable.is_a?(Bottle) + UnpackStrategy.detect(download, prioritize_extension: true) + .extract_nestedly(to: HOMEBREW_CELLAR) + elsif json_download + FileUtils.touch(download, mtime: Time.now) + end download rescue DownloadError, ChecksumMismatchError, Resource::BottleManifest::Error @@ -89,15 +101,12 @@ module Homebrew sig { override.returns(String) } def download_name = downloadable.download_name - sig { returns(T::Boolean) } - def bottle? = downloadable.is_a?(Bottle) - - sig { returns(T::Boolean) } - def api? = downloadable.is_a?(API::Download) - private sig { returns(Downloadable) } attr_reader :downloadable + + sig { returns(T::Boolean) } + attr_reader :pour end end diff --git a/Library/Homebrew/software_spec.rb b/Library/Homebrew/software_spec.rb index a6172182cb..4d773d00f3 100644 --- a/Library/Homebrew/software_spec.rb +++ b/Library/Homebrew/software_spec.rb @@ -82,9 +82,7 @@ class SoftwareSpec end sig { override.returns(String) } - def download_type - "formula" - end + def download_type = "Formula" def owner=(owner) @name = owner.name diff --git a/Library/Homebrew/test/api/cask_spec.rb b/Library/Homebrew/test/api/cask_spec.rb index 372027293f..e2717cdf18 100644 --- a/Library/Homebrew/test/api/cask_spec.rb +++ b/Library/Homebrew/test/api/cask_spec.rb @@ -55,14 +55,14 @@ RSpec.describe Homebrew::API::Cask do before do allow(Homebrew::API).to receive(:fetch_json_api_file).and_return([[], true]) - allow_any_instance_of(Homebrew::API::Download).to receive(:fetch) - allow_any_instance_of(Homebrew::API::Download).to receive(:symlink_location).and_return( + allow_any_instance_of(Homebrew::API::SourceDownload).to receive(:fetch) + allow_any_instance_of(Homebrew::API::SourceDownload).to receive(:symlink_location).and_return( TEST_FIXTURE_DIR/"cask/Casks/everything.rb", ) end it "specifies the correct URL and sha256" do - expect(Homebrew::API::Download).to receive(:new).with( + expect(Homebrew::API::SourceDownload).to receive(:new).with( "https://raw.githubusercontent.com/Homebrew/homebrew-cask/abcdef1234567890abcdef1234567890abcdef12/Casks/everything.rb", Checksum.new("d8d0d6b2e5ff65388eccb82236fd3aa157b4a29bb043a1f72b97f0e9b70e8320"), any_args,