Help us shape the future of Go! Take the 2026 Go Developer Survey

build

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: BSD-3-Clause

README

build

Docs CI Go Reference status

wctr build: build OCI images from a Containerfile (or a Dockerfile, same syntax) for the go-wasmcontainers engine, whose containers are WASI preview 1 (wasip1/wasm) modules. Pure Go (CGO_ENABLED=0), so it also runs in the browser playground. The builder is a separate module that drives the engine through its API, the way BuildKit sits beside a container daemon: each step runs in a container made from the previous step's image, which is then committed.

Status: M1. Multi-stage builds, the build cache, .containerignore, and every instruction below are done, and tested against the real engine (v0.17.0, in-process, wazero), which serves every endpoint a build needs (docs/ENGINE.md). Shell-form RUN runs /bin/sh from the shell base image through the engine's WASIX process subset (engine v0.17.0 or later). wctr-build works with a wctrd of v0.12.0 or later (v0.17.0 for shell-form RUN); cli.ForEngine plugs the command into wctr through its extension hook.

Demo

$ go install github.com/go-wasmcontainers/build/cmd/wctr-build@latest
$ cat Containerfile
FROM scratch AS tools
COPY tool.wasm /bin/tool.wasm
ARG GREETING=hi
RUN ["/bin/tool.wasm", "write", "/out/greeting", "made at build time"]

FROM scratch
COPY --from=tools /bin/tool.wasm /bin/
COPY --from=tools /out/ /data/
ENTRYPOINT ["/bin/tool.wasm", "cat"]
CMD ["/data/greeting"]
$ wctr-build -H "$WCTR_HOST" -t lab/app:v1 .
[1] STEP 1/5: FROM scratch
…
Successfully built 0f32949b3a81
Successfully tagged lab/app:v1
$ wctr run --rm lab/app:v1
made at build time

This sequence is TestMultiStageBuildAndRun in builder, which builds the Go test program testdata/wasm/tool for wasip1 and runs the result.

Command

wctr build [OPTIONS] PATH | - (standalone: wctr-build, with -H/--host unix:///path, else $WCTR_HOST, else unix:///var/run/wctr.sock).

Flag
-t, --tag NAME[:TAG] name the image (repeatable)
-f, --file FILE the Containerfile (-: standard input). Default PATH/Containerfile, else PATH/Dockerfile
--build-arg K=V a build argument (K alone: from the environment)
--target STAGE build up to this stage
--no-cache run every step
--progress plain plain progress (the only kind; auto is accepted)
-q, --quiet print only the image ID

PATH is the build context, sent without what its .containerignore (else .dockerignore) excludes; - reads the context as a tar on standard input, the Containerfile inside it.

Instructions

Instruction Support
FROM [--platform=wasip1/wasm] IMAGE [AS NAME] images are pulled for wasip1/wasm (another platform is an error); scratch; earlier stages; ARGs declared before the first FROM; ONBUILD triggers of the base run after it
ARG, ENV, LABEL the reference's scoping: global ARGs, re-declaration in a stage, ENV over ARG, predefined proxy ARGs (out of the cache key) and TARGETPLATFORM & co.
RUN ["prog", "arg"] runs a WebAssembly program in the step's container; --network=none
RUN cmd (shell form) runs SHELL (default ["/bin/sh", "-c"]) with the command: on the shell base image (FROM shell, ghcr.io/go-wasmcontainers/shell) pipes, redirections, &&, command substitution and coreutils work, e.g. RUN echo hi > /x && cat /x | wc -c (engine v0.17.0 or later, WASIX process subset). In an image without a runnable shell, the build stops with "RUN in shell form needs the shell "/bin/sh" … the image has no shell, or the engine cannot spawn processes yet". A command the shell cannot find is the shell's status 127, reported as such.
COPY, ADD from the context or --from= a stage (name or index) or an image; wildcards (filepath.Match), directory contents, trailing /, relative destinations against WORKDIR, --chown=UID[:GID] (numeric), --chmod=OCTAL, --link (accepted, no effect); ADD unpacks local tar, tar.gz, tar.bz2 archives (--unpack=false to keep them)
WORKDIR relative to the previous one; created when missing
CMD, ENTRYPOINT, SHELL, USER, EXPOSE, VOLUME, LABEL, MAINTAINER recorded in the image (a new ENTRYPOINT clears an inherited CMD); USER is recorded only: WASI has no users
STOPSIGNAL, HEALTHCHECK recorded, with a warning: a WebAssembly guest gets no signals, and the engine runs no health checks yet
ONBUILD recorded; run when the image is used as a base
Not yet here-documents, RUN --mount, ADD from a URL or Git, COPY --parents/--exclude, --chown by name, symbolic --chmod, xz/zstd archives in ADD — each is a precise error, never ignored

Every step adds one history entry (created_by like /bin/sh -c #(nop) ENV A=1 for metadata, COPY file:<digest> in /dst); metadata steps add an empty layer.

Cache

A step is reused when the parent image, the configuration it produces, the instruction (after expansion) and the digest of its inputs are the same: for COPY/ADD the names, modes, owners, links and contents of what is copied (not modification times, as the reference says); for RUN the command, its build arguments (so a changed --build-arg misses from the first RUN after the ARG, as the reference describes), and its network. A hit is used only after the engine confirms the image still exists. The cache is kept in $XDG_CACHE_HOME/wasmcontainers/build-cache.json (0600, bounded), in memory in a browser.

Packages

Package What it does
parser Containerfile/Dockerfile → instructions (directives, comments, continuation, exec and shell forms, flags, ENV/LABEL/ARG/HEALTHCHECK/ONBUILD); BuildKit's golden parser corpus passes unchanged
expand variable substitution as the reference specifies ($v, ${v}, ${v:-w} … ${v//p/r}), word splitting; nothing is executed
ignore .containerignore / .dockerignore matching
buildcontext the build context in memory, from a tar; Pack sends a directory through an os.Root
driver the engine API client the builder uses (Engine interface)
builder the build: stages, instructions, layers, cache
cli the cobra command, Command() for wctr build
cmd/wctr-build the standalone program

Security

The Containerfile and the build context are untrusted input.

  • Nothing on the host but the context: the builder reads the context from a tar held in memory; the client sends a directory through an os.Root, so no file outside it is read, whatever links it holds; symbolic links are sent as links, never followed, and owners are not sent (files from the context belong to root unless --chown).
  • Paths: context names that are absolute, hold .. or NUL are refused; COPY sources lose .. and a leading / (as the reference says); a source that goes through a symbolic link is followed only inside the context — a link to an absolute path or above the root is an error; destinations are cleaned and absolute.
  • Bounds: context 256 MiB / 65536 entries / 4096-byte names / depth 128; one COPY layer the same; ADD archives stop at the same size whatever their compression ratio; Containerfile 1 MiB, 4096 instructions; 1000 steps per build (ONBUILD triggers counted); 127 layers; one expanded word 1 MiB, ${} nesting 32; a stage's ARG and ENV values 1 MiB together, an image configuration 1 MiB.
  • Expansion never re-scans a value (no injection), runs nothing, and ${v//p/r} inserts r literally.
  • No network from the builder: ADD from a URL is refused; a RUN step's network is the engine's, --network=none honoured, host refused.
  • Fuzzed in CI: the parser, the expander, the ignore matcher, the context reader. govulncheck runs on every change.

Performance

go test -run '^$' -bench Build -cpu 1 ./builder (Apple M4 Max, go1.27.3; load average 66 when measured, so indicative only): a seven-instruction build with one RUN takes 61.7 ms cold (--no-cache, module already compiled by the engine) and 4.8 ms when every step is a cache hit (one image inspect per step), 12.8× faster. Layers are computed in memory from the context, bounded as above. Not yet compared with BuildKit on the same Containerfile — an open item.

In the browser, wctr build costs 0.98 MB of js/wasm: a program holding the engine's (v0.12.0) wctr command tree is 13,777,834 bytes, 14,757,857 with the build command added (go1.27.3, GOOS=js).

Limitations

  • An engine of v0.12.0 or later is required (the endpoints in docs/ENGINE.md); from v0.14.0 the WORKDIR check is a stat-only HEAD that moves no tar (tested on an 8 MiB directory).
  • Shell-form RUN needs engine v0.17.0 or later and an image with a shell (the shell base image); the shell has no signals, job control or process substitution (see its README).
  • A step's output streams as it runs (the log stream is followed from before the start, so no line is lost); stdout and stderr are printed as they come, without a per-step prefix.
  • Until ghcr.io/go-wasmcontainers/shell is published, the tests build that image from a pinned commit of go-wasmcontainers/shell ($WCTR_SHELL_SRC; CI sets it and fails rather than skip).
  • RUN pkgx install … (installing go-pkgx bottles) is a later goal; what it needs is in docs/ENGINE.md.

References

Every format is implemented from its specification: docs/REFERENCES.md, including the measurement behind writing the parser rather than importing BuildKit's (+6.1 MB to a js/wasm binary). Docker is a trademark of Docker, Inc.; this project is not affiliated with it. "Dockerfile" is used only as the name of the file format it reads, and the engine API is described as compatible with a subset of the Docker Engine API.

License

BSD 3-Clause, see LICENSE. The golden test files copied from BuildKit under */testdata/buildkit are Apache-2.0 (see the NOTICE and LICENSE beside them).

Directories

Path Synopsis
Package buildcontext holds a build context: the files a Containerfile's COPY and ADD read, received as a tar stream (from wctr, or from a stage or image through the engine) and kept in memory.
Package buildcontext holds a build context: the files a Containerfile's COPY and ADD read, received as a tar stream (from wctr, or from a stage or image through the engine) and kept in memory.
Package builder builds an OCI image from a Containerfile by driving the engine: each step runs in a container made from the previous step's image, which is then committed — RUN executes a WebAssembly program there, COPY and ADD extract a computed layer into it, and metadata instructions (ENV, LABEL, CMD, …) commit an empty layer with the new configuration, so the image's history has one entry per instruction.
Package builder builds an OCI image from a Containerfile by driving the engine: each step runs in a container made from the previous step's image, which is then committed — RUN executes a WebAssembly program there, COPY and ADD extract a computed layer into it, and metadata instructions (ENV, LABEL, CMD, …) commit an empty layer with the new configuration, so the image's history has one entry per instruction.
Package cli is the `build` command: `wctr build` (ForEngine, for wctr's extension hook), and the standalone `wctr-build`.
Package cli is the `build` command: `wctr build` (ForEngine, for wctr's extension hook), and the standalone `wctr-build`.
cmd
wctr-build command
Command wctr-build builds an image from a Containerfile with a wctrd engine: `wctr-build -H unix:///path -t name .`.
Command wctr-build builds an image from a Containerfile with a wctrd engine: `wctr-build -H unix:///path -t name .`.
Package driver is the builder's view of the engine: the operations a build needs (inspect, pull, load, tag, create, archive in and out, run, commit), and Client, which performs them over the engine's API — the subset of the Docker Engine API that wctrd serves, plus the endpoints a builder needs from it (commit, tag, load, container archives).
Package driver is the builder's view of the engine: the operations a build needs (inspect, pull, load, tag, create, archive in and out, run, commit), and Client, which performs them over the engine's API — the subset of the Docker Engine API that wctrd serves, plus the endpoints a builder needs from it (commit, tag, load, container archives).
Package expand substitutes variables in Containerfile words the way the Dockerfile reference's "Environment replacement" section specifies, with the grammar of BuildKit's frontend/dockerfile/shell lexer: single and double quotes, the escape token, $name and ${name}, and the modifiers ${name:-word} ${name-word} ${name:+word} ${name+word} ${name?word} ${name:?word} ${name#pattern} ${name##pattern} ${name%pattern} ${name%%pattern} ${name/pattern/repl} ${name//pattern/repl}.
Package expand substitutes variables in Containerfile words the way the Dockerfile reference's "Environment replacement" section specifies, with the grammar of BuildKit's frontend/dockerfile/shell lexer: single and double quotes, the escape token, $name and ${name}, and the modifiers ${name:-word} ${name-word} ${name:+word} ${name+word} ${name?word} ${name:?word} ${name#pattern} ${name##pattern} ${name%pattern} ${name%%pattern} ${name/pattern/repl} ${name//pattern/repl}.
Package ignore reads .containerignore / .dockerignore files and matches build-context paths against them, with the semantics of containerignore(5) and moby/patternmatcher: Go filepath.Match syntax plus "**" (any number of directories, including none), "!" exceptions where the last matching line wins, patterns cleaned with path.Clean and a leading "/" dropped, and a path excluded when it or any of its parent directories matches.
Package ignore reads .containerignore / .dockerignore files and matches build-context paths against them, with the semantics of containerignore(5) and moby/patternmatcher: Go filepath.Match syntax plus "**" (any number of directories, including none), "!" exceptions where the last matching line wins, patterns cleaned with path.Clean and a leading "/" dropped, and a path excluded when it or any of its parent directories matches.
internal
enginetest
Package enginetest is an in-process engine for the builder's tests: the real engine (daemon, API server, wazero executor) at the version in go.mod (New), and, for comparison, the same engine behind this package's prototype of the endpoints a builder needs (NewPrototype):
Package enginetest is an in-process engine for the builder's tests: the real engine (daemon, API server, wazero executor) at the version in go.mod (New), and, for comparison, the same engine behind this package's prototype of the endpoints a builder needs (NewPrototype):
Package parser reads a Containerfile (a Dockerfile: same syntax) into a list of instructions, following the grammar of the Dockerfile reference and of BuildKit's frontend/dockerfile/parser: parser directives (syntax, escape, check) before anything else, comments, line continuation with the escape token, and per-instruction argument forms (JSON array or shell string, whitespace lists, key=value pairs, ONBUILD sub-instructions, HEALTHCHECK types).
Package parser reads a Containerfile (a Dockerfile: same syntax) into a list of instructions, following the grammar of the Dockerfile reference and of BuildKit's frontend/dockerfile/parser: parser directives (syntax, escape, check) before anything else, comments, line continuation with the escape token, and per-instruction argument forms (JSON array or shell string, whitespace lists, key=value pairs, ONBUILD sub-instructions, HEALTHCHECK types).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL