SpuntoSpunto/@spunto/build

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

steps/extension-failures.ts
/** 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>
PartValue
Prefix[build] EXTENSION FAILED:, followed by one space
IdThe extension id as passed to --install-extension, for example esbenp.prettier-vscode. It runs up to the first whitespace.
ReasonOptional. 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 m flag 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.b yields 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

naming/index.ts
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. Continuing

That 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.

Edit on GitHub

On this page