From ed14c86facb62c4fb3fcff73c8ea3860c7dc8d29 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 30 Oct 2014 19:37:07 +0100 Subject: [PATCH] Issue #8876: distutils now falls back to copying files when hard linking doesn't work. This allows use with special filesystems such as VirtualBox shared folders. --- Lib/distutils/file_util.py | 32 +++++++++++++++++---------- Lib/distutils/tests/test_file_util.py | 32 ++++++++++++++++++++++++++- Misc/NEWS | 4 ++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/Lib/distutils/file_util.py b/Lib/distutils/file_util.py index 7b14efbc076..b3fee35a6cc 100644 --- a/Lib/distutils/file_util.py +++ b/Lib/distutils/file_util.py @@ -80,7 +80,8 @@ def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, (os.symlink) instead of copying: set it to "hard" or "sym"; if it is None (the default), files are copied. Don't set 'link' on systems that don't support it: 'copy_file()' doesn't check if hard or symbolic - linking is available. + linking is available. If hardlink fails, falls back to + _copy_file_contents(). Under Mac OS, uses the native file copy function in macostools; on other systems, uses '_copy_file_contents()' to copy file contents. @@ -132,24 +133,31 @@ def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, # (Unix only, of course, but that's the caller's responsibility) elif link == 'hard': if not (os.path.exists(dst) and os.path.samefile(src, dst)): - os.link(src, dst) + try: + os.link(src, dst) + return (dst, 1) + except OSError: + # If hard linking fails, fall back on copying file + # (some special filesystems don't support hard linking + # even under Unix, see issue #8876). + pass elif link == 'sym': if not (os.path.exists(dst) and os.path.samefile(src, dst)): os.symlink(src, dst) + return (dst, 1) # Otherwise (non-Mac, not linking), copy the file contents and # (optionally) copy the times and mode. - else: - _copy_file_contents(src, dst) - if preserve_mode or preserve_times: - st = os.stat(src) + _copy_file_contents(src, dst) + if preserve_mode or preserve_times: + st = os.stat(src) - # According to David Ascher , utime() should be done - # before chmod() (at least under NT). - if preserve_times: - os.utime(dst, (st[ST_ATIME], st[ST_MTIME])) - if preserve_mode: - os.chmod(dst, S_IMODE(st[ST_MODE])) + # According to David Ascher , utime() should be done + # before chmod() (at least under NT). + if preserve_times: + os.utime(dst, (st[ST_ATIME], st[ST_MTIME])) + if preserve_mode: + os.chmod(dst, S_IMODE(st[ST_MODE])) return (dst, 1) diff --git a/Lib/distutils/tests/test_file_util.py b/Lib/distutils/tests/test_file_util.py index d3db5cef0e0..a6d04f065d9 100644 --- a/Lib/distutils/tests/test_file_util.py +++ b/Lib/distutils/tests/test_file_util.py @@ -5,7 +5,7 @@ import shutil import errno from unittest.mock import patch -from distutils.file_util import move_file +from distutils.file_util import move_file, copy_file from distutils import log from distutils.tests import support from distutils.errors import DistutilsFileError @@ -78,6 +78,36 @@ class FileUtilTestCase(support.TempdirManager, unittest.TestCase): fobj.write('spam eggs') move_file(self.source, self.target, verbose=0) + def test_copy_file_hard_link(self): + with open(self.source, 'w') as f: + f.write('some content') + st = os.stat(self.source) + copy_file(self.source, self.target, link='hard') + st2 = os.stat(self.source) + st3 = os.stat(self.target) + self.assertTrue(os.path.samestat(st, st2), (st, st2)) + self.assertTrue(os.path.samestat(st2, st3), (st2, st3)) + with open(self.source, 'r') as f: + self.assertEqual(f.read(), 'some content') + + def test_copy_file_hard_link_failure(self): + # If hard linking fails, copy_file() falls back on copying file + # (some special filesystems don't support hard linking even under + # Unix, see issue #8876). + with open(self.source, 'w') as f: + f.write('some content') + st = os.stat(self.source) + with patch("os.link", side_effect=OSError(0, "linking unsupported")): + copy_file(self.source, self.target, link='hard') + st2 = os.stat(self.source) + st3 = os.stat(self.target) + self.assertTrue(os.path.samestat(st, st2), (st, st2)) + self.assertFalse(os.path.samestat(st2, st3), (st2, st3)) + for fn in (self.source, self.target): + with open(fn, 'r') as f: + self.assertEqual(f.read(), 'some content') + + def test_suite(): return unittest.makeSuite(FileUtilTestCase) diff --git a/Misc/NEWS b/Misc/NEWS index 20938600da8..6245bd0f79c 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -33,6 +33,10 @@ Core and Builtins Library ------- +- Issue #8876: distutils now falls back to copying files when hard linking + doesn't work. This allows use with special filesystems such as VirtualBox + shared folders. + - Issue #18853: Fixed ResourceWarning in shlex.__nain__. - Issue #9351: Defaults set with set_defaults on an argparse subparser