blob: 3d3d22efe00dd1bf16fd31145798187bb209888b [file]
import argparse
import json
import shlex
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'test', 'hil'))
from helper import hil_report # noqa: E402 stdlib-only; board_variants() reads a roster board's builds
from helper import hil_util # noqa: E402 ESP_IDF_FLASHERS; hil_boards() needs lib/agentrc (get_deps.py)
def _resolve_config_path(config_file):
if os.path.exists(config_file):
return config_file
# bare roster names resolve against the repo's test/hil (this script lives in
# .github/scripts); build.yml passes explicit paths, this is for hand-runs
repo_relative = os.path.join(os.path.dirname(__file__), '..', '..', 'test', 'hil', config_file)
if os.path.exists(repo_relative):
return repo_relative
raise FileNotFoundError(f'Config file not found: {config_file}')
def _variant_args(v):
# build_util.yml's Build step splices this string into bash source,
# so the quoting round-trips a spaced value into one argv item like
# build_board's argv path. The SAME string also reaches the get_deps
# env expansion and the artifact-name charset, where spaced/quoted
# values still fail (loudly) -- keep defines space-free
return ''.join(f' -D{shlex.quote(d)}' for d in v['defines']) + ''.join(f' --cflag={tok}' for tok in v['flags'])
def main():
parser = argparse.ArgumentParser()
parser.add_argument('config_files', nargs='+', help='Configuration JSON file(s)')
g = parser.add_mutually_exclusive_group()
g.add_argument('--select', help='selection manifest v1 (check_build.py --select-only); '
'scopes boards when hil.full is false')
# a whole selection as one argv can exceed MAX_ARG_STRLEN on a big diff, which
# would fail the step instead of falling open; callers that already have the
# selection on disk pass the path instead
g.add_argument('--select-file', help='file holding the same JSON as --select')
args = parser.parse_args()
raw = args.select
hil = None
try:
if args.select_file:
with open(args.select_file) as f:
raw = f.read()
if raw:
# imported here, under the fall-open: a broken selector must still leave the
# full roster
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'tools'))
import change_impact
hil = change_impact.check_manifest(json.loads(raw))['hil']
except Exception as e: # fail-open: an unusable selection must never red the job
# ALL of it is unusable, hil_examples included: keeping the -e lists would build
# a few examples per board while the rig, unfiltered, runs its whole test list
print(f'hil_ci_set_matrix: selection unusable ({e}) - full roster',
file=sys.stderr)
hil = None
# an explicit empty boards map stays a legitimate nothing-selected
selected = None if hil is None or hil['full'] else set(hil['boards'])
ex_map = hil['hil_examples'] if hil else {}
# Toolchain buckets must match the toolchains instantiated by the hil-build
# job in .github/workflows/build.yml. Keep all keys present (even if empty)
# so `fromJSON(hil_json)[toolchain]` always resolves to a list.
matrix = {
'arm-gcc': [],
'riscv-gcc': [],
'esp-idf': []
}
seen = {toolchain: set() for toolchain in matrix}
def append_build_arg(toolchain, build_arg):
if build_arg not in seen[toolchain]:
seen[toolchain].add(build_arg)
matrix[toolchain].append(build_arg)
for config_file in args.config_files:
with open(_resolve_config_path(config_file)) as f:
config = json.load(f)
for board in config['boards']:
if selected is not None and board['name'] not in selected:
continue
name = board['name']
try: # the board's flasher is agentrc's, by its probe
flasher = hil_util.hil_boards().resolve(board)['flasher']
except (ImportError, ValueError) as e:
raise SystemExit(f'{config_file}: {e}')
# esptool and openocd_esp boards must build under esp-idf; others default
# to arm-gcc but may opt into another bucket via an explicit "toolchain"
# field (e.g. RISC-V boards like ch32v20x need "riscv-gcc").
if flasher['name'] in hil_util.ESP_IDF_FLASHERS:
toolchain = 'esp-idf'
else:
toolchain = board.get('toolchain', 'arm-gcc')
if toolchain not in matrix:
# a board in no bucket would never be built, and the bare KeyError
# below would only say so as a traceback from the set-matrix job
raise SystemExit(
f'{name}: toolchain {toolchain!r} is not a build bucket '
f'({", ".join(matrix)}); add it here and to the hil-build / '
f'hil-build-esp jobs in .github/workflows/build.yml')
build_board = f'-b {name}'
# PR selection: build only the examples this board will run (its test
# list plus device/board_test, the parking firmware) - tools/build.py -e.
# Absent key (hand runs, full non-PR builds) keeps --target all.
for ex in ex_map.get(name, []):
build_board += f' -e {ex}'
# an always-on define (MAX3421_HOST=1, LOGGER=rtt) is a single self-named variant
try:
variants = hil_report.board_variants(board)
except ValueError as e:
raise SystemExit(f'{config_file}: {e}')
for v in variants:
if toolchain == 'esp-idf' and v['name'] == name and (v['defines'] or v['flags']):
# hil-build-esp sizes and uploads every leg as the board its --build-name names
raise SystemExit(f'{config_file}: {name}: an esp-idf variant needs a name of its own')
arg = build_board
if v['name'] != name:
arg += f' --build-name {v["name"]}'
append_build_arg(toolchain, arg + _variant_args(v))
# the SystemView capture's traced image: a leg of its own, never the test or parking build
try:
sv = hil_report.sysview_build(board)
except ValueError as e:
raise SystemExit(f'{config_file}: {e}')
# hil_test.py captures only beside a device/ or dual/ test, never for the park image alone
captured = name not in ex_map or any(ex.startswith(('device/', 'dual/')) and ex != 'device/board_test'
for ex in ex_map[name])
if sv and captured:
append_build_arg(toolchain, f'-b {name} --build-name {sv["name"]} -e {sv["example"]}' +
_variant_args(sv))
print(json.dumps(matrix))
if __name__ == '__main__':
main()