diff --git a/Library/Homebrew/debrew.rb b/Library/Homebrew/debrew.rb index 82407ecbc2..670446d3d2 100644 --- a/Library/Homebrew/debrew.rb +++ b/Library/Homebrew/debrew.rb @@ -3,10 +3,7 @@ require "mutex_m" require "debrew/irb" -require "warnings" -Warnings.ignore(/warning: callcc is obsolete; use Fiber instead/) do - require "continuation" -end +require "ignorable" # Helper module for debugging formulae. # @@ -14,31 +11,6 @@ end module Debrew 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 Formula def install @@ -106,28 +78,28 @@ module Debrew class << self extend Predicable - alias original_raise raise attr_predicate :active? attr_reader :debugged_exceptions end def self.debrew @active = true - Object.include Raise + Ignorable.hook_raise begin yield rescue SystemExit - original_raise + raise rescue Exception => e # rubocop:disable Lint/RescueException e.ignore if debug(e) == :ignore # execution jumps back to where the exception was thrown ensure + Ignorable.unhook_raise @active = false end end 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 puts e.backtrace.first.to_s @@ -137,15 +109,15 @@ module Debrew Menu.choose do |menu| menu.prompt = "Choose an action: " - menu.choice(:raise) { original_raise(e) } - menu.choice(:ignore) { return :ignore } if e.is_a?(Ignorable) + menu.choice(:raise) { raise(e) } + menu.choice(:ignore) { return :ignore } if e.is_a?(Ignorable::ExceptionMixin) menu.choice(:backtrace) { puts e.backtrace } - if e.is_a?(Ignorable) + if e.is_a?(Ignorable::ExceptionMixin) menu.choice(:irb) do puts "When you exit this IRB session, execution will continue." 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) synchronize { IRB.start_within(binding) } end diff --git a/Library/Homebrew/debrew/irb.rb b/Library/Homebrew/debrew/irb.rb index 6c2d285c13..af3c358c9d 100644 --- a/Library/Homebrew/debrew/irb.rb +++ b/Library/Homebrew/debrew/irb.rb @@ -5,8 +5,6 @@ require "irb" # @private module IRB - def self.parse_opts(argv: nil); end - def self.start_within(binding) unless @setup_done setup(nil, argv: []) @@ -19,7 +17,7 @@ module IRB @CONF[:IRB_RC]&.call(irb.context) @CONF[:MAIN_CONTEXT] = irb.context - trap("SIGINT") do + prev_trap = trap("SIGINT") do irb.signal_handle end @@ -28,6 +26,7 @@ module IRB irb.eval_input end ensure + trap("SIGINT", prev_trap) irb_at_exit end end diff --git a/Library/Homebrew/formula_versions.rb b/Library/Homebrew/formula_versions.rb index 5bb2fb4494..0701d643bd 100644 --- a/Library/Homebrew/formula_versions.rb +++ b/Library/Homebrew/formula_versions.rb @@ -45,7 +45,7 @@ class FormulaVersions yield @formula_at_revision[rev] ||= begin contents = file_contents_at_revision(rev) - nostdout { Formulary.from_contents(name, path, contents) } + nostdout { Formulary.from_contents(name, path, contents, ignore_errors: true) } end rescue *IGNORED_EXCEPTIONS => e # We rescue these so that we can skip bad versions and diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 2edac30e5b..9d38753f6f 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -47,24 +47,35 @@ module Formulary super 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? require "formula" + require "ignorable" mod = Module.new remove_const(namespace) if const_defined?(namespace) const_set(namespace, mod) - begin + eval_formula = lambda do # Set `BUILD_FLAGS` in the formula's namespace so we can # access them from within the formula's class scope. mod.const_set(:BUILD_FLAGS, flags) mod.module_eval(contents, path) rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersionError => e - remove_const(namespace) - raise FormulaUnreadableError.new(name, e) + if e.is_a?(Ignorable::ExceptionMixin) + e.ignore + else + remove_const(namespace) + raise FormulaUnreadableError.new(name, e) + end end + if ignore_errors + Ignorable.hook_raise(&eval_formula) + else + eval_formula.call + end + class_name = class_s(name) begin @@ -79,10 +90,10 @@ module Formulary 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 } 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 end @@ -150,23 +161,24 @@ module Formulary # Gets the formula instance. # `alias_path` can be overridden here in case an alias was used to refer to # 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 - 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 - def klass(flags:) - load_file(flags: flags) unless Formulary.formula_class_defined?(path) + def klass(flags:, ignore_errors:) + load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined?(path) Formulary.formula_class_get(path) end private - def load_file(flags:) + def load_file(flags:, ignore_errors:) $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? 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 @@ -191,10 +203,11 @@ module Formulary super name, Formulary.path(full_name) end - def get_formula(spec, force_bottle: false, flags: [], **) + def get_formula(spec, force_bottle: false, flags: [], ignore_errors: false, **) formula = begin 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 opoo <<~EOS Unreadable formula in #{@bottle_filename}: @@ -245,7 +258,7 @@ module Formulary super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path) end - def load_file(flags:) + def load_file(flags:, ignore_errors:) if %r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?[\w+-.@]+).rb} =~ url raise UsageError, "Installation of #{formula_name} from a GitHub commit URL is unsupported! " \ "`brew extract #{formula_name}` to a stable tap on GitHub instead." @@ -309,7 +322,7 @@ module Formulary [name, path] 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 rescue FormulaUnreadableError => e raise TapFormulaUnreadableError.new(tap, name, e.formula_error), "", e.backtrace @@ -319,7 +332,7 @@ module Formulary raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace end - def load_file(flags:) + def load_file(flags:, ignore_errors:) super rescue MethodDeprecatedError => e e.issues_url = tap.issues_url || tap.to_s @@ -348,10 +361,10 @@ module Formulary super name, path end - def klass(flags:) + def klass(flags:, ignore_errors:) $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? 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 @@ -362,7 +375,10 @@ module Formulary # * a formula pathname # * a formula URL # * 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 cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}" @@ -372,7 +388,8 @@ module Formulary end 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? cache[:formulary_factory] ||= {} cache[:formulary_factory][cache_key] ||= formula @@ -433,9 +450,13 @@ module Formulary end # 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) - .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 def self.to_rack(ref) diff --git a/Library/Homebrew/ignorable.rb b/Library/Homebrew/ignorable.rb new file mode 100644 index 0000000000..4ede76f392 --- /dev/null +++ b/Library/Homebrew/ignorable.rb @@ -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