Description
Desiderata
Here are a number of goals we would like to have, where the current organization falls shorts
Low latency Nix builds
For CI and local development a like, it is nice if repeated Nix builds avoid duplicated work. This means having fine-grained derivations with well-scoped inputs and outputs.
Ultimately, we would need Recursive Nix or RFC 92 to really nail this, but in the meantime we can at least focus on some lower hanging fruit: filtering sources just to the files relevant for the task at hand.
One way to do this is with builtins.filterSource
and friends, but keeping such predicates up to date is a bit frustrating. A more maintainable way is to leverage the repo directory structure so that separate build tasks correspond to separate directories.
This also makes it easy to understand the project structure at a glance.
Cross-project incremental development.
For proper builds, dependencies should always be built in separate derivations, for the reason of low latency defined above. But for the foreseeable future, developers wanted the best developer experience will still be doing impure builds in a development shell environment.
If the developer is just working on one project, giving that shell environment the same dependencies as the regular build is fine, but quite often the developer wants to work on multiple projects at once. Then, having the dependencies be pre-built in the development shell is very frustrating for two reasons:
- The developer must re-enter the shell every time they modify the dependency
- The developer must way for a from-scratch rebuild of the dependency when they renter the development shell.
This is an issue both within this repo and between repos:
- When trying to fix the Perl bindings (in tree) after a modification of Nix, one needs to wait for a from-scratch Nix rebuiild.
- The Python bindings from Experimental python bindings #7735 (in tree), have a best-effort mitigation of this, but it is not easy to write today.
- Developing Hydra (out of tree) also requires a from-scratch Nix rebuild. This makes solving issue Deduplicate code between Hydra and Nix hydra#1164, and is probably a reason why some of this duplication arose in the first place --- it is just too painful making small changes to Nix and easier to duplicate functionality in Hydra.
The relationship to repo organization is that in order to maintainably support both the development and release builds workflows, they must be as similar as possible. The means, the location of source files in the repo need to roughly correspond to the location of installed files in dependencies' outputs.
- Headers should "look the same" both installed (with
-I /nix/store/...
flags) or in tree (with `-I this/repo/...) flags. - No
-include config.h
should be done, because downstream projects should need to include headers not replicate such CLI flags.
The status quo
Today, we have a directory structure like this:
flake.nix
perl/
├── lib
│ └── Nix
│ ├── Store.pm
│ ├── Store.xs
│ ...
...
python/
├── ...
...
Makefile
src
├── libcmd
│ ├── command.cc
│ ├── command.hh
│ ...
├── libexpr
│ ├── attr-path.cc
│ ├── attr-path.hh
│ ├── tests/
│ │ ├── value/context.hh
│ │ ├── value/context.cc
│ │ ...
│ ...
...
Here are the problems with this:
-
Even though we have separate top-level projects, Nix itself is splatted on the top level
- The flakes.nix and perl binding sources thus pollute the build of Nix itself --- changing just the perl bindigns also invalidates the dev shell (so the developer must be careful not to leave the dev shell by mistake, or they will have to wait to reenter it!)
-
The header are directly in the root of
-I
search path entries, but they will be installed in$dev/include/nix/
-- a newnix
subdir! We have a non-standard pkg-config hacking around this, but this is not a good solution. -
The flake.nix is too big and hard to read.
-
The test headers are mix in with the library headers, and we have to manually be careful not to install them.
A plan
I propose we instead adopt something like this:
perl/
├── default.nix
├── lib
│ └── Nix
│ ├── Store.pm
│ ├── Store.xs
│ ...
...
python/
├── default.nix
├── ...
...
nix
├── default.nix
├── Makefile
├── libcmd
│ ├── include/nix/cmd
│ │ ├── command.hh
│ │ ...
│ └── src
│ ├── command.cc
│ ...
├── libexpr
│ ├── include/nix/expr
│ │ ├── attr-path.hh
│ │ ...
│ └── src
│ ├── attr-path.cc
│ ...
├── libexpr-tests
│ ├── include/nix/expr/tests/
│ │ ├── value/context.hh
│ │ │
│ │ ...
│ └── src
│ ├── value/context.hh
│ ...
...
integration-tests
├── authorization.nix
...
Note these changes:
-
Each project (Nix itself, Perl bindings, Python bindings) is confined to its own subdir, clarifying the project structure and avoiding input leakage for lower latency builds
-
Each project gets its own
default.nix
, which will becallPackage
d in the top-levelflake.nix
. This makes clear what dependencies of each "unit" are, and those files could someday be mirrored one-for-one in Nixpkgs easing maintenance. The top-levelflake.nix
is much shorter and clearer.- We might someday use sub-flakes for this, but in my opinion they are not yet mature enough.
-
The extra
src
dir is removed, we can go straight to talking about each library- This helps cut down on the extra subdir cost we are paying.
- Instead we have a
src
dir per library
-
The public headers are put in their own directory, and given the same prefix they would be installed under.
- N.B. Putting the library name in the prefix (
nix/
vsnix/expr
, for example, is an orthogonal choice we can decide on separately. - If we ever had private headers (perhaps useful if we want to ever move towards a partially stable C++ interface), those would also go in the
src
directory, indicating they are not installed.
- N.B. Putting the library name in the prefix (
-
The test libraries are completely separated out
- There is no risk of mixing up test srcs and headers for library proper srcs and headers
-
The integration tests (NixOS VM tests) are separated out, so that changes to them don't need to cause a
nix
rebuild
Bonus: Meson support
One of Meson's best features is Subprojects. This is designed to agglomerate a bunch of separate packages into one single incremental build with minimal hassle. Dependencies (or rather virtual things to depend-upon) can be declared in individual meson projects, and then when the projects are combined together needed-dependencies can be satisfied with these instead of externally (e.g. with `pkg-config).
If we switched to using Meson across the board (Nix itself, Python bindings, Perl bindings, Hydra), we would have a meson.build
in each sub-directory, and then a top-level meson.build
in the root of this repo adding all the others as sub-projects. The top-level meson.build
would just be used for development.
If a developer wants to develop Nix and hydra at the same time, they would unpack/symlink hydra to a new subdir along-side the others and then add it with subproject(...)
in the top-level meson.build
. Then incremental builds of Nix and Hydra together would just work --- no further manual steps needed!
Drawbacks
-
A lot of files will move around
- But this is a one time cost. Rebasing the PR that moves the files is hard, so we should decide on this issue first and then quickly implement whatever we decide on right after, but rebasing other PRs over that PR is easy.
-
File paths are longer and more redundant
- True. Cutting out the
src
directory helps a bit, but we still have things likenix/libstore/include/nix/store/foo-bar.hh
. But it doesn't seem that much can be done out of this without failing to meet our desiderata.
- True. Cutting out the
-
Files are further apart.
foo.cc
is no longer right next tofoo.hh
- True. But again, it doesn't seem that much can be done out of this without failing to meet our desiderata.