Extension failures
The [build] EXTENSION FAILED marker and the /etc/mp-extension-failures file — how a build reports extensions it could not install.
Installing a VS Code extension is a nice-to-have, not a prerequisite, so a miss must not fail the
build. But "must not fail the build" had degenerated into "is never reported". code-server does
not reliably exit non-zero on an unknown id, so a check of $? alone lets an
Extension 'x' not found scroll past as a success. A project would then run without the extension
it was configured with, and nothing anywhere would say so.
This is the second marker of the build log, and it works the same way as the
step markers: the script prints a prefixed line,
and the control plane greps it back out of the log it already stores. The emitter and the parser
live in one file (steps/extension-failures.ts) on purpose. A marker whose emitter and parser live
apart breaks silently. Module reference: steps.
The marker
/** Prefix every line reporting an extension the build could not install carries. */
export const EXTENSION_FAILED_MARKER = "[build] EXTENSION FAILED:"One line per failed extension, starting at column 0:
[build] EXTENSION FAILED: <id>
[build] EXTENSION FAILED: <id> — <reason>| Part | Value |
|---|---|
| Prefix | [build] EXTENSION FAILED:, followed by one space |
| Id | The extension id as passed to --install-extension, for example esbenp.prettier-vscode. It runs up to the first whitespace. |
| Reason | Optional. When present it follows — (space, em dash U+2014, space). It is free text for humans, and the parser ignores it. |
Emitting
export function extensionFailedLine(id: string, reason?: string): string {
return reason ? `${EXTENSION_FAILED_MARKER} ${id} — ${reason}` : `${EXTENSION_FAILED_MARKER} ${id}`
}
extensionFailedLine("acme.missing")
// → "[build] EXTENSION FAILED: acme.missing"
extensionFailedLine('"$_ext"', "not found on Open VSX")
// → '[build] EXTENSION FAILED: "$_ext" — not found on Open VSX'Unlike stepStartLine, this returns the text of the line, not an echo around it. The build
script decides how to print it. The id is interpolated as written, so pass a literal id or a shell
expansion ("$_ext"), never unvalidated user input on a path that the caller has not already quoted.
Parsing
const FAILED_RE = /^\[build\] EXTENSION FAILED: (\S+)/gm
export function parseFailedExtensions(logs: string): string[] {
return [...new Set(Array.from(logs.matchAll(FAILED_RE), (m) => m[1]))]
}- Start of line only (the
mflag with^). The marker travels in a log that may also quote it back, for example in an echo of the build command or a recap line. Matching mid-line would invent failures out of the recap.recap: [build] EXTENSION FAILED: a.byields nothing. - Script order, deduplicated. A retried install must not show the same extension twice in the UI.
parseFailedExtensions([
"[build] Installing extension: a.b",
"[build] EXTENSION FAILED: z.z — not found",
"[build] EXTENSION FAILED: a.a",
"[build] EXTENSION FAILED: z.z",
].join("\n"))
// → ["z.z", "a.a"]Spunto Lite's project page runs parseFailedExtensions over the current build's stored log, to flag
the configured extensions that the image does not contain.
The failures file
export const EXTENSION_FAILURES_FILE = "/etc/mp-extension-failures"This carries the same information as the marker, but on the image's side of the build instead of on its way out. It is a plain-text file in the image, with one extension id per line. It lets a worker, or anything running in it, tell what is missing without anyone re-reading the build log.
What the package does not prescribe
This format owns the wire bytes and nothing else. It does not decide whether the build should also
check that the extension landed on disk (Spunto Lite's original fix looked for
/opt/mp-extensions/<id>-*), what to write into the image, or when to give up. Those are the build
script's business.
Current status. The image script shipped by this package (buildImageScript, as of 0.10.0)
does not yet emit this marker and does not write EXTENSION_FAILURES_FILE. On a non-zero exit from
code-server --install-extension, it prints:
[build] Extension <id> failed (exit <n>) — ids are resolved against <registry>; an id that isn't published there can't be installed. ContinuingThat line does not match FAILED_RE. Until the generator emits extensionFailedLine(...),
parseFailedExtensions returns [] for builds produced by buildImageScript, and the failures
file is absent. The format above is the contract the reader already implements, and an emitter
has to follow it.
Build log markers
The ::spunto:step: protocol — the exact lines a build prints around each block, and how the control plane parses them back out of the log.
Project spec
The portable project file (kind "spunto/project") — every field, a complete example, legacy compatibility, and what deliberately does not travel.