Skip to content

Commit

Permalink
fix syncing of ruby version
Browse files Browse the repository at this point in the history
and auto-infer ruby version if the parent lockfile constrains it,
but a child lockfile does not
  • Loading branch information
ccutrer committed Mar 29, 2024
1 parent d218f9e commit fc8147a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 7 deletions.
23 changes: 19 additions & 4 deletions lib/bundler/multilock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def after_install_all(install: true)
p2 != "ruby" && p1 != p2 && MatchPlatform.platforms_match?(p2, p1)
end
end
lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version)
lockfile.instance_variable_set(:@ruby_version, parent_lockfile.ruby_version) if lockfile.ruby_version
unless lockfile.bundler_version == parent_lockfile.bundler_version
unlocking_bundler = parent_lockfile.bundler_version
lockfile.instance_variable_set(:@bundler_version, parent_lockfile.bundler_version)
Expand Down Expand Up @@ -341,7 +341,7 @@ def loaded!
raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]

# Gemfile.lock isn't explicitly specified, otherwise it would be active
default_lockfile_definition = lockfile_definitions[Bundler.default_lockfile(force_original: true)]
default_lockfile_definition = self.default_lockfile_definition
return unless default_lockfile_definition && default_lockfile_definition[:active] == false

raise GemfileEvalError, "No lockfiles marked as active"
Expand Down Expand Up @@ -410,6 +410,11 @@ def reset!
@loaded = false
end

# @!visibility private
def default_lockfile_definition
lockfile_definitions[Bundler.default_lockfile(force_original: true)]
end

private

def expand_lockfile(lockfile)
Expand Down Expand Up @@ -449,6 +454,12 @@ def write_lockfile(lockfile_definition,
builder = Dsl.new
builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
builder.eval_gemfile(gemfile)
if !builder.instance_variable_get(:@ruby_version) &&
(parent_lockfile = lockfile_definition[:parent]) &&
(parent_lockfile_definition = lockfile_definitions[parent_lockfile]) &&
(parent_ruby_version_requirement = parent_lockfile_definition[:ruby_version_requirement])
builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
end

definition = builder.to_definition(lockfile, { bundler: unlocking_bundler })
definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
Expand Down Expand Up @@ -506,11 +517,15 @@ def write_lockfile(lockfile_definition,
end
SharedHelpers.capture_filesystem_access do
definition.instance_variable_set(:@resolved_bundler_version, unlocking_bundler) if unlocking_bundler

# need to force it to _not_ preserve unknown sections, so that it
# will overwrite the ruby version
definition.instance_variable_set(:@unlocking_bundler, true)
if Bundler.gem_version >= Gem::Version.new("2.5.6")
definition.instance_variable_set(:@lockfile, lockfile_definition[:lockfile])
definition.lock(true)
definition.lock
else
definition.lock(lockfile_definition[:lockfile], true)
definition.lock(lockfile_definition[:lockfile])
end
end
ensure
Expand Down
13 changes: 10 additions & 3 deletions lib/bundler/multilock/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ def run(skip_base_checks: false)

success = true
unless skip_base_checks
base_check({ gemfile: Bundler.default_gemfile,
lockfile: Bundler.default_lockfile(force_original: true) })
default_lockfile_definition = Multilock.default_lockfile_definition
default_lockfile_definition ||= { gemfile: Bundler.default_gemfile,
lockfile: Bundler.default_lockfile(force_original: true) }
base_check(default_lockfile_definition)
end
Multilock.lockfile_definitions.each do |lockfile_name, lockfile_definition|
next if lockfile_name == Bundler.default_lockfile(force_original: true)
Expand Down Expand Up @@ -68,7 +70,7 @@ def base_check(lockfile_definition, check_missing_deps: false)
end
end

next false unless not_installed.empty? && definition.no_resolve_needed?
next false unless not_installed.empty?

# cache a sentinel so that we can share a cache regardless of the check_missing_deps argument
next :missing_deps unless (definition.locked_gems.dependencies.values - definition.dependencies).empty?
Expand Down Expand Up @@ -105,6 +107,11 @@ def deep_check(lockfile_definition)
"does not match the parent lockfile's version (@#{parent_parser.bundler_version}).")
success = false
end
unless parser.ruby_version == parent_parser.ruby_version
Bundler.ui.error("ruby (#{parser.ruby_version || "<none>"}) in #{lockfile_path} " \
"does not match the parent lockfile's version (#{parent_parser.ruby_version}).")
success = false
end

# look through top-level explicit dependencies for pinned requirements
if lockfile_definition[:enforce_pinned_additional_dependencies]
Expand Down
10 changes: 10 additions & 0 deletions lib/bundler/multilock/ext/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ module ClassMethods

# Significant changes:
# * evaluate the prepare block as part of the gemfile
# * keep track of the ruby version set in the default gemfile
# * apply that ruby version to alternate lockfiles if they didn't set one
# themselves
# * mark Multilock as loaded once the main gemfile is evaluated
# so that they're not loaded multiple times
def evaluate(gemfile, lockfile, unlock)
builder = new
builder.eval_gemfile(gemfile, &Multilock.prepare_block) if Multilock.prepare_block
builder.eval_gemfile(gemfile)
if (ruby_version_requirement = builder.instance_variable_get(:@ruby_version))
Multilock.lockfile_definitions[lockfile][:ruby_version_requirement] = ruby_version_requirement
elsif (parent_lockfile = Multilock.lockfile_definitions.dig(lockfile, :parent)) &&
(parent_lockfile_definition = Multilock.lockfile_definitions[parent_lockfile]) &&
(parent_ruby_version_requirement = parent_lockfile_definition[:ruby_version_requirement])
builder.instance_variable_set(:@ruby_version, parent_ruby_version_requirement)
end
Multilock.loaded!
builder.to_definition(lockfile, unlock)
end
Expand Down
46 changes: 46 additions & 0 deletions spec/bundler/multilock_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,44 @@
end
end

it "syncs ruby version" do
with_gemfile(<<~RUBY) do
gem "concurrent-ruby", "1.2.2"
lockfile do
ruby ">= 2.1"
end
lockfile "alt" do
end
RUBY
invoke_bundler("install")

expect(File.read("Gemfile.lock")).to include(Bundler::RubyVersion.system.to_s)
expect(File.read("Gemfile.alt.lock")).to include(Bundler::RubyVersion.system.to_s)

update_lockfile_ruby("Gemfile.alt.lock", "ruby 2.1.0p0")

expect do
invoke_bundler("check")
end.to raise_error(/ruby \(ruby 2.1.0p0\) in Gemfile.alt.lock does not match the parent lockfile's version/)

update_lockfile_ruby("Gemfile.alt.lock", nil)
expect do
invoke_bundler("check")
end.to raise_error(/ruby \(<none>\) in Gemfile.alt.lock does not match the parent lockfile's version/)

invoke_bundler("install")
expect(File.read("Gemfile.alt.lock")).to include(Bundler::RubyVersion.system.to_s)

update_lockfile_ruby("Gemfile.lock", "ruby 2.6.0p0")
update_lockfile_ruby("Gemfile.alt.lock", nil)

invoke_bundler("install")
expect(File.read("Gemfile.alt.lock")).to include("ruby 2.6.0p0")
end
end

private

def create_local_gem(name, content = "", subdirectory: true)
Expand Down Expand Up @@ -960,4 +998,12 @@ def update_lockfile_bundler(lockfile, version)

File.write(lockfile, new_contents)
end

def update_lockfile_ruby(lockfile, version)
old_contents = File.read(lockfile)
new_version = version ? "RUBY VERSION\n #{version}\n\n" : ""
new_contents = old_contents.gsub(/RUBY VERSION\n #{Bundler::RubyVersion::PATTERN}\n\n/o, new_version)

File.write(lockfile, new_contents)
end
end

0 comments on commit fc8147a

Please sign in to comment.