Merge pull request #16937 from Homebrew/ported-cmds

Convert next batch of dev commands to use AbstractCommand
This commit is contained in:
Mike McQuaid 2024-03-22 08:36:08 +00:00 committed by GitHub
commit 999ecf8b54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1269 additions and 1235 deletions

View File

@ -17,7 +17,7 @@ module Homebrew
class << self class << self
sig { returns(String) } sig { returns(String) }
def command_name = Utils.underscore(T.must(name).split("::").fetch(-1)).tr("_", "-") def command_name = Utils.underscore(T.must(name).split("::").fetch(-1)).tr("_", "-").delete_suffix("-cmd")
# @return the AbstractCommand subclass associated with the brew CLI command name. # @return the AbstractCommand subclass associated with the brew CLI command name.
sig { params(name: String).returns(T.nilable(T.class_of(AbstractCommand))) } sig { params(name: String).returns(T.nilable(T.class_of(AbstractCommand))) }

View File

@ -1,6 +1,7 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "csv" require "csv"

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "formula" require "formula"
@ -9,232 +9,231 @@ require "utils/pypi"
require "cask/cask_loader" require "cask/cask_loader"
module Homebrew module Homebrew
module_function module DevCmd
class Create < AbstractCommand
sig { returns(CLI::Parser) } cmd_args do
def create_args description <<~EOS
Homebrew::CLI::Parser.new do Generate a formula or, with `--cask`, a cask for the downloadable file at <URL>
description <<~EOS and open it in the editor. Homebrew will attempt to automatically derive the
Generate a formula or, with `--cask`, a cask for the downloadable file at <URL> formula name and version, but if it fails, you'll have to make your own template.
and open it in the editor. Homebrew will attempt to automatically derive the The `wget` formula serves as a simple example. For the complete API, see:
formula name and version, but if it fails, you'll have to make your own template. <https://rubydoc.brew.sh/Formula>
The `wget` formula serves as a simple example. For the complete API, see:
<https://rubydoc.brew.sh/Formula>
EOS
switch "--autotools",
description: "Create a basic template for an Autotools-style build."
switch "--cask",
description: "Create a basic template for a cask."
switch "--cmake",
description: "Create a basic template for a CMake-style build."
switch "--crystal",
description: "Create a basic template for a Crystal build."
switch "--go",
description: "Create a basic template for a Go build."
switch "--meson",
description: "Create a basic template for a Meson-style build."
switch "--node",
description: "Create a basic template for a Node build."
switch "--perl",
description: "Create a basic template for a Perl build."
switch "--python",
description: "Create a basic template for a Python build."
switch "--ruby",
description: "Create a basic template for a Ruby build."
switch "--rust",
description: "Create a basic template for a Rust build."
switch "--no-fetch",
description: "Homebrew will not download <URL> to the cache and will thus not add its SHA-256 " \
"to the formula for you, nor will it check the GitHub API for GitHub projects " \
"(to fill out its description and homepage)."
switch "--HEAD",
description: "Indicate that <URL> points to the package's repository rather than a file."
flag "--set-name=",
description: "Explicitly set the <name> of the new formula or cask."
flag "--set-version=",
description: "Explicitly set the <version> of the new formula or cask."
flag "--set-license=",
description: "Explicitly set the <license> of the new formula."
flag "--tap=",
description: "Generate the new formula within the given tap, specified as <user>`/`<repo>."
switch "-f", "--force",
description: "Ignore errors for disallowed formula names and names that shadow aliases."
conflicts "--autotools", "--cmake", "--crystal", "--go", "--meson", "--node",
"--perl", "--python", "--ruby", "--rust", "--cask"
conflicts "--cask", "--HEAD"
conflicts "--cask", "--set-license"
named_args :url, number: 1
end
end
# Create a formula from a tarball URL.
sig { void }
def create
args = create_args.parse
path = if args.cask?
create_cask(args:)
else
create_formula(args:)
end
exec_editor path
end
sig { params(args: CLI::Args).returns(Pathname) }
def create_cask(args:)
url = args.named.first
name = if args.set_name.blank?
stem = Pathname.new(url).stem.rpartition("=").last
print "Cask name [#{stem}]: "
__gets || stem
else
args.set_name
end
token = Cask::Utils.token_from(T.must(name))
cask_tap = Tap.fetch(args.tap || "homebrew/cask")
raise TapUnavailableError, cask_tap.name unless cask_tap.installed?
cask_path = cask_tap.new_cask_path(token)
cask_path.dirname.mkpath unless cask_path.dirname.exist?
raise Cask::CaskAlreadyCreatedError, token if cask_path.exist?
version = if args.set_version
Version.new(T.must(args.set_version))
else
Version.detect(url.gsub(token, "").gsub(/x86(_64)?/, ""))
end
interpolated_url, sha256 = if version.null?
[url, ""]
else
sha256 = if args.no_fetch?
""
else
strategy = DownloadStrategyDetector.detect(url)
downloader = strategy.new(url, token, version.to_s, cache: Cask::Cache.path)
downloader.fetch
downloader.cached_location.sha256
end
[url.gsub(version.to_s, "\#{version}"), sha256]
end
cask_path.atomic_write <<~RUBY
# Documentation: https://docs.brew.sh/Cask-Cookbook
# https://docs.brew.sh/Adding-Software-to-Homebrew#cask-stanzas
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
cask "#{token}" do
version "#{version}"
sha256 "#{sha256}"
url "#{interpolated_url}"
name "#{name}"
desc ""
homepage ""
# Documentation: https://docs.brew.sh/Brew-Livecheck
livecheck do
url ""
strategy ""
end
depends_on macos: ""
app ""
# Documentation: https://docs.brew.sh/Cask-Cookbook#stanza-zap
zap trash: ""
end
RUBY
puts "Please run `brew audit --cask --new #{token}` before submitting, thanks."
cask_path
end
sig { params(args: CLI::Args).returns(Pathname) }
def create_formula(args:)
mode = if args.autotools?
:autotools
elsif args.cmake?
:cmake
elsif args.crystal?
:crystal
elsif args.go?
:go
elsif args.meson?
:meson
elsif args.node?
:node
elsif args.perl?
:perl
elsif args.python?
:python
elsif args.ruby?
:ruby
elsif args.rust?
:rust
end
fc = FormulaCreator.new(
args.set_name,
args.set_version,
tap: args.tap,
url: args.named.first,
mode:,
license: args.set_license,
fetch: !args.no_fetch?,
head: args.HEAD?,
)
fc.parse_url
# ask for confirmation if name wasn't passed explicitly
if args.set_name.blank?
print "Formula name [#{fc.name}]: "
fc.name = __gets || fc.name
end
fc.verify
# Check for disallowed formula, or names that shadow aliases,
# unless --force is specified.
unless args.force?
if (reason = MissingFormula.disallowed_reason(fc.name))
odie <<~EOS
The formula '#{fc.name}' is not allowed to be created.
#{reason}
If you really want to create this formula use `--force`.
EOS EOS
switch "--autotools",
description: "Create a basic template for an Autotools-style build."
switch "--cask",
description: "Create a basic template for a cask."
switch "--cmake",
description: "Create a basic template for a CMake-style build."
switch "--crystal",
description: "Create a basic template for a Crystal build."
switch "--go",
description: "Create a basic template for a Go build."
switch "--meson",
description: "Create a basic template for a Meson-style build."
switch "--node",
description: "Create a basic template for a Node build."
switch "--perl",
description: "Create a basic template for a Perl build."
switch "--python",
description: "Create a basic template for a Python build."
switch "--ruby",
description: "Create a basic template for a Ruby build."
switch "--rust",
description: "Create a basic template for a Rust build."
switch "--no-fetch",
description: "Homebrew will not download <URL> to the cache and will thus not add its SHA-256 " \
"to the formula for you, nor will it check the GitHub API for GitHub projects " \
"(to fill out its description and homepage)."
switch "--HEAD",
description: "Indicate that <URL> points to the package's repository rather than a file."
flag "--set-name=",
description: "Explicitly set the <name> of the new formula or cask."
flag "--set-version=",
description: "Explicitly set the <version> of the new formula or cask."
flag "--set-license=",
description: "Explicitly set the <license> of the new formula."
flag "--tap=",
description: "Generate the new formula within the given tap, specified as <user>`/`<repo>."
switch "-f", "--force",
description: "Ignore errors for disallowed formula names and names that shadow aliases."
conflicts "--autotools", "--cmake", "--crystal", "--go", "--meson", "--node",
"--perl", "--python", "--ruby", "--rust", "--cask"
conflicts "--cask", "--HEAD"
conflicts "--cask", "--set-license"
named_args :url, number: 1
end end
Homebrew.with_no_api_env do # Create a formula from a tarball URL.
if Formula.aliases.include? fc.name sig { override.void }
realname = Formulary.canonical_name(fc.name) def run
odie <<~EOS path = if args.cask?
The formula '#{realname}' is already aliased to '#{fc.name}'. create_cask
Please check that you are not creating a duplicate. else
To force creation use `--force`. create_formula
EOS
end end
exec_editor path
end
private
sig { returns(Pathname) }
def create_cask
url = args.named.first
name = if args.set_name.blank?
stem = Pathname.new(url).stem.rpartition("=").last
print "Cask name [#{stem}]: "
__gets || stem
else
args.set_name
end
token = Cask::Utils.token_from(T.must(name))
cask_tap = Tap.fetch(args.tap || "homebrew/cask")
raise TapUnavailableError, cask_tap.name unless cask_tap.installed?
cask_path = cask_tap.new_cask_path(token)
cask_path.dirname.mkpath unless cask_path.dirname.exist?
raise Cask::CaskAlreadyCreatedError, token if cask_path.exist?
version = if args.set_version
Version.new(T.must(args.set_version))
else
Version.detect(url.gsub(token, "").gsub(/x86(_64)?/, ""))
end
interpolated_url, sha256 = if version.null?
[url, ""]
else
sha256 = if args.no_fetch?
""
else
strategy = DownloadStrategyDetector.detect(url)
downloader = strategy.new(url, token, version.to_s, cache: Cask::Cache.path)
downloader.fetch
downloader.cached_location.sha256
end
[url.gsub(version.to_s, "\#{version}"), sha256]
end
cask_path.atomic_write <<~RUBY
# Documentation: https://docs.brew.sh/Cask-Cookbook
# https://docs.brew.sh/Adding-Software-to-Homebrew#cask-stanzas
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
cask "#{token}" do
version "#{version}"
sha256 "#{sha256}"
url "#{interpolated_url}"
name "#{name}"
desc ""
homepage ""
# Documentation: https://docs.brew.sh/Brew-Livecheck
livecheck do
url ""
strategy ""
end
depends_on macos: ""
app ""
# Documentation: https://docs.brew.sh/Cask-Cookbook#stanza-zap
zap trash: ""
end
RUBY
puts "Please run `brew audit --cask --new #{token}` before submitting, thanks."
cask_path
end
sig { returns(Pathname) }
def create_formula
mode = if args.autotools?
:autotools
elsif args.cmake?
:cmake
elsif args.crystal?
:crystal
elsif args.go?
:go
elsif args.meson?
:meson
elsif args.node?
:node
elsif args.perl?
:perl
elsif args.python?
:python
elsif args.ruby?
:ruby
elsif args.rust?
:rust
end
fc = FormulaCreator.new(
args.set_name,
args.set_version,
tap: args.tap,
url: args.named.first,
mode:,
license: args.set_license,
fetch: !args.no_fetch?,
head: args.HEAD?,
)
fc.parse_url
# ask for confirmation if name wasn't passed explicitly
if args.set_name.blank?
print "Formula name [#{fc.name}]: "
fc.name = __gets || fc.name
end
fc.verify
# Check for disallowed formula, or names that shadow aliases,
# unless --force is specified.
unless args.force?
if (reason = MissingFormula.disallowed_reason(fc.name))
odie <<~EOS
The formula '#{fc.name}' is not allowed to be created.
#{reason}
If you really want to create this formula use `--force`.
EOS
end
Homebrew.with_no_api_env do
if Formula.aliases.include? fc.name
realname = Formulary.canonical_name(fc.name)
odie <<~EOS
The formula '#{realname}' is already aliased to '#{fc.name}'.
Please check that you are not creating a duplicate.
To force creation use `--force`.
EOS
end
end
end
path = fc.write_formula!
formula = Homebrew.with_no_api_env do
Formula[fc.name]
end
PyPI.update_python_resources! formula, ignore_non_pypi_packages: true if args.python?
puts "Please run `HOMEBREW_NO_INSTALL_FROM_API=1 brew audit --new #{fc.name}` before submitting, thanks."
path
end
sig { returns(T.nilable(String)) }
def __gets
gots = $stdin.gets.chomp
gots.empty? ? nil : gots
end end
end end
path = fc.write_formula!
formula = Homebrew.with_no_api_env do
Formula[fc.name]
end
PyPI.update_python_resources! formula, ignore_non_pypi_packages: true if args.python?
puts "Please run `HOMEBREW_NO_INSTALL_FROM_API=1 brew audit --new #{fc.name}` before submitting, thanks."
path
end
sig { returns(T.nilable(String)) }
def __gets
gots = $stdin.gets.chomp
gots.empty? ? nil : gots
end end
end end

View File

@ -1,62 +1,62 @@
# typed: strict # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "test_runner_formula" require "test_runner_formula"
require "github_runner_matrix" require "github_runner_matrix"
module Homebrew module Homebrew
sig { returns(Homebrew::CLI::Parser) } module DevCmd
def self.determine_test_runners_args class DetermineTestRunners < AbstractCommand
Homebrew::CLI::Parser.new do cmd_args do
usage_banner <<~EOS usage_banner <<~EOS
`determine-test-runners` {<testing-formulae> [<deleted-formulae>]|--all-supported} `determine-test-runners` {<testing-formulae> [<deleted-formulae>]|--all-supported}
Determines the runners used to test formulae or their dependents. For internal use in Homebrew taps. Determines the runners used to test formulae or their dependents. For internal use in Homebrew taps.
EOS EOS
switch "--all-supported", switch "--all-supported",
description: "Instead of selecting runners based on the chosen formula, return all supported runners." description: "Instead of selecting runners based on the chosen formula, return all supported runners."
switch "--eval-all", switch "--eval-all",
description: "Evaluate all available formulae, whether installed or not, to determine testing " \ description: "Evaluate all available formulae, whether installed or not, to determine testing " \
"dependents.", "dependents.",
env: :eval_all env: :eval_all
switch "--dependents", switch "--dependents",
description: "Determine runners for testing dependents. Requires `--eval-all` or `HOMEBREW_EVAL_ALL`.", description: "Determine runners for testing dependents. Requires `--eval-all` or `HOMEBREW_EVAL_ALL`.",
depends_on: "--eval-all" depends_on: "--eval-all"
named_args max: 2 named_args max: 2
conflicts "--all-supported", "--dependents" conflicts "--all-supported", "--dependents"
hide_from_man_page! hide_from_man_page!
end end
end
sig { void } sig { override.void }
def self.determine_test_runners def run
args = determine_test_runners_args.parse if args.no_named? && !args.all_supported?
raise Homebrew::CLI::MinNamedArgumentsError, 1
elsif args.all_supported? && !args.no_named?
raise UsageError, "`--all-supported` is mutually exclusive to other arguments."
end
if args.no_named? && !args.all_supported? testing_formulae = args.named.first&.split(",").to_a
raise Homebrew::CLI::MinNamedArgumentsError, 1 testing_formulae.map! { |name| TestRunnerFormula.new(Formulary.factory(name), eval_all: args.eval_all?) }
elsif args.all_supported? && !args.no_named? .freeze
raise UsageError, "`--all-supported` is mutually exclusive to other arguments." deleted_formulae = args.named.second&.split(",").to_a.freeze
end runner_matrix = GitHubRunnerMatrix.new(testing_formulae, deleted_formulae,
all_supported: args.all_supported?,
dependent_matrix: args.dependents?)
runners = runner_matrix.active_runner_specs_hash
testing_formulae = args.named.first&.split(",").to_a ohai "Runners", JSON.pretty_generate(runners)
testing_formulae.map! { |name| TestRunnerFormula.new(Formulary.factory(name), eval_all: args.eval_all?) }
.freeze
deleted_formulae = args.named.second&.split(",").to_a.freeze
runner_matrix = GitHubRunnerMatrix.new(testing_formulae, deleted_formulae,
all_supported: args.all_supported?,
dependent_matrix: args.dependents?)
runners = runner_matrix.active_runner_specs_hash
ohai "Runners", JSON.pretty_generate(runners) github_output = ENV.fetch("GITHUB_OUTPUT")
File.open(github_output, "a") do |f|
github_output = ENV.fetch("GITHUB_OUTPUT") f.puts("runners=#{runners.to_json}")
File.open(github_output, "a") do |f| f.puts("runners_present=#{runners.present?}")
f.puts("runners=#{runners.to_json}") end
f.puts("runners_present=#{runners.present?}") end
end end
end end
end end

View File

@ -1,94 +1,93 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "utils/github" require "utils/github"
module Homebrew module Homebrew
module_function module DevCmd
class DispatchBuildBottle < AbstractCommand
cmd_args do
description <<~EOS
Build bottles for these formulae with GitHub Actions.
EOS
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--timeout=",
description: "Build timeout (in minutes, default: 60)."
flag "--issue=",
description: "If specified, post a comment to this issue number if the job fails."
comma_array "--macos",
description: "macOS version (or comma-separated list of versions) the bottle should be built for."
flag "--workflow=",
description: "Dispatch specified workflow (default: `dispatch-build-bottle.yml`)."
switch "--upload",
description: "Upload built bottles."
switch "--linux",
description: "Dispatch bottle for Linux (using GitHub runners)."
switch "--linux-self-hosted",
description: "Dispatch bottle for Linux (using self-hosted runner)."
switch "--linux-wheezy",
description: "Use Debian Wheezy container for building the bottle on Linux."
sig { returns(CLI::Parser) } conflicts "--linux", "--linux-self-hosted"
def dispatch_build_bottle_args named_args :formula, min: 1
Homebrew::CLI::Parser.new do end
description <<~EOS
Build bottles for these formulae with GitHub Actions.
EOS
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--timeout=",
description: "Build timeout (in minutes, default: 60)."
flag "--issue=",
description: "If specified, post a comment to this issue number if the job fails."
comma_array "--macos",
description: "macOS version (or comma-separated list of versions) the bottle should be built for."
flag "--workflow=",
description: "Dispatch specified workflow (default: `dispatch-build-bottle.yml`)."
switch "--upload",
description: "Upload built bottles."
switch "--linux",
description: "Dispatch bottle for Linux (using GitHub runners)."
switch "--linux-self-hosted",
description: "Dispatch bottle for Linux (using self-hosted runner)."
switch "--linux-wheezy",
description: "Use Debian Wheezy container for building the bottle on Linux."
conflicts "--linux", "--linux-self-hosted" sig { override.void }
named_args :formula, min: 1 def run
end tap = Tap.fetch(args.tap || CoreTap.instance.name)
end user, repo = tap.full_name.split("/")
ref = "master"
workflow = args.workflow || "dispatch-build-bottle.yml"
def dispatch_build_bottle runners = []
args = dispatch_build_bottle_args.parse
tap = Tap.fetch(args.tap || CoreTap.instance.name) if (macos = args.macos&.compact_blank) && macos.present?
user, repo = tap.full_name.split("/") runners += macos.map do |element|
ref = "master" # We accept runner name syntax (11-arm64) or bottle syntax (arm64_big_sur)
workflow = args.workflow || "dispatch-build-bottle.yml" os, arch = element.then do |s|
tag = Utils::Bottles::Tag.from_symbol(s.to_sym)
[tag.to_macos_version, tag.arch]
rescue ArgumentError, MacOSVersion::Error
os, arch = s.split("-", 2)
[MacOSVersion.new(os), arch&.to_sym]
end
runners = [] if arch.present? && arch != :x86_64
"#{os}-#{arch}"
if (macos = args.macos&.compact_blank) && macos.present? else
runners += macos.map do |element| os.to_s
# We accept runner name syntax (11-arm64) or bottle syntax (arm64_big_sur) end
os, arch = element.then do |s| end
tag = Utils::Bottles::Tag.from_symbol(s.to_sym)
[tag.to_macos_version, tag.arch]
rescue ArgumentError, MacOSVersion::Error
os, arch = s.split("-", 2)
[MacOSVersion.new(os), arch&.to_sym]
end end
if arch.present? && arch != :x86_64 if args.linux?
"#{os}-#{arch}" runners << "ubuntu-22.04"
else elsif args.linux_self_hosted?
os.to_s runners << "linux-self-hosted-1"
end
raise UsageError, "Must specify `--macos`, `--linux` or `--linux-self-hosted` option." if runners.empty?
args.named.to_resolved_formulae.each do |formula|
# Required inputs
inputs = {
runner: runners.join(","),
formula: formula.name,
}
# Optional inputs
# These cannot be passed as nil to GitHub API
inputs[:timeout] = args.timeout if args.timeout
inputs[:issue] = args.issue if args.issue
inputs[:upload] = args.upload?
ohai "Dispatching #{tap} bottling request of formula \"#{formula.name}\" for #{runners.join(", ")}"
GitHub.workflow_dispatch_event(user, repo, workflow, ref, **inputs)
end end
end end
end end
if args.linux?
runners << "ubuntu-22.04"
elsif args.linux_self_hosted?
runners << "linux-self-hosted-1"
end
raise UsageError, "Must specify `--macos`, `--linux` or `--linux-self-hosted` option." if runners.empty?
args.named.to_resolved_formulae.each do |formula|
# Required inputs
inputs = {
runner: runners.join(","),
formula: formula.name,
}
# Optional inputs
# These cannot be passed as nil to GitHub API
inputs[:timeout] = args.timeout if args.timeout
inputs[:issue] = args.issue if args.issue
inputs[:upload] = args.upload?
ohai "Dispatching #{tap} bottling request of formula \"#{formula.name}\" for #{runners.join(", ")}"
GitHub.workflow_dispatch_event(user, repo, workflow, ref, **inputs)
end
end end
end end

View File

@ -1,133 +1,133 @@
# typed: strict # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "formula" require "formula"
require "cli/parser" require "cli/parser"
module Homebrew module Homebrew
module_function module DevCmd
class Edit < AbstractCommand
cmd_args do
description <<~EOS
Open a <formula>, <cask> or <tap> in the editor set by `EDITOR` or `HOMEBREW_EDITOR`,
or open the Homebrew repository for editing if no argument is provided.
EOS
sig { returns(CLI::Parser) } switch "--formula", "--formulae",
def edit_args description: "Treat all named arguments as formulae."
Homebrew::CLI::Parser.new do switch "--cask", "--casks",
description <<~EOS description: "Treat all named arguments as casks."
Open a <formula>, <cask> or <tap> in the editor set by `EDITOR` or `HOMEBREW_EDITOR`, switch "--print-path",
or open the Homebrew repository for editing if no argument is provided. description: "Print the file path to be edited, without opening an editor."
EOS
switch "--formula", "--formulae", conflicts "--formula", "--cask"
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
switch "--print-path",
description: "Print the file path to be edited, without opening an editor."
conflicts "--formula", "--cask" named_args [:formula, :cask, :tap], without_api: true
named_args [:formula, :cask, :tap], without_api: true
end
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_formula_path?(path)
path.fnmatch?("**/homebrew-core/Formula/**.rb", File::FNM_DOTMATCH)
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_cask_path?(path)
path.fnmatch?("**/homebrew-cask/Casks/**.rb", File::FNM_DOTMATCH)
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_formula_tap?(path)
path == CoreTap.instance.path
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_cask_tap?(path)
path == CoreCaskTap.instance.path
end
sig { params(path: Pathname, cask: T::Boolean).returns(T.noreturn) }
def raise_with_message!(path, cask)
name = path.basename(".rb").to_s
if (tap_match = Regexp.new("#{HOMEBREW_TAP_DIR_REGEX.source}$").match(path.to_s))
raise TapUnavailableError, CoreTap.instance.name if core_formula_tap?(path)
raise TapUnavailableError, CoreCaskTap.instance.name if core_cask_tap?(path)
raise TapUnavailableError, "#{tap_match[:user]}/#{tap_match[:repo]}"
elsif cask || core_cask_path?(path)
if !CoreCaskTap.instance.installed? && Homebrew::API::Cask.all_casks.key?(name)
command = "brew tap --force #{CoreCaskTap.instance.name}"
action = "tap #{CoreCaskTap.instance.name}"
else
command = "brew create --cask --set-name #{name} $URL"
action = "create a new cask"
end
elsif core_formula_path?(path) &&
!CoreTap.instance.installed? &&
Homebrew::API::Formula.all_formulae.key?(name)
command = "brew tap --force #{CoreTap.instance.name}"
action = "tap #{CoreTap.instance.name}"
else
command = "brew create --set-name #{name} $URL"
action = "create a new formula"
end
raise UsageError, <<~EOS
#{name} doesn't exist on disk.
Run #{Formatter.identifier(command)} to #{action}!
EOS
end
sig { void }
def edit
args = edit_args.parse
ENV["COLORTERM"] = ENV.fetch("HOMEBREW_COLORTERM", nil)
unless (HOMEBREW_REPOSITORY/".git").directory?
odie <<~EOS
Changes will be lost!
The first time you `brew update`, all local changes will be lost; you should
thus `brew update` before you `brew edit`!
EOS
end
paths = if args.named.empty?
# Sublime requires opting into the project editing path,
# as opposed to VS Code which will infer from the .vscode path
if which_editor(silent: true) == "subl"
["--project", "#{HOMEBREW_REPOSITORY}/.sublime/homebrew.sublime-project"]
else
# If no formulae are listed, open the project root in an editor.
[HOMEBREW_REPOSITORY]
end
else
expanded_paths = args.named.to_paths
expanded_paths.each do |path|
raise_with_message!(path, args.cask?) unless path.exist?
end end
if expanded_paths.any? do |path| sig { override.void }
(core_formula_path?(path) || core_cask_path?(path) || core_formula_tap?(path) || core_cask_tap?(path)) && def run
!Homebrew::EnvConfig.no_install_from_api? && ENV["COLORTERM"] = ENV.fetch("HOMEBREW_COLORTERM", nil)
!Homebrew::EnvConfig.no_env_hints?
end unless (HOMEBREW_REPOSITORY/".git").directory?
opoo <<~EOS odie <<~EOS
`brew install` ignores locally edited casks and formulae if Changes will be lost!
HOMEBREW_NO_INSTALL_FROM_API is not set. The first time you `brew update`, all local changes will be lost; you should
thus `brew update` before you `brew edit`!
EOS
end
paths = if args.named.empty?
# Sublime requires opting into the project editing path,
# as opposed to VS Code which will infer from the .vscode path
if which_editor(silent: true) == "subl"
["--project", "#{HOMEBREW_REPOSITORY}/.sublime/homebrew.sublime-project"]
else
# If no formulae are listed, open the project root in an editor.
[HOMEBREW_REPOSITORY]
end
else
expanded_paths = args.named.to_paths
expanded_paths.each do |path|
raise_with_message!(path, args.cask?) unless path.exist?
end
if expanded_paths.any? do |path|
!Homebrew::EnvConfig.no_install_from_api? &&
!Homebrew::EnvConfig.no_env_hints? &&
(core_formula_path?(path) || core_cask_path?(path) || core_formula_tap?(path) || core_cask_tap?(path))
end
opoo <<~EOS
`brew install` ignores locally edited casks and formulae if
HOMEBREW_NO_INSTALL_FROM_API is not set.
EOS
end
expanded_paths
end
if args.print_path?
paths.each { puts _1 }
return
end
exec_editor(*paths)
end
private
sig { params(path: Pathname).returns(T::Boolean) }
def core_formula_path?(path)
path.fnmatch?("**/homebrew-core/Formula/**.rb", File::FNM_DOTMATCH)
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_cask_path?(path)
path.fnmatch?("**/homebrew-cask/Casks/**.rb", File::FNM_DOTMATCH)
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_formula_tap?(path)
path == CoreTap.instance.path
end
sig { params(path: Pathname).returns(T::Boolean) }
def core_cask_tap?(path)
path == CoreCaskTap.instance.path
end
sig { params(path: Pathname, cask: T::Boolean).returns(T.noreturn) }
def raise_with_message!(path, cask)
name = path.basename(".rb").to_s
if (tap_match = Regexp.new("#{HOMEBREW_TAP_DIR_REGEX.source}$").match(path.to_s))
raise TapUnavailableError, CoreTap.instance.name if core_formula_tap?(path)
raise TapUnavailableError, CoreCaskTap.instance.name if core_cask_tap?(path)
raise TapUnavailableError, "#{tap_match[:user]}/#{tap_match[:repo]}"
elsif cask || core_cask_path?(path)
if !CoreCaskTap.instance.installed? && Homebrew::API::Cask.all_casks.key?(name)
command = "brew tap --force #{CoreCaskTap.instance.name}"
action = "tap #{CoreCaskTap.instance.name}"
else
command = "brew create --cask --set-name #{name} $URL"
action = "create a new cask"
end
elsif core_formula_path?(path) &&
!CoreTap.instance.installed? &&
Homebrew::API::Formula.all_formulae.key?(name)
command = "brew tap --force #{CoreTap.instance.name}"
action = "tap #{CoreTap.instance.name}"
else
command = "brew create --set-name #{name} $URL"
action = "create a new formula"
end
raise UsageError, <<~EOS
#{name} doesn't exist on disk.
Run #{Formatter.identifier(command)} to #{action}!
EOS EOS
end end
expanded_paths
end end
if args.print_path?
paths.each(&method(:puts))
return
end
exec_editor(*paths)
end end
end end

View File

@ -1,6 +1,7 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "utils/git" require "utils/git"
require "formulary" require "formulary"
@ -8,216 +9,220 @@ require "software_spec"
require "tap" require "tap"
module Homebrew module Homebrew
BOTTLE_BLOCK_REGEX = / bottle (?:do.+?end|:[a-z]+)\n\n/m module DevCmd
class Extract < AbstractCommand
BOTTLE_BLOCK_REGEX = / bottle (?:do.+?end|:[a-z]+)\n\n/m
sig { returns(CLI::Parser) } cmd_args do
def self.extract_args usage_banner "`extract` [`--version=`] [`--force`] <formula> <tap>"
Homebrew::CLI::Parser.new do description <<~EOS
usage_banner "`extract` [`--version=`] [`--force`] <formula> <tap>" Look through repository history to find the most recent version of <formula> and
description <<~EOS create a copy in <tap>. Specifically, the command will create the new
Look through repository history to find the most recent version of <formula> and formula file at <tap>`/Formula/`<formula>`@`<version>`.rb`. If the tap is not
create a copy in <tap>. Specifically, the command will create the new installed yet, attempt to install/clone the tap before continuing. To extract
formula file at <tap>`/Formula/`<formula>`@`<version>`.rb`. If the tap is not a formula from a tap that is not `homebrew/core` use its fully-qualified form of
installed yet, attempt to install/clone the tap before continuing. To extract <user>`/`<repo>`/`<formula>.
a formula from a tap that is not `homebrew/core` use its fully-qualified form of EOS
<user>`/`<repo>`/`<formula>. flag "--version=",
EOS description: "Extract the specified <version> of <formula> instead of the most recent."
flag "--version=", switch "-f", "--force",
description: "Extract the specified <version> of <formula> instead of the most recent." description: "Overwrite the destination formula if it already exists."
switch "-f", "--force",
description: "Overwrite the destination formula if it already exists."
named_args [:formula, :tap], number: 2, without_api: true named_args [:formula, :tap], number: 2, without_api: true
end end
end
def self.extract sig { override.void }
args = extract_args.parse def run
if (tap_with_name = args.named.first&.then { Tap.with_formula_name(_1) })
source_tap, name = tap_with_name
else
name = args.named.first.downcase
source_tap = CoreTap.instance
end
raise TapFormulaUnavailableError.new(source_tap, name) unless source_tap.installed?
if (tap_with_name = args.named.first&.then { Tap.with_formula_name(_1) }) destination_tap = Tap.fetch(args.named.second)
source_tap, name = tap_with_name unless Homebrew::EnvConfig.developer?
else odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap?
name = args.named.first.downcase odie "Cannot extract formula to homebrew/cask!" if destination_tap.core_cask_tap?
source_tap = CoreTap.instance odie "Cannot extract formula to the same tap!" if destination_tap == source_tap
end end
raise TapFormulaUnavailableError.new(source_tap, name) unless source_tap.installed? destination_tap.install unless destination_tap.installed?
destination_tap = Tap.fetch(args.named.second) repo = source_tap.path
unless Homebrew::EnvConfig.developer? pattern = if source_tap.core_tap?
odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap? [source_tap.new_formula_path(name), repo/"Formula/#{name}.rb"].uniq
odie "Cannot extract formula to homebrew/cask!" if destination_tap.core_cask_tap? else
odie "Cannot extract formula to the same tap!" if destination_tap == source_tap # A formula can technically live in the root directory of a tap or in any of its subdirectories
end [repo/"#{name}.rb", repo/"**/#{name}.rb"]
destination_tap.install unless destination_tap.installed?
repo = source_tap.path
pattern = if source_tap.core_tap?
[source_tap.new_formula_path(name), repo/"Formula/#{name}.rb"].uniq
else
# A formula can technically live in the root directory of a tap or in any of its subdirectories
[repo/"#{name}.rb", repo/"**/#{name}.rb"]
end
if args.version
ohai "Searching repository history"
version = args.version
version_segments = Gem::Version.new(version).segments if Gem::Version.correct?(version)
rev = T.let(nil, T.nilable(String))
test_formula = T.let(nil, T.nilable(Formula))
result = ""
loop do
rev = rev.nil? ? "HEAD" : "#{rev}~1"
rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern, before_commit: rev)
if rev.nil? && source_tap.shallow?
odie <<~EOS
Could not find #{name} but #{source_tap} is a shallow clone!
Try again after running:
git -C "#{source_tap.path}" fetch --unshallow
EOS
elsif rev.nil?
odie "Could not find #{name}! The formula or version may not have existed."
end end
file = repo/path if args.version
result = Utils::Git.last_revision_of_file(repo, file, before_commit: rev) ohai "Searching repository history"
if result.empty? version = args.version
odebug "Skipping revision #{rev} - file is empty at this revision" version_segments = Gem::Version.new(version).segments if Gem::Version.correct?(version)
next rev = T.let(nil, T.nilable(String))
end test_formula = T.let(nil, T.nilable(Formula))
result = ""
loop do
rev = rev.nil? ? "HEAD" : "#{rev}~1"
rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern, before_commit: rev)
if rev.nil? && source_tap.shallow?
odie <<~EOS
Could not find #{name} but #{source_tap} is a shallow clone!
Try again after running:
git -C "#{source_tap.path}" fetch --unshallow
EOS
elsif rev.nil?
odie "Could not find #{name}! The formula or version may not have existed."
end
test_formula = formula_at_revision(repo, name, file, rev) file = repo/path
break if test_formula.nil? || test_formula.version == version result = Utils::Git.last_revision_of_file(repo, file, before_commit: rev)
if result.empty?
odebug "Skipping revision #{rev} - file is empty at this revision"
next
end
if version_segments && Gem::Version.correct?(test_formula.version) test_formula = formula_at_revision(repo, name, file, rev)
test_formula_version_segments = Gem::Version.new(test_formula.version).segments break if test_formula.nil? || test_formula.version == version
if version_segments.length < test_formula_version_segments.length
odebug "Apply semantic versioning with #{test_formula_version_segments}" if version_segments && Gem::Version.correct?(test_formula.version)
break if version_segments == test_formula_version_segments.first(version_segments.length) test_formula_version_segments = Gem::Version.new(test_formula.version).segments
if version_segments.length < test_formula_version_segments.length
odebug "Apply semantic versioning with #{test_formula_version_segments}"
break if version_segments == test_formula_version_segments.first(version_segments.length)
end
end
odebug "Trying #{test_formula.version} from revision #{rev} against desired #{version}"
end
odie "Could not find #{name}! The formula or version may not have existed." if test_formula.nil?
else
# Search in the root directory of <repo> as well as recursively in all of its subdirectories
files = Dir[repo/"{,**/}"].filter_map do |dir|
Pathname.glob("#{dir}/#{name}.rb").find(&:file?)
end
if files.empty?
ohai "Searching repository history"
rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern)
odie "Could not find #{name}! The formula or version may not have existed." if rev.nil?
file = repo/path
version = T.must(formula_at_revision(repo, name, file, rev)).version
result = Utils::Git.last_revision_of_file(repo, file)
else
file = files.fetch(0).realpath
rev = T.let("HEAD", T.nilable(String))
version = Formulary.factory(file).version
result = File.read(file)
end end
end end
odebug "Trying #{test_formula.version} from revision #{rev} against desired #{version}" # The class name has to be renamed to match the new filename,
end # e.g. Foo version 1.2.3 becomes FooAT123 and resides in Foo@1.2.3.rb.
odie "Could not find #{name}! The formula or version may not have existed." if test_formula.nil? class_name = Formulary.class_s(name)
else
# Search in the root directory of <repo> as well as recursively in all of its subdirectories # Remove any existing version suffixes, as a new one will be added later
files = Dir[repo/"{,**/}"].filter_map do |dir| name.sub!(/\b@(.*)\z\b/i, "")
Pathname.glob("#{dir}/#{name}.rb").find(&:file?) versioned_name = Formulary.class_s("#{name}@#{version}")
result.sub!("class #{class_name} < Formula", "class #{versioned_name} < Formula")
# Remove bottle blocks, they won't work.
result.sub!(BOTTLE_BLOCK_REGEX, "")
path = destination_tap.path/"Formula/#{name}@#{version.to_s.downcase}.rb"
if path.exist?
unless args.force?
odie <<~EOS
Destination formula already exists: #{path}
To overwrite it and continue anyways, run:
brew extract --force --version=#{version} #{name} #{destination_tap.name}
EOS
end
odebug "Overwriting existing formula at #{path}"
path.delete
end
ohai "Writing formula for #{name} from revision #{rev} to:", path
path.dirname.mkpath
path.write result
end end
if files.empty? private
ohai "Searching repository history"
rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern) sig { params(repo: Pathname, name: String, file: Pathname, rev: String).returns(T.nilable(Formula)) }
odie "Could not find #{name}! The formula or version may not have existed." if rev.nil? def formula_at_revision(repo, name, file, rev)
file = repo/path return if rev.empty?
version = T.must(formula_at_revision(repo, name, file, rev)).version
result = Utils::Git.last_revision_of_file(repo, file) contents = Utils::Git.last_revision_of_file(repo, file, before_commit: rev)
else contents.gsub!("@url=", "url ")
file = files.fetch(0).realpath contents.gsub!("require 'brewkit'", "require 'formula'")
rev = T.let("HEAD", T.nilable(String)) contents.sub!(BOTTLE_BLOCK_REGEX, "")
version = Formulary.factory(file).version with_monkey_patch { Formulary.from_contents(name, file, contents, ignore_errors: true) }
result = File.read(file)
end end
end
# The class name has to be renamed to match the new filename, def with_monkey_patch
# e.g. Foo version 1.2.3 becomes FooAT123 and resides in Foo@1.2.3.rb. # Since `method_defined?` is not a supported type guard, the use of `alias_method` below is not typesafe:
class_name = Formulary.class_s(name) BottleSpecification.class_eval do
T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) do |*|
# do nothing
end
end
# Remove any existing version suffixes, as a new one will be added later Module.class_eval do
name.sub!(/\b@(.*)\z\b/i, "") T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
versioned_name = Formulary.class_s("#{name}@#{version}") define_method(:method_missing) do |*|
result.sub!("class #{class_name} < Formula", "class #{versioned_name} < Formula") # do nothing
end
end
# Remove bottle blocks, they won't work. Resource.class_eval do
result.sub!(BOTTLE_BLOCK_REGEX, "") T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) do |*|
# do nothing
end
end
path = destination_tap.path/"Formula/#{name}@#{version.to_s.downcase}.rb" DependencyCollector.class_eval do
if path.exist? if method_defined?(:parse_symbol_spec)
unless args.force? T.unsafe(self).alias_method :old_parse_symbol_spec,
odie <<~EOS :parse_symbol_spec
Destination formula already exists: #{path} end
To overwrite it and continue anyways, run: define_method(:parse_symbol_spec) do |*|
brew extract --force --version=#{version} #{name} #{destination_tap.name} # do nothing
EOS end
end end
odebug "Overwriting existing formula at #{path}"
path.delete
end
ohai "Writing formula for #{name} from revision #{rev} to:", path
path.dirname.mkpath
path.write result
end
# @private yield
sig { params(repo: Pathname, name: String, file: Pathname, rev: String).returns(T.nilable(Formula)) } ensure
def self.formula_at_revision(repo, name, file, rev) BottleSpecification.class_eval do
return if rev.empty? if method_defined?(:old_method_missing)
T.unsafe(self).alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
contents = Utils::Git.last_revision_of_file(repo, file, before_commit: rev) Module.class_eval do
contents.gsub!("@url=", "url ") if method_defined?(:old_method_missing)
contents.gsub!("require 'brewkit'", "require 'formula'") T.unsafe(self).alias_method :method_missing, :old_method_missing
contents.sub!(BOTTLE_BLOCK_REGEX, "") undef :old_method_missing
with_monkey_patch { Formulary.from_contents(name, file, contents, ignore_errors: true) } end
end end
private_class_method def self.with_monkey_patch Resource.class_eval do
# Since `method_defined?` is not a supported type guard, the use of `alias_method` below is not typesafe: if method_defined?(:old_method_missing)
BottleSpecification.class_eval do T.unsafe(self).alias_method :method_missing, :old_method_missing
T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing) undef :old_method_missing
define_method(:method_missing) do |*| end
# do nothing end
end
end
Module.class_eval do DependencyCollector.class_eval do
T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing) if method_defined?(:old_parse_symbol_spec)
define_method(:method_missing) do |*| T.unsafe(self).alias_method :parse_symbol_spec, :old_parse_symbol_spec
# do nothing undef :old_parse_symbol_spec
end end
end end
Resource.class_eval do
T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) do |*|
# do nothing
end
end
DependencyCollector.class_eval do
T.unsafe(self).alias_method :old_parse_symbol_spec, :parse_symbol_spec if method_defined?(:parse_symbol_spec)
define_method(:parse_symbol_spec) do |*|
# do nothing
end
end
yield
ensure
BottleSpecification.class_eval do
if method_defined?(:old_method_missing)
T.unsafe(self).alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
Module.class_eval do
if method_defined?(:old_method_missing)
T.unsafe(self).alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
Resource.class_eval do
if method_defined?(:old_method_missing)
T.unsafe(self).alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
DependencyCollector.class_eval do
if method_defined?(:old_parse_symbol_spec)
T.unsafe(self).alias_method :parse_symbol_spec, :old_parse_symbol_spec
undef :old_parse_symbol_spec
end end
end end
end end

View File

@ -1,32 +1,31 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "formula" require "formula"
require "cli/parser" require "cli/parser"
module Homebrew module Homebrew
module_function module DevCmd
class FormulaCmd < AbstractCommand
cmd_args do
description <<~EOS
Display the path where <formula> is located.
EOS
sig { returns(CLI::Parser) } named_args :formula, min: 1, without_api: true
def formula_args end
Homebrew::CLI::Parser.new do
description <<~EOS
Display the path where <formula> is located.
EOS
named_args :formula, min: 1, without_api: true sig { override.void }
def run
formula_paths = args.named.to_paths(only: :formula).select(&:exist?)
if formula_paths.blank? && args.named
.to_paths(only: :cask)
.any?(&:exist?)
odie "Found casks but did not find formulae!"
end
formula_paths.each { puts _1 }
end
end end
end end
def formula
args = formula_args.parse
formula_paths = args.named.to_paths(only: :formula).select(&:exist?)
if formula_paths.blank? && args.named
.to_paths(only: :cask)
.any?(&:exist?)
odie "Found casks but did not find formulae!"
end
formula_paths.each(&method(:puts))
end
end end

View File

@ -1,82 +1,83 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "cask/cask" require "cask/cask"
require "formula" require "formula"
module Homebrew module Homebrew
module_function module DevCmd
class GenerateCaskApi < AbstractCommand
sig { returns(CLI::Parser) } CASK_JSON_TEMPLATE = <<~EOS
def generate_cask_api_args ---
Homebrew::CLI::Parser.new do layout: cask_json
description <<~EOS ---
Generate `homebrew/cask` API data files for <#{HOMEBREW_API_WWW}>. {{ content }}
The generated files are written to the current directory.
EOS EOS
switch "-n", "--dry-run", description: "Generate API data without writing it to files." cmd_args do
description <<~EOS
Generate `homebrew/cask` API data files for <#{HOMEBREW_API_WWW}>.
The generated files are written to the current directory.
EOS
named_args :none switch "-n", "--dry-run", description: "Generate API data without writing it to files."
end
end
CASK_JSON_TEMPLATE = <<~EOS named_args :none
---
layout: cask_json
---
{{ content }}
EOS
def html_template(title)
<<~EOS
---
title: #{title}
layout: cask
---
{{ content }}
EOS
end
def generate_cask_api
args = generate_cask_api_args.parse
tap = CoreCaskTap.instance
raise TapUnavailableError, tap.name unless tap.installed?
unless args.dry_run?
directories = ["_data/cask", "api/cask", "api/cask-source", "cask", "api/internal/v3"].freeze
FileUtils.rm_rf directories
FileUtils.mkdir_p directories
end
Homebrew.with_no_api_env do
tap_migrations_json = JSON.dump(tap.tap_migrations)
File.write("api/cask_tap_migrations.json", tap_migrations_json) unless args.dry_run?
Cask::Cask.generating_hash!
tap.cask_files.each do |path|
cask = Cask::CaskLoader.load(path)
name = cask.token
json = JSON.pretty_generate(cask.to_hash_with_variations)
cask_source = path.read
html_template_name = html_template(name)
unless args.dry_run?
File.write("_data/cask/#{name}.json", "#{json}\n")
File.write("api/cask/#{name}.json", CASK_JSON_TEMPLATE)
File.write("api/cask-source/#{name}.rb", cask_source)
File.write("cask/#{name}.html", html_template_name)
end
rescue
onoe "Error while generating data for cask '#{path.stem}'."
raise
end end
homebrew_cask_tap_json = JSON.generate(tap.to_internal_api_hash) sig { override.void }
File.write("api/internal/v3/homebrew-cask.json", homebrew_cask_tap_json) unless args.dry_run? def run
tap = CoreCaskTap.instance
raise TapUnavailableError, tap.name unless tap.installed?
unless args.dry_run?
directories = ["_data/cask", "api/cask", "api/cask-source", "cask", "api/internal/v3"].freeze
FileUtils.rm_rf directories
FileUtils.mkdir_p directories
end
Homebrew.with_no_api_env do
tap_migrations_json = JSON.dump(tap.tap_migrations)
File.write("api/cask_tap_migrations.json", tap_migrations_json) unless args.dry_run?
Cask::Cask.generating_hash!
tap.cask_files.each do |path|
cask = Cask::CaskLoader.load(path)
name = cask.token
json = JSON.pretty_generate(cask.to_hash_with_variations)
cask_source = path.read
html_template_name = html_template(name)
unless args.dry_run?
File.write("_data/cask/#{name}.json", "#{json}\n")
File.write("api/cask/#{name}.json", CASK_JSON_TEMPLATE)
File.write("api/cask-source/#{name}.rb", cask_source)
File.write("cask/#{name}.html", html_template_name)
end
rescue
onoe "Error while generating data for cask '#{path.stem}'."
raise
end
homebrew_cask_tap_json = JSON.generate(tap.to_internal_api_hash)
File.write("api/internal/v3/homebrew-cask.json", homebrew_cask_tap_json) unless args.dry_run?
end
end
private
def html_template(title)
<<~EOS
---
title: #{title}
layout: cask
---
{{ content }}
EOS
end
end end
end end
end end

View File

@ -5,79 +5,79 @@ require "cli/parser"
require "formula" require "formula"
module Homebrew module Homebrew
module_function module DevCmd
class GenerateFormulaApi < AbstractCommand
sig { returns(CLI::Parser) } FORMULA_JSON_TEMPLATE = <<~EOS
def generate_formula_api_args ---
Homebrew::CLI::Parser.new do layout: formula_json
description <<~EOS ---
Generate `homebrew/core` API data files for <#{HOMEBREW_API_WWW}>. {{ content }}
The generated files are written to the current directory.
EOS EOS
switch "-n", "--dry-run", description: "Generate API data without writing it to files." cmd_args do
description <<~EOS
Generate `homebrew/core` API data files for <#{HOMEBREW_API_WWW}>.
The generated files are written to the current directory.
EOS
named_args :none switch "-n", "--dry-run", description: "Generate API data without writing it to files."
end
end
FORMULA_JSON_TEMPLATE = <<~EOS named_args :none
---
layout: formula_json
---
{{ content }}
EOS
def html_template(title)
<<~EOS
---
title: #{title}
layout: formula
redirect_from: /formula-linux/#{title}
---
{{ content }}
EOS
end
def generate_formula_api
args = generate_formula_api_args.parse
tap = CoreTap.instance
raise TapUnavailableError, tap.name unless tap.installed?
unless args.dry_run?
directories = ["_data/formula", "api/formula", "formula", "api/internal/v3"]
FileUtils.rm_rf directories + ["_data/formula_canonical.json"]
FileUtils.mkdir_p directories
end
Homebrew.with_no_api_env do
tap_migrations_json = JSON.dump(tap.tap_migrations)
File.write("api/formula_tap_migrations.json", tap_migrations_json) unless args.dry_run?
Formulary.enable_factory_cache!
Formula.generating_hash!
tap.formula_names.each do |name|
formula = Formulary.factory(name)
name = formula.name
json = JSON.pretty_generate(formula.to_hash_with_variations)
html_template_name = html_template(name)
unless args.dry_run?
File.write("_data/formula/#{name.tr("+", "_")}.json", "#{json}\n")
File.write("api/formula/#{name}.json", FORMULA_JSON_TEMPLATE)
File.write("formula/#{name}.html", html_template_name)
end
rescue
onoe "Error while generating data for formula '#{name}'."
raise
end end
homebrew_core_tap_json = JSON.generate(tap.to_internal_api_hash) sig { override.void }
File.write("api/internal/v3/homebrew-core.json", homebrew_core_tap_json) unless args.dry_run? def run
canonical_json = JSON.pretty_generate(tap.formula_renames.merge(tap.alias_table)) tap = CoreTap.instance
File.write("_data/formula_canonical.json", "#{canonical_json}\n") unless args.dry_run? raise TapUnavailableError, tap.name unless tap.installed?
unless args.dry_run?
directories = ["_data/formula", "api/formula", "formula", "api/internal/v3"]
FileUtils.rm_rf directories + ["_data/formula_canonical.json"]
FileUtils.mkdir_p directories
end
Homebrew.with_no_api_env do
tap_migrations_json = JSON.dump(tap.tap_migrations)
File.write("api/formula_tap_migrations.json", tap_migrations_json) unless args.dry_run?
Formulary.enable_factory_cache!
Formula.generating_hash!
tap.formula_names.each do |name|
formula = Formulary.factory(name)
name = formula.name
json = JSON.pretty_generate(formula.to_hash_with_variations)
html_template_name = html_template(name)
unless args.dry_run?
File.write("_data/formula/#{name.tr("+", "_")}.json", "#{json}\n")
File.write("api/formula/#{name}.json", FORMULA_JSON_TEMPLATE)
File.write("formula/#{name}.html", html_template_name)
end
rescue
onoe "Error while generating data for formula '#{name}'."
raise
end
homebrew_core_tap_json = JSON.generate(tap.to_internal_api_hash)
File.write("api/internal/v3/homebrew-core.json", homebrew_core_tap_json) unless args.dry_run?
canonical_json = JSON.pretty_generate(tap.formula_renames.merge(tap.alias_table))
File.write("_data/formula_canonical.json", "#{canonical_json}\n") unless args.dry_run?
end
end
private
def html_template(title)
<<~EOS
---
title: #{title}
layout: formula
redirect_from: /formula-linux/#{title}
---
{{ content }}
EOS
end
end end
end end
end end

View File

@ -1,38 +1,39 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "formula" require "formula"
require "completions" require "completions"
require "manpages" require "manpages"
require "system_command" require "system_command"
module Homebrew module Homebrew
extend SystemCommand::Mixin module DevCmd
class GenerateManCompletions < AbstractCommand
include SystemCommand::Mixin
sig { returns(CLI::Parser) } cmd_args do
def self.generate_man_completions_args description <<~EOS
Homebrew::CLI::Parser.new do Generate Homebrew's manpages and shell completions.
description <<~EOS EOS
Generate Homebrew's manpages and shell completions. named_args :none
EOS end
named_args :none
end
end
def self.generate_man_completions sig { override.void }
args = generate_man_completions_args.parse def run
Commands.rebuild_internal_commands_completion_list
Manpages.regenerate_man_pages(quiet: args.quiet?)
Completions.update_shell_completions!
Commands.rebuild_internal_commands_completion_list diff = system_command "git", args: [
Manpages.regenerate_man_pages(quiet: args.quiet?) "-C", HOMEBREW_REPOSITORY, "diff", "--exit-code", "docs/Manpage.md", "manpages", "completions"
Completions.update_shell_completions! ]
if diff.status.success?
diff = system_command "git", args: [ ofail "No changes to manpage or completions."
"-C", HOMEBREW_REPOSITORY, "diff", "--exit-code", "docs/Manpage.md", "manpages", "completions" else
] puts "Manpage and completions updated."
if diff.status.success? end
ofail "No changes to manpage or completions." end
else
puts "Manpage and completions updated."
end end
end end
end end

View File

@ -1,41 +1,40 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
module Homebrew module Homebrew
module_function module DevCmd
class InstallBundlerGems < AbstractCommand
cmd_args do
description <<~EOS
Install Homebrew's Bundler gems.
EOS
comma_array "--groups",
description: "Installs the specified comma-separated list of gem groups (default: last used). " \
"Replaces any previously installed groups."
comma_array "--add-groups",
description: "Installs the specified comma-separated list of gem groups, " \
"in addition to those already installed."
sig { returns(CLI::Parser) } conflicts "--groups", "--add-groups"
def install_bundler_gems_args
Homebrew::CLI::Parser.new do
description <<~EOS
Install Homebrew's Bundler gems.
EOS
comma_array "--groups",
description: "Installs the specified comma-separated list of gem groups (default: last used). " \
"Replaces any previously installed groups."
comma_array "--add-groups",
description: "Installs the specified comma-separated list of gem groups, " \
"in addition to those already installed."
conflicts "--groups", "--add-groups" named_args :none
end
named_args :none sig { override.void }
def run
groups = args.groups || args.add_groups || []
if groups.delete("all")
groups |= Homebrew.valid_gem_groups
elsif args.groups # if we have been asked to replace
Homebrew.forget_user_gem_groups!
end
Homebrew.install_bundler_gems!(groups:)
end
end end
end end
def install_bundler_gems
args = install_bundler_gems_args.parse
groups = args.groups || args.add_groups || []
if groups.delete("all")
groups |= Homebrew.valid_gem_groups
elsif args.groups # if we have been asked to replace
Homebrew.forget_user_gem_groups!
end
Homebrew.install_bundler_gems!(groups:)
end
end end

View File

@ -1,6 +1,7 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "formulary" require "formulary"
require "cask/cask_loader" require "cask/cask_loader"
require "cli/parser" require "cli/parser"
@ -27,73 +28,76 @@ class Symbol
end end
module Homebrew module Homebrew
module_function module DevCmd
class Irb < AbstractCommand
cmd_args do
description <<~EOS
Enter the interactive Homebrew Ruby shell.
EOS
switch "--examples",
description: "Show several examples."
switch "--pry",
env: :pry,
description: "Use Pry instead of IRB. Implied if `HOMEBREW_PRY` is set."
end
sig { returns(CLI::Parser) } # work around IRB modifying ARGV.
def irb_args sig { params(argv: T.nilable(T::Array[String])).void }
Homebrew::CLI::Parser.new do def initialize(argv = nil) = super(argv || ARGV.dup.freeze)
description <<~EOS
Enter the interactive Homebrew Ruby shell. sig { override.void }
EOS def run
switch "--examples", clean_argv
description: "Show several examples."
switch "--pry", if args.examples?
env: :pry, puts <<~EOS
description: "Use Pry instead of IRB. Implied if `HOMEBREW_PRY` is set." 'v8'.f # => instance of the v8 formula
:hub.f.latest_version_installed?
:lua.f.methods - 1.methods
:mpd.f.recursive_dependencies.reject(&:installed?)
'vlc'.c # => instance of the vlc cask
:tsh.c.livecheckable?
EOS
return
end
if args.pry?
Homebrew.install_bundler_gems!(groups: ["pry"])
require "pry"
else
require "irb"
end
require "formula"
require "keg"
require "cask"
ohai "Interactive Homebrew Shell", "Example commands available with: `brew irb --examples`"
if args.pry?
Pry.config.should_load_rc = false # skip loading .pryrc
Pry.config.history_file = "#{Dir.home}/.brew_pry_history"
Pry.config.memory_size = 100 # max lines to save to history file
Pry.config.prompt_name = "brew"
Pry.start
else
ENV["IRBRC"] = (HOMEBREW_LIBRARY_PATH/"brew_irbrc").to_s
IRB.start
end
end
private
# Remove the `--debug`, `--verbose` and `--quiet` options which cause problems
# for IRB and have already been parsed by the CLI::Parser.
def clean_argv
global_options = Homebrew::CLI::Parser
.global_options
.flat_map { |options| options[0..1] }
ARGV.reject! { |arg| global_options.include?(arg) }
end
end end
end end
def irb
# work around IRB modifying ARGV.
args = irb_args.parse(ARGV.dup.freeze)
clean_argv
if args.examples?
puts <<~EOS
'v8'.f # => instance of the v8 formula
:hub.f.latest_version_installed?
:lua.f.methods - 1.methods
:mpd.f.recursive_dependencies.reject(&:installed?)
'vlc'.c # => instance of the vlc cask
:tsh.c.livecheckable?
EOS
return
end
if args.pry?
Homebrew.install_bundler_gems!(groups: ["pry"])
require "pry"
else
require "irb"
end
require "formula"
require "keg"
require "cask"
ohai "Interactive Homebrew Shell", "Example commands available with: `brew irb --examples`"
if args.pry?
Pry.config.should_load_rc = false # skip loading .pryrc
Pry.config.history_file = "#{Dir.home}/.brew_pry_history"
Pry.config.memory_size = 100 # max lines to save to history file
Pry.config.prompt_name = "brew"
Pry.start
else
ENV["IRBRC"] = (HOMEBREW_LIBRARY_PATH/"brew_irbrc").to_s
IRB.start
end
end
# Remove the `--debug`, `--verbose` and `--quiet` options which cause problems
# for IRB and have already been parsed by the CLI::Parser.
def clean_argv
global_options = Homebrew::CLI::Parser
.global_options
.flat_map { |options| options[0..1] }
ARGV.reject! { |arg| global_options.include?(arg) }
end
end end

View File

@ -1,58 +1,57 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cache_store" require "cache_store"
require "linkage_checker" require "linkage_checker"
require "cli/parser" require "cli/parser"
module Homebrew module Homebrew
module_function module DevCmd
class Linkage < AbstractCommand
cmd_args do
description <<~EOS
Check the library links from the given <formula> kegs. If no <formula> are
provided, check all kegs. Raises an error if run on uninstalled formulae.
EOS
switch "--test",
description: "Show only missing libraries and exit with a non-zero status if any missing " \
"libraries are found."
switch "--strict",
depends_on: "--test",
description: "Exit with a non-zero status if any undeclared dependencies with linkage are found."
switch "--reverse",
description: "For every library that a keg references, print its dylib path followed by the " \
"binaries that link to it."
switch "--cached",
description: "Print the cached linkage values stored in `HOMEBREW_CACHE`, set by a previous " \
"`brew linkage` run."
sig { returns(CLI::Parser) } named_args :installed_formula
def linkage_args
Homebrew::CLI::Parser.new do
description <<~EOS
Check the library links from the given <formula> kegs. If no <formula> are
provided, check all kegs. Raises an error if run on uninstalled formulae.
EOS
switch "--test",
description: "Show only missing libraries and exit with a non-zero status if any missing " \
"libraries are found."
switch "--strict",
depends_on: "--test",
description: "Exit with a non-zero status if any undeclared dependencies with linkage are found."
switch "--reverse",
description: "For every library that a keg references, print its dylib path followed by the " \
"binaries that link to it."
switch "--cached",
description: "Print the cached linkage values stored in `HOMEBREW_CACHE`, set by a previous " \
"`brew linkage` run."
named_args :installed_formula
end
end
def linkage
args = linkage_args.parse
CacheStoreDatabase.use(:linkage) do |db|
kegs = if args.named.to_default_kegs.empty?
Formula.installed.filter_map(&:any_installed_keg)
else
args.named.to_default_kegs
end end
kegs.each do |keg|
ohai "Checking #{keg.name} linkage" if kegs.size > 1
result = LinkageChecker.new(keg, cache_db: db) sig { override.void }
def run
CacheStoreDatabase.use(:linkage) do |db|
kegs = if args.named.to_default_kegs.empty?
Formula.installed.filter_map(&:any_installed_keg)
else
args.named.to_default_kegs
end
kegs.each do |keg|
ohai "Checking #{keg.name} linkage" if kegs.size > 1
if args.test? result = LinkageChecker.new(keg, cache_db: db)
result.display_test_output(strict: args.strict?)
Homebrew.failed = true if result.broken_library_linkage?(test: true, strict: args.strict?) if args.test?
elsif args.reverse? result.display_test_output(strict: args.strict?)
result.display_reverse_output Homebrew.failed = true if result.broken_library_linkage?(test: true, strict: args.strict?)
else elsif args.reverse?
result.display_normal_output result.display_reverse_output
else
result.display_normal_output
end
end
end end
end end
end end

View File

@ -1,139 +1,140 @@
# typed: true # typed: true
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "formula" require "formula"
require "livecheck/livecheck" require "livecheck/livecheck"
require "livecheck/strategy" require "livecheck/strategy"
module Homebrew module Homebrew
module_function module DevCmd
class LivecheckCmd < AbstractCommand
cmd_args do
description <<~EOS
Check for newer versions of formulae and/or casks from upstream.
If no formula or cask argument is passed, the list of formulae and
casks to check is taken from `HOMEBREW_LIVECHECK_WATCHLIST` or
`~/.homebrew/livecheck_watchlist.txt`.
EOS
switch "--full-name",
description: "Print formulae and casks with fully-qualified names."
flag "--tap=",
description: "Check formulae and casks within the given tap, specified as <user>`/`<repo>."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to check them."
switch "--installed",
description: "Check formulae and casks that are currently installed."
switch "--newer-only",
description: "Show the latest version only if it's newer than the formula/cask."
switch "--json",
description: "Output information in JSON format."
switch "-r", "--resources",
description: "Also check resources for formulae."
switch "-q", "--quiet",
description: "Suppress warnings, don't print a progress bar for JSON output."
switch "--formula", "--formulae",
description: "Only check formulae."
switch "--cask", "--casks",
description: "Only check casks."
switch "--extract-plist",
description: "Include casks using the ExtractPlist livecheck strategy."
sig { returns(CLI::Parser) } conflicts "--debug", "--json"
def livecheck_args conflicts "--tap=", "--eval-all", "--installed"
Homebrew::CLI::Parser.new do conflicts "--cask", "--formula"
description <<~EOS conflicts "--formula", "--extract-plist"
Check for newer versions of formulae and/or casks from upstream.
If no formula or cask argument is passed, the list of formulae and
casks to check is taken from `HOMEBREW_LIVECHECK_WATCHLIST` or
`~/.homebrew/livecheck_watchlist.txt`.
EOS
switch "--full-name",
description: "Print formulae and casks with fully-qualified names."
flag "--tap=",
description: "Check formulae and casks within the given tap, specified as <user>`/`<repo>."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to check them."
switch "--installed",
description: "Check formulae and casks that are currently installed."
switch "--newer-only",
description: "Show the latest version only if it's newer than the formula/cask."
switch "--json",
description: "Output information in JSON format."
switch "-r", "--resources",
description: "Also check resources for formulae."
switch "-q", "--quiet",
description: "Suppress warnings, don't print a progress bar for JSON output."
switch "--formula", "--formulae",
description: "Only check formulae."
switch "--cask", "--casks",
description: "Only check casks."
switch "--extract-plist",
description: "Include casks using the ExtractPlist livecheck strategy."
conflicts "--debug", "--json" named_args [:formula, :cask], without_api: true
conflicts "--tap=", "--eval-all", "--installed"
conflicts "--cask", "--formula"
conflicts "--formula", "--extract-plist"
named_args [:formula, :cask], without_api: true
end
end
def watchlist_path
@watchlist_path ||= begin
watchlist = File.expand_path(Homebrew::EnvConfig.livecheck_watchlist)
unless File.exist?(watchlist)
previous_default_watchlist = File.expand_path("~/.brew_livecheck_watchlist")
if File.exist?(previous_default_watchlist)
odisabled "~/.brew_livecheck_watchlist", "~/.homebrew/livecheck_watchlist.txt"
watchlist = previous_default_watchlist
end
end end
watchlist sig { override.void }
end def run
end Homebrew.install_bundler_gems!(groups: ["livecheck"])
def livecheck all = args.eval_all?
args = livecheck_args.parse
Homebrew.install_bundler_gems!(groups: ["livecheck"]) if args.debug? && args.verbose?
puts args
all = args.eval_all? puts Homebrew::EnvConfig.livecheck_watchlist if Homebrew::EnvConfig.livecheck_watchlist.present?
if args.debug? && args.verbose?
puts args
puts Homebrew::EnvConfig.livecheck_watchlist if Homebrew::EnvConfig.livecheck_watchlist.present?
end
formulae_and_casks_to_check = Homebrew.with_no_api_env do
if args.tap
tap = Tap.fetch(args.tap)
formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
casks = args.formula? ? [] : tap.cask_files.map { |path| Cask::CaskLoader.load(path) }
formulae + casks
elsif args.installed?
formulae = args.cask? ? [] : Formula.installed
casks = args.formula? ? [] : Cask::Caskroom.casks
formulae + casks
elsif all
formulae = args.cask? ? [] : Formula.all(eval_all: args.eval_all?)
casks = args.formula? ? [] : Cask::Cask.all(eval_all: args.eval_all?)
formulae + casks
elsif args.named.present?
if args.formula?
args.named.to_formulae
elsif args.cask?
args.named.to_casks
else
args.named.to_formulae_and_casks
end end
elsif File.exist?(watchlist_path)
begin
names = Pathname.new(watchlist_path).read.lines
.reject { |line| line.start_with?("#") || line.blank? }
.map(&:strip)
named_args = CLI::NamedArgs.new(*names, parent: args) formulae_and_casks_to_check = Homebrew.with_no_api_env do
named_args.to_formulae_and_casks(ignore_unavailable: true) if args.tap
rescue Errno::ENOENT => e tap = Tap.fetch(T.must(args.tap))
onoe e formulae = args.cask? ? [] : tap.formula_files.map { |path| Formulary.factory(path) }
casks = args.formula? ? [] : tap.cask_files.map { |path| Cask::CaskLoader.load(path) }
formulae + casks
elsif args.installed?
formulae = args.cask? ? [] : Formula.installed
casks = args.formula? ? [] : Cask::Caskroom.casks
formulae + casks
elsif all
formulae = args.cask? ? [] : Formula.all(eval_all: args.eval_all?)
casks = args.formula? ? [] : Cask::Cask.all(eval_all: args.eval_all?)
formulae + casks
elsif args.named.present?
if args.formula?
args.named.to_formulae
elsif args.cask?
args.named.to_casks
else
args.named.to_formulae_and_casks
end
elsif File.exist?(watchlist_path)
begin
names = Pathname.new(watchlist_path).read.lines
.reject { |line| line.start_with?("#") || line.blank? }
.map(&:strip)
named_args = CLI::NamedArgs.new(*names, parent: args)
named_args.to_formulae_and_casks(ignore_unavailable: true)
rescue Errno::ENOENT => e
onoe e
end
else
raise UsageError, "A watchlist file is required when no arguments are given."
end
end
formulae_and_casks_to_check = formulae_and_casks_to_check.sort_by do |formula_or_cask|
formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
end
raise UsageError, "No formulae or casks to check." if formulae_and_casks_to_check.blank?
options = {
json: args.json?,
full_name: args.full_name?,
handle_name_conflict: !args.formula? && !args.cask?,
check_resources: args.resources?,
newer_only: args.newer_only?,
extract_plist: args.extract_plist?,
quiet: args.quiet?,
debug: args.debug?,
verbose: args.verbose?,
}.compact
Livecheck.run_checks(formulae_and_casks_to_check, **options)
end
private
def watchlist_path
@watchlist_path ||= begin
watchlist = File.expand_path(Homebrew::EnvConfig.livecheck_watchlist)
unless File.exist?(watchlist)
previous_default_watchlist = File.expand_path("~/.brew_livecheck_watchlist")
if File.exist?(previous_default_watchlist)
odisabled "~/.brew_livecheck_watchlist", "~/.homebrew/livecheck_watchlist.txt"
watchlist = previous_default_watchlist
end
end
watchlist
end end
else
raise UsageError, "A watchlist file is required when no arguments are given."
end end
end end
formulae_and_casks_to_check = formulae_and_casks_to_check.sort_by do |formula_or_cask|
formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name
end
raise UsageError, "No formulae or casks to check." if formulae_and_casks_to_check.blank?
options = {
json: args.json?,
full_name: args.full_name?,
handle_name_conflict: !args.formula? && !args.cask?,
check_resources: args.resources?,
newer_only: args.newer_only?,
extract_plist: args.extract_plist?,
quiet: args.quiet?,
debug: args.debug?,
verbose: args.verbose?,
}.compact
Livecheck.run_checks(formulae_and_casks_to_check, **options)
end end
end end

View File

@ -1,87 +1,86 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "utils/github" require "utils/github"
module Homebrew module Homebrew
module_function module DevCmd
class PrAutomerge < AbstractCommand
cmd_args do
description <<~EOS
Find pull requests that can be automatically merged using `brew pr-publish`.
EOS
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--workflow=",
description: "Workflow file to use with `brew pr-publish`."
flag "--with-label=",
description: "Pull requests must have this label."
comma_array "--without-labels",
description: "Pull requests must not have these labels (default: " \
"`do not merge`, `new formula`, `automerge-skip`, " \
"`pre-release`, `CI-published-bottle-commits`)."
switch "--without-approval",
description: "Pull requests do not require approval to be merged."
switch "--publish",
description: "Run `brew pr-publish` on matching pull requests."
switch "--autosquash",
description: "Instruct `brew pr-publish` to automatically reformat and reword commits " \
"in the pull request to the preferred format."
switch "--no-autosquash",
description: "Instruct `brew pr-publish` to skip automatically reformatting and rewording commits " \
"in the pull request to the preferred format.",
disable: true, # odisabled: remove this switch with 4.3.0
hidden: true
switch "--ignore-failures",
description: "Include pull requests that have failing status checks."
sig { returns(CLI::Parser) } named_args :none
def pr_automerge_args end
Homebrew::CLI::Parser.new do
description <<~EOS
Find pull requests that can be automatically merged using `brew pr-publish`.
EOS
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--workflow=",
description: "Workflow file to use with `brew pr-publish`."
flag "--with-label=",
description: "Pull requests must have this label."
comma_array "--without-labels",
description: "Pull requests must not have these labels (default: " \
"`do not merge`, `new formula`, `automerge-skip`, " \
"`pre-release`, `CI-published-bottle-commits`)."
switch "--without-approval",
description: "Pull requests do not require approval to be merged."
switch "--publish",
description: "Run `brew pr-publish` on matching pull requests."
switch "--autosquash",
description: "Instruct `brew pr-publish` to automatically reformat and reword commits " \
"in the pull request to the preferred format."
switch "--no-autosquash",
description: "Instruct `brew pr-publish` to skip automatically reformatting and rewording commits " \
"in the pull request to the preferred format.",
disable: true, # odisabled: remove this switch with 4.3.0
hidden: true
switch "--ignore-failures",
description: "Include pull requests that have failing status checks."
named_args :none sig { override.void }
end def run
end without_labels = args.without_labels || [
"do not merge",
"new formula",
"automerge-skip",
"pre-release",
"CI-published-bottle-commits",
]
tap = Tap.fetch(args.tap || CoreTap.instance.name)
def pr_automerge query = "is:pr is:open repo:#{tap.full_name} draft:false"
args = pr_automerge_args.parse query += args.ignore_failures? ? " -status:pending" : " status:success"
query += " review:approved" unless args.without_approval?
query += " label:\"#{args.with_label}\"" if args.with_label
without_labels.each { |label| query += " -label:\"#{label}\"" }
odebug "Searching: #{query}"
without_labels = args.without_labels || [ prs = GitHub.search_issues query
"do not merge", if prs.blank?
"new formula", ohai "No matching pull requests!"
"automerge-skip", return
"pre-release", end
"CI-published-bottle-commits",
]
tap = Tap.fetch(args.tap || CoreTap.instance.name)
query = "is:pr is:open repo:#{tap.full_name} draft:false" ohai "#{prs.count} matching pull #{Utils.pluralize("request", prs.count)}:"
query += args.ignore_failures? ? " -status:pending" : " status:success" pr_urls = []
query += " review:approved" unless args.without_approval? prs.each do |pr|
query += " label:\"#{args.with_label}\"" if args.with_label puts "#{tap.full_name unless tap.core_tap?}##{pr["number"]}: #{pr["title"]}"
without_labels&.each { |label| query += " -label:\"#{label}\"" } pr_urls << pr["html_url"]
odebug "Searching: #{query}" end
prs = GitHub.search_issues query publish_args = ["pr-publish"]
if prs.blank? publish_args << "--tap=#{tap}" if tap
ohai "No matching pull requests!" publish_args << "--workflow=#{args.workflow}" if args.workflow
return publish_args << "--autosquash" if args.autosquash?
end if args.publish?
safe_system HOMEBREW_BREW_FILE, *publish_args, *pr_urls
ohai "#{prs.count} matching pull #{Utils.pluralize("request", prs.count)}:" else
pr_urls = [] ohai "Now run:", " brew #{publish_args.join " "} \\\n #{pr_urls.join " \\\n "}"
prs.each do |pr| end
puts "#{tap.full_name unless tap.core_tap?}##{pr["number"]}: #{pr["title"]}" end
pr_urls << pr["html_url"]
end
publish_args = ["pr-publish"]
publish_args << "--tap=#{tap}" if tap
publish_args << "--workflow=#{args.workflow}" if args.workflow
publish_args << "--autosquash" if args.autosquash?
if args.publish?
safe_system HOMEBREW_BREW_FILE, *publish_args, *pr_urls
else
ohai "Now run:", " brew #{publish_args.join " "} \\\n #{pr_urls.join " \\\n "}"
end end
end end
end end

View File

@ -1,75 +1,74 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command"
require "cli/parser" require "cli/parser"
require "utils/github" require "utils/github"
module Homebrew module Homebrew
module_function module DevCmd
class PrPublish < AbstractCommand
cmd_args do
description <<~EOS
Publish bottles for a pull request with GitHub Actions.
Requires write access to the repository.
EOS
switch "--autosquash",
description: "If supported on the target tap, automatically reformat and reword commits " \
"to our preferred format."
switch "--large-runner",
description: "Run the upload job on a large runner."
flag "--branch=",
description: "Branch to use the workflow from (default: `master`)."
flag "--message=",
depends_on: "--autosquash",
description: "Message to include when autosquashing revision bumps, deletions and rebuilds."
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--workflow=",
description: "Target workflow filename (default: `publish-commit-bottles.yml`)."
sig { returns(CLI::Parser) } named_args :pull_request, min: 1
def pr_publish_args
Homebrew::CLI::Parser.new do
description <<~EOS
Publish bottles for a pull request with GitHub Actions.
Requires write access to the repository.
EOS
switch "--autosquash",
description: "If supported on the target tap, automatically reformat and reword commits " \
"to our preferred format."
switch "--large-runner",
description: "Run the upload job on a large runner."
flag "--branch=",
description: "Branch to use the workflow from (default: `master`)."
flag "--message=",
depends_on: "--autosquash",
description: "Message to include when autosquashing revision bumps, deletions and rebuilds."
flag "--tap=",
description: "Target tap repository (default: `homebrew/core`)."
flag "--workflow=",
description: "Target workflow filename (default: `publish-commit-bottles.yml`)."
named_args :pull_request, min: 1
end
end
def pr_publish
args = pr_publish_args.parse
tap = Tap.fetch(args.tap || CoreTap.instance.name)
workflow = args.workflow || "publish-commit-bottles.yml"
ref = args.branch || "master"
inputs = {
autosquash: args.autosquash?,
large_runner: args.large_runner?,
}
inputs[:message] = args.message if args.message.presence
args.named.uniq.each do |arg|
arg = "#{tap.default_remote}/pull/#{arg}" if arg.to_i.positive?
url_match = arg.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX
_, user, repo, issue = *url_match
odie "Not a GitHub pull request: #{arg}" unless issue
inputs[:pull_request] = issue
pr_labels = GitHub.pull_request_labels(user, repo, issue)
if pr_labels.include?("autosquash")
oh1 "Found `autosquash` label on ##{issue}. Requesting autosquash."
inputs[:autosquash] = true
end
if pr_labels.include?("large-bottle-upload")
oh1 "Found `large-bottle-upload` label on ##{issue}. Requesting upload on large runner."
inputs[:large_runner] = true
end end
if args.tap.present? && !T.must("#{user}/#{repo}".casecmp(tap.full_name)).zero? sig { override.void }
odie "Pull request URL is for #{user}/#{repo} but `--tap=#{tap.full_name}` was specified!" def run
end tap = Tap.fetch(args.tap || CoreTap.instance.name)
workflow = args.workflow || "publish-commit-bottles.yml"
ref = args.branch || "master"
ohai "Dispatching #{tap} pull request ##{issue}" inputs = {
GitHub.workflow_dispatch_event(user, repo, workflow, ref, **inputs) autosquash: args.autosquash?,
large_runner: args.large_runner?,
}
inputs[:message] = args.message if args.message.presence
args.named.uniq.each do |arg|
arg = "#{tap.default_remote}/pull/#{arg}" if arg.to_i.positive?
url_match = arg.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX
_, user, repo, issue = *url_match
odie "Not a GitHub pull request: #{arg}" unless issue
inputs[:pull_request] = issue
pr_labels = GitHub.pull_request_labels(user, repo, issue)
if pr_labels.include?("autosquash")
oh1 "Found `autosquash` label on ##{issue}. Requesting autosquash."
inputs[:autosquash] = true
end
if pr_labels.include?("large-bottle-upload")
oh1 "Found `large-bottle-upload` label on ##{issue}. Requesting upload on large runner."
inputs[:large_runner] = true
end
if args.tap.present? && !T.must("#{user}/#{repo}".casecmp(tap.full_name)).zero?
odie "Pull request URL is for #{user}/#{repo} but `--tap=#{tap.full_name}` was specified!"
end
ohai "Dispatching #{tap} pull request ##{issue}"
GitHub.workflow_dispatch_event(user, repo, workflow, ref, **inputs)
end
end
end end
end end
end end

View File

@ -39,6 +39,11 @@ RSpec.describe Homebrew::AbstractCommand do
expect(described_class.command("test-cat")).to be(TestCat) expect(described_class.command("test-cat")).to be(TestCat)
end end
it "removes -cmd suffix from command name" do
require "dev-cmd/formula"
expect(Homebrew::DevCmd::FormulaCmd.command_name).to eq("formula")
end
describe "when command name is overridden" do describe "when command name is overridden" do
before do before do
tac = Class.new(described_class) do tac = Class.new(described_class) do
@ -61,7 +66,7 @@ RSpec.describe Homebrew::AbstractCommand do
["cmd", "dev-cmd"].each do |dir| ["cmd", "dev-cmd"].each do |dir|
Dir[File.join(__dir__, "../#{dir}", "*.rb")].each { require(_1) } Dir[File.join(__dir__, "../#{dir}", "*.rb")].each { require(_1) }
end end
test_classes = ["Cat", "Tac"] test_classes = ["TestCat", "Tac"]
described_class.subclasses.each do |klass| described_class.subclasses.each do |klass|
next if test_classes.include?(klass.name) next if test_classes.include?(klass.name)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples "parseable arguments" do |argv: []| RSpec.shared_examples "parseable arguments" do |argv: nil|
subject(:method_name) { "#{command_name.tr("-", "_")}_args" } subject(:method_name) { "#{command_name.tr("-", "_")}_args" }
let(:command_name) do |example| let(:command_name) do |example|
@ -9,6 +9,8 @@ RSpec.shared_examples "parseable arguments" do |argv: []|
it "can parse arguments" do it "can parse arguments" do
if described_class if described_class
argv ||= described_class.parser.instance_variable_get(:@min_named_args)&.times&.map { "argument" }
argv ||= []
cmd = described_class.new(argv) cmd = described_class.new(argv)
expect(cmd.args).to be_a Homebrew::CLI::Args expect(cmd.args).to be_a Homebrew::CLI::Args
else else

View File

@ -30,7 +30,7 @@ RSpec.describe Homebrew::DevCmd::Bottle do
EOS EOS
end end
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
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

View File

@ -4,5 +4,5 @@ require "cmd/shared_examples/args_parse"
require "dev-cmd/bump-cask-pr" require "dev-cmd/bump-cask-pr"
RSpec.describe Homebrew::DevCmd::BumpCaskPr do RSpec.describe Homebrew::DevCmd::BumpCaskPr do
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
end end

View File

@ -4,5 +4,5 @@ require "cmd/shared_examples/args_parse"
require "dev-cmd/bump-revision" require "dev-cmd/bump-revision"
RSpec.describe Homebrew::DevCmd::BumpRevision do RSpec.describe Homebrew::DevCmd::BumpRevision do
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
end end

View File

@ -4,5 +4,5 @@ require "cmd/shared_examples/args_parse"
require "dev-cmd/bump-unversioned-casks" require "dev-cmd/bump-unversioned-casks"
RSpec.describe Homebrew::DevCmd::BumpUnversionedCasks do RSpec.describe Homebrew::DevCmd::BumpUnversionedCasks do
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
end end

View File

@ -4,7 +4,7 @@ require "cmd/shared_examples/args_parse"
require "dev-cmd/cat" require "dev-cmd/cat"
RSpec.describe Homebrew::DevCmd::Cat do RSpec.describe Homebrew::DevCmd::Cat do
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
it "prints the content of a given Formula", :integration_test do it "prints the content of a given Formula", :integration_test do
formula_file = setup_test_formula "testball" formula_file = setup_test_formula "testball"

View File

@ -4,7 +4,7 @@ require "cmd/shared_examples/args_parse"
require "dev-cmd/command" require "dev-cmd/command"
RSpec.describe Homebrew::DevCmd::Command do RSpec.describe Homebrew::DevCmd::Command do
it_behaves_like "parseable arguments", argv: ["foo"] it_behaves_like "parseable arguments"
it "returns the file for a given command", :integration_test do it "returns the file for a given command", :integration_test do
expect { brew "command", "info" } expect { brew "command", "info" }

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/create"
RSpec.describe "brew create" do RSpec.describe Homebrew::DevCmd::Create do
let(:url) { "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz" } let(:url) { "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz" }
let(:formula_file) { CoreTap.instance.new_formula_path("testball") } let(:formula_file) { CoreTap.instance.new_formula_path("testball") }

View File

@ -3,7 +3,7 @@
require "dev-cmd/determine-test-runners" require "dev-cmd/determine-test-runners"
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
RSpec.describe "brew determine-test-runners" do RSpec.describe Homebrew::DevCmd::DetermineTestRunners do
def get_runners(file) def get_runners(file)
runner_line = File.open(file).first runner_line = File.open(file).first
json_text = runner_line[/runners=(.*)/, 1] json_text = runner_line[/runners=(.*)/, 1]

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/dispatch-build-bottle"
RSpec.describe "brew dispatch-build-bottle" do RSpec.describe Homebrew::DevCmd::DispatchBuildBottle do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/edit"
RSpec.describe "brew edit" do RSpec.describe Homebrew::DevCmd::Edit do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "opens a given Formula in an editor", :integration_test do it "opens a given Formula in an editor", :integration_test do

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/extract"
RSpec.describe "brew extract" do RSpec.describe Homebrew::DevCmd::Extract do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
context "when extracting a formula" do context "when extracting a formula" do

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/formula"
RSpec.describe "brew formula" do RSpec.describe Homebrew::DevCmd::FormulaCmd do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "prints a given Formula's path", :integration_test do it "prints a given Formula's path", :integration_test do

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/generate-cask-api"
RSpec.describe "brew generate-cask-api" do RSpec.describe Homebrew::DevCmd::GenerateCaskApi do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/generate-formula-api"
RSpec.describe "brew generate-formula-api" do RSpec.describe Homebrew::DevCmd::GenerateFormulaApi do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/generate-man-completions"
RSpec.describe "brew generate-man-completions" do RSpec.describe Homebrew::DevCmd::GenerateManCompletions do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
require "cmd/shared_examples/args_parse"
require "dev-cmd/install-bundler-gems"
RSpec.describe Homebrew::DevCmd::InstallBundlerGems do
it_behaves_like "parseable arguments"
end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/irb"
RSpec.describe "brew irb" do RSpec.describe Homebrew::DevCmd::Irb do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
describe "integration test" do describe "integration test" do

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/linkage"
RSpec.describe "brew linkage" do RSpec.describe Homebrew::DevCmd::Linkage do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "works when no arguments are provided", :integration_test do it "works when no arguments are provided", :integration_test do

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/livecheck"
RSpec.describe "brew livecheck" do RSpec.describe Homebrew::DevCmd::LivecheckCmd do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "reports the latest version of a Formula", :integration_test, :needs_network do it "reports the latest version of a Formula", :integration_test, :needs_network do

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/pr-automerge"
RSpec.describe "brew pr-automerge" do RSpec.describe Homebrew::DevCmd::PrAutomerge do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "dev-cmd/pr-publish"
RSpec.describe "brew pr-publish" do RSpec.describe Homebrew::DevCmd::PrPublish do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
end end