209 lines
9.5 KiB
Python
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/>")
|