
Change styling based on output of brew typecheck and brew style Changes as per PR comments Remove --leaves flag functionality Simplify formulae and cask parsing as well as style changes as per PR comments Update cask and formulae parsing as per PR comment suggestion Add column formatting function as well as PR comment suggestions Add Sorbet struct for printing and minor logic changes as per PR comments Minor changes as per PR comments and fix formatting issue in output
481 lines
16 KiB
Ruby
481 lines
16 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "abstract_command"
|
|
require "missing_formula"
|
|
require "caveats"
|
|
require "options"
|
|
require "formula"
|
|
require "keg"
|
|
require "tab"
|
|
require "json"
|
|
require "utils/spdx"
|
|
require "deprecate_disable"
|
|
require "api"
|
|
|
|
module Homebrew
|
|
module Cmd
|
|
class Info < AbstractCommand
|
|
class NameSize < T::Struct
|
|
const :name, String
|
|
const :size, Integer
|
|
end
|
|
private_constant :NameSize
|
|
|
|
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])
|
|
|
|
cmd_args do
|
|
description <<~EOS
|
|
Display brief statistics for your Homebrew installation.
|
|
If a <formula> or <cask> is provided, show summary of information about it.
|
|
EOS
|
|
switch "--analytics",
|
|
description: "List global Homebrew analytics data or, if specified, installation and " \
|
|
"build error data for <formula> (provided neither `$HOMEBREW_NO_ANALYTICS` " \
|
|
"nor `$HOMEBREW_NO_GITHUB_API` are set)."
|
|
flag "--days=",
|
|
depends_on: "--analytics",
|
|
description: "How many days of analytics data to retrieve. " \
|
|
"The value for <days> must be `30`, `90` or `365`. The default is `30`."
|
|
flag "--category=",
|
|
depends_on: "--analytics",
|
|
description: "Which type of analytics data to retrieve. " \
|
|
"The value for <category> must be `install`, `install-on-request` or `build-error`; " \
|
|
"`cask-install` or `os-version` may be specified if <formula> is not. " \
|
|
"The default is `install`."
|
|
switch "--github-packages-downloads",
|
|
description: "Scrape GitHub Packages download counts from HTML for a core formula.",
|
|
hidden: true
|
|
switch "--github",
|
|
description: "Open the GitHub source page for <formula> and <cask> in a browser. " \
|
|
"To view the history locally: `brew log -p` <formula> or <cask>"
|
|
switch "--fetch-manifest",
|
|
description: "Fetch GitHub Packages manifest for extra information when <formula> is not installed."
|
|
flag "--json",
|
|
description: "Print a JSON representation. Currently the default value for <version> is `v1` for " \
|
|
"<formula>. For <formula> and <cask> use `v2`. See the docs for examples of using the " \
|
|
"JSON output: <https://docs.brew.sh/Querying-Brew>"
|
|
switch "--installed",
|
|
depends_on: "--json",
|
|
description: "Print JSON of formulae that are currently installed."
|
|
switch "--eval-all",
|
|
depends_on: "--json",
|
|
description: "Evaluate all available formulae and casks, whether installed or not, to print their " \
|
|
"JSON."
|
|
switch "--variations",
|
|
depends_on: "--json",
|
|
description: "Include the variations hash in each formula's JSON output."
|
|
switch "-v", "--verbose",
|
|
description: "Show more verbose analytics data for <formula>."
|
|
switch "--formula", "--formulae",
|
|
description: "Treat all named arguments as formulae."
|
|
switch "--cask", "--casks",
|
|
description: "Treat all named arguments as casks."
|
|
switch "--sizes",
|
|
description: "Show the size of installed formulae and casks."
|
|
|
|
conflicts "--installed", "--eval-all"
|
|
conflicts "--installed", "--all"
|
|
conflicts "--formula", "--cask"
|
|
conflicts "--fetch-manifest", "--cask"
|
|
conflicts "--fetch-manifest", "--json"
|
|
|
|
named_args [:formula, :cask]
|
|
end
|
|
|
|
sig { override.void }
|
|
def run
|
|
if args.sizes?
|
|
if args.no_named?
|
|
print_sizes
|
|
else
|
|
formulae, casks = args.named.to_formulae_to_casks
|
|
formulae = T.cast(formulae, T::Array[Formula])
|
|
print_sizes(formulae:, casks:)
|
|
end
|
|
elsif args.analytics?
|
|
if args.days.present? && VALID_DAYS.exclude?(args.days)
|
|
raise UsageError, "`--days` must be one of #{VALID_DAYS.join(", ")}."
|
|
end
|
|
|
|
if args.category.present?
|
|
if args.named.present? && VALID_FORMULA_CATEGORIES.exclude?(args.category)
|
|
raise UsageError,
|
|
"`--category` must be one of #{VALID_FORMULA_CATEGORIES.join(", ")} when querying formulae."
|
|
end
|
|
|
|
unless VALID_CATEGORIES.include?(args.category)
|
|
raise UsageError, "`--category` must be one of #{VALID_CATEGORIES.join(", ")}."
|
|
end
|
|
end
|
|
|
|
print_analytics
|
|
elsif (json = args.json)
|
|
print_json(json, args.eval_all?)
|
|
elsif args.github?
|
|
raise FormulaOrCaskUnspecifiedError if args.no_named?
|
|
|
|
exec_browser(*args.named.to_formulae_and_casks.map do |formula_keg_or_cask|
|
|
formula_or_cask = T.cast(formula_keg_or_cask, T.any(Formula, Cask::Cask))
|
|
github_info(formula_or_cask)
|
|
end)
|
|
elsif args.no_named?
|
|
print_statistics
|
|
else
|
|
print_info
|
|
end
|
|
end
|
|
|
|
sig { params(remote: String, path: String).returns(String) }
|
|
def github_remote_path(remote, path)
|
|
if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$}
|
|
"https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}"
|
|
else
|
|
"#{remote}/#{path}"
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
sig { void }
|
|
def print_statistics
|
|
return unless HOMEBREW_CELLAR.exist?
|
|
|
|
count = Formula.racks.length
|
|
puts "#{Utils.pluralize("keg", count, include_count: true)}, #{HOMEBREW_CELLAR.dup.abv}"
|
|
end
|
|
|
|
sig { void }
|
|
def print_analytics
|
|
if args.no_named?
|
|
Utils::Analytics.output(args:)
|
|
return
|
|
end
|
|
|
|
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
|
|
puts unless i.zero?
|
|
|
|
case obj
|
|
when Formula
|
|
Utils::Analytics.formula_output(obj, args:)
|
|
when Cask::Cask
|
|
Utils::Analytics.cask_output(obj, args:)
|
|
when FormulaOrCaskUnavailableError
|
|
Utils::Analytics.output(filter: obj.name, args:)
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
|
|
sig { void }
|
|
def print_info
|
|
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
|
|
puts unless i.zero?
|
|
|
|
case obj
|
|
when Formula
|
|
info_formula(obj)
|
|
when Cask::Cask
|
|
info_cask(obj)
|
|
when FormulaOrCaskUnavailableError
|
|
# The formula/cask could not be found
|
|
ofail obj.message
|
|
# No formula with this name, try a missing formula lookup
|
|
if (reason = MissingFormula.reason(obj.name, show_info: true))
|
|
$stderr.puts reason
|
|
end
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
|
|
sig { params(version: T.any(T::Boolean, String)).returns(Symbol) }
|
|
def json_version(version)
|
|
version_hash = {
|
|
true => :default,
|
|
"v1" => :v1,
|
|
"v2" => :v2,
|
|
}
|
|
|
|
raise UsageError, "invalid JSON version: #{version}" unless version_hash.include?(version)
|
|
|
|
version_hash[version]
|
|
end
|
|
|
|
sig { params(json: T.any(T::Boolean, String), eval_all: T::Boolean).void }
|
|
def print_json(json, eval_all)
|
|
raise FormulaOrCaskUnspecifiedError if !(eval_all || args.installed?) && args.no_named?
|
|
|
|
json = case json_version(json)
|
|
when :v1, :default
|
|
raise UsageError, "Cannot specify `--cask` when using `--json=v1`!" if args.cask?
|
|
|
|
formulae = if eval_all
|
|
Formula.all(eval_all:).sort
|
|
elsif args.installed?
|
|
Formula.installed.sort
|
|
else
|
|
args.named.to_formulae
|
|
end
|
|
|
|
if args.variations?
|
|
formulae.map(&:to_hash_with_variations)
|
|
else
|
|
formulae.map(&:to_hash)
|
|
end
|
|
when :v2
|
|
formulae, casks = T.let(
|
|
if eval_all
|
|
[
|
|
Formula.all(eval_all:).sort,
|
|
Cask::Cask.all(eval_all:).sort_by(&:full_name),
|
|
]
|
|
elsif args.installed?
|
|
[Formula.installed.sort, Cask::Caskroom.casks.sort_by(&:full_name)]
|
|
else
|
|
T.cast(args.named.to_formulae_to_casks, [T::Array[Formula], T::Array[Cask::Cask]])
|
|
end, [T::Array[Formula], T::Array[Cask::Cask]]
|
|
)
|
|
|
|
if args.variations?
|
|
{
|
|
"formulae" => formulae.map(&:to_hash_with_variations),
|
|
"casks" => casks.map(&:to_hash_with_variations),
|
|
}
|
|
else
|
|
{
|
|
"formulae" => formulae.map(&:to_hash),
|
|
"casks" => casks.map(&:to_h),
|
|
}
|
|
end
|
|
else
|
|
raise
|
|
end
|
|
|
|
puts JSON.pretty_generate(json)
|
|
end
|
|
|
|
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(String) }
|
|
def github_info(formula_or_cask)
|
|
path = case formula_or_cask
|
|
when Formula
|
|
formula = formula_or_cask
|
|
tap = formula.tap
|
|
return formula.path.to_s if tap.blank? || tap.remote.blank?
|
|
|
|
formula.path.relative_path_from(tap.path)
|
|
when Cask::Cask
|
|
cask = formula_or_cask
|
|
tap = cask.tap
|
|
return cask.sourcefile_path.to_s if tap.blank? || tap.remote.blank?
|
|
|
|
if cask.sourcefile_path.blank? || cask.sourcefile_path.extname != ".rb"
|
|
return "#{tap.default_remote}/blob/HEAD/#{tap.relative_cask_path(cask.token)}"
|
|
end
|
|
|
|
cask.sourcefile_path.relative_path_from(tap.path)
|
|
end
|
|
|
|
github_remote_path(tap.remote, path.to_s)
|
|
end
|
|
|
|
sig { params(formula: Formula).void }
|
|
def info_formula(formula)
|
|
specs = []
|
|
|
|
if (stable = formula.stable)
|
|
string = "stable #{stable.version}"
|
|
string += " (bottled)" if stable.bottled? && formula.pour_bottle?
|
|
specs << string
|
|
end
|
|
|
|
specs << "HEAD" if formula.head
|
|
|
|
attrs = []
|
|
attrs << "pinned at #{formula.pinned_version}" if formula.pinned?
|
|
attrs << "keg-only" if formula.keg_only?
|
|
|
|
puts "#{oh1_title(formula.full_name)}: #{specs * ", "}#{" [#{attrs * ", "}]" unless attrs.empty?}"
|
|
puts formula.desc if formula.desc
|
|
puts Formatter.url(formula.homepage) if formula.homepage
|
|
|
|
deprecate_disable_info_string = DeprecateDisable.message(formula)
|
|
if deprecate_disable_info_string.present?
|
|
deprecate_disable_info_string.tap { |info_string| info_string[0] = info_string[0].upcase }
|
|
puts deprecate_disable_info_string
|
|
end
|
|
|
|
conflicts = formula.conflicts.map do |conflict|
|
|
reason = " (because #{conflict.reason})" if conflict.reason
|
|
"#{conflict.name}#{reason}"
|
|
end.sort!
|
|
unless conflicts.empty?
|
|
puts <<~EOS
|
|
Conflicts with:
|
|
#{conflicts.join("\n ")}
|
|
EOS
|
|
end
|
|
|
|
kegs = formula.installed_kegs
|
|
heads, versioned = kegs.partition { |keg| keg.version.head? }
|
|
kegs = [
|
|
*heads.sort_by { |keg| -keg.tab.time.to_i },
|
|
*versioned.sort_by(&:scheme_and_version),
|
|
]
|
|
if kegs.empty?
|
|
puts "Not installed"
|
|
if (bottle = formula.bottle)
|
|
begin
|
|
bottle.fetch_tab(quiet: !args.debug?) if args.fetch_manifest?
|
|
bottle_size = bottle.bottle_size
|
|
installed_size = bottle.installed_size
|
|
puts "Bottle Size: #{disk_usage_readable(bottle_size)}" if bottle_size
|
|
puts "Installed Size: #{disk_usage_readable(installed_size)}" if installed_size
|
|
rescue RuntimeError => e
|
|
odebug e
|
|
end
|
|
end
|
|
else
|
|
puts "Installed"
|
|
kegs.each do |keg|
|
|
puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}"
|
|
tab = keg.tab.to_s
|
|
puts " #{tab}" unless tab.empty?
|
|
end
|
|
end
|
|
|
|
puts "From: #{Formatter.url(github_info(formula))}"
|
|
|
|
puts "License: #{SPDX.license_expression_to_string formula.license}" if formula.license.present?
|
|
|
|
unless formula.deps.empty?
|
|
ohai "Dependencies"
|
|
%w[build required recommended optional].map do |type|
|
|
deps = formula.deps.send(type).uniq
|
|
puts "#{type.capitalize}: #{decorate_dependencies deps}" unless deps.empty?
|
|
end
|
|
end
|
|
|
|
unless formula.requirements.to_a.empty?
|
|
ohai "Requirements"
|
|
%w[build required recommended optional].map do |type|
|
|
reqs = formula.requirements.select(&:"#{type}?")
|
|
next if reqs.to_a.empty?
|
|
|
|
puts "#{type.capitalize}: #{decorate_requirements(reqs)}"
|
|
end
|
|
end
|
|
|
|
if !formula.options.empty? || formula.head
|
|
ohai "Options"
|
|
Options.dump_for_formula formula
|
|
end
|
|
|
|
caveats = Caveats.new(formula)
|
|
if (caveats_string = caveats.to_s.presence)
|
|
ohai "Caveats", caveats_string
|
|
end
|
|
|
|
Utils::Analytics.formula_output(formula, args:)
|
|
end
|
|
|
|
sig { params(dependencies: T::Array[Dependency]).returns(String) }
|
|
def decorate_dependencies(dependencies)
|
|
deps_status = dependencies.map do |dep|
|
|
if dep.satisfied?([])
|
|
pretty_installed(dep_display_s(dep))
|
|
else
|
|
pretty_uninstalled(dep_display_s(dep))
|
|
end
|
|
end
|
|
deps_status.join(", ")
|
|
end
|
|
|
|
sig { params(requirements: T::Array[Requirement]).returns(String) }
|
|
def decorate_requirements(requirements)
|
|
req_status = requirements.map do |req|
|
|
req_s = req.display_s
|
|
req.satisfied? ? pretty_installed(req_s) : pretty_uninstalled(req_s)
|
|
end
|
|
req_status.join(", ")
|
|
end
|
|
|
|
sig { params(dep: Dependency).returns(String) }
|
|
def dep_display_s(dep)
|
|
return dep.name if dep.option_tags.empty?
|
|
|
|
"#{dep.name} #{dep.option_tags.map { |o| "--#{o}" }.join(" ")}"
|
|
end
|
|
|
|
sig { params(cask: Cask::Cask).void }
|
|
def info_cask(cask)
|
|
require "cask/info"
|
|
|
|
Cask::Info.info(cask, args:)
|
|
end
|
|
|
|
sig { params(title: String, items: T::Array[NameSize]).void }
|
|
def print_sizes_table(title, items)
|
|
return if items.blank?
|
|
|
|
ohai title
|
|
|
|
total_size = items.sum(&:size)
|
|
total_size_str = disk_usage_readable(total_size)
|
|
|
|
name_width = (items.map { |item| item.name.length } + [5]).max
|
|
size_width = (items.map { |item| disk_usage_readable(item.size).length } + [total_size_str.length]).max
|
|
|
|
items.each do |item|
|
|
puts format("%-#{name_width}s %#{size_width}s", item.name,
|
|
disk_usage_readable(item.size))
|
|
end
|
|
|
|
puts format("%-#{name_width}s %#{size_width}s", "Total", total_size_str)
|
|
end
|
|
|
|
sig { params(formulae: T::Array[Formula], casks: T::Array[Cask::Cask]).void }
|
|
def print_sizes(formulae: [], casks: [])
|
|
if formulae.blank? &&
|
|
(args.formulae? || (!args.casks? && args.no_named?))
|
|
formulae = Formula.installed
|
|
end
|
|
|
|
if casks.blank? &&
|
|
(args.casks? || (!args.formulae? && args.no_named?))
|
|
casks = Cask::Caskroom.casks
|
|
end
|
|
|
|
unless args.casks?
|
|
formula_sizes = formulae.map do |formula|
|
|
kegs = formula.installed_kegs
|
|
size = kegs.sum(&:disk_usage)
|
|
NameSize.new(name: formula.full_name, size:)
|
|
end
|
|
formula_sizes.sort_by! { |f| -f.size }
|
|
print_sizes_table("Formulae sizes:", formula_sizes)
|
|
end
|
|
|
|
return if casks.blank? || args.formulae?
|
|
|
|
cask_sizes = casks.filter_map do |cask|
|
|
installed_version = cask.installed_version
|
|
next unless installed_version.present?
|
|
|
|
versioned_staged_path = cask.caskroom_path.join(installed_version)
|
|
next unless versioned_staged_path.exist?
|
|
|
|
size = versioned_staged_path.children.sum(&:disk_usage)
|
|
NameSize.new(name: cask.full_name, size:)
|
|
end
|
|
cask_sizes.sort_by! { |c| -c.size }
|
|
print_sizes_table("Casks sizes:", cask_sizes)
|
|
end
|
|
end
|
|
end
|
|
end
|