Merge pull request #4488 from reitermarkus/system-command

Refactor `Hbc::SystemCommand`.
This commit is contained in:
Markus Reiter 2018-07-20 17:51:10 +02:00 committed by GitHub
commit 7ad999f5f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 142 additions and 77 deletions

View File

@ -1,6 +1,7 @@
require "hbc/artifact/moved" require "hbc/artifact/moved"
require "hbc/utils/hash_validator" require "extend/hash_validator"
using HashValidator
module Hbc module Hbc
module Artifact module Artifact
@ -20,7 +21,7 @@ module Hbc
raise CaskInvalidError.new(cask.token, "target required for #{english_name} '#{source_string}'") raise CaskInvalidError.new(cask.token, "target required for #{english_name} '#{source_string}'")
end end
target_hash.extend(HashValidator).assert_valid_keys(:target) target_hash.assert_valid_keys!(:target)
new(cask, source_string, **target_hash) new(cask, source_string, **target_hash)
end end

View File

@ -1,5 +1,8 @@
require "hbc/artifact/abstract_artifact" require "hbc/artifact/abstract_artifact"
require "extend/hash_validator"
using HashValidator
module Hbc module Hbc
module Artifact module Artifact
class Installer < AbstractArtifact class Installer < AbstractArtifact
@ -60,7 +63,7 @@ module Hbc
raise CaskInvalidError.new(cask, "invalid 'installer' stanza: Only one of #{VALID_KEYS.inspect} is permitted.") raise CaskInvalidError.new(cask, "invalid 'installer' stanza: Only one of #{VALID_KEYS.inspect} is permitted.")
end end
args.extend(HashValidator).assert_valid_keys(*VALID_KEYS) args.assert_valid_keys!(*VALID_KEYS)
new(cask, **args) new(cask, **args)
end end

View File

@ -1,8 +1,9 @@
require "vendor/plist/plist"
require "hbc/artifact/abstract_artifact" require "hbc/artifact/abstract_artifact"
require "hbc/utils/hash_validator" require "extend/hash_validator"
using HashValidator
require "vendor/plist/plist"
module Hbc module Hbc
module Artifact module Artifact
@ -10,9 +11,7 @@ module Hbc
attr_reader :pkg_relative_path attr_reader :pkg_relative_path
def self.from_args(cask, path, **stanza_options) def self.from_args(cask, path, **stanza_options)
stanza_options.extend(HashValidator).assert_valid_keys( stanza_options.assert_valid_keys!(:allow_untrusted, :choices)
:allow_untrusted, :choices
)
new(cask, path, **stanza_options) new(cask, path, **stanza_options)
end end

View File

@ -1,6 +1,7 @@
require "hbc/artifact/abstract_artifact" require "hbc/artifact/abstract_artifact"
require "hbc/utils/hash_validator" require "extend/hash_validator"
using HashValidator
module Hbc module Hbc
module Artifact module Artifact
@ -10,7 +11,7 @@ module Hbc
if target_hash if target_hash
raise CaskInvalidError unless target_hash.respond_to?(:keys) raise CaskInvalidError unless target_hash.respond_to?(:keys)
target_hash.extend(HashValidator).assert_valid_keys(:target) target_hash.assert_valid_keys!(:target)
end end
target_hash ||= {} target_hash ||= {}

View File

@ -53,31 +53,6 @@ module Hbc
end end
end end
class CaskCommandFailedError < CaskError
def initialize(cmd, stdout, stderr, status)
@cmd = cmd
@stdout = stdout
@stderr = stderr
@status = status
end
def to_s
s = "Command failed to execute!\n"
s.concat("\n")
s.concat("==> Failed command:\n")
s.concat(@cmd.join(" ")).concat("\n")
s.concat("\n")
s.concat("==> Standard Output of failed command:\n")
s.concat(@stdout).concat("\n")
s.concat("\n")
s.concat("==> Standard Error of failed command:\n")
s.concat(@stderr).concat("\n")
s.concat("\n")
s.concat("==> Exit status of failed command:\n")
s.concat(@status.inspect).concat("\n")
end
end
class CaskX11DependencyError < AbstractCaskErrorWithToken class CaskX11DependencyError < AbstractCaskErrorWithToken
def to_s def to_s
<<~EOS <<~EOS

View File

@ -3,8 +3,8 @@ require "vendor/plist/plist"
require "shellwords" require "shellwords"
require "extend/io" require "extend/io"
require "extend/hash_validator"
require "hbc/utils/hash_validator" using HashValidator
module Hbc module Hbc
class SystemCommand class SystemCommand
@ -19,6 +19,7 @@ module Hbc
end end
def run! def run!
@merged_output = []
@processed_output = { stdout: "", stderr: "" } @processed_output = { stdout: "", stderr: "" }
odebug command.shelljoin odebug command.shelljoin
@ -27,9 +28,11 @@ module Hbc
when :stdout when :stdout
puts line.chomp if print_stdout? puts line.chomp if print_stdout?
processed_output[:stdout] << line processed_output[:stdout] << line
@merged_output << [:stdout, line]
when :stderr when :stderr
$stderr.puts Formatter.error(line.chomp) if print_stderr? $stderr.puts Formatter.error(line.chomp) if print_stderr?
processed_output[:stderr] << line processed_output[:stderr] << line
@merged_output << [:stderr, line]
end end
end end
@ -41,11 +44,11 @@ module Hbc
@executable = executable @executable = executable
@args = args @args = args
@sudo = sudo @sudo = sudo
@input = input @input = [*input]
@print_stdout = print_stdout @print_stdout = print_stdout
@print_stderr = print_stderr @print_stderr = print_stderr
@must_succeed = must_succeed @must_succeed = must_succeed
options.extend(HashValidator).assert_valid_keys(:chdir) options.assert_valid_keys!(:chdir)
@options = options @options = options
@env = env @env = env
@ -84,7 +87,9 @@ module Hbc
def assert_success def assert_success
return if processed_status&.success? return if processed_status&.success?
raise CaskCommandFailedError.new(command, processed_output[:stdout], processed_output[:stderr], processed_status) raise ErrorDuringExecution.new(command,
status: processed_status,
output: @merged_output)
end end
def expanded_args def expanded_args
@ -113,7 +118,7 @@ module Hbc
end end
def write_input_to(raw_stdin) def write_input_to(raw_stdin)
[*input].each(&raw_stdin.method(:print)) input.each(&raw_stdin.method(:write))
end end
def each_line_from(sources) def each_line_from(sources)

View File

@ -4,14 +4,6 @@ require "stringio"
BUG_REPORTS_URL = "https://github.com/Homebrew/homebrew-cask#reporting-bugs".freeze BUG_REPORTS_URL = "https://github.com/Homebrew/homebrew-cask#reporting-bugs".freeze
# global methods
def odebug(title, *sput)
return unless ARGV.debug?
puts Formatter.headline(title, color: :magenta)
puts sput unless sput.empty?
end
module Hbc module Hbc
module Utils module Utils
def self.gain_permissions_remove(path, command: SystemCommand) def self.gain_permissions_remove(path, command: SystemCommand)

View File

@ -1,7 +0,0 @@
module HashValidator
def assert_valid_keys(*valid_keys)
unknown_keys = keys - valid_keys
return if unknown_keys.empty?
raise %Q(Unknown keys: #{unknown_keys.inspect}. Running "brew update" will likely fix it.)
end
end

View File

@ -69,7 +69,8 @@ class AbstractDownloadStrategy
def safe_system(*args) def safe_system(*args)
if @shutup if @shutup
quiet_system(*args) || raise(ErrorDuringExecution.new(args.shift, args)) return if quiet_system(*args)
raise(ErrorDuringExecution.new(args, status: $CHILD_STATUS))
else else
super(*args) super(*args)
end end

View File

@ -1,3 +1,5 @@
require "shellwords"
class UsageError < RuntimeError class UsageError < RuntimeError
attr_reader :reason attr_reader :reason
@ -524,9 +526,24 @@ end
# raised by safe_system in utils.rb # raised by safe_system in utils.rb
class ErrorDuringExecution < RuntimeError class ErrorDuringExecution < RuntimeError
def initialize(cmd, args = []) def initialize(cmd, status:, output: nil)
args = args.map { |a| a.to_s.gsub " ", "\\ " }.join(" ") s = "Failure while executing; `#{cmd.shelljoin.gsub(/\\=/, "=")}` exited with #{status.exitstatus}."
super "Failure while executing: #{cmd} #{args}"
unless [*output].empty?
format_output_line = lambda do |type, line|
if type == :stderr
Formatter.error(line)
else
line
end
end
s << " Here's the output:\n"
s << output.map(&format_output_line).join
s << "\n" unless s.end_with?("\n")
end
super s
end end
end end

View File

@ -0,0 +1,9 @@
module HashValidator
refine Hash do
def assert_valid_keys!(*valid_keys)
unknown_keys = keys - valid_keys
return if unknown_keys.empty?
raise ArgumentError, "invalid keys: #{unknown_keys.map(&:inspect).join(", ")}"
end
end
end

View File

@ -20,7 +20,9 @@ class Keg
# patchelf requires that the ELF file have a .dynstr section. # patchelf requires that the ELF file have a .dynstr section.
# Skip ELF files that do not have a .dynstr section. # Skip ELF files that do not have a .dynstr section.
return if ["cannot find section .dynstr", "strange: no string table"].include?(old_rpath) return if ["cannot find section .dynstr", "strange: no string table"].include?(old_rpath)
raise ErrorDuringExecution, "#{cmd_rpath}\n#{old_rpath}" unless $CHILD_STATUS.success? unless $CHILD_STATUS.success?
raise ErrorDuringExecution.new(cmd_rpath, status: $CHILD_STATUS, output: [:stdout, old_rpath])
end
rpath = old_rpath rpath = old_rpath
.split(":") .split(":")

View File

@ -118,12 +118,10 @@ module ELFShim
patchelf = DevelopmentTools.locate "patchelf" patchelf = DevelopmentTools.locate "patchelf"
if path.dylib? if path.dylib?
command = [patchelf, "--print-soname", path.expand_path.to_s] command = [patchelf, "--print-soname", path.expand_path.to_s]
soname = Utils.popen_read(*command).chomp soname = Utils.safe_popen_read(*command).chomp
raise ErrorDuringExecution, command unless $CHILD_STATUS.success?
end end
command = [patchelf, "--print-needed", path.expand_path.to_s] command = [patchelf, "--print-needed", path.expand_path.to_s]
needed = Utils.popen_read(*command).split("\n") needed = Utils.safe_popen_read(*command).split("\n")
raise ErrorDuringExecution, command unless $CHILD_STATUS.success?
[soname, needed] [soname, needed]
end end
@ -131,8 +129,7 @@ module ELFShim
soname = nil soname = nil
needed = [] needed = []
command = ["readelf", "-d", path.expand_path.to_s] command = ["readelf", "-d", path.expand_path.to_s]
lines = Utils.popen_read(*command).split("\n") lines = Utils.safe_popen_read(*command).split("\n")
raise ErrorDuringExecution, command unless $CHILD_STATUS.success?
lines.each do |s| lines.each do |s|
filename = s[/\[(.*)\]/, 1] filename = s[/\[(.*)\]/, 1]
next if filename.nil? next if filename.nil?

View File

@ -67,8 +67,7 @@ class EmbeddedPatch
def apply def apply
data = contents.gsub("HOMEBREW_PREFIX", HOMEBREW_PREFIX) data = contents.gsub("HOMEBREW_PREFIX", HOMEBREW_PREFIX)
args = %W[-g 0 -f -#{strip}] args = %W[-g 0 -f -#{strip}]
Utils.popen_write("patch", *args) { |p| p.write(data) } Utils.safe_popen_write("patch", *args) { |p| p.write(data) }
raise ErrorDuringExecution.new("patch", args) unless $CHILD_STATUS.success?
end end
def inspect def inspect

View File

@ -69,7 +69,7 @@ describe Hbc::SystemCommand, :cask do
it "throws an error" do it "throws an error" do
expect { expect {
described_class.run!(command) described_class.run!(command)
}.to raise_error(Hbc::CaskCommandFailedError) }.to raise_error(ErrorDuringExecution)
end end
end end

View File

@ -0,0 +1,63 @@
describe ErrorDuringExecution do
subject(:error) { described_class.new(command, status: status, output: output) }
let(:command) { ["false"] }
let(:status) { instance_double(Process::Status, exitstatus: exitstatus) }
let(:exitstatus) { 1 }
let(:output) { nil }
describe "#initialize" do
it "fails when only given a command" do
expect {
described_class.new(command)
}.to raise_error(ArgumentError)
end
it "fails when only given a status" do
expect {
described_class.new(status: status)
}.to raise_error(ArgumentError)
end
it "does not raise an error when given both a command and a status" do
expect {
described_class.new(command, status: status)
}.not_to raise_error
end
end
describe "#to_s" do
context "when only given a command and a status" do
its(:to_s) { is_expected.to eq "Failure while executing; `false` exited with 1." }
end
context "when additionally given the output" do
let(:output) {
[
[:stdout, "This still worked.\n"],
[:stderr, "Here something went wrong.\n"],
]
}
before do
allow($stdout).to receive(:tty?).and_return(true)
end
its(:to_s) {
is_expected.to eq <<~EOS
Failure while executing; `false` exited with 1. Here's the output:
This still worked.
#{Formatter.error("Here something went wrong.\n")}
EOS
}
end
context "when command arguments contain special characters" do
let(:command) { ["env", "PATH=/bin", "cat", "with spaces"] }
its(:to_s) {
is_expected
.to eq 'Failure while executing; `env PATH=/bin cat with\ spaces` exited with 1.'
}
end
end
end

View File

@ -183,9 +183,10 @@ describe CurlDownloadStrategyError do
end end
describe ErrorDuringExecution do describe ErrorDuringExecution do
subject { described_class.new("badprg", %w[arg1 arg2]) } subject { described_class.new(["badprg", "arg1", "arg2"], status: status) }
let(:status) { instance_double(Process::Status, exitstatus: 17) }
its(:to_s) { is_expected.to eq("Failure while executing: badprg arg1 arg2") } its(:to_s) { is_expected.to eq("Failure while executing; `badprg arg1 arg2` exited with 17.") }
end end
describe ChecksumMismatchError do describe ChecksumMismatchError do

View File

@ -28,6 +28,12 @@ def ohai(title, *sput)
puts sput puts sput
end end
def odebug(title, *sput)
return unless ARGV.debug?
puts Formatter.headline(title, color: :magenta)
puts sput unless sput.empty?
end
def oh1(title, options = {}) def oh1(title, options = {})
if $stdout.tty? && !ARGV.verbose? && options.fetch(:truncate, :auto) == :auto if $stdout.tty? && !ARGV.verbose? && options.fetch(:truncate, :auto) == :auto
title = Tty.truncate(title) title = Tty.truncate(title)
@ -296,7 +302,8 @@ end
# Kernel.system but with exceptions # Kernel.system but with exceptions
def safe_system(cmd, *args, **options) def safe_system(cmd, *args, **options)
Homebrew.system(cmd, *args, **options) || raise(ErrorDuringExecution.new(cmd, args)) return if Homebrew.system(cmd, *args, **options)
raise(ErrorDuringExecution.new([cmd, *args], status: $CHILD_STATUS))
end end
# prints no output # prints no output

View File

@ -5,7 +5,7 @@ module Utils
def self.safe_popen_read(*args, **options, &block) def self.safe_popen_read(*args, **options, &block)
output = popen_read(*args, **options, &block) output = popen_read(*args, **options, &block)
raise ErrorDuringExecution, args unless $CHILD_STATUS.success? raise ErrorDuringExecution(args, stdout: output, status: $CHILD_STATUS) unless $CHILD_STATUS.success?
output output
end end
@ -14,8 +14,8 @@ module Utils
end end
def self.safe_popen_write(*args, **options, &block) def self.safe_popen_write(*args, **options, &block)
output = popen_write(args, **options, &block) output = popen_write(*args, **options, &block)
raise ErrorDuringExecution, args unless $CHILD_STATUS.success? raise ErrorDuringExecution(args, stdout: output, status: $CHILD_STATUS) unless $CHILD_STATUS.success?
output output
end end