Skip to content

BUILD language reference

A BUILD file is a Starlark program that declares named targets. It may be named BUILD.vms instead, where a build directory beside it would clash with BUILD; see workspaces. Use lists, dictionaries, functions, and comprehensions to compose declarations. Commands run inside build containers through run() steps.

A mistake in how a BUILD file declares its targets, such as a misspelled argument or a value of the wrong type, fails the command as the file loads, before anything runs, with an error that names the file, line and column of the call that made it. No string given to a target or step, such as a name, an argument, a path, an owner or a variable, may hold a NUL byte, which can’t reach a container.

Target names and references

Every target has a unique, nonempty name within its BUILD file. Names cannot contain : or /, and scratch is reserved for the empty image. Declarations register targets; they do not return artifact objects. Refer to a target by its name or a label, optionally wrapped in dep().

Declare local dependencies before the targets that use them. References produced by a function of ctx can name later targets; dependency cycles are errors.

TargetProducesCan be a baseCan be an add() source
pull()ImageYesNo
glob()FilesetNoYes
task() with outputFilesetNoYes
task() without outputSuccessful checkNoNo
layer()ImageYesNo
index()Image indexNoNo
publication()A release, published by vamos publishNoNo

No target can depend on a publication(), which only the command line names. All target kinds except pull() accept params, a dictionary of parameter declarations. Target attributes other than name and params can be functions of ctx. See configuration for param(), dep(), arg(), and platform configuration.

pull()

pull(name = "node", image = "docker.io/library/node", tag = "24-alpine")
ArgumentMeaningDefault
nameTarget nameRequired
imageRepository name without a tag or digestRequired
tagRepository tag"latest"

A pull fetches the image for its configured platform. If the tag names an index, it selects the matching image. A single-platform image must match the configured platform; a mismatch is an error. Use fully qualified repository names to make the source explicit. An image’s tag belongs in tag, rather than inside image. Pull targets have no user parameters; only their platform configures them. Previously resolved tags are reused until explicitly rebuilt: a tag that names an index is resolved once, so a platform built for later gets its image from the index the tag named then, from the store if it holds it, or else from the registry. A pull whose image its registry doesn’t have, or won’t give, fails, suggesting to check the name, and to log in if the image is private; one whose registry can’t be reached fails, saying why.

glob()

glob(
name = "source",
include = ["src/**/*.ts", "package.json"],
exclude = ["**/*.test.ts"],
)

A glob selects files relative to the directory containing its BUILD file. name and a nonempty include list are required. Other selection arguments and their defaults are listed under file selection. Globs provide input filesets to add(); they cannot be named directly by vamos build or vamos export.

task()

task(
name = "compile",
base = "tools",
steps = [
add("source", "/workspace"),
workdir("/workspace"),
run(["npm", "run", "build"]),
],
source = "/workspace/dist",
include = ["**/*.js"],
)
ArgumentMeaningDefault
nameTarget nameRequired
baseA pull or layer reference, or "scratch"Required
stepsSteps applied in order[]
sourceAbsolute container directory to collect output fromOmitted: no output
includeNonempty list of output patterns relative to sourceRequired with source
Selection optionsExclusions, modes, and limitsSee file selection

Provide source and include together, or omit both for a task that only runs checks. Selection options require output to be declared. Output files keep their paths relative to source. For example, source = "/workspace/dist" and include = ["js/main.js"] produce the entry js/main.js. Only selected output survives the task’s temporary container.

A task inherits its base’s working directory and environment. Its base may use another platform, such as dep("tools", platform = EXEC_PLATFORM) for native cross-compilation. See platforms.

layer()

layer(
name = "app",
base = "runtime",
steps = [add("compile", "/app"), workdir("/app")],
command = ["node", "main.js"],
)
ArgumentMeaningDefault
nameTarget nameRequired
baseA pull or layer reference, or "scratch"Required
stepsSteps applied in order[]
commandImage’s default command as an argument listInherited from the base
entrypointImage’s entrypoint as an argument listInherited from the base

A layer keeps the base’s files and runtime configuration and applies its own changes. command sets the image’s command; it does not run while building. It follows the entrypoint, if one exists, as Docker’s CMD does. An omitted or empty command leaves the inherited command in place.

entrypoint sets the program the image runs, as Docker’s ENTRYPOINT does: docker run arguments are appended to it rather than replacing it.

layer(
name = "server",
base = "jre",
steps = [add("jar", "/app")],
entrypoint = ["java", "-jar", "/app/server.jar"],
)

docker run server --port 9 then runs java -jar /app/server.jar --port 9. As in a Dockerfile, giving entrypoint drops the inherited command, which would otherwise become its arguments; give command as well for default arguments. entrypoint = [] removes the inherited entrypoint, and an omitted entrypoint keeps it. A layer’s base must have the same platform as the layer.

index()

index(
name = "release",
layer = "app",
platforms = ["linux/amd64", "linux/arm64"],
)

name and layer are required. With a single pull or layer reference, platforms must be a nonempty list. vamos configures that target once for each listed platform and preserves the list’s order in the index.

To use a different target for each platform, give layer as a nonempty dictionary and omit platforms:

index(
name = "release",
layer = {
"linux/amd64": "app-amd64",
"linux/arm64": "app-arm64",
},
)

Each platform must be unique. A dep() in an index may pass user parameters but cannot specify platform; the list or dictionary already determines each image’s platform. An index cannot contain another index.

publication()

publication(
name = "release",
checks = ["lint", "test"],
images = {
"ghcr.io/team/app": "app",
"docker.io/team/app": "app",
"ghcr.io/team/worker": "worker",
},
)
ArgumentMeaningDefault
nameTarget nameRequired
imagesDictionary from each repository to the pull, layer or index published thereRequired
checksTasks that must succeed before anything is published[]

A publication declares a release: what must succeed, and which images go where. vamos publish builds every check and image in one run, and only if all of them succeed publishes each image to its repository under every --tag given; vamos build builds them and publishes nothing.

images is keyed by repository, a lowercase registry repository name without a tag or digest, since the tags come from the command line. It must not be empty, and two keys must not name the same repository, as busybox and docker.io/library/busybox do. Keying by repository lets one target go to several repositories, as a release mirrored to more than one registry needs, while a repository gets one image. Each check must be a task(), and each image a pull(), layer() or index(). Checks and images are configured as the publication names them, as a dependency is, so a dep() can pass them parameters.

Like other targets, a publication takes params, set with --set, and its checks and images can be functions of ctx, so that a parameter can choose its repositories:

publication(
name = "release",
params = {"env": param(default = "staging", values = ["staging", "production"])},
images = lambda ctx: {
"registry.example/" + ctx.env + "/app": "app",
},
)

A publication is a root only: no target can name it as a base, an add() source, an index’s image or another publication’s check or image. Its own result is never cached, so publishing it again pushes again, while its checks and images are reused from the build cache as usual.

Steps

Put steps in the steps list of a task or layer; a step made anywhere else is an error. They execute in order, and later steps see earlier changes.

add(source, dest, owner = "", group = "")

Copy a glob’s or task’s fileset beneath an absolute container directory. source is a target reference; dest is a path, which can contain environment variable references. Paths within the fileset are preserved:

# If "source" contains src/main.ts, this writes /app/src/main.ts.
add("source", "/app")
add("compile", "/app", owner = "node", group = "staff")

Later additions replace earlier files at the same paths. Files are owned by root unless owner or group overrides ownership. Names refer to users and groups in the image. Both arguments require strings, including numeric IDs such as owner = "1000", and neither holds :, as owner = "user:group" would; give the group as group. With only owner, the group is that user’s group; with only group, the owner remains root. New parent directories receive the selected ownership too. Directories the image already has keep their modes and owners, unless the fileset selects them itself, as include = ["."] selects every directory beneath it: a selected directory brings its own masked mode and the selected ownership, so a project directory added over /tmp replaces its mode, sticky bit and all. Create a destination first with workdir() when it should keep root ownership.

run(argv)

Run a nonempty list of command arguments in the container’s current working directory. The command must exit successfully. No shell is implicit:

run(["npm", "ci"])
run(["sh", "-c", "mkdir -p /out && printf 'hello\\n' > /out/message.txt"])

Use a shell explicitly for redirection, pipes, or shell variable expansion. The base image must contain every command you run, including the shell.

A command runs with chroot isolation, in the network of the process that runs vamos: the same interfaces and addresses, and the same DNS servers, from its /etc/resolv.conf. In a devcontainer, a step reaches what the devcontainer reaches, such as the services of a Docker Compose network that the devcontainer is on, by their names; on a coordinator’s workers, it reaches what the worker’s container reaches. The step gets a hostname of its own and an /etc/hosts that names it and host.containers.internal, neither of which stays in the image. Unlike podman’s, it gets none of the host’s mounts.conf mounts, such as subscription secrets at /run/secrets, though on a host in FIPS mode buildah still mounts the host’s FIPS settings for bases that use them. Network access is not an input of a target’s cache key: a step that downloads, or talks to a service, is reused like any other until its inputs change.

Secrets

A step that needs a credential, such as a token to upload source maps, is given it as a secret:

run(
["sh", "-c", "sentry-cli sourcemaps upload dist"],
secrets = [secret("sentry", env = "SENTRY_AUTH_TOKEN")],
)
run(["sh", "-c", "npm ci"], secrets = [secret("npmrc", file = "/root/.npmrc")])

secret(id, env = ...) sets the variable env for that step alone, and secret(id, file = ...) mounts a read-only file, owned by root, at that absolute path for that step alone. Each secret gives exactly one of them, and no two secrets of a step give the same variable or file. An ID is letters, digits, _, . or -, starting with a letter or digit, and a file is a clean absolute path. The value comes from the command line, --secret id=sentry,env=SENTRY_AUTH_TOKEN or --secret id=npmrc,src=.npmrc; see the CLI reference. A build that reaches a step whose secret the command line doesn’t give fails when the BUILD file loads, before anything runs.

A secret’s value never enters an image, the build cache, a trace or the graph. The step’s cache key covers each secret’s ID and the variable or file that the step names for it, but not its value, nor where --secret reads it, so a new value doesn’t run the step again, and a cached step’s replayed output is from the run that made it. A step that must run again, such as an upload for each release, should depend on what identifies the release, such as a parameter, build_env(VERSION = ctx.version). A step can still print its secret, and its output is kept with its result; don’t echo secrets.

env(**variables) and build_env(**variables)

env(NODE_ENV = "production", PATH = "/app/bin:$PATH")
build_env(npm_config_cache = "/tmp/npm")
env(**{"NAME-WITH-DASHES": "value"})

Both set string-valued variables for later steps in the target. env() also records them in the resulting image and makes them available to targets using that image. build_env() applies only within the current target and does not persist its assignments in the image. Each call needs at least one variable, given by a nonempty name that holds no =, once in a call. Names such as NAME-WITH-DASHES can be set, but cannot be referenced by vamos’s variable expansion.

Values expand $NAME and ${NAME} using the environment before that call. Variable references require an ASCII letter or underscore followed by ASCII letters, digits, or underscores. Unset variables expand to an empty string; $$ produces a literal dollar sign. Any other $ syntax, including ${NAME:-default}, $5, or a trailing $, is an error when the BUILD file loads. For runtime or shell expansion, put the expression inside a command passed to run(["sh", "-c", ...]).

workdir(path)

Create an absolute directory and use it for later commands and the resulting image’s working directory. As a Dockerfile’s WORKDIR does, it makes the directory for the image’s user, so that later commands, which run as that user, can write to it. The path can refer to environment variables, for example workdir("$HOME/app"). Both workdir() paths and add() destinations must be absolute after expansion.

File selection

glob() and output-producing task() targets share these arguments:

ArgumentMeaningDefault
includeNonempty list of paths or patterns to selectRequired
excludeList of paths or patterns to omit[]
mode_maskPermission bits to retain for files and directories0o755
allow_no_filesAllow a selection with no files or linksFalse
max_file_countMaximum number of selected files and links10000
max_file_sizeMaximum bytes in one file100 * 1024 * 1024
max_total_sizeMaximum total file bytes1024 * 1024 * 1024

Limits must be positive; raise them explicitly when a larger selection is intentional. mode_mask must be between 0 and 0o7777. It masks existing permission bits rather than assigning a fixed mode.

Patterns and paths

Patterns are relative to the BUILD directory for a glob, or to source for task output. Use / as the separator. Patterns cannot be empty or resolve outside the selection root after normalizing . and .. components. Use . to select the whole directory.

PatternSelects
main.goThat path
*.goGo files directly beneath the selection root
src/**/*.goGo files at any depth beneath src, including directly inside it
srcThe directory’s contents recursively
.The selection root’s contents recursively
**/node_modules in excludeEntire matching directories and their contents

* matches within a single path component, including names beginning with .. ? matches one character and [a-z] matches a character range. ** matches zero or more directories and must be a whole path component: use src/**/*.go, rather than src/**.go. An exclusion matching a directory prevents traversal into it. Empty directories are omitted; selected files bring the needed directory entries and their modes. Leading directories used to reach a pattern’s matches are not themselves selected entries.

Paths stay relative to the selection root, even when the pattern has a leading directory. Selecting src/main.go does not strip src/ when adding or exporting it.

Only directories that a pattern could match within are read. In each include, every path component before the first wildcard must exist and must not be a symbolic link, even when allow_no_files = True. For an include without wildcards, this applies to the entire path, including a literal file such as main.go. For example, include = ["link"] rejects a symbolic link. The project’s own directory may be reached through a link, as a shell’s $PWD can be. Links matched by wildcards or found within an included directory are copied as links and never followed. A task’s source may itself traverse links, resolved within its container.

vamos captures reachable globs once before execution, so their input files remain consistent throughout the build. Globs that no requested target uses are not read. Task outputs are selected after the task’s steps finish.