Add Meshtext Companion channel chat and automatic radio2 setup
This commit is contained in:
1 parent
1f1a499482
commit
05907f49e0
52 files changed
+7700
-128
No files matched your search
@@ -11,6 +11,9 @@ import sys
|
||||
import struct
|
||||
import zipfile
|
||||
import zlib
|
||||
from package_meshtext_license import (CAPABILITY as MESHTEXT_CAPABILITY,
|
||||
meshtext_evidence, notice_bytes as meshtext_notice_bytes,
|
||||
notice_record as meshtext_notice_record)
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools" / "mota"))
|
||||
from motalib import parse_nrf52_layout
|
||||
@@ -186,6 +189,19 @@ def main() -> int:
|
||||
|
||||
checks = []
|
||||
malformed = False
|
||||
third_party_notices = []
|
||||
try:
|
||||
codec_symbols, chat_symbols = meshtext_evidence(args.image)
|
||||
if codec_symbols:
|
||||
stem = args.output.name.removesuffix(".capabilities.json")
|
||||
third_party_notices.append(meshtext_notice_record(stem, meshtext_notice_bytes()))
|
||||
if chat_symbols:
|
||||
checks.append({"capability": MESHTEXT_CAPABILITY,
|
||||
"evidence": "; ".join(chat_symbols), "present": True,
|
||||
"source": "linked ELF symbols"})
|
||||
except (OSError, ValueError) as error:
|
||||
print(f"capability check: cannot qualify linked Meshtext code: {error}", file=sys.stderr)
|
||||
malformed = True
|
||||
application = b""
|
||||
if args.expect_application:
|
||||
try:
|
||||
@@ -245,6 +261,8 @@ def main() -> int:
|
||||
"ota_update_evidence": ota_evidence,
|
||||
"verified": not malformed and not missing and (not args.require_ota or ota_verified),
|
||||
}
|
||||
if third_party_notices:
|
||||
manifest["third_party_notices"] = third_party_notices
|
||||
if ota_verified and args.platform == "NRF52_PLATFORM" and "lora" in ota_methods:
|
||||
with zipfile.ZipFile(args.dfu_package) as package:
|
||||
app = json.loads(package.read("manifest.json"))["manifest"]["application"]
|
||||
|
||||
@@ -23,7 +23,8 @@ RECIPE_ENVIRONMENT = (
|
||||
"MESHCORE_NRF52_LEGACY_FLASH_OTA", "MESHCORE_REDUCED_TLS",
|
||||
)
|
||||
PACKAGE_SUFFIXES = (".bin", "-merged.bin", ".uf2", ".zip", ".hex",
|
||||
".capabilities.json", ".memory.json", ".font-license.txt")
|
||||
".capabilities.json", ".memory.json", ".font-license.txt",
|
||||
".meshtext-license.txt")
|
||||
|
||||
|
||||
def canonical_digest(value):
|
||||
|
||||
@@ -10,6 +10,7 @@ import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from package_nrf52_font_license import validate_artifact_notice
|
||||
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
|
||||
|
||||
|
||||
def digest(path):
|
||||
@@ -43,6 +44,7 @@ def artifact_files(stem):
|
||||
candidates = [stem.parent / (stem.name + suffix)
|
||||
for suffix in (".bin", "-merged.bin", ".uf2", ".zip", ".hex", ".capabilities.json",
|
||||
".font-license.txt")]
|
||||
candidates.append(stem.parent / (stem.name + ".meshtext-license.txt"))
|
||||
return [p for p in candidates if p.is_file()]
|
||||
|
||||
|
||||
@@ -50,6 +52,7 @@ def package_report(directory, stem):
|
||||
stem = Path(stem)
|
||||
report = validate_build(directory)
|
||||
validate_artifact_notice(stem)
|
||||
validate_meshtext_notice(stem)
|
||||
files = artifact_files(stem)
|
||||
if len(files) < 2 or not any(p.suffix in (".bin", ".uf2", ".zip", ".hex") for p in files):
|
||||
raise ValueError("cannot qualify an empty firmware package")
|
||||
@@ -64,6 +67,7 @@ def package_report(directory, stem):
|
||||
def validate_package(stem):
|
||||
stem = Path(stem)
|
||||
validate_artifact_notice(stem)
|
||||
validate_meshtext_notice(stem)
|
||||
report = passing_report(stem.parent / (stem.name + ".memory.json"))
|
||||
actual = {p.name: digest(p) for p in artifact_files(stem)}
|
||||
if len(actual) < 2 or actual != report.get("files"):
|
||||
|
||||
@@ -18,6 +18,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from firmware_memory_manifest import validate_package
|
||||
from check_nrf52_uf2 import check_uf2
|
||||
from package_nrf52_font_license import validate_artifact_notice
|
||||
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -161,6 +162,9 @@ def collect_artifacts(directory, version):
|
||||
font_notice = validate_artifact_notice(directory / stem)
|
||||
if font_notice is not None:
|
||||
files.append(font_notice)
|
||||
meshtext_notice = validate_meshtext_notice(directory / stem)
|
||||
if meshtext_notice is not None:
|
||||
files.append(meshtext_notice)
|
||||
manifest["runtime_ram"] = {key: memory[key] for key in (
|
||||
"passed", "available_internal_bytes", "required_heap_bytes", "elf_sha256")}
|
||||
accounted.update(files)
|
||||
@@ -192,8 +196,11 @@ def category(record):
|
||||
|
||||
|
||||
def sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def completed_matrix_failures(status, allow_partial=False):
|
||||
|
||||
@@ -23,6 +23,7 @@ from motalib import ( # noqa: E402
|
||||
)
|
||||
from check_esp32_app_size import app_partition_size # noqa: E402
|
||||
from firmware_memory_manifest import validate_package # noqa: E402
|
||||
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
|
||||
|
||||
|
||||
BOARDS = {
|
||||
@@ -587,6 +588,9 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
|
||||
"target-partitions.bin": table,
|
||||
"capabilities.json": capability_path.read_bytes(),
|
||||
}
|
||||
meshtext_notice = validate_meshtext_notice(build_dir / stem)
|
||||
if meshtext_notice is not None:
|
||||
files[meshtext_notice.name] = meshtext_notice.read_bytes()
|
||||
if spec.get("expander_bridge"):
|
||||
expander_body = (utility_build_dir / spec["expander_bridge"] /
|
||||
"firmware.bin").read_bytes()
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ship the adapted Meshtext MIT notice only for actually linked code."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from firmware_elf import FirmwareElf
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NOTICE_PATH = ROOT / "licenses/ESPP-Meshtext-MIT.txt"
|
||||
SIDECAR_SUFFIX = ".meshtext-license.txt"
|
||||
CAPABILITY = "companion.meshtext.channel_text"
|
||||
COMPONENT = "espp-meshtext"
|
||||
|
||||
|
||||
def notice_bytes():
|
||||
data = NOTICE_PATH.read_bytes()
|
||||
if not valid_notice(data):
|
||||
raise ValueError("Meshtext MIT source notice is incomplete")
|
||||
return data
|
||||
|
||||
|
||||
def valid_notice(data):
|
||||
return (0 < len(data) <= 262144 and all(text in data for text in (
|
||||
b"Copyright (c) 2022 esp-cpp", b"Permission is hereby granted, free of charge",
|
||||
b"included in all", b"copies or substantial portions of the Software",
|
||||
b'THE SOFTWARE IS PROVIDED "AS IS"', b"OTHER DEALINGS IN THE", b"SOFTWARE.")))
|
||||
|
||||
|
||||
def linked_symbols(path):
|
||||
"""Only allocated executable definitions qualify; debug strings do not."""
|
||||
if Path(path).read_bytes()[:4] != b"\x7fELF":
|
||||
return [] # Existing marker-only test fixtures are not linked ELFs.
|
||||
elf = FirmwareElf(path)
|
||||
result = []
|
||||
for name, (_address, size, index) in elf.symbols.items():
|
||||
if size <= 0 or index >= len(elf.sections):
|
||||
continue
|
||||
section = elf.sections[index]
|
||||
if section[1] != 8 and section[2] & 6 == 6:
|
||||
result.append(name)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def meshtext_evidence(path):
|
||||
symbols = linked_symbols(path)
|
||||
# Match the Itanium namespace prefix, not arbitrary names containing MT.
|
||||
codec = [name for name in symbols if name.startswith("_ZN4mesh2mt")]
|
||||
required = ("_ZN4mesh2mt10encodeTextE", "_ZN4mesh2mt10decodeTextE",
|
||||
"_ZN6MyMesh16sendMeshtextTextE", "_ZN6MyMesh19onForeignRadioFrameE",
|
||||
"_ZN4mesh10Dispatcher22queueForeignRadioFrameE")
|
||||
selected = []
|
||||
for operation in required:
|
||||
matches = [name for name in symbols if name.startswith(operation)]
|
||||
if not matches:
|
||||
return codec, []
|
||||
selected.append(matches[0])
|
||||
return codec, selected if codec else []
|
||||
|
||||
|
||||
def notice_record(stem, data):
|
||||
return {"component": COMPONENT, "license": "MIT",
|
||||
"file": Path(stem).name + SIDECAR_SUFFIX,
|
||||
"sha256": hashlib.sha256(data).hexdigest()}
|
||||
|
||||
|
||||
def sidecar(stem):
|
||||
stem = Path(stem)
|
||||
return stem.parent / (stem.name + SIDECAR_SUFFIX)
|
||||
|
||||
|
||||
def manifest_record(stem):
|
||||
stem = Path(stem)
|
||||
manifest = json.loads((stem.parent / (stem.name + ".capabilities.json")).read_text())
|
||||
records = [record for record in manifest.get("third_party_notices", [])
|
||||
if record.get("component") == COMPONENT]
|
||||
if len(records) > 1:
|
||||
raise ValueError(f"{stem}: duplicate Meshtext license declarations")
|
||||
if not records:
|
||||
if CAPABILITY in manifest.get("capabilities", []):
|
||||
raise ValueError(f"{stem}: Meshtext capability lacks its MIT notice")
|
||||
return None
|
||||
record = records[0]
|
||||
if (record.get("file") != sidecar(stem).name or record.get("license") != "MIT"
|
||||
or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", "")))):
|
||||
raise ValueError(f"{stem}: invalid Meshtext license declaration")
|
||||
return record
|
||||
|
||||
|
||||
def validate_artifact_notice(stem):
|
||||
record = manifest_record(stem)
|
||||
output = sidecar(stem)
|
||||
if record is None:
|
||||
if output.exists():
|
||||
raise ValueError(f"{output}: stale notice for an unaffected firmware")
|
||||
return None
|
||||
if not output.is_file():
|
||||
raise ValueError(f"{output}: missing Meshtext MIT notice")
|
||||
data = output.read_bytes()
|
||||
if not valid_notice(data) or hashlib.sha256(data).hexdigest() != record["sha256"]:
|
||||
raise ValueError(f"{output}: incomplete or mismatched Meshtext MIT notice")
|
||||
return output
|
||||
|
||||
|
||||
def collect_build_notice(directory, stem):
|
||||
codec, _chat = meshtext_evidence(Path(directory) / "firmware.elf")
|
||||
record = manifest_record(stem)
|
||||
if bool(codec) != bool(record):
|
||||
raise ValueError("Meshtext license declaration does not match the linked ELF")
|
||||
if not codec:
|
||||
return validate_artifact_notice(stem)
|
||||
data = notice_bytes()
|
||||
if record != notice_record(stem, data):
|
||||
raise ValueError("Meshtext license declaration does not match this build's source notice")
|
||||
output = sidecar(stem)
|
||||
if output.exists():
|
||||
return validate_artifact_notice(stem)
|
||||
temporary = output.with_name(f".{output.name}.{os.getpid()}.tmp")
|
||||
try:
|
||||
temporary.write_bytes(data)
|
||||
temporary.replace(output)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return validate_artifact_notice(stem)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=("collect", "validate"))
|
||||
parser.add_argument("path", type=Path)
|
||||
parser.add_argument("--stem", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.action == "collect":
|
||||
if args.stem is None:
|
||||
parser.error("collect requires --stem")
|
||||
collect_build_notice(args.path, args.stem)
|
||||
else:
|
||||
validate_artifact_notice(args.path)
|
||||
except (OSError, ValueError, KeyError, TypeError) as error:
|
||||
parser.exit(1, f"Meshtext license qualification failed: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user