2020# -i <packages_dir>: Directory containing wheel files (--find-links)
2121# -p <python_version>: Python version (default: 3.11)
2222# -q: Quick test mode (only run core tests)
23+ # -F: Force fresh venv (macOS only; delete and recreate instead of reusing)
2324#
2425# Examples:
2526# # From repo root with auto-detection:
3536# COPY python/README.md.in /tmp/README.md
3637#
3738# Note: To run target tests, set the necessary API keys (IONQ_API_KEY, etc.)
39+ #
40+ # TODO: Unify wheel validation around this script. Currently:
41+ # - ci_macos.yml uses this script (auto-detects from repo, runs everything)
42+ # - publishing.yml uses this script (selective dir copy to /tmp, no targets/)
43+ # - python_wheels.yml does NOT use this script; it runs pytest, snippets,
44+ # and examples directly with explicit `find` exclusions in Docker containers.
45+ # The goal is to have all wheel validation run through this script, replacing the
46+ # inline find/pytest commands in python_wheels.yml. Use -q for quick
47+ # (core pytest only, suitable for per-PR CI) and full mode for publishing.
48+ # This avoids duplicating skip logic between the script and workflow files.
3849
3950__optind__=$OPTIND
4051OPTIND=1
4152python_version=3.11
4253quick_test=false
43- while getopts " :c:f:i:p:qv:" opt; do
54+ fresh_venv=false
55+ while getopts " :c:f:Fi:p:qv:" opt; do
4456 case $opt in
4557 c)
4658 cuda_version_conda=" $OPTARG "
4759 ;;
4860 f)
4961 root_folder=" $OPTARG "
5062 ;;
63+ F)
64+ fresh_venv=true
65+ ;;
5166 p)
5267 python_version=" $OPTARG "
5368 ;;
@@ -68,6 +83,26 @@ while getopts ":c:f:i:p:qv:" opt; do
6883done
6984OPTIND=$__optind__
7085
86+ # Sanitize environment: unset variables that could leak build-tree or
87+ # system-installed CUDA-Q libraries into the validation environment.
88+ # Without this, DYLD_LIBRARY_PATH from a prior build step can cause the
89+ # wheel to load libraries from _skbuild/ instead of its own bundled copies,
90+ # masking packaging bugs.
91+ SANITIZE_VARS="
92+ DYLD_LIBRARY_PATH
93+ DYLD_FALLBACK_LIBRARY_PATH
94+ LD_LIBRARY_PATH
95+ CUDAQ_INSTALL_PREFIX
96+ CUDA_QUANTUM_PATH
97+ PYTHONPATH
98+ "
99+
100+ for var in $SANITIZE_VARS ; do
101+ unset " $var "
102+ done
103+
104+ echo " Environment sanitized (unset: $SANITIZE_VARS )"
105+
71106# Auto-detect repo structure if -f not provided
72107if [ -z " $root_folder " ]; then
73108 # Try to find repo root
@@ -89,13 +124,16 @@ if [ -z "$root_folder" ]; then
89124 rm -rf " ${staging_dir:? } "
90125 mkdir -p " $staging_dir "
91126
92- # Symlink test files to staging (mirrors CI copy structure)
93- ln -sf " $readme_src " " $staging_dir /README.md"
94- ln -sf " $repo_root /python/tests" " $staging_dir /tests"
95- ln -sf " $repo_root /docs/sphinx/examples/python" " $staging_dir /examples"
96- ln -sf " $repo_root /docs/sphinx/snippets/python" " $staging_dir /snippets"
127+ # Copy test files to staging (mirrors CI copy structure).
128+ # Use cp -r instead of symlinks for robustness: find(1) may not
129+ # follow initial symlinks in all environments, and CI runners may
130+ # have different filesystem semantics.
131+ cp -f " $readme_src " " $staging_dir /README.md"
132+ cp -r " $repo_root /python/tests" " $staging_dir /tests"
133+ cp -r " $repo_root /docs/sphinx/examples/python" " $staging_dir /examples"
134+ cp -r " $repo_root /docs/sphinx/snippets/python" " $staging_dir /snippets"
97135 if [ -d " $repo_root /docs/sphinx/targets/python" ]; then
98- ln -sf " $repo_root /docs/sphinx/targets/python" " $staging_dir /targets"
136+ cp -r " $repo_root /docs/sphinx/targets/python" " $staging_dir /targets"
99137 fi
100138
101139 root_folder=" $staging_dir "
111149
112150echo " Using test root folder: $root_folder "
113151
114- # Detect platform
152+ # Detect platform and GPU availability
115153is_macos=false
154+ has_cuda=false
116155if [ " $( uname) " = " Darwin" ]; then
117156 is_macos=true
118157 echo " macOS detected: running CPU-only validation"
158+ else
159+ if [ -x " $( command -v nvidia-smi) " ] && nvidia-smi & > /dev/null; then
160+ has_cuda=true
161+ fi
162+ fi
163+ if ! $has_cuda ; then
164+ echo " No CUDA GPU detected: GPU-dependent tests will be skipped"
119165fi
120166
167+ # Check if a Python file requires a GPU target that is not available.
168+ # Returns 0 (true) if the file should be skipped, 1 (false) otherwise.
169+ requires_unavailable_gpu_target () {
170+ local file=" $1 "
171+ if $has_cuda ; then
172+ return 1
173+ fi
174+ local targets
175+ targets=$( awk -F' "' ' /cudaq\.set_target/ {print $2}' " $file " )
176+ for t in $targets ; do
177+ case " $t " in
178+ nvidia|nvidia-fp64|nvidia-mgpu|dynamics|tensornet|remote-mqpu)
179+ echo " Skipping $file (requires GPU target '$t ')"
180+ return 0
181+ ;;
182+ esac
183+ done
184+ return 1
185+ }
186+
121187# Check that the `cuda_version_conda` is a full version string like "12.8.0" (Linux only)
122188if ! $is_macos && ! [[ $cuda_version_conda =~ ^[0-9]+\. [0-9]+\. [0-9]+$ ]]; then
123189 echo -e " \e[01;31mThe cuda_version_conda (-c) must be a full version string like '12.8.0'. Provided: '${cuda_version_conda} '.\e[0m" >&2
@@ -142,8 +208,13 @@ if $is_macos; then
142208 # macOS: use venv (simpler, no conda ToS issues, no MPI needed for CPU-only)
143209 venv_dir=" $HOME /.venv/cudaq-validation"
144210
211+ if $fresh_venv && [ -d " $venv_dir " ]; then
212+ echo " Removing existing venv at $venv_dir "
213+ rm -rf " $venv_dir "
214+ fi
215+
145216 if [ -d " $venv_dir " ]; then
146- echo " Reusing existing venv at $venv_dir (delete it to start fresh)"
217+ echo " Reusing existing venv at $venv_dir (use -F to start fresh)"
147218 else
148219 echo " Creating venv at $venv_dir "
149220 python3 -m venv " $venv_dir "
@@ -205,6 +276,13 @@ if ! $is_macos; then
205276fi
206277status_sum=0
207278
279+ # Run all tests from a temp directory so the repo tree (cwd, _skbuild/,
280+ # etc.) cannot accidentally be found via sys.path or dyld search paths
281+ # to ensure the wheel is installed in isolation.
282+ test_workdir=$( mktemp -d)
283+ echo " Running tests from isolated directory: $test_workdir "
284+ cd " $test_workdir "
285+
208286# Verify that the necessary GPU targets are installed and usable (Linux only)
209287if $is_macos ; then
210288 echo " Skipping GPU target verification on macOS (CPU-only)"
284362
285363# Run snippets in docs
286364for ex in $( find " $root_folder /snippets" -name ' *.py' ) ; do
365+ if requires_unavailable_gpu_target " $ex " ; then
366+ continue
367+ fi
287368 echo " Executing $ex "
288369 python3 " $ex "
289370 if [ ! $? -eq 0 ]; then
@@ -294,13 +375,13 @@ done
294375
295376# Run examples
296377for ex in $( find " $root_folder /examples" -name ' *.py' ) ; do
378+ if requires_unavailable_gpu_target " $ex " ; then
379+ continue
380+ fi
297381 skip_example=false
298- # Extract target names from cudaq.set_target("...") calls (awk splits on quotes, prints field 2)
299382 explicit_targets=$( awk -F' "' ' /cudaq\.set_target/ {print $2}' " $ex " )
300383 for t in $explicit_targets ; do
301384 if [ " $t " == " quera" ] || [ " $t " == " braket" ]; then
302- # Skipped because GitHub does not have the necessary authentication token
303- # to submit a (paid) job to Amazon Braket (includes QuEra).
304385 echo -e " \e[01;31mWarning: Explicitly set target braket or quera in $ex ; skipping validation due to paid submission.\e[0m" >&2
305386 skip_example=true
306387 elif [ " $t " == " pasqal" ] && [ -z " ${PASQAL_PASSWORD} " ]; then
@@ -318,9 +399,19 @@ for ex in $(find "$root_folder/examples" -name '*.py'); do
318399 fi
319400done
320401
402+ snippet_count=$( find " $root_folder /snippets" -name ' *.py' 2> /dev/null | wc -l)
403+ example_count=$( find " $root_folder /examples" -name ' *.py' 2> /dev/null | wc -l)
404+ if [ " $snippet_count " -eq 0 ] && [ " $example_count " -eq 0 ]; then
405+ echo -e " \e[01;31mNo snippets or examples found in $root_folder . Check staging setup.\e[0m" >&2
406+ status_sum=$(( status_sum + 1 ))
407+ fi
408+
321409# Run target tests if target folder exists.
322410if [ -d " $root_folder /targets" ]; then
323411 for ex in $( find " $root_folder /targets" -name ' *.py' ) ; do
412+ if requires_unavailable_gpu_target " $ex " ; then
413+ continue
414+ fi
324415 skip_example=false
325416 # Extract target names from cudaq.set_target("...") calls (awk splits on quotes, prints field 2)
326417 explicit_targets=$( awk -F' "' ' /cudaq\.set_target/ {print $2}' " $ex " )
@@ -345,6 +436,13 @@ if [ -d "$root_folder/targets" ]; then
345436 elif [ " $t " == " ionq" ] && [ -z " ${IONQ_API_KEY} " ]; then
346437 echo -e " \e[01;31mWarning: Explicitly set target ionq in $ex ; skipping validation due to missing API key.\e[0m" >&2
347438 skip_example=true
439+ elif [ " $t " == " quantum_machines" ] || [ " $t " == " quantinuum" ] || \
440+ [ " $t " == " orca" ] || [ " $t " == " orca-photonics" ] || \
441+ [ " $t " == " iqm" ] || [ " $t " == " infleqtion" ] || [ " $t " == " anyon" ]; then
442+ # These targets require remote backends that are not available
443+ # in CI or local dev without explicit setup.
444+ echo " Skipping $ex (remote target '$t ' not available)"
445+ skip_example=true
348446 fi
349447 done
350448 if ! $skip_example ; then
0 commit comments