
Homebrew's actually ended up using a fair few gems. While we want to avoid Bundler at runtime (and this PR still does that, in fact uses Bundler even less at runtime than it did before) writing our own version to use at build-time seems redundant.
175 lines
4.7 KiB
Ruby
175 lines
4.7 KiB
Ruby
require "open3"
|
|
require "shellwords"
|
|
require "plist"
|
|
|
|
require "extend/io"
|
|
|
|
require "hbc/utils/hash_validator"
|
|
|
|
module Hbc
|
|
class SystemCommand
|
|
attr_reader :command
|
|
|
|
def self.run(executable, options = {})
|
|
new(executable, options).run!
|
|
end
|
|
|
|
def self.run!(command, options = {})
|
|
run(command, options.merge(must_succeed: true))
|
|
end
|
|
|
|
def run!
|
|
@processed_output = { stdout: "", stderr: "" }
|
|
odebug "Executing: #{expanded_command.utf8_inspect}"
|
|
|
|
each_output_line do |type, line|
|
|
case type
|
|
when :stdout
|
|
processed_output[:stdout] << line
|
|
ohai line.chomp if options[:print_stdout]
|
|
when :stderr
|
|
processed_output[:stderr] << line
|
|
ohai line.chomp if options[:print_stderr]
|
|
end
|
|
end
|
|
|
|
assert_success if options[:must_succeed]
|
|
result
|
|
end
|
|
|
|
def initialize(executable, options)
|
|
@executable = executable
|
|
@options = options
|
|
process_options!
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :executable, :options, :processed_output, :processed_status
|
|
|
|
def process_options!
|
|
options.extend(HashValidator)
|
|
.assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo
|
|
sudo_prefix = %w[/usr/bin/sudo -E --]
|
|
sudo_prefix = sudo_prefix.insert(1, "-A") unless ENV["SUDO_ASKPASS"].nil?
|
|
@command = [executable]
|
|
options[:print_stderr] = true unless options.key?(:print_stderr)
|
|
@command.unshift(*sudo_prefix) if options[:sudo]
|
|
@command.concat(options[:args]) if options.key?(:args) && !options[:args].empty?
|
|
@command[0] = Shellwords.shellescape(@command[0]) if @command.size == 1
|
|
nil
|
|
end
|
|
|
|
def assert_success
|
|
return if processed_status && processed_status.success?
|
|
raise CaskCommandFailedError.new(command.utf8_inspect, processed_output[:stdout], processed_output[:stderr], processed_status)
|
|
end
|
|
|
|
def expanded_command
|
|
@expanded_command ||= command.map do |arg|
|
|
if arg.respond_to?(:to_path)
|
|
File.absolute_path(arg)
|
|
else
|
|
String(arg)
|
|
end
|
|
end
|
|
end
|
|
|
|
def each_output_line(&b)
|
|
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
|
|
Open3.popen3(*expanded_command)
|
|
|
|
write_input_to(raw_stdin)
|
|
raw_stdin.close_write
|
|
each_line_from [raw_stdout, raw_stderr], &b
|
|
|
|
@processed_status = raw_wait_thr.value
|
|
end
|
|
|
|
def write_input_to(raw_stdin)
|
|
[*options[:input]].each { |line| raw_stdin.print line }
|
|
end
|
|
|
|
def each_line_from(sources)
|
|
loop do
|
|
readable_sources = IO.select(sources)[0]
|
|
readable_sources.delete_if(&:eof?).first(1).each do |source|
|
|
type = (source == sources[0] ? :stdout : :stderr)
|
|
begin
|
|
yield(type, source.readline_nonblock || "")
|
|
rescue IO::WaitReadable, EOFError
|
|
next
|
|
end
|
|
end
|
|
break if readable_sources.empty?
|
|
end
|
|
sources.each(&:close_read)
|
|
end
|
|
|
|
def result
|
|
Result.new(command,
|
|
processed_output[:stdout],
|
|
processed_output[:stderr],
|
|
processed_status.exitstatus)
|
|
end
|
|
end
|
|
end
|
|
|
|
module Hbc
|
|
class SystemCommand
|
|
class Result
|
|
attr_accessor :command, :stdout, :stderr, :exit_status
|
|
|
|
def initialize(command, stdout, stderr, exit_status)
|
|
@command = command
|
|
@stdout = stdout
|
|
@stderr = stderr
|
|
@exit_status = exit_status
|
|
end
|
|
|
|
def plist
|
|
@plist ||= self.class._parse_plist(@command, @stdout.dup)
|
|
end
|
|
|
|
def success?
|
|
@exit_status.zero?
|
|
end
|
|
|
|
def merged_output
|
|
@merged_output ||= @stdout + @stderr
|
|
end
|
|
|
|
def to_s
|
|
@stdout
|
|
end
|
|
|
|
def self._warn_plist_garbage(command, garbage)
|
|
return true unless garbage =~ /\S/
|
|
external = File.basename(command.first)
|
|
lines = garbage.strip.split("\n")
|
|
opoo "Non-XML stdout from #{external}:"
|
|
$stderr.puts lines.map { |l| " #{l}" }
|
|
end
|
|
|
|
def self._parse_plist(command, output)
|
|
raise CaskError, "Empty plist input" unless output =~ /\S/
|
|
output.sub!(/\A(.*?)(<\?\s*xml)/m, '\2')
|
|
_warn_plist_garbage(command, Regexp.last_match[1]) if CLI.debug?
|
|
output.sub!(%r{(<\s*/\s*plist\s*>)(.*?)\Z}m, '\1')
|
|
_warn_plist_garbage(command, Regexp.last_match[2])
|
|
xml = Plist.parse_xml(output)
|
|
unless xml.respond_to?(:keys) && !xml.keys.empty?
|
|
raise CaskError, <<-EOS
|
|
Empty result parsing plist output from command.
|
|
command was:
|
|
#{command.utf8_inspect}
|
|
output we attempted to parse:
|
|
#{output}
|
|
EOS
|
|
end
|
|
xml
|
|
end
|
|
end
|
|
end
|
|
end
|