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
2 changes: 1 addition & 1 deletion bundler/spec/bundler/gem_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def sha512_hexdigest(path)
mock_confirm_message "#{app_name} (#{app_version}) installed."
subject.install_gem(nil, :local)
expect(app_gem_path).to exist
gem_command :list
installed_gems_list
expect(out).to include("#{app_name} (#{app_version})")
end
end
Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/commands/check_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
bundle "config set --local path vendor/bundle"
bundle :cache

gem_command "uninstall myrack", env: { "GEM_HOME" => vendored_gems.to_s }
uninstall_gem("myrack", env: { "GEM_HOME" => vendored_gems.to_s })

bundle "check", raise_on_error: false
expect(err).to include("* myrack (1.0.0)")
Expand Down
8 changes: 4 additions & 4 deletions bundler/spec/commands/clean_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ def should_not_have_gems(*gems)
gem "myrack"
G

gem_command :list
installed_gems_list
expect(out).to include("myrack (1.0.0)").and include("thin (1.0)")
end

Expand Down Expand Up @@ -498,7 +498,7 @@ def should_not_have_gems(*gems)
end
bundle :update, all: true

gem_command :list
installed_gems_list
expect(out).to include("foo (1.0.1, 1.0)")
end

Expand All @@ -522,7 +522,7 @@ def should_not_have_gems(*gems)
bundle "clean --force"

expect(out).to include("Removing foo (1.0)")
gem_command :list
installed_gems_list
expect(out).not_to include("foo (1.0)")
expect(out).to include("myrack (1.0.0)")
end
Expand Down Expand Up @@ -556,7 +556,7 @@ def should_not_have_gems(*gems)
expect(err).to include(system_gem_path.to_s)
expect(err).to include("grant write permissions")

gem_command :list
installed_gems_list
expect(out).to include("foo (1.0)")
expect(out).to include("myrack (1.0.0)")
end
Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/install/gems/compact_index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ def start
gem "activemerchant"
end
G
gem_command "uninstall activemerchant"
uninstall_gem("activemerchant")
bundle "update rails", artifice: "compact_index"
count = lockfile.match?("CHECKSUMS") ? 2 : 1 # Once in the specs, and once in CHECKSUMS
expect(lockfile.scan(/activemerchant \(/).size).to eq(count)
Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/support/builders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ def _build(opts)
Bundler.rubygems.build(@spec, opts[:skip_validation])
end
elsif opts[:skip_validation]
@context.gem_command "build --force #{@spec.name}", dir: lib_path
Dir.chdir(lib_path) { Gem::Package.build(@spec, true) }
else
Dir.chdir(lib_path) { Gem::Package.build(@spec) }
end
Expand Down
111 changes: 95 additions & 16 deletions bundler/spec/support/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,6 @@ def gembin(cmd, options = {})
sys_exec(cmd.to_s, options)
end

def gem_command(command, options = {})
env = options[:env] || {}
env["RUBYOPT"] = opt_add(opt_add("-r#{hax}", env["RUBYOPT"]), ENV["RUBYOPT"])
options[:env] = env

# Sometimes `gem install` commands hang at dns resolution, which has a
# default timeout of 60 seconds. When that happens, the timeout for a
# command is expired too. So give `gem install` commands a bit more time.
options[:timeout] = 120

sys_exec("#{Path.gem_bin} #{command}", options)
end

def sys_exec(cmd, options = {}, &block)
env = options[:env] || {}
env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"])
Expand Down Expand Up @@ -326,9 +313,20 @@ def self.install_dev_bundler
def install_gem(path, install_dir, default = false)
raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path)

args = "--no-document --ignore-dependencies --verbose --local --install-dir #{install_dir}"

gem_command "install #{args} '#{path}'"
require "rubygems/installer"

with_simulated_platform do
installer = Gem::Installer.at(
path.to_s,
install_dir: install_dir.to_s,
document: [],
ignore_dependencies: true,
wrappers: true,
env_shebang: true,
force: true
)
installer.install
end

if default
gem = Pathname.new(path).basename.to_s.match(/(.*)\.gem/)[1]
Expand All @@ -343,6 +341,57 @@ def install_gem(path, install_dir, default = false)
end
end

def uninstall_gem(name, options = {})
require "rubygems/uninstaller"

gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s

with_env_vars("GEM_HOME" => gem_home) do
Gem.clear_paths

uninstaller = Gem::Uninstaller.new(
name,
ignore: true,
executables: true,
all: true
)
uninstaller.uninstall
ensure
Gem.clear_paths
end
end

def installed_gems_list(options = {})
gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s

# Temporarily set GEM_HOME for the command
old_gem_home = ENV["GEM_HOME"]
ENV["GEM_HOME"] = gem_home
Gem.clear_paths

begin
require "rubygems/commands/list_command"

# Capture output from the list command
output_io = StringIO.new
cmd = Gem::Commands::ListCommand.new
cmd.ui = Gem::StreamUI.new(StringIO.new, output_io, StringIO.new, false)
cmd.invoke
output = output_io.string.strip
ensure
ENV["GEM_HOME"] = old_gem_home
Gem.clear_paths
end
Comment on lines +367 to +384
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The installed_gems_list method manually saves and restores ENV["GEM_HOME"], but this codebase has an established pattern for temporary environment variable modifications using the without_env_side_effects helper (see lines 393-415). Using this helper would be more consistent with the codebase's patterns and provide better safety guarantees, as it ensures all ENV changes are properly restored even if an exception occurs during the begin block.

Copilot uses AI. Check for mistakes.

# Create a fake command execution so `out` helper works
command_execution = Spec::CommandExecution.new("gem list", timeout: 60)
command_execution.original_stdout << output
command_execution.exitstatus = 0
command_executions << command_execution

output
end

def with_built_bundler(version = nil, opts = {}, &block)
require_relative "builders"

Expand Down Expand Up @@ -374,6 +423,36 @@ def without_env_side_effects
ENV.replace(backup)
end

# Simulate the platform set by BUNDLER_SPEC_PLATFORM for in-process
# operations, mirroring what hax.rb does for subprocesses.
def with_simulated_platform
spec_platform = ENV["BUNDLER_SPEC_PLATFORM"]
unless spec_platform
return yield
end

old_arch = RbConfig::CONFIG["arch"]
old_host_os = RbConfig::CONFIG["host_os"]

if /mingw|mswin/.match?(spec_platform)
Gem.class_variable_set(:@@win_platform, nil) # rubocop:disable Style/ClassVars
RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_")
end

RbConfig::CONFIG["arch"] = spec_platform
Gem::Platform.instance_variable_set(:@local, nil)
Gem.instance_variable_set(:@platforms, [])

yield
ensure
if spec_platform
RbConfig::CONFIG["arch"] = old_arch
RbConfig::CONFIG["host_os"] = old_host_os
Gem::Platform.instance_variable_set(:@local, nil)
Gem.instance_variable_set(:@platforms, [])
end
end

def with_path_added(path)
with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do
yield
Expand Down
Loading