diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index c865ad13ff..8b83014cd7 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -109,8 +109,9 @@ module Homebrew end end - sig { params(name: String, git_head: T.nilable(String)).returns(String) } - def self.fetch_homebrew_cask_source(name, git_head: nil) + sig { params(name: String, git_head: T.nilable(String), sha256: T.nilable(String)).returns(String) } + def self.fetch_homebrew_cask_source(name, git_head: nil, sha256: nil) + # TODO: unify with formula logic (https://github.com/Homebrew/brew/issues/14746) git_head = "master" if git_head.blank? raw_endpoint = "#{git_head}/Casks/#{name}.rb" return cache[raw_endpoint] if cache.present? && cache.key?(raw_endpoint) @@ -118,10 +119,13 @@ module Homebrew # 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" - output = Utils::Curl.curl_output("--fail", raw_source_url) + + url = raw_source_url + output = Utils::Curl.curl_output("--fail", url) if !output.success? || output.blank? - output = Utils::Curl.curl_output("--fail", api_source_url) + 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: @@ -131,7 +135,20 @@ module Homebrew end end - cache[raw_endpoint] = output.stdout + 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) } diff --git a/Library/Homebrew/api/cask.rb b/Library/Homebrew/api/cask.rb index a4fbcb4646..ccec8f083e 100644 --- a/Library/Homebrew/api/cask.rb +++ b/Library/Homebrew/api/cask.rb @@ -20,9 +20,9 @@ module Homebrew Homebrew::API.fetch "cask/#{token}.json" end - sig { params(token: String, git_head: T.nilable(String)).returns(String) } - def fetch_source(token, git_head: nil) - Homebrew::API.fetch_homebrew_cask_source token, git_head: git_head + sig { params(token: String, git_head: T.nilable(String), sha256: T.nilable(String)).returns(String) } + def fetch_source(token, git_head: nil, sha256: nil) + Homebrew::API.fetch_homebrew_cask_source token, git_head: git_head, sha256: sha256 end sig { returns(T::Boolean) } diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index 67d8d44f03..607bf5ce32 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -83,11 +83,12 @@ module Cask @tap end - def initialize(token, sourcefile_path: nil, source: nil, tap: nil, config: nil, - allow_reassignment: false, loaded_from_api: false, loader: nil, &block) + def initialize(token, sourcefile_path: nil, source: nil, source_checksum: nil, tap: nil, + config: nil, allow_reassignment: false, loaded_from_api: false, loader: nil, &block) @token = token @sourcefile_path = sourcefile_path @source = source + @ruby_source_checksum = source_checksum @tap = tap @allow_reassignment = allow_reassignment @loaded_from_api = loaded_from_api @@ -277,27 +278,28 @@ module Cask end { - "token" => token, - "full_token" => full_name, - "tap" => tap&.name, - "name" => name, - "desc" => desc, - "homepage" => homepage, - "url" => url, - "appcast" => appcast, - "version" => version, - "versions" => os_versions, - "installed" => versions.last, - "outdated" => outdated?, - "sha256" => sha256, - "artifacts" => artifacts_list, - "caveats" => (caveats unless caveats.empty?), - "depends_on" => depends_on, - "conflicts_with" => conflicts_with, - "container" => container&.pairs, - "auto_updates" => auto_updates, - "tap_git_head" => tap&.git_head, - "languages" => languages, + "token" => token, + "full_token" => full_name, + "tap" => tap&.name, + "name" => name, + "desc" => desc, + "homepage" => homepage, + "url" => url, + "appcast" => appcast, + "version" => version, + "versions" => os_versions, + "installed" => versions.last, + "outdated" => outdated?, + "sha256" => sha256, + "artifacts" => artifacts_list, + "caveats" => (caveats unless caveats.empty?), + "depends_on" => depends_on, + "conflicts_with" => conflicts_with, + "container" => container&.pairs, + "auto_updates" => auto_updates, + "tap_git_head" => tap&.git_head, + "languages" => languages, + "ruby_source_checksum" => ruby_source_checksum, } end @@ -349,6 +351,12 @@ module Cask hash end + def ruby_source_checksum + @ruby_source_checksum ||= { + "sha256" => Digest::SHA256.file(sourcefile_path).hexdigest, + } + end + def artifacts_list artifacts.map do |artifact| case artifact diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 7fa4689acc..5223ad2ee8 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -233,13 +233,21 @@ module Cask # Use the cask-source API if there are any `*flight` blocks or the cask has multiple languages if json_cask[:artifacts].any? { |artifact| FLIGHT_STANZAS.include?(artifact.keys.first) } || json_cask[:languages].any? - cask_source = Homebrew::API::Cask.fetch_source(token, git_head: json_cask[:tap_git_head]) + cask_source = Homebrew::API::Cask.fetch_source(token, + git_head: json_cask[:tap_git_head], + sha256: json_cask.dig(:ruby_source_checksum, :sha256)) return FromContentLoader.new(cask_source).load(config: config) end tap = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/") - Cask.new(token, tap: tap, source: cask_source, config: config, loaded_from_api: true, loader: self) do + Cask.new(token, + tap: tap, + source: cask_source, + source_checksum: json_cask[:ruby_source_checksum], + config: config, + loaded_from_api: true, + loader: self) do version json_cask[:version] if json_cask[:sha256] == "no_check" diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 2f4e77c38b..0986d75213 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -2133,6 +2133,7 @@ class Formula "disable_date" => disable_date, "disable_reason" => disable_reason, "tap_git_head" => tap_git_head, + "ruby_source_checksum" => {}, } if stable @@ -2183,6 +2184,16 @@ class Formula } end + if self.class.loaded_from_api && active_spec.resource_defined?("ruby-source") + hsh["ruby_source_checksum"] = { + "sha256" => resource("ruby-source").checksum.hexdigest, + } + elsif !self.class.loaded_from_api && path.exist? + hsh["ruby_source_checksum"] = { + "sha256" => Digest::SHA256.file(path).hexdigest, + } + end + hsh end diff --git a/Library/Homebrew/formula.rbi b/Library/Homebrew/formula.rbi index fc8d6c255c..5c23af84c9 100644 --- a/Library/Homebrew/formula.rbi +++ b/Library/Homebrew/formula.rbi @@ -17,7 +17,7 @@ class Formula def service?; end def version; end - def resource; end + def resource(name); end def deps; end def uses_from_macos_elements; end def requirements; end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index fa23be13f0..12eb8ec499 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1182,9 +1182,12 @@ class FormulaInstaller if pour_bottle?(output_warning: true) formula.fetch_bottle_tab else - if formula.core_formula? && Homebrew::EnvConfig.install_from_api? - url = "https://raw.githubusercontent.com/#{formula.tap.full_name}/#{formula.tap_git_head}/Formula/#{formula.name}.rb" - @formula = Formulary.factory(url, formula.active_spec_sym, + if formula.class.loaded_from_api + # TODO: unify with cask logic (https://github.com/Homebrew/brew/issues/14746) + resource = formula.resource("ruby-source") + resource.fetch + @formula = Formulary.factory(resource.cached_download, + formula.active_spec_sym, alias_path: formula.alias_path, flags: formula.class.build_flags, from: :formula_installer) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index fa7adbf86f..0aee5e3d6a 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -252,6 +252,13 @@ module Formulary link_overwrite overwrite_path end + resource "ruby-source" do + url "https://raw.githubusercontent.com/Homebrew/homebrew-core/#{json_formula["tap_git_head"]}/Formula/#{name}.rb" + if (ruby_source_sha256 = json_formula.dig("ruby_source_checksum", "sha256")).present? + sha256 ruby_source_sha256 + end + end + def install raise "Cannot build from source from abstract formula." end @@ -446,7 +453,13 @@ module Formulary class FromPathLoader < FormulaLoader def initialize(path) path = Pathname.new(path).expand_path - super path.basename(".rb").to_s, path + name = path.basename(".rb").to_s + + # For files we've downloaded, they will be prefixed with `{URL MD5}--`. + # Remove that prefix to get the original filename. + name = name.split("--", 2).last if path.dirname == HOMEBREW_CACHE/"downloads" + + super name, path end end diff --git a/Library/Homebrew/test/cask/cmd/list_spec.rb b/Library/Homebrew/test/cask/cmd/list_spec.rb index 9dab951c1d..6e7d13eb35 100644 --- a/Library/Homebrew/test/cask/cmd/list_spec.rb +++ b/Library/Homebrew/test/cask/cmd/list_spec.rb @@ -131,7 +131,10 @@ describe Cask::Cmd::List, :cask do "tap_git_head": null, "languages": [ - ] + ], + "ruby_source_checksum": { + "sha256": "#{Digest::SHA256.file(Tap.default_cask_tap.cask_dir/"local-caffeine.rb").hexdigest}" + } }, { "token": "local-transmission", @@ -166,7 +169,10 @@ describe Cask::Cmd::List, :cask do "tap_git_head": null, "languages": [ - ] + ], + "ruby_source_checksum": { + "sha256": "#{Digest::SHA256.file(Tap.default_cask_tap.cask_dir/"local-transmission.rb").hexdigest}" + } }, { "token": "multiple-versions", @@ -204,7 +210,10 @@ describe Cask::Cmd::List, :cask do "tap_git_head": null, "languages": [ - ] + ], + "ruby_source_checksum": { + "sha256": "#{Digest::SHA256.file(Tap.default_cask_tap.cask_dir/"multiple-versions.rb").hexdigest}" + } }, { "token": "third-party-cask", @@ -239,7 +248,10 @@ describe Cask::Cmd::List, :cask do "tap_git_head": null, "languages": [ - ] + ], + "ruby_source_checksum": { + "sha256": "#{Digest::SHA256.file(Tap.fetch("third-party", "tap").cask_dir/"third-party-cask.rb").hexdigest}" + } }, { "token": "with-languages", @@ -275,7 +287,10 @@ describe Cask::Cmd::List, :cask do "languages": [ "zh", "en-US" - ] + ], + "ruby_source_checksum": { + "sha256": "#{Digest::SHA256.file(Tap.default_cask_tap.cask_dir/"with-languages.rb").hexdigest}" + } } ] EOS