{ description = "Nomarchy — the rock-solid reproducibility of NixOS, the polish of an opinionated desktop"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; home-manager = { url = "github:nix-community/home-manager/release-26.05"; inputs.nixpkgs.follows = "nixpkgs"; }; stylix = { # Track the release branch matching nixpkgs/HM — master is built # against unstable and warns about the version skew on every # activation. (No home-manager input to follow anymore: stylix # vendors its HM integration.) url = "github:nix-community/stylix/release-26.05"; inputs.nixpkgs.follows = "nixpkgs"; }; # Pinned by Nomarchy so distro + hardware quirks are tested together # (consumed by lib.mkFlake's hardwareProfile). Its nixpkgs input is # only used by its own CI — follow ours to keep locks/ISO lean. nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, home-manager, stylix, nixos-hardware, ... }: let system = "x86_64-linux"; username = "nomarchy"; # reference host only; downstream picks its own pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; # Matches nixpkgs.config.allowUnfree in modules/nixos — the # standalone HM generation must come from like-configured pkgs. config.allowUnfree = true; }; # The guided installer — live-ISO only (not in the overlay): it bakes # in the downstream template, this checkout's flake.lock, and the # source identity of this very revision so installs work offline. gitUrl = "https://git.bemagri.xyz/bernardo/nomarchy.git"; nomarchyInstall = pkgs.callPackage ./pkgs/nomarchy-install { templateDir = ./templates/downstream; nomarchyLock = ./flake.lock; hardwareModuleNames = pkgs.writeText "nixos-hardware-modules.txt" (nixpkgs.lib.concatStringsSep "\n" (builtins.attrNames nixos-hardware.nixosModules)); nixpkgsPath = nixpkgs.outPath; # v1 is the release branch: installed machines track it on # `nix flake update` instead of the forge's default branch. flakeUrl = "git+${gitUrl}?ref=v1"; # Future updates re-resolve from the forge (original); the install # itself resolves from the ISO store (locked = path) — fully # offline, even from a dirty-tree ISO. originalNode = { type = "git"; url = gitUrl; ref = "v1"; }; lockedNode = { type = "path"; path = self.outPath; narHash = self.narHash; lastModified = self.lastModified or 0; }; rev = self.rev or null; }; in { # ─── Downstream API ──────────────────────────────────────────── # A user's machine flake imports these and layers its own # system.nix / home.nix on top (see templates/downstream). # Their theme-state.json lives in THEIR flake and is wired up via # the `nomarchy.stateFile` option — reading it stays pure. overlays.default = final: prev: { nomarchy-theme-sync = final.callPackage ./pkgs/nomarchy-theme-sync { # Presets baked into the package, so `nomarchy-theme-sync list` # works on machines that don't check out this repo. themesDir = ./themes; }; }; nixosModules.nomarchy = { imports = [ ./modules/nixos ]; nixpkgs.overlays = [ self.overlays.default ]; }; homeModules.nomarchy = { # stylix's home module is injected here because it needs the # flake input; modules/home/stylix.nix only configures it. imports = [ stylix.homeModules.stylix ./modules/home ]; }; # One-call downstream wrapper — see templates/downstream/flake.nix. # The generated flake is never hand-edited; machines live in # system.nix / home.nix. lib = import ./lib.nix { inherit nixpkgs home-manager nixos-hardware; nomarchy = self; }; templates.default = { path = ./templates/downstream; description = "Nomarchy machine configuration (downstream flake)"; }; packages.${system} = { nomarchy-theme-sync = pkgs.nomarchy-theme-sync; nomarchy-install = nomarchyInstall; default = pkgs.nomarchy-theme-sync; }; # ─── Checks ──────────────────────────────────────────────────── # `nix flake check` never evaluates templates/, so exercise the # downstream template through mkFlake directly (this bypasses the # template's own three-line flake.nix, which only resolves once # instantiated — everything else is covered here, including the # hardwareProfile → nixos-hardware mapping). checks.${system} = let downstream = self.lib.mkFlake { src = ./templates/downstream; username = "me"; hardwareProfile = "framework-13-7040-amd"; # exercises the mapping }; in { downstream-template-system = downstream.nixosConfigurations.default.config.system.build.toplevel; downstream-template-home = downstream.homeConfigurations.me.activationPackage; }; # ─── Reference host ──────────────────────────────────────────── # Manually wired rather than via lib.mkFlake: the repo-root # theme-state.json is load-bearing (live ISO + in-repo CLI dev), # which doesn't match mkFlake's src-layout convention. # Deliberately split rebuild paths: # system → sudo nixos-rebuild switch --flake .#nomarchy (rare) # desktop → home-manager switch --flake .#nomarchy (no sudo; # this is what `nomarchy-theme-sync apply` runs) # Theme changes never touch the system layer, so they never need # root and never rebuild the world. nixosConfigurations.nomarchy = nixpkgs.lib.nixosSystem { inherit system; specialArgs = { inherit username; }; modules = [ self.nixosModules.nomarchy { nomarchy.system.stateFile = ./theme-state.json; } ./hosts/default/configuration.nix ]; }; homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration { inherit pkgs; modules = [ self.homeModules.nomarchy { # The single source of truth — pure read: the file is part # of this flake's source. nomarchy.stateFile = ./theme-state.json; home = { inherit username; homeDirectory = "/home/${username}"; }; } ]; }; # ─── Live ISO ────────────────────────────────────────────────── # Full desktop on a bootable stick, no installation: # nix build .#nixosConfigurations.nomarchy-live.config.system.build.isoImage # Test in QEMU with tools/test-live-iso.sh. See docs/TESTING.md. nixosConfigurations.nomarchy-live = nixpkgs.lib.nixosSystem { inherit system; specialArgs = { inherit username; nomarchySrc = self; }; modules = [ "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" self.nixosModules.nomarchy ./hosts/live.nix # The ISO is a sealed artifact, so the desktop is baked in via # the HM NixOS module (the installed-system split into # nixos-rebuild vs home-manager switch doesn't apply here — # though the seeded ~/.nomarchy flake still allows standalone # `home-manager switch` for theme testing). home-manager.nixosModules.home-manager { home-manager = { useGlobalPkgs = true; useUserPackages = true; users.${username} = { imports = [ self.homeModules.nomarchy ]; nomarchy.stateFile = ./theme-state.json; }; }; # The standalone CLI that `nomarchy-theme-sync apply` runs for # theme switching, from the same pinned input — plus the # guided installer. environment.systemPackages = [ home-manager.packages.${system}.home-manager nomarchyInstall ]; # Keep the locked flake inputs in the ISO store so theme # switching on the live system works without a network: the # lockfile's narHashes resolve against these paths instead # of fetching from GitHub. Must be *transitive* — evaluating # the seeded flake also fetches stylix's own inputs. system.extraDependencies = let collectInputs = input: [ input.outPath ] ++ builtins.concatMap collectInputs (builtins.attrValues (input.inputs or { })); in collectInputs self ++ ( # Pre-build a system shaped like what nomarchy-install # GENERATES (snapper, auto-login, LUKS initrd, swapfile + # resume — not the bare template!) plus the template # desktop, so the offline install never builds packages # from source: a custom username/hostname then changes # only a handful of derivations. No hardware profile — # matches a VM install; profiled installs share most of it. let template = self.lib.mkFlake { src = ./templates/downstream; username = username; }; representativeInstall = nixpkgs.lib.nixosSystem { inherit system; specialArgs = { inherit username; }; modules = [ self.nixosModules.nomarchy { environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; } # The installer's autodetection emits these on real # machines (and the intel CPU module pulls the whole # iGPU stack) — without them an offline install on # common hardware builds graphics drivers from # source. Model-specific profiles may still need # network; the common ground is covered. nixos-hardware.nixosModules.common-cpu-intel nixos-hardware.nixosModules.common-cpu-amd nixos-hardware.nixosModules.common-gpu-amd nixos-hardware.nixosModules.common-pc-laptop nixos-hardware.nixosModules.common-pc-ssd nixos-hardware.nixosModules.common-pc (./templates/downstream + "/hardware-configuration.nix") (./templates/downstream + "/system.nix") ({ lib, ... }: { nomarchy.system.snapper.enable = true; nomarchy.system.greeter.autoLogin = username; # The installer writes these (keyboard-everywhere # incl. the LUKS prompt) — mirror them or the # initrd/vconsole pins don't match an offline # install's rebuild set. console.useXkbConfig = true; boot.initrd.systemd.enable = true; # nixos-generate-config keys microcode off # enableRedistributableFirmware (now on): the # real machine enables exactly one vendor — # pin both, or the offline install builds # amd-ucode/intel-ucode from source (found the # hard way: the 2700s regression hang). hardware.cpu.amd.updateMicrocode = true; hardware.cpu.intel.updateMicrocode = true; swapDevices = [{ device = "/swap/swapfile"; }]; boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000"; boot.kernelParams = [ "resume_offset=1" ]; boot.initrd.luks.devices.crypted.device = "/dev/disk/by-partlabel/disk-main-root"; # The real install is BTRFS with a vfat ESP — the # placeholder's bare ext4 would leave fs-gated bits # out of this pin (snapper via btrfs; mtools and # dosfstools enter systemPackages via vfat!). fileSystems."/" = lib.mkForce { device = "/dev/mapper/crypted"; fsType = "btrfs"; options = [ "subvol=@" "compress=zstd" "noatime" ]; }; fileSystems."/boot" = { device = "/dev/disk/by-uuid/0000-0000"; fsType = "vfat"; options = [ "umask=0077" ]; }; }) ]; }; in [ representativeInstall.config.system.build.toplevel template.homeConfigurations.${username}.activationPackage # The live session's `home-manager switch` builds the # REPO's standalone generation (the seeded ~/.nomarchy # flake) — not the template's, and not the NixOS-module # one baked into the ISO. It has drvs the other two # variants don't (user-dbus-services et al.), so pin it # and its direct build inputs too. self.homeConfigurations.${username}.activationPackage self.homeConfigurations.${username}.activationPackage.inputDerivation # home-files is rebuilt per theme and COPIES some of its # inputs (user-dbus-services) instead of referencing # them — the activation closure doesn't carry those, so # pin home-files' direct build inputs separately. self.homeConfigurations.${username}.config.home-files.inputDerivation # A real install rebuilds the drvs that embed the # user's hostname/username/UUIDs (etc, initrd, units, # toplevel, HM activation). inputDerivation pins each # one's direct build inputs… representativeInstall.config.system.build.toplevel.inputDerivation representativeInstall.config.system.build.etc.inputDerivation representativeInstall.config.system.build.initialRamdisk.inputDerivation template.homeConfigurations.${username}.activationPackage.inputDerivation # home-files COPIES user-dbus-services instead of # referencing it (same story as the self pin above) — # a custom-username install rebuilds home-files and # needs its direct inputs present. template.homeConfigurations.${username}.config.home-files.inputDerivation ] ++ [ # A theme switch re-renders stylix's base16 templates # (gtk.css, kvantum, …) on the machine: the renderer and # the stdenv its trivial drvs build with must be on # board, or an offline `apply ` cascades into # building mustache-go (and eventually stdenv) from # source. Found with tools/vm/gap-analysis.py. pkgs.mustache-go pkgs.stdenv ] ++ [ # …and the second-level build tools those rebuilds need # (their parents are themselves rebuilt, so parent # inputDerivations don't reach them): kernel-module # shrinking, unit-dir assembly, dbus config, /etc. pkgs.kmod.dev pkgs.nukeReferences pkgs.xorg.lndir pkgs.libxslt.bin pkgs.python3Minimal # system-path is rebuilt on EVERY machine: buildEnv's # package-list order follows module-graph order, and any # hardware profile/import reshuffles it. Its builder # wants these. pkgs.texinfo pkgs.getconf # greetd.toml embeds the auto-login user and is generated # by remarshal — without this pin an offline install # tries to BUILD remarshal, whose test closure pulls # matplotlib/ffmpeg/x265 (the ~1900-drv cascade). pkgs.remarshal # system-path/initrd-bin-env are buildEnvs rebuilt per # machine; their shared builder.pl is build-time-only # (never in any runtime closure) — an empty env's # inputDerivation retains it. (pkgs.buildEnv { name = "nomarchy-pin-buildenv"; paths = [ ]; }).inputDerivation # /etc/dbus-1 reassembly wants this setup hook. pkgs.findXMLCatalogs # nixos-generate-config keys microcode off # enableRedistributableFirmware (now on): exactly one # vendor per real machine, so pin both ucode cpios. pkgs.microcode-amd pkgs.microcode-intel # …and one representative disko script: its tool closure # (file, which, wrapper hooks, …) isn't otherwise on the # ISO, and offline nix would try to build it from source. # The user's actual args produce a different script drv, # but with identical inputs — built offline in seconds. (import "${pkgs.disko}/share/disko/cli.nix" { inherit pkgs; mode = "destroy,format,mount"; diskoFile = ./pkgs/nomarchy-install/disko-config.nix; mainDrive = "/dev/vda"; withLuks = true; swapSize = "2G"; }) ] ); } ]; }; }; }