diff options
Diffstat (limited to 'spec/bundler/install')
48 files changed, 11035 insertions, 3179 deletions
diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb index 1bca055c9f..c7ab7c3d7e 100644 --- a/spec/bundler/install/allow_offline_install_spec.rb +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -1,59 +1,58 @@ # frozen_string_literal: true -require "spec_helper" - -RSpec.describe "bundle install with :allow_offline_install" do - before do - bundle "config allow_offline_install true" - end +RSpec.describe "bundle install allows offline install" do context "with no cached data locally" do it "still installs" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G - expect(the_bundle).to include_gem("rack 1.0") + expect(the_bundle).to include_gem("myrack 1.0") end it "still fails when the network is down" do - install_gemfile <<-G, :artifice => "fail" + install_gemfile <<-G, artifice: "fail", raise_on_error: false source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G - expect(out).to include("Could not reach host testgemserver.local.") + expect(err).to include("Could not reach host testgemserver.local.") expect(the_bundle).to_not be_locked end end context "with cached data locally" do it "will install from the compact index" do - system_gems ["rack-1.0.0"] + system_gems ["myrack-1.0.0"], path: default_bundle_path - install_gemfile! <<-G, :artifice => "compact_index" + bundle_config "clean false" + install_gemfile <<-G, artifice: "compact_index" source "http://testgemserver.local" - gem "rack-obama" - gem "rack", "< 1.0" + gem "myrack-obama" + gem "myrack", "< 1.0" G - expect(the_bundle).to include_gems("rack-obama 1.0", "rack 0.9.1") + expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 0.9.1") gemfile <<-G source "http://testgemserver.local" - gem "rack-obama" + gem "myrack-obama" G - bundle! :update, :artifice => "fail" - expect(out).to include("Using the cached data for the new index because of a network error") + bundle :update, artifice: "fail", all: true + expect(stdboth).to include "Using the cached data for the new index because of a network error" - expect(the_bundle).to include_gems("rack-obama 1.0", "rack 1.0.0") + expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 1.0.0") end def break_git_remote_ops! FileUtils.mkdir_p(tmp("broken_path")) File.open(tmp("broken_path/git"), "w", 0o755) do |f| - f.puts strip_whitespace(<<-RUBY) + f.puts <<~RUBY #!/usr/bin/env ruby - if %w(fetch --force --quiet --tags refs/heads/*:refs/heads/*).-(ARGV).empty? || %w(clone --bare --no-hardlinks --quiet).-(ARGV).empty? + fetch_args = %w(fetch --force --quiet --no-tags) + clone_args = %w(clone --bare --no-hardlinks --quiet) + + if (fetch_args.-(ARGV).empty? || clone_args.-(ARGV).empty?) && File.exist?(ARGV[ARGV.index("--") + 1]) warn "git remote ops have been disabled" exit 1 end @@ -70,22 +69,26 @@ RSpec.describe "bundle install with :allow_offline_install" do end it "will install from a cached git repo" do - git = build_git "a", "1.0.0", :path => lib_path("a") - update_git("a", :path => git.path, :branch => "new_branch") - install_gemfile! <<-G + skip "doesn't print errors" if Gem.win_platform? + + git = build_git "a", "1.0.0", path: lib_path("a") + update_git("a", path: git.path, branch: "new_branch") + install_gemfile <<-G + source "https://gem.repo1" gem "a", :git => #{git.path.to_s.dump} G - break_git_remote_ops! { bundle! :update } - expect(out).to include("Using cached git data because of network errors") + break_git_remote_ops! { bundle :update, all: true } + expect(err).to include("Using cached git data because of network errors") expect(the_bundle).to be_locked break_git_remote_ops! do - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch" G end - expect(out).to include("Using cached git data because of network errors") + expect(err).to include("Using cached git data because of network errors") expect(the_bundle).to be_locked end end diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb index a1a9ab167d..c2eccb3ef2 100644 --- a/spec/bundler/install/binstubs_spec.rb +++ b/spec/bundler/install/binstubs_spec.rb @@ -1,23 +1,19 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do describe "when system_bindir is set" do - # On OS X, Gem.bindir defaults to /usr/bin, so system_bindir is useful if - # you want to avoid sudo installs for system gems with OS X's default ruby it "overrides Gem.bindir" do - expect(Pathname.new("/usr/bin")).not_to be_writable unless Process.euid == 0 + expect(Pathname.new("/usr/bin")).not_to be_writable gemfile <<-G - require 'rubygems' def Gem.bindir; "/usr/bin"; end - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s + bundle_config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s bundle :install - expect(the_bundle).to include_gems "rack 1.0.0" - expect(system_gem_path("altbin/rackup")).to exist + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(system_gem_path("altbin/myrackup")).to exist end end @@ -25,26 +21,29 @@ RSpec.describe "bundle install" do before do build_repo2 do build_gem "fake", "14" do |s| - s.executables = "rackup" + s.executables = "myrackup" end end - install_gemfile <<-G, :binstubs => true - source "file://#{gem_repo2}" + install_gemfile <<-G + source "https://gem.repo2" gem "fake" - gem "rack" + gem "myrack" G end - 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.") - end + it "warns about the situation" do + bundle "exec myrackup" - it "loads the correct spec's executable" do - gembin("rackup") - expect(out).to eq("1.2") + expect(last_command.stderr).to include( + "The `myrackup` executable in the `fake` gem is being loaded, but it's also present in other gems (myrack).\n" \ + "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \ + "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names." + ).or include( + "The `myrackup` executable in the `myrack` gem is being loaded, but it's also present in other gems (fake).\n" \ + "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \ + "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names." + ) end end end diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb index c1ce57e60e..86c22dad55 100644 --- a/spec/bundler/install/bundler_spec.rb +++ b/spec/bundler/install/bundler_spec.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do describe "with bundler dependencies" do before(:each) do build_repo2 do build_gem "rails", "3.0" do |s| - s.add_dependency "bundler", ">= 0.9.0.pre" + s.add_dependency "bundler", ">= 0.9.0" end build_gem "bundler", "0.9.1" build_gem "bundler", Bundler::VERSION @@ -15,57 +14,101 @@ RSpec.describe "bundle install" do it "are forced to the current bundler version" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0" G expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" end - it "are not added if not already present" do + it "are forced to the current bundler version even if not already present" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).not_to include_gems "bundler #{Bundler::VERSION}" + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" end - it "causes a conflict if explicitly requesting a different version" do - install_gemfile <<-G - source "file://#{gem_repo2}" + it "causes a conflict if explicitly requesting a different version of bundler" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails", "3.0" + gem "bundler", "0.9.1" + G + + nice_error = <<~E.strip + Could not find compatible versions + + Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.1 + and Gemfile depends on bundler = 0.9.1, + version solving has failed. + + Your bundle requires a different version of Bundler than the one you're running. + Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install` + E + expect(err).to include(nice_error) + end + + it "causes a conflict if explicitly requesting a non matching requirement on bundler" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails", "3.0" + gem "bundler", "~> 0.8" + G + + nice_error = <<~E.strip + Could not find compatible versions + + Because rails >= 3.0 depends on bundler >= 0.9.0 + and the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler >= 0.9.0, < 1.A, + rails >= 3.0 requires bundler >= 1.A. + So, because Gemfile depends on rails = 3.0 + and Gemfile depends on bundler ~> 0.8, + version solving has failed. + + Your bundle requires a different version of Bundler than the one you're running. + Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install` + E + expect(err).to include(nice_error) + end + + it "causes a conflict if explicitly requesting a version of bundler that doesn't exist" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "rails", "3.0" gem "bundler", "0.9.2" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Fetching source index from file:#{gem_repo2}/ - Resolving dependencies... - Bundler could not find compatible versions for gem "bundler": - In Gemfile: - bundler (= 0.9.2) + nice_error = <<~E.strip + Could not find compatible versions - Current Bundler version: - bundler (#{Bundler::VERSION}) - This Gemfile requires a different version of Bundler. - Perhaps you need to update Bundler by running `gem install bundler`? + Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.2 + and Gemfile depends on bundler = 0.9.2, + version solving has failed. - Could not find gem 'bundler (= 0.9.2)' in any of the sources + Your bundle requires a different version of Bundler than the one you're running, and that version could not be found. E - expect(out).to eq(nice_error) + expect(err).to include(nice_error) end it "works for gems with multiple versions in its dependencies" do + build_repo2 do + build_gem "multiple_versioned_deps" do |s| + s.add_dependency "weakling", ">= 0.0.1", "< 0.1" + end + end + install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "multiple_versioned_deps" G install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "multiple_versioned_deps" - gem "rack" + gem "myrack" G expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0" @@ -73,7 +116,7 @@ RSpec.describe "bundle install" do it "includes bundler in the bundle when it's a child dependency" do install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0" G @@ -83,8 +126,8 @@ RSpec.describe "bundle install" do it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack" + source "https://gem.repo2" + gem "myrack" G run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end" @@ -92,53 +135,131 @@ RSpec.describe "bundle install" do end it "causes a conflict if child dependencies conflict" do - install_gemfile <<-G - source "file://#{gem_repo2}" + bundle_config "force_ruby_platform true" + + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "activemerchant" - gem "rails_fail" + gem "rails_pinned_to_old_activesupport" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Fetching source index from file:#{gem_repo2}/ - Resolving dependencies... - Bundler could not find compatible versions for gem "activesupport": - In Gemfile: - activemerchant was resolved to 1.0, which depends on - activesupport (>= 2.0.0) + nice_error = <<~E.strip + Could not find compatible versions - rails_fail was resolved to 1.0, which depends on - activesupport (= 1.2.3) + Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3 + and every version of activemerchant depends on activesupport >= 2.0.0, + every version of rails_pinned_to_old_activesupport is incompatible with activemerchant >= 0. + So, because Gemfile depends on activemerchant >= 0 + and Gemfile depends on rails_pinned_to_old_activesupport >= 0, + version solving has failed. E - expect(out).to include(nice_error) + expect(err).to include(nice_error) end it "causes a conflict if a child dependency conflicts with the Gemfile" do - install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rails_fail" + bundle_config "force_ruby_platform true" + + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails_pinned_to_old_activesupport" gem "activesupport", "2.3.5" G - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Fetching source index from file:#{gem_repo2}/ - Resolving dependencies... - Bundler could not find compatible versions for gem "activesupport": - In Gemfile: - activesupport (= 2.3.5) + nice_error = <<~E.strip + Could not find compatible versions - rails_fail was resolved to 1.0, which depends on - activesupport (= 1.2.3) + Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3 + and Gemfile depends on rails_pinned_to_old_activesupport >= 0, + activesupport = 1.2.3 is required. + So, because Gemfile depends on activesupport = 2.3.5, + version solving has failed. E - expect(out).to include(nice_error) + expect(err).to include(nice_error) + end + + it "does not cause a conflict if new dependencies in the Gemfile require older dependencies than the lockfile" do + update_repo2 do + build_gem "rails_pinned_to_old_activesupport" do |s| + s.add_dependency "activesupport", "= 1.2.3" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem 'rails', "2.3.2" + G + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails_pinned_to_old_activesupport" + G + + expect(out).to include("Installing activesupport 1.2.3 (was 2.3.2)") + expect(err).to be_empty + end + + it "prints the previous version when switching to a previously downloaded gem" do + build_repo4 do + build_gem "rails", "7.0.3" + build_gem "rails", "7.0.4" + end + + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.4" + G + + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.3" + G + + install_gemfile <<-G + source "https://gem.repo4" + gem 'rails', "7.0.4" + G + + expect(out).to include("Using rails 7.0.4 (was 7.0.3)") + expect(err).to be_empty end - it "can install dependencies with newer bundler version" do + it "can install dependencies with newer bundler version with system gems" do + bundle_config "path.system true" + + system_gems "bundler-99999999.99.1" + install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0" G - simulate_bundler_version "10.0.0" + bundle "check" + expect(out).to include("The Gemfile's dependencies are satisfied") + end + + it "can install dependencies with newer bundler version with a local path" do + bundle_config "path .bundle" + + system_gems "bundler-99999999.99.1" + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0" + G bundle "check" expect(out).to include("The Gemfile's dependencies are satisfied") diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb new file mode 100644 index 0000000000..bad7b7cf34 --- /dev/null +++ b/spec/bundler/install/cooldown_spec.rb @@ -0,0 +1,433 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with the cooldown setting" do + before do + build_repo2 + end + + context "Gemfile DSL" do + it "accepts `source ..., cooldown: N` without error" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 5 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts `cooldown: 0` to disable cooldown for a source" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 0 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "CLI flag" do + before do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + end + + it "accepts --cooldown N on install" do + bundle "install --cooldown 7", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts --cooldown 0 as an escape hatch" do + bundle "install --cooldown 0", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "rejects a negative --cooldown value" do + bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end + end + + context "configuration" do + it "reads BUNDLE_COOLDOWN as an integer" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "reads `bundle config set cooldown N`" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "config set cooldown 7" + bundle "install", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "end-to-end with v2 compact index" do + before do + now = Time.now.utc + build_repo3 do + build_gem "ripe_gem", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "ripe_gem", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + + # parent only resolves with the in-cooldown child 2.0.0 + build_gem "child", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "child", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "parent", "1.0.0" do |s| + s.add_dependency "child", ">= 2.0.0" + s.date = now - (30 * 86_400) + end + + # a cooldown-eligible version exists above the in-cooldown locked one + build_gem "upgradable", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "upgradable", "3.0.0" do |s| + s.date = now - (30 * 86_400) + end + end + end + + it "excludes versions within the cooldown window" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects the latest version when --cooldown 0 is passed" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "applies cooldown declared per-source in the Gemfile" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do + gemfile <<-G + source "https://gem.repo3", cooldown: 0 + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "bypasses cooldown when bundle install uses an existing lockfile" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "annotates in-cooldown versions in bundle outdated table output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/) + end + + it "annotates in-cooldown versions in bundle outdated --parseable output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) + end + + it "excludes a locally-installed version that is still within the cooldown window" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects a locally-installed in-cooldown version when --cooldown 0 bypasses the filter" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "surfaces a cooldown hint when bundle update filters every candidate" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/excluded by the cooldown setting/) + expect(err).to match(/--cooldown 0/) + end + + it "keeps an in-cooldown locked version on bundle update --all instead of failing" do + # Lockfile written before cooldown was enabled pins the now-in-cooldown + # latest version. A full update must not downgrade below it, and cooldown + # must not filter it out, otherwise resolution becomes impossible (#9598). + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "does not fail bundle outdated when the locked version is in cooldown" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + # exit 0 means no outdated gems and, crucially, no resolution failure (exit 7) + expect(exitstatus).to eq(0) + end + + it "still applies cooldown and downgrades a gem that is updated explicitly" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "keeps an in-cooldown transitive dependency on bundle update --all" do + gemfile <<-G + source "https://gem.repo3" + gem "parent" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + child (2.0.0) + parent (1.0.0) + child (>= 2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parent + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("parent 1.0.0", "child 2.0.0") + end + + it "still upgrades to a cooldown-eligible version above the locked one" do + gemfile <<-G + source "https://gem.repo3" + gem "upgradable" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + upgradable (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + upgradable + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("upgradable 3.0.0") + end + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb index b66135c6b0..a3b4a87ecf 100644 --- a/spec/bundler/install/deploy_spec.rb +++ b/spec/bundler/install/deploy_spec.rb @@ -1,109 +1,139 @@ # frozen_string_literal: true -require "spec_helper" -RSpec.describe "install with --deployment or --frozen" do +RSpec.describe "install in deployment or frozen mode" do before do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - it "fails without a lockfile and says that --deployment requires a lock" do - bundle "install --deployment" - expect(out).to include("The --deployment flag requires a Gemfile.lock") + it "fails without a lockfile and says that deployment requires a lock" do + bundle_config "deployment true" + bundle "install", raise_on_error: false + expect(err).to include("The deployment setting requires a lockfile") end - it "fails without a lockfile and says that --frozen requires a lock" do - bundle "install --frozen" - expect(out).to include("The --frozen flag requires a Gemfile.lock") - end - - it "disallows --deployment --system" do - bundle "install --deployment --system" - expect(out).to include("You have specified both --deployment") - expect(out).to include("Please choose only one option") - expect(exitstatus).to eq(15) if exitstatus - end - - it "disallows --deployment --path --system" do - bundle "install --deployment --path . --system" - expect(out).to include("You have specified both --path") - expect(out).to include("as well as --system") - expect(out).to include("Please choose only one option") - expect(exitstatus).to eq(15) if exitstatus - end - - it "works after you try to deploy without a lock" do - bundle "install --deployment" - bundle :install - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack 1.0" + it "fails without a lockfile and says that frozen requires a lock" do + bundle_config "frozen true" + bundle "install", raise_on_error: false + expect(err).to include("The frozen setting requires a lockfile") end it "still works if you are not in the app directory and specify --gemfile" do bundle "install" - Dir.chdir tmp - simulate_new_machine - bundle "install --gemfile #{tmp}/bundled_app/Gemfile --deployment" - Dir.chdir bundled_app - expect(the_bundle).to include_gems "rack 1.0" + pristine_system_gems + bundle_config "deployment true" + bundle_config "path vendor/bundle" + bundle "install --gemfile #{tmp}/bundled_app/Gemfile", dir: tmp + expect(the_bundle).to include_gems "myrack 1.0" end it "works if you exclude a group with a git gem" do build_git "foo" gemfile <<-G + source "https://gem.repo1" group :test do gem "foo", :git => "#{lib_path("foo-1.0")}" end G bundle :install - bundle "install --deployment --without test" - expect(exitstatus).to eq(0) if exitstatus + bundle_config "deployment true" + bundle_config "without test" + bundle :install end - it "works when you bundle exec bundle", :ruby_repo do + it "works when you bundle exec bundle" do + skip "doesn't find bundle" if Gem.win_platform? + + bundle :install + bundle_config "deployment true" bundle :install - bundle "install --deployment" - bundle "exec bundle check" - expect(exitstatus).to eq(0) if exitstatus + bundle "exec bundle check", env: { "PATH" => path } end it "works when using path gems from the same path and the version is specified" do - build_lib "foo", :path => lib_path("nested/foo") - build_lib "bar", :path => lib_path("nested/bar") + build_lib "foo", path: lib_path("nested/foo") + build_lib "bar", path: lib_path("nested/bar") gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :path => "#{lib_path("nested")}" gem "bar", :path => "#{lib_path("nested")}" G - bundle! :install - bundle! "install --deployment" + bundle :install + bundle_config "deployment true" + bundle :install + end + + it "works when path gems are specified twice" do + build_lib "foo", path: lib_path("nested/foo") + gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("nested/foo")}" + gem "foo", :path => "#{lib_path("nested/foo")}" + G + + bundle :install + bundle_config "deployment true" + bundle :install end it "works when there are credentials in the source URL" do - install_gemfile(<<-G, :artifice => "endpoint_strict_basic_authentication", :quiet => true) + install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true) source "http://user:pass@localgemserver.test/" - gem "rack-obama", ">= 1.0" + gem "myrack-obama", ">= 1.0" G - bundle "install --deployment", :artifice => "endpoint_strict_basic_authentication" - - expect(exitstatus).to eq(0) if exitstatus + bundle_config "deployment true" + bundle :install, artifice: "endpoint_strict_basic_authentication" end it "works with sources given by a block" do install_gemfile <<-G - source "file://#{gem_repo1}" do - gem "rack" + source "https://gem.repo1" + source "https://gem.repo1" do + gem "myrack" end G - bundle "install --deployment" + bundle_config "deployment true" + bundle :install - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" + end + + context "when replacing a host with the same host with credentials" do + before do + bundle_config "path vendor/bundle" + bundle "install" + gemfile <<-G + source "http://user_name:password@localgemserver.test/" + gem "myrack" + G + + lockfile <<-G + GEM + remote: http://localgemserver.test/ + specs: + myrack (1.0.0) + + PLATFORMS + #{generic_local_platform} + + DEPENDENCIES + myrack + G + + bundle_config "deployment true" + end + + it "allows the replace" do + bundle :install + + expect(out).to match(/Bundle complete!/) + end end describe "with an existing lockfile" do @@ -111,189 +141,351 @@ RSpec.describe "install with --deployment or --frozen" do bundle "install" end - it "works with the --deployment flag if you didn't change anything" do - bundle "install --deployment" - expect(exitstatus).to eq(0) if exitstatus + it "installs gems by default to vendor/bundle" do + bundle_config "deployment true" + expect do + bundle "install" + end.not_to change { bundled_app_lock.mtime } + expect(out).to include("vendor/bundle") + end + + it "installs gems to custom path if specified" do + bundle_config "path vendor/bundle2" + bundle_config "deployment true" + bundle "install" + expect(out).to include("vendor/bundle2") end - it "works with the --frozen flag if you didn't change anything" do - bundle "install --frozen" - expect(exitstatus).to eq(0) if exitstatus + it "installs gems to custom path if specified, even when configured through ENV" do + bundle_config "deployment true" + bundle "install", env: { "BUNDLE_PATH" => "vendor/bundle2" } + expect(out).to include("vendor/bundle2") end - it "explodes with the --deployment flag if you make a change and don't check in the lockfile" do + it "works with the `frozen` setting" do + bundle_config "frozen true" + expect do + bundle "install" + end.not_to change { bundled_app_lock.mtime } + end + + it "works with BUNDLE_FROZEN if you didn't change anything" do + expect do + bundle :install, env: { "BUNDLE_FROZEN" => "true" } + end.not_to change { bundled_app_lock.mtime } + end + + it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G - bundle "install --deployment" - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile") - expect(out).to include("* rack-obama") - expect(out).not_to include("You have deleted from the Gemfile") - expect(out).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have added to the Gemfile") + expect(err).to include("* myrack-obama") + expect(err).not_to include("You have deleted from the Gemfile") + expect(err).not_to include("You have changed in the Gemfile") + end + + it "works if a path gem is missing but is in a without group" do + build_lib "path_gem" + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development + G + expect(the_bundle).to include_gems "path_gem 1.0" + FileUtils.rm_r lib_path("path_gem-1.0") + + bundle_config "path .bundle" + bundle_config "without development" + bundle_config "deployment true" + bundle :install, env: { "DEBUG" => "1" } + run "puts :WIN" + expect(out).to eq("WIN") + end + + it "works if a gem is missing, but it's on a different platform" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo1" do + gem "rake", platform: :#{not_local_tag} + end + G + + bundle :install, env: { "BUNDLE_FROZEN" => "true" } + expect(last_command).to be_success + end + + it "shows a good error if a gem is missing from the lockfile" do + build_repo4 do + build_gem "foo" + build_gem "bar" + end + + gemfile <<-G + source "https://gem.repo4" + + gem "foo" + gem "bar" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + bar + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false, artifice: "compact_index" + expect(err).to include("Your lockfile is missing \"bar\", but can't be updated because frozen mode is set") + end + + it "explodes if a path gem is missing" do + build_lib "path_gem" + install_gemfile <<-G + source "https://gem.repo1" + gem "rake" + gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development + G + expect(the_bundle).to include_gems "path_gem 1.0" + FileUtils.rm_r lib_path("path_gem-1.0") + + bundle_config "path .bundle" + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("The path `#{lib_path("path_gem-1.0")}` does not exist.") end it "can have --frozen set via an environment variable" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G ENV["BUNDLE_FROZEN"] = "1" + bundle "install", raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have added to the Gemfile") + expect(err).to include("* myrack-obama") + expect(err).not_to include("You have deleted from the Gemfile") + expect(err).not_to include("You have changed in the Gemfile") + end + + it "can have --deployment set via an environment variable" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + ENV["BUNDLE_DEPLOYMENT"] = "true" + bundle "install", raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have added to the Gemfile") + expect(err).to include("* myrack-obama") + expect(err).not_to include("You have deleted from the Gemfile") + expect(err).not_to include("You have changed in the Gemfile") + end + + it "installs gems by default to vendor/bundle when deployment mode is set via an environment variable" do + ENV["BUNDLE_DEPLOYMENT"] = "true" bundle "install" - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile") - expect(out).to include("* rack-obama") - expect(out).not_to include("You have deleted from the Gemfile") - expect(out).not_to include("You have changed in the Gemfile") + expect(out).to include("vendor/bundle") + end + + it "installs gems to custom path when deployment mode is set via an environment variable " do + ENV["BUNDLE_DEPLOYMENT"] = "true" + ENV["BUNDLE_PATH"] = "vendor/bundle2" + bundle "install" + expect(out).to include("vendor/bundle2") end it "can have --frozen set to false via an environment variable" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G ENV["BUNDLE_FROZEN"] = "false" + ENV["BUNDLE_DEPLOYMENT"] = "false" bundle "install" - expect(out).not_to include("deployment mode") + expect(out).not_to include("frozen mode") expect(out).not_to include("You have added to the Gemfile") - expect(out).not_to include("* rack-obama") + expect(out).not_to include("* myrack-obama") end - it "explodes with the --frozen flag if you make a change and don't check in the lockfile" do + it "explodes if you replace a gem and don't check in the lockfile" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama", "1.1" + source "https://gem.repo1" + gem "activesupport" G - bundle "install --frozen" - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile") - expect(out).to include("* rack-obama (= 1.1)") - expect(out).not_to include("You have deleted from the Gemfile") - expect(out).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have added to the Gemfile:\n* activesupport\n\n") + expect(err).to include("You have deleted from the Gemfile:\n* myrack") + expect(err).not_to include("You have changed in the Gemfile") end it "explodes if you remove a gem and don't check in the lockfile" do + gemfile 'source "https://gem.repo1"' + + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("Some dependencies were deleted") + expect(err).to include("frozen mode") + expect(err).to include("You have deleted from the Gemfile:\n* myrack") + expect(err).not_to include("You have changed in the Gemfile") + end + + it "explodes if you add a source" do gemfile <<-G - source "file://#{gem_repo1}" - gem "activesupport" + source "https://gem.repo1" + gem "myrack", :git => "git://hubz.com" G - bundle "install --deployment" - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile:\n* activesupport\n\n") - expect(out).to include("You have deleted from the Gemfile:\n* rack") - expect(out).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).not_to include("You have added to the Gemfile") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `no specified source` to `git://hubz.com`") end - it "explodes if you add a source" do + it "explodes if you change a source from git to the default" do + build_git "myrack" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-1.0")}" + G + gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "git://hubz.com" + source "https://gem.repo1" + gem "myrack" G - bundle "install --deployment" - expect(out).to include("deployment mode") - expect(out).to include("You have added to the Gemfile:\n* source: git://hubz.com (at master)") - expect(out).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).not_to include("You have deleted from the Gemfile") + expect(err).not_to include("You have added to the Gemfile") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack-1.0")}` to `no specified source`") end - it "explodes if you unpin a source" do - build_git "rack" + it "explodes if you change a source from git to the default, in presence of other git sources" do + build_lib "foo", path: lib_path("myrack/foo") + build_git "myrack", path: lib_path("myrack") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-1.0")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + gem "foo", :git => "#{lib_path("myrack")}" G gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" + gem "foo", :git => "#{lib_path("myrack")}" G - bundle "install --deployment" - expect(out).to include("deployment mode") - expect(out).to include("You have deleted from the Gemfile:\n* source: #{lib_path("rack-1.0")} (at master@#{revision_for(lib_path("rack-1.0"))[0..6]}") - expect(out).not_to include("You have added to the Gemfile") - expect(out).not_to include("You have changed in the Gemfile") + bundle_config "deployment true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `no specified source`") + expect(err).not_to include("You have added to the Gemfile") + expect(err).not_to include("You have deleted from the Gemfile") end - it "explodes if you unpin a source, leaving it pinned somewhere else" do - build_lib "foo", :path => lib_path("rack/foo") - build_git "rack", :path => lib_path("rack") + it "explodes if you change a source from path to git" do + build_git "myrack", path: lib_path("myrack") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack")}" - gem "foo", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :path => "#{lib_path("myrack")}" G gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "foo", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :git => "https:/my-git-repo-for-myrack" G - bundle "install --deployment" - expect(out).to include("deployment mode") - expect(out).to include("You have changed in the Gemfile:\n* rack from `no specified source` to `#{lib_path("rack")} (at master@#{revision_for(lib_path("rack"))[0..6]})`") - expect(out).not_to include("You have added to the Gemfile") - expect(out).not_to include("You have deleted from the Gemfile") + bundle_config "frozen true" + bundle :install, raise_on_error: false + expect(err).to include("frozen mode") + expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `https:/my-git-repo-for-myrack`") + expect(err).not_to include("You have added to the Gemfile") + expect(err).not_to include("You have deleted from the Gemfile") end it "remembers that the bundle is frozen at runtime" do - bundle "install --deployment" + bundle :lock + + bundle_config "deployment true" gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "1.0.0" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "myrack-obama" G - expect(the_bundle).not_to include_gems "rack 1.0.0" - expect(err).to include strip_whitespace(<<-E).strip -The dependencies in your gemfile changed + run "require 'myrack'", raise_on_error: false + expect(err).to include <<~E.strip + The dependencies in your gemfile changed, but the lockfile can't be updated because frozen mode is set (Bundler::ProductionError) -You have added to the Gemfile: -* rack (= 1.0.0) -* rack-obama + You have added to the Gemfile: + * myrack (= 1.0.0) + * myrack-obama -You have deleted from the Gemfile: -* rack + You have deleted from the Gemfile: + * myrack E end end context "with path in Gemfile and packed" do it "works fine after bundle package and bundle install --local" do - build_lib "foo", :path => lib_path("foo") - install_gemfile! <<-G + build_lib "foo", path: lib_path("foo") + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo")}" G - bundle! :install + bundle :install expect(the_bundle).to include_gems "foo 1.0" - bundle! "package --all" + + bundle :cache expect(bundled_app("vendor/cache/foo")).to be_directory - bundle! "install --local" - expect(out).to include("Using foo 1.0 from source at") - expect(out).to include("vendor/cache/foo") + bundle "install --local" + expect(out).to include("Updating files in vendor/cache") - simulate_new_machine - bundle! "install --deployment --verbose" - expect(out).not_to include("You are trying to install in deployment mode after changing your Gemfile") + pristine_system_gems + bundle_config "deployment true" + bundle "install --verbose" + expect(out).not_to include("can't be updated because frozen mode is set") expect(out).not_to include("You have added to the Gemfile") expect(out).not_to include("You have deleted from the Gemfile") - expect(out).to include("Using foo 1.0 from source at") expect(out).to include("vendor/cache/foo") expect(the_bundle).to include_gems "foo 1.0" end diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb index 738b2cf1bd..32ca455439 100644 --- a/spec/bundler/install/failure_spec.rb +++ b/spec/bundler/install/failure_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do context "installing a gem fails" do - it "prints out why that gem was being installed" do + it "prints out why that gem was being installed and the underlying error" do build_repo2 do build_gem "activesupport", "2.3.2" do |s| s.extensions << "Rakefile" @@ -15,13 +14,13 @@ RSpec.describe "bundle install" do end end - install_gemfile <<-G - source "file:#{gem_repo2}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "rails" G - expect(out).to end_with(<<-M.strip) + expect(err).to start_with("Gem::Ext::BuildError: ERROR: Failed to build gem native extension.") + expect(err).to end_with(<<-M.strip) An error occurred while installing activesupport (2.3.2), and Bundler cannot continue. -Make sure that `gem install activesupport -v '2.3.2'` succeeds before bundling. In Gemfile: rails was resolved to 2.3.2, which depends on @@ -29,5 +28,58 @@ In Gemfile: activesupport M end + + context "because the downloaded .gem was invalid" do + before do + build_repo4 do + build_gem "a" + end + + gem_repo4("gems", "a-1.0.gem").open("w") {|f| f << "<html></html>" } + end + + it "removes the downloaded .gem" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "a" + G + + expect(default_bundle_path("cache", "a-1.0.gem")).not_to exist + end + end + end + + context "when lockfile dependencies don't match the gemspec" do + before do + build_repo4 do + build_gem "myrack", "1.0.0" do |s| + s.add_dependency "myrack-test", "~> 1.0" + end + + build_gem "myrack-test", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + # First install to generate lockfile + bundle :install + + # Manually edit lockfile to have incorrect dependencies + lockfile_content = File.read(bundled_app_lock) + # Remove the myrack-test dependency from myrack + lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n") + File.write(bundled_app_lock, lockfile_content) + end + + it "reports the mismatch with detailed information" do + bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } + + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0") + expect(err).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") + expect(err).to include("Please run `bundle install` to regenerate the lockfile.") + end end end diff --git a/spec/bundler/install/force_spec.rb b/spec/bundler/install/force_spec.rb index dc4956a7ae..e0f6fb6364 100644 --- a/spec/bundler/install/force_spec.rb +++ b/spec/bundler/install/force_spec.rb @@ -1,36 +1,31 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do - describe "with --force" do - before :each do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - end + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + shared_examples_for "an option to force reinstalling gems" do it "re-installs installed gems" do - rack_lib = default_bundle_path("gems/rack-1.0.0/lib/rack.rb") + myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb") - bundle "install" - rack_lib.open("w") {|f| f.write("blah blah blah") } - bundle "install --force" + bundle :install + myrack_lib.open("w") {|f| f.write("blah blah blah") } + bundle :install, flag => true - expect(exitstatus).to eq(0) if exitstatus - expect(out).to include "Using bundler" - expect(out).to include "Installing rack 1.0.0" - expect(rack_lib.open(&:read)).to eq("RACK = '1.0.0'\n") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(out).to include "Installing myrack 1.0.0" + expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n") + expect(the_bundle).to include_gems "myrack 1.0.0" end it "works on first bundle install" do - bundle "install --force" + bundle :install, flag => true - expect(exitstatus).to eq(0) if exitstatus - expect(out).to include "Using bundler" - expect(out).to include "Installing rack 1.0.0" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(out).to include "Installing myrack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end context "with a git gem" do @@ -38,6 +33,7 @@ RSpec.describe "bundle install" do before do gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G end @@ -45,23 +41,31 @@ RSpec.describe "bundle install" do it "re-installs installed gems" do foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb") - bundle! "install" + bundle :install foo_lib.open("w") {|f| f.write("blah blah blah") } - bundle! "install --force" + bundle :install, flag => true - expect(out).to include "Using bundler" - expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" expect(foo_lib.open(&:read)).to eq("FOO = '1.0'\n") expect(the_bundle).to include_gems "foo 1.0" end it "works on first bundle install" do - bundle! "install --force" + bundle :install, flag => true - expect(out).to include "Using bundler" - expect(out).to include "Using foo 1.0 from #{lib_path("foo-1.0")} (at master@#{ref[0, 7]})" expect(the_bundle).to include_gems "foo 1.0" end end end + + describe "with --force" do + it_behaves_like "an option to force reinstalling gems" do + let(:flag) { "force" } + end + end + + describe "with --redownload" do + it_behaves_like "an option to force reinstalling gems" do + let(:flag) { "redownload" } + end + end end diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb index f02223d34d..3afa4f5daa 100644 --- a/spec/bundler/install/gemfile/eval_gemfile_spec.rb +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with gemfile that uses eval_gemfile" do before do - build_lib("gunks", :path => bundled_app.join("gems/gunks")) do |s| + build_lib("gunks", path: bundled_app("gems/gunks")) do |s| s.name = "gunks" s.version = "0.0.1" end @@ -11,43 +10,83 @@ RSpec.describe "bundle install with gemfile that uses eval_gemfile" do context "eval-ed Gemfile points to an internal gemspec" do before do - create_file "Gemfile-other", <<-G + gemfile "Gemfile-other", <<-G + source "https://gem.repo1" gemspec :path => 'gems/gunks' G end it "installs the gemspec specified gem" do install_gemfile <<-G + source "https://gem.repo1" eval_gemfile 'Gemfile-other' G expect(out).to include("Resolving dependencies") - expect(out).to include("Using gunks 0.0.1 from source at `gems/gunks`") expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}" + end + end + + context "eval-ed Gemfile points to an internal gemspec and uses a scoped source that duplicates the main Gemfile global source" do + before do + build_repo2 do + build_gem "rails", "6.1.3.2" + + build_gem "zip-zip", "0.3" + end + + gemfile bundled_app("gems/Gemfile"), <<-G + source "https://gem.repo2" + + gemspec :path => "\#{__dir__}/gunks" + + source "https://gem.repo2" do + gem "zip-zip" + end + G + end + + it "installs and finds gems correctly" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "rails" + + eval_gemfile File.join(__dir__, "gems/Gemfile") + G + expect(out).to include("Resolving dependencies") + expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "rails 6.1.3.2" end end context "eval-ed Gemfile has relative-path gems" do before do - build_lib("a", :path => "gems/a") - create_file "nested/Gemfile-nested", <<-G + build_lib("a", path: bundled_app("gems/a")) + gemfile bundled_app("nested/Gemfile-nested"), <<-G + source "https://gem.repo1" gem "a", :path => "../gems/a" G gemfile <<-G + source "https://gem.repo1" eval_gemfile "nested/Gemfile-nested" G end it "installs the path gem" do - bundle! :install + bundle :install expect(the_bundle).to include_gem("a 1.0") end # Make sure that we are properly comparing path based gems between the # parsed lockfile and the evaluated gemfile. - it "bundles with --deployment" do - bundle! :install - bundle! "install --deployment" + it "bundles with deployment mode configured" do + bundle :install + bundle_config "deployment true" + bundle :install end end @@ -56,12 +95,28 @@ RSpec.describe "bundle install with gemfile that uses eval_gemfile" do it "installs the gemspec specified gem" do install_gemfile <<-G + source "https://gem.repo1" eval_gemfile 'other/Gemfile-other' gemspec :path => 'gems/gunks' G expect(out).to include("Resolving dependencies") - expect(out).to include("Using gunks 0.0.1 from source at `gems/gunks`") expect(out).to include("Bundle complete") + + expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}" + end + end + + context "eval-ed Gemfile references other gemfiles" do + it "works with relative paths" do + gemfile "other/Gemfile-other", "gem 'myrack'" + gemfile "other/Gemfile", "eval_gemfile 'Gemfile-other'" + gemfile "Gemfile-alt", <<-G + source "https://gem.repo1" + eval_gemfile "other/Gemfile" + G + install_gemfile "eval_gemfile File.expand_path('Gemfile-alt')" + + expect(the_bundle).to include_gem "myrack 1.0.0" end end end diff --git a/spec/bundler/install/gemfile/force_ruby_platform_spec.rb b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb new file mode 100644 index 0000000000..bcc1f36823 --- /dev/null +++ b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with force_ruby_platform DSL option", :jruby do + context "when no transitive deps" do + before do + build_repo4 do + # Build a gem with platform specific versions + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + + # Build the exact same gem with a different name to compare using vs not using the option + build_gem("platform_specific_forced") + + build_gem("platform_specific_forced") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "pulls the pure ruby variant of the given gem" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "platform_specific_forced", :force_ruby_platform => true + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}" + end + + it "still respects a global `force_ruby_platform` config" do + install_gemfile <<-G, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" } + source "https://gem.repo4" + + gem "platform_specific_forced", :force_ruby_platform => true + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + + context "when also a transitive dependency" do + before do + build_repo4 do + build_gem("depends_on_platform_specific") {|s| s.add_dependency "platform_specific" } + + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "still pulls the ruby variant" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "depends_on_platform_specific" + gem "platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + + context "with transitive dependencies with platform specific versions" do + before do + build_repo4 do + build_gem("depends_on_platform_specific") do |s| + s.add_dependency "platform_specific" + end + + build_gem("depends_on_platform_specific") do |s| + s.add_dependency "platform_specific" + s.platform = Bundler.local_platform + end + + build_gem("platform_specific") + + build_gem("platform_specific") do |s| + s.platform = Bundler.local_platform + end + end + end + + it "ignores ruby variants for the transitive dependencies" do + install_gemfile <<-G, env: { "DEBUG_RESOLVER" => "true" } + source "https://gem.repo4" + + gem "depends_on_platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "depends_on_platform_specific 1.0 ruby" + expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}" + end + + it "reinstalls the ruby variant when a platform specific variant is already installed, the lockile has only ruby platform, and :force_ruby_platform is used in the Gemfile" do + skip "Can't simulate platform reliably on JRuby, installing a platform specific gem fails to activate io-wait because only the -java version is present, and we're simulating a different platform" if RUBY_ENGINE == "jruby" + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + platform_specific (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86-darwin-100" do + system_gems "platform_specific-1.0-x86-darwin-100", path: default_bundle_path + + install_gemfile <<-G, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, artifice: "compact_index" + source "https://gem.repo4" + + gem "platform_specific", :force_ruby_platform => true + G + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + end + end + end +end diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb index 1ea613c9d2..e51fc9247d 100644 --- a/spec/bundler/install/gemfile/gemspec_spec.rb +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -1,211 +1,216 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install from an existing gemspec" do before(:each) do - build_gem "bar", :to_system => true - build_gem "bar-dev", :to_system => true + build_repo2 do + build_gem "bar" + build_gem "bar-dev" + end end it "should install runtime and development dependencies" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("Gemfile", "source :rubygems\ngemspec") s.add_dependency "bar", "=1.0.0" s.add_development_dependency "bar-dev", "=1.0.0" end install_gemfile <<-G - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G expect(the_bundle).to include_gems "bar 1.0.0" - expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development end it "that is hidden should install runtime and development dependencies" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("Gemfile", "source :rubygems\ngemspec") s.add_dependency "bar", "=1.0.0" s.add_development_dependency "bar-dev", "=1.0.0" end - FileUtils.mv tmp.join("foo", "foo.gemspec"), tmp.join("foo", ".gemspec") + FileUtils.mv tmp("foo", "foo.gemspec"), tmp("foo", ".gemspec") install_gemfile <<-G - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G expect(the_bundle).to include_gems "bar 1.0.0" - expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development end it "should handle a list of requirements" do - build_gem "baz", "1.0", :to_system => true - build_gem "baz", "1.1", :to_system => true + update_repo2 do + build_gem "baz", "1.0" + build_gem "baz", "1.1" + end - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("Gemfile", "source :rubygems\ngemspec") s.add_dependency "baz", ">= 1.0", "< 1.1" end install_gemfile <<-G - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}' + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G expect(the_bundle).to include_gems "baz 1.0" end it "should raise if there are no gemspecs available" do - build_lib("foo", :path => tmp.join("foo"), :gemspec => false) + build_lib("foo", path: tmp("foo"), gemspec: false) - error = install_gemfile(<<-G) - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G - expect(error).to match(/There are no gemspecs at #{tmp.join('foo')}/) + expect(err).to match(/There are no gemspecs at #{tmp("foo")}/) end it "should raise if there are too many gemspecs available" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby) end - error = install_gemfile(<<-G) - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G - expect(error).to match(/There are multiple gemspecs at #{tmp.join('foo')}/) + expect(err).to match(/There are multiple gemspecs at #{tmp("foo")}/) end it "should pick a specific gemspec" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("foo2.gemspec", "") s.add_dependency "bar", "=1.0.0" s.add_development_dependency "bar-dev", "=1.0.0" end install_gemfile(<<-G) - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gems "bar 1.0.0" - expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development end it "should use a specific group for development dependencies" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.write("foo2.gemspec", "") s.add_dependency "bar", "=1.0.0" s.add_development_dependency "bar-dev", "=1.0.0" end install_gemfile(<<-G) - source "file://#{gem_repo2}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo', :development_group => :dev + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}', :name => 'foo', :development_group => :dev G expect(the_bundle).to include_gems "bar 1.0.0" - expect(the_bundle).not_to include_gems "bar-dev 1.0.0", :groups => :development - expect(the_bundle).to include_gems "bar-dev 1.0.0", :groups => :dev + expect(the_bundle).not_to include_gems "bar-dev 1.0.0", groups: :development + expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :dev end it "should match a lockfile even if the gemspec defines development dependencies" do - build_lib("foo", :path => tmp.join("foo")) do |s| - s.write("Gemfile", "source 'file://#{gem_repo1}'\ngemspec") + build_lib("foo", path: tmp("foo")) do |s| + s.write("Gemfile", "source 'https://gem.repo1'\ngemspec") s.add_dependency "actionpack", "=2.3.2" - s.add_development_dependency "rake", "=10.0.2" + s.add_development_dependency "rake", rake_version end - Dir.chdir(tmp.join("foo")) do - bundle "install" - # This should really be able to rely on $stderr, but, it's not written - # right, so we can't. In fact, this is a bug negation test, and so it'll - # ghost pass in future, and will only catch a regression if the message - # doesn't change. Exit codes should be used correctly (they can be more - # than just 0 and 1). - output = bundle("install --deployment") - expect(output).not_to match(/You have added to the Gemfile/) - expect(output).not_to match(/You have deleted from the Gemfile/) - expect(output).not_to match(/install in deployment mode after changing/) - end + bundle "install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + # This should really be able to rely on $stderr, but, it's not written + # right, so we can't. In fact, this is a bug negation test, and so it'll + # ghost pass in future, and will only catch a regression if the message + # doesn't change. Exit codes should be used correctly (they can be more + # than just 0 and 1). + bundle_config "deployment true" + output = bundle("install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }) + expect(output).not_to match(/You have added to the Gemfile/) + expect(output).not_to match(/You have deleted from the Gemfile/) + expect(output).not_to match(/the lockfile can't be updated because frozen mode is set/) end it "should match a lockfile without needing to re-resolve" do - build_lib("foo", :path => tmp.join("foo")) do |s| - s.add_dependency "rack" + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack" end - install_gemfile! <<-G - source "file://#{gem_repo1}" - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' G - bundle! "install", :verbose => true - expect(out).to include("Found no changes, using resolution from the lockfile") + bundle "install", verbose: true + + message = "Found no changes, using resolution from the lockfile" + expect(out.scan(message).size).to eq(1) end it "should match a lockfile without needing to re-resolve with development dependencies" do - simulate_platform java + simulate_platform "java" do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack" + s.add_development_dependency "thin" + end - build_lib("foo", :path => tmp.join("foo")) do |s| - s.add_dependency "rack" - s.add_development_dependency "thin" - end + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' + G - install_gemfile! <<-G - source "file://#{gem_repo1}" - gemspec :path => '#{tmp.join("foo")}' - G + bundle "install", verbose: true - bundle! "install", :verbose => true - expect(out).to include("Found no changes, using resolution from the lockfile") + message = "Found no changes, using resolution from the lockfile" + expect(out.scan(message).size).to eq(1) + end end - it "should match a lockfile on non-ruby platforms with a transitive platform dependency" do - simulate_platform java - simulate_ruby_engine "jruby" - - build_lib("foo", :path => tmp.join("foo")) do |s| + it "should match a lockfile on non-ruby platforms with a transitive platform dependency", :jruby_only do + build_lib("foo", path: tmp("foo")) do |s| s.add_dependency "platform_specific" end - install_gem "platform_specific-1.0-java" + system_gems "platform_specific-1.0-java", path: default_bundle_path - install_gemfile! <<-G - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G + gemspec :path => '#{tmp("foo")}' G - bundle! "update --bundler", :verbose => true - expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 JAVA" + bundle "update --bundler", artifice: "compact_index", verbose: true + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 java" end it "should evaluate the gemspec in its directory" do - build_lib("foo", :path => tmp.join("foo")) - File.open(tmp.join("foo/foo.gemspec"), "w") do |s| - s.write "raise 'ahh' unless Dir.pwd == '#{tmp.join("foo")}'" + build_lib("foo", path: tmp("foo")) + File.open(tmp("foo/foo.gemspec"), "w") do |s| + s.write "raise 'ahh' unless Dir.pwd == '#{tmp("foo")}'" end - install_gemfile <<-G - gemspec :path => '#{tmp.join("foo")}' + install_gemfile <<-G, raise_on_error: false + gemspec :path => '#{tmp("foo")}' G - expect(@err).not_to match(/ahh/) + expect(stdboth).not_to include("ahh") end it "allows the gemspec to activate other gems" do - # see https://github.com/bundler/bundler/issues/5409 + ENV["BUNDLE_PATH__SYSTEM"] = "true" + # see https://github.com/rubygems/bundler/issues/5409 # # issue was caused by rubygems having an unresolved gem during a require, # so emulate that - system_gems %w(rack-1.0.0 rack-0.9.1 rack-obama-1.0) + system_gems %w[myrack-1.0.0 myrack-0.9.1 myrack-obama-1.0] - build_lib("foo", :path => bundled_app) + build_lib("foo", path: bundled_app) gemspec = bundled_app("foo.gemspec").read bundled_app("foo.gemspec").open("w") do |f| - f.write "#{gemspec.strip}.tap { gem 'rack-obama'; require 'rack-obama' }" + f.write "#{gemspec.strip}.tap { gem 'myrack-obama'; require 'myrack/obama' }" end - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gemspec G @@ -213,69 +218,104 @@ RSpec.describe "bundle install from an existing gemspec" do end it "allows conflicts" do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.version = "1.0.0" s.add_dependency "bar", "= 1.0.0" end - build_gem "deps", :to_system => true do |s| + build_gem "deps", to_bundle: true do |s| s.add_dependency "foo", "= 0.0.1" end - build_gem "foo", "0.0.1", :to_system => true + build_gem "foo", "0.0.1", to_bundle: true install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "deps" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gems "foo 1.0.0" end - it "does not break Gem.finish_resolve with conflicts", :rubygems => ">= 2" do - build_lib("foo", :path => tmp.join("foo")) do |s| + it "does not break Gem.finish_resolve with conflicts" do + build_lib("foo", path: tmp("foo")) do |s| s.version = "1.0.0" s.add_dependency "bar", "= 1.0.0" end - build_repo2 do + update_repo2 do build_gem "deps" do |s| s.add_dependency "foo", "= 0.0.1" end build_gem "foo", "0.0.1" end - install_gemfile! <<-G - source "file://#{gem_repo2}" + install_gemfile <<-G + source "https://gem.repo2" gem "deps" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + gemspec :path => '#{tmp("foo")}', :name => 'foo' + G + + expect(the_bundle).to include_gems "foo 1.0.0" + + run "Gem.finish_resolve; puts 'WIN'" + expect(out).to eq("WIN") + end + + it "does not make Gem.try_activate warn when local gem has extensions" do + build_lib("foo", path: tmp("foo")) do |s| + s.version = "1.0.0" + s.add_c_extension + end + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' G expect(the_bundle).to include_gems "foo 1.0.0" - run! "Gem.finish_resolve; puts 'WIN'" + run "Gem.try_activate('irb/lc/es/error.rb'); puts 'WIN'" expect(out).to eq("WIN") + expect(err).to be_empty + end + + it "handles downgrades" do + build_lib "omg", "2.0", path: lib_path("omg") + + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => "#{lib_path("omg")}" + G + + build_lib "omg", "1.0", path: lib_path("omg") + + bundle :install + + expect(the_bundle).to include_gems "omg 1.0" end context "in deployment mode" do context "when the lockfile was not updated after a change to the gemspec's dependencies" do it "reports that installation failed" do - build_lib "cocoapods", :path => bundled_app do |s| + build_lib "cocoapods", path: bundled_app do |s| s.add_dependency "activesupport", ">= 1" end - install_gemfile! <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G + source "https://gem.repo1" gemspec G expect(the_bundle).to include_gems("cocoapods 1.0", "activesupport 2.3.5") - build_lib "cocoapods", :path => bundled_app do |s| + build_lib "cocoapods", path: bundled_app do |s| s.add_dependency "activesupport", ">= 1.0.1" end - bundle "install --deployment" + bundle_config "deployment true" + bundle :install, raise_on_error: false - expect(out).to include("changed") + expect(err).to include("changed") end end end @@ -283,119 +323,93 @@ RSpec.describe "bundle install from an existing gemspec" do context "when child gemspecs conflict with a released gemspec" do before do # build the "parent" gem that depends on another gem in the same repo - build_lib "source_conflict", :path => bundled_app do |s| - s.add_dependency "rack_middleware" + build_lib "source_conflict", path: bundled_app do |s| + s.add_dependency "myrack_middleware" end # build the "child" gem that is the same version as a released gem, but # has completely different and conflicting dependency requirements - build_lib "rack_middleware", "1.0", :path => bundled_app("rack_middleware") do |s| - s.add_dependency "rack", "1.0" # anything other than 0.9.1 + build_lib "myrack_middleware", "1.0", path: bundled_app("myrack_middleware") do |s| + s.add_dependency "myrack", "1.0" # anything other than 0.9.1 end end it "should install the child gemspec's deps" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gemspec G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end context "with a lockfile and some missing dependencies" do let(:source_uri) { "http://localgemserver.test" } - context "previously bundled for Ruby" do - let(:platform) { "ruby" } - let(:explicit_platform) { false } - - before do - build_lib("foo", :path => tmp.join("foo")) do |s| - s.add_dependency "rack", "=1.0.0" - end - - if explicit_platform - create_file( - tmp.join("foo", "foo-#{platform}.gemspec"), - build_spec("foo", "1.0", platform) do - dep "rack", "=1.0.0" - @spec.authors = "authors" - @spec.summary = "summary" - end.first.to_ruby - ) - end - - gemfile <<-G - source "#{source_uri}" - gemspec :path => "../foo" - G - - lockfile <<-L - PATH - remote: ../foo - specs: - foo (1.0) - rack (= 1.0.0) - - GEM - remote: #{source_uri} - specs: - rack (1.0.0) - - PLATFORMS - #{generic_local_platform} - - DEPENDENCIES - foo! - - BUNDLED WITH - #{Bundler::VERSION} - L + before do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack", "=1.0.0" end - context "using JRuby with explicit platform" do - let(:platform) { "java" } - let(:explicit_platform) { true } + gemfile <<-G + source "#{source_uri}" + gemspec :path => "../foo" + G - it "should install" do - simulate_ruby_engine "jruby" do - simulate_platform "java" do - results = bundle "install", :artifice => "endpoint" - expect(results).to include("Installing rack 1.0.0") - expect(the_bundle).to include_gems "rack 1.0.0" - end - end - end + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" end - context "using JRuby" do - let(:platform) { "java" } + lockfile <<-L + PATH + remote: ../foo + specs: + foo (1.0) + myrack (= 1.0.0) + + GEM + remote: #{source_uri} + specs: + myrack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end - it "should install" do - simulate_ruby_engine "jruby" do - simulate_platform "java" do - results = bundle "install", :artifice => "endpoint" - expect(results).to include("Installing rack 1.0.0") - expect(the_bundle).to include_gems "rack 1.0.0" - end - end - end + context "using JRuby with explicit platform", :jruby_only do + before do + create_file( + tmp("foo", "foo-java.gemspec"), + build_spec("foo", "1.0", "java") do + dep "myrack", "=1.0.0" + @spec.authors = "authors" + @spec.summary = "summary" + end.first.to_ruby + ) end - context "using Windows" do - it "should install" do - simulate_windows do - results = bundle "install", :artifice => "endpoint" - expect(results).to include("Installing rack 1.0.0") - expect(the_bundle).to include_gems "rack 1.0.0" - end - end + it "should install" do + results = bundle "install", artifice: "endpoint" + expect(results).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" end end - context "bundled for ruby and jruby" do + it "should install", :jruby do + results = bundle "install", artifice: "endpoint" + expect(results).to include("Installing myrack 1.0.0") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + context "bundled for multiple platforms" do let(:platform_specific_type) { :runtime } let(:dependency) { "platform_specific" } before do @@ -405,36 +419,49 @@ RSpec.describe "bundle install from an existing gemspec" do end end - build_lib "foo", :path => "." do |s| - if platform_specific_type == :runtime + build_lib "foo", path: bundled_app do |s| + case platform_specific_type + when :runtime s.add_runtime_dependency dependency - elsif platform_specific_type == :development + when :development s.add_development_dependency dependency else - raise "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + raise ArgumentError, "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" end end - %w(ruby jruby).each do |platform| - simulate_platform(platform) do - install_gemfile <<-G - source "file://#{gem_repo2}" - gemspec - G - end - end + gemfile <<-G + source "https://gem.repo2" + gemspec + G + + bundle_config "force_ruby_platform true" + bundle "install" + + simulate_new_machine + simulate_platform("jruby") { bundle "install" } + expect(lockfile).to include("platform_specific (1.0-java)") + simulate_platform("x64-mingw-ucrt") { bundle "install" } end context "on ruby" do before do - simulate_platform("ruby") + bundle_config "force_ruby_platform true" bundle :install end context "as a runtime dependency" do - it "keeps java dependencies in the lockfile" do - expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" - expect(lockfile).to eq strip_whitespace(<<-L) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: @@ -442,20 +469,22 @@ RSpec.describe "bundle install from an existing gemspec" do platform_specific GEM - remote: file:#{gem_repo2}/ + remote: https://gem.repo2/ specs: platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -463,30 +492,40 @@ RSpec.describe "bundle install from an existing gemspec" do context "as a development dependency" do let(:platform_specific_type) { :development } - it "keeps java dependencies in the lockfile" do - expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" - expect(lockfile).to eq strip_whitespace(<<-L) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: foo (1.0) GEM - remote: file:#{gem_repo2}/ + remote: https://gem.repo2/ specs: platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! platform_specific - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -495,32 +534,43 @@ RSpec.describe "bundle install from an existing gemspec" do let(:platform_specific_type) { :development } let(:dependency) { "indirect_platform_specific" } - it "keeps java dependencies in the lockfile" do - expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY" - expect(lockfile).to eq strip_whitespace(<<-L) + it "keeps all platform dependencies in the lockfile" do + expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 ruby" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo2, "indirect_platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0" + c.checksum gem_repo2, "platform_specific", "1.0", "java" + c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt" + end + + expect(lockfile).to eq <<~L PATH remote: . specs: foo (1.0) GEM - remote: file:#{gem_repo2}/ + remote: https://gem.repo2/ specs: indirect_platform_specific (1.0) platform_specific platform_specific (1.0) platform_specific (1.0-java) + platform_specific (1.0-x64-mingw-ucrt) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES foo! indirect_platform_specific - + #{checksums} BUNDLED WITH - #{Bundler::VERSION} + #{Bundler::VERSION} L end end @@ -530,34 +580,158 @@ RSpec.describe "bundle install from an existing gemspec" do context "with multiple platforms" do before do - build_lib("foo", :path => tmp.join("foo")) do |s| + build_lib("foo", path: tmp("foo")) do |s| s.version = "1.0.0" - s.add_development_dependency "rack" - s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "rack", "1.0.0" }.first.to_ruby + s.add_development_dependency "myrack" + s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "myrack", "1.0.0" }.first.to_ruby end end it "installs the ruby platform gemspec" do - simulate_platform "ruby" + bundle_config "force_ruby_platform true" - install_gemfile! <<-G - source "file://#{gem_repo1}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G - expect(the_bundle).to include_gems "foo 1.0.0", "rack 1.0.0" + expect(the_bundle).to include_gems "foo 1.0.0", "myrack 1.0.0" end - it "installs the ruby platform gemspec and skips dev deps with --without development" do - simulate_platform "ruby" + it "installs the ruby platform gemspec and skips dev deps with `without development` configured" do + bundle_config "force_ruby_platform true" - install_gemfile! <<-G, :without => "development" - source "file://#{gem_repo1}" - gemspec :path => '#{tmp.join("foo")}', :name => 'foo' + bundle_config "without development" + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' G expect(the_bundle).to include_gem "foo 1.0.0" - expect(the_bundle).not_to include_gem "rack" + expect(the_bundle).not_to include_gem "myrack" + end + end + + context "with multiple platforms and resolving for more specific platforms" do + before do + build_lib("chef", path: tmp("chef")) do |s| + s.version = "17.1.17" + s.write "chef-universal-mingw-ucrt.gemspec", build_spec("chef", "17.1.17", "universal-mingw-ucrt") {|sw| sw.runtime "win32-api", "~> 1.5.3" }.first.to_ruby + end + end + + it "does not remove the platform specific specs from the lockfile when updating" do + build_repo4 do + build_gem "win32-api", "1.5.3" do |s| + s.platform = "universal-mingw-ucrt" + end + end + + gemfile <<-G + source "https://gem.repo4" + gemspec :path => "../chef" + G + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "chef", "17.1.17" + c.no_checksum "chef", "17.1.17", "universal-mingw-ucrt" + c.checksum gem_repo4, "win32-api", "1.5.3", "universal-mingw-ucrt" + end + + initial_lockfile = <<~L + PATH + remote: ../chef + specs: + chef (17.1.17) + chef (17.1.17-universal-mingw-ucrt) + win32-api (~> 1.5.3) + + GEM + remote: https://gem.repo4/ + specs: + win32-api (1.5.3-universal-mingw-ucrt) + + PLATFORMS + #{lockfile_platforms("ruby", "x64-mingw-ucrt", "x86-mingw32")} + + DEPENDENCIES + chef! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile initial_lockfile + + bundle "update" + + expect(lockfile).to eq initial_lockfile + end + end + + context "with multiple locked platforms" do + before do + build_lib("activeadmin", path: tmp("activeadmin")) do |s| + s.version = "2.9.0" + s.add_dependency "railties", ">= 5.2", "< 6.2" + end + + build_repo4 do + build_gem "railties", "6.1.4" + + build_gem "jruby-openssl", "0.10.7" do |s| + s.platform = "java" + end + end + + install_gemfile <<-G + source "https://gem.repo4" + gemspec :path => "../activeadmin" + gem "jruby-openssl", :platform => :jruby + G + + bundle "lock --add-platform java" + end + + it "does not remove the platform specific specs from the lockfile when re-resolving due to gemspec changes" do + checksums = checksums_section_when_enabled do |c| + c.no_checksum "activeadmin", "2.9.0" + c.checksum gem_repo4, "jruby-openssl", "0.10.7", "java" + c.checksum gem_repo4, "railties", "6.1.4" + end + + expect(lockfile).to eq <<~L + PATH + remote: ../activeadmin + specs: + activeadmin (2.9.0) + railties (>= 5.2, < 6.2) + + GEM + remote: https://gem.repo4/ + specs: + jruby-openssl (0.10.7-java) + railties (6.1.4) + + PLATFORMS + #{lockfile_platforms("java")} + + DEPENDENCIES + activeadmin! + jruby-openssl + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + gemspec = tmp("activeadmin/activeadmin.gemspec") + File.write(gemspec, File.read(gemspec).sub(">= 5.2", ">= 6.0")) + + previous_lockfile = lockfile + + bundle "install --local" + + expect(lockfile).to eq(previous_lockfile.sub(">= 5.2", ">= 6.0")) end end end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index 5868c76570..b2a82caf01 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -1,22 +1,26 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with git sources" do - describe "when floating on master" do - before :each do - build_git "foo" do |s| - s.executables = "foobar" - end - - install_gemfile <<-G - source "file://#{gem_repo1}" + describe "when floating on main" do + let(:base_gemfile) do + <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G end + let(:install_base_gemfile) do + build_git "foo" do |s| + s.executables = "foobar" + end + + install_gemfile base_gemfile + end + it "fetches gems" do + install_base_gemfile expect(the_bundle).to include_gems("foo 1.0") run <<-RUBY @@ -27,11 +31,69 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end + it "does not (yet?) enforce CHECKSUMS" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + bundle_config "lockfile_checksums true" + gemfile base_gemfile + + lockfile <<~L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{revision} + specs: + foo (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + CHECKSUMS + foo (1.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle_config "frozen true" + + bundle "install" + expect(the_bundle).to include_gems("foo 1.0") + end + it "caches the git repo" do - expect(Dir["#{default_bundle_path}/cache/bundler/git/foo-1.0-*"].size).to eq(1) + install_base_gemfile + expect(Dir["#{default_cache_path}/git/foo-1.0-*"]).to have_attributes size: 1 + end + + it "does not write to cache on bundler/setup" do + install_base_gemfile + FileUtils.rm_r(default_cache_path) + ruby "require 'bundler/setup'" + expect(default_cache_path).not_to exist + end + + it "caches the git repo globally and properly uses the cached repo on the next invocation" do + install_base_gemfile + pristine_system_gems + bundle_config "global_gem_cache true" + bundle :install + expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes size: 1 + + bundle "install --verbose" + expect(err).to be_empty + expect(out).to include("Using foo 1.0 from #{lib_path("foo")}") end it "caches the evaluated gemspec" do + install_base_gemfile git = update_git "foo" do |s| s.executables = ["foobar"] # we added this the first time, so keep it now s.files = ["bin/foobar"] # updating git nukes the files list @@ -41,35 +103,35 @@ RSpec.describe "bundle install with git sources" do bundle "update foo" - sha = git.ref_for("master", 11) - spec_file = default_bundle_path.join("bundler/gems/foo-1.0-#{sha}/foo.gemspec").to_s - ruby_code = Gem::Specification.load(spec_file).to_ruby + sha = git.ref_for("main", 11) + spec_file = default_bundle_path("bundler/gems/foo-1.0-#{sha}/foo.gemspec") + expect(spec_file).to exist + ruby_code = Gem::Specification.load(spec_file.to_s).to_ruby file_code = File.read(spec_file) expect(file_code).to eq(ruby_code) end it "does not update the git source implicitly" do + install_base_gemfile update_git "foo" - in_app_root2 do - install_gemfile bundled_app2("Gemfile"), <<-G - git "#{lib_path("foo-1.0")}" do - gem 'foo' - end - G - end + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G - in_app_root do - run <<-RUBY - require 'foo' - puts "fail" if defined?(FOO_PREV_REF) - RUBY + run <<-RUBY + require 'foo' + puts "fail" if defined?(FOO_PREV_REF) + RUBY - expect(out).to be_empty - end + expect(out).to be_empty end it "sets up git gem executables on the path" do + install_base_gemfile bundle "exec foobar" expect(out).to eq("1.0") end @@ -77,32 +139,30 @@ RSpec.describe "bundle install with git sources" do it "complains if pinned specs don't exist in the git repo" do build_git "foo" - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}" G - expect(out).to include("Source contains 'foo' at: 1.0 ruby") + expect(err).to include("The source contains the following gems matching 'foo':\n * foo-1.0") end - it "complains with version and platform if pinned specs don't exist in the git repo" do - simulate_platform "java" - + it "complains with version and platform if pinned specs don't exist in the git repo", :jruby_only do build_git "only_java" do |s| s.platform = "java" end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" platforms :jruby do gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}" end G - expect(out).to include("Source contains 'only_java' at: 1.0 java") + expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java") end - it "complains with multiple versions and platforms if pinned specs don't exist in the git repo" do - simulate_platform "java" - + it "complains with multiple versions and platforms if pinned specs don't exist in the git repo", :jruby_only do build_git "only_java", "1.0" do |s| s.platform = "java" end @@ -112,42 +172,45 @@ RSpec.describe "bundle install with git sources" do s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec") end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" platforms :jruby do gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}" end G - expect(out).to include("Source contains 'only_java' at: 1.0 java, 1.1 java") + expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java\n * only_java-1.1-java") end it "still works after moving the application directory" do - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + install_base_gemfile + FileUtils.mv bundled_app, tmp("bundled_app.bck") - Dir.chdir tmp("bundled_app.bck") - expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: tmp("bundled_app.bck") end it "can still install after moving the application directory" do - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + install_base_gemfile + FileUtils.mv bundled_app, tmp("bundled_app.bck") - update_git "foo", "1.1", :path => lib_path("foo-1.0") + update_git "foo", "1.1", path: lib_path("foo-1.0") - Dir.chdir tmp("bundled_app.bck") gemfile tmp("bundled_app.bck/Gemfile"), <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end - gem "rack", "1.0" + gem "myrack", "1.0" G - bundle "update foo" + bundle "update foo", dir: tmp("bundled_app.bck") - expect(the_bundle).to include_gems "foo 1.1", "rack 1.0" + expect(the_bundle).to include_gems "foo 1.1", "myrack 1.0", dir: tmp("bundled_app.bck") end end @@ -155,8 +218,8 @@ RSpec.describe "bundle install with git sources" do before do build_git "foo" gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" git "#{lib_path("foo-1.0")}" do # this page left intentionally blank @@ -166,7 +229,7 @@ RSpec.describe "bundle install with git sources" do it "does not explode" do bundle "install" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end @@ -179,10 +242,12 @@ RSpec.describe "bundle install with git sources" do it "works" do install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do gem "foo" end G + expect(err).to be_empty run <<-RUBY require 'foo' @@ -194,11 +259,12 @@ RSpec.describe "bundle install with git sources" do it "works when the revision is a symbol" do install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do gem "foo" end G - expect(err).to lack_errors + expect(err).to be_empty run <<-RUBY require 'foo' @@ -207,17 +273,141 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end + + it "works when an abbreviated revision is added after an initial, potentially shallow clone" do + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => #{@revision[0..7].inspect} do + gem "foo" + end + G + end + + it "works when a tag that does not look like a commit hash is used as the value of :ref" do + build_git "foo" + @remote = build_git("bar", bare: true) + update_git "foo", remote: @remote.path + update_git "foo", push: "main" + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{@remote.path}" + G + + # Create a new tag on the remote that needs fetching + update_git "foo", tag: "v1.0.0" + update_git "foo", push: "v1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => "#{@remote.path}", :ref => "v1.0.0" + G + + expect(err).to be_empty + end + + it "works when the revision is a non-head ref" do + # want to ensure we don't fallback to main + update_git "foo", path: lib_path("foo-1.0") do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) + + # want to ensure we don't fallback to HEAD + update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'") + end + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do + gem "foo" + end + G + expect(err).to be_empty + + run <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO) + RUBY + + expect(out).to eq("WIN") + end + + it "works when the revision is a non-head ref and it was previously downloaded" do + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + # want to ensure we don't fallback to main + update_git "foo", path: lib_path("foo-1.0") do |s| + s.write("lib/foo.rb", "raise 'FAIL'") + end + + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) + + # want to ensure we don't fallback to HEAD + update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s| + s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'") + end + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do + gem "foo" + end + G + expect(err).to be_empty + + run <<-RUBY + require 'foo' + puts "WIN" if defined?(FOO) + RUBY + + expect(out).to eq("WIN") + end + + it "does not download random non-head refs" do + git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0")) + + bundle_config "global_gem_cache true" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + # ensure we also git fetch after cloning + bundle :update, all: true + + git("ls-remote .", Dir[home(".bundle/cache/git/foo-*")].first) + + expect(out).not_to include("refs/bundler/1") + end end describe "when specifying a branch" do let(:branch) { "branch" } let(:repo) { build_git("foo").path } - before(:each) do - update_git("foo", :path => repo, :branch => branch) - end it "works" do + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -229,7 +419,12 @@ RSpec.describe "bundle install with git sources" do context "when the branch starts with a `#`" do let(:branch) { "#149/redirect-url-fragment" } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -242,7 +437,12 @@ RSpec.describe "bundle install with git sources" do context "when the branch includes quotes" do let(:branch) { %('") } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, branch: branch) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :branch => #{branch.dump} do gem "foo" end @@ -256,12 +456,12 @@ RSpec.describe "bundle install with git sources" do describe "when specifying a tag" do let(:tag) { "tag" } let(:repo) { build_git("foo").path } - before(:each) do - update_git("foo", :path => repo, :tag => tag) - end it "works" do + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -273,7 +473,12 @@ RSpec.describe "bundle install with git sources" do context "when the tag starts with a `#`" do let(:tag) { "#149/redirect-url-fragment" } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -286,7 +491,12 @@ RSpec.describe "bundle install with git sources" do context "when the tag includes quotes" do let(:tag) { %('") } it "works" do + skip "git does not accept this" if Gem.win_platform? + + update_git("foo", path: repo, tag: tag) + install_gemfile <<-G + source "https://gem.repo1" git "#{repo}", :tag => #{tag.dump} do gem "foo" end @@ -299,186 +509,216 @@ RSpec.describe "bundle install with git sources" do describe "when specifying local override" do it "uses the local repository instead of checking a new one out" do - # We don't generate it because we actually don't need it - # build_git "rack", "0.8" - - build_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) bundle :install - expect(out).to match(/at #{lib_path('local-rack')}/) - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("LOCAL") end it "chooses the local repository on runtime" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config local.rack #{lib_path("local-rack")}) - run "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" expect(out).to eq("LOCAL") end it "unlocks the source when the dependencies have changed while switching to the local" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "rack.gemspec", build_spec("rack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby - s.write "lib/rack.rb", "puts :LOCAL" + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/myrack.rb", "puts :LOCAL" end - install_gemfile! <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle! %(config local.rack #{lib_path("local-rack")}) - bundle! :install - run! "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + run "require 'myrack'" expect(out).to eq("LOCAL") end it "updates specs on runtime" do system_gems "nokogiri-1.4.2" - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - lockfile0 = File.read(bundled_app("Gemfile.lock")) + lockfile0 = File.read(bundled_app_lock) - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) - update_git "rack", "0.8", :path => lib_path("local-rack") do |s| + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| s.add_dependency "nokogiri", "1.4.2" end - bundle %(config local.rack #{lib_path("local-rack")}) - run "require 'rack'" + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" - lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1 = File.read(bundled_app_lock) expect(lockfile1).not_to eq(lockfile0) end it "updates ref on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - lockfile0 = File.read(bundled_app("Gemfile.lock")) + lockfile0 = File.read(bundled_app_lock) - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) - update_git "rack", "0.8", :path => lib_path("local-rack") + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") - bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) bundle :install - lockfile1 = File.read(bundled_app("Gemfile.lock")) + lockfile1 = File.read(bundled_app_lock) expect(lockfile1).not_to eq(lockfile0) end - it "explodes if given path does not exist on install" do - build_git "rack", "0.8" + it "explodes and gives correct solution if given path does not exist on install" do + build_git "myrack", "0.8" install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) + + solution = "config unset local.myrack" + expect(err).to match(/Run `bundle #{solution}` to remove the local override/) + + bundle solution bundle :install - expect(out).to match(/Cannot use local override for rack-0.8 because #{Regexp.escape(lib_path('local-rack').to_s)} does not exist/) + + expect(err).to be_empty end - it "explodes if branch is not given on install" do - build_git "rack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + it "explodes and gives correct solution if branch is not given on install" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) + + solution = "config unset local.myrack" + expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) + + bundle solution bundle :install - expect(out).to match(/cannot use local override/i) + + expect(err).to be_empty end it "does not explode if disable_local_branch_check is given" do - build_git "rack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - bundle %(config local.rack #{lib_path("local-rack")}) - bundle %(config disable_local_branch_check true) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_branch_check true) bundle :install expect(out).to match(/Bundle complete!/) end it "explodes on different branches on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("rack-0.8")}/.", lib_path("local-rack")) + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "rack", "0.8", :path => lib_path("local-rack"), :branch => "another" do |s| - s.write "lib/rack.rb", "puts :LOCAL" + update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config local.rack #{lib_path("local-rack")}) - bundle :install - expect(out).to match(/is using branch another but Gemfile specifies master/) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/is using branch another but Gemfile specifies main/) end it "explodes on invalid revision on install" do - build_git "rack", "0.8" + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/The Gemfile lock is pointing to revision \w+/) + end + + it "does not explode on invalid revision on install" do + build_git "myrack", "0.8" - build_git "rack", "0.8", :path => lib_path("local-rack") do |s| - s.write "lib/rack.rb", "puts :LOCAL" + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}", :branch => "master" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" G - bundle %(config local.rack #{lib_path("local-rack")}) + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_revision_check true) bundle :install - expect(out).to match(/The Gemfile lock is pointing to revision \w+/) + expect(out).to match(/Bundle complete!/) end end @@ -500,75 +740,76 @@ RSpec.describe "bundle install with git sources" do # end it "installs from git even if a newer gem is available elsewhere" do - build_git "rack", "0.8" + build_git "myrack", "0.8" install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack-0.8")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" G - expect(the_bundle).to include_gems "rack 0.8" + expect(the_bundle).to include_gems "myrack 0.8" end it "installs dependencies from git even if a newer gem is available elsewhere" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" - build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| - s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" end - build_git "foo", :path => lib_path("nested") do |s| - s.add_dependency "rack", "= 1.0" + build_git "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("nested")}" G - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("WIN OVERRIDE") end it "correctly unlocks when changing to a git source" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G - build_git "rack", :path => lib_path("rack") + build_git "myrack", path: lib_path("myrack") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "1.0.0", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "correctly unlocks when changing to a git source without versions" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - build_git "rack", "1.2", :path => lib_path("rack") + build_git "myrack", "1.2", path: lib_path("myrack") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", :git => "#{lib_path("rack")}" + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" G - expect(the_bundle).to include_gems "rack 1.2" + expect(the_bundle).to include_gems "myrack 1.2" end end describe "block syntax" do it "pulls all gems from a git block" do - build_lib "omg", :path => lib_path("hi2u/omg") - build_lib "hi2u", :path => lib_path("hi2u") + build_lib "omg", path: lib_path("hi2u/omg") + build_lib "hi2u", path: lib_path("hi2u") install_gemfile <<-G + source "https://gem.repo1" path "#{lib_path("hi2u")}" do gem "omg" gem "hi2u" @@ -585,6 +826,7 @@ RSpec.describe "bundle install with git sources" do update_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" G @@ -602,7 +844,7 @@ RSpec.describe "bundle install with git sources" do end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" gem "rails", "2.3.2" G @@ -612,10 +854,10 @@ RSpec.describe "bundle install with git sources" do end it "runs the gemspec in the context of its parent directory" do - build_lib "bar", :path => lib_path("foo/bar"), :gemspec => false do |s| + build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s| s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') s.write "bar.gemspec", <<-G - $:.unshift Dir.pwd # For 1.9 + $:.unshift Dir.pwd require 'lib/version' Gem::Specification.new do |s| s.name = 'bar' @@ -627,12 +869,12 @@ RSpec.describe "bundle install with git sources" do G end - build_git "foo", :path => lib_path("foo") do |s| + build_git "foo", path: lib_path("foo") do |s| s.write "bin/foo", "" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "bar", :git => "#{lib_path("foo")}" gem "rails", "2.3.2" G @@ -641,14 +883,41 @@ RSpec.describe "bundle install with git sources" do expect(the_bundle).to include_gems "rails 2.3.2" end + it "runs the gemspec in the context of its parent directory, when using local overrides" do + build_git "foo", path: lib_path("foo"), gemspec: false do |s| + s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') + s.write "foo.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/foo/version' + Gem::Specification.new do |s| + s.name = 'foo' + s.author = 'no one' + s.version = FOO_VERSION + s.summary = 'Foo' + s.files = Dir["lib/**/*.rb"] + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "https://github.com/gems/foo", branch: "main" + G + + bundle %(config set local.foo #{lib_path("foo")}) + + expect(the_bundle).to include_gems "foo 1.0" + end + it "installs from git even if a rubygems gem is present" do - build_gem "foo", "1.0", :path => lib_path("fake_foo"), :to_system => true do |s| + build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s| s.write "lib/foo.rb", "raise 'FAIL'" end build_git "foo", "1.0" install_gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" G @@ -656,10 +925,10 @@ RSpec.describe "bundle install with git sources" do end it "fakes the gem out if there is no gemspec" do - build_git "foo", :gemspec => false + build_git "foo", gemspec: false install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" gem "rails", "2.3.2" G @@ -670,20 +939,22 @@ RSpec.describe "bundle install with git sources" do it "catches git errors and spits out useful output" do gemfile <<-G + source "https://gem.repo1" gem "foo", "1.0", :git => "omgomg" G - bundle :install + bundle :install, raise_on_error: false - expect(out).to include("Git error:") + expect(err).to include("Git error:") expect(err).to include("fatal") expect(err).to include("omgomg") end it "works when the gem path has spaces in it" do - build_git "foo", :path => lib_path("foo space-1.0") + build_git "foo", path: lib_path("foo space-1.0") install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo space-1.0")}" G @@ -694,6 +965,7 @@ RSpec.describe "bundle install with git sources" do build_git "forced", "1.0" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("forced-1.0")}" do gem 'forced' end @@ -704,48 +976,50 @@ RSpec.describe "bundle install with git sources" do s.write "lib/forced.rb", "FORCED = '1.1'" end - bundle "update" + bundle "update", all: true expect(the_bundle).to include_gems "forced 1.1" - Dir.chdir(lib_path("forced-1.0")) do - `git reset --hard HEAD^` - end + git("reset --hard HEAD^", lib_path("forced-1.0")) - bundle "update" + bundle "update", all: true expect(the_bundle).to include_gems "forced 1.0" end it "ignores submodules if :submodule is not passed" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + build_git "submodule", "1.0" build_git "has_submodule", "1.0" do |s| s.add_dependency "submodule" end - Dir.chdir(lib_path("has_submodule-1.0")) do - sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" - `git commit -m "submodulator"` - end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "#{lib_path("has_submodule-1.0")}" do gem "has_submodule" end G - expect(out).to match(/could not find gem 'submodule/i) + expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) expect(the_bundle).not_to include_gems "has_submodule 1.0" end it "handles repos with submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + build_git "submodule", "1.0" build_git "has_submodule", "1.0" do |s| s.add_dependency "submodule" end - Dir.chdir(lib_path("has_submodule-1.0")) do - sys_exec "git submodule add #{lib_path("submodule-1.0")} submodule-1.0" - `git commit -m "submodulator"` - end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("has_submodule-1.0")}", :submodules => true do gem "has_submodule" end @@ -754,10 +1028,33 @@ RSpec.describe "bundle install with git sources" do expect(the_bundle).to include_gems "has_submodule 1.0" end + it "does not warn when deiniting submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" + + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to be_empty + + expect(the_bundle).to include_gems "has_submodule 1.0" + expect(the_bundle).to_not include_gems "submodule 1.0" + end + it "handles implicit updates when modifying the source info" do git = build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem "foo" end @@ -767,6 +1064,7 @@ RSpec.describe "bundle install with git sources" do update_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do gem "foo" end @@ -780,14 +1078,15 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end - it "does not to a remote fetch if the revision is cached locally" do + it "does not do a remote fetch if the revision is cached locally" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - FileUtils.rm_rf(lib_path("foo-1.0")) + FileUtils.rm_r(lib_path("foo-1.0")) bundle "install" expect(out).not_to match(/updating/i) @@ -797,62 +1096,64 @@ RSpec.describe "bundle install with git sources" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G bundle "install" bundle "install" - expect(exitstatus).to eq(0) if exitstatus end it "prints a friendly error if a file blocks the git repo" do build_git "foo" + FileUtils.mkdir_p(default_bundle_path) FileUtils.touch(default_bundle_path("bundler")) - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - expect(exitstatus).to_not eq(0) if exitstatus - expect(out).to include("Bundler could not install a gem because it " \ + expect(last_command).to be_failure + expect(err).to include("Bundler could not install a gem because it " \ "needs to create a directory, but a file exists " \ "- #{default_bundle_path("bundler")}") end it "does not duplicate git gem sources" do - build_lib "foo", :path => lib_path("nested/foo") - build_lib "bar", :path => lib_path("nested/bar") + build_lib "foo", path: lib_path("nested/foo") + build_lib "bar", path: lib_path("nested/bar") - build_git "foo", :path => lib_path("nested") - build_git "bar", :path => lib_path("nested") + build_git "foo", path: lib_path("nested") + build_git "bar", path: lib_path("nested") - gemfile <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("nested")}" gem "bar", :git => "#{lib_path("nested")}" G - bundle "install" - expect(File.read(bundled_app("Gemfile.lock")).scan("GIT").size).to eq(1) + expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1) end describe "switching sources" do it "doesn't explode when switching Path to Git sources" do - build_gem "foo", "1.0", :to_system => true do |s| + build_gem "foo", "1.0", to_system: true do |s| s.write "lib/foo.rb", "raise 'fail'" end - build_lib "foo", "1.0", :path => lib_path("bar/foo") - build_git "bar", "1.0", :path => lib_path("bar") do |s| + build_lib "foo", "1.0", path: lib_path("bar/foo") + build_git "bar", "1.0", path: lib_path("bar") do |s| s.add_dependency "foo" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "bar", :path => "#{lib_path("bar")}" G install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "bar", :git => "#{lib_path("bar")}" G @@ -861,24 +1162,67 @@ RSpec.describe "bundle install with git sources" do it "doesn't explode when switching Gem to Git source" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack-obama" - gem "rack", "1.0.0" + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0" G - build_git "rack", "1.0" do |s| + build_git "myrack", "1.0" do |s| s.write "lib/new_file.rb", "puts 'USING GIT'" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack-obama" - gem "rack", "1.0.0", :git => "#{lib_path("rack-1.0")}" + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" G run "require 'new_file'" expect(out).to eq("USING GIT") end + + it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do + build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") + build_git "rails", "7.1.4", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_path("rails")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_path("rails")}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + + it "doesn't explode when adding an explicit ref to a git gem with dependencies" do + lib_root = lib_path("rails") + + build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") + build_git "rails", "7.1.4", path: lib_root do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + old_revision = revision_for(lib_root) + update_git "rails", "7.1.4", path: lib_root + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_root}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end end describe "bundle install after the remote has been updated" do @@ -886,17 +1230,16 @@ RSpec.describe "bundle install with git sources" do build_git "valim" install_gemfile <<-G - gem "valim", :git => "file://#{lib_path("valim-1.0")}" + source "https://gem.repo1" + gem "valim", :git => "#{lib_path("valim-1.0")}" G old_revision = revision_for(lib_path("valim-1.0")) update_git "valim" new_revision = revision_for(lib_path("valim-1.0")) - lockfile = File.read(bundled_app("Gemfile.lock")) - File.open(bundled_app("Gemfile.lock"), "w") do |file| - file.puts lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}") - end + old_lockfile = File.read(bundled_app_lock) + lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) bundle "install" @@ -913,32 +1256,43 @@ RSpec.describe "bundle install with git sources" do revision = revision_for(lib_path("foo-1.0")) install_gemfile <<-G - gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "#{revision}" + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" G - bundle "install" expect(out).to_not match(/Revision.*does not exist/) - install_gemfile <<-G - gem "foo", :git => "file://#{lib_path("foo-1.0")}", :ref => "deadbeef" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" G - bundle "install" - expect(out).to include("Revision deadbeef does not exist in the repository") + expect(err).to include("Revision deadbeef does not exist in the repository") + end + + it "gives a helpful error message when the remote branch no longer exists" do + build_git "foo" + + install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" + G + + expect(err).to include("Revision deadbeef does not exist in the repository") end end - describe "bundle install --deployment with git sources" do + describe "bundle install with deployment mode configured and git sources" do it "works" do - build_git "valim", :path => lib_path("valim") + build_git "valim", path: lib_path("valim") install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "valim", "= 1.0", :git => "#{lib_path("valim")}" G - simulate_new_machine + pristine_system_gems - bundle "install --deployment" - expect(exitstatus).to eq(0) if exitstatus + bundle_config "deployment true" + bundle :install end end @@ -946,12 +1300,12 @@ RSpec.describe "bundle install with git sources" do it "runs pre-install hooks" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.pre_install_hooks << lambda do |inst| STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" end @@ -959,19 +1313,19 @@ RSpec.describe "bundle install with git sources" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(err).to eq_err("Ran pre-install hook: foo-1.0") + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") end it "runs post-install hooks" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.post_install_hooks << lambda do |inst| STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" end @@ -979,39 +1333,38 @@ RSpec.describe "bundle install with git sources" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(err).to eq_err("Ran post-install hook: foo-1.0") + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") end it "complains if the install hook fails" do build_git "foo" gemfile <<-G + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.pre_install_hooks << lambda do |inst| false end H end - bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(out).to include("failed for foo-1.0") + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false + expect(err).to include("failed for foo-1.0") end end context "with an extension" do - it "installs the extension", :ruby_repo do + it "installs the extension" 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__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) File.open("\#{path}/foo.rb", "w") do |f| f.puts "FOO = 'YES'" @@ -1021,7 +1374,7 @@ RSpec.describe "bundle install with git sources" do end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G @@ -1031,14 +1384,14 @@ RSpec.describe "bundle install with git sources" do R expect(out).to eq("YES") - run! <<-R + run <<-R puts $:.grep(/ext/) R - expect(out).to eq(Pathname.glob(system_gem_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) end - it "does not use old extension after ref changes", :ruby_repo do - git_reader = build_git "foo", :no_default => true do |s| + it "does not use old extension after ref changes" do + git_reader = build_git "foo", no_default: true do |s| s.extensions = ["ext/extconf.rb"] s.write "ext/extconf.rb", <<-RUBY require "mkmf" @@ -1048,21 +1401,20 @@ RSpec.describe "bundle install with git sources" do end 2.times do |i| - Dir.chdir(git_reader.path) do - File.open("ext/foo.c", "w") do |file| - file.write <<-C - #include "ruby.h" - VALUE foo() { return INT2FIX(#{i}); } - void Init_foo() { rb_define_global_function("foo", &foo, 0); } - C - end - `git commit -m 'commit for iteration #{i}' ext/foo.c` + File.open(git_reader.path.join("ext/foo.c"), "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo(VALUE self) { return INT2FIX(#{i}); } + void Init_foo() { rb_define_global_function("foo", &foo, 0); } + C end - git_sha = git_reader.ref_for("HEAD") + git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) + + git_commit_sha = git_reader.ref_for("HEAD") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_sha}" + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" G run <<-R @@ -1085,12 +1437,12 @@ RSpec.describe "bundle install with git sources" do RUBY end - install_gemfile <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - expect(out).to end_with(<<-M.strip) + expect(err).to end_with(<<-M.strip) An error occurred while installing foo (1.0), and Bundler cannot continue. In Gemfile: @@ -1099,13 +1451,13 @@ In Gemfile: expect(out).not_to include("gem install foo") end - it "does not reinstall the extension", :ruby_repo, :rubygems => ">= 2.3.0" do + it "does not reinstall the extension" 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__) + path = File.expand_path("lib", __dir__) FileUtils.mkdir_p(path) cur_time = Time.now.to_f.to_s File.open("\#{path}/foo.rb", "w") do |f| @@ -1116,11 +1468,11 @@ In Gemfile: end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R @@ -1129,16 +1481,114 @@ In Gemfile: expect(installed_time).to match(/\A\d+\.\d+\z/) install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo-1.0")}" G - run! <<-R + run <<-R require 'foo' puts FOO R expect(out).to eq(installed_time) end + + it "does not reinstall the extension when changing another gem" 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", __dir__) + 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 "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + it "does reinstall the extension when changing refs" 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", __dir__) + 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 "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + + update_git("foo", branch: "branch2") + + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + + installed_time = out + + update_git("foo") + bundle "update foo" + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + end end it "ignores git environment variables" do @@ -1151,54 +1601,91 @@ In Gemfile: ENV["GIT_WORK_TREE"] = "bar" install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" git "#{lib_path("xxxxxx-1.0")}" do gem 'xxxxxx' end G - expect(exitstatus).to eq(0) if exitstatus expect(ENV["GIT_DIR"]).to eq("bar") expect(ENV["GIT_WORK_TREE"]).to eq("bar") end end describe "without git installed" do - it "prints a better error message" do + it "prints a better error message when installing" do + gemfile <<-G + source "https://gem.repo1" + + gem "rake", git: "https://github.com/ruby/rake" + G + + lockfile <<-L + GIT + remote: https://github.com/ruby/rake + revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af + specs: + rake (13.0.6) + + GEM + remote: https://rubygems.org/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rake! + + BUNDLED WITH + #{Bundler::VERSION} + L + + with_path_as("") do + bundle "install", raise_on_error: false + end + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "prints a better error message when updating" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G with_path_as("") do - bundle "update" + bundle "update", all: true, raise_on_error: false end - expect(out).to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") end - it "installs a packaged git gem successfully" do + it "doesn't need git in the new machine if an installed git gem is copied to another machine" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle "package --all" - simulate_new_machine + bundle_config_global "path vendor/bundle" + bundle :install + pristine_system_gems - bundle "install", :env => { "PATH" => "" } + bundle "install", env: { "PATH" => "" } expect(out).to_not include("You need to install git to be able to use gems from git repositories.") - expect(exitstatus).to be_zero if exitstatus end end - describe "when the git source is overriden with a local git repo" do + describe "when the git source is overridden with a local git repo" do before do - bundle "config --global local.foo #{lib_path("foo")}" + bundle_config_global "local.foo #{lib_path("foo")}" end describe "and git output is colorized" do @@ -1209,10 +1696,11 @@ In Gemfile: end it "installs successfully" do - build_git "foo", "1.0", :path => lib_path("foo") + build_git "foo", "1.0", path: lib_path("foo") gemfile <<-G - gem "foo", :git => "#{lib_path("foo")}", :branch => "master" + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" G bundle :install @@ -1226,15 +1714,14 @@ In Gemfile: let(:credentials) { "user1:password1" } it "does not display the password" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "https://#{credentials}@github.com/company/private-repo" do gem "foo" end G - bundle :install - expect(out).to_not include("password1") - expect(err).to_not include("password1") + expect(stdboth).to_not include("password1") expect(out).to include("Fetching https://user1@github.com/company/private-repo") end end @@ -1243,15 +1730,14 @@ In Gemfile: let(:credentials) { "oauth_token" } it "displays the oauth scheme but not the oauth token" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do gem "foo" end G - bundle :install - expect(out).to_not include("oauth_token") - expect(err).to_not include("oauth_token") + expect(stdboth).to_not include("oauth_token") expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") end end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb index a3a5eeefdf..4013b112ec 100644 --- a/spec/bundler/install/gemfile/groups_spec.rb +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with groups" do describe "installing with no options" do before :each do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" end @@ -15,7 +14,7 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs gems in a group block into that group" do @@ -26,7 +25,7 @@ RSpec.describe "bundle install with groups" do puts ACTIVESUPPORT R - expect(err).to eq_err("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- activesupport/) end it "installs gems with inline :groups into those groups" do @@ -37,11 +36,11 @@ RSpec.describe "bundle install with groups" do puts THIN R - expect(err).to eq_err("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up everything if Bundler.setup is used with no groups" do - output = run("require 'rack'; puts RACK") + output = run("require 'myrack'; puts MYRACK") expect(output).to eq("1.0.0") output = run("require 'activesupport'; puts ACTIVESUPPORT") @@ -58,7 +57,7 @@ RSpec.describe "bundle install with groups" do puts THIN RUBY - expect(err).to eq_err("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- thin/) end it "sets up old groups when they have previously been removed" do @@ -71,12 +70,12 @@ RSpec.describe "bundle install with groups" do end end - describe "installing --without" do + describe "without option" do describe "with gems assigned to a single group" do before :each do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" end @@ -87,54 +86,58 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle :install, :without => "emo" - expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default] end - it "does not install gems from the excluded group" do - bundle :install, :without => "emo" - expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + it "respects global `without` configuration, but does not save it locally" do + bundle_config_global "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default] + bundle "config list" + expect(out).not_to include("Set for your local app (#{bundled_app(".bundle/config")}): [:emo]") + expect(out).to include("Set for the current user (#{home(".bundle/config")}): [:emo]") end - it "does not install gems from the previously excluded group" do - bundle :install, :without => "emo" - expect(the_bundle).not_to include_gems "activesupport 2.3.5" + it "allows running application where groups where configured by a different user" do + bundle_config "without emo" + bundle :install + bundle "exec ruby -e 'puts 42'", env: { "BUNDLE_USER_HOME" => tmp("new_home").to_s } + expect(out).to include("42") + end + + it "does not install gems from the excluded group" do + bundle_config "without emo" bundle :install - expect(the_bundle).not_to include_gems "activesupport 2.3.5" + expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default] end it "does not say it installed gems from the excluded group" do - bundle :install, :without => "emo" + bundle_config "without emo" + bundle :install expect(out).not_to include("activesupport") end it "allows Bundler.setup for specific groups" do - bundle :install, :without => "emo" - run("require 'rack'; puts RACK", :default) + bundle_config "without emo" + bundle :install + run("require 'myrack'; puts MYRACK", :default) expect(out).to eq("1.0.0") end it "does not effect the resolve" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "activesupport" group :emo do gem "rails", "2.3.2" end G - bundle :install, :without => "emo" - expect(the_bundle).to include_gems "activesupport 2.3.2", :groups => [:default] - end - - it "still works on a different machine and excludes gems" do - bundle :install, :without => "emo" - - simulate_new_machine - bundle :install, :without => "emo" - - expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] - expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "activesupport 2.3.2", groups: [:default] end it "still works when BUNDLE_WITHOUT is set" do @@ -143,86 +146,52 @@ RSpec.describe "bundle install with groups" do bundle :install expect(out).not_to include("activesupport") - expect(the_bundle).to include_gems "rack 1.0.0", :groups => [:default] - expect(the_bundle).not_to include_gems "activesupport 2.3.5", :groups => [:default] + expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default] + expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default] ENV["BUNDLE_WITHOUT"] = nil end - it "clears without when passed an empty list" do - bundle :install, :without => "emo" - - bundle 'install --without ""' - expect(the_bundle).to include_gems "activesupport 2.3.5" - end - - it "doesn't clear without when nothing is passed" do - bundle :install, :without => "emo" - - bundle :install - expect(the_bundle).not_to include_gems "activesupport 2.3.5" - end - it "does not install gems from the optional group" do bundle :install expect(the_bundle).not_to include_gems "thin 1.0" end - it "does install gems from the optional group when requested" do - bundle :install, :with => "debugging" - expect(the_bundle).to include_gems "thin 1.0" - end - - it "does install gems from the previously requested group" do - bundle :install, :with => "debugging" - expect(the_bundle).to include_gems "thin 1.0" + it "installs gems from the optional group when requested" do + bundle_config "with debugging" bundle :install expect(the_bundle).to include_gems "thin 1.0" end - it "does install gems from the optional groups requested with BUNDLE_WITH" do + it "installs gems from the optional groups requested with BUNDLE_WITH" do ENV["BUNDLE_WITH"] = "debugging" bundle :install expect(the_bundle).to include_gems "thin 1.0" ENV["BUNDLE_WITH"] = nil end - it "clears with when passed an empty list" do - bundle :install, :with => "debugging" - bundle 'install --with ""' - expect(the_bundle).not_to include_gems "thin 1.0" - end - - it "does remove groups from without when passed at with" do - bundle :install, :without => "emo" - bundle :install, :with => "emo" - expect(the_bundle).to include_gems "activesupport 2.3.5" - end + it "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do + ENV["BUNDLE_WITH"] = "debugging" - it "does remove groups from with when passed at without" do - bundle :install, :with => "debugging" - bundle :install, :without => "debugging" - expect(the_bundle).not_to include_gems "thin 1.0" - end + bundle :install + expect(the_bundle).to include_gem "thin 1.0" - it "errors out when passing a group to with and without" do - bundle :install, :with => "emo debugging", :without => "emo" - expect(out).to include("The offending groups are: emo") - end + ENV["BUNDLE_WITHOUT"] = "debugging" + expect(the_bundle).to include_gem "thin 1.0" - it "can add and remove a group at the same time" do - bundle :install, :with => "debugging", :without => "emo" - expect(the_bundle).to include_gems "thin 1.0" - expect(the_bundle).not_to include_gems "activesupport 2.3.5" + bundle :install + expect(the_bundle).to include_gem "thin 1.0" end - it "does have no effect when listing a not optional group in with" do - bundle :install, :with => "emo" + it "has no effect when listing a not optional group in with" do + bundle_config "with emo" + bundle :install expect(the_bundle).to include_gems "activesupport 2.3.5" end - it "does have no effect when listing an optional group in without" do - bundle :install, :without => "debugging" + it "has no effect when listing an optional group in without" do + bundle_config "without debugging" + bundle :install expect(the_bundle).not_to include_gems "thin 1.0" end end @@ -230,8 +199,8 @@ RSpec.describe "bundle install with groups" do describe "with gems assigned to multiple groups" do before :each do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo, :lolercoaster do gem "activesupport", "2.3.5" end @@ -239,20 +208,22 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle :install, :without => "emo lolercoaster" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle_config "without emo lolercoaster" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs the gem if any of its groups are installed" do - bundle "install --without emo" - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" end describe "with a gem defined multiple times in different groups" do before :each do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do gem "activesupport", "2.3.5" @@ -264,23 +235,21 @@ RSpec.describe "bundle install with groups" do G end - it "installs the gem w/ option --without emo" do - bundle "install --without emo" + it "installs the gem unless all groups are excluded" do + bundle_config "without emo" + bundle :install expect(the_bundle).to include_gems "activesupport 2.3.5" - end - it "installs the gem w/ option --without lolercoaster" do - bundle "install --without lolercoaster" + bundle_config "without lolercoaster" + bundle :install expect(the_bundle).to include_gems "activesupport 2.3.5" - end - it "does not install the gem w/ option --without emo lolercoaster" do - bundle "install --without emo lolercoaster" + bundle_config "without emo lolercoaster" + bundle :install expect(the_bundle).not_to include_gems "activesupport 2.3.5" - end - it "does not install the gem w/ option --without 'emo lolercoaster'" do - bundle "install --without 'emo lolercoaster'" + bundle "config set --local without 'emo lolercoaster'" + bundle :install expect(the_bundle).not_to include_gems "activesupport 2.3.5" end end @@ -289,8 +258,8 @@ RSpec.describe "bundle install with groups" do describe "nesting groups" do before :each do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" group :emo do group :lolercoaster do gem "activesupport", "2.3.5" @@ -300,13 +269,15 @@ RSpec.describe "bundle install with groups" do end it "installs gems in the default group" do - bundle :install, :without => "emo lolercoaster" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle_config "without emo lolercoaster" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" end it "installs the gem if any of its groups are installed" do - bundle "install --without emo" - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" end end end @@ -314,8 +285,8 @@ RSpec.describe "bundle install with groups" do describe "when loading only the default group" do it "should not load all groups" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "activesupport", :groups => :development G @@ -323,7 +294,7 @@ RSpec.describe "bundle install with groups" do require "bundler" Bundler.setup :default Bundler.require :default - puts RACK + puts MYRACK begin require "activesupport" rescue LoadError @@ -336,36 +307,39 @@ RSpec.describe "bundle install with groups" do end end - describe "when locked and installed with --without" do + describe "when locked and installed with `without` setting" do before(:each) do build_repo2 - system_gems "rack-0.9.1" do - install_gemfile <<-G, :without => :rack - source "file://#{gem_repo2}" - gem "rack" - group :rack do - gem "rack_middleware" - end - G - end + system_gems "myrack-0.9.1" + + bundle_config "without myrack" + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + + group :myrack do + gem "myrack_middleware" + end + G end - it "uses the correct versions even if --without was used on the original" do - expect(the_bundle).to include_gems "rack 0.9.1" - expect(the_bundle).not_to include_gems "rack_middleware 1.0" + it "uses versions from excluded gems in a machine without the without configuration" do + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack_middleware 1.0" simulate_new_machine bundle :install - expect(the_bundle).to include_gems "rack 0.9.1" - expect(the_bundle).to include_gems "rack_middleware 1.0" + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).to include_gems "myrack_middleware 1.0" end it "does not hit the remote a second time" do - FileUtils.rm_rf gem_repo2 - bundle "install --without rack" - expect(err).to lack_errors + FileUtils.rm_r gem_repo2 + bundle_config "without myrack" + bundle :install, verbose: true + expect(stdboth).not_to match(/fetching/i) end end end diff --git a/spec/bundler/install/gemfile/install_if.rb b/spec/bundler/install/gemfile/install_if.rb deleted file mode 100644 index b1717ad583..0000000000 --- a/spec/bundler/install/gemfile/install_if.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -describe "bundle install with install_if conditionals" do - it "follows the install_if DSL" do - install_gemfile <<-G - source "file://#{gem_repo1}" - install_if(lambda { true }) do - gem "activesupport", "2.3.5" - end - gem "thin", :install_if => false - install_if(lambda { false }) do - gem "foo" - end - gem "rack" - G - - expect(the_bundle).to include_gems("rack 1.0", "activesupport 2.3.5") - expect(the_bundle).not_to include_gems("thin") - expect(the_bundle).not_to include_gems("foo") - - lockfile_should_be <<-L - GEM - remote: file:#{gem_repo1}/ - specs: - activesupport (2.3.5) - foo (1.0) - rack (1.0.0) - thin (1.0) - rack - - PLATFORMS - ruby - - DEPENDENCIES - activesupport (= 2.3.5) - foo - rack - thin - - BUNDLED WITH - #{Bundler::VERSION} - L - end -end diff --git a/spec/bundler/install/gemfile/install_if_spec.rb b/spec/bundler/install/gemfile/install_if_spec.rb new file mode 100644 index 0000000000..05a6d15129 --- /dev/null +++ b/spec/bundler/install/gemfile/install_if_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with install_if conditionals" do + it "follows the install_if DSL" do + install_gemfile <<-G + source "https://gem.repo1" + install_if(lambda { true }) do + gem "activesupport", "2.3.5" + end + gem "thin", :install_if => false + install_if(lambda { false }) do + gem "foo" + end + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0", "activesupport 2.3.5") + expect(the_bundle).not_to include_gems("thin") + expect(the_bundle).not_to include_gems("foo") + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "activesupport", "2.3.5" + c.checksum gem_repo1, "foo", "1.0" + c.checksum gem_repo1, "myrack", "1.0.0" + c.checksum gem_repo1, "thin", "1.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + activesupport (2.3.5) + foo (1.0) + myrack (1.0.0) + thin (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport (= 2.3.5) + foo + myrack + thin + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end +end diff --git a/spec/bundler/install/gemfile/lockfile_spec.rb b/spec/bundler/install/gemfile/lockfile_spec.rb new file mode 100644 index 0000000000..19bd7074b2 --- /dev/null +++ b/spec/bundler/install/gemfile/lockfile_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with a lockfile present" do + let(:gf) { <<-G } + source "https://gem.repo1" + + gem "myrack", "1.0.0" + G + + it "touches the lockfile on install even when nothing has changed" do + install_gemfile(gf) + expect { bundle :install }.to change { bundled_app_lock.mtime } + end + + context "gemfile evaluation" do + let(:gf) { super() + "\n\n File.open('evals', 'a') {|f| f << %(1\n) } unless ENV['BUNDLER_SPEC_NO_APPEND']" } + + context "with plugins disabled" do + before do + bundle_config "plugins false" + end + + it "does not evaluate the gemfile twice when the gem is already installed" do + install_gemfile(gf) + bundle :install + + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" } + + expect(bundled_app("evals").read.lines.to_a.size).to eq(2) + end + + it "does not evaluate the gemfile twice when the gem is not installed" do + gemfile(gf) + bundle :install + + with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" } + + expect(bundled_app("evals").read.lines.to_a.size).to eq(1) + end + end + end +end diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb new file mode 100644 index 0000000000..02b0e7d772 --- /dev/null +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -0,0 +1,401 @@ +# frozen_string_literal: true + +RSpec.describe "override DSL" do + context "with a version: string operation" do + it "replaces a direct dependency requirement with the override version spec" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "replaces a transitive dependency requirement" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "replaces the requirement even when the Gemfile pins a different version" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack", "= 1.0.0" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "applies the override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do + build_repo2 do + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + + install_gemfile <<-G + source "https://gem.repo2" + override "has_prerelease", version: "= 1.1.pre" + gem "has_prerelease" + G + + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + context "with a version: :ignore_upper operation" do + it "strips a < upper bound on a direct dependency" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "< 1.0" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "folds ~> into >= so newer versions become reachable" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "~> 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end + + context "with a version: nil operation" do + it "drops a direct dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack", "= 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "drops a transitive dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "applies a transitive-only override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + end + + context "lockfile contents" do + it "does not record the override directive in Gemfile.lock" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(lockfile).not_to match(/override/i) + end + end + + context "with a required_ruby_version: operation" do + it "lets the resolver pick a gem whose required_ruby_version excludes the current Ruby with :ignore_upper" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: :ignore_upper + gem "needs_old_ruby" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + end + + it "lets the resolver pick the gem with required_ruby_version: nil" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: nil + gem "needs_old_ruby" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + end + + it "applies to a transitive dependency's required_ruby_version" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "wraps_old", "1.0" do |s| + s.add_dependency "needs_old_ruby" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: :ignore_upper + gem "wraps_old" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby (1.0)") + expect(lockfile).to include("wraps_old (1.0)") + end + + it "re-resolves a direct dep when a metadata override is added against an existing lockfile" do + build_repo2 do + build_gem "selectable", "1.0" + build_gem "selectable", "2.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + gemfile <<-G + source "https://gem.repo2" + override "selectable", required_ruby_version: :ignore_upper + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (2.0)") + end + end + + context "with a required_rubygems_version: operation" do + it "lets the resolver pick a gem whose required_rubygems_version excludes the current RubyGems with :ignore_upper" do + build_repo2 do + build_gem "needs_old_rubygems", "1.0" do |s| + s.required_rubygems_version = "< #{Gem.rubygems_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_rubygems", required_rubygems_version: :ignore_upper + gem "needs_old_rubygems" + G + + bundle :lock + expect(lockfile).to include("needs_old_rubygems (1.0)") + end + end + + context "with an :all target" do + it "applies required_ruby_version: :ignore_upper to every gem" do + build_repo2 do + build_gem "needs_old_ruby_a", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "needs_old_ruby_b", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "needs_old_ruby_a" + gem "needs_old_ruby_b" + G + + bundle :lock + expect(lockfile).to include("needs_old_ruby_a (1.0)") + expect(lockfile).to include("needs_old_ruby_b (1.0)") + end + + it "is overridden by a per-gem override on the same field" do + build_repo2 do + build_gem "permissive", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "still_blocked", "1.0" do |s| + s.required_ruby_version = "= #{Gem.ruby_version}.999" + end + end + + # :all says ignore_upper (would unblock both), but per-gem on + # still_blocked nails it to a hard requirement that still fails. + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + override "still_blocked", required_ruby_version: "= #{Gem.ruby_version}.999" + gem "permissive" + gem "still_blocked" + G + + bundle :lock, raise_on_error: false + expect(err).to include("still_blocked") + end + + it "preserves locked versions when an :all metadata override is added without bundle update" do + build_repo2 do + build_gem "selectable", "1.0" + build_gem "selectable", "2.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem "selectable" + G + + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "selectable" + G + + # :all override alone does not pre-unlock locked specs; narrow change + # should not trigger unrelated lockfile churn. + bundle :lock + expect(lockfile).to include("selectable (1.0)") + + # bundle update opts the user into re-resolution under the override. + bundle "update selectable" + expect(lockfile).to include("selectable (2.0)") + end + end + + context "diagnostic on resolve failure" do + it "lists active overrides with their Gemfile location" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "= #{Gem.ruby_version}.999" + end + end + + gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: "= #{Gem.ruby_version}.999" + gem "needs_old_ruby" + G + + bundle :lock, raise_on_error: false + expect(err).to include("Bundler applied the following overrides") + expect(err).to include("override \"needs_old_ruby\", required_ruby_version:") + expect(err).to match(/declared at Gemfile:\d+/) + end + end + + context "install-time compatibility" do + it "installs a gem whose required_ruby_version excludes the current Ruby when an override removes the constraint" do + build_repo2 do + build_gem "needs_old_ruby", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override "needs_old_ruby", required_ruby_version: nil + gem "needs_old_ruby" + G + + expect(the_bundle).to include_gems "needs_old_ruby 1.0" + end + + it "installs a gem whose required_rubygems_version excludes the current RubyGems when an override removes it" do + build_repo2 do + build_gem "needs_old_rubygems", "1.0" do |s| + s.required_rubygems_version = "< #{Gem.rubygems_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override "needs_old_rubygems", required_rubygems_version: nil + gem "needs_old_rubygems" + G + + expect(the_bundle).to include_gems "needs_old_rubygems 1.0" + end + + it "installs every gem when :all required_ruby_version override is in effect" do + build_repo2 do + build_gem "needs_old_ruby_a", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + build_gem "needs_old_ruby_b", "1.0" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + override :all, required_ruby_version: :ignore_upper + gem "needs_old_ruby_a" + gem "needs_old_ruby_b" + G + + expect(the_bundle).to include_gems "needs_old_ruby_a 1.0", "needs_old_ruby_b 1.0" + end + end +end diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb index a1c41aebbb..b069488531 100644 --- a/spec/bundler/install/gemfile/path_spec.rb +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with explicit source paths" do it "fetches gems" do build_lib "foo" install_gemfile <<-G - path "#{lib_path("foo-1.0")}" - gem 'foo' + path "#{lib_path("foo-1.0")}" do + gem 'foo' + end G expect(the_bundle).to include_gems("foo 1.0") @@ -26,7 +26,7 @@ RSpec.describe "bundle install with explicit source paths" do it "supports relative paths" do build_lib "foo" - relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new(Dir.pwd)) + relative_path = lib_path("foo-1.0").relative_path_from(bundled_app) install_gemfile <<-G gem 'foo', :path => "#{relative_path}" @@ -48,71 +48,116 @@ RSpec.describe "bundle install with explicit source paths" do end it "expands paths raise error with not existing user's home dir" do + skip "problems with ~ expansion" if Gem.win_platform? + build_lib "foo" username = "some_unexisting_user" relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("/home/#{username}").expand_path) - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false gem 'foo', :path => "~#{username}/#{relative_path}" G - expect(out).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.") - expect(out).to match("user #{username} doesn't exist") + expect(err).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.") + expect(err).to match("user #{username} doesn't exist") end it "expands paths relative to Bundler.root" do - build_lib "foo", :path => bundled_app("foo-1.0") + build_lib "foo", path: bundled_app("foo-1.0") install_gemfile <<-G gem 'foo', :path => "./foo-1.0" G - bundled_app("subdir").mkpath - Dir.chdir(bundled_app("subdir")) do - expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("foo 1.0", dir: bundled_app("subdir").mkpath) + end + + it "sorts paths consistently on install and update when they start with ./" do + build_lib "demo", path: lib_path("demo") + build_lib "aaa", path: lib_path("demo/aaa") + + gemfile lib_path("demo/Gemfile"), <<-G + source "https://gem.repo1" + gemspec + gem "aaa", :path => "./aaa" + G + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "aaa", "1.0" + c.no_checksum "demo", "1.0" end + + lockfile = <<~L + PATH + remote: . + specs: + demo (1.0) + + PATH + remote: aaa + specs: + aaa (1.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + aaa! + demo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install, dir: lib_path("demo") + expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile) + bundle :update, all: true, dir: lib_path("demo") + expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile) end it "expands paths when comparing locked paths to Gemfile paths" do - build_lib "foo", :path => bundled_app("foo-1.0") + build_lib "foo", path: bundled_app("foo-1.0") install_gemfile <<-G - gem 'foo', :path => File.expand_path("../foo-1.0", __FILE__) + gem 'foo', :path => File.expand_path("foo-1.0", __dir__) G - bundle "install --frozen" - expect(exitstatus).to eq(0) if exitstatus + bundle_config "frozen true" + bundle :install end it "installs dependencies from the path even if a newer gem is available elsewhere" do - system_gems "rack-1.0.0" + system_gems "myrack-1.0.0" - build_lib "rack", "1.0", :path => lib_path("nested/bar") do |s| - s.write "lib/rack.rb", "puts 'WIN OVERRIDE'" + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" end - build_lib "foo", :path => lib_path("nested") do |s| - s.add_dependency "rack", "= 1.0" + build_lib "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" end install_gemfile <<-G - source "file://#{gem_repo1}" gem "foo", :path => "#{lib_path("nested")}" G - run "require 'rack'" + run "require 'myrack'" expect(out).to eq("WIN OVERRIDE") end it "works" do - build_gem "foo", "1.0.0", :to_system => true do |s| + build_gem "foo", "1.0.0", to_system: true do |s| s.write "lib/foo.rb", "puts 'FAIL'" end - build_lib "omg", "1.0", :path => lib_path("omg") do |s| + build_lib "omg", "1.0", path: lib_path("omg") do |s| s.add_dependency "foo" end - build_lib "foo", "1.0.0", :path => lib_path("omg/foo") + build_lib "foo", "1.0.0", path: lib_path("omg/foo") install_gemfile <<-G gem "omg", :path => "#{lib_path("omg")}" @@ -121,8 +166,88 @@ RSpec.describe "bundle install with explicit source paths" do expect(the_bundle).to include_gems "foo 1.0" end + it "works when using prereleases of 0.0.0" do + build_lib "foo", "0.0.0.dev", path: lib_path("foo") + + gemfile <<~G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (0.0.0.dev) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + expect(the_bundle).to include_gems "foo 0.0.0.dev" + end + + it "works when using uppercase prereleases of 0.0.0" do + build_lib "foo", "0.0.0.SNAPSHOT", path: lib_path("foo") + + gemfile <<~G + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (0.0.0.SNAPSHOT) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + expect(the_bundle).to include_gems "foo 0.0.0.SNAPSHOT" + end + + it "handles downgrades" do + build_lib "omg", "2.0", path: lib_path("omg") + + install_gemfile <<-G + gem "omg", :path => "#{lib_path("omg")}" + G + + build_lib "omg", "1.0", path: lib_path("omg") + + bundle :install + + expect(the_bundle).to include_gems "omg 1.0" + end + it "prefers gemspecs closer to the path root" do - build_lib "premailer", "1.0.0", :path => lib_path("premailer") do |s| + build_lib "premailer", "1.0.0", path: lib_path("premailer") do |s| s.write "gemfiles/ruby187.gemspec", <<-G Gem::Specification.new do |s| s.name = 'premailer' @@ -142,7 +267,7 @@ RSpec.describe "bundle install with explicit source paths" do expect(the_bundle).to include_gems "premailer 1.0.0" end - it "warns on invalid specs", :rubygems => "1.7" do + it "warns on invalid specs" do build_lib "foo" gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s @@ -154,106 +279,152 @@ RSpec.describe "bundle install with explicit source paths" do G end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false gem "foo", :path => "#{lib_path("foo-1.0")}" G - expect(out).to_not include("ERROR REPORT") - expect(out).to_not include("Your Gemfile has no gem server sources.") - expect(out).to match(/is not valid. Please fix this gemspec./) - expect(out).to match(/The validation error was 'missing value for attribute version'/) - expect(out).to match(/You have one or more invalid gemspecs that need to be fixed/) + expect(err).to match(/is not valid. Please fix this gemspec./) + expect(err).to match(/The validation error was 'missing value for attribute version'/) + expect(err).to match(/You have one or more invalid gemspecs that need to be fixed/) end it "supports gemspec syntax" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", "1.0" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" end - gemfile = <<-G - source "file://#{gem_repo1}" + gemfile lib_path("foo/Gemfile"), <<-G + source "https://gem.repo1" gemspec G - File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts gemfile } + bundle "install", dir: lib_path("foo") + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") + end - Dir.chdir(lib_path("foo")) do - bundle "install" - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + it "does not unlock dependencies of path sources" do + build_repo4 do + build_gem "graphql", "2.0.15" + build_gem "graphql", "2.0.16" + end + + build_lib "foo", "0.1.0", path: lib_path("foo") do |s| + s.add_dependency "graphql", "~> 2.0" end + + gemfile_path = lib_path("foo/Gemfile") + + gemfile gemfile_path, <<-G + source "https://gem.repo4" + gemspec + G + + lockfile_path = lib_path("foo/Gemfile.lock") + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "0.1.0" + c.checksum gem_repo4, "graphql", "2.0.15" + end + + original_lockfile = <<~L + PATH + remote: . + specs: + foo (0.1.0) + graphql (~> 2.0) + + GEM + remote: https://gem.repo4/ + specs: + graphql (2.0.15) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile lockfile_path, original_lockfile + + build_lib "foo", "0.1.1", path: lib_path("foo") do |s| + s.add_dependency "graphql", "~> 2.0" + end + + bundle "install", dir: lib_path("foo") + expect(lockfile_path).to read_as(original_lockfile.gsub("foo (0.1.0)", "foo (0.1.1)")) end it "supports gemspec syntax with an alternative path" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", "1.0" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "1.0" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gemspec :path => "#{lib_path("foo")}" G expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end it "doesn't automatically unlock dependencies when using the gemspec syntax" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", ">= 1.0" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", ">= 1.0" end - Dir.chdir lib_path("foo") - - install_gemfile lib_path("foo/Gemfile"), <<-G - source "file://#{gem_repo1}" + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" gemspec G - build_gem "rack", "1.0.1", :to_system => true + build_gem "myrack", "1.0.1", to_system: true - bundle "install" + bundle "install", dir: lib_path("foo") - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") end it "doesn't automatically unlock dependencies when using the gemspec syntax and the gem has development dependencies" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack", ">= 1.0" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", ">= 1.0" s.add_development_dependency "activesupport" end - Dir.chdir lib_path("foo") - - install_gemfile lib_path("foo/Gemfile"), <<-G - source "file://#{gem_repo1}" + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" gemspec G - build_gem "rack", "1.0.1", :to_system => true + build_gem "myrack", "1.0.1", to_system: true - bundle "install" + bundle "install", dir: lib_path("foo") - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo") end it "raises if there are multiple gemspecs" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.write "bar.gemspec", build_spec("bar", "1.0").first.to_ruby end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false gemspec :path => "#{lib_path("foo")}" G - expect(exitstatus).to eq(15) if exitstatus - expect(out).to match(/There are multiple gemspecs/) + expect(exitstatus).to eq(15) + expect(err).to match(/There are multiple gemspecs/) end it "allows :name to be specified to resolve ambiguity" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.write "bar.gemspec" end @@ -269,10 +440,12 @@ RSpec.describe "bundle install with explicit source paths" do s.executables = "foobar" end - install_gemfile <<-G - path "#{lib_path("foo-1.0")}" - gem 'foo' + install_gemfile <<-G, verbose: true + path "#{lib_path("foo-1.0")}" do + gem 'foo' + end G + expect(out).to include("Using foo 1.0 from source at `#{lib_path("foo-1.0")}` and installing its executables") expect(the_bundle).to include_gems "foo 1.0" bundle "exec foobar" @@ -281,13 +454,13 @@ RSpec.describe "bundle install with explicit source paths" do it "handles directories in bin/" do build_lib "foo" - lib_path("foo-1.0").join("foo.gemspec").rmtree + FileUtils.rm_rf lib_path("foo-1.0").join("foo.gemspec") lib_path("foo-1.0").join("bin/performance").mkpath install_gemfile <<-G gem 'foo', '1.0', :path => "#{lib_path("foo-1.0")}" G - expect(err).to lack_errors + expect(err).to be_empty end it "removes the .gem file after installing" do @@ -317,9 +490,9 @@ RSpec.describe "bundle install with explicit source paths" do end it "keeps source pinning" do - build_lib "foo", "1.0", :path => lib_path("foo") - build_lib "omg", "1.0", :path => lib_path("omg") - build_lib "foo", "1.0", :path => lib_path("omg/foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") + build_lib "omg", "1.0", path: lib_path("omg") + build_lib "foo", "1.0", path: lib_path("omg/foo") do |s| s.write "lib/foo.rb", "puts 'FAIL'" end @@ -332,7 +505,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "works when the path does not have a gemspec" do - build_lib "foo", :gemspec => false + build_lib "foo", gemspec: false gemfile <<-G gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}" @@ -344,54 +517,50 @@ RSpec.describe "bundle install with explicit source paths" do end it "works when the path does not have a gemspec but there is a lockfile" do - lockfile <<-L - PATH - remote: vendor/bar - specs: - - GEM - remote: http://rubygems.org + lockfile <<~L + PATH + remote: vendor/bar + specs: L - in_app_root { FileUtils.mkdir_p("vendor/bar") } + FileUtils.mkdir_p(bundled_app("vendor/bar")) install_gemfile <<-G gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard" G - expect(exitstatus).to eq(0) if exitstatus end context "existing lockfile" do it "rubygems gems don't re-resolve without changes" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack-obama', '1.0' + source "https://gem.repo1" + gem 'myrack-obama', '1.0' gem 'net-ssh', '1.0' G - bundle :check, :env => { "DEBUG" => 1 } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0" end it "source path gems w/deps don't re-resolve without changes" do - build_lib "rack-obama", "1.0", :path => lib_path("omg") do |s| + build_lib "myrack-obama", "1.0", path: lib_path("omg") do |s| s.add_dependency "yard" end - build_lib "net-ssh", "1.0", :path => lib_path("omg") do |s| + build_lib "net-ssh", "1.0", path: lib_path("omg") do |s| s.add_dependency "yard" end install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack-obama', :path => "#{lib_path("omg")}" + source "https://gem.repo1" + gem 'myrack-obama', :path => "#{lib_path("omg")}" gem 'net-ssh', :path => "#{lib_path("omg")}" G - bundle :check, :env => { "DEBUG" => 1 } + bundle :check, env: { "DEBUG" => "1" } expect(out).to match(/using resolution from the lockfile/) - expect(the_bundle).to include_gems "rack-obama 1.0", "net-ssh 1.0" + expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0" end end @@ -410,10 +579,10 @@ RSpec.describe "bundle install with explicit source paths" do describe "when the gem version in the path is updated" do before :each do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| + build_lib "foo", "1.0", path: lib_path("foo") do |s| s.add_dependency "bar" end - build_lib "bar", "1.0", :path => lib_path("foo/bar") + build_lib "bar", "1.0", path: lib_path("foo/bar") install_gemfile <<-G gem "foo", :path => "#{lib_path("foo")}" @@ -421,7 +590,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "unlocks all gems when the top level gem is updated" do - build_lib "foo", "2.0", :path => lib_path("foo") do |s| + build_lib "foo", "2.0", path: lib_path("foo") do |s| s.add_dependency "bar" end @@ -431,7 +600,7 @@ RSpec.describe "bundle install with explicit source paths" do end it "unlocks all gems when a child dependency gem is updated" do - build_lib "bar", "2.0", :path => lib_path("foo/bar") + build_lib "bar", "2.0", path: lib_path("foo/bar") bundle "install" @@ -441,42 +610,280 @@ RSpec.describe "bundle install with explicit source paths" do describe "when dependencies in the path are updated" do before :each do - build_lib "foo", "1.0", :path => lib_path("foo") + build_lib "foo", "1.0", path: lib_path("foo") install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "foo", :path => "#{lib_path("foo")}" G end it "gets dependencies that are updated in the path" do - build_lib "foo", "1.0", :path => lib_path("foo") do |s| - s.add_dependency "rack" + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" end bundle "install" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "keeps using the same version if it's compatible" do + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" + end + + bundle "install" + + expect(the_bundle).to include_gems "myrack 0.9.1" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "0.9.1" + end + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack (= 0.9.1) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" + end + + bundle "install" + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "keeps using the same version even when another dependency is added" do + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" + end + + bundle "install" + + expect(the_bundle).to include_gems "myrack 0.9.1" + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + c.checksum gem_repo1, "myrack", "0.9.1" + end + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack (= 0.9.1) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack" + s.add_dependency "rake", rake_version + end + + bundle "install" + + checksums.checksum gem_repo1, "rake", rake_version + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack + rake (= #{rake_version}) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + rake (#{rake_version}) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "does not remove existing ruby platform" do + build_lib "foo", "1.0", path: lib_path("foo") do |s| + s.add_dependency "myrack", "0.9.1" + end + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + lockfile <<~L + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + checksums.checksum gem_repo1, "myrack", "0.9.1" + + expect(lockfile).to eq <<~G + PATH + remote: #{lib_path("foo")} + specs: + foo (1.0) + myrack (= 0.9.1) + + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + foo! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G + end + end + + context "when platform specific version locked, and having less dependencies that the generic version that's actually installed" do + before do + build_repo4 do + build_gem "racc", "1.8.1" + build_gem "mini_portile2", "2.8.2" + end + + build_lib "nokogiri", "1.18.9", path: lib_path("nokogiri") do |s| + s.add_dependency "mini_portile2", "~> 2.8.2" + s.add_dependency "racc", "~> 1.4" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri", path: "#{lib_path("nokogiri")}" + G + + lockfile <<~L + PATH + remote: #{lib_path("nokogiri")} + specs: + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + + GEM + remote: https://rubygems.org/ + specs: + racc (1.8.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + nokogiri! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle "install" end end describe "switching sources" do it "doesn't switch pinned git sources to rubygems when pinning the parent gem to a path source" do - build_gem "foo", "1.0", :to_system => true do |s| + build_gem "foo", "1.0", to_system: true do |s| s.write "lib/foo.rb", "raise 'fail'" end - build_lib "foo", "1.0", :path => lib_path("bar/foo") - build_git "bar", "1.0", :path => lib_path("bar") do |s| + build_lib "foo", "1.0", path: lib_path("bar/foo") + build_git "bar", "1.0", path: lib_path("bar") do |s| s.add_dependency "foo" end install_gemfile <<-G - source "file://#{gem_repo1}" gem "bar", :git => "#{lib_path("bar")}" G install_gemfile <<-G - source "file://#{gem_repo1}" gem "bar", :path => "#{lib_path("bar")}" G @@ -484,23 +891,23 @@ RSpec.describe "bundle install with explicit source paths" do end it "switches the source when the gem existed in rubygems and the path was already being used for another gem" do - build_lib "foo", "1.0", :path => lib_path("foo") - build_gem "bar", "1.0", :to_system => true do |s| + build_lib "foo", "1.0", path: lib_path("foo") + build_gem "bar", "1.0", to_bundle: true do |s| s.write "lib/bar.rb", "raise 'fail'" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "bar" path "#{lib_path("foo")}" do gem "foo" end G - build_lib "bar", "1.0", :path => lib_path("foo/bar") + build_lib "bar", "1.0", path: lib_path("foo/bar") install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" path "#{lib_path("foo")}" do gem "foo" gem "bar" @@ -513,21 +920,17 @@ RSpec.describe "bundle install with explicit source paths" do describe "when there are both a gemspec and remote gems" do it "doesn't query rubygems for local gemspec name" do - build_lib "private_lib", "2.2", :path => lib_path("private_lib") - gemfile = <<-G + build_lib "private_lib", "2.2", path: lib_path("private_lib") + gemfile lib_path("private_lib/Gemfile"), <<-G source "http://localgemserver.test" gemspec - gem 'rack' + gem 'myrack' G - File.open(lib_path("private_lib/Gemfile"), "w") {|f| f.puts gemfile } - - Dir.chdir(lib_path("private_lib")) do - bundle :install, :env => { "DEBUG" => 1 }, :artifice => "endpoint" - expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=rack$}) - expect(out).not_to match(/^HTTP GET.*private_lib/) - expect(the_bundle).to include_gems "private_lib 2.2" - expect(the_bundle).to include_gems "rack 1.0" - end + bundle :install, env: { "DEBUG" => "1" }, artifice: "endpoint", dir: lib_path("private_lib") + expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=myrack$}) + expect(out).not_to match(/^HTTP GET.*private_lib/) + expect(the_bundle).to include_gems "private_lib 2.2", dir: lib_path("private_lib") + expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("private_lib") end end @@ -540,7 +943,6 @@ RSpec.describe "bundle install with explicit source paths" do File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.pre_install_hooks << lambda do |inst| STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" end @@ -548,8 +950,8 @@ RSpec.describe "bundle install with explicit source paths" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(err).to eq_err("Ran pre-install hook: foo-1.0") + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") end it "runs post-install hooks" do @@ -560,7 +962,6 @@ RSpec.describe "bundle install with explicit source paths" do File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.post_install_hooks << lambda do |inst| STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" end @@ -568,8 +969,8 @@ RSpec.describe "bundle install with explicit source paths" do end bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(err).to eq_err("Ran post-install hook: foo-1.0") + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") end it "complains if the install hook fails" do @@ -580,16 +981,37 @@ RSpec.describe "bundle install with explicit source paths" do File.open(lib_path("install_hooks.rb"), "w") do |h| h.write <<-H - require 'rubygems' Gem.pre_install_hooks << lambda do |inst| false end H end - bundle :install, - :requires => [lib_path("install_hooks.rb")] - expect(out).to include("failed for foo-1.0") + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false + expect(err).to include("failed for foo-1.0") + end + + it "loads plugins from the path gem" do + foo_file = home("foo_plugin_loaded") + bar_file = home("bar_plugin_loaded") + expect(foo_file).not_to be_file + expect(bar_file).not_to be_file + + build_lib "foo" do |s| + s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{foo_file}')") + end + + build_git "bar" do |s| + s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{bar_file}')") + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + gem "bar", :path => "#{lib_path("bar-1.0")}" + G + + expect(foo_file).to be_file + expect(bar_file).to be_file end end end diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb index c6eaec7ca6..e12933ebcf 100644 --- a/spec/bundler/install/gemfile/platform_spec.rb +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -1,34 +1,33 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install across platforms" do it "maintains the same lockfile if all gems are compatible across platforms" do lockfile <<-G GEM - remote: file:#{gem_repo1}/ + remote: https://gem.repo1/ specs: - rack (0.9.1) + myrack (0.9.1) PLATFORMS #{not_local} DEPENDENCIES - rack + myrack G install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - expect(the_bundle).to include_gems "rack 0.9.1" + expect(the_bundle).to include_gems "myrack 0.9.1" end it "pulls in the correct platform specific gem" do lockfile <<-G GEM - remote: file:#{gem_repo1} + remote: https://gem.repo1 specs: platform_specific (1.0) platform_specific (1.0-java) @@ -41,100 +40,395 @@ RSpec.describe "bundle install across platforms" do platform_specific G - simulate_platform "java" + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific 1.0 java" + end + end + + it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do + lockfile <<-G + GEM + remote: https://gem.repo1 + specs: + platform_specific (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + G + + bundle_config "frozen true" + install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "platform_specific" G - expect(the_bundle).to include_gems "platform_specific 1.0 JAVA" + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + expect(err).to be_empty end - it "works with gems that have different dependencies" do - simulate_platform "java" - install_gemfile <<-G - source "file://#{gem_repo1}" + context "on universal Rubies" do + before do + build_repo4 do + build_gem "darwin_single_arch" do |s| + s.platform = "ruby" + end + build_gem "darwin_single_arch" do |s| + s.platform = "arm64-darwin" + end + build_gem "darwin_single_arch" do |s| + s.platform = "x86_64-darwin" + end + end + end - gem "nokogiri" - G + it "pulls in the correct architecture gem" do + lockfile <<-G + GEM + remote: https://gem.repo4 + specs: + darwin_single_arch (1.0) + darwin_single_arch (1.0-arm64-darwin) + darwin_single_arch (1.0-x86_64-darwin) - expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + PLATFORMS + ruby - simulate_new_machine + DEPENDENCIES + darwin_single_arch + G - simulate_platform "ruby" - install_gemfile <<-G - source "file://#{gem_repo1}" + simulate_platform "universal-darwin-21" do + simulate_ruby_platform "universal.x86_64-darwin21" do + install_gemfile <<-G + source "https://gem.repo4" - gem "nokogiri" - G + gem "darwin_single_arch" + G - expect(the_bundle).to include_gems "nokogiri 1.4.2" - expect(the_bundle).not_to include_gems "weakling" + expect(the_bundle).to include_gems "darwin_single_arch 1.0 x86_64-darwin" + end + end + end + + it "pulls in the correct architecture gem on arm64e macOS Ruby" do + lockfile <<-G + GEM + remote: https://gem.repo4 + specs: + darwin_single_arch (1.0) + darwin_single_arch (1.0-arm64-darwin) + darwin_single_arch (1.0-x86_64-darwin) + + PLATFORMS + ruby + + DEPENDENCIES + darwin_single_arch + G + + simulate_platform "universal-darwin-21" do + simulate_ruby_platform "universal.arm64e-darwin21" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "darwin_single_arch" + G + + expect(the_bundle).to include_gems "darwin_single_arch 1.0 arm64-darwin" + end + end + end end - it "works the other way with gems that have different dependencies" do - simulate_platform "ruby" - install_gemfile <<-G - source "file://#{gem_repo1}" + it "works with gems that have different dependencies" do + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" - gem "nokogiri" - G + gem "nokogiri" + G - simulate_platform "java" - bundle "install" + expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3" + + pristine_system_gems + bundle_config "force_ruby_platform true" + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + expect(the_bundle).not_to include_gems "weakling" - expect(the_bundle).to include_gems "nokogiri 1.4.2 JAVA", "weakling 0.0.3" + simulate_new_machine + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3" + end end - it "works with gems that have extra platform-specific runtime dependencies" do - simulate_platform x64_mac + it "does not keep unneeded platforms for gems that are used" do + build_repo4 do + build_gem "empyrean", "0.1.0" + build_gem "coderay", "1.1.2" + build_gem "method_source", "0.9.0" + build_gem("spoon", "0.0.6") {|s| s.add_dependency "ffi" } + build_gem "pry", "0.11.3" do |s| + s.platform = "java" + s.add_dependency "coderay", "~> 1.1.0" + s.add_dependency "method_source", "~> 0.9.0" + s.add_dependency "spoon", "~> 0.0" + end + build_gem "pry", "0.11.3" do |s| + s.add_dependency "coderay", "~> 1.1.0" + s.add_dependency "method_source", "~> 0.9.0" + end + build_gem("ffi", "1.9.23") {|s| s.platform = "java" } + build_gem("ffi", "1.9.23") + end + + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo4" + + gem "empyrean", "0.1.0" + gem "pry" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "coderay", "1.1.2" + c.checksum gem_repo4, "empyrean", "0.1.0" + c.checksum gem_repo4, "ffi", "1.9.23", "java" + c.checksum gem_repo4, "method_source", "0.9.0" + c.checksum gem_repo4, "pry", "0.11.3", "java" + c.checksum gem_repo4, "spoon", "0.0.6" + end - update_repo2 do - build_gem "facter", "2.4.6" - build_gem "facter", "2.4.6" do |s| - s.platform = "universal-darwin" - s.add_runtime_dependency "CFPropertyList" + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --add-platform ruby" + + checksums.checksum gem_repo4, "pry", "0.11.3" + + good_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(lockfile).to eq good_lockfile + + bad_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + coderay (1.1.2) + empyrean (0.1.0) + ffi (1.9.23) + ffi (1.9.23-java) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry (0.11.3-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) + spoon (0.0.6) + ffi + + PLATFORMS + java + ruby + + DEPENDENCIES + empyrean (= 0.1.0) + pry + #{checksums} + BUNDLED WITH + 1.16.1 + L + + aggregate_failures do + lockfile bad_lockfile + bundle :install, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle :update, all: true, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle "update ffi", env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle "update empyrean", env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile + + lockfile bad_lockfile + bundle :lock, env: { "BUNDLER_VERSION" => Bundler::VERSION } + expect(lockfile).to eq good_lockfile end - build_gem "CFPropertyList" end + end - install_gemfile! <<-G - source "file://#{gem_repo2}" + it "works with gems with platform-specific dependency having different requirements order" do + simulate_platform "x86_64-darwin-15" do + update_repo2 do + build_gem "fspath", "3" + build_gem "image_optim_pack", "1.2.3" do |s| + s.add_dependency "fspath", ">= 2.1", "< 4" + end + build_gem "image_optim_pack", "1.2.3" do |s| + s.platform = "universal-darwin" + s.add_dependency "fspath", "< 4", ">= 2.1" + end + end - gem "facter" - G + install_gemfile <<-G + source "https://gem.repo2" + G + + install_gemfile <<-G + source "https://gem.repo2" + + gem "image_optim_pack" + G - expect(out).to include "Unable to use the platform-specific (universal-darwin) version of facter (2.4.6) " \ - "because it has different dependencies from the ruby version. " \ - "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." + expect(err).not_to include "Unable to use the platform-specific" - expect(the_bundle).to include_gem "facter 2.4.6" - expect(the_bundle).not_to include_gem "CFPropertyList" + expect(the_bundle).to include_gem "image_optim_pack 1.2.3 universal-darwin" + end end it "fetches gems again after changing the version of Ruby" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle "install --path vendor/bundle" + bundle_config "path vendor/bundle" + bundle :install - new_version = Gem::ConfigMap[:ruby_version] == "1.8" ? "1.9.1" : "1.8" - FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, new_version)) + FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8")) - bundle "install --path vendor/bundle" - expect(vendored_gems("gems/rack-1.0.0")).to exist + bundle :install + expect(vendored_gems("gems/myrack-1.0.0")).to exist + end + + it "keeps existing platforms when installing with force_ruby_platform" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "platform_specific", "1.0" + c.checksum gem_repo1, "platform_specific", "1.0", "java" + end + + lockfile <<-G + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0-java) + + PLATFORMS + java + + DEPENDENCIES + platform_specific + #{checksums} + G + + bundle_config "force_ruby_platform true" + + install_gemfile <<-G + source "https://gem.repo1" + gem "platform_specific" + G + + checksums.checksum gem_repo1, "platform_specific", "1.0" + + expect(the_bundle).to include_gem "platform_specific 1.0 ruby" + + expect(lockfile).to eq <<~G + GEM + remote: https://gem.repo1/ + specs: + platform_specific (1.0) + platform_specific (1.0-java) + + PLATFORMS + java + ruby + + DEPENDENCIES + platform_specific + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + G end end RSpec.describe "bundle install with platform conditionals" do it "installs gems tagged w/ the current platforms" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" platforms :#{local_tag} do gem "nokogiri" @@ -146,20 +440,63 @@ RSpec.describe "bundle install with platform conditionals" do it "does not install gems tagged w/ another platforms" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" platforms :#{not_local_tag} do gem "nokogiri" end G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" expect(the_bundle).not_to include_gems "nokogiri 1.4.2" end + it "installs gems tagged w/ another platform but also dependent on the current one transitively" do + build_repo4 do + build_gem "activesupport", "6.1.4.1" do |s| + s.add_dependency "tzinfo", "~> 2.0" + end + + build_gem "tzinfo", "2.0.4" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "activesupport" + + platforms :#{not_local_tag} do + gem "tzinfo", "~> 1.2" + end + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + activesupport (6.1.4.1) + tzinfo (~> 2.0) + tzinfo (2.0.4) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + activesupport + tzinfo (~> 1.2) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + + expect(the_bundle).to include_gems "tzinfo 2.0.4" + end + it "installs gems tagged w/ the current platforms inline" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "nokogiri", :platforms => :#{local_tag} G expect(the_bundle).to include_gems "nokogiri 1.4.2" @@ -167,17 +504,17 @@ RSpec.describe "bundle install with platform conditionals" do it "does not install gems tagged w/ another platforms inline" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" gem "nokogiri", :platforms => :#{not_local_tag} G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" expect(the_bundle).not_to include_gems "nokogiri 1.4.2" end it "installs gems tagged w/ the current platform inline" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "nokogiri", :platform => :#{local_tag} G expect(the_bundle).to include_gems "nokogiri 1.4.2" @@ -185,7 +522,7 @@ RSpec.describe "bundle install with platform conditionals" do it "doesn't install gems tagged w/ another platform inline" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "nokogiri", :platform => :#{not_local_tag} G expect(the_bundle).not_to include_gems "nokogiri 1.4.2" @@ -195,21 +532,20 @@ RSpec.describe "bundle install with platform conditionals" do build_git "foo" install_gemfile <<-G + source "https://gem.repo1" platform :#{not_local_tag} do gem "foo", :git => "#{lib_path("foo-1.0")}" end G - bundle :show - expect(exitstatus).to eq(0) if exitstatus + bundle :list end it "does not attempt to install gems from :rbx when using --local" do - simulate_platform "ruby" - simulate_ruby_engine "ruby" + bundle_config "force_ruby_platform true" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "some_gem", :platform => :rbx G @@ -218,48 +554,85 @@ RSpec.describe "bundle install with platform conditionals" do end it "does not attempt to install gems from other rubies when using --local" do - simulate_platform "ruby" - simulate_ruby_engine "ruby" - other_ruby_version_tag = RUBY_VERSION =~ /^1\.8/ ? :ruby_19 : :ruby_18 - + bundle_config "force_ruby_platform true" gemfile <<-G - source "file://#{gem_repo1}" - gem "some_gem", platform: :#{other_ruby_version_tag} + source "https://gem.repo1" + gem "some_gem", platform: :ruby_22 G bundle "install --local" expect(out).not_to match(/Could not find gem 'some_gem/) end - it "prints a helpful warning when a dependency is unused on any platform" do - simulate_platform "ruby" - simulate_ruby_engine "ruby" + it "does not print a warning when a dependency is unused on a platform different from the current one" do + bundle_config "force_ruby_platform true" gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack", :platform => [:mingw, :mswin, :x64_mingw, :jruby] + gem "myrack", :platform => [:windows, :jruby] G - bundle! "install" + bundle "install" + + expect(err).to be_empty - expect(out).to include <<-O.strip -The dependency #{Gem::Dependency.new("rack", ">= 0")} will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. - O + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + myrack + #{checksums_section_when_enabled} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "resolves fine when a dependency is unused on a platform different from the current one, but reintroduced transitively" do + bundle_config "force_ruby_platform true" + + build_repo4 do + build_gem "listen", "3.7.1" do |s| + s.add_dependency "ffi" + end + + build_gem "ffi", "1.15.5" + end + + install_gemfile <<~G + source "https://gem.repo4" + + gem "listen" + gem "ffi", :platform => :windows + G + expect(err).to be_empty end end RSpec.describe "when a gem has no architecture" do it "still installs correctly" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + build_repo2 do + # The rcov gem is platform mswin32, but has no arch + build_gem "rcov" do |s| + s.platform = Gem::Platform.new([nil, "mswin32", nil]) + s.write "lib/rcov.rb", "RCOV = '1.0.0'" + end + end - gemfile <<-G - # Try to install gem with nil arch - source "http://localgemserver.test/" - gem "rcov" - G + gemfile <<-G + # Try to install gem with nil arch + source "http://localgemserver.test/" + gem "rcov" + G - bundle :install, :artifice => "windows" - expect(the_bundle).to include_gems "rcov 1.0.0" + bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(the_bundle).to include_gems "rcov 1.0.0" + end end end diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb index b9d9683758..d937abd714 100644 --- a/spec/bundler/install/gemfile/ruby_spec.rb +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -1,109 +1,157 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "ruby requirement" do def locked_ruby_version - Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(lockfile).ruby_version) + Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(File.read(bundled_app_lock)).ruby_version) end - # As discovered by https://github.com/bundler/bundler/issues/4147, there is + # As discovered by https://github.com/rubygems/bundler/issues/4147, there is # no test coverage to ensure that adding a gem is possible with a ruby # requirement. This test verifies the fix, committed in bfbad5c5. it "allows adding gems" do install_gemfile <<-G - source "file://#{gem_repo1}" - ruby "#{RUBY_VERSION}" - gem "rack" + source "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" G install_gemfile <<-G - source "file://#{gem_repo1}" - ruby "#{RUBY_VERSION}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" + gem "myrack-obama" G - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack-obama 1.0" + expect(the_bundle).to include_gems "myrack-obama 1.0" end it "allows removing the ruby version requirement" do install_gemfile <<-G - source "file://#{gem_repo1}" - ruby "~> #{RUBY_VERSION}" - gem "rack" + source "https://gem.repo1" + ruby "~> #{Gem.ruby_version}" + gem "myrack" G expect(lockfile).to include("RUBY VERSION") install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(lockfile).not_to include("RUBY VERSION") end it "allows changing the ruby version requirement to something compatible" do install_gemfile <<-G - source "file://#{gem_repo1}" - ruby ">= 1.0.0" - gem "rack" + source "https://gem.repo1" + ruby ">= #{current_ruby_minor}" + gem "myrack" G + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) - simulate_ruby_version "5100" - install_gemfile <<-G - source "file://#{gem_repo1}" - ruby ">= 1.0.1" - gem "rack" + source "https://gem.repo1" + ruby ">= #{Gem.ruby_version}" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) end it "allows changing the ruby version requirement to something incompatible" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" ruby ">= 1.0.0" - gem "rack" + gem "myrack" G - expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + RUBY VERSION + ruby 2.1.4 + + BUNDLED WITH + #{Bundler::VERSION} + L - simulate_ruby_version "5100" + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) install_gemfile <<-G - source "file://#{gem_repo1}" - ruby ">= 5000.0" - gem "rack" + source "https://gem.repo1" + ruby ">= #{current_ruby_minor}" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" - expect(locked_ruby_version.versions).to eq(["5100"]) + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(locked_ruby_version).to eq(Bundler::RubyVersion.system) end it "allows requirements with trailing whitespace" do - install_gemfile! <<-G - source "file://#{gem_repo1}" - ruby "#{RUBY_VERSION}\\n \t\\n" - gem "rack" + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{Gem.ruby_version}\\n \t\\n" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "fails gracefully with malformed requirements" do - install_gemfile <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" ruby ">= 0", "-.\\0" - gem "rack" + gem "myrack" G - expect(out).to include("There was an error parsing") # i.e. DSL error, not error template + expect(err).to include("There was an error parsing") # i.e. DSL error, not error template + end + + it "allows picking up ruby version from a file" do + create_file ".ruby-version", Gem.ruby_version.to_s + + install_gemfile <<-G + source "https://gem.repo1" + ruby file: ".ruby-version" + gem "myrack" + G + + expect(lockfile).to include("RUBY VERSION") + end + + it "reads the ruby version file from the right folder when nested Gemfiles are involved" do + create_file ".ruby-version", Gem.ruby_version.to_s + + gemfile <<-G + source "https://gem.repo1" + ruby file: ".ruby-version" + gem "myrack" + G + + nested_dir = bundled_app(".ruby-lsp") + + FileUtils.mkdir nested_dir + + gemfile ".ruby-lsp/Gemfile", <<-G + eval_gemfile(File.expand_path("../Gemfile", __dir__)) + G + + bundle "install", dir: nested_dir + + expect(bundled_app(".ruby-lsp/Gemfile.lock").read).to include("RUBY VERSION") end end diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb index c5375b4abf..654d638e1f 100644 --- a/spec/bundler/install/gemfile/sources_spec.rb +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -1,323 +1,515 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with gems on multiple sources" do # repo1 is built automatically before all of the specs run - # it contains rack-obama 1.0.0 and rack 0.9.1 & 1.0.0 amongst other gems + # it contains myrack-obama 1.0.0 and myrack 0.9.1 & 1.0.0 amongst other gems - context "without source affinity" do - before do - # Oh no! Someone evil is trying to hijack rack :( - # need this to be broken to check for correct source ordering - build_repo gem_repo3 do - build_gem "rack", repo3_rack_version do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" - end - end - end + context "with source affinity" do + context "with sources given by a block" do + before do + # Oh no! Someone evil is trying to hijack myrack :( + # need this to be broken to check for correct source ordering + build_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end - context "with multiple toplevel sources" do - let(:repo3_rack_version) { "1.0.0" } + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + end + end - before do gemfile <<-G - source "file://#{gem_repo3}" - source "file://#{gem_repo1}" - gem "rack-obama" - gem "rack" + source "https://gem.repo3" + source "https://gem.repo1" do + gem "thin" # comes first to test name sorting + gem "myrack" + end + gem "myrack-obama" # should come from repo3! G - bundle "config major_deprecations true" end - it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first" do - bundle :install - - expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") - expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(out).to include("Installed from: file:#{gem_repo1}") - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + it "installs the gems without any warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("myrack-obama 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote1") end - it "errors when disable_multisource is set" do - bundle "config disable_multisource true" - bundle :install - expect(out).to include("Each source after the first must include a block") - expect(exitstatus).to eq(4) if exitstatus + it "can cache and deploy" do + bundle :cache, artifice: "compact_index" + + expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist + expect(bundled_app("vendor/cache/myrack-obama-1.0.gem")).to exist + + bundle_config "deployment true" + bundle :install, artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0") end end - context "when different versions of the same gem are in multiple sources" do - let(:repo3_rack_version) { "1.2" } - + context "with sources set by an option" do before do - gemfile <<-G - source "file://#{gem_repo3}" - source "file://#{gem_repo1}" - gem "rack-obama" - gem "rack", "1.0.0" # force it to install the working version in repo1 + # Oh no! Someone evil is trying to hijack myrack :( + # need this to be broken to check for correct source ordering + build_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end + + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + end + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo3" + gem "myrack-obama" # should come from repo3! + gem "myrack", :source => "https://gem.repo1" G - bundle "config major_deprecations true" end - it "warns about ambiguous gems, but installs anyway" do - bundle :install - - expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") - expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(out).to include("Installed from: file:#{gem_repo1}") - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0", :source => "remote1") + it "installs the gems without any warning" do + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0") end end - end - context "with source affinity" do - context "with sources given by a block" do + context "when a pinned gem has an indirect dependency in the pinned source" do before do - # Oh no! Someone evil is trying to hijack rack :( - # need this to be broken to check for correct source ordering - build_repo gem_repo3 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" + build_repo3 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end + # we need a working myrack gem in repo3 + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" + end + gemfile <<-G - source "file://#{gem_repo3}" - source "file://#{gem_repo1}" do - gem "thin" # comes first to test name sorting - gem "rack" + source "https://gem.repo2" + source "https://gem.repo3" do + gem "depends_on_myrack" end - gem "rack-obama" # shoud come from repo3! G end - it "installs the gems without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("rack-obama 1.0.0") - expect(the_bundle).to include_gems("rack 1.0.0", :source => "remote1") + context "and not in any other sources" do + before do + build_repo(gem_repo2) {} + end + + it "installs from the same source without any warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end end - it "can cache and deploy" do - bundle :package + context "and in another source" do + before do + # need this to be broken to check for correct source ordering + build_repo gem_repo2 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end + end + end + + it "installs from the same source without any warning" do + bundle :install, artifice: "compact_index" - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - expect(bundled_app("vendor/cache/rack-obama-1.0.gem")).to exist + expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") - bundle "install --deployment" + # In https://github.com/bundler/bundler/issues/3585 this failed + # when there is already a lockfile, and the gems are missing, so try again + system_gems [] + bundle :install, artifice: "compact_index" - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end end end - context "with sources set by an option" do + context "when a pinned gem has an indirect dependency in a different source" do before do - # Oh no! Someone evil is trying to hijack rack :( - # need this to be broken to check for correct source ordering - build_repo gem_repo3 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" + # In these tests, we need a working myrack gem in repo2 and not repo3 + + build_repo3 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end + build_repo gem_repo2 do + build_gem "myrack", "1.0.0" + end + end + + context "and not in any other sources" do + before do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2" + source "https://gem.repo3" do + gem "depends_on_myrack" + end + G + end + + it "installs from the other source without any warning" do + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0") + end + end + end + + context "when a top-level gem can only be found in an scoped source" do + before do + build_repo2 + + build_repo3 do + build_gem "private_gem_1", "1.0.0" + build_gem "private_gem_2", "1.0.0" + end + gemfile <<-G - source "file://#{gem_repo3}" - gem "rack-obama" # should come from repo3! - gem "rack", :source => "file://#{gem_repo1}" + source "https://gem.repo2" + + gem "private_gem_1" + + source "https://gem.repo3" do + gem "private_gem_2" + end G end - it "installs the gems without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("rack-obama 1.0.0", "rack 1.0.0") + it "fails" do + bundle :install, artifice: "compact_index", raise_on_error: false + expect(err).to include("Could not find gem 'private_gem_1' in rubygems repository https://gem.repo2/ or installed locally.") end end - context "with an indirect dependency" do + context "when a top-level gem has an indirect dependency" do before do - build_repo gem_repo3 do - build_gem "depends_on_rack", "1.0.1" do |s| - s.add_dependency "rack" + build_repo gem_repo2 do + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end + + build_repo3 do + build_gem "unrelated_gem", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "depends_on_myrack" + + source "https://gem.repo3" do + gem "unrelated_gem" + end + G end - context "when the indirect dependency is in the pinned source" do + context "and the dependency is only in the top-level source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" + end + end + + it "installs the dependency from the top-level source without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3") + end + end + + context "and the dependency is only in a pinned source" do before do - # we need a working rack gem in repo3 update_repo gem_repo3 do - build_gem "rack", "1.0.0" + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end end + end - gemfile <<-G - source "file://#{gem_repo2}" - source "file://#{gem_repo3}" do - gem "depends_on_rack" + it "does not find the dependency" do + bundle :install, artifice: "compact_index", raise_on_error: false + expect(err).to end_with <<~E.strip + Could not find compatible versions + + Because every version of depends_on_myrack depends on myrack >= 0 + and myrack >= 0 could not be found in rubygems repository https://gem.repo2/ or installed locally, + depends_on_myrack cannot be used. + So, because Gemfile depends on depends_on_myrack >= 0, + version solving has failed. + E + end + end + + context "and the dependency is in both the top-level and a pinned source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" + end + + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" end - G + end end - context "and not in any other sources" do - before do - build_repo(gem_repo2) {} + it "installs the dependency from the top-level source without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(run("require 'myrack'; puts MYRACK")).to eq("1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0") + expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3") + end + end + end + + context "when a scoped gem has a deeply nested indirect dependency" do + before do + build_repo3 do + build_gem "depends_on_depends_on_myrack", "1.0.1" do |s| + s.add_dependency "depends_on_myrack" end - it "installs from the same source without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" end end - context "and in another source" do - before do - # need this to be broken to check for correct source ordering - build_repo gem_repo2 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" - end - end + gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo3" do + gem "depends_on_depends_on_myrack" end + G + end - it "installs from the same source without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + context "and the dependency is only in the top-level source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" end end + + it "installs the dependency from the top-level source" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0") + expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote2") + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", source: "remote3") + end end - context "when the indirect dependency is in a different source" do + context "and the dependency is only in a pinned source" do before do - # In these tests, we need a working rack gem in repo2 and not repo3 - build_repo gem_repo2 do - build_gem "rack", "1.0.0" + build_repo2 + + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" end end - context "and not in any other sources" do - before do - gemfile <<-G - source "file://#{gem_repo2}" - source "file://#{gem_repo3}" do - gem "depends_on_rack" - end - G + it "installs the dependency from the pinned source" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end + end + + context "and the dependency is in both the top-level and a pinned source" do + before do + update_repo gem_repo2 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end end - it "installs from the other source without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" end end - context "and in yet another source" do - before do - gemfile <<-G - source "file://#{gem_repo1}" - source "file://#{gem_repo2}" - source "file://#{gem_repo3}" do - gem "depends_on_rack" - end - G - end + it "installs the dependency from the pinned source without warning" do + bundle :install, artifice: "compact_index" + expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3") + end + end + end + + context "when a top-level gem has an indirect dependency present in the default source, but with a different version from the one resolved" do + before do + build_lib "activesupport", "7.0.0.alpha", path: lib_path("rails/activesupport") + build_lib "rails", "7.0.0.alpha", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.0.0.alpha" + end + + build_repo gem_repo2 do + build_gem "activesupport", "6.1.2" - it "installs from the other source and warns about ambiguous gems" do - bundle "config major_deprecations true" - bundle :install - expect(out).to have_major_deprecation a_string_including("Your Gemfile contains multiple primary sources.") - expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(out).to include("Installed from: file:#{gem_repo2}") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + build_gem "webpacker", "5.2.1" do |s| + s.add_dependency "activesupport", ">= 5.2" end end - context "and only the dependency is pinned" do - before do - # need this to be broken to check for correct source ordering - build_repo gem_repo2 do - build_gem "rack", "1.0.0" do |s| - s.write "lib/rack.rb", "RACK = 'FAIL'" - end - end + gemfile <<-G + source "https://gem.repo2" + + gemspec :path => "#{lib_path("rails")}" + + gem "webpacker", "~> 5.0" + G + end - gemfile <<-G - source "file://#{gem_repo3}" # contains depends_on_rack - source "file://#{gem_repo2}" # contains broken rack + it "installs all gems without warning" do + bundle :install, artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", "rails 7.0.0.alpha") + expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", source: "path@#{lib_path("rails/activesupport")}") + expect(the_bundle).to include_gems("rails 7.0.0.alpha", source: "path@#{lib_path("rails")}") + end + end - gem "depends_on_rack" # installed from gem_repo3 - gem "rack", :source => "file://#{gem_repo1}" - G + context "when a pinned gem has an indirect dependency with more than one level of indirection in the default source " do + before do + build_repo3 do + build_gem "handsoap", "0.2.5.5" do |s| + s.add_dependency "nokogiri", ">= 1.2.3" end + end - it "installs the dependency from the pinned source without warning" do - bundle :install + update_repo gem_repo2 do + build_gem "nokogiri", "1.11.1" do |s| + s.add_dependency "racca", "~> 1.4" + end - expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + build_gem "racca", "1.5.2" + end - # In https://github.com/bundler/bundler/issues/3585 this failed - # when there is already a lock file, and the gems are missing, so try again - system_gems [] - bundle :install + gemfile <<-G + source "https://gem.repo2" - expect(out).not_to include("Warning: the gem 'rack' was found in multiple sources.") - expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0") + source "https://gem.repo3" do + gem "handsoap" end + + gem "nokogiri" + G + end + + it "installs from the default source without any warnings or errors and generates a proper lockfile" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo3, "handsoap", "0.2.5.5" + c.checksum gem_repo2, "nokogiri", "1.11.1" + c.checksum gem_repo2, "racca", "1.5.2" end + + expected_lockfile = <<~L + GEM + remote: https://gem.repo2/ + specs: + nokogiri (1.11.1) + racca (~> 1.4) + racca (1.5.2) + + GEM + remote: https://gem.repo3/ + specs: + handsoap (0.2.5.5) + nokogiri (>= 1.2.3) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + handsoap! + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose", artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3") + expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2") + expect(lockfile).to eq(expected_lockfile) + + # Even if the gems are already installed + FileUtils.rm bundled_app_lock + bundle "install --verbose", artifice: "compact_index" + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2") + expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3") + expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2") + expect(lockfile).to eq(expected_lockfile) end end context "with a gem that is only found in the wrong source" do before do - build_repo gem_repo3 do + build_repo3 do build_gem "not_in_repo1", "1.0.0" end - gemfile <<-G - source "file://#{gem_repo3}" - gem "not_in_repo1", :source => "file://#{gem_repo1}" + install_gemfile <<-G, artifice: "compact_index", raise_on_error: false + source "https://gem.repo3" + gem "not_in_repo1", :source => "https://gem.repo1" G end it "does not install the gem" do - bundle :install - expect(out).to include("Could not find gem 'not_in_repo1'") + expect(err).to include("Could not find gem 'not_in_repo1'") end end context "with an existing lockfile" do before do - system_gems "rack-0.9.1", "rack-1.0.0" + system_gems "myrack-0.9.1", "myrack-1.0.0", path: default_bundle_path lockfile <<-L GEM - remote: file:#{gem_repo1} - remote: file:#{gem_repo3} + remote: https://gem.repo1 specs: - rack (0.9.1) + + GEM + remote: https://gem.repo3 + specs: + myrack (0.9.1) PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES - rack! + myrack! L gemfile <<-G - source "file://#{gem_repo1}" - source "file://#{gem_repo3}" do - gem 'rack' + source "https://gem.repo1" + source "https://gem.repo3" do + gem 'myrack' end G end - # Reproduction of https://github.com/bundler/bundler/issues/3298 + # Reproduction of https://github.com/rubygems/bundler/issues/3298 it "does not unlock the installed gem on exec" do - expect(the_bundle).to include_gems("rack 0.9.1") + expect(the_bundle).to include_gems("myrack 0.9.1") end end @@ -326,113 +518,96 @@ RSpec.describe "bundle install with gems on multiple sources" do build_lib "foo" gemfile <<-G - gem "rack", :source => "file://#{gem_repo1}" + source "https://gem.repo1" + gem "myrack", :source => "https://gem.repo1" gem "foo", :path => "#{lib_path("foo-1.0")}" G end it "does not unlock the non-path gem after install" do - bundle :install + bundle :install, artifice: "compact_index" bundle %(exec ruby -e 'puts "OK"') expect(out).to include("OK") - expect(exitstatus).to eq(0) if exitstatus end end end context "when an older version of the same gem also ships with Ruby" do before do - system_gems "rack-0.9.1" + system_gems "myrack-0.9.1" - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" # shoud come from repo1! + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + gem "myrack" # should come from repo1! G end it "installs the gems without any warning" do - bundle :install - expect(out).not_to include("Warning") - expect(the_bundle).to include_gems("rack 1.0.0") + expect(err).not_to include("Warning") + expect(the_bundle).to include_gems("myrack 1.0.0") end end context "when a single source contains multiple locked gems" do before do - # 1. With these gems, + # With these gems, build_repo4 do build_gem "foo", "0.1" build_gem "bar", "0.1" end - # 2. Installing this gemfile will produce... + # Installing this gemfile... gemfile <<-G - source 'file://#{gem_repo1}' - gem 'rack' - gem 'foo', '~> 0.1', :source => 'file://#{gem_repo4}' - gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + source 'https://gem.repo1' + gem 'myrack' + gem 'foo', '~> 0.1', :source => 'https://gem.repo4' + gem 'bar', '~> 0.1', :source => 'https://gem.repo4' G - # 3. this lockfile. - lockfile <<-L - GEM - remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote1/ - remote: file:/Users/andre/src/bundler/bundler/tmp/gems/remote4/ - specs: - bar (0.1) - foo (0.1) - rack (1.0.0) - - PLATFORMS - ruby - - DEPENDENCIES - bar (~> 0.1)! - foo (~> 0.1)! - rack - L - - bundle "install --path ../gems/system" + bundle_config "path ../gems/system" + bundle :install, artifice: "compact_index" - # 4. Then we add some new versions... - update_repo4 do + # And then we add some new versions... + build_repo4 do build_gem "foo", "0.2" build_gem "bar", "0.3" end end it "allows them to be unlocked separately" do - # 5. and install this gemfile, updating only foo. - install_gemfile <<-G - source 'file://#{gem_repo1}' - gem 'rack' - gem 'foo', '~> 0.2', :source => 'file://#{gem_repo4}' - gem 'bar', '~> 0.1', :source => 'file://#{gem_repo4}' + # And install this gemfile, updating only foo. + install_gemfile <<-G, artifice: "compact_index" + source 'https://gem.repo1' + gem 'myrack' + gem 'foo', '~> 0.2', :source => 'https://gem.repo4' + gem 'bar', '~> 0.1', :source => 'https://gem.repo4' G - # 6. Which should update foo to 0.2, but not the (locked) bar 0.1 - expect(the_bundle).to include_gems("foo 0.2") - expect(the_bundle).to include_gems("bar 0.1") + # It should update foo to 0.2, but not the (locked) bar 0.1 + expect(the_bundle).to include_gems("foo 0.2", "bar 0.1") end end context "re-resolving" do context "when there is a mix of sources in the gemfile" do before do - build_repo3 + build_repo3 do + build_gem "myrack" + end + build_lib "path1" build_lib "path2" build_git "git1" build_git "git2" - install_gemfile <<-G - source "file://#{gem_repo1}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" gem "rails" - source "file://#{gem_repo3}" do - gem "rack" + source "https://gem.repo3" do + gem "myrack" end gem "path1", :path => "#{lib_path("path1-1.0")}" @@ -443,7 +618,7 @@ RSpec.describe "bundle install with gems on multiple sources" do end it "does not re-resolve" do - bundle :install, :verbose => true + bundle :install, artifice: "compact_index", verbose: true expect(out).to include("using resolution from the lockfile") expect(out).not_to include("re-resolving dependencies") end @@ -452,67 +627,687 @@ RSpec.describe "bundle install with gems on multiple sources" do context "when a gem is installed to system gems" do before do - install_gemfile! <<-G - source "file://#{gem_repo1}" - gem "rack" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + gem "myrack" G end context "and the gemfile changes" do it "is still able to find that gem from remote sources" do - source_uri = "file://#{gem_repo1}" - second_uri = "file://#{gem_repo4}" - build_repo4 do - build_gem "rack", "2.0.1.1.forked" + build_gem "myrack", "2.0.1.1.forked" build_gem "thor", "0.19.1.1.forked" end # When this gemfile is installed... - gemfile <<-G - source "#{source_uri}" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" - source "#{second_uri}" do - gem "rack", "2.0.1.1.forked" + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" gem "thor" end - gem "rack-obama" + gem "myrack-obama" G - # It creates this lockfile. - lockfile <<-L - GEM - remote: #{source_uri}/ - remote: #{second_uri}/ - specs: - rack (2.0.1.1.forked) - rack-obama (1.0) - rack - thor (0.19.1.1.forked) - - PLATFORMS - ruby - - DEPENDENCIES - rack (= 2.0.1.1.forked)! - rack-obama - thor! - L - # Then we change the Gemfile by adding a version to thor gemfile <<-G - source "#{source_uri}" + source "https://gem.repo1" - source "#{second_uri}" do - gem "rack", "2.0.1.1.forked" + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" gem "thor", "0.19.1.1.forked" end - gem "rack-obama" + gem "myrack-obama" G - # But we should still be able to find rack 2.0.1.1.forked and install it - bundle! :install + # But we should still be able to find myrack 2.0.1.1.forked and install it + bundle :install, artifice: "compact_index" + end + end + end + + describe "source changed to one containing a higher version of a dependency" do + before do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + + gem "myrack" + G + + build_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "bar" + end + + build_lib("gemspec_test", path: tmp("gemspec_test")) do |s| + s.add_dependency "bar", "=1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2" + gem "myrack" + gemspec :path => "#{tmp("gemspec_test")}" + G + end + + it "conservatively installs the existing locked version" do + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "when Gemfile overrides a gemspec development dependency to change the default source" do + before do + build_repo4 do + build_gem "bar" + end + + build_lib("gemspec_test", path: tmp("gemspec_test")) do |s| + s.add_development_dependency "bar" + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "bar" + end + + gemspec :path => "#{tmp("gemspec_test")}" + G + end + + it "does not print warnings" do + expect(err).to be_empty + end + end + + it "doesn't update version when a gem uses a source block but a higher version from another source is already installed locally" do + build_repo2 do + build_gem "example", "0.1.0" + end + + build_repo4 do + build_gem "example", "1.0.2" + end + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo4" + + gem "example", :source => "https://gem.repo2" + G + + bundle "info example" + expect(out).to include("example (0.1.0)") + + system_gems "example-1.0.2", path: default_bundle_path, gem_repo: gem_repo4 + + bundle "update example --verbose", artifice: "compact_index" + expect(out).not_to include("Using example 1.0.2") + expect(out).to include("Using example 0.1.0") + end + + it "fails immediately with a helpful error when a rubygems source does not exist and bundler/setup is required" do + gemfile <<-G + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "example" + end + G + + ruby <<~R, raise_on_error: false + require 'bundler/setup' + R + + expect(last_command).to be_failure + expect(err).to include("Could not find gem 'example' in locally installed gems.") + end + + it "fails immediately with a helpful error when a non retriable network error happens while resolving sources" do + gemfile <<-G + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "example" + end + G + + bundle "install", artifice: nil, raise_on_error: false + + expect(last_command).to be_failure + expect(err).to include("Could not reach host gem.repo4. Check your network connection and try again.") + end + + context "when an indirect dependency is available from multiple ambiguous sources" do + it "raises, suggesting a source block" do + build_repo4 do + build_gem "depends_on_myrack" do |s| + s.add_dependency "myrack" + end + build_gem "myrack" + end + + install_gemfile <<-G, artifice: "compact_index_extra_api", raise_on_error: false + source "https://global.source" + + source "https://scoped.source/extra" do + gem "depends_on_myrack" + end + + source "https://scoped.source" do + gem "thin" + end + G + expect(last_command).to be_failure + expect(err).to eq <<~EOS.strip + The gem 'myrack' was found in multiple relevant sources. + * rubygems repository https://scoped.source/ + * rubygems repository https://scoped.source/extra/ + You must add this gem to the source block for the source you wish it to be installed from. + EOS + expect(the_bundle).not_to be_locked + end + end + + context "when default source includes old gems with nil required_ruby_version" do + before do + build_repo2 do + build_gem "ruport", "1.7.0.3" do |s| + s.add_dependency "pdf-writer", "1.1.8" + end + end + + build_repo gem_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_ruby_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra" + G + end + + it "handles that fine" do + bundle "install", artifice: "compact_index_extra" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + c.checksum gem_repo2, "ruport", "1.7.0.3" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + GEM + remote: https://gem.repo4/extra/ + specs: + ruport (1.7.0.3) + pdf-writer (= 1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruport (= 1.7.0.3)! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when default source includes old gems with nil required_rubygems_version" do + before do + build_repo2 do + build_gem "ruport", "1.7.0.3" do |s| + s.add_dependency "pdf-writer", "1.1.8" + end + end + + build_repo gem_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_rubygems_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra" + G + end + + it "handles that fine" do + bundle "install", artifice: "compact_index_extra" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + c.checksum gem_repo2, "ruport", "1.7.0.3" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + GEM + remote: https://gem.repo4/extra/ + specs: + ruport (1.7.0.3) + pdf-writer (= 1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ruport (= 1.7.0.3)! + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when default source uses the old API and includes old gems with nil required_rubygems_version" do + before do + build_repo4 do + build_gem "pdf-writer", "1.1.8" + end + + path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz" + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) + spec.instance_variable_set(:@required_rubygems_version, nil) + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) end + + gemfile <<~G + source "https://gem.repo4" + + gem "pdf-writer", "= 1.1.8" + G + end + + it "handles that fine" do + bundle "install --verbose", artifice: "endpoint" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "pdf-writer", "1.1.8" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + pdf-writer (1.1.8) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + pdf-writer (= 1.1.8) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when mistakenly adding a top level gem already depended on and cached under the wrong source" do + before do + build_repo4 do + build_gem "some_private_gem", "0.1.0" do |s| + s.add_dependency "example", "~> 1.0" + end + end + + build_repo2 do + build_gem "example", "1.0.0" + end + + install_gemfile <<~G, artifice: "compact_index" + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "some_private_gem" + end + G + + gemfile <<~G + source "https://gem.repo2" + + source "https://gem.repo4" do + gem "some_private_gem" + gem "example" # MISTAKE, example is not available at gem.repo4 + end + G + end + + it "shows a proper error message and does not generate a corrupted lockfile" do + expect do + bundle :install, artifice: "compact_index", raise_on_error: false, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + end.not_to change { lockfile } + + expect(err).to include("Could not find gem 'example' in rubygems repository https://gem.repo4/") + end + end + + context "when a gem has versions in two sources, but only the locked one has updates" do + let(:original_lockfile) do + <<~L + GEM + remote: https://main.source/ + specs: + activesupport (1.0) + bigdecimal + bigdecimal (1.0.0) + + GEM + remote: https://main.source/extra/ + specs: + foo (1.0) + bigdecimal + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + activesupport + foo! + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo3 do + build_gem "activesupport" do |s| + s.add_dependency "bigdecimal" + end + + build_gem "bigdecimal", "1.0.0" + build_gem "bigdecimal", "3.3.1" + end + + build_repo4 do + build_gem "foo" do |s| + s.add_dependency "bigdecimal" + end + + build_gem "bigdecimal", "1.0.0" + end + + gemfile <<~G + source "https://main.source" + + gem "activesupport" + + source "https://main.source/extra" do + gem "foo" + end + G + + lockfile original_lockfile + end + + it "properly upgrades the lockfile when updating that specific gem" do + bundle "update bigdecimal --conservative", artifice: "compact_index_extra_api", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo3.to_s } + + expect(lockfile).to eq original_lockfile.gsub("bigdecimal (1.0.0)", "bigdecimal (3.3.1)") + end + end + + context "when switching a gem with components from rubygems to git source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_git "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_git "actionpack", "7.0.0", path: lib_path("rails") + build_git "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the git source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", git: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting on GIT first, then GEM + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end + + context "when switching a gem with components from rubygems to path source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_lib "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_lib "actionpack", "7.0.0", path: lib_path("rails") + build_lib "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the path source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", path: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting appropriately + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end + + context "when a scoped rubygems source is missing a transitive dependency" do + before do + build_repo2 do + build_gem "fallback_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_repo3 do + build_gem "private_parent", "1.0.0" do |s| + s.add_dependency "fallback_dep" + end + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + source "https://gem.repo3" do + gem "private_parent", "1.0.0" + end + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("private_parent 1.0.0", "fallback_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("private_parent 1.0.0", source: "remote3") + expect(the_bundle).to include_gems("fallback_dep 1.0.0", source: "remote2") + end + end + + context "when a path gem has a transitive dependency that does not exist in the path source" do + before do + build_repo2 do + build_gem "missing_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_lib "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s| + s.add_dependency "missing_dep" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + gem "parent_gem", path: "#{lib_path("parent_gem")}" + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency when updating" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "path@#{lib_path("parent_gem")}") + expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2") + end + end + + context "when a git gem has a transitive dependency that does not exist in the git source" do + before do + build_repo2 do + build_gem "missing_dep", "1.0.0" + build_gem "foo", "1.0.0" + end + + build_git "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s| + s.add_dependency "missing_dep" + end + + gemfile <<-G + source "https://gem.repo2" + + gem "foo" + + gem "parent_gem", git: "#{lib_path("parent_gem")}" + G + + bundle :install, artifice: "compact_index" + end + + it "falls back to the default rubygems source for that dependency when updating" do + build_repo2 do + build_gem "foo", "2.0.0" + end + + system_gems [] + + bundle "update foo", artifice: "compact_index" + + expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0") + expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "git@#{lib_path("parent_gem")}") + expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2") end end end diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb index cc6c82c0ff..97b1d233bf 100644 --- a/spec/bundler/install/gemfile/specific_platform_spec.rb +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -1,114 +1,1972 @@ # frozen_string_literal: true -require "spec_helper" -RSpec.describe "bundle install with specific_platform enabled" do - before do - bundle "config specific_platform true" - - build_repo2 do - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" } +RSpec.describe "bundle install with specific platforms" do + let(:google_protobuf) { <<-G } + source "https://gem.repo2" + gem "google-protobuf" + G - build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.5") + it "locks to the specific darwin platform" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + install_gemfile(google_protobuf) + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + expect(the_bundle.locked_platforms).to include("universal-darwin") + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include( + "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin" + ) + end + end - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86_64-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x64-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.4") - - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86_64-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-linux" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x64-mingw32" } - build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "universal-darwin" } + it "still installs the platform specific variant when locked only to ruby, and the platform specific variant has different dependencies" do + simulate_platform "x86_64-darwin-15" do + build_repo4 do + build_gem("sass-embedded", "1.72.0") do |s| + s.add_dependency "rake" + end - build_gem("google-protobuf", "3.0.0.alpha.4.0") - build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") - build_gem("google-protobuf", "3.0.0.alpha.3") - build_gem("google-protobuf", "3.0.0.alpha.2.0") - build_gem("google-protobuf", "3.0.0.alpha.1.1") - build_gem("google-protobuf", "3.0.0.alpha.1.0") + build_gem("sass-embedded", "1.72.0") do |s| + s.platform = "x86_64-darwin-15" + end - build_gem("facter", "2.4.6") - build_gem("facter", "2.4.6") do |s| - s.platform = "universal-darwin" - s.add_runtime_dependency "CFPropertyList" + build_gem "rake" end - build_gem("CFPropertyList") + + gemfile <<~G + source "https://gem.repo4" + + gem "sass-embedded" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + rake (1.0) + sass-embedded (1.72.0) + rake + + PLATFORMS + ruby + + DEPENDENCIES + sass-embedded + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing sass-embedded 1.72.0 (x86_64-darwin-15)") + + expect(the_bundle).to include_gem("sass-embedded 1.72.0 x86_64-darwin-15") end end - let(:google_protobuf) { <<-G } - source "file:#{gem_repo2}" - gem "google-protobuf" - G + it "understands that a non-platform specific gem in a old lockfile doesn't necessarily mean installing the non-specific variant" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + install_gemfile(google_protobuf) + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.5.0.5.1) - context "when on a darwin machine" do - before { simulate_platform "x86_64-darwin-15" } + PLATFORMS + ruby - it "locks to both the specific darwin platform and ruby" do - install_gemfile!(google_protobuf) - expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + DEPENDENCIES + google-protobuf + + BUNDLED WITH + #{Bundler::VERSION} + L + + # force strict usage of the lockfile by setting frozen mode + bundle_config "frozen true" + + # make sure the platform that got actually installed with the old bundler is used expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( - google-protobuf-3.0.0.alpha.5.0.5.1 - google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin - )) end + end + + it "understands that a non-platform specific gem in a new lockfile locked only to ruby doesn't necessarily mean installing the non-specific variant" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + gemfile google_protobuf + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.4.0" + end + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.4.0) + + PLATFORMS + ruby + + DEPENDENCIES + google-protobuf + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + + checksums.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.5.0.5.1" + + # make sure the platform that the platform specific dependency is used, since we're only locked to ruby + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + + # make sure we're still only locked to ruby + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + google-protobuf (3.0.0.alpha.5.0.5.1) + + PLATFORMS + ruby + + DEPENDENCIES + google-protobuf + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when running on a legacy lockfile locked only to ruby" do + # Exercises the legacy lockfile path (use_exact_resolved_specifications? = false) + # because most_specific_locked_platform is ruby, matching the generic platform. + # Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries + # both platforms before falling back, preserving lockfile integrity. + + around do |example| + build_repo4 do + build_gem "nokogiri", "1.3.10" + build_gem "nokogiri", "1.3.10" do |s| + s.platform = "arm64-darwin" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.3.10) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-22", &example + end + + it "still installs the generic ruby variant if necessary" do + bundle "install" + expect(the_bundle).to include_gem("nokogiri 1.3.10") + expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin") + end + + it "still installs the generic ruby variant if necessary, even in frozen mode" do + bundle "install", env: { "BUNDLE_FROZEN" => "true" } + expect(the_bundle).to include_gem("nokogiri 1.3.10") + expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin") + end + end + + context "when platform-specific gem has incompatible required_ruby_version" do + # Key insight: candidate_platforms tries [target, platform, ruby] in order. + # Ruby platform is last since it requires compilation, but works when + # precompiled gems are incompatible with the current Ruby version. + # + # Note: This fix requires the lockfile to include both ruby and platform- + # specific variants (typical after `bundle lock --add-platform`). If the + # lockfile only has platform-specific gems, frozen mode cannot help because + # Bundler.setup would still expect the locked (incompatible) gem. + + # Exercises the exact spec path (use_exact_resolved_specifications? = true) + # because lockfile has platform-specific entry as most_specific_locked_platform + it "falls back to ruby platform in frozen mode when lockfile includes both variants" do + build_repo4 do + build_gem "nokogiri", "1.18.10" + build_gem "nokogiri", "1.18.10" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + # Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`) + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.10) + nokogiri (1.18.10-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install", env: { "BUNDLE_FROZEN" => "true" } + expect(the_bundle).to include_gem("nokogiri 1.18.10") + expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux") + end + end + end + + it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do + simulate_platform "x86_64-darwin-15" do + build_repo2 do + build_gem("libv8", "8.4.255.0") + build_gem("libv8", "8.4.255.0") {|s| s.platform = "universal-darwin" } + + build_gem("mini_racer", "1.0.0") do |s| + s.add_dependency "libv8" + end + end + + # Consistent location to install and look for gems + bundle_config "path vendor/bundle" + + gemfile <<-G + source "https://gem.repo2" + gem "libv8" + G + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + libv8 (8.4.255.0) + + PLATFORMS + ruby + + DEPENDENCIES + libv8 + + BUNDLED WITH + #{Bundler::VERSION} + L - it "caches both the universal-darwin and ruby gems when --all-platforms is passed" do + bundle "install --verbose" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing libv8 8.4.255.0 (universal-darwin)") + + bundle "add mini_racer --verbose" + expect(out).to include("Using libv8 8.4.255.0 (universal-darwin)") + end + end + + it "chooses platform specific gems even when resolving upon materialization and the API returns more specific platforms first" do + simulate_platform "x86_64-darwin-15" do + build_repo4 do + build_gem("grpc", "1.50.0") + build_gem("grpc", "1.50.0") {|s| s.platform = "universal-darwin" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "grpc" + G + + # simulate lockfile created with old bundler, which only locks for ruby platform + lockfile <<-L + GEM + remote: https://gem.repo4/ + specs: + grpc (1.50.0) + + PLATFORMS + ruby + + DEPENDENCIES + grpc + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose", artifice: "compact_index_precompiled_before" + expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version") + expect(out).to include("Installing grpc 1.50.0 (universal-darwin)") + end + end + + it "caches the universal-darwin gem when --all-platforms is passed and properly picks it up on further bundler invocations" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem gemfile(google_protobuf) - bundle! "package --all-platforms" - expect([cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1"), cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")]). - to all(exist) + bundle "cache --all-platforms" + expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist + + bundle "install --verbose" + expect(err).to be_empty end + end + + it "caches the universal-darwin gem when cache_all_platforms is configured and properly picks it up on further bundler invocations" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + gemfile(google_protobuf) + bundle_config "cache_all_platforms true" + bundle "cache" + expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist - it "uses the platform-specific gem with extra dependencies" do - install_gemfile! <<-G - source "file:#{gem_repo2}" + bundle "install --verbose" + expect(err).to be_empty + end + end + + it "caches multiplatform git gems with a single gemspec when --all-platforms is passed" do + git = build_git "pg_array_parser", "1.0" + + gemfile <<-G + source "https://gem.repo1" + gem "pg_array_parser", :git => "#{lib_path("pg_array_parser-1.0")}" + G + + lockfile <<-L + GIT + remote: #{lib_path("pg_array_parser-1.0")} + revision: #{git.ref_for("main")} + specs: + pg_array_parser (1.0-java) + pg_array_parser (1.0) + + GEM + specs: + + PLATFORMS + #{lockfile_platforms("java")} + + DEPENDENCIES + pg_array_parser! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "cache --all-platforms" + + expect(err).to be_empty + end + + it "uses the platform-specific gem with extra dependencies" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem_with_different_dependencies_per_platform + install_gemfile <<-G + source "https://gem.repo2" gem "facter" G + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle.locked_platforms).to include("universal-darwin") expect(the_bundle).to include_gems("facter 2.4.6 universal-darwin", "CFPropertyList 1.0") - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(["CFPropertyList-1.0", - "facter-2.4.6", - "facter-2.4.6-universal-darwin"]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include("CFPropertyList-1.0", + "facter-2.4.6-universal-darwin") end + end - context "when adding a platform via lock --add_platform" do - it "adds the foreign platform" do - install_gemfile!(google_protobuf) - bundle! "lock --add-platform=#{x64_mingw}" + context "when adding a platform via lock --add_platform" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + end - expect(the_bundle.locked_gems.platforms).to eq([rb, x64_mingw, pl("x86_64-darwin-15")]) - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( - google-protobuf-3.0.0.alpha.5.0.5.1 + it "adds the foreign platform" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + install_gemfile(google_protobuf) + bundle "lock --add-platform=x64-mingw-ucrt" + + expect(the_bundle.locked_platforms).to include("x64-mingw-ucrt", "universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(*%w[ google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin - google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw32 - )) + google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw-ucrt + ]) end + end - it "falls back on plain ruby when that version doesnt have a platform-specific gem" do - install_gemfile!(google_protobuf) - bundle! "lock --add-platform=#{java}" + it "falls back on plain ruby when that version doesn't have a platform-specific gem" do + simulate_platform "x86_64-darwin-15" do + setup_multiplatform_gem + install_gemfile(google_protobuf) + bundle "lock --add-platform=java" - expect(the_bundle.locked_gems.platforms).to eq([java, rb, pl("x86_64-darwin-15")]) - expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( - google-protobuf-3.0.0.alpha.5.0.5.1 - google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin - )) + expect(the_bundle.locked_platforms).to include("java", "universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to include( + "google-protobuf-3.0.0.alpha.5.0.5.1", + "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin" + ) + end + end + end + + it "installs sorbet-static, which does not provide a pure ruby variant, in absence of a lockfile, just fine", :truffleruby do + skip "does not apply to Windows" if Gem.win_platform? + + build_repo2 do + build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform } + end + + gemfile <<~G + source "https://gem.repo2" + + gem "sorbet-static", "0.5.6403" + G + + bundle "install --verbose" + end + + it "installs sorbet-static, which does not provide a pure ruby variant, in presence of a lockfile, just fine", :truffleruby do + skip "does not apply to Windows" if Gem.win_platform? + + build_repo2 do + build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform } + end + + gemfile <<~G + source "https://gem.repo2" + + gem "sorbet-static", "0.5.6403" + G + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + sorbet-static (0.5.6403-#{Bundler.local_platform}) + + PLATFORMS + ruby + + DEPENDENCIES + sorbet-static (= 0.5.6403) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --verbose" + end + + it "does not resolve if the current platform does not match any of available platform specific variants for a top level dependency" do + build_repo4 do + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.6433" + G + + error_message = <<~ERROR.strip + Could not find gem 'sorbet-static (= 0.5.6433)' with platform 'arm64-darwin-21' in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.6433)': + * sorbet-static-0.5.6433-universal-darwin-20 + * sorbet-static-0.5.6433-x86_64-linux + ERROR + + simulate_platform "arm64-darwin-21" do + bundle "lock", raise_on_error: false + end + + expect(err).to include(error_message).once + + # Make sure it doesn't print error twice in verbose mode + + simulate_platform "arm64-darwin-21" do + bundle "lock --verbose", raise_on_error: false + end + + expect(err).to include(error_message).once + end + + it "shows a platform mismatch hint when the current platform is not in the lockfile's platforms" do + build_repo4 do + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux-musl" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.6433" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.6433-x86_64-linux-musl) + + PLATFORMS + x86_64-linux-musl + + DEPENDENCIES + sorbet-static (= 0.5.6433) + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install", raise_on_error: false + end + + expect(err).to include("Your current platform (x86_64-linux) is not included in the lockfile's platforms (x86_64-linux-musl)") + expect(err).to include("bundle lock --add-platform x86_64-linux") + end + + it "does not resolve if the current platform does not match any of available platform specific variants for a transitive dependency" do + build_repo4 do + build_gem("sorbet", "0.5.6433") {|s| s.add_dependency "sorbet-static", "= 0.5.6433" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" } + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet", "0.5.6433" + G + + error_message = <<~ERROR.strip + Could not find compatible versions + + Because every version of sorbet depends on sorbet-static = 0.5.6433 + and sorbet-static = 0.5.6433 could not be found in rubygems repository https://gem.repo4/ or installed locally for any resolution platforms (arm64-darwin-21), + sorbet cannot be used. + So, because Gemfile depends on sorbet = 0.5.6433, + version solving has failed. + + The source contains the following gems matching 'sorbet-static (= 0.5.6433)': + * sorbet-static-0.5.6433-universal-darwin-20 + * sorbet-static-0.5.6433-x86_64-linux + ERROR + + simulate_platform "arm64-darwin-21" do + bundle "lock", raise_on_error: false + end + + expect(err).to include(error_message).once + + # Make sure it doesn't print error twice in verbose mode + + simulate_platform "arm64-darwin-21" do + bundle "lock --verbose", raise_on_error: false + end + + expect(err).to include(error_message).once + end + + it "does not generate a lockfile if ruby platform is forced and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static", "0.5.9889") {|s| s.platform = Gem::Platform.local } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.9889" + G + + bundle "lock", raise_on_error: false, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" } + + expect(err).to include <<~ERROR.rstrip + Could not find gem 'sorbet-static (= 0.5.9889)' with platform 'ruby' in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.9889)': + * sorbet-static-0.5.9889-#{Gem::Platform.local} + ERROR + end + + it "automatically fixes the lockfile if ruby platform is locked and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static-and-runtime", "0.5.10160") do |s| + s.add_dependency "sorbet", "= 0.5.10160" + s.add_dependency "sorbet-runtime", "= 0.5.10160" + end + + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-runtime", "0.5.10160") + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static-and-runtime" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + sorbet-static-and-runtime + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet", "0.5.10160" + c.checksum gem_repo4, "sorbet-runtime", "0.5.10160" + c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local + c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + sorbet-static-and-runtime + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile if both ruby platform and a more specific platform are locked, and some gem has no ruby variant available" do + build_repo4 do + build_gem "nokogiri", "1.12.0" + build_gem "nokogiri", "1.12.0" do |s| + s.platform = "x86_64-darwin" + end + + build_gem "nokogiri", "1.13.0" + build_gem "nokogiri", "1.13.0" do |s| + s.platform = "x86_64-darwin" + end + + build_gem("sorbet-static", "0.5.10601") do |s| + s.platform = "x86_64-darwin" + end + end + + simulate_platform "x86_64-darwin-22" do + install_gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + end + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin" + c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.12.0) + nokogiri (1.12.0-x86_64-darwin) + sorbet-static (0.5.10601-x86_64-darwin) + + PLATFORMS + ruby + x86_64-darwin + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-darwin-22" do + bundle "update --conservative nokogiri" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.0-x86_64-darwin) + sorbet-static (0.5.10601-x86_64-darwin) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do + build_repo4 do + build_gem("sorbet-static-and-runtime", "0.5.10160") do |s| + s.add_dependency "sorbet", "= 0.5.10160" + s.add_dependency "sorbet-runtime", "= 0.5.10160" + end + + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-runtime", "0.5.10160") + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static-and-runtime" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + ruby + + DEPENDENCIES + sorbet-static-and-runtime + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet", "0.5.10160" + c.checksum gem_repo4, "sorbet-runtime", "0.5.10160" + c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local + c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-runtime (0.5.10160) + sorbet-static (0.5.10160-#{Gem::Platform.local}) + sorbet-static-and-runtime (0.5.10160) + sorbet (= 0.5.10160) + sorbet-runtime (= 0.5.10160) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + sorbet-static-and-runtime + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically fixes the lockfile when adding a gem that introduces dependencies with no ruby platform variants transitively" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.18.2" + + build_gem "nokogiri", "1.18.2" do |s| + s.platform = "x86_64-linux" + end + + build_gem("sorbet", "0.5.11835") do |s| + s.add_dependency "sorbet-static", "= 0.5.11835" + end + + build_gem "sorbet-static", "0.5.11835" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.2) + nokogiri (1.18.2-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.18.2", "x86_64-linux" + c.checksum gem_repo4, "sorbet", "0.5.11835" + c.checksum gem_repo4, "sorbet-static", "0.5.11835", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.18.2) + nokogiri (1.18.2-x86_64-linux) + sorbet (0.5.11835) + sorbet-static (= 0.5.11835) + sorbet-static (0.5.11835-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile if multiple platforms locked, but no valid versions of direct dependencies for all of them" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + aarch64-linux + arm-linux + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + c.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile without removing other variants if it's missing platform gems, but they are installed locally" do + simulate_platform "x86_64-darwin-21" do + build_repo4 do + build_gem("sorbet-static", "0.5.10549") do |s| + s.platform = "universal-darwin-20" + end + + build_gem("sorbet-static", "0.5.10549") do |s| + s.platform = "universal-darwin-21" + end + end + + # Make sure sorbet-static-0.5.10549-universal-darwin-21 is installed + install_gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "= 0.5.10549" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20" + c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21" + end + + # Make sure the lockfile is missing sorbet-static-0.5.10549-universal-darwin-21 + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.10549-universal-darwin-20) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + sorbet-static (= 0.5.10549) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.10549-universal-darwin-20) + sorbet-static (0.5.10549-universal-darwin-21) + + PLATFORMS + x86_64-darwin + + DEPENDENCIES + sorbet-static (= 0.5.10549) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile if locked only to ruby, and some locked specs don't meet locked dependencies" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem("ibandit", "0.7.0") do |s| + s.add_dependency "i18n", "~> 0.7.0" + end + + build_gem("i18n", "0.7.0.beta1") + build_gem("i18n", "0.7.0") + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ibandit", "~> 0.7.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + i18n (0.7.0.beta1) + ibandit (0.7.0) + i18n (~> 0.7.0) + + PLATFORMS + ruby + + DEPENDENCIES + ibandit (~> 0.7.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update i18n" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + i18n (0.7.0) + ibandit (0.7.0) + i18n (~> 0.7.0) + + PLATFORMS + ruby + + DEPENDENCIES + ibandit (~> 0.7.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile" do + build_repo4 do + build_gem "nokogiri", "1.13.8" + build_gem "nokogiri", "1.13.8" do |s| + s.platform = Gem::Platform.local + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + + gem "tzinfo", "~> 1.2", platform: :#{not_local_tag} + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.8" + c.checksum gem_repo4, "nokogiri", "1.13.8", Gem::Platform.local + end + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.8) + nokogiri (1.13.8-#{Gem::Platform.local}) + + PLATFORMS + #{lockfile_platforms("ruby")} + + DEPENDENCIES + nokogiri + tzinfo (~> 1.2) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + bundle "lock --update" + + expect(lockfile).to eq(original_lockfile) + end + + it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile, and the lockfile only has ruby" do + build_repo4 do + build_gem "nokogiri", "1.13.8" + build_gem "nokogiri", "1.13.8" do |s| + s.platform = "arm64-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + + gem "tzinfo", "~> 1.2", platforms: %i[windows jruby] + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.13.8" + end + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.13.8) + + PLATFORMS + ruby + + DEPENDENCIES + nokogiri + tzinfo (~> 1.2) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "arm64-darwin-23" do + bundle "lock --update" + end + + expect(lockfile).to eq(original_lockfile) + end + + it "does not remove ruby when adding a new gem to the Gemfile" do + build_repo4 do + build_gem "concurrent-ruby", "1.2.2" + build_gem "myrack", "3.0.7" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "concurrent-ruby" + gem "myrack" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "concurrent-ruby", "1.2.2" + c.checksum gem_repo4, "myrack", "3.0.7" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + concurrent-ruby (1.2.2) + + PLATFORMS + ruby + + DEPENDENCIES + concurrent-ruby + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + concurrent-ruby (1.2.2) + myrack (3.0.7) + + PLATFORMS + #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])} + + DEPENDENCIES + concurrent-ruby + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "can fallback to a source gem when platform gems are incompatible with current ruby version" do + setup_multiplatform_gem_with_source_gem + + gemfile <<~G + source "https://gem.repo2" + + gem "my-precompiled-gem" + G + + # simulate lockfile which includes both a precompiled gem with: + # - Gem the current platform (with incompatible ruby version) + # - A source gem with compatible ruby version + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + my-precompiled-gem (3.0.0) + my-precompiled-gem (3.0.0-#{Bundler.local_platform}) + + PLATFORMS + ruby + #{Bundler.local_platform} + + DEPENDENCIES + my-precompiled-gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + end + + it "automatically adds the ruby variant to the lockfile if the specific platform is locked and we move to a newer ruby version for which a native package is not available" do + # + # Given an existing application using native gems (e.g., nokogiri) + # And a lockfile generated with a stable ruby version + # When want test the application against ruby-head and `bundle install` + # Then bundler should fall back to the generic ruby platform gem + # + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri", "1.14.0" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri (= 1.14.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0" + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri (= 1.14.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "automatically fixes the lockfile when only ruby platform locked, and adding a dependency with subdependencies not valid for ruby" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem("sorbet", "0.5.10160") do |s| + s.add_dependency "sorbet-static", "= 0.5.10160" + end + + build_gem("sorbet-static", "0.5.10160") do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10160) + sorbet-static (= 0.5.10160) + sorbet-static (0.5.10160-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + sorbet + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "locks specific platforms automatically" do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x64-mingw-ucrt" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "java" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "universal-darwin-22" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.14.0" + c.checksum gem_repo4, "nokogiri", "1.14.0", "arm-linux" + c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux" + end + + # locks all compatible platforms, excluding Java and Windows + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + arm-linux + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + FileUtils.rm bundled_app_lock + + bundle "lock" + + checksums.delete "nokogiri", "arm-linux" + checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "universal-darwin-22" + checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux" + + # locks only platforms compatible with all gems in the bundle + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0) + nokogiri (1.14.0-x86_64-linux) + sorbet-static (0.5.10696-universal-darwin-22) + sorbet-static (0.5.10696-x86_64-linux) + + PLATFORMS + universal-darwin-22 + x86_64-linux + + DEPENDENCIES + nokogiri + sorbet-static + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not fail when a platform variant is incompatible with the current ruby and another equivalent platform specific variant is part of the resolution" do + build_repo4 do + build_gem "nokogiri", "1.15.5" + + build_gem "nokogiri", "1.15.5" do |s| + s.platform = "x86_64-linux" + s.required_ruby_version = "< #{current_ruby_minor}.dev" + end + + build_gem "sass-embedded", "1.69.5" + + build_gem "sass-embedded", "1.69.5" do |s| + s.platform = "x86_64-linux-gnu" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sass-embedded" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.15.5" + c.checksum gem_repo4, "sass-embedded", "1.69.5" + c.checksum gem_repo4, "sass-embedded", "1.69.5", "x86_64-linux-gnu" + end + + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + # locks all compatible platforms, excluding Java and Windows + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.15.5) + sass-embedded (1.69.5) + sass-embedded (1.69.5-x86_64-linux-gnu) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + sass-embedded + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not add ruby platform gem if it brings extra dependencies not resolved originally" do + build_repo4 do + build_gem "nokogiri", "1.15.5" do |s| + s.add_dependency "mini_portile2", "~> 2.8.2" + end + + build_gem "nokogiri", "1.15.5" do |s| + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "nokogiri", "1.15.5", "x86_64-linux" + end + + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.15.5-x86_64-linux) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + nokogiri + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + ["x86_64-linux", "x86_64-linux-musl"].each do |host_platform| + describe "on host platform #{host_platform}" do + it "adds current musl platform" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-musl" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform host_platform do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux" + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-x86_64-linux) + rcee_precompiled (0.5.0-x86_64-linux-musl) + + PLATFORMS + x86_64-linux + x86_64-linux-musl + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + end + end + + it "adds current musl platform, when there are also gnu variants" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-gnu" + end + + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "x86_64-linux-musl" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform "x86_64-linux-musl" do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-gnu" + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-x86_64-linux-gnu) + rcee_precompiled (0.5.0-x86_64-linux-musl) + + PLATFORMS + x86_64-linux-gnu + x86_64-linux-musl + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not add current platform if there's an equivalent less specific platform among the ones resolved" do + build_repo4 do + build_gem "rcee_precompiled", "0.5.0" do |s| + s.platform = "universal-darwin" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "rcee_precompiled", "0.5.0" + G + + simulate_platform "x86_64-darwin-15" do + bundle "lock" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "universal-darwin" + end + + expect(lockfile).to eq(<<~L) + GEM + remote: https://gem.repo4/ + specs: + rcee_precompiled (0.5.0-universal-darwin) + + PLATFORMS + universal-darwin + + DEPENDENCIES + rcee_precompiled (= 0.5.0) + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + it "does not re-resolve when a specific platform, but less specific than the current platform, is locked" do + build_repo4 do + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.0) + + PLATFORMS + arm64-darwin + + DEPENDENCIES + nokogiri! + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-23" do + bundle "install --verbose" + + expect(out).to include("Found no changes, using resolution from the lockfile") + end + end + + it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem" do + build_repo4 do + build_gem "ffi" + + build_gem "ffi" do |s| + s.platform = "x86_64-linux" + end + + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + gem "nokogiri" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.0) + nokogiri (1.0) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + ffi + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update nokogiri" + + expect(lockfile).to eq(original_lockfile) + end + end + + it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem, and variants for other platform also locked" do + build_repo4 do + build_gem "ffi" + + build_gem "ffi" do |s| + s.platform = "x86_64-linux" + end + + build_gem "ffi" do |s| + s.platform = "java" + end + + build_gem "nokogiri" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "ffi" + gem "nokogiri" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + ffi (1.0) + ffi (1.0-java) + nokogiri (1.0) + + PLATFORMS + java + x86_64-linux + + DEPENDENCIES + ffi + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update nokogiri" + + expect(lockfile).to eq(original_lockfile) + end + end + + it "does not remove platform specific gems from lockfile when using a ruby version that does not match their ruby requirements, since they may be useful in other rubies" do + build_repo4 do + build_gem("google-protobuf", "3.25.5") + build_gem("google-protobuf", "3.25.5") do |s| + s.required_ruby_version = "< #{current_ruby_minor}.dev" + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "google-protobuf", "~> 3.0" + G + + original_lockfile = <<~L + GEM + remote: https://gem.repo4/ + specs: + google-protobuf (3.25.5) + google-protobuf (3.25.5-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + google-protobuf (~> 3.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + lockfile original_lockfile + + simulate_platform "x86_64-linux" do + bundle "lock --update" + end + + expect(lockfile).to eq(original_lockfile) + end + + private + + def setup_multiplatform_gem + build_repo2 do + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw-ucrt" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw-ucrt" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.4.0") + build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") + end + end + + def setup_multiplatform_gem_with_different_dependencies_per_platform + build_repo2 do + build_gem("facter", "2.4.6") + build_gem("facter", "2.4.6") do |s| + s.platform = "universal-darwin" + s.add_dependency "CFPropertyList" + end + build_gem("CFPropertyList") + end + end + + def setup_multiplatform_gem_with_source_gem + build_repo2 do + build_gem("my-precompiled-gem", "3.0.0") + build_gem("my-precompiled-gem", "3.0.0") do |s| + s.platform = Bundler.local_platform + + # purposely unresolvable + s.required_ruby_version = ">= 1000.0.0" end end end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb index bc49053081..83875a3d0e 100644 --- a/spec/bundler/install/gemfile_spec.rb +++ b/spec/bundler/install/gemfile_spec.rb @@ -1,98 +1,192 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do context "with duplicated gems" do it "will display a warning" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem 'rails', '~> 4.0.0' gem 'rails', '~> 4.0.0' G - expect(out).to include("more than once") + expect(err).to include("more than once") end end context "with --gemfile" do it "finds the gemfile" do gemfile bundled_app("NotGemfile"), <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + + # Specify BUNDLE_GEMFILE for `the_bundle` + # to retrieve the proper Gemfile + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "respects lockfile and BUNDLE_LOCKFILE" do + gemfile bundled_app("NotGemfile"), <<-G + lockfile "ReallyNotGemfile.lock" + source "https://gem.repo1" + gem 'myrack' G - bundle :install, :gemfile => bundled_app("NotGemfile") + bundle :install, gemfile: bundled_app("NotGemfile") ENV["BUNDLE_GEMFILE"] = "NotGemfile" - expect(the_bundle).to include_gems "rack 1.0.0" + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "respects BUNDLE_LOCKFILE during bundle install" do + ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock" + + gemfile bundled_app("NotGemfile"), <<-G + source "https://gem.repo1" + gem 'myrack' + G + + bundle :install, gemfile: bundled_app("NotGemfile") + expect(bundled_app("ReallyNotGemfile.lock")).to exist + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + expect(the_bundle).to include_gems "myrack 1.0.0" end end context "with gemfile set via config" do before do gemfile bundled_app("NotGemfile"), <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - bundle "config --local gemfile #{bundled_app("NotGemfile")}" + bundle_config "gemfile #{bundled_app("NotGemfile")}" end it "uses the gemfile to install" do bundle "install" - bundle "show" + bundle "list" - expect(out).to include("rack (1.0.0)") + expect(out).to include("myrack (1.0.0)") end it "uses the gemfile while in a subdirectory" do bundled_app("subdir").mkpath - Dir.chdir(bundled_app("subdir")) do - bundle "install" - bundle "show" + bundle "install", dir: bundled_app("subdir") + bundle "list", dir: bundled_app("subdir") - expect(out).to include("rack (1.0.0)") - end + expect(out).to include("myrack (1.0.0)") end end - context "with deprecated features" do - before :each do - in_app_root + it "reports that lib is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :lib => "myrack" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :lib as an option for gem 'myrack', but it is invalid/) + end + + it "reports that type is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :type => "development" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :type as an option for gem 'myrack', but it is invalid/) + end + + it "reports that gemfile is an invalid option" do + gemfile <<-G + source "https://gem.repo1" + + gem "myrack", :gemfile => "foo" + G + + bundle :install, raise_on_error: false + expect(err).to match(/You passed :gemfile as an option for gem 'myrack', but it is invalid/) + end + + context "when an internal error happens" do + let(:bundler_bug) do + create_file("bundler_bug.rb", <<~RUBY) + require "bundler" + + module Bundler + class Dsl + def source(source, *args, &blk) + nil.name + end + end + end + RUBY + + bundled_app("bundler_bug.rb").to_s end - it "reports that lib is an invalid option" do - gemfile <<-G - gem "rack", :lib => "rack" - G + it "shows culprit file and line" do + skip "ruby-core test setup has always \"lib\" in $LOAD_PATH so `require \"bundler\"` always activates the local version rather than using RubyGems gem activation stuff, causing conflicts" if ruby_core? - bundle :install - expect(out).to match(/You passed :lib as an option for gem 'rack', but it is invalid/) + install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false + expect(err).to include("bundler_bug.rb:6") end end - context "with engine specified in symbol" do + context "with engine specified in symbol", :jruby_only do it "does not raise any error parsing Gemfile" do - simulate_ruby_version "2.3.0" do - simulate_ruby_engine "jruby", "9.1.2.0" do - install_gemfile! <<-G - source "file://#{gem_repo1}" - ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" - G - - expect(out).to match(/Bundle complete!/) - end - end + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}" + G + + expect(out).to match(/Bundle complete!/) end it "installation succeeds" do - simulate_ruby_version "2.3.0" do - simulate_ruby_engine "jruby", "9.1.2.0" do - install_gemfile! <<-G - source "file://#{gem_repo1}" - ruby "2.3.0", :engine => :jruby, :engine_version => "9.1.2.0" - gem "rack" - G - - expect(the_bundle).to include_gems "rack 1.0.0" - end - end + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end + + context "with a Gemfile containing non-US-ASCII characters" do + it "reads the Gemfile with the UTF-8 encoding by default" do + install_gemfile <<-G + source "https://gem.repo1" + + str = "Il était une fois ..." + puts "The source encoding is: " + str.encoding.name + G + + expect(out).to include("The source encoding is: UTF-8") + expect(out).not_to include("The source encoding is: ASCII-8BIT") + expect(out).to include("Bundle complete!") + end + + it "respects the magic encoding comment" do + # NOTE: This works thanks to #eval interpreting the magic encoding comment + install_gemfile <<-G + # encoding: iso-8859-1 + source "https://gem.repo1" + + str = "Il #{"\xE9".dup.force_encoding("binary")}tait une fois ..." + puts "The source encoding is: " + str.encoding.name + G + + expect(out).to include("The source encoding is: ISO-8859-1") + expect(out).to include("Bundle complete!") end end end diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb index e9e671105a..9db73b84b5 100644 --- a/spec/bundler/install/gems/compact_index_spec.rb +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "compact index api" do let(:source_hostname) { "localgemserver.test" } @@ -8,12 +7,27 @@ RSpec.describe "compact index api" do it "should use the API" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "has a debug mode" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index", env: { "DEBUG_COMPACT_INDEX" => "true" } + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(err).to include("[Bundler::CompactIndexClient] available?") + expect(err).to include("[Bundler::CompactIndexClient] fetching versions") + expect(err).to include("[Bundler::CompactIndexClient] info(myrack)") + expect(err).to include("[Bundler::CompactIndexClient] fetching info/myrack") + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should URI encode gem names" do @@ -22,8 +36,8 @@ RSpec.describe "compact index api" do gem " sinatra" G - bundle :install, :artifice => "compact_index" - expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + bundle :install, artifice: "compact_index", raise_on_error: false + expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.") end it "should handle nested dependencies" do @@ -32,7 +46,7 @@ RSpec.describe "compact index api" do gem "rails" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" expect(out).to include("Fetching gem metadata from #{source_uri}") expect(the_bundle).to include_gems( "rails 2.3.2", @@ -45,23 +59,23 @@ RSpec.describe "compact index api" do end it "should handle case sensitivity conflicts" do - build_repo4 do - build_gem "rack", "1.0" do |s| - s.add_runtime_dependency("Rack", "0.1") + build_repo4(build_compact_index: false) do + build_gem "myrack", "1.0" do |s| + s.add_dependency("Myrack", "0.1") end - build_gem "Rack", "0.1" + build_gem "Myrack", "0.1" end - install_gemfile! <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo4 } + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } source "#{source_uri}" - gem "rack", "1.0" - gem "Rack", "0.1" + gem "myrack", "1.0" + gem "Myrack", "0.1" G # can't use `include_gems` here since the `require` will conflict on a # case-insensitive FS - run! "Bundler.require; puts Gem.loaded_specs.values_at('rack', 'Rack').map(&:full_name)" - expect(out).to eq("rack-1.0\nRack-0.1") + run "Bundler.require; puts Gem.loaded_specs.values_at('myrack', 'Myrack').map(&:full_name)" + expect(out).to eq("myrack-1.0\nMyrack-0.1") end it "should handle multiple gem dependencies on the same gem" do @@ -70,20 +84,21 @@ RSpec.describe "compact index api" do gem "net-sftp" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" expect(the_bundle).to include_gems "net-sftp 1.1.1" end - it "should use the endpoint when using --deployment" do + it "should use the endpoint when using deployment mode" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" - bundle "install --deployment", :artifice => "compact_index" + bundle_config "deployment true" + bundle :install, artifice: "compact_index" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles git dependencies that are in rubygems" do @@ -94,17 +109,17 @@ RSpec.describe "compact index api" do gemfile <<-G source "#{source_uri}" - git "file:///#{lib_path("foo-1.0")}" do + git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" expect(the_bundle).to include_gems("rails 2.3.2") end - it "handles git dependencies that are in rubygems using --deployment" do + it "handles git dependencies that are in rubygems using deployment mode" do build_git "foo" do |s| s.executables = "foobar" s.add_dependency "rails", "2.3.2" @@ -112,95 +127,102 @@ RSpec.describe "compact index api" do gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" - bundle "install --deployment", :artifice => "compact_index" + bundle_config "deployment true" + bundle :install, artifice: "compact_index" expect(the_bundle).to include_gems("rails 2.3.2") end - it "doesn't fail if you only have a git gem with no deps when using --deployment" do + it "doesn't fail if you only have a git gem with no deps when using deployment mode" do build_git "foo" gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle "install", :artifice => "compact_index" - bundle "install --deployment", :artifice => "compact_index" + bundle "install", artifice: "compact_index" + bundle_config "deployment true" + bundle :install, artifice: "compact_index" - expect(exitstatus).to eq(0) if exitstatus expect(the_bundle).to include_gems("foo 1.0") end - it "falls back when the API errors out" do - simulate_platform mswin - + it "falls back when the API URL returns 403 Forbidden" do gemfile <<-G source "#{source_uri}" - gem "rcov" + gem "myrack" G - bundle! :install, :artifice => "windows" - expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rcov 1.0.0" + bundle :install, verbose: true, artifice: "compact_index_forbidden" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "falls back when the API URL returns 403 Forbidden" do + it "falls back when the versions endpoint has a checksum mismatch" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :verbose => true, :artifice => "compact_index_forbidden" + bundle :install, verbose: true, artifice: "compact_index_checksum_mismatch" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(out).to include("The checksum of /versions does not match the checksum provided by the server!") + expect(out).to include("Calculated checksums #{{ "sha-256" => "8KfZiM/fszVkqhP/m5s9lvE6M9xKu4I1bU4Izddp5Ms=" }.inspect} did not match expected #{{ "sha-256" => "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" }.inspect}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "falls back when the versions endpoint has a checksum mismatch" do + it "shows proper path when permission errors happen", :permissions do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :verbose => true, :artifice => "compact_index_checksum_mismatch" - expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(out).to include <<-'WARN' -The checksum of /versions does not match the checksum provided by the server! Something is wrong (local checksum is "\"d41d8cd98f00b204e9800998ecf8427e\"", was expecting "\"123\""). - WARN - expect(the_bundle).to include_gems "rack 1.0.0" + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + versions.dirname.mkpath + versions.write("created_at") + FileUtils.chmod("-r", versions) + + bundle :install, artifice: "compact_index", raise_on_error: false + + expect(err).to include( + "There was an error while trying to read from `#{versions}`. It is likely that you need to grant read permissions for that path." + ) end it "falls back when the user's home directory does not exist or is not writable" do - ENV["HOME"] = nil + ENV["HOME"] = tmp("missing_home").to_s gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles host redirects" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_host_redirect" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "handles host redirects without Net::HTTP::Persistent" do + it "handles host redirects without Gem::Net::HTTP::Persistent" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G FileUtils.mkdir_p lib_path @@ -216,91 +238,132 @@ The checksum of /versions does not match the checksum provided by the server! So H end - bundle! :install, :artifice => "compact_index_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + bundle :install, artifice: "compact_index_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")] expect(out).to_not match(/Too many redirects/) - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "times out when Bundler::Fetcher redirects too much" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "compact_index_redirects" - expect(out).to match(/Too many redirects/) + bundle :install, artifice: "compact_index_redirects", raise_on_error: false + expect(err).to match(/Too many redirects/) end context "when --full-index is specified" do it "should use the modern index for install" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "install --full-index", :artifice => "compact_index" + bundle "install --full-index", artifice: "compact_index" expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the modern index for update" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "update --full-index", :artifice => "compact_index" + bundle "update --full-index", artifice: "compact_index", all: true expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end + it "does not double check for gems that are only installed locally" do + build_repo2 do + build_gem "net_a" do |s| + s.add_dependency "net_b" + s.add_dependency "net_build_extensions" + end + + build_gem "net_b" + + build_gem "net_build_extensions" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY + end + end + + system_gems %w[myrack-1.0.0 thin-1.0 net_a-1.0], gem_repo: gem_repo2 + bundle_config "path.system true" + ENV["BUNDLER_SPEC_ALL_REQUESTS"] = <<~EOS.strip + #{source_uri}/versions + #{source_uri}/info/myrack + EOS + + install_gemfile <<-G, artifice: "compact_index", verbose: true, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + source "#{source_uri}" + gem "myrack" + G + + expect(stdboth).not_to include "Double checking" + end + it "fetches again when more dependencies are found in subsequent sources" do build_repo2 do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + install_gemfile <<-G, artifice: "compact_index_extra", verbose: true source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle! :install, :artifice => "compact_index_extra" - expect(the_bundle).to include_gems "back_deps 1.0" + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" end it "fetches gem versions even when those gems are already installed" do gemfile <<-G source "#{source_uri}" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle! :install, :artifice => "compact_index_extra_api" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.0.0" build_repo4 do - build_gem "rack", "1.2" do |s| - s.executables = "rackup" + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" end end gemfile <<-G source "#{source_uri}" do; end source "#{source_uri}/extra" - gem "rack", "1.2" + gem "myrack", "1.2" G - bundle! :install, :artifice => "compact_index_extra_api" - expect(the_bundle).to include_gems "rack 1.2" + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" end - it "considers all possible versions of dependencies from all api gem sources" do - # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that - # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 - # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other - # repo and installs it. + it "resolves indirect dependencies to the most scoped source that includes them" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on + # specific version of activesupport that exists only in repo1. There + # happens also be a version of activesupport in repo4, but not the one that + # version 1.0.0 of somegem wants. This test makes sure that bundler tries to + # use the version in the most scoped source, even if not compatible, and + # gives a resolution error build_repo4 do build_gem "activesupport", "1.2.0" build_gem "somegem", "1.0.0" do |s| @@ -310,14 +373,14 @@ The checksum of /versions does not match the checksum provided by the server! So gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem 'somegem', '1.0.0' + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end G - bundle! :install, :artifice => "compact_index_extra_api" + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false - expect(the_bundle).to include_gems "somegem 1.0.0" - expect(the_bundle).to include_gems "activesupport 1.2.3" + expect(err).to include("Could not find compatible versions") end it "prints API output properly with back deps" do @@ -325,45 +388,62 @@ The checksum of /versions does not match the checksum provided by the server! So build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle! :install, :artifice => "compact_index_extra" + bundle :install, artifice: "compact_index_extra" expect(out).to include("Fetching gem metadata from http://localgemserver.test/") expect(out).to include("Fetching source index from http://localgemserver.test/extra") end - it "does not fetch every spec if the index of gems is large when doing back deps" do + it "does not fetch every spec when doing back deps" do build_repo2 do build_gem "back_deps" do |s| s.add_dependency "foo" end build_gem "missing" - # need to hit the limit - 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| - build_gem "gem#{i}" - end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + install_gemfile <<-G, artifice: "compact_index_extra_missing" source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle! :install, :artifice => "compact_index_extra_missing" expect(the_bundle).to include_gems "back_deps 1.0" end + it "does not fetch every spec when doing back deps & everything is the compact index" do + build_repo4 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + + FileUtils.rm_r Dir[gem_repo4("gems/foo-*.gem")] + end + + install_gemfile <<-G, artifice: "compact_index_extra_api_missing" + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + expect(the_bundle).to include_gem "back_deps 1.0" + end + it "uses the endpoint if all sources support it" do gemfile <<-G source "#{source_uri}" @@ -371,113 +451,74 @@ The checksum of /versions does not match the checksum provided by the server! So gem 'foo' G - bundle! :install, :artifice => "compact_index_api_missing" + bundle :install, artifice: "compact_index_api_missing" expect(the_bundle).to include_gems "foo 1.0" end - it "fetches again when more dependencies are found in subsequent sources using --deployment" do + it "fetches again when more dependencies are found in subsequent sources using deployment mode" do build_repo2 do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle! :install, :artifice => "compact_index_extra" - - bundle "install --deployment", :artifice => "compact_index_extra" + bundle :install, artifice: "compact_index_extra" + bundle_config "deployment true" + bundle :install, artifice: "compact_index_extra" expect(the_bundle).to include_gems "back_deps 1.0" end it "does not refetch if the only unmet dependency is bundler" do + build_repo2 do + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + end + gemfile <<-G source "#{source_uri}" gem "bundler_dep" G - bundle! :install, :artifice => "compact_index" + bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } expect(out).to include("Fetching gem metadata from #{source_uri}") end - it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do - sudo "mkdir -p #{system_gem_path("bin")}" - sudo "chown -R root #{system_gem_path("bin")}" - - gemfile <<-G - source "#{source_uri}" - gem "rails" - G - bundle! :install, :artifice => "compact_index" - expect(the_bundle).to include_gems "rails 2.3.2" - end - - it "installs the binstubs" do - gemfile <<-G - source "#{source_uri}" - gem "rack" - G - - bundle "install --binstubs", :artifice => "compact_index" - - gembin "rackup" - expect(out).to eq("1.0.0") - end - - it "installs the bins when using --path and uses autoclean" do - gemfile <<-G - source "#{source_uri}" - gem "rack" - G - - bundle "install --path vendor/bundle", :artifice => "compact_index" - - expect(vendored_gems("bin/rackup")).to exist - end - - it "installs the bins when using --path and uses bundle clean" do - gemfile <<-G - source "#{source_uri}" - gem "rack" - G - - bundle "install --path vendor/bundle --no-clean", :artifice => "compact_index" - - expect(vendored_gems("bin/rackup")).to exist - end - it "prints post_install_messages" do gemfile <<-G source "#{source_uri}" - gem 'rack-obama' + gem 'myrack-obama' G - bundle! :install, :artifice => "compact_index" - expect(out).to include("Post-install message from rack:") + bundle :install, artifice: "compact_index" + expect(out).to include("Post-install message from myrack:") end it "should display the post install message for a dependency" do gemfile <<-G source "#{source_uri}" - gem 'rack_middleware' + gem 'myrack_middleware' G - bundle! :install, :artifice => "compact_index" - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + bundle :install, artifice: "compact_index" + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") end context "when using basic authentication" do let(:user) { "user" } let(:password) { "pass" } let(:basic_auth_source_uri) do - uri = URI.parse(source_uri) + uri = Gem::URI.parse(source_uri) uri.user = user uri.password = password @@ -487,114 +528,100 @@ The checksum of /versions does not match the checksum provided by the server! So it "passes basic authentication details and strips out creds" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_basic_authentication" + bundle :install, artifice: "compact_index_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic authentication creds for modern index" do + it "passes basic authentication details and strips out creds also in verbose mode" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "endopint_marshal_fail_basic_authentication" + bundle :install, verbose: true, artifice: "compact_index_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" - end - - it "strips http basic auth creds when it can't reach the server" do - gemfile <<-G - source "#{basic_auth_source_uri}" - gem "rack" - G - - bundle :install, :artifice => "endpoint_500" - expect(out).not_to include("#{user}:#{password}") - end - - it "strips http basic auth creds when warning about ambiguous sources" do - gemfile <<-G - source "#{basic_auth_source_uri}" - source "file://#{gem_repo1}" - gem "rack" - G - - bundle! :install, :artifice => "compact_index_basic_authentication" - expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") - expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "does not pass the user / password to different hosts on redirect" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_creds_diff_host" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_creds_diff_host" + expect(the_bundle).to include_gems "myrack 1.0.0" end describe "with authentication details in bundle config" do before do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G end it "reads authentication details by host name from bundle config" do - bundle "config #{source_hostname} #{user}:#{password}" + bundle "config set #{source_hostname} #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + bundle :install, artifice: "compact_index_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "reads authentication details by full url from bundle config" do # The trailing slash is necessary here; Fetcher canonicalizes the URI. - bundle "config #{source_uri}/ #{user}:#{password}" + bundle "config set #{source_uri}/ #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + bundle :install, artifice: "compact_index_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the API" do - bundle "config #{source_hostname} #{user}:#{password}" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" + bundle "config set #{source_hostname} #{user}:#{password}" + bundle :install, artifice: "compact_index_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "prefers auth supplied in the source uri" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle "config #{source_hostname} otheruser:wrong" + bundle "config set #{source_hostname} otheruser:wrong" - bundle! :install, :artifice => "compact_index_strict_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_strict_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "shows instructions if auth is not provided for the source" do - bundle :install, :artifice => "compact_index_strict_basic_authentication" - expect(out).to include("bundle config #{source_hostname} username:password") + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false + expect(err).to include("bundle config set --global #{source_hostname} username:password") end it "fails if authentication has already been provided, but failed" do - bundle "config #{source_hostname} #{user}:wrong" + bundle "config set #{source_hostname} #{user}:wrong" - bundle :install, :artifice => "compact_index_strict_basic_authentication" - expect(out).to include("Bad username or password") + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false + expect(err).to include("Bad username or password") + end + + it "does not fallback to old dependency API if bad authentication is provided" do + bundle "config set #{source_hostname} #{user}:wrong" + + bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false, verbose: true + expect(err).to include("Bad username or password") + expect(out).to include("HTTP 401 Unauthorized http://user@localgemserver.test/versions") + expect(out).not_to include("HTTP 401 Unauthorized http://user@localgemserver.test/api/v1/dependencies") end end @@ -604,16 +631,16 @@ The checksum of /versions does not match the checksum provided by the server! So it "passes basic authentication details" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle! :install, :artifice => "compact_index_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "compact_index_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end end end - context "when ruby is compiled without openssl", :ruby_repo do + context "when ruby is compiled without openssl" do before do # Install a monkeypatch that reproduces the effects of openssl being # missing when the fetcher runs, as happens in real life. The reason @@ -626,180 +653,360 @@ The checksum of /versions does not match the checksum provided by the server! So end end - it "explains what to do to get it" do + it "explains what to do to get it, and includes original error" do gemfile <<-G source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } - expect(out).to include("OpenSSL") + bundle :install, env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false, artifice: nil + expect(err).to include("recompile Ruby").and include("cannot load such file") end end context "when SSL certificate verification fails" do it "explains what happened" do # Install a monkeypatch that reproduces the effects of openssl raising - # a certificate validation error when Rubygems tries to connect. + # a certificate validation error when RubyGems tries to connect. gemfile <<-G - class Net::HTTP + class Gem::Net::HTTP def start raise OpenSSL::SSL::SSLError, "certificate verify failed" end end source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install - expect(out).to match(/could not verify the SSL certificate/i) + bundle :install, raise_on_error: false + expect(err).to match(/could not verify the SSL certificate/i) end end context ".gemrc with sources is present" do - before do + it "uses other sources declared in the Gemfile" do File.open(home(".gemrc"), "w") do |file| - file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + file.puts({ sources: ["https://rubygems.org"] }.to_yaml) end - end - after do - home(".gemrc").rmtree - end - - it "uses other sources declared in the Gemfile" do - gemfile <<-G - source "#{source_uri}" - gem 'rack' - G + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G - bundle! :install, :artifice => "compact_index_forbidden" + bundle :install, artifice: "compact_index_forbidden" + ensure + FileUtils.rm_rf home(".gemrc") + end end end - it "performs partial update with a non-empty range" do + it "performs update with etag not-modified" do + versions_etag = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions.etag" + ) + expect(versions_etag.file?).to eq(false) + gemfile <<-G source "#{source_uri}" - gem 'rack', '0.9.1' + gem 'myrack', '0.9.1' G - # Initial install creates the cached versions file - bundle! :install, :artifice => "compact_index" + # Initial install creates the cached versions file and etag file + bundle :install, artifice: "compact_index" + + expect(versions_etag.file?).to eq(true) + previous_content = versions_etag.binread # Update the Gemfile so we can check subsequent install was successful gemfile <<-G source "#{source_uri}" - gem 'rack', '1.0.0' + gem 'myrack', '1.0.0' G - # Second install should make only a partial request to /versions - bundle! :install, :artifice => "compact_index_partial_update" + # Second install should match etag + bundle :install, artifice: "compact_index_etag_match" - expect(the_bundle).to include_gems "rack 1.0.0" + expect(versions_etag.binread).to eq(previous_content) + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs full update when range is ignored" do + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + # Initial install creates the cached versions file and etag file + bundle :install, artifice: "compact_index" + + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + # Modify the cached file. The ranged request will be based on this but, + # in this test, the range is ignored so this gets overwritten, allowing install. + versions.write "ruining this file" + + bundle :install, artifice: "compact_index_range_ignored" + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs partial update with a non-empty range" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + # Initial install creates the cached versions file + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" end it "performs partial update while local cache is updated by another process" do gemfile <<-G source "#{source_uri}" - gem 'rack' + gem 'myrack' G - # Create an empty file to trigger a partial download - versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", - "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") - FileUtils.mkdir_p(File.dirname(versions)) - FileUtils.touch(versions) + # Create a partial cache versions file + versions = compact_index_cache_path.join( + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + versions.dirname.mkpath + versions.write("created_at") - bundle! :install, :artifice => "compact_index_concurrent_download" + bundle :install, artifice: "compact_index_concurrent_download" - expect(File.read(versions)).to start_with("created_at") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(versions.read).to start_with("created_at") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs a partial update that fails digest check, then a full update" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update_bad_digest", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs full update if server endpoints serve partial content responses but don't have incremental content and provide no digest" do + build_repo4 do + build_gem "myrack", "0.9.1" + end + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + build_repo4 do + build_gem "myrack", "1.0.0" + end + + install_gemfile <<-G, artifice: "compact_index_partial_update_no_digest_not_incremental", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "performs full update of compact index info cache if range is not satisfiable" do + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '0.9.1' + G + + bundle :install, artifice: "compact_index" + + cache_path = compact_index_cache_path.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5") + + # We must remove the etag so that we don't ignore the range and get a 304 Not Modified. + myrack_info_etag_path = File.join(cache_path, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073") + File.unlink(myrack_info_etag_path) if File.exist?(myrack_info_etag_path) + + myrack_info_path = File.join(cache_path, "info", "myrack") + expected_myrack_info_content = File.read(myrack_info_path) + + # Modify the cache files to make the range not satisfiable + File.open(myrack_info_path, "a") {|f| f << "0.9.2 |checksum:c55b525b421fd833a93171ad3d7f04528ca8e87d99ac273f8933038942a5888c" } + + # Update the Gemfile so the next install does its normal things + gemfile <<-G + source "#{source_uri}" + gem 'myrack', '1.0.0' + G + + # The cache files now being longer means the requested range is going to be not satisfiable + # Bundler must end up requesting the whole file to fix things up. + bundle :install, artifice: "compact_index_range_not_satisfiable" + + resulting_myrack_info_content = File.read(myrack_info_path) + + expect(resulting_myrack_info_content).to eq(expected_myrack_info_content) end it "fails gracefully when the source URI has an invalid scheme" do - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false source "htps://rubygems.org" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(15) if exitstatus - expect(out).to end_with(<<-E.strip) + expect(exitstatus).to eq(15) + expect(err).to end_with(<<-E.strip) The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`? E end - describe "checksum validation", :rubygems => ">= 2.3.0" do + describe "checksum validation" do + before do + lockfile <<-L + GEM + remote: #{source_uri} + specs: + myrack (1.0.0) + + PLATFORMS + ruby + + DEPENDENCIES + #{checksums_section} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "handles checksums from the server in base64" do + api_checksum = checksum_digest(gem_repo1, "myrack", "1.0.0") + myrack_checksum = [[api_checksum].pack("H*")].pack("m0") + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => myrack_checksum } + source "#{source_uri}" + gem "myrack" + G + + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems("myrack 1.0.0") + end + it "raises when the checksum does not match" do - install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", raise_on_error: false source "#{source_uri}" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(19) if exitstatus - expect(out). - to include("Bundler cannot continue installing rack (1.0.0)."). - and include("The checksum for the downloaded `rack-1.0.0.gem` does not match the checksum given by the server."). - and include("This means the contents of the downloaded gem is different from what was uploaded to the server, and could be a potential security issue."). - and include("To resolve this issue:"). - and include("1. delete the downloaded gem located at: `#{system_gem_path}/gems/rack-1.0.0/rack-1.0.0.gem`"). - and include("2. run `bundle install`"). - and include("If you wish to continue installing the downloaded gem, and are certain it does not pose a security issue despite the mismatching checksum, do the following:"). - and include("1. run `bundle config disable_checksum_validation true` to turn off checksum verification"). - and include("2. run `bundle install`"). - and match(/\(More info: The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/) + gem_path = default_cache_path.dirname.join("myrack-1.0.0.gem") + + expect(exitstatus).to eq(37) + expect(err).to eq <<~E.strip + Bundler found mismatched checksums. This is a potential security risk. + myrack (1.0.0) sha256=2222222222222222222222222222222222222222222222222222222222222222 + from the API at http://localgemserver.test/ + #{checksum_to_lock(gem_repo1, "myrack", "1.0.0")} + from the gem at #{gem_path} + + If you trust the API at http://localgemserver.test/, to resolve this issue you can: + 1. remove the gem at #{gem_path} + 2. run `bundle install` + + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + E end it "raises when the checksum is the wrong length" do - install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => "checksum!" } + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => "checksum!", "DEBUG" => "1" }, verbose: true, raise_on_error: false source "#{source_uri}" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(5) if exitstatus - expect(out).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + expect(exitstatus).to eq(14) + expect(err).to include('Invalid checksum for myrack-0.9.1: "checksum!" is not a valid SHA256 hex or base64 digest') end it "does not raise when disable_checksum_validation is set" do - bundle! "config disable_checksum_validation true" - install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + bundle_config "disable_checksum_validation true" + install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum" source "#{source_uri}" - gem "rack" + gem "myrack" G end end it "works when cache dir is world-writable" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" File.umask(0000) source "#{source_uri}" - gem "rack" + gem "myrack" G end it "doesn't explode when the API dependencies are wrong" do - install_gemfile <<-G, :artifice => "compact_index_wrong_dependencies", :env => { "DEBUG" => "true" } + install_gemfile <<-G, artifice: "compact_index_wrong_dependencies", env: { "DEBUG" => "true" }, raise_on_error: false source "#{source_uri}" gem "rails" G - deps = [Gem::Dependency.new("rake", "= 10.0.2"), + deps = [Gem::Dependency.new("rake", "= #{rake_version}"), Gem::Dependency.new("actionpack", "= 2.3.2"), Gem::Dependency.new("activerecord", "= 2.3.2"), Gem::Dependency.new("actionmailer", "= 2.3.2"), Gem::Dependency.new("activeresource", "= 2.3.2")] - expect(out).to include(<<-E.strip).and include("rails-2.3.2 from rubygems remote at #{source_uri}/ has either corrupted API or lockfile dependencies") -Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API or the lockfile (#{deps.map(&:to_s).join(", ")}). -Either installing with `--full-index` or running `bundle update rails` should fix the problem. + expect(out).to include("rails-2.3.2 from rubygems remote at #{source_uri}/ has corrupted API dependencies") + expect(err).to include(<<-E.strip) +Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API (#{deps.map(&:to_s).join(", ")}). +Running `bundle update rails` should fix the problem. E end it "does not duplicate specs in the lockfile when updating and a dependency is not installed" do - install_gemfile! <<-G, :artifice => "compact_index" + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" source "#{source_uri}" do gem "rails" gem "activemerchant" end G - gem_command! :uninstall, "activemerchant" - bundle! "update rails", :artifice => "compact_index" - expect(lockfile.scan(/activemerchant \(/).size).to eq(1) + uninstall_gem("activemerchant") + bundle "update rails", artifice: "compact_index" + count = lockfile.match?("CHECKSUMS") ? 2 : 1 # Once in the specs, and once in CHECKSUMS + expect(lockfile.scan(/activemerchant \(/).size).to eq(count) + end + + it "handles an API that does not provide checksums info (undocumented, support may get removed)" do + install_gemfile <<-G, artifice: "compact_index_no_checksums" + source "https://gem.repo1" + gem "rake" + G end end diff --git a/spec/bundler/install/gems/dependency_api_fallback_spec.rb b/spec/bundler/install/gems/dependency_api_fallback_spec.rb new file mode 100644 index 0000000000..c7b0c537e4 --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_fallback_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.describe "gemcutter's dependency API" do + context "when Gemcutter API takes too long to respond" do + before do + bundle_config "timeout 1" + end + + it "times out and falls back on the modern index" do + install_gemfile <<-G, artifice: "endpoint_timeout" + source "https://gem.repo1" + gem "myrack" + G + + expect(out).to include("Fetching source index from https://gem.repo1/") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end +end diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb index d495490745..32a1b98b6d 100644 --- a/spec/bundler/install/gems/dependency_api_spec.rb +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "gemcutter's dependency API" do let(:source_hostname) { "localgemserver.test" } @@ -8,12 +7,12 @@ RSpec.describe "gemcutter's dependency API" do it "should use the API" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should URI encode gem names" do @@ -22,8 +21,8 @@ RSpec.describe "gemcutter's dependency API" do gem " sinatra" G - bundle :install, :artifice => "endpoint" - expect(out).to include("' sinatra' is not a valid gem name because it contains whitespace.") + bundle :install, artifice: "endpoint", raise_on_error: false + expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.") end it "should handle nested dependencies" do @@ -32,7 +31,7 @@ RSpec.describe "gemcutter's dependency API" do gem "rails" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}/...") expect(the_bundle).to include_gems( "rails 2.3.2", @@ -50,20 +49,21 @@ RSpec.describe "gemcutter's dependency API" do gem "net-sftp" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(the_bundle).to include_gems "net-sftp 1.1.1" end - it "should use the endpoint when using --deployment" do + it "should use the endpoint when using deployment mode" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" - bundle "install --deployment", :artifice => "endpoint" + bundle_config "deployment true" + bundle :install, artifice: "endpoint" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles git dependencies that are in rubygems" do @@ -74,17 +74,17 @@ RSpec.describe "gemcutter's dependency API" do gemfile <<-G source "#{source_uri}" - git "file:///#{lib_path("foo-1.0")}" do + git "#{lib_path("foo-1.0")}" do gem 'foo' end G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" expect(the_bundle).to include_gems("rails 2.3.2") end - it "handles git dependencies that are in rubygems using --deployment" do + it "handles git dependencies that are in rubygems using deployment mode" do build_git "foo" do |s| s.executables = "foobar" s.add_dependency "rails", "2.3.2" @@ -92,41 +92,50 @@ RSpec.describe "gemcutter's dependency API" do gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle :install, :artifice => "endpoint" + bundle :install, artifice: "endpoint" - bundle "install --deployment", :artifice => "endpoint" + bundle_config "deployment true" + bundle :install, artifice: "endpoint" expect(the_bundle).to include_gems("rails 2.3.2") end - it "doesn't fail if you only have a git gem with no deps when using --deployment" do + it "doesn't fail if you only have a git gem with no deps when using deployment mode" do build_git "foo" gemfile <<-G source "#{source_uri}" - gem 'foo', :git => "file:///#{lib_path("foo-1.0")}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" G - bundle "install", :artifice => "endpoint" - bundle "install --deployment", :artifice => "endpoint" + bundle "install", artifice: "endpoint" + bundle_config "deployment true" + bundle :install, artifice: "endpoint" - expect(exitstatus).to eq(0) if exitstatus expect(the_bundle).to include_gems("foo 1.0") end it "falls back when the API errors out" do - simulate_platform mswin + simulate_platform "x86-mswin32" do + build_repo2 do + # The rcov gem is platform mswin32, but has no arch + build_gem "rcov" do |s| + s.platform = Gem::Platform.new([nil, "mswin32", nil]) + s.write "lib/rcov.rb", "RCOV = '1.0.0'" + end + end - gemfile <<-G - source "#{source_uri}" - gem "rcov" - G + gemfile <<-G + source "#{source_uri}" + gem "rcov" + G - bundle :install, :artifice => "windows" - expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rcov 1.0.0" + bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "rcov 1.0.0" + end end it "falls back when hitting the Gemcutter Dependency Limit" do @@ -137,10 +146,10 @@ RSpec.describe "gemcutter's dependency API" do gem "actionmailer" gem "activeresource" gem "thin" - gem "rack" + gem "myrack" gem "rails" G - bundle :install, :artifice => "endpoint_fallback" + bundle :install, artifice: "endpoint_fallback" expect(out).to include("Fetching source index from #{source_uri}") expect(the_bundle).to include_gems( @@ -150,7 +159,7 @@ RSpec.describe "gemcutter's dependency API" do "activeresource 2.3.2", "activesupport 2.3.2", "thin 1.0.0", - "rack 1.0.0", + "myrack 1.0.0", "rails 2.3.2" ) end @@ -158,39 +167,39 @@ RSpec.describe "gemcutter's dependency API" do it "falls back when Gemcutter API doesn't return proper Marshal format" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :verbose => true, :artifice => "endpoint_marshal_fail" + bundle :install, verbose: true, artifice: "endpoint_marshal_fail" expect(out).to include("could not fetch from the dependency API, trying the full index") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "falls back when the API URL returns 403 Forbidden" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :verbose => true, :artifice => "endpoint_api_forbidden" + bundle :install, verbose: true, artifice: "endpoint_api_forbidden" expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "handles host redirects" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_host_redirect" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "handles host redirects without Net::HTTP::Persistent" do + it "handles host redirects without Gem::Net::HTTP::Persistent" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G FileUtils.mkdir_p lib_path @@ -206,42 +215,42 @@ RSpec.describe "gemcutter's dependency API" do H end - bundle :install, :artifice => "endpoint_host_redirect", :requires => [lib_path("disable_net_http_persistent.rb")] + bundle :install, artifice: "endpoint_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")] expect(out).to_not match(/Too many redirects/) - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "timeouts when Bundler::Fetcher redirects too much" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_redirect" - expect(out).to match(/Too many redirects/) + bundle :install, artifice: "endpoint_redirect", raise_on_error: false + expect(err).to match(/Too many redirects/) end context "when --full-index is specified" do it "should use the modern index for install" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "install --full-index", :artifice => "endpoint" + bundle "install --full-index", artifice: "endpoint" expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the modern index for update" do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G - bundle "update --full-index", :artifice => "endpoint" + bundle "update --full-index", artifice: "endpoint", all: true expect(out).to include("Fetching source index from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end end @@ -250,46 +259,49 @@ RSpec.describe "gemcutter's dependency API" do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle :install, :artifice => "endpoint_extra" - expect(the_bundle).to include_gems "back_deps 1.0" + bundle :install, artifice: "endpoint_extra" + expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0" end it "fetches gem versions even when those gems are already installed" do gemfile <<-G source "#{source_uri}" - gem "rack", "1.0.0" + gem "myrack", "1.0.0" G - bundle :install, :artifice => "endpoint_extra_api" + bundle :install, artifice: "endpoint_extra_api" build_repo4 do - build_gem "rack", "1.2" do |s| - s.executables = "rackup" + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" end end gemfile <<-G source "#{source_uri}" do; end source "#{source_uri}/extra" - gem "rack", "1.2" + gem "myrack", "1.2" G - bundle :install, :artifice => "endpoint_extra_api" - expect(the_bundle).to include_gems "rack 1.2" + bundle :install, artifice: "endpoint_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" end - it "considers all possible versions of dependencies from all api gem sources" do - # In this scenario, the gem "somegem" only exists in repo4. It depends on specific version of activesupport that - # exists only in repo1. There happens also be a version of activesupport in repo4, but not the one that version 1.0.0 - # of somegem wants. This test makes sure that bundler actually finds version 1.2.3 of active support in the other - # repo and installs it. + it "resolves indirect dependencies to the most scoped source that includes them" do + # In this scenario, the gem "somegem" only exists in repo4. It depends on + # specific version of activesupport that exists only in repo1. There + # happens also be a version of activesupport in repo4, but not the one that + # version 1.0.0 of somegem wants. This test makes sure that bundler tries to + # use the version in the most scoped source, even if not compatible, and + # gives a resolution error build_repo4 do build_gem "activesupport", "1.2.0" build_gem "somegem", "1.0.0" do |s| @@ -299,14 +311,14 @@ RSpec.describe "gemcutter's dependency API" do gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem 'somegem', '1.0.0' + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end G - bundle :install, :artifice => "endpoint_extra_api" + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false - expect(the_bundle).to include_gems "somegem 1.0.0" - expect(the_bundle).to include_gems "activesupport 1.2.3" + expect(err).to include("Could not find compatible versions") end it "prints API output properly with back deps" do @@ -314,159 +326,122 @@ RSpec.describe "gemcutter's dependency API" do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle :install, :artifice => "endpoint_extra" + bundle :install, artifice: "endpoint_extra" - expect(out).to include("Fetching gem metadata from http://localgemserver.test/..") + expect(out).to include("Fetching gem metadata from http://localgemserver.test/.") expect(out).to include("Fetching source index from http://localgemserver.test/extra") end - it "does not fetch every spec if the index of gems is large when doing back deps" do + it "does not fetch every spec when doing back deps" do build_repo2 do build_gem "back_deps" do |s| s.add_dependency "foo" end build_gem "missing" - # need to hit the limit - 1.upto(Bundler::Source::Rubygems::API_REQUEST_LIMIT) do |i| - build_gem "gem#{i}" - end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end - gemfile <<-G + install_gemfile <<-G, artifice: "endpoint_extra_missing" source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle :install, :artifice => "endpoint_extra_missing" expect(the_bundle).to include_gems "back_deps 1.0" end - it "uses the endpoint if all sources support it" do - gemfile <<-G - source "#{source_uri}" - - gem 'foo' - G - - bundle :install, :artifice => "endpoint_api_missing" - expect(the_bundle).to include_gems "foo 1.0" - end - - it "fetches again when more dependencies are found in subsequent sources using --deployment" do + it "fetches again when more dependencies are found in subsequent sources using deployment mode" do build_repo2 do build_gem "back_deps" do |s| s.add_dependency "foo" end - FileUtils.rm_rf Dir[gem_repo2("gems/foo-*.gem")] + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] end gemfile <<-G source "#{source_uri}" - source "#{source_uri}/extra" - gem "back_deps" + source "#{source_uri}/extra" do + gem "back_deps" + end G - bundle :install, :artifice => "endpoint_extra" - - bundle "install --deployment", :artifice => "endpoint_extra" + bundle :install, artifice: "endpoint_extra" + bundle_config "deployment true" + bundle "install", artifice: "endpoint_extra" expect(the_bundle).to include_gems "back_deps 1.0" end - it "does not refetch if the only unmet dependency is bundler" do - gemfile <<-G - source "#{source_uri}" - - gem "bundler_dep" - G - - bundle :install, :artifice => "endpoint" - expect(out).to include("Fetching gem metadata from #{source_uri}") - end - - it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do - sudo "mkdir -p #{system_gem_path("bin")}" - sudo "chown -R root #{system_gem_path("bin")}" + it "does not fetch all marshaled specs" do + build_repo2 do + build_gem "foo", "1.0" + build_gem "foo", "2.0" + end - gemfile <<-G + install_gemfile <<-G, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, verbose: true source "#{source_uri}" - gem "rails" - G - bundle :install, :artifice => "endpoint" - expect(the_bundle).to include_gems "rails 2.3.2" - end - it "installs the binstubs" do - gemfile <<-G - source "#{source_uri}" - gem "rack" + gem "foo" G - bundle "install --binstubs", :artifice => "endpoint" - - gembin "rackup" - expect(out).to eq("1.0.0") + expect(out).to include("foo-2.0.gemspec.rz") + expect(out).not_to include("foo-1.0.gemspec.rz") end - it "installs the bins when using --path and uses autoclean" do - gemfile <<-G - source "#{source_uri}" - gem "rack" - G - - bundle "install --path vendor/bundle", :artifice => "endpoint" - - expect(vendored_gems("bin/rackup")).to exist - end + it "does not refetch if the only unmet dependency is bundler" do + build_repo2 do + build_gem "bundler_dep" do |s| + s.add_dependency "bundler" + end + end - it "installs the bins when using --path and uses bundle clean" do gemfile <<-G source "#{source_uri}" - gem "rack" - G - bundle "install --path vendor/bundle --no-clean", :artifice => "endpoint" + gem "bundler_dep" + G - expect(vendored_gems("bin/rackup")).to exist + bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s } + expect(out).to include("Fetching gem metadata from #{source_uri}") end it "prints post_install_messages" do gemfile <<-G source "#{source_uri}" - gem 'rack-obama' + gem 'myrack-obama' G - bundle :install, :artifice => "endpoint" - expect(out).to include("Post-install message from rack:") + bundle :install, artifice: "endpoint" + expect(out).to include("Post-install message from myrack:") end it "should display the post install message for a dependency" do gemfile <<-G source "#{source_uri}" - gem 'rack_middleware' + gem 'myrack_middleware' G - bundle :install, :artifice => "endpoint" - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + bundle :install, artifice: "endpoint" + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") end context "when using basic authentication" do let(:user) { "user" } let(:password) { "pass" } let(:basic_auth_source_uri) do - uri = URI.parse(source_uri) + uri = Gem::URI.parse(source_uri) uri.user = user uri.password = password @@ -476,114 +451,128 @@ RSpec.describe "gemcutter's dependency API" do it "passes basic authentication details and strips out creds" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" + bundle :install, artifice: "endpoint_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic authentication creds for modern index" do + it "passes basic authentication details and strips out creds also in verbose mode" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endopint_marshal_fail_basic_authentication" + bundle :install, verbose: true, artifice: "endpoint_basic_authentication" expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic auth creds when it can't reach the server" do + it "strips http basic authentication creds for modern index" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_500" + bundle :install, artifice: "endpoint_marshal_fail_basic_authentication" expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "strips http basic auth creds when warning about ambiguous sources" do + it "strips http basic auth creds when it can't reach the server" do gemfile <<-G source "#{basic_auth_source_uri}" - source "file://#{gem_repo1}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" - expect(out).to include("Warning: the gem 'rack' was found in multiple sources.") + bundle :install, artifice: "endpoint_500", raise_on_error: false expect(out).not_to include("#{user}:#{password}") - expect(the_bundle).to include_gems "rack 1.0.0" end it "does not pass the user / password to different hosts on redirect" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_creds_diff_host" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_creds_diff_host" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + describe "with host including dashes" do + before do + gemfile <<-G + source "http://local-gemserver.test" + gem "myrack" + G + end + + it "reads authentication details from a valid ENV variable" do + bundle :install, artifice: "endpoint_strict_basic_authentication", env: { "BUNDLE_LOCAL___GEMSERVER__TEST" => "#{user}:#{password}" } + + expect(out).to include("Fetching gem metadata from http://local-gemserver.test") + expect(the_bundle).to include_gems "myrack 1.0.0" + end end describe "with authentication details in bundle config" do before do gemfile <<-G source "#{source_uri}" - gem "rack" + gem "myrack" G end it "reads authentication details by host name from bundle config" do - bundle "config #{source_hostname} #{user}:#{password}" + bundle "config set #{source_hostname} #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + bundle :install, artifice: "endpoint_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "reads authentication details by full url from bundle config" do # The trailing slash is necessary here; Fetcher canonicalizes the URI. - bundle "config #{source_uri}/ #{user}:#{password}" + bundle "config set #{source_uri}/ #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + bundle :install, artifice: "endpoint_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "should use the API" do - bundle "config #{source_hostname} #{user}:#{password}" - bundle :install, :artifice => "endpoint_strict_basic_authentication" + bundle "config set #{source_hostname} #{user}:#{password}" + bundle :install, artifice: "endpoint_strict_basic_authentication" expect(out).to include("Fetching gem metadata from #{source_uri}") - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "prefers auth supplied in the source uri" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle "config #{source_hostname} otheruser:wrong" + bundle "config set #{source_hostname} otheruser:wrong" - bundle :install, :artifice => "endpoint_strict_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_strict_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end it "shows instructions if auth is not provided for the source" do - bundle :install, :artifice => "endpoint_strict_basic_authentication" - expect(out).to include("bundle config #{source_hostname} username:password") + bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false + expect(err).to include("bundle config set --global #{source_hostname} username:password") end it "fails if authentication has already been provided, but failed" do - bundle "config #{source_hostname} #{user}:wrong" + bundle "config set #{source_hostname} #{user}:wrong" - bundle :install, :artifice => "endpoint_strict_basic_authentication" - expect(out).to include("Bad username or password") + bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false + expect(err).to include("Bad username or password") end end @@ -593,16 +582,16 @@ RSpec.describe "gemcutter's dependency API" do it "passes basic authentication details" do gemfile <<-G source "#{basic_auth_source_uri}" - gem "rack" + gem "myrack" G - bundle :install, :artifice => "endpoint_basic_authentication" - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, artifice: "endpoint_basic_authentication" + expect(the_bundle).to include_gems "myrack 1.0.0" end end end - context "when ruby is compiled without openssl", :ruby_repo do + context "when ruby is compiled without openssl" do before do # Install a monkeypatch that reproduces the effects of openssl being # missing when the fetcher runs, as happens in real life. The reason @@ -615,57 +604,53 @@ RSpec.describe "gemcutter's dependency API" do end end - it "explains what to do to get it" do + it "explains what to do to get it, and includes original error" do gemfile <<-G source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install, :env => { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" } - expect(out).to include("OpenSSL") + bundle :install, artifice: "fail", env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false + expect(err).to include("recompile Ruby").and include("cannot load such file") end end context "when SSL certificate verification fails" do it "explains what happened" do # Install a monkeypatch that reproduces the effects of openssl raising - # a certificate validation error when Rubygems tries to connect. + # a certificate validation error when RubyGems tries to connect. gemfile <<-G - class Net::HTTP + class Gem::Net::HTTP def start raise OpenSSL::SSL::SSLError, "certificate verify failed" end end source "#{source_uri.gsub(/http/, "https")}" - gem "rack" + gem "myrack" G - bundle :install - expect(out).to match(/could not verify the SSL certificate/i) + bundle :install, raise_on_error: false + expect(err).to match(/could not verify the SSL certificate/i) end end context ".gemrc with sources is present" do - before do + it "uses other sources declared in the Gemfile" do File.open(home(".gemrc"), "w") do |file| - file.puts({ :sources => ["https://rubygems.org"] }.to_yaml) + file.puts({ sources: ["https://rubygems.org"] }.to_yaml) end - end - after do - home(".gemrc").rmtree - end - - it "uses other sources declared in the Gemfile" do - gemfile <<-G - source "#{source_uri}" - gem 'rack' - G - - bundle "install", :artifice => "endpoint_marshal_fail" + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G - expect(exitstatus).to eq(0) if exitstatus + bundle "install", artifice: "endpoint_marshal_fail" + ensure + FileUtils.rm_rf home(".gemrc") + end end end end diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb index 9b1d8e5424..6d5aa456fe 100644 --- a/spec/bundler/install/gems/env_spec.rb +++ b/spec/bundler/install/gems/env_spec.rb @@ -1,108 +1,107 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with ENV conditionals" do describe "when just setting an ENV key as a string" do before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" env "BUNDLER_TEST" do - gem "rack" + gem "myrack" end G end it "excludes the gems when the ENV variable is not set" do bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "includes the gems when the ENV variable is set" do ENV["BUNDLER_TEST"] = "1" bundle :install - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when just setting an ENV key as a symbol" do before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" env :BUNDLER_TEST do - gem "rack" + gem "myrack" end G end it "excludes the gems when the ENV variable is not set" do bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "includes the gems when the ENV variable is set" do ENV["BUNDLER_TEST"] = "1" bundle :install - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when setting a string to match the env" do before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" env "BUNDLER_TEST" => "foo" do - gem "rack" + gem "myrack" end G end it "excludes the gems when the ENV variable is not set" do bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "excludes the gems when the ENV variable is set but does not match the condition" do ENV["BUNDLER_TEST"] = "1" bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "includes the gems when the ENV variable is set and matches the condition" do ENV["BUNDLER_TEST"] = "foo" bundle :install - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end describe "when setting a regex to match the env" do before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" env "BUNDLER_TEST" => /foo/ do - gem "rack" + gem "myrack" end G end it "excludes the gems when the ENV variable is not set" do bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "excludes the gems when the ENV variable is set but does not match the condition" do ENV["BUNDLER_TEST"] = "fo" bundle :install - expect(the_bundle).not_to include_gems "rack" + expect(the_bundle).not_to include_gems "myrack" end it "includes the gems when the ENV variable is set and matches the condition" do ENV["BUNDLER_TEST"] = "foobar" bundle :install - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end end diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb index 2c2d3c16a1..a30b53d6ad 100644 --- a/spec/bundler/install/gems/flex_spec.rb +++ b/spec/bundler/install/gems/flex_spec.rb @@ -1,33 +1,32 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle flex_install" do it "installs the gems as expected" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked end it "installs even when the lockfile is invalid" do install_gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack', '1.0' + source "https://gem.repo1" + gem 'myrack', '1.0' G bundle :install - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).to be_locked end @@ -35,19 +34,19 @@ RSpec.describe "bundle flex_install" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack-obama", "1.0" + source "https://gem.repo2" + gem "myrack-obama", "1.0" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" end describe "adding new gems" do @@ -55,38 +54,38 @@ RSpec.describe "bundle flex_install" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.5" + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" end it "keeps child dependencies pinned" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" G update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack-obama" + source "https://gem.repo2" + gem "myrack-obama" gem "thin" G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0", "thin 1.0" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0", "thin 1.0" end end @@ -94,43 +93,43 @@ RSpec.describe "bundle flex_install" do it "removes gems without changing the versions of remaining gems" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G - expect(the_bundle).to include_gems "rack 1.0.0" + expect(the_bundle).to include_gems "myrack 1.0.0" expect(the_bundle).not_to include_gems "activesupport 2.3.5" install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.2' G - expect(the_bundle).to include_gems "rack 1.0.0", "activesupport 2.3.2" + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2" end it "removes top level dependencies when removed from the Gemfile while leaving other dependencies intact" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' gem 'activesupport', '2.3.5' G update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack' + source "https://gem.repo2" + gem 'myrack' G expect(the_bundle).not_to include_gems "activesupport 2.3.5" @@ -139,110 +138,187 @@ RSpec.describe "bundle flex_install" do it "removes child dependencies" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'rack-obama' + source "https://gem.repo2" + gem 'myrack-obama' gem 'activesupport' G - expect(the_bundle).to include_gems "rack 1.0.0", "rack-obama 1.0.0", "activesupport 2.3.5" + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0", "activesupport 2.3.5" update_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem 'activesupport' G expect(the_bundle).to include_gems "activesupport 2.3.5" - expect(the_bundle).not_to include_gems "rack-obama", "rack" + expect(the_bundle).not_to include_gems "myrack-obama", "myrack" end end - describe "when Gemfile conflicts with lockfile" do + describe "when running bundle install and Gemfile conflicts with lockfile" do before(:each) do build_repo2 install_gemfile <<-G - source "file://#{gem_repo2}" - gem "rack_middleware" + source "https://gem.repo2" + gem "myrack_middleware" G - expect(the_bundle).to include_gems "rack_middleware 1.0", "rack 0.9.1" + expect(the_bundle).to include_gems "myrack_middleware 1.0", "myrack 0.9.1" - build_repo2 - update_repo2 do - build_gem "rack-obama", "2.0" do |s| - s.add_dependency "rack", "=1.2" + build_repo2 do + build_gem "myrack-obama", "2.0" do |s| + s.add_dependency "myrack", "=1.2" end - build_gem "rack_middleware", "2.0" do |s| - s.add_dependency "rack", ">=1.0" + build_gem "myrack_middleware", "2.0" do |s| + s.add_dependency "myrack", ">=1.0" end end gemfile <<-G - source "file://#{gem_repo2}" - gem "rack-obama", "2.0" - gem "rack_middleware" + source "https://gem.repo2" + gem "myrack-obama", "2.0" + gem "myrack_middleware" G end it "does not install gems whose dependencies are not met" do - bundle :install - ruby <<-RUBY + bundle :install, raise_on_error: false + ruby <<-RUBY, raise_on_error: false require 'bundler/setup' RUBY - expect(err).to match(/could not find gem 'rack-obama/i) + expect(err).to match(/could not find gem 'myrack-obama/i) end - it "suggests bundle update when the Gemfile requires different versions than the lock" do - nice_error = <<-E.strip.gsub(/^ {8}/, "") - Fetching source index from file:#{gem_repo2}/ - Resolving dependencies... - Bundler could not find compatible versions for gem "rack": - In snapshot (Gemfile.lock): - rack (= 0.9.1) + it "discards the locked gems when the Gemfile requires different versions than the lock" do + bundle_config "force_ruby_platform true" - In Gemfile: - rack-obama (= 2.0) was resolved to 2.0, which depends on - rack (= 1.2) + nice_error = <<~E.strip + Could not find compatible versions - rack_middleware was resolved to 1.0, which depends on - rack (= 0.9.1) + Because myrack-obama >= 2.0 depends on myrack = 1.2 + and myrack = 1.2 could not be found in rubygems repository https://gem.repo2/ or installed locally, + myrack-obama >= 2.0 cannot be used. + So, because Gemfile depends on myrack-obama = 2.0, + version solving has failed. + E - Running `bundle update` will rebuild your snapshot from scratch, using only - the gems in your Gemfile, which may resolve the conflict. + bundle :install, retry: 0, raise_on_error: false + expect(err).to end_with(nice_error) + end + + it "does not include conflicts with a single requirement tree, because that can't possibly be a conflict" do + bundle_config "force_ruby_platform true" + + bad_error = <<~E.strip + Bundler could not find compatible versions for gem "myrack-obama": + In Gemfile: + myrack-obama (= 2.0) E - bundle :install, :retry => 0 - expect(out).to eq(nice_error) + bundle "update myrack_middleware", retry: 0, raise_on_error: false + expect(err).not_to end_with(bad_error) + end + end + + describe "when running bundle update and Gemfile conflicts with lockfile" do + before(:each) do + build_repo4 do + build_gem "jekyll-feed", "0.16.0" + build_gem "jekyll-feed", "0.15.1" + + build_gem "github-pages", "226" do |s| + s.add_dependency "jekyll-feed", "0.15.1" + end + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "jekyll-feed", "~> 0.12" + G + + gemfile <<-G + source "https://gem.repo4" + gem "github-pages", "~> 226" + gem "jekyll-feed", "~> 0.12" + G + end + + it "discards the conflicting lockfile information and resolves properly" do + bundle :update, raise_on_error: false, all: true + expect(err).to be_empty end end describe "subtler cases" do before :each do install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" G gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" - gem "rack-obama" + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "myrack-obama" G end - it "does something" do - expect do - bundle "install" - end.not_to change { File.read(bundled_app("Gemfile.lock")) } + it "should work when you install" do + bundle "install" - expect(out).to include("rack = 0.9.1") - expect(out).to include("locked at 1.0.0") - expect(out).to include("bundle update rack") + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "0.9.1" + c.checksum gem_repo1, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + myrack-obama (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + myrack-obama + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end it "should work when you update" do - bundle "update rack" + bundle "update myrack" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "0.9.1" + c.checksum gem_repo1, "myrack-obama", "1.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + myrack-obama (1.0) + myrack + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + myrack-obama + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L end end @@ -250,30 +326,39 @@ RSpec.describe "bundle flex_install" do it "updates the lockfile" do build_repo2 install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G + install_gemfile <<-G - source "file://#{gem_repo1}" - source "file://#{gem_repo2}" - gem "rack" + source "https://gem.repo1" + source "https://gem.repo2" do + end + gem "myrack" G - lockfile_should_be <<-L - GEM - remote: file:#{gem_repo1}/ - remote: file:#{gem_repo2}/ - specs: - rack (1.0.0) + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) - PLATFORMS - ruby + GEM + remote: https://gem.repo2/ + specs: - DEPENDENCIES - rack + PLATFORMS + #{lockfile_platforms} - BUNDLED WITH - #{Bundler::VERSION} + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} L end end @@ -283,37 +368,36 @@ RSpec.describe "bundle flex_install" do before(:each) do build_repo2 do build_gem "capybara", "0.3.9" do |s| - s.add_dependency "rack", ">= 1.0.0" + s.add_dependency "myrack", ">= 1.0.0" end - build_gem "rack", "1.1.0" + build_gem "myrack", "1.1.0" build_gem "rails", "3.0.0.rc4" do |s| - s.add_dependency "rack", "~> 1.1.0" + s.add_dependency "myrack", "~> 1.1.0" end - build_gem "rack", "1.2.1" + build_gem "myrack", "1.2.1" build_gem "rails", "3.0.0" do |s| - s.add_dependency "rack", "~> 1.2.1" + s.add_dependency "myrack", "~> 1.2.1" end end end - it "prints the correct error message" do + it "resolves them" do # install Rails 3.0.0.rc install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0.0.rc4" gem "capybara", "0.3.9" G # upgrade Rails to 3.0.0 and then install again install_gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "rails", "3.0.0" gem "capybara", "0.3.9" G - - expect(out).to include("Gemfile.lock") + expect(err).to be_empty end end end diff --git a/spec/bundler/install/gems/fund_spec.rb b/spec/bundler/install/gems/fund_spec.rb new file mode 100644 index 0000000000..8a3a51270a --- /dev/null +++ b/spec/bundler/install/gems/fund_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with gem sources" do + before do + build_repo2 do + build_gem "has_funding_and_other_metadata" do |s| + s.metadata = { + "bug_tracker_uri" => "https://example.com/user/bestgemever/issues", + "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md", + "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", + "homepage_uri" => "https://bestgemever.example.io", + "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding", + "source_code_uri" => "https://example.com/user/bestgemever", + "wiki_uri" => "https://example.com/user/bestgemever/wiki", + } + end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + + build_gem "gem_with_dependent_funding", "1.0" do |s| + s.add_dependency "has_funding" + end + end + end + + context "when gems include a fund URI" do + it "displays the plural fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).to include("2 installed gems you directly depend on are looking for funding.") + end + + it "displays the singular fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + + context "when gems include a fund URI but `ignore_funding_requests` is configured" do + before do + bundle_config "ignore_funding_requests true" + end + + it "does not display the plural fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding_and_other_metadata' + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).not_to include("2 installed gems you directly depend on are looking for funding.") + end + + it "does not display the singular fund message after installing" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'has_funding' + gem 'myrack-obama' + G + + expect(out).not_to include("1 installed gem you directly depend on is looking for funding.") + end + end + + context "when gems do not include fund messages" do + it "does not display any fund messages" do + install_gemfile <<-G + source "https://gem.repo2" + gem "activesupport" + G + + expect(out).not_to include("gem you depend on") + end + end + + context "when a dependency includes a fund message" do + it "does not display the fund message" do + install_gemfile <<-G + source "https://gem.repo2" + gem 'gem_with_dependent_funding' + G + + expect(out).not_to include("gem you depend on") + end + end + end + + context "with git sources" do + context "when gems include fund URI" do + it "displays the fund message after installing" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + build_git "also_has_funding", "1.1" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.1")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is not updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + gemfile <<-G + source "https://gem.repo1" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + end +end diff --git a/spec/bundler/install/gems/gemfile_source_header_spec.rb b/spec/bundler/install/gems/gemfile_source_header_spec.rb new file mode 100644 index 0000000000..dc35c8d741 --- /dev/null +++ b/spec/bundler/install/gems/gemfile_source_header_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe "fetching dependencies with a mirrored source" do + let(:mirror) { "https://server.example.org" } + + before do + build_repo2 + + gemfile <<-G + source "#{mirror}" + gem 'weakling' + G + + bundle_config "mirror.#{mirror} https://gem.repo2" + end + + it "sets the 'X-Gemfile-Source' and 'User-Agent' headers and bundles successfully" do + bundle :install, artifice: "endpoint_mirror_source" + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end +end diff --git a/spec/bundler/install/gems/mirror_probe_spec.rb b/spec/bundler/install/gems/mirror_probe_spec.rb new file mode 100644 index 0000000000..564062ccf6 --- /dev/null +++ b/spec/bundler/install/gems/mirror_probe_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.describe "fetching dependencies with a not available mirror" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + gem 'weakling' + G + end + + context "with a specific fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/__FALLBACK_TIMEOUT/" => "true", + "BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror") + end + + it "install a gem using the original uri when the mirror is not responding" do + bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }, verbose: true + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a global fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1", + "BUNDLE_MIRROR__ALL" => "https://gem.mirror") + end + + it "install a gem using the original uri when the mirror is not responding" do + bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" } + + expect(out).to include("Installing weakling") + expect(out).to include("Bundle complete") + expect(the_bundle).to include_gems "weakling 0.0.3" + end + end + + context "with a specific mirror without a fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror") + end + + it "fails to install the gem with a timeout error when the mirror is not responding" do + bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false + + expect(out).to be_empty + expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.") + end + end + + context "with a global mirror without a fallback timeout" do + before do + bundle_config_global("BUNDLE_MIRROR__ALL" => "https://gem.mirror") + end + + it "fails to install the gem with a timeout error when the mirror is not responding" do + bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false + + expect(out).to be_empty + expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.") + end + end +end diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb index 798156fb12..e1fbeac454 100644 --- a/spec/bundler/install/gems/mirror_spec.rb +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with a mirror configured" do describe "when the mirror does not match the gem source" do before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - bundle "config --local mirror.http://gems.example.org http://gem-mirror.example.org" + bundle_config "mirror.http://gems.example.org http://gem-mirror.example.org" end it "installs from the normal location" do bundle :install - expect(out).to include("Fetching source index from file:#{gem_repo1}") - expect(the_bundle).to include_gems "rack 1.0" + expect(out).to include("Fetching gem metadata from https://gem.repo1") + expect(the_bundle).to include_gems "myrack 1.0" end end @@ -23,18 +22,18 @@ RSpec.describe "bundle install with a mirror configured" do before :each do gemfile <<-G # This source is bogus and doesn't have the gem we're looking for - source "file://#{gem_repo2}" + source "https://gem.repo2" - gem "rack" + gem "myrack" G - bundle "config --local mirror.file://#{gem_repo2} file://#{gem_repo1}" + bundle_config "mirror.https://gem.repo2 https://gem.repo1" end it "installs the gem from the mirror" do - bundle :install - expect(out).to include("Fetching source index from file:#{gem_repo1}") - expect(out).not_to include("Fetching source index from file:#{gem_repo2}") - expect(the_bundle).to include_gems "rack 1.0" + bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s } + expect(out).to include("Fetching gem metadata from https://gem.repo1") + expect(out).not_to include("Fetching gem metadata from https://gem.repo2") + expect(the_bundle).to include_gems "myrack 1.0" end end end diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb index dcf67e976e..d5b10d2c8f 100644 --- a/spec/bundler/install/gems/native_extensions_spec.rb +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require "spec_helper" -RSpec.describe "installing a gem with native extensions", :ruby_repo do +RSpec.describe "installing a gem with native extensions" do it "installs" do build_repo2 do build_gem "c_extension" do |s| @@ -10,7 +9,7 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -34,14 +33,13 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do end gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "c_extension" G - bundle "config build.c_extension --with-c_extension=hello" + bundle_config "build.c_extension --with-c_extension=hello" bundle "install" - expect(out).not_to include("extconf.rb failed") expect(out).to include("Installing c_extension 1.0 with native extensions") run "Bundler.require; puts CExtension.new.its_true" @@ -55,7 +53,7 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do require "mkmf" name = "c_extension_bundle" dir_config(name) - raise "OMG" unless with_config("c_extension") == "hello" + raise ArgumentError unless with_config("c_extension") == "hello" create_makefile(name) E @@ -77,16 +75,110 @@ RSpec.describe "installing a gem with native extensions", :ruby_repo do C end - bundle! "config build.c_extension --with-c_extension=hello" + bundle_config "build.c_extension --with-c_extension=hello" - install_gemfile! <<-G + install_gemfile <<-G + source "https://gem.repo1" gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} G - expect(out).not_to include("extconf.rb failed") - expect(out).to include("Using c_extension 1.0") + expect(err).to_not include("warning: conflicting chdir during another chdir block") - run! "Bundler.require; puts CExtension.new.its_true" + run "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end + + it "installs correctly from git when multiple gems with extensions share one repository" do + build_repo2 do + ["one", "two"].each do |n| + build_lib "c_extension_#{n}", "1.0", path: lib_path("gems/c_extension_#{n}") do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle_#{n}" + dir_config(name) + raise ArgumentError unless with_config("c_extension_#{n}") == "#{n}" + create_makefile(name) + E + + s.write "ext/c_extension_#{n}.c", <<-C + #include "ruby.h" + + VALUE c_extension_#{n}_value(VALUE self) { + return rb_str_new_cstr("#{n}"); + } + + void Init_c_extension_bundle_#{n}() { + VALUE c_Extension = rb_define_class("CExtension_#{n}", rb_cObject); + rb_define_method(c_Extension, "value", c_extension_#{n}_value, 0); + } + C + + s.write "lib/c_extension_#{n}.rb", <<-C + require "c_extension_bundle_#{n}" + C + end + end + build_git "gems", path: lib_path("gems"), gemspec: false + end + + bundle_config "build.c_extension_one --with-c_extension_one=one" + bundle_config "build.c_extension_two --with-c_extension_two=two" + + # 1st time, require only one gem -- only one of the extensions gets built. + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension_one", :git => #{lib_path("gems").to_s.dump} + G + + # 2nd time, require both gems -- we need both extensions to be built now. + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension_one", :git => #{lib_path("gems").to_s.dump} + gem "c_extension_two", :git => #{lib_path("gems").to_s.dump} + G + + run "Bundler.require; puts CExtension_one.new.value; puts CExtension_two.new.value" + expect(out).to eq("one\ntwo") + end + + it "install with multiple build flags" do + build_git "c_extension" do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-E + require "mkmf" + name = "c_extension_bundle" + dir_config(name) + raise ArgumentError unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola" + create_makefile(name) + E + + s.write "ext/c_extension.c", <<-C + #include "ruby.h" + + VALUE c_extension_true(VALUE self) { + return Qtrue; + } + + void Init_c_extension_bundle() { + VALUE c_Extension = rb_define_class("CExtension", rb_cObject); + rb_define_method(c_Extension, "its_true", c_extension_true, 0); + } + C + + s.write "lib/c_extension.rb", <<-C + require "c_extension_bundle" + C + end + + bundle_config "build.c_extension --with-c_extension=hello --with-c_extension_bundle-dir=hola" + + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} + G + + run "Bundler.require; puts CExtension.new.its_true" expect(out).to eq("true") end end diff --git a/spec/bundler/install/gems/no_build_extension_spec.rb b/spec/bundler/install/gems/no_build_extension_spec.rb new file mode 100644 index 0000000000..31f0170433 --- /dev/null +++ b/spec/bundler/install/gems/no_build_extension_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-build-extension" do + before do + build_repo2 do + build_gem "with_extension" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/with_extension.rb", "w") do |f| + f.puts "WITH_EXTENSION = 'YES'" + end + end + RUBY + end + end + end + + it "skips building native extensions and warns when no_build_extension is set" do + bundle_config "no_build_extension true" + + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + build_complete = default_bundle_path("extensions").join( + Gem::Platform.local.to_s, + Gem.extension_api_version.to_s, + "with_extension-1.0", + "gem.build_complete" + ) + expect(build_complete).not_to exist + expect(err).to include("with_extension-1.0 contains native extensions that were not built") + expect(err).to include("unset no_build_extension and run `bundle pristine with_extension`") + end + + it "builds native extensions by default" do + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + expect(out).to include("Installing with_extension 1.0 with native extensions") + end +end diff --git a/spec/bundler/install/gems/no_install_plugin_spec.rb b/spec/bundler/install/gems/no_install_plugin_spec.rb new file mode 100644 index 0000000000..e040e6b813 --- /dev/null +++ b/spec/bundler/install/gems/no_install_plugin_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-install-plugin" do + before do + build_repo2 do + build_gem "with_plugin", "1.0" do |s| + s.write "lib/rubygems_plugin.rb", "# plugin code" + end + + build_gem "with_plugin", "2.0" + end + end + + let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") } + + it "does not generate the plugin wrapper and warns when no_install_plugin is set" do + bundle_config "no_install_plugin true" + + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + + expect(plugin_path).not_to exist + expect(err).to include("with_plugin-1.0 contains plugins that were not installed") + expect(err).to include("unset no_install_plugin and run `bundle pristine with_plugin`") + end + + it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + expect(plugin_path).to exist + + bundle_config "no_install_plugin true" + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "2.0" + G + + expect(plugin_path).not_to exist + end + + it "generates the plugin wrapper by default" do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + + expect(plugin_path).to exist + end +end diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb index c3ea3e7c51..e49fd2a9a3 100644 --- a/spec/bundler/install/gems/post_install_spec.rb +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -1,31 +1,30 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do context "with gem sources" do context "when gems include post install messages" do it "should display the post-install messages after installing" do gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' + source "https://gem.repo1" + gem 'myrack' gem 'thin' - gem 'rack-obama' + gem 'myrack-obama' G bundle :install - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") expect(out).to include("Post-install message from thin:") expect(out).to include("Thin's post install message") - expect(out).to include("Post-install message from rack-obama:") - expect(out).to include("Rack-obama's post install message") + expect(out).to include("Post-install message from myrack-obama:") + expect(out).to include("Myrack-obama's post install message") end end context "when gems do not include post install messages" do it "should not display any post-install messages" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "activesupport" G @@ -34,16 +33,16 @@ RSpec.describe "bundle install" do end end - context "when a dependecy includes a post install message" do + context "when a dependency includes a post install message" do it "should display the post install message" do gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack_middleware' + source "https://gem.repo1" + gem 'myrack_middleware' G bundle :install - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") end end end @@ -55,7 +54,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -69,7 +68,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G bundle :install @@ -78,7 +77,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's 1.1 post install message" end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.1")}' G bundle :install @@ -92,7 +91,7 @@ RSpec.describe "bundle install" do s.post_install_message = "Foo's post install message" end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -111,7 +110,7 @@ RSpec.describe "bundle install" do s.post_install_message = nil end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem 'foo', :git => '#{lib_path("foo-1.0")}' G @@ -124,11 +123,11 @@ RSpec.describe "bundle install" do context "when ignore post-install messages for gem is set" do it "doesn't display any post-install messages" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config ignore_messages.rack true" + bundle_config "ignore_messages.myrack true" bundle :install expect(out).not_to include("Post-install message") @@ -138,11 +137,11 @@ RSpec.describe "bundle install" do context "when ignore post-install messages for all gems" do it "doesn't display any post-install messages" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "config ignore_messages true" + bundle_config "ignore_messages true" bundle :install expect(out).not_to include("Post-install message") diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb index 7a341fd14f..111d361aab 100644 --- a/spec/bundler/install/gems/resolving_spec.rb +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -1,10 +1,72 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with install-time dependencies" do - it "installs gems with implicit rake dependencies", :ruby_repo do + before do + build_repo2 do + build_gem "with_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/implicit_rake_dep.rb", "w") do |f| + f.puts "IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + build_gem "another_implicit_rake_dep" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f| + f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'" + end + end + RUBY + end + + # Test complicated gem dependencies for install + build_gem "net_a" do |s| + s.add_dependency "net_b" + s.add_dependency "net_build_extensions" + end + + build_gem "net_b" + + build_gem "net_build_extensions" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/net_build_extensions.rb", "w") do |f| + f.puts "NET_BUILD_EXTENSIONS = 'YES'" + end + end + RUBY + end + + build_gem "net_c" do |s| + s.add_dependency "net_a" + s.add_dependency "net_d" + end + + build_gem "net_d" + + build_gem "net_e" do |s| + s.add_dependency "net_d" + end + end + end + + it "installs gems with implicit rake dependencies" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "with_implicit_rake_dep" gem "another_implicit_rake_dep" gem "rake" @@ -19,48 +81,69 @@ RSpec.describe "bundle install with install-time dependencies" do expect(out).to eq("YES\nYES") end - it "installs gems with a dependency with no type" do + it "installs gems with implicit rake dependencies without rake previously installed" do + with_path_as("") do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_implicit_rake_dep" + gem "another_implicit_rake_dep" + gem "rake" + G + end + + run <<-R + require 'implicit_rake_dep' + require 'another_implicit_rake_dep' + puts IMPLICIT_RAKE_DEP + puts ANOTHER_IMPLICIT_RAKE_DEP + R + expect(out).to eq("YES\nYES") + end + + it "does not install gems with a dependency with no type" do build_repo2 path = "#{gem_repo2}/#{Gem::MARSHAL_SPEC_DIR}/actionpack-2.3.2.gemspec.rz" - spec = Marshal.load(Gem.inflate(File.read(path))) + spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path))) spec.dependencies.each do |d| - d.instance_variable_set(:@type, :fail) + d.instance_variable_set(:@type, "fail") end - File.open(path, "w") do |f| + File.open(path, "wb") do |f| f.write Gem.deflate(Marshal.dump(spec)) end - install_gemfile <<-G - source "file://#{gem_repo2}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem "actionpack", "2.3.2" G - expect(the_bundle).to include_gems "actionpack 2.3.2", "activesupport 2.3.2" + expect(err).to include("Downloading actionpack-2.3.2 revealed dependencies not in the API (activesupport (= 2.3.2)).") + + expect(the_bundle).not_to include_gems "actionpack 2.3.2", "activesupport 2.3.2" end describe "with crazy rubygem plugin stuff" do it "installs plugins" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "net_b" G expect(the_bundle).to include_gems "net_b 1.0" end - it "installs plugins depended on by other plugins", :ruby_repo do - install_gemfile <<-G - source "file://#{gem_repo1}" + it "installs plugins depended on by other plugins" do + install_gemfile <<-G, env: { "DEBUG" => "1" } + source "https://gem.repo2" gem "net_a" G expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0" end - it "installs multiple levels of dependencies", :ruby_repo do - install_gemfile <<-G - source "file://#{gem_repo1}" + it "installs multiple levels of dependencies" do + install_gemfile <<-G, env: { "DEBUG" => "1" } + source "https://gem.repo2" gem "net_c" gem "net_e" G @@ -68,32 +151,48 @@ RSpec.describe "bundle install with install-time dependencies" do expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0" end + context "with ENV['BUNDLER_DEBUG_RESOLVER'] set" do + it "produces debug output" do + gemfile <<-G + source "https://gem.repo2" + gem "net_c" + gem "net_e" + G + + bundle :install, env: { "BUNDLER_DEBUG_RESOLVER" => "1", "DEBUG" => "1" } + + expect(out).to include("Resolving dependencies...") + end + end + context "with ENV['DEBUG_RESOLVER'] set" do it "produces debug output" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "net_c" gem "net_e" G - bundle :install, :env => { "DEBUG_RESOLVER" => "1" } + bundle :install, env: { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" } - expect(err).to include("Creating possibility state for net_c") + expect(out).to include("Resolving dependencies...") end end context "with ENV['DEBUG_RESOLVER_TREE'] set" do it "produces debug output" do gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "net_c" gem "net_e" G - bundle :install, :env => { "DEBUG_RESOLVER_TREE" => "1" } + bundle :install, env: { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" } - expect(err).to include(" net_b") - expect(err).to include(" net_build_extensions (1.0)") + expect(out).to include(" net_b"). + and include("Resolving dependencies..."). + and include("Solution found after 1 attempts:"). + and include("selected net_b 1.0") end end end @@ -102,19 +201,438 @@ RSpec.describe "bundle install with install-time dependencies" do context "allows only an older version" do it "installs the older version" do build_repo2 do - build_gem "rack", "9001.0.0" do |s| + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "myrack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + end + + install_gemfile <<-G + ruby "#{Gem.ruby_version}" + source "https://gem.repo2" + gem 'myrack' + G + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + it "installs the older version when using servers not implementing the compact index API" do + build_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "myrack", "9001.0.0" do |s| s.required_ruby_version = "> 9000" end end - install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } - ruby "#{RUBY_VERSION}" - source "http://localgemserver.test/" - gem 'rack' + install_gemfile <<-G, artifice: "endpoint" + ruby "#{Gem.ruby_version}" + source "https://gem.repo2" + gem 'myrack' G - expect(out).to_not include("rack-9001.0.0 requires ruby version > 9000") - expect(the_bundle).to include_gems("rack 1.2") + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + context "when there is a lockfile using the newer incompatible version" do + before do + build_repo2 do + build_gem "parallel_tests", "3.7.0" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + end + + build_gem "parallel_tests", "3.8.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem 'parallel_tests' + G + + checksums = checksums_section do |c| + c.checksum gem_repo2, "parallel_tests", "3.8.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + parallel_tests (3.8.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parallel_tests + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically updates lockfile to use the older version" do + bundle "install --verbose" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "parallel_tests", "3.7.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + parallel_tests (3.7.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parallel_tests + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "gives a meaningful error if we're in frozen mode" do + expect do + bundle "install", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + end.not_to change { lockfile } + + expect(err).to eq("parallel_tests-3.8.0 requires ruby version >= #{next_ruby_minor}, which is incompatible with the current version, #{Gem.ruby_version}") + end + end + + context "with transitive dependencies in a lockfile" do + before do + build_repo2 do + build_gem "rubocop", "1.28.2" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + + s.add_dependency "rubocop-ast", ">= 1.17.0", "< 2.0" + end + + build_gem "rubocop", "1.35.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + + s.add_dependency "rubocop-ast", ">= 1.20.1", "< 2.0" + end + + build_gem "rubocop-ast", "1.17.0" do |s| + s.required_ruby_version = ">= #{current_ruby_minor}" + end + + build_gem "rubocop-ast", "1.21.0" do |s| + s.required_ruby_version = ">= #{next_ruby_minor}" + end + end + + gemfile <<-G + source "https://gem.repo2" + gem 'rubocop' + G + + checksums = checksums_section do |c| + c.checksum gem_repo2, "rubocop", "1.35.0" + c.checksum gem_repo2, "rubocop-ast", "1.21.0" + end + + lockfile <<~L + GEM + remote: https://gem.repo2/ + specs: + rubocop (1.35.0) + rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (1.21.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rubocop + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "automatically updates lockfile to use the older compatible versions" do + bundle "install --verbose" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo2, "rubocop", "1.28.2" + c.checksum gem_repo2, "rubocop-ast", "1.17.0" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo2/ + specs: + rubocop (1.28.2) + rubocop-ast (>= 1.17.0, < 2.0) + rubocop-ast (1.17.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rubocop + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "with a Gemfile and lockfile that don't resolve under the current platform" do + before do + build_repo4 do + build_gem "sorbet", "0.5.10554" do |s| + s.add_dependency "sorbet-static", "0.5.10554" + end + + build_gem "sorbet-static", "0.5.10554" do |s| + s.platform = "universal-darwin-21" + end + end + + gemfile <<~G + source "https://gem.repo4" + gem 'sorbet', '= 0.5.10554' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet (0.5.10554) + sorbet-static (= 0.5.10554) + sorbet-static (0.5.10554-universal-darwin-21) + + PLATFORMS + arm64-darwin-21 + + DEPENDENCIES + sorbet (= 0.5.10554) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a proper error" do + simulate_platform "aarch64-linux" do + bundle "install", raise_on_error: false + end + + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static (= 0.5.10554)' valid for all resolution platforms (arm64-darwin-21, aarch64-linux) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.10554)': + * sorbet-static-0.5.10554-universal-darwin-21 + E + expect(err).to include(nice_error) + expect(err).to include("Your current platform (aarch64-linux) is not included in the lockfile's platforms (arm64-darwin-21)") + expect(err).to include("bundle lock --add-platform aarch64-linux") + end + end + + context "when adding a new gem that does not resolve under all locked platforms" do + before do + simulate_platform "x86_64-linux" do + build_repo4 do + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "x86_64-linux" + end + build_gem "nokogiri", "1.14.0" do |s| + s.platform = "arm-linux" + end + + build_gem "sorbet-static", "0.5.10696" do |s| + s.platform = "x86_64-linux" + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.14.0-arm-linux) + nokogiri (1.14.0-x86_64-linux) + + PLATFORMS + arm-linux + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + gem "sorbet-static" + G + + bundle "lock", raise_on_error: false + end + end + + it "raises a proper error" do + nice_error = <<~E.strip + Could not find gems matching 'sorbet-static' valid for all resolution platforms (arm-linux, x86_64-linux) in rubygems repository https://gem.repo4/ or installed locally. + + The source contains the following gems matching 'sorbet-static': + * sorbet-static-0.5.10696-x86_64-linux + E + expect(err).to end_with(nice_error) + end + end + + context "when locked generic variant supports current Ruby, but locked specific variant does not" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.3) + nokogiri (1.16.3-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "nokogiri", "1.16.3" + build_gem "nokogiri", "1.16.3" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile original_lockfile + end + + it "keeps both variants in the lockfile when installing, and uses the generic one since it's compatible" do + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + expect(lockfile).to eq(original_lockfile) + expect(the_bundle).to include_gems("nokogiri 1.16.3") + end + end + + it "keeps both variants in the lockfile when updating, and uses the generic one since it's compatible" do + simulate_platform "x86_64-linux" do + bundle "update --verbose" + + expect(lockfile).to eq(original_lockfile) + expect(the_bundle).to include_gems("nokogiri 1.16.3") + end + end + end + + it "gives a meaningful error on ruby version mismatches between dependencies" do + build_repo4 do + build_gem "requires-old-ruby" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + end + end + + build_lib("foo", path: bundled_app) do |s| + s.required_ruby_version = ">= #{Gem.ruby_version}" + + s.add_dependency "requires-old-ruby" + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gemspec + G + + expect(err).to end_with <<~E.strip + Could not find compatible versions + + Because every version of foo depends on requires-old-ruby >= 0 + and every version of requires-old-ruby depends on Ruby < #{Gem.ruby_version}, + every version of foo requires Ruby < #{Gem.ruby_version}. + So, because Gemfile depends on foo >= 0 + and current Ruby version is = #{Gem.ruby_version}, + version solving has failed. + E + end + + it "installs the older version under rate limiting conditions" do + build_repo4 do + build_gem "myrack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + build_gem "myrack", "1.2" + build_gem "foo1", "1.0" + end + + install_gemfile <<-G, artifice: "compact_index_rate_limited" + ruby "#{Gem.ruby_version}" + source "https://gem.repo4" + gem 'myrack' + gem 'foo1' + G + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") + end + + it "installs the older not platform specific version" do + build_repo4 do + build_gem "myrack", "9001.0.0" do |s| + s.required_ruby_version = "> 9000" + end + build_gem "myrack", "1.2" do |s| + s.platform = "x86-mingw32" + s.required_ruby_version = "> 9000" + end + build_gem "myrack", "1.2" + end + + simulate_platform "x86-mingw32" do + install_gemfile <<-G, artifice: "compact_index" + ruby "#{Gem.ruby_version}" + source "https://gem.repo4" + gem 'myrack' + G + end + + expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000") + expect(err).to_not include("myrack-1.2-#{Bundler.local_platform} requires ruby version > 9000") + expect(the_bundle).to include_gems("myrack 1.2") end end @@ -127,33 +645,49 @@ RSpec.describe "bundle install with install-time dependencies" do end end - let(:ruby_requirement) { %("#{RUBY_VERSION}") } - let(:error_message_requirement) { "~> #{RUBY_VERSION}.0" } + let(:ruby_requirement) { %("#{Gem.ruby_version}") } + let(:error_message_requirement) { "= #{Gem.ruby_version}" } + + it "raises a proper error that mentions the current Ruby version during resolution" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem 'require_ruby' + G + + expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") + + nice_error = <<~E.strip + Could not find compatible versions + + Because every version of require_ruby depends on Ruby > 9000 + and Gemfile depends on require_ruby >= 0, + Ruby > 9000 is required. + So, because current Ruby version is #{error_message_requirement}, + version solving has failed. + E + expect(err).to end_with(nice_error) + end shared_examples_for "ruby version conflicts" do it "raises an error during resolution" do - install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } - source "http://localgemserver.test/" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" ruby #{ruby_requirement} gem 'require_ruby' G expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") - nice_error = strip_whitespace(<<-E).strip - Fetching gem metadata from http://localgemserver.test/. - Fetching version metadata from http://localgemserver.test/ - Resolving dependencies... - Bundler could not find compatible versions for gem "ruby\0": - In Gemfile: - ruby\0 (#{error_message_requirement}) + nice_error = <<~E.strip + Could not find compatible versions - require_ruby was resolved to 1.0, which depends on - ruby\0 (> 9000) - - Could not find gem 'ruby\0 (> 9000)', which is required by gem 'require_ruby', in any of the sources. + Because every version of require_ruby depends on Ruby > 9000 + and Gemfile depends on require_ruby >= 0, + Ruby > 9000 is required. + So, because current Ruby version is #{error_message_requirement}, + version solving has failed. E - expect(out).to eq(nice_error) + expect(err).to end_with(nice_error) end end @@ -161,14 +695,13 @@ RSpec.describe "bundle install with install-time dependencies" do describe "with a < requirement" do let(:ruby_requirement) { %("< 5000") } - let(:error_message_requirement) { "< 5000" } it_behaves_like "ruby version conflicts" end describe "with a compound requirement" do - let(:ruby_requirement) { %("< 5000", "> 0.1") } - let(:error_message_requirement) { "< 5000, > 0.1" } + let(:reqs) { ["> 0.1", "< 5000"] } + let(:ruby_requirement) { reqs.map(&:dump).join(", ") } it_behaves_like "ruby version conflicts" end @@ -183,13 +716,71 @@ RSpec.describe "bundle install with install-time dependencies" do end end - install_gemfile <<-G - source "file://#{gem_repo2}" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" gem 'require_rubygems' G - expect(out).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000") - expect(out).to include("require_rubygems-1.0 requires rubygems version > 9000, which is incompatible with the current version, #{Gem::VERSION}") + expect(err).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000") + nice_error = <<~E.strip + Because every version of require_rubygems depends on RubyGems > 9000 + and Gemfile depends on require_rubygems >= 0, + RubyGems > 9000 is required. + So, because current RubyGems version is = #{Gem::VERSION}, + version solving has failed. + E + expect(err).to end_with(nice_error) + end + end + + context "when non platform specific gems bring more dependencies", :truffleruby_only do + before do + build_repo4 do + build_gem "foo", "1.0" do |s| + s.add_dependency "bar" + end + + build_gem "foo", "2.0" do |s| + s.platform = "x86_64-linux" + end + + build_gem "bar" + end + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + G + end + + it "locks both ruby and current platform, and resolve to ruby variants that install on truffleruby" do + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "foo", "1.0" + c.checksum gem_repo4, "bar", "1.0" + end + + simulate_platform "x86_64-linux" do + bundle "install" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (1.0) + foo (1.0) + bar + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + foo + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end end end end diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb index 9a79a05b32..96a305bb76 100644 --- a/spec/bundler/install/gems/standalone_spec.rb +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -1,13 +1,18 @@ # frozen_string_literal: true -require "spec_helper" -RSpec.shared_examples "bundle install --standalone" do +RSpec.describe "bundle install --standalone" do shared_examples "common functionality" do it "still makes the gems available to normal bundler" do args = expected_gems.map {|k, v| "#{k} #{v}" } expect(the_bundle).to include_gems(*args) end + it "still makes system gems unavailable to normal bundler" do + system_gems "myrack-1.0.0" + + expect(the_bundle).to_not include_gems("myrack") + end + it "generates a bundle/bundler/setup.rb" do expect(bundled_app("bundle/bundler/setup.rb")).to exist end @@ -22,15 +27,66 @@ RSpec.shared_examples "bundle install --standalone" do testrb << "\nrequire \"#{k}\"" testrb << "\nputs #{k.upcase}" end - Dir.chdir(bundled_app) do - ruby testrb, :no_lib => true + ruby testrb + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "makes the gems available without bundler nor rubygems" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" end + in_bundled_app %(#{Gem.ruby} --disable-gems -w -e #{testrb.shellescape}) expect(out).to eq(expected_gems.values.join("\n")) end + it "makes the gems available without bundler via Kernel.require" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nKernel.require \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + ruby testrb + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "makes system gems unavailable without bundler" do + system_gems "myrack-1.0.0" + + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + begin + require "myrack" + rescue LoadError + puts "LoadError" + end + RUBY + ruby testrb + + expect(out).to eq("LoadError") + end + it "works on a different system" do - FileUtils.mv(bundled_app, "#{bundled_app}2") + begin + FileUtils.mv(bundled_app, "#{bundled_app}2") + rescue Errno::ENOTEMPTY + puts "Couldn't rename test app since the target folder has these files: #{Dir.glob("#{bundled_app}2/*")}" + raise + end testrb = String.new <<-RUBY $:.unshift File.expand_path("bundle") @@ -41,9 +97,23 @@ RSpec.shared_examples "bundle install --standalone" do testrb << "\nrequire \"#{k}\"" testrb << "\nputs #{k.upcase}" end - Dir.chdir("#{bundled_app}2") do - ruby testrb, :no_lib => true + ruby testrb, dir: "#{bundled_app}2" + + expect(out).to eq(expected_gems.values.join("\n")) + end + + it "skips activating gems" do + testrb = String.new <<-RUBY + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + gem "do_not_activate_me" + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" end + in_bundled_app %(#{Gem.ruby} -w -e #{testrb.shellescape}) expect(out).to eq(expected_gems.values.join("\n")) end @@ -51,10 +121,12 @@ RSpec.shared_examples "bundle install --standalone" do describe "with simple gems" do before do - install_gemfile <<-G, :standalone => true - source "file://#{gem_repo1}" + gemfile <<-G + source "https://gem.repo1" gem "rails" G + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -67,27 +139,162 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" end - describe "with gems with native extension", :ruby_repo do + describe "with default gems and a lockfile", :ruby_repo do + it "works and points to the vendored copies, not to the default copies" do + base_system_gems "stringio", "psych", "etc", path: scoped_gem_path(bundled_app("bundle")) + + build_gem "foo", "1.0.0", to_system: true, default: true do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0", to_system: true, default: true + + build_repo4 do + build_gem "foo", "1.0.0" do |s| + s.add_dependency "bar" + end + + build_gem "bar", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + G + + bundle "lock", dir: cwd + + bundle_config "path #{bundled_app("bundle")}" + + bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s } + + load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") } + + expect(load_path_lines).to eq [ + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bar-1.0.0/lib")', + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/foo-1.0.0/lib")', + ] + end + + it "works for gems with extensions and points to the vendored copies, not to the default copies" do + simulate_platform "arm64-darwin-23" do + base_system_gems "stringio", "psych", "etc", "shellwords", "open3", path: scoped_gem_path(bundled_app("bundle")) + + build_gem "baz", "1.0.0", to_system: true, default: true, &:add_c_extension + + build_repo4 do + build_gem "baz", "1.0.0", &:add_c_extension + end + + gemfile <<-G + source "https://gem.repo4" + gem "baz" + G + + bundle_config "path #{bundled_app("bundle")}" + + bundle "lock", dir: cwd + + bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s } + end + + load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") } + + expect(load_path_lines).to eq [ + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-23/#{Gem.extension_api_version}/baz-1.0.0")', + '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/baz-1.0.0/lib")', + ] + end + end + + describe "with Gemfiles using absolute path sources and resulting bundle moved to a folder hierarchy with different nesting" do + before do + build_lib "minitest", "1.0.0", path: lib_path("minitest") + + Dir.mkdir bundled_app("app") + + gemfile bundled_app("app/Gemfile"), <<-G + source "https://gem.repo1" + gem "minitest", :path => "#{lib_path("minitest")}" + G + + bundle "install", standalone: true, dir: bundled_app("app") + + Dir.mkdir tmp("one_more_level") + FileUtils.mv bundled_app, tmp("one_more_level") + end + + it "also works" do + ruby <<-RUBY, dir: tmp("one_more_level/bundled_app/app") + require "./bundle/bundler/setup" + + require "minitest" + puts MINITEST + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + end + + let(:cwd) { bundled_app } + + describe "with Gemfiles using relative path sources and app moved to a different root" do + before do + FileUtils.mkdir_p bundled_app("app/vendor") + + build_lib "minitest", "1.0.0", path: bundled_app("app/vendor/minitest") + + gemfile bundled_app("app/Gemfile"), <<-G + source "https://gem.repo1" + gem "minitest", :path => "vendor/minitest" + G + + bundle "install", standalone: true, dir: bundled_app("app") + + FileUtils.mv(bundled_app("app"), bundled_app2("app")) + end + + it "also works" do + ruby <<-RUBY, dir: bundled_app2("app") + require "./bundle/bundler/setup" + + require "minitest" + puts MINITEST + RUBY + + expect(out).to eq("1.0.0") + expect(err).to be_empty + end + end + + describe "with gems with native extension" do before do - install_gemfile <<-G, :standalone => true - source "file://#{gem_repo1}" + bundle_config "path #{bundled_app("bundle")}" + install_gemfile <<-G, standalone: true, dir: cwd + source "https://gem.repo1" gem "very_simple_binary" G end - it "generates a bundle/bundler/setup.rb with the proper paths", :rubygems => "2.4" do - extension_line = File.read(bundled_app("bundle/bundler/setup.rb")).each_line.find {|line| line.include? "/extensions/" }.strip - expect(extension_line).to start_with '$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/' - expect(extension_line).to end_with '/very_simple_binary-1.0"' + it "generates a bundle/bundler/setup.rb with the proper paths" do + expected_path = bundled_app("bundle/bundler/setup.rb") + script_content = File.read(expected_path) + expect(script_content).to include("def self.ruby_api_version") + expect(script_content).to include("def self.extension_api_version") + extension_line = script_content.each_line.find {|line| line.include? "/extensions/" }.strip + platform = Gem::Platform.local + expect(extension_line).to start_with '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/' + expect(extension_line).to end_with platform.to_s + '/#{Gem.extension_api_version}/very_simple_binary-1.0")' end end describe "with gem that has an invalid gemspec" do before do - build_git "bar", :gemspec => false do |s| + build_git "bar", gemspec: false do |s| s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') s.write "bar.gemspec", <<-G - lib = File.expand_path('../lib/', __FILE__) + lib = File.expand_path('lib/', __dir__) $:.unshift lib unless $:.include?(lib) require 'bar/version' @@ -101,14 +308,16 @@ RSpec.shared_examples "bundle install --standalone" do end G end - install_gemfile <<-G, :standalone => true + bundle_config "path #{bundled_app("bundle")}" + install_gemfile <<-G, standalone: true, dir: cwd, raise_on_error: false + source "https://gem.repo1" gem "bar", :git => "#{lib_path("bar-1.0")}" G end it "outputs a helpful error message" do - expect(out).to include("You have one or more invalid gemspecs that need to be fixed.") - expect(out).to include("bar 1.0 has an invalid gemspec") + expect(err).to include("You have one or more invalid gemspecs that need to be fixed.") + expect(err).to include("bar.gemspec is not valid") end end @@ -116,11 +325,13 @@ RSpec.shared_examples "bundle install --standalone" do before do build_git "devise", "1.0" - install_gemfile <<-G, :standalone => true - source "file://#{gem_repo1}" + gemfile <<-G + source "https://gem.repo1" gem "rails" gem "devise", :git => "#{lib_path("devise-1.0")}" G + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -138,15 +349,17 @@ RSpec.shared_examples "bundle install --standalone" do before do build_git "devise", "1.0" - install_gemfile <<-G, :standalone => true - source "file://#{gem_repo1}" + gemfile <<-G + source "https://gem.repo1" gem "rails" group :test do gem "rspec" - gem "rack-test" + gem "myrack-test" end G + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd end let(:expected_gems) do @@ -159,74 +372,72 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" it "allows creating a standalone file with limited groups" do - bundle "install --standalone default" + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: "default", dir: cwd - Dir.chdir(bundled_app) do - load_error_ruby <<-RUBY, "spec", :no_lib => true - $:.unshift File.expand_path("bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - require "spec" - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end - it "allows --without to limit the groups used in a standalone" do - bundle "install --standalone --without test" + it "allows `without` configuration to limit the groups used in a standalone" do + bundle_config "path #{bundled_app("bundle")}" + bundle_config "without test" + bundle :install, standalone: true, dir: cwd - Dir.chdir(bundled_app) do - load_error_ruby <<-RUBY, "spec", :no_lib => true - $:.unshift File.expand_path("bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - require "spec" - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end - it "allows --path to change the location of the standalone bundle" do - bundle "install --standalone --path path/to/bundle" + it "allows `path` configuration to change the location of the standalone bundle" do + bundle_config "path path/to/bundle" + bundle "install", standalone: true, dir: cwd - Dir.chdir(bundled_app) do - ruby <<-RUBY, :no_lib => true - $:.unshift File.expand_path("path/to/bundle") - require "bundler/setup" + ruby <<-RUBY + $:.unshift File.expand_path("path/to/bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - RUBY - end + require "actionpack" + puts ACTIONPACK + RUBY expect(out).to eq("2.3.2") end - it "allows remembered --without to limit the groups used in a standalone" do - bundle "install --without test" - bundle "install --standalone" + it "allows `without` to limit the groups used in a standalone" do + bundle_config "without test" + bundle :install, dir: cwd + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd - Dir.chdir(bundled_app) do - load_error_ruby <<-RUBY, "spec", :no_lib => true - $:.unshift File.expand_path("bundle") - require "bundler/setup" + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" - require "actionpack" - puts ACTIONPACK - require "spec" - RUBY - end + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY expect(out).to eq("2.3.2") - expect(err).to eq("ZOMG LOAD ERROR") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) end end @@ -239,7 +450,8 @@ RSpec.shared_examples "bundle install --standalone" do source "#{source_uri}" gem "rails" G - bundle "install --standalone", :artifice => "endpoint" + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, artifice: "endpoint", dir: cwd end let(:expected_gems) do @@ -252,67 +464,61 @@ RSpec.shared_examples "bundle install --standalone" do include_examples "common functionality" end end +end - describe "with --binstubs" do - before do - install_gemfile <<-G, :standalone => true, :binstubs => true - source "file://#{gem_repo1}" - gem "rails" - G - end +RSpec.describe "bundle install --standalone run in a subdirectory" do + let(:cwd) { bundled_app("bob").tap(&:mkpath) } - let(:expected_gems) do - { - "actionpack" => "2.3.2", - "rails" => "2.3.2", - } - end + before do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + end - include_examples "common functionality" + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd - it "creates stubs that use the standalone load path" do - Dir.chdir(bundled_app) do - expect(`bin/rails -v`.chomp).to eql "2.3.2" - end - end + expect(bundled_app("bundle/bundler/setup.rb")).to exist + end - it "creates stubs that can be executed from anywhere" do - require "tmpdir" - Dir.chdir(Dir.tmpdir) do - sys_exec!(%(#{bundled_app("bin/rails")} -v)) - expect(out).to eq("2.3.2") - end + context "when path set to a relative path" do + before do + bundle_config "path bundle" end - it "creates stubs that can be symlinked" do - pending "File.symlink is unsupported on Windows" if Bundler::WINDOWS + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd - symlink_dir = tmp("symlink") - FileUtils.mkdir_p(symlink_dir) - symlink = File.join(symlink_dir, "rails") - - File.symlink(bundled_app("bin/rails"), symlink) - sys_exec!("#{symlink} -v") - expect(out).to eq("2.3.2") - end - - it "creates stubs with the correct load path" do - extension_line = File.read(bundled_app("bin/rails")).each_line.find {|line| line.include? "$:.unshift" }.strip - expect(extension_line).to eq %($:.unshift File.expand_path "../../bundle", path.realpath) + expect(bundled_app("bundle/bundler/setup.rb")).to exist end end end -RSpec.describe "bundle install --standalone" do - include_examples("bundle install --standalone") -end - -RSpec.describe "bundle install --standalone run in a subdirectory" do +RSpec.describe "bundle install --standalone --local" do before do - subdir = bundled_app("bob") - FileUtils.mkdir_p(subdir) - Dir.chdir(subdir) + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + system_gems "myrack-1.0.0", path: default_bundle_path end - include_examples("bundle install --standalone") + it "generates script pointing to system gems" do + bundle "install --standalone --local --verbose" + + expect(out).to include("Using myrack 1.0.0") + + load_error_ruby <<-RUBY, "spec" + require "./bundler/setup" + + require "myrack" + puts MYRACK + require "spec" + RUBY + + expect(out).to eq("1.0.0") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) + end end diff --git a/spec/bundler/install/gems/sudo_spec.rb b/spec/bundler/install/gems/sudo_spec.rb deleted file mode 100644 index 13abffc14e..0000000000 --- a/spec/bundler/install/gems/sudo_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -RSpec.describe "when using sudo", :sudo => true do - describe "and BUNDLE_PATH is writable" do - context "but BUNDLE_PATH/build_info is not writable" do - before do - subdir = system_gem_path("cache") - subdir.mkpath - sudo "chmod u-w #{subdir}" - end - - it "installs" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - - expect(out).to_not match(/an error occurred/i) - expect(system_gem_path("cache/rack-1.0.0.gem")).to exist - expect(the_bundle).to include_gems "rack 1.0" - end - end - end - - describe "and GEM_HOME is owned by root" do - before :each do - chown_system_gems_to_root - end - - it "installs" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", '1.0' - gem "thin" - G - - expect(system_gem_path("gems/rack-1.0.0")).to exist - expect(system_gem_path("gems/rack-1.0.0").stat.uid).to eq(0) - expect(the_bundle).to include_gems "rack 1.0" - end - - it "installs rake and a gem dependent on rake in the same session" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rake" - gem "another_implicit_rake_dep" - G - bundle "install" - expect(system_gem_path("gems/another_implicit_rake_dep-1.0")).to exist - end - - it "installs when BUNDLE_PATH is owned by root" do - bundle_path = tmp("owned_by_root") - FileUtils.mkdir_p bundle_path - sudo "chown -R root #{bundle_path}" - - ENV["BUNDLE_PATH"] = bundle_path.to_s - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", '1.0' - G - - expect(bundle_path.join("gems/rack-1.0.0")).to exist - expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) - expect(the_bundle).to include_gems "rack 1.0" - end - - it "installs when BUNDLE_PATH does not exist" do - root_path = tmp("owned_by_root") - FileUtils.mkdir_p root_path - sudo "chown -R root #{root_path}" - bundle_path = root_path.join("does_not_exist") - - ENV["BUNDLE_PATH"] = bundle_path.to_s - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", '1.0' - G - - expect(bundle_path.join("gems/rack-1.0.0")).to exist - expect(bundle_path.join("gems/rack-1.0.0").stat.uid).to eq(0) - expect(the_bundle).to include_gems "rack 1.0" - end - - it "installs extensions/ compiled by Rubygems 2.2", :rubygems => "2.2" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "very_simple_binary" - G - - expect(system_gem_path("gems/very_simple_binary-1.0")).to exist - binary_glob = system_gem_path("extensions/*/*/very_simple_binary-1.0") - expect(Dir.glob(binary_glob).first).to be - end - end - - describe "and BUNDLE_PATH is not writable" do - before do - sudo "chmod ugo-w #{default_bundle_path}" - end - - it "installs" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", '1.0' - G - - expect(default_bundle_path("gems/rack-1.0.0")).to exist - expect(the_bundle).to include_gems "rack 1.0" - end - - it "cleans up the tmpdirs generated" do - require "tmpdir" - Dir.glob("#{Dir.tmpdir}/bundler*").each do |tmpdir| - FileUtils.remove_entry_secure(tmpdir) - end - - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - tmpdirs = Dir.glob("#{Dir.tmpdir}/bundler*") - - expect(tmpdirs).to be_empty - end - end - - describe "and GEM_HOME is not writable" do - it "installs" do - gem_home = tmp("sudo_gem_home") - sudo "mkdir -p #{gem_home}" - sudo "chmod ugo-w #{gem_home}" - - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", '1.0' - G - - bundle :install, :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } - expect(gem_home.join("bin/rackup")).to exist - expect(the_bundle).to include_gems "rack 1.0", :env => { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => nil } - end - end - - describe "and root runs install" do - let(:warning) { "Don't run Bundler as root." } - - before do - gemfile %(source "file://#{gem_repo1}") - end - - it "warns against that" do - bundle :install, :sudo => true - expect(out).to include(warning) - end - - context "when ENV['BUNDLE_SILENCE_ROOT_WARNING'] is set" do - it "skips the warning" do - bundle :install, :sudo => :preserve_env, :env => { "BUNDLE_SILENCE_ROOT_WARNING" => true } - expect(out).to_not include(warning) - end - end - - context "when silence_root_warning is passed as an option" do - it "skips the warning" do - bundle :install, :sudo => true, :silence_root_warning => true - expect(out).to_not include(warning) - end - end - - context "when silence_root_warning = false" do - it "warns against that" do - bundle :install, :sudo => true, :silence_root_warning => false - expect(out).to include(warning) - end - end - end -end diff --git a/spec/bundler/install/gems/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb index cdad9a8821..be37673aa1 100644 --- a/spec/bundler/install/gems/win32_spec.rb +++ b/spec/bundler/install/gems/win32_spec.rb @@ -1,27 +1,25 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install with win32-generated lockfile" do it "should read lockfile" do - File.open(bundled_app("Gemfile.lock"), "wb") do |f| + File.open(bundled_app_lock, "wb") do |f| f << "GEM\r\n" - f << " remote: file:#{gem_repo1}/\r\n" + f << " remote: https://gem.repo1/\r\n" f << " specs:\r\n" f << "\r\n" - f << " rack (1.0.0)\r\n" + f << " myrack (1.0.0)\r\n" f << "\r\n" f << "PLATFORMS\r\n" f << " ruby\r\n" f << "\r\n" f << "DEPENDENCIES\r\n" - f << " rack\r\n" + f << " myrack\r\n" end install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" - gem "rack" + gem "myrack" G - expect(exitstatus).to eq(0) if exitstatus end end diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb index 97eaf149c1..fb2271c830 100644 --- a/spec/bundler/install/gemspecs_spec.rb +++ b/spec/bundler/install/gemspecs_spec.rb @@ -1,110 +1,179 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do describe "when a gem has a YAML gemspec" do before :each do build_repo2 do - build_gem "yaml_spec", :gemspec => :yaml + build_gem "yaml_spec", gemspec: :yaml end end it "still installs correctly" do gemfile <<-G - source "file://#{gem_repo2}" + source "https://gem.repo2" gem "yaml_spec" G bundle :install - expect(err).to lack_errors + expect(err).to be_empty end it "still installs correctly when using path" do - build_lib "yaml_spec", :gemspec => :yaml + build_lib "yaml_spec", gemspec: :yaml install_gemfile <<-G + source "https://gem.repo1" gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}" G - expect(err).to lack_errors + expect(err).to be_empty end end it "should use gemspecs in the system cache when available" do gemfile <<-G - source "http://localtestserver.gem" - gem 'rack' + source "http://localgemserver.test" + gem 'myrack' G - FileUtils.mkdir_p "#{tmp}/gems/system/specifications" - File.open("#{tmp}/gems/system/specifications/rack-1.0.0.gemspec", "w+") do |f| + system_gems "myrack-1.0.0", path: default_bundle_path + + FileUtils.mkdir_p "#{default_bundle_path}/specifications" + File.open("#{default_bundle_path}/specifications/myrack-1.0.0.gemspec", "w+") do |f| spec = Gem::Specification.new do |s| - s.name = "rack" + s.name = "myrack" s.version = "1.0.0" - s.add_runtime_dependency "activesupport", "2.3.2" + s.add_dependency "activesupport", "2.3.2" end f.write spec.to_ruby end - bundle :install, :artifice => "endpoint_marshal_fail" # force gemspec load - expect(the_bundle).to include_gems "activesupport 2.3.2" + bundle :install, artifice: "endpoint_marshal_fail" # force gemspec load + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2" + end + + it "does not hang when gemspec has incompatible encoding" do + create_file("foo.gemspec", <<-G) + Gem::Specification.new do |gem| + gem.name = "pry-byebug" + gem.version = "3.4.2" + gem.author = "David RodrÃguez" + gem.summary = "Good stuff" + end + G + + install_gemfile <<-G, env: { "LANG" => "C" } + source "https://gem.repo1" + gemspec + G + + expect(out).to include("Bundle complete!") + end + + it "reads gemspecs respecting their encoding" do + create_file "version.rb", <<-RUBY + module Persistent💎 + VERSION = "0.0.1" + end + RUBY + + create_file "persistent-dmnd.gemspec", <<-G + require_relative "version" + + Gem::Specification.new do |gem| + gem.name = "persistent-dmnd" + gem.version = Persistent💎::VERSION + gem.author = "Ivo Anjo" + gem.summary = "Unscratchable stuff" + end + G + + install_gemfile <<-G + source "https://gem.repo1" + gemspec + G + + expect(out).to include("Bundle complete!") end context "when ruby version is specified in gemspec and gemfile" do - it "installs when patch level is not specified and the version matches" do - build_lib("foo", :path => bundled_app) do |s| + it "installs when patch level is not specified and the version matches", + if: RUBY_PATCHLEVEL >= 0 do + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = "~> #{RUBY_VERSION}.0" end install_gemfile <<-G ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby' + source "https://gem.repo1" gemspec G expect(the_bundle).to include_gems "foo 1.0" end it "installs when patch level is specified and the version still matches the current version", - :if => RUBY_PATCHLEVEL >= 0 do - build_lib("foo", :path => bundled_app) do |s| + if: RUBY_PATCHLEVEL >= 0 do + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}' + source "https://gem.repo1" gemspec G expect(the_bundle).to include_gems "foo 1.0" end - it "fails and complains about patchlevel on patchlevel mismatch", - :if => RUBY_PATCHLEVEL >= 0 do + it "installs gems ignoring the mismatch even when patchlevel is mismatch", + if: RUBY_PATCHLEVEL >= 0 do patchlevel = RUBY_PATCHLEVEL.to_i + 1 - build_lib("foo", :path => bundled_app) do |s| + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}" end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}' + source "https://gem.repo1" gemspec G - expect(out).to include("Ruby patchlevel") - expect(out).to include("but your Gemfile specified") - expect(exitstatus).to eq(18) if exitstatus + expect(the_bundle).to include_gems "foo 1.0" end it "fails and complains about version on version mismatch" do version = Gem::Requirement.create(RUBY_VERSION).requirements.first.last.bump.version - build_lib("foo", :path => bundled_app) do |s| + build_lib("foo", path: bundled_app) do |s| s.required_ruby_version = version end - install_gemfile <<-G + install_gemfile <<-G, raise_on_error: false ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby' + source "https://gem.repo1" gemspec G - expect(out).to include("Ruby version") - expect(out).to include("but your Gemfile specified") - expect(exitstatus).to eq(18) if exitstatus + expect(err).to include("Ruby version") + expect(err).to include("but your Gemfile specified") + expect(exitstatus).to eq(18) + end + + it "validates gemspecs just once when everything installed and lockfile up to date" do + build_lib "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gemspec path: "#{lib_path("foo-1.0")}" + + module Monkey + def validate(spec) + puts "Validate called on \#{spec.full_name}" + end + end + Bundler.rubygems.extend(Monkey) + G + + bundle "install" + + expect(out).to include("Validate called on foo-1.0").once end end end diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb index 04f2380b45..1172d661ae 100644 --- a/spec/bundler/install/git_spec.rb +++ b/spec/bundler/install/git_spec.rb @@ -1,52 +1,80 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do context "git sources" do it "displays the revision hash of the gem repository" do - build_git "foo", "1.0", :path => lib_path("foo") + build_git "foo", "1.0", path: lib_path("foo") - install_gemfile <<-G + install_gemfile <<-G, verbose: true + source "https://gem.repo1" gem "foo", :git => "#{lib_path("foo")}" G - bundle :install - expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master@#{revision_for(lib_path("foo"))[0..6]})") - expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + end + + it "displays the revision hash of the gem repository when passed a relative local path" do + build_git "foo", "1.0", path: lib_path("foo") + + relative_path = lib_path("foo").relative_path_from(bundled_app) + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{relative_path}" + G + + expect(out).to include("Using foo 1.0 from #{relative_path} (at main@#{revision_for(lib_path("foo"))[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + end + + it "displays the correct default branch", git: ">= 2.28.0" do + build_git "foo", "1.0", path: lib_path("foo"), default_branch: "non-standard" + + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at non-standard@#{revision_for(lib_path("foo"))[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" end it "displays the ref of the gem repository when using branch~num as a ref" do - build_git "foo", "1.0", :path => lib_path("foo") + skip "maybe branch~num notation doesn't work on Windows' git" if Gem.win_platform? + + build_git "foo", "1.0", path: lib_path("foo") rev = revision_for(lib_path("foo"))[0..6] - update_git "foo", "2.0", :path => lib_path("foo"), :gemspec => true + update_git "foo", "2.0", path: lib_path("foo"), gemspec: true rev2 = revision_for(lib_path("foo"))[0..6] - update_git "foo", "3.0", :path => lib_path("foo"), :gemspec => true + update_git "foo", "3.0", path: lib_path("foo"), gemspec: true - install_gemfile! <<-G - gem "foo", :git => "#{lib_path("foo")}", :ref => "master~2" + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :ref => "main~2" G - bundle! :install - expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at master~2@#{rev})") - expect(the_bundle).to include_gems "foo 1.0", :source => "git@#{lib_path("foo")}" + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main~2@#{rev})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" - update_git "foo", "4.0", :path => lib_path("foo"), :gemspec => true + update_git "foo", "4.0", path: lib_path("foo"), gemspec: true - bundle! :update - expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at master~2@#{rev2})") - expect(the_bundle).to include_gems "foo 2.0", :source => "git@#{lib_path("foo")}" + bundle :update, all: true, verbose: true + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main~2@#{rev2})") + expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}" end - it "should check out git repos that are missing but not being installed" do - build_git "foo" + it "allows git repos that are missing but not being installed" do + revision = build_git("foo").ref_for("HEAD") gemfile <<-G - gem "foo", :git => "file://#{lib_path("foo-1.0")}", :group => :development + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :group => :development G lockfile <<-L GIT - remote: file://#{lib_path("foo-1.0")} + remote: #{lib_path("foo-1.0")} + revision: #{revision} specs: foo (1.0) @@ -57,10 +85,285 @@ RSpec.describe "bundle install" do foo! L - bundle "install --path=vendor/bundle --without development" + bundle_config "path vendor/bundle" + bundle_config "without development" + bundle :install expect(out).to include("Bundle complete!") - expect(vendored_gems("bundler/gems/foo-1.0-#{revision_for(lib_path("foo-1.0"))[0..11]}")).to be_directory + end + + it "allows multiple gems from the same git source" do + build_repo2 do + build_lib "foo", "1.0", path: lib_path("gems/foo") + build_lib "zebra", "2.0", path: lib_path("gems/zebra") + build_git "gems", path: lib_path("gems"), gemspec: false + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "foo", :git => "#{lib_path("gems")}", :glob => "foo/*.gemspec" + gem "zebra", :git => "#{lib_path("gems")}", :glob => "zebra/*.gemspec" + G + + bundle "info foo" + expect(out).to include("* foo (1.0 #{revision_for(lib_path("gems"))[0..6]})") + + bundle "info zebra" + expect(out).to include("* zebra (2.0 #{revision_for(lib_path("gems"))[0..6]})") + end + + it "should always sort dependencies in the same order" do + # This Gemfile + lockfile had a problem where the first + # `bundle install` would change the order, but the second would + # change it back. + + # NOTE: both gems MUST have the same path! It has to be two gems in one repo. + + test = build_git "test", "1.0.0", path: lib_path("test-and-other") + other = build_git "other", "1.0.0", path: lib_path("test-and-other") + test_ref = test.ref_for("HEAD") + other_ref = other.ref_for("HEAD") + + gemfile <<-G + source "https://gem.repo1" + + gem "test", git: #{test.path.to_s.inspect} + gem "other", ref: #{other_ref.inspect}, git: #{other.path.to_s.inspect} + G + + lockfile <<-L + GIT + remote: #{test.path} + revision: #{test_ref} + specs: + test (1.0.0) + + GIT + remote: #{other.path} + revision: #{other_ref} + ref: #{other_ref} + specs: + other (1.0.0) + + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + other! + test! + + BUNDLED WITH + #{Bundler::VERSION} + L + + # If GH#6743 is present, the first `bundle install` will change the + # lockfile, by flipping the order (`other` would be moved to the top). + # + # The second `bundle install` would then change the lockfile back + # to the original. + # + # The fix makes it so it may change it once, but it will not change + # it a second time. + # + # So, we run `bundle install` once, and store the value of the + # modified lockfile. + bundle :install + modified_lockfile = lockfile + + # If GH#6743 is present, the second `bundle install` would change the + # lockfile back to what it was originally. + # + # This `expect` makes sure it doesn't change a second time. + bundle :install + expect(lockfile).to eq(modified_lockfile) + + expect(out).to include("Bundle complete!") + end + + it "allows older revisions of git source when clean true" do + build_git "foo", "1.0", path: lib_path("foo") + rev = revision_for(lib_path("foo")) + + bundle_config "path vendor/bundle" + bundle_config "clean true" + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + old_lockfile = lockfile + + update_git "foo", "2.0", path: lib_path("foo"), gemspec: true + rev2 = revision_for(lib_path("foo")) + + bundle :update, all: true, verbose: true + expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main@#{rev2[0..6]})") + expect(out).to include("Removing foo (#{rev[0..11]})") + expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}" + + lockfile(old_lockfile) + + bundle :install, verbose: true + expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})") + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + end + + context "when install directory exists" do + let(:checkout_confirmation_log_message) { "Checking out revision" } + let(:using_foo_confirmation_log_message) { "Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})" } + + context "and no contents besides .git directory are present" do + it "reinstalls gem" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile = <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # validate that the installed directory exists and has some expected contents + install_directory = default_bundle_path("bundler/gems/foo-#{revision_for(lib_path("foo"))[0..11]}") + dot_git_directory = install_directory.join(".git") + lib_directory = install_directory.join("lib") + gemspec = install_directory.join("foo.gemspec") + expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist + + # remove all elements in the install directory except .git directory + FileUtils.rm_r(lib_directory) + gemspec.delete + + expect(dot_git_directory).to exist + expect(lib_directory).not_to exist + expect(gemspec).not_to exist + + # rerun bundle install + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # validate that it reinstalls all components + expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist + end + end + + context "and contents besides .git directory are present" do + # we want to confirm that the change to try to detect partial installs and reinstall does not + # result in repeatedly reinstalling the gem when it is fully installed + it "does not reinstall gem" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile = <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}" + G + + install_gemfile gemfile, verbose: true + + expect(out).to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}" + + # rerun bundle install + install_gemfile gemfile, verbose: true + + # it isn't altogether straight-forward to validate that bundle didn't do soething on the second run, however, + # the presence of the 2nd log message confirms install got past the point that it would have logged the above if + # it was going to + expect(out).not_to include(checkout_confirmation_log_message) + expect(out).to include(using_foo_confirmation_log_message) + end + end + end + end + + describe "with excluded groups" do + it "works if you exclude a group with a git gem", ruby: ">= 3.3" do + build_git "production_gem", "1.0" + build_git "development_gem", "1.0" + + gemfile <<-G + source "https://gem.repo1" + + gem "production_gem", :git => "#{lib_path("production_gem-1.0")}" + + group :development do + gem "development_gem", :git => "#{lib_path("development_gem-1.0")}" + end + G + + # First install all groups to create lockfile + bundle :install + + # Set without and reinstall + bundle_config "without development" + bundle :install + + # Verify only production gem is available + expect(the_bundle).to include_gems("production_gem 1.0") + expect(the_bundle).not_to include_gems("development_gem 1.0") + end + + it "resolves indirect dependencies from a git source not in the requested groups" do + build_lib "activesupport", "1.0", path: lib_path("rails/activesupport") + build_git "activerecord", "1.0", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 1.0" + end + + gemfile <<-G + source "https://gem.repo1" + + gem "activerecord", :git => "#{lib_path("rails")}" + + group :ci do + gem "myrack" + end + G + + bundle_config "only ci" + bundle :install + + expect(the_bundle).to include_gems("myrack 1.0.0") + expect(the_bundle).not_to include_gems("activerecord 1.0") + end + + it "resolves indirect dependencies from a git source not in the requested groups (without compact_index dependency API)" do + build_lib "activesupport", "1.0", path: lib_path("rails/activesupport") + build_git "activerecord", "1.0", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 1.0" + end + + gemfile <<-G + source "https://gem.repo1" + + gem "activerecord", :git => "#{lib_path("rails")}" + + group :ci do + gem "myrack" + end + G + + # Force the RubygemsAggregate code path in find_source_requirements by + # making the dependency API unavailable. + bundle_config "only ci" + bundle :install, artifice: "endpoint_api_forbidden" + + expect(the_bundle).to include_gems("myrack 1.0.0") + expect(the_bundle).not_to include_gems("activerecord 1.0") end end end diff --git a/spec/bundler/install/global_cache_spec.rb b/spec/bundler/install/global_cache_spec.rb new file mode 100644 index 0000000000..4cffa65b2a --- /dev/null +++ b/spec/bundler/install/global_cache_spec.rb @@ -0,0 +1,305 @@ +# frozen_string_literal: true + +RSpec.describe "global gem caching" do + # Uses subprocess because this setting must apply across multiple app directories (bundled_app and bundled_app2) + before { bundle "config set global_gem_cache true" } + + describe "using the cross-application user cache" do + let(:source) { "http://localgemserver.test" } + let(:source2) { "http://gemserver.example.org" } + + def cache_base + # Use the unified global gem cache path if available (from RubyGems), + # otherwise fall back to the Bundler-specific cache location + if Gem.respond_to?(:global_gem_cache_path) + Pathname.new(Gem.global_gem_cache_path) + else + home(".bundle", "cache", "gems") + end + end + + def source_global_cache(*segments) + cache_base.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments) + end + + def source2_global_cache(*segments) + cache_base.join("gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments) + end + + it "caches gems into the global cache on download" do + install_gemfile <<-G, artifice: "compact_index" + source "#{source}" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + end + + it "uses globally cached gems if they exist" do + source_global_cache.mkpath + FileUtils.cp(gem_repo1("gems/myrack-1.0.0.gem"), source_global_cache("myrack-1.0.0.gem")) + + install_gemfile <<-G, artifice: "compact_index_no_gem" + source "#{source}" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "shows a proper error message if a cached gem is corrupted" do + skip "This example is not working on ruby/ruby repo" if ruby_core? + + source_global_cache.mkpath + FileUtils.touch(source_global_cache("myrack-1.0.0.gem")) + + install_gemfile <<-G, artifice: "compact_index_no_gem", raise_on_error: false + source "#{source}" + gem "myrack" + G + + expect(err).to include("Gem::Package::FormatError: package metadata is missing in #{source_global_cache("myrack-1.0.0.gem")}") + end + + it "uses a shorter path for the cache to not hit filesystem limits" do + install_gemfile <<-G, artifice: "compact_index", verbose: true + source "http://#{"a" * 255}.test" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + source_segment = "a" * 222 + ".a3cb26de2edfce9f509a65c611d99c4b" + source_cache = cache_base.join(source_segment) + cached_gem = source_cache.join("myrack-1.0.0.gem") + expect(cached_gem).to exist + ensure + # We cleanup dummy files created by this spec manually because due to a + # Ruby on Windows bug, `FileUtils.rm_rf` (run in our global after hook) + # cannot traverse directories with such long names. So we delete + # everything explicitly to workaround the bug. An alternative workaround + # would be to shell out to `rm -rf`. That also works fine, but I went with + # the more verbose and explicit approach. This whole ensure block can be + # removed once/if https://bugs.ruby-lang.org/issues/21177 is fixed, and + # once the fix propagates to all supported rubies. + File.delete cached_gem + Dir.rmdir source_cache + + File.delete compact_index_cache_path.join(source_segment, "info", "myrack") + Dir.rmdir compact_index_cache_path.join(source_segment, "info") + File.delete compact_index_cache_path.join(source_segment, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073") + Dir.rmdir compact_index_cache_path.join(source_segment, "info-etags") + Dir.rmdir compact_index_cache_path.join(source_segment, "info-special-characters") + File.delete compact_index_cache_path.join(source_segment, "versions") + File.delete compact_index_cache_path.join(source_segment, "versions.etag") + Dir.rmdir compact_index_cache_path.join(source_segment) + end + + describe "when the same gem from different sources is installed" do + it "should use the appropriate one from the global cache" do + bundle_config "path.system true" + + install_gemfile <<-G, artifice: "compact_index" + source "#{source}" + gem "myrack" + G + + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + # myrack 1.0.0 is not installed and it is in the global cache + + install_gemfile <<-G, artifice: "compact_index" + source "#{source2}" + gem "myrack", "0.9.1" + G + + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + # myrack 0.9.1 is not installed and it is in the global cache + + gemfile <<-G + source "#{source}" + gem "myrack", "1.0.0" + G + + bundle :install, artifice: "compact_index_no_gem" + # myrack 1.0.0 is installed and myrack 0.9.1 is not + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "myrack 0.9.1" + pristine_system_gems + + gemfile <<-G + source "#{source2}" + gem "myrack", "0.9.1" + G + + bundle :install, artifice: "compact_index_no_gem" + # myrack 0.9.1 is installed and myrack 1.0.0 is not + expect(the_bundle).to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack 1.0.0" + end + + it "should not install if the wrong source is provided" do + bundle_config "path.system true" + + gemfile <<-G + source "#{source}" + gem "myrack" + G + + bundle :install, artifice: "compact_index" + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + # myrack 1.0.0 is not installed and it is in the global cache + + gemfile <<-G + source "#{source2}" + gem "myrack", "0.9.1" + G + + bundle :install, artifice: "compact_index" + pristine_system_gems + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + # myrack 0.9.1 is not installed and it is in the global cache + + gemfile <<-G + source "#{source2}" + gem "myrack", "1.0.0" + G + + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + bundle :install, artifice: "compact_index_no_gem", raise_on_error: false + expect(err).to include("Internal Server Error 500") + expect(err).not_to include("ERROR REPORT TEMPLATE") + + # myrack 1.0.0 is not installed and myrack 0.9.1 is not + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "myrack 0.9.1" + + gemfile <<-G + source "#{source}" + gem "myrack", "0.9.1" + G + + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source2_global_cache("myrack-0.9.1.gem")).to exist + bundle :install, artifice: "compact_index_no_gem", raise_on_error: false + expect(err).to include("Internal Server Error 500") + expect(err).not_to include("ERROR REPORT TEMPLATE") + + # myrack 0.9.1 is not installed and myrack 1.0.0 is not + expect(the_bundle).not_to include_gems "myrack 0.9.1" + expect(the_bundle).not_to include_gems "myrack 1.0.0" + end + end + + describe "when installing gems from a different directory" do + it "uses the global cache as a source" do + bundle_config "path.system true" + + install_gemfile <<-G, artifice: "compact_index" + source "#{source}" + gem "myrack" + gem "activesupport" + G + + # Both gems are installed and in the global cache + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + pristine_system_gems + # Both gems are now only in the global cache + expect(the_bundle).not_to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + + install_gemfile <<-G, artifice: "compact_index_no_gem" + source "#{source}" + gem "myrack" + G + + # myrack is installed and both are in the global cache + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + + create_file bundled_app2("gems.rb"), <<-G + source "#{source}" + gem "activesupport" + G + + # Neither gem is installed and both are in the global cache + expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2 + expect(the_bundle).not_to include_gems "activesupport 2.3.5", dir: bundled_app2 + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + + # Install using the global cache instead of by downloading the .gem + # from the server + bundle :install, artifice: "compact_index_no_gem", dir: bundled_app2 + + # activesupport is installed and both are in the global cache + expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2 + expect(the_bundle).to include_gems "activesupport 2.3.5", dir: bundled_app2 + + expect(source_global_cache("myrack-1.0.0.gem")).to exist + expect(source_global_cache("activesupport-2.3.5.gem")).to exist + end + end + end + + describe "extension caching" do + it "works" do + skip "gets incorrect ref in path" if Gem.win_platform? + skip "fails for unknown reason when run by ruby-core" if ruby_core? + + build_git "very_simple_git_binary", &:add_c_extension + build_lib "very_simple_path_binary", &:add_c_extension + revision = revision_for(lib_path("very_simple_git_binary-1.0"))[0, 12] + + install_gemfile <<-G + source "https://gem.repo1" + + gem "very_simple_binary" + gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}" + gem "very_simple_path_binary", :path => "#{lib_path("very_simple_path_binary-1.0")}" + G + + gem_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope, + "gem.repo1.443.#{Digest(:MD5).hexdigest("gem.repo1.443./")}", "very_simple_binary-1.0") + git_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope, + "very_simple_git_binary-1.0-#{revision}", "very_simple_git_binary-1.0") + + cached_extensions = Pathname.glob(home(".bundle", "cache", "extensions", "*", "*", "*", "*", "*")).sort + expect(cached_extensions).to eq [gem_binary_cache, git_binary_cache].sort + + run <<-R + require 'very_simple_binary_c'; puts ::VERY_SIMPLE_BINARY_IN_C + require 'very_simple_git_binary_c'; puts ::VERY_SIMPLE_GIT_BINARY_IN_C + R + expect(out).to eq "VERY_SIMPLE_BINARY_IN_C\nVERY_SIMPLE_GIT_BINARY_IN_C" + + FileUtils.rm_r Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")] + + gem_binary_cache.join("very_simple_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } + git_binary_cache.join("very_simple_git_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" } + + bundle_config "path different_path" + bundle :install + + expect(Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]).to all(end_with(".rb")) + + run <<-R + require 'very_simple_binary_c' + require 'very_simple_git_binary_c' + R + expect(out).to eq "very_simple_binary_c.rb\nvery_simple_git_binary_c.rb" + end + end +end diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb index 7a501d42b3..49360e511e 100644 --- a/spec/bundler/install/path_spec.rb +++ b/spec/bundler/install/path_spec.rb @@ -1,66 +1,87 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do - describe "with --path" do + describe "with path configured" do before :each do - build_gem "rack", "1.0.0", :to_system => true do |s| - s.write "lib/rack.rb", "puts 'FAIL'" + build_gem "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "puts 'FAIL'" end gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end - it "does not use available system gems with bundle --path vendor/bundle" do - bundle "install --path vendor/bundle" - expect(the_bundle).to include_gems "rack 1.0.0" + it "does not use available system gems with `vendor/bundle" do + bundle_config "path vendor/bundle" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "uses system gems with `path.system` configured with more priority than `path`" do + bundle_config "path.system true" + bundle_config_global "path vendor/bundle" + bundle :install + run "require 'myrack'", raise_on_error: false + expect(out).to include("FAIL") end it "handles paths with regex characters in them" do dir = bundled_app("bun++dle") dir.mkpath - Dir.chdir(dir) do - bundle "install --path vendor/bundle" - expect(out).to include("installed into ./vendor/bundle") - end + bundle_config "path #{dir.join("vendor/bundle")}" + bundle :install, dir: dir + expect(out).to include("installed into `./vendor/bundle`") + + FileUtils.rm_rf dir + end - dir.rmtree + it "prints a message to let the user know where gems where installed" do + bundle_config "path vendor/bundle" + bundle :install + expect(out).to include("gems are installed into `./vendor/bundle`") end - it "prints a warning to let the user know what has happened with bundle --path vendor/bundle" do - bundle "install --path vendor/bundle" - expect(out).to include("gems are installed into ./vendor") + it "installs the bundle relatively to repository root, when Bundler run from the same directory" do + bundle "config set path vendor/bundle", dir: bundled_app.parent + bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/vendor/bundle`") + expect(bundled_app("vendor/bundle")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "disallows --path vendor/bundle --system" do - bundle "install --path vendor/bundle --system" - expect(out).to include("Please choose only one option.") - expect(exitstatus).to eq(15) if exitstatus + it "installs the bundle relatively to repository root, when Bundler run from a different directory" do + bundle "config set path vendor/bundle", dir: bundled_app + bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/vendor/bundle`") + expect(bundled_app("vendor/bundle")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "remembers to disable system gems after the first time with bundle --path vendor/bundle" do - bundle "install --path vendor/bundle" - FileUtils.rm_rf bundled_app("vendor") - bundle "install" + it "installs the standalone bundle relative to the cwd" do + bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app.parent + expect(out).to include("installed into `./bundled_app/bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app("subdir").tap(&:mkpath) + expect(out).to include("installed into `../bundle`") + expect(bundled_app("bundle")).to be_directory + expect(bundled_app("bundle/ruby")).to be_directory end end describe "when BUNDLE_PATH or the global path config is set" do before :each do - build_lib "rack", "1.0.0", :to_system => true do |s| - s.write "lib/rack.rb", "raise 'FAIL'" + build_lib "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "raise 'FAIL'" end gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G end @@ -68,111 +89,126 @@ RSpec.describe "bundle install" do if type == :env ENV["BUNDLE_PATH"] = location elsif type == :global - bundle "config path #{location}", "no-color" => nil + bundle "config set path #{location}", "no-color" => nil end end [:env, :global].each do |type| - it "installs gems to a path if one is specified" do - set_bundle_path(type, bundled_app("vendor2").to_s) - bundle "install --path vendor/bundle" + context "when set via #{type}" do + it "installs gems to a path if one is specified" do + set_bundle_path(type, bundled_app("vendor2").to_s) + bundle_config "path vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(bundled_app("vendor2")).not_to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" - end + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(bundled_app("vendor2")).not_to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" + end - it "installs gems to BUNDLE_PATH with #{type}" do - set_bundle_path(type, bundled_app("vendor").to_s) + it "installs gems to ." do + set_bundle_path(type, ".") + bundle_config_global "disable_shared_gems true" - bundle :install + bundle :install - expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" - end + paths_to_exist = %w[cache/myrack-1.0.0.gem gems/myrack-1.0.0 specifications/myrack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) } + expect(paths_to_exist).to all exist + expect(the_bundle).to include_gems "myrack 1.0.0" + end - it "installs gems to BUNDLE_PATH relative to root when relative" do - set_bundle_path(type, "vendor") + it "installs gems to the path" do + set_bundle_path(type, bundled_app("vendor").to_s) - FileUtils.mkdir_p bundled_app("lol") - Dir.chdir(bundled_app("lol")) do bundle :install + + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - expect(bundled_app("vendor/gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + it "installs gems to the path relative to root when relative" do + set_bundle_path(type, "vendor") + + FileUtils.mkdir_p bundled_app("lol") + bundle :install, dir: bundled_app("lol") + + expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" + end end end it "installs gems to BUNDLE_PATH from .bundle/config" do - config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s + bundle_config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end it "sets BUNDLE_PATH as the first argument to bundle install" do - bundle "install --path ./vendor/bundle" + bundle_config "path ./vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end it "disables system gems when passing a path to install" do # This is so that vendored gems can be distributed to others - build_gem "rack", "1.1.0", :to_system => true - bundle "install --path ./vendor/bundle" + build_gem "myrack", "1.1.0", to_system: true + bundle_config "path ./vendor/bundle" + bundle :install - expect(vendored_gems("gems/rack-1.0.0")).to be_directory - expect(the_bundle).to include_gems "rack 1.0.0" + expect(vendored_gems("gems/myrack-1.0.0")).to be_directory + expect(the_bundle).to include_gems "myrack 1.0.0" end - it "re-installs gems whose extensions have been deleted", :ruby_repo, :rubygems => ">= 2.3" do - build_lib "very_simple_binary", "1.0.0", :to_system => true do |s| + it "re-installs gems whose extensions have been deleted" do + build_lib "very_simple_binary", "1.0.0", to_system: true do |s| s.write "lib/very_simple_binary.rb", "raise 'FAIL'" end gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo1" gem "very_simple_binary" G - bundle "install --path ./vendor/bundle" + bundle_config "path ./vendor/bundle" + bundle :install expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory expect(vendored_gems("extensions")).to be_directory - expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1" - vendored_gems("extensions").rmtree + FileUtils.rm_rf vendored_gems("extensions") - run "require 'very_simple_binary_c'" + run "require 'very_simple_binary_c'", raise_on_error: false expect(err).to include("Bundler::GemNotFound") - bundle "install --path ./vendor/bundle" + bundle_config "path ./vendor/bundle" + bundle :install expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory expect(vendored_gems("extensions")).to be_directory - expect(the_bundle).to include_gems "very_simple_binary 1.0", :source => "remote1" + expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1" end end describe "to a file" do before do - in_app_root do - `touch /tmp/idontexist bundle` - end + FileUtils.touch bundled_app("bundle") end it "reports the file exists" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" + source "https://gem.repo1" + gem "myrack" G - bundle "install --path bundle" - expect(out).to match(/file already exists/) + bundle_config "path bundle" + bundle :install, raise_on_error: false + expect(err).to include("file already exists") end end end diff --git a/spec/bundler/install/post_bundle_message_spec.rb b/spec/bundler/install/post_bundle_message_spec.rb deleted file mode 100644 index 4453e4190f..0000000000 --- a/spec/bundler/install/post_bundle_message_spec.rb +++ /dev/null @@ -1,190 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" - -RSpec.describe "post bundle message" do - before :each do - gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "activesupport", "2.3.5", :group => [:emo, :test] - group :test do - gem "rspec" - end - gem "rack-obama", :group => :obama - G - end - - let(:bundle_show_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." } - let(:bundle_deployment_message) { "Bundled gems are installed into ./vendor" } - let(:bundle_complete_message) { "Bundle complete!" } - let(:bundle_updated_message) { "Bundle updated!" } - let(:installed_gems_stats) { "4 Gemfile dependencies, 5 gems now installed." } - - describe "for fresh bundle install" do - it "without any options" do - bundle :install - expect(out).to include(bundle_show_message) - expect(out).not_to include("Gems in the group") - expect(out).to include(bundle_complete_message) - expect(out).to include(installed_gems_stats) - end - - it "with --without one group" do - bundle "install --without emo" - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the group emo were not installed") - expect(out).to include(bundle_complete_message) - expect(out).to include(installed_gems_stats) - end - - it "with --without two groups" do - bundle "install --without emo test" - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the groups emo and test were not installed") - expect(out).to include(bundle_complete_message) - expect(out).to include("4 Gemfile dependencies, 3 gems now installed.") - end - - it "with --without more groups" do - bundle "install --without emo obama test" - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the groups emo, obama and test were not installed") - expect(out).to include(bundle_complete_message) - expect(out).to include("4 Gemfile dependencies, 2 gems now installed.") - end - - describe "with --path and" do - it "without any options" do - bundle "install --path vendor" - expect(out).to include(bundle_deployment_message) - expect(out).to_not include("Gems in the group") - expect(out).to include(bundle_complete_message) - end - - it "with --without one group" do - bundle "install --without emo --path vendor" - expect(out).to include(bundle_deployment_message) - expect(out).to include("Gems in the group emo were not installed") - expect(out).to include(bundle_complete_message) - end - - it "with --without two groups" do - bundle "install --without emo test --path vendor" - expect(out).to include(bundle_deployment_message) - expect(out).to include("Gems in the groups emo and test were not installed") - expect(out).to include(bundle_complete_message) - end - - it "with --without more groups" do - bundle "install --without emo obama test --path vendor" - expect(out).to include(bundle_deployment_message) - expect(out).to include("Gems in the groups emo, obama and test were not installed") - expect(out).to include(bundle_complete_message) - end - - it "with an absolute --path inside the cwd" do - bundle "install --path #{bundled_app}/cache" - expect(out).to include("Bundled gems are installed into ./cache") - expect(out).to_not include("Gems in the group") - expect(out).to include(bundle_complete_message) - end - - it "with an absolute --path outside the cwd" do - bundle "install --path #{bundled_app}_cache" - expect(out).to include("Bundled gems are installed into #{bundled_app}_cache") - expect(out).to_not include("Gems in the group") - expect(out).to include(bundle_complete_message) - end - end - - describe "with misspelled or non-existent gem name" do - it "should report a helpful error message" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "not-a-gem", :group => :development - G - expect(out).to include("Could not find gem 'not-a-gem' in any of the gem sources listed in your Gemfile.") - end - - it "should report a helpful error message with reference to cache if available" do - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - G - bundle :cache - expect(bundled_app("vendor/cache/rack-1.0.0.gem")).to exist - install_gemfile <<-G - source "file://#{gem_repo1}" - gem "rack" - gem "not-a-gem", :group => :development - G - expect(out).to include("Could not find gem 'not-a-gem' in any of the gem sources listed in your Gemfile or in gems cached in vendor/cache.") - end - end - end - - describe "for second bundle install run" do - it "without any options" do - 2.times { bundle :install } - expect(out).to include(bundle_show_message) - expect(out).to_not include("Gems in the groups") - expect(out).to include(bundle_complete_message) - expect(out).to include(installed_gems_stats) - end - - it "with --without one group" do - bundle "install --without emo" - bundle :install - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the group emo were not installed") - expect(out).to include(bundle_complete_message) - expect(out).to include(installed_gems_stats) - end - - it "with --without two groups" do - bundle "install --without emo test" - bundle :install - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the groups emo and test were not installed") - expect(out).to include(bundle_complete_message) - end - - it "with --without more groups" do - bundle "install --without emo obama test" - bundle :install - expect(out).to include(bundle_show_message) - expect(out).to include("Gems in the groups emo, obama and test were not installed") - expect(out).to include(bundle_complete_message) - end - end - - describe "for bundle update" do - it "without any options" do - bundle :update - expect(out).not_to include("Gems in the groups") - expect(out).to include(bundle_updated_message) - end - - it "with --without one group" do - bundle :install, :without => :emo - bundle :update - expect(out).to include("Gems in the group emo were not installed") - expect(out).to include(bundle_updated_message) - end - - it "with --without two groups" do - bundle "install --without emo test" - bundle :update - expect(out).to include("Gems in the groups emo and test were not installed") - expect(out).to include(bundle_updated_message) - end - - it "with --without more groups" do - bundle "install --without emo obama test" - bundle :update - expect(out).to include("Gems in the groups emo, obama and test were not installed") - expect(out).to include(bundle_updated_message) - end - end -end diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb index 6c32094d90..9f764d127c 100644 --- a/spec/bundler/install/prereleases_spec.rb +++ b/spec/bundler/install/prereleases_spec.rb @@ -1,11 +1,19 @@ # frozen_string_literal: true -require "spec_helper" RSpec.describe "bundle install" do + before do + build_repo2 do + build_gem "not_released", "1.0.pre" + + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + end + describe "when prerelease gems are available" do it "finds prereleases" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "not_released" G expect(the_bundle).to include_gems "not_released 1.0.pre" @@ -13,7 +21,7 @@ RSpec.describe "bundle install" do it "uses regular releases if available" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "has_prerelease" G expect(the_bundle).to include_gems "has_prerelease 1.0" @@ -21,7 +29,7 @@ RSpec.describe "bundle install" do it "uses prereleases if requested" do install_gemfile <<-G - source "file://#{gem_repo1}" + source "https://gem.repo2" gem "has_prerelease", "1.1.pre" G expect(the_bundle).to include_gems "has_prerelease 1.1.pre" @@ -30,13 +38,17 @@ RSpec.describe "bundle install" do describe "when prerelease gems are not available" do it "still works" do - build_repo3 + build_repo3 do + build_gem "myrack" + end + FileUtils.rm_r Dir[gem_repo3("prerelease*")] + install_gemfile <<-G - source "file://#{gem_repo3}" - gem "rack" + source "https://gem.repo3" + gem "myrack" G - expect(the_bundle).to include_gems "rack 1.0" + expect(the_bundle).to include_gems "myrack 1.0" end end end diff --git a/spec/bundler/install/process_lock_spec.rb b/spec/bundler/install/process_lock_spec.rb new file mode 100644 index 0000000000..b096291d1a --- /dev/null +++ b/spec/bundler/install/process_lock_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +RSpec.describe "process lock spec" do + describe "when an install operation is already holding a process lock" do + before { FileUtils.mkdir_p(default_bundle_path) } + + it "will not run a second concurrent bundle install until the lock is released" do + thread = Thread.new do + Bundler::ProcessLock.lock(default_bundle_path) do + sleep 1 # ignore quality_spec + expect(the_bundle).not_to include_gems "myrack 1.0" + end + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + thread.join + expect(the_bundle).to include_gems "myrack 1.0" + end + + context "when creating a lock raises Errno::ENOTSUP" do + before { allow(File).to receive(:open).and_raise(Errno::ENOTSUP) } + + it "skips creating the lockfile and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + + context "when creating a lock raises Errno::EPERM" do + before { allow(File).to receive(:open).and_raise(Errno::EPERM) } + + it "skips creating the lockfile and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + + context "when creating a lock raises Errno::EROFS" do + before { allow(File).to receive(:open).and_raise(Errno::EROFS) } + + it "skips creating the lockfile and yields" do + processed = false + Bundler::ProcessLock.lock(default_bundle_path) { processed = true } + + expect(processed).to eq true + end + end + + it "refreshes gem specification cache after waiting for lock" do + build_repo2 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + # First, install the gem so it's available + bundle "install" + expect(out).to include("Installing myrack") + + # Queue for thread-safe communication + lock_acquired = Queue.new + can_release_lock = Queue.new + install_output = Queue.new + + # Thread holds lock (simulating another bundle process that just finished installing) + thread = Thread.new do + Bundler::ProcessLock.lock(default_bundle_path) do + # Signal that we have the lock + lock_acquired << true + # Wait until main thread signals we can release + can_release_lock.pop + end + end + + # Wait for thread to acquire lock + lock_acquired.pop + + # Start another install in a thread - it will wait for the lock + install_thread = Thread.new do + bundle "install", verbose: true + install_output << out + end + + # Give subprocess time to start and begin waiting for lock + sleep 0.5 + + # Signal thread to release the lock + can_release_lock << true + + # Wait for both threads to complete + thread.join + install_thread.join + + second_install_out = install_output.pop + + expect(the_bundle).to include_gems "myrack 1.0.0" + # The second install should have refreshed its cache after acquiring + # the lock and seen that myrack was already installed + expect(second_install_out).to include("Using myrack") + end + end +end diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb index ab531bdad6..e7f64dc227 100644 --- a/spec/bundler/install/security_policy_spec.rb +++ b/spec/bundler/install/security_policy_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require "spec_helper" + require "rubygems/security" # unfortunately, testing signed gems with a provided CA is extremely difficult @@ -9,38 +9,35 @@ RSpec.describe "policies with unsigned gems" do before do build_security_repo gemfile <<-G - source "file://#{security_repo}" - gem "rack" + source "https://gems.security" + gem "myrack" gem "signed_gem" G end it "will work after you try to deploy without a lock" do - bundle "install --deployment" + bundle "install --deployment", raise_on_error: false bundle :install - expect(exitstatus).to eq(0) if exitstatus - expect(the_bundle).to include_gems "rack 1.0", "signed_gem 1.0" + expect(the_bundle).to include_gems "myrack 1.0", "signed_gem 1.0" end it "will fail when given invalid security policy" do - bundle "install --trust-policy=InvalidPolicyName" - expect(out).to include("Rubygems doesn't know about trust policy") + bundle "install --trust-policy=InvalidPolicyName", raise_on_error: false + expect(err).to include("RubyGems doesn't know about trust policy") end it "will fail with High Security setting due to presence of unsigned gem" do - bundle "install --trust-policy=HighSecurity" - expect(out).to include("security policy didn't allow") + bundle "install --trust-policy=HighSecurity", raise_on_error: false + expect(err).to include("security policy didn't allow") end - # This spec will fail on Rubygems 2 rc1 due to a bug in policy.rb. the bug is fixed in rc3. - it "will fail with Medium Security setting due to presence of unsigned gem", :unless => ENV["RGV"] == "v2.0.0.rc.1" do - bundle "install --trust-policy=MediumSecurity" - expect(out).to include("security policy didn't allow") + it "will fail with Medium Security setting due to presence of unsigned gem" do + bundle "install --trust-policy=MediumSecurity", raise_on_error: false + expect(err).to include("security policy didn't allow") end it "will succeed with no policy" do bundle "install" - expect(exitstatus).to eq(0) if exitstatus end end @@ -48,30 +45,28 @@ RSpec.describe "policies with signed gems and no CA" do before do build_security_repo gemfile <<-G - source "file://#{security_repo}" + source "https://gems.security" gem "signed_gem" G end it "will fail with High Security setting, gem is self-signed" do - bundle "install --trust-policy=HighSecurity" - expect(out).to include("security policy didn't allow") + bundle "install --trust-policy=HighSecurity", raise_on_error: false + expect(err).to include("security policy didn't allow") end it "will fail with Medium Security setting, gem is self-signed" do - bundle "install --trust-policy=MediumSecurity" - expect(out).to include("security policy didn't allow") + bundle "install --trust-policy=MediumSecurity", raise_on_error: false + expect(err).to include("security policy didn't allow") end it "will succeed with Low Security setting, low security accepts self signed gem" do bundle "install --trust-policy=LowSecurity" - expect(exitstatus).to eq(0) if exitstatus expect(the_bundle).to include_gems "signed_gem 1.0" end it "will succeed with no policy" do bundle "install" - expect(exitstatus).to eq(0) if exitstatus expect(the_bundle).to include_gems "signed_gem 1.0" end end diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb index d42978ce4c..c92af7bfb0 100644 --- a/spec/bundler/install/yanked_spec.rb +++ b/spec/bundler/install/yanked_spec.rb @@ -1,72 +1,254 @@ # frozen_string_literal: true -require "spec_helper" RSpec.context "when installing a bundle that includes yanked gems" do - before(:each) do + it "throws an error when the original gem version is yanked" do build_repo4 do build_gem "foo", "9.0.0" end - end - it "throws an error when the original gem version is yanked" do lockfile <<-L GEM - remote: file://#{gem_repo4} + remote: https://gem.repo4 specs: foo (10.0.0) PLATFORMS - ruby + #{lockfile_platforms} DEPENDENCIES foo (= 10.0.0) L - install_gemfile <<-G - source "file://#{gem_repo4}" - gem "foo", "10.0.0" + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "foo", "10.0.0" G - expect(out).to include("Your bundle is locked to foo (10.0.0)") + expect(err).to include("Your bundle is locked to foo (10.0.0)") + end + + context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + actiontext (6.1.6) + nokogiri (>= 1.8) + foo (1.0.0) + nokogiri (1.13.8-#{Bundler.local_platform}) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + actiontext (= 6.1.6) + foo (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + skip "Materialization on Windows is not yet strict, so the example does not detect the gem has been yanked" if Gem.win_platform? + + build_repo4 do + build_gem "foo", "1.0.0" + build_gem "foo", "1.0.1" + build_gem "actiontext", "6.1.7" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "actiontext", "6.1.6" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "actiontext", "6.1.7" do |s| + s.add_dependency "nokogiri", ">= 1.8" + end + build_gem "nokogiri", "1.13.8" + end + + gemfile <<~G + source "https://gem.repo4" + gem "foo", "1.0.0" + gem "actiontext", "6.1.6" + G + + lockfile original_lockfile + end + + context "and a re-resolve is necessary" do + before do + gemfile gemfile.sub('"foo", "1.0.0"', '"foo", "1.0.1"') + end + + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the old index is used" do + bundle "install", artifice: "endpoint", verbose: true + + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end + + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the compact index API is used" do + bundle "install", artifice: "compact_index", verbose: true + + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end + end + + it "reports the yanked gem properly when the old index is used" do + bundle "install", artifice: "endpoint", raise_on_error: false + + expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})") + end + + it "reports the yanked gem properly when the compact index API is used" do + bundle "install", artifice: "compact_index", raise_on_error: false + + expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})") + end end it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do - install_gemfile <<-G - source "file://#{gem_repo4}" - gem "foo", "10.0.0" + build_repo4 do + build_gem "foo", "9.0.0" + end + + bundle_config "force_ruby_platform true" + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "foo", "10.0.0" + G + + expect(err).not_to include("Your bundle is locked to foo (10.0.0)") + expect(err).to include("Could not find gem 'foo (= 10.0.0)' in") + end +end + +RSpec.context "when resolving a bundle that includes yanked gems, but unlocking an unrelated gem" do + before(:each) do + build_repo4 do + build_gem "foo", "10.0.0" + + build_gem "bar", "1.0.0" + build_gem "bar", "2.0.0" + end + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + foo (9.0.0) + bar (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo + bar + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "foo" + gem "bar" G + end + + it "does not update the yanked gem" do + bundle "lock --update bar" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + bar (2.0.0) + foo (9.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + bar + foo - expect(out).not_to include("Your bundle is locked to foo (10.0.0)") - expect(out).to include("Could not find gem 'foo (= 10.0.0)' in any of the gem sources") + BUNDLED WITH + #{Bundler::VERSION} + L end end RSpec.context "when using gem before installing" do it "does not suggest the author has yanked the gem" do gemfile <<-G - source "file://#{gem_repo1}" - gem "rack", "0.9.1" + source "https://gem.repo1" + gem "myrack", "0.9.1" G lockfile <<-L - GEM - remote: file://#{gem_repo1} - specs: - rack (0.9.1) + GEM + remote: https://gem.repo1 + specs: + myrack (0.9.1) - PLATFORMS - ruby + PLATFORMS + #{lockfile_platforms} - DEPENDENCIES - rack (= 0.9.1) + DEPENDENCIES + myrack (= 0.9.1) + L + + bundle :list, raise_on_error: false + + expect(err).to include("Could not find myrack-0.9.1 in locally installed gems") + expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from") + expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.") + expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.") + + # Check error message is still correct when multiple platforms are locked + lockfile lockfile.gsub(/PLATFORMS\n #{lockfile_platforms}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}") + + bundle :list, raise_on_error: false + expect(err).to include("Could not find myrack-0.9.1 in locally installed gems") + end + + it "does not suggest the author has yanked the gem when using more than one gem, but shows all gems that couldn't be found in the source" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "myrack_middleware", "1.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo1 + specs: + myrack (0.9.1) + myrack_middleware (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack (= 0.9.1) + myrack_middleware (1.0) L - bundle :list + bundle :list, raise_on_error: false - expect(out).to include("Could not find rack-0.9.1 in any of the sources") - expect(out).to_not include("Your bundle is locked to rack (0.9.1), but that version could not be found in any of the sources listed in your Gemfile.") - expect(out).to_not include("If you haven't changed sources, that means the author of rack (0.9.1) has removed it.") - expect(out).to_not include("You'll need to update your bundle to a different version of rack (0.9.1) that hasn't been removed in order to install.") + expect(err).to include("Could not find myrack-0.9.1, myrack_middleware-1.0 in locally installed gems") + expect(err).to include("Install missing gems with `bundle install`.") + expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from") + expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.") + expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.") end end |
