diff --git a/configure.ac b/configure.ac index 83d76c1784346c..caf3475a38d0b9 100644 --- a/configure.ac +++ b/configure.ac @@ -3580,7 +3580,8 @@ AS_CASE(["$target_os"], AC_ARG_ENABLE(multiarch, AS_HELP_STRING([--enable-multiarch], [enable multiarch compatible directories]), - [multiarch=], [unset multiarch]) + [AS_CASE([$enableval], [no], [unset multiarch], [multiarch=])], + [unset multiarch]) AS_IF([test ${multiarch+set}], [ AC_DEFINE(ENABLE_MULTIARCH) ]) diff --git a/dir.c b/dir.c index d81ae28ee9e1b3..9f2d36b633f09f 100644 --- a/dir.c +++ b/dir.c @@ -2803,8 +2803,10 @@ glob_opendir(ruby_glob_entries_t *ent, DIR *dirp, int flags, rb_encoding *enc) } if (count >= capacity) { capacity += 256; - if (!(newp = GLOB_REALLOC_N(ent->sort.entries, capacity))) + if (!(newp = GLOB_REALLOC_N(ent->sort.entries, capacity))) { + GLOB_FREE(rdp); goto nomem; + } ent->sort.entries = newp; } ent->sort.entries[count++] = rdp; diff --git a/imemo.c b/imemo.c index f236194cd27377..a6550a11a79b53 100644 --- a/imemo.c +++ b/imemo.c @@ -560,8 +560,7 @@ static inline void imemo_fields_free(struct rb_fields *fields) { if (FL_TEST_RAW((VALUE)fields, OBJ_FIELD_HEAP)) { - shape_id_t shape_id = RBASIC_SHAPE_ID((VALUE)fields); - RUBY_ASSERT(rb_shape_complex_p(shape_id)); + RUBY_ASSERT(rb_shape_complex_p(RBASIC_SHAPE_ID((VALUE)fields))); st_free_table(fields->as.complex.table); } } diff --git a/lib/bundler.rb b/lib/bundler.rb index 8bec10c0db8746..12dde90fc5d835 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -62,6 +62,7 @@ module Bundler autoload :MatchRemoteMetadata, File.expand_path("bundler/match_remote_metadata", __dir__) autoload :Materialization, File.expand_path("bundler/materialization", __dir__) autoload :NULL, File.expand_path("bundler/constants", __dir__) + autoload :Override, File.expand_path("bundler/override", __dir__) autoload :ProcessLock, File.expand_path("bundler/process_lock", __dir__) autoload :RemoteSpecification, File.expand_path("bundler/remote_specification", __dir__) autoload :Resolver, File.expand_path("bundler/resolver", __dir__) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index a620475c18cf48..a64b67cb44d020 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -10,13 +10,14 @@ class << self attr_accessor :no_lock end - attr_writer :lockfile + attr_writer :lockfile, :overrides attr_reader( :dependencies, :locked_checksums, :locked_deps, :locked_gems, + :overrides, :platforms, :ruby_version, :lockfile, @@ -58,7 +59,7 @@ def self.build(gemfile, lockfile, unlock) # to be updated or true if all gems should be updated # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version # @param optional_groups [Array(String)] A list of optional groups - def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], overrides = []) unlock ||= {} if unlock == true @@ -88,6 +89,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles + @overrides = overrides @lockfile = lockfile @lockfile_contents = String.new @@ -633,7 +635,20 @@ def resolver end def expanded_dependencies - dependencies_with_bundler + metadata_dependencies + apply_overrides_to(dependencies_with_bundler) + metadata_dependencies + end + + def apply_overrides_to(deps) + return deps if @overrides.empty? + deps.map {|dep| apply_override_to(dep) } + end + + def apply_override_to(dep) + override = @overrides.find {|o| o.target == dep.name && o.field == :version } + return dep unless override + new_dep = dep.dup + new_dep.instance_variable_set(:@requirement, override.apply_to(dep.requirement)) + new_dep end def dependencies_with_bundler @@ -1029,7 +1044,7 @@ def converge_dependencies @locked_specs.delete(locked_specs.select {|s| s.source != dep.source }) end - unless dep.matches_spec?(locked_specs.first) + unless apply_override_to(dep).matches_spec?(locked_specs.first) @gems_to_unlock << name dep_changed = true end @@ -1039,9 +1054,25 @@ def converge_dependencies @changed_dependencies << name if dep_changed end + converge_overrides_outside_dependencies + @changed_dependencies.any? end + def converge_overrides_outside_dependencies + @overrides.each do |override| + next unless override.target.is_a?(String) + + name = override.target + next if @changed_dependencies.include?(name) + next if @dependencies.any? {|d| d.name == name } + next if @originally_locked_specs[name].empty? + + @gems_to_unlock << name + @changed_dependencies << name + end + end + # Remove elements from the locked specs that are expired. This will most # commonly happen if the Gemfile has changed since the lockfile was last # generated @@ -1273,7 +1304,7 @@ def unlocked_resolution_base def new_resolution_base(last_resolve:, unlock:) new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms - Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms) + Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, overrides: @overrides) end def new_resolver(base) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 6f06c4e918797d..c7a7d855eef179 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -22,7 +22,7 @@ def self.evaluate(gemfile, lockfile, unlock) GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z} GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z} - attr_reader :gemspecs, :gemfile + attr_reader :gemspecs, :gemfile, :overrides attr_accessor :dependencies def initialize @@ -40,6 +40,7 @@ def initialize @gemfile = nil @gemfiles = [] @lockfile = nil + @overrides = [] add_git_sources end @@ -184,10 +185,31 @@ def github(repo, options = {}) with_source(git_source) { yield } end + SUPPORTED_OVERRIDE_FIELDS = [:version].freeze + SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS = [:ignore_upper].freeze + + def override(target, **operations) + validate_override_target!(target) + + if target == :all && operations.key?(:version) + raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem" + end + + operations.each do |field, operation| + validate_override_field!(field) + validate_override_operation!(operation) + validate_override_uniqueness!(target, field) + end + + operations.each do |field, operation| + @overrides << Override.new(target, field, operation) + end + end + def to_definition(lockfile, unlock) check_primary_source_safety lockfile = @lockfile unless @lockfile.nil? - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides) end def group(*args, &blk) @@ -244,6 +266,34 @@ def check_primary_source_safety private + def validate_override_target!(target) + return if target == :all + return if target.is_a?(String) + raise ArgumentError, "override target must be :all or a gem name string, got #{target.inspect}" + end + + def validate_override_field!(field) + return if SUPPORTED_OVERRIDE_FIELDS.include?(field) + raise ArgumentError, "unsupported override field `#{field}:`; only `version:` is currently supported" + end + + def validate_override_operation!(operation) + case operation + when String, nil + # ok + when Symbol + return if SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS.include?(operation) + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + else + raise ArgumentError, "override operation must be a String, Symbol, or nil, got #{operation.inspect}" + end + end + + def validate_override_uniqueness!(target, field) + return unless @overrides.any? {|o| o.target == target && o.field == field } + raise ArgumentError, "duplicate override for #{target.inspect} `#{field}:`" + end + def add_dependency(name, version = nil, options = {}) options["gemfile"] = @gemfile options["source"] ||= @source diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index a837f994cd90da..9e21f8f1d2a1ab 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -115,6 +115,17 @@ def initialize(lockfile, strict: false) "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock." end + @valid = lockfile.strip.empty? || + lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } + + unless @valid + SharedHelpers.feature_deprecated!( + "Your #{@lockfile_path} does not appear to be a valid lockfile. " \ + "Run `rm #{@lockfile_path}` and then `bundle install` to generate a new lockfile. " \ + "This will raise a LockfileError in a future version of Bundler." + ) + end + lockfile.split(/((?:\r?\n)+)/) do |line| # split alternates between the line and the following whitespace next @pos.advance!(line) if line.match?(/^\s*$/) @@ -164,6 +175,10 @@ def may_include_redundant_platform_specific_gems? bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2") end + def valid? + @valid + end + private TYPES = { diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index f49d8e568c9bed..031eef686c9427 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ADD" "1" "April 2026" "" +.TH "BUNDLE\-ADD" "1" "May 2026" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index 9dbd4f1d3a1f90..246daeae53edb1 100644 --- a/lib/bundler/man/bundle-binstubs.1 +++ b/lib/bundler/man/bundle-binstubs.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-BINSTUBS" "1" "April 2026" "" +.TH "BUNDLE\-BINSTUBS" "1" "May 2026" "" .SH "NAME" \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index e2052ab0ac1606..38ea0479612404 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CACHE" "1" "April 2026" "" +.TH "BUNDLE\-CACHE" "1" "May 2026" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 index 825a2889d57a88..6cd474d90ab795 100644 --- a/lib/bundler/man/bundle-check.1 +++ b/lib/bundler/man/bundle-check.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CHECK" "1" "April 2026" "" +.TH "BUNDLE\-CHECK" "1" "May 2026" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index 0eae33d08d1926..eb90636c17170b 100644 --- a/lib/bundler/man/bundle-clean.1 +++ b/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CLEAN" "1" "April 2026" "" +.TH "BUNDLE\-CLEAN" "1" "May 2026" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 8835ae75bd9e92..61487ca55e7921 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONFIG" "1" "April 2026" "" +.TH "BUNDLE\-CONFIG" "1" "May 2026" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index c86b90e3bd23f1..5d3f65365faaef 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONSOLE" "1" "April 2026" "" +.TH "BUNDLE\-CONSOLE" "1" "May 2026" "" .SH "NAME" \fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 index fe9a8a35b9afa8..4c59871b66ad55 100644 --- a/lib/bundler/man/bundle-doctor.1 +++ b/lib/bundler/man/bundle-doctor.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-DOCTOR" "1" "April 2026" "" +.TH "BUNDLE\-DOCTOR" "1" "May 2026" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-env.1 b/lib/bundler/man/bundle-env.1 index 29c4ac2a8e21a5..25fcb648917f76 100644 --- a/lib/bundler/man/bundle-env.1 +++ b/lib/bundler/man/bundle-env.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ENV" "1" "April 2026" "" +.TH "BUNDLE\-ENV" "1" "May 2026" "" .SH "NAME" \fBbundle\-env\fR \- Print information about the environment Bundler is running under .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 index fec7bee39c338c..c3a6a09d578ae6 100644 --- a/lib/bundler/man/bundle-exec.1 +++ b/lib/bundler/man/bundle-exec.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-EXEC" "1" "April 2026" "" +.TH "BUNDLE\-EXEC" "1" "May 2026" "" .SH "NAME" \fBbundle\-exec\fR \- Execute a command in the context of the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1 index 2eb07a6c8d8cad..caee1f81dd64a0 100644 --- a/lib/bundler/man/bundle-fund.1 +++ b/lib/bundler/man/bundle-fund.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-FUND" "1" "April 2026" "" +.TH "BUNDLE\-FUND" "1" "May 2026" "" .SH "NAME" \fBbundle\-fund\fR \- Lists information about gems seeking funding assistance .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index bdb84faebdd0a2..87d756824698c9 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-GEM" "1" "April 2026" "" +.TH "BUNDLE\-GEM" "1" "May 2026" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 index 6e6ad14624d739..3bcfd047e5464b 100644 --- a/lib/bundler/man/bundle-help.1 +++ b/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-HELP" "1" "April 2026" "" +.TH "BUNDLE\-HELP" "1" "May 2026" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index b18b70309c505d..49c2295f8c981f 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INFO" "1" "April 2026" "" +.TH "BUNDLE\-INFO" "1" "May 2026" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index 5ea1c3b4783733..63e2376c3fd3fb 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INIT" "1" "April 2026" "" +.TH "BUNDLE\-INIT" "1" "May 2026" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index c2f9bfeea1f79b..a764d031ed12c3 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INSTALL" "1" "April 2026" "" +.TH "BUNDLE\-INSTALL" "1" "May 2026" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1 index e99cf67638f392..3af277ef867a95 100644 --- a/lib/bundler/man/bundle-issue.1 +++ b/lib/bundler/man/bundle-issue.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ISSUE" "1" "April 2026" "" +.TH "BUNDLE\-ISSUE" "1" "May 2026" "" .SH "NAME" \fBbundle\-issue\fR \- Get help reporting Bundler issues .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1 index eb5f7203ec1d76..ab5996d2be7a23 100644 --- a/lib/bundler/man/bundle-licenses.1 +++ b/lib/bundler/man/bundle-licenses.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LICENSES" "1" "April 2026" "" +.TH "BUNDLE\-LICENSES" "1" "May 2026" "" .SH "NAME" \fBbundle\-licenses\fR \- Print the license of all gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 index 69276822d2cd00..e759e0d4493b3c 100644 --- a/lib/bundler/man/bundle-list.1 +++ b/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LIST" "1" "April 2026" "" +.TH "BUNDLE\-LIST" "1" "May 2026" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index ba1915af2e1597..396c8ff6ca9766 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LOCK" "1" "April 2026" "" +.TH "BUNDLE\-LOCK" "1" "May 2026" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index 99166e8580f4cf..2aab59f14b9b16 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OPEN" "1" "April 2026" "" +.TH "BUNDLE\-OPEN" "1" "May 2026" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index 87725b9029e3da..b739234d8daf07 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OUTDATED" "1" "April 2026" "" +.TH "BUNDLE\-OUTDATED" "1" "May 2026" "" .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 index 486e2d4406bf6d..39b71112635311 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLATFORM" "1" "April 2026" "" +.TH "BUNDLE\-PLATFORM" "1" "May 2026" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index 1c3feead7630ee..d182c7789ba12f 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLUGIN" "1" "April 2026" "" +.TH "BUNDLE\-PLUGIN" "1" "May 2026" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 index cbfc51399a7029..f6cc06657161dc 100644 --- a/lib/bundler/man/bundle-pristine.1 +++ b/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PRISTINE" "1" "April 2026" "" +.TH "BUNDLE\-PRISTINE" "1" "May 2026" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 index f8981f9fcfcbe1..2ca40e74db4fb2 100644 --- a/lib/bundler/man/bundle-remove.1 +++ b/lib/bundler/man/bundle-remove.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-REMOVE" "1" "April 2026" "" +.TH "BUNDLE\-REMOVE" "1" "May 2026" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index aaf146fa271b8d..a2142694b8d317 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-SHOW" "1" "April 2026" "" +.TH "BUNDLE\-SHOW" "1" "May 2026" "" .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index e5f18f2a1e2674..6a749644e31a1d 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-UPDATE" "1" "April 2026" "" +.TH "BUNDLE\-UPDATE" "1" "May 2026" "" .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 index 24b5dcef45d7a5..751a408312a900 100644 --- a/lib/bundler/man/bundle-version.1 +++ b/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VERSION" "1" "April 2026" "" +.TH "BUNDLE\-VERSION" "1" "May 2026" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index 492de63295a132..167815631a2b64 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE" "1" "April 2026" "" +.TH "BUNDLE" "1" "May 2026" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index db04250b8b8c7e..0874bb5a4a7baf 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "GEMFILE" "5" "April 2026" "" +.TH "GEMFILE" "5" "May 2026" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" @@ -460,6 +460,42 @@ The \fBgemspec\fR method adds any runtime dependencies as gem requirements in th The \fBgemspec\fR method supports optional \fB:path\fR, \fB:glob\fR, \fB:name\fR, and \fB:development_group\fR options, which control where bundler looks for the \fB\.gemspec\fR, the glob it uses to look for the gemspec (defaults to: \fB{,*,*/*}\.gemspec\fR), what named \fB\.gemspec\fR it uses (if more than one is present), and which group development dependencies are included in\. .P When a \fBgemspec\fR dependency encounters version conflicts during resolution, the local version under development will always be selected \-\- even if there are remote versions that better match other requirements for the \fBgemspec\fR gem\. +.SH "OVERRIDE" +The \fBoverride\fR directive rewrites the version requirement on another gem before resolution runs\. It targets the common case where an upstream gem's published metadata is too narrow on the current project's machine \-\- a stale upper bound, an unwanted floor, or a transitive pin that has to be lifted\. +.IP "" 4 +.nf +override , : +.fi +.IP "" 0 +.P +\fB\fR is a gem name string\. \fB\fR is \fBversion:\fR\. \fB\fR is one of: +.IP "\(bu" 4 +a version requirement string (e\.g\. \fB">= 8\.0"\fR), which \fBreplaces\fR the target's version requirement absolutely\. The original requirement, both direct and transitive, is discarded in favour of the override\. +.IP "\(bu" 4 +\fB:ignore_upper\fR, which removes upper\-bound operators (\fB<\fR and \fB<=\fR) from the existing requirement and folds \fB~>\fR into its lower bound (\fB~> 1\.5\fR becomes \fB>= 1\.5\fR)\. Other operators, including \fB!=\fR, are preserved\. +.IP "\(bu" 4 +\fBnil\fR, which collapses the requirement to \fB>= 0\fR (no constraint at all)\. +.IP "" 0 +.P +Multiple \fBoverride\fR calls for distinct targets are allowed; declaring the same \fBtarget\fR and \fBfield\fR twice is an error\. +.IP "" 4 +.nf +source "https://rubygems\.org" + +# Force every reference to "rails" \-\- direct or transitive \-\- to >= 8\.0\. +override "rails", version: ">= 8\.0" + +# Strip the upper bound on nokogiri\. +override "nokogiri", version: :ignore_upper + +# Drop the version pin on legacy entirely\. +override "legacy", version: nil + +gem "rails", "~> 7\.0" +.fi +.IP "" 0 +.P +The override only affects resolution; \fBGemfile\.lock\fR continues to reflect the resolved versions, not the rewritten requirements\. .SH "SOURCE PRIORITY" When attempting to locate a gem to satisfy a gem requirement, bundler uses the following priority order: .IP "1." 4 diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index 18d7bb826e4439..1779fc0a015209 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -541,6 +541,45 @@ When a `gemspec` dependency encounters version conflicts during resolution, the local version under development will always be selected -- even if there are remote versions that better match other requirements for the `gemspec` gem. +## OVERRIDE + +The `override` directive rewrites the version requirement on another gem +before resolution runs. It targets the common case where an upstream gem's +published metadata is too narrow on the current project's machine -- a stale +upper bound, an unwanted floor, or a transitive pin that has to be lifted. + + override , : + +`` is a gem name string. `` is `version:`. `` is +one of: + + * a version requirement string (e.g. `">= 8.0"`), which **replaces** the + target's version requirement absolutely. The original requirement, both + direct and transitive, is discarded in favour of the override. + * `:ignore_upper`, which removes upper-bound operators (`<` and `<=`) from + the existing requirement and folds `~>` into its lower bound (`~> 1.5` + becomes `>= 1.5`). Other operators, including `!=`, are preserved. + * `nil`, which collapses the requirement to `>= 0` (no constraint at all). + +Multiple `override` calls for distinct targets are allowed; declaring the +same `target` and `field` twice is an error. + + source "https://rubygems.org" + + # Force every reference to "rails" -- direct or transitive -- to >= 8.0. + override "rails", version: ">= 8.0" + + # Strip the upper bound on nokogiri. + override "nokogiri", version: :ignore_upper + + # Drop the version pin on legacy entirely. + override "legacy", version: nil + + gem "rails", "~> 7.0" + +The override only affects resolution; `Gemfile.lock` continues to reflect +the resolved versions, not the rewritten requirements. + ## SOURCE PRIORITY When attempting to locate a gem to satisfy a gem requirement, diff --git a/lib/bundler/override.rb b/lib/bundler/override.rb new file mode 100644 index 00000000000000..1ca6d2fde5d4e6 --- /dev/null +++ b/lib/bundler/override.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Bundler + class Override + UPPER_BOUND_OPERATORS = ["<", "<="].freeze + + attr_reader :target, :field, :operation + + def initialize(target, field, operation) + @target = target + @field = field + @operation = operation + end + + def apply_to(requirement) + case operation + when nil + Gem::Requirement.default + when :ignore_upper + remove_upper_bounds(requirement) + when String + Gem::Requirement.new(operation) + else + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + end + end + + private + + def remove_upper_bounds(requirement) + return Gem::Requirement.default if requirement.nil? || requirement.none? + + preserved = requirement.requirements.filter_map do |op, version| + if UPPER_BOUND_OPERATORS.include?(op) + nil + elsif op == "~>" + [">=", version] + else + [op, version] + end + end + + return Gem::Requirement.default if preserved.empty? + + Gem::Requirement.new(preserved.map {|op, v| "#{op} #{v}" }) + end + end +end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 3c361d8ea51a9f..5e934e2a12e6b1 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -510,7 +510,7 @@ def requirement_to_range(requirement) end def to_dependency_hash(dependencies, packages) - dependencies.inject({}) do |deps, dep| + apply_overrides(dependencies).inject({}) do |deps, dep| package = packages[dep.name] current_req = deps[package] @@ -526,6 +526,16 @@ def to_dependency_hash(dependencies, packages) end end + def apply_overrides(dependencies) + return dependencies if @base.overrides.empty? + + dependencies.map do |dep| + override = @base.overrides.find {|o| o.target == dep.name && o.field == :version } + next dep unless override + Gem::Dependency.new(dep.name, override.apply_to(dep.requirement)) + end + end + def bundler_not_found_message(conflict_dependencies) candidate_specs = filter_matching_specs(default_bundler_source.specs.search("bundler"), conflict_dependencies) diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb index 932a92ff4156ca..00bdd08303dfd1 100644 --- a/lib/bundler/resolver/base.rb +++ b/lib/bundler/resolver/base.rb @@ -5,9 +5,10 @@ module Bundler class Resolver class Base - attr_reader :packages, :requirements, :source_requirements, :locked_specs + attr_reader :packages, :requirements, :source_requirements, :locked_specs, :overrides def initialize(source_requirements, dependencies, base, platforms, options) + @overrides = options.delete(:overrides) || [] @source_requirements = source_requirements @locked_specs = options[:locked_specs] diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 82e782ba257a6a..366a23aea725d7 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -220,10 +220,11 @@ def generate_bin(spec, options = {}) # Some gem authors put absolute paths in their gemspec # and we have to save them from themselves spec.files = spec.files.filter_map do |path| - next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path) + pathname = Pathname.new(path) + next path unless pathname.absolute? next if File.directory?(path) begin - Pathname.new(path).relative_path_from(gem_dir).to_s + pathname.relative_path_from(gem_dir).to_s rescue ArgumentError path end diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb index 78e968fe493084..10556871b2a68e 100644 --- a/lib/rubygems/win_platform.rb +++ b/lib/rubygems/win_platform.rb @@ -8,7 +8,6 @@ module Gem WIN_PATTERNS = [ /bccwin/i, - /cygwin/i, /djgpp/i, /mingw/i, /mswin/i, diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb index 8c7d5667ac6657..8c4a5a0331e238 100644 --- a/spec/bundler/bundler/definition_spec.rb +++ b/spec/bundler/bundler/definition_spec.rb @@ -3,6 +3,24 @@ require "bundler/definition" RSpec.describe Bundler::Definition do + describe "#overrides" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } + end + + subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) } + + it "defaults to an empty array" do + expect(subject.overrides).to eq([]) + end + + it "is writable" do + override = Bundler::Override.new("rails", :version, ">= 8.0") + subject.overrides = [override] + expect(subject.overrides).to eq([override]) + end + end + describe "#lock" do before do allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 6ba2e728b57323..39f745c05efe63 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -366,4 +366,108 @@ end end end + + describe "#override" do + it "stores an Override for a gem with a version: operation" do + subject.override("rails", version: ">= 8.0") + + expect(subject.overrides.size).to eq(1) + override = subject.overrides.first + expect(override.target).to eq("rails") + expect(override.field).to eq(:version) + expect(override.operation).to eq(">= 8.0") + end + + it "accepts :ignore_upper as the operation" do + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.first.operation).to eq(:ignore_upper) + end + + it "accepts nil as the operation" do + subject.override("legacy", version: nil) + expect(subject.overrides.first.operation).to be_nil + end + + it "appends to overrides across multiple statements" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.map(&:target)).to eq(["rails", "nokogiri"]) + end + + it "is empty by default" do + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when target is :all and version: is given" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "rejects :all + version: even when other fields are also given" do + expect do + subject.override(:all, required_ruby_version: :ignore_upper, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "does not record any override when :all + version: is rejected" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError) + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when target is neither :all nor a string" do + expect do + subject.override(:rails, version: ">= 8.0") + end.to raise_error(ArgumentError, /target must be :all or a gem name string/) + end + + it "raises ArgumentError for an unsupported field" do + expect do + subject.override("rails", required_ruby_version: :ignore_upper) + end.to raise_error(ArgumentError, /unsupported override field `required_ruby_version:`/) + end + + it "raises ArgumentError for a non-string, non-symbol, non-nil operation" do + expect do + subject.override("rails", version: 42) + end.to raise_error(ArgumentError, /override operation must be a String, Symbol, or nil/) + end + + it "raises ArgumentError for an unsupported symbol operation" do + expect do + subject.override("rails", version: :explode) + end.to raise_error(ArgumentError, /unsupported override operation/) + end + + it "rejects atomically when one field in a multi-field call is invalid" do + expect do + subject.override("rails", version: ">= 8.0", required_ruby_version: :ignore_upper) + end.to raise_error(ArgumentError, /unsupported override field/) + expect(subject.overrides).to eq([]) + end + + it "raises ArgumentError when the same target and field are overridden twice" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError, /duplicate override for "rails" `version:`/) + end + + it "keeps the original override when a duplicate is rejected" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError) + expect(subject.overrides.size).to eq(1) + expect(subject.overrides.first.operation).to eq(">= 8.0") + end + + it "allows different targets with the same field" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.size).to eq(2) + end + end end diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb index f38da2c9932183..4b493a37576b52 100644 --- a/spec/bundler/bundler/lockfile_parser_spec.rb +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -129,6 +129,7 @@ shared_examples_for "parsing" do it "parses correctly" do + expect(subject.valid?).to be(true) expect(subject.sources).to eq sources expect(subject.dependencies).to eq dependencies expect(subject.specs).to eq specs @@ -191,6 +192,66 @@ include_examples "parsing" end + context "when the content does not contain any recognized lockfile sections" do + let(:lockfile_contents) { "hello world\nlorem ipsum\n" } + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content looks like a Gemfile DSL" do + let(:lockfile_contents) { <<~G } + source "https://rubygems.org" + gem "rake" + G + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content is empty" do + let(:lockfile_contents) { "" } + + it "does not raise and is valid" do + expect { subject }.not_to raise_error + expect(subject.valid?).to be(true) + end + end + context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" } let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join } diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb new file mode 100644 index 00000000000000..da3b5aad87a086 --- /dev/null +++ b/spec/bundler/bundler/override_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Override do + describe "#apply_to" do + context "when operation is a version spec string" do + it "replaces the existing requirement entirely" do + override = described_class.new("rails", :version, ">= 8.0") + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 8.0")) + end + + it "ignores the existing requirement regardless of its content" do + override = described_class.new("rails", :version, "= 1.0") + result = override.apply_to(Gem::Requirement.new(">= 99.0")) + expect(result).to eq(Gem::Requirement.new("= 1.0")) + end + end + + context "when operation is :ignore_upper" do + it "removes < and <= operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0")) + end + + it "keeps >, >=, = operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("> 1.0", "<= 2.0")) + expect(result).to eq(Gem::Requirement.new("> 1.0")) + end + + it "converts ~> to >= preserving the lower bound" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("~> 1.5")) + expect(result).to eq(Gem::Requirement.new(">= 1.5")) + end + + it "preserves != exclusion constraints" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "!= 1.5.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0", "!= 1.5.0")) + end + + it "returns the default requirement when only upper bounds remain" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is nil" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(nil)).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is already the default" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(Gem::Requirement.default)).to eq(Gem::Requirement.default) + end + end + + context "when operation is nil" do + it "returns the default requirement" do + override = described_class.new("rails", :version, nil) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + end + + context "when operation is unsupported" do + it "raises ArgumentError" do + override = described_class.new("rails", :version, 42) + expect { override.apply_to(Gem::Requirement.default) }.to raise_error(ArgumentError, /unsupported override operation/) + end + end + end +end diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb new file mode 100644 index 00000000000000..7a7f8078a81753 --- /dev/null +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +RSpec.describe "override DSL" do + context "with a version: string operation" do + it "replaces a direct dependency requirement with the override version spec" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "replaces a transitive dependency requirement" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "replaces the requirement even when the Gemfile pins a different version" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack", "= 1.0.0" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "applies the override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do + build_repo2 do + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + + install_gemfile <<-G + source "https://gem.repo2" + override "has_prerelease", version: "= 1.1.pre" + gem "has_prerelease" + G + + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end + end + + context "with a version: :ignore_upper operation" do + it "strips a < upper bound on a direct dependency" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "< 1.0" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "folds ~> into >= so newer versions become reachable" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "~> 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end + + context "with a version: nil operation" do + it "drops a direct dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack", "= 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "drops a transitive dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "applies a transitive-only override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + end +end diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb index b91deb7ed3e6f2..fb9c0811493f2c 100644 --- a/spec/bundler/support/windows_tag_group.rb +++ b/spec/bundler/support/windows_tag_group.rb @@ -189,6 +189,8 @@ module WindowsTagGroup "spec/bundler/uri_normalizer_spec.rb", "spec/install/gems/no_build_extension_spec.rb", "spec/install/gems/no_install_plugin_spec.rb", + "spec/bundler/override_spec.rb", + "spec/install/gemfile/override_spec.rb", ], }.freeze end diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 04e15b6d87db72..cad9bf5cc89a6d 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1846,19 +1846,21 @@ def test_sort! assert_equal([1, 2, 3, 4], a) end - def test_freeze_inside_sort! + def test_freeze_inside_sort_bang array = [1, 2, 3, 4, 5] frozen_array = nil assert_raise(FrozenError) do count = 0 array.sort! do |a, b| - array.freeze if (count += 1) == 6 + array.freeze if (count += 1) == 3 frozen_array ||= array.map.to_a if array.frozen? b <=> a end end assert_equal(frozen_array, array) + end + def test_freeze_inside_sort_bang_non_numeric_block object = Object.new array = [1, 2, 3, 4, 5] object.define_singleton_method(:>){|_| array.freeze; true} @@ -1867,7 +1869,9 @@ def test_freeze_inside_sort! object end end + end + def test_freeze_inside_sort_bang_non_numeric_no_block object = Object.new array = [object, object] object.define_singleton_method(:>){|_| array.freeze; true} diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock index f96e796794e4fa..989b0b0412023e 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b37650fabd8ba515910a0dc089dcb6348eb3c35fbf91698cb226435be2babc" +checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73b806faa66006e491458b48a78725621c1ac5a2a6efe2614c90711a7780b80" +checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9" dependencies = [ "bindgen", "lazy_static", diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml index e9e21e0f4f8aba..47e978ceb4bc29 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.125" +rb-sys = "0.9.127" diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock index 358d0f5a41fc45..e8ee8f237c5d69 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock @@ -146,18 +146,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b37650fabd8ba515910a0dc089dcb6348eb3c35fbf91698cb226435be2babc" +checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73b806faa66006e491458b48a78725621c1ac5a2a6efe2614c90711a7780b80" +checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9" dependencies = [ "bindgen", "lazy_static", diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml index f9583c0e337c66..6595b6aafa6eee 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.125" +rb-sys = "0.9.127"