Skip to content

Commit 1876f16

Browse files
committed
UserWarning: Both CUDA_HOME and CUDA_PATH are set but differ
1 parent 0c5f6c2 commit 1876f16

File tree

2 files changed

+224
-3
lines changed

2 files changed

+224
-3
lines changed

cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,51 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
import os
5+
import warnings
56
from typing import Optional
67

78

9+
def _paths_differ(a: str, b: str) -> bool:
10+
"""
11+
Return True if paths are observably different.
12+
13+
Strategy:
14+
1) Compare os.path.normcase(os.path.normpath(...)) for quick, robust textual equality.
15+
- Handles trailing slashes and case-insensitivity on Windows.
16+
2) If still different AND both exist, use os.path.samefile to resolve symlinks/junctions.
17+
3) Otherwise (nonexistent paths or samefile unavailable), treat as different.
18+
"""
19+
norm_a = os.path.normcase(os.path.normpath(a))
20+
norm_b = os.path.normcase(os.path.normpath(b))
21+
if norm_a == norm_b:
22+
return False
23+
24+
try:
25+
if os.path.exists(a) and os.path.exists(b):
26+
# samefile raises on non-existent paths; only call when both exist.
27+
return not os.path.samefile(a, b)
28+
except OSError:
29+
# Fall through to "different" if samefile isn't applicable/available.
30+
pass
31+
32+
# If normalized strings differ and we couldn't prove they're the same entry, treat as different.
33+
return True
34+
35+
836
def get_cuda_home_or_path() -> Optional[str]:
937
cuda_home = os.environ.get("CUDA_HOME")
10-
if cuda_home is None:
11-
cuda_home = os.environ.get("CUDA_PATH")
12-
return cuda_home
38+
cuda_path = os.environ.get("CUDA_PATH")
39+
40+
if cuda_home and cuda_path and _paths_differ(cuda_home, cuda_path):
41+
warnings.warn(
42+
"Both CUDA_HOME and CUDA_PATH are set but differ:\n"
43+
f" CUDA_HOME={cuda_home}\n"
44+
f" CUDA_PATH={cuda_path}\n"
45+
"Using CUDA_HOME (higher priority).",
46+
UserWarning,
47+
stacklevel=2,
48+
)
49+
50+
if cuda_home is not None:
51+
return cuda_home
52+
return cuda_path
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import os
5+
import pathlib
6+
import sys
7+
import warnings
8+
9+
import pytest
10+
11+
from cuda.pathfinder._utils.env_vars import _paths_differ, get_cuda_home_or_path
12+
13+
skip_symlink_tests = pytest.mark.skipif(
14+
sys.platform == "win32",
15+
reason="Exercising symlinks intentionally omitted for simplicity",
16+
)
17+
18+
19+
def unset_env(monkeypatch):
20+
"""Helper to clear both env vars for each test."""
21+
monkeypatch.delenv("CUDA_HOME", raising=False)
22+
monkeypatch.delenv("CUDA_PATH", raising=False)
23+
24+
25+
def test_returns_none_when_unset(monkeypatch):
26+
unset_env(monkeypatch)
27+
assert get_cuda_home_or_path() is None
28+
29+
30+
def test_empty_cuda_home_preserved(monkeypatch):
31+
# empty string is returned as-is if set.
32+
monkeypatch.setenv("CUDA_HOME", "")
33+
monkeypatch.setenv("CUDA_PATH", "/does/not/matter")
34+
assert get_cuda_home_or_path() == ""
35+
36+
37+
def test_prefers_cuda_home_over_cuda_path(monkeypatch, tmp_path):
38+
unset_env(monkeypatch)
39+
home = tmp_path / "home"
40+
path = tmp_path / "path"
41+
home.mkdir()
42+
path.mkdir()
43+
44+
monkeypatch.setenv("CUDA_HOME", str(home))
45+
monkeypatch.setenv("CUDA_PATH", str(path))
46+
47+
# Different directories -> warning + prefer CUDA_HOME
48+
with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"):
49+
result = get_cuda_home_or_path()
50+
assert pathlib.Path(result) == home
51+
52+
53+
def test_uses_cuda_path_if_home_missing(monkeypatch, tmp_path):
54+
unset_env(monkeypatch)
55+
only_path = tmp_path / "path"
56+
only_path.mkdir()
57+
monkeypatch.setenv("CUDA_PATH", str(only_path))
58+
assert pathlib.Path(get_cuda_home_or_path()) == only_path
59+
60+
61+
def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_path):
62+
"""
63+
Trailing slashes should not trigger a warning, thanks to normpath.
64+
This works cross-platform.
65+
"""
66+
unset_env(monkeypatch)
67+
d = tmp_path / "cuda"
68+
d.mkdir()
69+
70+
with_slash = str(d) + ("/" if os.sep == "/" else "\\")
71+
monkeypatch.setenv("CUDA_HOME", str(d))
72+
monkeypatch.setenv("CUDA_PATH", with_slash)
73+
74+
# No warning; same logical directory
75+
with warnings.catch_warnings(record=True) as record:
76+
result = get_cuda_home_or_path()
77+
assert pathlib.Path(result) == d
78+
assert len(record) == 0
79+
80+
81+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific case-folding check")
82+
def test_no_warning_on_windows_case_only_difference(monkeypatch, tmp_path):
83+
"""
84+
On Windows, paths differing only by case should not warn because normcase collapses case.
85+
"""
86+
unset_env(monkeypatch)
87+
d = tmp_path / "Cuda"
88+
d.mkdir()
89+
90+
upper = str(d).upper()
91+
lower = str(d).lower()
92+
monkeypatch.setenv("CUDA_HOME", upper)
93+
monkeypatch.setenv("CUDA_PATH", lower)
94+
95+
with warnings.catch_warnings(record=True) as record:
96+
warnings.simplefilter("always")
97+
result = get_cuda_home_or_path()
98+
assert pathlib.Path(result).samefile(d)
99+
assert len(record) == 0
100+
101+
102+
def test_warning_when_both_exist_and_are_different(monkeypatch, tmp_path):
103+
unset_env(monkeypatch)
104+
a = tmp_path / "a"
105+
b = tmp_path / "b"
106+
a.mkdir()
107+
b.mkdir()
108+
109+
monkeypatch.setenv("CUDA_HOME", str(a))
110+
monkeypatch.setenv("CUDA_PATH", str(b))
111+
112+
# Different actual dirs -> warning
113+
with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"):
114+
result = get_cuda_home_or_path()
115+
assert pathlib.Path(result) == a
116+
117+
118+
def test_nonexistent_paths_fall_back_to_text_comparison(monkeypatch, tmp_path):
119+
"""
120+
If one or both paths don't exist, we compare normalized strings.
121+
Different strings should warn.
122+
"""
123+
unset_env(monkeypatch)
124+
a = tmp_path / "does_not_exist_a"
125+
b = tmp_path / "does_not_exist_b"
126+
127+
monkeypatch.setenv("CUDA_HOME", str(a))
128+
monkeypatch.setenv("CUDA_PATH", str(b))
129+
130+
with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"):
131+
result = get_cuda_home_or_path()
132+
assert pathlib.Path(result) == a
133+
134+
135+
@skip_symlink_tests
136+
def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path):
137+
"""
138+
If both paths exist and one is a symlink/junction to the other, we should NOT warn.
139+
"""
140+
unset_env(monkeypatch)
141+
real_dir = tmp_path / "real"
142+
real_dir.mkdir()
143+
144+
link_dir = tmp_path / "alias"
145+
146+
os.symlink(str(real_dir), str(link_dir), target_is_directory=True)
147+
148+
# Set env vars to real and alias
149+
monkeypatch.setenv("CUDA_HOME", str(real_dir))
150+
monkeypatch.setenv("CUDA_PATH", str(link_dir))
151+
152+
# Because they resolve to the same entry, no warning should be raised
153+
with warnings.catch_warnings(record=True) as record:
154+
warnings.simplefilter("always")
155+
result = get_cuda_home_or_path()
156+
assert pathlib.Path(result) == real_dir
157+
assert len(record) == 0
158+
159+
160+
# --- unit tests for the helper itself (optional but nice to have) ---
161+
162+
163+
def test_paths_differ_text_only(tmp_path):
164+
a = tmp_path / "x"
165+
b = tmp_path / "x" / ".." / "x" # normalizes to same
166+
assert _paths_differ(str(a), str(b)) is False
167+
168+
a = tmp_path / "x"
169+
b = tmp_path / "y"
170+
assert _paths_differ(str(a), str(b)) is True
171+
172+
173+
@skip_symlink_tests
174+
def test_paths_differ_samefile(tmp_path):
175+
real_dir = tmp_path / "r"
176+
real_dir.mkdir()
177+
alias = tmp_path / "alias"
178+
os.symlink(str(real_dir), str(alias), target_is_directory=True)
179+
180+
# Should detect equivalence via samefile
181+
assert _paths_differ(str(real_dir), str(alias)) is False

0 commit comments

Comments
 (0)