From c72386e3c302c0e0be15ade2712a63f97fc9a73d Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Tue, 1 Apr 2025 15:12:12 +0100 Subject: [PATCH] bundle/commands/cleanup: correctly handle `.keepme` references. Extract the relevant logic from `formula.rb`, moving to `keg.rb` and then use this logic in `bundle/commands/cleanup.rb` to ensure that we don't say we need to uninstall formulae that should be still kept. --- Library/Homebrew/bundle/commands/cleanup.rb | 7 +++++ Library/Homebrew/formula.rb | 3 +-- Library/Homebrew/keg.rb | 10 +++++++ .../test/bundle/commands/cleanup_spec.rb | 26 ++++++++++++++++++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Library/Homebrew/bundle/commands/cleanup.rb b/Library/Homebrew/bundle/commands/cleanup.rb index 3109c607d9..319388d904 100644 --- a/Library/Homebrew/bundle/commands/cleanup.rb +++ b/Library/Homebrew/bundle/commands/cleanup.rb @@ -106,6 +106,13 @@ module Homebrew current_formulae.reject! do |f| Homebrew::Bundle::BrewInstaller.formula_in_array?(f[:full_name], kept_formulae) end + + # Don't try to uninstall formulae with keepme references + current_formulae.reject! do |f| + Formula[f[:full_name]].installed_kegs.any? do |keg| + keg.keepme_refs.present? + end + end current_formulae.map { |f| f[:full_name] } end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 2573657f4f..a8b2554b89 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -3146,8 +3146,7 @@ class Formula opoo "Skipping (old) #{keg} due to it being linked" unless quiet elsif pinned? && keg == Keg.new(@pin.path.resolved_path) opoo "Skipping (old) #{keg} due to it being pinned" unless quiet - elsif (keepme = keg/".keepme") && keepme.exist? && keepme.readable? && - (keepme_refs = keepme.readlines.map(&:strip).select { |ref| Pathname(ref).exist? }.presence) + elsif (keepme_refs = keg.keepme_refs.presence) opoo "Skipping #{keg} as it is needed by #{keepme_refs.join(", ")}" unless quiet else eligible_for_cleanup << keg diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 4e4218fbd5..40f96d48ba 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -95,6 +95,8 @@ class Keg PYC_EXTENSIONS = %w[.pyc .pyo].freeze LIBTOOL_EXTENSIONS = %w[.la .lai].freeze + KEEPME_FILE = ".keepme" + # @param path if this is a file in a keg, returns the containing {Keg} object. def self.for(path) original_path = path @@ -592,6 +594,14 @@ class Keg end end + sig { returns(T::Array[String]) } + def keepme_refs + keepme = path/KEEPME_FILE + return [] if !keepme.exist? || !keepme.readable? + + keepme.readlines.select { |ref| File.exist?(ref.strip) } + end + def binary_executable_or_library_files [] end diff --git a/Library/Homebrew/test/bundle/commands/cleanup_spec.rb b/Library/Homebrew/test/bundle/commands/cleanup_spec.rb index 4dce283dce..2291120f26 100644 --- a/Library/Homebrew/test/bundle/commands/cleanup_spec.rb +++ b/Library/Homebrew/test/bundle/commands/cleanup_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Homebrew::Bundle::Commands::Cleanup do it "computes which formulae to uninstall" do dependencies_arrays_hash = { dependencies: [], build_dependencies: [] } - allow(Homebrew::Bundle::BrewDumper).to receive(:formulae).and_return [ + formulae_hash = [ { name: "a2", full_name: "a2", aliases: ["a"], dependencies: ["d"] }, { name: "c", full_name: "c" }, { name: "d", full_name: "homebrew/tap/d", aliases: ["d2"] }, @@ -70,6 +70,17 @@ RSpec.describe Homebrew::Bundle::Commands::Cleanup do { name: "builddependency2", full_name: "builddependency2" }, { name: "caskdependency", full_name: "homebrew/tap/caskdependency" }, ].map { |formula| dependencies_arrays_hash.merge(formula) } + allow(Homebrew::Bundle::BrewDumper).to receive(:formulae).and_return(formulae_hash) + + formulae_hash.each do |hash_formula| + name = hash_formula[:name] + full_name = hash_formula[:full_name] + tap_name = full_name.rpartition("/").first.presence || "homebrew/core" + tap = Tap.fetch(tap_name) + f = formula(name, tap:) { url "#{name}-1.0" } + stub_formula_loader f, full_name + end + allow(Homebrew::Bundle::CaskDumper).to receive(:formula_dependencies).and_return(%w[caskdependency]) expect(described_class.formulae_to_uninstall).to eql %w[ c @@ -93,6 +104,19 @@ RSpec.describe Homebrew::Bundle::Commands::Cleanup do expect(described_class.taps_to_untap).to eql(%w[z homebrew/tap]) end + it "ignores formulae with .keepme references when computing which formulae to uninstall" do + name = full_name ="c" + allow(Homebrew::Bundle::BrewDumper).to receive(:formulae).and_return([{ name:, full_name: }]) + f = formula(name) { url "#{name}-1.0" } + stub_formula_loader f, name + + keg = instance_double(Keg) + allow(keg).to receive(:keepme_refs).and_return(["/some/file"]) + allow(f).to receive(:installed_kegs).and_return([keg]) + + expect(described_class.formulae_to_uninstall).to be_empty + end + it "computes which VSCode extensions to uninstall" do allow(Homebrew::Bundle::VscodeExtensionDumper).to receive(:extensions).and_return(%w[z]) expect(described_class.vscode_extensions_to_uninstall).to eql(%w[z])