Introduction
Turnkey is a toolchain management framework for Nix flakes that simplifies declaring and managing build tools in development environments.
What is Turnkey?
Turnkey bridges declarative TOML configuration with Nix package resolution, providing:
- Simple Configuration: Declare toolchains in
toolchain.toml - Reproducible Environments: Nix ensures consistent tool versions across machines
- Incremental Builds: Fast, cached builds that only rebuild what changed
- Language Support: Go, Rust, Python, TypeScript, Solidity, Jsonnet, and more
Key Features
- Declarative toolchain management via TOML
- Automatic dependency cell generation for the build system
- Native tool wrappers with auto-sync (
go,cargo,uv) - Modular Nix flake integration
Who Should Use This?
Turnkey is designed for teams who:
- Want reproducible development environments
- Need fast, incremental builds across multiple languages
- Need to manage multiple language toolchains
- Value declarative, version-controlled configuration
Next Steps
- Installation - Set up Turnkey in your project
- Quick Start - Build your first project
Why Turnkey
Modern software development faces a fundamental tension: we want the simplicity of working with familiar tools while also needing the reproducibility and scalability of sophisticated build systems.
Turnkey bridges this gap.
The Problem
Consider a typical development scenario. You have a project that uses Go, some Rust libraries, a Python testing framework, and TypeScript for the frontend. Each language has its own:
- Package manager (go mod, cargo, pip/uv, npm/pnpm)
- Build conventions
- Test runners
- IDE integrations
This works fine for small projects. But as projects grow, you encounter challenges:
- "Works on my machine" - Different developers have different tool versions
- Slow CI/CD - Every change rebuilds everything, even unrelated code
- Dependency hell - Conflicting versions across languages and packages
- AI agent friction - Automated tools struggle with slow, non-incremental builds
The enterprise answer to these problems is typically a monorepo with a sophisticated build system like Bazel or Buck2. But adopting a monorepo means:
- Rewriting all your build logic
- Learning new command-line tools
- Breaking IDE integrations
- Significant upfront investment
The Turnkey Solution
Turnkey takes a different approach: keep your familiar tools working normally while adding build system benefits invisibly.
# These still work exactly as expected
go build ./...
cargo test
pytest
npm run build
# But now you also have Buck2's power when you need it
buck2 build //...
buck2 test //...
The key insight is that most developers don't need to think about the build system most of the time. They want to:
- Write code
- Run tests
- Get fast feedback
Turnkey provides this while maintaining a single source of truth for dependencies and builds that enables advanced features like:
- Hermetic, reproducible builds
- Incremental compilation across languages
- Remote caching and execution
- Atomic changes across the entire codebase
Who Is Turnkey For?
Turnkey is designed for teams that want:
Enterprise-grade infrastructure without abandoning their existing workflows. Your go build still works. Your IDE still works. Your junior developers don't need to learn build system internals to be productive.
A growth path from prototype to production. Start with normal language tooling. Adopt incremental build features as your needs grow. No big-bang rewrites.
AI-friendly development with fast feedback loops. AI coding assistants work better when builds are fast and incremental. Turnkey's caching means AI agents can iterate quickly.
Reproducibility without ceremony. Nix handles tool versioning. The build system handles caching. You focus on writing code.
The Turnkey Philosophy
- Tools should enhance, not replace - Native commands work normally
- Complexity should be opt-in - Start simple, add sophistication as needed
- Reproducibility is non-negotiable - Same inputs always produce same outputs
- Fast feedback enables better code - Incremental builds by default
In the following chapters, we'll explore the core principles that make this possible and how the architecture enables a seamless developer experience.
Core Principles
Turnkey is built on four core principles that guide every design decision. These principles often exist in tension with each other, and Turnkey's value lies in finding the right balance.
1. Native Tool Compatibility
Your existing commands should just work.
When you run go build, it should build your Go code. When you run cargo test, it should test your Rust code. LSP servers should provide autocomplete. IDEs should find definitions. This isn't a compromise - it's a requirement.
How It Works
Turnkey provides transparent wrappers (tw) around native tools that:
- Pass through all commands unchanged by default
- Watch for dependency file changes (go.mod, Cargo.lock, etc.)
- Automatically regenerate build system dependency cells when needed
- Never block or modify the developer's primary workflow
# The 'tw' wrapper is transparent
tw go get github.com/foo/bar # Works exactly like 'go get'
# But also updates build system deps if go.mod changed
# Or use 'go' directly - it still works
go build ./... # Normal Go build, no Buck2 involved
Why This Matters
- Zero learning curve for basic workflows
- IDE integrations continue working - gopls, rust-analyzer, pyright all function normally
- Existing scripts and CI remain valid - no migration required
- Developers stay in their comfort zone while infrastructure improves beneath them
2. Monorepo Benefits Without Monorepo Storage
Get unified versioning without storing the world in your repository.
Traditional monorepos store all code in one repository, enabling atomic changes and unified versioning. But this comes with costs:
- Massive repository size
- Complex code ownership
- Slow git operations
- Storage of third-party code
Turnkey provides the benefits of a monorepo without these costs through virtual cells.
How It Works
your-repo/
├── src/ # Your source code
├── go.mod # Normal Go module
├── Cargo.toml # Normal Cargo workspace
└── .turnkey/
├── godeps/ # Virtual cell: Go dependencies
├── rustdeps/ # Virtual cell: Rust dependencies
└── prelude/ # Virtual cell: Build system prelude
The .turnkey/ directory contains cells - the build system's unit of code organization. These cells are:
- Generated from your lock files (go.sum, Cargo.lock, etc.)
- Deterministically reproducible via Nix
- Treated as source code by the build system (enabling caching and incrementality)
- Never committed to git (they're derived data)
The Result
- Atomic changes across your code and its dependencies
- Unified versioning - one lock file controls one version
- Hermetic builds - Nix ensures reproducibility
- Fast git operations - repository stays small
3. Incremental Build and Test
Only rebuild and retest what actually changed.
Modern CI/CD often wastes enormous resources rebuilding unchanged code. A small typo fix shouldn't trigger a full rebuild of the entire project.
How It Works
The incremental build system tracks fine-grained dependencies between:
- Source files
- Build rules
- Test targets
- Generated artifacts
When a file changes, the build system determines the minimal set of actions needed:
# Edit a single Go file
vim pkg/utils/helper.go
# The build system only rebuilds affected targets
tk build //... # Rebuilds only what depends on helper.go
tk test //... # Runs only tests that might be affected
Combined with remote caching, this means:
- CI builds are fast because most artifacts are cached
- Local builds benefit from CI's cached artifacts
- AI agents can iterate quickly with sub-second feedback
Why This Matters for AI
AI coding assistants (like Claude Code) benefit enormously from fast builds:
- Quick iterations mean more experiments per session
- Fast test feedback enables test-driven development
- Immediate error messages allow rapid course correction
Turnkey's incremental builds make AI-assisted development practical at scale.
4. Continuum of Experience
Scale from prototype to enterprise without rewrites.
Software projects exist on a spectrum:
| Stage | Needs |
|---|---|
| Prototype | Quick iteration, minimal ceremony |
| Startup | Fast CI, some reproducibility |
| Growth | Caching, parallelism, reliability |
| Enterprise | Compliance, governance, audit trails |
Traditional build systems force you to choose: simple but limited, or powerful but complex. Turnkey provides a continuum:
Level 1: Just Use Native Tools
go build ./...
cargo test
pytest
No turnkey involvement. Everything works normally.
Level 2: Add Hermetic Tooling
# toolchain.toml
[toolchains]
go = {}
rust = {}
python = {}
Now your tools are versioned by Nix. "Works on my machine" disappears.
Level 3: Enable Incremental Builds
tk build //...
tk test //...
Get incremental builds and caching. CI becomes faster.
Level 4: Add Remote Caching
Share build artifacts across developers and CI. Builds that took 10 minutes now take 30 seconds.
Level 5: Remote Execution
Distribute builds across a cluster. Massive parallelism. Enterprise scale.
Why This Matters
You don't have to adopt everything at once. Start with Level 1 or 2. Move to higher levels as your needs grow. The underlying infrastructure supports your growth without requiring rewrites.
These four principles - native compatibility, virtual monorepo, incremental builds, and progressive adoption - form the foundation of Turnkey's design. In the next chapter, we'll see how the architecture implements these principles in practice.
Note: Turnkey currently uses Buck2 as its incremental build system. The architecture is designed to potentially support other build systems like Bazel in the future.
Architecture Overview
Turnkey combines three powerful technologies - Nix, an incremental build system, and devenv - into a cohesive developer experience. This chapter explains how these pieces fit together.
Current Implementation: Turnkey uses Buck2 as its incremental build system. The architecture is designed to potentially support other systems like Bazel in the future.
The Three Pillars
┌─────────────────────────────────────────────────────────────┐
│ Developer Experience │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ go build │ │ tk build │ │ IDE / LSP │ │
│ │ cargo test │ │ tk test │ │ Autocomplete │ │
│ │ pytest │ │ tk run │ │ Go to definition │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Turnkey │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ tw wrappers │ │ tk CLI │ │ Dep generators │ │
│ │ Auto-sync │ │ Build wrap │ │ godeps-gen, etc. │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Core Technologies │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Nix │ │Build System │ │ devenv │ │
│ │ Hermetic │ │ Incremental │ │ Shell environment │ │
│ │ packages │ │ builds │ │ configuration │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Nix: Hermetic Package Management
Nix provides reproducible package management. Every tool, compiler, and library has a precise version controlled by the flake.nix and flake.lock files.
What Nix provides:
- Exact versions of go, cargo, python, node, etc.
- System libraries and compilers
- Build tools (buck2 itself)
- Dependency fetching with verified hashes
Key benefit: When you enter the development shell, you have the exact same tools as every other developer and CI system.
Incremental Build System (Buck2)
The build system provides fast, incremental, and correct builds. It tracks dependencies at a fine-grained level and only rebuilds what's necessary.
What the build system provides:
- Dependency tracking between files and targets
- Parallel execution of independent tasks
- Remote caching (share builds across machines)
- Remote execution (distribute builds to a cluster)
Key benefit: After initial setup, builds are dramatically faster because unchanged code isn't rebuilt.
devenv: Developer Shell Configuration
devenv provides a declarative shell environment configured through Nix. It handles:
- Environment variable setup
- Shell hooks and initialization
- Service management (databases, etc.)
- Integration with direnv for automatic activation
Key benefit: Entering a project directory automatically sets up the complete development environment.
The Flow of Data
From Lock Files to Build System Cells
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ go.mod/go.sum │────▶│ godeps-gen │────▶│ go-deps.toml │
│ (native lock) │ │ (generator) │ │ (intermediate) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ .turnkey/godeps │◀────│ Nix │◀────│ go-deps.toml │
│ (Buck2 cell) │ │ (fetcher) │ │ (with hashes) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
- Native lock files (go.sum, Cargo.lock, pnpm-lock.yaml) define exact dependency versions
- Dependency generators (godeps-gen, rustdeps-gen, etc.) parse lock files and output intermediate TOML
- Nix fetches dependencies with verified hashes and creates build-system-compatible cells
- The build system treats these cells as source code, enabling full incrementality
The tw Wrapper Flow
Developer runs: tw go get github.com/foo/bar
│
▼
┌─────────────────┐
│ Snapshot state │ (hash go.mod, go.sum)
└─────────────────┘
│
▼
┌─────────────────┐
│ Run go get │ (native command)
└─────────────────┘
│
▼
┌─────────────────┐
│ Check for diff │ (did lock files change?)
└─────────────────┘
│
┌─────────┴─────────┐
▼ ▼
[No change] [Files changed]
│ │
│ ▼
│ ┌─────────────────┐
│ │ Run godeps-gen │
│ └─────────────────┘
│ │
└───────────────────┘
│
▼
[Done]
The tw wrapper ensures the build system's view of dependencies stays synchronized with native tools, without requiring developer intervention.
Directory Structure
A typical Turnkey-enabled project looks like:
project/
├── .buckconfig → Symlink to generated config (Buck2)
├── .buckroot → Marks project root for build system
├── .envrc → Activates devenv via direnv
├── flake.nix → Nix flake configuration
├── flake.lock → Locked Nix dependencies
├── toolchain.toml → Turnkey toolchain declaration
│
├── src/ → Your source code
│ ├── cmd/
│ ├── pkg/
│ └── rules.star → Build rules
│
├── go.mod → Go module definition
├── go.sum → Go dependency lock
├── go-deps.toml → Generated dependency manifest
│
├── Cargo.toml → Rust workspace definition
├── Cargo.lock → Rust dependency lock
├── rust-deps.toml → Generated dependency manifest
│
└── .turnkey/ → Generated artifacts (gitignored)
├── prelude/ → Build system prelude cell
├── toolchains/ → Toolchain definitions
├── godeps/ → Go dependency cell
├── rustdeps/ → Rust dependency cell
└── sync.toml → Sync configuration
What's Committed to Git
- Source code (
src/) - Native project files (
go.mod,Cargo.toml, etc.) - Lock files (
go.sum,Cargo.lock, etc.) - Turnkey configuration (
toolchain.toml) - Nix configuration (
flake.nix,flake.lock) - Generated dependency manifests (
go-deps.toml, etc.)
What's Generated (Not Committed)
.turnkey/directory (regenerated from lock files).buckconfig(symlinked to Nix store, Buck2-specific)- Build outputs (
buck-out/)
The Toolchain Flow
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ toolchain.toml │────▶│ Registry │────▶│ Nix packages │
│ │ │ (mapping) │ │ │
│ [toolchains] │ │ go = pkgs.go │ │ /nix/store/... │
│ go = {} │ │ rust = pkgs... │ │ │
│ rust = {} │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ Buck2 targets │◀────│ mappings │
│ │ │ │
│ toolchains//:go │ │ Generate rules │
│ toolchains//... │ │ from registry │
└─────────────────┘ └─────────────────┘
toolchain.tomldeclares what toolchains you need- The registry maps toolchain names to Nix packages
- mappings translate these into build system toolchain targets
- The build system uses the toolchain targets for builds
Summary
Turnkey's architecture achieves its goals through careful layering:
| Layer | Responsibility | Technology |
|---|---|---|
| Top | Developer UX | Native tools, tw/tk wrappers |
| Middle | Orchestration | Turnkey, dependency generators |
| Bottom | Execution | Nix (packages), build system (builds), devenv (shell) |
Each layer can be understood independently, and the boundaries are clean enough that you can use partial features without understanding the whole system.
For detailed information about specific components, see the reference documentation.
Installation
Prerequisites
Before installing Turnkey, ensure you have:
- Nix with flakes enabled
- direnv (recommended) for automatic environment activation
Enabling Nix Flakes
If you haven't enabled flakes, add to ~/.config/nix/nix.conf:
experimental-features = nix-command flakes
Adding Turnkey to Your Project
New Project
Use the Turnkey template to create a new project:
nix flake init -t github:firefly-engineering/turnkey
Existing Project
Add Turnkey to your flake.nix inputs:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
turnkey.url = "github:firefly-engineering/turnkey";
};
outputs = { self, nixpkgs, turnkey, ... }: {
# Your flake configuration
};
}
Verifying Installation
After setup, enter the development shell:
nix develop
You should see the welcome message and have access to your declared toolchains.
Quick Start
This guide walks you through building your first project with Turnkey.
Create a toolchain.toml
Create a toolchain.toml file in your project root:
[toolchains]
buck2 = {}
go = {}
This declares that your project needs Buck2 and Go.
Configure Your Flake
Update your flake.nix to use Turnkey:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
turnkey.url = "github:firefly-engineering/turnkey";
devenv.url = "github:cachix/devenv";
};
outputs = inputs@{ turnkey, devenv, ... }:
turnkey.lib.mkFlake { inherit inputs; } {
imports = [
devenv.flakeModule
turnkey.flakeModules.turnkey
];
perSystem = { ... }: {
turnkey.toolchains = {
enable = true;
declarationFiles.default = ./toolchain.toml;
buck2.enable = true;
};
};
};
}
Enter the Shell
nix develop
Build Something
Create a simple Go program and build it with Buck2:
tk build //path/to:target
Next Steps
- Project Setup - Detailed project configuration
- toolchain.toml - Full configuration reference
Project Setup
This guide covers how to create a new Turnkey project or add Turnkey to an existing project.
New Project
Create a new Buck2 project using the Turnkey flake template:
mkdir my-project && cd my-project
nix flake init -t github:firefly-engineering/turnkey
direnv allow # If using direnv
This creates:
flake.nix- Nix flake configuration with Turnkey enabledtoolchain.toml- Toolchain declaration (Go enabled by default).envrc- direnv configuration with symlink sync.gitignore- Ignores Turnkey-managed filesrules.star- Root build file (template)
Existing Project
Add Turnkey to an existing Nix flake project:
1. Add Turnkey Input to flake.nix
{
inputs = {
# ... existing inputs ...
turnkey.url = "github:firefly-engineering/turnkey";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.devenv.flakeModule
inputs.turnkey.flakeModules.turnkey
];
# ... rest of config ...
perSystem = { pkgs, ... }: {
turnkey = {
enable = true;
declarationFile = ./toolchain.toml;
};
devenv.shells.default = {
turnkey.buck2.enable = true;
};
};
};
}
2. Create toolchain.toml
[toolchains]
go = {}
# Add more as needed: rust, python, cxx
3. Update .gitignore
# Turnkey managed files
.buckconfig
.buckroot
.turnkey/
buck-out/
4. Create .envrc (if using direnv)
Turnkey provides a direnv library that handles all symlink management automatically:
use flake . --no-pure-eval
# Source the turnkey library and activate
source "$TURNKEY_DIRENV_LIB"
use_turnkey
The use_turnkey function handles:
- Buck2 symlink management (
.buckconfig, cell symlinks) watch_filedeclarations for automatic reloads- Optional dependency file regeneration
Then allow it:
direnv allow
Directory Structure
A typical Turnkey project has this structure:
my-project/
├── .buckconfig # Buck2 configuration (generated symlink)
├── .buckroot # Empty file marking project boundary
├── .envrc # direnv configuration
├── .turnkey/ # Generated cells (gitignored)
│ ├── prelude/ # Buck2 prelude
│ ├── toolchains/ # Language toolchains
│ ├── godeps/ # Go dependency cell (if configured)
│ └── rustdeps/ # Rust dependency cell (if configured)
├── flake.nix # Nix flake configuration
├── flake.lock # Locked dependencies
├── toolchain.toml # Toolchain declarations
├── go-deps.toml # Go dependencies (if using Go)
├── rust-deps.toml # Rust dependencies (if using Rust)
└── rules.star # Root build file
Generated Files
When you enter the devenv shell, Turnkey generates:
| File | Description |
|---|---|
.buckconfig | Symlink to Nix-managed Buck2 configuration |
.buckroot | Empty file marking project boundary |
.turnkey/toolchains | Symlink to generated toolchains cell |
.turnkey/godeps | Symlink to Go dependencies cell (if configured) |
.turnkey/prelude | Symlink to prelude (if using nix strategy) |
Prelude Strategies
Turnkey supports four strategies for providing the Buck2 prelude:
Bundled (Default)
Uses Buck2's built-in bundled prelude. Simplest option, no configuration needed.
devenv.shells.default.turnkey.buck2 = {
enable = true;
prelude.strategy = "bundled";
};
Git
Clones prelude from a git repository. Good for pinning to a specific version.
devenv.shells.default.turnkey.buck2 = {
enable = true;
prelude = {
strategy = "git";
gitOrigin = "https://github.com/facebook/buck2-prelude.git";
commitHash = "abc123..."; # Required
};
};
Nix
Uses a Nix derivation containing the prelude. Best for reproducibility.
devenv.shells.default.turnkey.buck2 = {
enable = true;
prelude = {
strategy = "nix";
path = pkgs.fetchFromGitHub {
owner = "facebook";
repo = "buck2-prelude";
rev = "...";
hash = "sha256-...";
};
};
};
Path
Uses a local filesystem path. Good for development/testing.
devenv.shells.default.turnkey.buck2 = {
enable = true;
prelude = {
strategy = "path";
path = "/path/to/local/prelude";
};
};
direnv Integration
For automatic environment activation with full Turnkey support, create .envrc:
use flake . --no-pure-eval
source "$TURNKEY_DIRENV_LIB"
use_turnkey
Then allow it:
direnv allow
The turnkey direnv library provides additional options:
use_turnkey --skip-regen- Skip dependency file regenerationuse_turnkey --skip-sync- Skip symlink synchronization- Environment variables like
TURNKEY_SKIP_ALL=1for CI environments
Buck2 Configuration
The .buckconfig is generated automatically. For project-specific settings, create .buckconfig.local:
[build]
# Custom build settings
[project]
# Project-specific settings
Verifying Setup
After entering the shell, verify Buck2 is configured:
# Check toolchains
buck2 targets toolchains//...
# Run Go via toolchain
buck2 run toolchains//:go[go] -- version
# Build a target
buck2 build //...
Common Issues
Symlinks Not Created
If .turnkey/ symlinks aren't created:
- Check that you're using direnv or manually sourcing the environment
- Verify environment variables are set:
echo $TURNKEY_BUCK2_CONFIG echo $TURNKEY_BUCK2_TOOLCHAINS_CELL - Re-allow direnv:
direnv allow
Buck2 Can't Find Cells
If Buck2 reports missing cells:
- Check
.buckconfigis a valid symlink:ls -la .buckconfig - Verify cell paths in
.buckconfigexist:cat .buckconfig - Ensure you've entered the Nix shell:
nix develop
toolchain.toml
The toolchain.toml file declares which toolchains your project needs.
Basic Structure
[toolchains]
buck2 = {}
go = {}
rust = {}
python = {}
Each key under [toolchains] is a toolchain name that will be resolved from the registry.
Version Pinning
You can pin specific versions when the registry provides multiple versions:
[toolchains]
go = { version = "1.22" } # Pin to Go 1.22
python = { version = "3.11" } # Pin to Python 3.11
rust = {} # Use registry default
If no version is specified, the registry's default version is used.
Available Toolchains
Build Systems
buck2- Buck2 build system
Languages
go- Go compiler and toolsrust- Rust compiler (rustc)cargo- Cargo package managerclippy- Rust linterrustfmt- Rust formatterrust-analyzer- Rust LSP serverpython- Python interpreteruv- Python package managerruff- Python linter and formatternodejs- Node.js runtimetypescript- TypeScript compilerbiome- Fast linter/formatter for JS/TS/JSON
Solidity
solc- Solidity compilerfoundry- Ethereum dev toolkit (forge, cast, anvil)
Other Tools
nix- Nix package managerreindeer- Rust Buck2 target generatorjsonnet- Jsonnet to JSON compilermdbook- Documentation tooltk- Turnkey CLI wrapper for buck2
Internal Tools
Dependency generators (godeps-gen, rustdeps-gen, pydeps-gen, jsdeps-gen, soldeps-gen) are automatically included when their corresponding language is enabled. You don't need to list them in toolchain.toml.
For example, if you have go = {} in your toolchain.toml and buck2.go.enable = true in your flake.nix, godeps-gen will automatically be available in your shell.
Example Configurations
Minimal Go Project
[toolchains]
buck2 = {}
go = {}
Full-Stack Project
[toolchains]
# Build
buck2 = {}
# Backend
go = {}
python = {}
# Frontend
nodejs = {}
typescript = {}
biome = {}
# Development
nix = {}
Pinned Versions
[toolchains]
go = { version = "1.22" }
python = { version = "3.11" }
nodejs = { version = "20" }
rust = { version = "1.75" }
Custom Registries
The registry mapping toolchain names to packages can be customized in your flake.nix. See Registry Pattern for details on:
- Adding custom toolchains via
registryExtensions - Creating reusable registry overlays
- Multi-version toolchain support
Buck2 Integration
Turnkey provides first-class Buck2 integration with automatic toolchain and dependency cell generation.
Enabling Buck2
In your flake.nix:
turnkey.toolchains = {
enable = true;
declarationFiles.default = ./toolchain.toml;
buck2.enable = true;
};
Generated Cells
When Buck2 integration is enabled, Turnkey generates:
Toolchains Cell
Located at .turnkey/toolchains/, contains toolchain rules for each declared language:
toolchains//:go- Go toolchaintoolchains//:rust- Rust toolchaintoolchains//:python- Python toolchain- etc.
Prelude Cell
The Buck2 prelude is provided via Nix at .turnkey/prelude/. This ensures reproducible builds with a pinned prelude version.
Prelude Strategies
turnkey.toolchains.buck2.prelude = {
strategy = "nix"; # default, recommended
# Other options: "bundled", "git", "path"
};
- nix (default): Uses Turnkey's Nix-backed prelude with custom extensions
- bundled: Uses Buck2's built-in prelude
- git: Uses a git checkout
- path: Uses a local filesystem path
Dependency Cells
Language-specific dependency cells are generated when configured:
godeps//- Go dependencies from go-deps.tomlrustdeps//- Rust dependencies from rust-deps.tomlpydeps//- Python dependencies from python-deps.toml
See Managing Dependencies for configuration details.
The .turnkey Directory
Turnkey uses a .turnkey directory in your project root to store build artifacts, caches, and generated cells. This convention provides automatic isolation from language toolchains.
Why .turnkey?
The .turnkey directory serves as the isolation directory for Buck2 builds. By using a dot-prefixed name, we get automatic exclusion from most language toolchains:
| Tool | Behavior | Configuration Needed |
|---|---|---|
| Go | Ignores directories starting with . or _ | None (built-in) |
| Cargo | Doesn't auto-discover crates in dot directories | None (built-in) |
| pytest | Automatically ignores dot directories | None (built-in) |
| Jest | Requires explicit configuration | Yes |
| Vitest | Requires explicit configuration | Yes |
This means Go won't try to compile generated Buck2 cells, Cargo won't discover them as workspace members, and pytest won't scan them for tests.
Directory Structure
.turnkey/
├── books/ # mdbook serve output (gitignored)
├── prelude/ # Symlink to Buck2 prelude derivation
├── toolchains/ # Symlink to generated toolchains cell
├── godeps/ # Symlink to Go dependencies cell
├── rustdeps/ # Symlink to Rust dependencies cell
└── jsdeps/ # Symlink to JavaScript dependencies cell
The symlinks point to Nix store paths containing the generated Buck2 cells.
Buck2 Configuration
The .buckconfig sets the isolation directory:
[buck2]
isolation_dir = .turnkey
This tells Buck2 to store all build outputs under .turnkey/buck-out/ instead of the default buck-out/.
The tk Command
The tk command wraps buck2 and automatically translates the --isolation-dir flag to use .turnkey-prefixed directories:
# These are equivalent:
tk --isolation-dir=foo build //...
buck2 --isolation-dir=.turnkey-foo build //...
This allows multiple isolated builds while maintaining the dot-prefix convention.
JavaScript/TypeScript Configuration
Unlike Go, Cargo, and pytest, JavaScript test runners need explicit configuration to ignore dot directories.
Jest
Add to your jest.config.js:
module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'/buck-out/',
'/\\.' // Ignore all dot-prefixed directories
],
};
Or in package.json:
{
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
"/buck-out/",
"/\\."
]
}
}
Vitest
Add to your vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
exclude: [
'**/node_modules/**',
'**/buck-out/**',
'**/.*/**' // Ignore all dot-prefixed directories
],
},
});
Migration Notes
If you're migrating from a project that used buck-out/ directly:
-
One-time cache invalidation: Buck2 caches are stored per isolation directory. Switching to
.turnkeymeans a clean rebuild on first run. -
Update .gitignore: Ensure
.turnkey/is in your.gitignore:.turnkey/ -
Update CI scripts: If CI scripts reference
buck-out/, update them to.turnkey/buck-out/.
Multiple Isolation Directories
For advanced use cases (parallel builds, different configurations), you can use multiple isolation directories:
# Development build
tk build //...
# Release build with different isolation
tk --isolation-dir=release build //...
# Creates .turnkey-release/
# CI build
tk --isolation-dir=ci build //...
# Creates .turnkey-ci/
Each isolation directory maintains its own:
- Buck2 daemon
- Build cache
- Output artifacts
This is useful for:
- Running multiple Buck2 daemons simultaneously
- Keeping CI caches separate from local development
- Testing different build configurations
Shell Environment
Turnkey configures your development shell with all declared toolchains.
Environment Variables
When entering the shell, Turnkey sets:
PATH- Includes all toolchain binariesTURNKEY_DIRENV_LIB- Path to direnv integration library
direnv Integration
For automatic shell activation, use direnv with .envrc:
use flake
Shell Entry Hooks
Turnkey performs these actions on shell entry:
- Symlinks
.turnkey/preludeto the prelude cell - Symlinks
.turnkey/toolchainsto the generated toolchains - Updates dependency cell symlinks if configured
- Displays welcome message (if configured)
Verbose Mode
For debugging, set TURNKEY_VERBOSE=1:
TURNKEY_VERBOSE=1 nix develop
Multiple Shells
You can define multiple shells with different toolchains:
turnkey.toolchains.declarationFiles = {
default = ./toolchain.toml;
ci = ./toolchain.ci.toml;
};
Access with:
nix develop .#ci
IDE Integration
This guide explains how to configure your IDE to work seamlessly with Turnkey's automatic dependency synchronization.
Overview
Turnkey can automatically update rules.star files when you modify source code imports. While this happens automatically when running tk build, you can also configure your IDE to trigger sync on file save for immediate feedback.
VS Code
Run on Save Extension
Install the Run on Save extension, then add to your workspace .vscode/settings.json:
{
"emeraldwalk.runonsave": {
"commands": [
{
"match": "\\.(go|rs|py|ts|tsx|sol)$",
"cmd": "tk rules sync --quiet ${fileDirname}"
}
]
}
}
This runs tk rules sync on the directory containing the modified file whenever you save a source file.
Task-based Approach
Alternatively, create a VS Code task in .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "Sync rules.star",
"type": "shell",
"command": "tk rules sync",
"presentation": {
"reveal": "silent",
"panel": "shared"
},
"problemMatcher": []
}
]
}
Then bind it to a keyboard shortcut in keybindings.json:
{
"key": "ctrl+shift+s",
"command": "workbench.action.tasks.runTask",
"args": "Sync rules.star"
}
JetBrains IDEs (IntelliJ, GoLand, PyCharm, etc.)
File Watchers
-
Go to Settings > Tools > File Watchers
-
Click + to add a new watcher
-
Configure:
- Name:
Turnkey Rules Sync - File type:
Go files(or your language) - Scope:
Project Files - Program:
tk - Arguments:
rules sync --quiet $FileDir$ - Output paths to refresh:
$FileDir$/rules.star - Working directory:
$ProjectFileDir$
- Name:
-
Under Advanced Options:
- Check: "Trigger the watcher on external changes"
- Uncheck: "Auto-save edited files to trigger the watcher"
External Tools
Alternatively, set up an external tool:
-
Go to Settings > Tools > External Tools
-
Click + to add:
- Name:
Sync rules.star - Program:
tk - Arguments:
rules sync - Working directory:
$ProjectFileDir$
- Name:
-
Assign a keyboard shortcut in Keymap settings
Neovim
Add to your Neovim configuration:
-- Auto-run tk rules sync on save for supported file types
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = { "*.go", "*.rs", "*.py", "*.ts", "*.tsx", "*.sol" },
callback = function()
local file_dir = vim.fn.expand("%:p:h")
vim.fn.jobstart({ "tk", "rules", "sync", "--quiet", file_dir }, {
on_exit = function(_, code)
if code ~= 0 then
vim.notify("tk rules sync failed", vim.log.levels.WARN)
end
end,
})
end,
})
Emacs
Add to your Emacs configuration:
(defun turnkey-sync-rules ()
"Run tk rules sync on the current file's directory."
(when (and buffer-file-name
(string-match-p "\\.\\(go\\|rs\\|py\\|ts\\|tsx\\|sol\\)$" buffer-file-name))
(let ((default-directory (file-name-directory buffer-file-name)))
(start-process "tk-rules-sync" nil "tk" "rules" "sync" "--quiet" "."))))
(add-hook 'after-save-hook #'turnkey-sync-rules)
Configuration Options
sync.toml Settings
Configure rules sync behavior in .turnkey/sync.toml:
[rules]
enabled = true # Enable rules.star sync (default: false)
auto_sync = true # Auto-sync before tk build (default: true)
strict = false # Fail if rules would change - for CI (default: false)
[rules.go]
internal_prefix = "//src/go"
external_cell = "godeps"
[rules.rust]
internal_prefix = "//src/rust"
external_cell = "rustdeps"
[rules.python]
internal_prefix = "//src/python"
external_cell = "pydeps"
Command Line Options
tk rules sync # Sync only stale files (git-based detection)
tk rules sync --force # Force sync all files
tk rules sync --verbose # Show detailed output
tk rules sync --dry-run # Show what would change without writing
tk rules check # Check if any files need sync (exit 1 if stale)
tk rules check --force # Check all files, not just git-changed
Staleness Detection
Turnkey uses intelligent staleness detection to minimize unnecessary work:
- Git-based (default): Only checks directories with uncommitted source file changes
- Mtime-based (with
--force): Compares modification times of source files vs rules.star
This means tk rules sync is nearly instant in most cases, making it suitable for on-save hooks.
Preservation Markers
If you have manual dependencies that shouldn't be auto-managed, use preservation markers:
go_binary(
name = "my-app",
srcs = ["main.go"],
deps = [
# turnkey:auto-start
"godeps//vendor/github.com/google/uuid:uuid",
# turnkey:auto-end
# turnkey:preserve-start
# Manual override for special case
"//special:dep",
# turnkey:preserve-end
],
)
Dependencies between preserve-start and preserve-end markers are never modified by sync.
Troubleshooting
Sync not running
- Ensure
deps-extractis in your PATH (built withcargo install --path src/rust/deps-extract) - Check that
[rules] enabled = truein.turnkey/sync.toml - Verify the file type is supported (Go, Rust, Python, TypeScript, Solidity)
Sync too slow
- Use the default staleness detection (don't use
--forcein on-save hooks) - Target a specific directory:
tk rules sync src/cmd/myapp
Wrong dependencies detected
- Check your
*-deps.tomlfiles are up to date (runtk sync) - Verify internal prefix configuration in sync.toml
- Run
tk rules sync --verboseto see what's being detected
FUSE Composition Layer
The FUSE composition layer provides a unified filesystem view of your repository and its dependencies at a fixed mount location. This enables:
- Predictable paths for remote cache compatibility
- Transparent editing of external dependencies
- Automatic consistency management during updates
Quick Start
Manual (ad-hoc)
# Start the daemon for a single repo
turnkey-composed start --mount-point ~/firefly/turnkey --repo-root . --backend fuse
# Work from the mount point
cd ~/firefly/turnkey
buck2 build root//...
# Stop
turnkey-composed stop
As a service (recommended)
# Install the service (runs on login)
turnkey-composed install --start
# Edit the config to declare your mounts
vim ~/.config/turnkey/composed.toml
With home-manager (declarative)
{
imports = [ turnkey.homeManagerModules.turnkey-composed ];
services.turnkey-composed = {
enable = true;
package = turnkey.packages.${system}.turnkey-composed;
mounts = {
myproject = {
repo = "/Users/me/src/myproject";
mountPoint = "/firefly/myproject";
};
};
};
}
Prerequisites
Linux
# Verify FUSE is available
ls /dev/fuse
# If missing, install fuse3
sudo apt install fuse3 # Debian/Ubuntu
sudo dnf install fuse3 # Fedora
macOS
Install FUSE-T (no kernel extension, works on Apple Silicon):
brew install macos-fuse-t/homebrew-cask/fuse-t
Mount points under /: macOS root is read-only. The daemon
automatically manages /etc/synthetic.conf entries and activates them
via apfs.util -t when a mount point like /firefly/turnkey is
requested. This requires sudo (the daemon prompts when needed).
For paths under ~ (e.g., ~/firefly/turnkey), no special setup is
needed.
Service Configuration
The service reads ~/.config/turnkey/composed.toml:
# Mount a project
[[mounts]]
repo = "/Users/me/src/myproject"
mount_point = "/firefly/myproject"
# Mount another project
[[mounts]]
repo = "/Users/me/src/other-project"
mount_point = "/firefly/other"
backend = "fuse" # Optional: "auto" (default), "fuse", or "symlink"
The daemon watches this file for changes. When you add a new [[mounts]]
entry, the daemon picks it up and mounts it automatically — no restart
needed.
Home-Manager Module
The declarative alternative to editing the TOML file directly:
{
imports = [ turnkey.homeManagerModules.turnkey-composed ];
services.turnkey-composed = {
enable = true;
package = turnkey.packages.${system}.turnkey-composed;
mounts = {
myproject = {
repo = "/Users/me/src/myproject";
mountPoint = "/firefly/myproject";
};
other = {
repo = "/Users/me/src/other";
mountPoint = "/firefly/other";
backend = "fuse"; # Optional
};
};
};
}
This generates the config file and manages the launchd agent (macOS) or systemd user service (Linux).
Service Management
# Install and start the service
turnkey-composed install --start
# Uninstall the service
turnkey-composed uninstall
# The service runs `turnkey-composed serve` which:
# - Reads ~/.config/turnkey/composed.toml
# - Builds cells via nix for each repo
# - Mounts all entries
# - Watches for config and manifest changes
How Cell Discovery Works
On startup, turnkey-composed:
- Runs
nix evalto list*-cellpackages from each repo's flake - Runs
nix buildto build all cells in a single invocation (~3-4s if cached) - Uses the Nix store paths to populate
external/in the FUSE mount
Cells are always built from the current flake state. The daemon watches
manifest files (go-deps.toml, rust-deps.toml, etc.) and rebuilds
cells automatically when they change.
Mount Structure
/firefly/myproject/
├── .buckconfig # Virtual - generated by layout
├── .buckroot # Virtual - marks Buck2 root
├── root/ # Pass-through to your repository
│ ├── src/
│ ├── docs/
│ ├── flake.nix
│ └── ...
└── external/ # Dependency cells (from Nix store)
├── godeps/
├── rustdeps/
├── prelude/
├── toolchains/
└── ...
Buck2 runs from the mount root. Source targets use the root// cell
prefix: buck2 build root//src/cmd/tk:tk.
CLI Reference
Single Mount
# Start (foreground)
turnkey-composed start --mount-point <path> --repo-root <path> [--backend fuse|symlink|auto]
# With explicit config file
turnkey-composed start --config <path>
Service Mode
# Run as a service (reads ~/.config/turnkey/composed.toml)
turnkey-composed serve [--config <path>]
# Install/uninstall the system service
turnkey-composed install [--start]
turnkey-composed uninstall
Control
turnkey-composed status # Check daemon status
turnkey-composed refresh # Trigger manual cell rebuild
turnkey-composed stop # Stop the daemon
Platform Notes
Linux
Uses native FUSE via /dev/fuse with the fuser Rust crate. Best
performance.
macOS
Uses FUSE-T with direct C FFI bindings to libfuse3. FUSE-T translates FUSE operations to NFS internally. No kernel extension required.
The daemon handles synthetic firmlinks automatically for mount points
under / (manages /etc/synthetic.conf and runs apfs.util -t).
Symlinks (CI / Fallback)
Fastest for CI. No daemon needed. Automatically selected when FUSE is unavailable.
Integration with IDEs
VS Code / Cursor
{
"go.goroot": "/firefly/myproject/root",
"rust-analyzer.linkedProjects": ["/firefly/myproject/root/Cargo.toml"]
}
IntelliJ / GoLand
Set the project root to the FUSE mount point for consistent path resolution.
Building Projects
Turnkey integrates with Buck2 for building projects.
The tk Command
Use tk instead of buck2 directly. It provides:
- Automatic dependency sync before builds
- Consistent behavior across the team
tk build //path/to:target
Common Build Commands
# Build a specific target
tk build //src/examples/go-hello:go-hello
# Build all targets
tk build //...
# Build with verbose output
tk build //... -v
# Build in release mode
tk build //... -c release
Build Outputs
Build outputs are placed in buck-out/.turnkey/:
buck-out/
└── .turnkey/
├── gen/
│ └── root/
│ └── path/to/target/
└── tmp/
└── ...
Why .turnkey? The isolation directory starts with a dot so that language tools ignore it:
- Go skips directories starting with
.when scanning for packages - Cargo ignores dot-directories
- pytest ignores dot-directories by default
This prevents errors like Go trying to parse generated .go files in build outputs, or pytest collecting test files from there.
To find the output path for a specific target:
tk build //path/to:target --show-output
Skipping Sync
If you know dependencies haven't changed:
tk --no-sync build //...
Troubleshooting
Missing Toolchain
If you see "toolchain not found", ensure:
- The toolchain is declared in
toolchain.toml - You've re-entered the shell after adding it
Stale Dependencies
If builds fail with missing dependencies:
tk sync
tk build //...
Running Tests
Turnkey supports running tests via Buck2.
Test Commands
# Run tests for a specific target
tk test //path/to:target-test
# Run all tests
tk test //...
# Run tests matching a pattern
tk test //src/examples/...
Language-Specific Tests
Go Tests
tk test //src/go/pkg/mypackage:mypackage_test
Rust Tests
tk test //src/rust/mycrate:mycrate-test
Python Tests
tk test //src/python/mymodule:test
Test Output
Test results are displayed in the console. For detailed output:
tk test //... -- --nocapture
Filtering Tests
Pass arguments after -- to the test runner:
# Run specific test function (Go)
tk test //pkg:pkg_test -- -run TestSpecificFunction
# Run specific test (Rust)
tk test //crate:crate-test -- specific_test_name
Continuous Testing
For development, use Buck2's file watching:
tk test //path/to:target-test --watch
Managing Dependencies
This guide covers how external dependencies are managed in Turnkey projects.
Core Principles
1. No In-Repo Vendoring
Dependencies are never vendored into the repository. All dependency sources live in the Nix store.
- No
vendor/directories committed to git - No
node_modules/,__pycache__/, or similar cached dependencies - The repository contains only source code and dependency declarations
2. Language-Native Declarations Are the Source of Truth
Each language has its own dependency declaration format. These are the sole source of truth for what dependencies are needed:
| Language | Declaration Files |
|---|---|
| Go | go.mod, go.sum |
| Rust | Cargo.toml, Cargo.lock |
| Python | pyproject.toml, uv.lock |
These files define the dependency graph at the module level (not package/subpackage level).
3. Per-Module Fetching with Deterministic Hashes
Dependencies are fetched individually by Nix, each with its own content hash:
go.mod/go.sum → godeps-gen → go-deps.toml → Nix fetches each module
The intermediate TOML file (go-deps.toml, rust-deps.toml, etc.) contains:
- Module/crate/package identifiers
- Versions (from lock file)
- Nix-compatible SRI hashes (from prefetching)
4. Dependency Cells for Buck2
Dependencies are assembled into Buck2 cells by Nix:
go-deps.toml → go-deps-cell.nix → .turnkey/godeps/ (symlink to Nix store)
The cell contains:
- Fetched source files for each dependency
- Generated rules.star files for Buck2 to consume
- Any scaffolding needed by build tools (e.g.,
modules.txtfor Go)
Data Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ Source of Truth │
│ │
│ go.mod / go.sum Cargo.toml / Cargo.lock pyproject.toml │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Hash Generation Tools │
│ │
│ godeps-gen rustdeps-gen pydeps-gen │
│ │
│ Reads dependency declaration, fetches each module via nix-prefetch-* │
│ Outputs TOML with per-module SRI hashes │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Dependency TOML Files │
│ │
│ go-deps.toml rust-deps.toml python-deps.toml │
│ │
│ [deps."github.com/foo/bar"] │
│ version = "v1.2.3" │
│ hash = "sha256-..." │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Nix Cell Builders │
│ │
│ go-deps-cell.nix rust-deps-cell.nix python-deps-cell.nix │
│ │
│ - Reads TOML, fetches each module via fetchFromGitHub/fetchurl │
│ - Assembles into directory structure │
│ - Generates rules.star files │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Buck2 Cells (in .turnkey/) │
│ │
│ .turnkey/godeps/ .turnkey/rustdeps/ .turnkey/pydeps/ │
│ (symlinks to Nix store) │
│ │
│ Contains: source files, rules.star files, cell config │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Buck2 Build │
│ │
│ buck2 build //my/package:target │
│ │
│ References deps as: godeps//vendor/github.com/foo/bar:bar │
│ All sources already in Nix store - no network access needed │
└─────────────────────────────────────────────────────────────────────────┘
Auto-Sync with Wrapped Tools
When using go, cargo, or uv in a Turnkey shell, the tools are
transparently wrapped to trigger automatic dependency synchronization when
dependency files change.
# These trigger auto-sync when dependency files change
go get github.com/some/package
cargo add serde
uv add requests
How Auto-Sync Works
- The wrapper captures a hash of dependency files before running the command
- The actual tool runs (e.g.,
go get) - After completion, the wrapper checks if dependency files changed
- If changed,
tk syncis triggered automatically
Verbose Mode
Use verbose mode to see what the wrapper is doing:
tw -v go get github.com/some/package
Manual Sync
Force a full dependency sync with:
tk sync
Or sync specific languages:
tk sync --go
tk sync --rust
tk sync --python
Go Dependencies
Configuration
turnkey.toolchains.buck2.go = {
enable = true;
depsFile = ./go-deps.toml;
};
Generating go-deps.toml
godeps-gen --prefetch -o go-deps.toml
Options:
--prefetch: Fetch Nix hashes using nix-prefetch-github (required for valid hashes)--indirect: Include indirect (transitive) dependencies (default: true)-o: Output file (default: stdout)
Using Dependencies in Build Files
go_binary(
name = "hello",
srcs = ["main.go"],
deps = [
"godeps//vendor/github.com/spf13/cobra:cobra",
],
)
Local Replace Directives
Turnkey supports replace directives in go.mod that point to local paths.
This is essential for monorepo setups.
In go.mod:
replace github.com/company/shared-lib => ../shared-lib
In go-deps.toml (generated by godeps-gen):
[replace."github.com/company/shared-lib"]
import_path = "github.com/company/shared-lib"
local_path = "../shared-lib"
Configure the mapping in flake.nix:
turnkey.toolchains.buck2.go = {
enable = true;
depsFile = ./go-deps.toml;
localReplaces = {
"github.com/company/shared-lib" = "//src/shared-lib:shared-lib";
};
};
See the Go language guide for detailed documentation.
External Fork Replace Directives
Turnkey also supports replace directives that point to external forks:
In go.mod:
replace github.com/original/pkg => github.com/myfork/pkg v1.2.3
In go-deps.toml (generated by godeps-gen):
[deps."github.com/original/pkg@v1.2.3"]
import_path = "github.com/original/pkg"
fetch_path = "github.com/myfork/pkg"
version = "v1.2.3"
hash = "sha256-..."
The cell builder fetches from fetch_path but stores under import_path, so
your code continues importing from the original path while using the fork's
source.
See the Go language guide for detailed documentation.
Rust Dependencies
Configuration
turnkey.toolchains.buck2.rust = {
enable = true;
depsFile = ./rust-deps.toml;
};
Generating rust-deps.toml
rustdeps-gen --cargo-lock Cargo.lock -o rust-deps.toml
Options:
--cargo-lock: Path to Cargo.lock file (default: Cargo.lock)--no-prefetch: Skip prefetching (produces incorrect hashes)-o: Output file (default: stdout)
Handling Special Cases
Some Rust crates require additional configuration. See the Rust Dependency Handling guide for:
- Build scripts that emit rustc flags
- Generated source files
- Native code compilation
Python Dependencies
Configuration
turnkey.toolchains.buck2.python = {
enable = true;
depsFile = ./python-deps.toml;
};
Recommended Workflow (using uv)
# 1. Generate lock file from pyproject.toml
uv lock
# 2. Export to PEP 751 format
uv export --format pylock.toml -o pylock.toml
# 3. Generate python-deps.toml with Nix hashes
pydeps-gen --lock pylock.toml -o python-deps.toml
Input Formats
| Format | Flag | Reproducibility | Notes |
|---|---|---|---|
| pylock.toml (PEP 751) | --lock | Best | Exact versions and URLs |
| pyproject.toml | --pyproject | Varies | Uses latest matching versions |
| requirements.txt | --requirements | Varies | Pin versions with == for reproducibility |
CLI Options
--lock <PATH> Path to pylock.toml (PEP 751 lock file) - RECOMMENDED
--pyproject <PATH> Path to pyproject.toml
--requirements <PATH> Path to requirements.txt
-o, --output <PATH> Output file (default: stdout)
--no-prefetch Skip prefetching (produces placeholder hashes)
--include-dev Include dev dependencies from optional-dependencies.dev
Anti-Patterns to Avoid
Never Use vendorHash
Nix's buildGoModule has a vendorHash that hashes the output of
go mod vendor. This is problematic:
- Implementation-dependent: The hash changes based on which packages are actually imported
- Opaque: You can't know the hash without running the build and letting it fail
- Unstable: Adding a new import from an existing module can change the hash
Instead, use per-module fetching where each module has its own deterministic hash.
Never Vendor in Repository
Even temporarily. If you see a vendor/ directory in the repo, something is
wrong.
Never Compute Hashes from Vendored Output
The hash should come from the source (e.g., GitHub tarball), not from transformed/vendored output.
Troubleshooting
Dependencies Not Found
If Buck2 can't find a dependency:
-
Check that the deps TOML file is up to date:
tk sync -
Verify the cell symlink exists:
ls -la .turnkey/godeps -
Check the target path format:
# Correct format godeps//vendor/github.com/spf13/cobra:cobra # Wrong - missing vendor/ prefix godeps//github.com/spf13/cobra:cobra
Hash Mismatch Errors
If you get hash mismatch errors when building:
-
Regenerate the deps file with
--prefetch:godeps-gen --prefetch -o go-deps.toml -
Re-enter the dev shell:
exit nix develop
Stale Dependencies
If dependency changes aren't picked up:
-
Kill the Buck2 daemon:
buck2 kill -
Force a full sync:
tk sync
Python Workspaces
Turnkey lays out Python source as a uv workspace, in parallel to the Cargo workspace pattern used for Rust. A single uv.lock resolves every Python package in the monorepo against a consistent dependency set, while each package keeps its own pyproject.toml declaring exactly what it consumes.
Two tracks run side by side over the same source:
- uv track —
uv sync,uv run, IDE language servers, REPL. Members are installed editable so source edits are reflected immediately. - Buck2 track —
tk build,tk test. External packages are vendored into thepydepscell built frompython-deps.toml.
Repository Layout
/repo/
├── pyproject.toml # Workspace root: members + uv.lock anchor
├── uv.lock # Single resolved lockfile (managed by uv)
├── pylock.toml # PEP 751 export from uv.lock
├── python-deps.toml # Generated for Buck2/Nix from pylock.toml
└── src/python/<member>/
├── pyproject.toml # [project] + hatchling build backend
├── rules.star # Buck2 targets for the member
└── turnkey/<member>/ # Source under shared turnkey.* namespace
├── __init__.py
└── ...
Tests live in a sibling tests/ directory inside each member, kept outside the importable namespace.
The turnkey.* Namespace Convention
Every workspace member contributes a subpackage under the shared turnkey PEP 420 implicit namespace package. No member defines a top-level turnkey/__init__.py; Python's import system resolves turnkey.cargo, turnkey.cfg, etc. by walking every sys.path entry that exposes a turnkey/<name>/ directory.
Downstream Projects: Pick Your Own Namespace
The turnkey.* prefix is this repository's namespace. If you adopt the same workspace pattern in a different monorepo, choose a namespace specific to your organisation — e.g. acme.<name> — to avoid colliding with packages on PyPI or other turnkey-based repos. The mechanics are identical; substitute turnkey for your namespace throughout this guide.
Member pyproject.toml
Each library member uses the hatchling backend and points it at the turnkey/ directory:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "turnkey-cargo"
version = "0.1.0"
description = "Cargo manifest and feature-graph utilities"
requires-python = ">=3.11"
dependencies = [
"turnkey-cfg", # cross-member dep
]
[tool.uv.sources]
turnkey-cfg = { workspace = true }
[tool.hatch.build.targets.wheel]
packages = ["turnkey"] # everything under turnkey/<name>/ is the wheel content
packages = ["turnkey"] is the key line: it tells hatchling that the wheel's content is whatever lives under the turnkey/ directory of this member. Combined with PEP 420 namespace resolution, every member ships only its own turnkey/<name>/ slice without anyone owning turnkey/__init__.py.
Cross-Member Dependencies
Declare the dep under [project] dependencies with the bare package name, then pin its source to the workspace under [tool.uv.sources]:
dependencies = ["turnkey-cfg"]
[tool.uv.sources]
turnkey-cfg = { workspace = true }
This mirrors Cargo.toml's serde.workspace = true pattern — the consumer member doesn't pin a version, the lockfile reconciles it.
External Dependencies
Declare externals in the member that consumes them, never the workspace root:
# src/examples/python-hello-deps/pyproject.toml
[project]
name = "turnkey-example-python-hello-deps"
dependencies = ["six>=1.16.0"]
The single uv.lock at the workspace root resolves every external version-consistently across members.
Non-Packaged Members
Some members exist only to declare dependencies, not to be installed (typical for application-like entrypoints or examples). Mark them non-packaged:
[project]
name = "turnkey-example-python-hello-deps"
version = "0.1.0"
dependencies = ["six>=1.16.0"]
[tool.uv]
package = false # uv won't build/install this member
No [build-system] is required. uv still resolves the member's dependencies as part of the workspace lock.
Root pyproject.toml
The workspace root anchors membership and the shared lockfile:
[project]
name = "turnkey"
version = "0.1.0"
requires-python = ">=3.11"
# Listing members as dependencies makes the default `uv sync` install all
# of them in one shot — no `--all-packages` flag needed.
dependencies = [
"turnkey-buck",
"turnkey-buildsystem",
"turnkey-cargo",
"turnkey-cfg",
"turnkey-example-python-hello",
"turnkey-example-python-hello-deps",
]
[dependency-groups]
# Dev tooling — auto-installed by 'uv sync' so 'uv run pytest' Just Works.
dev = ["pytest>=7.0"]
[tool.uv.workspace]
members = [
"src/python/cargo",
"src/python/buck",
"src/python/buildsystem",
"src/python/cfg",
"src/examples/python-hello",
"src/examples/python-hello-deps",
]
[tool.uv.sources]
turnkey-buck = { workspace = true }
turnkey-buildsystem = { workspace = true }
turnkey-cargo = { workspace = true }
turnkey-cfg = { workspace = true }
turnkey-example-python-hello = { workspace = true }
turnkey-example-python-hello-deps = { workspace = true }
[tool.uv]
package = false # the root itself isn't a packaged project
Buck2 Integration
Member source paths are spelled relative to the member's rules.star:
load("@prelude//:rules.bzl", "python_library", "python_test")
python_library(
name = "cargo",
srcs = [
"turnkey/cargo/__init__.py",
"turnkey/cargo/features.py",
"turnkey/cargo/toml.py",
],
base_module = "",
deps = ["//src/python/cfg:cfg"],
visibility = ["PUBLIC"],
)
python_test(
name = "test_toml",
srcs = ["tests/test_toml.py"],
base_module = "tests",
deps = [":cargo"],
)
base_module = "" tells Buck2 to install sources at their declared srcs paths, so files land at turnkey/cargo/... in the runtime tree — matching the import prefix the rest of the codebase uses.
Adding or Updating Dependencies
# 1. Edit the member that needs the dep
$EDITOR src/python/cargo/pyproject.toml # add to [project] dependencies
# 2. Regenerate the lock
uv lock
# 3. Refresh editable installs (optional but recommended)
uv sync
# 4. Export to PEP 751 lock for the Buck2 pipeline
# --all-packages: include externals from every member
# --no-dev: exclude dev tooling (pytest etc.) from the pydeps cell
uv export --all-packages --no-dev --format pylock.toml -o pylock.toml
# 5. Refresh python-deps.toml for the pydeps cell
# (tk sync picks this up automatically when pylock.toml is newer)
tk sync
Steps 2–4 are manual today; future work can fold them into tk sync as a pre-step.
Running Code
| Task | uv track | Buck2 track |
|---|---|---|
| Run all tests | uv run pytest | tk test //src/python/... |
| Run a single member's tests | uv run pytest src/python/cargo | tk test //src/python/cargo:test_toml |
| Run an example | uv run --package <pkg-name> <script> | tk run //src/examples/python-hello-deps:python-hello-deps |
| REPL with members available | uv run python | n/a |
| IDE language server | Point at .venv/bin/python | n/a |
Both tracks resolve external dependencies the same way (uv.lock is the single source of truth), but the install paths differ: the uv track installs into .venv/, the Buck2 track materialises external packages into .turnkey/pydeps/vendor/<name>/.
See Also
- Managing Dependencies — overall dependency flow across all languages.
- Python — Buck2 build rules for Python targets.
Go Support
Turnkey provides comprehensive Go support with Buck2 integration.
Setup
Add to toolchain.toml:
[toolchains]
go = {}
godeps-gen = {}
Enable Go dependencies in flake.nix:
turnkey.toolchains.buck2.go = {
enable = true;
depsFile = ./go-deps.toml;
};
Project Structure
my-project/
├── go.mod
├── go.sum
├── go-deps.toml # Generated from go.mod
├── cmd/
│ └── myapp/
│ ├── main.go
│ └── rules.star
└── pkg/
└── mylib/
├── lib.go
└── rules.star
Build Rules
In rules.star:
load("@prelude//go:go.bzl", "go_binary", "go_library")
go_binary(
name = "myapp",
srcs = ["main.go"],
deps = ["//pkg/mylib:mylib"],
)
External Dependencies
Reference third-party packages via the godeps cell:
go_library(
name = "mylib",
srcs = ["lib.go"],
deps = ["godeps//github.com/pkg/errors:errors"],
)
Auto-Sync
The go command is wrapped to auto-sync dependencies:
go get github.com/some/package # Triggers sync
go mod tidy # Triggers sync
Local Replace Directives
Turnkey supports replace directives in go.mod that point to local paths within your monorepo. This is useful for:
- Internal packages shared across multiple modules
- Local development overrides
- Monorepo setups with multiple Go modules
How It Works
-
In go.mod, declare the local replacement:
module github.com/company/myapp require github.com/company/shared-lib v1.0.0 replace github.com/company/shared-lib => ../shared-lib -
Run godeps-gen to update
go-deps.toml. The local replace will be output:[replace."github.com/company/shared-lib"] import_path = "github.com/company/shared-lib" local_path = "../shared-lib" -
Configure the target mapping in your
flake.nix. You need to tell Turnkey which Buck2 target corresponds to each local replacement:turnkey.toolchains.buck2.go = { enable = true; depsFile = ./go-deps.toml; localReplaces = { # Map import path -> Buck2 target "github.com/company/shared-lib" = "//src/shared-lib:shared-lib"; }; }; -
Write the local target's rules.star. The target must export the package with the correct import path:
# src/shared-lib/rules.star go_library( name = "shared-lib", package_name = "github.com/company/shared-lib", srcs = glob(["*.go"]), visibility = ["PUBLIC"], )
Subpackages
Local replacements automatically handle subpackages. If you replace github.com/company/shared-lib, then imports of github.com/company/shared-lib/subpkg will resolve to //src/shared-lib/subpkg:subpkg.
Example: Monorepo Setup
my-monorepo/
├── go.mod # Root module with replace directives
├── go-deps.toml # Generated by godeps-gen
├── flake.nix # Configure localReplaces here
├── src/
│ ├── app/
│ │ ├── main.go # imports github.com/company/shared-lib
│ │ └── rules.star
│ └── shared-lib/
│ ├── lib.go # The local replacement
│ ├── subpkg/
│ │ └── sub.go
│ └── rules.star
In rules.star for the app:
go_binary(
name = "app",
srcs = ["main.go"],
deps = [
"//src/shared-lib:shared-lib", # Resolved from local replace
],
)
External Fork Replacements
Turnkey supports replace directives that point to external forks (not local paths). This is useful when:
- Using a forked version of a dependency with bug fixes
- Using a maintained fork of an abandoned project
- Testing changes before upstreaming
How It Works
When godeps-gen encounters an external replace directive like:
replace github.com/original/pkg => github.com/myfork/pkg v1.2.3
It will:
- Set the dependency's
import_pathtogithub.com/original/pkg(for correct imports) - Set the
fetch_pathtogithub.com/myfork/pkg(where to actually fetch from) - Use the replacement version
In go.mod
module github.com/company/myapp
require github.com/original/pkg v1.0.0
replace github.com/original/pkg => github.com/myfork/pkg v1.2.3
Generated go-deps.toml
[deps."github.com/original/pkg@v1.2.3"]
import_path = "github.com/original/pkg"
fetch_path = "github.com/myfork/pkg"
version = "v1.2.3"
hash = "sha256-..."
How the Cell Builder Uses This
The Nix cell builder:
- Fetches the source from
fetch_path(the fork) - Stores it in the vendor directory under
import_path(the original path) - Generates Buck2 rules using the original import path
This means your code continues to import from the original path (github.com/original/pkg), but the actual source comes from your fork.
Version Handling
External replaces can change the version:
| go.mod replace | Result |
|---|---|
=> github.com/fork v1.2.3 | Uses v1.2.3 from fork |
=> github.com/fork (no version) | Uses the required version from fork |
Version-specific replaces are also supported:
// Only replace v1.0.0, not other versions
replace github.com/pkg v1.0.0 => github.com/fork/pkg v1.0.1
Common Use Cases
Using a fork with a fix:
// Your fork has a critical bug fix not yet merged upstream
replace github.com/upstream/logger => github.com/you/logger v1.0.1-patched
Using a maintained fork:
// Original project abandoned, using community fork
replace github.com/old/abandoned => github.com/community/maintained v2.0.0
Testing before upstreaming:
// Test your changes before creating a PR
replace github.com/original/pkg => github.com/you/pkg v0.0.0-20240101
Rust Support
Turnkey provides Rust support with automatic dependency management.
Setup
Add to toolchain.toml:
[toolchains]
rust = {}
cargo = {}
rustdeps-gen = {}
Enable Rust dependencies in flake.nix:
turnkey.toolchains.buck2.rust = {
enable = true;
depsFile = ./rust-deps.toml;
featuresFile = ./rust-features.toml; # Optional
};
Project Structure
my-project/
├── Cargo.toml
├── Cargo.lock
├── rust-deps.toml # Generated from Cargo.lock
├── rust-features.toml # Manual feature overrides
└── rust/
└── mycrate/
├── src/
│ └── lib.rs
└── rules.star
Build Rules
In rules.star:
load("@prelude//rust:rust.bzl", "rust_library", "rust_binary")
rust_library(
name = "mycrate",
srcs = glob(["src/**/*.rs"]),
deps = ["rustdeps//serde:serde"],
)
External Dependencies
Reference crates via the rustdeps cell:
deps = [
"rustdeps//serde:serde",
"rustdeps//tokio:tokio",
]
Feature Overrides
Use rust-features.toml for manual feature control:
[overrides]
serde = ["derive", "std"]
tokio = ["full"]
Auto-Sync
The cargo command is wrapped to auto-sync:
cargo add serde # Triggers sync
Python Support
Turnkey provides Python support with Buck2 integration.
Python source in this repo is laid out as a uv workspace, with each package owning its own
pyproject.tomland contributing to a sharedturnkey.*PEP 420 namespace. This page covers the Buck2 build rules; read the workspace workflow guide first for the overall layout and the uv/Buck2 dual-track model.
Setup
Add to toolchain.toml:
[toolchains]
python = {}
uv = {}
pydeps-gen = {}
Enable Python dependencies in flake.nix:
turnkey.toolchains.buck2.python = {
enable = true;
depsFile = ./python-deps.toml;
};
Project Structure
my-project/
├── pyproject.toml
├── uv.lock
├── python-deps.toml # Generated from uv.lock
└── python/
└── mypackage/
├── __init__.py
├── main.py
└── rules.star
Build Rules
In rules.star:
load("@prelude//python:python.bzl", "python_library", "python_binary", "python_test")
python_library(
name = "mypackage",
srcs = glob(["**/*.py"]),
deps = ["pydeps//requests:requests"],
)
python_binary(
name = "main",
main = "main.py",
deps = [":mypackage"],
)
python_test(
name = "test",
srcs = ["test_main.py"],
deps = [":mypackage"],
)
External Dependencies
Reference packages via the pydeps cell:
deps = [
"pydeps//requests:requests",
"pydeps//click:click",
]
Auto-Sync
The uv command is wrapped to auto-sync:
uv add requests # Triggers sync
TypeScript Support
Turnkey provides TypeScript support via custom Buck2 rules.
Setup
Add to toolchain.toml:
[toolchains]
nodejs = {}
typescript = {}
Project Structure
my-project/
└── ts/
└── myapp/
├── src/
│ └── index.ts
├── tsconfig.json # Optional
└── rules.star
Build Rules
In rules.star:
load("@prelude//typescript:typescript.bzl", "typescript_binary", "typescript_library")
typescript_library(
name = "lib",
srcs = glob(["src/**/*.ts"]),
)
typescript_binary(
name = "myapp",
main = "src/index.ts",
srcs = glob(["src/**/*.ts"]),
deps = [":lib"],
)
Running TypeScript
tk run //ts/myapp:myapp
Configuration
The TypeScript toolchain uses sensible defaults. For custom configuration, provide a tsconfig.json:
typescript_binary(
name = "myapp",
main = "src/index.ts",
srcs = glob(["src/**/*.ts"]),
tsconfig = "tsconfig.json",
)
Note on Dependencies
TypeScript/JavaScript dependency management (npm/pnpm) is not yet fully integrated. For now, use genrule for npm-based builds or reference pre-built JavaScript.
Solidity Support
Turnkey provides Solidity smart contract support with Buck2 integration, including compilation, testing with Foundry, and dependency management.
Setup
Add to toolchain.toml:
[toolchains]
solidity = {}
foundry = {}
Enable Solidity dependencies in flake.nix (if using external libraries):
turnkey.toolchains.buck2.solidity = {
enable = true;
depsFile = ./solidity-deps.toml;
};
Project Structure
my-project/
├── foundry.toml # Foundry configuration
├── solidity-deps.toml # Generated dependency manifest
├── src/
│ └── contracts/
│ ├── MyToken.sol
│ └── rules.star
└── test/
├── MyToken.t.sol
└── rules.star
Build Rules
solidity_library
Compile Solidity source files:
load("@prelude//solidity:solidity.bzl", "solidity_library")
solidity_library(
name = "my_token",
srcs = ["MyToken.sol"],
deps = ["//soldeps:openzeppelin_contracts"],
solc_version = "0.8.20", # Optional: specify compiler version
optimizer = True,
optimizer_runs = 200,
)
solidity_contract
Extract a specific contract from compiled sources:
load("@prelude//solidity:solidity.bzl", "solidity_contract")
solidity_contract(
name = "my_token_artifact",
contract = "MyToken", # Contract name in source
deps = [":my_token"],
)
This produces:
{name}.abi- Contract ABI (JSON){name}.bin- Deployment bytecode
solidity_test
Run tests with Foundry's forge test:
load("@prelude//solidity:solidity.bzl", "solidity_test")
solidity_test(
name = "my_token_test",
srcs = ["MyToken.t.sol"],
deps = [
"//src/contracts:my_token",
"//soldeps:forge-std",
],
fuzz_runs = 256, # Optional: fuzz test iterations
)
External Dependencies
OpenZeppelin and npm packages
Reference npm packages via the soldeps cell:
solidity_library(
name = "my_token",
srcs = ["MyToken.sol"],
deps = ["//soldeps:openzeppelin_contracts"],
)
In your Solidity file:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
Import remappings are auto-generated based on the dependency structure.
Foundry git dependencies
Dependencies declared in foundry.toml are also supported:
[dependencies]
forge-std = "github:foundry-rs/forge-std"
solady = "github:vectorized/solady"
Compiler Version
You can specify the Solidity compiler version per-target:
solidity_library(
name = "legacy_contract",
srcs = ["Legacy.sol"],
solc_version = "0.7.6", # Use older compiler
)
solidity_library(
name = "modern_contract",
srcs = ["Modern.sol"],
solc_version = "0.8.20", # Use newer compiler
)
Building and Testing
# Build contracts
tk build //src/contracts:my_token
# Run tests
tk test //test:my_token_test
# Build all Solidity targets
tk build //... --target-platforms //platforms:solidity
Forge Integration
The solidity_test rule wraps Foundry's forge test, supporting:
- Unit tests
- Fuzz testing
- Fork testing (with
fork_urlattribute) - Gas reports
solidity_test(
name = "integration_test",
srcs = ["Integration.t.sol"],
deps = [":my_token"],
fork_url = "https://eth-mainnet.g.alchemy.com/v2/...", # Optional
)
Jsonnet Support
Turnkey provides Jsonnet support for generating JSON configuration files with Buck2 integration. Jsonnet is a data templating language that extends JSON with variables, functions, and imports.
Setup
Add to toolchain.toml:
[toolchains]
jsonnet = {}
Turnkey uses jrsonnet, a fast Rust implementation of Jsonnet.
Project Structure
my-project/
├── config/
│ ├── base.libsonnet # Shared configuration
│ ├── dev.jsonnet # Development config
│ ├── prod.jsonnet # Production config
│ └── rules.star
Build Rules
jsonnet_library
Compile Jsonnet files to JSON:
load("@prelude//jsonnet:jsonnet.bzl", "jsonnet_library")
jsonnet_library(
name = "config-dev",
srcs = ["dev.jsonnet"],
deps = [":base"], # Dependencies on other jsonnet_library targets
ext_strs = {
"env": "development",
"region": "us-west-2",
},
)
Attributes
| Attribute | Description |
|---|---|
srcs | Jsonnet source files (first file is entry point) |
deps | Dependencies on other jsonnet_library targets |
out | Output filename (defaults to <src>.json) |
ext_strs | External string variables (--ext-str key=value) |
ext_codes | External code variables (--ext-code key=value) |
tla_strs | Top-level argument strings (--tla-str key=value) |
tla_codes | Top-level argument code (--tla-code key=value) |
Example
base.libsonnet
{
// Shared configuration
appName: 'my-app',
version: '1.0.0',
// Environment-specific overrides
envConfig(env):: {
development: {
logLevel: 'debug',
replicas: 1,
},
production: {
logLevel: 'warn',
replicas: 3,
},
}[env],
}
dev.jsonnet
local base = import 'base.libsonnet';
local env = std.extVar('env');
base {
environment: env,
config: base.envConfig(env),
}
rules.star
load("@prelude//jsonnet:jsonnet.bzl", "jsonnet_library")
# Shared library
jsonnet_library(
name = "base",
srcs = ["base.libsonnet"],
)
# Development config
jsonnet_library(
name = "config-dev",
srcs = ["dev.jsonnet"],
deps = [":base"],
ext_strs = {"env": "development"},
)
# Production config
jsonnet_library(
name = "config-prod",
srcs = ["dev.jsonnet"], # Same template, different vars
deps = [":base"],
ext_strs = {"env": "production"},
out = "config-prod.json",
)
Building
# Build a specific config
tk build //config:config-dev
# View the output
tk build //config:config-dev --show-output
cat $(tk build //config:config-dev --show-output 2>&1 | grep -o 'buck-out/[^ ]*')
# Build all configs
tk build //config:...
External Variables
ext_strs (External Strings)
Pass string values from the build system:
jsonnet_library(
name = "config",
srcs = ["config.jsonnet"],
ext_strs = {
"env": "production",
"version": "1.2.3",
},
)
Access in Jsonnet:
{
environment: std.extVar('env'),
version: std.extVar('version'),
}
ext_codes (External Code)
Pass Jsonnet expressions:
jsonnet_library(
name = "config",
srcs = ["config.jsonnet"],
ext_codes = {
"replicas": "3",
"features": "['auth', 'api']",
},
)
Top-Level Arguments
For parameterized configs using functions:
// config.jsonnet
function(env, replicas=1) {
environment: env,
replicas: replicas,
}
jsonnet_library(
name = "config",
srcs = ["config.jsonnet"],
tla_strs = {"env": "production"},
tla_codes = {"replicas": "5"},
)
Use Cases
- Kubernetes manifests - Generate YAML/JSON configs with environment-specific values
- Application configuration - Type-safe config generation with inheritance
- Infrastructure as Code - Generate Terraform JSON, CloudFormation, etc.
- CI/CD pipelines - Generate pipeline configs from templates
CLI Reference
This reference covers all Turnkey CLI commands.
tk - Buck2 Wrapper
tk is the primary Turnkey CLI. It wraps Buck2 with automatic dependency synchronization.
Overview
When using Buck2 with Nix-managed dependencies, certain files must be regenerated when source files change. tk solves this by automatically running sync operations before buck2 commands that read the build graph.
Quick Start
# Use tk just like buck2 - it syncs automatically
tk build //some:target # syncs first, then builds
tk test //some:target # syncs first, then tests
tk run //some:target # syncs first, then runs
# Explicit sync operations
tk sync # manually sync all stale files
tk check # check if files are stale (for CI)
# Skip sync when needed
tk --no-sync build //... # skip sync, run buck2 directly
Command Reference
tk build/run/test/... (Buck2 passthrough)
Most tk commands are passed through to Buck2. Commands that read the build graph automatically sync first:
Sync-first commands (sync before running):
build- Build targetsrun- Run a targettest- Run testsquery- Query the build graphcquery- Configured queryuquery- Unconfigured querytargets- List targetsaudit- Audit the buildbxl- Run BXL scripts
Pass-through commands (no sync):
clean- Clean build artifactskill- Kill Buck2 daemonkillall- Kill all Buck2 processesstatus- Show daemon statuslog- View build logsrage- Generate debug reporthelp- Show helpdocs- Open documentationinit- Initialize a project
Unknown commands default to syncing first (safe default).
tk sync
Explicitly synchronize all stale files.
tk sync # sync stale files
tk sync --verbose # show what's being synced
tk sync --dry-run # show what would be synced without doing it
Exit codes:
0- Success (files synced or nothing to sync)1- Sync failed
tk check
Check if any files are stale without regenerating them. Useful for CI validation.
tk check # check staleness
tk check --verbose # show detailed status
Exit codes:
0- All files up-to-date1- Files are stale (runtk syncto fix)
Example CI usage:
- name: Check files in sync
run: tk check
tk completion
Generate shell completion scripts.
tk completion bash # output bash completion script
tk completion zsh # output zsh completion script
tk completion fish # output fish completion script
Enable completions:
# Bash (add to ~/.bashrc)
eval "$(tk completion bash)"
# Zsh (add to ~/.zshrc)
eval "$(tk completion zsh)"
# Fish (run once)
tk completion fish > ~/.config/fish/completions/tk.fish
tk rules
Manage rules.star files that define Buck2 build targets from source files. This command automatically detects imports from source files and updates the deps list in rules.star.
tk rules check # Check if rules.star files need updates
tk rules sync # Update rules.star files with detected dependencies
tk rules help # Show help
Options:
| Flag | Description |
|---|---|
--all, -a | Process all files (skip staleness detection) |
--force, -f | Same as --all |
--verbose, -v | Show detailed output including skipped files |
--quiet, -q | Suppress output |
--dry-run, -n | Show what would be changed without writing |
Staleness Detection:
By default, only files where source files are newer than rules.star are processed. Use --all or --force to check/sync all files.
Examples:
tk rules check # Check stale rules.star files
tk rules check --all # Check all rules.star files
tk rules sync # Update stale rules.star files
tk rules sync --all # Force update all files
tk rules sync src/cmd/tk # Sync specific directory
tk rules sync --dry-run # Preview changes without writing
Preserving Manual Dependencies:
If you have manual dependencies that shouldn't be auto-detected, use preservation markers in your rules.star:
# turnkey:preserve-start
"//some/manual:dep",
# turnkey:preserve-end
Dependencies within these markers are preserved during sync.
tk Flags
Flags must come before the subcommand:
| Flag | Description |
|---|---|
--no-sync | Skip sync, run Buck2 directly |
--no-local | Skip local target overrides from .turnkey/local.toml |
--verbose, -v | Show what tk is doing |
--dry-run, -n | Show what would be synced without doing it |
--quiet, -q | Suppress non-error output |
--help, -h | Show help |
Examples:
tk --no-sync build //... # skip sync
tk --no-local run //target # skip local overrides
tk --verbose sync # verbose sync
tk -v -n sync # dry-run with verbose output
Configuration
Sync Configuration File
tk reads staleness rules from .turnkey/sync.toml. This file is automatically generated from your Nix configuration.
When you configure dependency files in your flake.nix:
goDepsFilegenerates a Go deps rulerustDepsFilegenerates a Rust deps rulepythonDepsFilegenerates a Python deps rule
Example generated sync.toml:
[[deps]]
name = "go"
sources = ["go.mod", "go.sum"]
target = "go-deps.toml"
generator = ["godeps-gen", "--go-mod", "go.mod", "--go-sum", "go.sum", "--prefetch"]
[[deps]]
name = "rust"
sources = ["Cargo.toml", "Cargo.lock"]
target = "rust-deps.toml"
generator = ["rustdeps-gen", "--cargo-lock", "Cargo.lock"]
Each [[deps]] entry defines:
name- Human-readable name for this rulesources- Files that trigger regeneration when modifiedtarget- The generated filegenerator- Command to regenerate the target
Local Target Overrides
tk supports per-developer local overrides via .turnkey/local.toml. This file is not committed to git, allowing each developer to customize target arguments for their local environment.
Use cases:
- Different network addresses for local development
- Debug flags for specific targets
- Custom ports or configuration
Example .turnkey/local.toml:
# Override args for tk run
[run."//docs/user-manual"]
args = ["-n", "192.168.1.100"]
# Override args for tk build
[build."//src/cmd/server:server"]
args = ["--config=debug"]
# Pattern matching with "..."
[test."//src/..."]
args = ["--verbose", "--timeout=60s"]
How it works:
When you run a command that matches a configured target:
tk run //docs/user-manual
# Becomes: buck2 run //docs/user-manual -- -n 192.168.1.100
The args are injected after --, which passes them to the target binary.
Pattern matching:
Patterns ending with ... match any target with that prefix:
//src/...matches//src:foo,//src/pkg:bar,//src/cmd/tool:main//...matches any target
Disable for a single command:
tk --no-local run //docs/user-manual # skips local.toml
Verbose output:
tk --verbose run //docs/user-manual
# Output: tk: applying local override for run //docs/user-manual: [-n 192.168.1.100]
Shell Integration
buck2 alias to tk:
# In devenv shell, buck2 is aliased to tk
buck2 build //... # actually runs: tk build //...
Disable with:
TURNKEY_NO_ALIAS=1 buck2 build //... # uses raw buck2
tw - Native Tool Wrapper
tw wraps native language tools (go, cargo, uv) to keep dependency files in sync when using standard workflows.
The Problem
When you run go get github.com/foo/bar, Go updates go.mod and go.sum. But Buck2 needs go-deps.toml to know about the new dependency. Without auto-sync, you'd need to manually regenerate it.
The Solution
Turnkey transparently wraps go, cargo, and uv so that dependency sync happens automatically:
go get github.com/foo/bar # Just works - go-deps.toml is auto-updated
How It Works
User runs: go get github.com/foo/bar
│
▼
Shell wrapper (provides 'go' binary)
• Sets TURNKEY_REAL_GO to actual go binary path
• Calls: tw go get github.com/foo/bar
│
▼
tw (turnkey wrapper)
1. Loads .turnkey/sync.toml configuration
2. Finds wrapper rule for 'go'
3. Checks if 'get' is a mutating subcommand → yes
4. Captures SHA256 hashes of go.mod, go.sum
5. Runs the real 'go get' command
6. Compares hashes - detects changes
7. Runs godeps-gen to regenerate go-deps.toml
Supported Tools
| Tool | Mutating Commands | Watch Files | Deps Target |
|---|---|---|---|
go | get, mod | go.mod, go.sum | go-deps.toml |
cargo | add, remove, update | Cargo.toml, Cargo.lock | rust-deps.toml |
uv | add, remove, lock, sync | pyproject.toml, uv.lock | python-deps.toml |
Escape Hatches
Bypass for a Single Command
TURNKEY_NO_WRAP=1 go get github.com/foo/bar
This runs the real go directly, skipping tw entirely.
Disable Sync for a Command
tw --no-sync go get github.com/foo/bar
This runs through tw but skips the sync step even if files change.
Verbose Output
tw -v go get github.com/foo/bar
Shows what tw is doing:
tw: capturing state of [go.mod go.sum]
tw: detected changes in [go.mod go.sum], running sync
Syncing go-deps.toml...
Running: godeps-gen --go-mod go.mod --go-sum go.sum --prefetch
Regenerated go-deps.toml
Non-Mutating Commands
Commands not in mutating_subcommands pass through without any overhead:
go build ./... # No snapshot, no sync check - just runs go build
go version # Direct passthrough
godeps-gen
Generate go-deps.toml from go.mod and go.sum.
Usage
godeps-gen [OPTIONS]
Options
| Option | Description |
|---|---|
--go-mod PATH | Path to go.mod file (default: go.mod) |
--go-sum PATH | Path to go.sum file (default: go.sum) |
--prefetch | Fetch Nix hashes using nix-prefetch-github |
--indirect | Include indirect dependencies (default: true) |
-o, --output PATH | Output file (default: stdout) |
Examples
# Generate with prefetched hashes
godeps-gen --prefetch -o go-deps.toml
# Use custom paths
godeps-gen --go-mod src/go.mod --go-sum src/go.sum -o go-deps.toml
# Quick check without fetching (placeholder hashes)
godeps-gen --no-prefetch
rustdeps-gen
Generate rust-deps.toml from Cargo.lock.
Usage
rustdeps-gen [OPTIONS]
Options
| Option | Description |
|---|---|
--cargo-lock PATH | Path to Cargo.lock file (default: Cargo.lock) |
--no-prefetch | Skip prefetching (produces incorrect hashes) |
-o, --output PATH | Output file (default: stdout) |
Examples
# Generate from default Cargo.lock
rustdeps-gen -o rust-deps.toml
# Use custom path
rustdeps-gen --cargo-lock rust/Cargo.lock -o rust-deps.toml
pydeps-gen
Generate python-deps.toml from Python dependency files.
Usage
pydeps-gen [OPTIONS]
Options
| Option | Description |
|---|---|
--lock PATH | Path to pylock.toml (PEP 751 lock file) - RECOMMENDED |
--pyproject PATH | Path to pyproject.toml |
--requirements PATH | Path to requirements.txt |
-o, --output PATH | Output file (default: stdout) |
--no-prefetch | Skip prefetching (produces placeholder hashes) |
--include-dev | Include dev dependencies |
Input Formats
| Format | Flag | Reproducibility | Notes |
|---|---|---|---|
| pylock.toml (PEP 751) | --lock | Best | Exact versions and URLs |
| pyproject.toml | --pyproject | Varies | Uses latest matching versions |
| requirements.txt | --requirements | Varies | Pin versions with == |
Recommended Workflow
# 1. Generate lock file from pyproject.toml
uv lock
# 2. Export to PEP 751 format
uv export --format pylock.toml -o pylock.toml
# 3. Generate python-deps.toml with Nix hashes
pydeps-gen --lock pylock.toml -o python-deps.toml
Examples
# From PEP 751 lock file (best for reproducibility)
pydeps-gen --lock pylock.toml -o python-deps.toml
# From pyproject.toml (resolves to latest matching versions)
pydeps-gen --pyproject pyproject.toml -o python-deps.toml
# From requirements.txt
pydeps-gen --requirements requirements.txt -o python-deps.toml
# Include dev dependencies
pydeps-gen --lock pylock.toml --include-dev -o python-deps.toml
Troubleshooting
"tk: .buckconfig not found"
tk looks for .buckconfig to find the project root. Make sure you're in a Buck2 project directory.
"tk: failed to load sync config"
The .turnkey/sync.toml file is missing or invalid. Ensure you're in a Turnkey project with proper configuration.
"tk: sync failed: generator command failed"
The generator command failed. Check that:
- The generator command is correct
- Required tools are in PATH
- Source files exist
Sync is slow
If sync takes a long time:
- Use
--prefetchwith godeps-gen to cache downloads - Check if generators are doing unnecessary work
Bypass tk
If you need to use raw Buck2:
# Option 1: --no-sync flag
tk --no-sync build //...
# Option 2: TURNKEY_NO_ALIAS environment variable
TURNKEY_NO_ALIAS=1 buck2 build //...
Troubleshooting
Common issues and solutions when using Turnkey.
Shell Issues
"attribute 'X' missing" when entering shell
Cause: A toolchain in toolchain.toml isn't in the registry.
Solution: Either remove the toolchain from toolchain.toml or add it to your registry in flake.nix.
Changes to toolchain.toml not taking effect
Cause: Nix flake caching.
Solution:
- Stage changes:
git add toolchain.toml - Re-enter shell:
exit && nix develop
Build Issues
"toolchain not found" error
Cause: The language toolchain wasn't generated.
Solution: Ensure the toolchain is:
- Declared in
toolchain.toml - Has a mapping in
nix/buck2/mappings.nix(for custom toolchains)
"missing BUCK file" or "missing rules.star"
Cause: Buck2 can't find build files.
Solution: Check that:
.buckconfighas[buildfile] name = rules.star- All cells have proper
.buckconfigwith buildfile settings
Stale dependency errors
Cause: Dependency cells out of sync with lock files.
Solution:
tk sync
tk build //...
Dependency Issues
godeps cell missing packages
Cause: go-deps.toml out of date.
Solution:
godeps-gen > go-deps.toml
git add go-deps.toml
# Re-enter shell
Rust feature conflicts
Cause: Conflicting feature requirements across crates.
Solution: Create rust-features.toml with explicit overrides:
[overrides]
problematic-crate = ["feature1", "feature2"]
FUSE Issues
FUSE not available on Linux
Cause: FUSE kernel module not loaded or /dev/fuse missing.
Solution:
# Load FUSE module
sudo modprobe fuse
# Verify
ls /dev/fuse
If persistent, add fuse to /etc/modules-load.d/.
"FUSE-T not installed" on macOS
Cause: FUSE-T package not installed.
Solution:
brew install macos-fuse-t/homebrew-cask/fuse-t
Mount point already in use
Cause: Previous daemon didn't unmount cleanly.
Solution:
# Force unmount
tk compose down --force
# Or manually
fusermount3 -uz /firefly/myproject # Linux
umount -f /firefly/myproject # macOS
"Permission denied" on mount
Cause: User not in fuse group or mount point permissions.
Solution:
# Add user to fuse group (Linux)
sudo usermod -aG fuse $USER
# Log out and back in
# Check mount point permissions
sudo mkdir -p /firefly/myproject
sudo chown $USER:$USER /firefly/myproject
Daemon won't start
Cause: Various issues with daemon lifecycle.
Solution:
# Check for existing processes
pgrep -f turnkey-composed
# Kill stale processes
pkill -9 -f turnkey-composed
# Remove stale socket
rm -f /run/turnkey-composed/*.sock
# Start with debug logging
TURNKEY_FUSE_DEBUG=1 tk compose up
Files appear stale or missing
Cause: Dependency cells updating or policy blocking access.
Solution:
# Check daemon status
tk compose status
# Force refresh
tk compose refresh
# If in "building" state, wait or use lenient policy
TURNKEY_ACCESS_POLICY=lenient tk build //...
"Resource temporarily unavailable" (EAGAIN)
Cause: CI policy returning errors during updates.
Solution:
- Wait for the build to complete
- Switch to development policy for interactive use
- Add retry logic in CI scripts
Build hangs waiting for FUSE
Cause: Strict policy blocking during long Nix builds.
Solution:
# Check what's blocking
tk compose status --verbose
# Use lenient policy for quick iteration
TURNKEY_ACCESS_POLICY=lenient tk build //...
# Or increase timeout
TURNKEY_BLOCK_TIMEOUT=600 tk build //...
Edits not persisting after restart
Cause: Edits stored in overlay, need to generate patches.
Solution:
# Generate patches before stopping
tk compose patch
# Then stop
tk compose down
Container/Docker issues
Cause: FUSE requires privileged access in containers.
Solution:
# Run container with FUSE access
docker run --device /dev/fuse --cap-add SYS_ADMIN ...
# Or disable FUSE and use symlinks
TURNKEY_FUSE_BACKEND=symlink tk build //...
Getting Help
- Check GitHub Issues
- Enable verbose mode:
TURNKEY_VERBOSE=1 nix develop - Check Buck2 logs:
tk log show - FUSE debug logs:
TURNKEY_FUSE_DEBUG=1 tk compose up