Port Homebrew::DevCmd::Bottle

This commit is contained in:
Douglas Eichelberger 2024-03-18 12:31:44 -07:00
parent 0f2efd3939
commit ee0c967ce0
3 changed files with 792 additions and 786 deletions

View File

@ -1,6 +1,7 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "formula" require "formula"
require "utils/bottles" require "utils/bottles"
require "tab" require "tab"
@ -13,7 +14,12 @@ require "utils/gzip"
require "api" require "api"
require "extend/hash/deep_merge" require "extend/hash/deep_merge"
BOTTLE_ERB = <<-EOS.freeze module Homebrew
module DevCmd
class Bottle < AbstractCommand
include FileUtils
BOTTLE_ERB = <<-EOS.freeze
bottle do bottle do
<% if [HOMEBREW_BOTTLE_DEFAULT_DOMAIN.to_s, <% if [HOMEBREW_BOTTLE_DEFAULT_DOMAIN.to_s,
"#{HOMEBREW_BOTTLE_DEFAULT_DOMAIN}/bottles"].exclude?(root_url) %> "#{HOMEBREW_BOTTLE_DEFAULT_DOMAIN}/bottles"].exclude?(root_url) %>
@ -28,18 +34,15 @@ BOTTLE_ERB = <<-EOS.freeze
<%= line %> <%= line %>
<% end %> <% end %>
end end
EOS EOS
MAXIMUM_STRING_MATCHES = 100 MAXIMUM_STRING_MATCHES = 100
ALLOWABLE_HOMEBREW_REPOSITORY_LINKS = [ ALLOWABLE_HOMEBREW_REPOSITORY_LINKS = [
%r{#{Regexp.escape(HOMEBREW_LIBRARY)}/Homebrew/os/(mac|linux)/pkgconfig}, %r{#{Regexp.escape(HOMEBREW_LIBRARY)}/Homebrew/os/(mac|linux)/pkgconfig},
].freeze ].freeze
module Homebrew cmd_args do
sig { returns(CLI::Parser) }
def self.bottle_args
Homebrew::CLI::Parser.new do
description <<~EOS description <<~EOS
Generate a bottle (binary package) from a formula that was installed with Generate a bottle (binary package) from a formula that was installed with
`--build-bottle`. `--build-bottle`.
@ -89,24 +92,22 @@ module Homebrew
named_args [:installed_formula, :file], min: 1, without_api: true named_args [:installed_formula, :file], min: 1, without_api: true
end end
end
def self.bottle
args = bottle_args.parse
sig { override.void }
def run
if args.merge? if args.merge?
Homebrew.install_bundler_gems!(groups: ["ast"]) Homebrew.install_bundler_gems!(groups: ["ast"])
return merge(args:) return merge
end end
gnu_tar_formula_ensure_installed_if_needed!(only_json_tab: args.only_json_tab?) gnu_tar_formula_ensure_installed_if_needed!
args.named.to_resolved_formulae(uniq: false).each do |formula| args.named.to_resolved_formulae(uniq: false).each do |formula|
bottle_formula formula, args: bottle_formula formula
end end
end end
def self.keg_contain?(string, keg, ignores, formula_and_runtime_deps_names = nil, args:) def keg_contain?(string, keg, ignores, formula_and_runtime_deps_names = nil)
@put_string_exists_header, @put_filenames = nil @put_string_exists_header, @put_filenames = nil
print_filename = lambda do |str, filename| print_filename = lambda do |str, filename|
@ -138,7 +139,8 @@ module Homebrew
end end
end end
text_matches = Keg.text_matches_in_file(file, string, ignores, linked_libraries, formula_and_runtime_deps_names) text_matches = Keg.text_matches_in_file(file, string, ignores, linked_libraries,
formula_and_runtime_deps_names)
result = true if text_matches.any? result = true if text_matches.any?
next if !args.verbose? || text_matches.empty? next if !args.verbose? || text_matches.empty?
@ -153,10 +155,10 @@ module Homebrew
end end
end end
keg_contain_absolute_symlink_starting_with?(string, keg, args:) || result keg_contain_absolute_symlink_starting_with?(string, keg) || result
end end
def self.keg_contain_absolute_symlink_starting_with?(string, keg, args:) def keg_contain_absolute_symlink_starting_with?(string, keg)
absolute_symlinks_start_with_string = [] absolute_symlinks_start_with_string = []
keg.find do |pn| keg.find do |pn|
next if !pn.symlink? || !(link = pn.readlink).absolute? next if !pn.symlink? || !(link = pn.readlink).absolute?
@ -174,7 +176,7 @@ module Homebrew
!absolute_symlinks_start_with_string.empty? !absolute_symlinks_start_with_string.empty?
end end
def self.cellar_parameter_needed?(cellar) def cellar_parameter_needed?(cellar)
default_cellars = [ default_cellars = [
Homebrew::DEFAULT_MACOS_CELLAR, Homebrew::DEFAULT_MACOS_CELLAR,
Homebrew::DEFAULT_MACOS_ARM_CELLAR, Homebrew::DEFAULT_MACOS_ARM_CELLAR,
@ -183,7 +185,7 @@ module Homebrew
cellar.present? && default_cellars.exclude?(cellar) cellar.present? && default_cellars.exclude?(cellar)
end end
def self.generate_sha256_line(tag, digest, cellar, tag_column, digest_column) def generate_sha256_line(tag, digest, cellar, tag_column, digest_column)
line = "sha256 " line = "sha256 "
tag_column += line.length tag_column += line.length
digest_column += line.length digest_column += line.length
@ -198,7 +200,7 @@ module Homebrew
%Q(#{line}"#{digest}") %Q(#{line}"#{digest}")
end end
def self.bottle_output(bottle, root_url_using) def bottle_output(bottle, root_url_using)
cellars = bottle.checksums.filter_map do |checksum| cellars = bottle.checksums.filter_map do |checksum|
cellar = checksum["cellar"] cellar = checksum["cellar"]
next unless cellar_parameter_needed? cellar next unless cellar_parameter_needed? cellar
@ -226,24 +228,24 @@ module Homebrew
erb.result(erb_binding).gsub(/^\s*$\n/, "") erb.result(erb_binding).gsub(/^\s*$\n/, "")
end end
def self.sudo_purge def sudo_purge
return unless ENV["HOMEBREW_BOTTLE_SUDO_PURGE"] return unless ENV["HOMEBREW_BOTTLE_SUDO_PURGE"]
system "/usr/bin/sudo", "--non-interactive", "/usr/sbin/purge" system "/usr/bin/sudo", "--non-interactive", "/usr/sbin/purge"
end end
sig { returns(T::Array[String]) } sig { returns(T::Array[String]) }
def self.tar_args def tar_args
[].freeze [].freeze
end end
sig { params(gnu_tar_formula: Formula).returns(String) } sig { params(gnu_tar_formula: Formula).returns(String) }
def self.gnu_tar(gnu_tar_formula) def gnu_tar(gnu_tar_formula)
"#{gnu_tar_formula.opt_bin}/tar" "#{gnu_tar_formula.opt_bin}/tar"
end end
sig { params(mtime: String).returns(T::Array[String]) } sig { params(mtime: String).returns(T::Array[String]) }
def self.reproducible_gnutar_args(mtime) def reproducible_gnutar_args(mtime)
# Ensure gnu tar is set up for reproducibility. # Ensure gnu tar is set up for reproducibility.
# https://reproducible-builds.org/docs/archives/ # https://reproducible-builds.org/docs/archives/
[ [
@ -260,8 +262,8 @@ module Homebrew
].freeze ].freeze
end end
sig { params(only_json_tab: T::Boolean).returns(T.nilable(Formula)) } sig { returns(T.nilable(Formula)) }
def self.gnu_tar_formula_ensure_installed_if_needed!(only_json_tab:) def gnu_tar_formula_ensure_installed_if_needed!
gnu_tar_formula = begin gnu_tar_formula = begin
Formula["gnu-tar"] Formula["gnu-tar"]
rescue FormulaUnavailableError rescue FormulaUnavailableError
@ -274,21 +276,21 @@ module Homebrew
gnu_tar_formula gnu_tar_formula
end end
sig { params(args: T.untyped, mtime: String).returns([String, T::Array[String]]) } sig { params(mtime: String).returns([String, T::Array[String]]) }
def self.setup_tar_and_args!(args, mtime) def setup_tar_and_args!(mtime)
# Without --only-json-tab bottles are never reproducible # Without --only-json-tab bottles are never reproducible
default_tar_args = ["tar", tar_args].freeze default_tar_args = ["tar", tar_args].freeze
return default_tar_args unless args.only_json_tab? return default_tar_args unless args.only_json_tab?
# Use gnu-tar as it can be set up for reproducibility better than libarchive # Use gnu-tar as it can be set up for reproducibility better than libarchive
# and to be consistent between macOS and Linux. # and to be consistent between macOS and Linux.
gnu_tar_formula = gnu_tar_formula_ensure_installed_if_needed!(only_json_tab: args.only_json_tab?) gnu_tar_formula = gnu_tar_formula_ensure_installed_if_needed!
return default_tar_args if gnu_tar_formula.blank? return default_tar_args if gnu_tar_formula.blank?
[gnu_tar(gnu_tar_formula), reproducible_gnutar_args(mtime)].freeze [gnu_tar(gnu_tar_formula), reproducible_gnutar_args(mtime)].freeze
end end
def self.formula_ignores(formula) def formula_ignores(formula)
ignores = [] ignores = []
cellar_regex = Regexp.escape(HOMEBREW_CELLAR) cellar_regex = Regexp.escape(HOMEBREW_CELLAR)
prefix_regex = Regexp.escape(HOMEBREW_PREFIX) prefix_regex = Regexp.escape(HOMEBREW_PREFIX)
@ -318,7 +320,7 @@ module Homebrew
ignores.compact ignores.compact
end end
def self.bottle_formula(formula, args:) def bottle_formula(formula)
local_bottle_json = args.json? && formula.local_bottle_path.present? local_bottle_json = args.json? && formula.local_bottle_path.present?
unless local_bottle_json unless local_bottle_json
@ -366,7 +368,7 @@ module Homebrew
end || 0 end || 0
end end
filename = Bottle::Filename.create(formula, bottle_tag, rebuild) filename = ::Bottle::Filename.create(formula, bottle_tag, rebuild)
local_filename = filename.to_s local_filename = filename.to_s
bottle_path = Pathname.pwd/local_filename bottle_path = Pathname.pwd/local_filename
@ -395,7 +397,8 @@ module Homebrew
tab_json = Utils::Bottles.file_from_bottle(bottle_path, tab_path) tab_json = Utils::Bottles.file_from_bottle(bottle_path, tab_path)
tab = Tab.from_file_content(tab_json, tab_path) tab = Tab.from_file_content(tab_json, tab_path)
tag_spec = Formula[formula.name].bottle_specification.tag_specification_for(bottle_tag, no_older_versions: true) tag_spec = Formula[formula.name].bottle_specification.tag_specification_for(bottle_tag,
no_older_versions: true)
relocatable = [:any, :any_skip_relocation].include?(tag_spec.cellar) relocatable = [:any, :any_skip_relocation].include?(tag_spec.cellar)
skip_relocation = tag_spec.cellar == :any_skip_relocation skip_relocation = tag_spec.cellar == :any_skip_relocation
@ -445,7 +448,7 @@ module Homebrew
sudo_purge sudo_purge
# Tar then gzip for reproducible bottles. # Tar then gzip for reproducible bottles.
tar_mtime = tab.source_modified_time.strftime("%Y-%m-%d %H:%M:%S") tar_mtime = tab.source_modified_time.strftime("%Y-%m-%d %H:%M:%S")
tar, tar_args = setup_tar_and_args!(args, tar_mtime) tar, tar_args = setup_tar_and_args!(tar_mtime)
safe_system tar, "--create", "--numeric-owner", safe_system tar, "--create", "--numeric-owner",
*tar_args, *tar_args,
"--file", tar_path, "#{formula.name}/#{formula.pkg_version}" "--file", tar_path, "#{formula.name}/#{formula.pkg_version}"
@ -482,7 +485,7 @@ module Homebrew
else else
HOMEBREW_REPOSITORY HOMEBREW_REPOSITORY
end.to_s end.to_s
if keg_contain?(repository_reference, keg, ignores + ALLOWABLE_HOMEBREW_REPOSITORY_LINKS, args:) if keg_contain?(repository_reference, keg, ignores + ALLOWABLE_HOMEBREW_REPOSITORY_LINKS)
odie "Bottle contains non-relocatable reference to #{repository_reference}!" odie "Bottle contains non-relocatable reference to #{repository_reference}!"
end end
@ -490,16 +493,14 @@ module Homebrew
if args.skip_relocation? if args.skip_relocation?
skip_relocation = true skip_relocation = true
else else
relocatable = false if keg_contain?(prefix_check, keg, ignores, formula_and_runtime_deps_names, args:) relocatable = false if keg_contain?(prefix_check, keg, ignores, formula_and_runtime_deps_names)
relocatable = false if keg_contain?(cellar, keg, ignores, formula_and_runtime_deps_names, args:) relocatable = false if keg_contain?(cellar, keg, ignores, formula_and_runtime_deps_names)
if keg_contain?(HOMEBREW_LIBRARY.to_s, keg, ignores, formula_and_runtime_deps_names, args:) relocatable = false if keg_contain?(HOMEBREW_LIBRARY.to_s, keg, ignores, formula_and_runtime_deps_names)
relocatable = false
end
if prefix != prefix_check if prefix != prefix_check
relocatable = false if keg_contain_absolute_symlink_starting_with?(prefix, keg, args:) relocatable = false if keg_contain_absolute_symlink_starting_with?(prefix, keg)
relocatable = false if keg_contain?("#{prefix}/etc", keg, ignores, args:) relocatable = false if keg_contain?("#{prefix}/etc", keg, ignores)
relocatable = false if keg_contain?("#{prefix}/var", keg, ignores, args:) relocatable = false if keg_contain?("#{prefix}/var", keg, ignores)
relocatable = false if keg_contain?("#{prefix}/share/vim", keg, ignores, args:) relocatable = false if keg_contain?("#{prefix}/share/vim", keg, ignores)
end end
skip_relocation = relocatable && !keg.require_relocation? skip_relocation = relocatable && !keg.require_relocation?
end end
@ -599,13 +600,13 @@ module Homebrew
json_path.write(JSON.pretty_generate(json)) json_path.write(JSON.pretty_generate(json))
end end
def self.parse_json_files(filenames) def parse_json_files(filenames)
filenames.map do |filename| filenames.map do |filename|
JSON.parse(File.read(filename)) JSON.parse(File.read(filename))
end end
end end
def self.merge_json_files(json_files) def merge_json_files(json_files)
json_files.reduce({}) do |hash, json_file| json_files.reduce({}) do |hash, json_file|
json_file.each_value do |json_hash| json_file.each_value do |json_hash|
json_bottle = json_hash["bottle"] json_bottle = json_hash["bottle"]
@ -618,7 +619,7 @@ module Homebrew
end end
end end
def self.merge(args:) def merge
bottles_hash = merge_json_files(parse_json_files(args.named)) bottles_hash = merge_json_files(parse_json_files(args.named))
any_cellars = ["any", "any_skip_relocation"] any_cellars = ["any", "any_skip_relocation"]
@ -689,7 +690,7 @@ module Homebrew
all_bottle_hash = T.let(nil, T.nilable(Hash)) all_bottle_hash = T.let(nil, T.nilable(Hash))
bottle_hash["bottle"]["tags"].each do |tag, tag_hash| bottle_hash["bottle"]["tags"].each do |tag, tag_hash|
filename = Bottle::Filename.new( filename = ::Bottle::Filename.new(
formula_name, formula_name,
PkgVersion.parse(bottle_hash["formula"]["pkg_version"]), PkgVersion.parse(bottle_hash["formula"]["pkg_version"]),
Utils::Bottles::Tag.from_symbol(tag.to_sym), Utils::Bottles::Tag.from_symbol(tag.to_sym),
@ -699,7 +700,7 @@ module Homebrew
if all_bottle && all_bottle_hash.nil? if all_bottle && all_bottle_hash.nil?
all_bottle_tag_hash = tag_hash.dup all_bottle_tag_hash = tag_hash.dup
all_filename = Bottle::Filename.new( all_filename = ::Bottle::Filename.new(
formula_name, formula_name,
PkgVersion.parse(bottle_hash["formula"]["pkg_version"]), PkgVersion.parse(bottle_hash["formula"]["pkg_version"]),
Utils::Bottles::Tag.from_symbol(:all), Utils::Bottles::Tag.from_symbol(:all),
@ -735,7 +736,7 @@ module Homebrew
require "utils/ast" require "utils/ast"
formula_ast = Utils::AST::FormulaAST.new(path.read) formula_ast = Utils::AST::FormulaAST.new(path.read)
checksums = old_checksums(formula, formula_ast, bottle_hash, args:) checksums = old_checksums(formula, formula_ast, bottle_hash)
update_or_add = checksums.nil? ? "add" : "update" update_or_add = checksums.nil? ? "add" : "update"
checksums&.each(&bottle.method(:sha256)) checksums&.each(&bottle.method(:sha256))
@ -772,7 +773,7 @@ module Homebrew
end end
end end
def self.merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash) def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash)
mismatches = [] mismatches = []
checksums = [] checksums = []
@ -812,12 +813,13 @@ module Homebrew
[mismatches, checksums] [mismatches, checksums]
end end
def self.old_checksums(formula, formula_ast, bottle_hash, args:) def old_checksums(formula, formula_ast, bottle_hash)
bottle_node = formula_ast.bottle_block bottle_node = formula_ast.bottle_block
return if bottle_node.nil? return if bottle_node.nil?
return [] unless args.keep_old? return [] unless args.keep_old?
old_keys = T.cast(Utils::AST.body_children(bottle_node.body), T::Array[RuboCop::AST::SendNode]).map(&:method_name) old_keys = T.cast(Utils::AST.body_children(bottle_node.body),
T::Array[RuboCop::AST::SendNode]).map(&:method_name)
old_bottle_spec = formula.bottle_specification old_bottle_spec = formula.bottle_specification
mismatches, checksums = merge_bottle_spec(old_keys, old_bottle_spec, bottle_hash["bottle"]) mismatches, checksums = merge_bottle_spec(old_keys, old_bottle_spec, bottle_hash["bottle"])
if mismatches.present? if mismatches.present?
@ -828,6 +830,8 @@ module Homebrew
end end
checksums checksums
end end
end
end
end end
require "extend/os/dev-cmd/bottle" require "extend/os/dev-cmd/bottle"

View File

@ -2,7 +2,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Homebrew module Homebrew
class << self module DevCmd
class Bottle < AbstractCommand
undef tar_args undef tar_args
sig { returns(T::Array[String]) } sig { returns(T::Array[String]) }
@ -21,4 +22,5 @@ module Homebrew
"#{gnu_tar_formula.opt_bin}/gtar" "#{gnu_tar_formula.opt_bin}/gtar"
end end
end end
end
end end

View File

@ -3,7 +3,7 @@
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/bottle" require "dev-cmd/bottle"
RSpec.describe "brew bottle" do RSpec.describe Homebrew::DevCmd::Bottle do
def stub_hash(parameters) def stub_hash(parameters)
<<~EOS <<~EOS
{ {
@ -30,7 +30,7 @@ RSpec.describe "brew bottle" do
EOS EOS
end end
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments", argv: ["foo"]
it "builds a bottle for the given Formula", :integration_test do it "builds a bottle for the given Formula", :integration_test do
install_test_formula "testball", build_bottle: true install_test_formula "testball", build_bottle: true
@ -308,8 +308,8 @@ RSpec.describe "brew bottle" do
end end
end end
describe Homebrew do describe "bottle_cmd" do
subject(:homebrew) { described_class } subject(:homebrew) { described_class.new(["foo"]) }
let(:hello_hash_big_sur) do let(:hello_hash_big_sur) do
JSON.parse stub_hash( JSON.parse stub_hash(