diff options
-rw-r--r-- | lib/bundler.rb | 14 | ||||
-rw-r--r-- | lib/bundler/cli.rb | 4 | ||||
-rw-r--r-- | lib/bundler/cli/exec.rb | 58 | ||||
-rw-r--r-- | lib/bundler/rubygems_ext.rb | 2 | ||||
-rw-r--r-- | lib/bundler/rubygems_integration.rb | 17 | ||||
-rw-r--r-- | lib/bundler/settings.rb | 2 | ||||
-rw-r--r-- | lib/bundler/source/git.rb | 4 | ||||
-rw-r--r-- | lib/bundler/source/path.rb | 15 | ||||
-rw-r--r-- | spec/bundler/bundler_spec.rb | 43 | ||||
-rw-r--r-- | spec/bundler/rubygems_integration_spec.rb | 33 | ||||
-rw-r--r-- | spec/commands/clean_spec.rb | 6 | ||||
-rw-r--r-- | spec/commands/exec_spec.rb | 88 | ||||
-rw-r--r-- | spec/commands/package_spec.rb | 18 | ||||
-rw-r--r-- | spec/install/gemfile/git_spec.rb | 41 |
14 files changed, 288 insertions, 57 deletions
diff --git a/lib/bundler.rb b/lib/bundler.rb index 5cfff3d573..2958e64cee 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -355,18 +355,16 @@ module Bundler # depend on "./" relative paths. SharedHelpers.chdir(path.dirname.to_s) do contents = path.read - if contents[0..2] == "---" # YAML header - spec = eval_yaml_gemspec(path, contents) + spec = if contents[0..2] == "---" # YAML header + eval_yaml_gemspec(path, contents) else - spec = eval_gemspec(path, contents) + eval_gemspec(path, contents) end - Bundler.rubygems.validate(spec) if spec && validate + return unless spec + spec.loaded_from = path.expand_path.to_s + Bundler.rubygems.validate(spec) if validate spec end - rescue Gem::InvalidSpecificationException => e - error_message = "The gemspec at #{file} is not valid. Please fix this gemspec.\n" \ - "The validation error was '#{e.message}'\n" - raise Gem::InvalidSpecificationException.new(error_message) end def clear_gemspec_cache diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 015892ea29..0bd82fd128 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -170,7 +170,11 @@ module Bundler map "i" => "install" def install require "bundler/cli/install" + no_install = Bundler.settings[:no_install] + Bundler.settings[:no_install] = false Install.new(options.dup).run + ensure + Bundler.settings[:no_install] = no_install end desc "update [OPTIONS]", "update the current environment" diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb index 77e75580f5..865ac20248 100644 --- a/lib/bundler/cli/exec.rb +++ b/lib/bundler/cli/exec.rb @@ -18,16 +18,30 @@ module Bundler end def run - ui = Bundler.ui - raise ArgumentError if cmd.nil? - + validate_cmd! SharedHelpers.set_bundle_environment - bin_path = Bundler.which(@cmd) + if bin_path = Bundler.which(cmd) + kernel_load(bin_path, *args) && return if ruby_shebang?(bin_path) + # First, try to exec directly to something in PATH + kernel_exec([bin_path, cmd], *args) + else + # Just exec using the given command + kernel_exec(cmd, *args) + end + end + + private + + def validate_cmd! + return unless cmd.nil? + Bundler.ui.error "bundler: exec needs a command to run" + exit 128 + end + + def kernel_exec(*args) + ui = Bundler.ui Bundler.ui = nil - # First, try to exec directly to something in PATH - Kernel.exec([bin_path, @cmd], *args) if bin_path - # Just exec using the given command - Kernel.exec(@cmd, *args) + Kernel.exec(*args) rescue Errno::EACCES, Errno::ENOEXEC Bundler.ui = ui Bundler.ui.error "bundler: not executable: #{cmd}" @@ -37,10 +51,32 @@ module Bundler Bundler.ui.error "bundler: command not found: #{cmd}" Bundler.ui.warn "Install missing gem executables with `bundle install`" exit 127 - rescue ArgumentError + end + + def kernel_load(file, *args) + args.pop if args.last.is_a?(Hash) + ARGV.replace(args) + $0 = file + ui = Bundler.ui + Bundler.ui = nil + require "bundler/setup" + Kernel.load(file) + rescue SystemExit + raise + rescue Exception => e # rubocop:disable Lint/RescueException Bundler.ui = ui - Bundler.ui.error "bundler: exec needs a command to run" - exit 128 + Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})" + backtrace = e.backtrace.take_while {|bt| !bt.start_with?(__FILE__) } + abort "#{e.class}: #{e.message}\n #{backtrace.join("\n ")}" + end + + def ruby_shebang?(file) + possibilities = [ + "#!/usr/bin/env ruby\n", + "#!#{Gem.ruby}\n", + ] + first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) } + possibilities.any? {|shebang| first_line.start_with?(shebang) } end end end diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index 22c22a2dd0..2f36c29cd9 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -53,7 +53,7 @@ module Gem if method_defined?(:extension_dir) alias_method :rg_extension_dir, :extension_dir def extension_dir - @extension_dir ||= if source.respond_to?(:extension_dir_name) + @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) File.expand_path(File.join(extensions_dir, source.extension_dir_name)) else rg_extension_dir diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 0f567db12a..4e5ca7d1f8 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -54,10 +54,23 @@ module Bundler def validate(spec) Bundler.ui.silence { spec.validate(false) } + rescue Gem::InvalidSpecificationException => e + error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \ + "The validation error was '#{e.message}'\n" + raise Gem::InvalidSpecificationException.new(error_message) rescue Errno::ENOENT nil end + def set_installed_by_version(spec, installed_by_version = Gem::VERSION) + return unless spec.respond_to?(:installed_by_version=) + spec.installed_by_version = Gem::Version.create(installed_by_version) + end + + def spec_missing_extensions?(spec) + !spec.respond_to?(:missing_extensions?) || spec.missing_extensions? + end + def path(obj) obj.to_s end @@ -503,9 +516,7 @@ module Bundler # Missing summary is downgraded to a warning in later versions, # so we set it to an empty string to prevent an exception here. spec.summary ||= "" - Bundler.ui.silence { spec.validate(false) } - rescue Errno::ENOENT - nil + RubygemsIntegration.instance_method(:validate).bind(self).call(spec) end end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 634d9dc14a..175c9a76db 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -3,7 +3,7 @@ require "uri" module Bundler class Settings - BOOL_KEYS = %w(frozen cache_all no_prune disable_local_branch_check disable_shared_gems ignore_messages gem.mit gem.coc silence_root_warning).freeze + BOOL_KEYS = %w(frozen cache_all no_prune disable_local_branch_check disable_shared_gems ignore_messages gem.mit gem.coc silence_root_warning no_install).freeze NUMBER_KEYS = %w(retry timeout redirect ssl_verify_mode).freeze DEFAULT_CONFIG = { :retry => 3, :timeout => 10, :redirect => 5 }.freeze diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 90bbd13d34..228ab61a67 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -222,6 +222,10 @@ module Bundler private + def build_extensions(installer) + super if Bundler.rubygems.spec_missing_extensions?(installer.spec) + end + def serialize_gemspecs_in(destination) expanded_path = destination.expand_path(Bundler.root) Dir["#{expanded_path}/#{@glob}"].each do |spec_path| diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 6bfd2e88c7..9790ba5164 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -135,9 +135,12 @@ module Bundler if File.directory?(expanded_path) # We sort depth-first since `<<` will override the earlier-found specs Dir["#{expanded_path}/#{@glob}"].sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| - next unless spec = Bundler.load_gemspec(file, :validate) - spec.loaded_from = file.to_s + next unless spec = Bundler.load_gemspec(file) spec.source = self + Bundler.rubygems.set_installed_by_version(spec) + # Validation causes extension_dir to be calculated, which depends + # on #source, so we validate here instead of load_gemspec + Bundler.rubygems.validate(spec) index << spec end @@ -194,8 +197,7 @@ module Bundler SharedHelpers.chdir(gem_dir) do installer = Path::Installer.new(spec, :env_shebang => false) run_hooks(:pre_install, installer) - installer.build_extensions unless disable_extensions - run_hooks(:post_build, installer) + build_extensions(installer) unless disable_extensions installer.generate_bin run_hooks(:post_install, installer) end @@ -213,6 +215,11 @@ module Bundler Bundler.ui.warn "The validation message from Rubygems was:\n #{e.message}" end + def build_extensions(installer) + installer.build_extensions + run_hooks(:post_build, installer) + end + def run_hooks(type, installer) hooks_meth = "#{type}_hooks" return unless Gem.respond_to?(hooks_meth) diff --git a/spec/bundler/bundler_spec.rb b/spec/bundler/bundler_spec.rb index 9d4353f718..84d2922f37 100644 --- a/spec/bundler/bundler_spec.rb +++ b/spec/bundler/bundler_spec.rb @@ -75,32 +75,31 @@ describe Bundler do end end - context "validate is true" do - subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) } - - context "and there are gemspec validation errors" do - let(:ui_shell) { double(:ui_shell) } + it "sets loaded_from" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end - before do - allow(Bundler::UI::Shell).to receive(:new).and_return(ui_shell) - allow(Bundler.rubygems).to receive(:validate) { - raise Gem::InvalidSpecificationException.new("TODO is not an author") - } - end + expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s) + end - it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message" do - File.open(app_gemspec_path, "wb") do |file| - file.puts <<-GEMSPEC.gsub(/^\s+/, "") - Gem::Specification.new do |gem| - gem.author = "TODO" - end - GEMSPEC - end + context "validate is true" do + subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) } - expect { subject }.to raise_error(Gem::InvalidSpecificationException, - "The gemspec at #{app_gemspec_path} is not valid. "\ - "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n") + it "validates the specification" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC end + expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + subject end end end diff --git a/spec/bundler/rubygems_integration_spec.rb b/spec/bundler/rubygems_integration_spec.rb index 917d673b51..fbc49c414c 100644 --- a/spec/bundler/rubygems_integration_spec.rb +++ b/spec/bundler/rubygems_integration_spec.rb @@ -7,16 +7,43 @@ describe Bundler::RubygemsIntegration do end context "#validate" do - let(:spec) { double("spec", :summary => "") } + let(:spec) do + Gem::Specification.new do |s| + s.name = "to-validate" + s.version = "1.0.0" + s.loaded_from = __FILE__ + end + end + subject { Bundler.rubygems.validate(spec) } it "skips overly-strict gemspec validation", :rubygems => "< 1.7" do expect(spec).to_not receive(:validate) - Bundler.rubygems.validate(spec) + subject end it "validates with packaging mode disabled", :rubygems => "1.7" do expect(spec).to receive(:validate).with(false) - Bundler.rubygems.validate(spec) + subject + end + + it "should set a summary to avoid an overly-strict error", :rubygems => "~> 1.7.0" do + spec.summary = nil + expect { subject }.not_to raise_error + expect(spec.summary).to eq("") + end + + context "with an invalid spec" do + before do + expect(spec).to receive(:validate).with(false). + and_raise(Gem::InvalidSpecificationException.new("TODO is not an author")) + end + + it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message", + :rubygems => "1.7" do + expect { subject }.to raise_error(Gem::InvalidSpecificationException, + "The gemspec at #{__FILE__} is not valid. "\ + "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n") + end end end diff --git a/spec/commands/clean_spec.rb b/spec/commands/clean_spec.rb index 95035555d4..37f569b267 100644 --- a/spec/commands/clean_spec.rb +++ b/spec/commands/clean_spec.rb @@ -660,9 +660,11 @@ describe "bundle clean" do gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}" G - bundle "install --path vendor/bundle" - bundle :clean + bundle! "install --path vendor/bundle" + expect(vendored_gems("bundler/gems/extensions")).to exist + expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist + bundle! :clean expect(out).to eq("") expect(vendored_gems("bundler/gems/extensions")).to exist diff --git a/spec/commands/exec_spec.rb b/spec/commands/exec_spec.rb index 16e853bb1d..be2f8aa2a5 100644 --- a/spec/commands/exec_spec.rb +++ b/spec/commands/exec_spec.rb @@ -363,4 +363,92 @@ describe "bundle exec" do 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}" + RUBY + + before do + path.open("w") {|f| f << executable } + path.chmod(0755) + + install_gemfile <<-G + gem "rack" + G + end + + let(:exec) { "EXEC: load" } + let(:args) { "ARGS: #{path} arg1 arg2" } + let(:rack) { "RACK: 1.0.0" } + let(:exit_code) { 0 } + let(:expected) { [exec, args, rack].join("\n") } + let(:expected_err) { "" } + + subject { bundle "exec #{path} arg1 arg2", :expect_err => true } + + shared_examples_for "it runs" do + it "like a normally executed executable 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 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}:7" + + (Bundler.current_ruby.ruby_18? ? "" : ":in `<top (required)>'") + end + it_behaves_like "it runs" + end + + context "when the file uses the current ruby shebang" 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 or available on this machine.\e[0m +\e[33mRun `bundle install` to install missing gems.\e[0m + EOS + + it_behaves_like "it runs" + end + end end diff --git a/spec/commands/package_spec.rb b/spec/commands/package_spec.rb index 0ebb65b1a9..08b36b1e9d 100644 --- a/spec/commands/package_spec.rb +++ b/spec/commands/package_spec.rb @@ -38,6 +38,8 @@ describe "bundle package" do Gem::Specification.new do |s| s.name = "mygem" s.version = "0.1.1" + s.summary = "" + s.authors = ["gem author"] s.add_development_dependency "nokogiri", "=1.4.2" end G @@ -50,8 +52,8 @@ describe "bundle package" do gemspec D - bundle "package --all" - sleep 20 + bundle! "package --all" + expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist @@ -86,6 +88,18 @@ describe "bundle package" do should_not_be_installed "rack 1.0.0", :expect_err => true expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist end + + it "does not prevent installing gems with bundle install" do + gemfile <<-D + source "file://#{gem_repo1}" + gem 'rack' + D + + bundle "package --no-install" + bundle "install" + + should_be_installed "rack 1.0.0" + end end context "with --all-platforms" do diff --git a/spec/install/gemfile/git_spec.rb b/spec/install/gemfile/git_spec.rb index 66a721521e..5081d35fa7 100644 --- a/spec/install/gemfile/git_spec.rb +++ b/spec/install/gemfile/git_spec.rb @@ -980,6 +980,47 @@ describe "bundle install with git sources" do expect(out).to include("An error occurred while installing foo (1.0)") expect(out).not_to include("gem install foo") end + + it "does not reinstall the extension", :rubygems => ">= 2.3.0" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("../lib", __FILE__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\d+\.\d+/) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end end it "ignores git environment variables" do |