# frozen_string_literal: true require "spec_helper" RSpec.describe "bundle exec" do let(:system_gems_to_install) { %w(rack-1.0.0 rack-0.9.1) } before :each do system_gems(system_gems_to_install) end it "activates the correct gem" do gemfile <<-G gem "rack", "0.9.1" G bundle "exec rackup" expect(out).to eq("0.9.1") end it "works when the bins are in ~/.bundle" do install_gemfile <<-G gem "rack" G bundle "exec rackup" expect(out).to eq("1.0.0") end it "works when running from a random directory", :ruby_repo do install_gemfile <<-G gem "rack" G bundle "exec 'cd #{tmp("gems")} && rackup'" expect(out).to include("1.0.0") end it "works when exec'ing something else" do install_gemfile 'gem "rack"' bundle "exec echo exec" expect(out).to eq("exec") end it "works when exec'ing to ruby" do install_gemfile 'gem "rack"' bundle "exec ruby -e 'puts %{hi}'" expect(out).to eq("hi") end it "accepts --verbose" do install_gemfile 'gem "rack"' bundle "exec --verbose echo foobar" expect(out).to eq("foobar") end it "passes --verbose to command if it is given after the command" do install_gemfile 'gem "rack"' bundle "exec echo --verbose" expect(out).to eq("--verbose") end it "handles --keep-file-descriptors" do require "tempfile" command = Tempfile.new("io-test") command.sync = true command.write <<-G if ARGV[0] IO.for_fd(ARGV[0].to_i) else require 'tempfile' io = Tempfile.new("io-test-fd") args = %W[#{Gem.ruby} -I#{lib} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}] args << { io.to_i => io } if RUBY_VERSION >= "2.0" exec(*args) end G install_gemfile "" sys_exec("#{Gem.ruby} #{command.path}") if Bundler.current_ruby.ruby_2? expect(out).to eq("") else expect(out).to eq("Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec.") end expect(err).to lack_errors end it "accepts --keep-file-descriptors" do install_gemfile "" bundle "exec --keep-file-descriptors echo foobar" expect(err).to lack_errors end it "can run a command named --verbose" do install_gemfile 'gem "rack"' File.open("--verbose", "w") do |f| f.puts "#!/bin/sh" f.puts "echo foobar" end File.chmod(0o744, "--verbose") with_path_as(".") do bundle "exec -- --verbose" end expect(out).to eq("foobar") end it "handles different versions in different bundles" do build_repo2 do build_gem "rack_two", "1.0.0" do |s| s.executables = "rackup" end end install_gemfile <<-G source "file://#{gem_repo1}" gem "rack", "0.9.1" G Dir.chdir bundled_app2 do install_gemfile bundled_app2("Gemfile"), <<-G source "file://#{gem_repo2}" gem "rack_two", "1.0.0" G end bundle! "exec rackup" expect(out).to eq("0.9.1") Dir.chdir bundled_app2 do bundle! "exec rackup" expect(out).to eq("1.0.0") end end it "handles gems installed with --without" do install_gemfile <<-G, :without => :middleware source "file://#{gem_repo1}" gem "rack" # rack 0.9.1 and 1.0 exist group :middleware do gem "rack_middleware" # rack_middleware depends on rack 0.9.1 end G bundle "exec rackup" expect(out).to eq("0.9.1") expect(the_bundle).not_to include_gems "rack_middleware 1.0" end it "does not duplicate already exec'ed RUBYOPT" do install_gemfile <<-G gem "rack" G rubyopt = ENV["RUBYOPT"] rubyopt = "-rbundler/setup #{rubyopt}" bundle "exec 'echo $RUBYOPT'" expect(out).to have_rubyopts(rubyopt) bundle "exec 'echo $RUBYOPT'", :env => { "RUBYOPT" => rubyopt } expect(out).to have_rubyopts(rubyopt) end it "does not duplicate already exec'ed RUBYLIB", :ruby_repo do install_gemfile <<-G gem "rack" G rubylib = ENV["RUBYLIB"] rubylib = "#{rubylib}".split(File::PATH_SEPARATOR).unshift "#{bundler_path}" rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) bundle "exec 'echo $RUBYLIB'" expect(out).to include(rubylib) bundle "exec 'echo $RUBYLIB'", :env => { "RUBYLIB" => rubylib } expect(out).to include(rubylib) end it "errors nicely when the argument doesn't exist" do install_gemfile <<-G gem "rack" G bundle "exec foobarbaz" expect(exitstatus).to eq(127) if exitstatus expect(out).to include("bundler: command not found: foobarbaz") expect(out).to include("Install missing gem executables with `bundle install`") end it "errors nicely when the argument is not executable" do install_gemfile <<-G gem "rack" G bundle "exec touch foo" bundle "exec ./foo" expect(exitstatus).to eq(126) if exitstatus expect(out).to include("bundler: not executable: ./foo") end it "errors nicely when no arguments are passed" do install_gemfile <<-G gem "rack" G bundle "exec" expect(exitstatus).to eq(128) if exitstatus expect(out).to include("bundler: exec needs a command to run") end it "raises a helpful error when exec'ing to something outside of the bundle", :ruby_repo, :rubygems => ">= 2.5.2" do install_gemfile! <<-G source "file://#{gem_repo1}" gem "with_license" G [true, false].each do |l| bundle! "config disable_exec_load #{l}" bundle "exec rackup" expect(err).to include "can't find executable rackup for gem rack. rack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?" end end # Different error message on old RG versions (before activate_bin_path) because they # called `Kernel#gem` directly it "raises a helpful error when exec'ing to something outside of the bundle", :rubygems => "< 2.5.2" do install_gemfile! <<-G source "file://#{gem_repo1}" gem "with_license" G [true, false].each do |l| bundle! "config disable_exec_load #{l}" bundle "exec rackup" expect(err).to include "rack is not part of the bundle. Add it to your Gemfile." end end describe "with help flags" do each_prefix = proc do |string, &blk| 1.upto(string.length) {|l| blk.call(string[0, l]) } end each_prefix.call("exec") do |exec| describe "when #{exec} is used" do before(:each) do install_gemfile <<-G gem "rack" G create_file("print_args", <<-'RUBY') #!/usr/bin/env ruby puts "args: #{ARGV.inspect}" RUBY bundled_app("print_args").chmod(0o755) end it "shows executable's man page when --help is after the executable" do bundle "#{exec} print_args --help" expect(out).to eq('args: ["--help"]') end it "shows executable's man page when --help is after the executable and an argument" do bundle "#{exec} print_args foo --help" expect(out).to eq('args: ["foo", "--help"]') bundle "#{exec} print_args foo bar --help" expect(out).to eq('args: ["foo", "bar", "--help"]') bundle "#{exec} print_args foo --help bar" expect(out).to eq('args: ["foo", "--help", "bar"]') end it "shows executable's man page when the executable has a -" do FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template")) bundle "#{exec} docker-template build discourse --help" expect(out).to eq('args: ["build", "discourse", "--help"]') end it "shows executable's man page when --help is after another flag" do bundle "#{exec} print_args --bar --help" expect(out).to eq('args: ["--bar", "--help"]') end it "uses executable's original behavior for -h" do bundle "#{exec} print_args -h" expect(out).to eq('args: ["-h"]') end it "shows bundle-exec's man page when --help is between exec and the executable", :ruby_repo do with_fake_man do bundle "#{exec} --help cat" end expect(out).to include(%(["#{root}/man/bundle-exec.1"])) end it "shows bundle-exec's man page when --help is before exec", :ruby_repo do with_fake_man do bundle "--help #{exec}" end expect(out).to include(%(["#{root}/man/bundle-exec.1"])) end it "shows bundle-exec's man page when -h is before exec", :ruby_repo do with_fake_man do bundle "-h #{exec}" end expect(out).to include(%(["#{root}/man/bundle-exec.1"])) end it "shows bundle-exec's man page when --help is after exec", :ruby_repo do with_fake_man do bundle "#{exec} --help" end expect(out).to include(%(["#{root}/man/bundle-exec.1"])) end it "shows bundle-exec's man page when -h is after exec", :ruby_repo do with_fake_man do bundle "#{exec} -h" end expect(out).to include(%(["#{root}/man/bundle-exec.1"])) end end end end describe "with gem executables" do describe "run from a random directory" do before(:each) do install_gemfile <<-G gem "rack" G end it "works when unlocked", :ruby_repo do bundle "exec 'cd #{tmp("gems")} && rackup'" expect(out).to eq("1.0.0") expect(out).to include("1.0.0") end it "works when locked", :ruby_repo do expect(the_bundle).to be_locked bundle "exec 'cd #{tmp("gems")} && rackup'" expect(out).to include("1.0.0") end end describe "from gems bundled via :path" do before(:each) do build_lib "fizz", :path => home("fizz") do |s| s.executables = "fizz" end install_gemfile <<-G gem "fizz", :path => "#{File.expand_path(home("fizz"))}" G end it "works when unlocked" do bundle "exec fizz" expect(out).to eq("1.0") end it "works when locked" do expect(the_bundle).to be_locked bundle "exec fizz" expect(out).to eq("1.0") end end describe "from gems bundled via :git" do before(:each) do build_git "fizz_git" do |s| s.executables = "fizz_git" end install_gemfile <<-G gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}" G end it "works when unlocked" do bundle "exec fizz_git" expect(out).to eq("1.0") end it "works when locked" do expect(the_bundle).to be_locked bundle "exec fizz_git" expect(out).to eq("1.0") end end describe "from gems bundled via :git with no gemspec" do before(:each) do build_git "fizz_no_gemspec", :gemspec => false do |s| s.executables = "fizz_no_gemspec" end install_gemfile <<-G gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}" G end it "works when unlocked" do bundle "exec fizz_no_gemspec" expect(out).to eq("1.0") end it "works when locked" do expect(the_bundle).to be_locked bundle "exec fizz_no_gemspec" expect(out).to eq("1.0") end end end it "performs an automatic bundle install" do gemfile <<-G source "file://#{gem_repo1}" gem "rack", "0.9.1" gem "foo" G bundle "config auto_install 1" bundle "exec rackup" expect(out).to include("Installing foo 1.0") end describe "with gems bundled via :path with invalid gemspecs" do it "outputs the gemspec validation errors", :rubygems => ">= 1.7.2" do build_lib "foo" gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s File.open(gemspec, "w") do |f| f.write <<-G Gem::Specification.new do |s| s.name = 'foo' s.version = '1.0' s.summary = 'TODO: Add summary' s.authors = 'Me' end G end install_gemfile <<-G gem "foo", :path => "#{lib_path("foo-1.0")}" G bundle "exec irb" expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid") expect(err).to match('"TODO" is not a summary') end end describe "with gems bundled for deployment" do it "works when calling bundler from another script" do gemfile <<-G module Monkey def bin_path(a,b,c) raise Gem::GemNotFoundException.new('Fail') end end Bundler.rubygems.extend(Monkey) G bundle "install --deployment" bundle "exec ruby -e '`#{bindir.join("bundler")} -v`; puts $?.success?'" expect(out).to match("true") end end context "`load`ing a ruby file instead of `exec`ing" do let(:path) { bundled_app("ruby_executable") } let(:shebang) { "#!/usr/bin/env ruby" } let(:executable) { <<-RUBY.gsub(/^ */, "").strip } #{shebang} require "rack" puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}" puts "ARGS: \#{$0} \#{ARGV.join(' ')}" puts "RACK: \#{RACK}" process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip puts "PROCESS: \#{process_title}" RUBY before do path.open("w") {|f| f << executable } path.chmod(0o755) install_gemfile <<-G gem "rack" G end let(:exec) { "EXEC: load" } let(:args) { "ARGS: #{path} arg1 arg2" } let(:rack) { "RACK: 1.0.0" } let(:process) do title = "PROCESS: #{path}" title += " arg1 arg2" if RUBY_VERSION >= "2.1" title end let(:exit_code) { 0 } let(:expected) { [exec, args, rack, process].join("\n") } let(:expected_err) { "" } subject { bundle "exec #{path} arg1 arg2" } shared_examples_for "it runs" do it "like a normally executed executable" do subject expect(exitstatus).to eq(exit_code) if exitstatus expect(err).to eq(expected_err) expect(out).to eq(expected) end end it_behaves_like "it runs" context "the executable exits explicitly" do let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" } context "with exit 0" do it_behaves_like "it runs" end context "with exit 99" do let(:exit_code) { 99 } it_behaves_like "it runs" end end context "the executable is empty" do let(:executable) { "" } let(:exit_code) { 0 } let(:expected) { "#{path} is empty" } let(:expected_err) { "" } if LessThanProc.with(RUBY_VERSION).call("1.9") # Kernel#exec in ruby < 1.9 will raise Errno::ENOEXEC if the command content is empty, # even if the command is set as an executable. pending "Kernel#exec is different" else it_behaves_like "it runs" end end context "the executable raises" do let(:executable) { super() << "\nraise 'ERROR'" } let(:exit_code) { 1 } let(:expected) { super() << "\nbundler: failed to load command: #{path} (#{path})" } let(:expected_err) do "RuntimeError: ERROR\n #{path}:10" + (Bundler.current_ruby.ruby_18? ? "" : ":in `'") end it_behaves_like "it runs" end context "when the file uses the current ruby shebang", :ruby_repo do let(:shebang) { "#!#{Gem.ruby}" } it_behaves_like "it runs" end context "when Bundler.setup fails" do before do gemfile <<-G gem 'rack', '2' G ENV["BUNDLER_FORCE_TTY"] = "true" end let(:exit_code) { Bundler::GemNotFound.new.status_code } let(:expected) { <<-EOS.strip } \e[31mCould not find gem 'rack (= 2)' in any of the gem sources listed in your Gemfile.\e[0m \e[33mRun `bundle install` to install missing gems.\e[0m EOS it_behaves_like "it runs" end context "when the executable exits non-zero via at_exit" do let(:executable) { super() + "\n\nat_exit { $! ? raise($!) : exit(1) }" } let(:exit_code) { 1 } it_behaves_like "it runs" end context "when disable_exec_load is set" do let(:exec) { "EXEC: exec" } let(:process) { "PROCESS: ruby #{path} arg1 arg2" } before do bundle "config disable_exec_load true" end it_behaves_like "it runs" end context "regarding $0 and __FILE__" do let(:executable) { super() + <<-'RUBY' } puts "$0: #{$0.inspect}" puts "__FILE__: #{__FILE__.inspect}" RUBY let(:expected) { super() + <<-EOS.chomp } $0: #{path.to_s.inspect} __FILE__: #{path.to_s.inspect} EOS it_behaves_like "it runs" context "when the path is relative" do let(:path) { super().relative_path_from(bundled_app) } if LessThanProc.with(RUBY_VERSION).call("1.9") pending "relative paths have ./ __FILE__" else it_behaves_like "it runs" end end context "when the path is relative with a leading ./" do let(:path) { Pathname.new("./#{super().relative_path_from(Pathname.pwd)}") } if LessThanProc.with(RUBY_VERSION).call("< 1.9") pending "relative paths with ./ have absolute __FILE__" else it_behaves_like "it runs" end end end context "signals being trapped by bundler" do let(:executable) { strip_whitespace <<-RUBY } #{shebang} begin Thread.new do puts 'Started' # For process sync STDOUT.flush sleep 1 # ignore quality_spec raise "Didn't receive INT at all" end.join rescue Interrupt puts "foo" end RUBY it "receives the signal" do skip "popen3 doesn't provide a way to get pid " unless RUBY_VERSION >= "1.9.3" bundle("exec #{path}") do |_, o, thr| o.gets # Consumes 'Started' and ensures that thread has started Process.kill("INT", thr.pid) end expect(out).to eq("foo") end end end context "nested bundle exec", :ruby_repo do let(:system_gems_to_install) { super() << :bundler } context "with shared gems disabled" do before do gemfile <<-G source "file://#{gem_repo1}" gem "rack" G bundle :install, :system_bundler => true, :path => "vendor/bundler" end it "overrides disable_shared_gems so bundler can be found" do file = bundled_app("file_that_bundle_execs.rb") create_file(file, <<-RB) #!#{Gem.ruby} puts `bundle exec echo foo` RB file.chmod(0o777) bundle! "exec #{file}", :system_bundler => true expect(out).to eq("foo") end end context "with a system gem that shadows a default gem" do let(:openssl_version) { "99.9.9" } let(:expected) { ruby "gem 'openssl', '< 999999'; require 'openssl'; puts OpenSSL::VERSION", :artifice => nil } it "only leaves the default gem in the stdlib available" do skip "openssl isn't a default gem" if expected.empty? install_gemfile! "" # must happen before installing the broken system gem build_repo4 do build_gem "openssl", openssl_version do |s| s.write("lib/openssl.rb", <<-RB) raise "custom openssl should not be loaded, it's not in the gemfile!" RB end end system_gems(:bundler, "openssl-#{openssl_version}", :gem_repo => gem_repo4) file = bundled_app("require_openssl.rb") create_file(file, <<-RB) #!/usr/bin/env ruby require "openssl" puts OpenSSL::VERSION warn Gem.loaded_specs.values.map(&:full_name) RB file.chmod(0o777) aggregate_failures do expect(bundle!("exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) expect(bundle!("exec bundle exec #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) expect(bundle!("exec ruby #{file}", :system_bundler => true, :artifice => nil)).to eq(expected) expect(run!(file.read, :no_lib => true, :artifice => nil)).to eq(expected) end # sanity check that we get the newer, custom version without bundler sys_exec("#{Gem.ruby} #{file}") expect(err).to include("custom openssl should not be loaded") end end end end