Skip to content

Commit 3474dfd

Browse files
2 parents 6dcc73d + a656257 commit 3474dfd

8 files changed

Lines changed: 132 additions & 55 deletions

File tree

.github/workflows/rubygems.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ jobs:
4343
os: { name: Ubuntu, value: ubuntu-24.04 }
4444
use_psych: true
4545

46+
- ruby: { name: no symlinks, value: 4.0.0 }
47+
os: { name: Windows, value: windows-2025 }
48+
symlink: off
4649
env:
4750
RUBYGEMS_USE_PSYCH: ${{ matrix.use_psych || 'false' }}
4851

4952
steps:
53+
- name: disable development mode on Windows
54+
run: powershell -c "Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock -Name AllowDevelopmentWithoutDevLicense -Value 0"
55+
if: matrix.symlink == 'off'
5056
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5157
with:
5258
persist-credentials: false
@@ -59,7 +65,7 @@ jobs:
5965
run: bin/rake setup
6066
- name: Run Test
6167
run: bin/rake test
62-
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby'
68+
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off'
6369
- name: Run Test isolatedly
6470
run: bin/rake test:isolated
6571
if: matrix.ruby.name == '3.4' && matrix.os.name != 'Windows'
@@ -69,6 +75,11 @@ jobs:
6975
- name: Run Test (Truffleruby)
7076
run: TRUFFLERUBYOPT="--experimental-options --testing-rubygems" bin/rake test
7177
if: matrix.ruby.name == 'truffleruby'
78+
- name: Run Test with non-Admin user
79+
run: |
80+
gem inst win32-process --no-doc --conservative
81+
ruby bin/windows_run_as_user ruby -S rake test
82+
if: matrix.symlink == 'off'
7283

7384
timeout-minutes: 60
7485

bin/windows_run_as_user

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require "win32/process"
2+
require "rbconfig"
3+
4+
testuser = "testuser"
5+
testpassword = "Password123+"
6+
7+
# Remove a previous test user if present
8+
# See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/net-user
9+
system("net user #{testuser} /del 2>NUL")
10+
# Create a new non-admin user
11+
system("net user #{testuser} \"#{testpassword}\" /add")
12+
13+
pinfo = nil
14+
IO.pipe do |stdout_read, stdout_write|
15+
cmd = ARGV.join(" ")
16+
env = {
17+
"TMP" => "#{Dir.pwd}/tmp",
18+
"TEMP" => "#{Dir.pwd}/tmp"
19+
}
20+
pinfo = Process.create command_line: cmd,
21+
with_logon: testuser,
22+
password: testpassword,
23+
cwd: Dir.pwd,
24+
environment: ENV.to_h.merge(env).map{|k,v| "#{k}=#{v}" },
25+
startup_info: { stdout: stdout_write, stderr: stdout_write }
26+
27+
stdout_write.close
28+
stdout_read.each_line do |line|
29+
puts(line)
30+
end
31+
end
32+
33+
# Wait for process to terminate
34+
sleep 1 while !(ecode=Process.get_exitcode(pinfo.process_id))
35+
36+
exit ecode

bundler/lib/bundler/resolver/strategy.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class Resolver
55
class Strategy
66
def initialize(source)
77
@source = source
8+
@package_priority_cache = {}
89
end
910

1011
def next_package_and_version(unsatisfied)
@@ -17,10 +18,12 @@ def next_package_and_version(unsatisfied)
1718

1819
def next_term_to_try_from(unsatisfied)
1920
unsatisfied.min_by do |package, range|
20-
matching_versions = @source.versions_for(package, range)
21-
higher_versions = @source.versions_for(package, range.upper_invert)
21+
@package_priority_cache[[package, range]] ||= begin
22+
matching_versions = @source.versions_for(package, range)
23+
higher_versions = @source.versions_for(package, range.upper_invert)
2224

23-
[matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
25+
[matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
26+
end
2427
end
2528
end
2629

lib/rubygems/package.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
470470

471471
symlinks.each do |name, target, destination, real_destination|
472472
if File.exist?(real_destination)
473-
File.symlink(target, destination)
473+
create_symlink(target, destination)
474474
else
475475
alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring"
476476
end
@@ -725,6 +725,21 @@ def limit_read(io, name, limit)
725725
raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
726726
bytes
727727
end
728+
729+
if Gem.win_platform?
730+
# Create a symlink and fallback to copy the file or directory on Windows,
731+
# where symlink creation needs special privileges in form of the Developer Mode.
732+
def create_symlink(old_name, new_name)
733+
File.symlink(old_name, new_name)
734+
rescue Errno::EACCES
735+
from = File.expand_path(old_name, File.dirname(new_name))
736+
FileUtils.cp_r(from, new_name)
737+
end
738+
else
739+
def create_symlink(old_name, new_name)
740+
File.symlink(old_name, new_name)
741+
end
742+
end
728743
end
729744

730745
require_relative "package/digest_io"

test/rubygems/helper.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,24 @@ def nmake_found?
12601260
system("nmake /? 1>NUL 2>&1")
12611261
end
12621262

1263+
@@symlink_supported = nil
1264+
1265+
# This is needed for Windows environment without symlink support enabled (the default
1266+
# for non admin) to be able to skip test for features using symlinks.
1267+
def symlink_supported?
1268+
if @@symlink_supported.nil?
1269+
begin
1270+
File.symlink(File.join(@tempdir, "a"), File.join(@tempdir, "b"))
1271+
rescue NotImplementedError, SystemCallError
1272+
@@symlink_supported = false
1273+
else
1274+
File.unlink(File.join(@tempdir, "b"))
1275+
@@symlink_supported = true
1276+
end
1277+
end
1278+
@@symlink_supported
1279+
end
1280+
12631281
# In case we're building docs in a background process, this method waits for
12641282
# that process to exit (or if it's already been reaped, or never happened,
12651283
# swallows the Errno::ECHILD error).

test/rubygems/installer_test_case.rb

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -237,21 +237,4 @@ def test_ensure_writable_dir_creates_missing_parent_directories
237237
assert_directory_exists non_existent_parent, "Parent directory should exist now"
238238
assert_directory_exists target_dir, "Target directory should exist now"
239239
end
240-
241-
@@symlink_supported = nil
242-
243-
# This is needed for Windows environment without symlink support enabled (the default
244-
# for non admin) to be able to skip test for features using symlinks.
245-
def symlink_supported?
246-
if @@symlink_supported.nil?
247-
begin
248-
File.symlink("", "")
249-
rescue Errno::ENOENT, Errno::EEXIST
250-
@@symlink_supported = true
251-
rescue NotImplementedError, SystemCallError
252-
@@symlink_supported = false
253-
end
254-
end
255-
@@symlink_supported
256-
end
257240
end

test/rubygems/test_gem_installer.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,12 @@ def test_generate_bin_with_dangling_symlink
759759

760760
errors = @ui.error.split("\n")
761761
assert_equal "WARNING: ascii_binder-0.1.10.1 ships with a dangling symlink named bin/ascii_binder pointing to missing bin/asciibinder file. Ignoring", errors.shift
762-
assert_empty errors
763-
762+
if symlink_supported?
763+
assert_empty errors
764+
else
765+
assert_match(/Unable to use symlinks, installing wrapper/i,
766+
errors.to_s)
767+
end
764768
assert_empty @ui.output
765769
end
766770

test/rubygems/test_gem_package.rb

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ def test_add_files
175175
end
176176

177177
def test_add_files_symlink
178+
unless symlink_supported?
179+
omit("symlink - developer mode must be enabled on Windows")
180+
end
178181
spec = Gem::Specification.new
179182
spec.files = %w[lib/code.rb lib/code_sym.rb lib/code_sym2.rb]
180183

@@ -185,16 +188,8 @@ def test_add_files_symlink
185188
end
186189

187190
# NOTE: 'code.rb' is correct, because it's relative to lib/code_sym.rb
188-
begin
189-
File.symlink("code.rb", "lib/code_sym.rb")
190-
File.symlink("../lib/code.rb", "lib/code_sym2.rb")
191-
rescue Errno::EACCES => e
192-
if Gem.win_platform?
193-
pend "symlink - must be admin with no UAC on Windows"
194-
else
195-
raise e
196-
end
197-
end
191+
File.symlink("code.rb", "lib/code_sym.rb")
192+
File.symlink("../lib/code.rb", "lib/code_sym2.rb")
198193

199194
package = Gem::Package.new "bogus.gem"
200195
package.spec = spec
@@ -583,25 +578,45 @@ def test_extract_tar_gz_symlink_relative_path
583578
tar.add_symlink "lib/foo.rb", "../relative.rb", 0o644
584579
end
585580

586-
begin
587-
package.extract_tar_gz tgz_io, @destination
588-
rescue Errno::EACCES => e
589-
if Gem.win_platform?
590-
pend "symlink - must be admin with no UAC on Windows"
591-
else
592-
raise e
593-
end
594-
end
581+
package.extract_tar_gz tgz_io, @destination
595582

596583
extracted = File.join @destination, "lib/foo.rb"
597584
assert_path_exist extracted
598-
assert_equal "../relative.rb",
599-
File.readlink(extracted)
585+
if symlink_supported?
586+
assert_equal "../relative.rb",
587+
File.readlink(extracted)
588+
end
600589
assert_equal "hi",
590+
File.read(extracted),
591+
"should read file content either by following symlink or on Windows by reading copy"
592+
end
593+
594+
def test_extract_tar_gz_symlink_directory
595+
package = Gem::Package.new @gem
596+
package.verify
597+
598+
tgz_io = util_tar_gz do |tar|
599+
tar.add_symlink "link", "lib/orig", 0o644
600+
tar.mkdir "lib", 0o755
601+
tar.mkdir "lib/orig", 0o755
602+
tar.add_file "lib/orig/file.rb", 0o644 do |io|
603+
io.write "ok"
604+
end
605+
end
606+
607+
package.extract_tar_gz tgz_io, @destination
608+
extracted = File.join @destination, "link/file.rb"
609+
assert_path_exist extracted
610+
if symlink_supported?
611+
assert_equal "lib/orig",
612+
File.readlink(File.dirname(extracted))
613+
end
614+
assert_equal "ok",
601615
File.read(extracted)
602616
end
603617

604618
def test_extract_symlink_into_symlink_dir
619+
omit "Symlinks not supported or not enabled" unless symlink_supported?
605620
package = Gem::Package.new @gem
606621
tgz_io = util_tar_gz do |tar|
607622
tar.mkdir "lib", 0o755
@@ -665,14 +680,10 @@ def test_extract_symlink_parent
665680
destination_subdir = File.join @destination, "subdir"
666681
FileUtils.mkdir_p destination_subdir
667682

668-
expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError]
669-
670-
e = assert_raise(*expected_exceptions) do
683+
e = assert_raise(Gem::Package::SymlinkError) do
671684
package.extract_tar_gz tgz_io, destination_subdir
672685
end
673686

674-
pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e
675-
676687
assert_equal("installing symlink 'lib/link' pointing to parent path #{@destination} of " \
677688
"#{destination_subdir} is not allowed", e.message)
678689

@@ -700,14 +711,10 @@ def test_extract_symlink_parent_doesnt_delete_user_dir
700711
tar.add_symlink "link/dir", ".", 16_877
701712
end
702713

703-
expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError]
704-
705-
e = assert_raise(*expected_exceptions) do
714+
e = assert_raise(Gem::Package::SymlinkError) do
706715
package.extract_tar_gz tgz_io, destination_subdir
707716
end
708717

709-
pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e
710-
711718
assert_equal("installing symlink 'link' pointing to parent path #{destination_user_dir} of " \
712719
"#{destination_subdir} is not allowed", e.message)
713720

0 commit comments

Comments
 (0)