Skip to content

Commit

Permalink
improve caching
Browse files Browse the repository at this point in the history
introduce a cache object to share things between syncing and check
  • Loading branch information
ccutrer committed Mar 20, 2024
1 parent 92a7606 commit 8d45782
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 155 deletions.
23 changes: 16 additions & 7 deletions lib/bundler/multilock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ def after_install_all(install: true)

default_root = Bundler.root

checker = Check.new
cache = Cache.new
checker = Check.new(cache)
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|
# we already wrote the default lockfile
Expand All @@ -170,7 +172,7 @@ def after_install_all(install: true)
Bundler.settings.temporary(frozen: true) do
Bundler.ui.silence do
up_to_date = checker.base_check(lockfile_definition, check_missing_deps: true) &&
checker.check(lockfile_definition)
checker.deep_check(lockfile_definition)
end
end
if up_to_date
Expand All @@ -195,12 +197,11 @@ def after_install_all(install: true)

parent = lockfile_definition[:parent]
parent_root = parent.dirname
checker.load_lockfile(parent)
parent_specs = checker.lockfile_specs[parent]
parent_specs = cache.specs(parent)

# adjust locked paths from the parent lockfile to be relative to _this_ gemfile
adjusted_parent_lockfile_contents =
checker.lockfile_contents[parent].gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
cache.contents(parent).gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
remote_path = Pathname.new($1)
next remote if remote_path.absolute?

Expand All @@ -222,8 +223,13 @@ def after_install_all(install: true)

if lockfile_definition[:lockfile].exist?
# if the lockfile already exists, "merge" it together
parent_lockfile = LockfileParser.new(adjusted_parent_lockfile_contents)
lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
parent_lockfile = if adjusted_parent_lockfile_contents == cache.contents(lockfile_definition[:lockfile])
cache.parser(parent)
else
local_parser_cache[adjusted_parent_lockfile_contents] ||=
LockfileParser.new(adjusted_parent_lockfile_contents)
end
lockfile = cache.parser(lockfile_definition[:lockfile])

dependency_changes = false
# replace any duplicate specs with what's in the default lockfile
Expand Down Expand Up @@ -251,6 +257,8 @@ def after_install_all(install: true)
end

new_contents = LockfileGenerator.generate(lockfile)
# require "debug"
# debugger
else
# no lockfile? just start out with the parent lockfile's contents to inherit its
# locked gems
Expand All @@ -269,6 +277,7 @@ def after_install_all(install: true)
dependency_changes: dependency_changes,
unlocking_bundler: unlocking_bundler)
end
cache.invalidate_lockfile(lockfile_definition[:lockfile]) if had_changes

# if we had changes, bundler may have updated some common
# dependencies beyond the default lockfile, so re-run it
Expand Down
89 changes: 89 additions & 0 deletions lib/bundler/multilock/cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

module Bundler
module Multilock
# caches lockfiles across multiple lockfile checks or sync runs
class Cache
def initialize
@contents = {}
@parsers = {}
@specs = {}
@reverse_dependencies = {}
@base_checks = {}
@deep_checks = {}
@missing_specs = Set.new
@logged_missing = false
end

# Removes a given lockfile's associated cached data
#
# Should be called if the lockfile is modified
# @param lockfile_name [Pathname]
# @return [void]
def invalidate_lockfile(lockfile_name)
@contents.delete(lockfile_name)
@parsers.delete(lockfile_name)
@specs.delete(lockfile_name)
@reverse_dependencies.delete(lockfile_name)
@base_checks.delete(lockfile_name)
@deep_checks.delete(lockfile_name)
end

# @param lockfile_name [Pathname]
# @return [String] the raw contents of the lockfile
def contents(lockfile_name)
@contents[lockfile_name] ||= lockfile_name.read.freeze
end

# @param lockfile_name [Pathname]
# @return [LockfileParser]
def parser(lockfile_name)
@parsers[lockfile_name] ||= LockfileParser.new(contents(lockfile_name))
end

def specs(lockfile_name)
@specs[lockfile_name] ||= parser(lockfile_name).specs.to_h do |spec|
[[spec.name, spec.platform], spec]
end
end

# @param lockfile_name [Pathname]
# @return [Hash<String, Gem::Requirement>] hash of gem name to requirement for that gem
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

reverse_dependencies
end
end

def log_missing_spec(spec)
return if @missing_specs.include?(spec)

Bundler.ui.error "The following gems are missing" if @missing_specs.empty?
@missing_specs << spec
Bundler.ui.error(" * #{spec.name} (#{spec.version})")
end

def base_check(lockfile_name)
@base_checks.fetch(lockfile_name) { @base_checks[lockfile_name] = yield }
end

def deep_check(lockfile_name)
@deep_checks.fetch(lockfile_name) { @deep_checks[lockfile_name] = yield }
end
end
end
end
Loading

0 comments on commit 8d45782

Please sign in to comment.