Index: CMakeLists.txt =================================================================== diff -u -rac303b902c681a25ff0d910dd56ab309669e381f -r6cdf791210cfa0d96514094d33510e639f9bc0b6 --- CMakeLists.txt (.../CMakeLists.txt) (revision ac303b902c681a25ff0d910dd56ab309669e381f) +++ CMakeLists.txt (.../CMakeLists.txt) (revision 6cdf791210cfa0d96514094d33510e639f9bc0b6) @@ -6,6 +6,7 @@ ) include(cmake/Debug.cmake) +include(cmake/PythonVenv.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) Index: cmake/PythonVenv.cmake =================================================================== diff -u --- cmake/PythonVenv.cmake (revision 0) +++ cmake/PythonVenv.cmake (revision 6cdf791210cfa0d96514094d33510e639f9bc0b6) @@ -0,0 +1,113 @@ +# PythonVenv.cmake — project-wide Python venv shared by every component that +# ships Python tooling (currently MsgUtils; LogReader will join later). +# +# Why a venv at all: PEP 668 distros (Debian/Ubuntu with Python 3.11+, etc.) +# forbid `pip install` into the system or user site-packages. The venv lets +# editable installs work without `--break-system-packages` and without +# polluting the developer's machine. +# +# Why project-level (and not per-component): more than one component needs a +# Python interpreter to install into; pointing each at the same venv keeps +# every installed package mutually importable, avoids duplicate venv setup +# code in every CMakeLists.txt, and makes "where do my Python deps live" +# answerable in one place. +# +# Usage: +# include(cmake/PythonVenv.cmake) # creates ${PROJECT_PYTHON} +# register_python_package() # pip install -e the given pyproject +# +# Re-installs only when the package's pyproject.toml changes — keyed via a +# build-tree stamp file, so reconfigures stay fast. + +if(DEFINED PROJECT_PYTHON_INCLUDED) + return() +endif() +set(PROJECT_PYTHON_INCLUDED TRUE) + +find_package(Python3 REQUIRED COMPONENTS Interpreter) + +set(PROJECT_VENV_DIR ${CMAKE_BINARY_DIR}/.venv CACHE INTERNAL "Project Python venv root") +set(PROJECT_PYTHON ${PROJECT_VENV_DIR}/bin/python CACHE INTERNAL "Project venv python interpreter") + +# Probe pip inside the venv. A half-created venv (missing python3-venv on the +# host) can have python but no pip — we want to catch that and report it +# clearly rather than failing on the first `pip install`. +execute_process( + COMMAND ${PROJECT_PYTHON} -m pip --version + RESULT_VARIABLE _venv_pip_ok + OUTPUT_QUIET + ERROR_QUIET +) +if(NOT _venv_pip_ok EQUAL 0) + message(STATUS "Creating project Python venv at ${PROJECT_VENV_DIR}") + file(REMOVE_RECURSE ${PROJECT_VENV_DIR}) + execute_process( + COMMAND ${Python3_EXECUTABLE} -m venv ${PROJECT_VENV_DIR} + RESULT_VARIABLE _venv_result + OUTPUT_VARIABLE _venv_output + ERROR_VARIABLE _venv_output + ) + if(NOT _venv_result EQUAL 0 OR NOT EXISTS ${PROJECT_PYTHON}) + message(FATAL_ERROR + "Failed to create venv at ${PROJECT_VENV_DIR}.\n" + "Install the venv module (e.g. 'sudo apt install python3-venv' or 'python3-full' on Debian/Ubuntu, " + "matching your Python version).\n" + "Output:\n${_venv_output}") + endif() + execute_process( + COMMAND ${PROJECT_PYTHON} -m pip --version + RESULT_VARIABLE _venv_pip_ok + OUTPUT_QUIET + ERROR_QUIET + ) + if(NOT _venv_pip_ok EQUAL 0) + message(FATAL_ERROR + "Venv at ${PROJECT_VENV_DIR} was created but pip is not available inside it. " + "Install the matching python3-venv package and reconfigure.") + endif() +endif() + +# Editable installs via pyproject.toml require pip >= 21.3. Upgrade once per +# configure; cheap when already up-to-date. +execute_process( + COMMAND ${PROJECT_PYTHON} -m pip install --upgrade pip --quiet + RESULT_VARIABLE _pip_upgrade_result +) +if(NOT _pip_upgrade_result EQUAL 0) + message(FATAL_ERROR "Failed to upgrade pip in project venv (pip returned ${_pip_upgrade_result})") +endif() + +# register_python_package() +# +# Installs the pyproject at editable into the project venv. +# Re-runs only when /pyproject.toml's mtime moves past a stamp +# file in the build tree, so reconfiguring on an unchanged tree is a no-op. +# Stamps are namespaced by the leaf directory name so two pyprojects in +# different directories don't clobber each other. +function(register_python_package _pkg_dir) + if(NOT IS_ABSOLUTE "${_pkg_dir}") + message(FATAL_ERROR "register_python_package: path must be absolute: ${_pkg_dir}") + endif() + set(_pyproject "${_pkg_dir}/pyproject.toml") + if(NOT EXISTS "${_pyproject}") + message(FATAL_ERROR "register_python_package: no pyproject.toml at ${_pkg_dir}") + endif() + + get_filename_component(_leaf "${_pkg_dir}" NAME) + set(_stamp "${CMAKE_BINARY_DIR}/python-venv-stamps/${_leaf}.installed") + + # Compare stamp mtime to the pyproject's. IS_NEWER_THAN treats a missing + # stamp as "older", so first configure always installs. + if("${_pyproject}" IS_NEWER_THAN "${_stamp}") + message(STATUS "Installing Python package ${_leaf} into project venv") + execute_process( + COMMAND ${PROJECT_PYTHON} -m pip install -e ${_pkg_dir} --quiet + RESULT_VARIABLE _pip_result + ) + if(NOT _pip_result EQUAL 0) + message(FATAL_ERROR "Failed to install ${_leaf} into project venv (pip returned ${_pip_result})") + endif() + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/python-venv-stamps") + file(TOUCH "${_stamp}") + endif() +endfunction() Index: lib/MsgUtils/CMakeLists.txt =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r6cdf791210cfa0d96514094d33510e639f9bc0b6 --- lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision 6cdf791210cfa0d96514094d33510e639f9bc0b6) @@ -11,74 +11,18 @@ find_package(Protobuf REQUIRED) find_package(absl REQUIRED) -find_package(Python3 REQUIRED COMPONENTS Interpreter) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network SerialBus) set(MSGUTILS_SCRIPTS_DIR ${CMAKE_SOURCE_DIR}/scripts/MsgUtils) -# Create a build-local venv so pip install works on distros that enforce PEP 668 -# (Debian/Ubuntu with Python 3.11+, etc.) without polluting system or user site-packages. -set(MSGUTILS_VENV_DIR ${CMAKE_CURRENT_BINARY_DIR}/msgutils-venv) -set(MSGUTILS_PYTHON ${MSGUTILS_VENV_DIR}/bin/python) -# Probe pip inside the venv — a half-created venv missing python3-venv has python but no pip. -execute_process( - COMMAND ${MSGUTILS_PYTHON} -m pip --version - RESULT_VARIABLE _venv_pip_ok - OUTPUT_QUIET - ERROR_QUIET -) -if(NOT _venv_pip_ok EQUAL 0) - message(STATUS "Creating msgutils Python venv at ${MSGUTILS_VENV_DIR}") - file(REMOVE_RECURSE ${MSGUTILS_VENV_DIR}) - execute_process( - COMMAND ${Python3_EXECUTABLE} -m venv ${MSGUTILS_VENV_DIR} - RESULT_VARIABLE _venv_result - OUTPUT_VARIABLE _venv_output - ERROR_VARIABLE _venv_output - ) - if(NOT _venv_result EQUAL 0 OR NOT EXISTS ${MSGUTILS_PYTHON}) - message(FATAL_ERROR - "Failed to create venv at ${MSGUTILS_VENV_DIR}.\n" - "Install the venv module (e.g. 'sudo apt install python3-venv' or 'python3-full' on Debian/Ubuntu, " - "matching your Python version).\n" - "Output:\n${_venv_output}") - endif() - # Verify pip is actually usable in the new venv. - execute_process( - COMMAND ${MSGUTILS_PYTHON} -m pip --version - RESULT_VARIABLE _venv_pip_ok - OUTPUT_QUIET - ERROR_QUIET - ) - if(NOT _venv_pip_ok EQUAL 0) - message(FATAL_ERROR - "Venv at ${MSGUTILS_VENV_DIR} was created but pip is not available inside it. " - "Install the matching python3-venv package and reconfigure.") - endif() -endif() - +# The project-wide venv ({CMAKE_BINARY_DIR}/.venv) is set up by +# cmake/PythonVenv.cmake at the top level; ${PROJECT_PYTHON} resolves to its +# interpreter. We install the msgutils package editable so the generator +# scripts below can `import msgutils` regardless of how they're invoked. include(cmake/MsgUtils.cmake) +register_python_package(${MSGUTILS_SCRIPTS_DIR}) -# Upgrade pip to ensure pyproject.toml-based editable installs are supported (requires pip >= 21.3). -execute_process( - COMMAND ${MSGUTILS_PYTHON} -m pip install --upgrade pip --quiet - RESULT_VARIABLE _pip_upgrade_result -) -if(NOT _pip_upgrade_result EQUAL 0) - message(FATAL_ERROR "Failed to upgrade pip in msgutils venv (pip returned ${_pip_upgrade_result})") -endif() - -# install the Python package in editable mode so that the generated protobuf files can be imported by the Python scripts -# without needing to be copied to the Python package directory -execute_process( - COMMAND ${MSGUTILS_PYTHON} -m pip install -e ${MSGUTILS_SCRIPTS_DIR} --quiet - RESULT_VARIABLE _pip_result -) -if(NOT _pip_result EQUAL 0) - message(FATAL_ERROR "Failed to install msgutils Python package (pip returned ${_pip_result})") -endif() - # set(DENALI_MSG_CSV ${CMAKE_CURRENT_SOURCE_DIR}/../../data/FW_Messages_List.csv) set(LEAHI_MSG_CSV ${CMAKE_CURRENT_SOURCE_DIR}/../../data/Leahi_Staging_FW_Messages_List.csv) set(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/../../data/LeahiUnhandled.conf) Index: lib/MsgUtils/cmake/MsgUtils.cmake =================================================================== diff -u -r2c00c6e743844c9a71fa03ce5a5c436ef3836484 -r6cdf791210cfa0d96514094d33510e639f9bc0b6 --- lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision 2c00c6e743844c9a71fa03ce5a5c436ef3836484) +++ lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision 6cdf791210cfa0d96514094d33510e639f9bc0b6) @@ -37,7 +37,7 @@ ${_header_path} ${_source_path} COMMAND - ${MSGUTILS_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgDefsCpp.py + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgDefsCpp.py --namespace ${_namespace} --header_dir ${_header_dir} --source_dir ${_source_dir} @@ -79,7 +79,7 @@ OUTPUT ${_protobuf_abs_filename} COMMAND - ${MSGUTILS_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateProtobuf.py + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateProtobuf.py --namespace ${_namespace} --output_dir ${_output_path} ${${_input_confs}} @@ -128,7 +128,7 @@ ${_header_path} ${_source_path} COMMAND - ${MSGUTILS_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgDefsCpp.py + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgDefsCpp.py --namespace ${_namespace} --header_dir ${_header_dir} --source_dir ${_source_dir} @@ -170,7 +170,7 @@ OUTPUT ${_protobuf_abs_filename} COMMAND - ${MSGUTILS_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateProtobuf.py + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateProtobuf.py --namespace ${_namespace} --output_dir ${_output_path} ${${_input_csvs}}