Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
20fa41f
family_json: split Windows compile commands with the C runtime's rules
HiFiPhile Oct 17, 2026
9ddd5a1
build: keep repo paths posix and tool output UTF-8 on Windows
HiFiPhile Oct 17, 2026
20a4d1b
build-doc: read and write the reference docs as UTF-8 and name paths …
HiFiPhile Oct 17, 2026
22bf5ff
make-release: read git and gh as UTF-8 and print PR titles as UTF-8
HiFiPhile Oct 17, 2026
1ea5f36
update-sponsor: run the codespell guard and print labels on Windows
HiFiPhile Oct 17, 2026
a544712
usbtest: refuse run_case and wedge_drill off Linux, print kernel sour…
HiFiPhile Oct 17, 2026
162915c
usb-kernel-recover: refuse off Linux before any verb runs
HiFiPhile Oct 17, 2026
f37e4e7
hil-contract: run hil_remote.py on Windows
HiFiPhile Oct 17, 2026
c9ae2d0
hil-contract: run test_hil_remote on Windows
HiFiPhile Oct 17, 2026
6a066b8
hil-contract: refuse unnameable paths before the lease, resolve rsync…
HiFiPhile Oct 17, 2026
3389a32
family_json: wait for the Windows lock past LK_LOCK's 10 s and docume…
HiFiPhile Oct 17, 2026
db1a6dd
build: split a quoted compiler path and report paths dropped as anoth…
HiFiPhile Oct 17, 2026
4188e28
usbtest: refuse off Linux through the verdict line after argparse, ke…
HiFiPhile Oct 17, 2026
6d3eeba
usbtest: fix the POSIX stdout test's escapes
HiFiPhile Oct 17, 2026
1b8832a
make-release: bump the release files as UTF-8 with LF, reconfigure st…
HiFiPhile Oct 17, 2026
20c298f
update-sponsor: reconfigure stdio only on Windows
HiFiPhile Oct 17, 2026
946230c
build-doc: sort boards by name on every host, test the exit status as…
HiFiPhile Oct 17, 2026
d9c1f71
update-sponsor: keep the codespell probe's typo out of the codespell …
HiFiPhile Oct 17, 2026
e6af283
build: count a dependency on another drive as outside in the coverage…
HiFiPhile Oct 17, 2026
ab1c368
skills: test the UTF-8 reads under a legacy locale, run them on updat…
hathach Oct 18, 2026
40f4a50
hil-contract: skip relative PATH dirs for rsync.exe, keep the exit wh…
hathach Oct 18, 2026
f9125b9
build: pin the child's output to UTF-8, keep the audited checkout's f…
hathach Oct 18, 2026
c385945
family_json: test the Windows lock and portable rule on every host, n…
hathach Oct 18, 2026
51dd1f1
hil-contract: never rewrite a killed run's record through a reused pi…
hathach Oct 18, 2026
4a3cc84
skills: tidy the Windows test fixtures and the usbtest refusal paragraph
hathach Oct 18, 2026
69132b4
Merge remote-tracking branch 'origin/master' into windows-skill-scripts
hathach Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .claude/skills/build-doc/scripts/gen_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def metadata(path):
"""key: value pairs of the /* metadata: ... */ block, {} when the file or block is missing."""
if not path.exists():
return {}
m = re.search(r'/\*\s*metadata:(.*?)\*/', path.read_text(), re.DOTALL)
m = re.search(r'/\*\s*metadata:(.*?)\*/', path.read_text(encoding='utf-8'), re.DOTALL)
if not m:
return {}
return {k: v.strip() for k, v in re.findall(r'^[ \t]*(\w+):[ \t]*(.+)$', m.group(1), re.MULTILINE)}
Expand All @@ -30,7 +30,8 @@ def metadata(path):
def boards(bsp):
"""{manufacturer: {board: [name, family, url, note]}} plus the paths skipped for lack of metadata."""
vendors, skipped = {}, []
for family_dir in sorted(p for p in bsp.iterdir() if p.is_dir()):
# by name: Windows paths compare case-insensitively, which would reorder boards.rst
for family_dir in sorted((p for p in bsp.iterdir() if p.is_dir()), key=lambda p: p.name):
family_c = family_dir / 'family.c'
if not family_c.exists():
family_c = family_dir / 'boards' / 'family.c'
Expand All @@ -39,7 +40,7 @@ def boards(bsp):
skipped.append(family_dir)
continue
manufacturer = f_meta.get('manufacturer', '')
for board_dir in sorted(p for p in (family_dir / 'boards').iterdir() if p.is_dir()):
for board_dir in sorted((p for p in (family_dir / 'boards').iterdir() if p.is_dir()), key=lambda p: p.name):
b_meta = metadata(board_dir / 'board.h')
if not b_meta:
skipped.append(board_dir)
Expand Down Expand Up @@ -107,7 +108,7 @@ def hil_rows(entries):
def hil_md(root):
out = [HEADER, '']
for title, src in (('ci rig', 'test/hil/tinyusb.json'), ('hfp rig', 'test/hil/hfp.json')):
entries = json.loads((root / src).read_text()).get('boards', [])
entries = json.loads((root / src).read_text(encoding='utf-8')).get('boards', [])
if not entries:
continue
out += [f'### {title}', '', f'{len(entries)} boards, from `{src}`.', '',
Expand All @@ -124,14 +125,14 @@ def main():
gen_deps_doc(root / 'docs/reference/dependencies.rst')
print(f'wrote {root / "docs/reference/dependencies.rst"}')
vendors, skipped = boards(root / 'hw' / 'bsp')
(root / 'docs/reference/boards.rst').write_text(boards_rst(vendors))
(root / 'docs/reference/boards.rst').write_text(boards_rst(vendors), encoding='utf-8')
print(f'wrote {root / "docs/reference/boards.rst"}')
(root / 'docs/reference/hil_boards.md').write_text(hil_md(root))
(root / 'docs/reference/hil_boards.md').write_text(hil_md(root), encoding='utf-8')
print(f'wrote {root / "docs/reference/hil_boards.md"}')
if skipped:
print('no metadata block, left out of boards.rst:', file=sys.stderr)
for path in skipped:
print(f' {path.relative_to(root)}', file=sys.stderr)
print(f' {path.relative_to(root).as_posix()}', file=sys.stderr)
return 0


Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/build/scripts/check_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def source_lines(entry):
configuration does not satisfy is an empty translation unit, whatever built."""
argv = family_json.preprocess_argv(entry) + ['-E', entry['file']]
try:
r = subprocess.run(argv, capture_output=True, text=True, cwd=entry['directory'])
r = subprocess.run(argv, capture_output=True, encoding='utf-8', errors='replace', cwd=entry['directory'])
except OSError:
return None
if r.returncode != 0:
Expand Down Expand Up @@ -659,7 +659,7 @@ def stale_options(build_dir, supplied):
def roster_variants(boards, config):
"""(board, build name, defines, cflags) for each variant hil_test.py runs of each board."""
try:
roster = {b['name']: b for b in json.loads(Path(config).read_text())['boards']}
roster = {b['name']: b for b in json.loads(Path(config).read_text(encoding='utf-8'))['boards']}
except (OSError, ValueError, KeyError, TypeError) as e:
fail(f'could not read the HIL roster {config}: {e}')
unknown = [b for b in boards if b not in roster]
Expand Down Expand Up @@ -743,7 +743,7 @@ def run(cmd, verbose):
# streamed as it arrives: a variant build runs for minutes, and a silent buffer reads as a
# stall. PYTHONUNBUFFERED, or tools/build.py block-buffers its piped stdout until it exits
out = []
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=ROOT,
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', errors='replace', cwd=ROOT,
Comment thread
hathach marked this conversation as resolved.
env={**os.environ, 'PYTHONUNBUFFERED': '1'}) as p:
for line in p.stdout:
out.append(line)
Expand Down
28 changes: 22 additions & 6 deletions .claude/skills/build/scripts/coverage_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class or port driver whose CFG_* guard compiled its body away) contributes nothi
from pathlib import Path

ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(ROOT / 'tools'))
import family_json # noqa: E402 split_command: a compile command as its compiler's argv
Comment thread
hathach marked this conversation as resolved.
Outdated
DEPS_HEADER = re.compile(r'^(\S.*): #deps \d+')
ROLES = ('device', 'host', 'dual', 'typec')
FORMAT = 3
Expand All @@ -46,11 +48,20 @@ class ExtractError(Exception):
pass


# Windows: the paths rel() found on another drive than the checkout, outside it as a '..' is on
# POSIX; graph counts them per board on stderr, so a checkout split across drives shows up
other_drive = set()


def rel(path, root, base=None):
"""Repo-relative path of `path` (relative ones resolved against `base`, default `root`),
or None for a file outside the checkout (toolchain headers) or a generated one."""
p = os.path.normpath(path if os.path.isabs(path) else os.path.join(base or root, path))
r = os.path.relpath(p, root)
try:
r = Path(os.path.relpath(p, root)).as_posix()
except ValueError: # Windows: another drive (a toolchain header) is outside
other_drive.add(p)
return None
return None if r.startswith('..') or r.startswith('cmake-build') else r


Expand Down Expand Up @@ -219,14 +230,15 @@ def is_live(obj_path, compiler, cwd):
if '__gnu_lto_slim' not in r.stdout:
return defines_symbols(r.stdout)
try:
n = run([compiler + '-nm', obj_path], cwd)
nm = compiler[:-4] + '-nm.exe' if compiler.lower().endswith('.exe') else compiler + '-nm'
n = run([nm, obj_path], cwd)
except OSError:
return True
return n.returncode != 0 or nm_defines_symbols(n.stdout)


def compiler_of(entry):
return entry['arguments'][0] if 'arguments' in entry else entry['command'].split()[0]
return entry['arguments'][0] if 'arguments' in entry else family_json.split_command(entry['command'])[0]


def extract(build_dir, root, example=None):
Expand All @@ -235,11 +247,11 @@ def extract(build_dir, root, example=None):
its CMake inputs are its own too. `nodeps` lists live objects ninja recorded no deps
for: only their source is known."""
src_of = {}
with open(os.path.join(build_dir, 'compile_commands.json')) as fh:
with open(os.path.join(build_dir, 'compile_commands.json'), encoding='utf-8') as fh:
cc = json.load(fh)
for e in cc:
out = e['output'] if os.path.isabs(e['output']) else os.path.join(e['directory'], e['output'])
src_of[os.path.relpath(out, build_dir)] = e
src_of[Path(os.path.relpath(out, build_dir)).as_posix()] = e # as ninja names it
targets = run_ok(['ninja', '-C', build_dir, '-t', 'targets', 'all'], root)
known = {line.split(':', 1)[0] for line in targets.splitlines()}
deps = parse_ninja_deps(run_ok(['ninja', '-C', build_dir, '-t', 'deps'], root))
Expand Down Expand Up @@ -272,7 +284,7 @@ def dep_files(obj):
s.add(f)
continue
a = os.path.normpath(os.path.join(build_dir, d))
for t in (d, os.path.relpath(a, build_dir), a):
for t in (d, Path(os.path.relpath(a, build_dir)).as_posix(), a):
Comment thread
HiFiPhile marked this conversation as resolved.
Outdated
if t in known:
s |= generated_inputs(t)
break
Expand Down Expand Up @@ -365,6 +377,7 @@ def cmd_graph(a):
name = f'audit-{os.getpid()}-{board}'
build_dir = os.path.join(root, 'cmake-build', f'cmake-build-{name}')
shutil.rmtree(build_dir, ignore_errors=True)
other_drive.clear()
try:
r = run([sys.executable, 'tools/build.py', '-b', board, '--build-name', name], root)
rec = {'format': FORMAT, 'head': head, 'board': board, 'config': 'default',
Expand All @@ -378,6 +391,9 @@ def cmd_graph(a):
finally:
shutil.rmtree(build_dir, ignore_errors=True)
rec['secs'] = round(time.monotonic() - t0, 1)
if other_drive:
print(f'{board}: {len(other_drive)} path(s) on another drive than the checkout counted as outside it, '
f'e.g. {min(other_drive)}', file=sys.stderr)
with open(path, 'w') as fh:
json.dump(rec, fh)
print(f'{board}: {rec["status"]} {rec["secs"]}s '
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/hil-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ python3 test/hil/hil_test.py -b stm32f723disco "$CONFIG"

### Remote (dev PC → ci.lan only)

`scripts/hil_remote.py` takes `hil_test.py`'s own arguments, minus the config. It stages the harness, the config and the firmware the run will read under `-B` (default `cmake-build`, the `build` skill's `--shared` layout), runs `hil_test.py` on `ci.lan` with `tinyusb.json`, and copies the report pair and `<config>.failed` back to the checkout root:
`scripts/hil_remote.py` takes `hil_test.py`'s own arguments, minus the config. It stages the harness, the config and the firmware the run will read under `-B` (default `cmake-build`, the `build` skill's `--shared` layout), runs `hil_test.py` on `ci.lan` with `tinyusb.json`, and copies the report pair and `<config>.failed` back to the checkout root. On a Windows dev PC it needs an MSYS2 or Cygwin `rsync.exe` on PATH and refuses without one; that rsync must find its own distribution's `ssh` first on PATH, since with Windows OpenSSH (`System32\OpenSSH`) the argument conversion and the pipes rsync relies on break. The Windows path is not yet proved end to end on a rig:
Comment thread
hathach marked this conversation as resolved.
Outdated

```bash
# All boards built under cmake-build/:
Expand Down
Loading
Loading