2024-06-30 17:49:27 +01:00
|
|
|
# typed: strict
|
2020-11-26 08:17:20 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
require "abstract_command"
|
2020-11-26 08:17:20 +00:00
|
|
|
require "formula"
|
2021-08-06 02:30:44 -04:00
|
|
|
require "api"
|
2023-03-02 23:28:56 -05:00
|
|
|
require "os/mac/xcode"
|
2020-11-26 08:17:20 +00:00
|
|
|
|
|
|
|
module Homebrew
|
2024-03-21 21:59:48 -07:00
|
|
|
module DevCmd
|
|
|
|
class Unbottled < AbstractCommand
|
|
|
|
cmd_args do
|
|
|
|
description <<~EOS
|
|
|
|
Show the unbottled dependents of formulae.
|
|
|
|
EOS
|
|
|
|
flag "--tag=",
|
|
|
|
description: "Use the specified bottle tag (e.g. `big_sur`) instead of the current OS."
|
|
|
|
switch "--dependents",
|
|
|
|
description: "Skip getting analytics data and sort by number of dependents instead."
|
|
|
|
switch "--total",
|
|
|
|
description: "Print the number of unbottled and total formulae."
|
|
|
|
switch "--lost",
|
|
|
|
description: "Print the `homebrew/core` commits where bottles were lost in the last week."
|
|
|
|
switch "--eval-all",
|
|
|
|
description: "Evaluate all available formulae and casks, whether installed or not, to check them. " \
|
2025-06-03 11:06:35 -04:00
|
|
|
"Implied if `$HOMEBREW_EVAL_ALL` is set."
|
2024-03-21 21:59:48 -07:00
|
|
|
|
|
|
|
conflicts "--dependents", "--total", "--lost"
|
|
|
|
|
|
|
|
named_args :formula
|
2023-10-15 15:14:48 -07:00
|
|
|
end
|
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
sig { override.void }
|
|
|
|
def run
|
|
|
|
Formulary.enable_factory_cache!
|
2022-11-15 05:22:42 +00:00
|
|
|
|
2024-07-01 19:13:38 +01:00
|
|
|
@bottle_tag = T.let(
|
|
|
|
if (tag = args.tag)
|
|
|
|
Utils::Bottles::Tag.from_symbol(tag.to_sym)
|
|
|
|
else
|
|
|
|
Utils::Bottles.tag
|
|
|
|
end,
|
|
|
|
T.nilable(Utils::Bottles::Tag),
|
|
|
|
)
|
2024-07-02 15:24:01 +01:00
|
|
|
return unless @bottle_tag
|
2023-07-06 16:47:09 +01:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if args.lost?
|
|
|
|
if args.named.present?
|
|
|
|
raise UsageError, "`brew unbottled --lost` cannot be used with formula arguments!"
|
|
|
|
elsif !CoreTap.instance.installed?
|
|
|
|
raise UsageError, "`brew unbottled --lost` requires `homebrew/core` to be tapped locally!"
|
|
|
|
else
|
|
|
|
output_lost_bottles
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
2022-03-08 19:24:55 +00:00
|
|
|
|
2024-07-02 15:24:01 +01:00
|
|
|
os = @bottle_tag.system
|
|
|
|
arch = if Hardware::CPU::INTEL_ARCHS.include?(@bottle_tag.arch)
|
2024-03-21 21:59:48 -07:00
|
|
|
:intel
|
2024-07-02 15:24:01 +01:00
|
|
|
elsif Hardware::CPU::ARM_ARCHS.include?(@bottle_tag.arch)
|
2024-03-21 21:59:48 -07:00
|
|
|
:arm
|
|
|
|
else
|
2024-07-02 15:24:01 +01:00
|
|
|
raise "Unknown arch #{@bottle_tag.arch}."
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
Homebrew::SimulateSystem.with(os:, arch:) do
|
|
|
|
all = args.eval_all?
|
|
|
|
if args.total?
|
|
|
|
if !all && !Homebrew::EnvConfig.eval_all?
|
2025-06-03 11:06:35 -04:00
|
|
|
raise UsageError, "`brew unbottled --total` needs `--eval-all` passed or `$HOMEBREW_EVAL_ALL` set!"
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
all = true
|
|
|
|
end
|
2020-12-28 09:49:54 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if args.named.blank?
|
|
|
|
ohai "Getting formulae..."
|
|
|
|
elsif all
|
|
|
|
raise UsageError, "Cannot specify formulae when using `--eval-all`/`--total`."
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
formulae, all_formulae, formula_installs = formulae_all_installs_from_args(all)
|
|
|
|
deps_hash, uses_hash = deps_uses_from_formulae(all_formulae)
|
|
|
|
|
|
|
|
if args.dependents?
|
|
|
|
formula_dependents = {}
|
|
|
|
formulae = formulae.sort_by do |f|
|
|
|
|
dependents = uses_hash[f.name]&.length || 0
|
|
|
|
formula_dependents[f.name] ||= dependents
|
|
|
|
end.reverse
|
|
|
|
elsif all
|
|
|
|
output_total(formulae)
|
|
|
|
return
|
|
|
|
end
|
2023-07-06 16:47:09 +01:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
noun, hash = if args.named.present?
|
|
|
|
[nil, {}]
|
|
|
|
elsif args.dependents?
|
|
|
|
["dependents", formula_dependents]
|
|
|
|
else
|
|
|
|
["installs", formula_installs]
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-07-02 15:24:01 +01:00
|
|
|
return if hash.nil?
|
|
|
|
|
|
|
|
output_unbottled(formulae, deps_hash, noun, hash, args.named.present?)
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
private
|
2020-11-26 16:54:21 +00:00
|
|
|
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
|
|
|
params(all: T::Boolean).returns([T::Array[Formula], T::Array[Formula], T.nilable(T::Hash[Symbol, Integer])])
|
|
|
|
}
|
2024-03-21 21:59:48 -07:00
|
|
|
def formulae_all_installs_from_args(all)
|
|
|
|
if args.named.present?
|
|
|
|
formulae = all_formulae = args.named.to_formulae
|
|
|
|
elsif args.dependents?
|
|
|
|
if !args.eval_all? && !Homebrew::EnvConfig.eval_all?
|
|
|
|
raise UsageError,
|
2025-06-03 11:06:35 -04:00
|
|
|
"`brew unbottled --dependents` needs `--eval-all` passed or `$HOMEBREW_EVAL_ALL` set!"
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
2020-11-26 16:54:21 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-06-30 17:49:27 +01:00
|
|
|
@sort = T.let(" (sorted by number of dependents)", T.nilable(String))
|
2024-03-21 21:59:48 -07:00
|
|
|
elsif all
|
|
|
|
formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
|
|
|
|
else
|
|
|
|
formula_installs = {}
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
ohai "Getting analytics data..."
|
|
|
|
analytics = Homebrew::API::Analytics.fetch "install", 90
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if analytics.blank?
|
|
|
|
raise UsageError,
|
|
|
|
"default sort by analytics data requires " \
|
2025-06-03 11:06:35 -04:00
|
|
|
"`$HOMEBREW_NO_GITHUB_API` and `$HOMEBREW_NO_ANALYTICS` to be unset."
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
2023-10-16 22:20:48 +02:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
formulae = analytics["items"].filter_map do |i|
|
|
|
|
f = i["formula"].split.first
|
|
|
|
next if f.include?("/")
|
|
|
|
next if formula_installs[f].present?
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
formula_installs[f] = i["count"]
|
|
|
|
begin
|
|
|
|
Formula[f]
|
|
|
|
rescue FormulaUnavailableError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
2024-06-30 17:49:27 +01:00
|
|
|
@sort = T.let(" (sorted by installs in the last 90 days; top 10,000 only)", T.nilable(String))
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
all_formulae = Formula.all(eval_all: args.eval_all?)
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-09-14 10:33:35 -04:00
|
|
|
# Remove deprecated and disabled formulae as we do not care if they are unbottled
|
|
|
|
formulae = Array(formulae).reject { |f| f.deprecated? || f.disabled? } if formulae.present?
|
|
|
|
all_formulae = Array(all_formulae).reject { |f| f.deprecated? || f.disabled? } if all_formulae.present?
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-07-01 19:13:38 +01:00
|
|
|
[T.let(formulae, T::Array[Formula]), T.let(all_formulae, T::Array[Formula]),
|
2024-07-02 15:24:01 +01:00
|
|
|
T.let(formula_installs, T.nilable(T::Hash[Symbol, Integer]))]
|
2020-11-26 08:17:20 +00:00
|
|
|
end
|
|
|
|
|
2024-07-04 10:35:00 +01:00
|
|
|
sig {
|
|
|
|
params(all_formulae: T::Array[Formula]).returns([T::Hash[String, T.untyped], T::Hash[String, T.untyped]])
|
|
|
|
}
|
2024-03-21 21:59:48 -07:00
|
|
|
def deps_uses_from_formulae(all_formulae)
|
|
|
|
ohai "Populating dependency tree..."
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
deps_hash = {}
|
|
|
|
uses_hash = {}
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
all_formulae.each do |f|
|
|
|
|
deps = Dependency.expand(f, cache_key: "unbottled") do |_, dep|
|
|
|
|
Dependency.prune if dep.optional?
|
|
|
|
end.map(&:to_formula)
|
|
|
|
deps_hash[f.name] = deps
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
deps.each do |dep|
|
|
|
|
uses_hash[dep.name] ||= []
|
|
|
|
uses_hash[dep.name] << f
|
|
|
|
end
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
[deps_hash, uses_hash]
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-07-04 10:35:00 +01:00
|
|
|
sig { params(formulae: T::Array[Formula]).void }
|
2024-03-21 21:59:48 -07:00
|
|
|
def output_total(formulae)
|
2024-07-02 15:24:01 +01:00
|
|
|
return unless @bottle_tag
|
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
ohai "Unbottled :#{@bottle_tag} formulae"
|
2024-09-14 10:33:35 -04:00
|
|
|
unbottled_formulae = formulae.count do |f|
|
|
|
|
!f.bottle_specification.tag?(@bottle_tag, no_older_versions: true)
|
2021-02-23 04:41:29 +00:00
|
|
|
end
|
2020-12-28 09:49:54 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
puts "#{unbottled_formulae}/#{formulae.length} remaining."
|
2021-05-31 15:24:20 +01:00
|
|
|
end
|
|
|
|
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
|
|
|
params(formulae: T::Array[Formula], deps_hash: T::Hash[T.any(Symbol, String), T.untyped],
|
|
|
|
noun: T.nilable(String), hash: T::Hash[T.any(Symbol, String), T.untyped],
|
2024-07-04 10:35:00 +01:00
|
|
|
any_named_args: T::Boolean).void
|
2024-07-01 19:13:38 +01:00
|
|
|
}
|
2024-03-21 21:59:48 -07:00
|
|
|
def output_unbottled(formulae, deps_hash, noun, hash, any_named_args)
|
2024-07-02 15:24:01 +01:00
|
|
|
return unless @bottle_tag
|
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
ohai ":#{@bottle_tag} bottle status#{@sort}"
|
|
|
|
any_found = T.let(false, T::Boolean)
|
2020-12-23 10:57:10 +01:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
formulae.each do |f|
|
|
|
|
name = f.name.downcase
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if f.disabled?
|
|
|
|
puts "#{Tty.bold}#{Tty.green}#{name}#{Tty.reset}: formula disabled" if any_named_args
|
|
|
|
next
|
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
requirements = f.recursive_requirements
|
2024-07-02 15:24:01 +01:00
|
|
|
if @bottle_tag.linux?
|
2024-03-21 21:59:48 -07:00
|
|
|
if requirements.any? { |r| r.is_a?(MacOSRequirement) && !r.version }
|
|
|
|
puts "#{Tty.bold}#{Tty.red}#{name}#{Tty.reset}: requires macOS" if any_named_args
|
|
|
|
next
|
2025-04-02 20:58:27 -04:00
|
|
|
elsif requirements.any? { |r| r.is_a?(ArchRequirement) && r.arch != @bottle_tag.arch }
|
|
|
|
if any_named_args
|
|
|
|
puts "#{Tty.bold}#{Tty.red}#{name}#{Tty.reset}: doesn't support #{@bottle_tag.arch} Linux"
|
|
|
|
end
|
|
|
|
next
|
2024-03-21 21:59:48 -07:00
|
|
|
end
|
|
|
|
elsif requirements.any?(LinuxRequirement)
|
|
|
|
puts "#{Tty.bold}#{Tty.red}#{name}#{Tty.reset}: requires Linux" if any_named_args
|
|
|
|
next
|
|
|
|
else
|
2024-07-02 15:24:01 +01:00
|
|
|
macos_version = @bottle_tag.to_macos_version
|
2024-03-21 21:59:48 -07:00
|
|
|
macos_satisfied = requirements.all? do |r|
|
|
|
|
case r
|
|
|
|
when MacOSRequirement
|
|
|
|
next true unless r.version_specified?
|
|
|
|
|
|
|
|
macos_version.compare(r.comparator, r.version)
|
|
|
|
when XcodeRequirement
|
|
|
|
next true unless r.version
|
|
|
|
|
2025-01-12 23:31:46 -08:00
|
|
|
Version.new(::OS::Mac::Xcode.latest_version(macos: macos_version)) >= r.version
|
2024-03-21 21:59:48 -07:00
|
|
|
when ArchRequirement
|
2024-07-02 15:24:01 +01:00
|
|
|
r.arch == @bottle_tag.arch
|
2024-03-21 21:59:48 -07:00
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
unless macos_satisfied
|
|
|
|
puts "#{Tty.bold}#{Tty.red}#{name}#{Tty.reset}: doesn't support this macOS" if any_named_args
|
|
|
|
next
|
|
|
|
end
|
|
|
|
end
|
2023-10-15 15:14:48 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if f.bottle_specification.tag?(@bottle_tag, no_older_versions: true)
|
|
|
|
puts "#{Tty.bold}#{Tty.green}#{name}#{Tty.reset}: already bottled" if any_named_args
|
|
|
|
next
|
|
|
|
end
|
2023-10-16 20:51:38 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
deps = Array(deps_hash[f.name]).reject do |dep|
|
|
|
|
dep.bottle_specification.tag?(@bottle_tag, no_older_versions: true)
|
|
|
|
end
|
2023-10-17 22:41:50 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
if deps.blank?
|
|
|
|
count = " (#{hash[f.name]} #{noun})" if noun
|
|
|
|
puts "#{Tty.bold}#{Tty.green}#{name}#{Tty.reset}#{count}: ready to bottle"
|
|
|
|
next
|
|
|
|
end
|
2023-10-15 15:14:48 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
any_found ||= true
|
|
|
|
count = " (#{hash[f.name]} #{noun})" if noun
|
|
|
|
puts "#{Tty.bold}#{Tty.yellow}#{name}#{Tty.reset}#{count}: unbottled deps: #{deps.join(" ")}"
|
|
|
|
end
|
|
|
|
return if any_found
|
|
|
|
return if any_named_args
|
2023-10-15 15:14:48 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
puts "No unbottled dependencies found!"
|
|
|
|
end
|
2023-10-15 15:14:48 -07:00
|
|
|
|
2024-07-04 10:35:00 +01:00
|
|
|
sig { void }
|
2024-03-21 21:59:48 -07:00
|
|
|
def output_lost_bottles
|
|
|
|
ohai ":#{@bottle_tag} lost bottles"
|
|
|
|
|
|
|
|
bottle_tag_regex_fragment = " +sha256.* #{@bottle_tag}: "
|
|
|
|
|
|
|
|
# $ git log --patch --no-ext-diff -G'^ +sha256.* sonoma:' --since=@{'1 week ago'}
|
|
|
|
git_log = %w[git log --patch --no-ext-diff]
|
|
|
|
git_log << "-G^#{bottle_tag_regex_fragment}"
|
|
|
|
git_log << "--since=@{'1 week ago'}"
|
|
|
|
|
|
|
|
bottle_tag_sha_regex = /^[+-]#{bottle_tag_regex_fragment}/
|
|
|
|
|
|
|
|
processed_formulae = Set.new
|
|
|
|
commit = T.let(nil, T.nilable(String))
|
|
|
|
formula = T.let(nil, T.nilable(String))
|
|
|
|
lost_bottles = 0
|
|
|
|
|
|
|
|
CoreTap.instance.path.cd do
|
|
|
|
Utils.safe_popen_read(*git_log) do |io|
|
|
|
|
io.each_line do |line|
|
|
|
|
case line
|
|
|
|
when /^commit [0-9a-f]{40}$/
|
|
|
|
# Example match: `commit 7289b409b96a752540befef1a56b8a818baf1db7`
|
|
|
|
if commit && formula && lost_bottles.positive? && processed_formulae.exclude?(formula)
|
|
|
|
puts "#{commit}: bottle lost for #{formula}"
|
|
|
|
end
|
|
|
|
processed_formulae << formula
|
|
|
|
commit = line.split.last
|
|
|
|
formula = nil
|
|
|
|
when %r{^diff --git a/Formula/}
|
|
|
|
# Example match: `diff --git a/Formula/a/aws-cdk.rb b/Formula/a/aws-cdk.rb`
|
|
|
|
formula = line.split("/").last.chomp(".rb\n")
|
|
|
|
formula = CoreTap.instance.formula_renames.fetch(formula, formula)
|
|
|
|
lost_bottles = 0
|
|
|
|
when bottle_tag_sha_regex
|
|
|
|
# Example match: `- sha256 cellar: :any_skip_relocation, sonoma: "f0a4..."`
|
|
|
|
next if processed_formulae.include?(formula)
|
|
|
|
|
|
|
|
case line.chr
|
|
|
|
when "+" then lost_bottles -= 1
|
|
|
|
when "-" then lost_bottles += 1
|
|
|
|
end
|
|
|
|
when /^[+] +sha256.* all: /
|
|
|
|
# Example match: `+ sha256 cellar: :any_skip_relocation, all: "9e35..."`
|
|
|
|
lost_bottles -= 1
|
|
|
|
end
|
2023-10-15 15:14:48 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
return if !commit || !formula || !lost_bottles.positive? || processed_formulae.include?(formula)
|
2023-10-15 15:14:48 -07:00
|
|
|
|
2024-03-21 21:59:48 -07:00
|
|
|
puts "#{commit}: bottle lost for #{formula}"
|
|
|
|
end
|
|
|
|
end
|
2023-10-15 15:14:48 -07:00
|
|
|
end
|
2020-11-26 08:17:20 +00:00
|
|
|
end
|