Skip to content
Merged
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: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 4.0.21 / 2026-09-16

### Enhancements:

* Stop vendoring resolv for two regexps. Pull request [#9877](https://github.com/ruby/rubygems/pull/9877) by Hiroshi SHIBATA
* Normalize absolute symlink targets during gem extraction. Pull request [#9860](https://github.com/ruby/rubygems/pull/9860) by Hiroshi SHIBATA
* Installs bundler 4.0.21 as a default gem.

## 4.0.20 / 2026-09-02

### Enhancements:
Expand Down
2 changes: 0 additions & 2 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,6 @@ lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb
lib/rubygems/vendor/optparse/lib/optparse/time.rb
lib/rubygems/vendor/optparse/lib/optparse/uri.rb
lib/rubygems/vendor/optparse/lib/optparse/version.rb
lib/rubygems/vendor/resolv/COPYING
lib/rubygems/vendor/resolv/lib/resolv.rb
lib/rubygems/vendor/securerandom/COPYING
lib/rubygems/vendor/securerandom/lib/securerandom.rb
lib/rubygems/vendor/timeout/COPYING
Expand Down
13 changes: 13 additions & 0 deletions bundler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 4.0.21 / 2026-09-16

### Enhancements:

* Don't update bundler to a prerelease unless asked for one. Pull request [#9869](https://github.com/ruby/rubygems/pull/9869) by Hiroshi SHIBATA
* Reject Bundler redirects that downgrade https to http. Pull request [#9859](https://github.com/ruby/rubygems/pull/9859) by Hiroshi SHIBATA

### Bug fixes:

* Support `safe.bareRepository=explicit` in git sources. Pull request [#9876](https://github.com/ruby/rubygems/pull/9876) by Hiroshi SHIBATA
* Stop resolving locally for empty CHECKSUMS entries. Pull request [#9862](https://github.com/ruby/rubygems/pull/9862) by Hiroshi SHIBATA
* Expand the git source gemspec path before the chdir. Pull request [#9845](https://github.com/ruby/rubygems/pull/9845) by Hiroshi SHIBATA

## 4.0.20 / 2026-09-02

### Enhancements:
Expand Down
4 changes: 2 additions & 2 deletions bundler/lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def install
method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)"
method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed"
method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock"
method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler"
method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
Expand Down Expand Up @@ -642,7 +642,7 @@ def inject(*)
method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status"
method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major"
method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated"
method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler"
method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
def lock
require_relative "cli/lock"
Expand Down
2 changes: 1 addition & 1 deletion bundler/lib/bundler/cli/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def run

update_bundler = options[:bundler]

Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler
Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler, pre: options[:pre]) if update_bundler

Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]

Expand Down
14 changes: 12 additions & 2 deletions bundler/lib/bundler/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def setup_domain!(options = {})

sources.cached!

if options[:add_checksums] || (!options[:local] && install_needed?)
if options[:add_checksums] || (!options[:local] && (install_needed? || @locked_spec_with_empty_checksums))
sources.remote!
true
else
Expand Down Expand Up @@ -639,7 +639,7 @@ def something_changed?
@missing_lockfile_dep ||
@unlocking_bundler ||
@locked_spec_with_missing_checksums ||
@locked_spec_with_empty_checksums ||
empty_checksums_actionable? ||
@locked_spec_with_missing_deps ||
@locked_spec_with_invalid_deps
end
Expand All @@ -648,6 +648,16 @@ def resolve_needed?
unlocking? || something_changed?
end

# Only a remote fetch can fill an empty CHECKSUMS entry, so it justifies a
# resolution only when one is coming. Resolving locally for it would repeat
# on every `Bundler.setup` without changing the lockfile. Frozen mode still
# has to refuse the entry.
def empty_checksums_actionable?
return false unless @locked_spec_with_empty_checksums

Bundler.frozen_bundle? || !sources.local_mode?
end

def should_add_extra_platforms?
!lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
end
Expand Down
9 changes: 9 additions & 0 deletions bundler/lib/bundler/fetcher/downloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def fetch(uri, headers = {}, counter = 0)
response
when Gem::Net::HTTPRedirection
new_uri = Gem::URI.parse(response["location"])
# Following a downgrade would put the credentials this request carries
# on a plaintext connection, so refuse it like Gem::RemoteFetcher does.
if https?(uri) && !https?(new_uri)
raise HTTPError, "Redirecting to a non-https URI is not allowed: #{URICredentialsFilter.credential_filtered_uri(new_uri)}"
end
if new_uri.host == uri.host
new_uri.user = uri.user
new_uri.password = uri.password
Expand Down Expand Up @@ -105,6 +110,10 @@ def request(uri, headers)

private

def https?(uri)
uri.scheme&.casecmp("https")&.zero?
end

def validate_uri_scheme!(uri)
return if /\Ahttps?\z/.match?(uri.scheme)
raise InvalidOption,
Expand Down
2 changes: 1 addition & 1 deletion bundler/lib/bundler/man/bundle-lock.1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Lock the gems specified in Gemfile\.
Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\.
.TP
\fB\-\-bundler[=BUNDLER]\fR
Update the locked version of bundler to the given version or the latest version if no version is given\.
Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\.
.TP
\fB\-\-local\fR
Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\.
Expand Down
5 changes: 3 additions & 2 deletions bundler/lib/bundler/man/bundle-lock.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ Lock the gems specified in Gemfile.
of gems or updating all gems if no list is given.

* `--bundler[=BUNDLER]`:
Update the locked version of bundler to the given version or the latest
version if no version is given.
Update the locked version of bundler. BUNDLER can be a version such as
`4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the
latest released version, which never selects a prerelease.

* `--local`:
Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the
Expand Down
2 changes: 1 addition & 1 deletion bundler/lib/bundler/man/bundle-update.1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Do not attempt to fetch gems remotely and use the gem cache instead\.
Update the locked version of Ruby to the current version of Ruby\.
.TP
\fB\-\-bundler[=BUNDLER]\fR
Update the locked version of bundler to the invoked bundler version\.
Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\.
.TP
\fB\-\-force\fR, \fB\-\-redownload\fR
Force reinstalling every gem, even if already installed\.
Expand Down
4 changes: 3 additions & 1 deletion bundler/lib/bundler/man/bundle-update.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ gem.
Update the locked version of Ruby to the current version of Ruby.

* `--bundler[=BUNDLER]`:
Update the locked version of bundler to the invoked bundler version.
Update the locked version of bundler. BUNDLER can be a version such as
`4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the
latest released version, which never selects a prerelease.

* `--force`, `--redownload`:
Force reinstalling every gem, even if already installed.
Expand Down
24 changes: 15 additions & 9 deletions bundler/lib/bundler/self_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def install_locked_bundler_and_restart_with_it_if_needed
install_and_restart_with(restart_version)
end

def update_bundler_and_restart_with_it_if_needed(target)
spec = resolve_update_version_from(target)
def update_bundler_and_restart_with_it_if_needed(target, pre: false)
spec = resolve_update_version_from(target, pre: pre)
return unless spec

version = spec.version
Expand Down Expand Up @@ -108,9 +108,9 @@ def autoswitching_applies?
lockfile_version
end

def resolve_update_version_from(target)
def resolve_update_version_from(target, pre: false)
requirement = Gem::Requirement.new(target)
update_candidate = find_latest_matching_spec(requirement)
update_candidate = find_latest_matching_spec(requirement, pre: pre)

if update_candidate.nil?
raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist"
Expand All @@ -137,18 +137,24 @@ def remote_specs
end
end

def find_latest_matching_spec(requirement)
def find_latest_matching_spec(requirement, pre: false)
Bundler.configure
local_result = find_latest_matching_spec_from_collection(local_specs, requirement)
# A bare `bundle update --bundler` must stay on releases, like `gem update
# --system`, so only `--pre` or a prerelease requirement opts into one.
allow_prerelease = pre || requirement.prerelease?

local_result = find_latest_matching_spec_from_collection(local_specs, requirement, allow_prerelease)
return local_result if local_result && requirement.specific?

remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement)
remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement, allow_prerelease)
return remote_result if local_result.nil?

[local_result, remote_result].max
[local_result, remote_result].compact.max
end

def find_latest_matching_spec_from_collection(specs, requirement)
def find_latest_matching_spec_from_collection(specs, requirement, allow_prerelease)
specs = specs.reject {|spec| spec.version.prerelease? } unless allow_prerelease

specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) }
end

Expand Down
10 changes: 6 additions & 4 deletions bundler/lib/bundler/source/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,12 @@ def fetch
def validate_spec(_spec); end

def load_gemspec(file)
dirname = Pathname.new(file).dirname
SharedHelpers.chdir(dirname.to_s) do
stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent)
stub.full_gem_path = dirname.expand_path(root).to_s
# Expand the path before the chdir below, since resolving it inside the
# block would base it on the gemspec directory instead of `root`.
gemspec_path = Pathname.new(file).expand_path(root)
SharedHelpers.chdir(gemspec_path.dirname.to_s) do
stub = Gem::StubSpecification.gemspec_stub(gemspec_path.to_s, install_path.parent, install_path.parent)
stub.full_gem_path = gemspec_path.dirname.to_s
StubSpecification.from_stub(stub)
end
end
Expand Down
10 changes: 9 additions & 1 deletion bundler/lib/bundler/source/git/git_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,15 @@ def capture3_args_for(cmd, dir)

return ["git", *opts, *cmd] unless dir

["git", "-C", dir.to_s, *opts, *cmd]
# With safe.bareRepository=explicit, git refuses to discover a bare
# repository from -C, so the cache clone is named with --git-dir.
# Working trees, like a local override, still go through -C.
location = bare_repo?(dir) ? "--git-dir" : "-C"
["git", location, dir.to_s, *opts, *cmd]
end

def bare_repo?(dir)
File.exist?(File.join(dir, "objects")) && File.exist?(File.join(dir, "HEAD"))
end

def extra_clone_args
Expand Down
2 changes: 1 addition & 1 deletion bundler/lib/bundler/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: false

module Bundler
VERSION = "4.0.20".freeze
VERSION = "4.0.21".freeze

def self.bundler_major_version
@bundler_major_version ||= gem_version.segments.first
Expand Down
13 changes: 13 additions & 0 deletions bundler/spec/bundler/fetcher/downloader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@
subject.fetch(uri, options, counter)
end
end

context "when the redirect uri downgrades https to http" do
let(:uri) { Gem::URI("https://username:password@www.uri-to-fetch.com/api/v2/endpoint") }

before { http_response["location"] = "http://www.uri-to-fetch.com/api/v2/endpoint" }

it "should raise a Bundler::HTTPError instead of following the redirect" do
expect(subject).to receive(:fetch).with(uri, options, 0).and_call_original
expect(subject).not_to receive(:fetch).with(Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 1)
expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError,
"Redirecting to a non-https URI is not allowed: http://www.uri-to-fetch.com/api/v2/endpoint")
end
end
end

context "when the request response is a Gem::Net::HTTPSuccess" do
Expand Down
26 changes: 26 additions & 0 deletions bundler/spec/bundler/source/git_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,30 @@
end
end
end

describe "#load_gemspec" do
let(:options) do
{ "uri" => uri, "revision" => "123abc" }
end

before do
allow(Bundler).to receive(:root).and_return(tmp)
allow(subject).to receive(:install_path).and_return(tmp("install/bar-123abc"))

create_file(tmp("bar/bar.gemspec"), <<~GEMSPEC)
Gem::Specification.new do |s|
s.name = "bar"
s.version = "1.0"
end
GEMSPEC
end

it "resolves a relative path against the root, not the gemspec directory" do
spec = Dir.chdir(tmp) { subject.send(:load_gemspec, "bar/bar.gemspec") }

expect(spec.name).to eq("bar")
expect(spec.loaded_from).to eq(tmp("bar/bar.gemspec").to_s)
expect(spec.full_gem_path).to eq(tmp("bar").to_s)
end
end
end
20 changes: 20 additions & 0 deletions bundler/spec/commands/lock_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,26 @@
expect(lockfile).to end_with("BUNDLED WITH\n 99\n")
end

it "does not update the bundler version in the lockfile to a prerelease version, unless the target version allows prereleases" do
build_repo4 do
build_gem "bundler", "55"
build_gem "bundler", "56.0.0.beta1"
end

system_gems "bundler-55", gem_repo: gem_repo4

install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
source "https://gem.repo4"
G
lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')

bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
expect(lockfile).to end_with("BUNDLED WITH\n 55\n")

bundle "lock --update --bundler '> 0.a' --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
expect(lockfile).to end_with("BUNDLED WITH\n 56.0.0.beta1\n")
end

it "supports adding new platforms when there's no previous lockfile" do
gemfile_with_rails_weakling_and_foo_from_repo4

Expand Down
Loading
Loading