Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions src/content/docs/recipes/build/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ charly box build --cache image [image...] # Image cache (read-only, de
charly box build --cache gha [image...] # GitHub Actions cache
charly box build --no-cache [image...] # Disable cache entirely
charly box build --jobs N [image...] # Max concurrent images per DAG level (0=auto from defaults.jobs, else 4)
charly box build --podman-jobs N [image...] # Max concurrent stages within a single build (0=auto: min(NCPU, defaults.stage_parallelism_cap))
charly box build --stage-parallelism N [image...] # Max concurrent stages within a single build (0=auto: min(NCPU, defaults.stage_parallelism_cap))
charly box build <image> --include-disabled # Build an `enabled: false` image without flipping authored config
charly box build <image> --dev-local-pkg # EVAL-BED ONLY: build localpkg candies (the charly toolchain) from LOCAL in-dev source, not the published release
```
Expand Down Expand Up @@ -170,18 +170,18 @@ cat .build/my-image/Containerfile # Inspect generated output
5. For each level: build images in parallel (up to `--jobs` concurrent, default 4)
6. After all builds: `charly box merge --all` (if `merge.auto` enabled, skipped for `--push`)

## Parallelism: `--jobs` vs `--podman-jobs`
## Parallelism: `--jobs` vs `--stage-parallelism`

charly exposes **two** parallelism knobs with distinct meanings:

| Flag | Env var | `defaults:` key | What it controls |
|---|---|---|---|
| `--jobs N` | `CHARLY_BUILD_JOBS` | `jobs` | **Outer** concurrency: how many charly-level images to build in parallel within a DAG level (e.g., when `charly box build` rebuilds the whole graph). |
| `--podman-jobs N` | `CHARLY_PODMAN_JOBS` | `stage_parallelism` (+ `stage_parallelism_cap`) | **Inner** concurrency: how many stages of a *single* multi-stage build run concurrently. It becomes the BuildKit daemon's `[worker.oci] max-parallelism`, applied when the daemon starts. |
| `--stage-parallelism N` | `CHARLY_STAGE_PARALLELISM` | `stage_parallelism` (+ `stage_parallelism_cap`) | **Inner** concurrency: how many stages of a *single* multi-stage build run concurrently. It becomes the BuildKit daemon's `[worker.oci] max-parallelism`, applied when the daemon starts. |

Both knobs are **config-driven**: precedence is **CLI flag → env → `defaults:`
in `charly.yml` → built-in fallback** (4). The inner auto value (when
`--podman-jobs` / `CHARLY_PODMAN_JOBS` / `defaults.stage_parallelism` are all unset) is
`--stage-parallelism` / `CHARLY_STAGE_PARALLELISM` / `defaults.stage_parallelism` are all unset) is
CPU-proportional, capped at `defaults.stage_parallelism_cap`: `min(NCPU, cap)`.
The cap is the operative ceiling, and it is the project's own to set. (The 20-run
race gate required before raising it on a new engine version is in `CHANGELOG/`.)
Expand All @@ -195,15 +195,15 @@ defaults:
```

```bash
charly box build <image> --podman-jobs 16 # fully parallel stages (override)
CHARLY_PODMAN_JOBS=8 charly update <image> --build # via env
charly box build <image> --podman-jobs 1 # fully serialised, worst-case debugging
charly box build <image> --stage-parallelism 16 # fully parallel stages (override)
CHARLY_STAGE_PARALLELISM=8 charly update <image> --build # via env
charly box build <image> --stage-parallelism 1 # fully serialised, worst-case debugging
```

Source: `candy/plugin-build/resolve.go` (`resolveDriveStageParallelism`, feeding
`buildkit.ResolveStageParallelism`; the host-resolved `StageParallelism` value is the
`MaxParallelism` the drive hands the daemon in `candy/plugin-build/buildkit.go`).
Covered by `buildkit/build_helpers_test.go` (`TestResolveStageParallelism`). The outer `--jobs` knob and the inner `--podman-jobs`
Covered by `buildkit/build_helpers_test.go` (`TestResolveStageParallelism`). The outer `--jobs` knob and the inner `--stage-parallelism`
are separate fields so the two semantics don't get conflated.

## Build-context excludes (`defaults.context_ignore`)
Expand All @@ -227,8 +227,9 @@ defaults:

Why it matters: the build context tar is streamed to the engine on **every**
build regardless of cache state, so excluding large never-COPYed directories is
the dominant warm-rebuild win. podman reads `.containerignore`; docker reads
`.dockerignore` — emitting both keeps the two engines in lockstep. Only add a
the dominant warm-rebuild win. BuildKit's dockerfile frontend reads `.dockerignore`;
`.containerignore` is the engine-native name kept for engine-native builds — emitting both
keeps the two readers in lockstep. Only add a
directory you've confirmed no Containerfile COPY/ADDs from (generated
Containerfiles COPY only from `candy/`, `templates/`, `.build/`). Source:
`candy/plugin-build/host_prep.go:writeContextIgnore` + `charly/host_build_buildengine.go`
Expand Down Expand Up @@ -286,8 +287,8 @@ what actually orders them; **within one image's tag rows**, the
*"creation time, not the tag"* across to the tag budget — it is true of the first and
false of the second.

Images referenced by a container (`podman ps -a`) are skipped, and `rmi` runs without
`-f` as a backstop.
Images referenced by a running container are skipped, and removal runs without `-f` as a
backstop (`charly clean` owns the prune; never call the engine's prune directly).

```yaml
# charly.yml — defaults:
Expand Down Expand Up @@ -343,7 +344,7 @@ Rationale:
a CalVer tag is self-describing and rotates naturally.

Any stray `:latest` tag in local storage is never refreshed by a new
build; prune it with `podman image prune` if you want it gone.
build; prune it with `charly clean` if you want it gone.

## Cache Efficiency

Expand All @@ -363,7 +364,7 @@ Three kinds of source changes are real cache invalidators — if you see a long

### What does NOT invalidate cache

- **CalVer tag shifts alone.** The Containerfile emits `ARG BASE_IMAGE=<registry>/<name>:<calver>` with a fresh CalVer on every generate. That ARG default appears in the Containerfile text but is not part of the cache key for subsequent RUN/COPY steps — podman/buildah resolves the FROM to an image SHA first, and caches downstream steps off that SHA. If the SHA is unchanged, cache hits. The cache cost comes from content changes in the layer itself, not the tag.
- **CalVer tag shifts alone.** The Containerfile emits `ARG BASE_IMAGE=<registry>/<name>:<calver>` with a fresh CalVer on every generate. That ARG default appears in the Containerfile text but is not part of the cache key for subsequent RUN/COPY steps — BuildKit resolves the FROM to an image digest first, and caches downstream steps off that digest. If the SHA is unchanged, cache hits. The cache cost comes from content changes in the layer itself, not the tag.
- **Baked `plan:` edits.** LABEL directives are emitted last in every final stage (after the last USER). Editing a baked step (a `check:`/`agent-check:` step, or a runtime-context `run:` step) — a common layer mutation — only re-runs the final LABEL block (~2 seconds on a 138-step stack like `immich-ml`). (Editing a build/deploy-context `run:` step changes the install timeline, so it re-runs that RUN/COPY instead.) See [`/charly-internals:generate-source`](/recipes/internals/generate-source/) for the rationale.
- **Re-running `charly box build` without source changes.** Fully cached; seconds to complete.

Expand All @@ -387,13 +388,13 @@ Three kinds of source changes are real cache invalidators — if you see a long

## Build Flow Details

**Internal base images** use exact CalVer tags in Containerfiles (`FROM ghcr.io/opencharly/fedora:2026.46.1415`). This ensures each image references the precise version of its parent. Both Docker and Podman resolve local images before pulling from registry.
**Internal base images** use exact CalVer tags in Containerfiles (`FROM ghcr.io/opencharly/fedora:2026.46.1415`), so each image names the precise version of its parent. **The builder does not resolve that tag through a local-image lookup**: BuildKit's OCI worker has no local-first resolution. The built image is stored in the configured RUN engine's store (`LoadExport`: load → tag → verify), where every later read of it — capability labels, deploy, status, inline merge, `charly clean` — resolves it; it is ALSO pushed to charly's local OCI registry, which the buildkitd mirrors `ghcr.io/opencharly` to, so a dependent box's `FROM ${BASE_IMAGE}` resolves a locally built base instead of the upstream registry (`opencharly/charly#937`, `opencharly/layer-charly-build#25`).

## Push Mode

BuildKit's image exporter is the ONE push path — `--output type=image,name=<ref>,push=true` —
and the only one that can push the multi-platform image a `--push` build produces. It
covers both `docker buildx --push` and `podman manifest push`.
replaces the engine-native push drivers — no per-engine push path remains.

Source: `candy/plugin-build/buildkit.go` (`pushOutput`).

Expand Down Expand Up @@ -532,11 +533,11 @@ Options, in order of least to most disruptive:

1. **Just retry the build.** Often the cache clears itself once the partial package finishes downloading or gets superseded by a checksum mismatch.
2. **Bump a content-hash on the builder layer** (e.g. `echo "" >> candy/pixi/charly.yml`) to evict the cache mount associated with that build-stage's hash, then revert the cosmetic change. Same workaround as the scratch-stage cache issue above.
3. **`podman image prune -af` is NOT the right hammer** — see next note.
3. **The engine's `image prune -af` is NOT the right hammer** — use `charly clean`; see next note.

### `podman image prune -af` caveat — removes tagged-but-idle images
### Engine-level `image prune -a -f` caveat — removes tagged-but-idle images

`podman image prune -a -f` will delete tagged images that are **not referenced by a running container**. That means the image you just built via `charly box build` — and haven't started as a container yet — is eligible for prune. Observed sequence: `charly box build` → `podman image prune -af` → `sudo podman load` (manual rootful refresh) fails with `image not known`. Protect the fresh build by starting a container or use `prune` with explicit filters instead.
An engine-level `image prune -a -f` deletes tagged images that are **not referenced by a running container**. That means the image you just built via `charly box build` — and haven't started as a container yet — is eligible for prune. Observed sequence: `charly box build` → an engine-level `image prune -af` → a manual rootful reload fails with `image not known`. Protect the fresh build by starting a container or use `prune` with explicit filters instead.

### Forcing a rebuild of one layer's scratch stage

Expand Down
14 changes: 13 additions & 1 deletion src/content/docs/recipes/coder/arch-coder.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ Arch Linux counterpart of [`/charly-coder:fedora-coder`](/recipes/coder/fedora-c

## Definition

The list below is the **resolved composition**, not the box's `refs` list: it names **32 candies** by
their candy names, of which **26 are the box's direct refs** in `box/arch-coder/charly.yml`. The other
six — `charly`, `layer-nodejs`, `rust`, `direnv`, `layer-gemini`, `google-cloud` — are still in the
closure but now arrive **transitively at the same tags** through the composed refs, after the `charly#739`
"ONE SOURCE per layer" rule dropped those six direct refs. That rule's own mapping, in the manifest's
comment, names the dropped **refs** (which is why the two spellings differ: the list uses candy names,
the mapping uses ref names): `layer-charly` ← `pod-charly-mcp`, `layer-nodejs` and `layer-rust` ←
`layer-language-runtimes`, `layer-direnv` ← `layer-agent-forwarding`, `layer-gemini` and
`layer-google-cloud` ← `layer-google-cloud-npm`. So "26 direct candies" above and the 32 names below
describe the same box at two depths, and the closure is unchanged.

```yaml
arch-coder:
base: arch # bare-local in the self-contained box/arch submodule
Expand Down Expand Up @@ -118,7 +129,8 @@ Candies in roughly the same groups as fedora-coder, with these Arch-specific not
- `gh` — GitHub CLI from Arch's `extra` repo (no third-party apt repo needed).
- `container-nesting` — Arch equivalents for podman + buildah + skopeo + fuse-overlayfs + crun.

The remaining 25+ candies are identical to fedora-coder at the *composition* level. Each candy's `pac:` section handles the Arch-specific package names.
The other 22 of the box's 26 direct candies are identical to fedora-coder at the *composition* level — the
four noted above are the direct entries that differ, and `nodejs` arrives inside `language-runtimes`. Each candy's `pac:` section handles the Arch-specific package names.

## Quick start

Expand Down
9 changes: 4 additions & 5 deletions src/content/docs/recipes/coder/charly-arch.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ Resolved OCI security label:
| `devices` | `[/dev/fuse, /dev/net/tun]` (from [`/charly-distros:container-nesting`](/recipes/distros/container-nesting/)) |
| `privileged` | `false` |

See [`/charly-openclaw:openclaw-desktop`](/recipes/openclaw/openclaw-desktop/) for the sibling rootless-first
box that proves this posture works under streaming-desktop +
nested-VM load, and [`/charly-distros:container-nesting`](/recipes/distros/container-nesting/) for the kernel
See [`/charly-distros:charly-fedora`](/recipes/distros/charly-fedora/) for the sibling rootless-first box and
[`/charly-distros:container-nesting`](/recipes/distros/container-nesting/) for the kernel
`mount_too_revealing()` RCA.

### Network + port publishing
Expand All @@ -73,8 +72,8 @@ mappings to work with. (That leg also handles host-networked containers
via `HostConfig.NetworkMode` detection — `ContainerInspection.IsHostNetworked()` in
`sdk/kit/checkvars.go` — so the bridge isn't strictly required
— but it remains the portable default.) If host-port 2222 is already taken by
another running box (canonical conflict: [`/charly-openclaw:openclaw-desktop`](/recipes/openclaw/openclaw-desktop/)
or any `selkies-desktop-*` variant), remap at config time:
another running box (canonical conflict: any
`selkies-desktop-*` variant), remap at config time:
`charly config charly-arch -p 2223:2222`.

### MCP gateway (charly-mcp)
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/recipes/core/charly-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ Kong `sep:"none"` on all `-e` flags means commas in values are preserved (no spl

**Tunnel persistence:** `charly config setup` automatically persists tunnel config from charly.yml back to charly.yml via `saveDeployState`. Tunnel is a deploy-time concern — see [`/charly-core:deploy`](/recipes/core/deploy/) for tunnel configuration.

**Tunnel is charly.yml-only:** `labels.go:238` deliberately skips parsing the `ai.opencharly.tunnel` OCI image label. Tunnel config is ONLY sourced from `charly.yml`. New instances created with `charly config setup -i <name>` do NOT inherit tunnel config from the base image's charly.yml entry — you must manually add `tunnel: {provider: tailscale, private: all}` to the instance's charly.yml entry, then re-run `charly config setup` to regenerate the quadlet with `ExecStartPost=tailscale serve` commands.
**Tunnel is charly.yml-only:** `sdk/deploykit/capabilities.go` deliberately skips parsing the `ai.opencharly.tunnel` OCI image label. Tunnel config is ONLY sourced from `charly.yml`. New instances created with `charly config setup -i <name>` do NOT inherit tunnel config from the base image's charly.yml entry — you must manually add `tunnel: {provider: tailscale, private: all}` to the instance's charly.yml entry, then re-run `charly config setup` to regenerate the quadlet with `ExecStartPost=tailscale serve` commands.

Source: `spec/hostenv/envfile.go` (`normalizeNoProxy`), `sdk/deploykit/deploy_state.go` (`MergeEnvVars`, `SaveDeployState`), `sep:"none"` in `candy/plugin-fleet/deploy_cmd.go` + `candy/plugin-pod/pod_cmd.go` (the former `charly/config_image.go`/`shell.go`/`start.go` are DELETED, K-wave 2).

Expand All @@ -587,7 +587,7 @@ Source: `spec/hostenv/envfile.go` (`normalizeNoProxy`), `sdk/deploykit/deploy_st
- [`/charly-build:settings`](/recipes/build/settings/) — Runtime settings (engine, run_mode, encrypted_storage_path)
- [`/charly-core:service`](/recipes/core/service/) — Service lifecycle (start, stop, status, logs)
- [`/charly-image:layer`](/recipes/image/layer/) — Volume, secret, `env_provide` / `env_require` / `env_accept` declarations, security resource cap fields, `service:` blocks
- [`/charly-image:image`](/recipes/image/image/) — Image composition, inheritance, OCI label emission, tunnel charly.yml-only note (`labels.go:238`)
- [`/charly-image:image`](/recipes/image/image/) — Image composition, inheritance, OCI label emission, tunnel charly.yml-only note (`sdk/deploykit/capabilities.go`)
- [`/charly-core:charly-doctor`](/recipes/core/charly-doctor/) — Host GPU/device detection driving `appendAutoDetectedEnv()` (DRINODE, HSA_OVERRIDE_GFX_VERSION)
- [`/charly-core:shell`](/recipes/core/shell/) — Interactive shells share the same `appendAutoDetectedEnv()` path
- [`/charly-selkies:chrome`](/recipes/selkies/chrome/) — Chrome HTTP proxy (`env_accept`), NO_PROXY auto-enrichment, cgroup resource caps
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/recipes/core/charly-doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Each check shows the binary path and version when available, or an install hint
## Cross-References

- [`/charly-automation:udev`](/recipes/automation/udev/) — install udev rules for GPU device access
- [`/charly-core:charly-config`](/recipes/core/charly-config/) — `engine.build`, `engine.run`, `secret_backend` settings, `--no-auto-detect` flag, DRINODE injection via `appendAutoDetectedEnv()`
- [`/charly-core:charly-config`](/recipes/core/charly-config/) — `engine.run`, `secret_backend` settings, `--no-auto-detect` flag, DRINODE injection via `appendAutoDetectedEnv()`
- [`/charly-automation:enc`](/recipes/automation/enc/) — credential lookup path behind the Secret Service collection + keyring-index checks; iteration-capable ssClient; broken-collection troubleshooting
- [`/charly-build:secrets`](/recipes/build/secrets/) — `charly secrets set/list/prune` commands referenced by the keyring-index remediation hint
- [`/charly-build:settings`](/recipes/build/settings/) — `keyring_collection_label`, `secret_backend`, and other runtime config keys surfaced by the Secret Storage checks
Expand Down
Loading
Loading