Skip to content
Merged
113 changes: 113 additions & 0 deletions docs/migrating-to-4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Migrating a project to labelle CLI 4.0

CLI 4.0 finishes [RFC #406](rfc-package-commands.md) with its last phase,
[RFC cli#471](https://github.com/labelle-toolkit/labelle-cli/issues/471): the
CLI core no longer knows any platform, backend or store. What it used to
hard-code now comes from two places:

- **The assembler** answers every backend and target question through
`labelle-assembler describe`: which backend package the project resolves
to, the generated target directory, the asset format, and whether the pair
is supported.
- **Providers** own everything platform-specific: iOS comes from
[labelle-ios](https://github.com/labelle-toolkit/labelle-ios), and SDL2 on
Windows from the `sdl2` provider in
[labelle-sdl](https://github.com/labelle-toolkit/labelle-sdl).

A `desktop` project on bgfx, raylib or sokol with `.gamepad = .none`, pinned
to assembler 0.118.0 or newer, needs no edit.

## Checklist

1. [Pin assembler 0.118.0 or newer](#1-pin-assembler-01180-or-newer).
2. [iOS: add labelle-ios](#2-ios-add-labelle-ios) if you build for `ios`.
3. [SDL2 on Windows: add the `sdl2` provider](#3-sdl2-add-the-sdl2-provider)
if a Windows build links SDL2.
4. [Check the smaller changes](#4-also-changed).

## 1. Pin assembler 0.118.0 or newer

`generate`, `describe` and `upgrade backend` need assembler protocol 7, which
shipped in assembler 0.118.0. An older `.assembler_version` fails with a
one-line error that names `labelle upgrade assembler`.

```zig
.assembler_version = "0.120.0",
```

`labelle upgrade all` bumps the assembler first, then asks it to bump the
backend (`labelle-assembler upgrade backend`). If the assembler refuses,
`project.labelle` is restored, unless you pass `--force`.

## 2. iOS: add labelle-ios

The core `labelle ios` command, the simulator launch and the forced sokol
backend are gone. The `ios` target belongs to the labelle-ios provider:

```zig
.plugins = .{
.{ .name = "ios", .repo = "github.com/labelle-toolkit/labelle-ios", .version = "0.1.0" },
},
```

Then use the normal commands: `labelle build|run|bundle --platform=ios`.

- `labelle ios …` is now an unknown command.
- The backend is no longer switched to sokol behind your back. Set
`.backend = .sokol` (or a `.backend_package` whose manifest supports iOS)
yourself; `describe` refuses an unsupported pair with its reason.
- iOS targets generate into `.labelle/<backend>_ios`.
- A leftover `.ios = .{ … }` block in `project.labelle` still parses and is
ignored. labelle-ios reads its settings from `providers/ios.json`.

## 3. SDL2: add the `sdl2` provider

The CLI no longer downloads SDL2 on Windows, sets `LABELLE_SDL2_LIB`, or
copies `SDL2.dll` beside the game. Any of these replaces it:

- Add the provider. It provisions SDL2 on Windows and stages the DLLs, and
does nothing on macOS or Linux:

```zig
.{ .name = "sdl2", .repo = "github.com/labelle-toolkit/labelle-sdl", .version = "0.4.0" },
```

`labelle sdl2 doctor` checks the install; `labelle sdl2 install` fetches it.
- Point `LABELLE_SDL2_LIB` at your own SDL2 MinGW `lib` directory.
- Turn the gamepad off with `.gamepad = .none`, if you don't use one.

Without any of them, a Windows build that needs SDL2 stops with one line:
`SDL2 not found: set LABELLE_SDL2_LIB or use .gamepad = .none`.
`labelle doctor` prints the same advice when the project links SDL2 and
doesn't list `sdl2`.

## 4. Also changed

- **Target directory.** The generated directory is the one the assembler
reports. For a third-party `.backend_package` without a matching
`.backend`, that is `.labelle/<package>_<target>`, where 3.x wrongly used
`.labelle/bgfx_<target>`.
- **Unknown targets.** The assembler judges them. A name no provider declares
and the assembler can't generate for stops with describe's reason, for
example: `backend 'bgfx' has no target 'probe-target': this assembler
generates for desktop ios android wasm`.
- **`labelle astc --backend`** is accepted and ignored, with a deprecation
line. Texture capabilities come from the project's backend package, and for
the `.backend` shorthand `describe` locates it, so the declared default
block is used instead of the conservative one.
- **`.platform`** must be an enum literal (`.platform = .desktop`); the CLI
reads it as a target name.
- **`labelle doctor --fix`** has nothing left to fix in the core. Use each
provider's `labelle <namespace> doctor --fix`.
- **`labelle test`** always runs the generated tests in `.labelle/tests/`.
- **`--docker`** refuses macOS targets, including a bare `--docker` on a Mac
(since 3.1). Linux and Windows cross builds are unchanged.

## Troubleshooting

| Message | Fix |
|---|---|
| `assembler … protocol … labelle upgrade assembler` | [Step 1](#1-pin-assembler-01180-or-newer) |
| `unknown command 'ios'` | [Step 2](#2-ios-add-labelle-ios) |
| `SDL2 not found: set LABELLE_SDL2_LIB …` | [Step 3](#3-sdl2-add-the-sdl2-provider) |
| `backend '…' cannot build target '…'` | Pick a backend that supports the target, or add the provider that owns it |
4 changes: 2 additions & 2 deletions docs/provider-contract-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Every field below is required on the wires that define it, except the optional `
| `run` | **Optional**, **wire `1.2.0`+**: present on every `run`-step hook context (any phase) and absent (never null) everywhere else. An object with the required keys `env`, `args` and `timeout_ms`, plus `watch` from wire `1.3.0` and `outcome_file` from wire `1.5.0` (see below) |
| `cache_dir` | **Wire `1.3.0`+, required there, never null**: the provider's persistent cache directory, on every context (commands and hooks). See [Provider cache](#provider-cache). The key doesn't exist below `1.3.0` |
| `env_file` | **Wire `1.3.0`+, required there**: on a `before generate`, `after generate` or `before build` hook, an absolute path the hook may write its environment contribution to; null on every other hook and on commands. See [Environment contributions](#environment-contributions). The key doesn't exist below `1.3.0` |
| `final_step` | **Wire `1.4.0`+, required there**: on a hook, the last lifecycle step of the CLI command that runs it (see [§6](#6-hook-execution)): `"generate"` (`labelle generate`, and the legacy `labelle ios` subcommand, which only runs the generate hooks), `"build"`, `"run"` (`labelle run`, every watched rebuild included) or `"bundle"`. It is always a step the hook's own step leads to: any value on a `generate` hook, never `"generate"` on a `build` hook, and exactly its own step on a `run` or `bundle` hook; anything else is an error. Null on a command. The key doesn't exist below `1.4.0` |
| `final_step` | **Wire `1.4.0`+, required there**: on a hook, the last lifecycle step of the CLI command that runs it (see [§6](#6-hook-execution)): `"generate"` (`labelle generate`), `"build"`, `"run"` (`labelle run`, every watched rebuild included) or `"bundle"`. It is always a step the hook's own step leads to: any value on a `generate` hook, never `"generate"` on a `build` hook, and exactly its own step on a `run` or `bundle` hook; anything else is an error. Null on a command. The key doesn't exist below `1.4.0` |

The `run` object holds the `labelle run` options for a hook that wraps or replaces the launch:

Expand Down Expand Up @@ -124,7 +124,7 @@ A `before generate`, `after generate` or `before build` hook receives `env_file`

**Scope.** The merged environment reaches every later zig invocation and hook process of the same command: the generation-time fingerprint pass (`zig build --list-steps`, which configures the generated build and so is the first toolchain consumer: a `before generate` contribution reaches it, an `after generate` one does not), the core `zig build` compile, and every later hook and replacement (`after generate`, `before`/`replace`/`after build`, `bundle` and `run` hooks). It never reaches the build of a provider's own tool (`zig build <build_step>` in the provider's package), which runs on the plain inherited environment plus the CLI's cache variables, so a provider cannot change how other providers' tools are built. The game the core `labelle run` launches keeps its own environment (the inherited one plus the run options); a provider that must reach the game uses a `run` replacement. The core stages no runtime library beside a built exe: it only merges the contribution into the environment above. Staging one (a DLL next to a desktop exe, say) is a provider's job, in an `after build` hook, which sees the merged environment as the compile did; the `sdl2` provider's `stage` hook copies `SDL2.dll` this way (cli#471 S4).

**No silent bypass.** A build path that cannot carry a provider's contribution or its optimize default refuses before any hook, generation or build, naming the hook or package. `--docker` builds in a container the contributions never reach, so a `build`, `run` or `bundle` (any command that reaches the container build) whose plan has a hook that may contribute (one in a contributing slot whose provider negotiates `1.3.0` or newer) is refused; `labelle generate --docker` is not, since it stops after generation and its host-side fingerprint pass gets the contributions, and neither is a build the target owner replaces, since its `replace build` hook gets them like every hook: `--docker doesn't carry provider environment contributions; build without --docker`. (`--docker` does pass the effective optimize mode as `-Doptimize`.) The legacy `labelle ios` subcommand runs its own `zig build` after generation, with neither, so it is refused when the plan has such a hook or the target's owner declares an optimize default; `labelle build --platform=ios` carries both.
**No silent bypass.** A build path that cannot carry a provider's contribution or its optimize default refuses before any hook, generation or build, naming the hook or package. `--docker` builds in a container the contributions never reach, so a `build`, `run` or `bundle` (any command that reaches the container build) whose plan has a hook that may contribute (one in a contributing slot whose provider negotiates `1.3.0` or newer) is refused; `labelle generate --docker` is not, since it stops after generation and its host-side fingerprint pass gets the contributions, and neither is a build the target owner replaces, since its `replace build` hook gets them like every hook: `--docker doesn't carry provider environment contributions; build without --docker`. (`--docker` does pass the effective optimize mode as `-Doptimize`.) The legacy `labelle ios` subcommand, which ran its own `zig build` carrying neither and was refused for both, left the core with RFC cli#471 I5; `labelle build --platform=ios` carries both.

### Run outcome

Expand Down
16 changes: 7 additions & 9 deletions docs/provider-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,15 +351,13 @@ build` hook signed, stripped or patched in `zig-out/` is what runs.

- No JSON progress relay or validation for hook (or command) stdout yet: the
child's stdio is inherited exactly as for provider commands.
- The legacy `labelle ios …` subcommand handler is not a hook point for
`build`/`run`; it only shares the `generate` hooks. It leaves with platform
extraction (its target already goes through the resolver, so it needs the
pinned provider like `--platform=<t>`). `labelle android …` left with
cli#405: it is the `android` provider's namespace, and `--platform=android`
runs the ordinary hook points.
- A provider target other than the legacy `ios` run branch must
replace `run` (`NoRunReplacement`, [provider
targets](provider-targets.md#labelle-run)).
- `labelle android …` left with cli#405 and `labelle ios …` with RFC cli#471
I5: neither is a built-in, and `--platform=android` / `--platform=ios` run
the ordinary hook points of their pinned provider
([labelle-ios](https://github.com/labelle-toolkit/labelle-ios) for `ios`).
- Every provider target must replace `run` (`NoRunReplacement`, [provider
targets](provider-targets.md#labelle-run)); the core has no launch of its
own for any of them.
- `labelle bundle` for the core `desktop` target is still refused on Linux
and Windows — before any install or build — because the core packager is
macOS-only and no hook can replace it (nobody may own `desktop`). A
Expand Down
13 changes: 7 additions & 6 deletions docs/provider-local-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ Run `labelle <namespace> <command> [arguments...]` from a project directory or
one of its descendants. Discovery walks to the nearest `project.labelle` and
reads manifests from that project's `.plugins`; it never searches global pins.
Built-ins take precedence, followed by package namespaces, then directory
shorthand. A package cannot claim a built-in namespace. The existing `ios`
and `wasm` commands remain reserved until their extraction lands; their
target already resolves like `--platform=<t>` does, through the pinned
provider that declares it ([provider targets](provider-targets.md)).
`android` left the reserved list with its extraction (cli#405): the
`android` package declares it as its namespace. A first word that is no
shorthand. A package cannot claim a built-in namespace. No platform word
is reserved any more: `android` left the list with its extraction
(cli#405; the `android` package declares it as its namespace), `wasm` with
the legacy browser commands (RFC cli#466 PR B), and `ios` when iOS moved
into [labelle-ios](https://github.com/labelle-toolkit/labelle-ios) (RFC
cli#471 I5). Their targets resolve like any other, through the pinned
provider that declares them ([provider targets](provider-targets.md)). A first word that is no
built-in, no pinned package's namespace and no directory is an unknown
command; when the cached schema-2 registry names a package declaring it as
a namespace, the diagnostic says so instead (`(registry: <package>)`).
Expand Down
Loading
Loading