ruby/spec/bundler/install/gems/resolving_spec.rb
David Rodríguez 7285e76493
Fix truffleruby removing gems from lockfile
When resolving on truffleruby, and multiple platforms are included in
the lockfile, Bundler will not respect existing platforms, but always
force ruby variants. That means removal of existing version specific
variants, introducing lockfile churn between implementations.

To prevent this, we introduce the distinction between
`Dependency#force_ruby_platform`, only settable via Gemfile, and
`Dependency#default_force_ruby_platform`, which is always true on
truffleruby for certain dependency names. This way, when resolving
lockfile gems for other platforms on truffleruby, we keep platform
specific variants in the lockfile.

However, that introduces the problem that if only platform specific
variants are locked in the lockfile, Bundler won't be able to
materialize on truffleruby because the generic variant will be missing.
To fix this additional problem, we make sure the generic "ruby" platform
is always added when resolving on truffleruby.
2024-08-05 11:58:07 +09:00

727 lines
21 KiB
Ruby

# 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 or the lockfile (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 --verbose", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
end.not_to change { lockfile }
expect(err).to include("parallel_tests-3.8.0 requires ruby version >= #{next_ruby_minor}")
expect(err).not_to include("That means the author of parallel_tests (3.8.0) has removed it.")
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
parallel_tests
#{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 lock file 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 end_with(nice_error)
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
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