diff options
author | John W. Parent <45471568+johnwparent@users.noreply.github.com> | 2024-04-08 17:10:02 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-08 14:10:02 -0700 |
commit | d5c886494200896f21cfcddee8453a56802fcf68 (patch) | |
tree | 9a883eb3e7fbe46a4dcc394cf1c639c9eb7f9ab5 /lib | |
parent | b3cef1072d4ed08fe67705d8301a5a61c06008c2 (diff) | |
download | spack-d5c886494200896f21cfcddee8453a56802fcf68.tar.gz spack-d5c886494200896f21cfcddee8453a56802fcf68.tar.bz2 spack-d5c886494200896f21cfcddee8453a56802fcf68.tar.xz spack-d5c886494200896f21cfcddee8453a56802fcf68.zip |
Windows bugfix: safe rename if renaming file onto itself (#43456)
* Generally use os.replace on Windows and Linux
* Windows behavior for os.replace differs when the destination exists
and is a symlink to a directory: on Linux the dst is replaced and
on Windows this fails - this PR makes Windows behave like Linux
(by deleting the dst before doing the rename unless src and dst
are the same)
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/llnl/util/filesystem.py | 27 | ||||
-rw-r--r-- | lib/spack/spack/test/llnl/util/filesystem.py | 91 |
2 files changed, 113 insertions, 5 deletions
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py index 390d1f651e..722f51f3db 100644 --- a/lib/spack/llnl/util/filesystem.py +++ b/lib/spack/llnl/util/filesystem.py @@ -198,15 +198,32 @@ def getuid(): return os.getuid() +def _win_rename(src, dst): + # os.replace will still fail if on Windows (but not POSIX) if the dst + # is a symlink to a directory (all other cases have parity Windows <-> Posix) + if os.path.islink(dst) and os.path.isdir(os.path.realpath(dst)): + if os.path.samefile(src, dst): + # src and dst are the same + # do nothing and exit early + return + # If dst exists and is a symlink to a directory + # we need to remove dst and then perform rename/replace + # this is safe to do as there's no chance src == dst now + os.remove(dst) + os.replace(src, dst) + + @system_path_filter def rename(src, dst): # On Windows, os.rename will fail if the destination file already exists + # os.replace is the same as os.rename on POSIX and is MoveFileExW w/ + # the MOVEFILE_REPLACE_EXISTING flag on Windows + # Windows invocation is abstracted behind additonal logic handling + # remaining cases of divergent behavior accross platforms if sys.platform == "win32": - # Windows path existence checks will sometimes fail on junctions/links/symlinks - # so check for that case - if os.path.exists(dst) or islink(dst): - os.remove(dst) - os.rename(src, dst) + _win_rename(src, dst) + else: + os.replace(src, dst) @system_path_filter diff --git a/lib/spack/spack/test/llnl/util/filesystem.py b/lib/spack/spack/test/llnl/util/filesystem.py index d3b229112b..a38faaef27 100644 --- a/lib/spack/spack/test/llnl/util/filesystem.py +++ b/lib/spack/spack/test/llnl/util/filesystem.py @@ -9,6 +9,7 @@ import os import shutil import stat import sys +from contextlib import contextmanager import pytest @@ -908,3 +909,93 @@ def test_find_first_file(tmpdir, bfs_depth): # Should find first dir assert os.path.samefile(fs.find_first(root, "a", bfs_depth=bfs_depth), os.path.join(root, "a")) + + +def test_rename_dest_exists(tmpdir): + + @contextmanager + def setup_test_files(): + a = tmpdir.join("a", "file1") + b = tmpdir.join("a", "file2") + fs.touchp(a) + fs.touchp(b) + with open(a, "w") as oa, open(b, "w") as ob: + oa.write("I am A") + ob.write("I am B") + yield a, b + shutil.rmtree(tmpdir.join("a")) + + @contextmanager + def setup_test_dirs(): + a = tmpdir.join("d", "a") + b = tmpdir.join("d", "b") + fs.mkdirp(a) + fs.mkdirp(b) + yield a, b + shutil.rmtree(tmpdir.join("d")) + + # test standard behavior of rename + # smoke test + with setup_test_files() as files: + a, b = files + fs.rename(str(a), str(b)) + assert os.path.exists(b) + assert not os.path.exists(a) + with open(b, "r") as ob: + content = ob.read() + assert content == "I am A" + + # test relatitve paths + # another sanity check/smoke test + with setup_test_files() as files: + a, b = files + with fs.working_dir(str(tmpdir)): + fs.rename(os.path.join("a", "file1"), os.path.join("a", "file2")) + assert os.path.exists(b) + assert not os.path.exists(a) + with open(b, "r") as ob: + content = ob.read() + assert content == "I am A" + + # Test rename symlinks to same file + c = tmpdir.join("a", "file1") + a = tmpdir.join("a", "link1") + b = tmpdir.join("a", "link2") + fs.touchp(c) + symlink(c, a) + symlink(c, b) + fs.rename(str(a), str(b)) + assert os.path.exists(b) + assert not os.path.exists(a) + assert os.path.realpath(b) == c + shutil.rmtree(tmpdir.join("a")) + + # test rename onto itself + a = tmpdir.join("a", "file1") + b = a + fs.touchp(a) + with open(a, "w") as oa: + oa.write("I am A") + fs.rename(str(a), str(b)) + # check a, or b, doesn't matter, same file + assert os.path.exists(a) + # ensure original file was not duplicated + assert len(os.listdir(tmpdir.join("a"))) == 1 + with open(a, "r") as oa: + assert oa.read() + shutil.rmtree(tmpdir.join("a")) + + # test rename onto symlink + # to directory from symlink to directory + # (this is something spack does when regenerating views) + with setup_test_dirs() as dirs: + a, b = dirs + link1 = tmpdir.join("f", "link1") + link2 = tmpdir.join("f", "link2") + fs.mkdirp(tmpdir.join("f")) + symlink(a, link1) + symlink(b, link2) + fs.rename(str(link1), str(link2)) + assert os.path.exists(link2) + assert os.path.realpath(link2) == a + shutil.rmtree(tmpdir.join("f")) |