Skip to main content

Architecture

The complete picture in one page. Detailed walkthroughs of each piece live in their own sections.

Block diagram

┌─────────────────────────────────────────────────────────────┐
│ glixos-core │
│ (project-owned flake) │
│ ├─ flake.nix # nixosModules.glixos, lib.import… │
│ ├─ modules/core/ # base system │
│ ├─ modules/desktop/ # optional desktop profile │
│ └─ lib/importManifest.nix │
└──────────────────────┬──────────────────────────────────────┘
│ flake input

┌─────────────────────────────────────────────────────────────┐
│ user-packages flake │
│ (user-owned git repo, generated by `glix init`) │
│ ├─ flake.nix # glix-managed regions: │
│ │ # • inputs │
│ │ # • nixosConfigurations hosts │
│ ├─ hosts/<host>/ │
│ │ ├─ glix.toml # the ONLY file glix writes │
│ │ └─ default.nix # hardware/users; static after init │
│ ├─ shared/ # user-owned, free-form │
│ └─ flake.lock # pinned inputs │
└──────────────────────┬──────────────────────────────────────┘
│ nixos-rebuild switch --flake .#<host>

Running system

Data flow for glix add

$ glix add --scope=home --user=alice github:owner/cool-pkg

┌── glix (Go) ─────────────────────────────────────────────┐
│ 1. resolveRepo() → repo.Repo{Root: ~/.config/glixos} │
│ 2. resolveRegistryURL() → registry URL or default │
│ 3. resolver.Resolve(ref, opts) │
│ - URI? → passthrough │
│ - registry? │
│ - nix registry list? │
│ returns Resolution{Ref, Source} │
│ 4. manifest.Load(hosts/laptop/glix.toml) │
│ 5. manifest.Package{Flake, Scope, User, ...} │
│ 6. repo.TakeSnapshot() ← in-memory state for rollback │
│ 7. manifest.Save() → glix.toml written atomically │
│ 8. flake.PatchFile(flake.nix, {inputs, hosts}) │
│ 9. nix flake lock │
│ on failure: snapshot.Restore() then return error │
│10. repo.Commit("glix add laptop: cool-pkg (...)") │
└──────────────────────────────────────────────────────────┘

Data flow for nixos-rebuild switch

nixos-rebuild --flake .#laptop switch


flake.nix → outputs.nixosConfigurations.laptop


mkHost "laptop" "x86_64-linux"


let manifest = glixos-core.lib.importManifest {
manifestPath = ./hosts/laptop/glix.toml;
inputs;
defaultUser = "alice";
};
│ produces: { systemModules, homeModules, homeModulesByUser, manifest }

nixosSystem {
modules = [
glixos-core.nixosModules.glixos # OS base
home-manager.nixosModules.home-manager
./hosts/laptop # hardware, users
] ++ manifest.systemModules; # per-package system modules
}

hosts/laptop/default.nix:
home-manager.users = lib.mapAttrs
(_: mods: { imports = mods ++ [ { home.stateVersion = "24.11"; } ]; })
manifest.homeModulesByUser; # per-user home-manager modules

Invariants

  • Manifest is the truth. Re-running nix flake lock from scratch must reproduce the same flake.lock modulo input timestamps, given the same glix.toml.
  • Anchored regions are exclusive. Nothing outside >>> glix-managed >>><<< glix-managed <<< is ever rewritten by glix. The patcher refuses to operate if a marker is missing.
  • One commit per mutation. If a command fails partway, snapshots restore state and no commit is created. If it succeeds, exactly one commit is created.
  • Schema is additive. Adding a field with omitempty does not bump schema. A breaking change bumps schema and forces an explicit migration in manifest.Load.

Where the boundaries are

BoundaryCrossed by
Go ↔ Nixmanifest.Loadbuiltins.fromTOML
glix ↔ userThe anchored regions; everything else is user-owned
glixos-coreuser-packagesOne flake input edge; no inverse
glix.toml ↔ a package module_module.args.glixConfig (string→string)
glixnixos-rebuildinternal/nix/exec.go thin wrappers

Next