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

Dependency Fixups

Some dependencies don't build under Buck2 as they are. A Rust crate's build.rs never runs, so whatever it generates, compiles or tells rustc has to come from somewhere else; a dependency may need a patch. A fixup is what turnkey supplies for one dependency to make it build: a patch, the output its build script would generate, the flags it would pass.

Fixups come in fixup sets: modules of class turnkeyFixups (ADR 0003). Your repository brings the sets it needs, and its own fixups, through one option. turnkey applies no fixups you didn't bring.

Bringing fixups

perSystem = { ... }: {
  turnkey.toolchains.buck2.fixups = {
    # Sets published by other flakes
    imports = [
      inputs.turnkey.modules.turnkeyFixups.serde
      inputs.acme-fixups.modules.turnkeyFixups.default
    ];

    # This repository's own fixups
    rust.zerocopy.buildScript.skip = true;
  };
};

A fixup is keyed by the dependency's name in its own ecosystem: rust.<crate>, go."<import path>", python.<distribution>, javascript.<package>, solidity.<package>.

turnkey's published fixups

turnkey publishes the fixups its own repository needs, one module per family, under inputs.turnkey.modules.turnkeyFixups:

ModuleCrates
serdeserde, serde_core, serde_json
thiserrorthiserror
ringring 0.17
rustixrustix
nixnix
fuserfuser
tree-sittertree-sitter and its rust, python, solidity, starlark and typescript grammars
build-script-skipscrates whose build script turnkey's builds need nothing from
defaultall of the above

Import only what your lock needs: an imported fixup for a crate you don't lock is silently unused.

Every build script needs a fixup

Buck2 never runs build.rs, so every crate you lock that has one needs a fixup saying what stands in for it. The Rust cell fails to build otherwise:

error: turnkey: serde 1.0.228 has a build.rs, and no fixup says what stands in for it; a published fixup set does: add `inputs.turnkey.modules.turnkeyFixups.serde` to turnkey.toolchains.buck2.fixups.imports

When no published set accounts for the crate, the error says how to write the fixup. If the crate's build script only probes the compiler or the target, or emits cfgs for features you don't use, the build needs nothing from it:

rust.zerocopy.buildScript.skip = true;

Otherwise, give it a build script generating what its build.rs would, and the flags it would pass. The developer manual's Dependency Generators page describes the whole record and how to diagnose what a crate needs.

Versions

A fixup applies to every locked version of its dependency. Fields that hold only for some versions go in versions entries, whose bounds are compared with the locked version:

rust.ring.versions = [
  {
    when = { atLeast = "0.17"; below = "0.18"; };
    buildScript.generate = ...;
  }
];

Every entry whose bounds hold applies. A version no entry covers gets only the fixup's other fields, so a new major version of a crate is reported as unaccounted for rather than built with a fixup written for another.

Patches

rust.some-crate.patches = [ ./patches/some-crate-fix.patch ];
go."github.com/foo/bar".patches = [ ./patches/bar.patch ];

A fixup's patches apply to the dependency's own source, in order, with -p1: a plain git diff in a checkout of the dependency works as is. Patches from several sets apply in import order, before a Rust crate's build script runs.

Fixup patches are separate from the patches tk compose patch writes to .turnkey/patches/<cell>/ from the FUSE edit layer. Those are this repository's local, exact-version workarounds.

  • Rust cell: each patch goes in its package's directory, .turnkey/patches/rustdeps/vendor/<crate>@<version>/, and applies in that crate's own derivation, after its fixup. Changing a patch rebuilds only that crate and what depends on it.
    • A directory may also be named after the crate alone (vendor/anyhow/); it then goes to the version the cell's unversioned alias points at.
    • A patch that doesn't apply exactly, with no fuzz, fails the build and names the crate.
    • A patch file left directly under rustdeps/, from before this layout, fails evaluation: move it into its package's directory, or regenerate it with tk compose patch.
  • Go cell: each patch goes in its module's directory, .turnkey/patches/godeps/vendor/<module path>/, and applies in that module's own derivation, after its fixup.
  • Python cell: each patch goes in its distribution's directory, .turnkey/patches/pydeps/vendor/<name>/, and applies in that distribution's own derivation, after its fixup and before its rules.star is written. Changing a patch rebuilds only that distribution and what depends on it.
    • Patches apply to the distribution's unpacked wheel, the installed layout, not its sdist. A patch written against sdist paths (such as vendor/requests/src/requests/...) doesn't apply: regenerate it with tk compose patch.
    • A patch that doesn't apply exactly, with no fuzz, fails the build and names the distribution.
    • A patch file left directly under pydeps/, from before this layout, fails evaluation: move it into its distribution's directory, or regenerate it with tk compose patch. So does a directory naming a distribution python-deps.toml doesn't hold.
  • Solidity cell: each patch goes in its package's directory, .turnkey/patches/soldeps/vendor/<name>/ (for a scoped npm package, vendor/@<scope>/<name>/, such as vendor/@openzeppelin/contracts/), and applies in that package's own derivation, after its fixup. Changing a patch rebuilds only that package.
    • A patch that doesn't apply exactly, with no fuzz, fails the build and names the package.
    • A patch file left directly under soldeps/, from before this layout, or in a directory that is no package's, fails evaluation: move it into its package's directory, or regenerate it with tk compose patch.
  • JavaScript cell: each patch goes in its package's directory, .turnkey/patches/jsdeps/vendor/<name>@<version>/ (for a scoped package, vendor/@<scope>/<name>@<version>/), and applies in that package's own derivation, after its fixup, so to every instance of it. Changing a patch rebuilds only that package.
    • A directory may also be named after a direct dependency alone (vendor/lodash/); it then goes to the version the root package.json resolves it to.
    • A patch that doesn't apply exactly, with no fuzz, fails the build and names the package.
    • A patch file left directly under jsdeps/, from before this layout, or in a directory that is no package's, fails evaluation: move it into its package's directory, or regenerate it with tk compose patch.

When sets disagree

Fixups merge field by field, as NixOS modules do: flags and patches from every set concatenate. Two sets that give a crate different build scripts, or an environment variable different values, fail evaluation, naming both files:

error: The option `rust.serde.buildScript.generate.<function body>' has conflicting definition values:
- In `conflicting/flake.nix#modules.turnkeyFixups.default': "echo another serde"
- In `acme/flake.nix#modules.turnkeyFixups.default': "..."

Resolve it in your own fixups:

  • Override one field with lib.mkForce: rust.serde.buildScript.generate = lib.mkForce "...";
  • Drop one fixup an imported set brings: rust.ring.enable = false;
  • Drop a whole module: disabledModules = [ ... ];, or don't import it.

Unused fixups

A fixup written in your own turnkey.toolchains.buck2.fixups that matches no locked dependency, or a versions entry matching none of its locked versions, warns at evaluation: it usually means a rename or an upgrade left it behind. Fixups from imported sets never warn, so one organization-wide set can serve many repositories that each lock only part of it.

Publishing a fixup set

A fixup set is a plain module; publish it from any flake as modules.turnkeyFixups.<name>. With flake-parts, import its modules module, which stamps the module's class:

{
  imports = [ inputs.flake-parts.flakeModules.modules ];

  flake.modules.turnkeyFixups = {
    openssl = ./fixups/openssl.nix;
    default = { imports = [ ./fixups/openssl.nix ]; };
  };
}

Without flake-parts, set the class yourself:

outputs = { self, ... }: {
  modules.turnkeyFixups.default = {
    _class = "turnkeyFixups";
    imports = [ ./fixups/openssl.nix ];
  };
};

A set's module receives pkgs and lib, and may hold fixups for several languages at once, such as for a library packaged for both Rust and Python:

# fixups/acme-proto.nix
{ ... }:
{
  rust.acme-proto = {
    buildScript.skip = true;
    patches = [ ./acme-proto-rust.patch ];
  };
  python.acme-proto.patches = [ ./acme-proto-python.patch ];
}