tan is the standalone build CLI for Alp Lab E1M and E1M-X projects. It reads
hardware metadata from an alp-sdk
checkout, creates build plans, and runs the tools needed to build, inspect,
flash, and debug firmware. VS Code is optional. The implementation is Python.
Prerequisites. On Linux and macOS the installer needs a downloader --
curl or wget, either one -- plus tar and sha256sum (macOS:
shasum). Nothing else. tar and the digest tool are already present on a
stock Debian/Ubuntu and on macOS; a downloader is not. A pristine
ubuntu:24.04 has neither curl nor wget, so the command below fails there
with bash: curl: command not found until you install one:
sudo apt-get update && sudo apt-get install -y curl # or: wgetThat also pulls ca-certificates, which ubuntu:24.04 does not ship either
and which the download needs. On Windows, install.ps1 uses only PowerShell
built-ins, so there is nothing to install first.
Linux and macOS:
curl -fsSL https://raw.githubusercontent.com/alplabai/tan-cli/main/install.sh | shThe same install with wget, if that is the downloader the host has:
wget -qO- https://raw.githubusercontent.com/alplabai/tan-cli/main/install.sh | shWindows PowerShell:
irm https://raw.githubusercontent.com/alplabai/tan-cli/main/install.ps1 | iexThe installers download the release for your platform, verify its SHA-256
digest, and install it for the current user. Open a new terminal if tan is not
immediately on PATH.
That prerequisite list is the whole of it, and it is deliberately shorter than
the one a build needs: the release asset is a self-contained freeze, so the
installed tan runs on a host with no python3, no git and no compiler --
tan --version and tan doctor both work there. Building firmware needs more;
see What a build needs below.
For a system-wide install, pass --system (Unix) or -System (Windows)
through to the script -- piping straight into sh or iex swallows a bare
--system/-System before the installer ever sees it:
curl -fsSL https://raw.githubusercontent.com/alplabai/tan-cli/main/install.sh | sh -s -- --system&([scriptblock]::Create((irm https://raw.githubusercontent.com/alplabai/tan-cli/main/install.ps1))) -SystemSee docs/release-contract.md for asset names,
manual verification, and OS support.
Every release from v0.5.0 on publishes four archives:
- Windows x64
- Linux x64 with glibc
- macOS x64
- macOS arm64
Linux arm64, Linux musl, and Windows arm64 do not have prebuilt archives. Install from source on those hosts.
Python 3.12 or newer is required. Install into a virtual environment, not the
system interpreter: on a PEP 668 host (Debian/Ubuntu, including stock
ubuntu:24.04) a bare python3 -m pip install ./python refuses with
error: externally-managed-environment instead of installing anywhere.
Debian/Ubuntu's python3 package also does not include venv itself --
python3 -m venv fails there until python3-venv is installed:
sudo apt-get install -y python3-venv # Debian/Ubuntu only
git clone https://github.com/alplabai/tan-cli
cd tan-cli
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
python3 -m pip install ./python
tan --versionFor serial monitoring, install the optional dependency from the checkout:
python3 -m pip install "./python[monitor]"tan is not on PyPI yet: release.yml's publish_pypi job exists and is
opt-in, off until the repository variable TAN_PYPI_PUBLISH is true and a
pending publisher is configured on pypi.org (tan-cli#1054). PyPI is the
intended standalone channel; the npm shim that used to be advertised here is
retired, and the alp-tan-cli crate on crates.io is a stale v0.4-era Rust CLI,
no longer built from this repository and not the current program. Until the
first upload lands, use a GitHub release or a source checkout.
Getting tan onto the host and building firmware with it are different sets
of tools, and only the first one is short. A build needs, on PATH:
- Linux:
git,cmake,python3,ninja,xz,wget. - macOS:
git,cmake,python3,ninja. - Windows:
git,cmake,python,ninja.
Beyond that list: on Debian/Ubuntu tan bootstrap cannot create its workspace
virtual environment until python3-venv is installed. On native Windows, west sdk install needs a 7-Zip-compatible archive tool on PATH instead -- west
delegates .7z extraction to patoolib, which shells out to an external
7z/7za/7zr/7zz/7zzs/unar binary and has no pure-Python fallback
(winget install -e --id 7zip.7zip). tan doctor has a dedicated sevenZip
check for this, on every native-Windows host: it warns when none of those
binaries is on PATH and names the same winget command. It does not wait for
the Zephyr SDK to be missing first — a host that already has the SDK and no
7-Zip is exactly the host whose next west sdk install dies with
Zephyr SDK setup requires '7z' (tan-cli#736). tan bootstrap's own
cross-toolchain phase (below) checks for the same thing before it ever runs
west sdk install itself, and refuses cleanly instead of hitting that error.
Do not assemble any of these lists by hand. tan doctor reads its checks from
the SDK's own metadata/bootstrap.json, so it stays correct when the SDK
changes it, and it names what is missing on this host -- Windows included --
together with the command that fixes it:
bootstrap.prerequisites-missing: missing from PATH: cmake, ninja
doctor.zephyr-sdk: Zephyr SDK toolchain not detected (ZEPHYR_SDK_INSTALL_DIR unset)
-- from an initialised west workspace, run `west sdk install`
doctor.west-resolved: west resolved neither through the workspace venv nor PATH
-- no build slice can be executed. Run `tan bootstrap`
tan doctor needs none of those tools itself, so run it first, on the bare
host, rather than guessing -- this holds on Windows exactly as it does on
Linux and macOS.
Start in an empty working directory:
git clone https://github.com/alplabai/alp-sdk
tan bootstrap --sdk-root ./alp-sdk
tan init --name my-app
cd my-app
tan validate
tan build
tan size
tan run --flash --confirmWhat those commands do:
-
bootstrapprepares west, Zephyr, the Python environment and SDK dependencies into a workspace venv next to the SDK checkout -- which for the Quickstart's own invocation means./.venv, notalp-workspace/.venv:west init -l ./alp-sdkforces the west topdir to be the checkout's PARENT, and starting in an empty directory makes that parent the current directory.alp-workspace/appears only in the two cases further down: you pass--workspace, or the directory held something besides the checkout andbootstraprelocated it there for you.bootstrapthen, as its final phase (ADR 0021 Lane 1 P1), acquires the Zephyr SDK cross-toolchain (arm-zephyr-eabi) thattan buildneeds for real silicon, into the artifact-keyed store~/.alp/toolchains/zephyr-sdk-<version>-arm-zephyr-eabi/(or$ALP_TOOLCHAIN_ROOT, shared across every project pinning that same version). It reads the version to install from the SDK checkout's ownmetadata/toolchains.json, so a pin bump there reaches you on your nexttan bootstrapwith notanupgrade needed. A secondtan bootstrapagainst the same pin is near-instant: it probes the existing install's compiler and re-verifies its stamp rather than reinstalling. Pass--no-toolchainto skip this phase (the rest ofbootstrapis unaffected, andnative_simbuilds never need a cross toolchain at all). This phase does not needfileon PATH, on any host: it passes--no-hosttoolsto the underlyingwest sdk install(tan-cli#1176), so the SDK's own host-tools step -- the part that needsfile, and that dies with "Host tools installation failed" naming nothing without it -- never runs at all. That is deliberate: it is what lets this phase complete on a host carrying only the prerequisites above. What it skips is the SDK'shosttools/bundle, so this phase installs nodtc,openocd,bossacorqemu-system-*; Zephyr treatsdtcandopenocdas optional and a real-silicon build needs neither, but if you want them, run the manual command below WITHOUT--no-hosttools(and then you do needfile). This phase lists the SDK releases through the GitHub API, whose anonymous quota is counted per source IP -- so behind a shared office egress, a corporate VPN or a runner pool it can be exhausted by traffic that is not yours, and the download fails with403 API rate limit exceeded. Set$TAN_GITHUB_TOKENto authenticate it. The token needs no scopes -- listing public releases requires none.tanalso reads$GH_TOKENand$GITHUB_TOKEN, in that order behind$TAN_GITHUB_TOKEN, so an existinggh auth loginsession or a CI job's own token authenticates the download with nothing new to set. That means an ambient$GH_TOKENyou set for something else will be used here too;$TAN_GITHUB_TOKENoverrides it fortanalone, and unsetting all three restores the anonymous download exactly as it was. Every messagetanprints about this names the variable, never its value, and thebootstrap.sdk-credential-unstagedwarning tells you when a variable was set but not used.How the token is handled, since it is a secret:
- Environment only, never a flag. There is deliberately no
--github-token: a flag value lands in shell history, in the host process table for the whole multi-minute download, and in the argv people paste into bug reports. - Never an argv element.
tanstages it in a privatenetrcand pointswest sdk installat it with$NETRC, so it appears in no log, no--dry-runplan, nodata.plannedCommandsand no JSON envelope. - Offered to
api.github.comand to nothing else. This is narrower thanwest sdk install --personal-access-token, which puts anAuthorizationheader on the session: a netrc credential is matched per host, so the release CDN that serves the actual multi-hundred-megabyte archive never sees your token. - It touches disk. The staged
netrcis a real file -- mode0600inside a0700directory under the toolchain root, deleted when the download returns and swept on the nexttan bootstrapif a crash skipped that. It is deliberately not under$TMPDIR: that sweep identifies what to delete by NAME, and a name is only proof of ownership inside a directorytanowns. The sweep also leaves alone anything recent enough to belong to a concurrenttan bootstrap-- under six hours old — so twotanruns at once cannot silently de-authenticate each other. - It replaces your own
netrcfor that one child.$NETRChas no~/.netrcfallback behind it, so an unrelated credential in your own~/.netrcis not visible towest sdk installwhiletanis authenticating the download. Nothing is set at all when you supply no token. tanstops claiming it if it cannot verify the route. The netrc hand-off depends on how the Zephyr in your workspace handles credentials, whichtanneither owns nor pins. Before printing that it authenticated the download,tanchecks thewest sdk installin that workspace still matches what the hand-off was measured against; if it does not, you get abootstrap.sdk-credential-unverifiedwarning naming what to do instead, rather than an assurance that quietly stopped being true.
- Environment only, never a flag. There is deliberately no
-
If you skip the toolchain phase, or need to point at a different pin, run
west sdk installby hand from inside the workspace venv:source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 export ZEPHYR_BASE="$PWD/zephyr" west sdk install --version 1.0.1 -t arm-zephyr-eabi
the exact command
tan doctor'szephyrSdkcheck also names, so it stays correct if that pin ever moves. On a minimal Linux host this also needsfileon PATH (Debian/Ubuntu:sudo apt-get install -y file); without it the SDK's own host-tools step fails with "Host tools installation failed" and names nothing. alp-sdk'smetadata/bootstrap.json(manualInstallHints.posix.note[2]) calls a missingfile"WARN-only, not a bootstrap.sh prerequisite", and both statements are true: that note is written for the--no-hosttoolsinvocation in its ownnote[0], which never runs the host-tools step. The command above installs host tools, so it needsfile. Add--no-hosttoolsand it does not.Those two paths are the Quickstart's own layout -- an empty starting directory and no
--workspace, so the west topdir is the current directory. Do not hand-adjust them for a different layout: asktan.tan bootstrapprints the activate andZEPHYR_BASElines for the workspace it actually created, andtan bootstrap --print-envreprints them at any time, on stdout so the block can be redirected into a file. Note that the venv-activation line it emits is a comment: sourcing that output setsZEPHYR_BASEandZEPHYR_TOOLCHAIN_VARIANTbut does NOT activate the venv, so run thesource ...line yourself as well. Reach for it if you passed--workspace <path>, whose venv is<path>/.venvand Zephyr<path>/zephyr, or ifbootstraprelocated the checkout intoalp-workspace/because the starting directory held something besides the checkout, whose venv is thenalp-workspace/.venv. -
initcreates a Zephyr application and pins the SDK checkout in.alp/sdk-path. -
validatechecksboard.yamland related metadata. -
buildplans, materialises, and builds every core slice. -
sizereports firmware use against the SoM memory budget. -
run --flashbuilds and then runs or programs the selected target. On a hardware target (a native_sim/host target always just runs),--flashalone only previews the write: every slice comes backplanned, nothing reaches the device, and the run exits non-zero naming the remedy. Add--confirm(as above) to actually arm the write, or setALP_FLASH_FORCE=1in the environment, orflash_args.confirm: truein the manifest -- the same three-way gatetan flash --confirmalready has. This is deliberate, not a bug: a fresh checkout must not silently reprogram an attached module.
Run tan doctor if setup or toolchain discovery fails; its toolchain check
reports stamp-vs-pin for the resolved project (a version-skewed or
never-verified install is a fail naming tan bootstrap, never a silent
"looks present"), and its zephyrSdk check answers the broader "does any
toolchain exist on this host at all" question, unconditionally. tan doctor --fix installs missing prerequisites, but only at a real, interactive
terminal -- it is a no-op (exit 4) under a pipe, a redirect, or CI, so it is
not a scripted-onboarding remedy. It never spawns sudo itself: it runs a
prerequisite's manifest install command directly when already root, and
otherwise prints the exact command to run by hand.
If you do not want the west workspace next to the SDK checkout, choose it explicitly:
tan bootstrap --sdk-root ./alp-sdk --workspace /path/to/alp-workspace--workspace does not simply relocate where the workspace metadata is
written: the west topdir is always the checkout's parent, so this moves
the alp-sdk checkout itself to /path/to/alp-workspace/alp-sdk and updates
the machine-global ~/.alp/sdk-default pointer to it. Run this before
anything else that references --sdk-root ./alp-sdk by its old path, or
those calls stop resolving; if a project's .alp/sdk-path already pins the
old location, re-run tan init/tan bootstrap from that project after the
move.
| Task | Command |
|---|---|
| Create a project | tan init --name my-app |
| Check a project | tan validate |
| Build firmware | tan build |
| Build and run or flash | tan run --flash --confirm (--confirm arms the write on a hardware target; see the quickstart) |
| Flash an existing build | tan flash --confirm |
| Inspect firmware size | tan size |
| Create an image | tan image |
| Remove build output | tan clean |
| Generate configuration files | tan generate |
| Check the host setup | tan doctor |
| Start a serial monitor | tan monitor |
| Generate debugger settings | tan debug-config |
| List examples and presets | tan examples, tan presets |
| Explain resolved project settings | tan inspect |
| Explain a template or generation target | tan explain |
| Show help | tan <command> --help |
When a project's build/system-manifest.yaml names more than one debug
target class (e.g. a yocto A-cluster slice beside a zephyr M-core slice),
debug-config exits 2 with debug-config.target-kind-ambiguous; pass
--target-kind <zephyr-mcu|baremetal-mcu|yocto-userspace|native-host>, or
--core <core_id> to narrow to one slice. A multi-core SoM whose cores all
share one target class (e.g. two Zephyr cores) is unaffected -- the classes
fold to one and no flag is needed.
The full command surface also includes scaffold, completion, diff,
pinmux, inspect, trace, support-bundle, kconfig, faultdecode,
model, and new-som. migrate, lock, and quality forward to their
corresponding west alp-* commands: migrate requires --check,
--preview, or --apply; quality requires --profile. The other
commands run directly in tan.
tan model check statically screens a declared model's NPU eligibility with
no NPU toolchain installed. It reports npu-eligible | cpu-certain |
undetermined -- deliberately never fits, which is reserved for a real
compile or a bench measurement. For what those words do and do not claim, and
for the MAC-weighted computeOnNpuPctMax figure, see
docs/model-check-static-screen.md.
For Alif Ensemble MRAM flashing with SETOOLS, see
docs/setools.md.
Most project commands find alp-sdk in this order:
--sdk-root <path>- the project's
.alp/sdk-pathpin - the user's default SDK pointer
- a nearby
alp-sdkcheckout
Step 3 is one pointer shared across every project on the host and is
last-writer-wins: another project's tan bootstrap can repoint it. tan
warns (sdk.global-default-foreign-project) rather than resolving silently;
use --sdk-root or the project pin to be explicit.
Use --sdk-root when more than one checkout is nearby or when you want a
one-off override:
tan build --sdk-root /path/to/alp-sdktan sdk list and tan sdk current work today. tan sdk install and
tan sdk switch are not implemented yet, so clone the SDK yourself and use
--sdk-root or let tan init write the project pin.
Add --format json for machine-readable output:
tan build --format jsonThe stable top-level envelope is:
{command, ok, exitCode, project, data, issues}
plus an optional sdk object, present only on commands that resolved an
alp-sdk checkout -- absent, never null, when none was resolved.
You do not need a flag for this. tan already treats a run as
non-interactive when stdin or stderr is not a terminal — piped, redirected,
or a CI runner — and --format json settles it on its own, whatever the
terminal looks like. That rule is applied unasked (tan/core/consent.py's
can_prompt, whose not json_mode is an unconditional term). It reads stdin
and stderr and deliberately never stdout — which matters more than it
sounds, because in text mode nothing is written to stdout at all
(tan/env.py). stderr is therefore both the report channel and half the
consent gate: tan doctor --fix 2> log.txt from a real terminal is treated as
unattended and the fix is suppressed (doctor.fix-suppressed), while
> log.txt leaves consent untouched and captures an empty file. Where a
command has a documented default it takes it; where it has none it fails rather
than asking.
--ci and --non-interactive exist as explicit "do not ask me" signals on top
of that, and every registered command parses them:
tan/core/global_flags.py holds the shared spec and injects it into any
command that does not already declare the flag itself. What keeps that true is
split in two. tests/gates/test_global_flags_gate.py fails the build for the
29 commands that reject an unknown option; it cannot speak for lock,
migrate and quality, which register ignore_unknown_options
(west_forward_cmd.py) and would swallow an undeclared flag into the west
passthrough rather than reject it — those three are held by
test_west_forward_command.py's
test_a_leading_global_flag_is_consumed_not_forwarded. They are not ROOT
options either: a leading one is relocated across the subcommand boundary, so
tan --ci doctor and tan doctor --ci are the same run, while a bare
tan --ci with no subcommand is No such option: --ci.
tan build accepts all ten of the shared flags (global_flags.py's
_GLOBAL_FLAG_SPECS) — zero of them refuse the invocation. Three —
--project, --board-yaml, --sdk-root — are already declared by build
itself and pass through untouched. The other seven — --target, --all,
--verbose, --quiet, --no-color, --non-interactive, --ci — are
accepted and dropped, the SAME accept_global_flags mechanism 17 other
commands already use: the oracle's own cli.rs declares them ONLY on the
shared GlobalArgs struct and build's Rust handler never read them either,
so this is not a narrower stand-in for refusing them, it is the identical
oracle behaviour.
Separately, four flags LOCAL to build — never part of the shared set —
still refuse the invocation outright (tan-cli#427): --plan, --manifest,
--manifest-from and --no-auto-bootstrap. Each is RETIRED rather than
deferred or accepted — parsed (never a Click typo error) but refused with
build.flag-retired (exit 2), naming what to do instead directly in the
message: --plan-from (with --materialise/--execute) for --plan, a
native tan build's own build/system-manifest.yaml for --manifest,
opening the file directly for --manifest-from, and running tan bootstrap
yourself for --no-auto-bootstrap — this port has no implicit "run
tan bootstrap on a missing Zephyr workspace" trigger for that one to
disable, and is not building one just to give the flag something to switch
off, so it is retired outright rather than left pending.
--plan-from, --materialise, --native, --execute, --build-root,
--format and --pristine are unaffected by either bucket:
$ tan build --ci --plan-from plan.json --format json
{"command":"build","ok":true,"exitCode":0,"project":{...},
"data":{"schemaVersion":1,...},"issues":[]}
$ tan build --no-auto-bootstrap --format json
{"command":"build","ok":false,"exitCode":2,...,"issues":[{"code":"build.flag-retired",
"severity":"error","message":"`--no-auto-bootstrap` is retired: `tan build` never
bootstraps implicitly, so there is nothing for it to disable -- run `tan bootstrap`
yourself when a workspace needs preparing."}]}So a script that adds --ci to every tan invocation no longer breaks on
build — none of the ten shared flags refuse. Of the four BUILD-local flags
that do refuse, each names itself and says what to run instead:
--no-auto-bootstrap is retired rather than pending (tan-cli#427), because
tan build never bootstraps implicitly. Elsewhere --ci does three separate
things, and only the first is consent:
- Consent, on the two commands that read the flag for it —
doctor --fix(doctor_cmd.py'sfix_allowed) andscaffold(scaffold_cmd.py'sinteractive), both throughcan_prompt. This is the half worth reaching for: it is what stopstan doctor --fixrunning unattendedwinget installs under a pty-allocating runner. It is NOT the whole prompting surface, though —new-somprompts too, gates on stdio alone (stdin_is_tty() and stderr_is_tty(),new_som_cmd.py) anddels both flags unread, so under that same pty-allocating runnertan new-som --ciblocks onNew SoM SKU (E1M-<UPPERCASE> shaped)forever. Pass its required flags there instead. - Colour, which has nothing to do with prompting. Inside
tan/env.py'suse_color(if no_color or ci or no_color_requested())--ciis a second spelling of--no-color, so ondoctorandsize— that helper's only two callers — it changes text output where no prompt was ever possible. It is not a synonym outside it:faultdecodecolours through its own_use_color(no_color)and ignores--ci.--non-interactivechanges colour nowhere. - The refusal message. Where stdio is already non-tty the rule above has
decided first, but
--cistill names itself indoctor'sdoctor.fix-suppressedreason list — anissues[]difference a JSON consumer sees.
Rely on the stdio rule for the consent half; reach for --ci on doctor and
scaffold when you want that refusal regardless of what stdio looks like —
build accepts and drops --ci (and --verbose, --quiet, --no-color,
--target, --all, --non-interactive) identically, doing nothing with any
of them.
alp-sdk-vscode -> tan -> alp-sdk
VS Code UI CLI metadata, schemas, examples, west extensions
alp-sdk: hardware metadata, schemas, examples, and the remainingwest alp-*extensions.tan: SDK selection, planning, build execution, and the manifest thatflash/size/imageread.alp-sdk-vscode: an optional UI that invokestan.
A successful build writes build/system-manifest.yaml, which records the
per-core artifacts and is reused by downstream commands.
New implementation work belongs under python/:
python3.12 -m venv .venv
.venv/bin/python -m pip install -e "./python[monitor]"
.venv/bin/python -m pip install pytest
(cd python && ../.venv/bin/python -m pytest tests -q)
python3 python/scripts/version_check.py --selftest --selfAlways install into a venv you create — never a bare pip install -e ./python
or pip install --user -e ./python. Run without an active venv, that writes
an editable install into your OS user site-packages, and from that moment
every bare python3 process on the machine resolves import tan to
whichever checkout was installed last, regardless of which worktree it is
actually running in — including another developer's or another agent's
checkout, on a shared box (tan-cli#665). It costs nothing to notice while it
is happening: tan --version and which tan keep answering normally, so a
full pytest tests -q run can report hundreds of misleading failures (or,
worse, a false green) with no other symptom. tests/conftest.py's
tan_under_test fixture refuses loudly at session start if import tan
resolves to anything outside this checkout's own python/ — that is the
backstop, not a substitute for using a venv in the first place.
That run means the same thing on every machine, including a bench host with
real debug tooling installed. The suite neutralises the debug/flash probe
identities — JLinkExe, openocd, pyocd, west and friends —
for its own duration (python/tests/conftest.py, PROBE_TOOLS), so a
which()-gated branch answers the way it answers on a CI runner rather than the
way this host happens to be provisioned. Before that (tan-cli#603) seven
test_flow_d_preflight_* cases passed locally and failed on ubuntu, windows
and macos at once. Ordinary host tooling — git, python3, sleep, the
coreutils the installer-script tests execute — is left alone. A test that
needs a probe tool present seeds its own and points PATH at it, which is
what makes the inventory readable in the test.
Useful directories:
python/tan/commands/ command orchestration, filesystem and subprocess work
python/tan/core/ domain logic and wire models
python/tan/planner/ in-process planner, mirrored from alp-sdk
python/tan/templates/ project templates included in the package
python/tests/ unit, conformance, parity, and repository gates
contract/ the JSON envelope goldens shared with alp-sdk-vscode
Release assets are PyInstaller freezes of python/. The Rust implementation
this program was ported from is deleted; the behaviour it was measured against
survives as the frozen captures under python/tests/fixtures/oracle_captures/.
- Release assets and verification
tan model checkand the static NPU-eligibility screen- SETOOLS setup
- Development roadmap
- Changelog
- License
tan is licensed under Apache-2.0.