-
-
Notifications
You must be signed in to change notification settings - Fork 159
[RFC 0194] Flake Entrypoint #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,389 @@ | ||
| --- | ||
| feature: flake-entrypoint | ||
| start-date: 2025-12-08 | ||
| author: Robert Hensing (@roberth) | ||
| co-authors: (to be determined) | ||
| shepherd-team: (names, to be nominated and accepted by RFC steering committee) | ||
| shepherd-leader: (name to be appointed by RFC steering committee) | ||
| related-issues: (will contain links to implementation PRs) | ||
| --- | ||
|
|
||
| # Summary | ||
| [summary]: #summary | ||
|
|
||
| Reduce boilerplate in flakes by changing how Nix acquires flake outputs, | ||
| allowing frameworks to handle common patterns. | ||
| Combined with [RFC 193](https://github.com/NixOS/rfcs/pull/193), this allows all | ||
| Nix code to be removed from some flakes. | ||
|
|
||
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Common flake patterns require repetitive Nix code that could be handled by frameworks if inputs were separated from implementation. | ||
|
|
||
| Systems like flake-parts, flake-utils-plus and blueprint already provide abstractions over flake outputs. | ||
| This proposal would enable these frameworks to be invoked declaratively, | ||
| in a standard way, | ||
| without boilerplate. | ||
|
|
||
| # Detailed design | ||
| [design]: #detailed-design | ||
|
|
||
| ## Entrypoint selection | ||
|
|
||
| Flake output invocation is changed to go through an "entrypoint", | ||
| which is the flake that provides a function to help produce the flake output attributes. | ||
|
|
||
| If `inputs.entrypoint` exists, Nix uses it as the entrypoint. | ||
| Otherwise, Nix uses the built-in entrypoint. | ||
|
|
||
| The built-in entrypoint reads `flake.nix` and expects an `outputs` attribute, | ||
| maintaining backward compatibility with existing flakes. | ||
|
|
||
| ## Entrypoint function signature | ||
|
|
||
| An entrypoint exposes `outputs.lib.callFlakeEntrypoint`, | ||
| which Nix invokes to produce the flake's outputs. | ||
|
|
||
| The function receives: | ||
|
|
||
| ```nix | ||
| { | ||
| # The resolved and invoked/dependency-injected flake inputs | ||
| inputs, | ||
| # The parsed flake metadata (currently from flake.nix) | ||
| flakeMeta, | ||
| # The invoked/dependency-injected *result* | ||
| self, | ||
| # The base directory of the flake | ||
| flakeDir, | ||
| # The sourceInfo, where `sourceInfo.outPath` may be `flakeDir` or any | ||
| # parent of it (if applicable; subdirectory flakes) | ||
| sourceInfo, | ||
| # ... is mandatory for forward compatibility | ||
| ... | ||
| }: | ||
| # Returns standard flake outputs | ||
| { | ||
| packages = { ... }; | ||
| apps = { ... }; | ||
| # etc. | ||
| } | ||
| ``` | ||
|
Comment on lines
+48
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be possible to make this the format for This would only require a small addition to the default entrypoint; after reading
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean for the use case of defining the implementation of an entrypoint to be used elsewhere? Alternatively, if you mean for consuming an entrypoint, you would typically have something more framework-specific or domain-specific, and then So I think what you're proposing is to make this a second format that the default built-in entrypoint understands. |
||
|
|
||
| The `...` parameter is mandatory for forward compatibility. | ||
|
|
||
| ## flakeMeta schema | ||
|
|
||
| The `flakeMeta` parameter contains the flake's metadata, | ||
| which is the attribute set from `flake.nix` minus the `outputs` function. | ||
|
|
||
| Currently this includes: | ||
|
|
||
| ```nix | ||
| { | ||
| description = "..."; # optional | ||
| nixConfig = { ... }; # optional | ||
| inputs = { ... }; # the input specifications | ||
| } | ||
| ``` | ||
|
|
||
| When [RFC 193](https://github.com/NixOS/rfcs/pull/193) is implemented, | ||
| this data would come from `flake.toml` instead of `flake.nix`. | ||
|
|
||
| The entrypoint can use this metadata to inform how it processes the flake, | ||
| such as reading the `description` or using `inputs` specifications | ||
| to understand the flake's structure. | ||
|
|
||
| ## Configuration discovery | ||
|
|
||
| How entrypoints discover and read their configuration files is framework-specific. | ||
| Each entrypoint framework defines its own conventions for configuration. | ||
|
|
||
| For example: | ||
| - flake-parts might read `flakeModules` from `flake.nix` or `parts.nix` | ||
| - `numtide/blueprint` could infer outputs by analyzing the source tree structure | ||
| - A hypothetical framework could read `project.toml` | ||
|
|
||
| Entrypoints use the provided `flakeDir` parameter to locate files relative to the flake root. | ||
|
|
||
| This RFC does not standardize configuration file naming or location, | ||
| allowing frameworks flexibility in their design. | ||
|
|
||
| ## Built-in entrypoint specification | ||
|
|
||
| The built-in entrypoint maintains backward compatibility with existing flakes. | ||
| When no `inputs.entrypoint` is specified, | ||
| Nix uses its current flake evaluation behavior. | ||
|
|
||
| Conceptually, this is approximately equivalent to an entrypoint that: | ||
| - Reads `flake.nix` from the flake directory | ||
| - Evaluates its `outputs` attribute as a function | ||
| - Passes the traditional arguments (`self` and the resolved `inputs`) | ||
| - Returns the resulting attribute set as the flake's outputs | ||
|
|
||
| The actual implementation details of how Nix performs outputs invocation are | ||
| somewhat intricate and some aspects would need to be worked out during prototype development. | ||
| The code for this is not provided in this RFC, | ||
| but it is derivable by refactoring Nix's `call-flake.nix` in small behavior-preserving steps. | ||
|
|
||
| ## Compatibility | ||
|
|
||
| The following negative space is changed and may require a few projects to adapt if they already use these: | ||
| - `inputs.entrypoint` is assigned meaning, changing how flake outputs are formed. | ||
|
|
||
| Flakes that do not have `inputs.entrypoint` remain compatible. | ||
|
|
||
| # Examples and Interactions | ||
| [examples-and-interactions]: #examples-and-interactions | ||
|
|
||
| ## Current flake.nix | ||
|
|
||
| ```nix | ||
| { | ||
| inputs = { | ||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||
| }; | ||
|
|
||
| outputs = { self, nixpkgs }: | ||
| let | ||
| system = "x86_64-linux"; | ||
| pkgs = nixpkgs.legacyPackages.${system}; | ||
| in { | ||
| packages.${system}.default = pkgs.hello; | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ## Using framework entrypoint | ||
|
|
||
| **flake.nix**: | ||
| ```nix | ||
| { | ||
| inputs = { | ||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||
| entrypoint.url = "github:hercules-ci/flake-parts"; | ||
| entrypoint.inputs.nixpkgs-lib.follows = "nixpkgs"; | ||
| }; | ||
|
|
||
| # No outputs function needed - the entrypoint handles it | ||
| } | ||
| ``` | ||
|
|
||
| **parts.nix** (read by flake-parts entrypoint): | ||
| ```nix | ||
| { ... }: { | ||
| perSystem = { pkgs }: { | ||
| packages.default = pkgs.hello; | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| **Evaluation flow**: | ||
| 1. Nix sees `inputs.entrypoint` is defined | ||
| 2. Nix evaluates the flake-parts flake and calls its `outputs.lib.callFlakeEntrypoint` | ||
| 3. flake-parts receives `flakeDir`, `inputs`, etc. as parameters | ||
| 4. flake-parts reads `parts.nix` from `flakeDir` (by its own convention) | ||
| 5. flake-parts processes the module and returns standard flake outputs | ||
| 6. Those outputs become this flake's outputs | ||
|
|
||
| ## Implementing an entrypoint | ||
|
|
||
| `flake.nix` serves as the base that bootstraps entrypoints. | ||
| This example shows what authoring a framework looks like: | ||
|
|
||
| ```nix | ||
| { | ||
| outputs = { self, nixpkgs }: | ||
| let | ||
| system = "x86_64-linux"; | ||
| pkgs = nixpkgs.legacyPackages.${system}; | ||
| in { | ||
| packages.${system}.default = pkgs.hello; | ||
|
|
||
| # Entrypoints are accessed here | ||
| lib.callFlakeEntrypoint = | ||
| { | ||
| # The resolved and invoked/dependency-injected flake inputs | ||
| inputs, | ||
| # The parsed flake metadata | ||
| flakeMeta, | ||
| # The invoked/dependency-injected *result* | ||
| self, | ||
| # The base directory of flake | ||
| flakeDir, | ||
| # The sourceInfo, where `sourceInfo.outPath` may be `flakeDir` or any | ||
| # parent of it (if applicable; subdirectory flakes) | ||
| sourceInfo, | ||
| # ... is mandatory for forward compatibility | ||
| ... | ||
| }: | ||
| # Imagine some useful expression, returning the usual flake outputs | ||
| { | ||
| packages = { ... }; | ||
| apps = { ... }; | ||
| }; | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| The built-in entrypoint reads `flake.nix` and expects an `outputs` attribute. | ||
| Alternative entrypoints (specified via `inputs.entrypoint`) can implement different conventions. | ||
|
|
||
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| ## Breaking change | ||
|
|
||
| Projects currently using `inputs.entrypoint` for other purposes would need to rename that input. | ||
|
|
||
| This seems unlikely for any given project. | ||
|
|
||
| ## Adds conceptual complexity | ||
|
|
||
| Users need to understand another mechanism beyond the basic `outputs` function, | ||
| increasing the learning curve for newcomers to Nix flakes. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would not be the case if the default shape of a flake was just to be a nix function. In fact, most people wouldn't even need to know about In fact, the learning curve might even be flattened; a |
||
|
|
||
| ## Implicit behavior | ||
|
|
||
| The mechanism makes flake evaluation less explicit. | ||
| Looking at a `flake.nix` with only `inputs.entrypoint` doesn't immediately show | ||
| what outputs will be produced without understanding the entrypoint framework. | ||
|
|
||
| However, many hand-written flakes don't make this obvious from reading the code either. | ||
|
|
||
| ## Mitigatable: Ecosystem fragmentation risk | ||
|
|
||
| Multiple competing entrypoint frameworks could emerge, | ||
| each with different conventions for configuration and behavior. | ||
| This could make it harder for users to understand and switch between different projects. | ||
|
|
||
| However, this is already the case. | ||
| Entrypoint frameworks can simultaneously implement specific opinions about project conventions, | ||
| while agreeing on a common interface for Nix-level extension / composition. | ||
|
|
||
| ## Non-drawback: Performance overhead | ||
|
|
||
| Using an entrypoint adds another flake evaluation to the dependency graph. | ||
| Every flake using an entrypoint must evaluate that entrypoint flake first, | ||
| which increases evaluation time compared to direct `outputs` functions. | ||
|
|
||
| The overhead of the default entrypoint is completely negligible and only paid once per flake. | ||
|
|
||
| ## Non-drawback: Framework migration cost | ||
|
|
||
| Existing frameworks (flake-parts, flake-utils-plus, blueprint) would need to: | ||
| - Expose a `lib.callFlakeEntrypoint` function | ||
| - Maintain compatibility with their current invocation patterns | ||
| - Document and support both usage patterns during transition | ||
|
|
||
| However, this is a small effort and they can implement this at their own pace. | ||
|
|
||
| ## Non-drawback: Debugging difficulty | ||
|
|
||
| When an entrypoint fails or produces unexpected outputs, | ||
| users must understand both the entrypoint framework's behavior and Nix's invocation mechanism. | ||
| Error messages may be less clear than with direct `outputs` functions. | ||
|
|
||
| Entrypoints do not make this worse than using a framework in `outputs`. | ||
|
|
||
| # Alternatives | ||
| [alternatives]: #alternatives | ||
|
|
||
| ## Keep current structure | ||
|
|
||
| Maintain the status quo where frameworks must be manually invoked in each flake's `outputs` function. | ||
| This avoids all the drawbacks but continues to require boilerplate in every flake. | ||
|
|
||
| ## Built-in support for common patterns | ||
|
|
||
| Instead of an entrypoint mechanism, | ||
| Nix could provide built-in support for common flake patterns | ||
| (like per-system iteration or module systems). | ||
| This would reduce boilerplate without the indirection layer. | ||
|
|
||
| However, this approach would: | ||
| - Require extensive design work to identify and standardize patterns | ||
| - Make Nix itself more complex and opinionated | ||
| - Reduce flexibility for framework authors to innovate | ||
| - Still require users to write some Nix code | ||
| - Ossify its whole domain due to the combination of Hyrum's law and the desire for reproducibility | ||
|
|
||
| ## Extend outputs to accept framework function | ||
|
|
||
| Allow `outputs` to be set to a framework function directly, | ||
| such as `outputs = flake-parts.lib.mkFlake;`. | ||
| The framework function would receive similar parameters to `callFlakeEntrypoint`. | ||
| However, this would require the input (such as `flake-parts` in this example) to be brought into scope, | ||
| requiring that `outputs` is a function, and when `outputs` is a function, | ||
| it should probably just be compatible with the status quo semantics for it. | ||
| So that puts us back at square one. | ||
|
|
||
| This would be less convenient but wouldn't require the `inputs.entrypoint` special case. | ||
| However, it still requires writing the function invocation in every flake | ||
| and doesn't enable the zero-Nix-code vision when combined with RFC 193. | ||
|
|
||
| ## Template-based code generation | ||
|
|
||
| Use code generation tools (like `nix flake init`) to generate the boilerplate | ||
| instead of removing it via abstraction. | ||
|
|
||
| This approach keeps flakes explicit but: | ||
| - Generated code still needs to be maintained and understood | ||
| - Doesn't reduce the amount of code in repositories | ||
| - Makes it harder to adopt framework improvements, as it requires regenerating, | ||
| and redoing any changes, flipping the supposed advantage of having the code | ||
| right under your fingers on its head. | ||
|
|
||
| ## Different entrypoint naming | ||
|
|
||
| Instead of `inputs.entrypoint`, use a different name or location | ||
| (e.g., `outputs.entrypoint`, `framework`, or a top-level `entrypoint` attribute). | ||
|
|
||
| However, `inputs.entrypoint` aligns well with the inputs concept | ||
| since the entrypoint is itself a dependency. | ||
| The chosen approach keeps the interface and implementation simple. | ||
|
|
||
| # Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| ## RFC 193 - TOML Flakes | ||
|
|
||
| [RFC 193](https://github.com/NixOS/rfcs/pull/193) proposes using `flake.toml` for declarative input specifications. | ||
| Combined with the entrypoint mechanism proposed here, | ||
| flakes could be derived without *any* custom Nix wiring code, | ||
| instead inferring outputs exclusively from the source tree and designated `inputs`. | ||
| Expressions could be provided to adjust behavior on an as-needed basis only. | ||
|
|
||
| The entrypoint mechanism can be implemented independently of TOML, | ||
| but the two features work well together. | ||
|
|
||
| ## flake-parts, flake-utils-plus and blueprint | ||
|
|
||
| Framework systems like flake-parts, flake-utils-plus and blueprint already provide abstractions over flake outputs. | ||
| This proposal would enable these frameworks to be specified declaratively, | ||
| in a standard way, | ||
| without boilerplate. | ||
|
|
||
|
|
||
| # Unresolved questions | ||
| [unresolved]: #unresolved-questions | ||
|
|
||
| - What outcomes result from a prototype of this feature? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An implementation in nix gated by an |
||
| - How should errors be reported when an entrypoint fails? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A simple message |
||
| - Should there be a standard way for entrypoints to declare what files they read? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should be part of the first iteration. There might be some UX benefits to this, but I think initially, entrypoints should handle errors and error messages themselves. |
||
|
|
||
| # Future work | ||
| [future]: #future-work | ||
|
|
||
| - Standardize common entrypoint patterns | ||
| - Define metadata fields that entrypoints can use (beyond just inputs) | ||
| - Develop tooling for validating entrypoint implementations | ||
|
|
||
| # Credit | ||
|
|
||
| Having been part of the Nix team, | ||
| I suspect that I've learned some of these ideas from the team, especially Eelco. | ||
|
|
||
| Thank you to @ruro for raising the good point that the entrypoint mechanism is independent from TOML changes, | ||
| enabling this feature to be implemented and discussed separately. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the vision is to use a project like naersk or pyproject2nix with either modifications or a wrapper flake that provides callFlakeEntrypoint and if the stars align you only need the flake.toml?
Makes TOML for RFC 193 more appealing to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, yes!
To be realistic, most lang2nix still need some configuration and some hints here or there, and Nix is a pretty good language for that.
But on the other hand, those could be read from a more practical file, whether that's:
flake.toml.nixfiles in standardized placesBoth strike me as good, and it points a point on the horizon for frameworks to work towards, towards reducing configuration without purpose. I'd rather work towards an achievable goal than one that's never quite 100% solvable because of an external factor like a required
flake.nix.