switch to extracted bundler-multilock gem
well, somewhat. it's vendored for now, bugfixes and improvements have been going into that gem, and we'd like those fixes in Canvas. Change-Id: Ib4f30926acddb364779b9f91b1ee129ba6b17ff0 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/327463 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jeremy Stanley <jeremy@instructure.com> Reviewed-by: Jacob Burroughs <jburroughs@instructure.com> QA-Review: Cody Cutrer <cody@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com> Build-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
d7e35b1ac0
commit
450d78155b
|
@ -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 && \
|
||||
\
|
||||
|
|
68
Gemfile
68
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
--color
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module BundlerLockfileExtensions
|
||||
module Bundler
|
||||
module Dsl
|
||||
def add_lockfile(*args, **kwargs)
|
||||
BundlerLockfileExtensions.add_lockfile(*args, builder: self, **kwargs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
bundle check || bundle install
|
||||
bundle exec rspec spec
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
module Multilock
|
||||
VERSION = "1.0.7"
|
||||
end
|
||||
end
|
|
@ -18,8 +18,33 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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?
|
Loading…
Reference in New Issue