Skip to content
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

Create a paradigm for multi-application configuration #75

Open
llakala opened this issue Oct 26, 2024 · 5 comments · May be fixed by #127
Open

Create a paradigm for multi-application configuration #75

llakala opened this issue Oct 26, 2024 · 5 comments · May be fixed by #127

Comments

@llakala
Copy link
Owner

llakala commented Oct 26, 2024

We've created many paradigms in the lifetime of this project. These include:

  • Using home-manager as a module for a far simpler filetree
  • Using a custom import function to avoid default.nix, which can cause bugs
  • Making said import function recursive, but limited to 1 depth, to allow splitting a single file's configuration into multiple files in a subfolder
  • Using myLib to separate real lib functions from our custom ones
  • Using helper functions such as lib.singleton to de-nest code
  • Using an hm alias to easily configure something via home-manager
  • Using hostVars for easy access to something that may differ between hosts
  • Ensuring any custom modules don't hide functionality, and only abstract boilerplate
  • Running unfree packages without a reliance on an external input via mkUnfreeNixpkgs

The length of this list, and the number of paradigms I'm probably forgetting, serves as an example of the many problems we have solved. Antipatterns that once seemed impossible to avoid are now no longer a concern.

So, what's the point of all this? The point is that we can solve problems.

And with that, let's get into the problem.

I am the lorax, I speak for the trees

Our configuration attempts to take the stance that most files should be a single actor that isn't load-bearing. What does this mean?
Well, let's think of a NixOS configuration as a tree.

The flake.nix is the trunk. It's reasonable to expect that a change to the flake could easily break something. The flake is connected to limbs. We can think of the functions in myLib as a limb. If we broke mkUnfreeNixpkgs, we could expect an error elsewhere in the configuration. The same is true for our custom modules. The nature of these files is that they provide functionality to other files, and are thus expected to have a larger impact if they're changed.

But, what about leaves? These are the individual files. These leaves can rely on branches. Any file relying on the existence of the hm alias, or hostVars, or myLib, or a custom module, But, ideally, they shouldn't rely on each other. A leaf should only expect the tree to remain unchanged. When we encounter a leaf relying on another leaf, it typically means we should create an abstraction within the tree.

One of these recent abstractions are twigs, or subfolders. Previously, something like configuring Firefox was expected to be limited to a single leaf. But, if Firefox required a lot of configuration, the file's length ended up ballooning out of control. To solve this, we introduced subfolders. Each leaf now represented a single aspect of the Firefox configuration. These leaves are expected to be relatively independent, but they can rely on the existence of another. For anything within the Firefox home-manager module to be configured, hm.programs.firefox.enable has to be true. The other files can expect that this will be true, and just set things up under this assumption.

But, what if two leaves need to be friends?

Leafeo and Leafiet: A love story

Let's use an example. Yazi has a plugin that integrates Starship. We want to enable this plugin, but only on the condition that we're using Starship. If we ever stop using Starship, it should be made clear to the user that we have to disable this integration too.

This integration, like most others, follow a pattern of:

# yazi.nix
if starship.isUsed then
   yazi.integrations = starship.package;

Let's make. From now on, we'll call the program integrating another program the actor. The program being integrated will be the prop. Rewriting our code to accommodate this new terminology:

# actor.nix
if prop.isUsed then
   actor.integrations = prop.package;

Now, we're set up to go through some hypothetical solutions to this problem,

@llakala llakala mentioned this issue Oct 28, 2024
@llakala
Copy link
Owner Author

llakala commented Oct 28, 2024

Solution 1: The actor buys the prop

In this solution, the actor moves its config to a folder, so we can have a twig with multiple files. Their structure will look something like this:

apps/cli/actor
├── actor.nix
└── prop.nix

The contents of prop.nix would look something like this:

{ config, lib, ... }:
let
  propExists = config.programs.prop.enabled;
in
{
  actor.integrations = lib.mkIf propExists
  [
    config.programs.prop.package
  ];
}

There are pros and cons to this, which I'll go into later after editing this!

@llakala
Copy link
Owner Author

llakala commented Oct 28, 2024

Solution 1b: the prop has the actor's address written on it

TODO: WRITE THIS

@llakala
Copy link
Owner Author

llakala commented Oct 28, 2024

Solution 2: The prop says that it's available via a module

TODO: WRITE THIS

@llakala
Copy link
Owner Author

llakala commented Oct 28, 2024

Solution 3: The integration is stored at a storage unit

TODO: WRITE THIS

@llakala
Copy link
Owner Author

llakala commented Feb 9, 2025

Coming back to this after many months. Ignore all the TODOs above -- let's be real, they're not getting done.

What we really want from multi-application configuration is to be able to rely on something else for the system being set up - whether it be abbreviations, integration with the shell, or whatever. Relying on the use of another program means that if I ever STOP using that program, I'll have all sorts of "program state", where everything that relied on that has to change.

In my previous ideas, I considered using a file structure to enforce this. I don't think that's the best way. Instead, I think assertions may be where it's at.

Let's say my system has some global variable usingZshAbbrs. Any Nix file that wants to add custom abbreviations could simply assert that usingZshAbbrs was true. If I someday swapped to Fish, usingZshAbbrs would be false, and all of these files would then create an error. This would make it trivial to find and fix these instances.

I think this is perfect. The only trouble is choosing where to source these booleans from. I could either:

  • check whether the relevant enable options were set to true. This inherently relies on a bit of state from someone ELSE (that being NixOS / Home-Manager). That option could hypothetically become detached from what it should be in the future, and I'd have to rewrite all the assertions to reflect this.
  • Have a global source of truth for features. I've done a similar thing in the past. Instead of grabbing the host name from networking.hostName, I grab it from config.hostVars.hostname, a custom module that exists purely so I can access the values in it. I like this centralized approach, but the trouble with it here is that you have to remember to actually change it.

So, we have a worry with both. Either we rely on the option being a source of truth, or myself being a source of truth - both of which are fallible. My instincts say to use the global source of truth method, and simply find a way to tie the functionality TO the global source of truth. So, for example, in the apps/zsh/zsh.nix file, we would set the value. This might mean that instead of using booleans, we would set up a value like abbreviationsProvider, and set the value to zsh in the apps/zsh/zsh.nix file.

I'd like to start by setting this up for Gnome, Fish, and Starship. We COULD implement this for Home-Manager - although I worry that doing that would leave these assertions absolutely everywhere. The truth is, there aren't many of these integrations currently - I've been holding back on them BECAUSE of this issue. Creating a module and getting the scaffolding for future assertions would mean I could start creating a lot of these.

@llakala llakala linked a pull request Feb 9, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant