Merge pull request #4453 from reitermarkus/fix-system-command

Fix `SystemCommand` `:path`.
This commit is contained in:
Markus Reiter 2018-07-11 18:26:29 +02:00 committed by GitHub
commit e1957a9d04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 46 deletions

View File

@ -14,16 +14,32 @@ module Hbc
To complete the installation of Cask #{cask}, you must also To complete the installation of Cask #{cask}, you must also
run the installer at run the installer at
'#{path}' '#{cask.staged_path.join(path)}'
EOS EOS
end end
end end
module ScriptInstaller module ScriptInstaller
def install_phase(command: nil, **_) def install_phase(command: nil, **_)
ohai "Running #{self.class.dsl_key} script '#{path.relative_path_from(cask.staged_path)}'" ohai "Running #{self.class.dsl_key} script '#{path}'"
FileUtils.chmod "+x", path unless path.executable?
command.run(path, **args, path: PATH.new(HOMEBREW_PREFIX/"bin", HOMEBREW_PREFIX/"sbin", ENV["PATH"])) absolute_path = if path.absolute?
path
else
cask.staged_path.join(path)
end
if absolute_path.exist? && !absolute_path.executable?
FileUtils.chmod "+x", absolute_path
end
executable = if absolute_path.exist?
absolute_path
else
path
end
command.run!(executable, **args, env: { "PATH" => PATH.new(HOMEBREW_PREFIX/"bin", HOMEBREW_PREFIX/"sbin", ENV["PATH"]) })
end end
end end
@ -54,7 +70,7 @@ module Hbc
super(cask) super(cask)
if args.key?(:manual) if args.key?(:manual)
@path = cask.staged_path.join(args[:manual]) @path = Pathname(args[:manual])
@args = [] @args = []
extend(ManualInstaller) extend(ManualInstaller)
return return
@ -65,17 +81,16 @@ module Hbc
) )
raise CaskInvalidError.new(cask, "#{self.class.dsl_key} missing executable") if path.nil? raise CaskInvalidError.new(cask, "#{self.class.dsl_key} missing executable") if path.nil?
path = Pathname(path) @path = Pathname(path)
@path = path.absolute? ? path : cask.staged_path.join(path)
extend(ScriptInstaller) extend(ScriptInstaller)
end end
def summarize def summarize
path.relative_path_from(cask.staged_path).to_s path.to_s
end end
def to_h def to_h
{ path: path.relative_path_from(cask.staged_path).to_s }.tap do |h| { path: path }.tap do |h|
h[:args] = args unless is_a?(ManualInstaller) h[:args] = args unless is_a?(ManualInstaller)
end end
end end

View File

@ -47,7 +47,7 @@ module Hbc
@must_succeed = must_succeed @must_succeed = must_succeed
options.extend(HashValidator).assert_valid_keys(:chdir) options.extend(HashValidator).assert_valid_keys(:chdir)
@options = options @options = options
@env = { "PATH" => ENV["PATH"] }.merge(env) @env = env
@env.keys.grep_v(/^[\w&&\D]\w*$/) do |name| @env.keys.grep_v(/^[\w&&\D]\w*$/) do |name|
raise ArgumentError, "Invalid variable name: '#{name}'" raise ArgumentError, "Invalid variable name: '#{name}'"
@ -55,7 +55,7 @@ module Hbc
end end
def command def command
[*sudo_prefix, executable, *args] [*sudo_prefix, *env_args, executable, *args]
end end
private private
@ -64,18 +64,22 @@ module Hbc
attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed? attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed?
def env_args
return [] if env.empty?
variables = env.map do |name, value|
sanitized_name = Shellwords.escape(name)
sanitized_value = Shellwords.escape(value)
"#{sanitized_name}=#{sanitized_value}"
end
["env", *variables]
end
def sudo_prefix def sudo_prefix
return [] unless sudo? return [] unless sudo?
askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : [] askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : []
prefix = ["/usr/bin/sudo", *askpass_flags, "-E"] ["/usr/bin/sudo", *askpass_flags, "-E", "--"]
env.each do |name, value|
sanitized_name = Shellwords.escape(name)
sanitized_value = Shellwords.escape(value)
prefix << "#{sanitized_name}=#{sanitized_value}"
end
prefix << "--"
end end
def assert_success def assert_success
@ -97,7 +101,7 @@ module Hbc
executable, *args = expanded_command executable, *args = expanded_command
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
Open3.popen3(env, [executable, executable], *args, **options) Open3.popen3([executable, executable], *args, **options)
write_input_to(raw_stdin) write_input_to(raw_stdin)
raw_stdin.close_write raw_stdin.close_write

View File

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

View File

@ -0,0 +1,40 @@
describe Hbc::Artifact::Installer, :cask do
let(:staged_path) { mktmpdir }
let(:cask) { instance_double("Cask", staged_path: staged_path, config: nil) }
subject(:installer) { described_class.new(cask, **args) }
let(:command) { Hbc::SystemCommand }
let(:args) { {} }
describe "#install_phase" do
context "when given a manual installer" do
let(:args) { { manual: "installer" } }
it "shows a message prompting to run the installer manually" do
expect {
installer.install_phase(command: command)
}.to output(%r{run the installer at\s+'#{staged_path}/installer'}).to_stdout
end
end
context "when given a script installer" do
let(:executable) { staged_path/"executable" }
let(:args) { { script: { executable: "executable" } } }
before(:each) do
FileUtils.touch executable
end
it "looks for the executable in HOMEBREW_PREFIX" do
expect(command).to receive(:run!).with(
executable,
a_hash_including(
env: { "PATH" => PATH.new("#{HOMEBREW_PREFIX}/bin", "#{HOMEBREW_PREFIX}/sbin", ENV["PATH"]) },
),
)
installer.install_phase(command: command)
end
end
end
end

View File

@ -520,7 +520,7 @@ describe Hbc::DSL, :cask do
it "allows installer manual to be specified" do it "allows installer manual to be specified" do
installer = cask.artifacts.first installer = cask.artifacts.first
expect(installer).to be_a(Hbc::Artifact::Installer::ManualInstaller) expect(installer).to be_a(Hbc::Artifact::Installer::ManualInstaller)
expect(installer.path).to eq(cask.staged_path.join("Caffeine.app")) expect(installer.path).to eq(Pathname("Caffeine.app"))
end end
end end
end end

View File

@ -2,7 +2,7 @@ describe Hbc::SystemCommand, :cask do
describe "#initialize" do describe "#initialize" do
let(:env_args) { ["bash", "-c", 'printf "%s" "${A?}" "${B?}" "${C?}"'] } let(:env_args) { ["bash", "-c", 'printf "%s" "${A?}" "${B?}" "${C?}"'] }
describe "given some environment variables" do context "when given some environment variables" do
subject { subject {
described_class.new( described_class.new(
"env", "env",
@ -15,10 +15,10 @@ describe Hbc::SystemCommand, :cask do
its("run!.stdout") { is_expected.to eq("123") } its("run!.stdout") { is_expected.to eq("123") }
describe "the resulting command line" do describe "the resulting command line" do
it "does not include the given variables" do it "includes the given variables explicitly" do
expect(Open3) expect(Open3)
.to receive(:popen3) .to receive(:popen3)
.with(a_hash_including("PATH"), ["env"] * 2, *env_args, {}) .with(["env", "env"], "A=1", "B=2", "C=3", "env", *env_args, {})
.and_call_original .and_call_original
subject.run! subject.run!
@ -26,7 +26,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "given some environment variables and sudo: true" do context "when given some environment variables and sudo: true" do
subject { subject {
described_class.new( described_class.new(
"env", "env",
@ -41,9 +41,8 @@ describe Hbc::SystemCommand, :cask do
it "includes the given variables explicitly" do it "includes the given variables explicitly" do
expect(Open3) expect(Open3)
.to receive(:popen3) .to receive(:popen3)
.with(an_instance_of(Hash), ["/usr/bin/sudo"] * 2, .with(["/usr/bin/sudo", "/usr/bin/sudo"], "-E", "--",
"-E", a_string_starting_with("PATH="), "env", "A=1", "B=2", "C=3", "env", *env_args, {})
"A=1", "B=2", "C=3", "--", "env", *env_args, {})
.and_wrap_original do |original_popen3, *_, &block| .and_wrap_original do |original_popen3, *_, &block|
original_popen3.call("/usr/bin/true", &block) original_popen3.call("/usr/bin/true", &block)
end end
@ -54,7 +53,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "when the exit code is 0" do context "when the exit code is 0" do
describe "its result" do describe "its result" do
subject { described_class.run("/usr/bin/true") } subject { described_class.run("/usr/bin/true") }
@ -63,10 +62,10 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "when the exit code is 1" do context "when the exit code is 1" do
let(:command) { "/usr/bin/false" } let(:command) { "/usr/bin/false" }
describe "and the command must succeed" do context "and the command must succeed" do
it "throws an error" do it "throws an error" do
expect { expect {
described_class.run!(command) described_class.run!(command)
@ -74,7 +73,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "and the command does not have to succeed" do context "and the command does not have to succeed" do
describe "its result" do describe "its result" do
subject { described_class.run(command) } subject { described_class.run(command) }
@ -84,7 +83,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "given a pathname" do context "when given a pathname" do
let(:command) { "/bin/ls" } let(:command) { "/bin/ls" }
let(:path) { Pathname(Dir.mktmpdir) } let(:path) { Pathname(Dir.mktmpdir) }
@ -100,7 +99,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "with both STDOUT and STDERR output from upstream" do context "with both STDOUT and STDERR output from upstream" do
let(:command) { "/bin/bash" } let(:command) { "/bin/bash" }
let(:options) { let(:options) {
{ args: [ { args: [
@ -119,7 +118,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "with default options" do context "with default options" do
it "echoes only STDERR" do it "echoes only STDERR" do
expected = [2, 4, 6].map { |i| "#{i}\n" }.join expected = [2, 4, 6].map { |i| "#{i}\n" }.join
expect { expect {
@ -130,7 +129,7 @@ describe Hbc::SystemCommand, :cask do
include_examples("it returns '1 2 3 4 5 6'") include_examples("it returns '1 2 3 4 5 6'")
end end
describe "with print_stdout" do context "with print_stdout" do
before do before do
options.merge!(print_stdout: true) options.merge!(print_stdout: true)
end end
@ -144,7 +143,7 @@ describe Hbc::SystemCommand, :cask do
include_examples("it returns '1 2 3 4 5 6'") include_examples("it returns '1 2 3 4 5 6'")
end end
describe "without print_stderr" do context "without print_stderr" do
before do before do
options.merge!(print_stderr: false) options.merge!(print_stderr: false)
end end
@ -158,7 +157,7 @@ describe Hbc::SystemCommand, :cask do
include_examples("it returns '1 2 3 4 5 6'") include_examples("it returns '1 2 3 4 5 6'")
end end
describe "with print_stdout but without print_stderr" do context "with print_stdout but without print_stderr" do
before do before do
options.merge!(print_stdout: true, print_stderr: false) options.merge!(print_stdout: true, print_stderr: false)
end end
@ -174,7 +173,7 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "with a very long STDERR output" do context "with a very long STDERR output" do
let(:command) { "/bin/bash" } let(:command) { "/bin/bash" }
let(:options) { let(:options) {
{ args: [ { args: [
@ -190,10 +189,23 @@ describe Hbc::SystemCommand, :cask do
end end
end end
describe "given an invalid variable name" do context "when given an invalid variable name" do
it "raises an ArgumentError" do it "raises an ArgumentError" do
expect { described_class.run("true", env: { "1ABC" => true }) } expect { described_class.run("true", env: { "1ABC" => true }) }
.to raise_error(ArgumentError, /variable name/) .to raise_error(ArgumentError, /variable name/)
end end
end end
it "looks for executables in a custom PATH" do
mktmpdir do |path|
(path/"tool").write <<~SH
#!/bin/sh
echo Hello, world!
SH
FileUtils.chmod "+x", path/"tool"
expect(described_class.run("tool", env: { "PATH" => path }).stdout).to include "Hello, world!"
end
end
end end

View File

@ -1,5 +1,5 @@
def sudo(*args) def sudo(*args)
["/usr/bin/sudo", "-E", "PATH=#{ENV["PATH"]}", "--"] + args.flatten ["/usr/bin/sudo", "-E", "--"] + args.flatten
end end
module Hbc module Hbc

View File

@ -80,7 +80,7 @@ shared_examples Hbc::Staged do
allow(staged).to receive(:Pathname).and_return(fake_pathname) allow(staged).to receive(:Pathname).and_return(fake_pathname)
Hbc::FakeSystemCommand.expects_command( Hbc::FakeSystemCommand.expects_command(
["/usr/bin/sudo", "-E", "PATH=#{ENV["PATH"]}", "--", "/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname], sudo("/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname),
) )
staged.set_ownership(fake_pathname.to_s) staged.set_ownership(fake_pathname.to_s)
@ -93,7 +93,7 @@ shared_examples Hbc::Staged do
allow(staged).to receive(:Pathname).and_return(fake_pathname) allow(staged).to receive(:Pathname).and_return(fake_pathname)
Hbc::FakeSystemCommand.expects_command( Hbc::FakeSystemCommand.expects_command(
["/usr/bin/sudo", "-E", "PATH=#{ENV["PATH"]}", "--", "/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname, fake_pathname], sudo("/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname, fake_pathname),
) )
staged.set_ownership([fake_pathname.to_s, fake_pathname.to_s]) staged.set_ownership([fake_pathname.to_s, fake_pathname.to_s])
@ -105,7 +105,7 @@ shared_examples Hbc::Staged do
allow(staged).to receive(:Pathname).and_return(fake_pathname) allow(staged).to receive(:Pathname).and_return(fake_pathname)
Hbc::FakeSystemCommand.expects_command( Hbc::FakeSystemCommand.expects_command(
["/usr/bin/sudo", "-E", "PATH=#{ENV["PATH"]}", "--", "/usr/sbin/chown", "-R", "--", "other_user:other_group", fake_pathname], sudo("/usr/sbin/chown", "-R", "--", "other_user:other_group", fake_pathname),
) )
staged.set_ownership(fake_pathname.to_s, user: "other_user", group: "other_group") staged.set_ownership(fake_pathname.to_s, user: "other_user", group: "other_group")