diff --git a/.github/workflows/python_metapackages.yml b/.github/workflows/python_metapackages.yml index 500239b882..bb548387bd 100644 --- a/.github/workflows/python_metapackages.yml +++ b/.github/workflows/python_metapackages.yml @@ -77,7 +77,7 @@ jobs: echo "Creating README.md for cuda-quantum package" echo "# Welcome to the CUDA-Q Python API" > README.md - echo "This package is deprecated and new versions are published under the name `cudaq` instead." >> README.md + echo "This package is deprecated and new versions are published under the name \`cudaq\` instead." >> README.md echo "For more information, please see [CUDA-Q on PyPI](https://pypi.org/project/cudaq)." >> README.md echo "Building cuda-quantum metapackage ..." @@ -195,8 +195,6 @@ jobs: fi - name: Test installation error - # FIXME: We actually don't give a proper install error if there are conflicting packages on the system... - if: matrix.cuda_version == '' run: | python=python${{ matrix.python_version }} $python -m pip install pypiserver @@ -211,11 +209,13 @@ jobs: set -e && check_package=cuda-quantum ;; 12.*) + test_conflicting=cuda-quantum-cu11 $python -m pip install cuda-quantum-cu11==${{ inputs.cudaq_version }} \ --extra-index-url http://localhost:8080 set +e # continue on error $python -m pip install cudaq==${{ inputs.cudaq_version }} -v \ - --extra-index-url http://localhost:8080 + --extra-index-url http://localhost:8080 \ + 2>&1 | tee /tmp/install.out set -e && check_package=cudaq ;; *) @@ -225,17 +225,37 @@ jobs: set -e if [ -z "$($python -m pip list | grep cuda-quantum)" ]; then # if we don't have a 0.8.0 version for this python version, test other conflict + test_conflicting=cuda-quantum-cu12 $python -m pip install cuda-quantum-cu12==${{ inputs.cudaq_version }} \ --extra-index-url http://localhost:8080 + else + test_conflicting=cuda-quantum fi set +e # continue on error $python -m pip install cudaq==${{ inputs.cudaq_version }} -v \ - --extra-index-url http://localhost:8080 + --extra-index-url http://localhost:8080 \ + 2>&1 | tee /tmp/install.out set -e && check_package=cudaq ;; esac - if [ -n "$($python -m pip list | grep ${check_package})" ]; then + # The autodetection will fail if the runner does not have a GPU. + # In that case, we will only check if the install failed, if the + # package we want to test the conflict detection for is not the + # default package that is installed when there is no GPU. + if [ -f /tmp/install.out ] && [ -z "$(cat /tmp/install.out | grep -o 'Autodetection succeeded')" ]; then + # Autodetection failed - a default choice of the binary distribution will be installed. + echo "::warning::Autodetection to determine cudaq binary distribution failed." + # Take the first Identified best package because the logs print multiple lines. + # They should all be the same, if they differ in the build environment then there is probably issues. + installed_default=$(cat /tmp/install.out | sed -nE 's/.*Identified (\S*) as the best package.*/\1/p' | head -n 1) + echo "::warning::The installed default is ${installed_default}, the potential conflicting package is ${test_conflicting}" + if [ "$installed_default" == "$test_conflicting" ]; then + check_package=none + fi + fi + + if [ "$check_package" != "none" ] && [ -n "$($python -m pip list | grep ${check_package})" ]; then echo "::error file=python_metapackages.yml::Unexpected installation of ${check_package} package." exit 1 fi diff --git a/python/metapackages/setup.py b/python/metapackages/setup.py index c30c1152e0..79664b05f6 100644 --- a/python/metapackages/setup.py +++ b/python/metapackages/setup.py @@ -8,6 +8,7 @@ import ctypes, os, sys import importlib.util +import site, glob from setuptools import setup from typing import Optional @@ -116,22 +117,76 @@ def _get_cuda_version() -> Optional[int]: return None -def _infer_best_package() -> str: +def _check_package_installed(package_name) -> bool: + """ + Checks if a python package is installed globally or in user packages. + """ + + # ideally you would use `importlib.util.find_spec` to check for the file. + # For some reason, that does not work during pip install, which I'm + # not clear about for now. This is worked around by getting the site + # packages and searching for the files manually. + + def _check_in_directory(directory, package_name): + """ + Helper function to check if a package exists in a given directory. + """ + search_pattern = os.path.join(directory, f"{package_name}-*.dist-info") + matches = glob.glob(search_pattern) + if matches: + _log( + f"Found the following matches for '{package_name}*' in {directory}:" + ) + for match in matches: + _log(match) + return True + return False + + def _replace_hyphens_with_underscores(input_string): + return input_string.replace("-", "_") + + # ``` + # Normalize because package name hyphens replaced with underscores + # ex: + # # ls /usr/local/lib/python3.10/dist-packages | grep cuda + # cuda_quantum_cu11-0.9.0.dist-info + # cuda_quantum_cu11.libs + # cuda_quantum_cu12-0.9.0.dist-info + # cuda_quantum_cu12.libs + # ``` + normalized_package_name = _replace_hyphens_with_underscores(package_name) + + # Check in user site-packages + user_site_packages = site.getusersitepackages() + if os.path.exists(user_site_packages): + _log(f"User site-packages directory: {user_site_packages}") + return _check_in_directory(user_site_packages, normalized_package_name) + else: + _log("User site-packages directory does not exist.") + + # Check in global site-packages + site_packages_dirs = site.getsitepackages() + _log(f"Global site-packages directories: {site_packages_dirs}") + for directory in site_packages_dirs: + if _check_in_directory(directory, normalized_package_name): + return True + + _log( + f"No matches found for '{normalized_package_name}' in any site-packages directory." + ) + return False + +def _infer_best_package() -> str: + """ + Checks what packages should be installed, and handles potential conflicts. + """ # Find the existing wheel installation installed = [] - for pkg_suffix in ['', '-cu11', '-cu12']: - _log(f"Looking for existing installation of cuda-quantum{pkg_suffix}.") - try: - package_spec = importlib.util.find_spec(f"cuda-quantum{pkg_suffix}") - if package_spec is None: - _log("No installation found.") - else: - installed.append(f"cuda-quantum{pkg_suffix}") - _log("Installation found.") - except: - _log("No installation found.") - pass + for pkg in ['cuda-quantum', 'cuda-quantum-cu11', 'cuda-quantum-cu12']: + _log(f"Looking for existing installation of {pkg}.") + if _check_package_installed(pkg): + installed.append(pkg) cuda_version = _get_cuda_version() if cuda_version is None: