Skip to content

Commit c351ae7

Browse files
larskanismatzbot
authored andcommitted
[ruby/rubygems] Fallback to copy symlinks on Windows
(ruby/rubygems#9296) Symlinks are not permitted by default for a Windows user. To use them, a switch called "Development Mode" in the system settings has to be enabled. ## What was the end-user or developer problem that led to this PR? Ordinary users as well as administrators are unable per default to install gems using symlinks. One such problematical gem is `haml-rails-3.0.0`. It uses symlinks for [files and directories](https://github.com/haml/haml-rails/tree/9f4703ddff0644ba52529c5cf41c1624829b16a7/lib/generators/haml/scaffold/templates). The resulting error message is not very helpful: ``` $ gem inst haml-rails Fetching haml-rails-3.0.0.gem ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the directory. (Gem::FilePermissionError) C:/ruby/lib/ruby/4.0.0/rubygems/installer.rb:308:in 'Gem::Installer#install' C:/ruby/lib/ruby/4.0.0/rubygems/resolver/specification.rb:105:in 'Gem::Resolver::Specification#install' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:192:in 'block in Gem::RequestSet#install' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Array#each' C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Gem::RequestSet#install' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:207:in 'Gem::Commands::InstallCommand#install_gem' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:223:in 'block in Gem::Commands::InstallCommand#install_gems' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Array#each' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Gem::Commands::InstallCommand#install_gems' C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:162:in 'Gem::Commands::InstallCommand#execute' C:/ruby/lib/ruby/4.0.0/rubygems/command.rb:326:in 'Gem::Command#invoke_with_build_args' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:252:in 'Gem::CommandManager#invoke_command' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:193:in 'Gem::CommandManager#process_args' C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:151:in 'Gem::CommandManager#run' C:/ruby/lib/ruby/4.0.0/rubygems/gem_runner.rb:56:in 'Gem::GemRunner#run' C:/ruby/bin/gem.cmd:20:in '<main>' ``` ## What is your fix for the problem, implemented in this PR? Instead of working around the situation in the affected gem or to skip symlinks completely, I think the better solution would be to make copies of the files in question. This would allow Windows users to install and use the gem smoothly. The switch for the "Developer Mode" is available in the Windows registry under `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock` entry `AllowDevelopmentWithoutDevLicense` ruby/rubygems@ca6c5791fe
1 parent d8353e1 commit c351ae7

File tree

5 files changed

+78
-51
lines changed

5 files changed

+78
-51
lines changed

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)