From 1c6430e5ef766b14ccf5c7e4b869b7c3dfacfca0 Mon Sep 17 00:00:00 2001 From: Eden Rochman Date: Sun, 7 Jun 2026 20:22:46 +0200 Subject: [PATCH 1/3] info: add --deps flag to --sizes for dependency footprint breakdown When used with --sizes, the new --deps flag shows the true disk cost of installed formulae by analyzing exclusive vs shared dependencies. This addresses the feedback from #22391 to integrate footprint analysis directly into `brew info --sizes` rather than as a separate command. --- Library/Homebrew/cmd/info.rb | 189 +++++++++++++++++- .../sorbet/rbi/dsl/homebrew/cmd/info.rbi | 3 + 2 files changed, 191 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 2118e0cab6481..87e1f13470f93 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -81,6 +81,9 @@ class NameSize < T::Struct description: "Treat all named arguments as casks." switch "--sizes", description: "Show the size of installed formulae and casks." + switch "--deps", + depends_on: "--sizes", + description: "Show dependency size breakdown with exclusive deps and total disk footprint." conflicts "--installed", "--eval-all" conflicts "--formula", "--cask" @@ -93,7 +96,14 @@ class NameSize < T::Struct sig { override.void } def run if args.sizes? - if args.no_named? + if args.deps? + formulae = if args.no_named? + Formula.installed + else + args.named.to_formulae + end + print_sizes_with_deps(formulae) + elsif args.no_named? print_sizes else formulae, casks = args.named.to_formulae_to_casks @@ -999,6 +1009,183 @@ def print_sizes(formulae: [], casks: []) cask_sizes.sort_by! { |c| -c.size } print_sizes_table("Casks sizes:", cask_sizes) end + + sig { params(formulae: T::Array[Formula]).void } + def print_sizes_with_deps(formulae) + reverse_map = build_reverse_dep_map + + if args.no_named? + analyses = formulae.filter_map do |formula| + analyze_formula_footprint(formula, reverse_map) + rescue FormulaUnavailableError + nil + end + analyses.sort_by! { |a| -a[:total_footprint] } + print_footprint_table(analyses) + else + formulae.each_with_index do |formula, i| + puts if i.positive? + analysis = analyze_formula_footprint(formula, reverse_map) + print_footprint_single(analysis) + end + end + end + + sig { returns(T::Hash[String, T::Set[String]]) } + def build_reverse_dep_map + reverse_map = T.let({}, T::Hash[String, T::Set[String]]) + + Formula.installed.each do |formula| + keg = formula.any_installed_keg + next unless keg + + deps = keg.runtime_dependencies + next unless deps.is_a?(Array) + + deps.each do |dep| + next unless dep.is_a?(Hash) + + full_name = dep["full_name"] + next unless full_name + + dep_name = Utils.name_from_full_name(full_name) + (reverse_map[dep_name] ||= Set.new).add(formula.name) + end + end + + reverse_map + end + + sig { + params( + formula: Formula, + reverse_map: T::Hash[String, T::Set[String]], + ).returns(T::Hash[Symbol, T.untyped]) + } + def analyze_formula_footprint(formula, reverse_map) + kegs = formula.installed_kegs + raise FormulaUnavailableError, formula.name if kegs.empty? + + direct_size = kegs.sum(&:disk_usage) + + keg = formula.any_installed_keg + tab_deps = keg&.runtime_dependencies + exclusive_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) + shared_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) + + if tab_deps.is_a?(Array) + tab_deps.each do |dep| + next unless dep.is_a?(Hash) + + full_name = dep["full_name"] + next unless full_name + + dep_name = Utils.name_from_full_name(full_name) + dep_formula = begin + Formula[dep_name] + rescue FormulaUnavailableError + next + end + + dep_kegs = dep_formula.installed_kegs + next if dep_kegs.empty? + + dep_size = dep_kegs.sum(&:disk_usage) + dependents = reverse_map[dep_name] || Set.new + + if dependents.size == 1 && dependents.include?(formula.name) + exclusive_deps << { name: dep_name, size: dep_size } + else + also_needed_by = (dependents.to_a - [formula.name]).sort + shared_deps << { name: dep_name, size: dep_size, also_needed_by: } + end + end + end + + exclusive_deps_size = exclusive_deps.sum { |d| d[:size] } + total_footprint = direct_size + exclusive_deps_size + + { + name: formula.full_name, + direct_size:, + exclusive_deps:, + shared_deps:, + exclusive_deps_size:, + total_footprint:, + } + end + + sig { params(analyses: T::Array[T::Hash[Symbol, T.untyped]]).void } + def print_footprint_table(analyses) + return if analyses.empty? + + ohai "Formulae footprint:" + + name_width = (analyses.map { |a| a[:name].length } + [7]).max + fmt = "%-#{name_width}s %10s %10s %10s" + + puts format(fmt, "Name", "Direct", "Excl.Deps", "Total") + analyses.each do |a| + puts format( + fmt, + a[:name], + Formatter.disk_usage_readable(a[:direct_size]), + Formatter.disk_usage_readable(a[:exclusive_deps_size]), + Formatter.disk_usage_readable(a[:total_footprint]), + ) + end + + grand_total = analyses.sum { |a| a[:total_footprint] } + puts format(fmt, "Total", "", "", Formatter.disk_usage_readable(grand_total)) + end + + sig { params(analysis: T::Hash[Symbol, T.untyped]).void } + def print_footprint_single(analysis) + name = analysis[:name] + direct = Formatter.disk_usage_readable(analysis[:direct_size]) + exclusive = analysis[:exclusive_deps] + shared = analysis[:shared_deps] + total = Formatter.disk_usage_readable(analysis[:total_footprint]) + + if exclusive.empty? && shared.empty? + puts "#{name}: #{direct}" + return + end + + if exclusive.empty? + puts "#{name}: #{direct} (direct), no exclusive deps" + else + dep_count = exclusive.size + excl_size = Formatter.disk_usage_readable(analysis[:exclusive_deps_size]) + puts "#{name}: #{direct} (direct) + #{excl_size} " \ + "(#{dep_count} exclusive #{(dep_count == 1) ? "dep" : "deps"}) = #{total} total" + end + + if shared.any? + shared_size = Formatter.disk_usage_readable(shared.sum { |d| d[:size] }) + puts " #{shared_size} in shared deps (would not be freed)" + end + + return unless args.verbose? + + if exclusive.any? + puts "" + puts "Exclusive dependencies (only needed by #{name}):" + exclusive.sort_by { |d| -d[:size] }.each do |dep| + puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}" + end + end + + return if shared.none? + + puts "" + puts "Shared dependencies (also needed by other formulae):" + shared.sort_by { |d| -d[:size] }.each do |dep| + also = dep[:also_needed_by] + also_str = also.empty? ? "" : " (also: #{also.join(", ")})" + puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}#{also_str}" + end + end end end end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/info.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/info.rbi index b1fb21788bd1e..3b9864ef0d29e 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/info.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/info.rbi @@ -26,6 +26,9 @@ class Homebrew::Cmd::Info::Args < Homebrew::CLI::Args sig { returns(T.nilable(String)) } def days; end + sig { returns(T::Boolean) } + def deps?; end + sig { returns(T::Boolean) } def eval_all?; end From b8114e4ce31b2ecf964630aa7cf4f0583c398cf7 Mon Sep 17 00:00:00 2001 From: Eden Rochman Date: Mon, 8 Jun 2026 15:25:58 +0200 Subject: [PATCH 2/3] Inline single-use methods in print_sizes_with_deps, add unit tests Inline build_reverse_dep_map, analyze_formula_footprint, print_footprint_table, and print_footprint_single into print_sizes_with_deps since each was only called once. Use Array() instead of is_a?(Array) guard for runtime_dependencies. Add four non-integration tests covering no-deps, exclusive deps, shared deps, and table output formats. --- Library/Homebrew/cmd/info.rb | 200 ++++++++++--------------- Library/Homebrew/test/cmd/info_spec.rb | 75 ++++++++++ 2 files changed, 158 insertions(+), 117 deletions(-) diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 87e1f13470f93..0f26e816f068e 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -1012,69 +1012,33 @@ def print_sizes(formulae: [], casks: []) sig { params(formulae: T::Array[Formula]).void } def print_sizes_with_deps(formulae) - reverse_map = build_reverse_dep_map - - if args.no_named? - analyses = formulae.filter_map do |formula| - analyze_formula_footprint(formula, reverse_map) - rescue FormulaUnavailableError - nil - end - analyses.sort_by! { |a| -a[:total_footprint] } - print_footprint_table(analyses) - else - formulae.each_with_index do |formula, i| - puts if i.positive? - analysis = analyze_formula_footprint(formula, reverse_map) - print_footprint_single(analysis) - end - end - end - - sig { returns(T::Hash[String, T::Set[String]]) } - def build_reverse_dep_map reverse_map = T.let({}, T::Hash[String, T::Set[String]]) - Formula.installed.each do |formula| - keg = formula.any_installed_keg + Formula.installed.each do |installed_formula| + keg = installed_formula.any_installed_keg next unless keg - deps = keg.runtime_dependencies - next unless deps.is_a?(Array) - - deps.each do |dep| + Array(keg.runtime_dependencies).each do |dep| next unless dep.is_a?(Hash) full_name = dep["full_name"] next unless full_name dep_name = Utils.name_from_full_name(full_name) - (reverse_map[dep_name] ||= Set.new).add(formula.name) + (reverse_map[dep_name] ||= Set.new).add(installed_formula.name) end end - reverse_map - end - - sig { - params( - formula: Formula, - reverse_map: T::Hash[String, T::Set[String]], - ).returns(T::Hash[Symbol, T.untyped]) - } - def analyze_formula_footprint(formula, reverse_map) - kegs = formula.installed_kegs - raise FormulaUnavailableError, formula.name if kegs.empty? - - direct_size = kegs.sum(&:disk_usage) + analyses = formulae.filter_map do |formula| + kegs = formula.installed_kegs + next if kegs.empty? - keg = formula.any_installed_keg - tab_deps = keg&.runtime_dependencies - exclusive_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) - shared_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) + direct_size = kegs.sum(&:disk_usage) + keg = formula.any_installed_keg + exclusive_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) + shared_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) - if tab_deps.is_a?(Array) - tab_deps.each do |dep| + Array(keg&.runtime_dependencies).each do |dep| next unless dep.is_a?(Hash) full_name = dep["full_name"] @@ -1100,90 +1064,92 @@ def analyze_formula_footprint(formula, reverse_map) shared_deps << { name: dep_name, size: dep_size, also_needed_by: } end end - end - exclusive_deps_size = exclusive_deps.sum { |d| d[:size] } - total_footprint = direct_size + exclusive_deps_size + exclusive_deps_size = exclusive_deps.sum { |d| d[:size] } + total_footprint = direct_size + exclusive_deps_size + + { + name: formula.full_name, + direct_size:, + exclusive_deps:, + shared_deps:, + exclusive_deps_size:, + total_footprint:, + } + end - { - name: formula.full_name, - direct_size:, - exclusive_deps:, - shared_deps:, - exclusive_deps_size:, - total_footprint:, - } - end + if args.no_named? + return if analyses.empty? - sig { params(analyses: T::Array[T::Hash[Symbol, T.untyped]]).void } - def print_footprint_table(analyses) - return if analyses.empty? + analyses.sort_by! { |a| -a[:total_footprint] } - ohai "Formulae footprint:" + ohai "Formulae footprint:" - name_width = (analyses.map { |a| a[:name].length } + [7]).max - fmt = "%-#{name_width}s %10s %10s %10s" + name_width = (analyses.map { |a| a[:name].length } + [7]).max + fmt = "%-#{name_width}s %10s %10s %10s" - puts format(fmt, "Name", "Direct", "Excl.Deps", "Total") - analyses.each do |a| - puts format( - fmt, - a[:name], - Formatter.disk_usage_readable(a[:direct_size]), - Formatter.disk_usage_readable(a[:exclusive_deps_size]), - Formatter.disk_usage_readable(a[:total_footprint]), - ) - end + puts format(fmt, "Name", "Direct", "Excl.Deps", "Total") + analyses.each do |a| + puts format( + fmt, + a[:name], + Formatter.disk_usage_readable(a[:direct_size]), + Formatter.disk_usage_readable(a[:exclusive_deps_size]), + Formatter.disk_usage_readable(a[:total_footprint]), + ) + end - grand_total = analyses.sum { |a| a[:total_footprint] } - puts format(fmt, "Total", "", "", Formatter.disk_usage_readable(grand_total)) - end + grand_total = analyses.sum { |a| a[:total_footprint] } + puts format(fmt, "Total", "", "", Formatter.disk_usage_readable(grand_total)) + else + analyses.each_with_index do |analysis, i| + puts if i.positive? - sig { params(analysis: T::Hash[Symbol, T.untyped]).void } - def print_footprint_single(analysis) - name = analysis[:name] - direct = Formatter.disk_usage_readable(analysis[:direct_size]) - exclusive = analysis[:exclusive_deps] - shared = analysis[:shared_deps] - total = Formatter.disk_usage_readable(analysis[:total_footprint]) + name = analysis[:name] + direct = Formatter.disk_usage_readable(analysis[:direct_size]) + exclusive = analysis[:exclusive_deps] + shared = analysis[:shared_deps] + total = Formatter.disk_usage_readable(analysis[:total_footprint]) - if exclusive.empty? && shared.empty? - puts "#{name}: #{direct}" - return - end + if exclusive.empty? && shared.empty? + puts "#{name}: #{direct}" + next + end - if exclusive.empty? - puts "#{name}: #{direct} (direct), no exclusive deps" - else - dep_count = exclusive.size - excl_size = Formatter.disk_usage_readable(analysis[:exclusive_deps_size]) - puts "#{name}: #{direct} (direct) + #{excl_size} " \ - "(#{dep_count} exclusive #{(dep_count == 1) ? "dep" : "deps"}) = #{total} total" - end + if exclusive.empty? + puts "#{name}: #{direct} (direct), no exclusive deps" + else + dep_count = exclusive.size + excl_size = Formatter.disk_usage_readable(analysis[:exclusive_deps_size]) + puts "#{name}: #{direct} (direct) + #{excl_size} " \ + "(#{dep_count} exclusive #{(dep_count == 1) ? "dep" : "deps"}) = #{total} total" + end - if shared.any? - shared_size = Formatter.disk_usage_readable(shared.sum { |d| d[:size] }) - puts " #{shared_size} in shared deps (would not be freed)" - end + if shared.any? + shared_size = Formatter.disk_usage_readable(shared.sum { |d| d[:size] }) + puts " #{shared_size} in shared deps (would not be freed)" + end - return unless args.verbose? + next unless args.verbose? - if exclusive.any? - puts "" - puts "Exclusive dependencies (only needed by #{name}):" - exclusive.sort_by { |d| -d[:size] }.each do |dep| - puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}" - end - end + if exclusive.any? + puts "" + puts "Exclusive dependencies (only needed by #{name}):" + exclusive.sort_by { |d| -d[:size] }.each do |dep| + puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}" + end + end - return if shared.none? + next if shared.none? - puts "" - puts "Shared dependencies (also needed by other formulae):" - shared.sort_by { |d| -d[:size] }.each do |dep| - also = dep[:also_needed_by] - also_str = also.empty? ? "" : " (also: #{also.join(", ")})" - puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}#{also_str}" + puts "" + puts "Shared dependencies (also needed by other formulae):" + shared.sort_by { |d| -d[:size] }.each do |dep| + also = dep[:also_needed_by] + also_str = also.empty? ? "" : " (also: #{also.join(", ")})" + puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}#{also_str}" + end + end end end end diff --git a/Library/Homebrew/test/cmd/info_spec.rb b/Library/Homebrew/test/cmd/info_spec.rb index 6b0a0304363a1..f8f7cb02582d1 100644 --- a/Library/Homebrew/test/cmd/info_spec.rb +++ b/Library/Homebrew/test/cmd/info_spec.rb @@ -1080,6 +1080,81 @@ def installed_info_cask .and not_to_output.to_stderr end + describe "#print_sizes_with_deps" do + it "prints direct size for a formula with no dependencies" do + testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } + keg = instance_double(Keg, disk_usage: 1_000_000, runtime_dependencies: nil) + allow(testball).to receive_messages(installed_kegs: [keg], any_installed_keg: keg) + allow(Formula).to receive(:installed).and_return([testball]) + + info = described_class.new(["--sizes", "--deps", "testball"]) + + expect { info.send(:print_sizes_with_deps, [testball]) } + .to output(/testball: 1MB\n/).to_stdout + .and not_to_output.to_stderr + end + + it "shows exclusive dependency breakdown in footprint" do + testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } + libfoo = formula("libfoo") { url "https://brew.sh/libfoo-1.0.tar.gz" } + + testball_keg = instance_double(Keg, + disk_usage: 1_000_000, + runtime_dependencies: [{ "full_name" => "libfoo", "version" => "1.0" }]) + libfoo_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: nil) + + allow(testball).to receive_messages(installed_kegs: [testball_keg], any_installed_keg: testball_keg) + allow(libfoo).to receive_messages(installed_kegs: [libfoo_keg], any_installed_keg: libfoo_keg) + allow(Formula).to receive(:installed).and_return([testball]) + allow(Formulary).to receive(:factory).with("libfoo").and_return(libfoo) + + info = described_class.new(["--sizes", "--deps", "testball"]) + + expect { info.send(:print_sizes_with_deps, [testball]) } + .to output(/testball: 1MB \(direct\) \+ 500KB \(1 exclusive dep\) = 1\.5MB total/).to_stdout + .and not_to_output.to_stderr + end + + it "shows shared dependency info when a dep is needed by multiple formulae" do + testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } + other = formula("other") { url "https://brew.sh/other-0.1.tar.gz" } + libbar = formula("libbar") { url "https://brew.sh/libbar-1.0.tar.gz" } + + testball_keg = instance_double(Keg, + disk_usage: 1_000_000, + runtime_dependencies: [{ "full_name" => "libbar", "version" => "1.0" }]) + other_keg = instance_double(Keg, + disk_usage: 800_000, + runtime_dependencies: [{ "full_name" => "libbar", "version" => "1.0" }]) + libbar_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: nil) + + allow(testball).to receive_messages(installed_kegs: [testball_keg], any_installed_keg: testball_keg) + allow(other).to receive_messages(installed_kegs: [other_keg], any_installed_keg: other_keg) + allow(libbar).to receive_messages(installed_kegs: [libbar_keg], any_installed_keg: libbar_keg) + allow(Formula).to receive(:installed).and_return([testball, other]) + allow(Formulary).to receive(:factory).with("libbar").and_return(libbar) + + info = described_class.new(["--sizes", "--deps", "testball"]) + + expect { info.send(:print_sizes_with_deps, [testball]) } + .to output(/testball: 1MB \(direct\), no exclusive deps\n\s+500KB in shared deps/).to_stdout + .and not_to_output.to_stderr + end + + it "prints a table when no formulae are named" do + testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } + keg = instance_double(Keg, disk_usage: 2_000_000, runtime_dependencies: nil) + allow(testball).to receive_messages(installed_kegs: [keg], any_installed_keg: keg) + allow(Formula).to receive(:installed).and_return([testball]) + + info = described_class.new(["--sizes", "--deps"]) + + expect { info.send(:print_sizes_with_deps, [testball]) } + .to output(/Formulae footprint:.*Name\s+Direct\s+Excl\.Deps\s+Total.*testball/m).to_stdout + .and not_to_output.to_stderr + end + end + describe "::installation_status" do it "prints on-request installs explicitly" do expect(described_class.installation_status(instance_double(Tab, installed_on_request: true))) From d39ee134ba239616089c82f4a18ecec2334ae4bb Mon Sep 17 00:00:00 2001 From: Eden Rochman Date: Thu, 18 Jun 2026 11:35:13 +0200 Subject: [PATCH 3/3] Merging the feedback: use Formula#runtime_dependencies, T::Struct over hashes, remove FormulaUnavailableError guard, simplify reverse map building --- Library/Homebrew/cmd/info.rb | 109 +++++++++++++------------ Library/Homebrew/test/cmd/info_spec.rb | 8 +- 2 files changed, 59 insertions(+), 58 deletions(-) diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 32ea4260cf98b..98e7bf4717f9e 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -24,6 +24,29 @@ class NameSize < T::Struct end private_constant :NameSize + class ExclusiveDep < T::Struct + const :name, String + const :size, Integer + end + private_constant :ExclusiveDep + + class SharedDep < T::Struct + const :name, String + const :size, Integer + const :also_needed_by, T::Array[String] + end + private_constant :SharedDep + + class FormulaFootprint < T::Struct + const :name, String + const :direct_size, Integer + const :exclusive_deps, T::Array[ExclusiveDep] + const :shared_deps, T::Array[SharedDep] + const :exclusive_deps_size, Integer + const :total_footprint, Integer + end + private_constant :FormulaFootprint + VALID_DAYS = %w[30 90 365].freeze VALID_FORMULA_CATEGORIES = %w[install install-on-request build-error].freeze VALID_CATEGORIES = T.let((VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze, T::Array[String]) @@ -1014,17 +1037,9 @@ def print_sizes_with_deps(formulae) reverse_map = T.let({}, T::Hash[String, T::Set[String]]) Formula.installed.each do |installed_formula| - keg = installed_formula.any_installed_keg - next unless keg - - Array(keg.runtime_dependencies).each do |dep| - next unless dep.is_a?(Hash) - - full_name = dep["full_name"] - next unless full_name - - dep_name = Utils.name_from_full_name(full_name) - (reverse_map[dep_name] ||= Set.new).add(installed_formula.name) + installed_formula.runtime_dependencies.each do |dep| + deps = reverse_map[dep.name] ||= Set.new + deps << installed_formula.name end end @@ -1033,82 +1048,69 @@ def print_sizes_with_deps(formulae) next if kegs.empty? direct_size = kegs.sum(&:disk_usage) - keg = formula.any_installed_keg - exclusive_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) - shared_deps = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) - - Array(keg&.runtime_dependencies).each do |dep| - next unless dep.is_a?(Hash) + exclusive_deps = T.let([], T::Array[ExclusiveDep]) + shared_deps = T.let([], T::Array[SharedDep]) - full_name = dep["full_name"] - next unless full_name - - dep_name = Utils.name_from_full_name(full_name) - dep_formula = begin - Formula[dep_name] - rescue FormulaUnavailableError - next - end - - dep_kegs = dep_formula.installed_kegs + formula.runtime_dependencies.each do |dep| + dep_kegs = Formula[dep.name].installed_kegs next if dep_kegs.empty? dep_size = dep_kegs.sum(&:disk_usage) - dependents = reverse_map[dep_name] || Set.new + dependents = reverse_map[dep.name] || Set.new if dependents.size == 1 && dependents.include?(formula.name) - exclusive_deps << { name: dep_name, size: dep_size } + exclusive_deps << ExclusiveDep.new(name: dep.name, size: dep_size) else also_needed_by = (dependents.to_a - [formula.name]).sort - shared_deps << { name: dep_name, size: dep_size, also_needed_by: } + shared_deps << SharedDep.new(name: dep.name, size: dep_size, also_needed_by:) end end - exclusive_deps_size = exclusive_deps.sum { |d| d[:size] } + exclusive_deps_size = exclusive_deps.sum(&:size) total_footprint = direct_size + exclusive_deps_size - { + FormulaFootprint.new( name: formula.full_name, direct_size:, exclusive_deps:, shared_deps:, exclusive_deps_size:, total_footprint:, - } + ) end if args.no_named? return if analyses.empty? - analyses.sort_by! { |a| -a[:total_footprint] } + analyses.sort_by! { |a| -a.total_footprint } ohai "Formulae footprint:" - name_width = (analyses.map { |a| a[:name].length } + [7]).max + name_width = (analyses.map { |a| a.name.length } + [7]).max fmt = "%-#{name_width}s %10s %10s %10s" puts format(fmt, "Name", "Direct", "Excl.Deps", "Total") analyses.each do |a| puts format( fmt, - a[:name], - Formatter.disk_usage_readable(a[:direct_size]), - Formatter.disk_usage_readable(a[:exclusive_deps_size]), - Formatter.disk_usage_readable(a[:total_footprint]), + a.name, + Formatter.disk_usage_readable(a.direct_size), + Formatter.disk_usage_readable(a.exclusive_deps_size), + Formatter.disk_usage_readable(a.total_footprint), ) end - grand_total = analyses.sum { |a| a[:total_footprint] } + grand_total = analyses.sum(&:total_footprint) puts format(fmt, "Total", "", "", Formatter.disk_usage_readable(grand_total)) else analyses.each_with_index do |analysis, i| puts if i.positive? - name = analysis[:name] - direct = Formatter.disk_usage_readable(analysis[:direct_size]) - exclusive = analysis[:exclusive_deps] - shared = analysis[:shared_deps] - total = Formatter.disk_usage_readable(analysis[:total_footprint]) + name = analysis.name + direct = Formatter.disk_usage_readable(analysis.direct_size) + exclusive = analysis.exclusive_deps + shared = analysis.shared_deps + total = Formatter.disk_usage_readable(analysis.total_footprint) if exclusive.empty? && shared.empty? puts "#{name}: #{direct}" @@ -1119,13 +1121,13 @@ def print_sizes_with_deps(formulae) puts "#{name}: #{direct} (direct), no exclusive deps" else dep_count = exclusive.size - excl_size = Formatter.disk_usage_readable(analysis[:exclusive_deps_size]) + excl_size = Formatter.disk_usage_readable(analysis.exclusive_deps_size) puts "#{name}: #{direct} (direct) + #{excl_size} " \ "(#{dep_count} exclusive #{(dep_count == 1) ? "dep" : "deps"}) = #{total} total" end if shared.any? - shared_size = Formatter.disk_usage_readable(shared.sum { |d| d[:size] }) + shared_size = Formatter.disk_usage_readable(shared.sum(&:size)) puts " #{shared_size} in shared deps (would not be freed)" end @@ -1134,8 +1136,8 @@ def print_sizes_with_deps(formulae) if exclusive.any? puts "" puts "Exclusive dependencies (only needed by #{name}):" - exclusive.sort_by { |d| -d[:size] }.each do |dep| - puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}" + exclusive.sort_by { |d| -d.size }.each do |dep| + puts " #{dep.name.ljust(16)} #{Formatter.disk_usage_readable(dep.size)}" end end @@ -1143,10 +1145,9 @@ def print_sizes_with_deps(formulae) puts "" puts "Shared dependencies (also needed by other formulae):" - shared.sort_by { |d| -d[:size] }.each do |dep| - also = dep[:also_needed_by] - also_str = also.empty? ? "" : " (also: #{also.join(", ")})" - puts " #{dep[:name].ljust(16)} #{Formatter.disk_usage_readable(dep[:size])}#{also_str}" + shared.sort_by { |d| -d.size }.each do |dep| + also_str = dep.also_needed_by.empty? ? "" : " (also: #{dep.also_needed_by.join(", ")})" + puts " #{dep.name.ljust(16)} #{Formatter.disk_usage_readable(dep.size)}#{also_str}" end end end diff --git a/Library/Homebrew/test/cmd/info_spec.rb b/Library/Homebrew/test/cmd/info_spec.rb index f8f7cb02582d1..74e76d6cba10a 100644 --- a/Library/Homebrew/test/cmd/info_spec.rb +++ b/Library/Homebrew/test/cmd/info_spec.rb @@ -1083,7 +1083,7 @@ def installed_info_cask describe "#print_sizes_with_deps" do it "prints direct size for a formula with no dependencies" do testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } - keg = instance_double(Keg, disk_usage: 1_000_000, runtime_dependencies: nil) + keg = instance_double(Keg, disk_usage: 1_000_000, runtime_dependencies: []) allow(testball).to receive_messages(installed_kegs: [keg], any_installed_keg: keg) allow(Formula).to receive(:installed).and_return([testball]) @@ -1101,7 +1101,7 @@ def installed_info_cask testball_keg = instance_double(Keg, disk_usage: 1_000_000, runtime_dependencies: [{ "full_name" => "libfoo", "version" => "1.0" }]) - libfoo_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: nil) + libfoo_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: []) allow(testball).to receive_messages(installed_kegs: [testball_keg], any_installed_keg: testball_keg) allow(libfoo).to receive_messages(installed_kegs: [libfoo_keg], any_installed_keg: libfoo_keg) @@ -1126,7 +1126,7 @@ def installed_info_cask other_keg = instance_double(Keg, disk_usage: 800_000, runtime_dependencies: [{ "full_name" => "libbar", "version" => "1.0" }]) - libbar_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: nil) + libbar_keg = instance_double(Keg, disk_usage: 500_000, runtime_dependencies: []) allow(testball).to receive_messages(installed_kegs: [testball_keg], any_installed_keg: testball_keg) allow(other).to receive_messages(installed_kegs: [other_keg], any_installed_keg: other_keg) @@ -1143,7 +1143,7 @@ def installed_info_cask it "prints a table when no formulae are named" do testball = formula("testball") { url "https://brew.sh/testball-0.1.tar.gz" } - keg = instance_double(Keg, disk_usage: 2_000_000, runtime_dependencies: nil) + keg = instance_double(Keg, disk_usage: 2_000_000, runtime_dependencies: []) allow(testball).to receive_messages(installed_kegs: [keg], any_installed_keg: keg) allow(Formula).to receive(:installed).and_return([testball])