Files
SystemSimulationApp/app/simulation/native_codegen/self_test.py
T

209 lines
9.5 KiB
Python

"""Small, uncached compile/link/run check using the production native toolchain."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import signal
import subprocess
import tempfile
from threading import Event
from time import monotonic
from typing import Callable
from .build import CACHE, NativeCommandError, link_library_arguments, platform_build_inputs, toolchain
from .processes import command_environment, is_driver_launch_failure
STAGE_MESSAGES = {
"toolchain": "正在查找编译器与 SUNDIALS",
"preparing": "正在准备最小自检程序",
"preprocessing": "正在预处理最小自检程序",
"compiling": "正在编译最小自检程序",
"linking": "正在链接最小自检程序",
"running": "正在运行最小自检程序",
"schema": "正在检查模型 XML 解析环境",
}
OUTPUT_MARKER = "native-runtime-check-ok"
# One smooth scalar equation exercises every library used by production BDF.
# Its known answer is independent of the user's model and cannot hit model cache.
SOURCE = r'''
#include <cvode/cvode.h>
#include <cvode/cvode_ls.h>
#include <nvector/nvector_serial.h>
#include <sunmatrix/sunmatrix_dense.h>
#include <sunlinsol/sunlinsol_dense.h>
#include <math.h>
#include <stdio.h>
static int rhs(sunrealtype t, N_Vector y, N_Vector dy, void *data) {
(void)t; (void)data;
NV_Ith_S(dy,0)=-NV_Ith_S(y,0);
return 0;
}
int main(void) {
SUNContext context=NULL;
N_Vector y=NULL;
SUNMatrix matrix=NULL;
SUNLinearSolver linear=NULL;
void *solver=NULL;
int result=1;
sunrealtype time=0;
if (SUNContext_Create(SUN_COMM_NULL,&context)) goto done;
y=N_VNew_Serial(1,context);
if (!y) goto done;
NV_Ith_S(y,0)=1;
matrix=SUNDenseMatrix(1,1,context);
if (!matrix) goto done;
linear=SUNLinSol_Dense(y,matrix,context);
solver=CVodeCreate(CV_BDF,context);
if (!linear || !solver) goto done;
if (CVodeInit(solver,rhs,0,y)<0 ||
CVodeSStolerances(solver,1e-8,1e-10)<0 ||
CVodeSetLinearSolver(solver,linear,matrix)<0 ||
CVode(solver,0.1,y,&time,CV_NORMAL)<0) goto done;
if (!isfinite(NV_Ith_S(y,0)) || fabs(NV_Ith_S(y,0)-exp(-0.1))>1e-6) goto done;
puts("native-runtime-check-ok");
result=0;
done:
if (result) fputs("Minimal BDF self-check failed.\n",stderr);
if (solver) CVodeFree(&solver);
if (linear) SUNLinSolFree(linear);
if (matrix) SUNMatDestroy(matrix);
if (y) N_VDestroy(y);
if (context) SUNContext_Free(&context);
return result;
}
'''
class CheckCancelled(Exception):
pass
class CheckFailure(RuntimeError):
def __init__(self, message: str, *, command=(), cwd=None, exit_code=None,
stdout="", stderr="", error_type="RuntimeError"):
super().__init__(message)
self.details = {"command": list(command), "cwd": str(cwd) if cwd else None,
"exitCode": exit_code, "stdout": stdout[-8000:],
"stderr": stderr[-8000:], "errorType": error_type}
def _stop_process_tree(process: subprocess.Popen) -> None:
# GCC starts cc1/as/ld. Killing only its driver can leave inherited pipes open.
if os.name == "nt":
try:
subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
except (OSError, subprocess.SubprocessError):
pass
if process.poll() is None:
process.kill()
else:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
def _command(command: list[str], directory: Path, stop: Event, timeout: float = 30) -> str:
if stop.is_set():
raise CheckCancelled()
try:
with subprocess.Popen(command, cwd=directory, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=command_environment(command[0]), close_fds=True,
start_new_session=os.name != "nt",
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0) as process:
deadline = monotonic() + timeout
while True:
try:
stdout, stderr = process.communicate(timeout=0.2)
break
except subprocess.TimeoutExpired:
if stop.is_set() or monotonic() >= deadline:
_stop_process_tree(process)
stdout, stderr = process.communicate()
if stop.is_set():
raise CheckCancelled()
raise CheckFailure(f"命令执行超过 {timeout:g} 秒", command=command,
cwd=directory, exit_code=process.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
error_type="TimeoutExpired")
except OSError as exc:
raise CheckFailure(str(exc), command=command, cwd=directory,
error_type=type(exc).__name__) from exc
output = stdout.decode("utf-8", errors="replace")
if process.returncode:
stderr_text = stderr.decode('utf-8', errors='replace')
message = ('GCC 已启动,但无法启动编译子进程;该错误不能直接判定为编译器不存在'
if is_driver_launch_failure(stderr_text) else '命令执行失败')
# Whole-check retries remain owned by warmup (initial check + 5 rounds).
raise CheckFailure(message, command=command, cwd=directory,
exit_code=process.returncode, stdout=output,
stderr=stderr_text,
error_type="CalledProcessError")
return output
def check_native_runtime(progress: Callable[..., None], stop: Event) -> None:
def stage(name: str, **details):
if stop.is_set():
raise CheckCancelled()
progress(stage=name, message=STAGE_MESSAGES[name], **details)
candidate = os.environ.get("SIMULATION_NATIVE_CC") or shutil.which("gcc")
stage("toolchain", compiler=candidate)
try:
compiler, sundials, version = toolchain()
except NativeCommandError as exc:
details = exc.details
failure = CheckFailure(str(exc), command=details['command'], cwd=details['cwd'],
exit_code=details['exitCode'], stdout=details['stdout'], stderr=details['stderr'],
error_type=details['errorType'])
failure.details['attempts'] = details['attempts']
raise failure from exc
except subprocess.SubprocessError as exc:
def decoded(value):
return value.decode("utf-8", errors="replace") if isinstance(value, bytes) else (value or "")
raise CheckFailure(str(exc), command=getattr(exc, "cmd", ()), cwd=Path.cwd(),
exit_code=getattr(exc, "returncode", None),
stdout=decoded(getattr(exc, "stdout", "")),
stderr=decoded(getattr(exc, "stderr", "")),
error_type=type(exc).__name__) from exc
except OSError as exc:
raise CheckFailure(str(exc), command=[candidate, "--version"] if candidate else [],
cwd=Path.cwd(), error_type=type(exc).__name__) from exc
compiler = str(Path(shutil.which(compiler) or compiler).resolve())
stage("preparing", compiler=compiler, compilerVersion=version, sundialsRoot=str(sundials))
flags, libraries, dlls, executable_name = platform_build_inputs(sundials)
# Keep the probe on the build filesystem: Linux /tmp may be mounted noexec.
# Disposable probes never populate or evict the model/object caches.
directory_root = CACHE.parent / "native-runtime-checks"
directory_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="check-", dir=directory_root) as temporary:
directory = Path(temporary).resolve()
source, preprocessed, obj = directory / "check.c", directory / "check.i", directory / "check.o"
executable = directory / executable_name
source.write_text(SOURCE, encoding="utf-8", newline="\n")
stage("preprocessing")
_command([compiler, *flags, "-E", "-P", f"-fmacro-prefix-map={directory}=/generated",
"-I", str(sundials / "include"), str(source), "-o", str(preprocessed)], directory, stop)
stage("compiling")
_command([compiler, *flags, "-x", "cpp-output", "-c", str(preprocessed), "-o", str(obj)], directory, stop)
stage("linking")
_command([compiler, *flags, str(obj), *link_library_arguments(libraries),
"-lm", "-o", str(executable)], directory, stop)
stage("running")
for dll in dlls:
shutil.copyfile(dll, directory / dll.name)
output = _command([str(executable)], directory, stop, timeout=10)
if output.strip() != OUTPUT_MARKER:
raise CheckFailure("最小程序未返回预期的自检成功标记", command=[str(executable)],
cwd=directory, exit_code=0, stdout=output, error_type="UnexpectedOutput")
stage("schema")
from app.system_xml import validate_system_xml_document
validate_system_xml_document(b"<System/>")