brew/Library/Homebrew/dev-cmd/unbottled.rb
apainintheneck e48c155f42 dev-cmd/unbottled: share common parts of bottle tag regex
This makes it easier to see that the two regexes are related
and could make it easier to change them in the future if
need be.
2023-10-17 22:41:50 -07:00

315 lines
9.5 KiB
Ruby

# typed: true
# frozen_string_literal: true
require "cli/parser"
require "formula"
require "api"
require "os/mac/xcode"
module Homebrew
module_function
sig { returns(CLI::Parser) }
def unbottled_args
Homebrew::CLI::Parser.new 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. " \
"Implied if `HOMEBREW_EVAL_ALL` is set."
conflicts "--dependents", "--total", "--lost"
named_args :formula
end
end
sig { void }
def unbottled
args = unbottled_args.parse
Formulary.enable_factory_cache!
@bottle_tag = if (tag = args.tag)
Utils::Bottles::Tag.from_symbol(tag.to_sym)
else
Utils::Bottles.tag
end
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
os = @bottle_tag.system
arch = if Hardware::CPU::INTEL_ARCHS.include?(@bottle_tag.arch)
:intel
elsif Hardware::CPU::ARM_ARCHS.include?(@bottle_tag.arch)
:arm
else
raise "Unknown arch #{@bottle_tag.arch}."
end
Homebrew::SimulateSystem.with os: os, arch: arch do
all = args.eval_all?
if args.total?
if !all && !Homebrew::EnvConfig.eval_all?
raise UsageError, "`brew unbottled --total` needs `--eval-all` passed or `HOMEBREW_EVAL_ALL` set!"
end
all = true
end
if args.named.blank?
ohai "Getting formulae..."
elsif all
raise UsageError, "Cannot specify formulae when using `--eval-all`/`--total`."
end
formulae, all_formulae, formula_installs =
formulae_all_installs_from_args(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
noun, hash = if args.named.present?
[nil, {}]
elsif args.dependents?
["dependents", formula_dependents]
else
["installs", formula_installs]
end
output_unbottled(formulae, deps_hash, noun, hash, args.named.present?)
end
end
def formulae_all_installs_from_args(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, "`brew unbottled --dependents` needs `--eval-all` passed or `HOMEBREW_EVAL_ALL` set!"
end
formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
@sort = " (sorted by number of dependents)"
elsif all
formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
else
formula_installs = {}
ohai "Getting analytics data..."
analytics = Homebrew::API::Analytics.fetch "install", 90
if analytics.blank?
raise UsageError,
"default sort by analytics data requires " \
"`HOMEBREW_NO_GITHUB_API` and `HOMEBREW_NO_ANALYTICS` to be unset"
end
formulae = analytics["items"].map do |i|
f = i["formula"].split.first
next if f.include?("/")
next if formula_installs[f].present?
formula_installs[f] = i["count"]
begin
Formula[f]
rescue FormulaUnavailableError
nil
end
end.compact
@sort = " (sorted by installs in the last 90 days; top 10,000 only)"
all_formulae = Formula.all(eval_all: args.eval_all?)
end
[formulae, all_formulae, formula_installs]
end
def deps_uses_from_formulae(all_formulae)
ohai "Populating dependency tree..."
deps_hash = {}
uses_hash = {}
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
deps.each do |dep|
uses_hash[dep.name] ||= []
uses_hash[dep.name] << f
end
end
[deps_hash, uses_hash]
end
def output_total(formulae)
ohai "Unbottled :#{@bottle_tag} formulae"
unbottled_formulae = 0
formulae.each do |f|
next if f.bottle_specification.tag?(@bottle_tag)
unbottled_formulae += 1
end
puts "#{unbottled_formulae}/#{formulae.length} remaining."
end
def output_unbottled(formulae, deps_hash, noun, hash, any_named_args)
ohai ":#{@bottle_tag} bottle status#{@sort}"
any_found = T.let(false, T::Boolean)
formulae.each do |f|
name = f.name.downcase
if f.disabled?
puts "#{Tty.bold}#{Tty.green}#{name}#{Tty.reset}: formula disabled" if any_named_args
next
end
requirements = f.recursive_requirements
if @bottle_tag.linux?
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
end
elsif requirements.any?(LinuxRequirement)
puts "#{Tty.bold}#{Tty.red}#{name}#{Tty.reset}: requires Linux" if any_named_args
next
else
macos_version = @bottle_tag.to_macos_version
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
Version.new(MacOS::Xcode.latest_version(macos: macos_version)) >= r.version
when ArchRequirement
r.arch == @bottle_tag.arch
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
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
deps = Array(deps_hash[f.name]).reject do |dep|
dep.bottle_specification.tag?(@bottle_tag, no_older_versions: true)
end
if deps.blank?
count = " (#{hash[f.name]} #{noun})" if noun
puts "#{Tty.bold}#{Tty.green}#{name}#{Tty.reset}#{count}: ready to bottle"
next
end
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
puts "No unbottled dependencies found!"
end
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
end
end
end
return if !commit || !formula || !lost_bottles.positive? || processed_formulae.include?(formula)
puts "#{commit}: bottle lost for #{formula}"
end
end