Files
Samuel Williams bd2a734486 [ruby/rubygems] Avoid duplicate relative path prefix in bundle exec
When bundle exec resolves an explicit relative path like ./script and falls back to Kernel.exec, Bundler currently prepends another ./ because the resolved path is not absolute. The same issue affects Windows paths such as .\script.

Only prepend ./ for non-absolute paths that do not already begin with an explicit relative path marker. Recognize the platform's alternative path separator without changing the meaning of backslashes on POSIX.

Add integration coverage for the original regression and focused coverage for primary and alternative path separators.

Closes https://github.com/ruby/rubygems/issues/8930

Assisted-By: devx/caa7d694-19ff-42bb-9306-686cb9d69649

https://github.com/ruby/rubygems/commit/d172a35a54
2026-09-02 03:10:34 +00:00

129 lines
3.4 KiB
Ruby

# frozen_string_literal: true
require_relative "../current_ruby"
module Bundler
class CLI::Exec
attr_reader :options, :args, :cmd
TRAPPED_SIGNALS = %w[INT].freeze
def initialize(options, args)
@options = options
@cmd = args.shift
@args = args
@args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby?
end
def run
validate_cmd!
SharedHelpers.set_bundle_environment
if bin_path = Bundler.which(cmd)
if !Bundler.settings[:disable_exec_load] && directly_loadable?(bin_path)
bin_path.delete_suffix!(".bat") if Gem.win_platform?
kernel_load(bin_path, *args)
else
bin_path = explicit_path(bin_path)
kernel_exec(bin_path, *args)
end
else
# exec using the given command
kernel_exec(cmd, *args)
end
end
private
def validate_cmd!
return unless cmd.nil?
Bundler.ui.error "bundler: exec needs a command to run"
exit 128
end
def kernel_exec(*args)
Kernel.exec(*args)
rescue Errno::EACCES, Errno::ENOEXEC
Bundler.ui.error "bundler: not executable: #{cmd}"
exit 126
rescue Errno::ENOENT
Bundler.ui.error "bundler: command not found: #{cmd}"
Bundler.ui.warn "Install missing gem executables with `bundle install`"
exit 127
end
def kernel_load(file, *args)
args.pop if args.last.is_a?(Hash)
ARGV.replace(args)
$0 = file
Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
require_relative "../setup"
TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") }
Kernel.load(file)
rescue SystemExit, SignalException
raise
rescue Exception # rubocop:disable Lint/RescueException
Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})"
Bundler::FriendlyErrors.disable!
raise
end
def process_title(file, args)
"#{file} #{args.join(" ")}".strip
end
def explicit_path(path)
if File.absolute_path?(path) || explicit_relative_path?(path)
path
else
".#{File::SEPARATOR}#{path}"
end
end
def explicit_relative_path?(path)
[File::SEPARATOR, File::ALT_SEPARATOR].compact.any? do |separator|
path.start_with?(".#{separator}", "..#{separator}")
end
end
def directly_loadable?(file)
if Gem.win_platform?
script_wrapper?(file)
else
ruby_shebang?(file)
end
end
def script_wrapper?(file)
script_file = file.delete_suffix(".bat")
return false unless File.exist?(script_file)
if File.zero?(script_file)
Bundler.ui.warn "#{script_file} is empty"
return false
end
header = File.open(file, "r") {|f| f.read(32) }
ruby_exe = "#{RbConfig::CONFIG["RUBY_INSTALL_NAME"]}#{RbConfig::CONFIG["EXEEXT"]}"
ruby_exe = "ruby.exe" if ruby_exe.empty?
header.include?(ruby_exe)
end
def ruby_shebang?(file)
possibilities = [
"#!/usr/bin/env ruby\n",
"#!/usr/bin/env jruby\n",
"#!/usr/bin/env truffleruby\n",
"#!#{Gem.ruby}\n",
]
if File.zero?(file)
Bundler.ui.warn "#{file} is empty"
return false
end
first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) }
possibilities.any? {|shebang| first_line.start_with?(shebang) }
end
end
end