Skip to main content

Build and manage custom executables

Use customTool when you have local source code that should produce one executable. Workstation hashes the source, builds a staged output, installs it atomically, and records its hash. A source change triggers rebuilding; an unchanged setup should converge without rerunning the compiler.

Start with a working shell tool​

Create tools/hello/hello.sh relative to your configuration entry point:

#!/bin/sh
printf 'Hello from my managed tool\n'

Declare it in workstation.config.ts:

import { configure, customTool, defineConfig, task } from "@dovocode/workstation";

export default defineConfig(configure(({ home }) => ({
resources: [customTool("hello", {
source: "tools/hello",
target: "~/.local/bin/workstation-hello",
build: { command: "cp", args: ["{source}/hello.sh", "{output}"] },
})],
tasks: {
hello: task(`${home}/.local/bin/workstation-hello`, [], {
description: "Run the installed custom executable",
}),
},
})));
workstation plan
workstation build
workstation hello
workstation build

The first build installs an executable with 0755 permissions. The task prints the greeting. A repeat build should converge. Edit the message in the source, run plan and build again, and the next invocation prints the new message. Add ~/.local/bin to your shell PATH to call the executable directly.

Build a compiled tool​

For a Go project with a go.mod and main package in tools/worker:

import { configure, customTool, defineConfig, files, tools } from "@dovocode/workstation";

const versions = { go: "latest" };

export default defineConfig(configure(({ home }) => ({ resources: [
tools.mise(versions),
files.mise("~/.config/mise/config.toml", versions),
{
...customTool("worker", {
source: "tools/worker",
target: "~/.local/bin/example-worker",
build: {
command: "mise",
args: ["--cd", home, "exec", "--", "go", "build", "-C", "{source}", "-o", "{output}", "."],
},
}),
dependsOn: ["package:mise:go", `file:${home}/.config/mise/config.toml`],
},
] })));

This template requires real Go source. It generates the exact locked compiler selection before building, and invokes mise from home so a source-local config does not select a different compiler. go build -C then selects the source directory. Include your other global tools in versions before replacing an existing mise file. See runtime activation for the installation/activation relationship.

Understand the build contract​

PlaceholderMeaning
{source}Absolute resolved source path
{output}Temporary sibling path where the build must write its executable
{target}Final installation path; useful as metadata, not the output to overwrite

Placeholders expand in arguments, cwd, and environment values. They do not expand in the command executable. Commands run without a shell; pass a shell explicitly for pipelines or redirections. The build must successfully create a regular file at {output}. Workstation sets executable permissions and renames it into place. Failed builds do not count as successful installations.

Keep builds repeatable​

Source hashing includes files, directory structure, and symlink target text. It does not apply .gitignore. Keep generated binaries, dependency caches, and other mutable output outside the source tree; otherwise the build can change its own fingerprint and trigger another rebuild.

Declare required compiler packages and use actual dependency IDs. customTool does not automatically discover build tools or translate a shell command into an activation environment. An arbitrary project-local version file can override your global selection when a build runs there; choose cwd deliberately.

Drift, removal, and limitations​

An externally modified installed executable is detected against its saved hash and blocks unsafe replacement. Investigate that change instead of deleting state. Removing a declaration removes an owned unchanged artifact; an adopted executable is retained according to ownership rules.

This helper manages one executable. Use a package backend for multi-file package installations, or provisioning for a verified retained vendor installer. Use a named task when you simply want to run an arbitrary project build on demand and do not need Workstation to own the output.