diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index 4dfaf9e7e045e..a0d453e50f6fe 100644 --- a/Library/Homebrew/build.rb +++ b/Library/Homebrew/build.rb @@ -11,6 +11,8 @@ require "debrew" require "fcntl" require "socket" +require "json" +require "json/add/core" class Build attr_reader :formula, :deps, :reqs @@ -190,7 +192,17 @@ def fixopt(f) build = Build.new(formula, options) build.install rescue Exception => e # rubocop:disable Lint/RescueException - Marshal.dump(e, error_pipe) + error_hash = JSON.parse e.to_json + + # Special case: We need to toss our build state into the error hash + # for proper analytics reporting and sensible error messages. + if e.is_a?(BuildError) + error_hash["cmd"] = e.cmd + error_hash["args"] = e.args + error_hash["env"] = e.env + end + + error_pipe.write error_hash.to_json error_pipe.close exit! 1 end diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index d21a62cde5adb..310ab94a6c691 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -93,12 +93,14 @@ def test exec(*args) end end - rescue ::Test::Unit::AssertionFailedError => e + rescue ChildProcessError => e ofail "#{f.full_name}: failed" - puts e.message - rescue Exception => e # rubocop:disable Lint/RescueException - ofail "#{f.full_name}: failed" - puts e, e.backtrace + case e.inner["json_class"] + when "Test::Unit::AssertionFailedError" + puts e.inner["m"] + else + puts e.inner["json_class"], e.backtrace + end ensure ENV.replace(env) end diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index dc3f5f9e363dc..ab6f5c6724a67 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -352,14 +352,16 @@ def initialize(formula) end class BuildError < RuntimeError - attr_reader :formula, :env + attr_reader :formula, :cmd, :args, :env attr_accessor :options def initialize(formula, cmd, args, env) @formula = formula + @cmd = cmd + @args = args @env = env - args = args.map { |arg| arg.to_s.gsub " ", "\\ " }.join(" ") - super "Failed executing: #{cmd} #{args}" + pretty_args = args.map { |arg| arg.to_s.gsub " ", "\\ " }.join(" ") + super "Failed executing: #{cmd} #{pretty_args}" end def issues @@ -596,3 +598,20 @@ def initialize(bottle_path, formula_path) EOS end end + +# Raised when a child process sends us an exception over its error pipe. +class ChildProcessError < RuntimeError + attr_reader :inner + + def initialize(inner) + @inner = inner + + super <<~EOS + An exception occured within a build process: + #{inner["json_class"]}: #{inner["m"]} + EOS + + # Clobber our real (but irrelevant) backtrace with that of the inner exception. + set_backtrace inner["b"] + end +end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 66a44a9cde19f..f0c834500f103 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -763,14 +763,25 @@ def build raise "Empty installation" end rescue Exception => e # rubocop:disable Lint/RescueException - e.options = display_options(formula) if e.is_a?(BuildError) + # If we've rescued a ChildProcessError and that ChildProcessError + # contains a BuildError, then we reconstruct the inner build error + # to make analytics happy. + if e.is_a?(ChildProcessError) && e.inner["json_class"] == "BuildError" + build_error = BuildError.new(formula, e["cmd"], e["args"], e["env"]) + build_error.set_backtrace e.backtrace + build_error.options = display_options(formula) + + e = build_error + end + ignore_interrupts do # any exceptions must leave us with nothing installed formula.update_head_version formula.prefix.rmtree if formula.prefix.directory? formula.rack.rmdir_if_possible end - raise + + raise e end def link(keg) diff --git a/Library/Homebrew/postinstall.rb b/Library/Homebrew/postinstall.rb index 53a5b7e751d93..916dbada37361 100644 --- a/Library/Homebrew/postinstall.rb +++ b/Library/Homebrew/postinstall.rb @@ -4,6 +4,7 @@ require "debrew" require "fcntl" require "socket" +require "json/add/core" begin error_pipe = UNIXSocket.open(ENV["HOMEBREW_ERROR_PIPE"], &:recv_io) @@ -15,7 +16,7 @@ formula.extend(Debrew::Formula) if ARGV.debug? formula.run_post_install rescue Exception => e # rubocop:disable Lint/RescueException - Marshal.dump(e, error_pipe) + error_pipe.write e.to_json error_pipe.close exit! 1 end diff --git a/Library/Homebrew/test.rb b/Library/Homebrew/test.rb index 3d5e62a8847ff..0c4b1bb36e43d 100644 --- a/Library/Homebrew/test.rb +++ b/Library/Homebrew/test.rb @@ -7,6 +7,7 @@ require "formula_assertions" require "fcntl" require "socket" +require "json/add/core" TEST_TIMEOUT_SECONDS = 5 * 60 @@ -28,7 +29,7 @@ raise "test returned false" if formula.run_test == false end rescue Exception => e # rubocop:disable Lint/RescueException - Marshal.dump(e, error_pipe) + error_pipe.write e.to_json error_pipe.close exit! 1 end diff --git a/Library/Homebrew/utils/fork.rb b/Library/Homebrew/utils/fork.rb index 5087ca716ce96..bf4e84143d7e5 100644 --- a/Library/Homebrew/utils/fork.rb +++ b/Library/Homebrew/utils/fork.rb @@ -1,5 +1,7 @@ require "fcntl" require "socket" +require "json" +require "json/add/core" module Utils def self.safe_fork(&_block) @@ -15,7 +17,7 @@ def self.safe_fork(&_block) write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) yield rescue Exception => e # rubocop:disable Lint/RescueException - Marshal.dump(e, write) + write.write e.to_json write.close exit! else @@ -36,7 +38,7 @@ def self.safe_fork(&_block) data = read.read read.close Process.wait(pid) unless socket.nil? - raise Marshal.load(data) unless data.nil? || data.empty? # rubocop:disable Security/MarshalLoad + raise ChildProcessError, JSON.parse(data) unless data.nil? || data.empty? raise Interrupt if $CHILD_STATUS.exitstatus == 130 raise "Forked child process failed: #{$CHILD_STATUS}" unless $CHILD_STATUS.success? end