Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 155 additions & 1 deletion Library/Homebrew/cmd/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -80,6 +103,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."
Comment on lines +106 to +108

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
switch "--deps",
depends_on: "--sizes",
description: "Show dependency size breakdown with exclusive deps and total disk footprint."
switch "--dependencies", "--deps",
depends_on: "--sizes",
description: "Show dependency size breakdown with exclusive dependencies and total disk footprint."


conflicts "--installed", "--eval-all"
conflicts "--formula", "--cask"
Expand All @@ -92,7 +118,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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails if passed any casks

end
print_sizes_with_deps(formulae)
elsif args.no_named?
print_sizes
else
formulae, casks = args.named.to_formulae_to_casks
Expand Down Expand Up @@ -998,6 +1031,127 @@ 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 = T.let({}, T::Hash[String, T::Set[String]])

Formula.installed.each do |installed_formula|
installed_formula.runtime_dependencies.each do |dep|
deps = reverse_map[dep.name] ||= Set.new
deps << installed_formula.name
end
end

analyses = formulae.filter_map do |formula|
kegs = formula.installed_kegs
next if kegs.empty?

direct_size = kegs.sum(&:disk_usage)
exclusive_deps = T.let([], T::Array[ExclusiveDep])
shared_deps = T.let([], T::Array[SharedDep])

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

if dependents.size == 1 && dependents.include?(formula.name)
exclusive_deps << ExclusiveDep.new(name: dep.name, size: dep_size)
else
also_needed_by = (dependents.to_a - [formula.name]).sort
shared_deps << SharedDep.new(name: dep.name, size: dep_size, also_needed_by:)
end
end

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 }

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(&: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)

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 shared.any?
shared_size = Formatter.disk_usage_readable(shared.sum(&:size))
puts " #{shared_size} in shared deps (would not be freed)"
end

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

next if shared.none?

puts ""
puts "Shared dependencies (also needed by other formulae):"
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
end
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/info.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions Library/Homebrew/test/cmd/info_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,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: [])
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: [])

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: [])

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: [])
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)))
Expand Down
Loading