v0.4.0-rc1
Pre-releaseAdded
- The JSON envelope vocabulary alp-sdk-vscode gates on is now a frozen,
tested, published contract (#106). The extension matches four issue codes
with===and reads a dozendatafield names behind?? []fallbacks, and
every one of those matches fails open — rename any and the extension does
not error, does not log and does not warn, it silently skips the check or
renders stale data, CI green on both sides. The headline case: rename
data.somsand the New Project wizard falls back to a static catalogue that
carries nocores, so a heterogeneous SoM scaffolds single-core with no IPC.
The reference part E1M-AEN801 is multi-core, so that is the default path, not
an edge case.contract/issue-codes.jsonis the single source for the frozen codes
(bootstrap.windows-unsupported— retired but RESERVED,
bootstrap.yocto-host,bootstrap.prerequisites-missing,
presets.sdk-root-unresolved), gated byfrozen_issue_codesin
crates/tan-cli/tests/contract.rs. The consumer is deliberately NOT
loosened to prefix matching: a prefix match onbootstrap.would swallow
codes it has no verdict for.- Four new golden envelopes extend the existing
contract/envelopes/suite
(12 → 18 tests):presets-no-sdk,presets-heterogeneous-som(ana55
yocto +m33zephyr fixture SoM — the worked example above, made
executable),explain-overview, andexamples-catalog. A case fixture can
now be a directory tree, so a case can carry a syntheticsdk/checkout
and pass--sdk-root ./sdk. doctor --build'sdatakeys get a key-set assertion rather than a golden
(its values are host facts):summary.{pass,warn,fail},nextSteps,
checks[].{name,status}, and the literal check nameworkspace.- Tagged releases now publish
envelope-contract.jsonbeside the
binaries — the frozen codes plus one golden envelope per command family —
so the extension's contract test can diff against a published artefact
instead of a hand-copied fixture. - Two consumer fields stay UNCOVERED and are documented as such in
contract/README.mdrather than quietly omitted:build --materialise's
data.written(needs a resolvable SDK + a Python spawn) andsdk list's
data.releases(network).
tan sdk listcarries GitHub'sdraft/prereleaseflags through (#122).
Both booleans were already in the Releases API responsetanparses but were
dropped before reaching either the JSON envelope or the text table — a
consumer asking "what is the latest SDK?" could not tell a release candidate
apart from a genuine release, with no error and no log line.SdkRelease
now carriesdraft/prerelease(defaultfalsewhen GitHub omits or
misencodes either key, never a reason to drop the release), and the
tan sdk listtable marks a flagged entry with[draft]/[prerelease].
tan does not filter on either flag or add a--include-prereleasesswitch —
the consumer decides what "latest" means; tan's job is only to publish the
fact it already has instead of destroying it. One caveat:fetch_releases
sends noAuthorizationheader, and GitHub returnsdraft: trueentries
only to a caller with push access, so against the publicalp-sdkrepo
[draft]never renders today — it activates the moment a token is added.tan doctor --buildchecksgit,python,dtcandgperf, and every
check can now carry a resolvedversion(#120, #123). Four of the six
host tools a build needs were previously invisible todata.checks[];git
andpythonare checked unconditionally (every backend's build-plan
emission runsalp_project.pythrough both, not just Zephyr's),dtcand
gperfare gated on the Zephyr entry indata.osSet(Yocto/baremetal-only
projects never see them) and — matching the retiredalp doctor's own
_check_dtc/_check_gperf— staywarnrather thanfail.python
reports a version FLOOR, not bare presence: an interpreter below the
manifest'spythonMinVersionfails with its own detail, distinct from "not
found".- Each
data.checks[]entry gains an optionalversion— absent, never
null, when unresolved or not meaningful (zephyrSdk,vendorToolchain)
— reporting whatever tan itself resolved rather than leaving a consumer to
re-probe PATH and risk a second, disagreeing answer.westResolved's
version comes from the SAME workspace-venv resolver its status does,
kept independent ofwest's bare-PATH version so the two rows can never
be attributed to the wrong resolver. missingPrerequisites[].toolfor python is now host-correct —python3
on a served POSIX host,pythonon Windows — matchingprerequisites. install.linux/.macos's own key andtan bootstrap'sposix_refusal
naming for the identical missing tool, instead of alwayspython, which
a POSIX consumer could not re-key back into that same install map.
- Each
- Vendor
board-diagnosticsandiot-starterfrom the SDK scaffold catalog
(#14). Closes out the last two vendorable entries from alp-sdk#864's
scaffold catalog (added by alp-sdk#903):board-diagnosticsnow emits the
SDK's real board self-test app (SoM/SoC identity, RUN operating-point
profile, on-module I2C management-bus scan) for both
E1M-AEN801/E1M-V2N101, andiot-starteremits the SDK's real Wi-Fi +
mqtts://MQTT/TLS telemetry app on the CC3501E bridge —E1M-AEN801only,
matching the SDK catalog's AEN-only + preview status.iot-starternarrows--somtoE1M-AEN801: any other SKU is rejected
withinit.invalid-sombefore a single file is planned, never a silent
fall-back onto the retired hand-written generator.
- The JSON envelope now names which alp-sdk root a command actually
resolved (#110). A new optional top-levelsdk: { root, sourceTier }key
reports the exact path + precedence tier (sdkRootFlag/projectPin/
globalDefault/discovery) the command used — so a consumer (the vscode
extension) can finally tell which SDK produced a result instead of guessing,
especially on the unpinned/first-run path where discovery now walks up to
an enclosing checkout (#101).- Populated from a value RECORDED at the moment one of
tan's three
resolvers actually resolved something, never from a second, fresh
resolution — the three resolvers have different candidate sets, so
re-resolving to fill the envelope could report a path the command never
actually used. - Absent entirely (not
null) when nothing resolved, keeping every
existing contract golden byte-identical.
- Populated from a value RECORDED at the moment one of
tan renode --sim-modeserves the studio hardware-simulator socket contract
(#77, socket half). The flag existed for CLI-surface stability but errored
"not yet ported", so studio had nothing to connect to. It now boots the
--image-bundle's firmware in headless Renode and exposes the two sockets the
gateway needs. The contract was ported from the RETIRED Python
(west alp-renode --sim-mode, deleted inalp-sdk@df312cecunder ADR-0020
Phase 4), not re-derived from issue prose — the prose omits four things the
implementation carries, and all four are honoured: theERR <reason>reply,
theready (timeout <N>s).readiness marker, LOWERCASE0xnnreply hex, and
the SecureSCB->VTOR(0xE000ED08) write the generated boot script needs on
ARMv8-M + TrustZone, whereLoadELFdoes not seed it and the core otherwise
HardFault-storms from address 0.- Both listeners are bound on ephemeral
127.0.0.1ports before the
descriptor names them, so a client that readssim-descriptor.jsonand
connects at once can never race into anECONNREFUSED. They are bound on
port 0 and their assigned ports read back rather than picked-then-rebound,
which removes the Python's bind-then-close TOCTOU window outright. <bundle>/sim-descriptor.jsoncarries exactly the schema's four keys —
control_socket,uart_socket(tcp://127.0.0.1:<port>URIs),
framebuffers,peripherals.- The control socket is line-oriented, one request → one reply, with three
verbs:sysbus ReadBytes <base> <count>(reply normalised from Renode's
bracketed UPPER-case list tocountspace-separated lowercase0xnn
tokens, scoped to the brackets so an echoed command address cannot leak in
as a phantom byte, and a short read is an error never a padded answer);
sysbus WriteBytes <base> <hex…>(expanded to per-bytesysbus WriteByte <base+i>, because Renode's ownWriteBytestakes(bytes, addr)— the
reverse of studio's order); and any other line forwarded verbatim. A
malformed line or monitor fault answersERR <reason>and keeps the
connection, and every reply is flattened to a single line so a client can
never desynchronise. - The
datapayload recordsdescriptor,controlPortanduartPort, so a
JSON consumer reads the descriptor's path and the two ports out of the
envelope instead of assuming<bundle>/sim-descriptor.jsonand parsing them
back out of the file it first has to find. - A CPU that halts on its first instruction fetch fails the run with an
renode.cpu-haltederror issue and exit 1, matching the plain smoke's latch
(issue #64). In sim mode the monitor owns Renode's stdout, so the halt is
latched in the pump thread — a halt landing between two client commands
belongs to no command's collection window, and previously would have
resurfaced at best as anERRon whichever command came next whiletan
still exited 0. Sim mode is exactly where a mis-seeded VTOR shows up this way. - Teardown sends the monitor's
quit, polls up to 1 s for Renode to act on it,
and only then kills — the Python'sterminate()+wait(10)+killon a
shorter budget. Killing immediately after the flush gave the emulation no
time to close its sockets or flush its log. - DEFERRED to a follow-up on the same issue, which stays OPEN: the
ram_console_bufRAM-ring → UART-socket streamer, the wired-UART console
path (Renode's own socket terminal), and the per-SKU sim profiles that fill
framebuffers/peripherals— empty for now. The UART socket accepts and
holds connections while streaming nothing, exactly as the Python did for an
image carrying noram_console_bufsymbol, so studio's serial view connects
and stays empty rather than failing to connect.--expectis reported as
ignored in sim mode rather than silently dropped: the console goes to the
socket, so there is no console text to scan. - That deferral is never silent. Every sim run carries a
renode.sim-profile-deferredwarning issue (and prints it in text mode)
stating thatframebuffers/peripheralsare both empty and that the UART
socket streams nothing, so an empty descriptor cannot read as a successful
one — the Python REFUSED a SKU with no profile outright, andtankeeps
exit 0 only because the control socket genuinely works without one. For
E1M-AEN801— the first-target SKU, whose Python console was a WIRED
hardware UART served by Renode's own socket terminal — the warning names that
deferred path as a second, independent reason its UART is silent.
- Both listeners are bound on ephemeral
- Shell completions are gated against clap (#92). The three scripts under
completion_scripts/are hand-maintained and nothing compared them to the
#[arg(long)]definitions, so flags drifted silently —--corewas missing
from all three since #66 and was caught only by a human reading the diff. A
test now walks clap's BUILT command tree and asserts, per subcommand and in
both directions, that every long flag appears in the arm each script actually
runs (and that no script offers a flag clap no longer accepts). Per-arm, not
file-wide: zsh's_argumentsarms do not inherit, so a global flag must be
repeated in each one, and a whole-file "appears somewhere" check reported
parity whiletan sdk --format<TAB>completed nothing. Fixing the drift the
gate then exposed makes several flags newly completable across all three
shells.completion_scripts/**is nowtext eol=lfin.gitattributes: a
CRLF checkout both breaks the scripts on their target shell and makes the
gate's layout markers miss, which would have surfaced as a misleading
"layout changed" panic on thewindows-latestleg only. tan doctorchecks the host environment:zephyrSdkHost,longPaths,
homePath. alp-sdk ADR 0021's cross-cutting requirements name three host
facts that decide whether a toolchain can be provisioned at all, and all
three previously surfaced only as a confusing failure much later.zephyrSdkHost—Fail. The pinned Zephyr SDK publishes host builds
forlinux-aarch64,linux-x86_64,macos-aarch64andwindows-x86_64
and nothing else (verified againstzephyrproject-rtos/sdk-ngv1.0.1,
which is what alp-sdkwest.yml'szephyr: v4.4.1pin requires via that
tree'sSDK_VERSIONfile). Two hosts are therefore unserved and they are
not the same case.windows-arm64has never been published, and the
ADR's remedy applies: route to WSL2, where the distro islinux-aarch64,
which is served.macos-x86_64— an Intel Mac — was published through
0.17.4and dropped in1.0.0, so it is equally unserved at the pin,
with no WSL2 equivalent and nomacos-aarch64substitute (Rosetta
translates x86_64 for Apple silicon, not the reverse); its remedy is a
Linux host, and it says so instead of repeating the WSL2 advice, which on
macOS cannot be followed.Fail, notWarn, because there is no artifact
to install — the same category as a missingninja, which
hostPrerequisitesalready fails on. Apple silicon and every other served
host pass. The arch compared is the machine's, resolved at runtime
(IsWow64Process2on Windows,sysctl.proc_translatedon macOS), not
std::env::consts::ARCH, which is the arch tan was compiled for. tan
shipsx86_64-pc-windows-msvcandx86_64-apple-darwinas their own
release assets and both run on aarch64 hardware — the first because
Windows-on-ARM emulates x64 transparently (making it the likeliest way tan
runs there at all, which would have left thewindows-arm64arm almost
unreachable), the second under Rosetta (which would have failed a fully
served Mac and sent its owner after a Linux box). Linux stays on the
constant: no Linux asset tan ships can differ from its host.longPaths—Warn, Windows only. Reads
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabledthrough
the registry API (notreg query: that costs a process and depends on the
PATHof a host that is by definition suspect). An absent value counts as
disabled, because absent is the Windows default; only a read that
genuinely failed reports "unknown", and that is aWarntoo rather than a
blindPass.Warnand notFailbecause Windows 11 still ships the flag
off, so failing would exit 4 on essentially every stock Windows host —
including the many that build fine from a short workspace root. It is a
probable cause, and its value is attribution: the failure it predicts
arrives as a CMake or compiler error about a file that plainly exists. The
fix is the elevatedNew-ItemPropertyone-liner, printed rather than run —
ADR 0021's Tier A promises zero elevation, so anHKLMwrite belongs to
the undecided Tier B consent flow.homePath—Warn, all platforms. Reports the actual resolved path
when it contains a space (USERPROFILEon Windows, elseHOME; the same
resolution~/.alpuses).Warn, notFail: it is a real historical
Zephyr breakage but a degraded-but-usable one, and a host whose account is
two words is not in the same category as one that cannot run the toolchain
at all. An unresolvable home is also aWarnrather than a silent pass.
On the PLAIN report only, never--build— these are host facts needing no
board.yaml, no workspace and no SDK, exactly ashostPrerequisitesbelow,
and ADR 0021 Lane 1 P0a runstan doctorbefore anything project-shaped
exists.zephyrSdkHostlooks adjacent to--build's existingzephyrSdk
probe but answers the opposite question — "can an SDK be installed on this
host at all" versus "is one installed here" — and reporting the SDK story
twice under two names is the trap this changelog documents below. The same
three checks are appended totan support-bundle's doctor payload, for the
same reasonhostPrerequisitesis. Consumer-visible:data.checks[]
grows by two entries (three on Windows),data.summarycounts them, and
zephyrSdkHostcan move plaintan doctorto exit 4 on awindows-arm64
or Intel-Mac host — which compounds the exit-4 change below, and is the
honest verdict for a machine no pinned toolchain serves.
(alp-sdk ADR 0021, tan-cli#70)
sdk.west-config-reconcile-failed(new issue code,tan sdk switch).
.west/config's reconciliation reported every failure — an unreadable
config, a read-only one, one held open by another process (the routine
Windows shape) — identically to "already correct", so the user was told the
switch was clean whilewestkept resolving its manifest from the stale
pointer. The three-way outcome (tan_core::ManifestReconcile) makes the
failure distinguishable, and it now surfaces in text and in the envelope with
the OS's own reason and what to do about it. Severitywarningat exit code
0, matchingclean.remove-failedandbuild.sdk-switch-pristine-failed—
a best-effort repair that failed while the command carried on. The exit code
is deliberate: the switch itself DID happen (the active-SDK pointer is
written) and failing it would block the escape hatch out of a broken
workspace.tan bootstrapgained the same distinction as a
west-config-reconcile-failedwarning, where it matters most:west update
is about to run against whatever manifest that unrewritten pointer names —
and a bootstrap whose reconcile failed no longer records the workspace as
synced, since that update resolved the OLD SDK's manifest.tan debug-config --pre-launch-task <TASK>. Opt-in re-entry for the
preLaunchTaskthe command used to emit unconditionally (see Changed). The
flag carries the task NAME rather than being a bare on/off switch, for two
reasons: a consumer that has actually registered aTaskProviderwill use
its own name, not one of ours, so a boolean would keep a tan-owned string
baked into a file the consumer owns; and with the name supplied from
outside, the four hardcoded task strings leave the contract entirely instead
of surviving as a default nobody can change. Off by default — nothing is
emitted unless a name is passed.contract/envelopes/pinsdebug-config --preview. Four goldens, one
per--target-kind(debug-config-preview-{zephyr-mcu,baremetal-mcu, yocto-userspace,native-host}). Unlike the seven existing cases these pin a
datavalue that is itself a consumer ARTEFACT rather than a report:
alp-sdk-vscode#342 writesdata.configurationinto the user'slaunch.json
verbatim, so the golden pins the emitted key SET. ThepreLaunchTaskbug
below was reachable only by reading two repos by hand; it would now fail
cargo test.--previewreads noboard.yaml, spawns no Python and probes
no PATH (the reasonbootstrap/doctorhave no golden), so it is
legitimately host-independent; the one absolute path it reflects back
(project.root/launchJsonPath) is tokenized as__WORKDIR__by the
harness.tan doctorreports a missing host prerequisite without--fix.
check_prerequisiteshad no caller outsidetan bootstrap, so the only way
a missingninjasurfaced wastan doctor --build --fix, which runs
bootstrap to find out — and in the extension a missingninjatherefore read
asfailed to launch (exit code: 1)from the bootstrap terminal. Plain
tan doctornow runs bootstrap's own gate (not a second copy of it) and
reports ahostPrerequisitescheck. The CHECK is on the plain report only,
not--build: prerequisites are a HOST fact needing noboard.yaml, no
workspace and no SDK, and alp-sdk ADR 0021's Lane 1 P0a runstan doctor
before the bootstrap terminal exists — while--buildalready probes
ninja/cmakethroughBuildToolProbeand would report them twice. (One
fact, one check — but--builddoes carry the machine-readable
missingPrerequisitesdata derived from those probes; see Changed.) The
check's detail names which tool list it checked against — the SDK's
metadata/bootstrap.jsonor tan's built-in fallback — so a run with no
resolvable SDK still checks the host and says which list it used, rather
than implying it read the SDK's. A manifest that resolved but was REFUSED
(unsupportedschemaVersion, unparseable) is a third case, not the second:
the refusal messagetan bootstraptreats as a fatalValidationFailureis
now carried into the check's detail as
metadata/bootstrap.json rejected: …and downgrades the check toWarn
(a refusal still outranks it asFail).tan doctoris the command a user
runs to find out whytan bootstraprefuses, so it is the last one that may
swallow the reason — it reports it without repeating bootstrap's exit code.
The same check is appended totan support-bundle's doctor payload, for the
reason below. Two caveats worth stating plainly: theninjacase above is
Windows-only, because the tool list is — the manifest's
prerequisites.posixis[git, cmake, python3]and names noninja, while
prerequisites.windowsadds it, an asymmetry the manifest records faithfully
rather than unifying (on Linux/macOS a missingninjastill surfaces only
throughtan doctor --build'sBuildToolProbe, which probes it by name on
every platform); and the gate spawns interpreter subprocesses
(probe_host_python, which is what makes this check a strict superset of the
retiredpythonone), so plaintan doctorandtan support-bundlenow cost
~0.5 s per invocation where they previously did PATH lookups only (measured
516/548/516 ms fortan --format json doctor, debug build, Windows host).
That is the price of the check, not a regression — but the extension may call
plaindoctoron activation, so it is recorded here rather than
misdiagnosed later. (alp-sdk ADR 0021 P0a)tan bootstrapreports its missing prerequisites as structured data.
The envelope's issue message is the message lines joined with a space, and
an install command contains the same spaces the join used — so
Missing required tools: ninja -> winget install -e --id Ninja-build.Ninja Install the tools above …cannot be split back into
<tool>/<command>pairs safely, and alp-sdk-vscode#347 deleted the parse
that tried.data.missingPrerequisitesnow carries
[{tool, command}]alongside the unchanged message:commandis the
winget installone-liner where tan knows one andnullwhere it does not
(an unlisted tool, and every POSIX host until alp-sdk#949 lands
prerequisites.install.posix) — never advice prose, which a consumer would
render as a runnable button that cannot work. The field isnull, not[],
on every run that did not reach the prerequisite gate, so "not reported" is
distinguishable from "reported empty".data.schemaVersionstays"2": the
field is additive and optional, and a consumer that does not know it is
unaffected. The two Python-floor refusals — which have no missing tool at
all, so no{tool, command}pair could carry their fix — now report under
their own codesbootstrap.python-not-runnableand
bootstrap.python-too-oldinstead ofbootstrap.prerequisites-missing;
a consumer matchingbootstrap.prerequisites-missingfor those two cases
must add the new codes.bootstrap.prerequisites-missingitself is
unchanged for the missing-tool case, message text included. (#70)tan buildauto-pristines a slice build dir left stale by an SDK switch.
Switching the active SDK (~/.alp/sdk/v0.11.0→~/.alp/sdk/v0.13.0) left
every previously-configured slice failing with west's rawBuild directory … is for application "…/v0.11.0/firmware/alp-stock-shim" … FATAL ERROR: refusing to proceed without --force, which in the extension surfaced only
asterminated with exit code: 1. Each slice build dir now carries a
.tan-sdk-rootstamp written before the tool spawns; a dir that is
configured but absent-or-differently stamped is wiped and re-configured,
reported asbuild.sdk-switch-pristinenaming both SDK roots. The wipe
skips any slice with an explicit-d/--build-dirand only fires under
the project's ownbuildroot. (#52)tan renode --core <CORE_ID>— boot ONE Zephyr slice of a multicore
project in the headless smoke. A manifest with more than one Zephyr slice (an
E1M-AEN801'sm55_hp+m55_he) was refused outright with "the Renode smoke
boots a single-Zephyr-slice system", leaving no way to smoke-test such a
project at all.--corenarrows the zephyr set before the runnable filter, so
an explicitly named blocked/skipped slice still boots exactly like a lone one
does (the smoke touches no hardware). A name matching no zephyr slice fails
withUnknownCore, listing the manifest's zephyr cores. The refusal message
now names the flag. Unchanged for a single-slice project.
Changed
-
tan sdk switch <version>resolves the bare version against more than one
cache root (#62). It joined~/.alp/sdk-cacheand nothing else, while the
layout that reported #62 keeps its SDKs under~/.alp/sdk(the VS Code
extension's install root) — sotan sdk switch v0.13.0failed with
path-not-foundon a version sitting right there on disk, and the whole
.west/configreconciliation shipped in #74 was unreachable for exactly the
users who needed it. Three roots are tried in a fixed order, first real
checkout wins:--destination(now honoured byswitch, not just
install), then~/.alp/sdk-cache(soinstall X && switch Xselects what
the install just wrote), then the parent directory of the currently active
SDK — no config declares a cache root, so where the active SDK sits is the
only authoritative record of where this machine keeps them. Not a filesystem
search: three named roots, each of which the user can point at. -
sdk.bootstrap-recommendedis derived from workspace state, not from
whether a rewrite fired. It was latched to the.west/configrewrite
happening, so a secondtan sdk switch— pointer already reconciled by the
first,topdir/zephyrandmodules/still the previous SDK's trees — went
silent exactly when the user had not acted on the advice yet. It now fires
whenever the workspace cannot be shown to match the selected SDK: the pointer
must name it AND atan bootstrapwest updatemust have been recorded
against it.tan bootstrapwrites that record (<topdir>/.west/ tan-workspace-sdk) after an update that actually ran; nothing else on disk
answers "which SDK's manifest were these trees checked out from", since
.west/configis rewritten by the reconcile itself without the trees
changing. A workspace bootstrapped before this record existed has none, so
the firstsdk switchafter upgrading advises a bootstrap it may not need —
onetan bootstraprun clears it for good. The message wording follows the
evidence: a diverged pointer proves the workspace belongs to another SDK, a
matching one with no record only means it cannot be confirmed. -
tan debug-configno longer emitspreLaunchTaskby default — it was
naming a task nothing defines. Every generated profile carried one of
alp: build active target,alp: build baremetal target,alp: deploy and start gdbserveroralp: build native_sim target. Notasks.jsonin this
repo or in a generated project defines them, and alp-sdk-vscode contributes
only{"type":"alpRun"}with noTaskProviderregistered for any of the
four. VS Code resolvespreLaunchTaskBEFORE launching, fails to find the
task, and aborts pre-launch — so the session never started, out of a
launch.jsonthat reads perfectly. Consumer-visible payload change:
data.configuration(and the writtenlaunch.json) has one fewer key.
Build-before-debug is still the behaviour we want, which is why the
capability came back as--pre-launch-taskabove rather than being deleted;
it just cannot be the default while nothing provides the task. -
The
doctorenvelope gaineddata.missingPrerequisitesand a
doctor.hostPrerequisitesissue code. The new check (see Added) reports
Failon every prerequisite refusal — each one blocks a build, and bootstrap
itself refuses to run against exactly these — so a host missing a
prerequisite now makes plaintan doctorexit4(doctorFailure) where it
previously passed, and raises adoctor.hostPrerequisiteserror issue. The
structured half rides ondata.missingPrerequisites, deliberately the same
key, the same[{tool, command}]element and the samenull-never-[]rule
as thebootstrapenvelope's field, so one fact does not get two
vocabularies. The code isdoctor.*, not thebootstrap.prerequisites-missing
a consumer may already match: in this CLI an issue code's prefix is the
command that emitted the envelope, without exception, and abootstrap.*code
inside adoctorenvelope would tell a consumer a command ran that did not.
missingPrerequisitesis present on bothdoctorpayloads — the plain
report (including its error envelopes) and--build'sBuildReadinessReport
— always as an explicit key,nullwhen there is no missing TOOL to name.
What differs is where each gets its list from, and that is deliberate:- plain
tan doctorcarries thehostPrerequisitesCHECK and fills the
field from its refusal.nullon a clean host, on an error envelope that
never reached the probe, and on the two Python-floor refusals, whose fix no
{tool, command}pair can carry. tan doctor --buildcarries the field as data only — there is no
hostPrerequisitescheck in that mode. It already probes
west/cmake/ninja/bitbakethroughBuildToolProbeand reports each
as its own check, so mirroring the aggregate check would report the same
tool twice under two names. The field is derived from exactly those
PATH-binary checks, so it inherits their OS gating (a Zephyr-only project is
never told to installbitbake; a non-Linux host getsyoctoHostinstead
of abitbakeentry) and their dedup (cmake, needed by two declared OSes,
appears once). Excluded on purpose:zephyrSdk(env-var detection, its fix
is a docs URL),bmaptool(two tools, one advisory, workingdd
fallback),yoctoHostandvendorToolchain(no tool name at all) — none
has a single{tool, command}pair that could carry it.commandis the
wingetone-liner only on Windows and only for a tool tan knows one for;
westandbitbakereportcommand: nullrather than an invented ID.
This is whatalp-sdk-vscodeneeds: it calls onlytan doctor --build
(src/toolchain.ts:219,:248), and itsrunToolchainFixpreviously had
nothing runnable to put behind a Fix button, so a missingninjareached
the user asfailed to launch (exit code: 1).
--build's payloadschemaVersionstays"1": the field is additive and
optional, and its other keys (generatedAt/osSet/summary/checks/
nextSteps) are unchanged. (alp-sdk ADR 0021 P0a) - plain
-
The retired
doctorpythoncheck. Plaintan doctorno longer emits a
pythoncheck. It probedcontext.python_binary, which in this CLI is always
the barepython3/python— literally the toolhostPrerequisitesnow probes
off the manifest's prerequisite list — so one host fact landed twice under two
names with two severities (WarnvsFail) and two different exit-code
consequences. The retired one was also the weaker probe: nopythonMinVersion
floor, and nopy-launcher widening, so a Windows host with only the launcher
installed got apythonWarnbeside ahostPrerequisitesPassabout the
same interpreter. A consumer matching thepythoncheck name or the
doctor.pythonissue code must move tohostPrerequisites/
doctor.hostPrerequisites, which reports the same fact as aFail.
tan doctor --buildis unaffected (it never had this check). -
tan support-bundle's doctor payload gainedmissingPrerequisitesand the
hostPrerequisitescheck. The bundle (payloadschemaVersion"1") built
itsDoctorReportwithout ever running the prerequisite gate, so it serialized
"missingPrerequisites": null— which that field defines as "checked, nothing
missing" — for a host nobody probed. A bundle is what a user attaches
precisely when bootstrap failed, so it both hid the missingninjaand
asserted the host was fine. It now runs the same gate, which also means a
missing prerequisite makestan support-bundleexit4and emit a
support-bundle.hostPrerequisiteserror issue (the bundle file is still
written). PayloadschemaVersionstays"1": additive and optional. -
nextStepsnow includes the remediation of every appended check.
nextStepswas computed once inside the report builders, beforetan-cli
appends the checks that need IO —hostPrerequisites,sdkProvenance, and
on--buildthe project/workspace preflight and the--fixbootstrap outcome
— so those checks'fixstrings never reached the field the envelope
documents as "deduplicated remediation steps for non-passing checks" and the
extension renders as a Fix button. Appending a check now re-derives the field
as part of the same call (tan_core::append_doctor_check/
prepend_doctor_checks), so there is no trailing recompute statement left for
a caller to forget.nextStepsgains entries and follows check order; on
--buildthe preflight'stan sdk switch <path>/tan initnow lead it. -
tan bootstrap's two Python-floor refusals report under their own issue
codes. A host whosepythondoes not run now raises
bootstrap.python-not-runnable, and one belowpythonMinVersionraises
bootstrap.python-too-old, instead of both sharing
bootstrap.prerequisites-missing— neither names a missing TOOL, so neither
can carry the newdata.missingPrerequisitesentries (see Added).
A consumer matchingbootstrap.prerequisites-missingfor those two cases
must add the new codes; the code is unchanged, message text included, for
the missing-tool case it originally described. (#70) -
Install commands come from the SDK manifest, not tan's
wingettable
(#90).data.missingPrerequisites[].command— the field alp-sdk-vscode's
runToolchainFixputs behind a Fix button — was rendered from a hardcoded
four-entrymatchon tool name, plus two more copies of
Python.Python.3.12embedded in the Windows Python-floor refusal prose. It is
read fromprerequisites.install(alp-sdk#959, ADR 0021 Lane 1 P0b) now, and
the table is deleted rather than kept as a fallback:fallback_facts—
which an SDK withoutmetadata/bootstrap.jsonfalls back to, i.e. every SDK a
customer can install today — carries the same commands, pinned byte-equal to
the vendored manifest by the fallback-vs-manifest field-for-field test, so no
host loses a command and there is no second, ungated copy of a drift-gated
fact. A manifest predating #959 has noinstallkey at all;
that stays a clean parse (it is additive at an unchangedschemaVersion: 1,
and a hard refusal there would reachtan build/tan runthrough
auto-bootstrap) and gap-fills from the same constants when theinstallkey is
absent entirely, the rule tan already applies to a build-plan key an older
producer omits. The gap-fill is per OS: an out-of-contractinstallthat
serves only some oflinux/macos/windowsfills the rest from the constants
instead of leaving them empty, so a manifest carryingwindowsalone cannot
silently strip every POSIX command (or, withinstall: {}, all of them). -
Every POSIX
missingPrerequisitesentry reportedcommand: null. That
branch had no install commands at all;prerequisites.install.linux/.macos
supply real ones, so Linux getssudo apt-get install -y cmakeand macOS
brew install cmakewhere both used to get nothing. Resolution is by HOST,
in one place: the manifest keys install commandslinux/macos/windows
while keying the tool LISTSposix/windows, and collapsing that asymmetry
anywhere else would hand a macOS user Debian's package manager. A POSIX host
the manifest does not serve (neither Linux nor macOS) keeps the all-null
behaviour rather than being handed the nearest OS's commands. The printed
POSIX refusal LINE is unchanged —bootstrap.shnames the tools and nothing
else, and it is still the parity oracle.tan doctor --build's
BuildToolProbeloses itsis_windowsfield with the table it existed to
gate. -
tan doctor --buildreports a REFUSEDmetadata/bootstrap.json(#90).
NewbootstrapManifestcheck,warn, indata.checks[]— with the rejection
message verbatim and the same fix prose plaindoctorputs in
hostPrerequisites' tail.--buildnow reads the manifest (for the install
commands above), and a version-skewed or unparseable one made it substitute
tan's compiled-in constants with nothing on the wire: no check, no issue,
andsdkProvenancereports only the git short-commit and
metadata/sdk_version.yaml, never the manifest.--buildis the mode
alp-sdk-vscode shells forrunToolchainFix, so on a futureschemaVersion: 2
SDK its Fix button would have run a stale command silently — the exact drift
the version-skew guard exists to prevent.warn, notfail: the exit code is
unchanged and the fallback commands are still real.
Removed
tan init --template host-tooling-starter(#14). Retired entirely
while closing out the SDK scaffold catalog — itsWizardTemplateId
variant, generator, and registry entry are gone, not just left unvendored.
tan init --template host-tooling-starternow exits 2 with
init.invalid-template.minimal-appis now the only template left
hand-generated, deliberately deferred (itscontract/golden is owned by
an in-flight contract-surface change).
Fixed
ninjawas a Windows-only prerequisite in tan's own fallback, so a POSIX
host on a legacy SDK still hit the original defect (#121). alp-sdk declared
ninjaa POSIX prerequisite with realinstall.linux/install.macos
commands in #971/#981 (mergedd6fd3a18), but the fact lives in THREE places
and only one had moved:metadata/bootstrap.jsonupstream -- fixed there;contract/fixtures/bootstrap/manifest.json, tan's vendored copy -- now
re-vendored byte-exact (sha2565202025aac269040f1893c843b2071d69f0a7f4bdd7b91755d832aa706466c7a,
5577 bytes, LF) withPINNED_SDK_TAGmoved0ed078a6->3ffd8774,
20 commits forward;fallback_facts, the hand-ported constants a legacy SDK with no manifest
actually uses -- which still saidprerequisites_posix: [git, cmake, python3]and served no POSIXninjacommand. Fixed here. That third copy
is the one a customer on a released SDK hits.- Three expectations moved with it, each because the data moved and not to
make the suite green:parses_every_field_of_the_real_manifest(reads the
re-vendored fixture),the_posix_refusal_stays_one_line_but_now_carries_real_commands
(whose name had outrun its assertion --ninjawas the last commandless
POSIX entry), anda_posix_host_gets_its_own_package_manager_and_never_winget,
which carried a written prediction that it would go red on exactly this
change, with instructions not to weaken it. - Note
ninja-buildvsninja: the package name differs from the binary
name, which is the whole argument for carrying these as data.
- A pre-release tag would have shipped to every customer as
latest.
release.ymlset neitherprereleasenormake_latest, so av0.4.0-rc1
tag's classification rested entirely on the action's default -- and
install.shfetchesreleases/latest/download/<asset>directly, which
GitHub excludes a pre-release from ONLY when the flag is set. Both flags are
now derived from the one fact that distinguishes them, the hyphen in the tag,
so they cannot disagree with each other or with the tag.publish_cratesandpublish_npmskip a pre-release. npm was the sharpest
of the three:npm publishpasses no--tag, so it defaults to the
latestdist-tag, and an unguarded rc would have become plain
npm i -g @alplabai/tan-- with npm unpublish far more restricted than a
crates.io yank. Skipping keeps an rc fully retractable, which is the reason
to cut one;--tag nextis the documented relaxation.docs/release-contract.mdgains the pre-release contract, and its Linux
target table is corrected: it documentedlinux/x64+linux/arm64as
consuming the-gnuassets with musl "not (yet) wired into"
releaseAssetForTarget, while the extension has mapped both to-musl
because the-gnuassets carry a glibc floor. The doc now separates the
zigbuild PIN (2.31) from the MEASURED floor of the shipped binary
(GLIBC_2.30, perreadelf -V), and warns off the "2.31 floor /
GLIBC_2.39 not found" wording the extension still carries -- the
phenomenon is real but both numbers in it are wrong
(alp-sdk-vscode#370).
alp-sdk fixed the same mix-up in its own install docs in alp-sdk#990.
tan explain --template edge-ai-starterdescribed a project that is not
the onetan initwrites (#124).project_template_detailsread the
wizard registry'slibsfield unconditionally, but that field is
deliberately blanked for a vendored template (its files come from the SDK's
--emit scaffoldtree instead) —edge-ai-starterreported "Default
libraries: (none)" while its vendoredboard.yamldeclares
libraries: [tflite-micro], one line under prose that names TFLite-Micro
directly.iot-starterandboard-diagnosticshad already been hand-synced
correct ahead of this fix (#128);edge-ai-starterwas the one live wrong
answer.explain's "Default libraries" line now derives from the vendored
board.yaml's ownlibraries:block (vendored_library_names_for, new in
tan-core::wizard::service::vendored) for every vendored template, instead
of a second hand-synced registry field that can drift from it — the
registry'slibsstays authoritative only forminimal-app, the one
template left hand-generated. "Default features" is unchanged (still
registry-sourced): a vendoredboard.yamlhas no representation for
iot-starter's inherentmqtt: true, so deriving that line fully is not
possible without reintroducing a different self-contradiction.- Follow-up hardening (review of #137):
vendored_library_namesparsed the
vendoredboard.yaml'slibraries:block with a hand-rolled line scan
that matched only the- name: <value>spelling, not the bare-shorthand
- <value>formtan_core::model's ownLibraryEntryalready accepts —
a future re-vendor shipping the shorthand form would have silently gone
back to "Default libraries: (none)" with no test catching it. It now
parses throughtan_core::model::BoardModel/LibraryEntrydirectly, so
both spellings are covered. Also widened
vendored_library_names_matches_across_familiesfrom asserting only the
edge-aiAEN/V2N pair to all four (minimal/sensor/edge-ai/
diagnostics) — the doc comment already claimed family-invariance for
every vendored template, but only one pair was checked.
- Follow-up hardening (review of #137):
tan bootstrapreused a workspace across a patch-level Zephyr bump, so the
next build was green against the wrong Zephyr AND the wrong hal_alif (#98).
The reuse test compared onlyMAJOR.MINOR, so upgrading alp-sdkv0.13.0->
dev(zephyrv4.4.0->v4.4.1, hal_alifv2.2.0->v2.3.0) printed
Reusing compatible alp-sdk workspaceand skippedwest updateentirely.
parse_zephyr_version_fileandparse_west_zephyr_pinnow carry
MAJOR.MINOR.PATCHanddecide_workspace_reusecompares the whole pin, with
a newStaleoutcome for a tree that IS this SDK's, just left behind.- Stale runs
west updaterather than only warning: a warning alone leaves
the next build green against the wrong Zephyr, which is the defect itself.
It is not the aggressive reading either -- it is byte-for-byte the command a
bootstrap with no$ZEPHYR_BASEwould run over the same topdir, gated on a
manifest that already proved the tree belongs to this SDK. It also fixes the
part a zephyr-only comparison never could:west updatemoveshal_alif,
cmsisandmcubootto their pins too. - The second route is closed as well.
tan build's auto-bootstrap fires on
is_warn("zephyrVersion"), which compared twoMAJOR.MINORvalues, so
4.4==4.4and no re-bootstrap fired -- making--no-auto-bootstrap's
own--helptext ("by default a text-mode build with ... a stale one, runs
tan bootstrapfirst") a false promise. It now reaches that branch.
- Stale runs
- An unreadable
metadata/bootstrap.jsonwas indistinguishable from an absent
one (#99).load_factstreated EVERY read error as "legacy SDK", so a
chmod 000manifest on adevtree and a released tree with no manifest
produced envelopes identical in every field carrying a verdict:ok:true,
exitCode:0,factsFromManifest:false, emptyissues. The conflation was
deliberate and its comment said why ("every released alp-sdk today has no
manifest at all") -- a premise that expired whendevshipped one. Only
ErrorKind::NotFoundfalls back now; every other kind is a hard error naming
the path and the OS error, in the same shapeparse_bootstrap_manifest
already produces. - Plain
tan doctorprobed nothing about the build environment and printed
byte-identical output across four materially different host states (#100).
It is the command alp-sdk'sbootstrapprints as the customer's very next
step andREADME.md's Quickstart documents as the health check that "catches
a missing toolchain/HAL", yet it ran only the debug-readiness set — the same
seven checks, same summary, same exit 4 on a host whose documented example
build failed on both Zephyr slices and on the host where it succeeded. It now
folds inprobe_build_preflight, the same calltan buildand
tan doctor --buildalready make, sosdk/workspace/westResolved
appear in plaintan doctortoo.--buildis unchanged: it keeps its own
board.yaml-derived OS-set resolution and itsBuildToolProbelayer, and its
envelope key set is now pinned by a test — it is the live cross-repo contract
alp-sdk-vscodeshells (["doctor","--build"],["doctor","--build","--fix"]),
and it has no plain-doctorconsumer, which is what makes the fold safe. The
preflight's ownboardYamlis dropped from the fold so exactly one check of
that name is ever emitted. tan doctor --fixparsed, was accepted, and did nothing (#100).run()
reads the flag only inside its--buildbranch, sotan doctor --fix
produced output line-for-line identical to plaintan doctor— no "fixed N",
no "nothing to fix", no error. It is nowrequires = "build"at the clap
level and fails as a usage error.--build --fixis unaffected.boardYamlhard-failed with exit 4 at an alp-sdk checkout root, where there
is noboard.yamland no reason for one (#100). That is exactly where
bootstraptells a customer to runtan doctor, so the first command a new
user typed reported1 failedfor a non-problem. A missingboard.yamlis
now a warning when no project was named and a failure once--projector
--board-yamlselected one.tan doctor --build's ownboardYamlstays a
hard fail — that mode answers "can this build run", and none can without it.tan doctorclaimedvadimcn.vscode-lldb is installed.on hosts with no
VS Code and counted it among the passes (#102). The standalone binary cannot
enumerate a marketplace extension; theDebuggerExtensionsStateall-true
literal at three call sites was an inherited assumption from the extension's
resolveCliDebugContext, wheretrueIS correct because that code can
introspect its own host. The four extension-presence checks
(codeLLDBExtension,cortexDebugExtension,cppToolsExtension, the MCU
companion viewers) now render a newunknownstatus outside VS Code: not a
pass, counted in no summary bucket, raising no issue and no next step. The
pass-throughtruedefaults stay intan-corefor the extension's use.
--buildemits nounknowncheck, so its envelope is untouched.sdkRoot's failure text named "The extension" from the standalone binary
(#102).tanitself did the resolving; the message now says
No alp-sdk checkout resolved.and points attan sdk switch <path>/
--sdk-root <path>.debug-configemitted"type": "codelldb", a debug type no extension
registers, so F5 refused every native_sim session (#104).
vadimcn.vscode-lldbv1.12.2 declares
contributes.debuggers[0].type = "lldb"—codelldbis the extension's
marketplace NAME, not a debug type, and VS Code answeredConfigured debug type 'codelldb' is not supported.native_sim is the only debug target
reachable with no probe and no board, i.e. the first debugging experience any
customer has, so this had never worked. The value is nowlldb, taken from
CodeLLDB's own manifest rather than from what the code used to say. The class
is detectable from here on:every_emitted_debug_type_is_one_an_extension_contributes
walks every target kind × server and checks the emittedtypeagainst a
hardcoded table of the three types tan can emit —cortex-debug
(marus25.cortex-debugv1.12.1),cppdbg(ms-vscode.cpptoolsv1.23.6),
lldb(vadimcn.vscode-lldbv1.12.2), exactly the extensionsdebug doctor
declares — each row naming the extension and version it was verified against,
and a compile-time guard failing the build if a newDebugTargetKindis added
without listing it.debug-configoverwrote a hand-resolvedlaunch.jsonvalue with a
<resolved-…>placeholder on every write (#105). A same-named configuration
was replaced wholesale, so a customer told to hand-fill
"device": "AE822F4M55_HP"got"device": "<resolved-device>"written back
over it on their next F5 — data loss on their own file, with no confirm and no
backup, and an unexitable loop around the advice they had just been given. The
same held for every<resolved-…>this command emits (svdFile/svdPath/
gdbPath/serverpath/searchDir/configFiles/miDebuggerPath). The write
plan now merges key-by-key over the existing entry under one narrow rule: an
incoming unresolved placeholder never overwrites a concrete existing value.
That rule is also what separates "the customer set this deliberately" from
"this is our old output" — our output for a field we cannot resolve is
literally an angle-bracket token, so anything concrete in the file is real.
The inverse still works: whenever a run CAN resolve a field the incoming value
is concrete and overwrites unconditionally, so a stale value that is now wrong
is still updateable (thecodelldb→lldbrepair above lands on existing
entries for exactly this reason). Arrays follow the same rule with a
whole-list case: an all-placeholder incomingconfigFileskeeps the existing
list intact, or a hand-added second.cfgwould be lost to a per-index merge
against a one-element draft. Key order follows the existing entry with new
keys appended, and keys the customer added that tan never writes
(preLaunchTask,serverArgs, …) are untouched.- The placeholder predicate called
<host>:<port>a resolved value. It
tested for the<resolved-PREFIX, so the yocto draft's two-token
miDebuggerServerAddresspassed as a real address. Concretely: a yocto config
whose<resolved-gdb>did resolve lost the "Placeholder fields … still need
resolution" note while its gdbserver address was still unusable — the note
going silent on exactly the config that cannot launch. The test is now any
angle-bracket token (is_unresolved_placeholder, matching the extension's
/<[^<>]*>/), and${workspaceFolder}-style VS Code substitutions still
count as resolved because they carry no angle bracket. One predicate in
tan-corenow backs both the note and the merge, so "still needs resolution"
and "do not overwrite this by hand-filled value" cannot disagree. tan doctor --buildrated a missingninjaorcmakewarn, so it exited
0 on a host that cannot build (#103).ninjais the generator CMake picks by
default on every Zephyr host, so its absence does not degrade a build, it stops
west buildoutright;cmakeis at least as blocking (Zephyr AND baremetal).
Both now reportfailwhen absent, which also makes--buildagree with plain
tan doctor, whosehostPrerequisitescheck has always called a missing
prerequisitefail. Deliberately NOT widened further:weststayswarn
because this check probes bare PATH while the executor resolves west from the
workspace venv, so a correctly bootstrapped host that builds fine routinely
fails the probe (the venv-aware verdict is the preflight'swestResolved
check, already in the same report);bitbake,zephyrSdk,bmaptooland
vendorToolchainare optional or advisory by design. Severity is independent
of whether the manifest carries an install one-liner — anullcommand means
tan cannot offer a button, not that the build will succeed.- A terminal user never saw the runnable install command
tan doctor --build
already had (#103).missingPrerequisites[].commandhas been sourced from
the SDK manifest'sprerequisites.install.<os>since #95, but text mode
renders onlychecksandnextSteps, so the CLI showedInstall Ninja.while
the VS Code extension's Fix button gotsudo apt-get install -y ninja-build
from the same report. The command is now appended to each check'sfixprose —
appended, not substituted, because the prose carries constraints the command
does not (cmake (>=3.20)) — and omitted when the manifest lists none, the
same "never invent one" rulecommandfollows. - A latent
retarget_board_yaml_sombug the vendorediotscaffold's
column-alignedsom.sku:comment exposed (#14). Retargeting onto a
tree's own SKU (a claimed byte-exact no-op) used to collapse the comment's
alignment to a fixed two-space gap. It now replaces only the value token,
leaving the rest of the line untouched. - The documented Quickstart
tan --project examples/<cat>/<name> build, run
from an alp-sdk checkout root, failed withno SDK selected(#101). SDK
auto-discovery only ever probed the workspace root itself and two named
SIBLINGS (alp-sdk,alp-sdk-upstream), never walking UP — and
cli_workspace_rootiscwd.join(--project), so the Quickstart's nested
example put the workspace root three levels BELOW the very checkout the
command was invoked from, where no lateral candidate can exist. Discovery now
falls back to the nearest ENCLOSING checkout (tan_core::nearest_ancestor_sdk,
walking parents forscripts/alp_project.pyand stopping at the first match
or the filesystem root). The tier is shared by both discovery paths —
util::discover_sdk_root(build/validate/doctor) and
tan_core::discover_workspace_sdk(tan sdk current'ssourceTier) — so the
documented "sourceTiernever claimsdiscoveryfor a path build won't
resolve" invariant still holds. It is a strict fallback: it only runs when
nothing lateral answered, so every workspace that resolved before resolves to
exactly the same path, and because the walk stops at the first match it
contributes at most ONE candidate and can never tripproject.rs's
deliberate two-or-more-is-ambiguous rule. no SDK selectedpointed at a remedy that reports success and fixes
nothing (#101). The.alp/sdk-pathpointertan sdk switchwrites is scoped
per--project(deliberately — pinned by
switch_and_current_use_project_scoped_workspace_root_not_process_cwd), so a
baretan sdk switch <path>printedSwitched project SDK to …, visibly
changedtan sdk current, and then left atan --project <p> …build failing
byte-for-byte identically. Under--projectthe check now names the scoped
invocation (tan --project <p> sdk switch <path>) and--sdk-root, the one
flag that always works and which the message never mentioned. The scoping
itself is unchanged.- A
zephyrslice that never loaded Zephyr was reported[+] okfor a host
x86-64 binary (#97). The out-of-the-box path —tan init --non-interactivethentan build --native— scaffolded theminimal-app
template, whose hand-generatedCMakeLists.txtnever calls
find_package(Zephyr ...).west build -b <board> <project>/srcconfigures
such a tree anyway (CMake only emits a dev warning about the missing
project()call), so the board name was never validated,ninjalinked a
host executable, the tool exited 0, and the executor reported[+] ok (rc=0)for an artefactreadelf -hcallsMachine: Advanced Micro Devices X86-64with nozephyr/build output at all. Two fixes, both required:- The executor now refuses such a slice. After a
zephyrslice exits 0 it
checks the build dir for evidence that Zephyr's boilerplate actually ran —
aZEPHYR_BASE:entry in<cwd>/build/CMakeCache.txt(what
find_package(Zephyr)caches — verified present, at line 42, in a real
Zephyr slice build dir, and absent from a baremetal plain-CMake build's
complete 339-line cache) or, as a fallback, a<cwd>/build/zephyr/
directory. Both signals are checked at the top of the build dir AND across
its immediate subdirectories, because--sysbuildis a live path here (the
V2N plan carries-DSB_CONF_FILE=…/zephyr/sysbuild/v2n/sysbuild.conf) and
its superbuild owns the top-level dir while the real per-image Zephyr builds
nest one level deeper — without that look the guard would fail a correct
V2N build. One level is enough; sysbuild nests per-image, not recursively.
Both are generated
build artefacts, deliberately NOT configure-log text: a grep for
ZephyrConfig.cmakebreaks the moment CMake rewords a line. With neither
present the slice fails with the customer-actionable cause — its
CMakeLists.txtmust callfind_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})beforeproject()— instead of an rc. The guard
stands down when the slice redirects west's build dir (-d/--build-dir),
where the evidence lives somewhere tan cannot see, the same refusal
resolve_zephyr_artefactand the SDK-switch wipe already make. Living in
the executor, it survives any future change to the default template or SKU. - The non-interactive
tan initdefaults are now a buildable pair:
zephyr-app(vendored from the SDK'sminimalscaffold, real
find_package(Zephyr)+board.yaml→alp.confviaEXTRA_CONF_FILE)
instead ofminimal-app, andDEFAULT_SOM_SKUE1M-AEN801instead of
E1M-AEN701. AEN701 has no qualified board tree in alp-sdk — only the two
loosezephyr/boards/alp_e1m_aen701_m55_{he,hp}.overlayfiles — so its
siblingm55_heslice died withNo board named 'alp_e1m_aen701_m55_he' found; AEN801 is the lead part and the only AEN
SKU carrying bothzephyr/boards/alp/e1m_aen801_m55_heand…_m55_hp.
minimal-appandE1M-AEN701both remain valid explicit--template/
--somvalues. This half shipped WITH the guard, never before it: alone it
would have removed them55_hefailure that was the only reason the run
exited non-zero, turning a red run into a green one with the host binary
still in place.
- The executor now refuses such a slice. After a
- A slice-confined unresolved
${TOOLCHAIN_ROOT}failed the WHOLE plan,
not just the slice that needed it.substitute_plan_tokensinspected
only the FIRST${...}-shaped token in each field and, on an unresolved
${TOOLCHAIN_ROOT}, returnedUnresolvedToolchainRootfor the entire
plan — so ONE Zephyr slice naming a toolchain this host hasn't installed
(e.g. a per-sliceZEPHYR_SDK_INSTALL_DIRoverride) refused to build
every OTHER slice too, even anative_simslice that needs no toolchain
at all. The pass now scans every token in a field to completion (so a
genuinely unknown token — a version/bug fact — still hard-fails the
plan regardless of where it sits relative to a known one), and when the
only problem in a slice's own fields is an unresolved${TOOLCHAIN_ROOT},
reports it as a demoted slice instead of erroring the plan.tan build --native's executor routes a demoted slice through the SAME
executionPolicy.missingToolseam a missingbitbake/westalready
uses — skip by default, fail undermissingTool: "fail"— naming the
slice and the host-specific advice (install a toolchain / disambiguate
ZEPHYR_SDK_INSTALL_DIR) in both the text recap and a
build.toolchain-root-unresolvedenvelope Issue (warningon skip,
erroron fail); the demoted slice's ownconfigArtefactsare stripped
before materialise ever sees them, so nothing with a live token in its
path or contents is ever written.boardYamlandsharedArtefacts[]
have no owning slice to route a skip to, so they keep the old hard
failure unchanged.tan build --materialisehas no per-slice dispatch
seam either, so it decides once, up front, instead: skip omits just the
demoted slice's artefacts (with a warning Issue naming it), fail writes
nothing at all, matching the exit-nonzero/nothing-written shape
--materialisealways had. Two notes for anything parsing the envelope
behind a pinnedSUPPORTED_CLI_VERSION(alp-sdk-vscode):build. toolchain-root-unresolvedis no longer only a plan-fatal error — it can
now ride anok:trueenvelope atwarningseverity when the skip policy
applies, so a consumer must not treat that code alone as anok:false
signal. Andsubstitute_slice's field-processing order changed
(env/envAppendPathnow precedecommand, wherecommandused to come
between them andconfigArtefacts) — a slice carrying an unresolved token
in BOTHenvandcommand.argsnow reports theenvfield name in
LeftoverToken, not thecommand.args[…]one a consumer may have
previously seen for that (rare) shape. (#89) tan debug-config --target-kind native-hostpointedprogramat
zephyr.elf, correcting #83. #83 fixed the slice SELECTION (native_sim is
found by board, not byos) but then took that slice'soutput_artefact
verbatim. A manifest never records the host runnable:resolve_zephyr_artefact
(build/execute/manifest.rs) is tan's ONLY writer ofoutput_artefact— the
field's "populated byOrchestrator.fan_out" lineage is stale, alp-sdk has
been planner/emit-only since alp-sdk#848 retiredfan_out— and it stores
<slice-cwd>/build/zephyr/zephyr.elfunconditionally for every zephyr slice,
native_sim included. There is no.exebranch anywhere. SoALP: Native Sim Debughanded CodeLLDB an ELF it cannot launch: the same failure #83 set out
to fix, one directory entry over.tan runhad it right all along
(find_native_sim_exeswaps in the siblingzephyr.exe), and the reason the
two drifted is that each path carried its own idea of the runnable — so the
swap is now one puretan_core::run::native_sim_exe_beside, called by BOTH.
#83's test fixtures wroteoutput_artefact: …/zephyr.exe, a manifest tan
cannot produce, which is exactly why they could not see this; they now write
zephyr.elfand assert the resolvedprogramis the sibling.exe. Only
thenative-hostarm transforms —zephyr-mcu,baremetal-mcuand
yocto-userspacestill want their artefact verbatim.debug-configemitted its launch configuration with scrambled key
order. Dropping the two unresolvedsvdFile/svdPathplaceholders used
serde_json::Map::remove, which under this workspace'spreserve_order
feature is a SWAP-remove: the last two keys were dragged up into the vacated
slots, so everyzephyr-mcuprofile shipped as…interface, device, servertypeinstead of…servertype, device, interface. Harmless to a debug
adapter, but key order matching the TS CLI is this module's stated contract,
and the new goldens would otherwise have pinned the scrambled form as
correct.shift_removenow.tan debug-config --target-kind native-hostpointed the debugger at a
Cortex-M ELF. The manifest slice was chosen byos, and native-host mapped
tozephyr— so on a board that builds a real Zephyr MCU slice as well as a
native_sim one, the firstos: zephyrslice won and itsoutput_artefact
overwroteprogram.ALP: Native Sim Debugthen handed CodeLLDB an ARM
binary to run on the host. Nothing flagged it: the value is a concrete
resolved path, so no<resolved-…>placeholder survived for a consumer to
catch, and the extension never sends--corefor this target, so that pin
could not disambiguate it either. The native-host slice is now selected by
the discriminator that already owns the question —run::native_sim_slice,
which matches the barenative_simboard and Zephyr's qualified
native_sim/…form — instead of byos. A single-native_sim project still
resolves its real artefact (returning nothing for native-host would have
regressed it to the draft's hard-coded
${workspaceFolder}/build/native_sim/zephyr/zephyr.exe, wrong whenever the
build dir is per-slice), and a project with no native_sim slice resolves
nothing rather than the wrong ELF.tan sdk listfailed behind an HTTP proxy or a TLS-intercepting
middlebox. Two independent causes on the only command that makes an
in-process HTTP request. It called a bareureq::get, and ureq 2.x's
default agent neither readsALL_PROXY/HTTPS_PROXY/HTTP_PROXY(that
needsAgentBuilder+try_proxy_from_env) nor consults the OS trust store
— its rustls config trusts only the bundled webpki roots, so a corporate
middlebox re-signing with a private CA from the Windows/macOS/Linux system
store failed the handshake outright. Every in-process HTTP call now goes
through one shared agent that honours the proxy environment (SOCKS included —
ureq readsALL_PROXYfirst, so itssocks-proxyfeature is now on;
without it asocks5://tunnel would have hard-failed where it previously
went direct) and trusts the bundled webpki roots and the system store
(ureq's ownnative-certsfeature would have swapped one for the other,
breaking a host with an empty OS store instead). The agent also caps a whole
request at 60 s — proxied now, a black-hole proxy would otherwise hangtan
forever, and the extension waits on process exit.
Scheme-correct by choice. OnlyALL_PROXY/HTTPS_PROXY(and their
lowercase aliases) select the proxy, in that precedence order.
HTTP_PROXY/http_proxyare not applied to thesehttps://requests, even
though ureq's owntry_proxy_from_envwould apply them regardless of scheme:
curl, git and Python all treatHTTP_PROXYas plain-HTTP-only, and a
corporate host exporting just that one would otherwise have its GitHub request
pushed through a proxy that may refuseCONNECT— breaking a machine that
worked going direct. An empty value (HTTPS_PROXY=) counts as unset.
NO_PROXYis honoured, for the same reason — ureq 2.12 has no support for
it, and without it a host that sets bothHTTPS_PROXYand aNO_PROXY
covering GitHub would go from working-direct to proxied. Matching follows
curl/git/Python:*bypasses everything; the list is comma-separated with
whitespace and empty entries ignored; comparison is case-insensitive; an entry
matches the host exactly or as a suffix on a label boundary, so both
github.comand.github.comcoverapi.github.comwhilehub.comcovers
neither; and a:porton an entry is ignored (every request here is 443).
The subprocessestanspawns for network work (git clonein
tan sdk install,pip/west updateintan bootstrap) are untouched by
any of this: they inherit the proxy environment and use their own trust
stores.
A handshake or proxy failure — includingtan sdk install'sgit clone—
now names the likely cause, a proxy or an untrusted corporate CA (without
naming a specific knob — git'shttp.sslCAInfowould be wrong advice on the
in-process path that shares the sentence), rather than
surfacing a raw error a user reads as "the network is down"; a proxy that is
set but unreachable is named too, from the environment, since ureq reports
that as a plain connect failure that never says "proxy". That sentence names
ALL_PROXY/HTTPS_PROXY/NO_PROXYand deliberately notHTTP_PROXY: both
paths that reach it arehttps://(the API GET and thegit clone), neither
appliesHTTP_PROXYto those, and a user who followed the advice and edited
it would see no effect. Only the message text of thesdk.fetch-failed/
sdk.install-failedissues gains that sentence; no issue code ordatafield
changed. Absent proxy environment variables behave exactly as before.tan sdk switchleft.west/configpinned to the old SDK version.
The reconciliation that keeps<topdir>/.west/config'smanifest.pathin
sync already existed fortan bootstrap(#31), butsdk switchonly ever
rewrote the active-SDK pointer files (.alp/sdk-path/
~/.alp/sdk-default) —westreads.west/configdirectly and
independently, so a switch left it naming the OLD checkout, silently, until
something needed the workspace (west flashfalling back to an unrelated
Zephyr tree and failing withunknown runner).sdk switchnow reconciles
it too, warning (never failing) and namingtan bootstrapas the next step
when it fires, and guards the rewrite on the old target being either a real
alp-sdk checkout or missing entirely (#62's reported state) — never a real,
unrelated directory that merely shares the same parent as the SDK just
switched to. As first shipped this reached only the path form (tan sdk switch /path/to/sdk): the bare-version form resolved~/.alp/sdk-cache
alone and never got that far for the~/.alp/sdklayout that reported it —
see the version-resolution entry under Changed, which lands in this same
release. (#62)tan flashcould not findwest's out-of-tree runners. No spawned
backend ever set a childcurrent_dir, so it inherited whatever directory
invokedtan flash.west's runner registration
(run_common.py'szephyr_module.parse_modules(ZEPHYR_BASE, command.manifest)) resolves out-of-tree runners (alp-sdk'salif_flash)
ONLY from the west workspace manifest, discovered by walking the child's own
cwd upward — never fromtan build'sEXTRA_ZEPHYR_MODULES— so on an
E1M-AEN801 benchzephyr_west_flashdied withFATAL ERROR: unknown runner "alif_flash".tan flashnow resolves the same workspace topdirtan build's legacywest alp-*entry already does and runs every spawned
child there. The resolver also now refuses a$ZEPHYR_BASEwhose manifest
isn't alp-sdk's (a stock/unrelated Zephyr checkout is still a west
workspace by the bare.west-dir test) rather than returning it
unconditionally — the exact shape that left this fix a no-op on a host with
such a$ZEPHYR_BASEalready exported. An app with no workspace above it
keeps today's inherited-cwd behavior. (#61)tan debug-configemitted a launch configuration that could not launch.
device,configFilesandsvdFileshipped as literal<resolved-…>
placeholders, andexecutablewas the fixedbuild/app/zephyr/zephyr.elf—
wrong for every heterogeneous project. Each value is now resolved from what
the build itself recorded: the per-core ELF fromsystem-manifest.yaml, and
device/serverpath/searchDir/configFiles/gdbPathfrom that
slice'srunners.yaml(the same filewest flashreads), via the new pure
tan_core::runners.--core <CORE_ID>picks the slice on a multicore board.
UnresolvedsvdFile/svdPathkeys are now dropped rather than left pointing
nowhere — cortex-debug fails a session on an unreadable SVD, while an absent
key only costs the peripheral view (no SVD is resolvable until alp-sdk#948).
The "placeholder fields still need resolution" note is now keyed off what is
actually left in the draft, and a board that registers no runner for the
requested server says so instead of leaving the user to guess. (#66)- The Renode smoke's CPU halted on an MRAM-linked image. Renode guesses
VectorTableOffsetfrom the LOWESTvaddrit sees. A Zephyr image linked to
MRAM has a.datainit segment that RUNS at 0x20000000 but is STORED at
0x80018348, so the guess pointed at memory nothing was loaded to: SP/PC read
back as zero and the CPU halted before executing one instruction, while the
run still exited 0.tan renodenow derives the real vector-table base from
the ELF — the load address of the LOAD segment containing the entry point —
and injects it as$vtorahead of the descriptor include, correct for both
the MRAM-linked and RAM-run shapes. Containment alone doesn't prove the
vector table starts where the segment does — an allocated
.note.gnu.build-id(or any offset/padded link) ahead of_vector_table
would satisfy it and still hand back a confident wrong address — so the
derivation is only trusted once the segment's own second word (the reset
vector, Thumb bit cleared) matches the entry point too; no match, no
answer. Inert until the descriptor reads$vtor(alp-sdk#947); an
unreadable or unexpected ELF injects nothing and leaves Renode exactly as
before. - The Renode smoke never actually booted, and reported success anyway.
build_renode_argvpassed--console --disable-xwt --hide-monitor --plain;
Renode 1.16.1 rejects that combination outright — "--hide-monitor and
--console cannot be set at the same time" — printing its usage page and
exiting 0, sotan renodereported a clean smoke while nothing was ever
simulated.--hide-monitorwas redundant (Renode's own--disable-xwthelp:
"It automatically sets HideMonitor") and is gone. A newrenode_rejected_argv
guard latches Renode's own refusal wording off the console and fails the run
(renode.argv-rejected, exit 1) regardless of exit status, so the next
incompatible flag cannot pass silently either. - The Renode smoke reported success when the CPU halted on its first
instruction fetch. Without--expect,tan renodehad exactly two
failure signals — a non-zeronatural_exitand the argv-rejection latch
above — and neither trips when Renode boots, halts the CPU on its first
instruction fetch, and shuts down cleanly: the run reportedok: true/
exit 0 while no firmware code ever ran. A newrenode_cpu_haltedpredicate
matches Renode's own two exact console wordings (CPU was halted/PC does not lay in memory), latched inrun_renodealongside
argv_rejectedand checked at the same priority — independently of
--expect/natural_exit, since the whole point is catching a run that
gave neither (renode.cpu-halted, exit 1). The$vtorinjection above
does not make this redundant: it stays inert until alp-sdk#947 wires
cpu VectorTableOffset $vtorinto the.resc, so the halt this guards
against still reproduces today. (#64) tan flashcould not find thewestthattan builduses.westis
installed INSIDE thetan bootstrapvenv, and nothing activates that venv for
a GUI-launched editor, so the ambient PATH has none.tan buildhas resolved
the west-capable workspace venv since #106;tan flashonly ever probed PATH,
so on such a host a build succeeded and the flash that followed failed every
Zephyr slice withflash: slice '<core>' backend 'zephyr_west_flash' needs one of ["west"] on PATH; none found.The venv resolution moved out of
commands::buildinto a sharedvenvmodule;flashnow uses it for the
required-tool gate, for the backend's argv (the program is spawned by its
absolute venv path), and for the child's PATH (so nestedwest/python
resolve too). The tool-probing plan builders (swd_probe,yocto_wic) see
the venv as well. With no west-capable venv — CI, an activated venv, the
contract harness — every argv and message stays byte-identical to before.
(#59)- The bootstrap manifest fixture was hand-written, not vendored, and
tan bootstrapsilently droppedmanualInstallHints.
contract/fixtures/bootstrap/manifest.json's_commentmatched no alp-sdk
commit at all. Re-vendored byte-for-byte from alp-sdk's
metadata/bootstrap.jsonat8b216a04(dev), which had split the old
nativeLibHints.windows.noteinto a shorter git-bash hint plus a new
manualInstallHints.windows.note(the Arm GNU Toolchain / Zephyr SDK
manual-install sentence, moved out of the "OPTIONAL NATIVE LIBRARIES"
heading it was wrongly printed under — alp-sdk#917 review item 7).
BootstrapFactsDochad no field for the new key, so parsing a real manifest
silently discarded that sentence whileoptional_libs_block's Windows
branch still hardcoded it AND appended the stalenativeLibHints.windows
copy, printing it twice. AddedManualInstallHint/ManualInstallHints,
wired them throughparse_bootstrap_manifestandBootstrapFacts, and made
the Windows branch readmanual_install_hintsinstead.PINNED_SDK_TAG
(.github/workflows/parity.yml) is now pinned to that same commit, so the
bootstrap-manifest byte-parity gate actually gates instead of
NOTICE-and-passing. (#69) tan bootstrapprinted the Arm GNU Toolchain URL and its PATH tip twice on
native Windows.contract/fixtures/bootstrap/manifest.jsonis re-vendored
byte-for-byte from alp-sdk0ed078a6— past alp-sdk#961 (Arm-toolchain
scoping) and #967 (dtc/gperf settled), which between them rewrote
manualInstallHints.windows.notefrom one terse sentence into five elements
and bumpedzephyr.versiontov4.4.1. Note element 4 now carries the Arm
installer URL and the "tick 'Add path to environment variable'" tip verbatim,
and element 1 carries the Zephyr-SDKwest sdk installfact together with its
workspace locator as prose — sooptional_libs_block's hardcoded
Arm/Zephyr-SDK block, kept only for as long as the vendored fixture predated
#961, became a word-for-word duplicate of the note printed immediately under
it. Deleted: the Windows arm is now the heading plus the manifest note and
nothing else, and the function no longer takes aworkspace_dir(#961 dropped
the interpolated resolved path upstream as well, sobootstrap.ps1prints no
path there either — tan follows the oracle it mirrors rather than re-adding a
locator the SDK deliberately replaced with prose). The hand-ported fallback
constants,ZEPHYR_VERSIONandPINNED_SDK_TAGmove with the fixture. Note
element 3 also retires the deleted heading's "host tools like dtc", which was
simply wrong on Windows: the Zephyr SDK's native-Windows hosttools bundle
ships neitherdtcnorgperf. No released SDK loses anything and every one
gains:metadata/bootstrap.jsonhas never existed on alp-sdkorigin/main
(absent from its whole history and fromv0.13.0), so a customer on a release
takes the fallback-constants path, which this change upgrades to the same five
elements — picking up the 7-Zip prerequisite, the dtc/gperf correction and the
Arm-toolchain scoping. The single degraded case is an alp-sdkdevcheckout
between #917 and #961, where the manifest exists but still has the
one-sentence note; it is dev-only and customer-unreachable. (#82)
Release assets
-gnuLinux binaries are cross-built with a pinned glibc 2.31
floor (runs on distros as old as Debian bookworm);-muslLinux
binaries are fully static and run on any distro/libc.- Every binary +
checksums.txtcarries a GitHub build-provenance
attestation. Verify with:
gh attestation verify <downloaded-file> --repo alplabai/tan-cli