Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More fixes #30

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 77 additions & 27 deletions lib/bundler/multilock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,24 @@ def add_lockfile(lockfile = nil,
# allow short-form lockfile names
lockfile = expand_lockfile(lockfile)

if lockfile_definitions.find { |definition| definition[:lockfile] == lockfile }
raise ArgumentError, "Lockfile #{lockfile} is already defined"
end
raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.key?(lockfile)

env_lockfile = lockfile if active && ENV["BUNDLE_LOCKFILE"] == "active"
env_lockfile ||= ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) }
active = env_lockfile == lockfile if env_lockfile

if active && (old_active = lockfile_definitions.find { |definition| definition[:active] })
if active && (old_active = lockfile_definitions.each_value.find { |definition| definition[:active] })
raise ArgumentError, "Only one lockfile (#{old_active[:lockfile]}) can be flagged as active"
end

parent = expand_lockfile(parent)
if parent != Bundler.default_lockfile(force_original: true) &&
!lockfile_definitions.find { |definition| definition[:lockfile] == parent } &&
!lockfile_definitions.key?(parent) &&
!parent.exist?
raise ArgumentError, "Parent lockfile #{parent} is not defined"
end

lockfile_definitions << (lockfile_def = {
lockfile_definitions[lockfile] = (lockfile_def = {
gemfile: (gemfile && Bundler.root.join(gemfile).expand_path) || Bundler.default_gemfile,
lockfile: lockfile,
active: active,
Expand Down Expand Up @@ -150,6 +148,7 @@ def after_install_all(install: true)
Bundler.ui.debug("Syncing to alternate lockfiles")

attempts = 1
previous_contents = Set.new

default_root = Bundler.root

Expand All @@ -158,8 +157,7 @@ def after_install_all(install: true)
synced_any = false
local_parser_cache = {}
Bundler.settings.temporary(cache_all_platforms: true, suppress_install_using_messages: true) do
lockfile_definitions.each do |lockfile_definition|
lockfile_name = lockfile_definition[:lockfile]
lockfile_definitions.each do |lockfile_name, lockfile_definition|
# we already wrote the default lockfile
next if lockfile_name == Bundler.default_lockfile(force_original: true)

Expand All @@ -168,6 +166,17 @@ def after_install_all(install: true)

relative_lockfile = lockfile_name.relative_path_from(Dir.pwd)

# prevent infinite loops of tick-tocking back and forth between two versions
current_contents = cache.contents(lockfile_name)
if previous_contents.include?(current_contents)
Bundler.ui.debug("Unable to converge on a single solution for #{lockfile_name}; " \
"perhaps there are conflicting requirements?")
attempts = 1
previous_contents.clear
next
end
previous_contents << current_contents

# already up to date?
up_to_date = false
Bundler.settings.temporary(frozen: true) do
Expand All @@ -178,6 +187,7 @@ def after_install_all(install: true)
end
if up_to_date
attempts = 1
previous_contents.clear
next
end

Expand Down Expand Up @@ -234,31 +244,57 @@ def after_install_all(install: true)
lockfile = cache.parser(lockfile_name)

dependency_changes = false
# replace any duplicate specs with what's in the default lockfile

spec_precedences = {}

check_precedence = lambda do |spec, parent_spec|
next :parent if spec.nil?
next :self if parent_spec.nil?
next spec_precedences[spec.name] if spec_precedences.key?(spec.name)

precedence = :self if cache.conflicting_requirements?(lockfile_name,
parent_lockfile_name,
spec,
parent_spec)

# look through all reverse dependencies; if any of them say it
# has to come from self, due to conflicts, then this gem has
# to come from self as well
[cache.reverse_dependencies(lockfile_name),
cache.reverse_dependencies(parent_lockfile_name)].each do |reverse_dependencies|
break if precedence == :self

reverse_dependencies[spec.name].each do |dep_name|
precedence = check_precedence.call(specs[dep_name], parent_specs[dep_name])
break if precedence == :self
end
end

spec_precedences[spec.name] = precedence || :parent
end

# replace any duplicate specs with what's in the parent lockfile
lockfile.specs.map! do |spec|
parent_spec = parent_specs[[spec.name, spec.platform]]
next spec unless parent_spec

# they're conflicting on purpose; don't inherit from the parent lockfile
next spec if cache.conflicting_requirements?(lockfile_name, parent_lockfile_name, spec, parent_spec)
next spec if check_precedence.call(spec, parent_spec) == :self

dependency_changes ||= spec != parent_spec
parent_spec
end

missing_specs = parent_specs.each_value.reject do |parent_spec|
specs.include?([parent_spec.name, parent_spec.platform])
new_spec = parent_spec.dup
new_spec.source = spec.source
new_spec
end
lockfile.specs.replace(missing_specs + lockfile.specs) unless missing_specs.empty?
lockfile.sources.replace(parent_lockfile.sources + lockfile.sources).uniq!

lockfile.platforms.replace(parent_lockfile.platforms).uniq!
# prune more specific platforms
lockfile.platforms.delete_if do |p1|
lockfile.platforms.any? do |p2|
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 @@ -291,12 +327,13 @@ def after_install_all(install: true)
# once to reset them back to the default lockfile's version.
# if it's already good, the `check` check at the beginning of
# the loop will skip the second sync anyway.
if had_changes && attempts < 2
if had_changes
attempts += 1
Bundler.ui.debug("Re-running sync to #{relative_lockfile} to reset common dependencies")
redo
else
attempts = 1
previous_contents.clear
end
end
end
Expand All @@ -316,7 +353,7 @@ def loaded!
@loaded = true
return if lockfile_definitions.empty?

return unless lockfile_definitions.none? { |definition| definition[:active] }
return unless lockfile_definitions.each_value.none? { |definition| definition[:active] }

if ENV["BUNDLE_LOCKFILE"]&.then { |l| expand_lockfile(l) } ==
Bundler.default_lockfile(force_original: true)
Expand All @@ -326,9 +363,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.find do |definition|
definition[:lockfile] == Bundler.default_lockfile(force_original: true)
end
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 @@ -393,10 +428,15 @@ def inject_preamble

# @!visibility private
def reset!
@lockfile_definitions = []
@lockfile_definitions = {}
@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 @@ -436,6 +476,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 All @@ -462,7 +508,7 @@ def write_lockfile(lockfile_definition,
Installer.install(gemfile.dirname, current_definition, {})
end
end
rescue RubyVersionMismatch, GemNotFound, SolveFailure
rescue RubyVersionMismatch, GemNotFound, SolveFailure, InstallError, ProductionError
# ignore
end
end
Expand Down Expand Up @@ -493,11 +539,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
65 changes: 43 additions & 22 deletions lib/bundler/multilock/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def initialize
@parsers = {}
@specs = {}
@reverse_dependencies = {}
@reverse_requirements = {}
@base_checks = {}
@deep_checks = {}
@base_check_messages = {}
Expand All @@ -29,6 +30,7 @@ def invalidate_lockfile(lockfile_name)
@parsers.delete(lockfile_name)
@specs.delete(lockfile_name)
@reverse_dependencies.delete(lockfile_name)
@reverse_requirements.delete(lockfile_name)
invalidate_checks(lockfile_name)
end

Expand All @@ -43,7 +45,9 @@ def invalidate_checks(lockfile_name)
# @param lockfile_name [Pathname]
# @return [String] the raw contents of the lockfile
def contents(lockfile_name)
@contents[lockfile_name] ||= lockfile_name.read.freeze
@contents.fetch(lockfile_name) do
@contents[lockfile_name] = lockfile_name.file? && lockfile_name.read.freeze
end
end

# @param lockfile_name [Pathname]
Expand All @@ -59,33 +63,25 @@ def specs(lockfile_name)
end

# @param lockfile_name [Pathname]
# @return [Hash<String, Gem::Requirement>] hash of gem name to requirement for that gem
# @return [Hash<String, Set<String>>] hash of gem name to set of gem names that depend on it
def reverse_dependencies(lockfile_name)
@reverse_dependencies[lockfile_name] ||= begin
# can use Gem::Requirement.default_prelease when Ruby 2.6 support is dropped
reverse_dependencies = Hash.new { |h, k| h[k] = Gem::Requirement.new(">= 0.a") }

lockfile = parser(lockfile_name)

lockfile.dependencies.each_value do |spec|
reverse_dependencies[spec.name].requirements.concat(spec.requirement.requirements)
end
lockfile.specs.each do |spec|
spec.dependencies.each do |dependency|
reverse_dependencies[dependency.name].requirements.concat(dependency.requirement.requirements)
end
end
ensure_reverse_data(lockfile_name)
@reverse_dependencies[lockfile_name]
end

reverse_dependencies
end
# @param lockfile_name [Pathname]
# @return [Hash<String, Gem::Requirement>] hash of gem name to requirement for that gem
def reverse_requirements(lockfile_name)
ensure_reverse_data(lockfile_name)
@reverse_requirements[lockfile_name]
end

def conflicting_requirements?(lockfile1_name, lockfile2_name, spec1, spec2)
reverse_dependencies1 = reverse_dependencies(lockfile1_name)[spec1.name]
reverse_dependencies2 = reverse_dependencies(lockfile2_name)[spec1.name]
reverse_requirements1 = reverse_requirements(lockfile1_name)[spec1.name]
reverse_requirements2 = reverse_requirements(lockfile2_name)[spec1.name]

!reverse_dependencies1.satisfied_by?(spec2.version) &&
!reverse_dependencies2.satisfied_by?(spec1.version)
!reverse_requirements1.satisfied_by?(spec2.version) &&
!reverse_requirements2.satisfied_by?(spec1.version)
end

def log_missing_spec(spec)
Expand Down Expand Up @@ -113,6 +109,31 @@ def #{type}_check(lockfile_name)
end
RUBY
end

private

def ensure_reverse_data(lockfile_name)
return if @reverse_requirements.key?(lockfile_name)

# can use Gem::Requirement.default_prelease when Ruby 2.6 support is dropped
reverse_requirements = Hash.new { |h, k| h[k] = Gem::Requirement.new(">= 0.a") }
reverse_dependencies = Hash.new { |h, k| h[k] = Set.new }

lockfile = parser(lockfile_name)

lockfile.dependencies.each_value do |dep|
reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
end
lockfile.specs.each do |spec|
spec.dependencies.each do |dep|
reverse_requirements[dep.name].requirements.concat(dep.requirement.requirements)
reverse_dependencies[dep.name] << spec.name
end
end

@reverse_requirements[lockfile_name] = reverse_requirements
@reverse_dependencies[lockfile_name] = reverse_dependencies
end
end
end
end
21 changes: 14 additions & 7 deletions lib/bundler/multilock/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ 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_definition|
next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
Multilock.lockfile_definitions.each do |lockfile_name, lockfile_definition|
next if lockfile_name == Bundler.default_lockfile(force_original: true)

unless lockfile_definition[:lockfile].exist?
Bundler.ui.error("Lockfile #{lockfile_definition[:lockfile]} does not exist.")
unless lockfile_name.exist?
Bundler.ui.error("Lockfile #{lockfile_name} does not exist.")
success = false
next
end
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
Loading
Loading