Merge branch 'main' into vulns-fix-flags

This commit is contained in:
Andrew Nesbitt authored and GitHub committed 2026-08-21 08:51:45 +01:00
commit e113b38fc3
61 files changed
+608 -96

No files matched your search

+2 -1
View File
@@ -191,7 +191,8 @@ module Homebrew
download_and_cache_data! unless data_loaded?
Homebrew::API.write_names_file!("formula", regenerate:) { formula_names }
Homebrew::API.write_aliases_file!("formula", regenerate:) { formula_aliases }
# Renames resolve like aliases in lightweight formula lookups.
Homebrew::API.write_aliases_file!("formula", regenerate:) { formula_aliases.merge(formula_renames) }
Homebrew::API.write_executables_file!(regenerate:, source: cached_packages_json_file_path) { formula_hashes }
end
+31 -18
View File
@@ -13,6 +13,18 @@ class CacheStoreDatabase
Key = type_member
Value = type_member
# Tracks the active users and shared database for one cache type.
class TypeReference < T::Struct
const :mutex, Thread::Mutex
prop :active_users, Integer, default: 0
prop :database, T.nilable(CacheStoreDatabase[T.anything, T.anything]), default: nil
end
private_constant :TypeReference
# Only reference creation is global; each cache type has its own lifecycle lock.
@type_references_mutex = T.let(Thread::Mutex.new, Thread::Mutex)
@type_references = T.let({}, T::Hash[Symbol, TypeReference])
# Yields the cache store database.
# Closes the database after use if it has been loaded.
sig {
@@ -24,28 +36,29 @@ class CacheStoreDatabase
.returns(T.type_parameter(:U))
}
def self.use(type, &_blk)
@db_type_reference_hash ||= T.let({}, T.nilable(T::Hash[T.untyped, T.untyped]))
@db_type_reference_hash[type] ||= {}
type_ref = @db_type_reference_hash[type]
type_ref[:count] ||= 0
type_ref[:count] += 1
type_ref[:db] ||= CacheStoreDatabase.new(type)
return_value = yield(type_ref[:db])
if type_ref[:count].positive?
type_ref[:count] -= 1
else
type_ref[:count] = 0
type_ref = @type_references_mutex.synchronize do
@type_references[type] ||= TypeReference.new(mutex: Thread::Mutex.new)
end
if type_ref[:count].zero?
type_ref[:db].write_if_dirty!
type_ref.delete(:db)
db = type_ref.mutex.synchronize do
type_ref.active_users += 1
type_ref.database ||= CacheStoreDatabase.new(type)
end
return_value
begin
return_value = yield(db)
return_value
ensure
# `break` from the block used to skip the decrement and leak the refcount.
type_ref.mutex.synchronize do
type_ref.active_users -= 1
if type_ref.active_users.zero?
# Prevent a replacement database from opening until the final write completes.
type_ref.database&.write_if_dirty!
type_ref.database = nil
end
end
end
end
# Creates a CacheStoreDatabase.
+2
View File
@@ -308,6 +308,8 @@ on_request: true)
sig { params(to: Pathname).void }
def extract_primary_container(to: @cask.staged_path)
download(quiet: true) if @cask.download.nil?
downloader.extract_primary_container(to:, verbose: verbose?)
end
+23
View File
@@ -1134,12 +1134,35 @@ module Homebrew
else
:stable
end
# homebrew/core formulae cannot define options, so defer their inflation until command execution.
install_from_api = !Homebrew::EnvConfig.no_install_from_api?
api_formula_cache_contents = if install_from_api && named_args.any?
%w[formula_names.txt formula_aliases.txt].filter_map do |file|
path = Homebrew::API::HOMEBREW_CACHE_API/file
path.read if path.file?
end
else
[]
end
# Only lowercase names, not paths, bottle filenames or URLs
named_args.filter_map do |arg|
next if arg.match?(HOMEBREW_CASK_TAP_CASK_REGEX)
begin
api_formula_name = arg[HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, :name]
if install_from_api && api_formula_name
api_formula_pattern = /^#{Regexp.escape(api_formula_name)}(?:\||$)/
cached_api_formula = arg != api_formula_name || api_formula_cache_contents.any? do |contents|
contents.match?(api_formula_pattern)
end
cached_api_formula ||= Homebrew::API::Internal.formula_hashes_cached? &&
(Homebrew::API.formula_name?(api_formula_name) ||
Homebrew::API.formula_aliases.key?(api_formula_name) ||
Homebrew::API.formula_renames.key?(api_formula_name))
next if cached_api_formula
end
Formulary.factory(arg, spec, flags: argv.select { |a| a.start_with?("--") })
rescue FormulaUnavailableError, FormulaSpecificationError
nil
+3 -1
View File
@@ -315,7 +315,9 @@ module Homebrew
sig { void }
def list_casks
casks = if args.no_named?
cask_paths = Cask::Caskroom.path.children.reject(&:file?).map do |path|
require "cask/cask_loader"
cask_paths = Cask::Caskroom.path.children.select(&:directory?).map do |path|
if path.symlink?
real_path = path.realpath
real_path.basename.to_s
+15
View File
@@ -45,6 +45,21 @@ module Homebrew
ohai "bundle install --standalone"
run_bundle "install", "--standalone"
require "bundler"
definition = Bundler::Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, false)
# Bundler ships with Ruby, outside the directory hashed by Bootsnap.
core_gem_names = definition.specs_for([:default])
.filter_map { |spec| spec.name if spec.name != "bundler" }
.sort
bootsnap_gem_names = Homebrew::Bootsnap.core_gem_names.sort
if core_gem_names != bootsnap_gem_names
raise <<~EOS
Bootsnap core gem list is out of date.
Expected: #{core_gem_names.join(", ")}
Actual: #{bootsnap_gem_names.join(", ")}
EOS
end
if GitHub::Actions.env_set? && HOMEBREW_PREFIX.to_s == HOMEBREW_LINUX_DEFAULT_PREFIX
ohai "chmod +t -R /home/linuxbrew/"
system "sudo", "chmod", "+t", "-R", "/home/linuxbrew/"
@@ -27,12 +27,14 @@ class CurlGitHubPackagesDownloadStrategy < CurlDownloadStrategy
sig { override.returns(Pathname) }
def cached_location
return super unless immutable_bottle_blob?
cached_location = @cached_location
return cached_location unless cached_location.nil?
return super if @resolved_basename.blank?
@cached_location = HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--#{Utils.safe_filename(@resolved_basename.to_s)}"
cached_location = HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--#{Utils.safe_filename(@resolved_basename.to_s)}"
return super if !immutable_bottle_blob? && (!mirrors.empty? || !cached_location.exist?)
@cached_location = cached_location
end
sig { returns(T::Boolean) }
@@ -10,8 +10,8 @@ module OS
requires_ancestor { ::Keg }
sig { params(relocation: ::Keg::Relocation, skip_protodesc_cold: T::Boolean).void }
def relocate_dynamic_linkage(relocation, skip_protodesc_cold: false)
sig { params(relocation: ::Keg::Relocation, with_placeholders: T::Boolean).void }
def relocate_dynamic_linkage(relocation, with_placeholders: false)
# Patching the dynamic linker of glibc breaks it.
return if name.match? Version.formula_optionally_versioned_regex(:glibc)
@@ -19,22 +19,23 @@ module OS
elf_files.each do |file|
file.ensure_writable do
change_rpath!(file, old_prefix, new_prefix, skip_protodesc_cold:)
change_rpath!(file, old_prefix, new_prefix, with_placeholders:)
end
end
end
sig {
params(file: ELFShim, old_prefix: T.any(String, Regexp), new_prefix: String,
skip_protodesc_cold: T::Boolean).returns(T::Boolean)
with_placeholders: T::Boolean).returns(T::Boolean)
}
def change_rpath!(file, old_prefix, new_prefix, skip_protodesc_cold: false)
def change_rpath!(file, old_prefix, new_prefix, with_placeholders: false)
return false if !file.elf? || !file.dynamic_elf?
# Skip relocation of files with `protodesc_cold` sections because patchelf.rb seems to break them,
# but only when bottling (as we don't want to break existing bottles that require relocation).
# https://github.com/Homebrew/homebrew-core/pull/232490#issuecomment-3161362452
return false if skip_protodesc_cold && file.section_names.include?("protodesc_cold")
# Also skip relocation of files with `.bun` sections
return false if with_placeholders && file.section_names.intersect?(["protodesc_cold", ".bun"])
updated = {}
old_rpath = file.rpath
@@ -23,8 +23,8 @@ module OS
end
end
sig { params(relocation: ::Keg::Relocation, skip_protodesc_cold: T::Boolean).void }
def relocate_dynamic_linkage(relocation, skip_protodesc_cold: false)
sig { params(relocation: ::Keg::Relocation, with_placeholders: T::Boolean).void }
def relocate_dynamic_linkage(relocation, with_placeholders: false)
mach_o_files.each do |file|
file.ensure_writable do
modified = T.let(false, T::Boolean)
+3 -3
View File
@@ -85,8 +85,8 @@ class Keg
end
end
sig { params(_relocation: Relocation, skip_protodesc_cold: T::Boolean).void }
def relocate_dynamic_linkage(_relocation, skip_protodesc_cold: false); end
sig { params(_relocation: Relocation, with_placeholders: T::Boolean).void }
def relocate_dynamic_linkage(_relocation, with_placeholders: false); end
JAVA_REGEX = %r{#{HOMEBREW_PREFIX}/opt/openjdk(@\d+(\.\d+)*)?/libexec(/openjdk\.jdk/Contents/Home)?}
@@ -172,7 +172,7 @@ class Keg
sig { returns(T::Array[Pathname]) }
def replace_locations_with_placeholders
relocation = prepare_relocation_to_placeholders.freeze
relocate_dynamic_linkage(relocation, skip_protodesc_cold: true)
relocate_dynamic_linkage(relocation, with_placeholders: true)
replace_text_in_files(relocation)
end
+6
View File
@@ -63,6 +63,12 @@ class Cask::DSL
sig { params(args: T.anything, kwargs: T.anything).void }
def mdimporter(*args, **kwargs); end
sig { params(block: T.proc.bind(Cask::DSL).returns(T.anything)).returns(T.anything) }
def on_arm(&block); end
sig { params(block: T.proc.bind(Cask::DSL).returns(T.anything)).returns(T.anything) }
def on_intel(&block); end
sig { params(args: T.anything, kwargs: T.anything).void }
def pkg(*args, **kwargs); end
@@ -48,6 +48,16 @@ module Tapioca
return_type: "void",
)
end
OnSystem::ARCH_OPTIONS.each do |arch|
klass.create_method(
"on_#{arch}",
parameters: [
create_block_param("block", type: "T.proc.bind(Cask::DSL).returns(T.anything)"),
],
return_type: "T.anything",
)
end
end
end
+42 -2
View File
@@ -3,6 +3,26 @@
module Homebrew
module Bootsnap
# This is the default Bundler group and its transitive dependencies. Optional
# groups must not rotate the compile cache used by the core load graph.
CORE_GEM_NAMES = %w[
bindata
concurrent-ruby
elftools
logger
patchelf
plist
ruby-macho
sorbet-runtime
].freeze
private_constant :CORE_GEM_NAMES
def self.core_gem_names = CORE_GEM_NAMES
private_class_method def self.gem_directories
Dir.children(File.join(Gem.paths.path, "gems")).sort
end
def self.key
@key ||= begin
require "digest/sha2"
@@ -10,7 +30,9 @@ module Homebrew
checksum = Digest::SHA256.new
checksum << RUBY_VERSION
checksum << RUBY_PLATFORM
checksum << Dir.children(File.join(Gem.paths.path, "gems")).join(",")
checksum << gem_directories
.select { |gem| core_gem_names.any? { |name| gem.start_with?("#{name}-") } }
.join(",")
checksum.hexdigest
end
@@ -23,6 +45,10 @@ module Homebrew
File.join(cache, "bootsnap", key)
end
private_class_method def self.load_path_cache
File.join(cache_dir, "bootsnap/load-path-cache")
end
private_class_method def self.ignore_directories
# We never do `require "vendor/bundle/ruby/..."` or `require "vendor/portable-ruby/..."`,
# so let's slim the cache a bit by excluding them.
@@ -46,6 +72,17 @@ module Homebrew
return
end
installed_gem_directories = gem_directories.join(",")
gem_directories_cache = File.join(cache_dir, "bootsnap/gem-directories")
if !File.exist?(gem_directories_cache) || File.read(gem_directories_cache) != installed_gem_directories
# The compile cache is shared across optional groups, but Bootsnap treats
# gem load paths as immutable, so invalidate their separate index.
require "fileutils"
FileUtils.rm_f load_path_cache
FileUtils.mkdir_p File.dirname(gem_directories_cache)
File.write(gem_directories_cache, installed_gem_directories)
end
::Bootsnap.setup(
cache_dir:,
ignore_directories:,
@@ -64,9 +101,12 @@ module Homebrew
return unless enabled?
::Bootsnap.unload_cache!
# Gem changes invalidate resolution, but compiled files validate themselves
# against their source contents, so only discard the persisted load path.
require "fileutils"
FileUtils.rm_f load_path_cache
@key = nil
# The compile cache doesn't get unloaded so we don't need to load it again!
load!(compile_cache: false)
end
+9
View File
@@ -2,12 +2,21 @@
module Homebrew
module Bootsnap
sig { returns(T::Array[String]) }
def self.core_gem_names; end
sig { returns(String) }
def self.key; end
sig { returns(T::Array[String]) }
private_class_method def self.gem_directories; end
sig { returns(String) }
private_class_method def self.cache_dir; end
sig { returns(String) }
private_class_method def self.load_path_cache; end
sig { returns(T::Array[String]) }
private_class_method def self.ignore_directories; end
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "abstract_command"
@@ -23,7 +23,7 @@ RSpec.describe Homebrew::AbstractCommand do
end
it "allows access to args" do
expect(TestCat.new(["--bar", "baz"]).args.bar).to eq("baz")
expect(TestCat.new(["--bar", "baz"]).args).to have_attributes(bar: "baz")
end
it "raises on invalid args" do
@@ -68,7 +68,9 @@ RSpec.describe Homebrew::AbstractCommand do
filename = File.basename(file, ".rb")
require(file)
command = described_class.command(filename)
expect(Pathname(File.join(__dir__, "../#{dir}/#{command.command_name}.rb"))).to exist
expect(command).not_to be_nil, "No command found for #{filename}"
expect(Pathname(File.join(__dir__, "../#{dir}/#{command&.command_name}.rb"))).to exist
end
end
end
@@ -219,6 +219,15 @@ RSpec.describe Homebrew::API::Internal do
expect((cache_dir/"internal/executables.txt").read).to eq("foo:foo-bin food\n")
end
it "writes formula aliases and renames as cached mappings" do
Homebrew::API.write_names_and_aliases
mappings = formulae_aliases.merge(formulae_renames).map { |old_name, name| "#{old_name}|#{name}" }.sort
expect((cache_dir/"formula_aliases.txt").read.lines(chomp: true)).to eq(
mappings,
)
end
it "returns the expected cask hashes" do
cask_hashes_output = described_class.cask_hashes
expect(cask_hashes_output).to eq cask_hashes
+2 -1
View File
@@ -1,10 +1,11 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "open3"
RSpec.describe "Bash" do
matcher :have_valid_bash_syntax do
T.bind(self, T.class_of(RSpec::Matchers::DSL::Matcher))
match do |file|
stdout, stderr, status = Open3.capture3("/bin/bash", "-n", file)
+2 -2
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "bundle_version"
@@ -41,7 +41,7 @@ RSpec.describe Homebrew::BundleVersion do
["2.5.2(3329)", "3329"] => "2.5.2,3329",
}
expected_mappings.each do |(short_version, version), expected_version|
test_each_hash(expected_mappings) do |(short_version, version), expected_version|
it "maps (#{short_version.inspect}, #{version.inspect}) to #{expected_version.inspect}" do
expect(described_class.new(short_version, version).nice_version)
.to eq expected_version
+65
View File
@@ -17,6 +17,71 @@ RSpec.describe CacheStoreDatabase do
# do nothing
end
end
it "releases the refcount when the block breaks" do
cache_store = instance_double(described_class, "cache_store", write_if_dirty!: nil)
allow(described_class).to receive(:new).with(type).and_return(cache_store)
expect(cache_store).to receive(:write_if_dirty!).twice
described_class.use(type) do |_db|
break
end
described_class.use(type) do |_db|
# do nothing
end
end
it "returns the block value" do
expect(described_class.use(type) { :return_value }).to eq(:return_value)
end
it "finishes writing before opening a replacement database" do
events = Queue.new
write_started = Queue.new
finish_write = Queue.new
first_cache_store = instance_double(described_class, "first_cache_store")
second_cache_store = instance_double(described_class, "second_cache_store", write_if_dirty!: nil)
allow(first_cache_store).to receive(:write_if_dirty!) do
events << :write_started
write_started << true
finish_write.pop
events << :write_finished
end
allow(described_class).to receive(:new).with(type).and_return(first_cache_store, second_cache_store)
first_use = Thread.new { described_class.use(type) { nil } }
write_started.pop
second_use = Thread.new do
events << :second_use_started
described_class.use(type) { events << :second_database_opened }
end
Thread.pass while second_use.status == "run"
finish_write << true
[first_use, second_use].each(&:value)
expect(Array.new(4) { events.pop }).to eq([
:write_started,
:second_use_started,
:write_finished,
:second_database_opened,
])
end
it "does not raise when used concurrently, including `break`" do
threads = Array.new(8) do |i|
Thread.new do
25.times do |n|
described_class.use(:concurrent_test) do |db|
break if n.odd?
db.delete("missing-#{i}-#{n}")
end
end
end
end
expect { threads.each(&:value) }.not_to raise_error
end
end
describe "#set" do
+3 -5
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
RSpec.describe Cask::Download, :cask do
@@ -9,11 +9,10 @@ RSpec.describe Cask::Download, :cask do
let(:full_token) { token }
let(:url) { instance_double(URL, to_s: url_to_s, specs: {}) }
let(:url_to_s) { "https://example.com/app.dmg" }
let(:version) { nil }
let(:cask) { instance_double(Cask::Cask, token:, full_token:, version:, url:) }
context "when cask has no version" do
let(:version) { nil }
it "returns the cask token" do
expect(download_name).to eq "example-cask"
end
@@ -133,6 +132,7 @@ RSpec.describe Cask::Download, :cask do
subject(:verification) { described_class.new(cask).verify_download_integrity(downloaded_path) }
let(:tap) { nil }
let(:expected_sha256) { nil }
let(:cask) { instance_double(Cask::Cask, token: "cask", sha256: expected_sha256, tap:) }
let(:cafebabe) { "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe" }
let(:deadbeef) { "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }
@@ -168,8 +168,6 @@ RSpec.describe Cask::Download, :cask do
end
context "when the expected checksum is nil" do
let(:expected_sha256) { nil }
it "outputs an error" do
expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr
end
@@ -1,6 +1,9 @@
# typed: false
# frozen_string_literal: true
require "cask/installer"
Warnings.ignore(/circular require considered harmful/) { require "install" }
RSpec.describe Cask::Installer, :cask do
def stub_dmg_extraction
allow(UnpackStrategy::Dmg).to receive(:can_extract?).and_return(true)
@@ -10,6 +13,25 @@ RSpec.describe Cask::Installer, :cask do
end
end
describe "#extract_primary_container" do
it "respects the installer's download integrity setting" do
cask = Cask::CaskLoader.load(cask_path("local-caffeine"))
installer = described_class.new(cask, verify_download_integrity: false)
download = instance_double(Cask::Download)
downloaded_path = Pathname("/path/to/downloaded/cask")
allow(installer).to receive(:downloader).and_return(download)
expect(download).to receive(:fetch)
.with(quiet: true, verify_download_integrity: false, timeout: nil)
.and_return(downloaded_path)
expect(download).to receive(:extract_primary_container).with(to: cask.staged_path, verbose: false)
installer.extract_primary_container
expect(cask.download).to eq(downloaded_path)
end
end
describe "#save_caskfile" do
it "stores casks loaded from Ruby source as JSON metadata" do
cask = Cask::CaskLoader.load(cask_path("local-caffeine"))
+15
View File
@@ -491,6 +491,21 @@ RSpec.describe Homebrew::Cleanup do
end
end
describe "::cleanup_bootsnap" do
it "removes stale keys and keeps the current key" do
cache = mktmpdir
current = cache/"bootsnap/current"
stale = cache/"bootsnap/stale"
current.mkpath
stale.mkpath
allow(Homebrew::Bootsnap).to receive(:key).and_return("current")
described_class.new(cache:).cleanup_bootsnap
expect([current.exist?, stale.exist?]).to eq([true, false])
end
end
describe "::cleanup_cache" do
it "removes legacy cask downloads during full cache cleanup", :cask do
cask = Cask::CaskLoader.load("local-transmission")
+88
View File
@@ -399,6 +399,94 @@ RSpec.describe Homebrew::CLI::Parser do
end
end
describe "formula options" do
subject(:parser) do
described_class.new(Cmd) do
formula_options
end
end
let(:api_cache) { mktmpdir }
before do
stub_const("Homebrew::API::HOMEBREW_CACHE_API", api_cache)
allow(Homebrew::EnvConfig).to receive(:no_install_from_api?).and_return(false)
end
it "does not inflate API-backed formulae" do
(api_cache/"formula_names.txt").write("foo\n")
expect(Formulary).not_to receive(:factory)
parser.parse(["foo"])
end
it "reads API cache files once for multiple formulae" do
(api_cache/"formula_names.txt").write("foo\n")
(api_cache/"formula_aliases.txt").write("bar|foo\n")
reads = Hash.new(0)
allow_any_instance_of(Pathname).to receive(:read).and_wrap_original do |method|
reads[method.receiver.basename.to_s] += 1
method.call
end
expect(Formulary).not_to receive(:factory)
parser.parse(%w[foo bar])
expect(reads).to eq("formula_names.txt" => 1, "formula_aliases.txt" => 1)
end
it "inflates API-backed formulae when commands access them" do
(api_cache/"formula_names.txt").write("foo\n")
full_formula = formula "foo", path: api_cache/"foo.rb", tap: Tap.fetch("user/repo") do
url "https://brew.sh/foo-1.0.tar.gz"
version "1.0"
end
expect(Formulary).to receive(:factory).once.and_return(full_formula)
expect(parser.parse(["foo"]).named.to_formulae).to eq([full_formula])
end
it "does not inflate API-backed formula aliases or renames" do
(api_cache/"formula_aliases.txt").write("foo-alias|foo\nfoo-old|foo\n")
expect(Formulary).not_to receive(:factory)
parser.parse(%w[foo-alias foo-old])
end
it "uses API metadata already loaded when the cache files are missing" do
allow(Homebrew::API::Internal).to receive(:formula_hashes_cached?).and_return(true)
allow(Homebrew::API).to receive_messages(formula_name?: false, formula_aliases: { "foo-alias" => "foo" },
formula_renames: { "foo-old" => "foo" })
allow(Homebrew::API).to receive(:formula_name?).with("foo").and_return(true)
expect(Formulary).not_to receive(:factory)
parser.parse(%w[foo foo-alias foo-old])
end
it "does not inflate a fully qualified core formula without API caches" do
allow(Homebrew::API::Internal).to receive(:formula_hashes_cached?).and_return(false)
expect(Formulary).not_to receive(:factory)
parser.parse(["homebrew/core/foo"])
end
it "inflates other formulae to parse their options" do
allow(Homebrew::API::Internal).to receive(:formula_hashes_cached?).and_return(false)
option = instance_double(Option, flag: "--with-foo", description: "Build with foo")
formula = instance_double(Formula, name: "foo", options: [option])
expect(Formulary).to receive(:factory).and_return(formula)
parser.parse(%w[foo --with-foo])
end
it "inflates formulae when API installs are disabled" do
allow(Homebrew::EnvConfig).to receive(:no_install_from_api?).and_return(true)
formula = instance_double(Formula, name: "foo", options: [])
expect(Formulary).to receive(:factory).and_return(formula)
parser.parse(["foo"])
end
end
describe "usage banner generation" do
it "includes `[options]` if more than two non-global options are available" do
parser = described_class.new(Cmd) do
+7 -1
View File
@@ -297,11 +297,17 @@ RSpec.describe Homebrew::Cmd::List do
.to raise_error(UsageError, /`brew list --json` requires `--versions`\./)
end
it "prints pinned formulae and casks", :cask, :integration_test do
it "prints formula and cask versions", :cask, :integration_test do
setup_test_formula "testball", tab_attributes: { installed_on_request: true }
Formula["testball"].pin
cask = Cask::CaskLoader.load("local-caffeine")
InstallHelper.stub_cask_installation(cask)
FileUtils.ln_s "missing-cask", Cask::Caskroom.path/"dangling-alias"
expect { brew "list", "--versions" }
.to output("testball 0.1\nlocal-caffeine 1.2.3\n").to_stdout
.and be_a_success
cask.pin
expect { brew "list", "--pinned", "--versions" }
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "cmd/shared_examples/args_parse"
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "cmd/shared_examples/args_parse"
@@ -19,7 +19,10 @@ RSpec.describe Homebrew::DevCmd::GenerateVulnsAdvisories do
resolves "CVE-2015-2305"
end
end
plain = formula("plain") { url "https://example.com/plain-1.0.tar.gz" }
plain = formula("plain") do
T.bind(self, T.class_of(Formula))
url "https://example.com/plain-1.0.tar.gz"
end
[nvi, plain].each do |f|
allow(f).to receive(:to_hash_with_variations)
.and_return({ "patches" => f.serialized_patches, "variations" => {} })
@@ -104,7 +107,12 @@ RSpec.describe Homebrew::DevCmd::GenerateVulnsAdvisories do
describe "#first_fixed_version" do
subject(:cmd) { described_class.new(["out"]) }
let(:current) { formula("x") { url "https://example.com/x-1.2.tar.gz" } }
let(:current) do
formula("x") do
T.bind(self, T.class_of(Formula))
url "https://example.com/x-1.2.tar.gz"
end
end
def with_history(revisions)
fv = instance_double(FormulaVersions)
@@ -6,4 +6,18 @@ require "dev-cmd/install-bundler-gems"
RSpec.describe Homebrew::DevCmd::InstallBundlerGems do
it_behaves_like "parseable arguments"
it "resets Bootsnap after cleaning gems" do
bundle = "/tmp/bundle"
allow(Homebrew).to receive(:setup_gem_environment!)
allow(Homebrew).to receive_messages(valid_gem_groups: [], user_gem_groups: [], user_vendor_version: 9)
allow(Homebrew).to receive(:find_in_path).with("bundle").and_return("/tmp")
allow(Homebrew).to receive(:`).with("#{bundle} check 2>&1").and_return("")
allow(Homebrew).to receive(:system).with(bundle, "clean", out: :err).and_return(true)
allow(Homebrew).to receive(:write_user_gem_groups)
expect(Homebrew::Bootsnap).to receive(:reset!)
Kernel.system("true")
with_env(HOMEBREW_TESTS: nil) { Homebrew.install_bundler_gems! }
end
end
@@ -6,4 +6,29 @@ require "dev-cmd/vendor-gems"
RSpec.describe Homebrew::DevCmd::VendorGems do
it_behaves_like "parseable arguments"
sig { returns(Homebrew::DevCmd::VendorGems) }
def vendor_gems_command
described_class.new(["--no-commit"]).tap do |command|
allow(command).to receive(:run_bundle)
allow(command).to receive(:ohai)
allow(Homebrew).to receive(:setup_gem_environment!)
allow(Homebrew).to receive(:valid_gem_groups).and_return([])
end
end
it "rejects a stale Bootsnap core gem list" do
command = vendor_gems_command
allow(Homebrew::Bootsnap).to receive(:core_gem_names).and_return([])
expect { command.run }.to raise_error(RuntimeError, /Bootsnap core gem list is out of date/)
end
it "accepts reordered Bootsnap core gems" do
command = vendor_gems_command
core_gem_names = Homebrew::Bootsnap.core_gem_names.reverse
allow(Homebrew::Bootsnap).to receive(:core_gem_names).and_return(core_gem_names)
expect { command.run }.not_to raise_error
end
end
@@ -88,6 +88,52 @@ RSpec.describe CurlGitHubPackagesDownloadStrategy do
.to eq(HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--foo--1.2.3.arm64_ventura.bottle.tar.gz")
end
context "with an existing resolved cache location" do
let(:url) { "https://#{GitHubPackages::URL_DOMAIN}/v2/homebrew/core/foo/manifests/1.2.3" }
let(:specs) { {} }
it "uses it without discovering cache files" do
strategy.resolved_basename = "foo-1.2.3.bottle_manifest.json"
cached_location = HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--foo-1.2.3.bottle_manifest.json"
cached_location.dirname.mkpath
cached_location.write("cached")
expect(Pathname).not_to receive(:glob)
expect(strategy.cached_location).to eq(cached_location)
end
it "does not discover cache files for 100 cached manifests" do
strategies = (1..100).map do |index|
url = "https://#{GitHubPackages::URL_DOMAIN}/v2/homebrew/core/foo#{index}/manifests/1.2.3"
strategy = described_class.new(url, "foo#{index}", version, **specs)
strategy.resolved_basename = "foo#{index}-1.2.3.bottle_manifest.json"
cached_location = HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--foo#{index}-1.2.3.bottle_manifest.json"
cached_location.dirname.mkpath
cached_location.write("cached")
strategy
end
expect(Pathname).not_to receive(:glob)
strategies.each(&:cached_location)
end
end
context "with a cached download using another basename" do
let(:url) { "https://#{GitHubPackages::URL_DOMAIN}/v2/homebrew/core/foo/manifests/1.2.4" }
let(:specs) { {} }
it "uses generic cache discovery" do
strategy.resolved_basename = "foo-1.2.4.bottle_manifest.json"
cached_location = HOMEBREW_CACHE/"downloads/#{Digest::SHA256.hexdigest(url)}--server-filename.json"
cached_location.dirname.mkpath
cached_location.write("cached")
expect(strategy.cached_location).to eq(cached_location)
end
end
context "with a custom cache" do
let(:cache) { HOMEBREW_CACHE/"custom-cache" }
let(:specs) { { bottle: true, cache: } }
@@ -107,7 +153,9 @@ RSpec.describe CurlGitHubPackagesDownloadStrategy do
it "uses generic cache discovery" do
strategy.resolved_basename = "foo--1.2.3.arm64_ventura.bottle.tar.gz"
expect(strategy.immutable_bottle_blob?).to be false
expect(Pathname).to receive(:glob).and_call_original
strategy.cached_location
end
end
end
@@ -118,7 +118,7 @@ RSpec.describe FormulaInstaller do
formula_installer = described_class.new(formula)
begin
expect do
Homebrew::Install.fetch_formulae([formula_installer])
formula_installer.fetch
formula_installer.install
end.to output(/Removing corrupt cached download/).to_stderr
@@ -3,6 +3,7 @@
require "formula"
require "formula_installer"
Warnings.ignore(/circular require considered harmful/) { require "install" }
require "keg"
require "sandbox"
require "tab"
+3 -3
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "os/linux/ld"
@@ -61,9 +61,9 @@ RSpec.describe OS::Linux::Ld do
end
describe "::library_paths" do
ld_etc = Pathname("")
let(:ld_etc) { Pathname(Dir.mktmpdir("homebrew-tests-ld-etc-", Dir.tmpdir)) }
before do
ld_etc = Pathname(Dir.mktmpdir("homebrew-tests-ld-etc-", Dir.tmpdir))
FileUtils.mkdir [ld_etc/"subdir1", ld_etc/"subdir2"]
(ld_etc/"ld.so.conf").write <<~EOS
# This line is a comment
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "os/mac/ffi/native_library"
@@ -8,9 +8,12 @@ RSpec.describe MacOS::FFI::NativeLibrary, :needs_macos do
Module.new do
extend MacOS::FFI::NativeLibrary
T.bind(self, T.all(Module, MacOS::FFI::NativeLibrary))
use_library "/usr/lib/libSystem.B.dylib"
def self.process_id
define_singleton_method(:process_id) do
T.bind(self, T.all(Module, MacOS::FFI::NativeLibrary))
function("getpid", [], Fiddle::TYPE_INT).call
end
end
+4 -1
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "extend/ENV"
@@ -21,6 +21,8 @@ RSpec.describe Requirement do
describe "#tags" do
subject(:req) { klass.new(tags) }
let(:tags) { [] }
context "with a single tag" do
let(:tags) { ["bar"] }
@@ -50,6 +52,7 @@ RSpec.describe Requirement do
describe "#fatal true is specified" do
let(:klass) do
Class.new(described_class) do
T.bind(self, T.class_of(Requirement))
fatal true
end
end
+2 -3
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "rubocops/urls"
@@ -322,10 +322,9 @@ RSpec.describe RuboCop::Cop::FormulaAudit::Urls do
end
context "when auditing Apache URLs" do
let(:expected_url) { "https://www.apache.org/dyn/closer.lua?path=apr/apr-1.7.6.tar.bz2" }
shared_examples "offense" do |url|
it "registers an offense and corrects" do
expected_url = "https://www.apache.org/dyn/closer.lua?path=apr/apr-1.7.6.tar.bz2"
message = "FormulaAudit/Urls: #{url} should be: #{expected_url}"
expect_offense(<<~RUBY, url:, message:)
@@ -2,12 +2,88 @@
# frozen_string_literal: true
RSpec.describe Homebrew::Bootsnap do
sig { params(gem_home: Pathname).returns(String) }
def bootsnap_key(gem_home)
stdout, stderr, status = Open3.capture3(
{ "HOMEBREW_NO_BOOTSNAP" => "1" },
*HOMEBREW_RUBY_EXEC_ARGS,
"-rrubygems", "-e", <<~RUBY, gem_home, HOMEBREW_LIBRARY_PATH/"startup/bootsnap.rb"
Gem.paths = { "GEM_HOME" => ARGV.fetch(0), "GEM_PATH" => ARGV.fetch(0) }
require ARGV.fetch(1)
puts Homebrew::Bootsnap.key
RUBY
)
raise stderr unless status.success?
stdout.chomp
end
describe "::key" do
it "ignores profiler gems and changes when a core gem changes" do
gem_home = mktmpdir
gems = gem_home/"gems"
(gems/"sorbet-runtime-1.0").mkpath
original_key = bootsnap_key(gem_home)
(gems/"stackprof-1.0").mkpath
profiler_gem_key = bootsnap_key(gem_home)
(gems/"sorbet-runtime-1.0").rmdir
(gems/"sorbet-runtime-2.0").mkpath
core_gem_key = bootsnap_key(gem_home)
expect([profiler_gem_key == original_key, core_gem_key == original_key]).to eq([true, false])
end
end
describe "::load!" do
it "does not error when the configured gem path is unavailable" do
with_env(HOMEBREW_BOOTSNAP_GEM_PATH: "#{TEST_TMPDIR}/missing-bootsnap", HOMEBREW_NO_BOOTSNAP: nil) do
expect { described_class.load! }.not_to raise_error
end
end
it "invalidates stale gem load paths without removing compiled files" do
cache = mktmpdir
load_path_cache = cache/"bootsnap/load-path-cache"
gem_directories = cache/"bootsnap/gem-directories"
compile_cache = cache/"bootsnap/compile-cache-iseq/cache"
load_path_cache.dirname.mkpath
load_path_cache.write("stale")
gem_directories.write("old-gem-1.0")
compile_cache.dirname.mkpath
compile_cache.write("")
allow(described_class).to receive_messages(enabled?: true, cache_dir: cache.to_s)
allow(Dir).to receive(:children).with(File.join(Gem.paths.path, "gems")).and_return(["new-gem-1.0"])
allow(Bootsnap).to receive(:setup)
described_class.load!
stale_cache_removed = !load_path_cache.exist?
load_path_cache.write("current")
described_class.load!
expect([stale_cache_removed, load_path_cache.read, gem_directories.read, compile_cache.exist?])
.to eq([true, "current", "new-gem-1.0", true])
end
end
describe "::reset!" do
it "removes the load path cache and keeps the compile cache" do
cache = mktmpdir
load_path_cache = cache/"bootsnap/load-path-cache"
compile_cache = cache/"bootsnap/compile-cache-iseq/cache"
load_path_cache.dirname.mkpath
load_path_cache.write("")
compile_cache.dirname.mkpath
compile_cache.write("")
allow(described_class).to receive_messages(enabled?: true, cache_dir: cache.to_s)
allow(Bootsnap).to receive(:unload_cache!)
allow(described_class).to receive(:load!)
described_class.reset!
expect([load_path_cache.exist?, compile_cache.exist?]).to eq([false, true])
end
end
describe "::prewarm!" do
@@ -4,13 +4,14 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Bazaar do
subject(:path) { repo }
let(:repo) do
mktmpdir.tap do |repo|
FileUtils.touch repo/"test"
(repo/".bzr").mkpath
end
end
let(:path) { repo }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["test"]
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Bzip2 do
let(:path) { TEST_FIXTURE_DIR/"cask/container.bz2" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.bz2" }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["container"]
@@ -4,13 +4,14 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Cvs do
subject(:path) { repo }
let(:repo) do
mktmpdir.tap do |repo|
FileUtils.touch repo/"test"
(repo/"CVS").mkpath
end
end
let(:path) { repo }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["CVS", "test"]
@@ -5,7 +5,7 @@ require_relative "shared_examples"
RSpec.describe UnpackStrategy::Dmg, :needs_macos do
describe "#mount" do
let(:path) { TEST_FIXTURE_DIR/"cask/container.dmg" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.dmg" }
include_examples "UnpackStrategy::detect"
@@ -4,6 +4,8 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Git do
subject(:path) { repo }
let(:repo) do
mktmpdir.tap do |repo|
system "git", "-C", repo, "init"
@@ -13,7 +15,6 @@ RSpec.describe UnpackStrategy::Git do
system "git", "-C", repo, "commit", "-m", "Add `test` file."
end
end
let(:path) { repo }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: [".git", "test"]
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Gzip do
let(:path) { TEST_FIXTURE_DIR/"cask/container.gz" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.gz" }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["container"], verbose: true
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Jar, :needs_unzip do
let(:path) { TEST_FIXTURE_DIR/"test.jar" }
subject(:path) { TEST_FIXTURE_DIR/"test.jar" }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["test.jar"]
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Lha do
let(:path) { TEST_FIXTURE_DIR/"test.lha" }
subject(:path) { TEST_FIXTURE_DIR/"test.lha" }
include_examples "UnpackStrategy::detect"
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Lzip do
let(:path) { TEST_FIXTURE_DIR/"test.lz" }
subject(:path) { TEST_FIXTURE_DIR/"test.lz" }
include_examples "UnpackStrategy::detect"
end
@@ -4,12 +4,13 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Mercurial do
subject(:path) { repo }
let(:repo) do
mktmpdir.tap do |repo|
(repo/".hg").mkpath
end
end
let(:path) { repo }
include_examples "UnpackStrategy::detect"
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::P7Zip do
let(:path) { TEST_FIXTURE_DIR/"cask/container.7z" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.7z" }
include_examples "UnpackStrategy::detect"
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Rar do
let(:path) { TEST_FIXTURE_DIR/"cask/container.rar" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.rar" }
include_examples "UnpackStrategy::detect"
end
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "mktemp"
@@ -6,7 +6,7 @@ require "unpack_strategy"
RSpec.shared_examples "UnpackStrategy::detect" do
it "is correctly detected" do
expect(UnpackStrategy.detect(path)).to be_a described_class
expect(UnpackStrategy.detect(subject)).to be_a described_class
end
end
@@ -14,7 +14,7 @@ RSpec.shared_examples "#extract" do |children: [], verbose: false|
specify "#extract" do
Mktemp.new("homebrew-test-unpack").run(chdir: false) do |mktemp|
unpack_dir = T.must(mktemp.tmpdir)
described_class.new(path).extract(to: unpack_dir, verbose:)
described_class.new(subject).extract(to: unpack_dir, verbose:)
expect(unpack_dir.children(false).map(&:to_s)).to match_array children
end
end
@@ -4,9 +4,10 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Subversion, :needs_svnadmin do
subject(:path) { working_copy }
let(:repo) { mktmpdir }
let(:working_copy) { mktmpdir }
let(:path) { working_copy }
before do
safe_system "svnadmin", "create", repo
@@ -4,13 +4,13 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Tar do
let(:path) { TEST_FIXTURE_DIR/"cask/container.tar.gz" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.tar.gz" }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["container"]
context "when TAR archive is corrupted" do
let(:path) do
subject(:path) do
(mktmpdir/"test.tar").tap do |path|
FileUtils.touch path
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Uncompressed do
let(:path) do
subject(:path) do
(mktmpdir/"test").tap do |path|
FileUtils.touch path
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Xar, :needs_macos do
let(:path) { TEST_FIXTURE_DIR/"cask/container.xar" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.xar" }
include_examples "UnpackStrategy::detect"
include_examples "#extract", children: ["container"]
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Xz do
let(:path) { TEST_FIXTURE_DIR/"cask/container.xz" }
subject(:path) { TEST_FIXTURE_DIR/"cask/container.xz" }
include_examples "UnpackStrategy::detect"
end
@@ -4,7 +4,7 @@
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Zip do
let(:path) { TEST_FIXTURE_DIR/"cask/MyFancyApp.zip" }
subject(:path) { TEST_FIXTURE_DIR/"cask/MyFancyApp.zip" }
include_examples "UnpackStrategy::detect"
@@ -13,7 +13,7 @@ RSpec.describe UnpackStrategy::Zip do
end
context "when ZIP archive is corrupted" do
let(:path) do
subject(:path) do
(mktmpdir/"test.zip").tap do |path|
FileUtils.touch path
end
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "utils/bottles"
@@ -53,9 +53,7 @@ RSpec.describe Utils::Bottles do
runtime_dependencies = described_class.load_tab(formula).runtime_dependencies
expect(runtime_dependencies).not_to be_nil
expect(runtime_dependencies.size).to eq(1)
expect(runtime_dependencies.first).to include("full_name" => "testball1")
expect(runtime_dependencies).to contain_exactly(a_hash_including("full_name" => "testball1"))
end
end
end
+3 -2
View File
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "version"
@@ -433,8 +433,9 @@ RSpec.describe Version do
describe "::detect" do
matcher :be_detected_from do |url, **specs|
T.bind(self, T.class_of(RSpec::Matchers::DSL::Matcher))
match do |expected|
@detected = described_class.detect(url, **specs)
@detected = Version.detect(url, **specs)
@detected == expected
end
+2 -1
View File
@@ -274,7 +274,6 @@ module Homebrew
exec bundle, "install", out: :err
end)
if $CHILD_STATUS.success?
Homebrew::Bootsnap.reset! if defined?(Homebrew::Bootsnap) # Gem install can run before Bootsnap loads
true
else
message = <<~EOS
@@ -302,6 +301,8 @@ module Homebrew
end
if bundle_installed
# `bundle clean` can remove load paths just as `bundle install` can add them.
Homebrew::Bootsnap.reset! if defined?(Homebrew::Bootsnap) # Gem setup can run before Bootsnap loads
write_user_gem_groups(groups)
@bundle_installed_groups = groups
end
+1 -1
View File
@@ -88,7 +88,7 @@ Homebrew's [Project Leader](https://docs.brew.sh/Homebrew-Governance#project-lea
Homebrew's [Lead Maintainers](https://docs.brew.sh/Homebrew-Governance#lead-maintainer) are [Bevan Kay](https://github.com/bevanjkay), [Carlo Cabrera](https://github.com/carlocab), [Issy Long](https://github.com/issyl0), [Justin Krehel](https://github.com/krehel), [Michael Cho](https://github.com/cho-m), [Mike McQuaid](https://github.com/MikeMcQuaid), [Nanda H Krishna](https://github.com/nandahkrishna), [Patrick Linnane](https://github.com/p-linnane), [Rui Chen](https://github.com/chenrui333), [Ruoyu Zhong](https://github.com/ZhongRuoyu), [Sam Ford](https://github.com/samford) and [Sean Molenaar](https://github.com/SMillerDev).
Homebrew's other Maintainers are [Andrew Nesbitt](https://github.com/andrew), [Anton Melnikov](https://github.com/botantony), [Bo Anderson](https://github.com/Bo98), [Branch Vincent](https://github.com/branchv), [Caleb Xu](https://github.com/alebcay), [Daeho Ro](https://github.com/daeho-ro), [Douglas Eichelberger](https://github.com/dduugg), [Dustin Rodrigues](https://github.com/dtrodrigues), [FX Coudert](https://github.com/fxcoudert), [Klaus Hipp](https://github.com/khipp), [Markus Reiter](https://github.com/reitermarkus), [Michka Popoff](https://github.com/iMichka), [Rylan Polster](https://github.com/Rylan12), [Stefan Baebler](https://github.com/stefanb), [Thierry Moisan](https://github.com/Moisan) and [William Woodruff](https://github.com/woodruffw).
Homebrew's other Maintainers are [Andrew Nesbitt](https://github.com/andrew), [Anton Melnikov](https://github.com/botantony), [Bo Anderson](https://github.com/Bo98), [Branch Vincent](https://github.com/branchv), [Caleb Xu](https://github.com/alebcay), [Daeho Ro](https://github.com/daeho-ro), [Douglas Eichelberger](https://github.com/dduugg), [Dustin Rodrigues](https://github.com/dtrodrigues), [FX Coudert](https://github.com/fxcoudert), [Klaus Hipp](https://github.com/khipp), [Markus Reiter](https://github.com/reitermarkus), [Michka Popoff](https://github.com/iMichka), [Rylan Polster](https://github.com/Rylan12), [Štefan Baebler](https://github.com/stefanb), [Thierry Moisan](https://github.com/Moisan) and [William Woodruff](https://github.com/woodruffw).
Homebrew was originally created by [Max Howell](https://github.com/mxcl).
+1 -1
View File
@@ -5157,7 +5157,7 @@ Ruoyu Zhong, Sam Ford and Sean Molenaar.
Homebrew's other Maintainers are Andrew Nesbitt, Anton Melnikov, Bo Anderson,
Branch Vincent, Caleb Xu, Daeho Ro, Douglas Eichelberger, Dustin Rodrigues, FX
Coudert, Klaus Hipp, Markus Reiter, Michka Popoff, Rylan Polster, Stefan
Coudert, Klaus Hipp, Markus Reiter, Michka Popoff, Rylan Polster, Štefan
Baebler, Thierry Moisan and William Woodruff.
## BUGS
+1 -1
View File
@@ -3358,7 +3358,7 @@ Homebrew\[u2019]s Project Leader is Mike McQuaid\.
.P
Homebrew\[u2019]s Lead Maintainers are Bevan Kay, Carlo Cabrera, Issy Long, Justin Krehel, Michael Cho, Mike McQuaid, Nanda H Krishna, Patrick Linnane, Rui Chen, Ruoyu Zhong, Sam Ford and Sean Molenaar\.
.P
Homebrew\[u2019]s other Maintainers are Andrew Nesbitt, Anton Melnikov, Bo Anderson, Branch Vincent, Caleb Xu, Daeho Ro, Douglas Eichelberger, Dustin Rodrigues, FX Coudert, Klaus Hipp, Markus Reiter, Michka Popoff, Rylan Polster, Stefan Baebler, Thierry Moisan and William Woodruff\.
Homebrew\[u2019]s other Maintainers are Andrew Nesbitt, Anton Melnikov, Bo Anderson, Branch Vincent, Caleb Xu, Daeho Ro, Douglas Eichelberger, Dustin Rodrigues, FX Coudert, Klaus Hipp, Markus Reiter, Michka Popoff, Rylan Polster, Štefan Baebler, Thierry Moisan and William Woodruff\.
.SH "BUGS"
See our issues on GitHub:
.TP