From 12a180f41ce06145cdf1ddb89e975edea7fc07f7 Mon Sep 17 00:00:00 2001 From: Frederik Hennig <frederik.hennig@fau.de> Date: Thu, 27 Mar 2025 11:04:16 +0100 Subject: [PATCH] finish codegen venv manager --- cmake/ManageCodegenVenv.py | 88 ++++++++++++++++++++++++++----- cmake/WalberlaCodegen.cmake | 54 +++++++++++++------ cmake/codegen-requirements.txt.in | 4 ++ cmake/test/reqs.txt | 2 + cmake/test/state.json | 1 + tests/CMakeLists.txt | 3 ++ 6 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 cmake/test/reqs.txt create mode 100644 cmake/test/state.json diff --git a/cmake/ManageCodegenVenv.py b/cmake/ManageCodegenVenv.py index 77f515d..33301ac 100644 --- a/cmake/ManageCodegenVenv.py +++ b/cmake/ManageCodegenVenv.py @@ -5,8 +5,9 @@ import sys import subprocess import json import shutil +import hashlib from contextlib import contextmanager -from dataclasses import dataclass, asdict +from dataclasses import dataclass, asdict, field from argparse import ArgumentParser from pathlib import Path @@ -17,6 +18,14 @@ class VenvState: main_requirements_file: str | None = None initialized: bool = False + user_requirements: list[str] = field(default_factory=list) + user_requirements_hash: str | None = None + + @property + def venv_python(self) -> Path: + assert self.venv_dir is not None + return Path(self.venv_dir) / "bin" / "python" + @staticmethod @contextmanager def lock(statefile: Path) -> Generator[VenvState, None, None]: @@ -41,31 +50,79 @@ class VenvState: statefile_bak.replace(statefile) +def reinitialize(state: VenvState): + assert state.venv_dir is not None + + venv_dir = Path(state.venv_dir) + if venv_dir.exists(): + shutil.rmtree(venv_dir) + + base_py = Path(sys.executable) + + # Create the virtual environment + venv_args = [base_py, "-m", "venv", state.venv_dir] + subprocess.run(venv_args).check_returncode() + + # Install base requirements + venv_py = str(state.venv_python) + install_args = [venv_py, "-m", "pip", "install", "-r", state.main_requirements_file] + subprocess.run(install_args).check_returncode() + + state.initialized = True + + def action_initialize(args): statefile = Path(args.statefile) with VenvState.lock(statefile) as state: if not state.initialized: p_venv_dir = Path(args.venv_dir).resolve() - if p_venv_dir.exists(): - shutil.rmtree(p_venv_dir) reqs_file = Path(args.requirements_file).resolve() state.venv_dir = str(p_venv_dir) state.main_requirements_file = str(reqs_file) - base_py = Path(sys.executable) + reinitialize(state) + + # Reset user requirements + state.user_requirements = [] + + +def action_require(args): + statefile = Path(args.statefile) + + with VenvState.lock(statefile) as state: + if not state.initialized: + raise RuntimeError("Virtual environment is not initialized.") + + for req in args.requirements: + state.user_requirements.append(req) + - # Create the virtual environment - venv_args = [base_py, "-m", "venv", state.venv_dir] - subprocess.run(venv_args).check_returncode() +def action_populate(args): + statefile = Path(args.statefile) + + with VenvState.lock(statefile) as state: + h = hashlib.sha256() + for req in state.user_requirements: + h.update(bytes(req, encoding="utf8")) + digest = h.hexdigest() - # Install base requirements - venv_py = Path(state.venv_dir).absolute() / "bin" / "python" - install_args = [venv_py, "-m", "pip", "install", "-r", state.main_requirements_file] - subprocess.run(install_args).check_returncode() + if digest != state.user_requirements_hash: + if state.user_requirements_hash is not None: + # Populate was run before -> rebuild entire environment + print( + "User requirements have changed - rebuilding virtual environment.", + flush=True + ) + reinitialize(state) - state.initialized = True + pip_args = [str(state.venv_python), "-m", "pip", "install"] + for req in state.user_requirements: + install_args = pip_args.copy() + [req] + subprocess.run(install_args).check_returncode() + + state.user_requirements_hash = digest def main(): @@ -93,6 +150,13 @@ def main(): ) parser_initialize.set_defaults(func=action_initialize) + parser_require = subparsers.add_parser("require") + parser_require.add_argument("requirements", nargs="+", type=str) + parser_require.set_defaults(func=action_require) + + parser_populate = subparsers.add_parser("populate") + parser_populate.set_defaults(func=action_populate) + args = parser.parse_args() args.func(args) diff --git a/cmake/WalberlaCodegen.cmake b/cmake/WalberlaCodegen.cmake index 435af24..ceafb3e 100644 --- a/cmake/WalberlaCodegen.cmake +++ b/cmake/WalberlaCodegen.cmake @@ -7,20 +7,25 @@ set( ) if( WALBERLA_CODEGEN_PRIVATE_VENV ) + find_package( Python COMPONENTS Interpreter REQUIRED ) + set(WALBERLA_CODEGEN_VENV_PATH ${CMAKE_CURRENT_BINARY_DIR}/codegen-venv CACHE PATH "Location of the virtual environment used for code generation") set(_venv_python_exe ${WALBERLA_CODEGEN_VENV_PATH}/bin/python) - set( - _WALBERLA_CODEGEN_VENV_MANAGER - ${sfg_walberla_SOURCE_DIR}/cmake/ManageCodegenVenv.py - CACHE INTERNAL - "venv manager filepath - for internal use only" - ) set( _WALBERLA_CODEGEN_VENV_STATEFILE ${CMAKE_CURRENT_BINARY_DIR}/walberla-venv-state.json CACHE INTERNAL "venv statefile - for internal use only" ) + set( + _WALBERLA_CODEGEN_VENV_INVOKE_MANAGER + ${Python_EXECUTABLE} + ${sfg_walberla_SOURCE_DIR}/cmake/ManageCodegenVenv.py + -s + ${_WALBERLA_CODEGEN_VENV_STATEFILE} + CACHE INTERNAL + "venv manager filepath - for internal use only" + ) set( WALBERLA_CODEGEN_VENV_REQUIREMENTS @@ -30,7 +35,6 @@ if( WALBERLA_CODEGEN_PRIVATE_VENV ) ) mark_as_advanced(WALBERLA_CODEGEN_VENV_REQUIREMENTS) - find_package( Python COMPONENTS Interpreter REQUIRED ) set( _requirements_file @@ -44,9 +48,7 @@ if( WALBERLA_CODEGEN_PRIVATE_VENV ) execute_process( COMMAND - ${Python_EXECUTABLE} - ${_WALBERLA_CODEGEN_VENV_MANAGER} - -s ${_WALBERLA_CODEGEN_VENV_STATEFILE} + $CACHE{_WALBERLA_CODEGEN_VENV_INVOKE_MANAGER} init ${WALBERLA_CODEGEN_VENV_PATH} ${_requirements_file} @@ -109,21 +111,39 @@ configure_file( message( STATUS "Wrote project-wide code generator configuration to ${WALBERLA_CODEGEN_CONFIG_MODULE}" ) -#[[ -Run `pip install` in the code generation environment with the given arguments. -#]] -function(walberla_codegen_venv_install) +function(walberla_codegen_venv_require) if(NOT WALBERLA_CODEGEN_PRIVATE_VENV) return() endif() - if(NOT _sfg_private_venv_done) - message( FATAL_ERROR "The private virtual environment for code generation was not initialized yet" ) + execute_process( + COMMAND + $CACHE{_WALBERLA_CODEGEN_VENV_INVOKE_MANAGER} + require + ${ARGV} + RESULT_VARIABLE _lastResult + ) + + if( ${_lastResult} ) + message( FATAL_ERROR "venv-require: Operation failed" ) + endif() +endfunction() + +function(walberla_codegen_venv_populate) + if(NOT WALBERLA_CODEGEN_PRIVATE_VENV) + return() endif() execute_process( - COMMAND ${WALBERLA_CODEGEN_PYTHON} -m pip install ${ARGV} + COMMAND + $CACHE{_WALBERLA_CODEGEN_VENV_INVOKE_MANAGER} + populate + RESULT_VARIABLE _lastResult ) + + if( ${_lastResult} ) + message( FATAL_ERROR "venv-populate: Operation failed" ) + endif() endfunction() # Code Generation Functions diff --git a/cmake/codegen-requirements.txt.in b/cmake/codegen-requirements.txt.in index 07abf2e..3d2115e 100644 --- a/cmake/codegen-requirements.txt.in +++ b/cmake/codegen-requirements.txt.in @@ -1,8 +1,12 @@ +# pystencils git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev +# lbmpy git+https://i10git.cs.fau.de/pycodegen/lbmpy.git@fhennig/pystencils2.0-compat +# pystencils-sfg git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git +# walberla-codegen -e ${sfg_walberla_SOURCE_DIR} diff --git a/cmake/test/reqs.txt b/cmake/test/reqs.txt new file mode 100644 index 0000000..9c593dc --- /dev/null +++ b/cmake/test/reqs.txt @@ -0,0 +1,2 @@ +pystencils +lbmpy \ No newline at end of file diff --git a/cmake/test/state.json b/cmake/test/state.json new file mode 100644 index 0000000..0de44ea --- /dev/null +++ b/cmake/test/state.json @@ -0,0 +1 @@ +{"venv_dir": "/media/data/fhennig/research-hpc/projects/2024_pystencils_nbackend/sfg-walberla/cmake/test/venv", "main_requirements_file": "/media/data/fhennig/research-hpc/projects/2024_pystencils_nbackend/sfg-walberla/cmake/test/reqs.txt", "initialized": true, "user_requirements": ["py-cpuinfo", "pyyaml"], "user_requirements_hash": "bebb0df474f163f05d50504ee0efc07ba064e08aabeb7b2c745d8f57200be4ac"} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 34ada83..312a02f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,9 @@ endif() add_subdirectory(${CMAKE_SOURCE_DIR}/.. ${CMAKE_BINARY_DIR}/sfg-walberla) +walberla_codegen_venv_require( py-cpuinfo ) +walberla_codegen_venv_populate() + # Test Directories include(CTest) -- GitLab