diff --git a/Lib/shutil.py b/Lib/shutil.py index a4aa0dfdd10b09..fcb3975ba33445 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -5,6 +5,7 @@ """ import os +import platform import sys import stat import fnmatch @@ -311,6 +312,31 @@ def _copyxattr(src, dst, *, follow_symlinks=True): def _copyxattr(*args, **kwargs): pass +if sys.platform == 'darwin': + # As of macOS 10.10, the `copyfile` API changed to not copy certain flags. + # This corresponds to kernel version 14.0. + mac_ver = tuple(int(x) for x in os.uname().release.split('.')) + if mac_ver >= (14, 0, 0): + def _fix_flags(dst, flags, follow_symlinks): + """Perform any modifications to `flags` before they can be applied + to `dst` with `chflags()`. + + """ + # Based on copyfile's copyfile_stat() + omit_flags = (stat.UF_TRACKED | stat.SF_RESTRICTED) + add_flags = 0 + + # If the kernel automatically put SF_RESTRICTED on the destination + # already, we don't want to clear it + st = os.stat(dst, follow_symlinks=follow_symlinks) + add_flags |= (st.st_flags & stat.SF_RESTRICTED) + + return (flags & ~omit_flags) | add_flags + +if '_fix_flags' not in vars(): + def _fix_flags(dst, flags, *args, **kwargs): + return flags + def copystat(src, dst, *, follow_symlinks=True): """Copy all stat info (mode bits, atime, mtime, flags) from src to dst. @@ -356,7 +382,8 @@ def lookup(name): pass if hasattr(st, 'st_flags'): try: - lookup("chflags")(dst, st.st_flags, follow_symlinks=follow) + flags = _fix_flags(dst, st.st_flags, follow_symlinks=follow) + lookup("chflags")(dst, flags, follow_symlinks=follow) except OSError as why: for err in 'EOPNOTSUPP', 'ENOTSUP': if hasattr(errno, err) and why.errno == getattr(errno, err): diff --git a/Lib/stat.py b/Lib/stat.py index 46837c06dacfb8..61066eb9e8c691 100644 --- a/Lib/stat.py +++ b/Lib/stat.py @@ -100,11 +100,13 @@ def S_ISSOCK(mode): UF_APPEND = 0x00000004 # file may only be appended to UF_OPAQUE = 0x00000008 # directory is opaque when viewed through a union stack UF_NOUNLINK = 0x00000010 # file may not be renamed or deleted -UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed -UF_HIDDEN = 0x00008000 # OS X: file should not be displayed +UF_COMPRESSED = 0x00000020 # macOS: file is compressed (some file-systems) +UF_TRACKED = 0x00000040 # macOS: file renames and deletes are tracked +UF_HIDDEN = 0x00008000 # macOS: file should not be displayed SF_ARCHIVED = 0x00010000 # file may be archived SF_IMMUTABLE = 0x00020000 # file may not be changed SF_APPEND = 0x00040000 # file may only be appended to +SF_RESTRICTED = 0x00080000 # macOS: entitlement required for writing SF_NOUNLINK = 0x00100000 # file may not be renamed or deleted SF_SNAPSHOT = 0x00200000 # file is a snapshot file diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 7e0a3292e0f8a4..6759583ad239fd 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -33,7 +33,11 @@ from test.support import TESTFN, FakePath TESTFN2 = TESTFN + "2" + MACOS = sys.platform.startswith("darwin") +if MACOS: + MACOS_VERS = tuple(int(x) for x in os.uname().release.split('.')) + try: import grp import pwd @@ -472,6 +476,19 @@ def _chflags_raiser(path, flags, *, follow_symlinks=True): finally: os.chflags = old_chflags + @unittest.skipUnless(MACOS and MACOS_VERS >= (14, 0, 0), + "requires macOS 10.10 or newer") + def test_copystat_macos_restricted(self): + # A sample restricted file. This file has always been present on macOS. + src = '/System/Library/CoreServices/SystemVersion.plist' + dst = os.path.join(self.mkdtemp(), 'file1') + write_file(dst, 'foo') + + # Check that the file is restricted befre, but not after copying + self.assertNotEqual(os.stat(src).st_flags & stat.SF_RESTRICTED, 0) + shutil.copystat(src, dst) + self.assertEqual(os.stat(dst).st_flags & stat.SF_RESTRICTED, 0) + @support.skip_unless_xattr def test_copyxattr(self): tmp_dir = self.mkdtemp() diff --git a/Misc/NEWS.d/next/Library/2017-12-18-02-19-43.bpo-32347.f8FJmS.rst b/Misc/NEWS.d/next/Library/2017-12-18-02-19-43.bpo-32347.f8FJmS.rst new file mode 100644 index 00000000000000..88e70ab85ff9c8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-12-18-02-19-43.bpo-32347.f8FJmS.rst @@ -0,0 +1,2 @@ +``shutil.copystat`` now implements flag copy behavior from ``copyfile(3)`` +on macOS 10.10 and above. Patch by Ryan Govostes. \ No newline at end of file