diff options
Diffstat (limited to 'spec/bundler/install')
45 files changed, 16030 insertions, 0 deletions
diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb new file mode 100644 index 0000000000..c7ab7c3d7e --- /dev/null +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +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" + source "http://testgemserver.local" + gem "myrack-obama" + G + expect(the_bundle).to include_gem("myrack 1.0") + end + + it "still fails when the network is down" do + install_gemfile <<-G, artifice: "fail", raise_on_error: false + source "http://testgemserver.local" + gem "myrack-obama" + G + 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 ["myrack-1.0.0"], path: default_bundle_path + + bundle_config "clean false" + install_gemfile <<-G, artifice: "compact_index" + source "http://testgemserver.local" + gem "myrack-obama" + gem "myrack", "< 1.0" + G + + expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 0.9.1") + + gemfile <<-G + source "http://testgemserver.local" + gem "myrack-obama" + G + + 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("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 <<~RUBY + #!/usr/bin/env ruby + 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 + ENV["PATH"] = ENV["PATH"].sub(/^.*?:/, "") + exec("git", *ARGV) + RUBY + end + + old_path = ENV["PATH"] + ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}" + yield if block_given? + ensure + ENV["PATH"] = old_path if block_given? + end + + it "will install from a cached git repo" do + 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, 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 + source "https://gem.repo1" + gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch" + G + end + expect(err).to include("Using cached git data because of network errors") + expect(the_bundle).to be_locked + end + end +end diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb new file mode 100644 index 0000000000..c2eccb3ef2 --- /dev/null +++ b/spec/bundler/install/binstubs_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "when system_bindir is set" do + it "overrides Gem.bindir" do + expect(Pathname.new("/usr/bin")).not_to be_writable + gemfile <<-G + def Gem.bindir; "/usr/bin"; end + source "https://gem.repo1" + gem "myrack" + G + + bundle_config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(system_gem_path("altbin/myrackup")).to exist + end + end + + describe "when multiple gems contain the same exe" do + before do + build_repo2 do + build_gem "fake", "14" do |s| + s.executables = "myrackup" + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "fake" + gem "myrack" + G + end + + it "warns about the situation" do + bundle "exec myrackup" + + 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 new file mode 100644 index 0000000000..86c22dad55 --- /dev/null +++ b/spec/bundler/install/bundler_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +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" + end + build_gem "bundler", "0.9.1" + build_gem "bundler", Bundler::VERSION + end + end + + it "are forced to the current bundler version" do + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0" + G + + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" + end + + it "are forced to the current bundler version even if not already present" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}" + end + + 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 + Could not find compatible versions + + 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. + + Your bundle requires a different version of Bundler than the one you're running, and that version could not be found. + E + 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 "https://gem.repo2" + + gem "multiple_versioned_deps" + G + + install_gemfile <<-G + source "https://gem.repo2" + + gem "multiple_versioned_deps" + gem "myrack" + G + + expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0" + end + + it "includes bundler in the bundle when it's a child dependency" do + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError; puts 'FAIL'; end" + expect(out).to eq("WIN") + end + + it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end" + expect(out).to eq("WIN") + end + + it "causes a conflict if child dependencies conflict" do + 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_pinned_to_old_activesupport" + G + + nice_error = <<~E.strip + Could not find compatible versions + + 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(err).to include(nice_error) + end + + it "causes a conflict if a child dependency conflicts with the Gemfile" do + 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 + Could not find compatible versions + + 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(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 with system gems" do + bundle_config "path.system true" + + 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") + 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") + end + end +end diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb new file mode 100644 index 0000000000..b3f57d93cc --- /dev/null +++ b/spec/bundler/install/cooldown_spec.rb @@ -0,0 +1,272 @@ +# 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 + 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 + end +end diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb new file mode 100644 index 0000000000..a3b4a87ecf --- /dev/null +++ b/spec/bundler/install/deploy_spec.rb @@ -0,0 +1,493 @@ +# frozen_string_literal: true + +RSpec.describe "install in deployment or frozen mode" do + before do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + 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_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" + 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_config "deployment true" + bundle_config "without test" + bundle :install + end + + 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 "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") + gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :path => "#{lib_path("nested")}" + gem "bar", :path => "#{lib_path("nested")}" + G + + 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) + source "http://user:pass@localgemserver.test/" + + gem "myrack-obama", ">= 1.0" + G + + 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 "https://gem.repo1" + source "https://gem.repo1" do + gem "myrack" + end + G + + bundle_config "deployment true" + bundle :install + + 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 + before do + bundle "install" + end + + 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 "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 "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 "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + 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 "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("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 "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + ENV["BUNDLE_FROZEN"] = "false" + ENV["BUNDLE_DEPLOYMENT"] = "false" + bundle "install" + expect(out).not_to include("frozen mode") + expect(out).not_to include("You have added to the Gemfile") + expect(out).not_to include("* myrack-obama") + end + + it "explodes if you replace a gem and don't check in the lockfile" do + gemfile <<-G + source "https://gem.repo1" + gem "activesupport" + G + + 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 "https://gem.repo1" + gem "myrack", :git => "git://hubz.com" + G + + 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 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 "https://gem.repo1" + gem "myrack" + G + + 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 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 "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + gem "foo", :git => "#{lib_path("myrack")}" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "foo", :git => "#{lib_path("myrack")}" + G + + 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 change a source from path to git" do + build_git "myrack", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :path => "#{lib_path("myrack")}" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "https:/my-git-repo-for-myrack" + G + + 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 :lock + + bundle_config "deployment true" + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "myrack-obama" + G + + 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: + * myrack (= 1.0.0) + * myrack-obama + + 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 + source "https://gem.repo1" + gem "foo", :path => "#{lib_path("foo")}" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + + bundle :cache + expect(bundled_app("vendor/cache/foo")).to be_directory + + bundle "install --local" + expect(out).to include("Updating files in vendor/cache") + + 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("vendor/cache/foo") + expect(the_bundle).to include_gems "foo 1.0" + end + end +end diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb new file mode 100644 index 0000000000..32ca455439 --- /dev/null +++ b/spec/bundler/install/failure_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "installing a gem fails" 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" + s.write "Rakefile", <<-RUBY + task :default do + abort "make installing activesupport-2.3.2 fail" + end + RUBY + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "rails" + G + 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. + +In Gemfile: + rails was resolved to 2.3.2, which depends on + actionmailer was resolved to 2.3.2, which depends on + 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 new file mode 100644 index 0000000000..e0f6fb6364 --- /dev/null +++ b/spec/bundler/install/force_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + 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 + myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb") + + bundle :install + myrack_lib.open("w") {|f| f.write("blah blah blah") } + bundle :install, flag => true + + 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, flag => true + + 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 + let!(:ref) { build_git("foo", "1.0").ref_for("HEAD", 11) } + + before do + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + end + + it "re-installs installed gems" do + foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb") + + bundle :install + foo_lib.open("w") {|f| f.write("blah blah blah") } + bundle :install, flag => true + + 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, flag => true + + 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 new file mode 100644 index 0000000000..3afa4f5daa --- /dev/null +++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with gemfile that uses eval_gemfile" do + before do + build_lib("gunks", path: bundled_app("gems/gunks")) do |s| + s.name = "gunks" + s.version = "0.0.1" + end + end + + context "eval-ed Gemfile points to an internal gemspec" do + before do + 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("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: 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 + 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 mode configured" do + bundle :install + bundle_config "deployment true" + bundle :install + end + end + + context "Gemfile uses gemspec paths after eval-ing a Gemfile" do + before { create_file "other/Gemfile-other" } + + 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("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 new file mode 100644 index 0000000000..e51fc9247d --- /dev/null +++ b/spec/bundler/install/gemfile/gemspec_spec.rb @@ -0,0 +1,737 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install from an existing gemspec" do + before(:each) do + 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("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 "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 + end + + it "that is hidden should install runtime and development dependencies" do + 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("foo", "foo.gemspec"), tmp("foo", ".gemspec") + + install_gemfile <<-G + 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 + end + + it "should handle a list of requirements" do + update_repo2 do + build_gem "baz", "1.0" + build_gem "baz", "1.1" + end + + 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 "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("foo"), gemspec: false) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' + G + 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("foo")) do |s| + s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby) + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gemspec :path => '#{tmp("foo")}' + G + expect(err).to match(/There are multiple gemspecs at #{tmp("foo")}/) + end + + it "should pick a specific gemspec" do + 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 "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 + end + + it "should use a specific group for development dependencies" do + 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 "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 + end + + it "should match a lockfile even if the gemspec defines development dependencies" do + 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", rake_version + 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("foo")) do |s| + s.add_dependency "myrack" + end + + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' + G + + 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" do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack" + s.add_development_dependency "thin" + end + + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}' + G + + bundle "install", verbose: true + + 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", :jruby_only do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "platform_specific" + end + + system_gems "platform_specific-1.0-java", path: default_bundle_path + + install_gemfile <<-G + gemspec :path => '#{tmp("foo")}' + G + + 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("foo")) + File.open(tmp("foo/foo.gemspec"), "w") do |s| + s.write "raise 'ahh' unless Dir.pwd == '#{tmp("foo")}'" + end + + install_gemfile <<-G, raise_on_error: false + gemspec :path => '#{tmp("foo")}' + G + expect(stdboth).not_to include("ahh") + end + + it "allows the gemspec to activate other gems" do + 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[myrack-1.0.0 myrack-0.9.1 myrack-obama-1.0] + + 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 'myrack-obama'; require 'myrack/obama' }" + end + + install_gemfile <<-G + source "https://gem.repo1" + gemspec + G + + expect(the_bundle).to include_gem "foo 1.0" + end + + it "allows conflicts" do + 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_bundle: true do |s| + s.add_dependency "foo", "= 0.0.1" + end + build_gem "foo", "0.0.1", to_bundle: true + + install_gemfile <<-G + source "https://gem.repo2" + gem "deps" + 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" do + build_lib("foo", path: tmp("foo")) do |s| + s.version = "1.0.0" + s.add_dependency "bar", "= 1.0.0" + end + 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 "https://gem.repo2" + gem "deps" + 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.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| + s.add_dependency "activesupport", ">= 1" + end + + 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| + s.add_dependency "activesupport", ">= 1.0.1" + end + + bundle_config "deployment true" + bundle :install, raise_on_error: false + + expect(err).to include("changed") + end + end + end + + 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 "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 "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 "https://gem.repo1" + gemspec + G + + 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" } + + before do + build_lib("foo", path: tmp("foo")) do |s| + s.add_dependency "myrack", "=1.0.0" + end + + gemfile <<-G + source "#{source_uri}" + gemspec :path => "../foo" + G + + checksums = checksums_section_when_enabled do |c| + c.no_checksum "foo", "1.0" + end + + 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 + + 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 + + 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 + + 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 + build_repo2 do + build_gem "indirect_platform_specific" do |s| + s.add_runtime_dependency "platform_specific" + end + end + + build_lib "foo", path: bundled_app do |s| + case platform_specific_type + when :runtime + s.add_runtime_dependency dependency + when :development + s.add_development_dependency dependency + else + raise ArgumentError, "wrong dependency type #{platform_specific_type}, can only be :development or :runtime" + 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 + bundle_config "force_ruby_platform true" + bundle :install + end + + context "as a runtime dependency" do + 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) + platform_specific + + GEM + 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} + L + end + end + + context "as a development dependency" do + let(:platform_specific_type) { :development } + + 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: 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} + L + end + end + + context "with an indirect platform-specific development dependency" do + let(:platform_specific_type) { :development } + let(:dependency) { "indirect_platform_specific" } + + 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: 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} + L + end + end + end + end + end + + context "with multiple platforms" do + before do + build_lib("foo", path: tmp("foo")) do |s| + s.version = "1.0.0" + 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 + bundle_config "force_ruby_platform true" + + install_gemfile <<-G + source "https://gem.repo1" + gemspec :path => '#{tmp("foo")}', :name => 'foo' + G + + 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` configured" do + bundle_config "force_ruby_platform true" + + 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 "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 new file mode 100644 index 0000000000..b2a82caf01 --- /dev/null +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -0,0 +1,1745 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + 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 + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + 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 + 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 + foospec = s.to_ruby.gsub(/s\.files.*/, 's.files = `git ls-files -z`.split("\x0")') + s.write "foo.gemspec", foospec + end + + bundle "update foo" + + 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" + + install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2 + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + run <<-RUBY + require 'foo' + puts "fail" if defined?(FOO_PREV_REF) + RUBY + + 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 + + it "complains if pinned specs don't exist in the git repo" do + build_git "foo" + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}" + G + + 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", :jruby_only do + build_git "only_java" do |s| + s.platform = "java" + end + + 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(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", :jruby_only do + build_git "only_java", "1.0" do |s| + s.platform = "java" + end + + build_git "only_java", "1.1" do |s| + s.platform = "java" + s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec") + end + + 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(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_config "path vendor/bundle" + install_base_gemfile + + FileUtils.mv bundled_app, tmp("bundled_app.bck") + + 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_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") + + gemfile tmp("bundled_app.bck/Gemfile"), <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + + gem "myrack", "1.0" + G + + bundle "update foo", dir: tmp("bundled_app.bck") + + expect(the_bundle).to include_gems "foo 1.1", "myrack 1.0", dir: tmp("bundled_app.bck") + end + end + + describe "with an empty git block" do + before do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + + git "#{lib_path("foo-1.0")}" do + # this page left intentionally blank + end + G + end + + it "does not explode" do + bundle "install" + expect(the_bundle).to include_gems "myrack 1.0" + end + end + + describe "when specifying a revision" do + before(:each) do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + end + + 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' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + 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 be_empty + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + 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 } + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying a tag" do + let(:tag) { "tag" } + let(:repo) { build_git("foo").path } + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + + 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 + G + + expect(the_bundle).to include_gems("foo 1.0") + end + end + end + + describe "when specifying local override" do + it "uses the local repository instead of checking a new one out" do + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + 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 + + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "chooses the local repository on runtime" do + build_git "myrack", "0.8" + + 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.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")}) + 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 "myrack", "0.8" + + 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.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 "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 + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + 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 set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "updates ref on install" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "explodes and gives correct solution if given path does not exist on install" do + build_git "myrack", "0.8" + + 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(/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(err).to be_empty + end + + 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 "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + 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(err).to be_empty + end + + it "does not explode if disable_local_branch_check is given" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + 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 "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + 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 "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(/is using branch another but Gemfile specifies main/) + end + + it "explodes on invalid revision on install" do + 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 "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 %(config set disable_local_revision_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + end + + describe "specified inline" do + # TODO: Figure out how to write this test so that it is not flaky depending + # on the current network situation. + # it "supports private git URLs" do + # gemfile <<-G + # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" + # G + # + # bundle :install + # + # # p out + # # p err + # puts err unless err.empty? # This spec fails randomly every so often + # err.should include("notthere.fallingsnow.net") + # err.should include("ssh") + # end + + it "installs from git even if a newer gem is available elsewhere" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + 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 "myrack-1.0.0" + + 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 "myrack", "= 1.0" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + G + + run "require 'myrack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "correctly unlocks when changing to a git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + build_git "myrack", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" + G + + 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 "https://gem.repo1" + gem "myrack" + G + + build_git "myrack", "1.2", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + G + + 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") + + install_gemfile <<-G + source "https://gem.repo1" + path "#{lib_path("hi2u")}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "uses a ref if specified" do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "correctly handles cases with invalid gemspecs" do + build_git "foo" do |s| + s.summary = nil + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + 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| + s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/version' + Gem::Specification.new do |s| + s.name = 'bar' + s.author = 'no one' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + end + G + end + + build_git "foo", path: lib_path("foo") do |s| + s.write "bin/foo", "" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("foo")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "bar 1.0" + 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| + 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 + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fakes the gem out if there is no gemspec" do + build_git "foo", gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("rails 2.3.2") + end + + 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, raise_on_error: false + + 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") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo space-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "handles repos that have been force-pushed" do + build_git "forced", "1.0" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("forced-1.0")}" do + gem 'forced' + end + G + expect(the_bundle).to include_gems "forced 1.0" + + update_git "forced" do |s| + s.write "lib/forced.rb", "FORCED = '1.1'" + end + + bundle "update", all: true + expect(the_bundle).to include_gems "forced 1.1" + + git("reset --hard HEAD^", lib_path("forced-1.0")) + + 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 + 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, raise_on_error: false + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + 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 + 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 + G + + 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 + G + + update_git "foo" + 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 + G + + run <<-RUBY + require 'foo' + puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' + RUBY + + expect(out).to eq("WIN") + end + + 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_r(lib_path("foo-1.0")) + + bundle "install" + expect(out).not_to match(/updating/i) + end + + it "doesn't blow up if bundle install is run twice in a row" do + build_git "foo" + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install" + bundle "install" + 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, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + 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_git "foo", path: lib_path("nested") + build_git "bar", path: lib_path("nested") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + gem "bar", :git => "#{lib_path("nested")}" + G + + 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| + 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| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :path => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "doesn't explode when switching Gem to Git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0" + G + + build_git "myrack", "1.0" do |s| + s.write "lib/new_file.rb", "puts 'USING GIT'" + end + + install_gemfile <<-G + 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 + it "installs" do + build_git "valim" + + install_gemfile <<-G + 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")) + + old_lockfile = File.read(bundled_app_lock) + lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) + + bundle "install" + + run <<-R + require "valim" + puts VALIM_PREV_REF + R + + expect(out).to eq(old_revision) + end + + it "gives a helpful error message when the remote ref no longer exists" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" + G + expect(out).to_not match(/Revision.*does not exist/) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + 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 with deployment mode configured and git sources" do + it "works" do + build_git "valim", path: lib_path("valim") + + install_gemfile <<-G + source "https://gem.repo1" + gem "valim", "= 1.0", :git => "#{lib_path("valim")}" + G + + pristine_system_gems + + bundle_config "deployment true" + bundle :install + end + end + + describe "gem install hooks" 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 + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + 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 + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + 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 + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + 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" 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) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + 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 + expect(out).to eq("YES") + + run <<-R + puts $:.grep(/ext/) + R + 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" do + git_reader = build_git "foo", no_default: true do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + create_makefile("foo") + RUBY + s.write "ext/foo.c", "void Init_foo() {}" + end + + 2.times do |i| + 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("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) + + git_commit_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" + G + + run <<-R + require 'foo' + puts foo + R + + expect(out).to eq(i.to_s) + end + end + + it "does not prompt to gem install if extension fails" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + raise + end + RUBY + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(err).to end_with(<<-M.strip) +An error occurred while installing foo (1.0), and Bundler cannot continue. + +In Gemfile: + foo + M + expect(out).not_to include("gem install foo") + end + + 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", __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 + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + 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 + build_git "xxxxxx" do |s| + s.executables = "xxxxxxbar" + end + + Bundler::SharedHelpers.with_clean_git_env do + ENV["GIT_DIR"] = "bar" + ENV["GIT_WORK_TREE"] = "bar" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("xxxxxx-1.0")}" do + gem 'xxxxxx' + end + G + + 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 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", all: true, 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 "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_config_global "path vendor/bundle" + bundle :install + pristine_system_gems + + bundle "install", env: { "PATH" => "" } + expect(out).to_not include("You need to install git to be able to use gems from git repositories.") + end + end + + describe "when the git source is overridden with a local git repo" do + before do + bundle_config_global "local.foo #{lib_path("foo")}" + end + + describe "and git output is colorized" do + before do + File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| + f.write("[color]\n\tui = always\n") + end + end + + it "installs successfully" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + context "git sources that include credentials" do + context "that are username and password" do + let(:credentials) { "user1:password1" } + + it "does not display the password" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "https://#{credentials}@github.com/company/private-repo" do + gem "foo" + end + G + + expect(stdboth).to_not include("password1") + expect(out).to include("Fetching https://user1@github.com/company/private-repo") + end + end + + context "that is an oauth token" do + let(:credentials) { "oauth_token" } + + it "displays the oauth scheme but not the oauth token" do + 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 + + expect(stdboth).to_not include("oauth_token") + expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") + end + end + end +end diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb new file mode 100644 index 0000000000..4013b112ec --- /dev/null +++ b/spec/bundler/install/gemfile/groups_spec.rb @@ -0,0 +1,345 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with groups" do + describe "installing with no options" do + before :each do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + group :emo do + gem "activesupport", "2.3.5" + end + gem "thin", :groups => [:emo] + G + end + + it "installs gems in the default group" do + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "installs gems in a group block into that group" do + expect(the_bundle).to include_gems "activesupport 2.3.5" + + load_error_run <<-R, "activesupport", :default + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(err_without_deprecations).to match(/cannot load such file -- activesupport/) + end + + it "installs gems with inline :groups into those groups" do + expect(the_bundle).to include_gems "thin 1.0" + + load_error_run <<-R, "thin", :default + require 'thin' + puts THIN + R + + 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 'myrack'; puts MYRACK") + expect(output).to eq("1.0.0") + + output = run("require 'activesupport'; puts ACTIVESUPPORT") + expect(output).to eq("2.3.5") + + output = run("require 'thin'; puts THIN") + expect(output).to eq("1.0") + end + + it "removes old groups when new groups are set up" do + load_error_run <<-RUBY, "thin", :emo + Bundler.setup(:default) + require 'thin' + puts THIN + RUBY + + expect(err_without_deprecations).to match(/cannot load such file -- thin/) + end + + it "sets up old groups when they have previously been removed" do + output = run <<-RUBY, :emo + Bundler.setup(:default) + Bundler.setup(:default, :emo) + require 'thin'; puts THIN + RUBY + expect(output).to eq("1.0") + end + end + + describe "without option" do + describe "with gems assigned to a single group" do + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + group :emo do + gem "activesupport", "2.3.5" + end + group :debugging, :optional => true do + gem "thin" + end + G + end + + it "installs gems in the default group" do + bundle_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default] + end + + 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 "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", groups: [:default] + end + + it "does not say it installed gems from the excluded group" do + bundle_config "without emo" + bundle :install + expect(out).not_to include("activesupport") + end + + it "allows Bundler.setup for specific groups" do + 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 "https://gem.repo1" + gem "activesupport" + group :emo do + gem "rails", "2.3.2" + end + G + + 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 + ENV["BUNDLE_WITHOUT"] = "emo" + + bundle :install + expect(out).not_to include("activesupport") + + 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 "does not install gems from the optional group" do + bundle :install + expect(the_bundle).not_to include_gems "thin 1.0" + end + + 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 "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 "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do + ENV["BUNDLE_WITH"] = "debugging" + + bundle :install + expect(the_bundle).to include_gem "thin 1.0" + + ENV["BUNDLE_WITHOUT"] = "debugging" + expect(the_bundle).to include_gem "thin 1.0" + + bundle :install + expect(the_bundle).to include_gem "thin 1.0" + end + + 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 "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 + + describe "with gems assigned to multiple groups" do + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + group :emo, :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + it "installs gems in the default group" do + 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_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 "https://gem.repo1" + gem "myrack" + + group :emo do + gem "activesupport", "2.3.5" + end + + group :lolercoaster do + gem "activesupport", "2.3.5" + end + G + end + + 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" + + bundle_config "without lolercoaster" + bundle :install + expect(the_bundle).to include_gems "activesupport 2.3.5" + + bundle_config "without emo lolercoaster" + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + + bundle "config set --local without 'emo lolercoaster'" + bundle :install + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + end + end + + describe "nesting groups" do + before :each do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + group :emo do + group :lolercoaster do + gem "activesupport", "2.3.5" + end + end + G + end + + it "installs gems in the default group" do + 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_config "without emo" + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5" + end + end + end + + describe "when loading only the default group" do + it "should not load all groups" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "activesupport", :groups => :development + G + + ruby <<-R + require "bundler" + Bundler.setup :default + Bundler.require :default + puts MYRACK + begin + require "activesupport" + rescue LoadError + puts "no activesupport" + end + R + + expect(out).to include("1.0") + expect(out).to include("no activesupport") + end + end + + describe "when locked and installed with `without` setting" do + before(:each) do + build_repo2 + + 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 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 "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_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_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 new file mode 100644 index 0000000000..b069488531 --- /dev/null +++ b/spec/bundler/install/gemfile/path_spec.rb @@ -0,0 +1,1017 @@ +# frozen_string_literal: true + +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")}" do + gem 'foo' + end + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports pinned paths" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "supports relative paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(bundled_app) + + install_gemfile <<-G + gem 'foo', :path => "#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "expands paths" do + build_lib "foo" + + relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("~").expand_path) + + install_gemfile <<-G + gem 'foo', :path => "~/#{relative_path}" + G + + expect(the_bundle).to include_gems("foo 1.0") + 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, raise_on_error: false + gem 'foo', :path => "~#{username}/#{relative_path}" + G + 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") + + install_gemfile <<-G + gem 'foo', :path => "./foo-1.0" + G + + 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") + + install_gemfile <<-G + gem 'foo', :path => File.expand_path("foo-1.0", __dir__) + G + + bundle_config "frozen true" + bundle :install + end + + it "installs dependencies from the path even if a newer gem is available elsewhere" do + system_gems "myrack-1.0.0" + + 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 "myrack", "= 1.0" + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("nested")}" + G + + run "require 'myrack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "works" do + 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| + s.add_dependency "foo" + end + + build_lib "foo", "1.0.0", path: lib_path("omg/foo") + + install_gemfile <<-G + gem "omg", :path => "#{lib_path("omg")}" + G + + 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| + s.write "gemfiles/ruby187.gemspec", <<-G + Gem::Specification.new do |s| + s.name = 'premailer' + s.version = '1.0.0' + s.summary = 'Hi' + s.authors = 'Me' + end + G + end + + install_gemfile <<-G + gem "premailer", :path => "#{lib_path("premailer")}" + G + + # Installation of the 'gemfiles' gemspec would fail since it will be unable + # to require 'premailer.rb' + expect(the_bundle).to include_gems "premailer 1.0.0" + end + + it "warns on invalid specs" do + build_lib "foo" + + gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s + File.open(gemspec, "w") do |f| + f.write <<-G + Gem::Specification.new do |s| + s.name = "foo" + end + G + end + + install_gemfile <<-G, raise_on_error: false + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + 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 "myrack", "1.0" + end + + gemfile lib_path("foo/Gemfile"), <<-G + source "https://gem.repo1" + gemspec + G + + 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 + + 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 "myrack", "1.0" + end + + install_gemfile <<-G + 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 "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 "myrack", ">= 1.0" + end + + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" + gemspec + G + + build_gem "myrack", "1.0.1", to_system: true + + 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 + + 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 "myrack", ">= 1.0" + s.add_development_dependency "activesupport" + end + + install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo") + source "https://gem.repo1" + gemspec + G + + build_gem "myrack", "1.0.1", to_system: true + + 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 + + it "raises if there are multiple gemspecs" do + 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, raise_on_error: false + gemspec :path => "#{lib_path("foo")}" + G + + 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| + s.write "bar.gemspec" + end + + install_gemfile <<-G + gemspec :path => "#{lib_path("foo")}", :name => "foo" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "sets up executables" do + build_lib "foo" do |s| + s.executables = "foobar" + end + + 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" + expect(out).to eq("1.0") + end + + it "handles directories in bin/" do + build_lib "foo" + 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 be_empty + end + + it "removes the .gem file after installing" do + build_lib "foo" + + install_gemfile <<-G + gem 'foo', :path => "#{lib_path("foo-1.0")}" + G + + expect(lib_path("foo-1.0").join("foo-1.0.gem")).not_to exist + end + + describe "block syntax" do + it "pulls all gems from a path block" do + build_lib "omg" + build_lib "hi2u" + + install_gemfile <<-G + path "#{lib_path}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + 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| + s.write "lib/foo.rb", "puts 'FAIL'" + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + gem "omg", :path => "#{lib_path("omg")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec" do + build_lib "foo", gemspec: false + + gemfile <<-G + gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "works when the path does not have a gemspec but there is a lockfile" do + lockfile <<~L + PATH + remote: vendor/bar + specs: + L + + FileUtils.mkdir_p(bundled_app("vendor/bar")) + + install_gemfile <<-G + gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard" + G + end + + context "existing lockfile" do + it "rubygems gems don't re-resolve without changes" do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack-obama', '1.0' + gem 'net-ssh', '1.0' + G + + bundle :check, env: { "DEBUG" => "1" } + expect(out).to match(/using resolution from the lockfile/) + 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 "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| + s.add_dependency "yard" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack-obama', :path => "#{lib_path("omg")}" + gem 'net-ssh', :path => "#{lib_path("omg")}" + G + + bundle :check, env: { "DEBUG" => "1" } + expect(out).to match(/using resolution from the lockfile/) + expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0" + end + end + + it "installs executable stubs" do + build_lib "foo" do |s| + s.executables = ["foo"] + end + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo-1.0")}" + G + + bundle "exec foo" + expect(out).to eq("1.0") + end + + 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| + s.add_dependency "bar" + end + build_lib "bar", "1.0", path: lib_path("foo/bar") + + install_gemfile <<-G + gem "foo", :path => "#{lib_path("foo")}" + G + end + + it "unlocks all gems when the top level gem is updated" do + build_lib "foo", "2.0", path: lib_path("foo") do |s| + s.add_dependency "bar" + end + + bundle "install" + + expect(the_bundle).to include_gems "foo 2.0", "bar 1.0" + end + + it "unlocks all gems when a child dependency gem is updated" do + build_lib "bar", "2.0", path: lib_path("foo/bar") + + bundle "install" + + expect(the_bundle).to include_gems "foo 1.0", "bar 2.0" + end + end + + describe "when dependencies in the path are updated" do + before :each do + build_lib "foo", "1.0", path: lib_path("foo") + + install_gemfile <<-G + 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 "myrack" + end + + bundle "install" + + 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| + 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| + s.add_dependency "foo" + end + + install_gemfile <<-G + gem "bar", :git => "#{lib_path("bar")}" + G + + install_gemfile <<-G + gem "bar", :path => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + 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_bundle: true do |s| + s.write "lib/bar.rb", "raise 'fail'" + end + + install_gemfile <<-G + 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") + + install_gemfile <<-G + source "https://gem.repo1" + path "#{lib_path("foo")}" do + gem "foo" + gem "bar" + end + G + + expect(the_bundle).to include_gems "bar 1.0" + end + end + + 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 lib_path("private_lib/Gemfile"), <<-G + source "http://localgemserver.test" + gemspec + gem 'myrack' + G + 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 + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + 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 + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + 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 + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + 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 new file mode 100644 index 0000000000..e12933ebcf --- /dev/null +++ b/spec/bundler/install/gemfile/platform_spec.rb @@ -0,0 +1,638 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install across platforms" do + it "maintains the same lockfile if all gems are compatible across platforms" do + lockfile <<-G + GEM + remote: https://gem.repo1/ + specs: + myrack (0.9.1) + + PLATFORMS + #{not_local} + + DEPENDENCIES + myrack + G + + install_gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "pulls in the correct platform specific gem" do + lockfile <<-G + GEM + remote: https://gem.repo1 + specs: + platform_specific (1.0) + platform_specific (1.0-java) + platform_specific (1.0-x86-mswin32) + + PLATFORMS + ruby + + DEPENDENCIES + platform_specific + G + + 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 "https://gem.repo1" + + gem "platform_specific" + G + + expect(the_bundle).to include_gems "platform_specific 1.0 ruby" + expect(err).to be_empty + end + + 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 + + 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) + + PLATFORMS + ruby + + DEPENDENCIES + darwin_single_arch + G + + simulate_platform "universal-darwin-21" do + simulate_ruby_platform "universal.x86_64-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 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 with gems that have different dependencies" do + simulate_platform "java" do + install_gemfile <<-G + source "https://gem.repo1" + + gem "nokogiri" + G + + 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" + + simulate_new_machine + bundle "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3" + end + end + + 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 + + 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 + end + end + + 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 + + install_gemfile <<-G + source "https://gem.repo2" + G + + install_gemfile <<-G + source "https://gem.repo2" + + gem "image_optim_pack" + G + + expect(err).not_to include "Unable to use the platform-specific" + + 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 "https://gem.repo1" + + gem "myrack", "1.0.0" + G + + bundle_config "path vendor/bundle" + bundle :install + + FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8")) + + 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 "https://gem.repo1" + + platforms :#{local_tag} do + gem "nokogiri" + end + G + + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + platforms :#{not_local_tag} do + gem "nokogiri" + end + G + + 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 "https://gem.repo1" + gem "nokogiri", :platforms => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "does not install gems tagged w/ another platforms inline" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "nokogiri", :platforms => :#{not_local_tag} + G + 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 "https://gem.repo1" + gem "nokogiri", :platform => :#{local_tag} + G + expect(the_bundle).to include_gems "nokogiri 1.4.2" + end + + it "doesn't install gems tagged w/ another platform inline" do + install_gemfile <<-G + source "https://gem.repo1" + gem "nokogiri", :platform => :#{not_local_tag} + G + expect(the_bundle).not_to include_gems "nokogiri 1.4.2" + end + + it "does not blow up on sources with all platform-excluded specs" 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 :list + end + + it "does not attempt to install gems from :rbx when using --local" do + bundle_config "force_ruby_platform true" + + gemfile <<-G + source "https://gem.repo1" + gem "some_gem", :platform => :rbx + G + + bundle "install --local" + expect(out).not_to match(/Could not find gem 'some_gem/) + end + + it "does not attempt to install gems from other rubies when using --local" do + bundle_config "force_ruby_platform true" + gemfile <<-G + 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 "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 "https://gem.repo1" + + gem "myrack", :platform => [:windows, :jruby] + G + + bundle "install" + + expect(err).to be_empty + + 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 "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 + + 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 new file mode 100644 index 0000000000..d937abd714 --- /dev/null +++ b/spec/bundler/install/gemfile/ruby_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +RSpec.describe "ruby requirement" do + def locked_ruby_version + Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(File.read(bundled_app_lock)).ruby_version) + end + + # 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 "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + ruby "#{Gem.ruby_version}" + gem "myrack" + gem "myrack-obama" + G + + expect(the_bundle).to include_gems "myrack-obama 1.0" + end + + it "allows removing the ruby version requirement" do + install_gemfile <<-G + source "https://gem.repo1" + ruby "~> #{Gem.ruby_version}" + gem "myrack" + G + + expect(lockfile).to include("RUBY VERSION") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + 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 "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) + + install_gemfile <<-G + source "https://gem.repo1" + ruby ">= #{Gem.ruby_version}" + gem "myrack" + G + + 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 "https://gem.repo1" + ruby ">= 1.0.0" + gem "myrack" + G + + 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 + + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + + install_gemfile <<-G + source "https://gem.repo1" + ruby ">= #{current_ruby_minor}" + gem "myrack" + G + + 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 "https://gem.repo1" + ruby "#{Gem.ruby_version}\\n \t\\n" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "fails gracefully with malformed requirements" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + ruby ">= 0", "-.\\0" + gem "myrack" + G + + 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 new file mode 100644 index 0000000000..654d638e1f --- /dev/null +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -0,0 +1,1313 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with gems on multiple sources" do + # repo1 is built automatically before all of the specs run + # it contains myrack-obama 1.0.0 and myrack 0.9.1 & 1.0.0 amongst other gems + + 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 + + build_gem "myrack-obama" do |s| + s.add_dependency "myrack" + end + end + + gemfile <<-G + 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 + end + + 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 "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 "with sources set by an option" 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 + + 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 + end + + 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 + + context "when a pinned gem has an indirect dependency in the pinned source" do + before do + 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 "https://gem.repo2" + source "https://gem.repo3" do + gem "depends_on_myrack" + end + G + end + + 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 + + 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(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") + + # 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(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 "when a pinned gem has an indirect dependency in a different source" do + before do + # 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 "https://gem.repo2" + + gem "private_gem_1" + + source "https://gem.repo3" do + gem "private_gem_2" + end + G + end + + 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 "when a top-level gem has an indirect dependency" do + before do + 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 "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 + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" do |s| + s.write "lib/myrack.rb", "MYRACK = 'FAIL'" + end + end + end + + 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 + 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(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 + + build_gem "depends_on_myrack", "1.0.1" do |s| + s.add_dependency "myrack" + end + end + + gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo3" do + gem "depends_on_depends_on_myrack" + end + G + end + + 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 "and the dependency is only in a pinned source" do + before do + build_repo2 + + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" + end + end + + 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 + + update_repo gem_repo3 do + build_gem "myrack", "1.0.0" + end + 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" + + build_gem "webpacker", "5.2.1" do |s| + s.add_dependency "activesupport", ">= 5.2" + end + end + + gemfile <<-G + source "https://gem.repo2" + + gemspec :path => "#{lib_path("rails")}" + + gem "webpacker", "~> 5.0" + G + end + + 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 + + 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 + + update_repo gem_repo2 do + build_gem "nokogiri", "1.11.1" do |s| + s.add_dependency "racca", "~> 1.4" + end + + build_gem "racca", "1.5.2" + end + + gemfile <<-G + source "https://gem.repo2" + + 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_repo3 do + build_gem "not_in_repo1", "1.0.0" + end + + 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 + expect(err).to include("Could not find gem 'not_in_repo1'") + end + end + + context "with an existing lockfile" do + before do + system_gems "myrack-0.9.1", "myrack-1.0.0", path: default_bundle_path + + lockfile <<-L + GEM + remote: https://gem.repo1 + specs: + + GEM + remote: https://gem.repo3 + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack! + L + + gemfile <<-G + source "https://gem.repo1" + source "https://gem.repo3" do + gem 'myrack' + end + G + end + + # 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("myrack 0.9.1") + end + end + + context "with a path gem in the same Gemfile" do + before do + build_lib "foo" + + gemfile <<-G + 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, artifice: "compact_index" + + bundle %(exec ruby -e 'puts "OK"') + + expect(out).to include("OK") + end + end + end + + context "when an older version of the same gem also ships with Ruby" do + before do + system_gems "myrack-0.9.1" + + 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 + 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 + # With these gems, + build_repo4 do + build_gem "foo", "0.1" + build_gem "bar", "0.1" + end + + # Installing this gemfile... + gemfile <<-G + source 'https://gem.repo1' + gem 'myrack' + gem 'foo', '~> 0.1', :source => 'https://gem.repo4' + gem 'bar', '~> 0.1', :source => 'https://gem.repo4' + G + + bundle_config "path ../gems/system" + bundle :install, artifice: "compact_index" + + # 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 + # 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 + + # 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 do + build_gem "myrack" + end + + build_lib "path1" + build_lib "path2" + build_git "git1" + build_git "git2" + + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + gem "rails" + + source "https://gem.repo3" do + gem "myrack" + end + + gem "path1", :path => "#{lib_path("path1-1.0")}" + gem "path2", :path => "#{lib_path("path2-1.0")}" + gem "git1", :git => "#{lib_path("git1-1.0")}" + gem "git2", :git => "#{lib_path("git2-1.0")}" + G + end + + it "does not re-resolve" do + 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 + end + end + + context "when a gem is installed to system gems" do + before do + 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 + build_repo4 do + build_gem "myrack", "2.0.1.1.forked" + build_gem "thor", "0.19.1.1.forked" + end + + # When this gemfile is installed... + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" + gem "thor" + end + gem "myrack-obama" + G + + # Then we change the Gemfile by adding a version to thor + gemfile <<-G + source "https://gem.repo1" + + source "https://gem.repo4" do + gem "myrack", "2.0.1.1.forked" + gem "thor", "0.19.1.1.forked" + end + gem "myrack-obama" + G + + # 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 new file mode 100644 index 0000000000..97b1d233bf --- /dev/null +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -0,0 +1,1973 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with specific platforms" do + let(:google_protobuf) { <<-G } + source "https://gem.repo2" + gem "google-protobuf" + G + + 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 + + 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("sass-embedded", "1.72.0") do |s| + s.platform = "x86_64-darwin-15" + end + + build_gem "rake" + end + + 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 + + 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) + + PLATFORMS + ruby + + 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") + 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 + + 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 "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 + + 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_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 include("CFPropertyList-1.0", + "facter-2.4.6-universal-darwin") + end + end + + 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 + + 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-mingw-ucrt + ]) + end + end + + 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_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 +end diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb new file mode 100644 index 0000000000..83875a3d0e --- /dev/null +++ b/spec/bundler/install/gemfile_spec.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with duplicated gems" do + it "will display a warning" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + + gem 'rails', '~> 4.0.0' + gem 'rails', '~> 4.0.0' + G + expect(err).to include("more than once") + end + end + + context "with --gemfile" do + it "finds the gemfile" do + gemfile bundled_app("NotGemfile"), <<-G + 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") + + ENV["BUNDLE_GEMFILE"] = "NotGemfile" + 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 "https://gem.repo1" + gem 'myrack' + G + + bundle_config "gemfile #{bundled_app("NotGemfile")}" + end + it "uses the gemfile to install" do + bundle "install" + bundle "list" + + expect(out).to include("myrack (1.0.0)") + end + it "uses the gemfile while in a subdirectory" do + bundled_app("subdir").mkpath + bundle "install", dir: bundled_app("subdir") + bundle "list", dir: bundled_app("subdir") + + expect(out).to include("myrack (1.0.0)") + end + end + + 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 "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? + + 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", :jruby_only do + it "does not raise any error parsing Gemfile" do + 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 + 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 new file mode 100644 index 0000000000..9db73b84b5 --- /dev/null +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -0,0 +1,1012 @@ +# frozen_string_literal: true + +RSpec.describe "compact index api" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + 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 + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + 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 + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + 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", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle case sensitivity conflicts" do + build_repo4(build_compact_index: false) do + build_gem "myrack", "1.0" do |s| + s.add_dependency("Myrack", "0.1") + end + build_gem "Myrack", "0.1" + end + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "#{source_uri}" + 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('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 + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + 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 mode" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + bundle :install, 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 "myrack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + 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 mode" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle :install, 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 mode" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle "install", artifice: "compact_index" + bundle_config "deployment true" + bundle :install, artifice: "compact_index" + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + 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 versions endpoint has a checksum mismatch" do + gemfile <<-G + source "#{source_uri}" + 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("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 "shows proper path when permission errors happen", :permissions do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + 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"] = tmp("missing_home").to_s + + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "handles host redirects without Gem::Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + 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 "myrack 1.0.0" + end + + it "times out when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + 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 "myrack" + G + + bundle "install --full-index", artifice: "compact_index" + expect(out).to include("Fetching source index from #{source_uri}") + 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 "myrack" + G + + 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 "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_r Dir[gem_repo2("gems/foo-*.gem")] + end + + install_gemfile <<-G, artifice: "compact_index_extra", verbose: true + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + 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 "myrack", "1.0.0" + G + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.0.0" + + build_repo4 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "myrack", "1.2" + G + bundle :install, artifice: "compact_index_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" + end + + 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| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end + G + + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false + + expect(err).to include("Could not find compatible versions") + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + 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 when doing back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] + end + + install_gemfile <<-G, artifice: "compact_index_extra_missing" + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + 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}" + + gem 'foo' + G + + 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 mode" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + 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", 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 'myrack-obama' + G + + 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 'myrack_middleware' + G + + 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 = Gem::URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle :install, artifice: "compact_index_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "passes basic authentication details and strips out creds also in verbose mode" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle :install, verbose: true, artifice: "compact_index_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + 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 "myrack" + G + + 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 "myrack" + G + end + + it "reads authentication details by host name from bundle config" do + 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 "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 set #{source_uri}/ #{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 "myrack 1.0.0" + end + + it "should use the API" do + 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 "myrack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle "config set #{source_hostname} otheruser:wrong" + + 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", 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 set #{source_hostname} #{user}:wrong" + + 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 + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + 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" 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 + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it, and includes original error" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "myrack" + G + + 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. + gemfile <<-G + class Gem::Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "myrack" + G + + 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 + it "uses other sources declared in the Gemfile" do + File.open(home(".gemrc"), "w") do |file| + file.puts({ sources: ["https://rubygems.org"] }.to_yaml) + end + + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G + + bundle :install, artifice: "compact_index_forbidden" + ensure + FileUtils.rm_rf home(".gemrc") + end + end + end + + 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 'myrack', '0.9.1' + G + + # 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 'myrack', '1.0.0' + G + + # Second install should match etag + bundle :install, artifice: "compact_index_etag_match" + + 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 'myrack' + G + + # 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" + + 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, raise_on_error: false + source "htps://rubygems.org" + gem "myrack" + G + 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" 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", raise_on_error: false + source "#{source_uri}" + gem "myrack" + G + + 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_MYRACK_CHECKSUM" => "checksum!", "DEBUG" => "1" }, verbose: true, raise_on_error: false + source "#{source_uri}" + gem "myrack" + G + 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" + source "#{source_uri}" + gem "myrack" + G + end + end + + it "works when cache dir is world-writable" do + install_gemfile <<-G, artifice: "compact_index" + File.umask(0000) + source "#{source_uri}" + 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" }, raise_on_error: false + source "#{source_uri}" + gem "rails" + G + 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("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" + source "https://gem.repo1" + source "#{source_uri}" do + gem "rails" + gem "activemerchant" + end + G + 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 new file mode 100644 index 0000000000..32a1b98b6d --- /dev/null +++ b/spec/bundler/install/gems/dependency_api_spec.rb @@ -0,0 +1,656 @@ +# frozen_string_literal: true + +RSpec.describe "gemcutter's dependency API" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "should use the API" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "should URI encode gem names" do + gemfile <<-G + source "#{source_uri}" + gem " sinatra" + G + + 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 + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + + bundle :install, artifice: "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}/...") + expect(the_bundle).to include_gems( + "rails 2.3.2", + "actionpack 2.3.2", + "activerecord 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2" + ) + end + + it "should handle multiple gem dependencies on the same gem" do + gemfile <<-G + source "#{source_uri}" + gem "net-sftp" + G + + bundle :install, artifice: "endpoint" + expect(the_bundle).to include_gems "net-sftp 1.1.1" + end + + it "should use the endpoint when using deployment mode" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + bundle :install, 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 "myrack 1.0.0" + end + + it "handles git dependencies that are in rubygems" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + 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 mode" do + build_git "foo" do |s| + s.executables = "foobar" + s.add_dependency "rails", "2.3.2" + end + + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle :install, 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 mode" do + build_git "foo" + gemfile <<-G + source "#{source_uri}" + gem 'foo', :git => "#{lib_path("foo-1.0")}" + G + + bundle "install", artifice: "endpoint" + bundle_config "deployment true" + bundle :install, artifice: "endpoint" + + expect(the_bundle).to include_gems("foo 1.0") + end + + it "falls back when the API errors out" do + 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 + + 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 + gemfile <<-G + source "#{source_uri}" + gem "activesupport" + gem "actionpack" + gem "actionmailer" + gem "activeresource" + gem "thin" + gem "myrack" + gem "rails" + G + bundle :install, artifice: "endpoint_fallback" + expect(out).to include("Fetching source index from #{source_uri}") + + expect(the_bundle).to include_gems( + "activesupport 2.3.2", + "actionpack 2.3.2", + "actionmailer 2.3.2", + "activeresource 2.3.2", + "activesupport 2.3.2", + "thin 1.0.0", + "myrack 1.0.0", + "rails 2.3.2" + ) + end + + it "falls back when Gemcutter API doesn't return proper Marshal format" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + 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 "myrack 1.0.0" + end + + it "falls back when the API URL returns 403 Forbidden" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, verbose: true, artifice: "endpoint_api_forbidden" + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "handles host redirects" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + bundle :install, artifice: "endpoint_host_redirect" + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "handles host redirects without Gem::Net::HTTP::Persistent" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + FileUtils.mkdir_p lib_path + File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h| + h.write <<-H + module Kernel + alias require_without_disabled_net_http require + def require(*args) + raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty? + require_without_disabled_net_http(*args) + end + end + H + end + + 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 "myrack 1.0.0" + end + + it "timeouts when Bundler::Fetcher redirects too much" do + gemfile <<-G + source "#{source_uri}" + gem "myrack" + G + + 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 "myrack" + G + + bundle "install --full-index", artifice: "endpoint" + expect(out).to include("Fetching source index from #{source_uri}") + 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 "myrack" + G + + bundle "update --full-index", artifice: "endpoint", all: true + expect(out).to include("Fetching source index from #{source_uri}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + 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_r Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + 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", "foo 1.0" + end + + it "fetches gem versions even when those gems are already installed" do + gemfile <<-G + source "#{source_uri}" + gem "myrack", "1.0.0" + G + bundle :install, artifice: "endpoint_extra_api" + + build_repo4 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + end + + gemfile <<-G + source "#{source_uri}" do; end + source "#{source_uri}/extra" + gem "myrack", "1.2" + G + bundle :install, artifice: "endpoint_extra_api" + expect(the_bundle).to include_gems "myrack 1.2" + end + + 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| + s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1 + end + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem 'somegem', '1.0.0' + end + G + + bundle :install, artifice: "compact_index_extra_api", raise_on_error: false + + expect(err).to include("Could not find compatible versions") + end + + it "prints API output properly with back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + bundle :install, artifice: "endpoint_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 when doing back deps" do + build_repo2 do + build_gem "back_deps" do |s| + s.add_dependency "foo" + end + build_gem "missing" + + FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")] + end + + install_gemfile <<-G, artifice: "endpoint_extra_missing" + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + expect(the_bundle).to include_gems "back_deps 1.0" + end + + 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_r Dir[gem_repo2("gems/foo-*.gem")] + end + + gemfile <<-G + source "#{source_uri}" + source "#{source_uri}/extra" do + gem "back_deps" + end + G + + 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 fetch all marshaled specs" do + build_repo2 do + build_gem "foo", "1.0" + build_gem "foo", "2.0" + end + + install_gemfile <<-G, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, verbose: true + source "#{source_uri}" + + gem "foo" + G + + expect(out).to include("foo-2.0.gemspec.rz") + expect(out).not_to include("foo-1.0.gemspec.rz") + 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: "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 'myrack-obama' + G + + 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 'myrack_middleware' + G + + 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 = Gem::URI.parse(source_uri) + uri.user = user + uri.password = password + + uri + end + + it "passes basic authentication details and strips out creds" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle :install, artifice: "endpoint_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "passes basic authentication details and strips out creds also in verbose mode" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle :install, verbose: true, artifice: "endpoint_basic_authentication" + expect(out).not_to include("#{user}:#{password}") + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "strips http basic authentication creds for modern index" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + 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 it can't reach the server" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle :install, artifice: "endpoint_500", raise_on_error: false + expect(out).not_to include("#{user}:#{password}") + end + + it "does not pass the user / password to different hosts on redirect" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + 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 "myrack" + G + end + + it "reads authentication details by host name from bundle config" do + 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 "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 set #{source_uri}/ #{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 "myrack 1.0.0" + end + + it "should use the API" do + 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 "myrack 1.0.0" + end + + it "prefers auth supplied in the source uri" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + bundle "config set #{source_hostname} otheruser:wrong" + + 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", 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 set #{source_hostname} #{user}:wrong" + + bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false + expect(err).to include("Bad username or password") + end + end + + describe "with no password" do + let(:password) { nil } + + it "passes basic authentication details" do + gemfile <<-G + source "#{basic_auth_source_uri}" + gem "myrack" + G + + 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" 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 + # we can't just overwrite openssl.rb is that Artifice uses it. + bundled_app("broken_ssl").mkpath + bundled_app("broken_ssl/openssl.rb").open("w") do |f| + f.write <<-RUBY + raise LoadError, "cannot load such file -- openssl" + RUBY + end + end + + it "explains what to do to get it, and includes original error" do + gemfile <<-G + source "#{source_uri.gsub(/http/, "https")}" + gem "myrack" + G + + 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. + gemfile <<-G + class Gem::Net::HTTP + def start + raise OpenSSL::SSL::SSLError, "certificate verify failed" + end + end + + source "#{source_uri.gsub(/http/, "https")}" + gem "myrack" + G + + 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 + it "uses other sources declared in the Gemfile" do + File.open(home(".gemrc"), "w") do |file| + file.puts({ sources: ["https://rubygems.org"] }.to_yaml) + end + + begin + gemfile <<-G + source "#{source_uri}" + gem 'myrack' + G + + 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 new file mode 100644 index 0000000000..6d5aa456fe --- /dev/null +++ b/spec/bundler/install/gems/env_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + + env "BUNDLER_TEST" do + 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 "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 "myrack 1.0" + end + end + + describe "when just setting an ENV key as a symbol" do + before :each do + gemfile <<-G + source "https://gem.repo1" + + env :BUNDLER_TEST do + 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 "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 "myrack 1.0" + end + end + + describe "when setting a string to match the env" do + before :each do + gemfile <<-G + source "https://gem.repo1" + + env "BUNDLER_TEST" => "foo" do + 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 "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 "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 "myrack 1.0" + end + end + + describe "when setting a regex to match the env" do + before :each do + gemfile <<-G + source "https://gem.repo1" + + env "BUNDLER_TEST" => /foo/ do + 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 "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 "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 "myrack 1.0" + end + end +end diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb new file mode 100644 index 0000000000..a30b53d6ad --- /dev/null +++ b/spec/bundler/install/gems/flex_spec.rb @@ -0,0 +1,403 @@ +# frozen_string_literal: true + +RSpec.describe "bundle flex_install" do + it "installs the gems as expected" do + install_gemfile <<-G + source "https://gem.repo1" + gem 'myrack' + G + + 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 "https://gem.repo1" + gem 'myrack' + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).to be_locked + + gemfile <<-G + source "https://gem.repo1" + gem 'myrack', '1.0' + G + + bundle :install + expect(the_bundle).to include_gems "myrack 1.0.0" + expect(the_bundle).to be_locked + end + + it "keeps child dependencies at the same version" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack-obama" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" + + update_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack-obama", "1.0" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0" + end + + describe "adding new gems" do + it "installs added gems without updating previously installed gems" do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack' + G + + update_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack' + gem 'activesupport', '2.3.5' + G + + 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 "https://gem.repo2" + gem "myrack-obama" + G + + update_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack-obama" + gem "thin" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0", "thin 1.0" + end + end + + describe "removing gems" do + it "removes gems without changing the versions of remaining gems" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack' + G + + 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 "https://gem.repo2" + gem 'myrack' + gem 'activesupport', '2.3.2' + G + + 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 "https://gem.repo2" + gem 'myrack' + gem 'activesupport', '2.3.5' + G + + update_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack' + G + + expect(the_bundle).not_to include_gems "activesupport 2.3.5" + end + + it "removes child dependencies" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem 'myrack-obama' + gem 'activesupport' + G + + 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 "https://gem.repo2" + gem 'activesupport' + G + + expect(the_bundle).to include_gems "activesupport 2.3.5" + expect(the_bundle).not_to include_gems "myrack-obama", "myrack" + end + end + + describe "when running bundle install and Gemfile conflicts with lockfile" do + before(:each) do + build_repo2 + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack_middleware 1.0", "myrack 0.9.1" + + build_repo2 do + build_gem "myrack-obama", "2.0" do |s| + s.add_dependency "myrack", "=1.2" + end + build_gem "myrack_middleware", "2.0" do |s| + s.add_dependency "myrack", ">=1.0" + end + end + + gemfile <<-G + 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, raise_on_error: false + ruby <<-RUBY, raise_on_error: false + require 'bundler/setup' + RUBY + expect(err).to match(/could not find gem 'myrack-obama/i) + end + + it "discards the locked gems when the Gemfile requires different versions than the lock" do + bundle_config "force_ruby_platform true" + + nice_error = <<~E.strip + Could not find compatible versions + + 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 + + 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 "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 "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "myrack-obama" + G + end + + it "should work when you install" do + bundle "install" + + 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 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 + + describe "when adding a new source" do + it "updates the lockfile" do + build_repo2 + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + install_gemfile <<-G + source "https://gem.repo1" + source "https://gem.repo2" do + end + gem "myrack" + G + + 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) + + GEM + remote: https://gem.repo2/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + # This was written to test github issue #636 + describe "when a locked child dependency conflicts" do + before(:each) do + build_repo2 do + build_gem "capybara", "0.3.9" do |s| + s.add_dependency "myrack", ">= 1.0.0" + end + + build_gem "myrack", "1.1.0" + build_gem "rails", "3.0.0.rc4" do |s| + s.add_dependency "myrack", "~> 1.1.0" + end + + build_gem "myrack", "1.2.1" + build_gem "rails", "3.0.0" do |s| + s.add_dependency "myrack", "~> 1.2.1" + end + end + end + + it "resolves them" do + # install Rails 3.0.0.rc + install_gemfile <<-G + 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 "https://gem.repo2" + gem "rails", "3.0.0" + gem "capybara", "0.3.9" + G + 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 new file mode 100644 index 0000000000..e1fbeac454 --- /dev/null +++ b/spec/bundler/install/gems/mirror_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + + gem "myrack" + G + 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 gem metadata from https://gem.repo1") + expect(the_bundle).to include_gems "myrack 1.0" + end + end + + describe "when the gem source matches a configured mirror" do + before :each do + gemfile <<-G + # This source is bogus and doesn't have the gem we're looking for + source "https://gem.repo2" + + gem "myrack" + G + bundle_config "mirror.https://gem.repo2 https://gem.repo1" + end + + it "installs the gem from the mirror" do + 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 new file mode 100644 index 0000000000..d5b10d2c8f --- /dev/null +++ b/spec/bundler/install/gems/native_extensions_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +RSpec.describe "installing a gem with native extensions" do + it "installs" do + build_repo2 do + build_gem "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" + 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 + end + + gemfile <<-G + source "https://gem.repo2" + gem "c_extension" + G + + bundle_config "build.c_extension --with-c_extension=hello" + bundle "install" + + expect(out).to include("Installing c_extension 1.0 with native extensions") + + run "Bundler.require; puts CExtension.new.its_true" + expect(out).to eq("true") + end + + it "installs from git" 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" + 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" + + install_gemfile <<-G + source "https://gem.repo1" + gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump} + G + + expect(err).to_not include("warning: conflicting chdir during another chdir block") + + 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 new file mode 100644 index 0000000000..e49fd2a9a3 --- /dev/null +++ b/spec/bundler/install/gems/post_install_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +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 "https://gem.repo1" + gem 'myrack' + gem 'thin' + gem 'myrack-obama' + G + + bundle :install + 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 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 "https://gem.repo1" + gem "activesupport" + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when a dependency includes a post install message" do + it "should display the post install message" do + gemfile <<-G + source "https://gem.repo1" + gem 'myrack_middleware' + G + + bundle :install + expect(out).to include("Post-install message from myrack:") + expect(out).to include("Myrack's post install message") + end + end + end + + context "with git sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + end + + it "should display the post-install messages if repo is updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + bundle :install + + build_git "foo", "1.1" do |s| + s.post_install_message = "Foo's 1.1 post install message" + end + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => '#{lib_path("foo-1.1")}' + G + bundle :install + + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's 1.1 post install message") + end + + it "should not display the post-install messages if repo is not updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + build_git "foo" do |s| + s.post_install_message = nil + end + gemfile <<-G + source "https://gem.repo1" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).not_to include("Post-install message") + end + end + end + + context "when ignore post-install messages for gem is set" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle_config "ignore_messages.myrack true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end + + context "when ignore post-install messages for all gems" do + it "doesn't display any post-install messages" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle_config "ignore_messages true" + + bundle :install + expect(out).not_to include("Post-install message") + end + end +end diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb new file mode 100644 index 0000000000..111d361aab --- /dev/null +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -0,0 +1,786 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with install-time dependencies" 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 "https://gem.repo2" + gem "with_implicit_rake_dep" + gem "another_implicit_rake_dep" + gem "rake" + G + + 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 "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(Bundler.rubygems.inflate(File.binread(path))) + spec.dependencies.each do |d| + d.instance_variable_set(:@type, "fail") + end + File.open(path, "wb") do |f| + f.write Gem.deflate(Marshal.dump(spec)) + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem "actionpack", "2.3.2" + G + + 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 "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" 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" do + install_gemfile <<-G, env: { "DEBUG" => "1" } + source "https://gem.repo2" + gem "net_c" + gem "net_e" + G + + 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 "https://gem.repo2" + gem "net_c" + gem "net_e" + G + + bundle :install, env: { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" } + + expect(out).to include("Resolving dependencies...") + end + end + + context "with ENV['DEBUG_RESOLVER_TREE'] set" do + it "produces debug output" do + gemfile <<-G + source "https://gem.repo2" + gem "net_c" + gem "net_e" + G + + bundle :install, env: { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" } + + 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 + + describe "when a required ruby version" do + context "allows only an older version" do + it "installs the older version" 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 + 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: "endpoint" + 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 + + 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 + + context "allows no gems" do + before do + build_repo2 do + build_gem "require_ruby" do |s| + s.required_ruby_version = "> 9000" + end + end + end + + 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, 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 = <<~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 + end + + it_behaves_like "ruby version conflicts" + + describe "with a < requirement" do + let(:ruby_requirement) { %("< 5000") } + + it_behaves_like "ruby version conflicts" + end + + describe "with a compound requirement" do + let(:reqs) { ["> 0.1", "< 5000"] } + let(:ruby_requirement) { reqs.map(&:dump).join(", ") } + + it_behaves_like "ruby version conflicts" + end + end + end + + describe "when a required rubygems version disallows a gem" do + it "does not try to install those gems" do + build_repo2 do + build_gem "require_rubygems" do |s| + s.required_rubygems_version = "> 9000" + end + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo2" + gem 'require_rubygems' + G + + 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 new file mode 100644 index 0000000000..96a305bb76 --- /dev/null +++ b/spec/bundler/install/gems/standalone_spec.rb @@ -0,0 +1,524 @@ +# frozen_string_literal: true + +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 + + it "makes the gems available without bundler" 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 + 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 + 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") + require "bundler/setup" + + RUBY + expected_gems.each do |k, _| + testrb << "\nrequire \"#{k}\"" + testrb << "\nputs #{k.upcase}" + end + 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 + end + + describe "with simple gems" do + before do + 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 + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + 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 + 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" 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| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + lib = File.expand_path('lib/', __dir__) + $:.unshift lib unless $:.include?(lib) + require 'bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'Anonymous' + s.require_path = [1,2] + end + G + end + 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(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 + + describe "with a combination of gems and git repos" do + before do + build_git "devise", "1.0" + + 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 + { + "actionpack" => "2.3.2", + "devise" => "1.0", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + + describe "with groups" do + before do + build_git "devise", "1.0" + + gemfile <<-G + source "https://gem.repo1" + gem "rails" + + group :test do + gem "rspec" + gem "myrack-test" + end + G + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, dir: cwd + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + + it "allows creating a standalone file with limited groups" do + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: "default", dir: cwd + + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + + expect(out).to eq("2.3.2") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) + end + + 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 + + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + + expect(out).to eq("2.3.2") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) + end + + 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 + + ruby <<-RUBY + $:.unshift File.expand_path("path/to/bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + RUBY + + expect(out).to eq("2.3.2") + end + + 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 + + load_error_ruby <<-RUBY, "spec" + $:.unshift File.expand_path("bundle") + require "bundler/setup" + + require "actionpack" + puts ACTIONPACK + require "spec" + RUBY + + expect(out).to eq("2.3.2") + expect(err_without_deprecations).to match(/cannot load such file -- spec/) + end + end + + describe "with gemcutter's dependency API" do + let(:source_uri) { "http://localgemserver.test" } + + describe "simple gems" do + before do + gemfile <<-G + source "#{source_uri}" + gem "rails" + G + bundle_config "path #{bundled_app("bundle")}" + bundle :install, standalone: true, artifice: "endpoint", dir: cwd + end + + let(:expected_gems) do + { + "actionpack" => "2.3.2", + "rails" => "2.3.2", + } + end + + include_examples "common functionality" + end + end +end + +RSpec.describe "bundle install --standalone run in a subdirectory" do + let(:cwd) { bundled_app("bob").tap(&:mkpath) } + + before do + gemfile <<-G + source "https://gem.repo1" + gem "rails" + G + end + + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd + + expect(bundled_app("bundle/bundler/setup.rb")).to exist + end + + context "when path set to a relative path" do + before do + bundle_config "path bundle" + end + + it "generates the script in the proper place" do + bundle :install, standalone: true, dir: cwd + + expect(bundled_app("bundle/bundler/setup.rb")).to exist + end + end +end + +RSpec.describe "bundle install --standalone --local" do + before do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + system_gems "myrack-1.0.0", path: default_bundle_path + end + + 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/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb new file mode 100644 index 0000000000..be37673aa1 --- /dev/null +++ b/spec/bundler/install/gems/win32_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with win32-generated lockfile" do + it "should read lockfile" do + File.open(bundled_app_lock, "wb") do |f| + f << "GEM\r\n" + f << " remote: https://gem.repo1/\r\n" + f << " specs:\r\n" + f << "\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 << " myrack\r\n" + end + + install_gemfile <<-G + source "https://gem.repo1" + + gem "myrack" + G + end +end diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb new file mode 100644 index 0000000000..fb2271c830 --- /dev/null +++ b/spec/bundler/install/gemspecs_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +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 + end + end + + it "still installs correctly" do + gemfile <<-G + source "https://gem.repo2" + gem "yaml_spec" + G + bundle :install + expect(err).to be_empty + end + + it "still installs correctly when using path" do + 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 be_empty + end + end + + it "should use gemspecs in the system cache when available" do + gemfile <<-G + source "http://localgemserver.test" + gem 'myrack' + G + + 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 = "myrack" + s.version = "1.0.0" + 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 "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", + 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| + s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" + end + + 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 "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| + s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}" + end + + 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(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| + s.required_ruby_version = version + end + + install_gemfile <<-G, raise_on_error: false + ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby' + source "https://gem.repo1" + gemspec + G + + 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 new file mode 100644 index 0000000000..1172d661ae --- /dev/null +++ b/spec/bundler/install/git_spec.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +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") + + 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@#{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 + 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 + rev2 = revision_for(lib_path("foo"))[0..6] + update_git "foo", "3.0", path: lib_path("foo"), gemspec: true + + install_gemfile <<-G, verbose: true + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :ref => "main~2" + G + + 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 + + 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 "allows git repos that are missing but not being installed" do + revision = build_git("foo").ref_for("HEAD") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :group => :development + G + + lockfile <<-L + GIT + remote: #{lib_path("foo-1.0")} + revision: #{revision} + specs: + foo (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo! + L + + bundle_config "path vendor/bundle" + bundle_config "without development" + bundle :install + + expect(out).to include("Bundle complete!") + 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 new file mode 100644 index 0000000000..49360e511e --- /dev/null +++ b/spec/bundler/install/path_spec.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + describe "with path configured" do + before :each do + build_gem "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "puts 'FAIL'" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + 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 + + bundle_config "path #{dir.join("vendor/bundle")}" + bundle :install, dir: dir + expect(out).to include("installed into `./vendor/bundle`") + + FileUtils.rm_rf dir + end + + 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 "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 "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 "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 + + 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 "myrack", "1.0.0", to_system: true do |s| + s.write "lib/myrack.rb", "raise 'FAIL'" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + def set_bundle_path(type, location) + if type == :env + ENV["BUNDLE_PATH"] = location + elsif type == :global + bundle "config set path #{location}", "no-color" => nil + end + end + + [:env, :global].each do |type| + 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/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 ." do + set_bundle_path(type, ".") + bundle_config_global "disable_shared_gems true" + + bundle :install + + 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 the path" do + set_bundle_path(type, bundled_app("vendor").to_s) + + 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 + + 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 + bundle_config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s + + bundle :install + + 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_config "path ./vendor/bundle" + bundle :install + + 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 "myrack", "1.1.0", to_system: true + bundle_config "path ./vendor/bundle" + bundle :install + + 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" 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 "https://gem.repo1" + gem "very_simple_binary" + G + + 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" + + FileUtils.rm_rf vendored_gems("extensions") + + run "require 'very_simple_binary_c'", raise_on_error: false + expect(err).to include("Bundler::GemNotFound") + + 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" + end + end + + describe "to a file" do + before do + FileUtils.touch bundled_app("bundle") + end + + it "reports the file exists" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + 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/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb new file mode 100644 index 0000000000..9f764d127c --- /dev/null +++ b/spec/bundler/install/prereleases_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +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 "https://gem.repo2" + gem "not_released" + G + expect(the_bundle).to include_gems "not_released 1.0.pre" + end + + it "uses regular releases if available" do + install_gemfile <<-G + source "https://gem.repo2" + gem "has_prerelease" + G + expect(the_bundle).to include_gems "has_prerelease 1.0" + end + + it "uses prereleases if requested" do + install_gemfile <<-G + source "https://gem.repo2" + gem "has_prerelease", "1.1.pre" + G + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + describe "when prerelease gems are not available" do + it "still works" do + build_repo3 do + build_gem "myrack" + end + FileUtils.rm_r Dir[gem_repo3("prerelease*")] + + install_gemfile <<-G + source "https://gem.repo3" + gem "myrack" + G + + 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 new file mode 100644 index 0000000000..e7f64dc227 --- /dev/null +++ b/spec/bundler/install/security_policy_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "rubygems/security" + +# unfortunately, testing signed gems with a provided CA is extremely difficult +# as 'gem cert' is currently the only way to add CAs to the system. + +RSpec.describe "policies with unsigned gems" do + before do + build_security_repo + gemfile <<-G + 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", raise_on_error: false + bundle :install + 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", 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", raise_on_error: false + expect(err).to include("security policy didn't allow") + end + + 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" + end +end + +RSpec.describe "policies with signed gems and no CA" do + before do + build_security_repo + gemfile <<-G + 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", 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", 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(the_bundle).to include_gems "signed_gem 1.0" + end + + it "will succeed with no policy" do + bundle "install" + 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 new file mode 100644 index 0000000000..c92af7bfb0 --- /dev/null +++ b/spec/bundler/install/yanked_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +RSpec.context "when installing a bundle that includes yanked gems" do + it "throws an error when the original gem version is yanked" do + build_repo4 do + build_gem "foo", "9.0.0" + end + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + foo (10.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + foo (= 10.0.0) + + L + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo4" + gem "foo", "10.0.0" + G + + 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 + 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 + + 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 "https://gem.repo1" + gem "myrack", "0.9.1" + G + + lockfile <<-L + GEM + remote: https://gem.repo1 + specs: + myrack (0.9.1) + + PLATFORMS + #{lockfile_platforms} + + 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, raise_on_error: false + + 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 |
