diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..344ca1841 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,34 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "version": "1.0.6", + "resolved": "ghcr.io/devcontainers-extra/features/apt-packages@sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd", + "integrity": "sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd" + }, + "ghcr.io/devcontainers-extra/features/cmake:1": { + "version": "1.0.0", + "resolved": "ghcr.io/devcontainers-extra/features/cmake@sha256:86e25502f6b1213a68d88e4e55c969eba0b5016b9666d216ab65f5047808bc90", + "integrity": "sha256:86e25502f6b1213a68d88e4e55c969eba0b5016b9666d216ab65f5047808bc90" + }, + "ghcr.io/devcontainers-extra/features/uv:1": { + "version": "1.0.2", + "resolved": "ghcr.io/devcontainers-extra/features/uv@sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8", + "integrity": "sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8" + }, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "version": "1.10.0", + "resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", + "integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e" + }, + "ghcr.io/devcontainers/features/git-lfs:1": { + "version": "1.2.5", + "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72", + "integrity": "sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "1.3.8", + "resolved": "ghcr.io/devcontainers/features/git@sha256:fd75977de13a9979000e0e78baf949adb0ca71d2398995fa22e0a36d7e7e7fe2", + "integrity": "sha256:fd75977de13a9979000e0e78baf949adb0ca71d2398995fa22e0a36d7e7e7fe2" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f88e4bb1f..f8507ba98 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "ghcr.io/devcontainers/features/git-lfs:1": {}, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/devcontainers-extra/features/apt-packages:1": { - "packages": "clang-format-14,libx11-dev,libxrandr-dev,libxxf86vm-dev,libgl1-mesa-dev" + "packages": "clang-format-14,libx11-dev,libxrandr-dev,libxxf86vm-dev,libgl1-mesa-dev,patchelf" }, "ghcr.io/devcontainers-extra/features/cmake:1": {}, "ghcr.io/devcontainers-extra/features/uv:1": {} @@ -14,6 +14,9 @@ "postCreateCommand": "uv tool install pre-commit && pre-commit install", "customizations": { "vscode": { + "settings": { + "cmake.buildDirectory": "${workspaceFolder}/build-devcontainer" + }, "extensions": [ "ms-vscode.cpptools", "ms-vscode.cmake-tools", diff --git a/.gitignore b/.gitignore index f3d14020a..543342f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ package-lock.json # Build directories build/ +build-devcontainer/ _build/ **/build/ install/ diff --git a/cmake/SetupPython.cmake b/cmake/SetupPython.cmake index 853b6d43f..3b1390915 100644 --- a/cmake/SetupPython.cmake +++ b/cmake/SetupPython.cmake @@ -150,15 +150,46 @@ if(BUILD_PYTHON_BINDINGS) else() set(_venv_python "${_build_venv}/bin/python") endif() - # Reuse an existing venv when possible. If the directory exists but the - # interpreter is missing, fail fast and require explicit cleanup. - if(EXISTS "${_build_venv}" AND NOT EXISTS "${_venv_python}") - message(FATAL_ERROR - "Found stale build venv directory at ${_build_venv}, but no interpreter at ${_venv_python}. " - "Please remove ${_build_venv} and reconfigure." + # Reuse the venv when its interpreter still runs. bin/python is an absolute + # symlink into a uv-managed Python, so it is only valid in the environment + # that created the venv. + set(_venv_usable FALSE) + if(EXISTS "${_venv_python}") + execute_process( + COMMAND "${_venv_python}" --version + RESULT_VARIABLE _venv_python_ok + OUTPUT_QUIET + ERROR_QUIET ) + if(_venv_python_ok EQUAL 0) + set(_venv_usable TRUE) + endif() endif() - if(NOT EXISTS "${_venv_python}") + + if(_venv_usable) + message(STATUS "Reusing existing build venv at ${_build_venv}") + elseif(EXISTS "${_build_venv}") + # Directory is present but its interpreter is not usable. Describe the + # actual failure: a dangling bin/python symlink (the common case when the + # venv was created in a different environment, e.g. a dev container vs the + # host, whose uv-managed Python lives elsewhere) versus an interpreter that + # is present but does not run. Only the former means the target is missing. + if(IS_SYMLINK "${_venv_python}" AND NOT EXISTS "${_venv_python}") + file(READ_SYMLINK "${_venv_python}" _venv_link_target) + string(CONCAT _venv_detail + "Its interpreter symlink is broken: ${_venv_python} -> " + "${_venv_link_target} (target does not exist). This build venv was " + "created in a different environment whose uv-managed Python lives " + "elsewhere.") + else() + set(_venv_detail "Its interpreter ${_venv_python} is missing or does not run.") + endif() + message(FATAL_ERROR + "Build venv at ${_build_venv} has no working Python interpreter. " + "${_venv_detail}\n" + "Remove the build directory ${CMAKE_BINARY_DIR} (or run 'cmake --fresh') " + "and reconfigure to recreate the venv for this environment.") + else() execute_process( COMMAND "${UV_EXECUTABLE}" venv --python "${Python3_EXECUTABLE}" "${_build_venv}" RESULT_VARIABLE _venv_ok @@ -167,8 +198,6 @@ if(BUILD_PYTHON_BINDINGS) if(NOT _venv_ok EQUAL 0) message(FATAL_ERROR "Failed to create build venv: ${_venv_err}") endif() - else() - message(STATUS "Reusing existing build venv at ${_build_venv}") endif() execute_process( COMMAND "${UV_EXECUTABLE}" pip install --python "${_venv_python}" "numpy>=2.0"