Skip to content
File

Blob: scripts/build_firmware.py

python148 lines
1#!/usr/bin/env python3
2"""Build the S3 radio with credential headers generated in the private build directory."""
3import argparse
4import json
5import os
6import subprocess
7 
8from config import read_env
9from project import (
10 CODEC_REVISION,
11 DSP_REVISION,
12 IDF_REVISION,
13 ROOT,
14 RUST_RELEASE,
15 VENDOR,
16 build_dir,
17 idf_environment,
18 sdk_root as configured_sdk_root,
19 verify_checkout,
20)
21 
22 
23def require_supported_sdk_configuration(build):
24 path = build / "config/sdkconfig.json"
25 try:
26 contents = path.read_text()
27 except (OSError, UnicodeError):
28 raise SystemExit(
29 "ESP-IDF did not produce a readable resolved sdkconfig.json. "
30 "Remove firmware/sdkconfig and rerun the firmware build."
31 ) from None
32 try:
33 config = json.loads(contents)
34 except json.JSONDecodeError:
35 raise SystemExit(
36 "ESP-IDF produced an invalid resolved sdkconfig.json. "
37 "Remove firmware/sdkconfig and rerun the firmware build."
38 ) from None
39 if not isinstance(config, dict):
40 raise SystemExit(
41 "ESP-IDF resolved sdkconfig.json must contain a JSON object. "
42 "Remove firmware/sdkconfig and rerun the firmware build."
43 )
44 
45 required_booleans = (
46 "COMPILER_OPTIMIZATION_PERF",
47 "COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE",
48 "MBEDTLS_HARDWARE_AES",
49 "MBEDTLS_HARDWARE_SHA",
50 "MBEDTLS_HARDWARE_MPI",
51 )
52 supported = all(config.get(name) is True for name in required_booleans)
53 supported = supported and config.get("SPIRAM_MALLOC_ALWAYSINTERNAL") == 1024
54 if not supported:
55 raise SystemExit(
56 "The resolved ESP-IDF settings do not match the supported S3 profile: "
57 "-O2, assertions, a 1 KiB PSRAM allocation threshold, and hardware "
58 "AES/SHA/MPI are required. Remove firmware/sdkconfig to reapply "
59 "firmware/sdkconfig.defaults."
60 )
61 
62 
63def main():
64 parser = argparse.ArgumentParser(description=__doc__)
65 parser.add_argument("--signaling-url", default=os.environ.get("SIGNALING_URL"),
66 help="Worker HTTPS origin (defaults to SIGNALING_URL)")
67 parser.add_argument(
68 "--crypto-self-test",
69 action="store_true",
70 help="Build an image that runs extended crypto tests before streaming",
71 )
72 parser.add_argument(
73 "--crypto-profile",
74 action="store_true",
75 help="Build an image that profiles native crypto calls and allocations for 30 seconds",
76 )
77 args = parser.parse_args()
78 if not args.signaling_url:
79 parser.error("Set SIGNALING_URL or --signaling-url to your deployed Worker HTTPS origin")
80 if not args.signaling_url.startswith("https://") or len(args.signaling_url) > 180:
81 raise SystemExit(
82 "The autonomous signaling URL must use HTTPS and be at most 180 characters"
83 )
84 
85 root = ROOT
86 sdk_root = configured_sdk_root()
87 sdk = sdk_root / "esp-idf"
88 build = build_dir()
89 verify_checkout(sdk, IDF_REVISION)
90 verify_checkout(VENDOR / "esp-dsp", DSP_REVISION)
91 verify_checkout(VENDOR / "esp-adf-libs", CODEC_REVISION)
92 version = subprocess.check_output(
93 ["rustup", "run", "esp-radio", "rustc", "--version"], text=True
94 )
95 if f"({RUST_RELEASE})" not in version:
96 raise SystemExit("Run make setup to install the pinned Xtensa Rust compiler")
97 private = build / "private"
98 private.mkdir(parents=True, exist_ok=True)
99 private.chmod(0o700)
100 values = read_env(root / ".credential.env")
101 ssid = values.get("WIFI_SSID", "").encode()
102 password = values.get("WIFI_PASSWORD", "").encode()
103 if not 1 <= len(ssid) <= 32 or len(password) > 64:
104 raise SystemExit("Check WIFI_SSID and WIFI_PASSWORD lengths in .credential.env")
105 
106 def c_bytes(value):
107 return '"' + "".join(f"\\x{byte:02x}" for byte in value) + '"'
108 
109 header = private / "wifi_private.h"
110 header.write_text(
111 f"#pragma once\n#define WIFI_SSID {c_bytes(ssid)}\n"
112 f"#define WIFI_PASSWORD {c_bytes(password)}\n"
113 )
114 header.chmod(0o600)
115 signaling = private / "signaling_private.h"
116 device_token = values.get("DEVICE_TOKEN", "")
117 if not 32 <= len(device_token) <= 128:
118 raise SystemExit(
119 "Set a 32–128 character DEVICE_TOKEN in .credential.env; run make secrets for initial setup"
120 )
121 signaling.write_text(
122 "#pragma once\n"
123 "#define SIGNALING_AUTONOMOUS 1\n"
124 f"#define SIGNALING_URL {c_bytes(args.signaling_url.rstrip('/').encode())}\n"
125 f"#define SIGNALING_DEVICE_TOKEN {c_bytes(device_token.encode())}\n"
126 )
127 signaling.chmod(0o600)
128 python, env = idf_environment()
129 os.umask(0o077)
130 command = [
131 str(python),
132 str(sdk / "tools/idf.py"),
133 "-B",
134 str(build),
135 f"-DRADIO_CRYPTO_SELF_TEST={'ON' if args.crypto_self_test else 'OFF'}",
136 f"-DRADIO_CRYPTO_PROFILE={'ON' if args.crypto_profile else 'OFF'}",
137 ]
138 configured = subprocess.run([*command, "reconfigure"], cwd=root / "firmware", env=env)
139 if configured.returncode:
140 raise SystemExit(configured.returncode)
141 require_supported_sdk_configuration(build)
142 result = subprocess.run([*command, "build"], cwd=root / "firmware", env=env)
143 raise SystemExit(result.returncode)
144 
145 
146if __name__ == "__main__":
147 main()