Merge pull request #11134 from Bo98/ignorable-formulary

Introduce Ignorable module and provide the facility to try and ignore errors when loading historical formulae
This commit is contained in:
Bo Anderson 2021-04-29 19:39:55 +01:00 committed by GitHub
commit 33711461f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 114 additions and 64 deletions

View File

@ -3,10 +3,7 @@
require "mutex_m" require "mutex_m"
require "debrew/irb" require "debrew/irb"
require "warnings" require "ignorable"
Warnings.ignore(/warning: callcc is obsolete; use Fiber instead/) do
require "continuation"
end
# Helper module for debugging formulae. # Helper module for debugging formulae.
# #
@ -14,31 +11,6 @@ end
module Debrew module Debrew
extend Mutex_m extend Mutex_m
# Marks exceptions which can be ignored and provides
# the ability to jump back to where it was raised.
module Ignorable
attr_accessor :continuation
def ignore
continuation.call
end
end
# Module for allowing to ignore exceptions.
module Raise
def raise(*)
callcc do |continuation|
super
rescue Exception => e # rubocop:disable Lint/RescueException
e.extend(Ignorable)
e.continuation = continuation
super(e)
end
end
alias fail raise
end
# Module for allowing to debug formulae. # Module for allowing to debug formulae.
module Formula module Formula
def install def install
@ -106,28 +78,28 @@ module Debrew
class << self class << self
extend Predicable extend Predicable
alias original_raise raise
attr_predicate :active? attr_predicate :active?
attr_reader :debugged_exceptions attr_reader :debugged_exceptions
end end
def self.debrew def self.debrew
@active = true @active = true
Object.include Raise Ignorable.hook_raise
begin begin
yield yield
rescue SystemExit rescue SystemExit
original_raise raise
rescue Exception => e # rubocop:disable Lint/RescueException rescue Exception => e # rubocop:disable Lint/RescueException
e.ignore if debug(e) == :ignore # execution jumps back to where the exception was thrown e.ignore if debug(e) == :ignore # execution jumps back to where the exception was thrown
ensure ensure
Ignorable.unhook_raise
@active = false @active = false
end end
end end
def self.debug(e) def self.debug(e)
original_raise(e) if !active? || !debugged_exceptions.add?(e) || !try_lock raise(e) if !active? || !debugged_exceptions.add?(e) || !try_lock
begin begin
puts e.backtrace.first.to_s puts e.backtrace.first.to_s
@ -137,15 +109,15 @@ module Debrew
Menu.choose do |menu| Menu.choose do |menu|
menu.prompt = "Choose an action: " menu.prompt = "Choose an action: "
menu.choice(:raise) { original_raise(e) } menu.choice(:raise) { raise(e) }
menu.choice(:ignore) { return :ignore } if e.is_a?(Ignorable) menu.choice(:ignore) { return :ignore } if e.is_a?(Ignorable::ExceptionMixin)
menu.choice(:backtrace) { puts e.backtrace } menu.choice(:backtrace) { puts e.backtrace }
if e.is_a?(Ignorable) if e.is_a?(Ignorable::ExceptionMixin)
menu.choice(:irb) do menu.choice(:irb) do
puts "When you exit this IRB session, execution will continue." puts "When you exit this IRB session, execution will continue."
set_trace_func proc { |event, _, _, id, binding, klass| set_trace_func proc { |event, _, _, id, binding, klass|
if klass == Raise && id == :raise && event == "return" if klass == Object && id == :raise && event == "return"
set_trace_func(nil) set_trace_func(nil)
synchronize { IRB.start_within(binding) } synchronize { IRB.start_within(binding) }
end end

View File

@ -5,8 +5,6 @@ require "irb"
# @private # @private
module IRB module IRB
def self.parse_opts(argv: nil); end
def self.start_within(binding) def self.start_within(binding)
unless @setup_done unless @setup_done
setup(nil, argv: []) setup(nil, argv: [])
@ -19,7 +17,7 @@ module IRB
@CONF[:IRB_RC]&.call(irb.context) @CONF[:IRB_RC]&.call(irb.context)
@CONF[:MAIN_CONTEXT] = irb.context @CONF[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do prev_trap = trap("SIGINT") do
irb.signal_handle irb.signal_handle
end end
@ -28,6 +26,7 @@ module IRB
irb.eval_input irb.eval_input
end end
ensure ensure
trap("SIGINT", prev_trap)
irb_at_exit irb_at_exit
end end
end end

View File

@ -45,7 +45,7 @@ class FormulaVersions
yield @formula_at_revision[rev] ||= begin yield @formula_at_revision[rev] ||= begin
contents = file_contents_at_revision(rev) contents = file_contents_at_revision(rev)
nostdout { Formulary.from_contents(name, path, contents) } nostdout { Formulary.from_contents(name, path, contents, ignore_errors: true) }
end end
rescue *IGNORED_EXCEPTIONS => e rescue *IGNORED_EXCEPTIONS => e
# We rescue these so that we can skip bad versions and # We rescue these so that we can skip bad versions and

View File

@ -47,24 +47,35 @@ module Formulary
super super
end end
def self.load_formula(name, path, contents, namespace, flags:) def self.load_formula(name, path, contents, namespace, flags:, ignore_errors:)
raise "Formula loading disabled by HOMEBREW_DISABLE_LOAD_FORMULA!" if Homebrew::EnvConfig.disable_load_formula? raise "Formula loading disabled by HOMEBREW_DISABLE_LOAD_FORMULA!" if Homebrew::EnvConfig.disable_load_formula?
require "formula" require "formula"
require "ignorable"
mod = Module.new mod = Module.new
remove_const(namespace) if const_defined?(namespace) remove_const(namespace) if const_defined?(namespace)
const_set(namespace, mod) const_set(namespace, mod)
begin eval_formula = lambda do
# Set `BUILD_FLAGS` in the formula's namespace so we can # Set `BUILD_FLAGS` in the formula's namespace so we can
# access them from within the formula's class scope. # access them from within the formula's class scope.
mod.const_set(:BUILD_FLAGS, flags) mod.const_set(:BUILD_FLAGS, flags)
mod.module_eval(contents, path) mod.module_eval(contents, path)
rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersionError => e rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersionError => e
remove_const(namespace) if e.is_a?(Ignorable::ExceptionMixin)
raise FormulaUnreadableError.new(name, e) e.ignore
else
remove_const(namespace)
raise FormulaUnreadableError.new(name, e)
end
end end
if ignore_errors
Ignorable.hook_raise(&eval_formula)
else
eval_formula.call
end
class_name = class_s(name) class_name = class_s(name)
begin begin
@ -79,10 +90,10 @@ module Formulary
end end
end end
def self.load_formula_from_path(name, path, flags:) def self.load_formula_from_path(name, path, flags:, ignore_errors:)
contents = path.open("r") { |f| ensure_utf8_encoding(f).read } contents = path.open("r") { |f| ensure_utf8_encoding(f).read }
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}" namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}"
klass = load_formula(name, path, contents, namespace, flags: flags) klass = load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors)
cache[path] = klass cache[path] = klass
end end
@ -150,23 +161,24 @@ module Formulary
# Gets the formula instance. # Gets the formula instance.
# `alias_path` can be overridden here in case an alias was used to refer to # `alias_path` can be overridden here in case an alias was used to refer to
# a formula that was loaded in another way. # a formula that was loaded in another way.
def get_formula(spec, alias_path: nil, force_bottle: false, flags: []) def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
alias_path ||= self.alias_path alias_path ||= self.alias_path
klass(flags: flags).new(name, path, spec, alias_path: alias_path, force_bottle: force_bottle) klass(flags: flags, ignore_errors: ignore_errors)
.new(name, path, spec, alias_path: alias_path, force_bottle: force_bottle)
end end
def klass(flags:) def klass(flags:, ignore_errors:)
load_file(flags: flags) unless Formulary.formula_class_defined?(path) load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined?(path)
Formulary.formula_class_get(path) Formulary.formula_class_get(path)
end end
private private
def load_file(flags:) def load_file(flags:, ignore_errors:)
$stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug?
raise FormulaUnavailableError, name unless path.file? raise FormulaUnavailableError, name unless path.file?
Formulary.load_formula_from_path(name, path, flags: flags) Formulary.load_formula_from_path(name, path, flags: flags, ignore_errors: ignore_errors)
end end
end end
@ -191,10 +203,11 @@ module Formulary
super name, Formulary.path(full_name) super name, Formulary.path(full_name)
end end
def get_formula(spec, force_bottle: false, flags: [], **) def get_formula(spec, force_bottle: false, flags: [], ignore_errors: false, **)
formula = begin formula = begin
contents = Utils::Bottles.formula_contents @bottle_filename, name: name contents = Utils::Bottles.formula_contents @bottle_filename, name: name
Formulary.from_contents(name, path, contents, spec, force_bottle: force_bottle, flags: flags) Formulary.from_contents(name, path, contents, spec, force_bottle: force_bottle,
flags: flags, ignore_errors: ignore_errors)
rescue FormulaUnreadableError => e rescue FormulaUnreadableError => e
opoo <<~EOS opoo <<~EOS
Unreadable formula in #{@bottle_filename}: Unreadable formula in #{@bottle_filename}:
@ -245,7 +258,7 @@ module Formulary
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path) super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path)
end end
def load_file(flags:) def load_file(flags:, ignore_errors:)
if %r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<formula_name>[\w+-.@]+).rb} =~ url if %r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?<formula_name>[\w+-.@]+).rb} =~ url
raise UsageError, "Installation of #{formula_name} from a GitHub commit URL is unsupported! " \ raise UsageError, "Installation of #{formula_name} from a GitHub commit URL is unsupported! " \
"`brew extract #{formula_name}` to a stable tap on GitHub instead." "`brew extract #{formula_name}` to a stable tap on GitHub instead."
@ -309,7 +322,7 @@ module Formulary
[name, path] [name, path]
end end
def get_formula(spec, alias_path: nil, force_bottle: false, flags: []) def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
super super
rescue FormulaUnreadableError => e rescue FormulaUnreadableError => e
raise TapFormulaUnreadableError.new(tap, name, e.formula_error), "", e.backtrace raise TapFormulaUnreadableError.new(tap, name, e.formula_error), "", e.backtrace
@ -319,7 +332,7 @@ module Formulary
raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace
end end
def load_file(flags:) def load_file(flags:, ignore_errors:)
super super
rescue MethodDeprecatedError => e rescue MethodDeprecatedError => e
e.issues_url = tap.issues_url || tap.to_s e.issues_url = tap.issues_url || tap.to_s
@ -348,10 +361,10 @@ module Formulary
super name, path super name, path
end end
def klass(flags:) def klass(flags:, ignore_errors:)
$stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug?
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}" namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}"
Formulary.load_formula(name, path, contents, namespace, flags: flags) Formulary.load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors)
end end
end end
@ -362,7 +375,10 @@ module Formulary
# * a formula pathname # * a formula pathname
# * a formula URL # * a formula URL
# * a local bottle reference # * a local bottle reference
def self.factory(ref, spec = :stable, alias_path: nil, from: nil, force_bottle: false, flags: []) def self.factory(
ref, spec = :stable, alias_path: nil, from: nil,
force_bottle: false, flags: [], ignore_errors: false
)
raise ArgumentError, "Formulae must have a ref!" unless ref raise ArgumentError, "Formulae must have a ref!" unless ref
cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}" cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}"
@ -372,7 +388,8 @@ module Formulary
end end
formula = loader_for(ref, from: from).get_formula(spec, alias_path: alias_path, formula = loader_for(ref, from: from).get_formula(spec, alias_path: alias_path,
force_bottle: force_bottle, flags: flags) force_bottle: force_bottle, flags: flags,
ignore_errors: ignore_errors)
if factory_cached? if factory_cached?
cache[:formulary_factory] ||= {} cache[:formulary_factory] ||= {}
cache[:formulary_factory][cache_key] ||= formula cache[:formulary_factory][cache_key] ||= formula
@ -433,9 +450,13 @@ module Formulary
end end
# Return a {Formula} instance directly from contents. # Return a {Formula} instance directly from contents.
def self.from_contents(name, path, contents, spec = :stable, alias_path: nil, force_bottle: false, flags: []) def self.from_contents(
name, path, contents, spec = :stable, alias_path: nil,
force_bottle: false, flags: [], ignore_errors: false
)
FormulaContentsLoader.new(name, path, contents) FormulaContentsLoader.new(name, path, contents)
.get_formula(spec, alias_path: alias_path, force_bottle: force_bottle, flags: flags) .get_formula(spec, alias_path: alias_path, force_bottle: force_bottle,
flags: flags, ignore_errors: ignore_errors)
end end
def self.to_rack(ref) def self.to_rack(ref)

View File

@ -0,0 +1,58 @@
# typed: false
# frozen_string_literal: true
require "warnings"
Warnings.ignore(/warning: callcc is obsolete; use Fiber instead/) do
require "continuation"
end
# Provides the ability to optionally ignore errors raised and continue execution.
#
# @api private
module Ignorable
# Marks exceptions which can be ignored and provides
# the ability to jump back to where it was raised.
module ExceptionMixin
attr_accessor :continuation
def ignore
continuation.call
end
end
def self.hook_raise
Object.class_eval do
alias_method :original_raise, :raise
def raise(*)
callcc do |continuation|
super
rescue Exception => e # rubocop:disable Lint/RescueException
unless e.is_a?(ScriptError)
e.extend(ExceptionMixin)
e.continuation = continuation
end
super(e)
end
end
alias_method :fail, :raise
end
return unless block_given?
yield
unhook_raise
end
def self.unhook_raise
Object.class_eval do
# False positive - https://github.com/rubocop/rubocop/issues/5022
# rubocop:disable Lint/DuplicateMethods
alias_method :raise, :original_raise
alias_method :fail, :original_raise
# rubocop:enable Lint/DuplicateMethods
undef :original_raise
end
end
end