Issy Long 49b9b6cf3f
dev-cmd/extract: Improve the usage instructions
- A friend got an error message when trying to use `brew extract` and it
  wasn't immediately obvious to me why. The usage banner only mentioned
  the "formula" argument, which they'd provided. This improves the error
  message when there aren't enough arguments so that others have a
  chance of figuring out how to use this command without having to look
  at the code for `extract_args`.

Before:

```
➜ brew extract --version='1.4' libftdi
Usage: brew extract [--version=] [--force] formula ...
[...]
Error: Invalid usage: this command requires a formula argument
```

After:

```
➜ brew extract --version='1.4' libftdi
Usage: brew extract [options] formula tap
[...]
Error: Invalid usage: This command requires at least 2 named arguments.
```

- I don't like the "at least 2" phrasing here but that's a dive into the
  arg parsing code that I don't have time for right now. An alternative
  was `named_args [:formula, :destination_tap]`, but that gave the error
  message "requires formula or destination_tap" which wasn't great
  either. I also tried `min: 2, max: 2` and that was the same "at least
  2" message.
2021-01-23 20:01:11 +00:00

231 lines
7.9 KiB
Ruby

# typed: false
# frozen_string_literal: true
require "cli/parser"
require "utils/git"
require "formulary"
require "software_spec"
require "tap"
def with_monkey_patch
BottleSpecification.class_eval do
alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) { |*| }
end
Module.class_eval do
alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) { |*| }
end
Resource.class_eval do
alias_method :old_method_missing, :method_missing if method_defined?(:method_missing)
define_method(:method_missing) { |*| }
end
DependencyCollector.class_eval do
alias_method :old_parse_symbol_spec, :parse_symbol_spec if method_defined?(:parse_symbol_spec)
define_method(:parse_symbol_spec) { |*| }
end
if defined?(DependencyCollector::Compat)
DependencyCollector::Compat.class_eval do
alias_method :old_parse_string_spec, :parse_string_spec if method_defined?(:parse_string_spec)
define_method(:parse_string_spec) { |*| }
end
end
yield
ensure
BottleSpecification.class_eval do
if method_defined?(:old_method_missing)
alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
Module.class_eval do
if method_defined?(:old_method_missing)
alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
Resource.class_eval do
if method_defined?(:old_method_missing)
alias_method :method_missing, :old_method_missing
undef :old_method_missing
end
end
DependencyCollector.class_eval do
if method_defined?(:old_parse_symbol_spec)
alias_method :parse_symbol_spec, :old_parse_symbol_spec
undef :old_parse_symbol_spec
end
end
if defined?(DependencyCollector::Compat)
DependencyCollector::Compat.class_eval do
if method_defined?(:old_parse_string_spec)
alias_method :parse_string_spec, :old_parse_string_spec
undef :old_parse_string_spec
end
end
end
end
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def extract_args
Homebrew::CLI::Parser.new do
usage_banner "`extract` [<--version>`=`] [<--force>] <formula> <tap>"
description <<~EOS
Look through repository history to find the most recent version of <formula> and
create a copy in <tap>`/Formula/`<formula>`@`<version>`.rb`. If the tap is not
installed yet, attempt to install/clone the tap before continuing. To extract
a formula from a tap that is not `homebrew/core` use its fully-qualified form of
<user>`/`<repo>`/`<formula>.
EOS
flag "--version=",
description: "Extract the specified <version> of <formula> instead of the most recent."
switch "-f", "--force",
description: "Overwrite the destination formula if it already exists."
named_args number: 2
end
end
def extract
args = extract_args.parse
if (match = args.named.first.match(HOMEBREW_TAP_FORMULA_REGEX))
name = match[3].downcase
source_tap = Tap.fetch(match[1], match[2])
raise TapFormulaUnavailableError.new(source_tap, name) unless source_tap.installed?
else
name = args.named.first.downcase
source_tap = CoreTap.instance
end
destination_tap = Tap.fetch(args.named.second)
unless Homebrew::EnvConfig.developer?
odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap?
odie "Cannot extract formula to the same tap!" if destination_tap == source_tap
end
destination_tap.install unless destination_tap.installed?
repo = source_tap.path
pattern = if source_tap.core_tap?
[repo/"Formula/#{name}.rb"]
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 = nil
test_formula = nil
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
file = repo/path
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
test_formula = formula_at_revision(repo, name, file, rev)
break if test_formula.nil? || test_formula.version == version
if version_segments && Gem::Version.correct?(test_formula.version)
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/"{,**/}"].map do |dir|
Pathname.glob(["#{dir}/#{name}.rb"]).find(&:file?)
end.compact
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 = formula_at_revision(repo, name, file, rev).version
result = Utils::Git.last_revision_of_file(repo, file)
else
file = files.first.realpath
rev = "HEAD"
version = Formulary.factory(file).version
result = File.read(file)
end
end
# The class name has to be renamed to match the new filename,
# e.g. Foo version 1.2.3 becomes FooAT123 and resides in Foo@1.2.3.rb.
class_name = Formulary.class_s(name)
# Remove any existing version suffixes, as a new one will be added later
name.sub!(/\b@(.*)\z\b/i, "")
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 do.+?end\n\n/m, "") if destination_tap != source_tap
path = destination_tap.path/"Formula/#{name}@#{version}.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:"
puts path
path.write result
end
# @private
def formula_at_revision(repo, name, file, rev)
return if rev.empty?
contents = Utils::Git.last_revision_of_file(repo, file, before_commit: rev)
contents.gsub!("@url=", "url ")
contents.gsub!("require 'brewkit'", "require 'formula'")
with_monkey_patch { Formulary.from_contents(name, file, contents) }
end
end