Hi @nrdxp + @edolstra -- I have a couple ideas for how to make things marginally better here but, tl;dr: I'd like feedback or direction from either or both of you on my idea before I invest more time, especially for exploring the proposed solutions that require C++ development. (Also, please let me know if you'd like me to move this from an issue comment to an RFC or even a, e.g., Google Doc design doc, for easier interaction.)
In my words at a minimum:
flake authors need to be able to request submodule-fetching for the repository hosting
flake.nix
(Please see my appendix for supporting use cases and UX/DX considerations if you aren't already convinced about this need. Also, @nrdxp, please let me know if you actually have a different motivating problem in mind?)
As I see it, given the constraint that flake authors need to be able to request submodule-fetching for the repository hosting flake.nix
, there are two basic ways to create the desired experience: either a) nix can fetch and process the self
-flake's flake.nix
separately from and prior to fetching the overall self
flake or b) the flake author can synthesize an additional input, possibly derived from self in some way, to use to obtain the necessary source materials and we can try to make things fast and ergonomic after the fact as best we can.
(Next, while I am fairly confident that (a) can be made to work with some probably-acceptable downsides, I'm instead going to offer some thoughts on (b) given @edolstra's preliminary skepticism/disfavor of (a).)
With that said, here are five or so ways to approach enabling flake authors to request submodule fetching for the repository hosting flake.nix
by synthesizing an additional input, approximately ordered from most to least preferable in my view:
-
an "inputs overlay". Method: we define a new flake attribute called
inputsOverlay
, with the usual functional form:final: prev: { ... }
and we use it to transform the providedinputs
values before they make their way to the flake'soutputs
function's arguments. -
by enabling flake authors to make "full use" of the nix language to write flakes. Method: allow flake authors to define the top-level flake.nix attrset to be recursive (already done, I think) and further allow them to define additional inputs whose attr values are thunks that evaluate to the required types but which may be defined in terms of other inputs' values; i.e.
rec {
description = "...";
inputs.self2.url = if inputs.self.type || "" != "git" then "git+https://example.com/repo?submodules=1&flake=0" else inputs.self.url;
outputs = { self, nixpkgs, self2 }: {...};
}
-
by making "self" even more magical. Method: when attributes are defined on
inputs.self
, we replace theself
attr ofoutputs
' arguments with afetchTree
application containing the revised attributes. -
by creating a "follows"-like behavior for inputs that need to be produced by modifying a prototype. Method: when an
inputs.<foo>.prototype = "bar"
is defined, [or with whatever other attribute name we pick instead of "prototype"], we initializeinputs.<foo>
withinputs.bar
's value instead of with{}
. -
by using attributes of the
self
argument in the body ofoutputs
to create a fixed-output derivation that fetches the necessary code.
So far as I know today, all of the above methods can be made to work but they all suffer from different problems and tradeoffs. For example:
The "inputs overlay" method would likely work but has a couple problems. Problems include:
a. it's one more advanced flakes feature / convention to have to learn, above and beyond ".submodules"
b. there's a decision to be made about whether the inputs overlay also allows modifying the inputs of input flakes -- i.e., similar to how ".follows" works or whether it only is used to synthesize inputs for the outputs function.
c. there's a compositionality question: when flake A uses an input overlay and flake B uses an input overlay, do these overlays interact at all? what if C uses A and B?
d. is one overlay enough or should we start off with a list, since most other overlays-using APIs work with ordered lists of overlays, in which case presumably the attribute should be named inputOverlays
?
The "full use" method may be the best choice, but it has drawbacks. Drawbacks include:
a. there's a modest security risk associated with increasing the power of the language that the nix flakes interpreter has to cope with even to check or to list the outputs of a flake. b. many people seem to prefer using helper libraries to express their intent rather than writing low-level nix programs to do so, so it might be harder to document how to use low-level nix to solve the "self-with-submodules" problem?
The "magical self" method can be made to work, but it will be limited. Limits include:
a. it's kind of specific to the current problem of making "submodules" work well, so I feel more pessimistic about its applicability to broader problems involved in specifying inputs, e.g., for people with large monorepos or who distribute everything through zip files, or who are not using git, etc etc etc.
The "prototype directive" method can be made to work, but really, why introduce a new way to do with strings what we can already do with nix unless we truly need to signal something special to the nix flakes interpreter?
The "FOD" method: while this can be done for now, we should IMO absolutely avoid even if only because it clashes with using the nix flake lock
tooling to manage input locking, whatever its other weaknesses.
Finally, in addition to the method-specific tradeoffs, there's also two other top-level issues that I think are worth mentioning:
a. regardless of method, it would be really really good to have a way to cache, coalesce, lazily avoid, etc etc etc. -- the overhead of having to make, process, maintain, and work with two copies of a potentially large repository just because submodule-processing is needed.
b. additionally, regardless of method, it's also unclear to me exactly how this input-synthesis idea should interact with flake locking and with "follows". What I see here is: in these schemes, we don't really want to manage another input separately from the "self" input; we are only specifying one instrumentally. Thus, how do we tell nix "actually, don't include and manage this synthetic input in your lock file; just pretend that the user had specified its options for you on the command line"?
I got interested in the problem I've articulated above because of some not-great user- and developer-experiences I had a few weeks ago. Here's what happened.
While writing my qemu-m1 flake, I needed to apply patches to qemu upstream and I also wanted to reuse nixpkgs' qemu expression with slight modifications.
In packaging these patches, I actually tried several ways to do this, including
- making an "external" flake to use to apply the relevant mbox of patches "externally".
- making a "published fork" flake with a submodules-enabled input that refetches the same underlying git repo as contains the flake.nix being processed and
- making a "published fork" flake without a secondary input during the brief period of time when nix#4423 was fixed.
Of these three approaches, only (3) provided the both the DX + the UX that I wanted. Here's why:
a. as a flake-publisher, I want to be able to fully control -- or at least specify defaults for all options/choices related to -- fetching and building via flake.nix
. (Specifically, I want don't want my users to have to specify any unusual flake-ref options to build my flake; I want to take care of that for them so that the 'obvious' way to use the flake works.)
b. as a flake-user, I want to be able to build a flake that I've heard about, I want things to "just work", and if for some reason they can't "just work", I want them to ideally fail quickly and with a friendly error message, not with an obscure build failure that I have to google or with no error message at all.
c. as a developer, I want to be able to maintain patches in whatever system suits me best -- mboxes, a feature branch, a patch-oriented VCS, ...
d. as a flake-developer patching an upstream release or repo, I want to be able to easily combine patches, upstream code, and pre-existing nix packaging e.g. via ergonomic and well-documented overrides.
Taken all together, these mean that as a flake author, I want the responsibility for specifying that submodules-processing is required to be on me the flake-author when it is needed and not on my user -- although, for extra points, I would also prefer not to have to fetch the whole source repository I'm working with twice, or to have two checkouts of it in the nix store.
Additionally, I want all of the above to work regardless of whether my flake is being built from a remote URL/flake-ref or it is being built "locally", as when I'm working on a check-ed out repo containing the flake.
(Note: this last issue caused a big DX hassle with the "second input" solution because of the fact that nix's current flakes infrastructure additionally treats the working copy and the "self" input magically in the sense that nix, in effect, treats the "self" input as being automatically stale whenever there's a working copy, whereas explicit lock-file management or a --refresh
argument is needed when nix is told to fetch the code that should be in the "self" input from another input, because submodule-processing is also needed.)
(Finally, also here, it's weird that we need to set .flake = false
on the alternative input in order to avoid unwanted recursion, despite the fact that the code in question does -- and must -- have a top-level flake.nix, since I'm trying to make a flakeified published fork, not an "out-of-band" or "external" flake.)