The workspace file

imp.workspace.js is the root marker imp looks for when finding the workspace root. It's evaluated once, before any BUILD.js file. Import rule modules here to register their product()s so BUILD.js files can use them; each rule supplies its pinned default toolchain when imported.

import "//rules/c/cmake";
import "//rules/workflows/build";
import "//rules/odin";
import "//rules/odin/odinfmt";

Static, known-shape configuration uses an export named after its schema namespace. For example, an Odin collections configuration is declared as:

export const odin = {
    collections: { lib: "library" },
};

When the namespace is also used by another workspace export, use the <namespace>Config form instead:

export const odinConfig = {
    collections: { lib: "library" },
};

Exported declarations are workspace targets

An export const name = ... at the top level of imp.workspace.js gets a stable address, //:name, exactly like an export from a root BUILD.js file. workspaceTargets() and the target graph see it; nothing about it is workspace-file-specific beyond where it's declared.

Rule-owned default toolchains have no workspace address until you export their getter result. This is optional for normal targets, which use the default automatically:

import { defaultOdinToolchain } from "//rules/odin";

export const odin = defaultOdinToolchain();

Use an explicit { default: true } declaration only when a workspace needs a different version or toolchain options:

import { odinToolchain } from "//rules/odin";

export const odin = odinToolchain("dev-2026-04", { default: true });

imp @TOOL resolves exported toolchains automatically

imp @odin build foo.odin -out:foo and imp @odinfmt run a managed toolchain binary directly, bypassing imp's own CLI parsing so the tool's flags never need a -- separator. TOOL is resolved purely from the workspace: imp looks up the export named TOOL at //:TOOL, and if its target kind has a "toolchain" product registered, calls that product to get an absolute binary path and runs it.

This means adding a new @tool needs no changes to imp itself — just an exported target whose kind resolves to a binary. A toolchain rule module opts in by subclassing Toolchain, which registers the "toolchain" product automatically from the subclass's bin():

import { Toolchain, toolName } from "imp:core";

export class MyToolchain extends Toolchain {
    static kind = "my-toolchain";
    static tool = toolName("mytool");
    constructor({ version }, opts) {
        super({ kind: MyToolchain.kind, attrs: { version } }, opts);
    }

    bin() {
        return resolveMyToolBin(this.attrs.version);
    }
}

export function myToolchain(version, opts = {}) {
    return new MyToolchain({ version }, { default: opts.default });
}

and the workspace file exports it:

export const mytool = myToolchain("1.2.3");

imp @mytool ... now works. If TOOL isn't exported from the workspace file, or its kind has no "toolchain" product, imp @TOOL fails with an error naming what's missing — there's no fixed list of "known tools" to update.

The one exception is kcov, which isn't workspace-driven at all — it's resolved from a fixed host install path, since coverage instrumentation isn't itself a build toolchain declared per-workspace.