Skip to content

v0.6.0

Choose a tag to compare

@Ikepeeee Ikepeeee released this 26 Sep 02:43
· 60 commits to main since this release
cb32d81

An environment is now something a library can hand you — configured, pinned, and with its command words attached.

Until now a command declaration took an image written with literal arguments, its words never left the module that wrote them, and #docker(...) could say nothing about a container beyond memory and cpus. A project could pin a toolchain, but not share one: the credentials, variables and mounts a tool needs had to be repeated at every call site, or wrapped in a module that re-implemented the tool's CLI as functions. v0.6.0 lets a command's environment be any expression, lets command words be imported, gives the container constructor the rest of its options — and makes the lock pin every registry image by digest, so what a library hands you is the same image on every machine.

Command words across modules

  • import command brings in the command words another module declares. import command { "python", "pip" } from "tools" makes a bare $ pip install ... dispatch to the environment the tools module declared for it. export command { "helm" } from "./lib/k8s.lask" imports and publishes again. A command declaration takes export / internal like any other, and is exported by default.
  • No other import brings a command word in, and there is no renaming: the word is the program the environment runs. A named or namespace import changes nothing about where a command runs, so importing a module for its functions never reroutes your commands.
  • A command's environment is any expression of type Environment: command { "aws" } on tools.aws(profile = "dev"), a namespace member, a call with computed arguments. It is elaborated in the declaring module and evaluated when a command it selects runs.
  • It may not have an effect. No command, file, stdin, log or random value, directly or through what it calls (E-TYPE-COMMAND-EFFECT); get_env is fine. This is what lets lask cmd evaluate a declaration without running anything or consuming the program's stdin.
  • Dispatch compares where an environment comes from, not its value: the same literal, the same binding, or the same declaration. Two separate calls conflict, since only running them could show they agree — declare such words together.
  • The words of a declaration are written in braces, command { "go", "gofmt" } on e, for symmetry with the import form. With the braces the position of on is lexical, so the editor marks it as a keyword there and leaves it an identifier elsewhere.

Container options

  • #docker(...) configures the container it launches. Beyond memory and cpus: memory_swap memory_reservation cpu_shares cpuset_cpus cpuset_mems pids_limit shm_size blkio_weight ulimits; workdir user env platform hostname init; read_only tmpfs cap_drop; network dns dns_search add_hosts publish; volumes; and build_args on a recipe.
  • Only options that stay inside the permission boundary of 10.7 are offered. privileged, cap_add, devices and the host namespaces are left out: they would make that guarantee untrue rather than narrow it. publish and volumes widen it, and are written where the environment is.
  • An option given null is left out, as are the null elements of a list or table, so a function that builds an environment can pass an optional parameter straight through. "" is a value and is passed on — PAGER="" means something. A list or table already typed Array<String> or Map<String> is accepted as it is.
  • Environment values are keyed by argument name, not by the order the arguments were written in, so #docker("a:1", memory = "1g", cpus = 2) and the same options reordered are one environment for dispatch.
  • build_args closes a gap 10.3 had already written down: the recipe hash covers the declared build arguments, and there was no way to declare any. Like dockerfile and context, it must be a literal, since it decides which image lask env build builds.
  • The environment in the command log is masked too. A docker environment can carry variables into the container, so a secret reaches the log through the environment value as readily as through the command string.

Images pinned by digest

  • A run never pulls. Spec 10.3 and 10.4 always said so; none of it was implemented, and a run passed the tag to docker run, which pulled whatever the tag named that day. Now lask deps sync and lask env build pull every registry image the program references, record its digest in lask.lock.json, and run, eval, cmd and file access run <repository>@<digest>.
  • A fresh machine gets the very image the lock names, whatever the tag names upstream, and is checked to carry it (E-IO-IMAGE-DIGEST). A missing image is E-IO-IMAGE-MISSING, naming lask env build.
  • A registry reference may omit its tag — #alpine means latest — since the lock pins what the bare name resolved to, and it cannot move under a run.
  • deps sync materializes images after the modules, also in a project that declares no dependency, and --frozen covers images too. envs --check checks that each image is actually present, and env list reports registry references and their pins, with JSON.
  • A reference computed at run time cannot be pinned: it runs as written if present. The REPL may still pull — it evaluates what is typed at it, which no lock records.

Language

  • A secret binding may be String | Null. --token!!: String | Null = null says "not given" the way every other optional parameter does, and a credential that may be unset can be read with find_env and still be masked when present. A null registers nothing, so the text an absent value prints as is never masked elsewhere in a log. mark_secret becomes T -> T for String or String | Null.
  • run(env, cmd) is the core command-execution function, formerly run_command(cmd, env): the environment comes first, as it does in $[env] cmd.
  • A re-exported name works everywhere its declaration does. t.go() through import * as t failed with internal: missing declaration go when t published go by export { go } from ..., and lask run answered no such function for it. Both now follow the re-export — keyword arguments, --help built from the declaring file under the published name, the function list, and shell completion for local paths — so a multi-file library that publishes its API from its entry module, as chapter 5 describes, has a CLI surface.
  • A recipe resolves inside the tree of the module that declares it. #docker(dockerfile = "images/ansible/Dockerfile") written in a library read that path from the importing project and failed there. A library can now ship the recipes of its environments, built from the dependency cache.

In the editor

  • Hovering a builtin documents it: the call with parameter names, the failure it raises, and the spec section it summarizes, condensed from chapter 15. Polymorphic builtins show their type parameters, as user declarations do, and a local that shadows a builtin is no longer mistaken for it.

Breaking changes

  • run_command(cmd, env) is now run(env, cmd). Rename the call and swap the arguments. A builtin call with keyword arguments is always E-TYPE-KEYWORD.
  • A command declaration's words are written in braces. command "go" on e is now a syntax error that says how to write command { "go" } on e.
  • E-TYPE-COMMAND-DECL is replaced by E-TYPE-COMMAND-EFFECT. The literal-arguments rule it enforced is gone; what is refused now is an environment with an effect.
  • A project that runs containers needs lask env build (or lask deps sync) before its first run. A registry image the lock does not pin, or one not on the daemon, is E-IO-IMAGE-MISSING rather than a pull. Commit the images section lask.lock.json gains.
  • build_args must be a literal, like dockerfile and context. Spec 10.2's claim that every container option had to be a literal is corrected: only these three are.

Lask is pre-1.0 and every feature is experimental until 1.0, so breaking changes remain possible; see compatibility.md.

Fixes and internals

  • CI pins the image before running the containerized task in the colon-bearing-directory check, as the install test does, now that a run no longer pulls.

Examples

  • example/ is reorganized into two groups, each numbered in reading order. 01-projects keeps the end-to-end work: 01-hello-world (was 01-basic) and 02-webapp-on-aws (was 04-webapp). The Docker and Terraform projects are gone; what they showed is now in the language tour.
  • example/02-language is a tour of the whole language: fourteen topics, one runnable module each, covering the Quick Reference from values and types through to doc comments and the CLI. The commentary lives in the code beside the thing it describes, and because the comment above a task is also its help text, lask run --help in any of them is a second index into the same prose.
  • 02-webapp-on-aws takes its environments from lask-module-tools instead of wrapping Terraform and the AWS CLI through lask-terraform and lask-aws. The tools run as written — $ terraform -chdir=infra apply ..., $ aws s3 sync ... — and their image tags, credentials and region are given once, in the environment each command word is declared on.

Documentation

  • The Quick Reference, the specification and the READMEs follow every change above, and paragraphs are no longer hard-wrapped, so the docs pass cleanly through translation tools.

Full Changelog: v0.5.0...v0.6.0