Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions bundler/lib/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ def definition(unlock = nil)
end
end

def definition?
defined?(@definition) && @definition
end

def frozen_bundle?
frozen = settings[:deployment]
frozen ||= settings[:frozen] unless feature_flag.deployment_means_frozen?
Expand All @@ -204,7 +208,7 @@ def frozen_bundle?

def locked_gems
@locked_gems ||=
if defined?(@definition) && @definition
if definition?
definition.locked_gems
elsif Bundler.default_lockfile.file?
lock = Bundler.read_file(Bundler.default_lockfile)
Expand All @@ -213,7 +217,7 @@ def locked_gems
end

def most_specific_locked_platform?(platform)
return false unless defined?(@definition) && @definition
return false unless definition?

definition.most_specific_locked_platform == platform
end
Expand Down
5 changes: 5 additions & 0 deletions bundler/lib/bundler/dependency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def initialize(name, version, options = {}, &blk)
@gemfile = options["gemfile"]

@autorequire = Array(options["require"] || []) if options.key?("require")
@force_version = options.fetch("force_version", false)
end

# Returns the platforms this dependency is valid for, in the same order as
Expand All @@ -111,6 +112,10 @@ def should_include?
@should_include && current_env? && current_platform?
end

def force_version?
@force_version
end

def current_env?
return true unless @env
if @env.is_a?(Hash)
Expand Down
6 changes: 5 additions & 1 deletion bundler/lib/bundler/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def self.evaluate(gemfile, lockfile, unlock)
VALID_PLATFORMS = Bundler::Dependency::PLATFORM_MAP.keys.freeze

VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules
platform platforms type source install_if gemfile].freeze
platform platforms type source install_if gemfile force_version].freeze

attr_reader :gemspecs
attr_accessor :dependencies
Expand Down Expand Up @@ -371,6 +371,10 @@ def normalize_options(name, version, opts)
opts["source"] = @sources.add_rubygems_source("remotes" => source)
end

if opts.key?("force_version") && (r = Gem::Requirement.new(version)) && !r.exact?
raise GemfileError, "Cannot use force_version for inexact version requirement `#{r}`"
end

git_name = (git_names & opts.keys).last
if @git_sources[git_name]
opts["git"] = @git_sources[git_name].call(opts[git_name])
Expand Down
7 changes: 5 additions & 2 deletions bundler/lib/bundler/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def start(requirements)
@gem_version_promoter.prerelease_specified = @prerelease_specified = {}
requirements.each {|dep| @prerelease_specified[dep.name] ||= dep.prerelease? }

@forced_requirements = requirements.select(&:force_version?).map {|dep| [dep.name, dep] }.to_h

@deivid-rodriguez deivid-rodriguez Jan 1, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, if I understand this new approach, you'd need to handle required_ruby_version and required_rubygems_version specially. Probably by changing them to be unconstrained? Otherwise even if all the requirements of the forced version are prioritized, they would still not be met in the case of required_ruby_version and required_rubygems_version. Right?

This makes me wonder whether @simi's suggestion of providing a way of "monkeypatching requirements" of a gem would be better:

 gem "minitest", "~> 5.14" do |s|
  s.required_ruby_version = ">= 2.5"
end

It would also make it possible to use the feature multiple times in a Gemfile which I believe it could currently cause issues with this approach if they have conflicting requirements?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the monkey patch syntax because it requires bringing all the parents into the equation in the force_version case and I don't think adds enough extra information to make it worth it. Also, I'm a bit wracking my brain in how to implement.

Here is an implementation of two additional flags to gem, :override_ruby_version and `:override_rubygems_version: lukaso#7 which I can fold into this PR.

The usage example is:

gem 'rutabaga', git: 'git@github.com:simplybusiness/rutabaga.git', branch: 'ruby-24-or-less', override_ruby_version: true, override_rubygems_version: true

There is no easy or clear place to patch into the loading of gemspecs that I could find, so I had to place to overrides in a number of strategic places.

@deivid-rodriguez deivid-rodriguez Jan 4, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the implementation either, I can see how it could get super tricky, but if we decided to do this, I was expecting to ship a single feature to accomplish this. Either force_version: <exact_version> as in: "ignore what you have to ignore during resolution to make sure you give me this specific version", or a more finer grained approach of being able to monkeypatch the gemspec constraints used for resolution.

I'll think about it more.


verify_gemfile_dependencies_are_found!(requirements)
dg = @resolver.resolve(requirements, @base_dg)
dg.
Expand Down Expand Up @@ -103,7 +105,8 @@ def indicate_progress
include Molinillo::SpecificationProvider

def dependencies_for(specification)
specification.dependencies_for_activated_platforms
deps = specification.dependencies_for_activated_platforms
deps.map {|dep| @forced_requirements[dep.name] || dep }
end

def search_for(dependency_proxy)
Expand Down Expand Up @@ -218,8 +221,8 @@ def relevant_sources_for_vertex(vertex)

def sort_dependencies(dependencies, activated, conflicts)
dependencies.sort_by do |dependency|
dependency.all_sources = relevant_sources_for_vertex(activated.vertex_named(dependency.name))
name = name_for(dependency)
dependency.all_sources = relevant_sources_for_vertex(activated.vertex_named(name))
vertex = activated.vertex_named(name)
[
@base_dg.vertex_named(name) ? 0 : 1,
Expand Down
4 changes: 4 additions & 0 deletions bundler/lib/bundler/rubygems_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def to_lock
end
out
end

def force_version?
false
end
end

# comparison is done order independently since rubygems 3.2.0.rc.2
Expand Down
4 changes: 4 additions & 0 deletions bundler/lib/bundler/source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def version_message(spec)
message = "#{spec.name} #{spec.version}"
message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?

if Bundler.definition? && Bundler.definition.dependencies.select {|d| d.name == spec.name }.any?(&:force_version?)
message += " [version forced]"
end

if Bundler.locked_gems
locked_spec = Bundler.locked_gems.specs.find {|s| s.name == spec.name }
locked_spec_version = locked_spec.version if locked_spec
Expand Down
161 changes: 161 additions & 0 deletions bundler/spec/install/gemfile/force_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "bundle install with a gemfile that forces a gem version" do
context "with a simple conflict" do
it "works" do
install_gemfile <<-G
source "#{file_uri_for(gem_repo1)}"
gem "rack_middleware"
gem "rack", "1.0.0", :force_version => true
G

expect(the_bundle).to include_gems("rack 1.0.0", "rack_middleware 1.0")
end

it "raises when forcing to an inexact version" do
gemfile <<-G
gem "rack", "> 1.0.0", :force_version => true
G

bundle :install, :quiet => true, :raise_on_error => false

expect(exitstatus).to_not eq(0)
expect(err).to include("Cannot use force_version for inexact version requirement `> 1.0.0`.")
end

it "raises when forcing without specifying a version" do
gemfile <<-G
gem "rack", :force_version => true
G

bundle :install, :quiet => true, :raise_on_error => false

expect(exitstatus).to_not eq(0)
expect(err).to include("Cannot use force_version for inexact version requirement `>= 0`.")
end

it "works when there's no conflict" do
install_gemfile <<-G
source "#{file_uri_for(gem_repo1)}"
gem "rack", "1.0.0", :force_version => true
G

expect(the_bundle).to include_gems("rack 1.0.0")
end

it "raises when gem doesn't exist" do
gemfile <<-G
source "#{file_uri_for(gem_repo1)}"
gem "rack_middleware"
gem "rack", "2.0.0", :force_version => true
G

bundle :install, :quiet => true, :raise_on_error => false

expect(exitstatus).to_not eq(0)
expect(err).to include("Could not find gem 'rack (= 2.0.0)")
end
end

context "with a complex conflict" do
it "works" do
install_gemfile <<-G
source "#{file_uri_for(gem_repo1)}"
gem "rails", "2.3.2"
gem "activesupport", "2.3.5", :force_version => true
G

expect(the_bundle).to include_gems("rails 2.3.2", "activesupport 2.3.5", "actionpack 2.3.2", "activerecord 2.3.2", "actionmailer 2.3.2", "activeresource 2.3.2")
end

it "resolves even with clashing requirements" do
build_repo4 do
build_gem "first_parent", %w[1.0.0] do |s|
s.add_dependency "wasabi", "~> 3.6"
end
build_gem "second_parent", %w[1.0.0] do |s|
s.add_dependency "wasabi", "~> 3.1.0"
end
build_gem "wasabi", %w[3.1.0 3.6.1]
end

install_gemfile <<-G
source "#{file_uri_for(gem_repo4)}"
gem "first_parent"
gem "second_parent"
gem "wasabi", "3.6.1", :force_version => true
G

expect(the_bundle).to include_gems("first_parent 1.0.0", "second_parent 1.0.0", "wasabi 3.6.1")
end

it "resolves even with complex multi-source" do
build_repo2 do
build_gem "sfmc-fuelsdk-ruby", %w[1.3.2] do |s|
s.add_dependency "wasabi", "3.6.1"
s.add_dependency "jwt", "~> 2.2"
end
build_gem "cognito-rack", %w[0.16.6] do |s|
s.add_dependency "jwt", "~> 2.2"
end
end

build_repo4 do
build_gem "skynet", %w[2.0.2] do |s|
s.add_dependency "sfmc-fuelsdk-ruby", "1.3.2"
end
build_gem "sfmc-fuelsdk-ruby", %w[1.3.0] do |s|
s.add_dependency "wasabi", "3.1.0"
s.add_dependency "jwt", [">= 1.0.0", "~> 1.0"]
end
build_gem "wasabi", %w[3.1.0 3.6.1]
build_gem "jwt", %w[1.0.0 2.2]
end

install_gemfile <<-G
source "#{file_uri_for(gem_repo4)}"
gem "skynet"
source "#{file_uri_for(gem_repo2)}" do
gem "sfmc-fuelsdk-ruby"
gem "cognito-rack"
end
G

gemfile <<-G
source "#{file_uri_for(gem_repo4)}"
gem "sfmc-fuelsdk-ruby", "1.3.0", :force_version => true
gem "jwt", "2.2", :force_version => true
gem "skynet"
source "#{file_uri_for(gem_repo2)}" do
gem "cognito-rack"
end
G

bundle "update sfmc-fuelsdk-ruby"

expect(the_bundle).to include_gems("skynet 2.0.2", "sfmc-fuelsdk-ruby 1.3.0", "wasabi 3.1.0")
end
end

context "shows indicator that force_version was active" do
it "works" do
gemfile <<-G
source "#{file_uri_for(gem_repo1)}"
gem "rack_middleware"
gem "rack", "1.0.0", :force_version => true
G

bundle :install

expect(out).to include("Installing rack 1.0.0 [version forced]")

if Gem::Version.create(Bundler::VERSION).segments.first < 3
bundle :install

expect(out).to include("Using rack 1.0.0 [version forced]")
end
end
end
end