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?