Decoupling `build-support` into a Deferred Higher-Order Function Library (`PkgsLib`)

prologue: Hi, I’m new to the discussion forums, not sure this is the right board/space for this, sorry if I’ve committed “cardinal-sin”

Pre-RFC: Decoupling build-support into a Deferred Higher-Order Function Library (PkgsLib)

Abstract

This RFC proposes a structural and architectural shift in how Nixpkgs manages its build orchestration tools.

Currently, build-support utilities (e.g., buildGoModule, buildRustPackage, fetchFromGitHub) live within the pkgs attribute set.

In a sense this means we’re treating package blueprints as if they’re packages themselves…

This doesn’t make any sense at all?!; and seems literally like an architectural relic, that came into existence because everyone agreed -

They require packages so… they don’t belong in lib?…

But “not belonging in lib” doesn’t mean they automatically belong in pkgs either. Realistically - build-support functions… function as if they are simply a “second lib”, which “happens to require” a pkgs argument.

So, maybe they should be treated like exactly that.

I propose refactoring build-support into a deferred, higher-order function library. eg: PkgsLib… or similar — and moving it outside the core package instantiation scope.

By passing pkgs as a lazy input argument through a multi-stage evaluation pipeline, we get strict conceptual alignment, eliminate core infinite recursion risks, and optimize evaluation latency.

Feel free to add to the case for a change like this + inform me of anything incorrect/stupid, and how it could be made less stupid.

Also, feel free to check out/add to this/these repo(s).

A centralized repo for “developing” the idea, where we can draw out the issues, ideas, etc. from any discussions; and you could help make this into an Actual RFC.

The repo where I’m working on extensions to a few build-support functions, which might be pulled into/become a future proof of concept repo, when finished


1. Motivation & Conceptual Misalignment

In the current Nixpkgs architecture, the boundary between lib and pkgs is defined - operationalized by system dependence:

  • lib contains pure, system-agnostic utility functions (logic).
  • pkgs contains platform-dependent derivations (software).

Because build-support functions require compilers, linkers, and fetchers, they are deeply system-dependent and are forced to reside inside pkgs.

However, this introduces a deep conceptual flaw: builders are not packages; they are higher-order functions that output package recipes.

Treating builders as standard attributes inside a mutually recursive package set creates two core problems:

The Bootstrap Catch-22:

Fetchers and basic builders frequently cause infinite recursion loops if a downstream package override accidentally alters an upstream dependency used by the builder itself.

Monolithic Evaluation Overhead:

Because builders are bound directly inside the massive pkgs fixpoint loop, decoupling structural build logic from concrete software compilation is incredibly difficult, impeding tree-wide optimization and modularity.

Silent Failures in Standard Nixpkgs:

Currently in Nixpkgs, functions like .overrideAttrs take an arbitrary, unchecked attribute set. If you make a typo in a dependency or a build flag name, Nix will not throw an error. It will silently ignore the typo, drops your configuration, and builds the wrong binary.

This fixes that… because the builder functions would be moved into a “deferred library”, they become what they have always been… **functions**.

Meaning all packages become modules

This pattern completely replaces fragile functional overrides (.override), with a type-checked, mergeable, and modular schema.

  • You Gain type checking/type safety
  • The Module System catches the Error
  • You enforce a strict schema.
  • If a user inputs an unrecognized variable name, when defining a package the evaluation immediately (and correctly) crashes with a clear traceback.

2. Proposed Architecture

I propose introducing an explicit/linear three-stage evaluation pipeline that separates pure tool bootstrapping, deferred build logic, and final package materialization.

Stage 0: The Immutable Bootstrap Layer

A minimal, raw set of primitive derivations (e.g., standard stdenv, bash, curl) is evaluated immediately. This layer is entirely locked down and cannot accept downstream overrides.

Stage 1: The PkgsLib Scope

build-support is converted into a library of partially applied functions or deferred modules.

  • Primitive actions (like fetchgit) pull their execution tools strictly from Stage 0.
  • High-level abstractions (like buildGoModule) accept the final package set (finalPkgs) as a lazy, late-bound argument to reference target compilers.

Stage 2: Final Resolution Loop (pkgs)

Using an open fixpoint (lib.fix), the final user-facing package set is materialized by feeding the instantiated pkgs back into PkgsLib.


Architectural Deep-Dive: Mechanics and Benefits

1. Passing pkgs as a Lazy Input Argument

Mechanism:

Instead of nesting builders directly inside the global package scope, PkgsLib is structured as a deferred function: makePkgsLib = finalPkgs: { … }.

Effect:

This introduces a “late binding” phase.

The blueprint logic for a builder sits entirely dormant in memory.

It does not look for compilers, libraries, or dependencies until a fully instantiated pkgs object is explicitly passed to it at the final step of evaluation.

2. Philosophical/Conceptual Alignment

The Mechanism:

build-support is removed from the pkgs attribute set and structurally relocated into its own functional layer.

The Impact:

This fixes a fundamental category error.

In current Nixpkgs…

pkgs.buildGoModule
(a function requiring pkgs) is structurally handled the exact same way as pkgs.hello

(a discrete derivation).

Relocating it, and other builders… to a dedicated library ensures the code structure reflects reality:

Builders are an engineering library of functions, not compiled software artifacts.

3. Eliminating Core Infinite Recursion Risks

  • The Mechanism: The multi-stage pipeline establishes a strict, one-way dependency boundary between the bootstrap layer (Stage 0) and the finalized packages (Stage 2).
  • The Impact: This completely isolates a major vector for evaluation failures.
    • The Status Quo: If a user writes an override to patch curl globally, fetchgit immediately tries to use that patched curl. If building that patched curl requires fetching a source code repository via fetchgit, evaluation crashes with an infinite recursion error.
    • The New Pipeline: The fetchgit builder inside Stage 1 is strictly pinned to bootstrapPkgs.curl from Stage 0. Downstream package overrides in Stage 2 cannot cross the boundary to reach backward into Stage 0. fetchgit safely fetches the source using the immutable bootstrap tools, breaking the recursive loop entirely.

4. Optimizing Evaluation Latency

  • The Mechanism: This leverages Nix’s native lazy evaluation model by separating structural abstractions from concrete values.
  • The Impact: In standard Nixpkgs, because builders are tightly coupled to the global recursive fixpoint, the evaluator must frequently wade through complex package dependency trees just to resolve a builder’s environment. Moving builders to a pure function layer creates a radical shortcut: the evaluator processes the builder logic instantaneously, deferring heavy package compilation logic until the exact moment a specific attribute path is built.

3. Conceptual Implementation Example

Straight-up vibe-coded/probably trash - aside from illustrating the concept; to get the idea out faster

let
  lib = import <nixpkgs/lib>;

  # STAGE 0: Isolated Bootstrap Tools
  ## Evaluates immediately. These are pinned and completely immune to downstream overrides,
  ## breaking the primary vector for infinite recursion in standard fetchers.
  ## evaluating only up to the point that we have a barebones coreutils+minimalCurl+stdenv (bound to system), basically running the stage0/hex0/mesC up to coreutils build process

  bootstrapPkgs = {
    stdenv = { /* primitive stdenv */ };
    curl = { /* primitive curl */ };
    coreutils = { /* primitive coreutils */ };
  };

  # STAGE 1: PkgsLib (Deferred Higher-Order Builders Library)
  # A decoupled logic layer. It takes finalPkgs as a late-bound argument.
  makePkgsLib = finalPkgs: {
    # Fetchers pin strictly to Stage 0 tools to ensure absolute purity and stability
    fetchgit = { url, sha256 }: bootstrapPkgs.stdenv.mkDerivation {
      name = "source";
      buildInputs = [ bootstrapPkgs.curl ];
      # ... fetch logic
    };

    # Ecosystem builders defer compiler selection to finalPkgs, allowing for dynamic overrides
    buildGoModule = { src, ... }@args: bootstrapPkgs.stdenv.mkDerivation (args // {
      buildInputs = [ finalPkgs.go ] ++ (args.buildInputs or []);
    });
  };

  # STAGE 2: Reconstructed Materialized Packages
  # The final open fixpoint loop where the user-facing package set is constructed.
  pkgs = lib.fix (finalPkgs: 
    let 
      pkgsLib = makePkgsLib finalPkgs;
    in {
      # Compilers live safely inside the final tier
      go = bootstrapPkgs.stdenv.mkDerivation { /* go compiler recipe */ };

      # Packages call upon the deferred library, passing inputs down cleanly
      myGoApp = pkgsLib.buildGoModule {
        pname = "app";
        version = "1.0";
        src = pkgsLib.fetchgit { url = "https://github.com"; sha256 = "..."; };
      };
    }
  );
in
  pkgs.myGoApp

4. Key Advantages

A. Total Elimination of Circular Bootstrapping

By forcing a strict downward data flow

Stage 0 (lib)
→
Stage 1 (+ "PkgsLib" + "bootstrap compilers")
→
Stage 2 (+ stdenv + generic pkg-expressions)

A primitive tool like fetchgit will never query finalPkgs for its environment. This effectively patches an entire class of infinite recursion bugs that plague complex package overrides.

B. Perfect Laziness and Memory Optimization

Because the structural rules of PkgsLib are separated from package instantiation, Nix’s lazy evaluation engine shines. No package derivation or compiler blueprint is evaluated until its specific attribute path is explicitly invoked.

C. Tree-wide Customization without Global Rebuilds

Downstream users can safely swap out compilers or flags inside finalPkgs without accidentally mutating the underlying fetching or building framework mechanics, enabling clean ecosystem testing.


5. Community Proofs of Concept: dream2nix, devenv, flake-parts

Because modifying the main Nixpkgs repository requires navigating massive legacy compatibility constraints, developers have built greenfield projects to prove that your proposed layout works flawlessly at scale.

dream2nix:

This project completely reimagines packaging by turning builders (buildGoModule, buildRustPackage) into modular, discrete plug-ins.
It handles package constraints almost exactly like this PkgsLib pipeline: it passes an instantiated pkgs attribute set downstream into structural module blueprints.

devenv and flake-parts

These systems heavily rely on specialArgs and deferred module functions to safely pass things like the system tuple (x86_64-linux) or pkgs downstream only when needed.

6. Drawbacks and Implementation Challenges

  • Refactoring Effort: Transitioning the entire Nixpkgs tree away from pkgs.buildGoModule to a decoupled structure represents a colossal breaking change.
  • Compatibility Layer Required: A backwards-compatible shim would be mandatory, mapping pkgs.buildGoModule back to pkgsLib.buildGoModule so that existing user expressions do not break.
  • Evaluation Performance Tuning: While lazy evaluation is protected, managing large-scale fixpoint injection across 80,000+ packages would have to be benchmarked to ensure the Nix evaluator does not suffer from excessive memory overhead.
1 Like

Welcome!

RFCs are very welcome. But, before anyone reviews your RFC, they’d probably want your guarantee that you understand it. If you don’t guarantee that you have a good understanding of your RFC, I don’t think anyone will bother reading it.

1 Like

This… really doesn’t sound like a very good idea. The benefits are pretty minor, as I see it, and the costs are significant.

Automation/AI policy

Haven’t read it myself, though :person_shrugging:

Fair enough/agreed, thanks for the point, I discredit myself too much.

As far as “anything AI” goes, I was saying I don’t know the ins and outs of the particular functions used in a snippet, and that the wording isn’t all mine. I asked it to elaborate on a set of bulletpoints for me.

I understand the concept, and I think

A: It’s one of the biggest issues in understanding nix from the outset. As an aside its unintuitive, initially I expected to be able to declare+build packages using lib, without adding functions, “hidden” inside of the packages directory.

B: People are already using this structure widely, (often with callPackage that has pkgs as arg eg: callPackage packages/pname.nix )

It’s similar in concept, but applied to nixpkgs as a whole. Ultimately creating a system/structure where each of the package expressions are practically treated as modules, which have a function of this builders-lib applied to them on import.

What do you mean by the benefits being minor?

I’m not sure you see how much a difference that makes?

Here…

The idea is centered partly on the need to Isolate dependency definitions away from standard functions and letting the data merge naturally.

just one benefit of that is this…

  1. Eliminates Circular Bootstrapping, and requires the bootstrap to only use tools/binaries from the minimal bootstrap.

Since nix requires the stdenv, and to separate itself from the host system, while also depending on that system for initial fetching/bootstrapping, that necessitates a perfect chain with no room for error.

The hex0/mesc to coreutils chain is ONLY a valid trustworthy bootstrap “if” it can confirm itself.

Whereas currently building a nix or lix binary for example… “on my machine”, (not supposed to be a problem here?) pulls in gssapi, and kerberos, and its dependencies, causing my “minimal”, not-overridden/barebones… nix closure to depend on kerberos?..

A bit of “enterprise-grade” network infrastructure tooling for the common nix-user… Should absolutely never be required for a binary bootstrap.
This means that even if the bootstrap itself functions in theory… the structure around it is broken.

I would feel that this alone is reason enough for such a change?
Not meaning to sound bitter if I do, just thought I would get a more elaborate response here?

Not sure if this would be part of this proposal, but spliting pkgslib from the rest of nixpkgs as a separate repo could allow downstream repos to depend on a smaller-sized input rather than on the entirety of nixpkgs. :thinking:

True, that partly is where the idea comes from, build-support is already a set of functions, if they didn’t depend on packages being defined… they would belong in lib.

So as far as I can tell, its the only reason that they aren’t in lib?

Practically… this is a request to put them where they belong… in a lib that depends on packages.

With the added benefits that it aligns with the push to modularize nixpkgs, adds type-checking, fixes overrides, and centralizes build logic.