diff --git a/Dockerfile.jenkins-cache b/Dockerfile.jenkins-cache
index ccd01782f6d..050dcc01ec4 100644
--- a/Dockerfile.jenkins-cache
+++ b/Dockerfile.jenkins-cache
@@ -17,8 +17,8 @@ RUN --mount=target=/tmp/src \
gems/**/*.gemspec \
gems/**/gem_version.rb \
gems/**/version.rb \
- gems/bundler_lockfile_extensions \
gems/plugins/**/Gemfile.d \
+ vendor/gems/bundler-multilock \
\
| tee /tmp/dst/ruby-runner.tar | md5sum > /tmp/dst/ruby-runner.tar.md5 && \
\
diff --git a/Gemfile b/Gemfile
index 363d8379a7b..a158d618112 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,9 +11,19 @@
source "https://rubygems.org/"
-plugin "bundler_lockfile_extensions", path: "gems/bundler_lockfile_extensions"
+# cleanup local envs automatically from the old plugin
+Plugin.uninstall(["bundler_lockfile_extensions"], {}) if Plugin.installed?("bundler_lockfile_extensions")
-require File.expand_path("config/canvas_rails_switcher", __dir__)
+# vendored until https://github.com/rubygems/rubygems/pull/6957 is merged and released
+plugin "bundler-multilock", "1.0.7", path: "vendor/gems/bundler-multilock"
+# the extra check here is in case `bundle check` or `bundle exec` gets run before `bundle install`,
+# and is also fixed by the same PR
+raise GemNotFound, "bundler-multilock plugin is not installed" if !is_a?(Bundler::Plugin::DSL) && !Plugin.installed?("bundler-multilock")
+return unless Plugin.installed?("bundler-multilock")
+
+Plugin.send(:load_plugin, "bundler-multilock")
+
+require_relative "config/canvas_rails_switcher"
# Bundler evaluates this from a non-global context for plugins, so we have
# to explicitly pop up to set global constants
@@ -22,47 +32,29 @@ require File.expand_path("config/canvas_rails_switcher", __dir__)
# will already be defined during the second Gemfile evaluation
::CANVAS_INCLUDE_PLUGINS = true unless defined?(::CANVAS_INCLUDE_PLUGINS)
-if Plugin.installed?("bundler_lockfile_extensions")
- Plugin.send(:load_plugin, "bundler_lockfile_extensions") unless defined?(BundlerLockfileExtensions)
+SUPPORTED_RAILS_VERSIONS.product([nil, true]).each do |rails_version, include_plugins|
+ lockfile = ["rails#{rails_version.delete(".")}", include_plugins && "plugins"].compact.join(".")
+ lockfile = nil if rails_version == SUPPORTED_RAILS_VERSIONS.first && !include_plugins
- unless BundlerLockfileExtensions.enabled?
- default = true
- SUPPORTED_RAILS_VERSIONS.product([nil, true]).each do |rails_version, include_plugins|
- prepare = lambda do
- Object.send(:remove_const, :CANVAS_RAILS)
- ::CANVAS_RAILS = rails_version
- Object.send(:remove_const, :CANVAS_INCLUDE_PLUGINS)
- ::CANVAS_INCLUDE_PLUGINS = include_plugins
- end
+ default = rails_version == CANVAS_RAILS && !!include_plugins
- lockfile = ["Gemfile", "rails#{rails_version.delete(".")}", include_plugins && "plugins", "lock"].compact.join(".")
- lockfile = nil if default
- # only the first lockfile is the default
- default = false
-
- current = rails_version == CANVAS_RAILS && include_plugins
-
- add_lockfile(lockfile,
- current:,
- prepare:,
- allow_mismatched_dependencies: rails_version != SUPPORTED_RAILS_VERSIONS.first,
- enforce_pinned_additional_dependencies: include_plugins)
- end
-
- Dir["Gemfile.d/*.lock", "gems/*/Gemfile.lock", base: Bundler.root].each do |gem_lockfile_name|
- return unless add_lockfile(gem_lockfile_name,
- gemfile: gem_lockfile_name.sub(/\.lock$/, ""),
- allow_mismatched_dependencies: false)
- end
+ lockfile(lockfile,
+ default:,
+ allow_mismatched_dependencies: rails_version != SUPPORTED_RAILS_VERSIONS.first,
+ enforce_pinned_additional_dependencies: include_plugins) do
+ Object.send(:remove_const, :CANVAS_RAILS)
+ ::CANVAS_RAILS = rails_version
+ Object.send(:remove_const, :CANVAS_INCLUDE_PLUGINS)
+ ::CANVAS_INCLUDE_PLUGINS = include_plugins
end
end
-# rubocop:enable Style/RedundantConstantBase
-# Bundler's first pass parses the entire Gemfile and calls to additional sources
-# makes it actually go and retrieve metadata from them even though the plugin will
-# never exist there. Short-circuit it here if we're in the plugin-specific DSL
-# phase to prevent that from happening.
-return if method(:source).owner == Bundler::Plugin::DSL
+Dir["Gemfile.d/*.lock", "gems/*/Gemfile.lock", base: Bundler.root].each do |gem_lockfile_name|
+ return unless lockfile(gem_lockfile_name,
+ gemfile: gem_lockfile_name.sub(/\.lock$/, ""),
+ allow_mismatched_dependencies: false)
+end
+# rubocop:enable Style/RedundantConstantBase
module PreferGlobalRubyGemsSource
def rubygems_sources
diff --git a/build/new-jenkins/library/vars/filesChangedStage.groovy b/build/new-jenkins/library/vars/filesChangedStage.groovy
index e8d0aecdcf3..0b4b87f3a69 100644
--- a/build/new-jenkins/library/vars/filesChangedStage.groovy
+++ b/build/new-jenkins/library/vars/filesChangedStage.groovy
@@ -72,7 +72,7 @@ def preBuild(stageConfig) {
stageConfig.value('addedOrDeletedSpecFiles', sh(script: 'git diff --name-only --diff-filter=AD HEAD^..HEAD | grep "_spec.rb"', returnStatus: true) == 0)
dir(env.LOCAL_WORKDIR) {
- stageConfig.value('bundleFiles', sh(script: 'git diff --name-only HEAD^..HEAD | grep -E "Gemfile|gemspec|bundler_lockfile_extensions"', returnStatus: true) == 0)
+ stageConfig.value('bundleFiles', sh(script: 'git diff --name-only HEAD^..HEAD | grep -E "Gemfile|gemspec"', returnStatus: true) == 0)
stageConfig.value('specFiles', sh(script: "${WORKSPACE}/build/new-jenkins/spec-changes.sh", returnStatus: true) == 0)
}
diff --git a/gems/bundler_lockfile_extensions/.rspec-local b/gems/bundler_lockfile_extensions/.rspec-local
deleted file mode 100644
index 4e1e0d2f722..00000000000
--- a/gems/bundler_lockfile_extensions/.rspec-local
+++ /dev/null
@@ -1 +0,0 @@
---color
diff --git a/gems/bundler_lockfile_extensions/Gemfile b/gems/bundler_lockfile_extensions/Gemfile
deleted file mode 100644
index 468ec286539..00000000000
--- a/gems/bundler_lockfile_extensions/Gemfile
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-source "https://rubygems.org"
-
-# Declare your gem's dependencies in broadcast_policy.gemspec.
-# Bundler will treat runtime dependencies like base dependencies, and
-# development dependencies will be added by default to the :development group.
-gemspec
diff --git a/gems/bundler_lockfile_extensions/Gemfile.lock b/gems/bundler_lockfile_extensions/Gemfile.lock
deleted file mode 100644
index 0e057360418..00000000000
--- a/gems/bundler_lockfile_extensions/Gemfile.lock
+++ /dev/null
@@ -1,38 +0,0 @@
-PATH
- remote: .
- specs:
- bundler_lockfile_extensions (0.0.2)
-
-GEM
- remote: https://rubygems.org/
- specs:
- byebug (11.1.3)
- diff-lcs (1.5.0)
- rspec (3.12.0)
- rspec-core (~> 3.12.0)
- rspec-expectations (~> 3.12.0)
- rspec-mocks (~> 3.12.0)
- rspec-core (3.12.2)
- rspec-support (~> 3.12.0)
- rspec-expectations (3.12.3)
- diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.12.0)
- rspec-mocks (3.12.6)
- diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.12.0)
- rspec-support (3.12.1)
-
-PLATFORMS
- aarch64-linux
- arm64-darwin
- ruby
- x86_64-darwin
- x86_64-linux
-
-DEPENDENCIES
- bundler_lockfile_extensions!
- byebug
- rspec (~> 3.12)
-
-BUNDLED WITH
- 2.4.19
diff --git a/gems/bundler_lockfile_extensions/bundler_lockfile_extensions.gemspec b/gems/bundler_lockfile_extensions/bundler_lockfile_extensions.gemspec
deleted file mode 100644
index 84093d754df..00000000000
--- a/gems/bundler_lockfile_extensions/bundler_lockfile_extensions.gemspec
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-Gem::Specification.new do |spec|
- spec.name = "bundler_lockfile_extensions"
- spec.version = "0.0.2"
- spec.authors = ["Instructure"]
- spec.summary = "Support Multiple Lockfiles"
-
- spec.files = Dir.glob("{lib,spec}/**/*") + %w[plugins.rb]
- spec.require_paths = ["lib"]
-
- spec.add_dependency "bundler", ">= 2.3.26"
-
- spec.add_development_dependency "byebug"
- spec.add_development_dependency "rspec", "~> 3.12"
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions.rb
deleted file mode 100644
index c38553c0443..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions.rb
+++ /dev/null
@@ -1,375 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-require "bundler_lockfile_extensions/bundler"
-require "bundler_lockfile_extensions/bundler/definition"
-require "bundler_lockfile_extensions/bundler/dsl"
-require "bundler_lockfile_extensions/bundler/source_list"
-
-# Extends Bundler to allow arbitrarily many lockfiles (and Gemfiles!)
-# for variations of the Gemfile, while keeping all of the lockfiles in sync.
-#
-# `bundle install`, `bundle lock`, and `bundle update` will operate only on
-# the default lockfile (Gemfile.lock), afterwhich all other lockfiles will
-# be re-created based on this default lockfile. Additional lockfiles can be
-# based on the same Gemfile, but vary at runtime based on something like an
-# environment variable, global variable, or constant. When defining such a
-# lockfile, you should use a prepare callback that sets up the proper
-# environment for that variation, even if that's not what would otherwise
-# be selected by the launching environment.
-#
-# Alternately (or in addition!), you can define a lockfile to use a completely
-# different Gemfile. This will have the effect that common dependencies between
-# the two Gemfiles will stay locked to the same version in each lockfile.
-#
-# A lockfile definition can opt in to requiring explicit pinning for
-# any dependency that exists in that variation, but does not exist in the default
-# lockfile. This is especially useful if for some reason a given
-# lockfile will _not_ be committed to version control (such as a variation
-# that will include private plugins).
-#
-# Finally, `bundle check` will enforce additional checks to compare the final
-# locked versions of dependencies between the various lockfiles to ensure
-# they end up the same. This check might be tripped if Gemfile variations
-# (accidentally!) have conflicting version constraints on a dependency, that
-# are still self-consistent with that single Gemfile variation.
-# `bundle install`, `bundle lock`, and `bundle update` will also verify these
-# additional checks. You can additionally explicitly allow version variations
-# between explicit dependencies (and their sub-dependencies), for cases where
-# the lockfile variation is specifically to transition to a new version of
-# a dependency (like a Rails upgrade).
-#
-module BundlerLockfileExtensions
- class << self
- attr_reader :lockfile_definitions
-
- def enabled?
- @lockfile_definitions
- end
-
- # @param lockfile [String] The lockfile path
- # @param Builder [::Bundler::DSL] The Bundler DSL
- # @param gemfile [String, nil]
- # The Gemfile for this lockfile (defaults to Gemfile)
- # @param current [true, false] If this is the currently active combination
- # @param prepare [Proc, nil]
- # The callback to set up the environment so your Gemfile knows this is
- # the intended lockfile, and to select dependencies appropriately.
- # @param allow_mismatched_dependencies [true, false]
- # Allows version differences in dependencies between this lockfile and
- # the default lockfile. Note that even with this option, only top-level
- # dependencies that differ from the default lockfile, and their transitive
- # depedencies, are allowed to mismatch.
- # @param enforce_pinned_additional_dependencies [true, false]
- # If dependencies are present in this lockfile that are not present in the
- # default lockfile, enforce that they are pinned.
- def add_lockfile(lockfile = nil,
- builder:,
- gemfile: nil,
- current: false,
- prepare: nil,
- allow_mismatched_dependencies: true,
- enforce_pinned_additional_dependencies: false)
- enable unless enabled?
-
- default = gemfile.nil? && lockfile.nil?
- if default && default_lockfile_definition
- raise ArgumentError, "Only one default lockfile (gemfile and lockfile unspecified) is allowed"
- end
- if current && @lockfile_definitions.any? { |definition| definition[:current] }
- raise ArgumentError, "Only one lockfile can be flagged as the current lockfile"
- end
-
- @lockfile_definitions << (lockfile_def = {
- gemfile: (gemfile && ::Bundler.root.join(gemfile).expand_path) || ::Bundler.default_gemfile,
- lockfile: (lockfile && ::Bundler.root.join(lockfile).expand_path) || ::Bundler.default_lockfile,
- default:,
- current:,
- prepare:,
- allow_mismatched_dependencies:,
- enforce_pinned_additional_dependencies:
- }.freeze)
-
- # if BUNDLE_LOCKFILE is specified, explicitly use only that lockfile, regardless of the command
- if ENV["BUNDLE_LOCKFILE"]
- if File.expand_path(ENV["BUNDLE_LOCKFILE"]) == lockfile_def[:lockfile].to_s
- prepare&.call
- set_lockfile = true
- # we started evaluating the project's primary gemfile, but got told to use a lockfile
- # associated with a different Gemfile. so we need to evaluate that Gemfile instead
- if lockfile_def[:gemfile] != ::Bundler.default_gemfile
- # share a cache between all lockfiles
- ::Bundler.cache_root = ::Bundler.root
- ENV["BUNDLE_GEMFILE"] = lockfile_def[:gemfile].to_s
- ::Bundler.root = ::Bundler.default_gemfile.dirname
- ::Bundler.default_lockfile = lockfile_def[:lockfile]
-
- builder.eval_gemfile(::Bundler.default_gemfile)
-
- return false
- end
- end
- else
- # always use the default lockfile for `bundle check`, `bundle install`,
- # `bundle lock`, and `bundle update`. `bundle cache` delegates to
- # `bundle install`, but we want that to run as-normal.
- set_lockfile = if (defined?(::Bundler::CLI::Check) ||
- defined?(::Bundler::CLI::Install) ||
- defined?(::Bundler::CLI::Lock) ||
- defined?(::Bundler::CLI::Update)) &&
- !defined?(::Bundler::CLI::Cache)
- prepare&.call if default
- default
- else
- current
- end
- end
- ::Bundler.default_lockfile = lockfile_def[:lockfile] if set_lockfile
- true
- end
-
- # @!visibility private
- def after_install_all(install: true)
- previous_recursive = @recursive
-
- return unless enabled?
- return if ENV["BUNDLE_LOCKFILE"] # explicitly working against a single lockfile
-
- # must be running `bundle cache`
- return unless ::Bundler.default_lockfile == default_lockfile_definition[:lockfile]
-
- require "bundler_lockfile_extensions/check"
-
- if ::Bundler.frozen_bundle? && !install
- # only do the checks if we're frozen
- exit 1 unless Check.run
- return
- end
-
- # this hook will be called recursively when it has to install gems
- # for a secondary lockfile. defend against that
- return if @recursive
-
- @recursive = true
-
- require "tempfile"
- require "bundler_lockfile_extensions/lockfile_generator"
-
- ::Bundler.ui.info ""
-
- default_lockfile_contents = ::Bundler.default_lockfile.read.freeze
- default_specs = ::Bundler::LockfileParser.new(default_lockfile_contents).specs.to_h do |spec| # rubocop:disable Rails/IndexBy
- [[spec.name, spec.platform], spec]
- end
- default_root = ::Bundler.root
-
- attempts = 1
-
- checker = Check.new
- ::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
- next if lockfile_definition[:default]
-
- # root needs to be set so that paths are output relative to the correct root in the lockfile
- ::Bundler.root = lockfile_definition[:gemfile].dirname
-
- relative_lockfile = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
-
- # already up to date?
- up_to_date = false
- ::Bundler.settings.temporary(frozen: true) do
- ::Bundler.ui.silence do
- up_to_date = checker.base_check(lockfile_definition) && checker.check(lockfile_definition, allow_mismatched_dependencies: false)
- end
- end
- if up_to_date
- attempts = 1
- next
- end
-
- if ::Bundler.frozen_bundle?
- # if we're frozen, you have to use the pre-existing lockfile
- unless lockfile_definition[:lockfile].exist?
- ::Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. Please make sure you have checked #{relative_lockfile} into version control before deploying.")
- exit 1
- end
-
- ::Bundler.ui.info("Installing gems for #{relative_lockfile}...")
- write_lockfile(lockfile_definition, lockfile_definition[:lockfile], install:)
- else
- ::Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
-
- # adjust locked paths from the default lockfile to be relative to _this_ gemfile
- adjusted_default_lockfile_contents = default_lockfile_contents.gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
- remote_path = Pathname.new($1)
- next remote if remote_path.absolute?
-
- relative_remote_path = remote_path.expand_path(default_root).relative_path_from(::Bundler.root).to_s
- remote.sub($1, relative_remote_path)
- end
-
- # add a source for the current gem
- gem_spec = default_specs[[File.basename(::Bundler.root), "ruby"]]
-
- if gem_spec
- adjusted_default_lockfile_contents += <<~TEXT
- PATH
- remote: .
- specs:
- #{gem_spec.to_lock}
- TEXT
- end
-
- if lockfile_definition[:lockfile].exist?
- # if the lockfile already exists, "merge" it together
- default_lockfile = ::Bundler::LockfileParser.new(adjusted_default_lockfile_contents)
- lockfile = ::Bundler::LockfileParser.new(lockfile_definition[:lockfile].read)
-
- dependency_changes = false
- # replace any duplicate specs with what's in the default lockfile
- lockfile.specs.map! do |spec|
- default_spec = default_specs[[spec.name, spec.platform]]
- next spec unless default_spec
-
- dependency_changes ||= spec != default_spec
- default_spec
- end
-
- lockfile.specs.replace(default_lockfile.specs + lockfile.specs).uniq!
- lockfile.sources.replace(default_lockfile.sources + lockfile.sources).uniq!
- lockfile.platforms.concat(default_lockfile.platforms).uniq!
- # prune more specific platforms
- lockfile.platforms.delete_if do |p1|
- lockfile.platforms.any? { |p2| p2 != "ruby" && p1 != p2 && ::Bundler::MatchPlatform.platforms_match?(p2, p1) }
- end
- lockfile.instance_variable_set(:@ruby_version, default_lockfile.ruby_version)
- lockfile.instance_variable_set(:@bundler_version, default_lockfile.bundler_version)
-
- new_contents = LockfileGenerator.generate(lockfile)
- else
- # no lockfile? just start out with the default lockfile's contents to inherit its
- # locked gems
- new_contents = adjusted_default_lockfile_contents
- end
-
- had_changes = false
- # Now build a definition based on the given Gemfile, with the combined lockfile
- Tempfile.create do |temp_lockfile|
- temp_lockfile.write(new_contents)
- temp_lockfile.flush
-
- had_changes = write_lockfile(lockfile_definition, temp_lockfile.path, install:, dependency_changes:)
- end
-
- # if we had changes, bundler may have updated some common
- # dependencies beyond the default lockfile, so re-run it
- # 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 < 3
- attempts += 1
- redo
- else
- attempts = 1
- end
- end
- end
- end
-
- exit 1 unless checker.run
- ensure
- @recursive = previous_recursive
- end
-
- private
-
- def enable
- @lockfile_definitions ||= []
-
- ::Bundler.singleton_class.prepend(Bundler::ClassMethods)
- ::Bundler::Definition.prepend(Bundler::Definition)
- ::Bundler::SourceList.prepend(Bundler::SourceList)
- end
-
- def default_lockfile_definition
- @default_lockfile_definition ||= @lockfile_definitions.find { |d| d[:default] }
- end
-
- def write_lockfile(lockfile_definition, lockfile, install:, dependency_changes: false)
- lockfile_definition[:prepare]&.call
- definition = ::Bundler::Definition.build(lockfile_definition[:gemfile], lockfile, false)
- definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
-
- resolved_remotely = false
- begin
- previous_ui_level = ::Bundler.ui.level
- ::Bundler.ui.level = "warn"
- begin
- definition.resolve_with_cache!
- rescue ::Bundler::GemNotFound, ::Bundler::SolveFailure
- definition = ::Bundler::Definition.build(lockfile_definition[:gemfile], lockfile, false)
- definition.resolve_remotely!
- resolved_remotely = true
- end
- definition.lock(lockfile_definition[:lockfile], true)
- ensure
- ::Bundler.ui.level = previous_ui_level
- end
-
- # if we're running `bundle install` or `bundle update`, and something is missing from
- # the secondary lockfile, install it.
- if install && (definition.missing_specs.any? || resolved_remotely)
- ::Bundler.with_default_lockfile(lockfile_definition[:lockfile]) do
- ::Bundler::Installer.install(lockfile_definition[:gemfile].dirname, definition, {})
- end
- end
-
- !definition.nothing_changed?
- end
- end
-
- @recursive = false
-end
-
-Bundler::Dsl.include(BundlerLockfileExtensions::Bundler::Dsl)
-
-# this is terrible, but we can't prepend into any module because we only load
-# _inside_ of the CLI commands already running
-if defined?(Bundler::CLI::Check)
- require "bundler_lockfile_extensions/check"
- at_exit do
- next unless $!.nil?
- next if $!.is_a?(SystemExit) && !$!.success?
-
- next if BundlerLockfileExtensions::Check.run
-
- Bundler.ui.warn("You can attempt to fix by running `bundle install`")
- exit 1
- end
-end
-if defined?(Bundler::CLI::Lock)
- at_exit do
- next unless $!.nil?
- next if $!.is_a?(SystemExit) && !$!.success?
-
- BundlerLockfileExtensions.after_install_all(install: false)
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler.rb
deleted file mode 100644
index 3b352b3e28a..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-module BundlerLockfileExtensions
- module Bundler
- module ClassMethods
- def self.prepended(klass)
- super
-
- klass.attr_writer :cache_root, :default_lockfile, :root
- end
-
- def app_cache(custom_path = nil)
- super(custom_path || @cache_root)
- end
-
- def default_lockfile(force_original: false)
- return @default_lockfile if @default_lockfile && !force_original
-
- super()
- end
-
- def with_default_lockfile(lockfile)
- previous_default_lockfile, @default_lockfile = @default_lockfile, lockfile
- yield
- ensure
- @default_lockfile = previous_default_lockfile
- end
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/definition.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/definition.rb
deleted file mode 100644
index cecc561e523..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/definition.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-module BundlerLockfileExtensions
- module Bundler
- module Definition
- def initialize(lockfile, *args)
- if ENV["BUNDLE_LOCKFILE"] && !::Bundler.instance_variable_get(:@default_lockfile)
- raise ::Bundler::GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}"
- end
-
- # we changed the default lockfile in BundlerLockfileExtensions.add_lockfile
- # since DSL.evaluate was called (re-entrantly); sub the proper value in
- if !lockfile.equal?(::Bundler.default_lockfile) && ::Bundler.default_lockfile(force_original: true) == lockfile
- lockfile = ::Bundler.default_lockfile
- end
- super
- end
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/dsl.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/dsl.rb
deleted file mode 100644
index f85c3a538a2..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/dsl.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-module BundlerLockfileExtensions
- module Bundler
- module Dsl
- def add_lockfile(*args, **kwargs)
- BundlerLockfileExtensions.add_lockfile(*args, builder: self, **kwargs)
- end
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/source_list.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/source_list.rb
deleted file mode 100644
index 0d0b32ff20a..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/bundler/source_list.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-module BundlerLockfileExtensions
- module Bundler
- module SourceList
- # consider them equivalent if the replacements just have a bunch of dups
- def equivalent_sources?(lock_sources, replacement_sources)
- super(lock_sources, replacement_sources.uniq)
- end
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/check.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/check.rb
deleted file mode 100644
index a22d67b5a4d..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/check.rb
+++ /dev/null
@@ -1,182 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-require "set"
-
-module BundlerLockfileExtensions
- class Check
- class << self
- def run # rubocop:disable Rails/Delegate
- new.run
- end
- end
-
- def initialize
- default_lockfile_contents = ::Bundler.default_lockfile.read
- @default_lockfile = ::Bundler::LockfileParser.new(default_lockfile_contents)
- @default_specs = @default_lockfile.specs.to_h do |spec| # rubocop:disable Rails/IndexBy
- [[spec.name, spec.platform], spec]
- end
- end
-
- def run
- return true unless ::Bundler.default_lockfile.exist?
-
- success = true
- BundlerLockfileExtensions.lockfile_definitions.each do |lockfile_definition|
- next unless lockfile_definition[:lockfile].exist?
-
- success = false unless check(lockfile_definition)
- end
- success
- end
-
- # this is mostly equivalent to the built in checks in `bundle check`, but even
- # more conservative, and returns false instead of exiting on failure
- def base_check(lockfile_definition)
- return false unless lockfile_definition[:lockfile].file?
-
- lockfile_definition[:prepare]&.call
- definition = ::Bundler::Definition.build(lockfile_definition[:gemfile], lockfile_definition[:lockfile], false)
- return false unless definition.send(:current_platform_locked?)
-
- begin
- definition.validate_runtime!
- definition.resolve_only_locally!
- not_installed = definition.missing_specs
- rescue ::Bundler::RubyVersionMismatch, ::Bundler::GemNotFound, ::Bundler::SolveFailure
- return false
- end
-
- not_installed.empty?
- end
-
- # this checks for mismatches between the default lockfile and the given lockfile,
- # and for pinned dependencies in lockfiles requiring them
- def check(lockfile_definition, allow_mismatched_dependencies: true)
- success = true
- proven_pinned = Set.new
- needs_pin_check = []
- lockfile = ::Bundler::LockfileParser.new(lockfile_definition[:lockfile].read)
- unless lockfile.platforms == @default_lockfile.platforms
- ::Bundler.ui.error("The platforms in #{lockfile_definition[:lockfile].relative_path_from(Dir.pwd)} do not match the default lockfile.")
- success = false
- end
- unless lockfile.bundler_version == @default_lockfile.bundler_version
- ::Bundler.ui.error("bundler (#{lockfile.bundler_version}) in #{lockfile_definition[:lockfile].relative_path_from(Dir.pwd)} does not match the default lockfile's version (@#{@default_lockfile.bundler_version}).")
- success = false
- end
-
- specs = lockfile.specs.group_by(&:name)
- allow_mismatched_dependencies = lockfile_definition[:allow_mismatched_dependencies] if allow_mismatched_dependencies
-
- # build list of top-level dependencies that differ from the default lockfile,
- # and all _their_ transitive dependencies
- if allow_mismatched_dependencies
- transitive_dependencies = Set.new
- # only dependencies that differ from the default lockfile
- pending_transitive_dependencies = lockfile.dependencies.reject do |name, dep|
- @default_lockfile.dependencies[name] == dep
- end.map(&:first)
-
- until pending_transitive_dependencies.empty?
- dep = pending_transitive_dependencies.shift
- next if transitive_dependencies.include?(dep)
-
- transitive_dependencies << dep
- platform_specs = specs[dep]
- unless platform_specs
- # should only be bundler that's missing a spec
- raise "Could not find spec for dependency #{dep}" unless dep == "bundler"
-
- next
- end
-
- pending_transitive_dependencies.concat(platform_specs.flat_map(&:dependencies).map(&:name).uniq)
- end
- end
-
- # look through top-level explicit dependencies for pinned requirements
- if lockfile_definition[:enforce_pinned_additional_dependencies]
- find_pinned_dependencies(proven_pinned, lockfile.dependencies.each_value)
- end
-
- # check for conflicting requirements (and build list of pins, in the same loop)
- specs.values.flatten.each do |spec|
- default_spec = @default_specs[[spec.name, spec.platform]]
-
- if lockfile_definition[:enforce_pinned_additional_dependencies]
- # look through what this spec depends on, and keep track of all pinned requirements
- find_pinned_dependencies(proven_pinned, spec.dependencies)
-
- needs_pin_check << spec unless default_spec
- end
-
- next unless default_spec
-
- # have to ensure Path sources are relative to their lockfile before comparing
- same_source = if [default_spec.source, spec.source].grep(::Bundler::Source::Path).length == 2
- lockfile_definition[:lockfile].dirname.join(spec.source.path).ascend.any?(::Bundler.default_lockfile.dirname.join(default_spec.source.path))
- else
- default_spec.source == spec.source
- end
-
- next if default_spec.version == spec.version && same_source
- next if allow_mismatched_dependencies && transitive_dependencies.include?(spec.name)
-
- ::Bundler.ui.error("#{spec}#{spec.git_version} in #{lockfile_definition[:lockfile].relative_path_from(Dir.pwd)} does not match the default lockfile's version (@#{default_spec.version}#{default_spec.git_version}); this may be due to a conflicting requirement, which would require manual resolution.")
- success = false
- end
-
- # now that we have built a list of every gem that is pinned, go through
- # the gems that were in this lockfile, but not the default lockfile, and
- # ensure it's pinned _somehow_
- needs_pin_check.each do |spec|
- pinned = case spec.source
- when ::Bundler::Source::Git
- spec.source.ref == spec.source.revision
- when ::Bundler::Source::Path
- true
- when ::Bundler::Source::Rubygems
- proven_pinned.include?(spec.name)
- else
- false
- end
-
- unless pinned
- ::Bundler.ui.error("#{spec} in #{lockfile_definition[:lockfile].relative_path_from(Dir.pwd)} has not been pinned to a specific version, which is required since it is not part of the default lockfile.")
- success = false
- end
- end
-
- success
- end
-
- private
-
- def find_pinned_dependencies(proven_pinned, dependencies)
- dependencies.each do |dependency|
- dependency.requirement.requirements.each do |requirement|
- proven_pinned << dependency.name if requirement.first == "="
- end
- end
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/lockfile_generator.rb b/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/lockfile_generator.rb
deleted file mode 100644
index 0419b023fd4..00000000000
--- a/gems/bundler_lockfile_extensions/lib/bundler_lockfile_extensions/lockfile_generator.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-#
-
-require "bundler/lockfile_generator"
-
-module BundlerLockfileExtensions
- # generates a lockfile based on another LockfileParser
- class LockfileGenerator < ::Bundler::LockfileGenerator
- def self.generate(lockfile)
- new(LockfileAdapter.new(lockfile)).generate!
- end
-
- private
-
- class LockfileAdapter < SimpleDelegator
- def sources
- self
- end
-
- def lock_sources
- __getobj__.sources
- end
-
- def resolve
- specs
- end
-
- def dependencies
- super.values
- end
-
- def locked_ruby_version
- ruby_version
- end
- end
-
- private_constant :LockfileAdapter
-
- def add_bundled_with
- add_section("BUNDLED WITH", definition.bundler_version.to_s)
- end
- end
-end
diff --git a/gems/bundler_lockfile_extensions/spec/bundler_lockfile_extensions_spec.rb b/gems/bundler_lockfile_extensions/spec/bundler_lockfile_extensions_spec.rb
deleted file mode 100644
index 5e8621398ed..00000000000
--- a/gems/bundler_lockfile_extensions/spec/bundler_lockfile_extensions_spec.rb
+++ /dev/null
@@ -1,470 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-
-require "tempfile"
-require "open3"
-require "fileutils"
-
-require_relative "spec_helper"
-
-describe "BundlerLockfileExtensions" do
- # the definition section for a gemfile with two lockfiles; one
- # that will have more gems than the default.
- let(:all_gems_definitions) do
- <<~RUBY
- add_lockfile(
- prepare: -> { ::INCLUDE_ALL_GEMS = false },
- current: false
- )
- add_lockfile(
- "Gemfile.full.lock",
- prepare: -> { ::INCLUDE_ALL_GEMS = true },
- current: true
- )
- RUBY
- end
-
- let(:all_gems_preamble) do
- "::INCLUDE_ALL_GEMS = true unless defined?(::INCLUDE_ALL_GEMS)"
- end
-
- it "generates a default Gemfile.lock when loaded, but not configured" do
- contents = <<~RUBY
- gem "concurrent-ruby", "1.2.2"
- RUBY
-
- with_gemfile("", contents) do
- invoke_bundler("install")
- output = invoke_bundler("info concurrent-ruby")
-
- expect(output).to include("1.2.2")
- expect(File.read("Gemfile.lock")).to include("1.2.2")
- end
- end
-
- it "disallows multiple default lockfiles" do
- with_gemfile(<<~RUBY) do
- add_lockfile()
- add_lockfile()
- RUBY
- expect { invoke_bundler("install") }.to raise_error(/Only one default lockfile/)
- end
- end
-
- it "disallows multiple current lockfiles" do
- with_gemfile(<<~RUBY) do
- add_lockfile(current: true)
- add_lockfile("Gemfile.new.lock", current: true)
- RUBY
- expect { invoke_bundler("install") }.to raise_error(/Only one lockfile/)
- end
- end
-
- it "generates custom lockfiles with varying versions" do
- definitions = <<~RUBY
- add_lockfile(
- prepare: -> { ::GEM_VERSION = "1.1.10" }
- )
- add_lockfile(
- "Gemfile.new.lock",
- prepare: -> { ::GEM_VERSION = "1.2.2" }
- )
- RUBY
-
- contents = <<~RUBY
- ::GEM_VERSION = "1.1.10" unless defined?(::GEM_VERSION)
- gem "concurrent-ruby", ::GEM_VERSION
- RUBY
-
- with_gemfile(definitions, contents) do
- invoke_bundler("install")
-
- expect(File.read("Gemfile.lock")).to include("1.1.10")
- expect(File.read("Gemfile.lock")).not_to include("1.2.2")
- expect(File.read("Gemfile.new.lock")).not_to include("1.1.10")
- expect(File.read("Gemfile.new.lock")).to include("1.2.2")
- end
- end
-
- it "generates lockfiles with a subset of gems" do
- contents = <<~RUBY
- if ::INCLUDE_ALL_GEMS
- gem "test_local", path: "test_local"
- end
- gem "concurrent-ruby", "1.2.2"
- RUBY
-
- with_gemfile(all_gems_definitions, contents, all_gems_preamble) do
- create_local_gem("test_local", "")
-
- invoke_bundler("install")
-
- expect(File.read("Gemfile.lock")).not_to include("test_local")
- expect(File.read("Gemfile.full.lock")).to include("test_local")
-
- expect(File.read("Gemfile.lock")).to include("concurrent-ruby")
- expect(File.read("Gemfile.full.lock")).to include("concurrent-ruby")
- end
- end
-
- it "fails if an additional lockfile contains an invalid gem" do
- definitions = <<~RUBY
- add_lockfile()
- add_lockfile(
- "Gemfile.new.lock"
- )
- RUBY
-
- contents = <<~RUBY
- gem "concurrent-ruby", ">= 1.2.2"
- RUBY
-
- with_gemfile(definitions, contents) do
- invoke_bundler("install")
-
- replace_lockfile_pin("Gemfile.new.lock", "concurrent-ruby", "9.9.9")
-
- expect { invoke_bundler("check") }.to raise_error(/concurrent-ruby.*does not match/m)
- end
- end
-
- it "preserves the locked version of a gem in an alternate lockfile when updating a different gem in common" do
- contents = <<~RUBY
- gem "net-ldap", "0.17.0"
-
- if ::INCLUDE_ALL_GEMS
- gem "net-smtp", "0.3.2"
- end
- RUBY
-
- with_gemfile(all_gems_definitions, contents, all_gems_preamble) do
- invoke_bundler("install")
-
- expect(invoke_bundler("info net-ldap")).to include("0.17.0")
- expect(invoke_bundler("info net-smtp")).to include("0.3.2")
-
- # loosen the requirement on both gems
- write_gemfile(all_gems_definitions, <<~RUBY, all_gems_preamble)
- gem "net-ldap", "~> 0.17"
-
- if ::INCLUDE_ALL_GEMS
- gem "net-smtp", "~> 0.3"
- end
- RUBY
-
- # but only update net-ldap
- invoke_bundler("update net-ldap")
-
- # net-smtp should be untouched, even though it's no longer pinned
- expect(invoke_bundler("info net-ldap")).not_to include("0.17.0")
- expect(invoke_bundler("info net-smtp")).to include("0.3.2")
- end
- end
-
- it "maintains consistency across multiple Gemfiles" do
- definitions = <<~RUBY
- add_lockfile()
- add_lockfile(
- "local_test/Gemfile.lock",
- gemfile: "local_test/Gemfile")
- RUBY
-
- contents = <<~RUBY
- gem "net-smtp", "0.3.2"
- RUBY
-
- with_gemfile(definitions, contents) do
- create_local_gem("local_test", <<~RUBY)
- spec.add_dependency "net-smtp", "~> 0.3"
- RUBY
-
- invoke_bundler("install")
-
- # locks to 0.3.2 in the local gem's lockfile, even though the local
- # gem itself would allow newer
- expect(File.read("local_test/Gemfile.lock")).to include("0.3.2")
- end
- end
-
- it "whines about non-pinned dependencies in flagged gemfiles" do
- definitions = <<~RUBY
- add_lockfile(
- prepare: -> { ::INCLUDE_ALL_GEMS = false },
- current: false
- )
- add_lockfile(
- "Gemfile.full.lock",
- prepare: -> { ::INCLUDE_ALL_GEMS = true },
- current: true,
- enforce_pinned_additional_dependencies: true
- )
- RUBY
-
- contents = <<~RUBY
- gem "net-ldap", "0.17.0"
-
- if ::INCLUDE_ALL_GEMS
- gem "net-smtp", "~> 0.3"
- end
- RUBY
-
- with_gemfile(definitions, contents, all_gems_preamble) do
- expect { invoke_bundler("install") }.to raise_error(/net-smtp \([0-9.]+\) in Gemfile.full.lock has not been pinned/)
-
- # not only have to pin net-smtp, but also its transitive dependencies
- write_gemfile(definitions, <<~RUBY, all_gems_preamble)
- gem "net-ldap", "0.17.0"
-
- if ::INCLUDE_ALL_GEMS
- gem "net-smtp", "0.3.2"
- gem "net-protocol", "0.2.1"
- gem "timeout", "0.3.2"
- end
- RUBY
-
- invoke_bundler("install") # no error, because it's now pinned
- end
- end
-
- context "with mismatched dependencies disallowed" do
- let(:all_gems_definitions) do
- <<~RUBY
- add_lockfile(
- prepare: -> { ::INCLUDE_ALL_GEMS = false },
- current: false
- )
- add_lockfile(
- "Gemfile.full.lock",
- prepare: -> { ::INCLUDE_ALL_GEMS = true },
- allow_mismatched_dependencies: false,
- current: true
- )
- RUBY
- end
-
- it "notifies about mismatched versions between different lockfiles" do
- contents = <<~RUBY
- if ::INCLUDE_ALL_GEMS
- gem "activesupport", "7.0.4.3"
- else
- gem "activesupport", "~> 6.1.0"
- end
- RUBY
-
- with_gemfile(all_gems_definitions, contents, all_gems_preamble) do
- expect { invoke_bundler("install") }.to raise_error(/activesupport \(7.0.4.3\) in Gemfile.full.lock does not match the default lockfile's version/)
- end
- end
-
- it "notifies about mismatched versions between different lockfiles for sub-dependencies" do
- definitions = <<~RUBY
- add_lockfile(
- prepare: -> { ::INCLUDE_ALL_GEMS = false },
- current: false
- )
- add_lockfile(
- "Gemfile.full.lock",
- prepare: -> { ::INCLUDE_ALL_GEMS = true },
- allow_mismatched_dependencies: false,
- current: true
- )
- RUBY
-
- contents = <<~RUBY
- gem "activesupport", "7.0.4.3" # depends on tzinfo ~> 2.0, so will get >= 2.0.6
-
- if ::INCLUDE_ALL_GEMS
- gem "tzinfo", "2.0.5"
- end
- RUBY
-
- with_gemfile(definitions, contents, all_gems_preamble) do
- expect { invoke_bundler("install") }.to raise_error(/tzinfo \(2.0.5\) in Gemfile.full.lock does not match the default lockfile's version/)
- end
- end
- end
-
- it "allows mismatched explicit dependencies by default" do
- contents = <<~RUBY
- if ::INCLUDE_ALL_GEMS
- gem "activesupport", "7.0.4.3"
- else
- gem "activesupport", "~> 6.1.0"
- end
- RUBY
-
- with_gemfile(all_gems_definitions, contents, all_gems_preamble) do
- invoke_bundler("install") # no error
- expect(File.read("Gemfile.lock")).to include("6.1.")
- expect(File.read("Gemfile.lock")).not_to include("7.0.4.3")
- expect(File.read("Gemfile.full.lock")).not_to include("6.1.")
- expect(File.read("Gemfile.full.lock")).to include("7.0.4.3")
- end
- end
-
- it "disallows mismatched implicit dependencies" do
- definitions = <<~RUBY
- add_lockfile()
- add_lockfile(
- "local_test/Gemfile.lock",
- allow_mismatched_dependencies: false,
- gemfile: "local_test/Gemfile")
- RUBY
- contents = <<~RUBY
- gem "snaky_hash", "2.0.1"
- RUBY
-
- with_gemfile(definitions, contents) do
- create_local_gem("local_test", <<~RUBY)
- spec.add_dependency "zendesk_api", "1.28.0"
- RUBY
-
- expect { invoke_bundler("install") }.to raise_error(%r{hashie \(4[0-9.]+\) in local_test/Gemfile.lock does not match the default lockfile's version \(@([0-9.]+)\)})
- end
- end
-
- it "removes transitive deps from secondary lockfiles when they disappear from the primary lockfile" do
- contents = <<~RUBY
- gem "pact-mock_service", "3.11.0"
- RUBY
-
- with_gemfile(all_gems_definitions, contents, all_gems_preamble) do
- # get 3.11.0 intalled
- invoke_bundler("install")
-
- write_gemfile(all_gems_definitions, <<~RUBY, all_gems_preamble)
- gem "pact-mock_service", "~> 3.11.0"
- RUBY
-
- # update the lockfiles with the looser dependency (but with 3.11.0)
- invoke_bundler("install")
-
- expect(File.read("Gemfile.lock")).to include("filelock")
- full_lock = File.read("Gemfile.full.lock")
- expect(full_lock).to include("filelock")
-
- # update the default lockfile to 3.11.2
- invoke_bundler("update")
-
- # but revert the full lockfile, and re-sync it
- # as part of a regular bundle install
- File.write("Gemfile.full.lock", full_lock)
-
- invoke_bundler("install")
-
- expect(File.read("Gemfile.lock")).not_to include("filelock")
- expect(File.read("Gemfile.full.lock")).not_to include("filelock")
- end
- end
-
- private
-
- def create_local_gem(name, content)
- FileUtils.mkdir_p(name)
- File.write("#{name}/#{name}.gemspec", <<~RUBY)
- Gem::Specification.new do |spec|
- spec.name = #{name.inspect}
- spec.version = "0.0.1"
- spec.authors = ["Instructure"]
- spec.summary = "for testing only"
-
- #{content}
- end
- RUBY
-
- File.write("#{name}/Gemfile", <<~RUBY)
- source "https://rubygems.org"
-
- gemspec
- RUBY
- end
-
- # creates a new temporary directory, writes the gemfile to it, and yields
- #
- # @param (see #write_gemfile)
- # @yield
- def with_gemfile(definitions, content = nil, preamble = nil)
- Dir.mktmpdir do |dir|
- Dir.chdir(dir) do
- write_gemfile(definitions, content, preamble)
-
- invoke_bundler("config frozen false")
-
- yield
- end
- end
- end
-
- # @param definitions [String]
- # Ruby code to set up lockfiles by calling add_lockfile. Called inside a
- # conditional for when BundlerLockfileExtensions is loaded the first time.
- # @param content [String]
- # Additional Ruby code for adding gem requirements to the Gemfile
- # @param preamble [String]
- # Additional Ruby code to execute prior to installing the plugin.
- def write_gemfile(definitions, content = nil, preamble = nil)
- raise ArgumentError, "Did you mean to use `with_gemfile`?" if block_given?
-
- File.write("Gemfile", <<~RUBY)
- source "https://rubygems.org"
-
- #{preamble}
-
- plugin "bundler_lockfile_extensions", path: #{File.dirname(__dir__).inspect}
- if Plugin.installed?("bundler_lockfile_extensions")
- Plugin.send(:load_plugin, "bundler_lockfile_extensions") unless defined?(BundlerLockfileExtensions)
-
- unless BundlerLockfileExtensions.enabled?
- #{definitions}
- end
- end
-
- #{content}
- RUBY
- end
-
- # Shells out to a new instance of bundler, with a clean bundler env
- #
- # @param subcommand [String] Args to pass to bundler
- # @raise [RuntimeError] if the bundle command fails
- def invoke_bundler(subcommand, env: {})
- output = nil
- bundler_version = ENV.fetch("BUNDLER_VERSION")
- command = "#{Gem.bin_path("bundler", "bundler", bundler_version)} #{subcommand}"
- Bundler.with_unbundled_env do
- output, status = Open3.capture2e(env, command)
-
- raise "bundle #{subcommand} failed: #{output}" unless status.success?
- end
- output
- end
-
- # Directly modifies a lockfile to adjust the version of a gem
- #
- # Useful for simulating certain unusual situations that can arise.
- #
- # @param lockfile [String] The lockfile's location
- # @param gem [String] The gem's name
- # @param version [String] The new version to "pin" the gem to
- def replace_lockfile_pin(lockfile, gem, version)
- new_contents = File.read(lockfile).gsub(%r{#{gem} \([0-9.]+\)}, "#{gem} (#{version})")
-
- File.write(lockfile, new_contents)
- end
-end
diff --git a/gems/bundler_lockfile_extensions/spec/spec_helper.rb b/gems/bundler_lockfile_extensions/spec/spec_helper.rb
deleted file mode 100644
index a1f053463ed..00000000000
--- a/gems/bundler_lockfile_extensions/spec/spec_helper.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-#
-# Copyright (C) 2023 - present Instructure, Inc.
-#
-# This file is part of Canvas.
-#
-# Canvas is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Affero General Public License as published by the Free
-# Software Foundation, version 3 of the License.
-#
-# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Affero General Public License along
-# with this program. If not, see .
-
-RSpec.configure do |config|
- config.expect_with :rspec do |c|
- c.max_formatted_output_length = nil
- end
-
- config.run_all_when_everything_filtered = true
- config.filter_run :focus
- config.order = "random"
-end
diff --git a/gems/bundler_lockfile_extensions/test.sh b/gems/bundler_lockfile_extensions/test.sh
deleted file mode 100755
index bc4ff117381..00000000000
--- a/gems/bundler_lockfile_extensions/test.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-set -e
-
-bundle check || bundle install
-bundle exec rspec spec
diff --git a/gems/tatl_tael/config/default.yml b/gems/tatl_tael/config/default.yml
index d90443a5d24..95940e26002 100644
--- a/gems/tatl_tael/config/default.yml
+++ b/gems/tatl_tael/config/default.yml
@@ -21,6 +21,7 @@ CopyrightLinter:
- "**/vendor/**"
- "gems/tatl_tael/spec/lib/tatl_tael/linters/fixtures/**"
- "gems/plugins/**"
+ - "vendor/**"
Regexes:
FirstLineExceptions:
- !ruby/regexp '/^#!/' # e.g. "#!/usr/bin/env ruby"
diff --git a/script/rlint b/script/rlint
index 5c9bce9ef71..28a26bcd201 100755
--- a/script/rlint
+++ b/script/rlint
@@ -15,7 +15,7 @@ require "optparse"
linter_options = {
linter_name: "Rubocop",
- file_regex: %r{(?:\.rb|\.rake|\.gemspec|/[^./]+)$},
+ file_regex: %r{(?:\.rb|\.rake|\.gemspec|Gemfile|/[^./]+)$},
format: "rubocop",
command: +"bin/rubocop --force-exclusion",
auto_correct: false,
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock.rb
new file mode 100644
index 00000000000..7d6771bd70c
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock.rb
@@ -0,0 +1,437 @@
+# frozen_string_literal: true
+
+require_relative "multilock/ext/bundler"
+require_relative "multilock/ext/definition"
+require_relative "multilock/ext/dsl"
+require_relative "multilock/ext/plugin"
+require_relative "multilock/ext/plugin/dsl"
+require_relative "multilock/ext/source_list"
+require_relative "multilock/version"
+
+module Bundler
+ module Multilock
+ class << self
+ # @!visibility private
+ attr_reader :lockfile_definitions
+ # @!visibility private
+ attr_accessor :prepare_block
+
+ # @param lockfile [String] The lockfile path (defaults to Gemfile.lock)
+ # @param builder [Dsl] The Bundler DSL
+ # @param gemfile [String, nil]
+ # The Gemfile for this lockfile (defaults to Gemfile)
+ # @param default [Boolean]
+ # If this lockfile should be the default (instead of Gemfile.lock)
+ # @param allow_mismatched_dependencies [true, false]
+ # Allows version differences in dependencies between this lockfile and
+ # the default lockfile. Note that even with this option, only top-level
+ # dependencies that differ from the default lockfile, and their transitive
+ # depedencies, are allowed to mismatch.
+ # @param enforce_pinned_additional_dependencies [true, false]
+ # If dependencies are present in this lockfile that are not present in the
+ # default lockfile, enforce that they are pinned.
+ # @yield
+ # Block executed only when this lockfile is active.
+ # @return [true, false] if the lockfile is the current lockfile
+ def add_lockfile(lockfile = nil,
+ builder:,
+ gemfile: nil,
+ default: nil,
+ allow_mismatched_dependencies: true,
+ enforce_pinned_additional_dependencies: false,
+ &block)
+ # terminology gets confusing here. The "default" param means
+ # "use this lockfile when not overridden by BUNDLE_LOCKFILE"
+ # but Bundler.defaul_lockfile (usually) means "Gemfile.lock"
+ # so refer to the former as "current" internally
+ current = default
+ current = true if current.nil? && lockfile_definitions.empty? && lockfile.nil? && gemfile.nil?
+
+ # allow short-form lockfile names
+ lockfile = "Gemfile.#{lockfile}.lock" if lockfile && !(lockfile.include?("/") || lockfile.end_with?(".lock"))
+ # if a gemfile was provided, but not a lockfile, infer the default lockfile for that gemfile
+ lockfile ||= "#{gemfile}.lock" if gemfile
+ # use absolute paths
+ lockfile = Bundler.root.join(lockfile).expand_path if lockfile
+ # use the default lockfile (Gemfile.lock) if none was given
+ lockfile ||= Bundler.default_lockfile(force_original: true)
+ if current && (old_current = lockfile_definitions.find { |definition| definition[:current] })
+ raise ArgumentError, "Only one lockfile (#{old_current[:lockfile]}) can be flagged as the default"
+ end
+
+ raise ArgumentError, "Lockfile #{lockfile} is already defined" if lockfile_definitions.any? do |definition|
+ definition[:lockfile] == lockfile
+ end
+
+ env_lockfile = ENV["BUNDLE_LOCKFILE"]
+ if env_lockfile
+ unless env_lockfile.include?("/") || env_lockfile.end_with?(".lock")
+ env_lockfile = "Gemfile.#{env_lockfile}.lock"
+ end
+ env_lockfile = Bundler.root.join(env_lockfile).expand_path
+ current = env_lockfile == lockfile
+ end
+
+ lockfile_definitions << (lockfile_def = {
+ gemfile: (gemfile && Bundler.root.join(gemfile).expand_path) || Bundler.default_gemfile,
+ lockfile: lockfile,
+ current: current,
+ prepare: block,
+ allow_mismatched_dependencies: allow_mismatched_dependencies,
+ enforce_pinned_additional_dependencies: enforce_pinned_additional_dependencies
+ })
+
+ if (defined?(CLI::Check) ||
+ defined?(CLI::Install) ||
+ defined?(CLI::Lock) ||
+ defined?(CLI::Update)) &&
+ !defined?(CLI::Cache) && !env_lockfile
+ # always use Gemfile.lock for `bundle check`, `bundle install`,
+ # `bundle lock`, and `bundle update`. `bundle cache` delegates to
+ # `bundle install`, but we want that to run as normal.
+ # If they're using BUNDLE_LOCKFILE, then they really do want to
+ # use a particular lockfile, and it overrides whatever they
+ # dynamically set in their gemfile
+ current = lockfile == Bundler.default_lockfile(force_original: true)
+ end
+
+ if current
+ block&.call
+ Bundler.default_lockfile = lockfile
+
+ # we started evaluating the project's primary gemfile, but got told to use a lockfile
+ # associated with a different Gemfile. so we need to evaluate that Gemfile instead
+ if lockfile_def[:gemfile] != Bundler.default_gemfile
+ # share a cache between all lockfiles
+ Bundler.cache_root = Bundler.root
+ ENV["BUNDLE_GEMFILE"] = lockfile_def[:gemfile].to_s
+ Bundler.root = Bundler.default_gemfile.dirname
+ Bundler.default_lockfile = lockfile
+
+ builder.eval_gemfile(Bundler.default_gemfile)
+
+ return false
+ end
+ end
+ true
+ end
+
+ # @!visibility private
+ def after_install_all(install: true)
+ loaded!
+ previous_recursive = @recursive
+
+ return if lockfile_definitions.empty?
+ return if ENV["BUNDLE_LOCKFILE"] # explicitly working against a single lockfile
+
+ # must be running `bundle cache`
+ return unless Bundler.default_lockfile == Bundler.default_lockfile(force_original: true)
+
+ require_relative "multilock/check"
+
+ if Bundler.frozen_bundle? && !install
+ # only do the checks if we're frozen
+ exit 1 unless Check.run
+ return
+ end
+
+ # this hook will be called recursively when it has to install gems
+ # for a secondary lockfile. defend against that
+ return if @recursive
+
+ @recursive = true
+
+ require "tempfile"
+ require_relative "multilock/lockfile_generator"
+
+ Bundler.ui.info ""
+
+ default_lockfile_contents = Bundler.default_lockfile.read.freeze
+ default_specs = LockfileParser.new(default_lockfile_contents).specs.to_h do |spec|
+ [[spec.name, spec.platform], spec]
+ end
+ default_root = Bundler.root
+
+ attempts = 1
+
+ checker = Check.new
+ synced_any = false
+ 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
+ next if lockfile_definition[:lockfile] == Bundler.default_lockfile(force_original: true)
+
+ # root needs to be set so that paths are output relative to the correct root in the lockfile
+ Bundler.root = lockfile_definition[:gemfile].dirname
+
+ relative_lockfile = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
+
+ # already up to date?
+ up_to_date = false
+ Bundler.settings.temporary(frozen: true) do
+ Bundler.ui.silence do
+ up_to_date = checker.base_check(lockfile_definition) &&
+ checker.check(lockfile_definition, allow_mismatched_dependencies: false)
+ end
+ end
+ if up_to_date
+ attempts = 1
+ next
+ end
+
+ if Bundler.frozen_bundle?
+ # if we're frozen, you have to use the pre-existing lockfile
+ unless lockfile_definition[:lockfile].exist?
+ Bundler.ui.error("The bundle is locked, but #{relative_lockfile} is missing. " \
+ "Please make sure you have checked #{relative_lockfile} " \
+ "into version control before deploying.")
+ exit 1
+ end
+
+ Bundler.ui.info("Installing gems for #{relative_lockfile}...")
+ write_lockfile(lockfile_definition, lockfile_definition[:lockfile], install: install)
+ else
+ Bundler.ui.info("Syncing to #{relative_lockfile}...") if attempts == 1
+ synced_any = true
+
+ # adjust locked paths from the default lockfile to be relative to _this_ gemfile
+ adjusted_default_lockfile_contents =
+ default_lockfile_contents.gsub(/PATH\n remote: ([^\n]+)\n/) do |remote|
+ remote_path = Pathname.new($1)
+ next remote if remote_path.absolute?
+
+ relative_remote_path = remote_path.expand_path(default_root).relative_path_from(Bundler.root).to_s
+ remote.sub($1, relative_remote_path)
+ end
+
+ # add a source for the current gem
+ gem_spec = default_specs[[File.basename(Bundler.root), "ruby"]]
+
+ if gem_spec
+ adjusted_default_lockfile_contents += <<~TEXT
+ PATH
+ remote: .
+ specs:
+ #{gem_spec.to_lock}
+ TEXT
+ end
+
+ if lockfile_definition[:lockfile].exist?
+ # if the lockfile already exists, "merge" it together
+ default_lockfile = LockfileParser.new(adjusted_default_lockfile_contents)
+ lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
+
+ dependency_changes = false
+ # replace any duplicate specs with what's in the default lockfile
+ lockfile.specs.map! do |spec|
+ default_spec = default_specs[[spec.name, spec.platform]]
+ next spec unless default_spec
+
+ dependency_changes ||= spec != default_spec
+ default_spec
+ end
+
+ lockfile.specs.replace(default_lockfile.specs + lockfile.specs).uniq!
+ lockfile.sources.replace(default_lockfile.sources + lockfile.sources).uniq!
+ lockfile.platforms.replace(default_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, default_lockfile.ruby_version)
+ lockfile.instance_variable_set(:@bundler_version, default_lockfile.bundler_version)
+
+ new_contents = LockfileGenerator.generate(lockfile)
+ else
+ # no lockfile? just start out with the default lockfile's contents to inherit its
+ # locked gems
+ new_contents = adjusted_default_lockfile_contents
+ end
+
+ had_changes = false
+ # Now build a definition based on the given Gemfile, with the combined lockfile
+ Tempfile.create do |temp_lockfile|
+ temp_lockfile.write(new_contents)
+ temp_lockfile.flush
+
+ had_changes = write_lockfile(lockfile_definition,
+ temp_lockfile.path,
+ install: install,
+ dependency_changes: dependency_changes)
+ end
+
+ # if we had changes, bundler may have updated some common
+ # dependencies beyond the default lockfile, so re-run it
+ # 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 < 3
+ attempts += 1
+ redo
+ else
+ attempts = 1
+ end
+ end
+ end
+ ensure
+ Bundler.root = default_root
+ end
+
+ exit 1 unless checker.run(skip_base_checks: !synced_any)
+ ensure
+ @recursive = previous_recursive
+ end
+
+ # @!visibility private
+ def loaded!
+ return if loaded?
+
+ @loaded = true
+ return if lockfile_definitions.empty?
+
+ return unless lockfile_definitions.none? { |definition| definition[:current] }
+
+ # Gemfile.lock isn't explicitly specified, otherwise it would be current
+ default_lockfile_definition = lockfile_definitions.find do |definition|
+ definition[:lockfile] == Bundler.default_lockfile(force_original: true)
+ end
+ if ENV["BUNDLE_LOCKFILE"] == Bundler.default_lockfile(force_original: true) && default_lockfile_definition
+ return
+ end
+
+ raise GemfileNotFound, "Could not locate lockfile #{ENV["BUNDLE_LOCKFILE"].inspect}" if ENV["BUNDLE_LOCKFILE"]
+
+ return unless default_lockfile_definition && default_lockfile_definition[:current] == false
+
+ raise GemfileEvalError, "No lockfiles marked as default"
+ end
+
+ # @!visibility private
+ def loaded?
+ @loaded
+ end
+
+ # @!visibility private
+ def inject_preamble
+ minor_version = Gem::Version.new(::Bundler::Multilock::VERSION).segments[0..1].join(".")
+ bundle_preamble1_match = %(plugin "bundler-multilock")
+ bundle_preamble1 = <<~RUBY
+ plugin "bundler-multilock", "~> #{minor_version}"
+ RUBY
+ bundle_preamble2 = <<~RUBY
+ return unless Plugin.installed?("bundler-multilock")
+
+ Plugin.send(:load_plugin, "bundler-multilock")
+ RUBY
+
+ gemfile = Bundler.default_gemfile.read
+
+ injection_point = 0
+ while gemfile.match?(/^(?:#|\n|source)/, injection_point)
+ if gemfile[injection_point] == "\n"
+ injection_point += 1
+ else
+ injection_point = gemfile.index("\n", injection_point)
+ injection_point += 1 if injection_point
+ injection_point ||= -1
+ end
+ end
+
+ modified = inject_specific_preamble(gemfile, injection_point, bundle_preamble2, add_newline: true)
+ modified = true if inject_specific_preamble(gemfile,
+ injection_point,
+ bundle_preamble1,
+ match: bundle_preamble1_match,
+ add_newline: false)
+
+ Bundler.default_gemfile.write(gemfile) if modified
+ end
+
+ # @!visibility private
+ def reset!
+ @lockfile_definitions = []
+ @loaded = false
+ end
+
+ private
+
+ def inject_specific_preamble(gemfile, injection_point, preamble, add_newline:, match: preamble)
+ return false if gemfile.include?(match)
+
+ add_newline = false unless gemfile[injection_point - 1] == "\n"
+
+ gemfile.insert(injection_point, "\n") if add_newline
+ gemfile.insert(injection_point, preamble)
+
+ true
+ end
+
+ def write_lockfile(lockfile_definition, lockfile, install:, dependency_changes: false)
+ prepare_block = lockfile_definition[:prepare]
+
+ gemfile = lockfile_definition[:gemfile]
+ # use avoid Definition.build, so that we don't have to evaluate
+ # the gemfile multiple times, each time we need a separate definition
+ builder = Dsl.new
+ builder.eval_gemfile(gemfile, &prepare_block) if prepare_block
+ builder.eval_gemfile(gemfile)
+
+ definition = builder.to_definition(lockfile, {})
+ definition.instance_variable_set(:@dependency_changes, dependency_changes) if dependency_changes
+ orig_definition = definition.dup # we might need it twice
+
+ current_lockfile = lockfile_definition[:lockfile]
+ if current_lockfile.exist?
+ definition.instance_variable_set(:@lockfile_contents, current_lockfile.read)
+ if install
+ current_definition = builder.to_definition(current_lockfile, {})
+ begin
+ current_definition.resolve_only_locally!
+ if current_definition.missing_specs.any?
+ Bundler.with_default_lockfile(current_lockfile) do
+ Installer.install(gemfile.dirname, current_definition, {})
+ end
+ end
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure
+ # ignore
+ end
+ end
+ end
+
+ resolved_remotely = false
+ begin
+ previous_ui_level = Bundler.ui.level
+ Bundler.ui.level = "warn"
+ begin
+ definition.resolve_with_cache!
+ rescue GemNotFound, SolveFailure
+ definition = orig_definition
+
+ definition.resolve_remotely!
+ resolved_remotely = true
+ end
+ definition.lock(lockfile_definition[:lockfile], true)
+ ensure
+ Bundler.ui.level = previous_ui_level
+ end
+
+ # if we're running `bundle install` or `bundle update`, and something is missing from
+ # the secondary lockfile, install it.
+ if install && (definition.missing_specs.any? || resolved_remotely)
+ Bundler.with_default_lockfile(lockfile_definition[:lockfile]) do
+ Installer.install(lockfile_definition[:gemfile].dirname, definition, {})
+ end
+ end
+
+ !definition.nothing_changed?
+ end
+ end
+
+ reset!
+
+ @recursive = false
+ @prepare_block = nil
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/check.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/check.rb
new file mode 100644
index 00000000000..abc985edd2f
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/check.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require "set"
+
+module Bundler
+ module Multilock
+ class Check
+ class << self
+ def run
+ new.run
+ end
+ end
+
+ def initialize
+ default_lockfile_contents = Bundler.default_lockfile.read
+ @default_lockfile = LockfileParser.new(default_lockfile_contents)
+ @default_specs = @default_lockfile.specs.to_h do |spec|
+ [[spec.name, spec.platform], spec]
+ end
+ end
+
+ def run(skip_base_checks: false)
+ return true unless Bundler.default_lockfile.exist?
+
+ success = true
+ unless skip_base_checks
+ missing_specs = base_check({ gemfile: Bundler.default_gemfile, lockfile: Bundler.default_lockfile },
+ return_missing: true).to_set
+ end
+ Multilock.lockfile_definitions.each do |lockfile_definition|
+ next if lockfile_definition[:lockfile] == Bundler.default_lockfile
+
+ unless lockfile_definition[:lockfile].exist?
+ Bundler.ui.error("Lockfile #{lockfile_definition[:lockfile]} does not exist.")
+ success = false
+ end
+
+ unless skip_base_checks
+ new_missing = base_check(lockfile_definition, log_missing: missing_specs, return_missing: true)
+ success = false unless new_missing.empty?
+ missing_specs.merge(new_missing)
+ end
+ success = false unless check(lockfile_definition)
+ end
+ success
+ end
+
+ # this is mostly equivalent to the built in checks in `bundle check`, but even
+ # more conservative, and returns false instead of exiting on failure
+ def base_check(lockfile_definition, log_missing: false, return_missing: false)
+ return return_missing ? [] : false unless lockfile_definition[:lockfile].file?
+
+ Multilock.prepare_block = lockfile_definition[:prepare]
+ definition = Definition.build(lockfile_definition[:gemfile], lockfile_definition[:lockfile], false)
+ return return_missing ? [] : false unless definition.send(:current_platform_locked?)
+
+ begin
+ definition.validate_runtime!
+ Bundler.ui.silence do
+ definition.resolve_only_locally!
+ end
+ not_installed = definition.missing_specs
+ rescue RubyVersionMismatch, GemNotFound, SolveFailure
+ return return_missing ? [] : false
+ end
+
+ if log_missing
+ not_installed.each do |spec|
+ next if log_missing.include?(spec)
+
+ Bundler.ui.error "The following gems are missing" if log_missing.empty?
+ Bundler.ui.error(" * #{spec.name} (#{spec.version})")
+ end
+ end
+
+ return not_installed if return_missing
+
+ not_installed.empty? && definition.no_resolve_needed?
+ ensure
+ Multilock.prepare_block = nil
+ end
+
+ # this checks for mismatches between the default lockfile and the given lockfile,
+ # and for pinned dependencies in lockfiles requiring them
+ def check(lockfile_definition, allow_mismatched_dependencies: true)
+ success = true
+ proven_pinned = Set.new
+ needs_pin_check = []
+ lockfile = LockfileParser.new(lockfile_definition[:lockfile].read)
+ lockfile_path = lockfile_definition[:lockfile].relative_path_from(Dir.pwd)
+ unless lockfile.platforms == @default_lockfile.platforms
+ Bundler.ui.error("The platforms in #{lockfile_path} do not match the default lockfile.")
+ success = false
+ end
+ unless lockfile.bundler_version == @default_lockfile.bundler_version
+ Bundler.ui.error("bundler (#{lockfile.bundler_version}) in #{lockfile_path} " \
+ "does not match the default lockfile's version (@#{@default_lockfile.bundler_version}).")
+ success = false
+ end
+
+ specs = lockfile.specs.group_by(&:name)
+ if allow_mismatched_dependencies
+ allow_mismatched_dependencies = lockfile_definition[:allow_mismatched_dependencies]
+ end
+
+ # build list of top-level dependencies that differ from the default lockfile,
+ # and all _their_ transitive dependencies
+ if allow_mismatched_dependencies
+ transitive_dependencies = Set.new
+ # only dependencies that differ from the default lockfile
+ pending_transitive_dependencies = lockfile.dependencies.reject do |name, dep|
+ @default_lockfile.dependencies[name] == dep
+ end.map(&:first)
+
+ until pending_transitive_dependencies.empty?
+ dep = pending_transitive_dependencies.shift
+ next if transitive_dependencies.include?(dep)
+
+ transitive_dependencies << dep
+ platform_specs = specs[dep]
+ unless platform_specs
+ # should only be bundler that's missing a spec
+ raise "Could not find spec for dependency #{dep}" unless dep == "bundler"
+
+ next
+ end
+
+ pending_transitive_dependencies.concat(platform_specs.flat_map(&:dependencies).map(&:name).uniq)
+ end
+ end
+
+ # look through top-level explicit dependencies for pinned requirements
+ if lockfile_definition[:enforce_pinned_additional_dependencies]
+ find_pinned_dependencies(proven_pinned, lockfile.dependencies.each_value)
+ end
+
+ # check for conflicting requirements (and build list of pins, in the same loop)
+ specs.values.flatten.each do |spec|
+ default_spec = @default_specs[[spec.name, spec.platform]]
+
+ if lockfile_definition[:enforce_pinned_additional_dependencies]
+ # look through what this spec depends on, and keep track of all pinned requirements
+ find_pinned_dependencies(proven_pinned, spec.dependencies)
+
+ needs_pin_check << spec unless default_spec
+ end
+
+ next unless default_spec
+
+ # have to ensure Path sources are relative to their lockfile before comparing
+ same_source = if [default_spec.source, spec.source].grep(Source::Path).length == 2
+ lockfile_definition[:lockfile]
+ .dirname
+ .join(spec.source.path)
+ .ascend
+ .any?(Bundler.default_lockfile.dirname.join(default_spec.source.path))
+ else
+ default_spec.source == spec.source
+ end
+
+ next if default_spec.version == spec.version && same_source
+ next if allow_mismatched_dependencies && transitive_dependencies.include?(spec.name)
+
+ Bundler.ui.error("#{spec}#{spec.git_version} in #{lockfile_path} " \
+ "does not match the default lockfile's version " \
+ "(@#{default_spec.version}#{default_spec.git_version}); " \
+ "this may be due to a conflicting requirement, which would require manual resolution.")
+ success = false
+ end
+
+ # now that we have built a list of every gem that is pinned, go through
+ # the gems that were in this lockfile, but not the default lockfile, and
+ # ensure it's pinned _somehow_
+ needs_pin_check.each do |spec|
+ pinned = case spec.source
+ when Source::Git
+ spec.source.ref == spec.source.revision
+ when Source::Path
+ true
+ when Source::Rubygems
+ proven_pinned.include?(spec.name)
+ else
+ false
+ end
+
+ next if pinned
+
+ Bundler.ui.error("#{spec} in #{lockfile_path} has not been pinned to a specific version, " \
+ "which is required since it is not part of the default lockfile.")
+ success = false
+ end
+
+ success
+ end
+
+ private
+
+ def find_pinned_dependencies(proven_pinned, dependencies)
+ dependencies.each do |dependency|
+ dependency.requirement.requirements.each do |requirement|
+ proven_pinned << dependency.name if requirement.first == "="
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/bundler.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/bundler.rb
new file mode 100644
index 00000000000..eccc24d1f09
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/bundler.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ module Ext
+ module BundlerClassMethods
+ def self.prepended(klass)
+ super
+
+ klass.attr_writer :cache_root, :default_lockfile, :root
+ end
+
+ ::Bundler.singleton_class.prepend(self)
+
+ def app_cache(custom_path = nil)
+ super(custom_path || @cache_root)
+ end
+
+ def default_lockfile(force_original: false)
+ return @default_lockfile if @default_lockfile && !force_original
+
+ super()
+ end
+
+ def with_default_lockfile(lockfile)
+ previous_default_lockfile, @default_lockfile = @default_lockfile, lockfile
+ yield
+ ensure
+ @default_lockfile = previous_default_lockfile
+ end
+
+ def reset!
+ super
+ Multilock.reset!
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/definition.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/definition.rb
new file mode 100644
index 00000000000..35454e6f4bc
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/definition.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ module Ext
+ module Definition
+ ::Bundler::Definition.prepend(self)
+
+ def initialize(lockfile, *args)
+ # we changed the default lockfile in Bundler::Multilock.add_lockfile
+ # since DSL.evaluate was called (re-entrantly); sub the proper value in
+ if !lockfile.equal?(Bundler.default_lockfile) &&
+ Bundler.default_lockfile(force_original: true) == lockfile
+ lockfile = Bundler.default_lockfile
+ end
+ super
+ end
+
+ def validate_runtime!
+ Multilock.loaded! unless Multilock.lockfile_definitions.empty?
+
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/dsl.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/dsl.rb
new file mode 100644
index 00000000000..aea4bb1ef81
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/dsl.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "set"
+
+module Bundler
+ module Multilock
+ module Ext
+ module Dsl
+ module ClassMethods
+ ::Bundler::Dsl.singleton_class.prepend(self)
+
+ # Significant changes:
+ # * evaluate the prepare block as part of the gemfile
+ # * 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)
+ Multilock.loaded!
+ builder.to_definition(lockfile, unlock)
+ end
+ end
+
+ ::Bundler::Dsl.prepend(self)
+
+ def initialize
+ super
+ @gemfiles = Set.new
+ Multilock.loaded! unless Multilock.lockfile_definitions.empty?
+ end
+
+ # Significant changes:
+ # * allow a block
+ def eval_gemfile(gemfile, contents = nil, &block)
+ expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
+ original_gemfile = @gemfile
+ @gemfile = expanded_gemfile_path
+ @gemfiles << expanded_gemfile_path
+ contents ||= Bundler.read_file(@gemfile.to_s)
+ if block
+ instance_eval(&block)
+ else
+ instance_eval(contents.dup.tap { |x| x.untaint if RUBY_VERSION < "2.7" }, gemfile.to_s, 1)
+ end
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ message = "There was an error " \
+ "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \
+ "`#{File.basename gemfile.to_s}`: #{e.message}"
+
+ raise Bundler::Dsl::DSLError.new(message, gemfile, e.backtrace, contents)
+ ensure
+ @gemfile = original_gemfile
+ end
+
+ def lockfile(*args, **kwargs, &block)
+ return true if Multilock.loaded?
+
+ Multilock.add_lockfile(*args, builder: self, **kwargs, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin.rb
new file mode 100644
index 00000000000..b4469fd7fe0
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ module Ext
+ module PluginExt
+ module ClassMethods
+ ::Bundler::Plugin.singleton_class.prepend(self)
+
+ def load_plugin(name)
+ return if @loaded_plugin_names.include?(name)
+
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin/dsl.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin/dsl.rb
new file mode 100644
index 00000000000..36a2afd1cee
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/plugin/dsl.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ module Ext
+ module PluginExt
+ module DSL
+ ::Bundler::Plugin::DSL.include(self)
+
+ def lockfile(...)
+ # pass
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/source_list.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/source_list.rb
new file mode 100644
index 00000000000..a5fdc2bc064
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/ext/source_list.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ module Ext
+ module SourceList
+ ::Bundler::SourceList.prepend(self)
+
+ # consider them equivalent if the replacements just have a bunch of dups
+ def equivalent_sources?(lock_sources, replacement_sources)
+ super(lock_sources, replacement_sources.uniq)
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/lockfile_generator.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/lockfile_generator.rb
new file mode 100644
index 00000000000..3e7ba4eb627
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/lockfile_generator.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "bundler/lockfile_generator"
+
+module Bundler
+ module Multilock
+ # generates a lockfile based on another LockfileParser
+ class LockfileGenerator < Bundler::LockfileGenerator
+ def self.generate(lockfile)
+ new(LockfileAdapter.new(lockfile)).generate!
+ end
+
+ private
+
+ class LockfileAdapter < SimpleDelegator
+ def sources
+ self
+ end
+
+ def lock_sources
+ __getobj__.sources
+ end
+
+ def resolve
+ specs
+ end
+
+ def dependencies
+ super.values
+ end
+
+ def locked_ruby_version
+ ruby_version
+ end
+ end
+
+ private_constant :LockfileAdapter
+
+ def add_bundled_with
+ add_section("BUNDLED WITH", definition.bundler_version.to_s)
+ end
+ end
+ end
+end
diff --git a/vendor/gems/bundler-multilock/lib/bundler/multilock/version.rb b/vendor/gems/bundler-multilock/lib/bundler/multilock/version.rb
new file mode 100644
index 00000000000..a7a6a17cbd0
--- /dev/null
+++ b/vendor/gems/bundler-multilock/lib/bundler/multilock/version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Bundler
+ module Multilock
+ VERSION = "1.0.7"
+ end
+end
diff --git a/gems/bundler_lockfile_extensions/plugins.rb b/vendor/gems/bundler-multilock/plugins.rb
similarity index 50%
rename from gems/bundler_lockfile_extensions/plugins.rb
rename to vendor/gems/bundler-multilock/plugins.rb
index abe413b0eb2..bac82a989a5 100644
--- a/gems/bundler_lockfile_extensions/plugins.rb
+++ b/vendor/gems/bundler-multilock/plugins.rb
@@ -18,8 +18,33 @@
# with this program. If not, see .
#
-require "bundler_lockfile_extensions"
+require_relative "lib/bundler/multilock"
+
+# this is terrible, but we can't prepend into these modules because we only load
+# _inside_ of the CLI commands already running
+if defined?(Bundler::CLI::Check)
+ require_relative "lib/bundler/multilock/check"
+ at_exit do
+ next unless $!.nil?
+ next if $!.is_a?(SystemExit) && !$!.success?
+
+ next if Bundler::Multilock::Check.run
+
+ Bundler.ui.warn("You can attempt to fix by running `bundle install`")
+ exit 1
+ end
+end
+if defined?(Bundler::CLI::Lock)
+ at_exit do
+ next unless $!.nil?
+ next if $!.is_a?(SystemExit) && !$!.success?
+
+ Bundler::Multilock.after_install_all(install: false)
+ end
+end
Bundler::Plugin.add_hook(Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL) do |_|
- BundlerLockfileExtensions.after_install_all
+ Bundler::Multilock.after_install_all
end
+
+Bundler::Multilock.inject_preamble unless Bundler::Multilock.loaded?