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

View File

@ -2,7 +2,8 @@
# frozen_string_literal: true
module Homebrew
class << self
module DevCmd
class Bottle < AbstractCommand
undef tar_args
sig { returns(T::Array[String]) }
@ -22,3 +23,4 @@ module Homebrew
end
end
end
end

View File

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