Execution environments
Docker. #local, a tag-pinned image, a digest-pinned one, resource limits and other container options, an image built from the local Dockerfile, and environments held in a Map<Environment> and chosen at run time.
bash
lask envs list # every image, and whether each is on the Docker daemon
lask sync
lask eval tool-versions
lask eval uname-in --env builtThe thing to take away: an environment is a value. A command that names none is a static error, never a quiet fall back to the host.
This topic has 2 files, so clone the repository to try it:
bash
git clone https://github.com/lask-task-runner/lask.git
cd lask/example/02-language/04-environmentsSource
These are example/02-language/04-environments in the lask repository.
lask
// Execution environments
//
// An environment is a value: it is declared once, passed around, stored
// in a map and selected at runtime like anything else.
//
// lask envs list # every image this module can reach for,
// # and whether each is on the Docker daemon
// lask sync # materialize them (run/eval never pull)
// lask eval alpine_version
// lask eval tool_versions
// lask eval uname_in --env built
// The host itself. There is no default environment, so `#local` is
// something you write — a command that names no environment at all is
// a static error, never a quiet fall back to the host.
host = #local
// An image is named by the head of the expression, and only there: no
// string or variable can name one, so reading the source is enough to
// know every image it uses. A tag is optional — a bare
// `#alpine` means `latest` — because `lask sync` pins the digest a
// reference resolves to in lask.lock.json, and every run uses that.
alpine = #alpine:3.22.2
// A digest pins the exact bytes, which no tag can promise.
pinned = #alpine@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
// Run options go in braces after the head: resource limits, for one.
// The braces make a Runnable — the environment, and how to run it. They
// are sugar for runnable(#alpine:3.22.2, memory = "256m", cpus = 1).
small = #alpine:3.22.2{memory: "256m", cpus: 1}
// It configures the rest of the container the same way: variables, the
// network, and what the container may write to. These are ordinary
// arguments — literals here, but they may be computed like any other.
// Variable names are not `lower_id`, so they are written as strings.
sealed = #alpine:3.22.2{
env: { "LANG": "C.UTF-8" },
network: "none",
read_only: true,
tmpfs: ["/tmp"],
cap_drop: ["ALL"]
}
// The other form of an image: a recipe, named by its Dockerfile's path.
// What decides the image goes in parentheses: the path, `context` and
// `build_args`, literals inside this module's own tree, which is what
// keeps the set of images a module uses enumerable and pinnable.
built = #./Dockerfile(context = ".")
// A function can let its caller choose the image, and say how to run
// whichever it gets. The default image is written here, and a caller's
// is written where it calls — `alpine_shell(image = #alpine:3.21)` — so
// either way it is a head someone wrote. runnable() takes an
// Environment, so the options are given once, here.
alpine_shell(--image: Environment = #alpine:3.22.2): Runnable =
runnable(image, env = { "PS1": "lask$ " }, read_only = true)
command { "uname" } on host
command { "jq" } on built
command { "id" } on sealed
// The kernel Lask itself is running on.
//
// @return The kernel of the machine Lask itself is running on.
kernel(): String = $ uname -s
|> trim
// The release of the tag-pinned image.
//
// @return The version line of the pinned image.
alpine_version(): String = $[alpine] cat /etc/alpine-release
|> trim
// A container that reaches no network and writes nowhere but /tmp.
//
// @return The user the sealed container runs as.
sealed_user(): String = $ id -un
|> trim
// The release of the digest-pinned image.
//
// The digest-pinned environment is the same image, named by content.
pinned_version(): String = $[pinned] cat /etc/alpine-release
|> trim
// The memory limit of a constrained container.
//
// Limits apply to the container this command runs in.
memory_limit(): String = $[small] sh -c 'cat /sys/fs/cgroup/memory.max'
|> trim
// The jq version of the image built from Dockerfile.
//
// jq is installed in no environment but the one built from Dockerfile,
// and dispatch is what sends the command there.
jq_version(): String = $ jq --version
|> trim
// Environments in a map are chosen by name at runtime. `@complete`
// then offers those names in the shell.
envs: Map<Environment> = {
"alpine": alpine,
"built": built,
"local": host
}
// Run `uname` in whichever environment is named.
//
// @param env Which environment to report on.
// @complete env @keys envs
// @example lask eval uname-in --env built
uname_in(--env: String = "alpine"): String = $[envs[env]] uname -s
|> trim
// The alpine release of any environment given.
//
// A task can also take an environment directly, since it is a value
// with a type like any other.
release_of(e: Environment): String = $[e] sh -c 'cat /etc/alpine-release 2>/dev/null || echo n/a'
|> trim
// What every environment here reports about itself.
//
// @return What each environment reports about itself.
tool_versions(): Record<host: String, alpine: String, built: String> = {
host: kernel(),
alpine: release_of(alpine),
built: jq_version()
}dockerfile
# The recipe form of an environment: an image built from this file
# rather than pulled by name. `lask sync` is what materializes it.
FROM alpine:3.22.2
RUN apk add --no-cache jq=1.8.2-r0