From 4b1e73bfca29cbe6ba8a04fe921ab8aab981535a Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 26 Feb 2024 20:45:58 +0100 Subject: [PATCH] Refactor de_export.py, extract model compilation (#2315) Move model compilation to a free function in a separate module. Easier to test and more reusable for recompilation after initial import. Related to #2306 --- python/sdist/amici/compile.py | 80 +++++++++++++++++++++++++++++++++ python/sdist/amici/de_export.py | 73 +++--------------------------- 2 files changed, 87 insertions(+), 66 deletions(-) create mode 100644 python/sdist/amici/compile.py diff --git a/python/sdist/amici/compile.py b/python/sdist/amici/compile.py new file mode 100644 index 0000000000..6c4a336afc --- /dev/null +++ b/python/sdist/amici/compile.py @@ -0,0 +1,80 @@ +""" +Functionality for building the C++ extensions of an amici-created model +package. +""" +import subprocess +import sys +from typing import Optional, Union +from pathlib import Path +import os + + +def build_model_extension( + package_dir: Union[str, Path], + verbose: Optional[Union[bool, int]] = False, + compiler: Optional[str] = None, + extra_msg: Optional[str] = None, +) -> None: + """ + Compile the model extension of an amici-created model package. + + :param package_dir: + Directory of the model package to be compiled. I.e., the directory + containing the `setup.py` file. + + :param verbose: + Make model compilation verbose. + + :param compiler: + Absolute path to the compiler executable to be used to build the Python + extension, e.g. ``/usr/bin/clang``. + + :param extra_msg: + Additional message to be printed in case of a failed build. + """ + # setup.py assumes it is run from within the model directory + package_dir = Path(package_dir) + script_args = [sys.executable, package_dir / "setup.py"] + + if verbose: + script_args.append("--verbose") + else: + script_args.append("--quiet") + + script_args.extend( + [ + "build_ext", + f"--build-lib={package_dir}", + # This is generally not required, but helps to reduce the path + # length of intermediate build files, that may easily become + # problematic on Windows, due to its ridiculous 255-character path + # length limit. + f'--build-temp={package_dir / "build"}', + ] + ) + + env = os.environ.copy() + if compiler is not None: + # CMake will use the compiler specified in the CXX environment variable + env["CXX"] = compiler + + # distutils.core.run_setup looks nicer, but does not let us check the + # result easily + try: + result = subprocess.run( + script_args, + cwd=str(package_dir), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + env=env, + ) + except subprocess.CalledProcessError as e: + print(e.output.decode("utf-8")) + print("Failed building the model extension.") + if extra_msg: + print(f"Note: {extra_msg}") + raise + + if verbose: + print(result.stdout.decode("utf-8")) diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index 513a7c0e63..37958edf97 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -15,8 +15,6 @@ import os import re import shutil -import subprocess -import sys from dataclasses import dataclass from itertools import chain from pathlib import Path @@ -61,6 +59,7 @@ _default_simplify, ) from .logging import get_logger, log_execution_time, set_log_level +from .compile import build_model_extension from .sympy_utils import ( _custom_pow_eval_derivative, _monkeypatched, @@ -2893,7 +2892,12 @@ def compile_model(self) -> None: """ Compiles the generated code it into a simulatable module """ - self._compile_c_code(compiler=self.compiler, verbose=self.verbose) + build_model_extension( + package_dir=self.model_path, + compiler=self.compiler, + verbose=self.verbose, + extra_msg="\n".join(self._build_hints), + ) def _prepare_model_folder(self) -> None: """ @@ -2950,69 +2954,6 @@ def _generate_c_code(self) -> None: CXX_MAIN_TEMPLATE_FILE, os.path.join(self.model_path, "main.cpp") ) - def _compile_c_code( - self, - verbose: Optional[Union[bool, int]] = False, - compiler: Optional[str] = None, - ) -> None: - """ - Compile the generated model code - - :param verbose: - Make model compilation verbose - - :param compiler: - Absolute path to the compiler executable to be used to build the Python - extension, e.g. ``/usr/bin/clang``. - """ - # setup.py assumes it is run from within the model directory - module_dir = self.model_path - script_args = [sys.executable, os.path.join(module_dir, "setup.py")] - - if verbose: - script_args.append("--verbose") - else: - script_args.append("--quiet") - - script_args.extend( - [ - "build_ext", - f"--build-lib={module_dir}", - # This is generally not required, but helps to reduce the path - # length of intermediate build files, that may easily become - # problematic on Windows, due to its ridiculous 255-character path - # length limit. - f'--build-temp={Path(module_dir, "build")}', - ] - ) - - env = os.environ.copy() - if compiler is not None: - # CMake will use the compiler specified in the CXX environment variable - env["CXX"] = compiler - - # distutils.core.run_setup looks nicer, but does not let us check the - # result easily - try: - result = subprocess.run( - script_args, - cwd=module_dir, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True, - env=env, - ) - except subprocess.CalledProcessError as e: - print(e.output.decode("utf-8")) - print("Failed building the model extension.") - if self._build_hints: - print("Note:") - print("\n".join(self._build_hints)) - raise - - if verbose: - print(result.stdout.decode("utf-8")) - def _generate_m_code(self) -> None: """ Create a Matlab script for compiling code files to a mex file