diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 53a02f61f31b..f5040d98f29e 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -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. @@ -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 @@ -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 + # C:\path\nmake.exe, 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" diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index d702d7f33937..6a5022780f5e 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -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") diff --git a/test/rubygems/test_gem_ext_builder.rb b/test/rubygems/test_gem_ext_builder.rb index 37204f3c472a..040877338bb3 100644 --- a/test/rubygems/test_gem_ext_builder.rb +++ b/test/rubygems/test_gem_ext_builder.rb @@ -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'