Skip to content

feat(cli): add ebuild doctor — one-command environment diagnosis - #72

Merged
srpatcha merged 3 commits into
masterfrom
feat/ebuild-doctor
Sep 1, 2026
Merged

srpatcha merged 3 commits into
masterfrom
feat/ebuild-doctor

Conversation

@srpatcha

Copy link
Copy Markdown
Member

The MLP list asks for "one-command environment diagnosis", and the absence shows: the same class of problem currently surfaces three different ways.

symptom the developer sees actual cause
compiler-not-found partway through a build no cross toolchain
eos/hal.h: No such file repo cache never fetched
footprint report silently absent no binutils

None of the three names the fix.

$ ebuild doctor
  OK    python             3.12.14
  OK    ninja              1.13.2 (/usr/bin/ninja)
  OK    host compiler      15.2.0 (/usr/bin/cc)
  OK    arm-none-eabi      14.2 (/usr/bin/arm-none-eabi-gcc)
  warn  xtensa-esp32-elf   not installed — no esp32 builds
  OK    eos repo           ~/.ebuild/repos/eos (master)

No problems. The host build path is ready.

Optional, for other targets:
  - install the xtensa-esp32-elf toolchain to target esp32

Three decisions worth reviewing

Read-only. It reports; it does not repair. ebuild setup fetches the repos, and installing a toolchain belongs to the developer's package manager — guessing which one they use is how a diagnostic tool starts doing damage.

Exit code is non-zero only for things that actually stop a build. A host-only machine legitimately has no cross toolchain. A doctor that always exits 1 stops being consulted, which costs more than it saves. Missing ninja → 1. Missing arm-none-eabi-gcc → 0.

A missing toolchain reports the boards it would have unlocked, not just its own absence. arm-none-eabi-gcc not found is a fact; "no stm32f4, stm32h7, nrf52, rp2040 or tms570 builds" is the consequence the developer is deciding about.

--json emits the same checks for CI and agrees with the text form on the exit code.

Verification

pytest: 255 passed, 19 new here. Tests cover the exit-code contract in both directions, the report's problem/advisory split, column alignment, and both output modes agreeing.

Branches off #71 (footprint), which branches off #70 (the master repair) — master cannot currently run its own suite.

🤖 Generated with Claude Code

Comment thread ebuild/system/doctor.py
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
Comment thread tests/unit/test_doctor.py

import json

import pytest
Comment thread tests/unit/test_doctor.py
Comment on lines +24 to +34
from ebuild.system.doctor import (
MISSING,
OK,
WARN,
Check,
exit_code,
format_report,
host_checks,
run_all,
toolchain_checks,
)
"""

import subprocess
from pathlib import Path
Comment thread ebuild/system/doctor.py
"--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=15)
branch = proc.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
srpatcha and others added 2 commits August 29, 2026 15:42
The MLP developer walk in the platform design document ends with a build that
says how much of the board it used:

    Flash: 384 KB
    RAM:    72 KB
    Ready to flash.

Nothing produced those numbers. A developer had to run `size` themselves and
remember which columns to add, which is not being told — it is being left to
find out.

    $ ebuild build
    [ok] Build completed successfully.

      Flash:     1.9 KB  of    1.00 MB  (0.2%)
      RAM  :   300.6 KB  of   192.0 KB  (156.6%)
    [warn] RAM usage 300.6 KB exceeds the board's 192.0 KB -- this image
           will not fit.

The accounting matches scripts/measure_footprint.py in the eos repo so the two
tools cannot disagree about what a number means:

    flash = text + data
    ram   = data + bss

`data` is charged to both because it is stored in flash and copied to RAM at
startup; reading `size`'s "dec" column instead understates RAM.

Capacity comes from the project's own board.yaml `memory.flash_size` /
`ram_size` when it ships one — the convention the descriptions under
hardware/board/ already use — and otherwise from a table of the reference part
for each board family. Boards that boot from removable storage, and
Linux-class parts with no fixed budget, are deliberately absent: a percentage
against a guessed ceiling reads as authoritative, so those report absolute
sizes only.

An over-budget image is a warning, not a build failure. It linked; it will not
fit. The developer needs to hear that now rather than from a board that will
not boot.

A cross build is measured with its own `size` and never falls back to the host
one — host `size` on an ARM ELF reports numbers for a different target and
nothing in the output would say so. Where no suitable tool exists the report
is skipped rather than guessed, and never fails the build.

Also fixes .gitignore, found while committing this: line 8 was a bare `build/`
from the Python-packaging block, which also matched `ebuild/build/` — the
source package holding dispatch.py, ninja_backend.py and toolchain.py. Those
four files predate the rule and stayed tracked, but every new module added
there was silently ignored, including this one. Anchored to `/build/`, which
keeps the setuptools artifact directory ignored and stops the pattern reaching
nested source.

235 tests pass, up from 202; 32 are new here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The MLP list asks for it, and the absence shows: the same class of problem
currently surfaces three different ways. A missing cross toolchain appears as
a compiler-not-found partway through a build. A missing repo cache appears as
`eos/hal.h: No such file`. A missing binutils silently drops the footprint
report. None of the three names the fix.

    $ ebuild doctor
      OK    python             3.12.14
      OK    ninja              1.13.2 (/usr/bin/ninja)
      OK    host compiler      15.2.0 (/usr/bin/cc)
      OK    arm-none-eabi      14.2 (/usr/bin/arm-none-eabi-gcc)
      warn  xtensa-esp32-elf   not installed — no esp32 builds
      OK    eos repo           ~/.ebuild/repos/eos (master)

    No problems. The host build path is ready.

    Optional, for other targets:
      - install the xtensa-esp32-elf toolchain to target esp32

Three decisions worth naming.

Read-only. It reports; it does not repair. `ebuild setup` fetches the repos,
and installing a toolchain belongs to the developer's package manager —
guessing which one they use is how a diagnostic tool starts doing damage.

The exit code is non-zero only for things that actually stop a build. A
host-only machine legitimately has no cross toolchain, and a doctor that
always exits 1 stops being consulted, which costs more than it saves.

A missing cross toolchain reports the boards it would have unlocked rather
than just its own absence. "arm-none-eabi-gcc not found" is a fact; "no
stm32f4, stm32h7, nrf52, rp2040 or tms570 builds" is the consequence the
developer is actually deciding about.

`--json` emits the same checks for CI, and agrees with the text form on the
exit code.

254 tests pass, up from 235; 19 are new here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread ebuild/cli/commands.py
from __future__ import annotations

import glob
import glob
Comment thread ebuild/cli/commands.py
import glob
import os
import re
import re
@srpatcha
srpatcha merged commit 605876e into master Sep 1, 2026
27 of 36 checks passed
@srpatcha
srpatcha deleted the feat/ebuild-doctor branch September 1, 2026 09:57
srpatcha pushed a commit that referenced this pull request Sep 1, 2026
`import ebuild.cli.commands` raises NameError on master, so the `ebuild`
command does not start and pytest cannot collect nine test modules --
the suite reports "9 errors during collection" and runs zero tests.

Four PRs landed their tests while their implementation was discarded
during conflict resolution, and the merges that did it were not gated by
a run: f5209ac, current master, has no CI runs at all.

  #64 (6a22e36)  tests/unit/test_golden_path_commands.py landed;
                 _workspace_repo_paths, _cached_repo_libraries,
                 _record_board_selection, _EOS_PROJECT_CONFIG and the
                 `configure --board` option did not.
  #89 (32348f0)  commands.py gained five `re.` uses, no `import re`.
  #90 (d3958f7)  the initramfs test kept its assertions but lost its
                 `_newc_members` helper and its `os` / `stat` imports.
  #92            registry.version_key was renamed version_sort_key
                 without updating its caller in the tests.

Restoring them, taken verbatim from the branches that were merged:

* commands.py: `import re`; `_workspace_repo_paths`,
  `_cached_repo_libraries`, `_record_board_selection`,
  `_EOS_PROJECT_CONFIG`; the `--board` option on `configure` and the
  call that persists it; and the `{**_workspace_repo_paths(), **...}`
  merge at both `_install_packages` call sites.

  Without that last hunk a scaffolded project that `use`s eos or eboot
  gets no include or link paths from ~/.ebuild/repos, which is the
  "fatal error: eos/hal.h: No such file or directory" that #64 fixed.
  Without the option, the documented golden path -- `configure --board`
  then a bare `build` -- fails with "no such option: --board".

* the initramfs test: `os` / `stat` imports and the `_newc_members`
  newc parser the assertions call.

* test_package_registry.py: import the name the module now exports.

Collection goes from 9 errors / 0 tests to 446 passing. The 12 that
still fail are pre-existing and out of scope here: `doctor` (#72) and
`package` (#77) are two more features whose implementations the same
merges dropped and which should be restored by their authors, and the
rest are separate bugs -- see the PR description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
srpatcha pushed a commit that referenced this pull request Sep 1, 2026
…green

The six remaining failures were all the same shape as the ones already
fixed: work that landed as tests while its implementation was discarded
during conflict resolution, or two implementations of one thing left side
by side with the wrong one wired in.

* backend precedence (#96). The 22 lines that stop detect_backend()
  outranking a build.yaml that declares targets were gone; the test file
  survived. A Makefile kept for `make flash` still won, the dispatcher ran
  the external tool, nothing declared was built, and the build reported
  success. Restored from bba6f75.

* dispatch errors. Three mechanisms sat on top of each other:
  BackendError + _validate_backend, and UnknownBackendError + a
  _unknown_backend() -- twice, the class and the function each defined
  two ways, the later shadowing the earlier. _validate_backend ran first
  and preempted the per-step error with a wrong type and a message that
  contradicted itself by listing ninja as supported while rejecting it.
  Consolidated on the surviving pair; BackendError stays as an alias so
  callers that catch it keep working. The guard moved ahead of the mkdir
  it used to precede, so rejecting a backend no longer leaves a build
  directory behind.

* registry ordering. registry.py carried both version_sort_key and a
  private _version_sort_key, and every caller used the private one, which
  ranks any non-numeric version below every numeric one -- so v2.9.3,
  littlefs's own tag format, sorted below 1.10.0 and get() picked the
  wrong latest. Dropped the duplicate; the callers now use the tested
  public key. One assertion in tests/ebuild encoded the private key's
  ordering and is updated, which is the one judgement call here.

* system_config. Parsed and validated in load_config(), then never passed
  to ProjectConfig, so the whole [system] section was inert.

* footprint on macOS. Apple size(1) prints "__TEXT __DATA __OBJC others",
  and _SIZE_LINE matched that row too -- reading __OBJC, always 0, as bss.
  Every footprint measured on a Mac silently reported RAM containing no
  zero-initialised data. Falls back to `size -m` and sums sections, so
  bss is the real 4096 rather than 0.

* doctor (#72) and package (#77). Both commands were absent from the
  group; ebuild/system/doctor.py and ebuild/build/firmware_image.py were
  already on master, only the CLI registration was lost. Restoring
  `package` surfaced a genuine collision: integration.py defines another
  `package`, and register_commands() runs last, so it had been silently
  replacing the .efw one. Renamed that to `package-deliverable` -- see the
  PR description, this one wants a maintainer's eye.

Also adds a missing `import glob`: _serial_ports() raised NameError on
every call, the same defect as the missing `import re`.

525 passed, 0 failed. mypy goes from 23 errors to 15; ruff is unchanged.
Verified end to end: `ebuild doctor` reports the environment, and
`ebuild build` links a running Mach-O executable and prints a footprint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant