From fc8147ad75671f1ab07ca59a4c6dd56a4e9f5011 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 28 Mar 2024 17:24:25 -0600 Subject: [PATCH] fix syncing of ruby version and auto-infer ruby version if the parent lockfile constrains it, but a child lockfile does not --- lib/bundler/multilock.rb | 23 +++++++++++++--- lib/bundler/multilock/check.rb | 13 ++++++--- lib/bundler/multilock/ext/dsl.rb | 10 +++++++ spec/bundler/multilock_spec.rb | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/lib/bundler/multilock.rb b/lib/bundler/multilock.rb index af1509e..77160f1 100644 --- a/lib/bundler/multilock.rb +++ b/lib/bundler/multilock.rb @@ -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) @@ -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" @@ -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) @@ -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 @@ -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 diff --git a/lib/bundler/multilock/check.rb b/lib/bundler/multilock/check.rb index c046526..2bc7b86 100644 --- a/lib/bundler/multilock/check.rb +++ b/lib/bundler/multilock/check.rb @@ -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) @@ -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? @@ -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 || ""}) 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] diff --git a/lib/bundler/multilock/ext/dsl.rb b/lib/bundler/multilock/ext/dsl.rb index 0235cf2..c83a231 100644 --- a/lib/bundler/multilock/ext/dsl.rb +++ b/lib/bundler/multilock/ext/dsl.rb @@ -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 diff --git a/spec/bundler/multilock_spec.rb b/spec/bundler/multilock_spec.rb index a2b2be3..bbd757d 100644 --- a/spec/bundler/multilock_spec.rb +++ b/spec/bundler/multilock_spec.rb @@ -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 \(\) 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) @@ -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