From e8238fe330f3761a0537232807ce0b32d3535b3e Mon Sep 17 00:00:00 2001 From: Ben Wibking Date: Mon, 20 Feb 2023 11:28:03 -0500 Subject: Patchel shutil.copystat to avoid PermissionError on Lustre (#27247) --- lib/spack/llnl/util/filesystem.py | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) (limited to 'lib') diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py index 4dfea7975d..99bf834eb2 100644 --- a/lib/spack/llnl/util/filesystem.py +++ b/lib/spack/llnl/util/filesystem.py @@ -84,6 +84,74 @@ __all__ = [ "visit_directory_tree", ] +if sys.version_info < (3, 7, 4): + # monkeypatch shutil.copystat to fix PermissionError when copying read-only + # files on Lustre when using Python < 3.7.4 + + def copystat(src, dst, follow_symlinks=True): + """Copy file metadata + Copy the permission bits, last access time, last modification time, and + flags from `src` to `dst`. On Linux, copystat() also copies the "extended + attributes" where possible. The file contents, owner, and group are + unaffected. `src` and `dst` are path names given as strings. + If the optional flag `follow_symlinks` is not set, symlinks aren't + followed if and only if both `src` and `dst` are symlinks. + """ + + def _nop(args, ns=None, follow_symlinks=None): + pass + + # follow symlinks (aka don't not follow symlinks) + follow = follow_symlinks or not (os.path.islink(src) and os.path.islink(dst)) + if follow: + # use the real function if it exists + def lookup(name): + return getattr(os, name, _nop) + + else: + # use the real function only if it exists + # *and* it supports follow_symlinks + def lookup(name): + fn = getattr(os, name, _nop) + if sys.version_info >= (3, 3): + if fn in os.supports_follow_symlinks: # novermin + return fn + return _nop + + st = lookup("stat")(src, follow_symlinks=follow) + mode = stat.S_IMODE(st.st_mode) + lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), follow_symlinks=follow) + + # We must copy extended attributes before the file is (potentially) + # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. + shutil._copyxattr(src, dst, follow_symlinks=follow) + + try: + lookup("chmod")(dst, mode, follow_symlinks=follow) + except NotImplementedError: + # if we got a NotImplementedError, it's because + # * follow_symlinks=False, + # * lchown() is unavailable, and + # * either + # * fchownat() is unavailable or + # * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW. + # (it returned ENOSUP.) + # therefore we're out of options--we simply cannot chown the + # symlink. give up, suppress the error. + # (which is what shutil always did in this circumstance.) + pass + if hasattr(st, "st_flags"): + try: + lookup("chflags")(dst, st.st_flags, follow_symlinks=follow) + except OSError as why: + for err in "EOPNOTSUPP", "ENOTSUP": + if hasattr(errno, err) and why.errno == getattr(errno, err): + break + else: + raise + + shutil.copystat = copystat + def getuid(): if is_windows: -- cgit v1.2.3-60-g2f50