Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a5d7d96
chore: introduce tests which should correctly-fail if a drive-letter …
maxnbk May 14, 2026
48fcdae
fix: canonical_path uses abspath on windows, with updates to package_…
maxnbk May 14, 2026
8e7e015
chore: introduce tests intended to validate the behavior of a windows…
maxnbk May 14, 2026
4bfce93
feat: introduce a config defaulted-False config-flag and link-resolu…
maxnbk May 14, 2026
ffb5a56
chore: introduce tests intended to validate the behavior of longpath-…
maxnbk May 14, 2026
f3d6cd9
fix: make resolve_links_on_windows config-flagged functionality longp…
maxnbk May 14, 2026
63b0c29
chore: mock-helper for producing tests which fail against multi-roote…
maxnbk May 26, 2026
536998d
chore: introduce is_subdirectory regression guards for real_path migr…
maxnbk May 26, 2026
470e26f
chore: introduce canonical_path idempotency regression guard
maxnbk May 26, 2026
8b18625
chore: add failing tests which represent os.path.realpath->real_path …
maxnbk May 26, 2026
63324e0
fix: add real_path helper to slice between abspath/realpath on Window…
maxnbk May 26, 2026
81f398f
chore: clarify the explicit use-case of canonical_path for path norma…
maxnbk May 26, 2026
84685cc
chore: add test to windows which clarify/regression-guard against imp…
maxnbk May 26, 2026
5bd81cb
chore: real_path tests as regression-guards and verification of platf…
maxnbk May 26, 2026
38d38d3
chore: update singular os.path.realpath-using test to test real_path …
maxnbk May 26, 2026
29a15fe
fix: update all os.path.realpath-using call-sites to use real_path in…
maxnbk May 26, 2026
b206fbb
Convert crlf to lf
JeanChristopheMorinPerso May 30, 2026
92124cf
Merge branch 'main' into fix/windows-drive-letters-remap-to-unc-paths
JeanChristopheMorinPerso May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/rez/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ def _parse_env_var(self, value):
"suite_alias_prefix_char": Char,
"cache_packages_path": OptionalStr,
"package_definition_python_path": OptionalStr,
"resolve_links_on_windows": Bool,
"tmpdir": OptionalStr,
"context_tmpdir": OptionalStr,
"default_shell": OptionalStr,
Expand Down
6 changes: 3 additions & 3 deletions src/rez/resolved_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from rez.utils.formatting import columnise, PackageRequest, ENV_VAR_REGEX, \
header_comment, minor_header_comment
from rez.utils.data_utils import deep_del
from rez.utils.filesystem import TempDirs, is_subdirectory, canonical_path
from rez.utils.filesystem import TempDirs, is_subdirectory, canonical_path, real_path
from rez.utils.memcached import pool_memcached_connections
from rez.utils.logging_ import print_debug, print_error, print_warning
from rez.utils.which import which
Expand Down Expand Up @@ -1896,8 +1896,8 @@ def _adjust_variant_for_bundling(cls, handle: dict, out: bool) -> None:

if is_subdirectory(repo_path, bundle_path):
vars_["location"] = os.path.relpath(
os.path.realpath(repo_path),
os.path.realpath(bundle_path)
real_path(repo_path),
real_path(bundle_path)
)

# serializing in, make repo absolute
Expand Down
20 changes: 20 additions & 0 deletions src/rez/rezconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@
# For further information, see :ref:`package-definition-sharing-code`.
package_definition_python_path = None

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


###############################################################################
# Extensions
Expand Down
6 changes: 3 additions & 3 deletions src/rez/serialise.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from rez.package_resources import package_rex_keys
from rez.utils.scope import ScopeContext
from rez.utils.sourcecode import SourceCode, early, late, include
from rez.utils.filesystem import TempDirs
from rez.utils.filesystem import TempDirs, real_path
from rez.utils.data_utils import ModifyList
from rez.exceptions import ResourceError, InvalidPackageError
from rez.utils.memcached import memcached
Expand Down Expand Up @@ -66,7 +66,7 @@ def open_file_for_write(filepath, mode=None):
yield stream
content = stream.getvalue()

filepath = os.path.realpath(filepath)
filepath = real_path(filepath)
tmpdir = tmpdir_manager.mkdtemp()
cache_filepath = os.path.join(tmpdir, os.path.basename(filepath))

Expand Down Expand Up @@ -123,7 +123,7 @@ def load_from_file(filepath: str, format_=FileFormat.py, update_data_callback=No
Returns:
dict:
"""
filepath = os.path.realpath(filepath)
filepath = real_path(filepath)
cache_filepath = file_cache.get(filepath)

if cache_filepath:
Expand Down
5 changes: 3 additions & 2 deletions src/rez/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

from rez.utils.execution import create_forwarding_script
from rez.utils.filesystem import real_path
from rez.exceptions import SuiteError, ResolvedContextError
from rez.resolved_context import ResolvedContext
from rez.utils.data_utils import cached_property
Expand Down Expand Up @@ -458,7 +459,7 @@ def save(self, path, verbose: bool = False):
at `path`, then it will be overwritten. Otherwise, if `path`
exists, an error is raised.
"""
path = os.path.realpath(path)
path = real_path(path)
if os.path.exists(path):
if self.load_path and self.load_path == path:
if verbose:
Expand Down Expand Up @@ -528,7 +529,7 @@ def load(cls, path: str) -> Suite:
raise SuiteError("Failed loading suite: %s" % str(e))

s = cls.from_dict(data)
s.load_path = os.path.realpath(path)
s.load_path = real_path(path)
return s

@classmethod
Expand Down
3 changes: 2 additions & 1 deletion src/rez/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rez.utils.platform_ import platform_
from rez.exceptions import RezSystemError
from rez.utils.data_utils import cached_property
from rez.utils.filesystem import real_path


class System(object):
Expand Down Expand Up @@ -242,7 +243,7 @@ def rez_bin_path(self):

validation_file = os.path.join(binpath, ".rez_production_install")
if os.path.exists(validation_file):
return os.path.realpath(binpath)
return real_path(binpath)

return None

Expand Down
212 changes: 212 additions & 0 deletions src/rez/tests/test_package_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
Test package repository plugin.
"""
import unittest
import unittest.mock
from contextlib import contextmanager

from rezplugins.package_repository import filesystem
from rez.exceptions import ResourceError
from rez.packages import create_package
from rez.tests.util import TestBase, TempdirMixin
from rez.utils.platform_ import platform_
from rez.utils.resources import ResourceHandle


class TestFilesystemPackageRepository(TestBase, TempdirMixin):
Expand Down Expand Up @@ -39,3 +43,211 @@ def test_mismatching_case(self):
pkg_repository._create_variant(variant, overrides={})
with self.assertRaises(filesystem.PackageRepositoryError):
pkg_repository._create_variant(case_mismatch_variant, overrides={})


# ---------------------------------------------------------------------------
# Helpers shared by the Windows path-form tests below.
# ---------------------------------------------------------------------------

_MOCK_DRIVE_TO_UNC = {"n": "\\\\nas\\studio"}


def _unc_expanding_realpath(path: str) -> str:
"""Simulate py3.8+ Windows os.path.realpath: N:\\ expansion to \\\\nas\\studio\\."""
norm = path.replace("/", "\\")
if len(norm) >= 2 and norm[1] == ":":
drive = norm[0].lower()
rest = norm[2:]
if drive in _MOCK_DRIVE_TO_UNC:
return _MOCK_DRIVE_TO_UNC[drive] + rest
return path


@contextmanager
def _simulate_py38_unc_expansion():
"""Patch os.path.realpath and the filesystem plugin's platform_ reference
to reproduce the py3.8+ Windows drive-letter -> UNC expansion bug."""
mock_plat = unittest.mock.Mock(spec=["has_case_sensitive_filesystem", "name"])
mock_plat.has_case_sensitive_filesystem = False
mock_plat.name = "windows"
with unittest.mock.patch("os.path.realpath", side_effect=_unc_expanding_realpath):
with unittest.mock.patch.object(filesystem, "platform_", mock_plat):
yield


@unittest.skipIf(
platform_.name != "windows",
"Windows drive-letter / UNC path-consistency tests are Windows-only.",
)
class TestFilesystemRepoWindowsPathForms(TestBase, TempdirMixin):
"""Verify that drive-letter and UNC path styles are preserved throughout
the repository lifecycle.

Root cause of bugs #1438 / #2045: With os.path.realpath from py3.8 onward,
Windows silently converts mapped drive letters to a UNC equivalent.
canonical_path calls realpath, so FileSystemPackageRepository.__init__
stores a UNC self.location even when the caller supplied a drive-letter
path. Subsequent make_resource_handle / get_resource_from_handle calls
that carry the original drive-letter path cause a ResourceError, due to
the apparent location mismatch.
"""

@classmethod
def setUpClass(cls):
TempdirMixin.setUpClass()
cls.settings = {}

@classmethod
def tearDownClass(cls):
TempdirMixin.tearDownClass()

# ------------------------------------------------------------------
# FileSystemPackageRepository.__init__ - self.location form
# ------------------------------------------------------------------

def test_repo_init_preserves_drive_letter_location(self):
"""repo.location must maintain drive-letter input path to drive-letter output path.

With the original realpath call, __init__ calls canonical_path,
converting N:\\ to \\\\nas\\studio\\. After the fix, canonical_path
on Windows must use abspath or configurably resolve symlinks, so
self.location stays as the caller supplied it.
"""
pool = filesystem.ResourcePool(cache_size=None)
with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository("N:\\packages", pool)

self.assertFalse(
repo.location.startswith("\\\\"),
f"repo.location was unexpectedly expanded to UNC: {repo.location!r}",
)
self.assertTrue(
repo.location.lower().startswith("n:\\"),
f"Expected drive-letter location starting with 'n:\\', got: {repo.location!r}",
)

def test_repo_init_preserves_unc_location(self):
"""repo.location must maintain UNC input path to UNC output path."""
pool = filesystem.ResourcePool(cache_size=None)
unc = "\\\\nas\\studio\\packages"
with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository(unc, pool)

self.assertTrue(
repo.location.startswith("\\\\"),
f"UNC repo.location lost its UNC form: {repo.location!r}",
)

# ------------------------------------------------------------------
# make_resource_handle - location comparison (base-class code path)
# ------------------------------------------------------------------

def test_make_resource_handle_drive_letter_no_mismatch(self):
"""make_resource_handle must not raise when both the repo and the caller
use consistent drive-letter paths.

__init__ used to UNC-expand self.location via realpath, so the base-class
make_resource_handle compared the caller's 'N:\\packages' against the
stored '\\\\nas\\studio\\packages' and raised ResourceError.
"""
pool = filesystem.ResourcePool(cache_size=None)
with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository("N:\\packages", pool)
try:
repo.make_resource_handle(
"filesystem.family",
location="N:\\packages",
name="mypkg",
)
except ResourceError as exc:
self.fail(
f"make_resource_handle raised ResourceError for matching "
f"drive-letter paths: {exc}"
)

def test_make_resource_handle_unc_no_mismatch(self):
"""make_resource_handle must not raise when both repo and caller use
consistent UNC paths."""
pool = filesystem.ResourcePool(cache_size=None)
unc = "\\\\nas\\studio\\packages"
with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository(unc, pool)
try:
repo.make_resource_handle(
"filesystem.family",
location=unc,
name="mypkg",
)
except ResourceError as exc:
self.fail(
f"make_resource_handle raised ResourceError for matching "
f"UNC paths: {exc}"
)

# ------------------------------------------------------------------
# get_resource_from_handle - filesystem-plugin code path
# ------------------------------------------------------------------

def test_get_resource_from_handle_drive_letter_no_mismatch(self):
"""get_resource_from_handle must not raise ResourceError when both
the handle and the repo use drive-letter pathing.

The filesystem plugin overrides get_resource_from_handle and applies
canonical_path as a bridge for maintaining path-style consistency -
but that bridge cannot work if canonical_path itself expands the
drive-letter to UNC (making both sides UNC when the repo was created
with a drive-letter path, or vice-versa).
"""
pool = filesystem.ResourcePool(cache_size=None)
drive_letter_path = "N:\\packages"

with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository(drive_letter_path, pool)
handle = ResourceHandle(
"filesystem.family",
{
"repository_type": "filesystem",
"location": drive_letter_path,
"name": "mypkg",
},
)
# Mock the pool's own get_resource_from_handle so we do not need
# actual packages on disk - we only want to exercise the location
# verification logic, not the resource loading.
with unittest.mock.patch.object(
repo.pool, "get_resource_from_handle", return_value=unittest.mock.Mock()
):
try:
repo.get_resource_from_handle(handle, verify_repo=True)
except ResourceError as exc:
self.fail(
f"get_resource_from_handle raised ResourceError for a "
f"drive-letter handle against a drive-letter repo: {exc}"
)

def test_get_resource_from_handle_unc_no_mismatch(self):
"""get_resource_from_handle must not raise ResourceError when both
handle and repo both use UNC paths."""
pool = filesystem.ResourcePool(cache_size=None)
unc = "\\\\nas\\studio\\packages"

with _simulate_py38_unc_expansion():
repo = filesystem.FileSystemPackageRepository(unc, pool)
handle = ResourceHandle(
"filesystem.family",
{
"repository_type": "filesystem",
"location": unc,
"name": "mypkg",
},
)
with unittest.mock.patch.object(
repo.pool, "get_resource_from_handle", return_value=unittest.mock.Mock()
):
try:
repo.get_resource_from_handle(handle, verify_repo=True)
except ResourceError as exc:
self.fail(
f"get_resource_from_handle raised ResourceError for a "
f"UNC handle against a UNC repo: {exc}"
)
Loading
Loading