Pinning tool versions

Every block below is a real engines map you can drop into lattice.json, either at the root, where it applies to every workspace, or inside a workspaces entry, where it overrides the root key by key.

The shape of the constraint decides what Lattice does with it. For the model behind that, see Engines and provisioning. For the table of built-in engines and drivers, see Toolchains.

Fail the run when the host tool is too old

Give the engine a bare version constraint and nothing else. Lattice checks the tool already on PATH and installs nothing:

{
  "engines": {
    "node": ">=20.0.0"
  }
}

Verify it with any run:

lattice run build

A host tool that satisfies the constraint logs nothing. One that does not fails the run before any task starts:

Error: engine 'node' on PATH is 18.19.0, which does not satisfy the constraint '>=20.0.0'

The constraint is a lower bound on whatever the version command reports, so the fix on a failing machine is to upgrade the host tool. Lattice installs nothing in this mode.

The bare-string form works for the 40 names Lattice already knows how to version-check. That is every built-in driver plus the language toolchains rust, python3, php, elixir, and haskell/ghc. The full list with each version command is on the Toolchains page. A bare string for any other name is rejected at load time. See A tool Lattice does not know.

Have Lattice install the tool

Add installCmd. Lattice then provisions the tool itself instead of trusting PATH. The install command runs once, into a directory Lattice controls, and the result is version-checked afterward:

{
  "engines": {
    "node": {
      "version": ">=20.0.0",
      "installCmd": "curl -fsSL https://get.volta.sh | VOLTA_HOME=$LATTICE_TOOLCHAIN_DIR bash && $LATTICE_TOOLCHAIN_DIR/bin/volta install node@20",
      "bin": "bin"
    }
  }
}

bin is the directory, relative to the install, that gets prepended to a task’s PATH. It defaults to bin. $LATTICE_TOOLCHAIN_DIR is available both as a literal substring, substituted before the shell runs the command, and as an environment variable, so installers of either kind work.

lattice setup
lattice run build

lattice setup provisions every declared engine before it installs any workspace’s dependencies, so a package manager that shells out to a provisioned compiler sees it on PATH during install. lattice run provisions on demand if you skip setup. A task never runs against a missing or wrong-versioned toolchain.

The first run logs the provisioning step, then the task:

lattice: provisioning engine 'demo' via installCmd into .lattice/toolchains/demo/tmp-2ee6e363
lattice: running `build` across 1 workspace

Every run after that logs nothing, because the existing verified install is reused. A failing install command stops the run with the installer’s own output underneath:

Error: engine 'node': installCmd failed:
<the installer's stderr>

An install that succeeds but still does not satisfy the constraint is also fatal:

Error: engine 'node' provisioned 19.8.1, which does not satisfy the constraint '>=20.0.0'

Both of those failures are in the installCmd itself: network access, a dead URL, or a platform-specific installer.

Where the provisioned tool lands

Every provisioned engine goes under .lattice/toolchains/<engine>/, inside the repo:

.lattice/toolchains/demo/1.2.3-2ee6e363/
  bin/          prepended to the task's PATH
  pins.json     the version installed and the hash that produced it
{
  "engine": "demo",
  "version": "1.2.3",
  "installHash": "2ee6e363",
  "bin": "bin"
}

The hash in the directory name is the first 8 hex characters of sha256(installCmd). Change the installCmd and Lattice provisions into a new directory rather than mutating the old one.

The version in the directory name is the one the tool reported once Lattice had installed it. An engine with no versionCmd and no built-in version rule has nothing to report, so it lands under unknown-<hash> and the hash identifies it. A version you have no way to check is an error instead, because Lattice does not record a version it did not read.

Give each workspace its own version

Declare engines inside a workspace instead of, or in addition to, the root. A workspace’s entries override the root key by key, and the rest of the root map still applies:

{
  "engines": {
    "node": ">=18.0.0"
  },
  "workspaces": [
    { "name": "legacy-api", "path": "services/legacy-api" },
    {
      "name": "web",
      "path": "apps/web",
      "engines": { "node": ">=20.0.0" }
    }
  ]
}

legacy-api accepts whatever node on PATH satisfies >=18.0.0, and web requires >=20.0.0. Both check the same PATH, so this works only if one host node satisfies both.

To give each workspace its own copy, add an installCmd per workspace:

{
  "workspaces": [
    {
      "name": "legacy-api",
      "path": "services/legacy-api",
      "engines": {
        "node": {
          "version": ">=18.0.0",
          "installCmd": "install-node.sh --version 18 --dest $LATTICE_TOOLCHAIN_DIR"
        }
      }
    },
    {
      "name": "web",
      "path": "apps/web",
      "engines": {
        "node": {
          "version": ">=20.0.0",
          "installCmd": "install-node.sh --version 20 --dest $LATTICE_TOOLCHAIN_DIR"
        }
      }
    }
  ]
}

The provisioned directory is addressed by the content of the resolved installCmd, so two workspaces asking for the same engine name with different install commands provision into two directories under .lattice/toolchains/node/, and each task gets only its own workspace’s bin directory on PATH. Verify one at a time:

lattice run build --filter legacy-api
lattice run build --filter web

A tool Lattice does not know

An engine name outside the well-known list has no built-in version rule, so the bare string form is rejected and the config fails to load:

{
  "engines": { "alpes": ">=2.0.0" }
}
Error: engine 'alpes' in root uses the string form, which carries only a version. 'alpes' is not a well-known engine, so Lattice cannot version-check it on its own. Use the object form with a `versionCmd`, like this: "alpes": { "version": ">=1.0.0", "versionCmd": "alpes --version" }

Use the object form and give Lattice the version command:

{
  "engines": {
    "alpes": {
      "version": ">=2.0.0",
      "versionCmd": "alpes --version"
    }
  }
}

That is validate-only, the same as a well-known engine with a bare string. Add installCmd to provision it instead, exactly as in Have Lattice install the tool.

Lattice reads the first version-looking substring versionCmd prints, so v2.6.7, alpes 2.6.7 (build 41), and 2.6 all parse. The command only needs to print a version somewhere in its output.

Remove a provisioned toolchain

.lattice/toolchains and .lattice/cache are both regenerable, and lattice init writes both into .gitignore. To reset every provisioned tool in the repo:

rm -rf .lattice
lattice run build

The next run reprovisions each declared installCmd and rebuilds the cache. Nothing outside the repo is touched. That is also the shortest fix for a provisioned tool in a state you would rather not debug.

Note that rm -rf .lattice also removes .lattice/bin, where Lattice keeps the versions of itself a repo has pinned, and the committed .lattice/schema.json. The next command rewrites the schema, and it re-downloads the pinned binary. See Upgrading.