Skip to content

Commit 440fbb4

Browse files
committed
docs: add phase-3 design document (EN + zh-CN)
Frozen design for Phase 3 — Build, Flash & Debug: - east build: CMake 3.21+, presets, pristine strategies, two-dash passthrough - Runner trait: async flash/debug/attach/reset, DebugSession RAII guard - OpenOCD runner: output classification, gdb foreground, subprocess lifecycle - External runner: declarative, template-driven, shell dispatch reuse - state.toml v1: schema versioning, atomic writes, build artifact tracking - Template engine extension: runner.* namespace - Manifest RunnerDecl: openocd/external validation, serial reserved for Phase 4 - Error model: BuildError, RunnerError, StateError with miette::Diagnostic - Subprocess lifecycle: SIGTERM/SIGKILL, zombie prevention, SubprocessHost trait - Crate dependency graph with east-runner MUST NOT depend on east-build rule https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
1 parent 8413191 commit 440fbb4

2 files changed

Lines changed: 560 additions & 0 deletions

File tree

docs/design/phase-3.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# Phase 3 Design Document — Build, Flash & Debug
2+
3+
**Status:** Active
4+
**Scope:** `east build`, `east flash`, `east debug`, `east attach`, `east reset`; `east-build` crate, `east-runner` crate, `.east/state.toml`.
5+
6+
## 1. Goal
7+
8+
Deliver the first end-to-end "build and run firmware on real hardware" experience:
9+
10+
1. **`east-build` crate** — CMake wrapper with preset support, pristine detection, build artifact tracking.
11+
2. **`east-runner` crate** — Runner trait, two runner kinds (External, OpenOCD), `DebugSession` RAII guard.
12+
3. **`.east/state.toml`** — persistent workspace state tracking build products, default runner, and preferences.
13+
14+
**Hard acceptance criterion:** the author must run `east build -p my-preset && east flash && east debug` on a real RISC-V dev board and hit a breakpoint in gdb.
15+
16+
### Explicit Non-Goals
17+
18+
- No serial ISP protocols / `SerialRunner` (Phase 4).
19+
- No `probe-rs` integration.
20+
- No GDB frontend / IDE integration.
21+
- No non-CMake build systems.
22+
- No sysbuild / multi-image builds.
23+
- No automatic port / device discovery.
24+
- No `east run` or `east test`.
25+
26+
## 2. `east build` Semantics
27+
28+
### 2.1 CMake Version
29+
30+
Minimum: **3.21** (Preset schema v3). Detected at CLI startup; lower versions produce a hard error.
31+
32+
### 2.2 Build Directory
33+
34+
Default: `<workspace_root>/build/<name>`, where `<name>` is the preset name (from `-p/--preset`) or `default`. Override: `-d/--build-dir <path>`.
35+
36+
### 2.3 Source Directory Precedence
37+
38+
1. `--source-dir` CLI flag.
39+
2. `build.source_dir` in config.
40+
3. `<workspace_root>/app` if that directory exists.
41+
4. `<workspace_root>`.
42+
43+
### 2.4 Pristine Strategies
44+
45+
| Strategy | Behavior |
46+
|---|---|
47+
| `always` | Remove build dir before configure. |
48+
| `never` | Never remove. |
49+
| `auto` (default) | Remove if: source dir changed, preset changed, toolchain file changed, or `CMakeCache.txt` is unreadable. |
50+
51+
### 2.5 Argument Passthrough
52+
53+
- First `--`: appended to configure step (`east build -- -DFOO=bar`).
54+
- Second `--`: appended to build step (`east build -- -DFOO=bar -- -v -j4`).
55+
56+
### 2.6 Success Criteria
57+
58+
`cmake --build` exits 0 AND at least one artifact matching `**/*.elf`, `**/*.bin`, or `**/*.hex` is found under the build directory. No artifact = warning (not error). Override pattern via `build.elf_pattern` config.
59+
60+
### 2.7 State Update
61+
62+
On success, write to `.east/state.toml`:
63+
64+
```toml
65+
[build]
66+
last_build_dir = "build/default"
67+
last_preset = "default"
68+
last_source_dir = "/abs/path/to/source"
69+
last_elf = "build/default/app/firmware.elf"
70+
last_bin = "build/default/app/firmware.bin"
71+
last_hex = ""
72+
last_configured_at = "2026-04-09T12:34:56Z"
73+
```
74+
75+
On failure, state.toml is not touched.
76+
77+
## 3. Runner Trait
78+
79+
```rust
80+
#[async_trait]
81+
pub trait Runner: Send + Sync {
82+
fn name(&self) -> &str;
83+
fn kind(&self) -> RunnerKind;
84+
fn capabilities(&self) -> RunnerCapabilities;
85+
async fn flash(&self, ctx: &RunCtx, opts: &FlashOpts) -> Result<(), RunnerError>;
86+
async fn debug(&self, ctx: &RunCtx, opts: &DebugOpts) -> Result<DebugSession, RunnerError>;
87+
async fn attach(&self, ctx: &RunCtx, opts: &AttachOpts) -> Result<DebugSession, RunnerError>;
88+
async fn reset(&self, ctx: &RunCtx, opts: &ResetOpts) -> Result<(), RunnerError>;
89+
}
90+
91+
pub enum RunnerKind {
92+
External,
93+
OpenOcd,
94+
// Serial reserved for Phase 4
95+
}
96+
97+
pub struct RunnerCapabilities {
98+
pub flash: bool,
99+
pub debug: bool,
100+
pub attach: bool,
101+
pub reset: bool,
102+
pub erase: bool, // always false in Phase 3
103+
}
104+
```
105+
106+
**Key decisions:**
107+
108+
- `capabilities()` is synchronous. CLI checks before dispatch with a clean error message.
109+
- `debug()`/`attach()` return `DebugSession` (RAII guard holding OpenOCD child process).
110+
- `DebugSession` Drop: SIGTERM → 2s wait → SIGKILL (Unix); `taskkill /T /F` (Windows).
111+
- `RunCtx` is populated by CLI from state.toml and manifest. Runners do not search for artifacts.
112+
113+
```rust
114+
pub struct RunCtx<'a> {
115+
pub workspace: &'a Workspace,
116+
pub manifest: &'a Manifest,
117+
pub config: &'a Config,
118+
pub state: &'a State,
119+
pub elf_path: Option<&'a Path>,
120+
pub bin_path: Option<&'a Path>,
121+
pub hex_path: Option<&'a Path>,
122+
pub openocd_binary: &'a Path,
123+
pub gdb_binary: &'a Path,
124+
}
125+
```
126+
127+
## 4. OpenOCD Runner
128+
129+
### 4.1 Commands
130+
131+
| Operation | Command |
132+
|---|---|
133+
| flash | `openocd -f <cfg> -c "program <artifact> verify reset exit"` |
134+
| reset | `openocd -f <cfg> -c "init; reset; exit"` |
135+
| debug | Background: `openocd -f <cfg>`, then foreground: `<gdb> <elf> -ex "target remote :<port>" -ex "load"` |
136+
| attach | Same as debug but gdb omits `load` |
137+
138+
Flash artifact precedence: elf > hex > bin (first present wins).
139+
140+
### 4.2 Config Path
141+
142+
Uses `ManifestRelativePath` from Phase 2.5. The `runners:` declaration carries `declared_in`, same as `CommandDecl`.
143+
144+
### 4.3 Binary Resolution
145+
146+
**gdb:** `--gdb` CLI flag > `runner.openocd.gdb` config > `runner.<name>.gdb` config > `riscv64-unknown-elf-gdb` PATH lookup.
147+
148+
**OpenOCD:** Same pattern with `runner.openocd.binary`, default `openocd`.
149+
150+
### 4.4 Port Defaults
151+
152+
gdb: 3333, telnet: 4444, TCL: 6666. Configurable per runner via `runner.<name>.gdb_port` etc.
153+
154+
## 5. OpenOCD Output Classification
155+
156+
OpenOCD writes almost everything to stderr, including success. **Never** use "stderr non-empty" as a failure signal.
157+
158+
- Capture both streams line-by-line via `tokio::io::BufReader`.
159+
- Classify each line:
160+
- **Ready:** matches `Listening on port \d+ for gdb connections`.
161+
- **Error:** matches `^Error:` or contains `failed` at end with non-zero exit.
162+
- **Progress:** everything else (logged at `tracing::debug!`).
163+
- On failure, include last 20 lines of captured output in the error.
164+
- Implemented in `OpenOcdOutputClassifier` module with unit tests on real captured fixtures.
165+
166+
## 6. External Runner
167+
168+
```yaml
169+
runners:
170+
- name: custom-tool
171+
type: external
172+
flash:
173+
command: "my-flasher --elf ${runner.elf} --port ${config.runner.custom-tool.port}"
174+
reset:
175+
command: "my-flasher --reset"
176+
```
177+
178+
- Each capability (`flash`/`reset`/`debug`/`attach`) is an optional object with a `command` field.
179+
- Command rendered by template engine with `runner.*` namespace.
180+
- Shell execution reuses Phase 2 `exec:` code path (`sh -c` / `cmd /C`).
181+
182+
## 7. Template Engine Extension
183+
184+
New namespace: `runner.*`.
185+
186+
| Binding | Description |
187+
|---|---|
188+
| `runner.elf` | Absolute path to elf (or empty) |
189+
| `runner.bin` | Absolute path to bin (or empty) |
190+
| `runner.hex` | Absolute path to hex (or empty) |
191+
| `runner.workspace_root` | Workspace root absolute path |
192+
| `runner.build_dir` | Build directory from state.toml |
193+
| `runner.name` | Name of the invoked runner |
194+
195+
Missing key remains a hard error. Must not break existing template tests.
196+
197+
## 8. `.east/state.toml` Schema v1
198+
199+
```toml
200+
schema_version = 1
201+
202+
[build]
203+
last_build_dir = "build/default"
204+
last_preset = "default"
205+
last_source_dir = "/abs/path/to/source"
206+
last_elf = "build/default/app/firmware.elf"
207+
last_bin = ""
208+
last_hex = ""
209+
last_configured_at = "2026-04-09T12:34:56Z"
210+
211+
[runner]
212+
default = "wch-link"
213+
```
214+
215+
- **Location:** `<workspace_root>/.east/state.toml`.
216+
- **Ownership:** `east-workspace::state` module.
217+
- **Schema versioning:** checked on load; mismatch = error asking user to delete and rebuild.
218+
- **Atomic writes:** write to `.east/state.toml.tmp`, then rename.
219+
- **Missing file:** return `State::default()` with `schema_version = 1` and empty fields.
220+
221+
## 9. Manifest Runner Declaration
222+
223+
```yaml
224+
runners:
225+
- name: wch-link
226+
type: openocd
227+
config: openocd/wch-riscv.cfg
228+
gdb_port: 3333
229+
- name: custom-tool
230+
type: external
231+
flash:
232+
command: "..."
233+
```
234+
235+
**Validation:**
236+
237+
- `name` matches `[a-z][a-z0-9-]*`.
238+
- `type` is `openocd` or `external`. `serial` is reserved → "not yet implemented, reserved for Phase 4" error.
239+
- `openocd`: `config` field required.
240+
- `external`: at least one capability present.
241+
- Runner names unique in resolved manifest; collision = hard error.
242+
243+
## 10. Error Model
244+
245+
| Crate | Error Type | Key Variants |
246+
|---|---|---|
247+
| `east-build` | `BuildError` | `CmakeNotFound`, `CmakeVersionTooLow`, `SourceDirNotFound`, `ConfigureFailed`, `BuildFailed`, `NoArtifactsFound` |
248+
| `east-runner` | `RunnerError` | `ConfigFileNotFound`, `BinaryNotFound`, `SpawnFailed`, `NonZeroExit`, `StartupTimeout`, `ArtifactMissing`, `CapabilityUnsupported` |
249+
| `east-workspace` | `StateError` | `SchemaVersionMismatch`, `TomlParse`, `Io` |
250+
251+
All derive `miette::Diagnostic`.
252+
253+
## 11. Subprocess Lifecycle Rules
254+
255+
- **Spawn:** `tokio::process::Command` with `stdin(null)`, `stdout(piped)`, `stderr(piped)` for background OpenOCD; `Stdio::inherit()` for foreground gdb.
256+
- **Termination:** SIGTERM → 2s wait → SIGKILL (Unix); `taskkill /T /F` (Windows).
257+
- **Zombie prevention:** always `Child::wait()`, including kill paths.
258+
- **Windows:** `CREATE_NEW_PROCESS_GROUP` flag; CLI Ctrl+C handler delegates to `DebugSession`.
259+
- **Testability:** `SubprocessHost` trait with `spawn`, `wait_ready`, `kill_gracefully`. Mock impl for tests; production uses `tokio::process`.
260+
261+
## 12. Crate Dependency Graph
262+
263+
```
264+
east-cli ─┬─► east-command ─► east-manifest
265+
├─► east-config
266+
├─► east-workspace ─► east-manifest
267+
├─► east-vcs
268+
├─► east-build ─► east-workspace, east-config
269+
└─► east-runner ─► east-manifest, east-workspace, east-config, east-command
270+
```
271+
272+
**Rule:** `east-runner` MUST NOT depend on `east-build`. Runners receive paths, not build semantics.
273+
274+
## 13. Testing Strategy
275+
276+
- **CMake fixture:** `tests/fixtures/phase3/hello-cmake/` — tiny project producing `.elf` via host compiler.
277+
- **Fake OpenOCD:** scripts with variants: `success-flash`, `success-listen`, `error-cfg-missing`, `slow-start`.
278+
- **Fake gdb:** script that connects, prints banner, exits 0.
279+
- **OpenOCD output fixtures:** captured from real hardware, one file per scenario.
280+
- **CI cannot test real hardware.** Manual validation required with hardware log in dev notes.
281+
282+
## 14. Performance Bars
283+
284+
- `east --version` < 30 ms (lazy loading of build/runner).
285+
- `east build` overhead < 50 ms before CMake invoked.
286+
- No zombie processes after any test path.
287+
- Zero `unsafe` continues.

0 commit comments

Comments
 (0)