diff --git a/bundler/lib/bundler.rb b/bundler/lib/bundler.rb index c72ad27c401b..cf4ca80eb30a 100644 --- a/bundler/lib/bundler.rb +++ b/bundler/lib/bundler.rb @@ -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? @@ -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) @@ -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 diff --git a/bundler/lib/bundler/dependency.rb b/bundler/lib/bundler/dependency.rb index af07e8bc363c..c35a40e5319d 100644 --- a/bundler/lib/bundler/dependency.rb +++ b/bundler/lib/bundler/dependency.rb @@ -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 @@ -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) diff --git a/bundler/lib/bundler/dsl.rb b/bundler/lib/bundler/dsl.rb index 1cc7908b8a88..9cf23eb278f1 100644 --- a/bundler/lib/bundler/dsl.rb +++ b/bundler/lib/bundler/dsl.rb @@ -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 @@ -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]) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 636dc8af4621..e463586984bd 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -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 + verify_gemfile_dependencies_are_found!(requirements) dg = @resolver.resolve(requirements, @base_dg) dg. @@ -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) @@ -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, diff --git a/bundler/lib/bundler/rubygems_ext.rb b/bundler/lib/bundler/rubygems_ext.rb index 0322b06d0787..824b873c3b54 100644 --- a/bundler/lib/bundler/rubygems_ext.rb +++ b/bundler/lib/bundler/rubygems_ext.rb @@ -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 diff --git a/bundler/lib/bundler/source.rb b/bundler/lib/bundler/source.rb index be00143f5a40..98868c9e53ad 100644 --- a/bundler/lib/bundler/source.rb +++ b/bundler/lib/bundler/source.rb @@ -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 diff --git a/bundler/spec/install/gemfile/force_spec.rb b/bundler/spec/install/gemfile/force_spec.rb new file mode 100644 index 000000000000..eb73b2dd96f5 --- /dev/null +++ b/bundler/spec/install/gemfile/force_spec.rb @@ -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