Skip to content
Open
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
21 changes: 20 additions & 1 deletion lib/rubygems/ext/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = [
target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/
make_program_name = ENV["MAKE"] || ENV["make"] || $1
make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make"
make_program = shellsplit(make_program_name)
make_program = shellsplit_command(make_program_name)

is_nmake = /\bnmake/i.match?(make_program_name)
# The installation of the bundled gems is failed when DESTDIR is empty in mswin platform.
Expand Down Expand Up @@ -102,6 +102,10 @@ 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)
# 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.
command = [[command.first, command.first]] if command.size == 1
output, status = begin
Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread|
stdin.close
Expand Down Expand Up @@ -148,6 +152,21 @@ def self.shellsplit(command)
Shellwords.split(command)
end

##
# Splits a command string such as ENV["MAKE"] into an argument list.
#
# On Windows, a value that names an existing file, such as
# <tt>C:\path\nmake.exe</tt>, is taken as a single word because POSIX
# shell splitting would consume the backslashes as escape characters.
# Any other value is split like a POSIX shell command line, where a
# path containing spaces must be quoted.

def self.shellsplit_command(command)
return [command] if Gem.win_platform? && File.file?(command)

shellsplit(command)
end

def self.shelljoin(command)
require "shellwords"

Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/ext/rake_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extensio
rake = ENV["rake"]

if rake
rake = shellsplit(rake)
rake = shellsplit_command(rake)
else
begin
rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake")
Expand Down
72 changes: 72 additions & 0 deletions test/rubygems/test_gem_ext_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,78 @@ def test_custom_make_with_options
assert_match(/install: OK/, results)
end

def test_class_shellsplit_command_windows_path
win_platform = Gem.win_platform?

make = if win_platform
FileUtils.mkdir_p File.join(@tempdir, "make dir")
File.join(@tempdir, "make dir", "nmake.exe").tr("/", "\\")
else
# backslashes and spaces are plain filename characters on POSIX
File.join(@tempdir, 'C:\make dir\nmake.exe')
end
FileUtils.touch make

Gem.win_platform = true

assert_equal [make], Gem::Ext::Builder.shellsplit_command(make)
ensure
Gem.win_platform = win_platform
end

def test_class_shellsplit_command_quoted_path
assert_equal ['C:\make dir\nmake.exe'],
Gem::Ext::Builder.shellsplit_command('"C:\make dir\nmake.exe"')
end

def test_class_shellsplit_command_with_arguments
win_platform = Gem.win_platform?

Gem.win_platform = true

assert_equal %w[make -j4], Gem::Ext::Builder.shellsplit_command("make -j4")

Gem.win_platform = false

assert_equal %w[make -j4], Gem::Ext::Builder.shellsplit_command("make -j4")
ensure
Gem.win_platform = win_platform
end

def test_class_shellsplit_command_existing_path_on_posix
win_platform = Gem.win_platform?

FileUtils.mkdir_p File.join(@tempdir, "make dir")
make = File.join(@tempdir, "make dir", "make")
FileUtils.touch make

Gem.win_platform = false

assert_equal make.split(" "), Gem::Ext::Builder.shellsplit_command(make)
ensure
Gem.win_platform = win_platform
end

def test_class_run_single_element_command_with_spaces
dir = File.join @tempdir, "cmd dir"
FileUtils.mkdir_p dir

if Gem.win_platform?
command = File.join dir, "say.cmd"
File.write command, "@echo spaces-ok\r\n"
else
command = File.join dir, "say"
File.write command, "#!/bin/sh\necho spaces-ok\n"
FileUtils.chmod 0o755, command
end

results = []

Gem::Ext::Builder.run([command], results)

assert_match(/spaces-ok/, results.join)
end

def test_class_run_closes_stdin
results = []
check_stdin_script = <<~'RUBY'
Expand Down