| import argparse |
| import json |
| import shlex |
| import os |
| import sys |
| |
| |
| 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 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='ci_select.py JSON; scopes boards when full=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 |
| sel = None |
| try: |
| if args.select_file: |
| with open(args.select_file) as f: |
| raw = f.read() |
| if raw: |
| sel = json.loads(raw) |
| if sel is not None and not isinstance(sel, dict): |
| raise ValueError(f'selection is {type(sel).__name__}, not an object') |
| except Exception as e: # fail-open: an unusable selection must never red the job |
| print(f'hil_ci_set_matrix: selection unusable ({e}) - full roster', |
| file=sys.stderr) |
| sel = None |
| |
| selected = None |
| if sel and not sel.get('full'): |
| # key ABSENT is an unusable selection, not "nothing selected" - same reading as |
| # ci_set_matrix.py. Filtering every board out would skip every hil-build leg and, |
| # through needs:, both rig jobs: an all-green PR with zero hardware coverage. |
| # An explicit boards: {} stays a legitimate nothing-selected. |
| if not isinstance(sel.get('boards'), dict): |
| print('hil_ci_set_matrix: selection has full false but no usable boards ' |
| 'map - full roster', file=sys.stderr) |
| sel = None # ALL of it is unusable, hil_examples included: keeping |
| # the -e lists would build a few examples per board |
| # while the rig, unfiltered, runs that board's whole |
| # test list - flash failures on the fail-open path |
| else: |
| selected = set(sel['boards']) |
| ex_map = (sel or {}).get('hil_examples') or {} |
| if not isinstance(ex_map, dict): |
| ex_map = {} |
| |
| # 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'] |
| flasher = board['flasher'] |
| # esptool 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'] == 'esptool': |
| 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}' |
| |
| # Each variant builds into cmake-build-<variant.name> with its own cmake |
| # -D defines and raw CFLAGS. No 'variant' -> a single build named after |
| # the board; an always-on define (MAX3421_HOST=1, LOGGER=rtt) is a single |
| # self-named variant carrying it. |
| variants = board.get('variant') or [{'name': name, 'flags': ''}] |
| for v in variants: |
| arg = build_board |
| if v['name'] != name: |
| arg += f' --build-name {v["name"]}' |
| # 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 |
| for d in v.get('defines', []): |
| arg += f' -D{shlex.quote(d)}' |
| for tok in v.get('flags', '').split(): |
| arg += f' --cflag={tok}' |
| append_build_arg(toolchain, arg) |
| |
| print(json.dumps(matrix)) |
| |
| |
| if __name__ == '__main__': |
| main() |