tirtc-device-builder 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +21 -31
- package/bin/esp32-kit-metadata.js +7 -6
- package/bin/install-esp32-kit.js +2 -0
- package/package.json +1 -1
- package/skills/tirtc-esp32-builder/SKILL.md +4 -4
- package/skills/tirtc-esp32-builder/USAGE.md +1 -1
- package/skills/tirtc-esp32-builder/assets/board-video-contract.example.json +2 -1
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +7 -5
- package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +1 -0
- package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +67 -38
- package/skills/tirtc-esp32-builder/assets/report-template.md +5 -1
- package/skills/tirtc-esp32-builder/assets/tirtc-runtime-contract.example.json +10 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +3 -3
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +3 -1
- package/skills/tirtc-esp32-builder/references/reporting.md +2 -2
- package/skills/tirtc-esp32-builder/references/runtime-contract.md +34 -0
- package/skills/tirtc-esp32-builder/references/video-contract.md +4 -3
- package/skills/tirtc-esp32-builder/references/workflow.md +3 -1
- package/skills/tirtc-esp32-builder/scripts/doctor.py +7 -1
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +177 -3
- package/skills/tirtc-esp32-builder/scripts/install_runtime_gate.py +66 -0
- package/skills/tirtc-esp32-builder/scripts/project_portability.py +2 -2
- package/skills/tirtc-esp32-builder/scripts/runtime_contract.py +314 -0
- package/skills/tirtc-esp32-builder/scripts/video_contract.py +74 -0
|
@@ -62,12 +62,14 @@ The adapter owns:
|
|
|
62
62
|
- DMA buffers, hardware clocks, power, reset, GPIO, refresh/key-frame requests, and realtime task allocation;
|
|
63
63
|
- bounded stop, resource release, and generation-aware flushing.
|
|
64
64
|
|
|
65
|
-
The stable modules own stream IDs, negotiated/contracted formats, TiRTC callback copying, connection handles, session generation, and H5/AI sequencing.
|
|
65
|
+
The stable modules own the discovered TiRTC service endpoint, stream IDs, negotiated/contracted formats, TiRTC callback copying, connection handles, session generation, and H5/AI sequencing. SDK lifecycle changes such as disconnect run in a worker/state-machine context, never directly inside an SDK callback.
|
|
66
66
|
|
|
67
67
|
## Verification loop
|
|
68
68
|
|
|
69
69
|
Use a bounded loop per layer: diagnose one failing invariant, make the smallest correction, and rerun that layer before moving forward. Change one high-risk variable per HIL comparison. Turn reusable invariants into tests or post-link gates. Stop and report when the remaining failure requires unavailable hardware, credentials, a new SDK binary, a public protocol change, or a user choice.
|
|
70
70
|
|
|
71
|
+
For every generated H5/AI project, validate `tirtc-runtime-contract.json` and run `install_runtime_gate.py <project>` before the ordinary build. Audio and video projects additionally install their media gates. A build that bypasses any applicable gate is not `BUILD_VERIFIED`.
|
|
72
|
+
|
|
71
73
|
Run the assessor once per layer:
|
|
72
74
|
|
|
73
75
|
- `--phase intake`: corroborated design evidence; success is `READY_TO_PORT`.
|
|
@@ -124,8 +124,14 @@ def compare_contract(
|
|
|
124
124
|
actual = config.get(key)
|
|
125
125
|
if expected is None:
|
|
126
126
|
mismatches.append(f"SDK contract does not declare {key}")
|
|
127
|
+
elif actual is None and expected.lower() == "off":
|
|
128
|
+
# ESP-IDF omits some disabled child booleans from the final sdkconfig
|
|
129
|
+
# when their parent dependency is off. An absent boolean therefore has
|
|
130
|
+
# the same effective value as "# ... is not set" for this fixed set of
|
|
131
|
+
# SDK ABI options. Expected-on and scalar values remain strict.
|
|
132
|
+
continue
|
|
127
133
|
elif actual is None:
|
|
128
|
-
mismatches.append(f"project does not
|
|
134
|
+
mismatches.append(f"project does not configure required {key}={expected}")
|
|
129
135
|
elif actual.lower() != expected.lower():
|
|
130
136
|
mismatches.append(f"{key}: expected {expected}, got {actual}")
|
|
131
137
|
return mismatches
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import re
|
|
9
10
|
import shutil
|
|
@@ -12,6 +13,7 @@ from pathlib import Path
|
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
from audio_contract import verify_contract as verify_audio_contract
|
|
16
|
+
from runtime_contract import verify_contract as verify_runtime_contract
|
|
15
17
|
from video_contract import verify_contract as verify_video_contract
|
|
16
18
|
|
|
17
19
|
|
|
@@ -63,6 +65,16 @@ FEATURE_HIL_LEVEL = {
|
|
|
63
65
|
"ai_talk": "L6",
|
|
64
66
|
}
|
|
65
67
|
SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
|
68
|
+
SOURCE_SCHEME_RE = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*):")
|
|
69
|
+
ALLOWED_SOURCE_SCHEMES = {
|
|
70
|
+
"http",
|
|
71
|
+
"https",
|
|
72
|
+
"device-kit",
|
|
73
|
+
"managed",
|
|
74
|
+
"official",
|
|
75
|
+
"user-input",
|
|
76
|
+
"user-supplied",
|
|
77
|
+
}
|
|
66
78
|
Requirement = tuple[str, str, int]
|
|
67
79
|
|
|
68
80
|
|
|
@@ -78,6 +90,62 @@ def load_ir(path: Path) -> dict[str, Any]:
|
|
|
78
90
|
return data
|
|
79
91
|
|
|
80
92
|
|
|
93
|
+
def sha256_file(path: Path) -> str:
|
|
94
|
+
digest = hashlib.sha256()
|
|
95
|
+
with path.open("rb") as stream:
|
|
96
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
97
|
+
digest.update(chunk)
|
|
98
|
+
return digest.hexdigest()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_source_locations(data: dict[str, Any], ir_directory: Path) -> list[str]:
|
|
102
|
+
"""Resolve local evidence and reject invented or machine-bound source locators."""
|
|
103
|
+
errors: list[str] = []
|
|
104
|
+
root = ir_directory.expanduser().resolve()
|
|
105
|
+
sources = data.get("sources")
|
|
106
|
+
if not isinstance(sources, list):
|
|
107
|
+
return errors
|
|
108
|
+
for index, item in enumerate(sources):
|
|
109
|
+
if not isinstance(item, dict):
|
|
110
|
+
continue
|
|
111
|
+
prefix = f"sources[{index}]"
|
|
112
|
+
location = item.get("location")
|
|
113
|
+
if not isinstance(location, str) or not location.strip():
|
|
114
|
+
continue
|
|
115
|
+
if ";" in location:
|
|
116
|
+
errors.append(
|
|
117
|
+
f"{prefix}.location must identify exactly one source, not a semicolon list"
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
scheme_match = SOURCE_SCHEME_RE.match(location)
|
|
121
|
+
if scheme_match is not None:
|
|
122
|
+
scheme = scheme_match.group(1).lower()
|
|
123
|
+
if scheme not in ALLOWED_SOURCE_SCHEMES:
|
|
124
|
+
errors.append(
|
|
125
|
+
f"{prefix}.location uses unsupported source scheme {scheme!r}"
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
relative = Path(location)
|
|
129
|
+
if relative.is_absolute():
|
|
130
|
+
errors.append(
|
|
131
|
+
f"{prefix}.location must be IR-relative or a supported source URI"
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
resolved = (root / relative).resolve()
|
|
135
|
+
if not resolved.exists():
|
|
136
|
+
errors.append(
|
|
137
|
+
f"{prefix}.location does not resolve from the IR directory: {location}"
|
|
138
|
+
)
|
|
139
|
+
continue
|
|
140
|
+
expected_sha = item.get("sha256")
|
|
141
|
+
if expected_sha is not None:
|
|
142
|
+
if not resolved.is_file():
|
|
143
|
+
errors.append(f"{prefix}.sha256 can only describe a regular file")
|
|
144
|
+
elif sha256_file(resolved).lower() != str(expected_sha).lower():
|
|
145
|
+
errors.append(f"{prefix}.sha256 does not match {location}")
|
|
146
|
+
return errors
|
|
147
|
+
|
|
148
|
+
|
|
81
149
|
def mapping(value: Any, path: str, errors: list[str]) -> dict[str, Any]:
|
|
82
150
|
if not isinstance(value, dict):
|
|
83
151
|
errors.append(f"{path} must be an object")
|
|
@@ -245,6 +313,16 @@ def validate_hardware_resources(
|
|
|
245
313
|
errors.append(
|
|
246
314
|
"hardware_resources.video_semantic_contract must be project-relative"
|
|
247
315
|
)
|
|
316
|
+
runtime_contract = resources.get("runtime_semantic_contract")
|
|
317
|
+
nullable_string(
|
|
318
|
+
runtime_contract,
|
|
319
|
+
"hardware_resources.runtime_semantic_contract",
|
|
320
|
+
errors,
|
|
321
|
+
)
|
|
322
|
+
if isinstance(runtime_contract, str) and Path(runtime_contract).is_absolute():
|
|
323
|
+
errors.append(
|
|
324
|
+
"hardware_resources.runtime_semantic_contract must be project-relative"
|
|
325
|
+
)
|
|
248
326
|
|
|
249
327
|
mapping_section = mapping(
|
|
250
328
|
resources.get("audio_channel_mapping"),
|
|
@@ -462,6 +540,11 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
|
462
540
|
source = mapping(item, prefix, errors)
|
|
463
541
|
for key in ("id", "kind", "location"):
|
|
464
542
|
nonempty_string(source.get(key), f"{prefix}.{key}", errors)
|
|
543
|
+
source_sha = source.get("sha256")
|
|
544
|
+
if source_sha is not None and (
|
|
545
|
+
not isinstance(source_sha, str) or not SHA256_RE.fullmatch(source_sha)
|
|
546
|
+
):
|
|
547
|
+
errors.append(f"{prefix}.sha256 must be a 64-character SHA-256")
|
|
465
548
|
source_id = source.get("id")
|
|
466
549
|
if isinstance(source_id, str) and source_id:
|
|
467
550
|
if source_id in source_ids:
|
|
@@ -993,12 +1076,46 @@ def artifact_requirement(
|
|
|
993
1076
|
)
|
|
994
1077
|
|
|
995
1078
|
|
|
1079
|
+
def artifact_file_requirement(
|
|
1080
|
+
data: dict[str, Any], artifact_sha256: str | None, project: Path
|
|
1081
|
+
) -> Requirement:
|
|
1082
|
+
recorded = artifact_requirement(data, artifact_sha256)
|
|
1083
|
+
if recorded[0] != "SATISFIED" or artifact_sha256 is None:
|
|
1084
|
+
return recorded
|
|
1085
|
+
record = matching_build_artifact(data, artifact_sha256)
|
|
1086
|
+
if record is None:
|
|
1087
|
+
return "BLOCKED", "matching build artifact record disappeared", 0
|
|
1088
|
+
value = record.get("path")
|
|
1089
|
+
if not isinstance(value, str) or not value:
|
|
1090
|
+
return "BLOCKED", "matching build artifact path is missing", 0
|
|
1091
|
+
relative = Path(value)
|
|
1092
|
+
if relative.is_absolute():
|
|
1093
|
+
return "BLOCKED", "matching build artifact path is not project-relative", 0
|
|
1094
|
+
root = project.expanduser().resolve()
|
|
1095
|
+
path = (root / relative).resolve()
|
|
1096
|
+
if root != path and root not in path.parents:
|
|
1097
|
+
return "BLOCKED", "matching build artifact path escapes project root", 0
|
|
1098
|
+
if not path.is_file():
|
|
1099
|
+
return "BLOCKED", f"recorded build artifact does not exist: {value}", 0
|
|
1100
|
+
if path.stat().st_size != record.get("size_bytes"):
|
|
1101
|
+
return "BLOCKED", f"recorded build artifact size is stale for {value}", 0
|
|
1102
|
+
if sha256_file(path).lower() != artifact_sha256.lower():
|
|
1103
|
+
return "BLOCKED", f"recorded build artifact SHA-256 is stale for {value}", 0
|
|
1104
|
+
return (
|
|
1105
|
+
"SATISFIED",
|
|
1106
|
+
f"artifact file {value} matches size and SHA-256",
|
|
1107
|
+
VERIFICATION_LEVELS["build_verified"],
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
|
|
996
1111
|
def assess_ir(
|
|
997
1112
|
data: dict[str, Any],
|
|
998
1113
|
artifact_sha256: str | None = None,
|
|
999
1114
|
phase: str | None = None,
|
|
1000
1115
|
audio_gate: Requirement | None = None,
|
|
1001
1116
|
video_gate: Requirement | None = None,
|
|
1117
|
+
runtime_gate: Requirement | None = None,
|
|
1118
|
+
artifact_gate: Requirement | None = None,
|
|
1002
1119
|
) -> dict[str, Any]:
|
|
1003
1120
|
selected_phase = phase or ("hil" if artifact_sha256 else "intake")
|
|
1004
1121
|
if selected_phase not in ASSESSMENT_PHASES:
|
|
@@ -1043,7 +1160,7 @@ def assess_ir(
|
|
|
1043
1160
|
resources = {}
|
|
1044
1161
|
|
|
1045
1162
|
if selected_phase in {"build", "hil"}:
|
|
1046
|
-
project.append(artifact_requirement(data, artifact_sha256))
|
|
1163
|
+
project.append(artifact_gate or artifact_requirement(data, artifact_sha256))
|
|
1047
1164
|
|
|
1048
1165
|
for feature in requested:
|
|
1049
1166
|
if schema_version == 1:
|
|
@@ -1124,6 +1241,15 @@ def assess_ir(
|
|
|
1124
1241
|
0,
|
|
1125
1242
|
)
|
|
1126
1243
|
)
|
|
1244
|
+
if selected_phase in {"build", "hil"}:
|
|
1245
|
+
requirements.append(
|
|
1246
|
+
runtime_gate
|
|
1247
|
+
or (
|
|
1248
|
+
"NEEDS_CONFIRMATION",
|
|
1249
|
+
"TiRTC runtime semantic gate was not executed for this project",
|
|
1250
|
+
0,
|
|
1251
|
+
)
|
|
1252
|
+
)
|
|
1127
1253
|
result[feature] = combine_requirements(
|
|
1128
1254
|
requirements, success_status=success_status
|
|
1129
1255
|
)
|
|
@@ -1203,7 +1329,7 @@ def command_validate(args: argparse.Namespace) -> int:
|
|
|
1203
1329
|
except ValueError as exc:
|
|
1204
1330
|
print(str(exc), file=sys.stderr)
|
|
1205
1331
|
return 2
|
|
1206
|
-
errors = validate_ir(data)
|
|
1332
|
+
errors = validate_ir(data) + validate_source_locations(data, args.path.parent)
|
|
1207
1333
|
if errors:
|
|
1208
1334
|
for error in errors:
|
|
1209
1335
|
print(f"error: {error}", file=sys.stderr)
|
|
@@ -1218,7 +1344,7 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
1218
1344
|
except ValueError as exc:
|
|
1219
1345
|
print(str(exc), file=sys.stderr)
|
|
1220
1346
|
return 2
|
|
1221
|
-
errors = validate_ir(data)
|
|
1347
|
+
errors = validate_ir(data) + validate_source_locations(data, args.path.parent)
|
|
1222
1348
|
if errors:
|
|
1223
1349
|
for error in errors:
|
|
1224
1350
|
print(f"error: {error}", file=sys.stderr)
|
|
@@ -1318,6 +1444,50 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
1318
1444
|
+ "; ".join(video_gate_result.get("errors", [])),
|
|
1319
1445
|
0,
|
|
1320
1446
|
)
|
|
1447
|
+
runtime_gate: Requirement | None = None
|
|
1448
|
+
runtime_gate_result: dict[str, Any] | None = None
|
|
1449
|
+
if data.get("schema_version") == 2 and selected_phase in {"build", "hil"}:
|
|
1450
|
+
relative_contract = data["hardware_resources"].get(
|
|
1451
|
+
"runtime_semantic_contract"
|
|
1452
|
+
)
|
|
1453
|
+
if not isinstance(relative_contract, str) or not relative_contract:
|
|
1454
|
+
runtime_gate = (
|
|
1455
|
+
"NEEDS_CONFIRMATION",
|
|
1456
|
+
"hardware_resources.runtime_semantic_contract is missing",
|
|
1457
|
+
0,
|
|
1458
|
+
)
|
|
1459
|
+
runtime_gate_result = {
|
|
1460
|
+
"ok": False,
|
|
1461
|
+
"errors": ["TiRTC runtime semantic contract is missing"],
|
|
1462
|
+
}
|
|
1463
|
+
else:
|
|
1464
|
+
contract = (project / relative_contract).resolve()
|
|
1465
|
+
if project != contract and project not in contract.parents:
|
|
1466
|
+
runtime_gate_result = {
|
|
1467
|
+
"ok": False,
|
|
1468
|
+
"errors": ["TiRTC runtime semantic contract escapes project root"],
|
|
1469
|
+
}
|
|
1470
|
+
else:
|
|
1471
|
+
runtime_gate_result = verify_runtime_contract(contract, project)
|
|
1472
|
+
if runtime_gate_result["ok"]:
|
|
1473
|
+
runtime_gate = (
|
|
1474
|
+
"SATISFIED",
|
|
1475
|
+
"TiRTC runtime semantic gate passed: "
|
|
1476
|
+
+ str(runtime_gate_result.get("summary", "verified")),
|
|
1477
|
+
VERIFICATION_LEVELS["build_verified"],
|
|
1478
|
+
)
|
|
1479
|
+
else:
|
|
1480
|
+
runtime_gate = (
|
|
1481
|
+
"BLOCKED",
|
|
1482
|
+
"TiRTC runtime semantic gate failed: "
|
|
1483
|
+
+ "; ".join(runtime_gate_result.get("errors", [])),
|
|
1484
|
+
0,
|
|
1485
|
+
)
|
|
1486
|
+
artifact_gate = (
|
|
1487
|
+
artifact_file_requirement(data, args.artifact_sha256, project)
|
|
1488
|
+
if selected_phase in {"build", "hil"}
|
|
1489
|
+
else None
|
|
1490
|
+
)
|
|
1321
1491
|
try:
|
|
1322
1492
|
assessment = assess_ir(
|
|
1323
1493
|
data,
|
|
@@ -1325,6 +1495,8 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
1325
1495
|
phase=selected_phase,
|
|
1326
1496
|
audio_gate=audio_gate,
|
|
1327
1497
|
video_gate=video_gate,
|
|
1498
|
+
runtime_gate=runtime_gate,
|
|
1499
|
+
artifact_gate=artifact_gate,
|
|
1328
1500
|
)
|
|
1329
1501
|
except ValueError as exc:
|
|
1330
1502
|
print(f"error: {exc}", file=sys.stderr)
|
|
@@ -1333,6 +1505,8 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
1333
1505
|
assessment["audio_semantic_gate"] = audio_gate_result
|
|
1334
1506
|
if video_gate_result is not None:
|
|
1335
1507
|
assessment["video_semantic_gate"] = video_gate_result
|
|
1508
|
+
if runtime_gate_result is not None:
|
|
1509
|
+
assessment["runtime_semantic_gate"] = runtime_gate_result
|
|
1336
1510
|
print(json.dumps(assessment, ensure_ascii=False, indent=2))
|
|
1337
1511
|
if args.strict:
|
|
1338
1512
|
statuses = {item["status"] for item in assessment["features"].values()}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Install the portable TiRTC runtime protocol gate into an ESP-IDF project."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MARKER = "# tirtc-runtime-semantic-gate-v1"
|
|
13
|
+
BLOCK = f"""
|
|
14
|
+
|
|
15
|
+
{MARKER}
|
|
16
|
+
add_custom_target(tirtc_runtime_semantic_gate ALL
|
|
17
|
+
COMMAND "${{PYTHON}}"
|
|
18
|
+
"${{CMAKE_CURRENT_LIST_DIR}}/tools/verify_runtime_contract.py"
|
|
19
|
+
"${{CMAKE_CURRENT_LIST_DIR}}/tirtc-runtime-contract.json"
|
|
20
|
+
--project "${{CMAKE_CURRENT_LIST_DIR}}"
|
|
21
|
+
--evidence-out "${{CMAKE_BINARY_DIR}}/runtime-contract-evidence.json"
|
|
22
|
+
COMMENT "Validating TiRTC endpoint, callbacks, streams and AI media negotiation"
|
|
23
|
+
VERBATIM
|
|
24
|
+
)
|
|
25
|
+
add_dependencies(${{CMAKE_PROJECT_NAME}}.elf tirtc_runtime_semantic_gate)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install(project: Path) -> None:
|
|
30
|
+
project = project.expanduser().resolve()
|
|
31
|
+
cmake = project / "CMakeLists.txt"
|
|
32
|
+
contract = project / "tirtc-runtime-contract.json"
|
|
33
|
+
if not cmake.is_file():
|
|
34
|
+
raise ValueError(f"ESP-IDF project CMakeLists.txt not found: {cmake}")
|
|
35
|
+
if not contract.is_file():
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"runtime contract not found: {contract}; create and verify it before installing the gate"
|
|
38
|
+
)
|
|
39
|
+
tools = project / "tools"
|
|
40
|
+
tools.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
destination = tools / "verify_runtime_contract.py"
|
|
42
|
+
source = Path(__file__).resolve().with_name("runtime_contract.py")
|
|
43
|
+
if destination.exists() and destination.read_bytes() != source.read_bytes():
|
|
44
|
+
raise ValueError(f"refusing to overwrite a different gate: {destination}")
|
|
45
|
+
if not destination.exists():
|
|
46
|
+
shutil.copyfile(source, destination)
|
|
47
|
+
text = cmake.read_text(encoding="utf-8")
|
|
48
|
+
if MARKER not in text:
|
|
49
|
+
cmake.write_text(text.rstrip() + BLOCK + "\n", encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> int:
|
|
53
|
+
parser = argparse.ArgumentParser()
|
|
54
|
+
parser.add_argument("project", type=Path)
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
try:
|
|
57
|
+
install(args.project)
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
60
|
+
return 2
|
|
61
|
+
print(f"installed TiRTC runtime semantic gate: {args.project.resolve()}")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
raise SystemExit(main())
|
|
@@ -41,8 +41,8 @@ def check_project(project_path: Path, export: bool = False) -> dict[str, Any]:
|
|
|
41
41
|
missing_sdk = [str(path.relative_to(project)) for path in sdk_required if not path.is_file()]
|
|
42
42
|
if missing_sdk:
|
|
43
43
|
errors.append("bundled TiRTC SDK is incomplete: " + ", ".join(missing_sdk))
|
|
44
|
-
if export and (project / "build"
|
|
45
|
-
errors.append("export source contains build/
|
|
44
|
+
if export and (project / "build").exists():
|
|
45
|
+
errors.append("export source contains a machine-bound build/ directory")
|
|
46
46
|
|
|
47
47
|
for path in source_files(project):
|
|
48
48
|
relative = path.relative_to(project)
|