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 lib/rubygems/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def handles?(args)
def handle_options(args)
args = add_extra_args(args)
check_deprecated_options(args)
@options = Marshal.load Marshal.dump @defaults # deep copy
@options = Gem::Util.deep_dup @defaults
parser.parse!(args)
@options[:args] = args
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/commands/exec_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def execute
def handle_options(args)
args = add_extra_args(args)
check_deprecated_options(args)
@options = Marshal.load Marshal.dump @defaults # deep copy
@options = Gem::Util.deep_dup @defaults
parser.order!(args) do |v|
# put the non-option back at the front of the list of arguments
args.unshift(v)
Expand Down
4 changes: 2 additions & 2 deletions lib/rubygems/config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ def initialize(args)
@use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH
@credential_store = normalize_credential_store(ENV["RUBYGEMS_CREDENTIAL_STORE"], DEFAULT_CREDENTIAL_STORE)

operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
operating_system_config = Gem::Util.deep_dup(OPERATING_SYSTEM_DEFAULTS)
platform_config = Gem::Util.deep_dup(PLATFORM_DEFAULTS)
system_config = load_file SYSTEM_WIDE_CONFIG_FILE
user_config = load_file config_file_name

Expand Down
4 changes: 3 additions & 1 deletion lib/rubygems/ext/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {})

require "open3"
# Set $SOURCE_DATE_EPOCH for the subprocess.
build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env)
# Under Ruby::Box mkmf makes RbConfig.expand recurse until SystemStackError.
# Drop $RUBY_BOX last so no caller can restore it.
build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env).merge("RUBY_BOX" => nil)
# A single-element command would be parsed as a shell command line,
# splitting an unquoted command path containing spaces. Use the
# [cmdname, argv0] form to keep exec semantics.
Expand Down
20 changes: 20 additions & 0 deletions lib/rubygems/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ def self.glob_files_in_dir(glob, base_path)
end
end

##
# Duplicates +obj+ without Marshal, which cannot resolve Gem:: constants from
# the root box under Ruby::Box (RUBY_BOX=1). Hashes and arrays are copied
# recursively, anything else with a plain +dup+, so an object's internals stay
# shared with the original, as are hash keys. Shared references are not
# preserved, and a cyclic +obj+ raises SystemStackError.

def self.deep_dup(obj) # :nodoc:
case obj
when Hash then obj.to_h {|k, v| [k, deep_dup(v)] }
when Array then obj.map {|e| deep_dup(e) }
else
begin
obj.dup
rescue TypeError
obj
end
end
end

##
# Corrects +path+ (usually returned by `Gem::URI.parse().path` on Windows), that
# comes with a leading slash.
Expand Down
16 changes: 16 additions & 0 deletions test/rubygems/test_gem_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "helper"
require "rubygems/command"
require "open3"

class Gem::Command
public :parser
Expand Down Expand Up @@ -399,4 +400,19 @@ def test_show_lookup_failure_suggestions_remote

assert_equal expected, @ui.error
end

def test_gem_cli_runs_under_ruby_box
omit "Ruby::Box is not available" unless defined?(Ruby::Box)
# Ruby 4.0 resolves Gem::NameTuple from the root box and fails autoload
omit "Ruby::Box is too unstable before 4.1" if Gem.ruby_version < Gem::Version.new("4.1.0.a")

boxed, warning, = Open3.capture3({ "RUBY_BOX" => "1" }, Gem.ruby, "-e", "print !Ruby::Box.current.nil?")
assert_equal "true", boxed, "RUBY_BOX=1 no longer boxes the subprocess: #{warning}"

env = { "RUBY_BOX" => "1", "GEM_HOME" => Gem.paths.home }
run_gem = 'require "rubygems/gem_runner"; Gem::GemRunner.new.run(["list", "--local"])'
output = IO.popen(env, [*ruby_with_rubygems_in_load_path, "-e", run_gem], err: [:child, :out], &:read)

assert Process.last_status.success?, output
end
end
18 changes: 18 additions & 0 deletions test/rubygems/test_gem_ext_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ def test_class_run_closes_stdin
assert_equal "STDIN: \"\"\n", command_output
end

def test_class_run_removes_ruby_box_from_env
ENV["RUBY_BOX"] = "1"

results = []

Gem::Ext::Builder.run([Gem.ruby, "-e", 'puts ENV.key?("RUBY_BOX")'], results)

assert_equal "false\n", results.last
end

def test_class_run_removes_ruby_box_passed_by_caller
results = []

Gem::Ext::Builder.run([Gem.ruby, "-e", 'puts ENV.key?("RUBY_BOX")'], results, nil, Dir.pwd, { "RUBY_BOX" => "1" })

assert_equal "false\n", results.last
end

def test_build_extensions
pend "terminates on mswin" if vc_windows? && ruby_repo?

Expand Down
38 changes: 38 additions & 0 deletions test/rubygems/test_gem_util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,42 @@ def test_correct_for_windows_path
path = "/home/skillet"
assert_equal "/home/skillet", Gem::Util.correct_for_windows_path(path)
end

def test_deep_dup
defaults = {
args: ["--local", +"extra"],
document: %w[ri],
nested: { list: [+"a", { key: +"b" }] },
version: Gem::Requirement.default,
wrappers: true,
count: 1,
sym: :install,
}

copy = Gem::Util.deep_dup defaults

assert_equal defaults, copy
refute_same defaults, copy
refute_same defaults[:args], copy[:args]
refute_same defaults[:args][1], copy[:args][1]
refute_same defaults[:nested], copy[:nested]
refute_same defaults[:nested][:list], copy[:nested][:list]
refute_same defaults[:nested][:list][1], copy[:nested][:list][1]
refute_same defaults[:version], copy[:version]

copy[:args] << "added"
copy[:nested][:list][1][:key] << "!"

assert_equal ["--local", "extra"], defaults[:args]
assert_equal "b", defaults[:nested][:list][1][:key]
end

def test_deep_dup_shares_the_internals_of_other_objects
struct = Struct.new(:list).new([+"a"])

copy = Gem::Util.deep_dup(struct: struct)[:struct]

refute_same struct, copy
assert_same struct.list, copy.list
end
end
Loading