Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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:

  1. "Works on my machine" - Different developers have different tool versions
  2. Slow CI/CD - Every change rebuilds everything, even unrelated code
  3. Dependency hell - Conflicting versions across languages and packages
  4. 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

  1. Tools should enhance, not replace - Native commands work normally
  2. Complexity should be opt-in - Start simple, add sophistication as needed
  3. Reproducibility is non-negotiable - Same inputs always produce same outputs
  4. 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:

StageNeeds
PrototypeQuick iteration, minimal ceremony
StartupFast CI, some reproducibility
GrowthCaching, parallelism, reliability
EnterpriseCompliance, 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)   │
└──────────────────┘     └──────────────────┘     └──────────────────┘
  1. Native lock files (go.sum, Cargo.lock, pnpm-lock.yaml) define exact dependency versions
  2. Dependency generators (godeps-gen, rustdeps-gen, etc.) parse lock files and output intermediate TOML
  3. Nix fetches dependencies with verified hashes and creates build-system-compatible cells
  4. 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   │
                        └─────────────────┘     └─────────────────┘
  1. toolchain.toml declares what toolchains you need
  2. The registry maps toolchain names to Nix packages
  3. mappings translate these into build system toolchain targets
  4. The build system uses the toolchain targets for builds

Summary

Turnkey's architecture achieves its goals through careful layering:

LayerResponsibilityTechnology
TopDeveloper UXNative tools, tw/tk wrappers
MiddleOrchestrationTurnkey, dependency generators
BottomExecutionNix (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

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 enabled
  • toolchain.toml - Toolchain declaration (Go enabled by default)
  • .envrc - direnv configuration with symlink sync
  • .gitignore - Ignores Turnkey-managed files
  • rules.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_file declarations 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:

FileDescription
.buckconfigSymlink to Nix-managed Buck2 configuration
.buckrootEmpty file marking project boundary
.turnkey/toolchainsSymlink to generated toolchains cell
.turnkey/godepsSymlink to Go dependencies cell (if configured)
.turnkey/preludeSymlink 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 regeneration
  • use_turnkey --skip-sync - Skip symlink synchronization
  • Environment variables like TURNKEY_SKIP_ALL=1 for 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

If .turnkey/ symlinks aren't created:

  1. Check that you're using direnv or manually sourcing the environment
  2. Verify environment variables are set:
    echo $TURNKEY_BUCK2_CONFIG
    echo $TURNKEY_BUCK2_TOOLCHAINS_CELL
    
  3. Re-allow direnv:
    direnv allow
    

Buck2 Can't Find Cells

If Buck2 reports missing cells:

  1. Check .buckconfig is a valid symlink:
    ls -la .buckconfig
    
  2. Verify cell paths in .buckconfig exist:
    cat .buckconfig
    
  3. 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 tools
  • rust - Rust compiler (rustc)
  • cargo - Cargo package manager
  • clippy - Rust linter
  • rustfmt - Rust formatter
  • rust-analyzer - Rust LSP server
  • python - Python interpreter
  • uv - Python package manager
  • ruff - Python linter and formatter
  • nodejs - Node.js runtime
  • typescript - TypeScript compiler
  • biome - Fast linter/formatter for JS/TS/JSON

Solidity

  • solc - Solidity compiler
  • foundry - Ethereum dev toolkit (forge, cast, anvil)

Other Tools

  • nix - Nix package manager
  • reindeer - Rust Buck2 target generator
  • jsonnet - Jsonnet to JSON compiler
  • mdbook - Documentation tool
  • tk - 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 toolchain
  • toolchains//:rust - Rust toolchain
  • toolchains//: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.toml
  • rustdeps// - Rust dependencies from rust-deps.toml
  • pydeps// - 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:

ToolBehaviorConfiguration Needed
GoIgnores directories starting with . or _None (built-in)
CargoDoesn't auto-discover crates in dot directoriesNone (built-in)
pytestAutomatically ignores dot directoriesNone (built-in)
JestRequires explicit configurationYes
VitestRequires explicit configurationYes

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:

  1. One-time cache invalidation: Buck2 caches are stored per isolation directory. Switching to .turnkey means a clean rebuild on first run.

  2. Update .gitignore: Ensure .turnkey/ is in your .gitignore:

    .turnkey/
    
  3. 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 binaries
  • TURNKEY_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:

  1. Symlinks .turnkey/prelude to the prelude cell
  2. Symlinks .turnkey/toolchains to the generated toolchains
  3. Updates dependency cell symlinks if configured
  4. 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

  1. Go to Settings > Tools > File Watchers

  2. Click + to add a new watcher

  3. 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$
  4. 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:

  1. Go to Settings > Tools > External Tools

  2. Click + to add:

    • Name: Sync rules.star
    • Program: tk
    • Arguments: rules sync
    • Working directory: $ProjectFileDir$
  3. 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:

  1. Git-based (default): Only checks directories with uncommitted source file changes
  2. 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

  1. Ensure deps-extract is in your PATH (built with cargo install --path src/rust/deps-extract)
  2. Check that [rules] enabled = true in .turnkey/sync.toml
  3. Verify the file type is supported (Go, Rust, Python, TypeScript, Solidity)

Sync too slow

  1. Use the default staleness detection (don't use --force in on-save hooks)
  2. Target a specific directory: tk rules sync src/cmd/myapp

Wrong dependencies detected

  1. Check your *-deps.toml files are up to date (run tk sync)
  2. Verify internal prefix configuration in sync.toml
  3. Run tk rules sync --verbose to 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
# 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:

  1. Runs nix eval to list *-cell packages from each repo's flake
  2. Runs nix build to build all cells in a single invocation (~3-4s if cached)
  3. 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).

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:

  1. The toolchain is declared in toolchain.toml
  2. 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:

LanguageDeclaration Files
Gogo.mod, go.sum
RustCargo.toml, Cargo.lock
Pythonpyproject.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.txt for 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

  1. The wrapper captures a hash of dependency files before running the command
  2. The actual tool runs (e.g., go get)
  3. After completion, the wrapper checks if dependency files changed
  4. If changed, tk sync is 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;
};
# 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

FormatFlagReproducibilityNotes
pylock.toml (PEP 751)--lockBestExact versions and URLs
pyproject.toml--pyprojectVariesUses latest matching versions
requirements.txt--requirementsVariesPin 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:

  1. Implementation-dependent: The hash changes based on which packages are actually imported
  2. Opaque: You can't know the hash without running the build and letting it fail
  3. 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:

  1. Check that the deps TOML file is up to date:

    tk sync
    
  2. Verify the cell symlink exists:

    ls -la .turnkey/godeps
    
  3. 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:

  1. Regenerate the deps file with --prefetch:

    godeps-gen --prefetch -o go-deps.toml
    
  2. Re-enter the dev shell:

    exit
    nix develop
    

Stale Dependencies

If dependency changes aren't picked up:

  1. Kill the Buck2 daemon:

    buck2 kill
    
  2. 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 trackuv sync, uv run, IDE language servers, REPL. Members are installed editable so source edits are reflected immediately.
  • Buck2 tracktk build, tk test. External packages are vendored into the pydeps cell built from python-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

Taskuv trackBuck2 track
Run all testsuv run pytesttk test //src/python/...
Run a single member's testsuv run pytest src/python/cargotk test //src/python/cargo:test_toml
Run an exampleuv run --package <pkg-name> <script>tk run //src/examples/python-hello-deps:python-hello-deps
REPL with members availableuv run pythonn/a
IDE language serverPoint at .venv/bin/pythonn/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

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

  1. 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
    
  2. 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"
    
  3. 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";
      };
    };
    
  4. 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:

  1. Set the dependency's import_path to github.com/original/pkg (for correct imports)
  2. Set the fetch_path to github.com/myfork/pkg (where to actually fetch from)
  3. 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:

  1. Fetches the source from fetch_path (the fork)
  2. Stores it in the vendor directory under import_path (the original path)
  3. 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 replaceResult
=> github.com/fork v1.2.3Uses 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.toml and contributing to a shared turnkey.* 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_url attribute)
  • 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

AttributeDescription
srcsJsonnet source files (first file is entry point)
depsDependencies on other jsonnet_library targets
outOutput filename (defaults to <src>.json)
ext_strsExternal string variables (--ext-str key=value)
ext_codesExternal code variables (--ext-code key=value)
tla_strsTop-level argument strings (--tla-str key=value)
tla_codesTop-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 targets
  • run - Run a target
  • test - Run tests
  • query - Query the build graph
  • cquery - Configured query
  • uquery - Unconfigured query
  • targets - List targets
  • audit - Audit the build
  • bxl - Run BXL scripts

Pass-through commands (no sync):

  • clean - Clean build artifacts
  • kill - Kill Buck2 daemon
  • killall - Kill all Buck2 processes
  • status - Show daemon status
  • log - View build logs
  • rage - Generate debug report
  • help - Show help
  • docs - Open documentation
  • init - 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-date
  • 1 - Files are stale (run tk sync to 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:

FlagDescription
--all, -aProcess all files (skip staleness detection)
--force, -fSame as --all
--verbose, -vShow detailed output including skipped files
--quiet, -qSuppress output
--dry-run, -nShow 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:

FlagDescription
--no-syncSkip sync, run Buck2 directly
--no-localSkip local target overrides from .turnkey/local.toml
--verbose, -vShow what tk is doing
--dry-run, -nShow what would be synced without doing it
--quiet, -qSuppress non-error output
--help, -hShow 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:

  • goDepsFile generates a Go deps rule
  • rustDepsFile generates a Rust deps rule
  • pythonDepsFile generates 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 rule
  • sources - Files that trigger regeneration when modified
  • target - The generated file
  • generator - 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

ToolMutating CommandsWatch FilesDeps Target
goget, modgo.mod, go.sumgo-deps.toml
cargoadd, remove, updateCargo.toml, Cargo.lockrust-deps.toml
uvadd, remove, lock, syncpyproject.toml, uv.lockpython-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

OptionDescription
--go-mod PATHPath to go.mod file (default: go.mod)
--go-sum PATHPath to go.sum file (default: go.sum)
--prefetchFetch Nix hashes using nix-prefetch-github
--indirectInclude indirect dependencies (default: true)
-o, --output PATHOutput 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

OptionDescription
--cargo-lock PATHPath to Cargo.lock file (default: Cargo.lock)
--no-prefetchSkip prefetching (produces incorrect hashes)
-o, --output PATHOutput 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

OptionDescription
--lock PATHPath to pylock.toml (PEP 751 lock file) - RECOMMENDED
--pyproject PATHPath to pyproject.toml
--requirements PATHPath to requirements.txt
-o, --output PATHOutput file (default: stdout)
--no-prefetchSkip prefetching (produces placeholder hashes)
--include-devInclude dev dependencies

Input Formats

FormatFlagReproducibilityNotes
pylock.toml (PEP 751)--lockBestExact versions and URLs
pyproject.toml--pyprojectVariesUses latest matching versions
requirements.txt--requirementsVariesPin versions with ==
# 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 --prefetch with 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:

  1. Stage changes: git add toolchain.toml
  2. Re-enter shell: exit && nix develop

Build Issues

"toolchain not found" error

Cause: The language toolchain wasn't generated.

Solution: Ensure the toolchain is:

  1. Declared in toolchain.toml
  2. 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:

  1. .buckconfig has [buildfile] name = rules.star
  2. All cells have proper .buckconfig with 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