update sensor bench
This commit is contained in:
+81
-11
@@ -1,6 +1,9 @@
|
||||
# Sensor Bench EXE Build
|
||||
|
||||
This folder contains the Windows PyInstaller build setup for Sensor Bench.
|
||||
This folder contains Windows PyInstaller build setups for:
|
||||
|
||||
- **Public** exhibition app → `dist/SensorBench/`
|
||||
- **Private** operator app → `dist/SensorBenchOperator/` (internal use only)
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -9,7 +12,8 @@ This folder contains the Windows PyInstaller build setup for Sensor Bench.
|
||||
- Python 3.10 build environment
|
||||
- Internet access to install `orisys` from PyPI
|
||||
|
||||
The build installs `orisys` from PyPI. Do not use `PYTHONPATH` or an editable `orisys-sdk` checkout for release builds.
|
||||
The build installs `orisys` from PyPI. Do not use `PYTHONPATH` or an editable
|
||||
`orisys-sdk` checkout for release builds.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -20,7 +24,8 @@ From the repository root (cmd or PowerShell):
|
||||
.\build\build_exe.bat
|
||||
```
|
||||
|
||||
The `.bat` wrappers bypass the local PowerShell execution policy. If you prefer calling PowerShell directly:
|
||||
The `.bat` wrappers bypass the local PowerShell execution policy. If you prefer
|
||||
calling PowerShell directly:
|
||||
|
||||
```bat
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build\create_env.ps1
|
||||
@@ -33,7 +38,23 @@ Output:
|
||||
dist/SensorBench/SensorBench.exe
|
||||
```
|
||||
|
||||
The build is `onedir` rather than `onefile` so startup is faster and bundled DLL/resource issues are easier to diagnose.
|
||||
After installing/upgrading `orisys`, the build syncs wheel sensor `*.json` into
|
||||
repo `config/` via `build/sync_wheel_configs.py` (for example `ddjx01.json`)
|
||||
before PyInstaller, never touching `locales/`. The helper reads from the
|
||||
installed wheel's bundled `orisys/configs/` directory instead of
|
||||
`orisys.configs.CONFIG_DIR`, which can point at the repo's local `config/`
|
||||
during builds. PyInstaller bundles that `config/`, then the script copies repo
|
||||
`config/` beside the EXE so operators can edit sensor JSON and locales.
|
||||
|
||||
The public build intentionally excludes:
|
||||
|
||||
- `internal/` (private operator UI)
|
||||
- `steps/` workflow JSON
|
||||
- `config/options.json` lab component metadata
|
||||
- `sensor_bench_operator` / former `sensor_bench.app` modules
|
||||
|
||||
The build is `onedir` rather than `onefile` so startup is faster and bundled
|
||||
DLL/resource issues are easier to diagnose.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -44,15 +65,64 @@ python tests/test_smoke.py
|
||||
python build/verify_dist.py
|
||||
```
|
||||
|
||||
`verify_dist.py` checks the expected public layout **and** fails if internal
|
||||
paths, operator modules, workflow locale keys, or private workflow text appear
|
||||
in the dist tree.
|
||||
|
||||
Manual hardware check:
|
||||
|
||||
1. Run `dist\SensorBench\SensorBench.exe`.
|
||||
2. Confirm the UI opens without setting `PYTHONPATH`.
|
||||
2. Confirm the exhibition UI opens without setting `PYTHONPATH`.
|
||||
3. Confirm the UI defaults to Simplified Chinese and can switch to English.
|
||||
4. Confirm Orisys status is available and fixed mode shows DDJX01.
|
||||
5. Use Next / Revert / New Sample Reset on the left workflow panel.
|
||||
6. Start preview with camera index `0`.
|
||||
7. Confirm raw preview and processed preview update.
|
||||
8. Record a short clip and verify it appears under `dist\SensorBench\recordings`.
|
||||
4. Confirm Orisys status is available, the config dropdown lists local editable
|
||||
sensor configs (for example `ddjx01`), and Start uses DDJX01.
|
||||
5. Confirm Search finds cameras (or logs none) and Camera still opens a
|
||||
video-file picker. There is no Cal (.npy) field; Reset resets the Orisys
|
||||
tracker while preview is running.
|
||||
6. Start preview with camera index `0` (or a searched index / video file).
|
||||
7. Confirm the image panel and FN/FX/FY plots update.
|
||||
8. Toggle processed/raw view and start/stop recording under
|
||||
`dist\SensorBench\recordings`.
|
||||
|
||||
Locale files are bundled under `_internal/config/locales/` (and can also be placed beside the EXE under `config/locales/` for overrides). Edit those JSON files to change operator-facing text without rebuilding.
|
||||
Locale files are bundled under `_internal/config/locales/` (and can also be
|
||||
placed beside the EXE under `config/locales/` for overrides). Edit those JSON
|
||||
files to change exhibition-facing text without rebuilding.
|
||||
|
||||
## Private operator EXE (internal only)
|
||||
|
||||
Do **not** ship `SensorBenchOperator` to customers. It includes workflow steps,
|
||||
lab `options.json`, and operator locale copy.
|
||||
|
||||
```bat
|
||||
.\build\create_env.bat
|
||||
.\build\build_operator_exe.bat
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```bat
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build\build_operator_exe.ps1
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
dist/SensorBenchOperator/SensorBenchOperator.exe
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
python tests/test_smoke.py
|
||||
python internal/tests/test_operator_smoke.py
|
||||
python build/verify_operator_dist.py
|
||||
```
|
||||
|
||||
Manual check:
|
||||
|
||||
1. Run `dist\SensorBenchOperator\SensorBenchOperator.exe`.
|
||||
2. Confirm the workflow/steps panel and component options appear.
|
||||
3. Confirm Start Preview / record / force curves still work with a camera.
|
||||
4. Confirm `config/options.json` and `steps/default.json` beside the EXE are editable.
|
||||
|
||||
Source-level operator docs: [`../internal/README.md`](../internal/README.md).
|
||||
|
||||
+20
-3
@@ -8,6 +8,7 @@ $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$DistDir = Join-Path $RepoRoot "dist"
|
||||
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work"
|
||||
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
||||
$RepoConfig = Join-Path $RepoRoot "config"
|
||||
|
||||
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
||||
@@ -16,10 +17,20 @@ if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
conda run -n $EnvName python -m pip install --upgrade pip
|
||||
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed in env '$EnvName'." }
|
||||
|
||||
conda run -n $EnvName python -m pip install -e ".[pack]"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Editable install failed in env '$EnvName'." }
|
||||
|
||||
conda run -n $EnvName python -m pip install --upgrade orisys
|
||||
if ($LASTEXITCODE -ne 0) { throw "orisys install failed in env '$EnvName'." }
|
||||
|
||||
# Sync Orisys wheel sensor configs into repo config/ before packaging.
|
||||
conda run -n $EnvName python build/sync_wheel_configs.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Config sync failed." }
|
||||
|
||||
conda run -n $EnvName python tests/test_smoke.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Smoke tests failed." }
|
||||
|
||||
if (Test-Path $WorkDir) {
|
||||
Remove-Item -Recurse -Force $WorkDir
|
||||
@@ -29,14 +40,20 @@ try {
|
||||
}
|
||||
|
||||
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed." }
|
||||
|
||||
# Public package only: copy exhibition config/. Never ship internal/ operator assets.
|
||||
Copy-Item -Recurse -Force $RepoConfig (Join-Path $DistDir "SensorBench")
|
||||
$InternalLeak = Join-Path $DistDir "SensorBench\internal"
|
||||
if (Test-Path $InternalLeak) {
|
||||
throw "Public dist must not contain internal/: $InternalLeak"
|
||||
}
|
||||
|
||||
Copy-Item -Recurse -Force (Join-Path $RepoRoot "config") (Join-Path $DistDir "SensorBench")
|
||||
Copy-Item -Recurse -Force (Join-Path $RepoRoot "steps") (Join-Path $DistDir "SensorBench")
|
||||
conda run -n $EnvName python build/verify_dist.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Distribution verification failed." }
|
||||
|
||||
Write-Host "Build complete: dist\SensorBench\SensorBench.exe"
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d "%~dp0.."
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build_operator_exe.ps1" %*
|
||||
@@ -0,0 +1,78 @@
|
||||
param(
|
||||
[string]$EnvName = "sensor-bench-build-py310"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$DistDir = Join-Path $RepoRoot "dist"
|
||||
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work_operator"
|
||||
$SpecPath = Join-Path $PSScriptRoot "sensor_bench_operator.spec"
|
||||
$RepoConfig = Join-Path $RepoRoot "config"
|
||||
$InternalRoot = Join-Path $RepoRoot "internal"
|
||||
$DistApp = Join-Path $DistDir "SensorBenchOperator"
|
||||
|
||||
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
||||
}
|
||||
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
conda run -n $EnvName python -m pip install --upgrade pip
|
||||
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed in env '$EnvName'." }
|
||||
|
||||
conda run -n $EnvName python -m pip install -e ".[pack]"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Editable install failed in env '$EnvName'." }
|
||||
|
||||
conda run -n $EnvName python -m pip install --upgrade orisys
|
||||
if ($LASTEXITCODE -ne 0) { throw "orisys install failed in env '$EnvName'." }
|
||||
|
||||
# Sync Orisys wheel sensor configs into repo config/ before packaging.
|
||||
conda run -n $EnvName python build/sync_wheel_configs.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Config sync failed." }
|
||||
|
||||
conda run -n $EnvName python tests/test_smoke.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Public smoke tests failed." }
|
||||
|
||||
conda run -n $EnvName python internal/tests/test_operator_smoke.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Operator smoke tests failed." }
|
||||
|
||||
if (Test-Path $WorkDir) {
|
||||
Remove-Item -Recurse -Force $WorkDir
|
||||
}
|
||||
if (Test-Path $DistApp) {
|
||||
Remove-Item -Recurse -Force $DistApp
|
||||
}
|
||||
|
||||
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller (operator) failed." }
|
||||
|
||||
# Editable sidecars beside the EXE: public sensor configs + operator overlays.
|
||||
# Copy sensor JSON first, then replace locales/options/steps so public
|
||||
# exhibition locales (no workflow) cannot win over operator catalogs.
|
||||
$DistConfig = Join-Path $DistApp "config"
|
||||
$DistLocales = Join-Path $DistConfig "locales"
|
||||
$DistSteps = Join-Path $DistApp "steps"
|
||||
if (Test-Path $DistConfig) {
|
||||
Remove-Item -Recurse -Force $DistConfig
|
||||
}
|
||||
if (Test-Path $DistSteps) {
|
||||
Remove-Item -Recurse -Force $DistSteps
|
||||
}
|
||||
Copy-Item -Recurse -Force $RepoConfig $DistConfig
|
||||
Copy-Item -Force (Join-Path $InternalRoot "config\options.json") (Join-Path $DistConfig "options.json")
|
||||
if (Test-Path $DistLocales) {
|
||||
Remove-Item -Recurse -Force $DistLocales
|
||||
}
|
||||
Copy-Item -Recurse -Force (Join-Path $InternalRoot "config\locales") $DistLocales
|
||||
Copy-Item -Recurse -Force (Join-Path $InternalRoot "steps") $DistSteps
|
||||
|
||||
conda run -n $EnvName python build/verify_operator_dist.py
|
||||
if ($LASTEXITCODE -ne 0) { throw "Operator distribution verification failed." }
|
||||
|
||||
Write-Host "Build complete: dist\SensorBenchOperator\SensorBenchOperator.exe"
|
||||
Write-Host "INTERNAL USE ONLY - do not ship this folder to customers."
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -11,13 +11,16 @@ work_dir = repo_root / "build" / "pyinstaller_work"
|
||||
|
||||
datas = [
|
||||
(str(repo_root / "config"), "config"),
|
||||
(str(repo_root / "steps"), "steps"),
|
||||
]
|
||||
binaries = []
|
||||
hiddenimports = [
|
||||
"sensor_bench.main",
|
||||
"sensor_bench.app",
|
||||
"sensor_bench.exhibition_app",
|
||||
"sensor_bench.acquisition",
|
||||
"sensor_bench.dd06_acquisition",
|
||||
"sensor_bench.roi_geometry",
|
||||
"sensor_bench.camera_discovery",
|
||||
"sensor_bench.force_calibration",
|
||||
"sensor_bench.config_loader",
|
||||
"sensor_bench.i18n",
|
||||
"sensor_bench.paths",
|
||||
@@ -50,6 +53,8 @@ excludes = [
|
||||
"pytest",
|
||||
"pip",
|
||||
"wheel",
|
||||
"sensor_bench_operator",
|
||||
"sensor_bench.app",
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
@@ -95,4 +100,3 @@ coll = COLLECT(
|
||||
upx_exclude=[],
|
||||
name="SensorBench",
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all, collect_submodules
|
||||
|
||||
|
||||
repo_root = Path.cwd()
|
||||
src_dir = repo_root / "src"
|
||||
operator_src = repo_root / "internal" / "src"
|
||||
work_dir = repo_root / "build" / "pyinstaller_work_operator"
|
||||
|
||||
datas = [
|
||||
# Operator-owned options / locales / workflow steps (private).
|
||||
(str(repo_root / "internal" / "config"), "config"),
|
||||
(str(repo_root / "internal" / "steps"), "steps"),
|
||||
# Public sensor JSON configs (ddjx01, mini, …) for the shared acquisition path.
|
||||
(str(repo_root / "config"), "config"),
|
||||
]
|
||||
binaries = []
|
||||
hiddenimports = [
|
||||
"sensor_bench_operator",
|
||||
"sensor_bench_operator.app",
|
||||
"sensor_bench_operator.paths",
|
||||
"sensor_bench.main",
|
||||
"sensor_bench.exhibition_app",
|
||||
"sensor_bench.acquisition",
|
||||
"sensor_bench.dd06_acquisition",
|
||||
"sensor_bench.roi_geometry",
|
||||
"sensor_bench.camera_discovery",
|
||||
"sensor_bench.force_calibration",
|
||||
"sensor_bench.config_loader",
|
||||
"sensor_bench.i18n",
|
||||
"sensor_bench.paths",
|
||||
"sensor_bench.plot_history",
|
||||
"sensor_bench.recorder",
|
||||
"orisys",
|
||||
"orisys_blend",
|
||||
"scipy.fft",
|
||||
"scipy.ndimage",
|
||||
"scipy.sparse.linalg",
|
||||
"numba",
|
||||
"llvmlite",
|
||||
]
|
||||
|
||||
for package in ("PyQt5", "pyqtgraph", "cv2", "orisys", "orisys_blend", "numba", "llvmlite"):
|
||||
pkg_datas, pkg_binaries, pkg_hidden = collect_all(package)
|
||||
datas += pkg_datas
|
||||
binaries += pkg_binaries
|
||||
hiddenimports += pkg_hidden
|
||||
|
||||
hiddenimports += collect_submodules("orisys_blend")
|
||||
hiddenimports += collect_submodules("sensor_bench")
|
||||
hiddenimports += collect_submodules("sensor_bench_operator")
|
||||
|
||||
excludes = [
|
||||
"cupy",
|
||||
"cupyx",
|
||||
"pyopencl",
|
||||
"open3d",
|
||||
"matplotlib",
|
||||
"tkinter",
|
||||
"pytest",
|
||||
"pip",
|
||||
"wheel",
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
[str(repo_root / "internal" / "run_operator.py")],
|
||||
pathex=[str(repo_root), str(src_dir), str(operator_src)],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=excludes,
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="SensorBenchOperator",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name="SensorBenchOperator",
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Copy Orisys wheel sensor JSON configs into repo config/ (exclude options.json)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def wheel_config_dir() -> Path:
|
||||
import orisys
|
||||
|
||||
inner_pkg = Path(orisys.__file__).resolve().parent
|
||||
for candidate in (inner_pkg / "configs", inner_pkg.parent / "configs"):
|
||||
if candidate.is_dir() and any(candidate.glob("*.json")):
|
||||
return candidate
|
||||
return inner_pkg.parent / "configs"
|
||||
|
||||
|
||||
def sync_configs(repo_root: Path) -> int:
|
||||
src = wheel_config_dir()
|
||||
if not src.is_dir():
|
||||
print(f"Orisys wheel configs directory not found: {src}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
dst = repo_root / "config"
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = 0
|
||||
for path in sorted(src.glob("*.json")):
|
||||
if path.stem.lower() == "options":
|
||||
continue
|
||||
shutil.copy2(path, dst / path.name)
|
||||
copied += 1
|
||||
|
||||
print(f"Synced {copied} config(s) from {src} to {dst}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
return sync_configs(repo_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+91
-5
@@ -1,7 +1,8 @@
|
||||
"""Verify the PyInstaller onedir output has the expected portable layout."""
|
||||
"""Verify the public PyInstaller onedir output and reject internal artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,23 +10,108 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIST = ROOT / "dist" / "SensorBench"
|
||||
|
||||
# Paths / substrings that must never appear in a public customer build.
|
||||
FORBIDDEN_RELATIVE_PATHS = (
|
||||
"steps",
|
||||
"steps/default.json",
|
||||
"config/options.json",
|
||||
"internal",
|
||||
)
|
||||
|
||||
FORBIDDEN_NAME_FRAGMENTS = (
|
||||
"sensor_bench_operator",
|
||||
"sensor_bench.app",
|
||||
"operator-ui",
|
||||
)
|
||||
|
||||
FORBIDDEN_LOCALE_KEYS = (
|
||||
"workflow",
|
||||
)
|
||||
|
||||
FORBIDDEN_TEXT_SNIPPETS = (
|
||||
"Inspection Workflow",
|
||||
"检测流程",
|
||||
"新样品",
|
||||
"冯总",
|
||||
"operator-ui",
|
||||
)
|
||||
|
||||
|
||||
def require(path: Path) -> None:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Missing build artifact: {path}")
|
||||
|
||||
|
||||
def forbid(path: Path, *, reason: str) -> None:
|
||||
if path.exists():
|
||||
raise RuntimeError(f"Public dist must not include {reason}: {path}")
|
||||
|
||||
|
||||
def _iter_text_files(root: Path) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower() in {".json", ".txt", ".md", ".py", ".toc", ".html"}:
|
||||
files.append(path)
|
||||
# PyInstaller may leave .pyc / .py modules under _internal
|
||||
if path.suffix.lower() == ".pyc" and "operator" in path.name.lower():
|
||||
files.append(path)
|
||||
return files
|
||||
|
||||
|
||||
def _assert_no_forbidden_names(root: Path) -> None:
|
||||
for path in root.rglob("*"):
|
||||
rel = path.relative_to(root).as_posix().lower()
|
||||
name = path.name.lower()
|
||||
for fragment in FORBIDDEN_NAME_FRAGMENTS:
|
||||
if fragment.lower() in rel or fragment.lower() in name:
|
||||
raise RuntimeError(
|
||||
f"Public dist contains forbidden name '{fragment}': {path}"
|
||||
)
|
||||
|
||||
|
||||
def _assert_locale_public(path: Path) -> None:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
for key in FORBIDDEN_LOCALE_KEYS:
|
||||
if key in data:
|
||||
raise RuntimeError(f"Public locale {path.name} still contains '{key}'")
|
||||
status = data.get("status")
|
||||
if isinstance(status, dict) and "workflow_done" in status:
|
||||
raise RuntimeError(f"Public locale {path.name} still contains status.workflow_done")
|
||||
|
||||
|
||||
def _assert_no_forbidden_text(root: Path) -> None:
|
||||
for path in _iter_text_files(root):
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
for snippet in FORBIDDEN_TEXT_SNIPPETS:
|
||||
if snippet in text:
|
||||
raise RuntimeError(
|
||||
f"Public dist text contains forbidden snippet {snippet!r} in {path}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require(DIST / "SensorBench.exe")
|
||||
require(DIST / "config" / "options.json")
|
||||
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||
require(DIST / "config" / "locales" / "en_US.json")
|
||||
require(DIST / "steps" / "default.json")
|
||||
require(DIST / "config" / "ddjx01.json")
|
||||
require(DIST / "_internal")
|
||||
print(f"Verified SensorBench dist layout: {DIST}")
|
||||
|
||||
for rel in FORBIDDEN_RELATIVE_PATHS:
|
||||
forbid(DIST / rel, reason=rel)
|
||||
|
||||
_assert_no_forbidden_names(DIST)
|
||||
_assert_locale_public(DIST / "config" / "locales" / "zh_CN.json")
|
||||
_assert_locale_public(DIST / "config" / "locales" / "en_US.json")
|
||||
_assert_no_forbidden_text(DIST / "config")
|
||||
|
||||
print(f"Verified public SensorBench dist layout: {DIST}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Verify the private SensorBenchOperator onedir layout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIST = ROOT / "dist" / "SensorBenchOperator"
|
||||
|
||||
|
||||
def require(path: Path) -> None:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Missing operator build artifact: {path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require(DIST / "SensorBenchOperator.exe")
|
||||
require(DIST / "_internal")
|
||||
require(DIST / "steps" / "default.json")
|
||||
require(DIST / "config" / "options.json")
|
||||
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||
require(DIST / "config" / "locales" / "en_US.json")
|
||||
require(DIST / "config" / "ddjx01.json")
|
||||
|
||||
options = json.loads((DIST / "config" / "options.json").read_text(encoding="utf-8"))
|
||||
if "sensor_types" not in options:
|
||||
raise RuntimeError("Operator options.json missing sensor_types")
|
||||
|
||||
locale = json.loads(
|
||||
(DIST / "config" / "locales" / "en_US.json").read_text(encoding="utf-8")
|
||||
)
|
||||
if "workflow" not in locale:
|
||||
raise RuntimeError("Operator locale missing workflow section")
|
||||
if "Inspection Workflow" not in json.dumps(locale, ensure_ascii=False):
|
||||
# Chinese catalog may be the default UI language; English must still ship.
|
||||
raise RuntimeError("Operator en_US locale missing Inspection Workflow title")
|
||||
|
||||
steps = json.loads((DIST / "steps" / "default.json").read_text(encoding="utf-8"))
|
||||
if not steps.get("steps"):
|
||||
raise RuntimeError("Operator steps/default.json has no steps")
|
||||
|
||||
# Guard against accidentally packaging the whole internal/ source tree.
|
||||
if (DIST / "internal").exists():
|
||||
raise RuntimeError(f"Operator dist should not nest internal/: {DIST / 'internal'}")
|
||||
if (DIST / "src").exists():
|
||||
raise RuntimeError(f"Operator dist should not ship raw src/: {DIST / 'src'}")
|
||||
|
||||
print(f"Verified internal SensorBenchOperator dist layout: {DIST}")
|
||||
print("INTERNAL USE ONLY - do not publish to customers.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user