make_tree_writable: handle junctions and add tests

As found out here, os.walk() by default follows junctions, which we don't
want and can even lead to loops:
https://github.com/msys2/msys2-autobuild/issues/101#issuecomment-2583121845

Integrate the workaround mentioned in the CPython bug report:
https://github.com/python/cpython/issues/67596#issuecomment-1918112817
Since this is Python 3.12+ only and we still support 3.10 make
it optional though.

This also adds tests, which uncovered some other minor issues:
It was not chmoding top-down, which meant that os.walk would
skip things if there were no read permissions. So chmod before
os.walk() lists the dir.
This commit is contained in:
Christoph Reiter 2025-01-10 17:51:21 +01:00
parent 35ff0b71b6
commit 4f60392b3e
2 changed files with 56 additions and 11 deletions

View File

@ -122,6 +122,29 @@ def run_cmd(msys2_root: PathLike, args: Sequence[PathLike], **kwargs: Any) -> No
check_call([executable, '-lc'] + [shlex_join([str(a) for a in args])], env=env, **kwargs)
def make_tree_writable(topdir: PathLike) -> None:
# Ensure all files and directories under topdir are writable
# (and readable) by owner.
# Taken from meson, and adjusted
def chmod(p: PathLike) -> None:
print(p)
os.chmod(p, os.stat(p).st_mode | stat.S_IWRITE | stat.S_IREAD)
chmod(topdir)
for root, dirs, files in os.walk(topdir):
for d in dirs:
chmod(os.path.join(root, d))
# Work around Python bug following junctions
# https://github.com/python/cpython/issues/67596#issuecomment-1918112817
if hasattr(os.path, 'isjunction'): # Python 3.12 only
dirs[:] = [d for d in dirs if not os.path.isjunction(os.path.join(root, d))]
for fname in files:
fpath = os.path.join(root, fname)
if os.path.isfile(fpath):
chmod(fpath)
def reset_git_repo(path: PathLike):
def clean():
@ -129,17 +152,6 @@ def reset_git_repo(path: PathLike):
check_call(["git", "clean", "-xfdf"], cwd=path)
check_call(["git", "reset", "--hard", "HEAD"], cwd=path)
def make_tree_writable(topdir: PathLike) -> None:
# Ensure all files and directories under topdir are writable
# (and readable) by owner.
# Taken from meson
for d, _, files in os.walk(topdir):
os.chmod(d, os.stat(d).st_mode | stat.S_IWRITE | stat.S_IREAD)
for fname in files:
fpath = os.path.join(d, fname)
if os.path.isfile(fpath):
os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD)
made_writable = False
for i in range(10):
try:

View File

@ -1,7 +1,40 @@
# type: ignore
import os
import stat
import tempfile
from pathlib import Path
from msys2_autobuild.utils import parse_optional_deps
from msys2_autobuild.queue import parse_buildqueue, get_cycles
from msys2_autobuild.build import make_tree_writable
def test_make_tree_writable():
with tempfile.TemporaryDirectory() as tempdir:
nested_dir = Path(tempdir) / "nested"
nested_junction = nested_dir / "junction"
nested_dir.mkdir()
file_path = nested_dir / "test_file.txt"
file_path.write_text("content")
# Create a junction loop if possible, to make sure we ignore it
if hasattr(os.path, 'isjunction') and os.name == 'nt':
import _winapi
_winapi.CreateJunction(str(nested_dir), str(nested_junction))
else:
nested_junction.mkdir()
# Remove permissions
for p in [tempdir, nested_dir, file_path, nested_junction]:
os.chmod(p, os.stat(p).st_mode & ~stat.S_IWRITE & ~stat.S_IREAD)
make_tree_writable(tempdir)
assert os.access(tempdir, os.W_OK) and os.access(tempdir, os.R_OK)
assert os.access(nested_dir, os.W_OK) and os.access(nested_dir, os.R_OK)
assert os.access(file_path, os.W_OK) and os.access(file_path, os.R_OK)
assert os.access(nested_junction, os.W_OK) and os.access(nested_junction, os.R_OK)
def test_parse_optional_deps():