Skip to content

Commit aae6873

Browse files
committed
feat: introduce a config defaulted-False config-flag and link-resolution-behavior intended to produce realpath-like behavior
Signed-off-by: Stephen Mackenzie <maxnbk@users.noreply.github.com>
1 parent 1a2a791 commit aae6873

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

src/rez/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ def _parse_env_var(self, value):
415415
"suite_alias_prefix_char": Char,
416416
"cache_packages_path": OptionalStr,
417417
"package_definition_python_path": OptionalStr,
418+
"resolve_links_on_windows": Bool,
418419
"tmpdir": OptionalStr,
419420
"context_tmpdir": OptionalStr,
420421
"default_shell": OptionalStr,

src/rez/rezconfig.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,26 @@
125125
# For further information, see :ref:`package-definition-sharing-code`.
126126
package_definition_python_path = None
127127

128+
# On Windows, whether to resolve symbolic links and junction points when
129+
# normalising filesystem paths (primarily inside ``canonical_path``).
130+
#
131+
# When ``False`` (default), rez uses ``os.path.abspath``, which normalises
132+
# separators and ``.``/``..`` components without following symlinks and without
133+
# expanding mapped drive letters to their UNC equivalents. This preserves the
134+
# path style supplied by the caller (drive-letter input, drive-letter output,
135+
# UNC input, UNC output), effectively defaulting to pre-Python-3.8 behaviour of
136+
# ``os.path.realpath`` on Windows.
137+
#
138+
# When ``True``, rez performs a component-by-component walk using
139+
# ``os.path.islink`` / ``os.readlink``. This resolves actual symlinks and
140+
# junction points without the drive-letter-to-UNC side-effect that
141+
# ``os.path.realpath`` introduced in Python 3.8. Useful when package
142+
# repositories are accessed through directory symlinks or junctions.
143+
#
144+
# This setting is a no-op on non-Windows platforms, which always resolve
145+
# symlinks via ``os.path.realpath``.
146+
resolve_links_on_windows = False
147+
128148

129149
###############################################################################
130150
# Extensions

src/rez/utils/filesystem.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,42 @@ def to_posixpath(path: str):
490490
return posixpath.sep.join(path.split(ntpath.sep))
491491

492492

493+
def _windows_realpath(path: str) -> str:
494+
"""Resolve symlinks and junctions on Windows without expanding mapped drives.
495+
496+
``os.path.realpath`` on Python 3.8+ Windows uses ``GetFinalPathNameByHandle``
497+
which expands mapped drive letters (e.g. ``N:\\``) to their underlying UNC
498+
server paths. This function resolves only actual filesystem symlinks and
499+
junction points, walking the path component-by-component so that the drive
500+
root (drive-letter or UNC prefix) is never touched.
501+
"""
502+
path = os.path.normpath(os.path.abspath(path))
503+
drive, rest = os.path.splitdrive(path)
504+
# Preserve the root separator so UNC paths keep their leading "\\" and
505+
# drive-letter paths keep their "\".
506+
result = drive + (os.sep if rest.startswith(os.sep) else "")
507+
for part in rest.lstrip(os.sep).split(os.sep):
508+
if not part:
509+
continue
510+
candidate = os.path.join(result, part)
511+
depth = 0
512+
while os.path.islink(candidate) and depth < 40:
513+
target = os.readlink(candidate)
514+
# os.readlink on Windows may return an extended-length path
515+
# (\\?\C:\... or \\?\UNC\server\share\...). Strip the prefix so
516+
# subsequent abspath/normpath calls produce ordinary paths.
517+
if target.startswith("\\\\?\\UNC\\"):
518+
target = "\\\\" + target[8:]
519+
elif target.startswith("\\\\?\\"):
520+
target = target[4:]
521+
if not os.path.isabs(target):
522+
target = os.path.join(os.path.dirname(candidate), target)
523+
candidate = os.path.normpath(os.path.abspath(target))
524+
depth += 1
525+
result = candidate
526+
return result
527+
528+
493529
def canonical_path(path: str, platform=None):
494530
r""" Resolves symlinks, and formats filepath.
495531
@@ -508,12 +544,19 @@ def canonical_path(path: str, platform=None):
508544
platform = platform_
509545

510546
# On Windows, os.path.realpath from py3.8 onwards silently converts drive
511-
# lettered paths to their UNC equivalents (N:\ → \\server\share\). By default,
512-
# use abspath instead to preserve the caller's form.
547+
# lettered paths to their UNC equivalents (N:\ → \\server\share\).
513548
# We check sys.platform rather than the platform argument because this is
514549
# an OS-level behaviour of os.path.realpath, not a user-configurable choice.
515550
if sys.platform == "win32":
516-
path = os.path.normpath(os.path.abspath(path))
551+
# Lazy import avoids a circular dependency (config imports filesystem).
552+
from rez.config import config # noqa: PLC0415
553+
if config.resolve_links_on_windows:
554+
path = _windows_realpath(path)
555+
else:
556+
# Default: abspath preserves the caller's path form (drive-letter
557+
# stays drive-letter, UNC stays UNC) and restores the pre-3.8
558+
# behaviour where realpath on Windows was equivalent to abspath.
559+
path = os.path.normpath(os.path.abspath(path))
517560
else:
518561
path = os.path.normpath(os.path.realpath(path))
519562

0 commit comments

Comments
 (0)