diff options
author | The Bundler Bot <bot@bundler.io> | 2017-07-24 21:08:04 +0000 |
---|---|---|
committer | The Bundler Bot <bot@bundler.io> | 2017-07-24 21:08:04 +0000 |
commit | 631231dc1fe8d8ca557a386afd10cb2fda83b959 (patch) | |
tree | dc3f91a96aeb0a368616c8d4d38c40400145104e | |
parent | a0b16613040382b5db2c3c14b4fdf04f9091e641 (diff) | |
parent | ad7fbf26c995440f7bf3b295cbd68be3855f394a (diff) | |
download | bundler-631231dc1fe8d8ca557a386afd10cb2fda83b959.tar.gz |
Auto merge of #5878 - bundler:seg-bundler-binstubs, r=indirect
Add a special bundler binstub that ensures the correct version is activated
### What was the end-user problem that led to this PR?
Closes https://github.com/bundler/bundler/issues/5876.
> Once Bundler 2 ships, each application will depend on a 1.x or 2.x version of Bundler.
We want to make sure that the user always has an _easy_ and _convenient_ way to activate the version of Bundler their app is depending upon.
### What was your diagnosis of the problem?
> One way to ensure that the application will always get the version of Bundler that it needs is with an application-and-bundler-version specific binstub. Let's make it so that bundle binstubs bundler will generate a binstub for the correct version of Bundler, and simply running bin/bundle will always provide the correct version of Bundler.
### What is your fix for the problem, implemented in this PR?
My fix implements a `bundle` binstub when `bundle binstubs bundler` is run
### Why did you choose this fix out of the possible options?
I chose this fix because it allows us to dynamically select the correct Bundler version (with appropriate overrides, such as `$BUNDLER_VERSION` and `bundle update --bundler`), both when invoking `bin/bundle`, and also when invoking other bundler-generated binstubs.
-rw-r--r-- | lib/bundler/cli/binstubs.rb | 5 | ||||
-rw-r--r-- | lib/bundler/installer.rb | 9 | ||||
-rw-r--r-- | lib/bundler/rubygems_integration.rb | 4 | ||||
-rw-r--r-- | lib/bundler/source/metadata.rb | 2 | ||||
-rwxr-xr-x | lib/bundler/templates/Executable | 3 | ||||
-rw-r--r-- | lib/bundler/templates/Executable.bundler | 93 | ||||
-rw-r--r-- | spec/commands/binstubs_spec.rb | 120 | ||||
-rw-r--r-- | spec/install/binstubs_spec.rb | 2 | ||||
-rw-r--r-- | spec/runtime/executable_spec.rb | 6 |
9 files changed, 225 insertions, 19 deletions
diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb index 38863c5e77..acec5741b7 100644 --- a/lib/bundler/cli/binstubs.rb +++ b/lib/bundler/cli/binstubs.rb @@ -31,9 +31,8 @@ module Bundler ) end - if spec.name == "bundler" - Bundler.ui.warn "Sorry, Bundler can only be run via RubyGems." - elsif options[:standalone] + if options[:standalone] + next Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") if gem_name == "bundler" installer.generate_standalone_bundler_executable_stubs(spec) else installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 5996d185da..9b648b0dd0 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -114,14 +114,17 @@ module Bundler # double-assignment to avoid warnings about variables that will be used by ERB bin_path = bin_path = Bundler.bin_path - template = template = File.read(File.expand_path("../templates/Executable", __FILE__)) relative_gemfile_path = relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path) ruby_command = ruby_command = Thor::Util.ruby_command + template_path = File.expand_path("../templates/Executable", __FILE__) + if spec.name == "bundler" + template_path += ".bundler" + spec.executables = %(bundle) + end + template = File.read(template_path) exists = [] spec.executables.each do |executable| - next if executable == "bundle" - binstub_path = "#{bin_path}/#{executable}" if File.exist?(binstub_path) && !options[:force] exists << executable diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 38374ae0b6..6654c292e6 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -450,9 +450,9 @@ module Bundler raise Gem::Exception, "no default executable for #{spec.full_name}" unless exec_name ||= spec.default_executable - unless spec.name == name + unless spec.name == exec_name Bundler::SharedHelpers.major_deprecation \ - "Bundler is using a binstub that was created for a different gem.\n" \ + "Bundler is using a binstub that was created for a different gem (#{spec.name}).\n" \ "You should run `bundle binstub #{gem_name}` " \ "to work around a system/bundle conflict." end diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb index 1d62a1f76a..93909002c7 100644 --- a/lib/bundler/source/metadata.rb +++ b/lib/bundler/source/metadata.rb @@ -14,6 +14,8 @@ module Bundler s.platform = Gem::Platform::RUBY s.source = self s.authors = ["bundler team"] + s.bindir = "exe" + s.executables = %w[bundle] # can't point to the actual gemspec or else the require paths will be wrong s.loaded_from = File.expand_path("..", __FILE__) end diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable index 86e73fbbc3..9289debc26 100755 --- a/lib/bundler/templates/Executable +++ b/lib/bundler/templates/Executable @@ -8,6 +8,9 @@ # this file is here to facilitate running it. # +bundle_binstub = File.expand_path("../bundle", __FILE__) +load(bundle_binstub) if File.file?(bundle_binstub) + require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../<%= relative_gemfile_path %>", Pathname.new(__FILE__).realpath) diff --git a/lib/bundler/templates/Executable.bundler b/lib/bundler/templates/Executable.bundler new file mode 100644 index 0000000000..085955c7a9 --- /dev/null +++ b/lib/bundler/templates/Executable.bundler @@ -0,0 +1,93 @@ +#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '<%= executable %>' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + return unless update = ARGV.find {|a| a.start_with?("--bundler") } # must have a --bundler arg + return unless update =~ /--bundler(?:=(.+))?/ + $1 || ">= 0.a" + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../<%= relative_gemfile_path %>", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= begin + env_var_version || cli_arg_version || + lockfile_version || "#{Gem::Requirement.default}.a" + end + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler(bundler_version) + end + + def activate_bundler(bundler_version) + gem_error = activation_error_handling do + gem "bundler", bundler_version + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("<%= spec.name %>", "<%= executable %>") +end diff --git a/spec/commands/binstubs_spec.rb b/spec/commands/binstubs_spec.rb index 89fc3866f7..fa44928625 100644 --- a/spec/commands/binstubs_spec.rb +++ b/spec/commands/binstubs_spec.rb @@ -50,15 +50,121 @@ RSpec.describe "bundle binstubs <gem>" do expect(out).to include("`bundle binstubs` needs at least one gem to run.") end - it "does not bundle the bundler binary" do - install_gemfile <<-G - source "file://#{gem_repo1}" - G + context "the bundle binstub" do + before do + if system_bundler_version == :bundler + system_gems :bundler + elsif system_bundler_version + build_repo4 do + build_gem "bundler", system_bundler_version do |s| + s.executables = "bundle" + s.bindir = "exe" + s.write "exe/bundle", "puts %(system bundler #{system_bundler_version}\\n\#{ARGV.inspect})" + end + end + system_gems "bundler-#{system_bundler_version}", :gem_repo => gem_repo4 + end + build_repo2 do + build_gem "prints_loaded_gems", "1.0" do |s| + s.executables = "print_loaded_gems" + s.write "bin/print_loaded_gems", <<-R + specs = Gem.loaded_specs.values.reject {|s| Bundler.rubygems.spec_default_gem?(s) } + puts specs.map(&:full_name).sort.inspect + R + end + end + install_gemfile! <<-G + source "file://#{gem_repo2}" + gem "rack" + gem "prints_loaded_gems" + G + bundle! "binstubs bundler rack prints_loaded_gems" + end + + let(:system_bundler_version) { Bundler::VERSION } + + it "runs bundler" do + sys_exec! "#{bundled_app("bin/bundle")} install" + expect(out).to eq %(system bundler #{system_bundler_version}\n["install"]) + end + + context "when BUNDLER_VERSION is set" do + it "runs the correct version of bundler" do + sys_exec "BUNDLER_VERSION='999.999.999' #{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end + + context "when a lockfile exists with a locked bundler version" do + it "runs the correct version of bundler when the version is newer" do + lockfile lockfile.gsub(system_bundler_version, "999.999.999") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + + it "runs the correct version of bundler when the version is older" do + lockfile lockfile.gsub(system_bundler_version, "1.0") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (1.0) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '1.0'`") + end + + it "runs the correct version of bundler when the version is a pre-release" do + lockfile lockfile.gsub(system_bundler_version, "1.12.0.a") + sys_exec "#{bundled_app("bin/bundle")} install" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (1.12.0.a) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '1.12.0.a'`") + end + end + + context "when update --bundler is called" do + before { lockfile.gsub(system_bundler_version, "1.1.1") } - bundle "binstubs bundler" + it "calls through to the latest bundler version" do + sys_exec! "#{bundled_app("bin/bundle")} update --bundler" + expect(last_command.stdout).to eq %(system bundler #{system_bundler_version}\n["update", "--bundler"]) + end + + it "calls through to the explicit bundler version" do + sys_exec "#{bundled_app("bin/bundle")} update --bundler=999.999.999" + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end - expect(bundled_app("bin/bundle")).not_to exist - expect(out).to include("Sorry, Bundler can only be run via RubyGems.") + context "without a lockfile" do + it "falls back to the latest installed bundler" do + FileUtils.rm bundled_app("Gemfile.lock") + sys_exec! bundled_app("bin/bundle").to_s + expect(out).to eq "system bundler #{system_bundler_version}\n[]" + end + end + + context "using another binstub" do + let(:system_bundler_version) { :bundler } + it "loads all gems" do + sys_exec! bundled_app("bin/print_loaded_gems").to_s + expect(out).to eq %(["bundler-#{Bundler::VERSION}", "prints_loaded_gems-1.0", "rack-1.2"]) + end + + context "when requesting a different bundler version" do + before { lockfile lockfile.gsub(Bundler::VERSION, "999.999.999") } + + it "attempts to load that version" do + sys_exec bundled_app("bin/rackup").to_s + expect(exitstatus).to eq(42) if exitstatus + expect(last_command.stderr).to include("Activating bundler (999.999.999) failed:"). + and include("To install the version of bundler this project requires, run `gem install bundler -v '999.999.999'`") + end + end + end end it "installs binstubs from git gems" do diff --git a/spec/install/binstubs_spec.rb b/spec/install/binstubs_spec.rb index e5acc84e6a..9f361035e0 100644 --- a/spec/install/binstubs_spec.rb +++ b/spec/install/binstubs_spec.rb @@ -38,7 +38,7 @@ RSpec.describe "bundle install" do it "prints a deprecation notice" do bundle "config major_deprecations true" gembin("rackup") - expect(out).to include("Bundler is using a binstub that was created for a different gem.") + expect(out).to include("Bundler is using a binstub that was created for a different gem (rack).") end it "loads the correct spec's executable" do diff --git a/spec/runtime/executable_spec.rb b/spec/runtime/executable_spec.rb index 545fc97330..388ee049d0 100644 --- a/spec/runtime/executable_spec.rb +++ b/spec/runtime/executable_spec.rb @@ -78,7 +78,7 @@ RSpec.describe "Running bin/* commands" do expect(out).to eq("1.0") end - it "don't bundle da bundla" do + it "creates a bundle binstub" do build_gem "bundler", Bundler::VERSION, :to_system => true do |s| s.executables = "bundle" end @@ -88,9 +88,9 @@ RSpec.describe "Running bin/* commands" do gem "bundler" G - bundle "binstubs bundler" + bundle! "binstubs bundler" - expect(bundled_app("bin/bundle")).not_to exist + expect(bundled_app("bin/bundle")).to exist end it "does not generate bin stubs if the option was not specified" do |