diff --git a/Library/Homebrew/api/formula_struct.rb b/Library/Homebrew/api/formula_struct.rb index 2d9fcb04ae4cc..273c4962ed3c5 100644 --- a/Library/Homebrew/api/formula_struct.rb +++ b/Library/Homebrew/api/formula_struct.rb @@ -103,6 +103,7 @@ def self.from_hash(formula_hash) const :keg_only_args, T::Array[T.any(String, Symbol)], default: [] const :license, SPDX::LicenseExpression const :link_overwrite_paths, T::Array[String], default: [] + const :local_patch_checksums, T::Hash[String, String], default: {} const :no_autobump_args, T::Hash[Symbol, T.any(String, Symbol)], default: {} const :oldnames, T::Array[String], default: [] const :post_install_defined, T::Boolean, default: false diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 39207e52b6578..fc8f86a1bde6a 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -3091,6 +3091,21 @@ def ruby_source_checksum Checksum.new(Digest::SHA256.file(path).hexdigest) if path.exist? end + # API generation requires every patch checksum; other metadata consumers can omit unreadable patches. + sig { returns(T::Hash[String, String]) } + def local_patch_checksums + [stable, head].compact.flat_map do |spec| + (spec.patches + spec.resources.values.flat_map(&:patches)).grep(LocalPatch).filter_map do |patch| + [patch.file.to_s, Digest::SHA256.hexdigest(patch.contents)] + rescue ArgumentError, SystemCallError => e + raise if Formula.generating_hash? + + opoo "#{full_name}: cannot checksum #{patch.file}: #{e}" + nil + end + end.to_h + end + sig { params(dependables: T::Hash[Symbol, T.untyped]).returns(T::Array[T::Hash[Symbol, T.untyped]]) } def merge_spec_dependables(dependables) # We have a hash of specs names (stable/head) to dependency lists. @@ -3126,6 +3141,7 @@ def to_hash }, "urls" => urls_hash, "patches" => serialized_patches, + "local_patch_checksums" => local_patch_checksums, "revision" => revision, "version_scheme" => version_scheme, "compatibility_version" => compatibility_version, diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 9fee2b32df947..d7cc2df81a0b5 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -401,6 +401,10 @@ def self.load_formula_from_struct!(name, formula_struct, api_source:, tap_git_he checksum = self.class.instance_variable_get(:@ruby_source_checksum_string) Checksum.new(checksum) if checksum end + + define_method(:local_patch_checksums) do + formula_struct.local_patch_checksums + end end mod.const_set(class_name, klass) diff --git a/Library/Homebrew/test/api/formula/formula_struct_generator_spec.rb b/Library/Homebrew/test/api/formula/formula_struct_generator_spec.rb index ba8e95948b3b7..6dd06fc44122d 100644 --- a/Library/Homebrew/test/api/formula/formula_struct_generator_spec.rb +++ b/Library/Homebrew/test/api/formula/formula_struct_generator_spec.rb @@ -193,6 +193,13 @@ expect(struct.stable_patches).to eq hash["patches"] end + it "preserves local patch checksums through internal API serialisation" do + hash["local_patch_checksums"] = { "Patches/foo.diff" => TEST_SHA256 } + + expect(struct.serialize["local_patch_checksums"]) + .to eq(hash["local_patch_checksums"]) + end + context "when input contains placeholders" do let(:caveats_with_placeholder) { "Message about $HOMEBREW_CELLAR/foo/1.0.0/bin/foo" } let(:log_path_with_placeholder) { "$HOMEBREW_PREFIX/var/log/foo.log" } diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index f62067008e80a..4bb3a1944e30f 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -2215,6 +2215,81 @@ def service_name = "custom.override.name" end describe "#to_hash patches" do + before do + allow_any_instance_of(LocalPatch).to receive(:contents).and_return("patch contents") + end + + it "includes checksums for stable, HEAD and resource patch files" do + f = formula "foo" do + T.bind(self, T.class_of(Formula)) + url "foo-1.0" + patch do + file "Patches/stable.diff" + end + resource "bar" do + url "bar-1.0" + patch do + file "Patches/resource.diff" + end + end + head do + url "https://example.com/foo.git" + patch do + file "Patches/head.diff" + end + resource "baz" do + url "baz-1.0" + patch do + file "Patches/head-resource.diff" + end + end + end + end + + expect(f.to_hash["local_patch_checksums"]).to eq( + %w[stable resource head head-resource].to_h do |name| + ["Patches/#{name}.diff", Digest::SHA256.hexdigest("patch contents")] + end, + ) + end + + shared_examples "an unreadable local patch" do |error| + let(:f) do + formula "foo" do + T.bind(self, T.class_of(Formula)) + url "foo-1.0" + patch do + file "Patches/unreadable.diff" + end + patch do + file "Patches/valid.diff" + end + end + end + + before do + allow(f.patchlist.fetch(0)).to receive(:contents).and_raise(error, "unreadable patch") + end + + it "serialises checksums for the remaining patches" do + expect(f.to_hash["local_patch_checksums"]) + .to eq("Patches/valid.diff" => Digest::SHA256.hexdigest("patch contents")) + end + + it "warns about the omitted checksum" do + expect { f.to_hash }.to output(%r{cannot checksum Patches/unreadable.diff}).to_stderr + end + + it "rejects unreadable patches during API generation" do + allow(described_class).to receive(:generating_hash?).and_return(true) + + expect { f.to_hash }.to raise_error(error, /unreadable patch/) + end + end + + it_behaves_like "an unreadable local patch", ArgumentError + it_behaves_like "an unreadable local patch", Errno::EACCES + it "serialises an external patch" do f = formula "foo" do T.bind(self, T.class_of(Formula)) diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 954007757494d..18e366152ce7e 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -864,6 +864,14 @@ def formula_json_contents(extra_items = {}) expect(formula.loaded_from_internal_api?).to be true end + it "loads local patch checksums from the API" do + allow(Homebrew::API::Formula).to receive(:all_formulae) + .and_return formula_json_contents("local_patch_checksums" => { "Patches/foo.diff" => TEST_SHA256 }) + + expect(described_class.factory(formula_name).local_patch_checksums) + .to eq("Patches/foo.diff" => TEST_SHA256) + end + it "returns a Formula with a compatibility_version" do allow(Homebrew::API::Formula).to receive(:all_formulae) .and_return formula_json_contents("compatibility_version" => 2)