Toolchains
Lattice ships two tables. One lists the task drivers it can recognize in a workspace and invoke a task through. The other lists the engines it knows how to read a version from. This page is both of them in full.
For the models these tables serve, see Driver detection and Engines and provisioning.
The built-in driver table
A driver is defined by four things: the fingerprint files that identify it in a
workspace directory, one or more roles, the command that prints its version, and
the template it invokes a task through. {task} is the literal placeholder each
template substitutes the task name into.
Selection follows role rank. The candidate holding the highest-ranked role drives the workspace, a tool with several roles competes with its highest one, and two candidates holding the same role conflict until a declaration names one. A tool whose only role is Runtime never drives a workspace on its own. Driver detection has the evidence ladder and the composition rules.
Two drivers have no fingerprint. Nothing on disk belongs to pip alone or to
the Kotlin toolchain alone, so both are selected by name in engines or in a
.tool-versions file, never by detection.
JavaScript and TypeScript
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
node |
Runtime | .nvmrc |
node --version |
node {task} |
deno |
Runtime, Package Manager, Task Runner | deno.json, deno.jsonc, deno.lock |
deno --version |
deno task {task} |
bun |
Runtime, Package Manager | bun.lockb, bun.lock |
bun --version |
bun run {task} |
pnpm |
Package Manager | pnpm-lock.yaml |
pnpm --version |
pnpm run {task} |
yarn |
Package Manager | yarn.lock |
yarn --version |
yarn {task} |
npm |
Package Manager | package-lock.json, npm-shrinkwrap.json |
npm --version |
npm run {task} |
turbo |
Task Runner | turbo.json |
turbo --version |
turbo run {task} |
nx |
Task Runner | nx.json |
nx --version |
nx run {task} |
deno fills three roles and competes as a task runner. bun fills two and
competes as a package manager. turbo and nx outrank every package manager
here, so a workspace holding both turbo.json and pnpm-lock.yaml resolves to
turbo and composes pnpm underneath it.
Rust
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
cargo |
Build Tool | Cargo.lock, rust-toolchain.toml, rust-toolchain |
cargo --version |
cargo {task} |
Go
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
go |
Build Tool | go.sum |
go version |
go {task} |
Python
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
uv |
Package Manager | uv.lock |
uv --version |
uv run {task} |
poetry |
Package Manager | poetry.lock |
poetry --version |
poetry run {task} |
pdm |
Package Manager | pdm.lock |
pdm --version |
pdm run {task} |
pipenv |
Package Manager | Pipfile.lock |
pipenv --version |
pipenv run {task} |
pip |
Package Manager | none, declaration only | pip --version |
pip {task} |
python |
Runtime | .python-version |
python --version |
python -m {task} |
requirements.txt is read by pip, uv, and pip-tools alike, so it identifies no
tool and is not a pip fingerprint. Declare pip under engines for a
workspace pip should drive. The file still counts toward
cache keys.
Ruby
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
bundler |
Package Manager | Gemfile.lock |
bundle --version |
bundle exec {task} |
rake |
Task Runner | Rakefile |
rake --version |
rake {task} |
ruby |
Runtime | .ruby-version |
ruby --version |
ruby {task} |
Java
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
gradle |
Build Tool | gradlew |
gradle --version |
./gradlew {task} |
maven |
Build Tool | mvnw |
mvn --version |
./mvnw {task} |
java |
Runtime | .java-version |
java -version |
java {task} |
Both build tools fingerprint the checked-in wrapper rather than the build file,
and both invoke through it. A build.gradle or a pom.xml on its own is a
generic ecosystem marker, not gradle or maven evidence.
Kotlin
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
kotlin |
Runtime | none, declaration only | kotlinc -version |
kotlin {task} |
Kotlin work is driven by gradle or maven, and no file on disk pins the Kotlin
toolchain specifically. kotlin is a runtime you declare and compose underneath
one of those. A .tool-versions entry naming kotlin counts as a declaration.
.NET
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
dotnet |
Build Tool | global.json |
dotnet --version |
dotnet {task} |
nuget |
Package Manager | packages.config |
nuget help |
nuget {task} |
nuget fingerprints only the legacy packages.config layout.
packages.lock.json is not nuget evidence: an SDK-style workspace can restore
with a lockfile and still be a dotnet workspace, and a package manager
outranks a build tool, so counting the lockfile would take the driver away from
dotnet. The lockfile still counts toward
cache keys.
nuget help is the version rule because nuget.exe prints its version in its
help output. Lattice reads the first version-shaped substring of whatever a
version command writes to stdout or stderr, so surrounding banner text does not
matter.
Swift and Objective-C
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
pod |
Package Manager | Podfile, Podfile.lock |
pod --version |
pod {task} |
swift |
Build Tool | Package.resolved |
swift --version |
swift {task} |
PHP
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
composer |
Package Manager | composer.lock |
composer --version |
composer {task} |
Elixir
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
mix |
Package Manager, Task Runner | mix.lock |
mix --version |
mix {task} |
Dart
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
dart |
Package Manager | pubspec.lock |
dart --version |
dart pub {task} |
Haskell
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
stack |
Build Tool | stack.yaml.lock |
stack --version |
stack {task} |
cabal |
Build Tool | cabal.project.freeze |
cabal --version |
cabal {task} |
Two build tools with the same role. A workspace holding both lockfiles is an
ambiguity, resolved by naming one under engines.
Language-agnostic task runners
| Tool | Roles | Fingerprint | Version command | Invoke template |
|---|---|---|---|---|
just |
Task Runner | justfile, .justfile |
just --version |
just {task} |
task |
Task Runner | Taskfile.yml, Taskfile.yaml |
task --version |
task {task} |
Neither belongs to an ecosystem. Either can sit above a language-specific driver in any workspace.
That is the complete set: 34 drivers, across 13 ecosystems plus the two tools above that belong to none.
Well-known engines
An engines entry can be a bare version-constraint string only when Lattice has
a built-in rule for reading that tool’s version. The table below is the rule set,
and it is where every driver above gets its version command. A string naming
anything outside this table is rejected by lattice.json validation.
{ "engines": { "node": ">=20.0.0" } }
| Engine | Version command |
|---|---|
node |
node --version |
deno |
deno --version |
bun |
bun --version |
pnpm |
pnpm --version |
yarn |
yarn --version |
npm |
npm --version |
rust |
rustc --version |
cargo |
cargo --version |
go |
go version |
python |
python --version |
python3 |
python3 --version |
pip |
pip --version |
uv |
uv --version |
poetry |
poetry --version |
pdm |
pdm --version |
pipenv |
pipenv --version |
ruby |
ruby --version |
bundler |
bundle --version |
rake |
rake --version |
java |
java -version |
kotlin |
kotlinc -version |
gradle |
gradle --version |
maven |
mvn --version |
dotnet |
dotnet --version |
nuget |
nuget help |
swift |
swift --version |
pod |
pod --version |
php |
php --version |
composer |
composer --version |
elixir |
elixir --version |
mix |
mix --version |
dart |
dart --version |
haskell |
ghc --version |
ghc |
ghc --version |
stack |
stack --version |
cabal |
cabal --version |
just |
just --version |
task |
task --version |
turbo |
turbo --version |
nx |
nx --version |
Forty engines. Every tool in the driver table appears here, so any driver can be pinned in string form.
Six names are engines and not drivers: rust, python3, php, elixir,
haskell, and ghc. Each pins a compiler or interpreter that a different tool
drives tasks with. Cargo drives a Rust workspace, composer a PHP one, mix an
Elixir one, and stack or cabal a Haskell one. haskell and ghc are two
spellings of one rule. python and python3 are two different interpreters and
two different rules.
A bare string is validate-only. Lattice runs the version
command against whatever is on PATH and fails before any task starts if the
result does not satisfy the constraint. Nothing is installed. Lattice ships no
install recipe for any engine in this table, well-known or not: provisioning
happens only when the config supplies an installCmd.
Declaring a tool Lattice does not know
A name outside the table above requires the object form with an explicit
versionCmd. Without one, validation rejects the config:
engine 'alpes' in root uses the string (version-only) form, but 'alpes' is not
a well-known engine Lattice can version-check on its own. Use the object form
with an explicit `versionCmd`, e.g. "alpes": { "version": ">=1.0.0", "versionCmd":
"alpes --version" }
Validate a tool already on PATH without installing it:
{
"engines": {
"alpes": {
"version": ">=2.6.7",
"versionCmd": "alpes --version"
}
}
}
Add installCmd to move from validate-only to provisioned. bin names the
directory inside the install that holds executables, and defaults to bin:
{
"engines": {
"alpes": {
"version": ">=2.6.7",
"versionCmd": "alpes --version",
"installCmd": "curl -fsSL https://alpes.example/install.sh | sh -s -- --dir $LATTICE_TOOLCHAIN_DIR",
"bin": "bin"
}
}
}
Lattice then runs installCmd into a directory under
.lattice/toolchains/alpes/, version-checks the result, records a pin, and
prepends that directory’s bin to the task’s PATH.
Engines and provisioning covers what
$LATTICE_TOOLCHAIN_DIR receives, how a pin is reused across runs, and how
PATH is assembled.