Architecture Decision Record
An architecture decision record (ADR) is a document that captures an important architectural decision made along with its context and consequences.
The template has all the info.
Usage
To interact with this ADR, enter the devshell and interact though the adrgen
tool.
1. Adopt semi-conventional file locations
Date: 2022-03-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
Repository navigation is among the first activities to build a mental model of any given repository.
The Nix Ecosystem has come up with some weak conventions: these are variations that are mainly informed by the nixpkgs
repository, itself.
Despite that, users find it difficult to quickly "wrap their head" around a new project.
This is often times a result of an organically grown file organization that has trouble keeping up with growing project semantics.
As a result, onboading onto a "new" nix project even within the same organizational context, sometimes can be a very frustrating and time-consuming activity.
Decision
What is the change that we're proposing and/or doing?
A semi-conventional folder structure shall be adopted.
That folder structure shall have an abstract organization concept.
At the same time, it shall leave the user maximum freedom of semantics and naming.
Hence, 3 levels of organization are adopted. These levels correspond to the abstract organizational concepts of:
- consistent collection of functionality ("what makes sense to group together?")
- repository output type ("what types of gitops artifacts are produced?")
- named outputs ("what are the actual outputs?")
Consequences
What becomes easier or more difficult to do because of this change?
With this design and despite complete freedom of concrete semantics, a prototypical mental model can be reused across different projects.
That same prototypical mental model also speeds up scaffolding of new content and code.
At the expense of nested folders, it may still be further expanded, if additional organization is required.
All the while that the primary meta-information about a project is properly communicated through these first three levels via the file system api, itself (think ls
/ rg
/ fd
).
On the other hand, this rigidity is sometimes overkill and users may resort to filler names such as "default
", because a given semantic only produces singletons.
This is acceptable, however, because this parallellity in addressing even these singleton values trades for very easy expansion or refactoring, as the meta-models of code organization already align.
2. Restrict the calling interface
Date: 2022-03-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
The Nix Ecosystem has optimized for contributor efficiency at the expense of local code readibility and local reasoning.
Over time, the callPackage
idiom was developed that destructures arbitrary attributes of an 80k upstream attributeset provided by nixpkgs
.
A complicating side condition is added, where overlays modify that original upstream packages set in arbitrary ways.
This is not a problem for people, who know nixpkgs by heart and it is not a problem for the author either.
It is a problem for the future code reader, Nix expert or less so, who needs to grasp the essence of "what's going on" under a productivity side condition.
Local reasoning is a tried and tested strategy to help mitigate those issues.
In a variant of this problem, we observe only somewhat convergent, but still largely diverging styles of passing arguments in general across the repository context.
Decision
What is the change that we're proposing and/or doing?
Encourage local reasoning by always fully qualifing identifiers within the scope of a single file.
In order to do so, the entry level nix files of this framework have exactly one possible interface: {inputs, cell}
.
inputs
represent the global inputs, whereas cell
keeps reference to the local context.
A Cell is the first ordering priciple for "consistent collection of functionality".
Consequences
What becomes easier or more difficult to do because of this change?
This restricts up to the prescribed 3 layers of organization the notion of "how files can communicate with each other".
That inter-files-interface is the only global context to really grasp, and it is structurally aligned across all Standard projects.
By virtue of this meta model of a global context and inter-file-communications, for a somewhat familiarized code reader the barriers to local reasoning are greatly reduced.
The two context references are well known (flake inputs & cell-local blocks) and easily discoverable.
For authors, this schema takes away any delay that might arise out of the consideration of how to best structure that inter-file-communication schema.
Out of experience, a significant and low value (and ad-hoc) design process can be leap-frogged via this guidance.
3. Hide system for mortals
Date: 2022-04-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
In the context of DevOps (Standard is a DevOps framework), cross compilation is a significatly lesser concern, than what it is for packagers.
The pervasive use of system
in the current Nix (and foremost Flakes) Ecosystem is an optimization (and in part education) choice for these packagers.
However, in the context of DevOps, while not being irrelevant, it accounts for a fair share of distraction potential.
This ultimately diminishes code-readibility and reasoning; and consequentially adoption. Especially in those code paths, where system
is a secondary concern.
Decision
What is the change that we're proposing and/or doing?
De-systemize everything to the "current" system and effectively hiding the explict manipulation from plain sight in most cases.
An attribute set, that differentiates for systems on any given level of its tree, is deSystemized
.
This means that all child attributes of the "current" system are lifted onto the "system"-level as siblings to the system attributes.
That also means, if explicit reference to system
is necessary, it is still there among the siblings.
The "current" system is brought into scope automatically, however.
What "current" means, is an early selector ("select early and forget"), usually determined by the user's operating system.
Consequences
What becomes easier or more difficult to do because of this change?
The explicit handling of system
in foreign contexts, where system
is not a primary concern is largely eliminated.
This makes using this framework a little easier for everybody, including packaging experts.
Since nixpkgs
, itself, exposes nixpkgs.system
and packaging without nixpkgs
is hardly imaginably, power-users still enjoy easy access to the "current" system, in case it's needed.
4. Early select system for conceptual untangling
Date: 2022-04-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
Building on the previous ADR, we saw why we hide system
from plain sight.
In that ADR, we mention "select early and forget" as a strategy to scope the current system consistently across the project.
The current best practices for flakes postulate system
as the second level selector of an output attribute.
For current flakes, type primes over system.
However, this design choice makes the lema "select early and forget" across multiple code-paths a pain to work with.
This handling is exacerbated by the distinction between "systemized" and "non-systemized" (e.g. lib
) output attributes.
In the overall set of optimization goals of this framework, this distinction is of extraordinarily poor value, more so, that function calls are memoized during a single evaluation, which renders the system selector computationally irrelevant where not used.
Decision
What is the change that we're proposing and/or doing?
- Move the
system
selector from the second level to the first level. - Apply the
system
selector regardless and without exception.
Consequences
What becomes easier or more difficult to do because of this change?
The motto "select early and forget" makes various code-paths easier to reason about and maintain.
The Nix CLI completion won't respond gracefully to these changes. However, the Nix CLI is explicitly not a primary target of this framework. The reason for this is that the use cases for the Nix CLI are somewhat skewed towards the packager use case, but in any case are (currently) not purpose built for the DevOps use case.
A simple patch to the Nix binary, can mitigate this for people whose muscle memory prefers the Nix CLI regardless. If you've already got that level of muscle memory, its meandering scope is probably anyways not an issue for you anymore.
5. Nixpkgs is still special, but not too much
Date: 2022-05-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
In general, Standard wouldn't treat any intput as special.
However, no project that requires source distributions of one of the 80k+ packages available in nixpkgs
can practically do without it.
Now, nixpkgs
has this weird and counter-intuitive mouthful of legacyPackages
, which was originally intended to ring an alarm bell and, for the non-nix-historians, still does.
Also, not very many other package collections adopt this idiom which makes it pretty much a singularity of the Nix package collection (nixpkgs
).
Decision
What is the change that we're proposing and/or doing?
If inputs.nixpkgs
is provided, in-scope legacyPackages
onto inputs.nixpkgs
, directly.
Consequences
What becomes easier or more difficult to do because of this change?
Users of Standard access packages as nixpkgs.<package-name>
.
Users that want to interact with nixos, do so by loading nixos = import (inputs.nixpkgs + "/nixos");
or similar.
The close coupling of the Nix Package Collection and NixOS now is broken.
This suites well the DevOps use case, which is not primarily concerned with the unseparable union of the Nix Packages Collection and NixOS.
It rather presents a plethora of use cases that content with the Nix Package Collection, alone, and where NixOS would present as a distraction.
Now, this separation is more explicit.
As another consequence of not treating nixpkgs
(or even the packaging use case) special is that Standard does not implement primary support for overlays
.
6. Avoid fix-point logic, such as overlays
Date: 2022-05-01
Status
accepted
Context
What is the issue that we're seeing that is motivating this decision or change?
Fix point logic is marvelously magic and also very practical.
A lot of people love the concept of nixpkgs
's overlays
.
However, we've all been suckers in the early days, and fix point logic wasn't probably one of the concepts that we grasped intuitivly and right at the beginning of our Nix journey.
The concept of recursivity all in itself is already demanding to reason about, where the concept of recourse-until-not-more-possible is even more mind-boggling.
Fix points are also clear instances of overloading global context.
And global context is a double edged sword between high-productivity for that one who has a good mental model of it and nightmare for that one who has to resort to local reasoning.
Decision
What is the change that we're proposing and/or doing?
In the interest of balancing productivity (for the veteran) and ease-of-onboarding (for the novice), we do not implement a prime support for fix-point logic, such as overlays
at the framework level.
Consequences
What becomes easier or more difficult to do because of this change?
Users who depend on it, need to scope its use to a particular Cell Block.
For the Nix package collection, users can do, for example: nixpkgs.appendOverlays [ /* ... */ ]
.
There is a small penalty in evaluating nixpkgs
a second time, since every moving of the fix point retriggers a complete evalutation.
But since this decision is made in the interest of balancing enacting trade-offs, this appears to be cost-effective in accordance with the overall optimization goals of Standard.