From c603e3e4775ccbfa80cb4d4553acfbf41e9469e3 Mon Sep 17 00:00:00 2001 From: Alex Spaulding Date: Thu, 28 May 2026 13:57:07 -0700 Subject: [PATCH] chore: checkpoint remaining migration and docs updates Capture the current in-progress repository migration, module reshaping, and documentation updates so remote state matches local committed work. Co-authored-by: Cursor --- .github/workflows/ci.yml | 3 - .sops.yaml | 20 + README.md | 13 +- den-aspects/styling.nix | 1454 +++++++++++++ docs/awesome-nix.md | 467 ++-- docs/den.md | 899 ++++++++ docs/dendritic-nix/01-foundations.md | 74 + docs/dendritic-nix/02-module-mechanics.md | 101 + docs/dendritic-nix/03-repo-implementation.md | 90 + docs/dendritic-nix/04-real-examples.md | 99 + docs/dendritic-nix/05-migration-playbook.md | 91 + docs/dendritic-nix/06-anti-patterns.md | 120 ++ docs/dendritic-nix/07-review-checklist.md | 44 + docs/dendritic-nix/README.md | 32 + docs/dendritic-patterns.md | 277 +++ docs/dentric_nix.md | 487 +---- docs/sops-nix.md | 494 +++++ docs/sops-nix/01-architecture.md | 72 + docs/sops-nix/02-key-management.md | 126 ++ docs/sops-nix/03-authoring-and-files.md | 101 + docs/sops-nix/04-consumption-patterns.md | 85 + docs/sops-nix/05-templates-and-services.md | 166 ++ docs/sops-nix/06-operations-and-rotation.md | 103 + docs/sops-nix/07-troubleshooting.md | 105 + docs/sops-nix/08-repo-reference.md | 151 ++ docs/sops-nix/README.md | 29 + docs/tmux.md | 30 +- docs/zsh-plugins.md | 72 +- explanation.md | 36 +- flake.lock | 391 ++-- flake.nix | 312 +-- hosts/darwin/mba/default.nix | 360 ++-- hosts/hm/8amps-linux/default.nix | 23 +- hosts/nixos/mba-asahi/default.nix | 52 +- hosts/nixos/nixos-test/default.nix | 46 +- .../system-manager/linux-generic/default.nix | 19 - metadata.json | 1392 +++++++++++- modules/apps/_vscode-common.nix | 165 +- modules/apps/antigravity.nix | 86 +- modules/apps/beeper.nix | 1241 ++++++----- modules/apps/brave.nix | 693 ++++++ modules/apps/chatgpt-cli.nix | 38 + modules/apps/common.nix | 125 -- modules/apps/cursor.nix | 111 +- modules/apps/eclipse-java-google-style.xml | 337 --- modules/apps/fastfetch.nix | 7 + modules/apps/firefox.nix | 245 +++ modules/apps/gh.nix | 54 + modules/apps/ghidra.nix | 7 + modules/apps/ghostty.nix | 535 +++-- modules/apps/java.nix | 11 + modules/apps/jetbrains.nix | 696 +++--- modules/apps/mas.nix | 207 +- modules/apps/safari.nix | 11 + modules/apps/spotify.nix | 107 +- modules/apps/sway.nix | 21 - modules/apps/vesktop.nix | 292 ++- modules/apps/vscode.nix | 37 +- modules/apps/wallpaper.nix | 259 ++- modules/configurations.nix | 25 + modules/darwin-appearance-sync.nix | 598 ++++++ modules/darwin-maintenance.nix | 121 +- modules/default.nix | 113 +- modules/dock.nix | 100 +- modules/editor.nix | 1912 ++++++++++++++++- modules/host-topology-den.nix | 227 ++ modules/linux-desktop.nix | 46 +- modules/microvm.nix | 111 +- modules/mobile.nix | 25 +- modules/opencode_dummy.nix | 29 +- modules/overlays.nix | 332 +-- modules/pkgs/_fancy-cat.nix | 10 +- modules/pkgs/_mas-sync.nix | 41 +- modules/python.nix | 54 +- modules/qt_dummy.nix | 21 +- modules/secrets.nix | 149 +- modules/{shell.nix => shell-config.nix} | 62 +- modules/sops-validation.nix | 112 + modules/styling.nix | 231 -- modules/terminal.nix | 121 +- modules/theme.nix | 20 - modules/treefmt-build-dep.nix | 35 + runner_debug.sh | 5 +- scripts/regenerate-hardware.sh | 19 +- scripts/sops-updatekeys.sh | 58 + scripts/wawona-vm-bridge.sh | 12 +- secrets/secrets.yaml | 18 +- subrepos/microvm.nix/.github/FUNDING.yml | 2 +- .../.github/workflows/prebuilt-stable.yml | 26 +- .../.github/workflows/prebuilt-unstable.yml | 26 +- subrepos/microvm.nix/CHANGELOG.md | 79 +- subrepos/microvm.nix/README.md | 30 +- subrepos/microvm.nix/checks/default.nix | 512 +++-- .../checks/imperative-template.nix | 72 +- subrepos/microvm.nix/checks/iperf.nix | 152 +- subrepos/microvm.nix/checks/machined.nix | 20 +- subrepos/microvm.nix/checks/shellcheck.nix | 28 +- .../microvm.nix/checks/shutdown-command.nix | 67 +- .../microvm.nix/checks/startup-shutdown.nix | 166 +- subrepos/microvm.nix/checks/vm.nix | 59 +- .../microvm.nix/doc/src/advanced-network.md | 3 +- subrepos/microvm.nix/doc/src/conventions.md | 4 +- subrepos/microvm.nix/doc/src/declarative.md | 2 +- subrepos/microvm.nix/doc/src/interfaces.md | 5 +- subrepos/microvm.nix/doc/src/intro.md | 6 +- .../microvm.nix/doc/src/microvm-command.md | 2 +- subrepos/microvm.nix/doc/src/options.md | 5 +- .../microvm.nix/doc/src/output-options.md | 4 +- subrepos/microvm.nix/doc/src/packages.md | 2 + .../microvm.nix/doc/src/routed-network.md | 2 +- subrepos/microvm.nix/doc/src/shares.md | 1 - .../microvm.nix/doc/src/simple-network.md | 1 + subrepos/microvm.nix/doc/src/ssh-deploy.md | 2 +- subrepos/microvm.nix/doc/src/vfkit-rosetta.md | 2 +- subrepos/microvm.nix/examples/graphics.nix | 122 +- .../microvm.nix/examples/microvms-host.nix | 69 +- .../microvm.nix/examples/no-flake-microvm.nix | 69 +- subrepos/microvm.nix/examples/qemu-vnc.nix | 118 +- subrepos/microvm.nix/flake-template/flake.nix | 42 +- subrepos/microvm.nix/flake.nix | 317 +-- subrepos/microvm.nix/lib/default.nix | 57 +- subrepos/microvm.nix/lib/macvtap.nix | 66 +- subrepos/microvm.nix/lib/runner.nix | 162 +- subrepos/microvm.nix/lib/runners/alioth.nix | 184 +- .../lib/runners/cloud-hypervisor.nix | 447 ++-- subrepos/microvm.nix/lib/runners/crosvm.nix | 313 +-- .../microvm.nix/lib/runners/firecracker.nix | 196 +- subrepos/microvm.nix/lib/runners/kvmtool.nix | 209 +- subrepos/microvm.nix/lib/runners/qemu.nix | 895 ++++---- .../microvm.nix/lib/runners/stratovirt.nix | 402 ++-- subrepos/microvm.nix/lib/runners/vfkit.nix | 160 +- subrepos/microvm.nix/lib/volumes.nix | 100 +- subrepos/microvm.nix/nixos-modules/host.nix | 15 +- .../nixos-modules/host/default.nix | 488 +++-- .../nixos-modules/host/options.nix | 289 +-- .../nixos-modules/microvm/asserts.nix | 192 +- .../nixos-modules/microvm/boot-disk.nix | 84 +- .../nixos-modules/microvm/default.nix | 10 +- .../nixos-modules/microvm/graphics.nix | 28 +- .../nixos-modules/microvm/interfaces.nix | 99 +- .../nixos-modules/microvm/mounts.nix | 273 ++- .../nixos-modules/microvm/optimization.nix | 20 +- .../nixos-modules/microvm/options.nix | 727 ++++--- .../nixos-modules/microvm/pci-devices.nix | 64 +- .../nixos-modules/microvm/rosetta.nix | 7 +- .../nixos-modules/microvm/ssh-deploy.nix | 82 +- .../nixos-modules/microvm/store-disk.nix | 99 +- .../nixos-modules/microvm/system.nix | 78 +- .../microvm/virtiofsd/default.nix | 104 +- .../nixos-modules/microvm/vsock-ssh.nix | 17 +- subrepos/microvm.nix/pkgs/build-microvm.nix | 20 +- subrepos/microvm.nix/pkgs/doc.nix | 106 +- subrepos/microvm.nix/pkgs/microvm-command.nix | 26 +- test-eval.nix | 2 +- test.nix | 31 - theme-selection.nix | 12 + treefmt.toml | 60 + 157 files changed, 18924 insertions(+), 7545 deletions(-) create mode 100644 .sops.yaml create mode 100644 den-aspects/styling.nix create mode 100644 docs/den.md create mode 100644 docs/dendritic-nix/01-foundations.md create mode 100644 docs/dendritic-nix/02-module-mechanics.md create mode 100644 docs/dendritic-nix/03-repo-implementation.md create mode 100644 docs/dendritic-nix/04-real-examples.md create mode 100644 docs/dendritic-nix/05-migration-playbook.md create mode 100644 docs/dendritic-nix/06-anti-patterns.md create mode 100644 docs/dendritic-nix/07-review-checklist.md create mode 100644 docs/dendritic-nix/README.md create mode 100644 docs/dendritic-patterns.md create mode 100644 docs/sops-nix.md create mode 100644 docs/sops-nix/01-architecture.md create mode 100644 docs/sops-nix/02-key-management.md create mode 100644 docs/sops-nix/03-authoring-and-files.md create mode 100644 docs/sops-nix/04-consumption-patterns.md create mode 100644 docs/sops-nix/05-templates-and-services.md create mode 100644 docs/sops-nix/06-operations-and-rotation.md create mode 100644 docs/sops-nix/07-troubleshooting.md create mode 100644 docs/sops-nix/08-repo-reference.md create mode 100644 docs/sops-nix/README.md delete mode 100644 hosts/system-manager/linux-generic/default.nix create mode 100644 modules/apps/brave.nix create mode 100644 modules/apps/chatgpt-cli.nix delete mode 100644 modules/apps/common.nix delete mode 100644 modules/apps/eclipse-java-google-style.xml create mode 100644 modules/apps/fastfetch.nix create mode 100644 modules/apps/firefox.nix create mode 100644 modules/apps/gh.nix create mode 100644 modules/apps/ghidra.nix create mode 100644 modules/apps/java.nix create mode 100644 modules/apps/safari.nix delete mode 100644 modules/apps/sway.nix create mode 100644 modules/configurations.nix create mode 100644 modules/darwin-appearance-sync.nix create mode 100644 modules/host-topology-den.nix rename modules/{shell.nix => shell-config.nix} (79%) create mode 100644 modules/sops-validation.nix delete mode 100644 modules/styling.nix delete mode 100644 modules/theme.nix create mode 100644 modules/treefmt-build-dep.nix create mode 100755 scripts/sops-updatekeys.sh delete mode 100644 test.nix create mode 100644 theme-selection.nix create mode 100644 treefmt.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c4b38ba..5fe4a525 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,6 @@ jobs: - name: "8amps-linux (Home Manager)" os: ubuntu-latest attr: "homeConfigurations.8amps-linux.activationPackage" - - name: "linux-generic (System Manager)" - os: ubuntu-latest - attr: "systemConfigs.linux-generic.config.build.toplevel" steps: - name: Checkout repository diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 00000000..d290bea6 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,20 @@ +# sops creation/decryption rules. +# +# `keys` defines the canonical age recipients. To rotate or add a new +# machine, add its age recipient here, then `sops updatekeys ` on +# every encrypted file so its data key gets re-wrapped. +# +# `creation_rules` controls what recipients sops uses when ENCRYPTING new +# files matching `path_regex`. Each matched file gets its data key +# encrypted to every listed recipient. +# +# The age private key is *derived at runtime* from the user's SSH ed25519 +# key via sops-nix's `sops.age.sshKeyPaths`, so there's no separate age +# private key to back up or rotate. +keys: + - &user_8amps age1alw70v2xd80yrkn2ukap264c64fa64qjq7rr4uh07amu8ahm9uzs9z485w +creation_rules: + # General secrets file (openai API key etc.). + - path_regex: secrets/secrets\.yaml$ + age: + - *user_8amps diff --git a/README.md b/README.md index 0d67cbc3..21181626 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Personal, declarative system configuration built by **Alex Spaulding (aspaulding Following the [dendritic pattern](https://github.com/mightyiam/dendritic), this repository should be cloned to the system configuration directory for your platform. The config is system-wide and shared across all users — editing requires admin privileges. -| Platform | Path | -|---|---| -| **NixOS** | `/etc/nixos/` | +| Platform | Path | +| ---------------------- | ---------------------------- | +| **NixOS** | `/etc/nixos/` | | **macOS (nix-darwin)** | `/etc/nix-darwin/.dotfiles/` | ```bash @@ -29,7 +29,7 @@ The install script will automatically clone the repository to `/etc/nix-darwin/. nix run github:aspauldingcode/.dotfiles#install ``` -*(Alternatively, if you have `git` installed and prefer to authenticate over SSH, you can run `nix run git+ssh://git@github.com/aspauldingcode/.dotfiles.git#install`)* +_(Alternatively, if you have `git` installed and prefer to authenticate over SSH, you can run `nix run git+ssh://git@github.com/aspauldingcode/.dotfiles.git#install`)_ ## Daily Usage @@ -49,12 +49,17 @@ nh os switch /etc/nixos -H my-nixos-host ``` ### Pro Tips for `nh` + - Use **`--ask`** to see a diff of what will change before confirming. - Use **`--update`** to update all your flake inputs (packages) to their latest versions. - The `NH_FLAKE` variable is set to `/etc/nix-darwin/.dotfiles#mba` (macOS) or `/etc/nixos` (NixOS), so `nh` always knows which host to build by default. ## Documentation +- **[Dendritic Nix Documentation Suite](docs/dendritic-nix/README.md)** — Full multi-file deep dive: foundations, mechanics, repo implementation, real examples, migration, and anti-patterns. +- **[Dendritic Nix: Patterns, Den, and Dendrix](docs/dendritic-patterns.md)** — Single-file overview of the pattern and ecosystem. +- **[Den — Deep Reference](docs/den.md)** — Detailed documentation on Den: aspects, hosts, policies, classes, pipeline, and how this flake uses them. +- **[sops-nix Documentation Suite](docs/sops-nix/README.md)** — Full multi-file reference for sops-nix: architecture, key management, authoring, templates, operations, and troubleshooting. - **[Zsh Plugins & Shell Extensions](docs/zsh-plugins.md)** — Full reference of all curated zsh plugins, CLI tools, and Nix-specific integrations. - **[Tmux Master Guide](docs/tmux.md)** — Learn how to use your optimized terminal multiplexer with interactive hints. diff --git a/den-aspects/styling.nix b/den-aspects/styling.nix new file mode 100644 index 00000000..cee644c5 --- /dev/null +++ b/den-aspects/styling.nix @@ -0,0 +1,1454 @@ +{ inputs, den, ... }: +let + selection = import ../theme-selection.nix; + selectedName = selection.name or "stylix"; + selectedLight = selection.schemes.light or null; + selectedDark = selection.schemes.dark or null; + + # Theme option declarations shared across all three module classes + # (NixOS, nix-darwin, home-manager). Inlined here so theme options + # always come along with styling regardless of class. + themeOptionsModule = + { lib, ... }: + { + options.dendritic.theme.name = lib.mkOption { + type = lib.types.str; + default = selectedName; + description = "Human-readable theme name used by generated browser themes."; + }; + + options.dendritic.theme.variant = lib.mkOption { + type = lib.types.enum [ + "dark" + "light" + ]; + default = "dark"; + description = "Global UI variant used to select Stylix palette."; + }; + + options.dendritic.theme.schemes.light = lib.mkOption { + type = lib.types.str; + default = + if selectedLight != null then selectedLight else throw "theme-selection.nix must set schemes.light"; + description = "Base16 scheme basename for light mode (without .yaml)."; + }; + + options.dendritic.theme.schemes.dark = lib.mkOption { + type = lib.types.str; + default = + if selectedDark != null then selectedDark else throw "theme-selection.nix must set schemes.dark"; + description = "Base16 scheme basename for dark mode (without .yaml)."; + }; + }; + + # ── Home Manager styling module ─────────────────────────────────────── + # Defined ONCE as a let-bound deferred module. Used in two places: + # 1. `den.aspects.styling.homeManager` — picked up by future + # `den.homes.*` consumers via aspect resolution. + # 2. `flake.modules.homeManager.dendritic` — picked up by current + # embedded HM users (the `home-manager.users.` blocks inside + # mba, mba-dark, mba-asahi, nixos-test, microvm host files). + # Both surfaces hold the same deferredModule, so there's no duplication + # and no bridge indirection. + stylingHmModule = + { + pkgs, + lib, + config, + ... + }: + let + isDarwin = pkgs.stdenv.hostPlatform.isDarwin; + desiredVariant = config.dendritic.theme.variant; + schemeName = + if desiredVariant == "light" then + config.dendritic.theme.schemes.light + else + config.dendritic.theme.schemes.dark; + stylixScheme = "${pkgs.base16-schemes}/share/themes/${schemeName}.yaml"; + colors = config.lib.stylix.colors.withHashtag; + paletteVariant = lib.attrByPath [ "stylix" "polarity" ] "dark" config; + in + { + imports = [ themeOptionsModule ]; + + config = { + stylix = { + enable = true; + polarity = desiredVariant; + base16Scheme = lib.mkForce stylixScheme; + override = { + slug = "stylix"; + scheme = "stylix"; + }; + image = lib.mkDefault ../wallpapers/mountain-sunset.png; + + targets.vscode.enable = true; + targets.ghostty.enable = true; + targets.neovim.enable = false; + targets.neovide.enable = false; + targets.nixvim.enable = true; + targets.spicetify.enable = lib.mkForce true; + targets.qt.enable = false; + targets.firefox.profileNames = [ + "default" + "default-release" + ]; + }; + + home.file."colors.toml".text = '' + [stylix] + variant = "${paletteVariant}" + + [palette] + base00 = "${colors.base00}" + base01 = "${colors.base01}" + base02 = "${colors.base02}" + base03 = "${colors.base03}" + base04 = "${colors.base04}" + base05 = "${colors.base05}" + base06 = "${colors.base06}" + base07 = "${colors.base07}" + base08 = "${colors.base08}" + base09 = "${colors.base09}" + base0A = "${colors.base0A}" + base0B = "${colors.base0B}" + base0C = "${colors.base0C}" + base0D = "${colors.base0D}" + base0E = "${colors.base0E}" + base0F = "${colors.base0F}" + ''; + + # ── Stylix-themed Firefox UI ────────────────────────────────── + programs.firefox.profiles = + let + c = config.lib.stylix.colors.withHashtag; + + commonCss = '' + :root { + --base00: ${c.base00}; --base01: ${c.base01}; --base02: ${c.base02}; --base03: ${c.base03}; + --base04: ${c.base04}; --base05: ${c.base05}; --base06: ${c.base06}; --base07: ${c.base07}; + --base08: ${c.base08}; --base09: ${c.base09}; --base0A: ${c.base0A}; --base0B: ${c.base0B}; + --base0C: ${c.base0C}; --base0D: ${c.base0D}; --base0E: ${c.base0E}; --base0F: ${c.base0F}; + } + ''; + + stylixUserChrome = commonCss + '' + /* Aggressive UI Overrides */ + :root { + --lwt-accent-color: var(--base00) !important; + --lwt-text-color: var(--base05) !important; + --toolbar-bgcolor: var(--base00) !important; + --toolbar-color: var(--base05) !important; + --toolbar-field-background-color: var(--base01) !important; + --toolbar-field-color: var(--base05) !important; + --toolbar-field-border-color: var(--base03) !important; + --toolbar-field-focus-background-color: var(--base01) !important; + --toolbar-field-focus-color: var(--base05) !important; + --toolbar-field-focus-border-color: var(--base0D) !important; + --lwt-selected-tab-background-color: var(--base02) !important; + --lwt-tab-text-color: var(--base05) !important; + --lwt-background-tab-text-color: var(--base04) !important; + --lwt-tab-line-color: var(--base0D) !important; + --tab-line-color: var(--base0D) !important; + --toolbar-field-focus-background-color: var(--base02) !important; + --toolbar-field-focus-border-color: var(--base0D) !important; + --arrowpanel-background: var(--base01) !important; + --arrowpanel-color: var(--base05) !important; + --arrowpanel-border-color: var(--base03) !important; + --panel-background: var(--base01) !important; + --panel-color: var(--base05) !important; + --panel-border-color: var(--base03) !important; + --panel-separator-color: var(--base03) !important; + --panel-item-hover-bgcolor: var(--base02) !important; + --panel-item-active-bgcolor: var(--base03) !important; + --button-bgcolor: var(--base02) !important; + --button-hover-bgcolor: var(--base03) !important; + --button-active-bgcolor: var(--base03) !important; + --button-color: var(--base05) !important; + } + + #nav-bar, #TabsToolbar, #PersonalToolbar, #navigator-toolbox, #sidebar-box, #sidebar-header { + background-color: var(--base00) !important; + background-image: none !important; + color: var(--base05) !important; + border: none !important; + box-shadow: none !important; + } + + .toolbarbutton-icon { + fill: var(--base05) !important; + fill-opacity: 1 !important; + } + .toolbarbutton-1:not([disabled]):hover .toolbarbutton-icon, + .toolbarbutton-1:not([disabled])[open] .toolbarbutton-icon { + fill: var(--base0D) !important; + } + + .tabbrowser-tab[selected="true"] .tab-background, + .tab-background[selected="true"] { + background-color: var(--base02) !important; + background-image: none !important; + border-color: var(--base03) !important; + } + + .tabbrowser-tab[selected="true"] .tab-label { + color: var(--base06) !important; + } + + .tabbrowser-tab:not([selected="true"]) .tab-background { + background-color: var(--base01) !important; + } + + .tab-line[selected="true"] { + background-color: var(--base0D) !important; + } + + .tab-throbber, + .tab-throbber[busy], + .tab-throbber[progress], + .tabbrowser-tab[busy] .tab-icon-image, + .tabbrowser-tab[progress] .tab-icon-image { + fill: var(--base0D) !important; + color: var(--base0D) !important; + } + + .tab-background[selected] { + outline: none !important; + border: 2px solid transparent !important; + box-shadow: none !important; + background-clip: padding-box !important; + } + .tab-background[selected] > .tab-context-line { + margin: -5px 0 0 !important; + } + .tabbrowser-tab[selected] > .tab-stack::before { + content: ""; + display: flex; + min-height: inherit; + border-radius: var(--tab-border-radius); + grid-area: 1 / 1; + margin-block: var(--tab-block-margin); + background-color: var(--base07) !important; + } + + #tabs-newtab-button .toolbarbutton-icon, + #new-tab-button .toolbarbutton-icon { + fill: var(--base05) !important; + } + #tabs-newtab-button, + #new-tab-button { + background-color: transparent !important; + border: none !important; + } + #tabs-newtab-button > .toolbarbutton-badge-stack, + #new-tab-button > .toolbarbutton-badge-stack { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + } + #tabs-newtab-button:hover > .toolbarbutton-badge-stack, + #new-tab-button:hover > .toolbarbutton-badge-stack { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + } + + #urlbar-background, + #searchbar, + .searchbar-textbox { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + color: var(--base05) !important; + } + + #urlbar { + --urlbar-box-bgcolor: var(--base01) !important; + --urlbar-box-hover-bgcolor: var(--base02) !important; + --urlbar-box-active-bgcolor: var(--base02) !important; + --urlbar-box-focus-bgcolor: var(--base02) !important; + } + + #urlbar:not([focused="true"]) #urlbar-background { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + } + + #urlbar[focused="true"] #urlbar-background { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + box-shadow: 0 0 3px var(--base0D) !important; + } + + #urlbar-input, + #searchbar input { + color: var(--base06) !important; + } + + #identity-box.extensionPage #identity-icon-box, + #identity-box.extensionPage #identity-permission-box, + #identity-box.extensionPage #identity-icon-labels { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + color: var(--base05) !important; + } + + #identity-box.chromeUI #identity-icon-box, + #identity-box.chromeUI #identity-permission-box, + #identity-box.chromeUI #identity-icon-labels, + #identity-box.localResource #identity-icon-box, + #identity-box.localResource #identity-permission-box, + #identity-box.localResource #identity-icon-labels { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + color: var(--base05) !important; + } + + #identity-icon-box, + #identity-permission-box, + #identity-icon-labels { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + color: var(--base05) !important; + } + + #urlbar[focused="true"] #identity-box.extensionPage #identity-icon-box, + #urlbar[focused="true"] #identity-box.extensionPage #identity-permission-box, + #urlbar[focused="true"] #identity-box.extensionPage #identity-icon-labels { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + + #urlbar[focused="true"] #identity-box.chromeUI #identity-icon-box, + #urlbar[focused="true"] #identity-box.chromeUI #identity-permission-box, + #urlbar[focused="true"] #identity-box.chromeUI #identity-icon-labels, + #urlbar[focused="true"] #identity-box.localResource #identity-icon-box, + #urlbar[focused="true"] #identity-box.localResource #identity-permission-box, + #urlbar[focused="true"] #identity-box.localResource #identity-icon-labels { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + + #identity-box.extensionPage #identity-icon-label { + color: var(--base05) !important; + } + + #identity-box.chromeUI #identity-icon-label, + #identity-box.localResource #identity-icon-label { + color: var(--base05) !important; + } + + button, + toolbarbutton, + .toolbarbutton-1, + .button-background, + .dialog-button, + .popup-notification-button, + moz-button::part(button) { + --button-bgcolor: var(--base02) !important; + --button-hover-bgcolor: var(--base03) !important; + --button-active-bgcolor: var(--base03) !important; + --button-color: var(--base05) !important; + appearance: none !important; + -moz-appearance: none !important; + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + box-shadow: none !important; + } + + button:hover, + toolbarbutton:hover, + .toolbarbutton-1:hover, + .button-background:hover, + .dialog-button:hover, + .popup-notification-button:hover, + moz-button:hover::part(button) { + background-color: var(--base03) !important; + color: var(--base06) !important; + border-color: var(--base03) !important; + } + + button[default], + button.primary, + button[dlgtype="accept"], + .popup-notification-primary-button, + .dialog-button[default], + moz-button[default]::part(button), + moz-button[variant="primary"]::part(button), + moz-button.popup-notification-primary-button::part(button) { + background-color: var(--base0D) !important; + color: var(--base00) !important; + border-color: var(--base0D) !important; + } + + button[default]:hover, + button.primary:hover, + button[dlgtype="accept"]:hover, + .popup-notification-primary-button:hover, + .dialog-button[default]:hover, + moz-button[default]:hover::part(button), + moz-button[variant="primary"]:hover::part(button), + moz-button.popup-notification-primary-button:hover::part(button) { + background-color: var(--base0C) !important; + color: var(--base00) !important; + border-color: var(--base0C) !important; + } + + #nav-bar toolbarbutton, + #TabsToolbar toolbarbutton, + #PersonalToolbar toolbarbutton { + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + } + #nav-bar toolbarbutton > .toolbarbutton-badge-stack, + #nav-bar toolbarbutton > .toolbarbutton-text, + #TabsToolbar toolbarbutton > .toolbarbutton-badge-stack, + #TabsToolbar toolbarbutton > .toolbarbutton-text, + #PersonalToolbar toolbarbutton > .toolbarbutton-badge-stack, + #PersonalToolbar toolbarbutton > .toolbarbutton-text { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + color: var(--base05) !important; + } + #nav-bar toolbarbutton:hover > .toolbarbutton-badge-stack, + #nav-bar toolbarbutton:hover > .toolbarbutton-text, + #TabsToolbar toolbarbutton:hover > .toolbarbutton-badge-stack, + #TabsToolbar toolbarbutton:hover > .toolbarbutton-text, + #PersonalToolbar toolbarbutton:hover > .toolbarbutton-badge-stack, + #PersonalToolbar toolbarbutton:hover > .toolbarbutton-text { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + + #sidebar-button > .toolbarbutton-badge-stack, + #alltabs-button > .toolbarbutton-badge-stack, + #tabs-alltabs-button > .toolbarbutton-badge-stack, + #PanelUI-button > .toolbarbutton-badge-stack, + #unified-extensions-button > .toolbarbutton-badge-stack, + #fxa-toolbar-menu-button > .toolbarbutton-badge-stack, + #firefox-view-button > .toolbarbutton-badge-stack, + #history-panelmenu > .toolbarbutton-badge-stack, + #reload-button > .toolbarbutton-badge-stack, + #stop-button > .toolbarbutton-badge-stack, + #back-button > .toolbarbutton-badge-stack, + #forward-button > .toolbarbutton-badge-stack { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + } + #sidebar-button:hover > .toolbarbutton-badge-stack, + #alltabs-button:hover > .toolbarbutton-badge-stack, + #tabs-alltabs-button:hover > .toolbarbutton-badge-stack, + #PanelUI-button:hover > .toolbarbutton-badge-stack, + #unified-extensions-button:hover > .toolbarbutton-badge-stack, + #fxa-toolbar-menu-button:hover > .toolbarbutton-badge-stack, + #firefox-view-button:hover > .toolbarbutton-badge-stack, + #history-panelmenu:hover > .toolbarbutton-badge-stack, + #reload-button:hover > .toolbarbutton-badge-stack, + #stop-button:hover > .toolbarbutton-badge-stack, + #back-button:hover > .toolbarbutton-badge-stack, + #forward-button:hover > .toolbarbutton-badge-stack { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + } + #sidebar-button .toolbarbutton-icon, + #alltabs-button .toolbarbutton-icon, + #tabs-alltabs-button .toolbarbutton-icon, + #unified-extensions-button .toolbarbutton-icon, + #PanelUI-button .toolbarbutton-icon, + #fxa-toolbar-menu-button .toolbarbutton-icon, + #firefox-view-button .toolbarbutton-icon, + #history-panelmenu .toolbarbutton-icon, + #reload-button .toolbarbutton-icon, + #stop-button .toolbarbutton-icon, + #back-button .toolbarbutton-icon, + #forward-button .toolbarbutton-icon { + fill: var(--base05) !important; + } + #sidebar-button:hover .toolbarbutton-icon, + #alltabs-button:hover .toolbarbutton-icon, + #tabs-alltabs-button:hover .toolbarbutton-icon, + #unified-extensions-button:hover .toolbarbutton-icon, + #PanelUI-button:hover .toolbarbutton-icon, + #fxa-toolbar-menu-button:hover .toolbarbutton-icon, + #firefox-view-button:hover .toolbarbutton-icon, + #history-panelmenu:hover .toolbarbutton-icon, + #reload-button:hover .toolbarbutton-icon, + #stop-button:hover .toolbarbutton-icon, + #back-button:hover .toolbarbutton-icon, + #forward-button:hover .toolbarbutton-icon { + fill: var(--base0D) !important; + } + + #fxa-toolbar-menu-button, + #fxa-toolbar-menu-button .toolbarbutton-icon, + #fxa-toolbar-menu-button image, + #fxa-toolbar-menu-button .toolbarbutton-badge-stack, + #fxa-toolbar-menu-button .toolbarbutton-badge-stack > image, + #fxa-avatar-image, + #fxa-avatar-image image, + #fxa-avatar-image > image { + -moz-context-properties: fill, stroke, fill-opacity, stroke-opacity !important; + fill: var(--base05) !important; + stroke: var(--base05) !important; + color: var(--base05) !important; + } + #fxa-toolbar-menu-button:hover .toolbarbutton-icon, + #fxa-toolbar-menu-button:hover image, + #fxa-toolbar-menu-button:hover .toolbarbutton-badge-stack > image, + #fxa-toolbar-menu-button:hover #fxa-avatar-image, + #fxa-toolbar-menu-button:hover #fxa-avatar-image image { + fill: var(--base0D) !important; + stroke: var(--base0D) !important; + color: var(--base0D) !important; + } + + #sidebar-main, + #sidebar-box, + #sidebar-header { + background-color: var(--base00) !important; + } + #sidebar-switcher-target, + #viewButton, + #sidebar-close { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + color: var(--base05) !important; + } + #sidebar-switcher-target:hover, + #viewButton:hover, + #sidebar-close:hover { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + #sidebar-placesTree treechildren::-moz-tree-row { + background-color: var(--base00) !important; + } + #sidebar-placesTree treechildren::-moz-tree-row(hover) { + background-color: var(--base01) !important; + } + #sidebar-placesTree treechildren::-moz-tree-row(selected) { + background-color: var(--base02) !important; + } + + #sidebar-main toolbarbutton, + #sidebar-main button, + #sidebar-main [role="button"], + #sidebar-main .subviewbutton, + #sidebar-main [id*="sidebar"][id*="button"], + #sidebar-main [class*="sidebar"][class*="button"] { + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + color: var(--base05) !important; + } + #sidebar-main toolbarbutton > .toolbarbutton-badge-stack, + #sidebar-main toolbarbutton > .toolbarbutton-text, + #sidebar-main .button-background, + #sidebar-main .subviewbutton, + #sidebar-main #sidebar-customize-button, + #sidebar-main #sidebar-button-bookmarks, + #sidebar-main #sidebar-button-history, + #sidebar-main #sidebar-button-syncedtabs, + #sidebar-main #sidebar-button-chat { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + color: var(--base05) !important; + } + #sidebar-main toolbarbutton:hover > .toolbarbutton-badge-stack, + #sidebar-main toolbarbutton:hover > .toolbarbutton-text, + #sidebar-main .button-background:hover, + #sidebar-main .subviewbutton:hover, + #sidebar-main #sidebar-customize-button:hover, + #sidebar-main #sidebar-button-bookmarks:hover, + #sidebar-main #sidebar-button-history:hover, + #sidebar-main #sidebar-button-syncedtabs:hover, + #sidebar-main #sidebar-button-chat:hover { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + #sidebar-main .toolbarbutton-icon, + #sidebar-main .subviewbutton-icon, + #sidebar-main image { + -moz-context-properties: fill, stroke !important; + fill: var(--base05) !important; + stroke: var(--base05) !important; + } + #sidebar-main toolbarbutton:hover .toolbarbutton-icon, + #sidebar-main .subviewbutton:hover .subviewbutton-icon, + #sidebar-main button:hover image { + fill: var(--base0D) !important; + stroke: var(--base0D) !important; + } + + #tab-modal-prompt-box, + .tabmodalprompt-mainContainer, + .tabmodalprompt-infoBody, + .tabmodalprompt-buttonContainer, + #commonDialog, + #commonDialog > dialog, + #commonDialog .dialogFrame, + #commonDialog .dialogOverlay, + #commonDialog .contentPromptDialog { + background-color: var(--base01) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .tabmodalprompt-mainContainer button, + .tabmodalprompt-mainContainer .dialog-button, + .tabmodalprompt-buttonContainer > button, + .tabmodalprompt-buttonContainer > .dialog-button, + .tabmodalprompt-buttonContainer button[default], + .tabmodalprompt-buttonContainer button[dlgtype="accept"], + .tabmodalprompt-buttonContainer button[dlgtype="cancel"], + #tabmodalprompt-button0, + #tabmodalprompt-button1, + #commonDialog button, + #commonDialog .dialog-button, + #commonDialog .dialog-button-box .dialog-button, + #commonDialog button[default], + #commonDialog button[dlgtype="accept"], + #commonDialog button[dlgtype="cancel"] { + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .tabmodalprompt-mainContainer button:hover, + .tabmodalprompt-mainContainer .dialog-button:hover, + .tabmodalprompt-buttonContainer > button:hover, + .tabmodalprompt-buttonContainer > .dialog-button:hover, + #commonDialog button:hover, + #commonDialog .dialog-button:hover { + background-color: var(--base03) !important; + color: var(--base06) !important; + } + + .tabmodalprompt-mainContainer button[default], + .tabmodalprompt-mainContainer .dialog-button[default], + .tabmodalprompt-buttonContainer button[default], + .tabmodalprompt-buttonContainer button[dlgtype="accept"], + #commonDialog button[default], + #commonDialog .dialog-button[default], + #commonDialog button[dlgtype="accept"] { + background-color: var(--base0D) !important; + color: var(--base00) !important; + border-color: var(--base0D) !important; + } + + .tabmodalprompt-mainContainer button[default]:hover, + .tabmodalprompt-mainContainer .dialog-button[default]:hover, + .tabmodalprompt-buttonContainer button[default]:hover, + .tabmodalprompt-buttonContainer button[dlgtype="accept"]:hover, + #commonDialog button[default]:hover, + #commonDialog .dialog-button[default]:hover, + #commonDialog button[dlgtype="accept"]:hover { + background-color: var(--base0C) !important; + color: var(--base00) !important; + border-color: var(--base0C) !important; + } + + #notification-popup, + #notification-popup popupnotification, + #notification-popup popupnotificationcontent, + .popup-notification-panel, + .popup-notification-body-container, + .popup-notification-content, + .popup-notification-description { + background-color: var(--base01) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + #notification-popup popupnotification { + --panel-background: var(--base01) !important; + --panel-color: var(--base05) !important; + --panel-border-color: var(--base03) !important; + --button-bgcolor: var(--base02) !important; + --button-hover-bgcolor: var(--base03) !important; + --button-active-bgcolor: var(--base03) !important; + --button-color: var(--base05) !important; + --button-primary-bgcolor: var(--base0D) !important; + --button-primary-hover-bgcolor: var(--base0C) !important; + --button-primary-active-bgcolor: var(--base0C) !important; + --button-primary-color: var(--base00) !important; + --checkbox-unchecked-bgcolor: var(--base01) !important; + --checkbox-unchecked-hover-bgcolor: var(--base02) !important; + --checkbox-checked-bgcolor: var(--base0D) !important; + --checkbox-checked-color: var(--base00) !important; + --checkbox-border-color: var(--base03) !important; + } + + #notification-popup .popup-notification-primary-button, + #notification-popup moz-button.popup-notification-primary-button, + #notification-popup .popup-notification-primary-button::part(button), + #notification-popup .popup-notification-secondary-button, + #notification-popup moz-button.popup-notification-secondary-button, + #notification-popup .popup-notification-secondary-button::part(button), + #notification-popup button, + #notification-popup moz-button::part(button), + #notification-popup .popup-notification-button { + appearance: none !important; + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + box-shadow: none !important; + } + + #notification-popup .popup-notification-primary-button, + #notification-popup moz-button.popup-notification-primary-button, + #notification-popup .popup-notification-primary-button::part(button) { + appearance: none !important; + background-color: var(--base0D) !important; + color: var(--base00) !important; + border-color: var(--base0D) !important; + } + + #notification-popup .popup-notification-primary-button:hover, + #notification-popup moz-button.popup-notification-primary-button:hover, + #notification-popup moz-button.popup-notification-primary-button:hover::part(button), + #notification-popup .popup-notification-primary-button::part(button):hover, + #notification-popup .popup-notification-secondary-button:hover, + #notification-popup moz-button.popup-notification-secondary-button:hover, + #notification-popup moz-button.popup-notification-secondary-button:hover::part(button), + #notification-popup .popup-notification-secondary-button::part(button):hover, + #notification-popup button:hover, + #notification-popup moz-button:hover::part(button), + #notification-popup .popup-notification-button:hover { + background-color: var(--base03) !important; + color: var(--base06) !important; + } + + #notification-popup .popup-notification-primary-button:hover, + #notification-popup moz-button.popup-notification-primary-button:hover, + #notification-popup moz-button.popup-notification-primary-button:hover::part(button), + #notification-popup .popup-notification-primary-button::part(button):hover { + background-color: var(--base0C) !important; + color: var(--base00) !important; + border-color: var(--base0C) !important; + } + + #notification-popup .popup-notification-checkbox > .checkbox-check, + #notification-popup .popup-notification-checkbox .checkbox-check, + #notification-popup .popup-notification-checkbox::part(checkbox), + #notification-popup checkbox.popup-notification-checkbox > .checkbox-check { + appearance: none !important; + background-color: var(--base01) !important; + border-color: var(--base03) !important; + color: var(--base05) !important; + } + + #notification-popup .popup-notification-checkbox[checked="true"] > .checkbox-check, + #notification-popup .popup-notification-checkbox[checked] > .checkbox-check, + #notification-popup checkbox.popup-notification-checkbox[checked="true"] > .checkbox-check { + background-color: var(--base0D) !important; + border-color: var(--base0D) !important; + color: var(--base00) !important; + } + + #notification-popup .popup-notification-checkbox .checkbox-icon, + #notification-popup .popup-notification-checkbox > .checkbox-check::before { + -moz-context-properties: fill, stroke !important; + fill: var(--base00) !important; + stroke: var(--base00) !important; + } + + #notification-popup .popup-notification-checkbox > .checkbox-label-box > .checkbox-label { + color: var(--base05) !important; + opacity: 1 !important; + } + + #appMenu-popup { + --arrowpanel-background: var(--base00) !important; + --arrowpanel-color: var(--base05) !important; + --arrowpanel-border-color: var(--base03) !important; + } + + #customization-container { + background-color: var(--base00) !important; + } + #customization-done-button { + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + #customization-done-button:hover { + background-color: var(--base03) !important; + border-color: var(--base0D) !important; + } + #customization-done-button:active { + background-color: var(--base04) !important; + } + + .urlbarView-row { + color: var(--base05) !important; + } + .urlbarView { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + .urlbarView-row[selected] { + background-color: var(--base02) !important; + color: var(--base06) !important; + } + .urlbarView-type-icon { + fill: var(--base05) !important; + } + .urlbarView-action { + color: var(--base04) !important; + } + + #unified-extensions-view, + .panel-arrowcontent { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + .unified-extensions-item { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + .unified-extensions-item:hover { + background-color: var(--base01) !important; + } + #unified-extensions-manage-extensions { + background-color: var(--base01) !important; + color: var(--base05) !important; + } + #unified-extensions-manage-extensions:hover { + background-color: var(--base02) !important; + } + + .webextension-browser-action { + filter: grayscale(100%) brightness(1.2) contrast(1.2) opacity(0.7) + drop-shadow(0 0 0 var(--base05)) !important; + } + toolbarbutton.webextension-browser-action .toolbarbutton-icon, + #unified-extensions-button .toolbarbutton-icon, + #unified-extensions-panel .toolbarbutton-icon { + fill: var(--base05) !important; + fill-opacity: 1 !important; + } + toolbarbutton.webextension-browser-action:hover .toolbarbutton-icon, + #unified-extensions-button:hover .toolbarbutton-icon, + #unified-extensions-panel toolbarbutton:hover .toolbarbutton-icon { + fill: var(--base0D) !important; + } + #unified-extensions-panel .unified-extensions-item-action-button, + #unified-extensions-panel toolbarbutton { + color: var(--base05) !important; + background-color: var(--base00) !important; + } + #unified-extensions-panel .unified-extensions-item-action-button:hover, + #unified-extensions-panel toolbarbutton:hover { + color: var(--base06) !important; + background-color: var(--base01) !important; + } + + #sidebar-header, + #sidebar-switcher-target, + #viewButton { + background-color: var(--base01) !important; + border: 1px solid var(--base03) !important; + color: var(--base05) !important; + } + #sidebar-switcher-target:hover, + #viewButton:hover { + background-color: var(--base02) !important; + border-color: var(--base0D) !important; + } + #sidebarMenu-popup { + --arrowpanel-background: var(--base00) !important; + --arrowpanel-color: var(--base05) !important; + --arrowpanel-border-color: var(--base03) !important; + } + #sidebarMenu-popup menuitem:hover { + background-color: var(--base01) !important; + } + + .panel-arrowbox, + .panel-arrow, + .panel-viewstack, + .panel-mainview, + .panel-subview { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + + .message-bar, + .message-bar-content, + .message-bar-button { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + + .checkbox-check { + background-color: transparent !important; + border: 1px solid var(--base04) !important; + } + .checkbox-label { + color: var(--base04) !important; + } + .text-link { + color: var(--base0D) !important; + } + .text-link:hover { + color: var(--base0C) !important; + text-decoration: none !important; + } + + menupopup, panel { + --panel-background: var(--base01) !important; + --panel-color: var(--base05) !important; + --panel-border-color: var(--base03) !important; + } + + menuitem, menu { + appearance: none !important; + color: var(--base05) !important; + } + + menuitem[_moz-menuactive="true"], menu[_moz-menuactive="true"] { + background-color: var(--base02) !important; + color: var(--base0D) !important; + } + ''; + + stylixUserContent = commonCss + '' + @-moz-document url-prefix(about:), url-prefix(chrome:) { + :root { + --in-content-page-background: var(--base00) !important; + --in-content-page-color: var(--base05) !important; + --in-content-box-background: var(--base01) !important; + --in-content-primary-button-background: var(--base0D) !important; + --in-content-primary-button-background-hover: var(--base0C) !important; + --in-content-primary-button-text-color: var(--base00) !important; + --in-content-button-background: var(--base02) !important; + --in-content-button-background-hover: var(--base03) !important; + --in-content-button-text-color: var(--base05) !important; + --in-content-border-color: var(--base03) !important; + --in-content-box-border-color: var(--base03) !important; + --in-content-deemphasized-text: var(--base04) !important; + --in-content-accent-color: var(--base0D) !important; + --in-content-accent-color-active: var(--base0C) !important; + --in-content-table-background: var(--base01) !important; + --in-content-table-border-color: var(--base03) !important; + --in-content-item-hover: var(--base02) !important; + --in-content-item-selected: var(--base02) !important; + } + body { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + .dialogBox, + .dialogOverlay, + .contentPromptDialog { + background-color: var(--base01) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + button, + html|button, + input[type="button"], + input[type="submit"], + input[type="reset"], + .button, + .dialog-button, + .popup-notification-button, + .popup-notification-secondary-button, + .popup-notification-primary-button, + moz-button::part(button) { + appearance: none !important; + -moz-appearance: none !important; + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + box-shadow: none !important; + } + + button:hover, + html|button:hover, + input[type="button"]:hover, + input[type="submit"]:hover, + input[type="reset"]:hover, + .button:hover, + .dialog-button:hover, + .popup-notification-button:hover, + .popup-notification-secondary-button:hover, + moz-button:hover::part(button) { + background-color: var(--base03) !important; + color: var(--base06) !important; + border-color: var(--base03) !important; + } + + button[default], + button.primary, + button[dlgtype="accept"], + .button-primary, + .popup-notification-primary-button, + moz-button[default]::part(button), + moz-button[variant="primary"]::part(button), + moz-button.popup-notification-primary-button::part(button) { + appearance: none !important; + -moz-appearance: none !important; + background-color: var(--base0D) !important; + color: var(--base00) !important; + border-color: var(--base0D) !important; + } + + button[default]:hover, + button.primary:hover, + button[dlgtype="accept"]:hover, + .button-primary:hover, + .popup-notification-primary-button:hover, + moz-button[default]:hover::part(button), + moz-button[variant="primary"]:hover::part(button), + moz-button.popup-notification-primary-button:hover::part(button) { + background-color: var(--base0C) !important; + color: var(--base00) !important; + border-color: var(--base0C) !important; + } + + checkbox, + input[type="checkbox"], + .checkbox-check, + .checkbox-icon { + accent-color: var(--base0D) !important; + border-color: var(--base03) !important; + } + } + + @-moz-document url-prefix(about:preferences) { + :root { + --background-color-canvas: var(--base00) !important; + --background-color-box: var(--base00) !important; + --background-color-box-info: var(--base00) !important; + --background-color-overlay: var(--base00) !important; + --background-color-list-item-hover: var(--base01) !important; + --background-color-list-item-selected: var(--base01) !important; + --border-color: var(--base03) !important; + --border-color-selected: var(--base0D) !important; + --text-color: var(--base05) !important; + --text-color-deemphasized: var(--base04) !important; + --text-color-list-item-hover: var(--base06) !important; + --color-accent-primary: var(--base0D) !important; + --color-accent-primary-hover: var(--base0C) !important; + --color-accent-primary-active: var(--base0C) !important; + --color-accent-primary-selected: var(--base0D) !important; + --text-color-accent-primary: var(--base00) !important; + --text-color-accent-primary-selected: var(--base00) !important; + --background-color-critical: var(--base08) !important; + --text-color-error: var(--base08) !important; + + --in-content-page-background: var(--base00) !important; + --in-content-page-color: var(--base05) !important; + --in-content-box-background: var(--base00) !important; + --in-content-box-border-color: var(--base03) !important; + --in-content-button-background: var(--base02) !important; + --in-content-button-background-hover: var(--base03) !important; + --in-content-button-text-color: var(--base05) !important; + --in-content-primary-button-background: var(--base0D) !important; + --in-content-primary-button-background-hover: var(--base0C) !important; + --in-content-primary-button-text-color: var(--base00) !important; + --in-content-table-background: var(--base00) !important; + --in-content-table-header-background: var(--base00) !important; + --in-content-table-border-color: var(--base03) !important; + --in-content-border-hover: var(--base03) !important; + --in-content-item-hover: var(--base01) !important; + --in-content-item-hover-text: var(--base06) !important; + --in-content-item-selected: var(--base02) !important; + --in-content-item-selected-text: var(--base06) !important; + --in-content-deemphasized-text: var(--base04) !important; + --checkbox-unchecked-bgcolor: var(--base01) !important; + --checkbox-unchecked-hover-bgcolor: var(--base02) !important; + --checkbox-checked-bgcolor: var(--base0D) !important; + --checkbox-checked-color: var(--base00) !important; + --link-color: var(--base0D) !important; + --link-color-hover: var(--base0C) !important; + } + + html, body, + #preferences-body, + #preferences-container, + #preferences-stack, + #mainPrefPane, + .main-content { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + + #category-box, + #categories, + #preferences-sidebar, + .sticky-container { + background-color: var(--base01) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .pane-container { + background-color: var(--base00) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .category, + .subcategory { + background-color: var(--base01) !important; + color: var(--base05) !important; + } + + .category:hover, + .subcategory:hover { + background-color: var(--base01) !important; + border-color: var(--base03) !important; + } + + .category[selected], + .subcategory[selected] { + background-color: var(--base01) !important; + color: var(--base06) !important; + border-color: var(--base03) !important; + box-shadow: inset 2px 0 0 var(--base0D) !important; + } + + groupbox, + .card, + .settings-box, + .content-blocking-category, + .section, + setting-group, + setting-pane, + setting-control, + moz-card { + background-color: var(--base00) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .header, + h1, h2, h3, h4, + .title, + .description, + .text-link { + color: var(--base05) !important; + } + + .description-deemphasized, + .text-deemphasized, + .help-link { + color: var(--base04) !important; + } + + #policies-container, + #enterprise-policy-container, + .enterprise-policy-container, + .info-box-container, + .info-box, + #isDefaultBox, + #setDefaultPane, + #isNotDefaultPane, + #defaultBrowserNotification, + .default-browser-notification, + setting-group[groupid="defaultBrowser"], + .notification-message, + .notification-inner { + background-color: var(--base01) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + .spotlight, + setting-group.spotlight, + setting-group[groupid="defaultBrowser"].spotlight { + background-color: color-mix(in srgb, var(--base0D) 16%, var(--base01)) !important; + outline-color: var(--base0D) !important; + border-color: var(--base0D) !important; + animation: none !important; + } + + #searchInput, + .search-container input, + .search-textbox, + .search-field { + background-color: var(--base00) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + #searchInput:focus, + .search-container input:focus { + background-color: var(--base00) !important; + border-color: var(--base0D) !important; + color: var(--base06) !important; + } + + input[type="checkbox"], + input[type="radio"], + .checkbox-check, + .radio-check { + accent-color: var(--base0D) !important; + } + + button { + background-color: var(--base02) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + button:hover { + background-color: var(--base03) !important; + color: var(--base06) !important; + } + + button.primary { + background-color: var(--base0D) !important; + color: var(--base00) !important; + } + + button.primary:hover { + background-color: var(--base0C) !important; + color: var(--base00) !important; + } + + radiogroup, + radio, + [role="radiogroup"], + [role="radio"], + .radio-group, + .choice-button, + .multi-choice-button, + .single-choice-button, + .radio-check, + .radio-icon, + .radio-label { + color: var(--base05) !important; + border-color: var(--base03) !important; + background-color: transparent !important; + } + + radio[selected="true"] .radio-check, + .radio-check[selected="true"] { + background-color: var(--base0D) !important; + border-color: var(--base0D) !important; + } + + checkbox, + input[type="checkbox"], + .checkbox-check, + .checkbox-icon, + .toggle-button { + border-color: var(--base03) !important; + background-color: var(--base01) !important; + color: var(--base05) !important; + } + + checkbox[checked="true"] .checkbox-check, + input[type="checkbox"]:checked, + .checkbox-check[checked="true"] { + background-color: var(--base0D) !important; + border-color: var(--base0D) !important; + color: var(--base00) !important; + } + + menulist, + menulist::part(label), + menulist::part(icon), + .menulist-label, + .folder-menu-list, + #downloadFolder, + #downloadFolderButton, + #translations-manage-description, + #translations-manage-settings-button, + #translations-manage-install-list, + .translations-manage-language, + .translations-manage-language > label, + #translateBox, + #translationsGroup, + #dictionariesGroup, + input[type="text"], + input[type="search"] { + background-color: var(--base00) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + #applicationsGroup, + #filter, + #handlersView, + #handlersView richlistitem, + #handlersView .actionsMenu, + #handlersView .actionsMenu > .menulist-label, + #handlersTable { + background-color: var(--base00) !important; + color: var(--base05) !important; + border-color: var(--base03) !important; + } + + #handlersView richlistitem:hover, + #handlersView richlistitem[selected], + .content-blocking-category:hover, + .setting-row:hover, + .search-container:hover, + setting-group:hover, + setting-control:hover, + .translations-manage-language:hover, + moz-card:hover { + background-color: var(--base01) !important; + color: var(--base06) !important; + border-color: var(--base03) !important; + } + } + ''; + in + { + default = { + isDefault = true; + id = 0; + userChrome = stylixUserChrome; + userContent = stylixUserContent; + }; + default-release = { + isDefault = false; + id = 1; + userChrome = stylixUserChrome; + userContent = stylixUserContent; + }; + }; + + # ── GTK Theming ───────────────────────────────────────────── + gtk = { + gtk4.theme = lib.mkForce null; + enable = lib.mkDefault (!isDarwin); + gtk4.extraConfig.gtk-application-prefer-dark-theme = 1; + }; + + # ── Qt Theming (Linux only) ───────────────────────────────── + qt = lib.mkIf (!isDarwin) { + enable = lib.mkForce true; + platformTheme.name = lib.mkForce "gtk3"; + }; + + # ── Terminal env ──────────────────────────────────────────── + programs.zsh.initContent = '' + export COLORTERM=truecolor + ''; + + # macOS appearance is the source of truth; darwin appearance-sync + # detects AppleInterfaceStyle and activates matching prebuilt + # profile. Do not push appearance from HM in the opposite direction. + }; + }; +in +{ + # ── Den styling aspect ────────────────────────────────────────────── + # System-level (nixos + darwin) Stylix is unified via the `os` custom + # class. HM-level Stylix lives in `homeManager`. The HM body is also + # mirrored into `flake.modules.homeManager.dendritic` below so that + # embedded HM users (`home-manager.users.` inside the four + # darwin/nixos hosts) pick it up via the existing dendritic monolith + # without going through `den.homes`. + den.aspects.styling = { + nixos = + { lib, config, ... }: + { + imports = [ inputs.stylix.nixosModules.stylix ]; + stylix.fonts.sizes = { + terminal = 12; + applications = 12; + desktop = 11; + }; + stylix.cursor = { + package = config._module.args.pkgs.bibata-cursors; + name = "Bibata-Modern-Ice"; + size = 24; + }; + stylix.opacity = { + terminal = 1.0; + popups = 0.95; + }; + specialisation = lib.mkDefault { + light.configuration.dendritic.theme.variant = lib.mkForce "light"; + dark.configuration.dendritic.theme.variant = lib.mkForce "dark"; + }; + }; + + darwin = + { pkgs, ... }: + { + imports = [ inputs.stylix.darwinModules.stylix ]; + fonts.packages = [ + pkgs.maple-mono.NF + pkgs.inter + pkgs.noto-fonts + ]; + }; + + # ── Shared Stylix config (nixos + darwin) ──────────────────────── + # Written ONCE here; den's `os` class forwards it into BOTH the + # nixos and darwin evaluations. Replaces what used to be the + # duplicated bodies in `modules/styling.nix`. + os = + { + pkgs, + lib, + config, + ... + }: + let + schemeName = + if config.dendritic.theme.variant == "light" then + config.dendritic.theme.schemes.light + else + config.dendritic.theme.schemes.dark; + stylixScheme = "${pkgs.base16-schemes}/share/themes/${schemeName}.yaml"; + in + { + imports = [ themeOptionsModule ]; + + stylix = { + enable = true; + enableReleaseChecks = false; + polarity = config.dendritic.theme.variant; + base16Scheme = lib.mkForce stylixScheme; + override = { + slug = "stylix"; + scheme = "stylix"; + }; + image = lib.mkDefault ../wallpapers/mountain-sunset.png; + + fonts = { + monospace = { + package = pkgs.maple-mono.NF; + name = "Maple Mono NF"; + }; + sansSerif = { + package = pkgs.inter; + name = "Inter"; + }; + serif = { + package = pkgs.noto-fonts; + name = "Noto Serif"; + }; + }; + }; + }; + + # Aspect-side HM Stylix body — picked up by `den.homes.*` consumers + # via `includes`. Currently no consumer beyond mirror to + # `flake.modules.homeManager.dendritic` below. + homeManager = stylingHmModule; + }; + + # ── Mirror into the flake-parts HM dendritic monolith ──────────── + # The embedded HM users in mba/mba-dark/mba-asahi/nixos-test/microvm + # all import `inputs.self.modules.homeManager.dendritic`. Defining + # the same `stylingHmModule` here makes them pick up the HM Stylix + # body without needing `den.homes.*` migration. + flake.modules.homeManager.dendritic = stylingHmModule; +} diff --git a/docs/awesome-nix.md b/docs/awesome-nix.md index 20ab18af..0a5ca5b1 100644 --- a/docs/awesome-nix.md +++ b/docs/awesome-nix.md @@ -14,346 +14,347 @@ A curated list of the best resources in the Nix community. [Nix](https://github.com/nixos/nix) is a powerful package manager for Linux and other Unix systems that makes package management reliable and reproducible. -*Please read the [contribution guidelines](CONTRIBUTING.md) before contributing.* +_Please read the [contribution guidelines](CONTRIBUTING.md) before contributing._ ## Contents -* [Resources](#resources) - * [Learning](#learning) - * [Discovery](#discovery) -* [Installation Media](#installation-media) -* [Channel History](#channel-history) -* [Deployment Tools](#deployment-tools) -* [Virtualisation](#virtualisation) -* [Command-Line Tools](#command-line-tools) -* [Development](#development) -* [DevOps](#devops) -* [Programming Languages](#programming-languages) - * [Arduino](#arduino) - * [Clojure](#clojure) - * [Crystal](#crystal) - * [Elm](#elm) - * [Gleam](#gleam) - * [Haskell](#haskell) - * [Haxe](#haxe) - * [Julia](#julia) - * [Lean](#lean) - * [Node.js](#nodejs) - * [OCaml](#ocaml) - * [PHP](#php) - * [PureScript](#purescript) - * [Python](#python) - * [Ruby](#ruby) - * [Rust](#rust) - * [Scala](#scala) - * [Zig](#zig) -* [NixOS Modules](#nixos-modules) -* [NixOS Configuration Editors](#nixos-configuration-editors) -* [Overlays](#overlays) -* [Distributions](#distributions) -* [Community](#community) +- [Resources](#resources) + - [Learning](#learning) + - [Discovery](#discovery) +- [Installation Media](#installation-media) +- [Channel History](#channel-history) +- [Deployment Tools](#deployment-tools) +- [Virtualisation](#virtualisation) +- [Command-Line Tools](#command-line-tools) +- [Development](#development) +- [DevOps](#devops) +- [Programming Languages](#programming-languages) + - [Arduino](#arduino) + - [Clojure](#clojure) + - [Crystal](#crystal) + - [Elm](#elm) + - [Gleam](#gleam) + - [Haskell](#haskell) + - [Haxe](#haxe) + - [Julia](#julia) + - [Lean](#lean) + - [Node.js](#nodejs) + - [OCaml](#ocaml) + - [PHP](#php) + - [PureScript](#purescript) + - [Python](#python) + - [Ruby](#ruby) + - [Rust](#rust) + - [Scala](#scala) + - [Zig](#zig) +- [NixOS Modules](#nixos-modules) +- [NixOS Configuration Editors](#nixos-configuration-editors) +- [Overlays](#overlays) +- [Distributions](#distributions) +- [Community](#community) ## Resources ### Learning -* [Building a Rust service with Nix](https://fasterthanli.me/series/building-a-rust-service-with-nix) - An in-depth blog series about creating a Rust application with Nix. -* [Explainix](https://zaynetro.com/explainix) - Explain Nix syntax visually. -* [How to Learn Nix](https://ianthehenry.com/posts/how-to-learn-nix/) - It's like a Let's Play, but for obscure software documentation. -* [Nix - A One Pager](https://code.tvl.fyi/about/nix/nix-1p) - A one page introduction to the Nix language. -* [nix-book](https://saylesss88.github.io) - A comprehensive guide to NixOS +- [Building a Rust service with Nix](https://fasterthanli.me/series/building-a-rust-service-with-nix) - An in-depth blog series about creating a Rust application with Nix. +- [Explainix](https://zaynetro.com/explainix) - Explain Nix syntax visually. +- [How to Learn Nix](https://ianthehenry.com/posts/how-to-learn-nix/) - It's like a Let's Play, but for obscure software documentation. +- [Nix - A One Pager](https://code.tvl.fyi/about/nix/nix-1p) - A one page introduction to the Nix language. +- [nix-book](https://saylesss88.github.io) - A comprehensive guide to NixOS hardening and configuration. -* [Nix from First Principles: Flake Edition](https://tonyfinn.com/blog/nix-from-first-principles-flake-edition/) - A modern crash-course to using Nix features, Flakes, and developing with Nix. -* [Nix in 100 Seconds](https://www.youtube.com/watch?v=FJVFXsNzYZQ) - A YouTube video from Fireship presenting Nix in 100 seconds. -* [Nix Notes](https://github.com/noteed/nix-notes) - A collection of short notes about Nix, each contributing to the same virtual machine image. -* [Nix Pills](https://nixos.org/guides/nix-pills/) - The best way to learn, with examples. -* [Nix Shorts](https://github.com/alper/nix-shorts) - A collection of short notes about how to use Nix, updated for Nix Flakes. -* [Nix Starter Config](https://github.com/Misterio77/nix-starter-configs) - A few simple Nix Flake templates for getting started with NixOS + home-manager. -* [nix.dev](https://nix.dev/) - An opinionated guide for developers about getting things done using the Nix ecosystem. -* [NixOS & Flakes Book](https://github.com/ryan4yin/nixos-and-flakes-book) - An unofficial and opinionated NixOS & Flakes book for beginners. -* [NixOS Asia Tutorial Series](https://nixos.asia/en/tutorial) - A series of high-level tutorials on using Nix Flakes, NixOS, home-manager, etc. -* [NixOS in Production](https://leanpub.com/nixos-in-production) - Free (pay-what-you-want) book in pdf format. -* [Official Nix manual](https://nixos.org/manual/nix/stable) - Latest stable version of the official Nix manual, best used as reference guide. Receives updates when available. -* [Official NixOS manual](https://nixos.org/manual/nixos/stable) - Latest stable version of the official NixOS manual, mix of tutorial and reference guide. Receives updates when available. -* [Official Nixpkgs manual](https://nixos.org/manual/nixpkgs/stable) - Latest stable version of the official Nixpkgs reference manual. Receives updates when available. -* [Tour of Nix](https://nixcloud.io/tour/) - An online interactive tutorial on Nix language constructs. -* [Wombat's Book of Nix](https://mhwombat.codeberg.page/nix-book/) - A book-length introduction to Nix and flakes. -* [Zero to Nix](https://zero-to-nix.com/) - A flake-centric guide to Nix and its concepts created by Determinate Systems to quickly onboard beginners. +- [Nix from First Principles: Flake Edition](https://tonyfinn.com/blog/nix-from-first-principles-flake-edition/) - A modern crash-course to using Nix features, Flakes, and developing with Nix. +- [Nix in 100 Seconds](https://www.youtube.com/watch?v=FJVFXsNzYZQ) - A YouTube video from Fireship presenting Nix in 100 seconds. +- [Nix Notes](https://github.com/noteed/nix-notes) - A collection of short notes about Nix, each contributing to the same virtual machine image. +- [Nix Pills](https://nixos.org/guides/nix-pills/) - The best way to learn, with examples. +- [Nix Shorts](https://github.com/alper/nix-shorts) - A collection of short notes about how to use Nix, updated for Nix Flakes. +- [Nix Starter Config](https://github.com/Misterio77/nix-starter-configs) - A few simple Nix Flake templates for getting started with NixOS + home-manager. +- [nix.dev](https://nix.dev/) - An opinionated guide for developers about getting things done using the Nix ecosystem. +- [NixOS & Flakes Book](https://github.com/ryan4yin/nixos-and-flakes-book) - An unofficial and opinionated NixOS & Flakes book for beginners. +- [NixOS Asia Tutorial Series](https://nixos.asia/en/tutorial) - A series of high-level tutorials on using Nix Flakes, NixOS, home-manager, etc. +- [NixOS in Production](https://leanpub.com/nixos-in-production) - Free (pay-what-you-want) book in pdf format. +- [Official Nix manual](https://nixos.org/manual/nix/stable) - Latest stable version of the official Nix manual, best used as reference guide. Receives updates when available. +- [Official NixOS manual](https://nixos.org/manual/nixos/stable) - Latest stable version of the official NixOS manual, mix of tutorial and reference guide. Receives updates when available. +- [Official Nixpkgs manual](https://nixos.org/manual/nixpkgs/stable) - Latest stable version of the official Nixpkgs reference manual. Receives updates when available. +- [Tour of Nix](https://nixcloud.io/tour/) - An online interactive tutorial on Nix language constructs. +- [Wombat's Book of Nix](https://mhwombat.codeberg.page/nix-book/) - A book-length introduction to Nix and flakes. +- [Zero to Nix](https://zero-to-nix.com/) - A flake-centric guide to Nix and its concepts created by Determinate Systems to quickly onboard beginners. ### Discovery -* [Home Manager Option Search](https://home-manager-options.extranix.com/) - Search through all 2000+ Home Manager options and read how to use them. +- [Home Manager Option Search](https://home-manager-options.extranix.com/) - Search through all 2000+ Home Manager options and read how to use them. -* [Nix Package Versions](https://lazamar.co.uk/nix-versions/) - Find all versions of a package that were available in a channel and the revision you can download it from. -* [nix-search-tv](https://github.com/3timeslazy/nix-search-tv) - CLI fuzzy finder for packages and options from Nixpkgs, Home Manager, and more. -* [Noogle](https://noogle.dev/) - Nix API search engine allowing to search functions based on their types and other attributes. -* [NüschtOS Search](https://github.com/NuschtOS/search) - Simple and fast static-page NixOS option search. -* [Searchix](https://searchix.ovh/) - Search Nix packages and options from NixOS, Darwin and Home Manager. +- [Nix Package Versions](https://lazamar.co.uk/nix-versions/) - Find all versions of a package that were available in a channel and the revision you can download it from. +- [nix-search-tv](https://github.com/3timeslazy/nix-search-tv) - CLI fuzzy finder for packages and options from Nixpkgs, Home Manager, and more. +- [Noogle](https://noogle.dev/) - Nix API search engine allowing to search functions based on their types and other attributes. +- [NüschtOS Search](https://github.com/NuschtOS/search) - Simple and fast static-page NixOS option search. +- [Searchix](https://searchix.ovh/) - Search Nix packages and options from NixOS, Darwin and Home Manager. ## Installation Media -* [nix-installer-scripts](https://github.com/dnkmmr69420/nix-installer-scripts) - Runs the official installer but does some tweaking as well such as adding fcontext for selinux and installing nix outside of the default profile so you don't accidently uninstall it. -* [nix-installer](https://github.com/DeterminateSystems/nix-installer) - Opinionated alternative to the official Nix install scripts. -* [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) - Install NixOS everywhere via SSH. -* [nixos-generators](https://github.com/nix-community/nixos-generators) - Take a NixOS config and build multiple different images types including VirtualBox VMs, Azure images, and installation ISOs. -* [nixos-infect](https://github.com/elitak/nixos-infect) - Replace a running non-NixOS Linux host with NixOS. -* [nixos-up](https://github.com/samuela/nixos-up) - Super easy NixOS installer that can be used from the installation ISO. +- [nix-installer-scripts](https://github.com/dnkmmr69420/nix-installer-scripts) - Runs the official installer but does some tweaking as well such as adding fcontext for selinux and installing nix outside of the default profile so you don't accidently uninstall it. +- [nix-installer](https://github.com/DeterminateSystems/nix-installer) - Opinionated alternative to the official Nix install scripts. +- [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) - Install NixOS everywhere via SSH. +- [nixos-generators](https://github.com/nix-community/nixos-generators) - Take a NixOS config and build multiple different images types including VirtualBox VMs, Azure images, and installation ISOs. +- [nixos-infect](https://github.com/elitak/nixos-infect) - Replace a running non-NixOS Linux host with NixOS. +- [nixos-up](https://github.com/samuela/nixos-up) - Super easy NixOS installer that can be used from the installation ISO. ## Channel History -* [`npc`](https://github.com/samestep/npc) - CLI to view and bisect Nixpkgs channel history. -* [Nix Infra Status](https://status.nixos.org) - Get the age and current Git commit of each Nix channel. -* [Nix Review Tools Reports](https://malob.github.io/nix-review-tools-reports/) - Reports showing problematic dependencies (dependencies causing the most failed builds) for major Hydra jobsets. +- [`npc`](https://github.com/samestep/npc) - CLI to view and bisect Nixpkgs channel history. +- [Nix Infra Status](https://status.nixos.org) - Get the age and current Git commit of each Nix channel. +- [Nix Review Tools Reports](https://malob.github.io/nix-review-tools-reports/) - Reports showing problematic dependencies (dependencies causing the most failed builds) for major Hydra jobsets. -* [nixpkgs PR tracker](https://nixpk.gs/pr-tracker.html) - A tracker for whether a PR has made it into a channel yet. +- [nixpkgs PR tracker](https://nixpk.gs/pr-tracker.html) - A tracker for whether a PR has made it into a channel yet. ## Deployment Tools -* [bento](https://github.com/rapenne-s/bento/) - A KISS deployment tool to keep your NixOS fleet (servers & workstations) up to date. -* [Clan](https://clan.lol) - A peer-to-peer deployment tool with inbuilt support for secrets and a module system to manage distributed networks. -* [Colmena](https://github.com/zhaofengli/colmena) - A simple, stateless NixOS deployment tool modeled after NixOps and morph. -* [comin](https://github.com/nlewo/comin) - A deployment tool to continuously pull from Git repositories. -* [deploy-rs](https://github.com/serokell/deploy-rs) - A simple multi-profile Nix-flake deploy tool. -* [krops](https://cgit.krebsco.de/krops/about/) - A lightweight toolkit to deploy NixOS systems, remotely or locally. -* [KubeNix](https://github.com/hall/kubenix) - A Kubernetes resource builder using Nix. -* [KuberNix](https://github.com/saschagrunert/kubernix) - Single-dependency Kubernetes clusters via Nix packages. -* [morph](https://github.com/DBCDK/morph) - A tool for managing existing NixOS hosts. -* [Nixery](https://github.com/tazjin/nixery) - A Docker-compatible container registry which builds images ad-hoc via Nix. -* [Nixinate](https://github.com/MatthewCroughan/nixinate) - A Nix flake library to provide app outputs for managing existing NixOS hosts over SSH. -* [Nixlets](https://gitlab.com/TECHNOFAB/nixlets) - Like Helm but using only Nix, uses Kubenix under the hood. -* [NixOps](https://github.com/NixOS/nixops) - The official Nix deployment tool, compatible with AWS, Hetzner, and more. -* [pushnix](https://github.com/arnarg/pushnix) - Simple cli utility that pushes NixOS configuration and triggers a rebuild using ssh. -* [terraform-nixos](https://github.com/nix-community/terraform-nixos) - A set of Terraform modules designed to deploy NixOS. -* [terranix](https://terranix.org) - Use Nix and the NixOS module system to write your Terraform code. +- [bento](https://github.com/rapenne-s/bento/) - A KISS deployment tool to keep your NixOS fleet (servers & workstations) up to date. +- [Clan](https://clan.lol) - A peer-to-peer deployment tool with inbuilt support for secrets and a module system to manage distributed networks. +- [Colmena](https://github.com/zhaofengli/colmena) - A simple, stateless NixOS deployment tool modeled after NixOps and morph. +- [comin](https://github.com/nlewo/comin) - A deployment tool to continuously pull from Git repositories. +- [deploy-rs](https://github.com/serokell/deploy-rs) - A simple multi-profile Nix-flake deploy tool. +- [krops](https://cgit.krebsco.de/krops/about/) - A lightweight toolkit to deploy NixOS systems, remotely or locally. +- [KubeNix](https://github.com/hall/kubenix) - A Kubernetes resource builder using Nix. +- [KuberNix](https://github.com/saschagrunert/kubernix) - Single-dependency Kubernetes clusters via Nix packages. +- [morph](https://github.com/DBCDK/morph) - A tool for managing existing NixOS hosts. +- [Nixery](https://github.com/tazjin/nixery) - A Docker-compatible container registry which builds images ad-hoc via Nix. +- [Nixinate](https://github.com/MatthewCroughan/nixinate) - A Nix flake library to provide app outputs for managing existing NixOS hosts over SSH. +- [Nixlets](https://gitlab.com/TECHNOFAB/nixlets) - Like Helm but using only Nix, uses Kubenix under the hood. +- [NixOps](https://github.com/NixOS/nixops) - The official Nix deployment tool, compatible with AWS, Hetzner, and more. +- [pushnix](https://github.com/arnarg/pushnix) - Simple cli utility that pushes NixOS configuration and triggers a rebuild using ssh. +- [terraform-nixos](https://github.com/nix-community/terraform-nixos) - A set of Terraform modules designed to deploy NixOS. +- [terranix](https://terranix.org) - Use Nix and the NixOS module system to write your Terraform code. ## Virtualisation -* [extra-container](https://github.com/erikarvstedt/extra-container) - Run declarative NixOS containers from the command line. -* [microvm](https://github.com/microvm-nix/microvm.nix) - NixOS-based MicroVMs. -* [nixos-shell](https://github.com/Mic92/nixos-shell) - Simple headless VM configuration using Nix (similar to Vagrant). +- [extra-container](https://github.com/erikarvstedt/extra-container) - Run declarative NixOS containers from the command line. +- [microvm](https://github.com/microvm-nix/microvm.nix) - NixOS-based MicroVMs. +- [nixos-shell](https://github.com/Mic92/nixos-shell) - Simple headless VM configuration using Nix (similar to Vagrant). ## Command-Line Tools -* [alejandra](https://github.com/kamadorueda/alejandra) - An opinionated Nix code formatter optimized for speed and consistency. -* [angrr](https://github.com/linyinfeng/angrr) - Auto Nix GC Roots Retention. This tool simply deletes auto GC roots based on the modified time of their symbolic link target. -* [comma](https://github.com/nix-community/comma) - Quickly run any binary; wraps together `nix run` and `nix-index`. -* [deadnix](https://github.com/astro/deadnix) - Scan Nix files for dead code. -* [devenv](https://github.com/cachix/devenv) - A Nix-based tool for creating developer shell environments quickly and reproducibly. -* [dix](https://github.com/faukah/dix) - Diff Nix; a super-fast tool to diff Nix related things. -* [manix](https://github.com/mlvzk/manix) - Find configuration options and function documentation for Nixpkgs, NixOS, and Home Manager. -* [nh](https://github.com/nix-community/nh) - Better output for `nix`, `nixos-rebuild`, `home-manager` and nix-darwin CLI leveraging `dix` and `nix-output-monitor`. -* [nix-alien](https://github.com/thiagokokada/nix-alien) - Run unpatched binaries on Nix/NixOS easily. -* [nix-diff](https://github.com/Gabriella439/nix-diff) - A tool to explain why two Nix derivations differ. -* [nix-du](https://github.com/symphorien/nix-du) - Visualise which gc-roots to delete to free some space in your Nix store. -* [nix-index](https://github.com/nix-community/nix-index) - Quickly locate Nix packages with specific files. -* [nix-init](https://github.com/nix-community/nix-init) - Generate Nix packages from URLs with hash prefetching, dependency inference, license detection, and more. -* [nix-melt](https://github.com/nix-community/nix-melt) - A ranger-like flake.lock viewer. -* [nix-output-monitor](https://github.com/maralorn/nix-output-monitor) - A tool to produce useful graphs and statistics when building derivations. -* [nix-prefetch](https://github.com/msteen/nix-prefetch) - A universal tool for updating source checksums. -* [nix-tree](https://github.com/utdemir/nix-tree) - Interactively browse the dependency graph of Nix derivations. -* [nixfmt](https://github.com/NixOS/nixfmt) - A formatter for Nix code, intended to easily apply a uniform style. -* [nixos-cli](https://github.com/nix-community/nixos-cli) - Configurable all-in-one CLI for common NixOS tools with an emphasis on improved user experience. -* [nixpkgs-hammering](https://github.com/jtojnar/nixpkgs-hammering) - An opinionated linter for Nixpkgs package expressions. -* [nurl](https://github.com/nix-community/nurl) - Generate Nix fetcher calls from repository URLs. -* [nvd](https://git.sr.ht/~khumba/nvd) - Diff package versions between two store paths; it's especially useful for comparing NixOS generations on rebuild. -* [optnix](https://git.sr.ht/~watersucks/optnix) - A terminal-based options searcher for Nix module systems. -* [statix](https://github.com/oppiliappan/statix) - A linter/fixer to check for and fix antipatterns in Nix code. +- [alejandra](https://github.com/kamadorueda/alejandra) - An opinionated Nix code formatter optimized for speed and consistency. +- [angrr](https://github.com/linyinfeng/angrr) - Auto Nix GC Roots Retention. This tool simply deletes auto GC roots based on the modified time of their symbolic link target. +- [comma](https://github.com/nix-community/comma) - Quickly run any binary; wraps together `nix run` and `nix-index`. +- [deadnix](https://github.com/astro/deadnix) - Scan Nix files for dead code. +- [devenv](https://github.com/cachix/devenv) - A Nix-based tool for creating developer shell environments quickly and reproducibly. +- [dix](https://github.com/faukah/dix) - Diff Nix; a super-fast tool to diff Nix related things. +- [manix](https://github.com/mlvzk/manix) - Find configuration options and function documentation for Nixpkgs, NixOS, and Home Manager. +- [nh](https://github.com/nix-community/nh) - Better output for `nix`, `nixos-rebuild`, `home-manager` and nix-darwin CLI leveraging `dix` and `nix-output-monitor`. +- [nix-alien](https://github.com/thiagokokada/nix-alien) - Run unpatched binaries on Nix/NixOS easily. +- [nix-diff](https://github.com/Gabriella439/nix-diff) - A tool to explain why two Nix derivations differ. +- [nix-du](https://github.com/symphorien/nix-du) - Visualise which gc-roots to delete to free some space in your Nix store. +- [nix-index](https://github.com/nix-community/nix-index) - Quickly locate Nix packages with specific files. +- [nix-init](https://github.com/nix-community/nix-init) - Generate Nix packages from URLs with hash prefetching, dependency inference, license detection, and more. +- [nix-melt](https://github.com/nix-community/nix-melt) - A ranger-like flake.lock viewer. +- [nix-output-monitor](https://github.com/maralorn/nix-output-monitor) - A tool to produce useful graphs and statistics when building derivations. +- [nix-prefetch](https://github.com/msteen/nix-prefetch) - A universal tool for updating source checksums. +- [nix-tree](https://github.com/utdemir/nix-tree) - Interactively browse the dependency graph of Nix derivations. +- [nixfmt](https://github.com/NixOS/nixfmt) - A formatter for Nix code, intended to easily apply a uniform style. +- [nixos-cli](https://github.com/nix-community/nixos-cli) - Configurable all-in-one CLI for common NixOS tools with an emphasis on improved user experience. +- [nixpkgs-hammering](https://github.com/jtojnar/nixpkgs-hammering) - An opinionated linter for Nixpkgs package expressions. +- [nurl](https://github.com/nix-community/nurl) - Generate Nix fetcher calls from repository URLs. +- [nvd](https://git.sr.ht/~khumba/nvd) - Diff package versions between two store paths; it's especially useful for comparing NixOS generations on rebuild. +- [optnix](https://git.sr.ht/~watersucks/optnix) - A terminal-based options searcher for Nix module systems. +- [statix](https://github.com/oppiliappan/statix) - A linter/fixer to check for and fix antipatterns in Nix code. ## Development -* [Arion](https://github.com/hercules-ci/arion) - Run `docker-compose` with help from Nix/NixOS. -* [attic](https://github.com/zhaofengli/attic) - Multi-tenant Nix Binary Cache. -* [cached-nix-shell](https://github.com/xzfc/cached-nix-shell) - A `nix-shell` replacement that uses caching to open subsequent shells quickly. -* [Cachix](https://www.cachix.org) - Hosted binary cache service; free for open-source projects. -* [compose2nix](https://github.com/aksiksi/compose2nix) - Generate a NixOS config from a Docker Compose project. -* [Conflake](https://ratson.github.io/conflake/) - A batteries included, autoload files, convention-based configuration framework for `flake.nix`. -* [Devbox](https://github.com/jetify-com/devbox) - Instant, portable, and predictable development environments. -* [devshell](https://github.com/numtide/devshell) - `mkShell` with extra bits and a toml config option to be able to onboard non-nix users. -* [dream2nix](https://github.com/nix-community/dream2nix) - A framework for automatically converting packages from other build systems to Nix. -* [flake-utils-plus](https://github.com/gytis-ivaskevicius/flake-utils-plus) - A lightweight Nix library flake for painless NixOS flake configuration. -* [flake-utils](https://github.com/numtide/flake-utils) - Pure Nix flake utility functions to help with writing flakes. -* [flake.parts](https://github.com/hercules-ci/flake-parts) - Minimal Nix modules framework for Flakes: split your flakes into modules and get things done with community modules. -* [flakelight](https://github.com/nix-community/flakelight) - A modular flake framework aiming to minimize boilerplate. -* [flox](https://github.com/flox/flox) - Manage and share development environments, package projects, and publish artifacts anywhere. -* [gitignore.nix](https://github.com/hercules-ci/gitignore.nix) - The most feature-complete and easy-to-use `.gitignore` integration. -* [haumea](https://github.com/nix-community/haumea) - Filesystem-based module system for the Nix language similar to traditional programming languages, with support for file hierarchy and visibility. -* [lorri](https://github.com/nix-community/lorri/) - A much better `nix-shell` for development that augments direnv. -* [make-shell](https://github.com/nicknovitski/make-shell) - `mkShell` meets modules, a modular almost-drop-in replacement for `pkgs.mkShell` function. -* [MCP-NixOS](https://github.com/utensils/mcp-nixos) - An MCP server that provides AI assistants with accurate information about NixOS packages, options, Home Manager, and nix-darwin configurations. -* [namaka](https://github.com/nix-community/namaka) - Snapshot testing for Nix based on haumea. -* [nil](https://github.com/oxalica/nil) - NIx Language server, an incremental analysis assistent for writing in Nix. -* [niv](https://github.com/nmattia/niv/) - Easy dependency management for Nix projects with package pinning. -* [nix2container](https://github.com/nlewo/nix2container) - An efficient container building workflow with Nix. -* [nix-direnv](https://github.com/nix-community/nix-direnv) - A fast loader and flake-compliant configuration for the direnv environment auto-loader. -* [nix-health](https://github.com/juspay/nix-health) - A program to check the health of your Nix install. Furthermore, individual projects can configure their own health checks in their `flake.nix`. -* [nix-oci](https://github.com/Dauliac/nix-oci) - A flake-parts module for building minimal, reproducible OCI containers using nix2container. -* [nix-update](https://github.com/Mic92/nix-update) - Update versions/source hashes of nix packages. -* [nixd](https://github.com/nix-community/nixd) - Nix language server, based on Nix libraries. -* [nixpkgs-review](https://github.com/Mic92/nixpkgs-review) - The best tool to verify that a pull-request in Nixpkgs is building properly. -* [Nixtest](https://gitlab.com/TECHNOFAB/nixtest) - Testing framework for Nix, with snapshot and unit test support, JUnit generation etc. -* [npins](https://github.com/andir/npins) - A simple tool for handling different types of dependencies in a Nix project. It is inspired by and comparable to Niv. -* [pog](https://github.com/jpetrucciani/pog) - A new, powerful way to do bash scripts. Pog is a powerful Nix library that transforms the way developers create command-line interfaces (CLIs). -* [pre-commit-hooks.nix](https://github.com/cachix/git-hooks.nix) - Run linters/formatters at commit time and on your CI. -* [rnix-lsp](https://github.com/nix-community/rnix-lsp) - A syntax-checking language server for Nix. -* [robotnix](https://github.com/nix-community/robotnix) - A declarative and reproducible build system for Android (AOSP) images. -* [services-flake](https://github.com/juspay/services-flake) - A NixOS-like service configuration framework for Nix flakes. -* [Snowfall Lib](https://github.com/snowfallorg/lib) - A library that makes it easy to manage your Nix flake by imposing an opinionated file structure. -* [templates](https://github.com/nix-community/templates) - Project templates for many languages using Nix flakes. -* [treefmt-nix](https://github.com/numtide/treefmt-nix) - A formatter that allows formatting all your project files with a single command, all via a single `.nix` file. +- [Arion](https://github.com/hercules-ci/arion) - Run `docker-compose` with help from Nix/NixOS. +- [attic](https://github.com/zhaofengli/attic) - Multi-tenant Nix Binary Cache. +- [cached-nix-shell](https://github.com/xzfc/cached-nix-shell) - A `nix-shell` replacement that uses caching to open subsequent shells quickly. +- [Cachix](https://www.cachix.org) - Hosted binary cache service; free for open-source projects. +- [compose2nix](https://github.com/aksiksi/compose2nix) - Generate a NixOS config from a Docker Compose project. +- [Conflake](https://ratson.github.io/conflake/) - A batteries included, autoload files, convention-based configuration framework for `flake.nix`. +- [Devbox](https://github.com/jetify-com/devbox) - Instant, portable, and predictable development environments. +- [devshell](https://github.com/numtide/devshell) - `mkShell` with extra bits and a toml config option to be able to onboard non-nix users. +- [dream2nix](https://github.com/nix-community/dream2nix) - A framework for automatically converting packages from other build systems to Nix. +- [flake-utils-plus](https://github.com/gytis-ivaskevicius/flake-utils-plus) - A lightweight Nix library flake for painless NixOS flake configuration. +- [flake-utils](https://github.com/numtide/flake-utils) - Pure Nix flake utility functions to help with writing flakes. +- [flake.parts](https://github.com/hercules-ci/flake-parts) - Minimal Nix modules framework for Flakes: split your flakes into modules and get things done with community modules. +- [flakelight](https://github.com/nix-community/flakelight) - A modular flake framework aiming to minimize boilerplate. +- [flox](https://github.com/flox/flox) - Manage and share development environments, package projects, and publish artifacts anywhere. +- [gitignore.nix](https://github.com/hercules-ci/gitignore.nix) - The most feature-complete and easy-to-use `.gitignore` integration. +- [haumea](https://github.com/nix-community/haumea) - Filesystem-based module system for the Nix language similar to traditional programming languages, with support for file hierarchy and visibility. +- [lorri](https://github.com/nix-community/lorri/) - A much better `nix-shell` for development that augments direnv. +- [make-shell](https://github.com/nicknovitski/make-shell) - `mkShell` meets modules, a modular almost-drop-in replacement for `pkgs.mkShell` function. +- [MCP-NixOS](https://github.com/utensils/mcp-nixos) - An MCP server that provides AI assistants with accurate information about NixOS packages, options, Home Manager, and nix-darwin configurations. +- [namaka](https://github.com/nix-community/namaka) - Snapshot testing for Nix based on haumea. +- [nil](https://github.com/oxalica/nil) - NIx Language server, an incremental analysis assistent for writing in Nix. +- [niv](https://github.com/nmattia/niv/) - Easy dependency management for Nix projects with package pinning. +- [nix2container](https://github.com/nlewo/nix2container) - An efficient container building workflow with Nix. +- [nix-direnv](https://github.com/nix-community/nix-direnv) - A fast loader and flake-compliant configuration for the direnv environment auto-loader. +- [nix-health](https://github.com/juspay/nix-health) - A program to check the health of your Nix install. Furthermore, individual projects can configure their own health checks in their `flake.nix`. +- [nix-oci](https://github.com/Dauliac/nix-oci) - A flake-parts module for building minimal, reproducible OCI containers using nix2container. +- [nix-update](https://github.com/Mic92/nix-update) - Update versions/source hashes of nix packages. +- [nixd](https://github.com/nix-community/nixd) - Nix language server, based on Nix libraries. +- [nixpkgs-review](https://github.com/Mic92/nixpkgs-review) - The best tool to verify that a pull-request in Nixpkgs is building properly. +- [Nixtest](https://gitlab.com/TECHNOFAB/nixtest) - Testing framework for Nix, with snapshot and unit test support, JUnit generation etc. +- [npins](https://github.com/andir/npins) - A simple tool for handling different types of dependencies in a Nix project. It is inspired by and comparable to Niv. +- [pog](https://github.com/jpetrucciani/pog) - A new, powerful way to do bash scripts. Pog is a powerful Nix library that transforms the way developers create command-line interfaces (CLIs). +- [pre-commit-hooks.nix](https://github.com/cachix/git-hooks.nix) - Run linters/formatters at commit time and on your CI. +- [rnix-lsp](https://github.com/nix-community/rnix-lsp) - A syntax-checking language server for Nix. +- [robotnix](https://github.com/nix-community/robotnix) - A declarative and reproducible build system for Android (AOSP) images. +- [services-flake](https://github.com/juspay/services-flake) - A NixOS-like service configuration framework for Nix flakes. +- [Snowfall Lib](https://github.com/snowfallorg/lib) - A library that makes it easy to manage your Nix flake by imposing an opinionated file structure. +- [templates](https://github.com/nix-community/templates) - Project templates for many languages using Nix flakes. +- [treefmt-nix](https://github.com/numtide/treefmt-nix) - A formatter that allows formatting all your project files with a single command, all via a single `.nix` file. ## DevOps -* [Makes](https://github.com/fluidattacks/makes) - A Nix-based CI/CD pipeline framework for building, testing, and releasing projects in any language, from anywhere. -* [Nix GitLab CI](https://gitlab.com/TECHNOFAB/nix-gitlab-ci) - Define GitLab CI pipelines in pure Nix with full access to all Nix packages (incl. caching). -* [nixidy](https://github.com/arnarg/nixidy) - Kubernetes GitOps with Nix and Argo CD. -* [Standard](https://github.com/divnix/std) - An opinionated Nix Flakes framework to keep Nix code in large projects organized, accompanied by a friendly CLI/TUI optized for DevOps scenarios. +- [Makes](https://github.com/fluidattacks/makes) - A Nix-based CI/CD pipeline framework for building, testing, and releasing projects in any language, from anywhere. +- [Nix GitLab CI](https://gitlab.com/TECHNOFAB/nix-gitlab-ci) - Define GitLab CI pipelines in pure Nix with full access to all Nix packages (incl. caching). +- [nixidy](https://github.com/arnarg/nixidy) - Kubernetes GitOps with Nix and Argo CD. +- [Standard](https://github.com/divnix/std) - An opinionated Nix Flakes framework to keep Nix code in large projects organized, accompanied by a friendly CLI/TUI optized for DevOps scenarios. ## Programming Languages ### Arduino -* [nixduino](https://github.com/boredom101/nixduino) - Nix-based tool to help build Arduino sketches. +- [nixduino](https://github.com/boredom101/nixduino) - Nix-based tool to help build Arduino sketches. ### Clojure -* [clj-nix](https://github.com/jlesquembre/clj-nix) - Nix helper functions for Clojure projects. +- [clj-nix](https://github.com/jlesquembre/clj-nix) - Nix helper functions for Clojure projects. ### Crystal -* [crystal2nix](https://github.com/nix-community/crystal2nix) - Convert `shard.lock` into Nix expressions. +- [crystal2nix](https://github.com/nix-community/crystal2nix) - Convert `shard.lock` into Nix expressions. ### Elm -* [elm2nix](https://github.com/cachix/elm2nix) - Convert `elm.json` into Nix expressions. +- [elm2nix](https://github.com/cachix/elm2nix) - Convert `elm.json` into Nix expressions. ### Gleam -* [nix-gleam](https://github.com/arnarg/nix-gleam) - Generic Nix builder for Gleam applications. +- [nix-gleam](https://github.com/arnarg/nix-gleam) - Generic Nix builder for Gleam applications. ### Haskell -* [cabal2nix](https://github.com/NixOS/cabal2nix) - Converts a Cabal file into a Nix build expression. -* [haskell-flake](https://github.com/srid/haskell-flake) - A `flake-parts` Nix module for Haskell development. -* [haskell.nix](https://github.com/input-output-hk/haskell.nix) - Alternative Haskell Infrastructure for Nixpkgs. -* [nix-haskell-mode](https://github.com/matthewbauer/nix-haskell-mode) - Automatic Haskell setup in Emacs. -* [nixkell](https://github.com/pwm/nixkell) - A Haskell project template using Nix and direnv. +- [cabal2nix](https://github.com/NixOS/cabal2nix) - Converts a Cabal file into a Nix build expression. +- [haskell-flake](https://github.com/srid/haskell-flake) - A `flake-parts` Nix module for Haskell development. +- [haskell.nix](https://github.com/input-output-hk/haskell.nix) - Alternative Haskell Infrastructure for Nixpkgs. +- [nix-haskell-mode](https://github.com/matthewbauer/nix-haskell-mode) - Automatic Haskell setup in Emacs. +- [nixkell](https://github.com/pwm/nixkell) - A Haskell project template using Nix and direnv. ### Haxe -* [haxix](https://github.com/MadMcCrow/haxix) - Nix flake to build haxe/Heaps.io projects. -* [kebab](https://github.com/bwkam/kebab) - Haxe packages for Nix. + +- [haxix](https://github.com/MadMcCrow/haxix) - Nix flake to build haxe/Heaps.io projects. +- [kebab](https://github.com/bwkam/kebab) - Haxe packages for Nix. ### Julia -* [Manifest2Nix.jl](https://codeberg.org/aniva/Manifest2Nix.jl) - A Nix library for creating reproducible Julia builds and experiments via precompilation. +- [Manifest2Nix.jl](https://codeberg.org/aniva/Manifest2Nix.jl) - A Nix library for creating reproducible Julia builds and experiments via precompilation. ### Lean -* [lean4-nix](https://github.com/lenianiva/lean4-nix) - Nix flake build for Lean 4, and `lake2nix`. +- [lean4-nix](https://github.com/lenianiva/lean4-nix) - Nix flake build for Lean 4, and `lake2nix`. ### Node.js -* [Napalm](https://github.com/nix-community/napalm) - Support for building npm packages in Nix with a lightweight npm registry. -* [node2nix](https://github.com/svanderburg/node2nix) - Generate Nix expression from a `package.json` (or `package-lock.json`) (to be stored as files). -* [npmlock2nix](https://github.com/nix-community/npmlock2nix) - Generate Nix expressions from a `package-lock.json` (in-memory), primarily for web projects. +- [Napalm](https://github.com/nix-community/napalm) - Support for building npm packages in Nix with a lightweight npm registry. +- [node2nix](https://github.com/svanderburg/node2nix) - Generate Nix expression from a `package.json` (or `package-lock.json`) (to be stored as files). +- [npmlock2nix](https://github.com/nix-community/npmlock2nix) - Generate Nix expressions from a `package-lock.json` (in-memory), primarily for web projects. ### OCaml -* [opam2nix](https://github.com/timbertson/opam2nix) - Generate Nix expressions from opam packages. +- [opam2nix](https://github.com/timbertson/opam2nix) - Generate Nix expressions from opam packages. ### PHP -* [composer-plugin-nixify](https://github.com/stephank/composer-plugin-nixify) - Composer plugin to help with Nix packaging. -* [composer2nix](https://github.com/svanderburg/composer2nix) - Generate Nix expressions to build composer packages. -* [composition-c4](https://github.com/fossar/composition-c4) - Support for building composer packages from a `composer.lock` (using IFD). -* [nix-phps](https://github.com/fossar/nix-phps) - Flake containing old and unmaintained PHP versions (intended for CI use). -* [nix-shell](https://github.com/loophp/nix-shell/) - Nix shells for PHP development. +- [composer-plugin-nixify](https://github.com/stephank/composer-plugin-nixify) - Composer plugin to help with Nix packaging. +- [composer2nix](https://github.com/svanderburg/composer2nix) - Generate Nix expressions to build composer packages. +- [composition-c4](https://github.com/fossar/composition-c4) - Support for building composer packages from a `composer.lock` (using IFD). +- [nix-phps](https://github.com/fossar/nix-phps) - Flake containing old and unmaintained PHP versions (intended for CI use). +- [nix-shell](https://github.com/loophp/nix-shell/) - Nix shells for PHP development. ### PureScript -* [Easy PureScript Nix](https://github.com/justinwoo/easy-purescript-nix) - A project to easily use PureScript and other tools with Nix. -* [purs-nix](https://github.com/purs-nix/purs-nix) - CLI and library combo designed for managing PureScript projects using Nix. It provides a Nix API that can be used within your projects, as well as a command-line interface for managing your development process. +- [Easy PureScript Nix](https://github.com/justinwoo/easy-purescript-nix) - A project to easily use PureScript and other tools with Nix. +- [purs-nix](https://github.com/purs-nix/purs-nix) - CLI and library combo designed for managing PureScript projects using Nix. It provides a Nix API that can be used within your projects, as well as a command-line interface for managing your development process. ### Python -* [poetry2nix](https://github.com/nix-community/poetry2nix) - Build Python packages directly from [Poetry's](https://python-poetry.org/) `poetry.lock`. No conversion step needed. -* [uv2nix](https://github.com/pyproject-nix/uv2nix) - Convert [`uv` workspaces](https://docs.astral.sh/uv/concepts/projects/workspaces/) into pure Nix derivations. +- [poetry2nix](https://github.com/nix-community/poetry2nix) - Build Python packages directly from [Poetry's](https://python-poetry.org/) `poetry.lock`. No conversion step needed. +- [uv2nix](https://github.com/pyproject-nix/uv2nix) - Convert [`uv` workspaces](https://docs.astral.sh/uv/concepts/projects/workspaces/) into pure Nix derivations. ### Ruby -* [Bundix](https://github.com/nix-community/bundix) - Generates a Nix expression for your Bundler-managed application. -* [ruby-nix](https://github.com/inscapist/ruby-nix) - Generates reproducible ruby/bundler app environment with Nix. +- [Bundix](https://github.com/nix-community/bundix) - Generates a Nix expression for your Bundler-managed application. +- [ruby-nix](https://github.com/inscapist/ruby-nix) - Generates reproducible ruby/bundler app environment with Nix. ### Rust -* [cargo2nix](https://github.com/cargo2nix/cargo2nix) - Granular caching, development shell, Nix & Rust integration. -* [crane](https://github.com/ipetkov/crane) - A Nix library for building Cargo projects with incremental artifact caching. -* [fenix](https://github.com/nix-community/fenix) - Rust toolchains and Rust analyzer nightly for nix. -* [naersk](https://github.com/nix-community/naersk) - Build Rust packages directly from `Cargo.lock`. No conversion step needed. -* [nix-cargo-integration](https://github.com/90-008/nix-cargo-integration) - A library that allows easy and effortless integration for Cargo projects. -* [nixpkgs-mozilla](https://github.com/mozilla/nixpkgs-mozilla) - Mozilla's overlay with Rust toolchains and Firefox. -* [rust-nix-templater](https://github.com/90-008/rust-nix-templater) - Generates Nix build and development files for Rust projects. -* [rust-overlay](https://github.com/oxalica/rust-overlay) - Pure and reproducible nix overlay of binary distributed Rust toolchains. +- [cargo2nix](https://github.com/cargo2nix/cargo2nix) - Granular caching, development shell, Nix & Rust integration. +- [crane](https://github.com/ipetkov/crane) - A Nix library for building Cargo projects with incremental artifact caching. +- [fenix](https://github.com/nix-community/fenix) - Rust toolchains and Rust analyzer nightly for nix. +- [naersk](https://github.com/nix-community/naersk) - Build Rust packages directly from `Cargo.lock`. No conversion step needed. +- [nix-cargo-integration](https://github.com/90-008/nix-cargo-integration) - A library that allows easy and effortless integration for Cargo projects. +- [nixpkgs-mozilla](https://github.com/mozilla/nixpkgs-mozilla) - Mozilla's overlay with Rust toolchains and Firefox. +- [rust-nix-templater](https://github.com/90-008/rust-nix-templater) - Generates Nix build and development files for Rust projects. +- [rust-overlay](https://github.com/oxalica/rust-overlay) - Pure and reproducible nix overlay of binary distributed Rust toolchains. ### Scala -* [sbt-derivation](https://github.com/zaninime/sbt-derivation) - mkDerivation for sbt, similar to buildGoModule. +- [sbt-derivation](https://github.com/zaninime/sbt-derivation) - mkDerivation for sbt, similar to buildGoModule. ### Zig -* [zig2nix](https://github.com/Cloudef/zig2nix) - Flake for packaging, building and running Zig projects. -* [zon2nix](https://github.com/nix-community/zon2nix) - Convert the dependencies in `build.zig.zon` to a Nix expression. +- [zig2nix](https://github.com/Cloudef/zig2nix) - Flake for packaging, building and running Zig projects. +- [zon2nix](https://github.com/nix-community/zon2nix) - Convert the dependencies in `build.zig.zon` to a Nix expression. ## NixOS Modules -* [base16.nix](https://github.com/SenchoPens/base16.nix) - Flake way to theme programs in [base16](https://github.com/chriskempson/base16) colorschemes, mustache template support included. -* [Home Manager](https://github.com/nix-community/home-manager) - Manage your user configuration just like NixOS. -* [impermanence](https://github.com/nix-community/impermanence) - Lets you choose what files and directories you want to keep between reboots. -* [musnix](https://github.com/musnix/musnix) - Do real-time audio work in NixOS. -* [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin) - Modules and packages for Bitcoin nodes with higher-layer protocols with an emphasis on security. -* [nix-darwin](https://github.com/nix-darwin/nix-darwin) - Manage macOS configuration just like on NixOS. -* [nix-mineral](https://github.com/cynicsketch/nix-mineral) - Conveniently and reasonably harden NixOS. -* [nix-topology](https://github.com/oddlama/nix-topology) - Generate infrastructure and network diagrams directly from your NixOS configuration. -* [NixOS hardware](https://github.com/NixOS/nixos-hardware) - NixOS profiles to optimize settings for different hardware. -* [NixOS-WSL](https://github.com/nix-community/NixOS-WSL) - Modules for running NixOS on the Windows Subsystem for Linux. -* [NixVim](https://github.com/nix-community/nixvim) - A Neovim distribution built with Nix modules and Nixpkgs. -* [Self Host Blocks](https://github.com/ibizaman/selfhostblocks) - Modular server management based on NixOS modules and focused on best practices. -* [Simple Nixos Mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) - A complete mailserver, managed with NixOS modules. -* [Stylix](https://github.com/nix-community/stylix) - System-wide colorscheming and typography for NixOS. +- [base16.nix](https://github.com/SenchoPens/base16.nix) - Flake way to theme programs in [base16](https://github.com/chriskempson/base16) colorschemes, mustache template support included. +- [Home Manager](https://github.com/nix-community/home-manager) - Manage your user configuration just like NixOS. +- [impermanence](https://github.com/nix-community/impermanence) - Lets you choose what files and directories you want to keep between reboots. +- [musnix](https://github.com/musnix/musnix) - Do real-time audio work in NixOS. +- [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin) - Modules and packages for Bitcoin nodes with higher-layer protocols with an emphasis on security. +- [nix-darwin](https://github.com/nix-darwin/nix-darwin) - Manage macOS configuration just like on NixOS. +- [nix-mineral](https://github.com/cynicsketch/nix-mineral) - Conveniently and reasonably harden NixOS. +- [nix-topology](https://github.com/oddlama/nix-topology) - Generate infrastructure and network diagrams directly from your NixOS configuration. +- [NixOS hardware](https://github.com/NixOS/nixos-hardware) - NixOS profiles to optimize settings for different hardware. +- [NixOS-WSL](https://github.com/nix-community/NixOS-WSL) - Modules for running NixOS on the Windows Subsystem for Linux. +- [NixVim](https://github.com/nix-community/nixvim) - A Neovim distribution built with Nix modules and Nixpkgs. +- [Self Host Blocks](https://github.com/ibizaman/selfhostblocks) - Modular server management based on NixOS modules and focused on best practices. +- [Simple Nixos Mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) - A complete mailserver, managed with NixOS modules. +- [Stylix](https://github.com/nix-community/stylix) - System-wide colorscheming and typography for NixOS. ## NixOS Configuration Editors ### Desktop apps -* [Nix Software Center](https://github.com/snowfallorg/nix-software-center) - Install and manage Nix packages. Desktop app in Rust and GTK. -* [NixOS Configuration Editor](https://github.com/snowfallorg/nixos-conf-editor) - Graphical editor for NixOS configuration. Desktop app in Rust and GTK. +- [Nix Software Center](https://github.com/snowfallorg/nix-software-center) - Install and manage Nix packages. Desktop app in Rust and GTK. +- [NixOS Configuration Editor](https://github.com/snowfallorg/nixos-conf-editor) - Graphical editor for NixOS configuration. Desktop app in Rust and GTK. ### Webinterface -* [MyNixOS](https://mynixos.com/) - Graphical editor for Nix flakes. Create and manage configurations and modules for NixOS and Nix home-manager. Rather a Nix generator than a Nix editor, because it does not allow to import Nix files. +- [MyNixOS](https://mynixos.com/) - Graphical editor for Nix flakes. Create and manage configurations and modules for NixOS and Nix home-manager. Rather a Nix generator than a Nix editor, because it does not allow to import Nix files. ## Overlays -* [awesome-nix-hpc](https://github.com/freuk/awesome-nix-hpc) - High Performance Computing package sets. -* [chaotic-nyx](https://github.com/chaotic-cx/nyx) - Daily bumped bleeding edge packages like `mesa_git` & others that aren't yet in Nixpkgs. Created by the makers of [Chaotic-AUR](https://github.com/chaotic-aur/). -* [neovim-nightly-overlay](https://github.com/nix-community/neovim-nightly-overlay) - Daily bumped Neovim nightly package. -* [nixpkgs-firefox-darwin](https://github.com/bandithedoge/nixpkgs-firefox-darwin) - Automatically updated Firefox binary packages for macOS. -* [nixpkgs-wayland](https://github.com/nix-community/nixpkgs-wayland) - Bleeding-edge Wayland packages. -* [NUR](https://github.com/nix-community/NUR/) - Nix User Repositories. The mother of all overlays, allowing access to user repositories and installing packages via attributes. -* [System Manager](https://github.com/numtide/system-manager) - A non-NixOS Linux system configuration tool built on Nix. -* [zig-overlay](https://github.com/mitchellh/zig-overlay) - A Nix flake packaging the Zig compiler. The flake mirrors the binaries built officially by Zig and does not build them from source. +- [awesome-nix-hpc](https://github.com/freuk/awesome-nix-hpc) - High Performance Computing package sets. +- [chaotic-nyx](https://github.com/chaotic-cx/nyx) - Daily bumped bleeding edge packages like `mesa_git` & others that aren't yet in Nixpkgs. Created by the makers of [Chaotic-AUR](https://github.com/chaotic-aur/). +- [neovim-nightly-overlay](https://github.com/nix-community/neovim-nightly-overlay) - Daily bumped Neovim nightly package. +- [nixpkgs-firefox-darwin](https://github.com/bandithedoge/nixpkgs-firefox-darwin) - Automatically updated Firefox binary packages for macOS. +- [nixpkgs-wayland](https://github.com/nix-community/nixpkgs-wayland) - Bleeding-edge Wayland packages. +- [NUR](https://github.com/nix-community/NUR/) - Nix User Repositories. The mother of all overlays, allowing access to user repositories and installing packages via attributes. +- [System Manager](https://github.com/numtide/system-manager) - A non-NixOS Linux system configuration tool built on Nix. +- [zig-overlay](https://github.com/mitchellh/zig-overlay) - A Nix flake packaging the Zig compiler. The flake mirrors the binaries built officially by Zig and does not build them from source. ## Distributions -* [nixbsd](https://github.com/nixos-bsd/nixbsd) - A NixOS fork with a FreeBSD kernel. -* [NixNG](https://github.com/nix-community/NixNG) - A GNU/Linux distribution similar to NixOS, defining difference is a focus on containers and lightweightness. -* [SnowflakeOS](https://snowflakeos.org/) - A NixOS-based Linux distribution focused on beginner friendliness and ease of use. +- [nixbsd](https://github.com/nixos-bsd/nixbsd) - A NixOS fork with a FreeBSD kernel. +- [NixNG](https://github.com/nix-community/NixNG) - A GNU/Linux distribution similar to NixOS, defining difference is a focus on containers and lightweightness. +- [SnowflakeOS](https://snowflakeos.org/) - A NixOS-based Linux distribution focused on beginner friendliness and ease of use. ## Community -* [#nix:nixos.org](https://matrix.to/#/#nix:nixos.org) -* [#nixos on Libera.Chat](https://web.libera.chat/?nick=Guest?#nixos) -* [Discord - Nix/Nixos (Unofficial)](https://discord.gg/BMUCQx6) -* [Discourse](https://discourse.nixos.org/) - The best place to get help and discuss Nix-related topics. -* [NixCon](https://nixcon.org/) - The annual community conference for contributors and users of Nix and NixOS. -* [Wiki (Official)](https://wiki.nixos.org) -* [Wiki (Unofficial)](https://nixos.wiki) +- [#nix:nixos.org](https://matrix.to/#/#nix:nixos.org) +- [#nixos on Libera.Chat](https://web.libera.chat/?nick=Guest?#nixos) +- [Discord - Nix/Nixos (Unofficial)](https://discord.gg/BMUCQx6) +- [Discourse](https://discourse.nixos.org/) - The best place to get help and discuss Nix-related topics. +- [NixCon](https://nixcon.org/) - The annual community conference for contributors and users of Nix and NixOS. +- [Wiki (Official)](https://wiki.nixos.org) +- [Wiki (Unofficial)](https://nixos.wiki) diff --git a/docs/den.md b/docs/den.md new file mode 100644 index 00000000..efb85794 --- /dev/null +++ b/docs/den.md @@ -0,0 +1,899 @@ +# Den — Deep Reference + +[Den](https://github.com/denful/den) is an **aspect-oriented, context-driven** framework for Nix configurations. It sits on top of the [Dendritic Pattern](./dendritic-patterns.md) and takes feature organization from the **file level** to the **function level**: aspects are reusable bundles that produce the right modules for NixOS, nix-darwin, Home Manager, and custom classes depending on _where_ they are applied (host, user, home, fleet, …). + +Official upstream docs: [den.denful.dev](https://den.denful.dev) + +This document explains Den in depth and documents **how this repository uses it**. + +--- + +## Table of contents + +1. [Why Den exists](#why-den-exists) +2. [Mental model](#mental-model) +3. [Core vocabulary](#core-vocabulary) +4. [The context pipeline](#the-context-pipeline) +5. [Entities: hosts, users, homes](#entities-hosts-users-homes) +6. [Aspects](#aspects) +7. [Nix classes](#nix-classes) +8. [Custom classes and forwarding](#custom-classes-and-forwarding) +9. [Schema](#schema) +10. [Policies](#policies) +11. [Batteries](#batteries) +12. [Flake integration](#flake-integration) +13. [den.lib API surface](#denlib-api-surface) +14. [This repository’s Den layout](#this-repositorys-den-layout) +15. [Patterns and recipes](#patterns-and-recipes) +16. [Pitfalls and debugging](#pitfalls-and-debugging) +17. [Further reading](#further-reading) + +--- + +## Why Den exists + +The Dendritic Pattern solves **feature locality**: Brave, Neovim, and theming live in one file each, exporting `flake.modules.*` for every class they touch. + +Den solves the **next layer of complexity**: + +| Problem | Dendritic alone | With Den | +| -------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | +| Declaring `nixosConfigurations` / `darwinConfigurations` | Hand-written in `flake.nix` or a `configurations.nix` | `den.hosts.*` → auto-generated flake outputs | +| Same feature on laptop + server + Mac | Import merged modules in each host file | `den.aspects.workstation` + `includes` on host aspects | +| NixOS + Darwin share 90% of system config | Duplicate bodies or heavy `mkIf` | Custom class `os` forwards once to both | +| User config depends on which host they’re on | `specialArgs` + conditionals everywhere | Parametric aspects `{ host, user }: …` | +| Standalone HM vs embedded HM | Separate wiring | `den.homes.*` + policies unify topology | + +Den’s tagline: **parametric configurations at the function level**. An aspect is not just a static module — it can be a function of context (`host`, `user`, `home`, …) that emits different Nix modules per class. + +Den has **zero runtime dependencies** outside Nix itself. `den.lib` is domain-agnostic; the OS framework (NixOS / nix-darwin / HM) is optional layers you opt into. + +--- + +## Mental model + +Think of Den as a **declarative data transformation pipeline**: + +```mermaid +flowchart LR + subgraph declare [You declare] + Hosts[den.hosts.*] + Homes[den.homes.*] + Aspects[den.aspects.*] + Schema[den.schema.*] + Policies[den.policies.*] + end + + subgraph pipeline [Den resolves] + Topology[Entity topology] + Includes[Aspect includes chains] + Classes[Class dispatch nixos darwin homeManager os ...] + Modules[deferredModule per class] + end + + subgraph outputs [Flake outputs] + NixOS[nixosConfigurations] + Darwin[darwinConfigurations] + HM[homeConfigurations] + PerSystem[packages checks devShells ...] + end + + Hosts --> Topology + Homes --> Topology + Aspects --> Includes + Schema --> Topology + Policies --> Classes + Topology --> Includes + Includes --> Classes + Classes --> Modules + Modules --> NixOS + Modules --> Darwin + Modules --> HM + Modules --> PerSystem +``` + +**You declare topology and features.** Den walks entities (hosts → users → homes), resolves aspect trees, dispatches class keys (`nixos`, `darwin`, `homeManager`, `os`, …), and produces merged modules. Host/home **instantiation** (`lib.nixosSystem`, `lib.darwinSystem`, `homeManagerConfiguration`) happens via policies at the end of the pipeline. + +--- + +## Core vocabulary + +| Term | Meaning | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| **Entity** | A named thing in your infra: host, user (on a host), standalone home, fleet, … | +| **Aspect** | A named configuration bundle under `den.aspects.`. Can be an attrset of class modules or a function `{ host, ... }: { nixos = ...; }` | +| **Class** | A Nix module “channel”: `nixos`, `darwin`, `homeManager`, `user`, `os`, `packages`, … | +| **Include** | Aspect composition: `includes = [ otherAspect den.batteries.foo ]` merges another aspect’s resolution into this one | +| **Policy** | Directed edge in the pipeline: “when host X is resolved, also instantiate flake output” or “route `os` class into `nixos`” | +| **Schema** | Base deferred modules per entity kind (`den.schema.host`, `den.schema.user`, …) — typed fields, defaults, shared includes | +| **Battery** | Reusable aspect or helper shipped with Den (`den.batteries.primary-user`, `den.batteries.os-class`, …) | +| **Context** | The set of entity bindings available during aspect resolution (`host`, `user`, `home`, …) | +| **Resolved** | Read-only option on entities: `config.resolved` → fully merged aspect tree for that entity | +| **mainModule** | Internal: `den.lib.aspects.resolve host.class host.resolved` — the module list passed to `nixosSystem` / `darwinSystem` | + +### Aspect naming convention + +By default, a host named `mba` looks up `den.aspects.mba`: + +```nix +# nix/lib/entities/_types.nix (simplified) +lookupAspect = den: config: + if den.aspects ? ${config.name} then + den.aspects.${config.name} + else + lib.warn "den.aspects.${config.name} not defined — entity gets empty aspect" { }; +``` + +If `den.aspects.mba` is missing, evaluation continues with an **empty aspect** and a warning — not a hard error. You can override `aspect` on the host entry explicitly when names diverge. + +--- + +## The context pipeline + +Den resolves aspects through a fixed handler pipeline (`den.lib.aspects.fx.pipeline`). Handlers run in order to: + +1. **Establish scope** — push/pop context (`host`, `user`, …) +2. **Process includes** — walk `includes` lists, detect cycles, deduplicate +3. **Compile class bodies** — turn aspect keys into deferred modules +4. **Forward classes** — map custom classes (e.g. `os` → `nixos`) +5. **Dispatch policies** — fire route/instantiate/provide effects +6. **Collect imports** — merge all modules for the requested class + +Public resolution entry points (`den.lib.aspects`): + +| Function | Purpose | +| ----------------------------------- | --------------------------------------------------------- | +| `resolve class aspectTree` | Full resolution → single deferred module for `class` | +| `resolveImports class aspectTree` | Same but skips entity instantiation (nested extraction) | +| `resolveWithState class aspectTree` | Full pipeline result including internal state (debugging) | + +Manual resolution (outside the flake output pipeline): + +```nix +let + aspect = den.lib.aspects.resolve "nixos" + (den.aspects.my-aspect { host = den.hosts.x86_64-linux.my-laptop; }); +in +lib.nixosSystem { modules = [ aspect ]; } +``` + +### Parametric vs static aspects + +**Static aspect** — plain attrset of class modules: + +```nix +den.aspects.igloo = { + nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.hello ]; }; + homeManager.programs.direnv.enable = true; +}; +``` + +**Parametric aspect** — function receiving context: + +```nix +den.aspects.cooper = { user, ... }: { + nixos.users.users.${user.userName}.description = "Alice Cooper"; +}; + +den.aspects.setHost = { host, ... }: { + networking.hostName = host.hostName; +}; +``` + +Parametric aspects only emit configuration when their required context keys are present in the current pipeline scope. That is how user-only or host-only snippets live in shared aspect files without `mkIf` spam. + +### `den.default` + +Global includes applied to every resolution root (via `resolveEntity` for kind `"default"`): + +```nix +# Built into Den — os class routing +den.default.includes = [ den.policies.os-to-host ]; +``` + +Use `den.default.includes` in your flake to inject batteries or policies everywhere. + +--- + +## Entities: hosts, users, homes + +### Host declaration + +Hosts live under **`den.hosts..`**: + +```nix +den.hosts.aarch64-darwin.mba = { + instantiate = darwinSystemWithInputs; # optional override +}; +``` + +Each host entity gets (from `nix/lib/entities/host.nix`): + +| Field | Default | Role | +| ------------- | ----------------------------------------------------------------- | --------------------------------------- | +| `name` | attr name (`mba`) | Logical name | +| `hostName` | same as `name` | `networking.hostName` unless overridden | +| `system` | parent key (`aarch64-darwin`) | Platform string | +| `class` | `darwin` if system ends with `-darwin`, else `nixos` | Which OS module class to resolve | +| `aspect` | `den.aspects.` | Aspect tree for this host | +| `instantiate` | `nixpkgs.lib.nixosSystem` or `darwin.lib.darwinSystem` | Builder function | +| `intoAttr` | `["nixosConfigurations" name]` or `["darwinConfigurations" name]` | Where to put flake output | +| `mainModule` | `resolve class resolved` | Module passed to `instantiate` | +| `users` | `{}` | Users on this host | + +**Instantiation** (policy `den.lib.policy.instantiate`): + +```nix +host.instantiate { + modules = [ host.mainModule ]; + specialArgs = { ... }; # only if your instantiate wrapper adds them +} +``` + +The result is stored at `flake.` — e.g. `flake.darwinConfigurations.mba`. + +### Users on hosts + +```nix +den.hosts.x86_64-linux.igloo.users.alice = { }; +den.hosts.aarch64-darwin.apple.users.alice = { }; +``` + +User entities carry: + +- `userName`, `name`, `aspect` (default `den.aspects.`) +- `classes` — home environment classes (default `["user"]`; set `["homeManager"]` to enable HM) +- `host` — back-reference to parent host + +Host aspects can push defaults to all users via `provides.to-users`: + +```nix +den.aspects.workstation.provides.to-users = { + homeManager = { pkgs, ... }: { + programs.vim.enable = true; + }; +}; +``` + +### Standalone homes + +```nix +den.homes.aarch64-darwin."vic@mac" = { }; +# or +den.homes.x86_64-linux.alice = { }; +``` + +Homes get their own aspect lookup (`den.aspects.`), `intoAttr` defaulting to `homeConfigurations`, and optional binding to a host’s `osConfig` for HM modules that need system context. + +This repo’s **`8amps-linux`** HM config is still hand-rolled in `modules/configurations.nix` — a future `den.homes.*` migration would give it the same aspect resolution as embedded users. + +--- + +## Aspects + +An aspect is the **unit of reusable configuration**. Structure: + +```nix +den.aspects.my-feature = { + # Composition + includes = [ + config.den.aspects.styling + den.batteries.primary-user + (den.batteries.user-shell "fish") + ]; + + # Class bodies (deferred modules or attrsets) + nixos = { pkgs, ... }: { ... }; + darwin = { pkgs, ... }: { ... }; + homeManager = { pkgs, lib, config, ... }: { ... }; + os = { ... }: { ... }; # forwarded to nixos + darwin via policy + + # Sub-aspects (namespaced under this aspect) + provides.emulation = { + nixos = { ... }: { ... }; + }; + + # Inline policies (cross-scope effects) + policies.to-igloo = { host, user, ... }: + lib.optional (host.name == "igloo") ( + den.lib.policy.provide { + class = "nixos"; + module.programs.nh.enable = true; + } + ); +}; +``` + +### `includes` + +- **Order matters** for merge semantics (later modules override earlier where options allow). +- Can reference other aspects, batteries, inline let-bound aspects, or angle-bracket batteries: ``. +- Cycles are detected (`checkDedupHandler` in the pipeline). + +### `provides.*` + +Nested aspect subtrees. Useful for grouping sub-features: + +```nix +den.aspects.gaming.provides.emulation.nixos = { pkgs, ... }: { ... }; + +# Consumed via: +includes = [ den.aspects.gaming.provides.emulation ]; +``` + +### Class key forms + +Den accepts several shapes for class modules: + +1. **Submodule function** — `{ pkgs, ... }: { config = ...; }` +2. **Flat attrset** — `homeManager.programs.direnv.enable = true` (shorthand) +3. **Bare deferred module** — passed directly as aspect value in advanced cases + +Flat form is sugar; the pipeline normalizes to deferred modules. + +### Collision policy + +When Den context args (`host`, `user`) collide with normal module args in flat class modules: + +```nix +den.config.classModuleCollisionPolicy = "error"; # default +# or "class-wins" | "den-wins" + +# Per-entity override: +den.schema.host.collisionPolicy = "den-wins"; +``` + +--- + +## Nix classes + +Classes are keys on aspects that dispatch to module types. Built-in OS classes: + +| Class | Description | +| ------------- | -------------------------------------------------- | +| `nixos` | NixOS system module | +| `darwin` | nix-darwin system module | +| `homeManager` | Home Manager module | +| `user` | Forwards into `{nixos\|darwin}.users.users.` | + +Flake output classes (when using flake-parts integration): + +| Class | Routes to | +| ----------------------------------------------------------- | ----------------------------------- | +| `packages`, `apps`, `checks`, `devShells`, `legacyPackages` | `flake..` | +| `flake`, `flake-parts` | Top-level flake / perSystem modules | + +Aspects can **register new classes**: + +```nix +den.aspects.my-stuff.classes.hjem = { + description = "Home Manager alternative"; +}; +``` + +Registered classes merge into `den.classes` via `modules/aspect-schema.nix`. + +--- + +## Custom classes and forwarding + +### The `os` class (built-in battery) + +The most important custom class for multi-platform repos. **`os`** means “apply this to both NixOS and nix-darwin”: + +```nix +den.aspects.my-host = { + os.networking.hostName = "foo"; + # equivalent to setting hostName in both nixos and darwin bodies +}; +``` + +Implementation (`modules/aspects/batteries/os-class.nix`): + +- Registers `den.classes.os` +- Adds `den.policies.os-to-host` to `den.default.includes` +- Policy routes `fromClass = "os"` → `intoClass = host.class` when class is `nixos` or `darwin` + +**This repo uses `os` in `den-aspects/styling.nix`** for shared Stylix + `dendritic.theme.*` options, with `nixos`-only and `darwin`-only extras in sibling keys. + +### `den.batteries.forward` + +General mechanism for custom classes. Example — platform-specific HM: + +```nix +den.aspects.hmPlatforms = { class, aspect-chain }: den.batteries.forward { + each = [ "Linux" "Darwin" ]; + fromClass = platform: "hm${platform}"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + guard = { pkgs, ... }: platform: lib.mkIf pkgs.stdenv."is${platform}"; + adaptArgs = { config, ... }: { osConfig = config; }; +}; + +den.aspects.tux = { + includes = [ den.aspects.hmPlatforms ]; + hmDarwin = { pkgs, ... }: { home.packages = [ pkgs.iterm2 ]; }; + hmLinux = { pkgs, ... }: { home.packages = [ pkgs.wl-clipboard-rs ]; }; +}; +``` + +Parameters: + +| Param | Role | +| ------------ | ------------------------------------------------------------------ | +| `each` | List of items to iterate | +| `fromClass` | Source class name(s) on the aspect | +| `intoClass` | Target class | +| `intoPath` | Path within target config (e.g. `[ "environment" "persistence" ]`) | +| `guard` | Conditional wrapper — only forward when guard returns true | +| `adaptArgs` | Inject extra module args (e.g. `osConfig`) | +| `fromAspect` | Which aspect node supplies the source modules | + +Custom classes are how Den implements `user`, `hjem`, `wsl`, `microvm`, and how you can build role-based or capability-based routing. + +--- + +## Schema + +`den.schema.` defines **base deferred modules** for entity kinds: + +```nix +den.schema.host = { host, lib, ... }: { + options.roles = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; }; +}; + +den.schema.user.includes = [ den.batteries.define-user ]; + +den.schema.user.classes = lib.mkDefault [ "homeManager" ]; +``` + +Default schema kinds: `conf`, `fleet`, `host`, `user`, `home`, `flake`, `flake-system`. + +Schema entries support: + +- **`includes` / `excludes`** — aspect chains applied to every entity of that kind +- **`collisionPolicy`** — per-kind collision handling +- **Custom options** — become fields on host/user/home records + +Entity **`id_hash`**: auto-computed stable identity for comparing entities without fragile `==` on module thunks. + +Every entity with structural content gets **`config.resolved`** — the merged aspect tree after includes, used to build `mainModule`. + +--- + +## Policies + +Policies declare **directed edges** in the resolution graph. Registry: `den.policies.`. + +### Built-in flake policies (`modules/policies/flake.nix`) + +```text +flake + └─ flake-to-systems → one flake-system per system + ├─ system-to-os-outputs → each host → resolve + instantiate + └─ system-to-hm-outputs → each home → resolve + instantiate +``` + +`den.lib.policy.instantiate host` calls: + +```nix +host.instantiate { modules = [ host.mainModule ]; } +``` + +and assigns the result to `config.flake` at `host.intoAttr`. + +### Policy helpers + +| Helper | Purpose | +| ---------------------------------------------------------- | ----------------------------------------------------------- | +| `den.lib.policy.route { fromClass; intoClass; path; ... }` | Route modules from one class to another location | +| `den.lib.policy.provide { class; module; }` | Inject a module cross-scope (e.g. user aspect → host nixos) | +| `den.lib.policy.include { ... }` | Shorthand include effect | +| `den.lib.policy.instantiate entity` | Produce flake configuration output | +| `den.lib.policy.resolve.to target ctx` | Navigate pipeline to a target kind | + +### Aspect-local policies + +Aspects can define `policies.` as functions `{ host, user, ... }: [ effects ]`: + +```nix +den.aspects.alice.policies.to-igloo = { host, user, ... }: + lib.optional (host.name == "igloo") ( + den.lib.policy.provide { + class = "nixos"; + module.programs.nh.enable = true; + } + ); +``` + +Remember to add `includes = [ den.aspects.alice.policies.to-igloo ]` (or rely on a parent include) so the policy participates in resolution. + +--- + +## Batteries + +Batteries are reusable aspects/helpers under `den.batteries.*`: + +| Battery | Purpose | +| -------------------------------- | -------------------------------------------------------- | +| `primary-user` | Mark user as admin / primary on host | +| `define-user` | Standard user option schema | +| `hostname` | Set hostname from entity | +| `user-shell "fish"` | Parametric default shell | +| `import-tree` | Filesystem auto-import into host/user/home schema | +| `unfree` / `insecure` | Predicate builders for nixpkgs config | +| `vm-autologin` / `tty-autologin` | VM/getty helpers | +| `os-class` | The `os` forwarding policy (always on via `den.default`) | + +Parametric batteries are functors: + +```nix +includes = [ + (den.batteries.user-shell "zsh") + (den.batteries.unfree [ "vscode" ]) +]; +``` + +Angle-bracket syntax resolves batteries from Den’s namespace: + +```nix +includes = [ ]; +``` + +--- + +## Flake integration + +### Minimal setup (this repo) + +**1. Input** + +```nix +# flake.nix +inputs.den.url = "github:denful/den"; +``` + +**2. Import flake module** + +```nix +# modules/host-topology-den.nix +imports = [ + inputs.den.flakeModule + ../den-aspects/styling.nix +]; +``` + +`inputs.den.flakeModule` auto-imports all modules under Den’s `modules/` tree (options, policies, batteries, outputs). + +**3. Declare hosts and aspects** (see [This repository’s Den layout](#this-repositorys-den-layout)). + +### flakeOutputs merge semantics + +Den defines `options.flake.nixosConfigurations`, `flake.darwinConfigurations`, `flake.homeConfigurations`, etc. with **lazy attr merge** so multiple modules can contribute. + +If you see: + +```text +If you see this message it likely means you have more than +one value for a flake output that was expected to be unique. +``` + +Import the matching output helper: + +```nix +imports = [ inputs.den.flakeOutputs.nixosConfigurations ]; +``` + +Or define your own merge strategy on `options.flake.`. + +### Without flake-parts + +Den works standalone via `imports = [ inputs.den.flakeModule ]` — see `templates/minimal` and `templates/noflake`. + +### With flake-parts (this repo) + +Your existing `flake-parts.lib.mkFlake` evaluation merges Den’s `systems`, `perSystem`, and `flake` options. Den’s `modules/outputs.nix` resolves `flake-parts` class modules from aspects when `inputs.flake-parts` is present. + +### Custom `instantiate` (required here) + +Den’s default `darwinSystem` / `nixosSystem` do **not** pass `specialArgs.inputs`. This repo’s host files destructure `inputs` at module top level: + +```nix +# hosts/darwin/mba/default.nix +{ inputs, pkgs, lib, ... }: { ... } +``` + +Fix in `host-topology-den.nix`: + +```nix +withInputs = builder: args: + builder (args // { + specialArgs = (args.specialArgs or { }) // { inherit inputs; }; + }); + +den.hosts.aarch64-darwin.mba = { + instantiate = withInputs inputs.nix-darwin.lib.darwinSystem; +}; +``` + +Any new host that uses `inputs.*` in its module tree needs this wrapper (or an equivalent `specialArgs` injection on the host entity). + +--- + +## den.lib API surface + +| Path | Role | +| -------------------------------- | ---------------------------------------- | +| `den.lib.aspects.resolve` | Resolve aspect tree → module for class | +| `den.lib.aspects.resolveImports` | Resolve without instantiation | +| `den.lib.aspects.hasAspect` | Introspection helpers | +| `den.lib.resolveEntity kind ctx` | Build resolution root for entity kind | +| `den.lib.policy.*` | Policy constructors | +| `den.lib.forward` | Low-level forwarding | +| `den.batteries.*` | Reusable aspects | +| `den.systems` | List of systems derived from hosts/homes | +| `den.schema.*` | Schema modules | +| `den.classes.*` | Class metadata | +| `den.lib.diag.*` | Diagram generation (fleet templates) | +| `den.lib.nh` | Nix Helper integration in templates | + +Den also exposes **`den.ful`** (internal namespaces) and **`flake.denful`** for cross-flake aspect sharing via `den.namespace`. + +--- + +## This repository’s Den layout + +Den adoption here is **incremental**: host topology + styling aspect use Den fully; feature modules remain primarily **flake-parts dendritic** with optional `den.aspects.*` mirrors. + +```text +flake.nix den input +modules/host-topology-den.nix den.flakeModule + all den.hosts / host aspects +den-aspects/styling.nix den.aspects.styling (os + nixos + darwin + HM) +modules/apps/brave.nix den.aspects.brave (mirror only) +modules/*.nix flake.modules.*.dendritic (primary feature path) +hosts/darwin/mba/default.nix raw host identity; imported by den.aspects.mba +modules/configurations.nix HM standalone (not yet den.homes.*) +``` + +### Host topology + +| Host | System | Aspect | Notes | +| ------------ | -------------- | ------------------------ | ------------------------------ | +| `mba` | aarch64-darwin | `den.aspects.mba` | Light theme override | +| `mba-dark` | aarch64-darwin | `den.aspects.mba-dark` | Dark theme `mkForce` | +| `mba-asahi` | aarch64-linux | `den.aspects.mba-asahi` | Apple Silicon NixOS | +| `nixos-test` | aarch64-linux | `den.aspects.nixos-test` | Test VM | +| `microvm` | aarch64-linux | `den.aspects.microvm` | vfkit guest; inline nixos body | + +Each host aspect follows the same pattern: + +```nix +den.aspects.mba = { + includes = [ config.den.aspects.styling ]; + darwin.imports = [ + { nixpkgs.config.allowUnsupportedSystem = true; } + ../hosts/darwin/mba + { dendritic.theme.variant = "light"; } + ]; +}; +``` + +- **`includes`** pulls shared Stylix/theming (`os` + HM + per-OS extras). +- **`darwin.imports`** preserves the existing host module verbatim. +- Small inline modules apply host-only overrides (theme variant). + +### Styling aspect (`den-aspects/styling.nix`) + +| Class | Content | +| ------------- | ---------------------------------------------------------------------------- | +| `os` | Shared Stylix enable, palette, fonts, wallpaper, `dendritic.theme.*` options | +| `nixos` | NixOS Stylix module, cursor, opacity, specialisations | +| `darwin` | nix-darwin Stylix module, font packages | +| `homeManager` | Full HM Stylix (Firefox CSS, Ghostty, etc.) | + +Dual export to dendritic monolith: + +```nix +den.aspects.styling.homeManager = stylingHmModule; +flake.modules.homeManager.dendritic = stylingHmModule; +``` + +Embedded HM users in host files import `inputs.self.modules.homeManager.dendritic`, so they receive styling **without** `den.homes.*`. + +### Brave aspect mirror (`modules/apps/brave.nix`) + +```nix +flake.modules.homeManager.dendritic = braveHmModule; +flake.modules.darwin.dendritic = braveDarwinModule; + +den.aspects.brave = { + homeManager = braveHmModule; + darwin = braveDarwinModule; +}; +``` + +Brave is active today via the **dendritic monolith** path (host imports `inputs.self.modules.*.dendritic`). The Den aspect exists so a host could later do: + +```nix +den.aspects.my-host.includes = [ config.den.aspects.brave ]; +``` + +…without touching HM import lists. + +### What is _not_ on Den yet + +| Area | Current state | Den target | +| ---------------------------------- | ------------------------------------ | ------------------------------------ | +| Feature modules (`modules/apps/*`) | `flake.modules.*.dendritic` only | Optional `den.aspects.*` per feature | +| Standalone HM `8amps-linux` | `modules/configurations.nix` | `den.homes.*` | +| User topology | Users declared inside host HM blocks | `den.hosts.*.users.*` + user aspects | +| Fleet / policies | Unused | `den.policies.*`, `den.schema.fleet` | + +--- + +## Patterns and recipes + +### Add a new Den-managed host + +1. Add host module under `hosts///`. +2. In `host-topology-den.nix`: + + ```nix + den.hosts.aarch64-darwin.my-host = { + instantiate = darwinSystemWithInputs; + }; + + den.aspects.my-host = { + includes = [ config.den.aspects.styling ]; + darwin.imports = [ ../hosts/darwin/my-host ]; + }; + ``` + +3. Switch: `nh darwin switch -H my-host`. + +Aspect name must match host name **or** set `aspect` explicitly on the host. + +### Share config between NixOS and Darwin once + +Use `os` class in an aspect: + +```nix +den.aspects.base = { + os = { pkgs, ... }: { + environment.systemPackages = [ pkgs.direnv ]; + }; +}; +``` + +### Migrate a hand-rolled `flake.nixosConfigurations.foo` + +1. Move inline module body into `den.aspects.foo.nixos`. +2. Declare `den.hosts..foo`. +3. Delete the old `flake.nixosConfigurations.foo` assignment. +4. Run `nix flake check`. + +This repo did that for `microvm` (previously in `modules/microvm.nix`). + +### Dual-export during migration + +Keep `flake.modules.homeManager.dendritic` **and** `den.aspects.feature.homeManager` pointing at the **same** let-bound module. Zero duplication, both consumption paths work. + +### Parametric user aspect + +```nix +den.aspects.8amps = { user, host, ... }: { + homeManager = { ... }; + # Host-specific override: + provides.mba = { + darwin = { ... }; + }; +}; +``` + +### Import-tree for non-dendritic legacy dirs + +```nix +den.schema.host.includes = [ + (den.batteries.import-tree.provides.host ./legacy-hosts) +]; +``` + +--- + +## Pitfalls and debugging + +### Missing aspect warning + +```text +evaluation warning: den.aspects.myhost not defined — entity gets empty aspect +``` + +Create `den.aspects.myhost` or rename host to match an existing aspect. + +### `inputs` not in scope in host modules + +Wrap `instantiate` with `withInputs` (see [Custom instantiate](#custom-instantiate-required-here)). + +### Duplicate flake output merge error + +Import `inputs.den.flakeOutputs.nixosConfigurations` (or darwin/home variant). + +### Aspect name ≠ host name + +Set explicitly: + +```nix +den.hosts.x86_64-linux.laptop = { + aspect = config.den.aspects.workstation; +}; +``` + +(Exact option path may use nested config — prefer matching names for simplicity.) + +### Entity comparison + +Use `host.id_hash`, not `host == otherHost`. + +### Debugging resolution + +```nix +# In nix repl or test module: +builtins.trace "resolved" ( + den.lib.aspects.resolveWithState "nixos" config.den.hosts.x86_64-linux.igloo.resolved +) +``` + +Templates under `github:denful/den?dir=templates/ci` exercise nearly every pipeline feature. + +### Evaluating without building + +```bash +nix eval .#nixosConfigurations.mba.config.system.build.toplevel --apply 'x: x.drvPath or "ok"' +nix flake check +``` + +--- + +## Further reading + +| Resource | URL | +| ----------------------------- | ---------------------------------------------------------------------------------------------------- | +| Den documentation | [den.denful.dev](https://den.denful.dev) | +| From Zero To Den | [den.denful.dev/guides/from-zero-to-den](https://den.denful.dev/guides/from-zero-to-den/) | +| From Flake To Den | [den.denful.dev/guides/from-flake-to-den](https://den.denful.dev/guides/from-flake-to-den/) | +| Context pipeline | [den.denful.dev/explanation/context-pipeline](https://den.denful.dev/explanation/context-pipeline/) | +| Custom classes | [den.denful.dev/guides/custom-classes](https://den.denful.dev/guides/custom-classes/) | +| Homes integration | [den.denful.dev/guides/home-manager](https://den.denful.dev/guides/home-manager/) | +| Den source (templates/ci) | [github.com/denful/den/tree/main/templates/ci](https://github.com/denful/den/tree/main/templates/ci) | +| Dendritic Pattern (this repo) | [docs/dendritic-patterns.md](./dendritic-patterns.md) | +| Dendrix (community modules) | [dendrix.denful.dev](https://dendrix.denful.dev/) | + +--- + +## Quick reference card + +```nix +# ── Bootstrap ── +imports = [ inputs.den.flakeModule ]; + +# ── Host ── +den.hosts.aarch64-darwin.mac = { instantiate = darwinSystemWithInputs; }; + +# ── Aspect ── +den.aspects.mac = { + includes = [ config.den.aspects.styling den.batteries.primary-user ]; + os = { networking.hostName = "mac"; }; + darwin = { imports = [ ./hosts/mac.nix ]; }; + homeManager = { programs.git.enable = true; }; +}; + +# ── Standalone home ── +den.homes.aarch64-darwin.alice = { }; + +# ── Schema defaults for all users ── +den.schema.user.classes = lib.mkDefault [ "homeManager" ]; + +# ── Manual resolve ── +den.lib.aspects.resolve "nixos" (den.aspects.mac { host = ...; }) +``` diff --git a/docs/dendritic-nix/01-foundations.md b/docs/dendritic-nix/01-foundations.md new file mode 100644 index 00000000..d798c1d6 --- /dev/null +++ b/docs/dendritic-nix/01-foundations.md @@ -0,0 +1,74 @@ +# 01 - Foundations + +## What Dendritic Nix is + +Dendritic Nix is a pattern for organizing Nix configurations by **feature** instead of by **target class** or **host folder**. + +Traditional split: + +- NixOS module in one file +- Home Manager module in another file +- nix-darwin module in yet another file + +Dendritic split: + +- one feature file can export all relevant class modules together. + +## Core problem it solves + +As configurations grow, complexity comes from: + +- multiple hosts, +- multiple module classes (`nixos`, `darwin`, `homeManager`, etc.), +- nested class evaluation (HM embedded in NixOS/nix-darwin), +- cross-cutting concerns (theme, editor, browser policy, secrets). + +Dendritic reduces “where do I edit this?” overhead by making feature ownership explicit. + +## The core rule set + +From the canonical pattern: + +1. every non-entry `.nix` file is a top-level module, +2. each module implements one feature across applicable classes, +3. lower-level modules are merged as option values (typically deferred modules), +4. paths represent feature intent, not class/host assignment. + +Source: [mightyiam/dendritic](https://github.com/mightyiam/dendritic). + +## Why deferred module merging matters + +In `flake-parts`, lower-level class modules are often represented under `flake.modules.*`. + +Example style: + +```nix +flake.modules.homeManager.dendritic = { ... }: { ... }; +flake.modules.darwin.dendritic = { ... }: { ... }; +``` + +Many files can assign to those same option paths. Nix module merging composes them into one final module per class. + +That is the key mechanic enabling: + +- one feature per file, +- no giant manual import list of every feature in every host. + +## What Dendritic is not + +- Not a replacement for Nix module semantics. +- Not tied only to flakes (can be adapted with `lib.evalModules`). +- Not only “boilerplate reduction”; it changes architectural ergonomics. + +## How it relates to Den + +Dendritic pattern: file-level feature composition with merged class modules. + +Den framework: function-level/context-aware aspect composition (`den.aspects.*`, `den.hosts.*`) built to solve additional topology and policy complexity. + +In this repo: + +- core features are mostly classic dendritic (`flake.modules.*.dendritic`), +- host topology is increasingly Den-driven. + +See: [`../den.md`](../den.md). diff --git a/docs/dendritic-nix/02-module-mechanics.md b/docs/dendritic-nix/02-module-mechanics.md new file mode 100644 index 00000000..9337dab6 --- /dev/null +++ b/docs/dendritic-nix/02-module-mechanics.md @@ -0,0 +1,101 @@ +# 02 - Module Mechanics + +This chapter explains exactly how dendritic composition works in practice. + +## Top-level auto-import + +In this repo, `modules/default.nix` auto-imports all non-private, non-entrypoint `.nix` files under `modules/`. + +That means adding `modules/apps/new-feature.nix` is enough for participation in top-level evaluation. + +## Class exports + +A dendritic feature file typically exports one or more of: + +- `flake.modules.nixos.dendritic` +- `flake.modules.darwin.dendritic` +- `flake.modules.homeManager.dendritic` + +Example pattern: + +```nix +{ + flake.modules.homeManager.dendritic = hmModule; + flake.modules.darwin.dendritic = darwinModule; +} +``` + +Multiple files assigning these keys are merged by module semantics. + +## Host consumption + +Hosts consume class-level merged modules once: + +```nix +imports = [ + inputs.self.modules.darwin.dendritic +]; +``` + +Embedded HM users consume: + +```nix +imports = [ + inputs.self.modules.homeManager.dendritic +]; +``` + +No per-feature imports required in host files. + +## Option namespace strategy + +Feature options are namespaced under `dendritic.*`: + +```nix +options.dendritic.apps.ghostty.enable = ... +``` + +Benefits: + +- avoids collisions with upstream module option names, +- gives hosts a clean feature toggle surface. + +## Cross-file composition patterns + +### Ordered composition + +Use `lib.mkOrder` when composing ordered lists across features (e.g. Dock apps). + +### Shared let-bound modules + +For dual export paths, define module body once and assign to multiple targets. + +Example from this repo: + +- feature module is exported both to `flake.modules.*.dendritic` and `den.aspects.`. + +### Class-selective exporting + +Only export classes where feature applies: + +- HM-only feature: export only `homeManager` class. +- Darwin-only feature: export only `darwin` class. + +## Decomposing a feature safely + +When splitting a large feature file: + +1. keep option namespace stable (`dendritic..*`), +2. split internals into private helpers (`_name.nix`) when needed, +3. keep final class exports predictable. + +Auto-import excludes `_*.nix` helpers in this repo, enabling internal refactors without changing top-level module graph. + +## Evaluating behavior + +When behavior feels surprising, check: + +1. did module file get auto-imported? +2. which class exports are present? +3. is host importing the correct merged class module? +4. are option merges (`mkDefault`/`mkForce`/`mkOrder`) behaving as expected? diff --git a/docs/dendritic-nix/03-repo-implementation.md b/docs/dendritic-nix/03-repo-implementation.md new file mode 100644 index 00000000..1a9afb34 --- /dev/null +++ b/docs/dendritic-nix/03-repo-implementation.md @@ -0,0 +1,90 @@ +# 03 - Implementation in This Repository + +This chapter maps the dendritic pattern to concrete files in this repo. + +## High-level structure + +- `flake.nix` + - flake inputs + - top-level flake-parts evaluation + - imports `./modules` +- `modules/default.nix` + - auto-imports all feature modules +- `modules/**/*.nix` + - feature-centric module files exporting class modules +- `hosts/**` + - identity/topology/host-specific glue and toggles + +## Main merged class outputs + +The dominant merge targets are: + +- `flake.modules.homeManager.dendritic` +- `flake.modules.darwin.dendritic` +- `flake.modules.nixos.dendritic` + +Most feature files contribute to one or more of these. + +## Real examples in this repo + +### Dock composition + +- `modules/dock.nix` defines `dendritic.dock.apps` option and base entries. +- app modules append entries with `lib.mkOrder`. + +This is a classic dendritic merge: distributed feature ownership + deterministic final order. + +### Brave as multi-class feature + +- file: `modules/apps/brave.nix` +- contributes HM + Darwin modules +- contains app settings, activation scripting, SOPS secret integration, dock registration + +This demonstrates “single feature file spanning multiple classes”. + +### Python feature across classes + +- file: `modules/python.nix` +- contributes NixOS + Darwin + HM modules under one feature namespace + +### Shared secrets feature + +- file: `modules/secrets.nix` +- exports all three classes and centralizes SOPS defaults + +## Host wiring examples + +### Darwin host + +`hosts/darwin/mba/default.nix` imports: + +- `inputs.self.modules.darwin.dendritic` +- `inputs.self.modules.homeManager.dendritic` (inside HM user block) + +and toggles feature options under `dendritic.*`. + +### NixOS host + +`hosts/nixos/*/default.nix` follows similar pattern: + +- imports merged `nixos` and embedded `homeManager` dendritic modules +- sets host-specific values while keeping features in modules. + +## Den interop in this repo + +Dendritic modules remain core feature layer. + +Den currently owns host topology in: + +- `modules/host-topology-den.nix` +- `den-aspects/styling.nix` + +There are dual-export patterns where feature modules also expose a Den aspect for future migration paths. + +## Practical takeaway + +In this repo, Dendritic Nix is not theoretical - it is the default module authoring model: + +- add new feature file, +- export relevant class modules, +- consume automatically through merged class imports already used by hosts. diff --git a/docs/dendritic-nix/04-real-examples.md b/docs/dendritic-nix/04-real-examples.md new file mode 100644 index 00000000..775604b5 --- /dev/null +++ b/docs/dendritic-nix/04-real-examples.md @@ -0,0 +1,99 @@ +# 04 - Real Dendritic Nix Examples + +This chapter answers the practical question: “show me real dendritic repos and what they do.” + +## A) Real examples in this repository + +### 1) Browser feature across classes + +- File: `modules/apps/brave.nix` +- Shows: + - HM and Darwin class exports in one feature file + - SOPS secret consumption + - activation scripting + - dock contribution + +Why it is dendritic: + +- feature-local ownership, not split by class directories. + +### 2) Dock as merge surface + +- Files: + - `modules/dock.nix` (base option + defaults) + - `modules/apps/*.nix` (ordered app contributions) + +Why it is dendritic: + +- multiple features merge into one shared option (`dendritic.dock.apps`) while staying independently owned. + +### 3) Cross-platform feature file + +- File: `modules/python.nix` +- Shows one feature emitting NixOS, Darwin, and HM behavior. + +### 4) Shared secrets feature + +- File: `modules/secrets.nix` +- Shows one feature wiring the same capability across all three classes. + +### 5) Theme stack with Den bridge + +- File: `den-aspects/styling.nix` +- Shows: + - Den aspect composition + - HM mirror export to dendritic merge target for backward compatibility + +Why this matters: + +- demonstrates real migration coexistence between classic dendritic exports and Den aspects. + +## B) Canonical upstream example (annotated template) + +Upstream repo includes a minimal annotated sample: + +- [mightyiam/dendritic/example](https://github.com/mightyiam/dendritic/tree/main/example) + +Representative files: + +- `example/modules/nixos.nix` - declares NixOS config options as deferred modules +- `example/modules/shell.nix` - feature module exporting multiple classes +- `example/modules/systems.nix` - systems declaration + +This example is intentionally small and incomplete by design; it teaches mechanics, not production topology. + +## C) Public real-world repos cited by upstream + +From [mightyiam/dendritic README](https://github.com/mightyiam/dendritic): + +- [mightyiam/infra](https://github.com/mightyiam/infra) +- [vic/vix](https://github.com/vic/vix) +- [drupol/nixos-x260](https://github.com/drupol/nixos-x260) +- [GaetanLepage/nix-config](https://github.com/GaetanLepage/nix-config) +- [bivsk/nix-iv](https://github.com/bivsk/nix-iv) + +These are useful to study larger production patterns: + +- feature slicing strategies, +- option namespacing discipline, +- host wiring styles, +- cross-class sharing and constraints. + +## D) Dendritic + Den ecosystem examples + +If you want deeper aspect-oriented extensions: + +- [denful/den](https://github.com/denful/den) +- [dendrix.denful.dev](https://dendrix.denful.dev/) + +These show how teams evolve from classic dendritic merges into context-driven aspect pipelines while keeping feature-centric design. + +## How to evaluate if a repo is truly dendritic + +Heuristics: + +1. Most non-entry files are top-level modules. +2. Files are feature-centric, not class-centric. +3. Lower-level class modules are merged under stable names. +4. Host files are relatively slim and import merged module sets. +5. Feature changes usually touch one feature file, not three class-specific files. diff --git a/docs/dendritic-nix/05-migration-playbook.md b/docs/dendritic-nix/05-migration-playbook.md new file mode 100644 index 00000000..9a7878b5 --- /dev/null +++ b/docs/dendritic-nix/05-migration-playbook.md @@ -0,0 +1,91 @@ +# 05 - Migration Playbook + +Use this when moving from a traditional host/class-split layout to a dendritic layout. + +## Starting point (typical) + +You may currently have: + +- `nixos/hosts//...` +- `home//...` +- `darwin//...` +- duplicated feature logic across class-specific files. + +## Migration goals + +1. feature files become primary source of truth, +2. host files become identity + selection layer, +3. class-specific exports are merged automatically. + +## Step-by-step plan + +### Step 1 - Define one merge namespace + +Pick stable merge targets: + +- `flake.modules.nixos.dendritic` +- `flake.modules.darwin.dendritic` +- `flake.modules.homeManager.dendritic` + +### Step 2 - Add module auto-import + +Use a top-level module that imports all non-entrypoint feature files. + +This repo does that in `modules/default.nix`. + +### Step 3 - Migrate one feature end-to-end + +Choose one feature (e.g., shell/editor/browser): + +1. collect class-specific fragments, +2. place in one feature file, +3. export appropriate class modules. + +Validate behavior before moving next feature. + +### Step 4 - Slim host files + +Hosts should mostly: + +- import merged class modules, +- set host identity and feature toggles. + +### Step 5 - Standardize option namespace + +Create a namespaced toggle/options surface such as `dendritic.*`. + +This avoids collisions and clarifies ownership. + +### Step 6 - Migrate incrementally + +Keep old and new paths side by side temporarily if needed. + +A practical coexistence pattern: + +- continue consuming merged dendritic module paths, +- optionally add Den aspect mirrors as future-proofing. + +## Validation after each migration step + +Checklist: + +1. evaluation succeeds (`nix flake check` where applicable), +2. host switch succeeds, +3. feature behaves correctly on all intended classes, +4. no hidden dependency on old class-specific file remains. + +## Common migration pitfalls + +- forgetting to remove old duplicate imports, +- missing option namespace declarations after move, +- overusing `specialArgs` pass-through instead of top-level config sharing, +- assigning too many unique lower-level module names (import explosion). + +## “Done” definition + +A migration is healthy when: + +- adding a new feature means adding one feature file, +- hosts rarely need per-feature import edits, +- cross-class feature behavior lives in one place, +- contributors can locate ownership quickly. diff --git a/docs/dendritic-nix/06-anti-patterns.md b/docs/dendritic-nix/06-anti-patterns.md new file mode 100644 index 00000000..548a683f --- /dev/null +++ b/docs/dendritic-nix/06-anti-patterns.md @@ -0,0 +1,120 @@ +# 06 - Anti-Patterns + +These are the most common ways teams accidentally defeat dendritic benefits. + +## 1) `specialArgs` as cross-file dependency bus + +Symptom: + +- values are passed through `specialArgs`/`extraSpecialArgs` chains to make modules work. + +Why this hurts: + +- hidden coupling, +- harder refactors, +- module portability drops. + +Preferred: + +- expose shared values in top-level module `config` and consume from there. + +Canonical warning source: + +- [mightyiam/dendritic anti-patterns](https://github.com/mightyiam/dendritic) + +## 2) Proliferation of named lower-level modules + +Symptom: + +- one unique `flake.modules..` per feature, +- hosts import long lists of these names. + +Why this hurts: + +- host imports become maintenance burden, +- adding/removing features requires host file churn. + +Preferred: + +- merge most features under one stable name per class (e.g. `.dendritic`). + +## 3) Class-centric file layout relabeled as dendritic + +Symptom: + +- still mostly `nixos/*.nix`, `home-manager/*.nix`, `darwin/*.nix`, +- feature intent scattered. + +Why this hurts: + +- same old navigation problem remains. + +Preferred: + +- files named/grouped by feature concern. + +## 4) Feature file that owns too many unrelated concerns + +Symptom: + +- huge “god feature” module with unrelated domains. + +Why this hurts: + +- reviewability and ownership collapse. + +Preferred: + +- split by cohesive feature boundary; use internal helper files if needed. + +## 5) Hardcoding host-specific values inside reusable feature modules + +Symptom: + +- feature module embeds one host’s identity details. + +Why this hurts: + +- feature cannot be reused cleanly. + +Preferred: + +- keep identity/topology in host layer; keep feature logic in feature module. + +## 6) Manual import lists for all features + +Symptom: + +- adding a feature requires touching multiple import files. + +Why this hurts: + +- brittle and easy to forget. + +Preferred: + +- auto-import non-entrypoint feature files. + +## 7) No clear namespace for feature options + +Symptom: + +- options spread into generic/global namespaces. + +Why this hurts: + +- collisions and unclear ownership. + +Preferred: + +- use `dendritic.*` (or another explicit feature namespace). + +## Quick anti-pattern detector + +If a simple feature change requires editing: + +- one class file per platform, +- plus host import lists, +- plus ad-hoc arg passing, + +you are drifting away from dendritic ergonomics. diff --git a/docs/dendritic-nix/07-review-checklist.md b/docs/dendritic-nix/07-review-checklist.md new file mode 100644 index 00000000..db1201c1 --- /dev/null +++ b/docs/dendritic-nix/07-review-checklist.md @@ -0,0 +1,44 @@ +# 07 - Review Checklist + +Use this checklist in PR reviews to keep dendritic architecture healthy. + +## File structure checks + +- [ ] New feature is implemented in a feature-centric file path. +- [ ] File is auto-importable (non-entrypoint, not private helper unless intended). +- [ ] No unnecessary class-split duplicate files were introduced. + +## Module export checks + +- [ ] Feature exports only relevant class modules. +- [ ] Exports target stable merge keys (`flake.modules.*.dendritic`) unless there is a clear exception. +- [ ] Option declarations and usage stay within namespace conventions (`dendritic.*`). + +## Host-layer checks + +- [ ] Host files are mostly topology/identity/toggles, not feature internals. +- [ ] Host files import merged class modules (not long per-feature import lists). + +## Merge behavior checks + +- [ ] `mkDefault` / `mkForce` / `mkOrder` usage is intentional and documented when non-obvious. +- [ ] Ordered aggregations (like lists) are deterministic when feature composition matters. + +## Dependency and coupling checks + +- [ ] No new fragile `specialArgs` pass-through chains were introduced. +- [ ] Cross-file value sharing uses top-level config/options where possible. + +## Real-world quality checks + +- [ ] Feature behavior was validated on each applicable class (Darwin/NixOS/HM). +- [ ] Migration coexistence paths are explicit when both dendritic and Den exports are present. +- [ ] Docs were updated if architecture or conventions changed. + +## “Looks dendritic” smell test + +- [ ] Could a new contributor discover “where this feature lives” in one or two jumps? +- [ ] Does adding another host require little/no feature-module churn? +- [ ] Would removing one feature be localized instead of invasive? + +If all three are “yes,” architecture is usually healthy. diff --git a/docs/dendritic-nix/README.md b/docs/dendritic-nix/README.md new file mode 100644 index 00000000..7b4ecce0 --- /dev/null +++ b/docs/dendritic-nix/README.md @@ -0,0 +1,32 @@ +# Dendritic Nix Documentation Suite + +This is the deep, multi-file documentation set for **Dendritic Nix** (sometimes misspelled as _dendretic_ / _dendretix_). + +Canonical naming: + +- **Dendritic Pattern** - the architecture pattern +- **Den** - aspect-oriented framework built around/alongside dendritic ideas +- **Dendrix** - community distribution of reusable dendritic modules + +Start here if you want a full understanding: + +1. [`01-foundations.md`](./01-foundations.md) +2. [`02-module-mechanics.md`](./02-module-mechanics.md) +3. [`03-repo-implementation.md`](./03-repo-implementation.md) +4. [`04-real-examples.md`](./04-real-examples.md) +5. [`05-migration-playbook.md`](./05-migration-playbook.md) +6. [`06-anti-patterns.md`](./06-anti-patterns.md) +7. [`07-review-checklist.md`](./07-review-checklist.md) + +Related docs in this repo: + +- [`../dendritic-patterns.md`](../dendritic-patterns.md) (single-file overview) +- [`../den.md`](../den.md) (deep Den framework reference) +- [`../sops-nix/README.md`](../sops-nix/README.md) (secret-management deep dive) + +Primary upstream references: + +- [mightyiam/dendritic](https://github.com/mightyiam/dendritic) +- [The Dendritic Pattern (NixOS Discourse)](https://discourse.nixos.org/t/the-dendritic-pattern/61271) +- [flake-parts modules option docs](https://flake.parts/options/flake-parts-modules.html) +- [Doc-Steve dendritic design guide](https://github.com/Doc-Steve/dendritic-design-with-flake-parts/wiki/Dendritic_Aspects) diff --git a/docs/dendritic-patterns.md b/docs/dendritic-patterns.md new file mode 100644 index 00000000..ee848d76 --- /dev/null +++ b/docs/dendritic-patterns.md @@ -0,0 +1,277 @@ +# Dendritic Nix: Patterns, Den, and Dendrix + +This repository is built on the **Dendritic Pattern** for Nix configurations, with an incremental adoption of **[den](https://github.com/denful/den)** for host topology and aspects. **[Dendrix](https://dendrix.denful.dev/)** is the community layer for sharing reusable dendritic modules. + +> For the full multi-file deep dive, start at [`docs/dendritic-nix/README.md`](./dendritic-nix/README.md). + +> **Naming:** You may see _dendretix_, _dendretic_, or _dendric_ in older notes — the canonical names are **Dendritic** (the pattern), **Den** (the framework), and **Dendrix** (the community distribution). + +## Overview + +| Concept | What it is | Primary link | +| --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- | +| **Dendritic Pattern** | Organize Nix configs by _feature_, not by _platform_ | [mightyiam/dendritic](https://github.com/mightyiam/dendritic) | +| **Den** | Aspect-oriented framework on top of the pattern | [denful/den](https://github.com/denful/den) | +| **Dendrix** | Community-curated library of shareable dendritic modules (“layers”) | [dendrix.denful.dev](https://dendrix.denful.dev/) | + +Together they address the same problem: **cross-cutting features** (editors, browsers, theming, gaming, secrets) that need configuration in NixOS, nix-darwin, and Home Manager at once — without scattering related code across disconnected directories. + +--- + +## The Dendritic Pattern + +The Dendritic Pattern was introduced by [@mightyiam](https://github.com/mightyiam) and documented in the [NixOS Discourse thread](https://discourse.nixos.org/t/the-dendritic-pattern/61271). Official docs live at [github.com/mightyiam/dendritic](https://github.com/mightyiam/dendritic). + +### Core idea + +Instead of splitting configuration by _where it applies_: + +``` +hosts/laptop/nixos.nix +hosts/laptop/home-manager.nix +hosts/desktop/nixos.nix +... +``` + +…split by _what feature you are configuring_: + +``` +modules/apps/brave.nix # Brave everywhere it is needed +modules/editor.nix # Neovim / editor setup +modules/dock.nix # macOS dock layout +den-aspects/styling.nix # Global theming (Stylix) +``` + +Each file is a **flake-parts module** that can contribute to multiple lower-level module classes in one place. + +### Rules + +1. **Every `.nix` file is a top-level flake-parts module** — except entry points (`flake.nix`, `default.nix`) and private helpers (files prefixed with `_`). +2. **Each file implements one feature** across every configuration class that feature touches. +3. **Lower-level modules merge under shared names** (for example `flake.modules.homeManager.dendritic`) rather than proliferating dozens of individually named imports. +4. **File paths name features**, not hosts or platforms — files can be moved or split freely. + +### How flake-parts fits + +[flake-parts](https://flake.parts) applies the Nix module system to flake _outputs_. In the dendritic pattern, feature modules set options like: + +```nix +flake.modules.homeManager.dendritic = { pkgs, lib, config, ... }: { ... }; +flake.modules.darwin.dendritic = { pkgs, lib, config, ... }: { ... }; +flake.modules.nixos.dendritic = { pkgs, lib, config, ... }: { ... }; +``` + +Every contribution to `flake.modules..dendritic` **merges** into one deferred module. Host configs import the merged result once: + +```nix +imports = [ + inputs.self.modules.darwin.dendritic +]; +``` + +…and embedded Home Manager users do the same on the HM side: + +```nix +home-manager.users."8amps".imports = [ + inputs.self.modules.homeManager.dendritic +]; +``` + +### Auto-import in this repo + +`modules/default.nix` discovers every eligible file under `modules/` and imports it as a flake-parts module: + +```nix +isAutoImportable = path: + let name = baseNameOf path; + in lib.hasSuffix ".nix" name + && name != "default.nix" + && !lib.hasPrefix "_" name; +``` + +Adding `modules/apps/my-feature.nix` is enough — no manual import list to maintain. + +### Example: dock + apps (cross-class feature) + +`modules/dock.nix` declares a shared option and macOS defaults: + +```nix +flake.modules.darwin.dendritic = { config, lib, ... }: { + options.dendritic.dock.apps = lib.mkOption { ... }; + config.dendritic.dock.apps = lib.mkOrder 0 [ "/System/Applications/Apps.app" ... ]; + config.system.defaults.dock.persistent-apps = config.dendritic.dock.apps; +}; +``` + +Individual app modules (`modules/apps/ghostty.nix`, `cursor.nix`, …) append their `.app` paths via `lib.mkOrder` at different priorities. The dock feature and each app stay in one file each, but compose at evaluation time. + +### Benefits + +- **Feature-local reasoning** — “Where is Brave configured?” → `modules/apps/brave.nix`. +- **No import-path archaeology** — modules share values through top-level `config`, not `specialArgs` pass-through chains. +- **Portable modules** — dendritic feature files can be copied or published (see Dendrix). +- **Shorter host files** — hosts wire identity and toggles; features live in `modules/`. + +### Anti-patterns (from upstream) + +- **Over-using `specialArgs`** to shuttle values between files — use top-level `config` instead. +- **One named lower-level module per feature** — prefer merging into shared names like `flake.modules.nixos.dendritic`. + +Further reading: + +- [Dendritic Pattern README](https://github.com/mightyiam/dendritic) +- [Doc-Steve: Dendritic Design with flake-parts](https://github.com/Doc-Steve/dendritic-design-with-flake-parts) +- [Flipping the Configuration Matrix](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/) (drupol) + +--- + +## Den: Aspect-Oriented Dendritic Nix + +[Den](https://github.com/denful/den) extends the dendritic idea from **file-level** to **function-level** configuration: aspects compose across hosts, users, and Nix classes, and `den.hosts.*` auto-generates flake outputs. + +This repo uses Den for host topology (`modules/host-topology-den.nix`), shared styling via the `os` class (`den-aspects/styling.nix`), and optional aspect mirrors on feature modules. Most app/editor features still flow through `flake.modules.*.dendritic`. + +**→ Full documentation: [docs/den.md](./den.md)** — context pipeline, entities, aspects, policies, custom classes, batteries, flake integration, repo layout, recipes, and debugging. + +Upstream: [den.denful.dev](https://den.denful.dev) + +--- + +## Dendrix: Community Dendritic Modules + +[Dendrix](https://dendrix.denful.dev/) is a **community-driven distribution** of dendritic flake-parts modules — conceptually similar to [NUR](https://github.com/nix-community/NUR), but for multi-class feature modules rather than single packages. + +Repository: [github.com/denful/dendrix](https://github.com/denful/dendrix) (formerly `vic/dendrix`; renamed to avoid confusion with [yunfachi/denix](https://github.com/yunfachi/denix)). + +### What Dendrix provides + +- **Layers** — opinionated, ready-to-enable feature bundles (gaming, desktop rices, dev shells, …) +- **Import trees** — structured ways to pull community aspects into your flake +- **Documentation** of community dendritic repos and which Nix classes each aspect targets + +Dendrix modules are ordinary flake-parts / Den aspects meant to be **mixed in**, not a full replacement for your own config. + +### Using Dendrix (typical workflow) + +1. Add `dendrix` as a flake input. +2. Import a layer or aspect tree into your flake-parts evaluation. +3. Enable features via options or `den.aspects` includes — the layer contributes to all relevant classes automatically. + +This repo does **not** currently depend on Dendrix; features are authored locally under `modules/` and `den-aspects/`. Dendrix is the path toward sharing or consuming community-maintained equivalents. + +--- + +## This Repository’s Architecture + +``` +flake.nix # inputs, flake-parts entry +├── modules/default.nix # auto-imports all feature modules +├── modules/ +│ ├── apps/*.nix # per-app dendritic features +│ ├── dock.nix, editor.nix, … # cross-cutting features +│ └── host-topology-den.nix # Den hosts + host aspects +├── den-aspects/ +│ └── styling.nix # den.aspects.styling (+ HM mirror) +└── hosts/ + ├── darwin/mba/ # host identity, HM user block, toggles + ├── nixos/*/ # same pattern for Linux hosts + └── hm/ # standalone HM entry modules +``` + +### Configuration flow + +```mermaid +flowchart TD + subgraph flake [flake-parts top level] + AutoImport[modules/default.nix auto-import] + DenMod[host-topology-den.nix + den-aspects] + end + + subgraph features [Feature modules] + Apps[modules/apps/*.nix] + Cross[modules/dock.nix editor.nix ...] + end + + subgraph merge [Merged module classes] + HMDend[flake.modules.homeManager.dendritic] + DarwinDend[flake.modules.darwin.dendritic] + NixOSDend[flake.modules.nixos.dendritic] + end + + subgraph hosts [Host evaluation] + DenHosts[den.hosts.* → flake.*Configurations] + HostFiles[hosts/darwin/mba etc.] + end + + AutoImport --> Apps + AutoImport --> Cross + Apps --> HMDend + Apps --> DarwinDend + Cross --> DarwinDend + DenMod --> DenHosts + HostFiles --> DenHosts + DenHosts --> HMDend + DenHosts --> DarwinDend + DenHosts --> NixOSDend +``` + +### The `dendritic.*` option namespace + +Feature modules declare options under `dendritic.*` (not `config.*` directly) to avoid collisions and to give hosts a stable toggle surface: + +```nix +# In a host's home-manager.users block: +dendritic.apps.ghostty.enable = true; +dendritic.apps.cursor.enable = true; +dendritic.theme.variant = "dark"; +``` + +Theme selection is centralized in `theme-selection.nix` and exposed via `dendritic.theme.*` options in `den-aspects/styling.nix`. + +### Adding a new feature module + +1. Create `modules/my-feature.nix` (or `modules/apps/my-feature.nix`). +2. Export to the appropriate merged classes: + + ```nix + { + flake.modules.homeManager.dendritic = { ... }; + flake.modules.darwin.dendritic = { ... }; # if needed + flake.modules.nixos.dendritic = { ... }; # if needed + } + ``` + +3. Optionally add a Den aspect for future `den.homes.*` / cross-flake sharing: + + ```nix + den.aspects.my-feature = { + homeManager = myHmModule; + darwin = myDarwinModule; + }; + ``` + +4. Enable from a host user block with `dendritic.my-feature.enable = true` (after defining the option). + +No changes to `flake.nix` or `modules/default.nix` are required — auto-import picks up the new file. + +--- + +## Comparison: Traditional vs Dendritic vs Den + +| Approach | Organization axis | Multi-class features | Host scaling | +| ---------------------------------- | ----------------------------- | --------------------------------------- | -------------------------------- | +| **Traditional** (Misterio77-style) | By host, then by module type | Split across HM/NixOS files | Many host folders | +| **Dendritic** | By feature | One file, multiple `flake.modules.*` | Host files import merged modules | +| **Den** | By feature + aspect functions | Aspects with `includes`, custom classes | `den.hosts` generates outputs | + +--- + +## External Resources + +- [The Dendritic Pattern (Discourse)](https://discourse.nixos.org/t/the-dendritic-pattern/61271) +- [mightyiam/dendritic](https://github.com/mightyiam/dendritic) — pattern docs + annotated example +- [denful/den](https://github.com/denful/den) — aspect-oriented framework +- [dendrix.denful.dev](https://dendrix.denful.dev/) — community layers +- [denful.dev](https://denful.dev/) — Vic’s dendritic ecosystem overview +- [flake.parts modules option](https://flake.parts/options/flake-parts-modules.html) +- [vic/import-tree](https://github.com/vic/import-tree) — filesystem auto-import helper diff --git a/docs/dentric_nix.md b/docs/dentric_nix.md index d52698c1..695c0ba3 100644 --- a/docs/dentric_nix.md +++ b/docs/dentric_nix.md @@ -1,486 +1,5 @@ -Skip to main content -What is dendritic Nix and how does it work? : r/NixOS +# Moved -Open menu - +This file previously contained a pasted Reddit thread. Proper documentation now lives in: -r/NixOS - - -Open chat -Create -Create post -Open inbox - -User Avatar -Expand user menu - -Back - -Go to NixOS -r/NixOS -• -4mo ago -PaceMakerParadox - -What is dendritic Nix and how does it work? - -Like what makes it different than flakes+home manager, I don't get it, is it just the same with boilerplate reduction? Cause that is what I am getting, I am probably missing something but that is really why I am asking. - -I also think is somehow includes the same syntax/modules that would work on any system dynamically, which like 1. How and 2. Wouldn' that not be declarative at that point?. - -What are the points/benefits for gaming? - -I rnad the GitHub and did not really get much beyond that. - -Upvote -29 - -Downvote - -33 -Go to comments - - -Share - u/NordicSemiconductor avatar -NordicSemiconductor -• -Promoted - -Nordic nRF54L Series, the next-level wireless SoCs - Thumbnail image: Nordic nRF54L Series, the next-level wireless SoCs - -Sort by: - -Best - -Search Comments -Expand comment search -Comments Section -Fereydoon37 -• -4mo ago -I skimmed through the tutorial and linked materials from the post yesterday. It seems like it's a new buzz word for creating reusable Nix code by focusing on the "feature" you want to achieve. - -For NixOS, as a desktop operating system, that could mean setting up your coding environment, which could import a subfeature for an editor like vim, or whatever is needed to use say Python. Another example would be a feature for gaming, with subfeatures for specific games like OpenMorrowind, or vendors like steam. - -The materials make a big point of not redefining these things for every computer that needs a feature, and not spreading things out over disconnected files for NixOS/nixpkgs, Home Manager, Darwin, etc. Apparently that's how a lot of people have been doing things. - -Instead you define the thing in one place, grouping all configuration for nixpkgs, home manager etc. together, and then include / activate that bundle for computers / users that need it. - -The tutorial uses flake-parts to accommodate this structure. It also mentions that you don't need to use flake-parts to write "dendritic" nix. The advantage of adopting flake-parts and their file organisation, is standardisation; people can exchange code, and the files become self-documenting to people familiar with the conventions. - -Personally I'm not sure that using flake-parts like this is superior to an ad-hoc file structure convention, and using the existing nixpkgs / home-manager module systems to define options, that computers / users opt into. - -The impression I had of how features can't be imported twice, and how to deal with things need to be activated / imported conditionally, makes me suspect it's a bit of a leaky abstraction, but I haven't given it it's fair shake either, so take that with a grain of salt. Basically, what I'm trying to say is that ostensibly the existing nixpkgs module system has better conflict handling / resolution. - -nixgang -• -4mo ago -This is where I'm at as well. It's true that reconciling HM/NixOS/disko/devshell/etc-modules for multiple machines requires some care to keep it DRY and maintainable, but putting an abstraction on top of them all doesn't seem right to me. - -I haven't studied enough to know for sure though, maybe it's good for some use cases, but probably not for me. - -Reddich07 -• -4mo ago -The only “abstraction” you introduce is that each module, previously residing in its own file, now gets a name within a top-level module. You can still separate each top-level module into multiple files, each containing your previous modules, if you prefer. The only difference would be two lines bracketing your module. You don’t make any other changes to your code. So, migration is quite straightforward. Of course, you do make structural changes when applying the Dendritic Pattern, but that’s independent of flake-parts. - -Initially, I was skeptical about using a new library that appears so frequently in every code fragment (What if I want to revert? What if development stops in the future?). However, the benefits are so significant that it became a no-brainer after I understood the concept. Spoiler alert: It’s like switching to flakes. You start questioning why this isn’t part of the core system and why it’s not the standard way to do it. I wonder if you’ll feel the same way. - -nixgang -• -4mo ago -Yes I know how flake-parts work and use it where it fits, what I'm questioning is the dendritic pattern, not flake-parts -More replies -u/ghostnation66 avatar -ghostnation66 -• -1mo ago -Do you know where I can learn more about basic nix? - -u/mightyiam avatar -mightyiam -• -1mo ago -Profile Badge for the Achievement Top 1% Commenter Top 1% Commenter -https://nix.dev https://zero-to-nix.com/ https://nixcademy.com/posts/ -u/Ashtefere avatar -Ashtefere -• -4mo ago -Def seems like buzzwording. - -I have a script that generates my flake file from a template and then it rebuilds and switches. - -The template generator also loops over a directory of single file flakes that include all the inputs for each flake as well, so i dont to write all the inputs for all my configs in one file - they are purely isolated. - -That way I can add/remove files to a folder to add/remove features to me config - makes it a lot easier to import flakes for things too. -u/Buttars0070 avatar -Buttars0070 -• -4mo ago -I'm definitely starting to feel friction with the traditional approach separating files across different configuration types. I'm regularly finding myself asking "where does that functionality live?" and once I figure out what type of configuration it is "is it a core feature or a system specific feature..." It's a mess. I'm going to experiment getting rid of separating my core from my feature and eventually move to something like you described. -u/Silly-Name-999 avatar -Silly-Name-999 -• -2mo ago -and not spreading things out over disconnected files for NixOS/nixpkgs, Home Manager, Darwin, etc. Apparently that's how a lot of people have been doing things. -... -Personally I'm not sure that using flake-parts like this is superior to an ad-hoc file structure convention, and using the existing nixpkgs / home-manager module systems to define options, that computers / users opt into. -How would you go about creating a single file that contains NixOS and nix-darwin configuration? Is your repo public? -u/Epistechne avatar -Epistechne -• -4mo ago -Still just starting to learn it myself so I will probably get some things wrong, but it is a pattern for how to write modules so that your configuration is more easily scaled to complex multisystem multi user configs while keeping maintenance low. - -Following this design pattern the flake file becomes a simple list of inputs without the complex outputs section I've seen in many configs. - -Modules don't require maintaining a bunch of relative paths for imports ../../../ , the structure makes it that you can often move files and directories without having to update paths. - -The modules you write are more easily composed together from smaller modules with less glue code and options. - -Modules have the code for different system classes (nixos, darwin, homemanager) in one file instead of scattered in separate files. -u/mightyiam avatar -mightyiam -• -4mo ago -Profile Badge for the Achievement Top 1% Commenter Top 1% Commenter -I love seeing how many have looked into the dendritic pattern. I want to do a Full Time Nix podcast episode about it with several users, in case anyone is interested. - -u/ghostnation66 avatar -ghostnation66 -• -1mo ago -Im interested -u/Vortriz avatar -Vortriz -• -4mo ago -• -Edited 4mo ago -simplest example: lets say you want to configure a program/feature that requires setup in both home-manager and nixos options. but since the HM part has to be imported into HM, you would create a separate file for it. this gets messy when you have separate out things like that. - -using dendritic pattern, you configure it in one file (or place) itself by thinking of it as a "feature" that you want to achieve. even if that feature might require you to mess with HM, nixos modules, hell even devshell, you should be able to define it nicely in one place. flake module from flake-parts allows you to achieve this. - -i use a dendritic pattern based framework (called unify) in my dots. - -example files: - -https://github.com/Vortriz/dotfiles/blob/main/modules/programs/terminal/shell.nix -https://github.com/Vortriz/dotfiles/blob/main/modules/toplevel/nix.nix -no_brains101 -• -4mo ago -• -Edited 4mo ago -Profile Badge for the Achievement Top 1% Commenter Top 1% Commenter -Kept hearing this term and was also curious, so I looked it up. TIL my config is like, almost but maybe not quite dendritic. pre-dendritic I guess, cause its been around for longer than that term. I have stuff grouped by feature and they export modules of both types from each feature when needed. However I did not extend that idea to my system configs using flake-parts modules, which does seem somewhat interesting. I just had a hub of features and imported/enabled the ones i needed in each system config - -It seems to be a new design pattern which aims to help people to figure out how to group their config by feature in a way that allows them to affect both home manager and nixos from 1 file (along with whatever else), rather than 2 separate files containing modules with a lot of duplicated code for each, plus a file exporting that other stuff. - -Commonly people do this pattern these days with flake parts modules that import both the nixos and home manager module associated with that feature, and because its flake-parts it can also do stuff like, output packages from the main flake as well if you had a wrapped package, or import them directly into the configurations exported by your flake - -Before that, people like myself who made their config before this term existed made files which were a function which you call with { inherit inputs; home-manager = true or false; } which returned a nixos or home manager module, and then called that in a hub to get both modules into a hub variable, and then called that hub from their flake and passed the hub to their home manager and nixos configs. Those main configs can then import whatever they want from that, and your flake can grab whatever from that hub too and export it. This allows you to not have to worry about the actual paths to those things, and keep stuff as easily shared between configs for home manager and nixos as possible, while also being able to export them from the top level flake - -If you give those modules an enable option you can auto-import them in every config and just enable them wherever, or you can just decide if you want to import them or not per config if they don't have an enable option. You can also stuff whatever you want into said hub, not just modules. - -Dendritic patten is basically just that, but with flake parts so that you dont have to deal with a hub variable you pass around and its slightly easier to share info between bundles. The parts can do those things directly, so you no longer have to pass the hub and import it there, you just do it right there in the flake-part module and then its imported in all the configs, just like if you had a hub, and imported all the modules it exported automatically. Or, I guess you could also have them export from the main flake there, and then use inputs.self as your hub. - -It seems there is a lot of ways to follow this pattern, but the key is to group stuff by feature, and then have each feature bundle be able to export nixos modules, home manager modules, packages etc. which may or may not be then automatically imported in your config for a particular machine or home manager. - -It seems kinda like a reasonable pattern, it is something you can partially apply too if you only want to go partially dendritic. IDK. I might drift more this way eventually, using flake-parts to manage my hub thing instead of passing it around might be useful. I already use flake parts for mapping my nixos and home manager configs to legacyPackages..nixos/homeConfigurations. so, whats the harm in using it for a bit more I guess. - -I think I like the idea where I use dendritic flake-parts modules for the things I had in my hub, and have them export themselves from the flake, and then in my nixos and home manager configs for the systems, I can grab them from inputs.self - -That way my hub thing can be better managed and organized using flake-parts, and otherwise I can keep the overall hub and import thing I have going on which I kinda like for the system/home configs. But who knows, lets see what I do I guess. That will at least be how I start, and then maybe I will extend it to my main configs which use that stuff and maybe I wont. - -IDK Im doing a lot of stuff right now. Ill get to it eventually lol. I was tending towards this, but I think this pattern can teach me something I can use to make it cleaner and more enjoyable and simple to use once I get it set up. On the bright side, my new nix-wrapper-modules repo fits into it kinda nicely, as they export packages, but can also export modules if you add one in an option, and one could make a nice little flake parts module to grab various outputs and do stuff with them. - -no_brains101 -• -4mo ago -Profile Badge for the Achievement Top 1% Commenter Top 1% Commenter -TL;DR - -flake-parts is the module system, applied to flake outputs. - -in your outputs function, you just call the flake-parts eval module function, and then you import modules / set module options which set up flake outputs rather than setting up the outputs directly. This lets you create options on top. - -Dendritic pattern is a pattern in which those flake modules are organized by feature, and output all the things, home-manager/nixos modules, packages, overlays, devShells, whatever, from the same file/directory. - -They can either take those things and export them directly from the main flake, or they can import them directly in the home manager and nixos configs exported by the flake, or both, different people do that differently. - -You can emulate this pattern without flake-parts, but flake-parts really makes it into a more organized thing rather than just having a hub and nix expressions which output a mix of stuff. -kesor -• -4mo ago -There are multiple aspects to it, the one I like most is using modules in such a way that the same file can be seen by both NixOS and Home Manager. - -Example: - -{ - flake.modules.nixos.system-secrets-sops = - { pkgs, inputs, ... }: - { - imports = [ inputs.sops-nix.nixosModules.sops ]; - - environment.systemPackages = with pkgs; [ - sops - gnupg - ]; - - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = ../../../../secrets/nixos.yaml; - - gnupg = { - home = "/var/lib/sops/gnupg"; - sshKeyPaths = [ ]; - }; - - age = { - keyFile = null; - sshKeyPaths = [ ]; - }; - }; - }; - - flake.modules.homeManager.sops = - { - pkgs, - lib, - config, - inputs, - ... - }: - { - imports = [ inputs.sops-nix.homeManagerModules.sops ]; - - options.sops.enable = lib.mkEnableOption "sops-nix secrets management"; - - config = lib.mkMerge [ - { - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = ../../../../secrets/home-manager.yaml; - gnupg = { - home = config.programs.gpg.homedir; - sshKeyPaths = [ ]; - }; - }; - } - (lib.mkIf config.sops.enable { - home.packages = with pkgs; [ - sops - gnupg - ]; - }) - ]; - }; -} -Then when I import inputs.self.modules.homeManager.sops in my Home Manager, or I import inputs.self.modules.nixos.sops in my NixOS modules, I get the code from that one single file. Make organizing related things/aspects close together in single files much more convenient. In my case inputs is a specialArgs in NixOS and extraSpecialArgs in Home Manager, and its the flake's inputs. - -So I guess the magic is inputs.self, for the flake to be able to reference itself. And some magic with import-tree that finds all my modules by scanning a folder. - -Example relevant piece of a flake.nix: - - outputs = - inputs: - inputs.flake-parts.lib.mkFlake { inherit inputs; } ( - { config, ... }: - let - inherit (inputs.nixpkgs-unstable) lib; - - treeModules = inputs.import-tree.initFilter ( - p: lib.hasSuffix "/default.nix" p && !lib.hasInfix "/_" p - ); - treeMachines = inputs.import-tree.initFilter ( - p: lib.hasSuffix "/configuration.nix" p && !lib.hasInfix "/_" p - ); - treeHomes = inputs.import-tree.initFilter (p: lib.hasSuffix ".nix" p && !lib.hasInfix "/_" p); - in - { - imports = [ - inputs.flake-parts.flakeModules.modules - (treeModules ./modules) - (treeMachines ./machines) - (treeHomes ./homes) - ]; - - systems = [ - "x86_64-linux" - "aarch64-linux" - ]; - - flake = { - nixosConfigurations = lib.mapAttrs ( - _: cfg: inputs.nixos-stable.lib.nixosSystem cfg - ) config.flake.machineConfigs; - - homeConfigurations = lib.mapAttrs ( - _: cfg: inputs.home-manager.lib.homeManagerConfiguration cfg - ) config.flake.homeConfigs; - }; - -BizNameTaken -• -4mo ago -You do not need to configure hm stuff in another file. You can write hm config in a nixos module with home-manager.users. = { ... };, or to make it even simpler, use lib.mkAliasOptionModule - -kesor -• -4mo ago -I don't want to nixos-rebuild each time I want to change something in home manager ... that is why I have home manager for. -u/Autodesk avatar -u/Autodesk -• -Promoted - -No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. - No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. - No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. - No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. - No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. - No waitlist. No lab schedule. Go from footage to editable CG. Create export-ready assets you can refine in tools like Maya, Blender, and Unreal Engine. -autodesk.com -Sign Up -Reddich07 -• -4mo ago -You might have missed my post about the (guide) I wrote. It should address all your questions. Here are the short answers: 1. No, it’s not a “boilerplate reduction.” 2. It has nothing to do with switching from “declarative” to “dynamically.” in any way. 3. It benefits gaming setups by making complex setups more manageable. - -If you only have one machine running Nix and are only interested in optimizing your setup, such as for gaming, the Dendritic Pattern won’t make much of a difference. You can usually handle this low-level complexity with any file/code structure (more or less elegant). - -The Dendritic Pattern is particularly useful when you want to use your code on multiple hosts and different configuration contexts, such as Home-Manager or Nix-Darwin. It organizes your code in a way that makes it easier to create, change, update, and fix bugs, which was previously a dependency nightmare. This is perhaps the main advantage. Unfortunately, it requires more than a short forum answer to fully understand. It‘s not a new „buzzword“ for things that people already did with their current configurations. I was told that the pattern was „discovered“ by people who used flake-parts and used „top-level“-modules which have 1. access to the flake outputs, 2. have the possibility to share values/code between different contexts (NixOS, Home-Manager, …). This sounds complicated at first, it isn‘t if you have seen some examples. The idea behind the Dendritic Pattern is so powerful, that it was redefined in a generic way (previously it was linked to flake-parts, but you can even do it without, but you may need some kind of tool/library). The most commonly used tool for the Dendritic Pattern is flake-parts. This is no coincidence, it fits perfectly. - -If you have a complex setup and want to refactor your code because you’ve reached a limit with your current structure, I can assure you that delving into the Dendritic Pattern is worth it. Once you understand the basics, it’s quite easy to work with. - -Feel free to ask if you don’t understand something in the guide. (Please use the linked guide thread for this, so that others can benefit from the answers there as well.) -u/zardvark avatar -zardvark -• -4mo ago -I don't think that there are any benefits to gaming, whatsoever. The primary benefit is simplifying the task of managing multiple machines. - -That said, I am far from expert. I have been reading about flake-parts and the dendritic pattern on and off for the past few weeks and while configuring the modules seems straightforward enough, I'm not so sure that I understand the glue that holds everything together. I've been looking at user configurations on the github, but since I'm not a developer, much of the configurations are a bit opaque to me. I like to understand the code, rather than copy / paste it and hope for the best, eh? And, I'm simply not there yet. - -BTW - Vimjoyer released an all too brief vid on flake-parts recently, for those interested. - -Anywho, I have multiple machines to manage, so I am quite interested in this topic, but thus far figuring this out has been slow going. - -u/Epistechne avatar -Epistechne -• -4mo ago -Did you see this post yet, I find it really helpful https://old.reddit.com/r/NixOS/comments/1pxqm2w/github_docstevedendriticdesignwithflakeparts_a/ - -u/zardvark avatar -zardvark -• -4mo ago -No, I had not seen that one. - -Thanks so much!!! -u/zardvark avatar -zardvark -• -4mo ago -Thanks again! - -I've only just scratched the surface, but this resource has already answered a few of my questions. - -Cheers!!! - -u/Epistechne avatar -Epistechne -• -4mo ago -No problem, I'm a nix noob and I think I would have struggled for months without this guide. It's really timely that he made it just as I'm starting to try learning it. -silver_blue_phoenix -• -4mo ago -I think i ended up building this by myself, as a natural evolution of using this system config flake when I started out. - -Don't know what flake parts is; don't think I need to use a new tool for doing what I'm doing by hand. - -My nixos config basically has the following structure; - -All nixos things are kept in a nixos folder. -Inside this folder, there is a default.nix that makes it so that every module in the subdirectory modules get's a option myNixOS..enable that when set to true, imports the said module. -Hosts directory has a default.nix that sets up modules shared across all hosts, and sets up my personal user -Each host lies in it's own subdirectory with an entry point /default.nix that configures the system, and enables whichever module it wants to. -I have a library function that generates a nixos config automatically from all the different folders in nixos/hosts -I have nix-darwin setup that doesn't play nice with this but I managed to integrate. I also have a raspberry-pi nix setup that also doesn't play super nice (I don't want the default.nix to be imported for example; it's just a wireguard server that doesn't need my user.) home-manager follows a similar layout. - -u/Epistechne avatar -Epistechne -• -4mo ago -Misterio77 repo is a nice structure but it is not dendritic, it's what dendritic is an alternative to. - -I'd read this article that shows which side of the config matrix these are on https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/#flipping-the-configuration-matrix -Reddich07 -• -4mo ago -„flake-parts provides the options that represent standard flake attributes and establishes a way of working with system (from the web page). Sounds very abstract, I know. For a comprehensive introduction, watch this video from @vimjoyer. Flake-parts is not a configuration setup; it’s a fundamental and incredibly powerful concept: how to work with flakes within your code. - -To clarify, your setup neither recreates the way to work with flakes (like flake-parts) nor utilizes the design pattern introduced with the Dendritic Pattern. - -Don’t misunderstand me: You have a highly sophisticated and effective setup that meets your requirements. That’s perfectly fine—no need to change it. Nix is a programming language, and you can accomplish anything without relying on specific tools or design patterns. Perhaps at some point in the future, you’ll encounter increased complexity that necessitates restructuring your setup. In such cases, consider exploring the Dendritic Pattern to see if it can assist you and if it’s worth the effort. -Fereydoon37 -• -4mo ago -What you're doing differently from the dendritic pattern, from the eyes of an outsider, is that you're not separating concerns as advocated. Instead of making a module for Web browsing, and another for gaming etc., your code is driven by hosts / users. - -For example, a quick skim reveals that you're setting up Firefox and steam for a specific user in one file. Instead you could opt into gaming and Web browsing 'features' for that user. That carries the advantage that if you add a Mac system, and you want to use a different Web browser like Safari there, or also want to provide say lutris globally to gaming set ups, you don't need to change anything to your system or user configuration; your 'business logic' if you will. You only need to touch the 'implementation' of the functionality (feature) you're affecting. Conversely if there's a problem with a feature, you immediately know where to look first. - -The separation of concerns and the divide between implementation and business / core logic, while good form, are nothing new. u/nixgang and I both seem to have implemented the core ideas already in mostly standard Nix, because frankly it's what emerges naturally from complex requirements. - -At one point I managed six hosts with 4 users that each had to do a distinct but overlapping subset of server duties, programming, low latency music and audio production, creative writing, and gaming amongst other things. Not abstracting over that is not tenable. - -I'm skeptic about the pattern as a rigid implementation thereof, and I'm afraid it might get in the way of abstracting even further. I do however see value in standardisation, and making it easy to follow for people without a formal background in informatics. -u/ThomasLeonHighbaugh avatar -ThomasLeonHighbaugh -• -20d ago -The use of the word "dendratic", calling to mind the brain and playing to the common superiority complexes had around here, this is hardcore buzzwording that is really just yet another module writing pattern that may work well for some, personally I am prone to compartmentalization in my life and in my Linux so its less appealing to me but that's fine. - -Write modules how you want to, please stop inventing new esoteric labels for the same things. Especially just to up sell a module writing tactic. Can we just make it a little less esoteric nonsense? -Community Info Section -r/NixOS - Joined -NixOS - Purely functional -Created Aug 26, 2011 -Public -48K -Weekly visitors -1.1K -Weekly contributions -COMMUNITY ACHIEVEMENTS -Repeat Contributor -Flag Planter -Repeat Contributor, -Flag Planter -2 unlocked - -View All -MODERATORS -Message Mods - u/kxra avatar -u/kxra -u/iElectric avatar -u/iElectric -Domen -View all moderators -PROMOTED - - sidebar promoted post thumbnail -Reddit Rules -Privacy Policy -User Agreement -Your Privacy Choices -Accessibility -Reddit, Inc. © 2026. All rights reserved. +**[Dendritic Nix: Patterns, Den, and Dendrix](./dendritic-patterns.md)** diff --git a/docs/sops-nix.md b/docs/sops-nix.md new file mode 100644 index 00000000..ebdfd3ca --- /dev/null +++ b/docs/sops-nix.md @@ -0,0 +1,494 @@ +# sops-nix Deep Dive + +This is a deep, practical guide to how `sops-nix` works and how this repository uses it. + +> For the full multi-file documentation set, start at [`docs/sops-nix/README.md`](./sops-nix/README.md). + +Primary upstream references: + +- [Mic92/sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md) +- [Mic92/sops-nix repository](https://github.com/Mic92/sops-nix) +- [sops-nix NixOS module implementation](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix) +- [sops-nix Home Manager module implementation](https://github.com/Mic92/sops-nix/blob/master/modules/home-manager/sops.nix) + +--- + +## What sops-nix solves + +`sops-nix` gives you declarative secret provisioning in Nix-based systems: + +- Secrets are stored encrypted in git via `sops`. +- Decryption happens at activation/runtime, not in the Nix store. +- Secret files are materialized with declared owner/group/mode. +- Services and apps consume secret paths via `config.sops.secrets..path`. + +This is exactly what you want in Nix: + +- reproducible builds, +- versioned infra, +- no plaintext secrets committed, +- no plaintext secrets baked into derivations. + +--- + +## How it works (high-level flow) + +1. You commit encrypted secret files (YAML/JSON/INI/dotenv/binary). +2. You define secret entries in Nix (`sops.secrets. = { ... };`). +3. Activation runs `sops-install-secrets`. +4. Secret files are decrypted into runtime locations with strict permissions. +5. Modules/services read secrets via stable path attributes. + +Key point: decryption occurs outside the immutable Nix store, so plaintext does not leak into `/nix/store`. + +--- + +## This repository's current architecture + +This repo defines one dendritic `sops-nix` feature module at: + +- `modules/secrets.nix` + +It exports a **single class** — `flake.modules.homeManager.dendritic` — +because every `sops.secrets.*` consumer in this repo (currently the +`anthropic_api_key` in `modules/editor.nix`) is a HM-class declaration. +The dendritic pattern explicitly says +([docs/dendritic-nix/02-module-mechanics.md](./dendritic-nix/02-module-mechanics.md)) +"Class-selective exporting: Only export classes where feature applies." + +Earlier revisions of this repo also exported `nixos`/`darwin` system +classes pointing at `/var/lib/sops-nix/key.txt`. They had zero consumers +and the key file did not exist on the live Mac; treated as dead code and +removed. The pattern for re-adding a system-level class is one block in +`modules/secrets.nix` keyed off the same `dendritic.secrets.*` options. + +### Inputs + +`flake.nix` includes: + +- `inputs.sops-nix.url = "github:Mic92/sops-nix";` + +### Option surface: `dendritic.secrets.*` + +| Option | Default | Purpose | +| ----------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------ | +| `dendritic.secrets.enable` | `true` | Whether sops-nix is wired in for the HM profile. | +| `dendritic.secrets.ageKeyPath` | `"${config.home.homeDirectory}/.ssh/id_ed25519"` | SSH ed25519 key that ssh-to-age derives the decryption key from. | +| `dendritic.secrets.defaultSopsFile` | `../secrets/secrets.yaml` | Sops-encrypted YAML file consumed by `sops.secrets.` declarations. | + +### Global defaults (after option resolution) + +- `sops.defaultSopsFormat = "yaml"` +- `sops.defaultSopsFile = config.dendritic.secrets.defaultSopsFile` +- `sops.age.sshKeyPaths = [ config.dendritic.secrets.ageKeyPath ]` + +### Key strategy used here + +Unified ssh-to-age bridge: + +- single class (HM): `sops.age.sshKeyPaths = [ ]` +- HM packages: `sops`, `age`, `ssh-to-age` + +No separate age private key file to bootstrap, persist, or back up — the +SSH ed25519 key IS the decryption credential. Cross-platform (Darwin and +Linux HM use the same path expression). + +--- + +## Files involved in this repo + +### `secrets/secrets.yaml` + +Encrypted SOPS document (tracked in git), currently containing at least: + +- `anthropic_api_key` + +This file includes SOPS metadata: + +- recipient list +- `mac` +- `lastmodified` +- SOPS file format version + +### `.sops.yaml` + +SOPS policy/config file that controls: + +- recipient identities +- path-based creation rules for new encrypted files + +This repo currently defines creation rules for: + +- `secrets/secrets.yaml` (general YAML secrets) + +### Consumers + +- `modules/editor.nix`: + - declares `sops.secrets.anthropic_api_key = {};` + - consumes via `config.sops.secrets.anthropic_api_key.path` + +--- + +## Secret lifecycle in detail + +### 1) Authoring + +You add/edit encrypted values with `sops`: + +```bash +sops secrets/secrets.yaml +``` + +Because `.sops.yaml` is present, recipient rules are applied automatically. + +### 2) Commit and review + +You commit ciphertext only. Reviewers can still inspect: + +- key names, +- structural diffs, +- recipient metadata. + +### 3) Activation + +On `nh darwin switch` / `nh os switch` / HM activation: + +- `sops-nix` decrypts requested secrets using configured key sources. +- files are written to runtime secret locations. + +### 4) Runtime consumption + +Modules read paths from `config.sops.secrets..path`. +Do not hardcode runtime directories manually; always use path attributes. + +--- + +## NixOS, Darwin, and Home Manager behavior differences + +Based on upstream behavior: + +- System module (`nixos` / `darwin`) decrypts into system runtime secret space. +- Home Manager module runs user-level secret management (`sops-nix.service`). +- HM places secrets in user runtime dirs and symlinks stable paths under user config tree. + +Upstream notes specifically call out HM runtime differences and `%r` expansion behavior (`$XDG_RUNTIME_DIR` on Linux and Darwin user temp dir on macOS) in templates and paths. + +Source: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## Declaring secrets correctly + +Basic declaration: + +```nix +sops.secrets.my_secret = {}; +``` + +With explicit properties: + +```nix +sops.secrets.my_secret = { + owner = "myuser"; + group = "mygroup"; + mode = "0400"; + sopsFile = ../secrets/other.yaml; +}; +``` + +Binary secret (no longer used in this repo — kept as a reference pattern): + +```nix +sops.secrets."my-binary-key" = { + format = "binary"; + sopsFile = ../secrets/my-binary-key.sops; + mode = "0400"; +}; +``` + +--- + +## Templates: injecting secrets into config files + +`sops-nix` supports rendering config files with secret placeholders: + +```nix +sops.templates."service-config.toml".content = '' + password = "${config.sops.placeholder.my_secret}" +''; +``` + +Then consume rendered config path: + +```nix +systemd.services.myservice.serviceConfig.ExecStart = + "${pkgs.myservice}/bin/myservice --config ${config.sops.templates."service-config.toml".path}"; +``` + +You can also set ownership for rendered files: + +```nix +sops.templates."service-config.toml".owner = "serviceuser"; +``` + +Template workflow and path usage are documented in upstream README: +[sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## Key management strategies (age) + +`sops-nix` supports multiple age key sources. + +## 1) Dedicated age key file + +```nix +sops.age.keyFile = "/var/lib/sops-nix/key.txt"; +sops.age.generateKey = true; +``` + +Pros: + +- clear separation from SSH auth keys +- explicit rotation lifecycle + +Cons: + +- another private key artifact to persist/back up carefully + +## 2) SSH private key paths via `ssh-to-age` bridge + +```nix +sops.age.sshKeyPaths = [ "/home/user/.ssh/id_ed25519" ]; +``` + +Pros: + +- no extra key file if SSH key already exists + +Cons: + +- couples secret decryptability to SSH key lifecycle +- passphrase/availability constraints can complicate activation + +Upstream also documents evolving native SSH key support discussions; treat current module options as source of truth: + +- [PR #779](https://github.com/Mic92/sops-nix/pull/779) +- [Issue #744](https://github.com/Mic92/sops-nix/issues/744) + +--- + +## Recipient management and rotation + +In `.sops.yaml`, recipients are declared once and applied by path rules. + +Typical rotation flow: + +1. Add new recipient to `.sops.yaml`. +2. Rewrap existing files: + +```bash +sops updatekeys secrets/secrets.yaml +``` + +3. Commit updated encrypted metadata. +4. Remove old recipients after rollout completes. + +--- + +## Operational runbook for this repository + +## Add a new text secret + +1. Edit encrypted file: + +```bash +sops secrets/secrets.yaml +``` + +2. Add secret key/value. +3. Declare in module where consumed: + +```nix +sops.secrets.my_new_secret = {}; +``` + +4. Consume via: + +```nix +config.sops.secrets.my_new_secret.path +``` + +5. Rebuild (`nh darwin switch` or equivalent). + +## Add a new binary secret + +1. Encrypt source binary to a `.sops` file with matching `.sops.yaml` rule. +2. Declare: + +```nix +sops.secrets."name" = { + format = "binary"; + sopsFile = ./name.sops; + mode = "0400"; +}; +``` + +3. Consume path from module activation script or service config. + +## Rotate recipient key + +1. Update `.sops.yaml` recipients. +2. Run `sops updatekeys` on each encrypted file. +3. Commit. +4. Verify activation on each host class (Darwin, Linux, HM-only if used). + +--- + +## Security model and common mistakes + +## Good practices + +- Keep secret values only in encrypted files. +- Use strict file modes (`0400` or `0440`) unless broader access is required. +- Reference secrets by `config.sops.secrets..path`. +- Separate secrets by blast radius when useful (multiple sops files). +- Keep decryption keys out of git and out of world-readable paths. + +## Common mistakes + +- Embedding secret literals in Nix expressions. +- Using `builtins.readFile` on secret plaintext during evaluation. +- Writing rendered secrets into store-backed paths. +- Forgetting to update recipient metadata after key changes. +- Assuming HM and system secret runtime paths are identical. + +--- + +## Home Manager caveats worth remembering + +Upstream explicitly notes: + +- HM module uses a user service (`sops-nix.service`). +- Runtime secret location differs from system module behavior. +- `home.homeDirectory` needs to be correctly set so secret symlink paths resolve as expected. + +Source: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## `neededForUsers` and early boot/user creation + +Upstream supports `neededForUsers = true` for cases where secrets must exist before normal user setup (e.g., hashed password files consumed during user creation). This is a specialized boot-order/use-case option and should only be used when necessary. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## Service restart/reload integration + +For system services, you can attach: + +- `restartUnits = [ ... ]` +- `reloadUnits = [ ... ]` + +to specific secrets so updates propagate to dependent services. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## Troubleshooting + +## "Error getting data key: 0 successful groups required, got 0" + +Usually means decryption key mismatch: + +- secret encrypted to recipients unavailable on target host +- wrong key file path +- SSH-derived key expectations differ from actual file/key format + +Check: + +- `.sops.yaml` recipients +- target key file exists and readable by activation context +- secret has been rewrapped with current recipients (`sops updatekeys`) + +Related context: [Issue #744](https://github.com/Mic92/sops-nix/issues/744). + +## Secret path exists but app cannot read it + +Check: + +- file mode and owner/group on secret declaration +- service user identity +- whether consumer runs in system or user context + +## Darwin-specific confusion around key location + +Upstream notes common Darwin path convention for age keys: + +- `$HOME/Library/Application Support/sops/age/keys.txt` + +or configure custom location explicitly. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +--- + +## Suggested improvements: status + +| # | Item | Status | Notes | +| --- | ----------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- | +| 1 | One primary age key strategy across classes | Done | Single HM class via `sops.age.sshKeyPaths`. System-level classes removed; re-add when a real system consumer arrives. | +| 2 | Key-rotation script that rewraps all encrypted files | Done | [`scripts/sops-updatekeys.sh`](../scripts/sops-updatekeys.sh) | +| 3 | Split secrets by domain (`secrets/editor.yaml`, etc.) | Open | Not done. The `dendritic.secrets.defaultSopsFile` option lets a host or feature module override per-call once needed. | +| 4 | CI validation that encrypted files still parse as SOPS docs | Done | [`modules/sops-validation.nix`](../modules/sops-validation.nix) → `nix flake check`'s `sops-files-parse` derivation. | + +--- + +## Quick reference snippets + +### Secret declaration + +```nix +sops.secrets.api_token = {}; +``` + +### Secret consumption + +```nix +programs.someapp.tokenFile = config.sops.secrets.api_token.path; +``` + +### Binary secret + +```nix +sops.secrets."tls-key" = { + format = "binary"; + sopsFile = ./tls-key.pem.sops; + mode = "0400"; +}; +``` + +### Template + +```nix +sops.templates."app.env".content = '' + TOKEN=${config.sops.placeholder.api_token} +''; +``` + +### Service config using rendered template + +```nix +systemd.services.app.serviceConfig.ExecStart = + "${pkgs.app}/bin/app --env-file ${config.sops.templates."app.env".path}"; +``` + +--- + +## Related docs in this repository + +- [Dendritic Nix: Patterns, Den, and Dendrix](./dendritic-patterns.md) +- [Den - Deep Reference](./den.md) diff --git a/docs/sops-nix/01-architecture.md b/docs/sops-nix/01-architecture.md new file mode 100644 index 00000000..4c23a619 --- /dev/null +++ b/docs/sops-nix/01-architecture.md @@ -0,0 +1,72 @@ +# 01 - Architecture + +## What sops-nix is + +`sops-nix` is a Nix module layer that takes encrypted SOPS files and materializes decrypted secrets at activation/runtime with declarative permissions. + +It is designed for: + +- encrypted secrets in git, +- no plaintext in `/nix/store`, +- reproducible config + runtime decryption. + +## Core model + +There are three planes: + +1. **Authoring plane** - encrypted secret files (`*.yaml`, binary, etc.) in your repository. +2. **Evaluation plane** - Nix evaluates declarations like `sops.secrets. = { ... };`. +3. **Activation/runtime plane** - `sops-install-secrets` decrypts files and writes runtime secret files. + +This separation is the reason secrets stay out of derivations. + +## End-to-end flow + +1. Add/update encrypted SOPS file. +2. Declare secret in Nix module (`sops.secrets.`). +3. Rebuild/switch. +4. Activation decrypts + writes secret files with configured ownership/mode. +5. Services/apps consume `config.sops.secrets..path`. + +## Supported module contexts + +`sops-nix` is available in: + +- NixOS (`inputs.sops-nix.nixosModules.sops`) +- nix-darwin (`inputs.sops-nix.darwinModules.sops`) +- Home Manager (`inputs.sops-nix.homeManagerModules.sops`) + +Each context has different activation mechanics and runtime locations. + +## Runtime behavior by context + +### NixOS and nix-darwin system modules + +- Secrets are managed by the system activation/service path. +- Good for system services and root-owned secrets. + +### Home Manager module + +- Uses user-level `sops-nix.service`. +- Runtime secret location differs from system module behavior. +- Secret paths are still consumed through `config.sops.secrets..path`. + +Upstream explicitly documents HM-specific behavior and requirements around `home.homeDirectory` and runtime dirs. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +## Why this is safe for Nix workflows + +- Encrypted files can be committed safely. +- Nix store may contain encrypted SOPS files, not plaintext. +- Decrypted values are created at activation/runtime only. +- Access controls are declarative (`owner`, `group`, `mode`). + +## What sops-nix does not do automatically + +- It does not rotate your recipients for you. +- It does not choose your key lifecycle policy. +- It does not magically reload arbitrary apps unless you wire restart/reload behavior. +- It does not prevent all misuse if you manually read/write plaintext into store-backed paths. + +Those are operational policy concerns, covered in later chapters. diff --git a/docs/sops-nix/02-key-management.md b/docs/sops-nix/02-key-management.md new file mode 100644 index 00000000..9e5067bb --- /dev/null +++ b/docs/sops-nix/02-key-management.md @@ -0,0 +1,126 @@ +# 02 - Key Management + +This chapter covers how `sops-nix` decrypts secrets and how to choose/manage key strategies. + +## Key sources in sops-nix + +Common age-based options: + +- `sops.age.keyFile` +- `sops.age.generateKey` +- `sops.age.sshKeyPaths` + +### Dedicated age key file + +Example: + +```nix +sops.age.keyFile = "/var/lib/sops-nix/key.txt"; +sops.age.generateKey = true; +``` + +Pros: + +- clear, explicit key artifact +- independent of SSH auth key lifecycle + +Cons: + +- extra secret artifact to back up/persist + +### SSH key bridge (`ssh-to-age` style) + +Example: + +```nix +sops.age.sshKeyPaths = [ "/home/user/.ssh/id_ed25519" ]; +``` + +Pros: + +- no separate age key file to manage + +Cons: + +- couples decryption to SSH key lifecycle +- passphrase and activation-context nuances can break decryption + +## Strategy in this repository + +Unified ssh-to-age bridge in `modules/secrets.nix`: + +- single class export: `flake.modules.homeManager.dendritic` +- `sops.age.sshKeyPaths = [ config.dendritic.secrets.ageKeyPath ]` + where `dendritic.secrets.ageKeyPath` defaults to + `"${config.home.homeDirectory}/.ssh/id_ed25519"` +- HM packages include `sops`, `age`, `ssh-to-age` + +The earlier mixed strategy (system modules pinned to +`/var/lib/sops-nix/key.txt`; HM pinned to ssh-to-age) was removed when +the system-class blocks turned out to have zero consumers. If a future +system-level consumer lands, add a sibling +`flake.modules.{nixos,darwin}.dendritic` block keyed off the same +`dendritic.secrets.*` options surface. + +## Recipient model and `.sops.yaml` + +SOPS encrypts a data key to one or more recipients. Those recipient rules are managed in `.sops.yaml`: + +- `keys:` aliases recipients +- `creation_rules:` maps file path regexes to recipient lists + +If a secret file is not encrypted to a key available on target host/user context, decryption fails. + +## Generating and inspecting keys + +Age key generation: + +```bash +mkdir -p ~/.config/sops/age +age-keygen -o ~/.config/sops/age/keys.txt +age-keygen -y ~/.config/sops/age/keys.txt +``` + +Convert SSH public key to age recipient: + +```bash +nix shell nixpkgs#ssh-to-age -c sh -lc 'ssh-to-age < ~/.ssh/id_ed25519.pub' +``` + +Upstream also notes Darwin-specific common location: + +- `$HOME/Library/Application Support/sops/age/keys.txt` + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +## Rotation model + +Rotation usually means: + +1. Add new recipient(s) in `.sops.yaml`. +2. Rewrap existing files: + +```bash +sops updatekeys secrets/secrets.yaml +``` + +3. Commit. +4. Roll out + verify. +5. Remove old recipients only after all hosts/users can decrypt with new recipients. + +## Native SSH support notes + +There is active upstream history around SSH-as-age handling and migration semantics: + +- [PR #779](https://github.com/Mic92/sops-nix/pull/779) +- [Issue #744](https://github.com/Mic92/sops-nix/issues/744) + +Treat the currently documented module options in the exact release you pin as authoritative. + +## Recommended policy for this repo + +If you want lowest operational surprise: + +- use one primary key source strategy across system + HM contexts, +- document recipient ownership and rotation cadence, +- verify decryption in all host contexts after any key change. diff --git a/docs/sops-nix/03-authoring-and-files.md b/docs/sops-nix/03-authoring-and-files.md new file mode 100644 index 00000000..72b9183c --- /dev/null +++ b/docs/sops-nix/03-authoring-and-files.md @@ -0,0 +1,101 @@ +# 03 - Authoring Secrets and File Layout + +This chapter covers how to create/update encrypted files and how to structure them. + +## Files in this repository + +Current secret-related files: + +- `.sops.yaml` - recipient + creation rules +- `secrets/secrets.yaml` - encrypted YAML secret store +- `modules/secrets.nix` - cross-class `sops-nix` defaults + +## Editing encrypted files + +Edit encrypted YAML: + +```bash +sops secrets/secrets.yaml +``` + +Edit encrypted binary usually means re-encrypting from source artifact into `.sops` output, then committing only ciphertext output. + +## Recommended naming conventions + +- Shared app/service values: `secrets/secrets.yaml` +- Large domain split (optional as secret count grows): + - `secrets/editor.yaml` + - `secrets/apps.yaml` + - `secrets/infra.yaml` +- Binary blobs: + - `something.ext.sops` and declare `format = "binary"` + +## Declaring secrets in modules + +Minimum declaration: + +```nix +sops.secrets.anthropic_api_key = {}; +``` + +Explicit source file: + +```nix +sops.secrets.anthropic_api_key = { + sopsFile = ../secrets/secrets.yaml; +}; +``` + +Binary declaration (reference pattern — no binary secrets currently in this repo): + +```nix +sops.secrets."my-binary-key" = { + format = "binary"; + sopsFile = ./my-binary-key.sops; + mode = "0400"; +}; +``` + +## Choosing one file vs many files + +### Single file benefits + +- simple editing workflow +- easy grep for key names +- fewer moving parts + +### Multi-file benefits + +- smaller blast radius by domain +- clearer ownership boundaries +- easier selective access policies if you later split recipients by file + +Either is valid. Optimize for your team/process size. + +## Preventing accidental plaintext commits + +Checklist: + +- Never commit raw `.pem`, `.env`, or plaintext YAML with secrets. +- Keep `.sops.yaml` creation rules tight enough to match intended paths. +- Review diffs for encrypted payload + `sops` metadata only. + +## Review patterns + +During PR review, check: + +- new secret key names are sensible, +- recipient list still correct, +- no plaintext values introduced outside encrypted payload, +- module declarations exist for any newly required consumers. + +## Runtime path usage discipline + +Never inline runtime secret path guesses like `/run/secrets/...`. +Always consume: + +```nix +config.sops.secrets..path +``` + +This keeps code resilient to backend/path behavior differences across contexts. diff --git a/docs/sops-nix/04-consumption-patterns.md b/docs/sops-nix/04-consumption-patterns.md new file mode 100644 index 00000000..6ea7cc85 --- /dev/null +++ b/docs/sops-nix/04-consumption-patterns.md @@ -0,0 +1,85 @@ +# 04 - Consuming Secrets in Nix Modules + +This chapter focuses on practical consumption patterns: service args, app config, activation scripts, and module wiring. + +## Golden rule + +Declare secret, then consume `.path`. + +```nix +sops.secrets.api_key = {}; + +programs.someapp.apiKeyFile = config.sops.secrets.api_key.path; +``` + +Do not use `builtins.readFile` on secret plaintext during evaluation. + +## Pattern A - direct file path option + +Many modules expect `...File` options: + +```nix +services.example.tokenFile = config.sops.secrets.token.path; +``` + +This is ideal when supported. + +## Pattern B - command substitution in app config + +Sometimes app config supports `cmd:` indirection. + +Example from this repo (`modules/editor.nix`): + +```nix +api_key = "cmd:cat ${config.sops.secrets.anthropic_api_key.path}" +``` + +This keeps the secret off static config literals and uses runtime file reads. + +## Pattern C - activation-time script consumption + +In activation scripts, reference the secret path at runtime. + +Example pattern (generic — no activation-script consumer in this repo currently): + +```sh +KEY_PATH="${config.sops.secrets."my-binary-key".path}" +if [ ! -r "$KEY_PATH" ]; then + echo "secret missing" >&2 + exit 1 +fi +``` + +This is useful for signing, materializing policy files, or generating artifacts that must not be done at evaluation time. + +## Pattern D - secret with strict permissions + +```nix +sops.secrets.private_key = { + mode = "0400"; + owner = "appuser"; + group = "appgroup"; +}; +``` + +Use this when service user is not root/default. + +## Context boundary notes + +System module and Home Manager module run in different activation contexts. Keep consumer and declaration in matching context unless you intentionally bridge them. + +## This repository's concrete consumers + +### Editor module + +- Declaration: `sops.secrets.anthropic_api_key = {};` +- Consumption: command reads from `config.sops.secrets.anthropic_api_key.path` + +## Safety checks before switch + +Before running `nh ... switch`, verify: + +1. secret is declared where used, +2. `sopsFile` resolves correctly (if overridden), +3. permission fields make sense for target runtime user, +4. consumer references `.path`, not plaintext values. diff --git a/docs/sops-nix/05-templates-and-services.md b/docs/sops-nix/05-templates-and-services.md new file mode 100644 index 00000000..a52ad7b3 --- /dev/null +++ b/docs/sops-nix/05-templates-and-services.md @@ -0,0 +1,166 @@ +# 05 - Templates and Service Integration + +This chapter covers `sops.templates`, placeholders, and service reload/restart behavior. + +## Why templates exist + +Some apps need a single config file that contains both: + +- non-secret config values, +- secret values. + +`sops.templates` lets you render that file during activation while keeping source templates non-sensitive. + +## Template basics + +```nix +sops.secrets.db_password = {}; + +sops.templates."myapp/config.toml".content = '' + [database] + password = "${config.sops.placeholder.db_password}" +''; +``` + +The rendered path is available as: + +```nix +config.sops.templates."myapp/config.toml".path +``` + +## Ownership and permissions for templates + +You can set ownership/mode for generated template outputs: + +```nix +sops.templates."myapp/config.toml" = { + content = '' + token = "${config.sops.placeholder.api_token}" + ''; + owner = "myapp"; + group = "myapp"; + mode = "0400"; +}; +``` + +## Wiring templates into systemd services + +```nix +systemd.services.myapp = { + serviceConfig = { + ExecStart = + "${pkgs.myapp}/bin/myapp --config ${config.sops.templates."myapp/config.toml".path}"; + User = "myapp"; + }; +}; +``` + +This is the preferred pattern when an app cannot consume separate secret files. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +## Restart and reload hooks + +Per-secret controls: + +```nix +sops.secrets."home-assistant-secrets.yaml" = { + restartUnits = [ "home-assistant.service" ]; + # or reloadUnits = [ "home-assistant.service" ]; +}; +``` + +Use `restartUnits` when service cannot hot-reload changed secret material. +Use `reloadUnits` when app supports safe reload semantics. + +## Home Manager caveat + +Upstream notes `restartUnits` semantics differ for systemd user services; treat HM user-service behavior explicitly and test. + +## `%r` runtime placeholder + +Upstream documents `%r` replacement for runtime dirs in HM scenarios: + +- Linux: `$XDG_RUNTIME_DIR` +- Darwin: `getconf DARWIN_USER_TEMP_DIR` + +This is useful for ephemeral user runtime files. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +## Recommended pattern in this repo + +For applications that currently shell `cat` paths manually, consider incremental migration to: + +- explicit `sops.templates` where app supports config files, +- path-only file options where app supports `...File`, +- restart/reload hooks only where needed. + +## Worked example: porting CodeCompanion from `cmd:cat` to `sops.templates` + +Today, [`modules/editor.nix`](../../modules/editor.nix) consumes the +`openai_api_key` secret via a `cmd:cat ${path}` shell indirection inside +the CodeCompanion plugin config. That works, but every plugin reload spawns +a `cat`, and the path is interpolated into a Lua string at HM eval time — +not great if the plugin ever caches the literal. + +The dendritic-grade port uses `sops.templates` to render a runtime env file +that CodeCompanion can source directly. Sketch (illustrative — not applied): + +```nix +flake.modules.homeManager.dendritic = + { config, pkgs, lib, ... }: + lib.mkIf config.dendritic.secrets.enable { + sops.secrets.openai_api_key = { }; + + # Renders a file at activation containing the plaintext secret, with + # strict 0400 mode and owned by the HM user. Path is content-addressed + # via config.sops.templates..path, never in /nix/store. + sops.templates."codecompanion.env" = { + mode = "0400"; + content = '' + OPENAI_API_KEY=${config.sops.placeholder.openai_api_key} + ''; + }; + + # Reference the rendered path from the plugin config. The plugin + # reads it once at editor startup. + programs.nixvim.extraConfigLuaPre = '' + vim.env.OPENAI_API_KEY = vim.fn.system( + "set -a; . " .. + vim.fn.shellescape("${config.sops.templates."codecompanion.env".path}") .. + "; printenv OPENAI_API_KEY" + ):gsub("\n$", "") + ''; + }; +``` + +Why this is better than `cmd:cat`: + +- The plugin never sees a literal path interpolated into Lua source; + it gets the value via `vim.env`, which is process-local. +- The rendered template is HM-managed, so removing the + `sops.secrets.anthropic_api_key` declaration also removes the env file. +- `sops.templates` honors `restartUnits`/`reloadUnits` for system services, + so if you later migrate to a daemon (e.g., a local `claude-code` HTTP + proxy), wiring restart on secret change is one line: + +```nix +sops.templates."codecompanion.env".restartUnits = [ + "my-claude-proxy.service" +]; +``` + +## Home Manager restart semantics: what works and what does not + +`restartUnits` / `reloadUnits` on a `sops.secrets.` or `sops.templates.` is unambiguous on **system** (`nixos`/`darwin`) sops modules — they map directly to systemd units. + +On **Home Manager**, behavior is more constrained: + +- Linux HM (under `systemd-user`): `restartUnits = [ "X.service" ]` works against systemd user units. Verified pattern. +- Darwin HM: there is no systemd user manager. `restartUnits` is silently no-op for `launchd` agents. If you need a Darwin app to pick up rotated secrets, do one of: + - quit and relaunch the app from an HM activation hook (the Brave module's `braveStylixReload` script is the canonical example in this repo), + - have the app re-read its secret file on a SIGHUP and send the signal from activation, + - schedule a `launchd` agent restart via `launchctl kickstart -k`. + +The rule of thumb: assume `restartUnits` is a systemd hint, not a portable contract. diff --git a/docs/sops-nix/06-operations-and-rotation.md b/docs/sops-nix/06-operations-and-rotation.md new file mode 100644 index 00000000..6002ae1a --- /dev/null +++ b/docs/sops-nix/06-operations-and-rotation.md @@ -0,0 +1,103 @@ +# 06 - Operations and Rotation + +This chapter is the operator runbook: day-to-day edits, onboarding, rotation, and rollout verification. + +## Daily edit workflow + +1. Edit encrypted secret file: + +```bash +sops secrets/secrets.yaml +``` + +2. Commit encrypted changes. +3. Rebuild on target host(s): + +```bash +nh darwin switch +# or +nh os switch +``` + +4. Verify consumer application behavior. + +## Add a new secret (text) + +1. Add key/value in `secrets/secrets.yaml` via `sops`. +2. Declare secret in relevant module: + +```nix +sops.secrets.my_new_secret = {}; +``` + +3. Consume via `.path`. +4. Rebuild and validate. + +## Add a new secret (binary) + +1. Encrypt binary into `*.sops`. +2. Add/confirm `.sops.yaml` creation rule for that path. +3. Declare with `format = "binary"`. +4. Consume via `.path`. +5. Rebuild and validate. + +## Onboard a new machine/user recipient + +1. Add recipient alias in `.sops.yaml` `keys:`. +2. Add that alias under matching `creation_rules`. +3. Rewrap existing files: + +```bash +sops updatekeys secrets/secrets.yaml +``` + +4. Commit. +5. Verify decryption on that machine/user context. + +## Rotate recipient keys + +Safe sequence: + +1. Add new recipients first. +2. Rewrap all secret files with `sops updatekeys`. +3. Deploy and validate all hosts. +4. Remove old recipients. +5. Rewrap + deploy again. + +This avoids lockout during partial rollout. + +## Verification checklist after key changes + +- `sops` can still open every encrypted file locally. +- each host class can switch successfully: + - Darwin + - NixOS + - HM context (if used standalone/system-wide) +- secret-consuming apps start/read as expected. + +## Incident response: decryption failure during switch + +Immediate checks: + +1. Was recipient removed too early? +2. Is target key file present and readable? +3. Are you in the expected context (system vs HM user)? +4. Did you rewrap all files (`updatekeys`) or only some? + +Rollback option: + +- temporarily re-add previous recipient and rewrap, +- redeploy, +- then do staged migration again. + +## Suggested automation (optional) + +Create a helper script to rewrap all secret files in one command: + +```bash +#!/usr/bin/env bash +set -euo pipefail +sops updatekeys secrets/secrets.yaml +``` + +This reduces forgotten-file errors during rotation. diff --git a/docs/sops-nix/07-troubleshooting.md b/docs/sops-nix/07-troubleshooting.md new file mode 100644 index 00000000..104e51b3 --- /dev/null +++ b/docs/sops-nix/07-troubleshooting.md @@ -0,0 +1,105 @@ +# 07 - Troubleshooting + +This chapter maps common failures to likely causes and fixes. + +## Error: `0 successful groups required, got 0` + +Typical meaning: no usable decryption key matched secret recipients. + +Check: + +1. `.sops.yaml` recipient list includes current host/user key. +2. Secret file was rewrapped (`sops updatekeys`) after recipient changes. +3. Correct key source is configured (`keyFile` vs `sshKeyPaths`). +4. Key file permissions/readability are valid for activation context. + +Related upstream discussion: + +- [Issue #744](https://github.com/Mic92/sops-nix/issues/744) + +## Secret declared but consumer cannot read file + +Likely causes: + +- wrong `owner`/`group`/`mode`, +- service runs as different user than expected, +- consumer in HM context but secret declared only in system context (or inverse), +- app expects text but secret is binary (or inverse). + +Fix: + +- align declaration context + consumer context, +- set explicit ownership fields, +- verify with app user permissions. + +## Secret path changed unexpectedly + +Cause: + +- hardcoded runtime paths instead of using `config.sops.secrets..path`. + +Fix: + +- always consume generated path attribute. + +## Home Manager secret service confusion + +Symptoms: + +- HM secret not available until user session/service state is healthy, +- restart hooks not behaving like system-unit expectations. + +Fix: + +- treat HM as user-service context, +- test in real login/session lifecycle, +- avoid assuming system-unit semantics apply unchanged. + +Reference: [sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md). + +## Darwin key location confusion + +Upstream common convention: + +- `$HOME/Library/Application Support/sops/age/keys.txt` + +If using custom locations, ensure your module configuration and actual file location match. + +## Binary secret appears corrupted + +Check: + +- declaration has `format = "binary"`, +- source `.sops` file was produced from original binary correctly, +- consumer treats output as binary, not text. + +## Activation succeeds but app still uses old secret + +Likely causes: + +- service not restarted/reloaded, +- app caches credentials, +- app reads another config source. + +Fix: + +- add `restartUnits` or `reloadUnits` where appropriate, +- confirm app startup args/path point to current secret/template paths. + +## Debug process (fast path) + +1. Confirm declaration exists (`sops.secrets.`). +2. Confirm secret key exists in encrypted file. +3. Confirm recipient can decrypt. +4. Confirm path attribute is what consumer uses. +5. Confirm service user can read file. +6. Confirm service reload/restart behavior. + +## Last-resort recovery strategy + +If rotation caused lockout: + +1. Re-add previous recipient in `.sops.yaml`. +2. `sops updatekeys` on all encrypted files. +3. Commit + deploy. +4. Re-run staged migration with explicit host verification at each stage. diff --git a/docs/sops-nix/08-repo-reference.md b/docs/sops-nix/08-repo-reference.md new file mode 100644 index 00000000..ffabe1af --- /dev/null +++ b/docs/sops-nix/08-repo-reference.md @@ -0,0 +1,151 @@ +# 08 - Repository Reference (This Dotfiles Repo) + +This chapter maps `sops-nix` concepts to exact files and patterns in this repository. + +## Core integration files + +- `flake.nix` + - declares `inputs.sops-nix` +- `modules/secrets.nix` + - single-class **Home Manager** export (`flake.modules.homeManager.dendritic`) + - imports `inputs.sops-nix.homeManagerModules.sops` + - declares the `dendritic.secrets.*` option surface (see below) + - sets defaults under that surface: + - `sops.defaultSopsFormat = "yaml"` + - `sops.defaultSopsFile = config.dendritic.secrets.defaultSopsFile` + - `sops.age.sshKeyPaths = [ config.dendritic.secrets.ageKeyPath ]` +- `modules/sops-validation.nix` + - contributes `perSystem.checks.sops-files-parse` + - validates every encrypted file structurally parses as a sops doc + - runs as part of `nix flake check` (no decryption key needed) +- `scripts/sops-updatekeys.sh` + - rewraps every sops-encrypted file against the current `.sops.yaml` + +Because `modules/default.nix` auto-imports all module files, both +`modules/secrets.nix` and `modules/sops-validation.nix` are included +automatically. + +### Why HM-only (and not NixOS / Darwin too) + +Per [`docs/dendritic-nix/02-module-mechanics.md`](../dendritic-nix/02-module-mechanics.md) +"Class-selective exporting: Only export classes where feature applies." + +Every `sops.secrets.*` consumer in this repo lives at HM level +(currently just `modules/editor.nix` anthropic_api_key). There were +previously sibling `nixos`/`darwin` class exports configured against +`/var/lib/sops-nix/key.txt`, but they had zero consumers and the key +file did not exist on the live Mac — pure dead code. + +If a future system-level consumer lands, add a sibling +`flake.modules.{nixos,darwin}.dendritic` block following the same shape +(same option namespace, equivalent `sops.age.*` configuration for the +target context). The current single-class file is the dendritic norm, +not a special case. + +## Option surface: `dendritic.secrets.*` + +| Option | Type | Default | Purpose | +| ----------------------------------- | ---- | ------------------------------------------------ | ------------------------------------------------------------------------ | +| `dendritic.secrets.enable` | bool | `true` | Whether sops-nix is wired in for the HM profile. | +| `dendritic.secrets.ageKeyPath` | str | `"${config.home.homeDirectory}/.ssh/id_ed25519"` | SSH ed25519 private key that ssh-to-age derives the decryption key from. | +| `dendritic.secrets.defaultSopsFile` | path | `../secrets/secrets.yaml` | Sops-encrypted YAML file consumed by `sops.secrets.` declarations. | + +Hosts override these in their HM user block, e.g.: + +```nix +home-manager.users."8amps" = { + dendritic.secrets.ageKeyPath = "/Volumes/USB/keys/id_ed25519"; + dendritic.secrets.defaultSopsFile = ../../secrets/per-host/mba.yaml; +}; +``` + +## Encrypted material in repo + +- `secrets/secrets.yaml` (encrypted YAML) + - currently includes `anthropic_api_key` +- `.sops.yaml` (creation + recipient rules) + +## `.sops.yaml` policy in this repo + +Current creation rules target: + +- `secrets/secrets.yaml` + +This means new encryption operations matching these paths inherit configured recipients automatically. + +## Current consumers + +### Editor module + +- file: `modules/editor.nix` +- declaration: + - `sops.secrets.anthropic_api_key = { };` +- consumption: + - plugin config reads from `config.sops.secrets.anthropic_api_key.path` + +## Key strategy in this repo + +Unified across the single (HM) export class: + +- `sops.age.sshKeyPaths = [ config.dendritic.secrets.ageKeyPath ]` +- HM packages: `sops`, `age`, `ssh-to-age` +- No separate `/var/lib/sops-nix/key.txt` to bootstrap or back up — the + SSH ed25519 key IS the decryption credential, via ssh-to-age. + +A new machine needs the same `~/.ssh/id_ed25519` registered as a +recipient in `.sops.yaml` (or have one of its existing recipients +re-encrypt files via `sops updatekeys`). + +## Recommended consistency improvements: status + +| # | Item | Status | Location | +| --- | ------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- | +| 1 | Standardize one key strategy across all contexts | Done | HM-only export via ssh-to-age; system blocks removed as dead code | +| 2 | Mass `sops updatekeys` script over all encrypted files | Done | [`scripts/sops-updatekeys.sh`](../../scripts/sops-updatekeys.sh) | +| 3 | CI lint that encrypted files are valid SOPS documents | Done | [`modules/sops-validation.nix`](../../modules/sops-validation.nix) → `nix flake check`'s `sops-files-parse` | +| 4 | Worked example of `sops.templates` + restart/reload semantics | Done | [`docs/sops-nix/05-templates-and-services.md`](./05-templates-and-services.md) "Worked example: porting CodeCompanion" | + +## Practical command reference (repo-specific) + +Edit shared YAML secrets: + +```bash +sops secrets/secrets.yaml +``` + +Rotate recipients for ALL current encrypted files (preferred): + +```bash +bash scripts/sops-updatekeys.sh +``` + +…which is equivalent to running, by hand: + +```bash +sops updatekeys --yes secrets/secrets.yaml +``` + +Validate every sops-encrypted file is structurally a sops doc: + +```bash +nix flake check # runs all checks +nix build .#checks.aarch64-darwin.sops-files-parse # just this one +``` + +Apply changes on Darwin host: + +```bash +nh darwin switch +``` + +Apply changes on NixOS host: + +```bash +nh os switch +``` + +## Where to continue + +- Architecture and concepts: [`01-architecture.md`](./01-architecture.md) +- Operations runbook: [`06-operations-and-rotation.md`](./06-operations-and-rotation.md) +- Troubleshooting: [`07-troubleshooting.md`](./07-troubleshooting.md) diff --git a/docs/sops-nix/README.md b/docs/sops-nix/README.md new file mode 100644 index 00000000..c4d5c48a --- /dev/null +++ b/docs/sops-nix/README.md @@ -0,0 +1,29 @@ +# sops-nix Documentation Suite + +This is the full multi-file deep dive for `sops-nix` in this repository. + +If you are new, read in this order: + +1. [`01-architecture.md`](./01-architecture.md) +2. [`02-key-management.md`](./02-key-management.md) +3. [`03-authoring-and-files.md`](./03-authoring-and-files.md) +4. [`04-consumption-patterns.md`](./04-consumption-patterns.md) +5. [`05-templates-and-services.md`](./05-templates-and-services.md) +6. [`06-operations-and-rotation.md`](./06-operations-and-rotation.md) +7. [`07-troubleshooting.md`](./07-troubleshooting.md) +8. [`08-repo-reference.md`](./08-repo-reference.md) + +Cross-reference: + +- Legacy single-file deep dive: [`../sops-nix.md`](../sops-nix.md) +- Dendritic architecture: [`../dendritic-patterns.md`](../dendritic-patterns.md) +- Den architecture: [`../den.md`](../den.md) + +Primary upstream references: + +- [Mic92/sops-nix README](https://github.com/Mic92/sops-nix/blob/master/README.md) +- [Mic92/sops-nix repository](https://github.com/Mic92/sops-nix) +- [NixOS module source](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix) +- [Home Manager module source](https://github.com/Mic92/sops-nix/blob/master/modules/home-manager/sops.nix) +- [Native SSH support discussion (PR #779)](https://github.com/Mic92/sops-nix/pull/779) +- [Decryption mismatch discussion (Issue #744)](https://github.com/Mic92/sops-nix/issues/744) diff --git a/docs/tmux.md b/docs/tmux.md index 06e8e63e..2acf3767 100644 --- a/docs/tmux.md +++ b/docs/tmux.md @@ -3,34 +3,41 @@ Welcome to your optimized **Tmux** environment. This setup is designed to be ergonomic, persistent, and beginner-friendly with interactive hints. ## ⌨️ The Prefix: `Ctrl-a` + The default prefix has been changed from `Ctrl-b` to **`Ctrl-a`** for better ergonomics. + > In this guide, `prefix` refers to pressing `Ctrl-a` then releasing before the next key. --- ## 🆘 How to learn keybinds (Interactive) + If you ever forget a keybind, just press: + ### **`prefix + ?`** + This opens an interactive **Which-Key** menu where you can search and execute any tmux command. This is your best friend for learning! --- ## 🏗️ Managing Panes & Windows -| Action | Keybind | -|---|---| -| **Split Vertical** | `prefix + |` | +| Action | Keybind | +| -------------------- | ------------ | --- | +| **Split Vertical** | `prefix + | ` | | **Split Horizontal** | `prefix + -` | -| **Kill Pane** | `prefix + x` | -| **Create Window** | `prefix + c` | -| **Next Window** | `prefix + n` | -| **Previous Window** | `prefix + p` | -| **Zoom Pane** | `prefix + z` | +| **Kill Pane** | `prefix + x` | +| **Create Window** | `prefix + c` | +| **Next Window** | `prefix + n` | +| **Previous Window** | `prefix + p` | +| **Zoom Pane** | `prefix + z` | --- ## 🖱️ Mouse Support + Mouse mode is **ON**. You can: + - Click to select panes - Drag to resize panes - Scroll to enter copy-mode @@ -39,7 +46,9 @@ Mouse mode is **ON**. You can: --- ## 🏃 Navigation (Vim-Style) + You can navigate between Tmux panes and Neovim splits seamlessly using: + - `Ctrl-h` (Left) - `Ctrl-j` (Down) - `Ctrl-k` (Up) @@ -48,7 +57,9 @@ You can navigate between Tmux panes and Neovim splits seamlessly using: --- ## 📋 Copy & Paste (Vi-Mode) + Enter copy mode with `prefix + [`: + 1. Navigate with `h`, `j`, `k`, `l` 2. Start selection with `v` 3. Copy with `y` (automatically syncs to system clipboard) @@ -57,7 +68,9 @@ Enter copy mode with `prefix + [`: --- ## 💾 Session Persistence + Your sessions are automatically saved and restored: + - **Save manually:** `prefix + Ctrl-s` - **Restore manually:** `prefix + Ctrl-r` - **Continuum:** Automatically saves every 15 minutes and restores on startup. @@ -65,5 +78,6 @@ Your sessions are automatically saved and restored: --- ## 🔗 URLs + Want to open a link in your terminal? Press **`prefix + u`** to fuzzy-search all URLs in the current pane and open them in your browser. diff --git a/docs/zsh-plugins.md b/docs/zsh-plugins.md index 3500008d..c0967840 100644 --- a/docs/zsh-plugins.md +++ b/docs/zsh-plugins.md @@ -8,48 +8,66 @@ Curated set of zsh plugins and CLI tools integrated into the Dendritic Nix confi ## Core Productivity & UX -### zsh-autosuggestions *(pre-existing)* +### zsh-autosuggestions _(pre-existing)_ + Fish-like inline autosuggestions as you type, drawn from command history. + - Suggestions appear in gray — press `→` or `End` to accept. - **Package:** `programs.zsh.autosuggestion.enable` -### zsh-syntax-highlighting *(pre-existing)* +### zsh-syntax-highlighting _(pre-existing)_ + Real-time syntax highlighting in the terminal. Valid commands turn green, errors turn red. + - **Package:** `programs.zsh.syntaxHighlighting.enable` -### zsh-completions *(pre-existing)* +### zsh-completions _(pre-existing)_ + Additional completion definitions for common tools not covered by default zsh. + - **Package:** `pkgs.zsh-completions` ### zsh-history-substring-search + Search history by **substring** rather than prefix. Much more powerful than default `up-arrow` behavior. + - **Keybindings:** - `↑` / `↓` — search history for commands containing the current input - Type `git co`, press `↑` → finds `git commit`, `git checkout`, etc. - **Package:** `programs.zsh.historySubstringSearch.enable` ### zsh-you-should-use + Reminds you when an alias exists for a command you just typed manually. + ``` $ git status Found existing alias for "git status". You should use: "gst" ``` + - Trains muscle memory over time. Zero friction — just prints a reminder. - **Package:** `pkgs.zsh-you-should-use` (sourced via `initContent`) ### sudo (ESC ESC) + Double-tap `Escape` to prepend `sudo` to the current or last command. Simple, indispensable. + - **Implementation:** Custom zsh widget in `initContent` ### colored-man-pages + Adds color to `man` pages using `LESS_TERMCAP` environment variables. Zero performance cost. + - **Implementation:** Environment variables in `initContent` ### extract + Smart archive extraction — `extract foo.tar.gz` handles `.tar.gz`, `.zip`, `.7z`, `.rar`, `.bz2`, `.xz`, and more. + ```bash extract archive.tar.xz # Just works, no flags to remember ``` + - **Implementation:** Shell function in `initContent` --- @@ -57,7 +75,9 @@ extract archive.tar.xz # Just works, no flags to remember ## Fuzzy Finding & Navigation ### fzf + General-purpose command-line fuzzy finder. Powers `Ctrl+R` history search, file search, and more. + - **Keybindings:** - `Ctrl+R` — fuzzy search command history - `Ctrl+T` — fuzzy find files in current directory @@ -65,21 +85,27 @@ General-purpose command-line fuzzy finder. Powers `Ctrl+R` history search, file - **Package:** `programs.fzf.enable` with `enableZshIntegration` ### fzf-tab + Replaces zsh's default tab-completion menu with fzf. Every `` becomes a fuzzy search. + ```bash cd /etc/ # Fuzzy-filter all dirs under /etc git checkout # Fuzzy-filter branches kill # Fuzzy-filter running processes ``` + - **Package:** `pkgs.zsh-fzf-tab` (sourced via `initContent`) ### zoxide + Smarter `cd` that learns your most-used directories. Replaces `cd` with `z`. + ```bash z darwin # Jumps to /etc/nix-darwin (if visited before) z mod shell # Jumps to /etc/nix-darwin/modules/shell.nix dir zi # Interactive fuzzy directory picker ``` + - **Package:** `programs.zoxide.enable` with `enableZshIntegration` --- @@ -87,13 +113,16 @@ zi # Interactive fuzzy directory picker ## Git Enhancements ### forgit + Interactive git wrapper powered by fzf. Makes git operations browsable and visual. + ```bash forgit::log # or gl — interactive git log with diff preview forgit::diff # or gd — interactive diff viewer forgit::add # or ga — interactive staging forgit::stash # interactive stash browser ``` + - **Package:** `pkgs.zsh-forgit` (sourced via `initContent`) --- @@ -101,7 +130,9 @@ forgit::stash # interactive stash browser ## Vim Integration ### zsh-vi-mode + Full vim-mode editing for the zsh command line. Consistent with Neovim muscle memory. + - **Modes:** Normal, Insert, Visual, Replace — just like Vim. - `Escape` enters Normal mode, `i`/`a` return to Insert. - Visual block selection, text objects (`ciw`, `da"`, etc.) all work. @@ -113,11 +144,14 @@ Full vim-mode editing for the zsh command line. Consistent with Neovim muscle me ## Developer Experience (CLI Tools) ### bat + `cat` replacement with syntax highlighting, line numbers, and git integration. + ```bash bat shell.nix # Syntax-highlighted file viewing bat --diff shell.nix # Shows git diff inline ``` + - Aliased as `cat` → `bat` for seamless adoption. - **Package:** `programs.bat.enable` @@ -126,72 +160,96 @@ bat --diff shell.nix # Shows git diff inline ## Nix-Specific Integrations ### direnv + nix-direnv + Automatically loads/unloads `nix develop` shell environments when you `cd` into a project with a `flake.nix`. + ```bash cd ~/myproject/ # devShell activates automatically cd ~ # devShell deactivates ``` + - Uses cached evaluations via `nix-direnv` — fast re-entry on subsequent visits. - Requires a `.envrc` file in the project root containing `use flake`. - **Package:** `programs.direnv.enable` + `programs.direnv.nix-direnv.enable` ### any-nix-shell + Keeps you in **zsh** when entering `nix-shell` or `nix develop`. Without this, Nix drops you into bash. + - **Package:** `pkgs.any-nix-shell` (sourced via `initContent`) ### comma (`,`) + Run any program from nixpkgs **without installing it**. + ```bash , cowsay "hello" # Downloads, runs cowsay, doesn't persist , httpie GET api.io # One-off HTTP request ``` + - Powered by `nix-index` — needs an initial index build (runs automatically). - **Package:** `pkgs.comma` ### nix-index + command-not-found + When you type a command that isn't installed, suggests which Nix package provides it. + ``` $ rg nix-index: rg is provided by nixpkgs#ripgrep ``` + - **Package:** `programs.nix-index.enable` with `enableZshIntegration` ### nix-zsh-completions + Tab completions for all `nix` CLI subcommands (`nix build`, `nix develop`, `nix flake show`, etc.). + - **Package:** `pkgs.nix-zsh-completions` ### manix + Fast CLI documentation search for NixOS / home-manager options. + ```bash manix "programs.zsh" # Search all option docs manix "services.openssh" # Find SSH service options ``` + - **Package:** `pkgs.manix` --- ## Prompt -### Starship *(pre-existing)* +### Starship _(pre-existing)_ + Cross-shell prompt with git status, nix-shell indicator, language versions, and more. Already configured with performance optimizations. + - **Package:** `programs.starship.enable` --- ## File Management -### Yazi *(pre-existing)* +### Yazi _(pre-existing)_ + Terminal file manager with image preview support. Invoked via `y` wrapper. + - **Package:** `programs.yazi.enable` --- ## System Monitoring -### btop *(pre-existing)* +### btop _(pre-existing)_ + Resource monitor themed by Stylix. + - **Package:** `programs.btop.enable` -### htop *(pre-existing)* +### htop _(pre-existing)_ + Classic process viewer. + - **Package:** `programs.htop.enable` diff --git a/explanation.md b/explanation.md index b983a600..2dd04622 100644 --- a/explanation.md +++ b/explanation.md @@ -3,17 +3,20 @@ This repo provides a single, shared configuration you can apply to thousands of machines without per-host files in the repo. Hardware and host identity are kept host-local where needed, while the repo stays generic and pure. ## Goals + - One shared Home Manager configuration applied everywhere. - NixOS and nix-darwin manage system settings; non‑NixOS Linux uses HM‑only. - Multi‑host support without flooding the repo with host folders. ## Repo Layout + - `flake.nix`: Core logic, inputs/outputs, shared HM modules, NixOS/Darwin wiring. - `flake.lock`: Pinned inputs for reproducibility. - `scripts/regenerate-hardware.sh`: Generates `hardware/.nix` + `hardware/.system` on NixOS hosts. - `hardware/` (optional): Host hardware files created on NixOS hosts. The repo works even without it. ## Flake Inputs + - `nixpkgs`, `nixpkgs-darwin`, `flake-utils` - `nix-darwin` (macOS system management) - `home-manager` (shared user config) @@ -21,6 +24,7 @@ This repo provides a single, shared configuration you can apply to thousands of - `determinate-nix` (recommended Nix installer on macOS/Linux) ## Outputs Overview + - `nixosConfigurations`: Built dynamically from `hardware/*.nix` + `*.system`. - Each NixOS config includes a minimal base module (`system.stateVersion`, admin user) and the shared HM for user `admin`. - `darwinConfigurations.myMac`: nix-darwin config with the shared HM for user `admin`. @@ -28,13 +32,17 @@ This repo provides a single, shared configuration you can apply to thousands of - `packages..default`: Example environment derivation. ## Shared Home Manager Modules + The flake defines `hmSharedModules` once and uses it on all platforms: + - Enable: `xdg`, `zsh`, `git`, `fzf`, `direnv` with `nix-direnv`, `starship`. - Packages: `git`, `curl`, `wget`, `ripgrep`, `fd`, `bat`, `tree`, `htop`, `neovim`, `podman`, `qemu`. - `home.stateVersion = "24.11"` is set inside HM modules when applied via NixOS/Darwin; for Linux HM‑only, it’s set in the HM output. ## NixOS (System + HM) + Multi‑host support is simple and pure: + 1. On the NixOS host, run: - `./scripts/regenerate-hardware.sh` - This writes `hardware/.nix` and `hardware/.system` in the repo. @@ -44,17 +52,22 @@ Multi‑host support is simple and pure: - `sudo nixos-rebuild switch --flake github:aspauldingcode/.dotfiles?ref=dev#` Notes: + - Each NixOS config automatically includes a minimal base module setting `system.stateVersion` and creating `users.users.admin` with `wheel` and `sudo` enabled. - The shared HM user (`admin`) is applied via `home-manager.nixosModules.home-manager` inside every NixOS config. ## macOS (System + HM) + Use nix-darwin for system, with shared HM for the same `admin` user: + - `darwin-rebuild switch --flake github:aspauldingcode/.dotfiles?ref=dev#myMac` - HM home directory on macOS: `/Users/admin`. - The flake disables Nix management inside darwin and favors Determinate Nix. ## Generic Linux (HM‑only) + For Fedora/Ubuntu or other non‑NixOS Linux, use the HM‑only output: + 1. Install Nix: - `curl -fsSL https://get.determinate.systems/nix | sh -s -- install` 2. Ensure user `admin` exists with home `/home/admin` and sudo per distro policy. @@ -64,7 +77,9 @@ For Fedora/Ubuntu or other non‑NixOS Linux, use the HM‑only output: - `nix build github:aspauldingcode/.dotfiles?ref=dev#homeConfigurations.admin-linux.activationPackage` ## Building and Testing on Non‑NixOS + You can validate NixOS configs from Fedora/Ubuntu: + - Create a stub: - `mkdir -p hardware` - `printf '{ ... }: { system.stateVersion = "24.11"; }\n' > hardware/devtest.nix` @@ -76,46 +91,55 @@ You can validate NixOS configs from Fedora/Ubuntu: - `./result/bin/run-devtest-vm` ## Fleet‑Scale Options (Avoid Repo Bloat) + If you have thousands of devices, you don’t need per‑host files in the repo: -1) Host‑local flake input overrides (recommended) +1. Host‑local flake input overrides (recommended) + - Keep the repo generic; inject hardware and identity from the host at build time. - Command pattern: - `sudo nixos-rebuild switch --flake github:aspauldingcode/.dotfiles?ref=dev#core \ - --override-input host-hw path:/etc/nixos/hardware-configuration.nix \ - --override-input host-params path:/etc/nixos/host.json` +--override-input host-hw path:/etc/nixos/hardware-configuration.nix \ +--override-input host-params path:/etc/nixos/host.json` - `host.json` example: - `{ "system": "x86_64-linux", "hostname": "esmeralda", "uuids": { "/": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "/boot": "1234-ABCD" } }` -2) Standardize disk labels or use Disko (most pure) +2. Standardize disk labels or use Disko (most pure) + - Reference `fileSystems` by label (e.g., `/dev/disk/by-label/nixos-root`) or declare disks via `disko`. - No per‑host hardware differences; a single config fits all machines with the same layout. -3) Parameterized hardware function (`mkHost`) +3. Parameterized hardware function (`mkHost`) + - Define a function that takes UUIDs/labels + hostname and returns a module: - `mkHost { hostname = "esmeralda"; uuids = { "/" = "..."; "/boot" = "..."; }; }` - Feed it via `host.json` on each machine (override input), keep the repo generic. Specializations + - Useful for a handful of alternative profiles; not ideal for thousands. ## Commands Cheat Sheet + - NixOS switch: `sudo nixos-rebuild switch --flake github:aspauldingcode/.dotfiles?ref=dev#` - macOS switch: `darwin-rebuild switch --flake github:aspauldingcode/.dotfiles?ref=dev#myMac` - Linux HM switch: `nix run nixpkgs#home-manager -- switch --flake github:aspauldingcode/.dotfiles?ref=dev#admin-linux` - Build NixOS VM: `nix build github:aspauldingcode/.dotfiles?ref=dev#nixosConfigurations..config.system.build.vm` ## Why It’s Simple + - Shared HM is defined once and reused everywhere. - NixOS/Darwin carry system responsibilities; Linux HM‑only focuses on user environment. - Multi‑host hardware handled via `hardware/*.nix` + `*.system` or host‑local inputs, keeping the repo clean. ## Notes + - HM user is `admin` by default; paths are `/home/admin` (Linux) and `/Users/admin` (macOS). - You can parameterize the username per host while keeping the same shared HM modules. - `sops-nix` is included for future secrets management; currently unused. - `packages.default` is a small example derivation; not required for usage. ## Contributing + - Use the `dev` branch for staging; reference flake URLs with `?ref=dev`. -- CI (optional): add `nix flake check` on pushes to `dev` before merging. \ No newline at end of file +- CI (optional): add `nix flake check` on pushes to `dev` before merging. diff --git a/flake.lock b/flake.lock index c50eb120..9852a95b 100644 --- a/flake.lock +++ b/flake.lock @@ -59,11 +59,11 @@ "base16-helix": { "flake": false, "locked": { - "lastModified": 1776754714, - "narHash": "sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=", + "lastModified": 1760703920, + "narHash": "sha256-m82fGUYns4uHd+ZTdoLX2vlHikzwzdu2s2rYM2bNwzw=", "owner": "tinted-theming", "repo": "base16-helix", - "rev": "4d508123037e7851ad36ebf7d9c48b0e9e1eb581", + "rev": "d646af9b7d14bff08824538164af99d0c521b185", "type": "github" }, "original": { @@ -89,6 +89,21 @@ "type": "github" } }, + "den": { + "locked": { + "lastModified": 1779284890, + "narHash": "sha256-M13hhd4qXKrAyqfTFFP8ov1dNu6acHyeQdUj/V5dj3g=", + "owner": "denful", + "repo": "den", + "rev": "8f1a59448043677ac8bc7854348c1b8ee6889c0b", + "type": "github" + }, + "original": { + "owner": "denful", + "repo": "den", + "type": "github" + } + }, "determinate-nix": { "inputs": { "determinate-nixd-aarch64-darwin": "determinate-nixd-aarch64-darwin", @@ -147,14 +162,32 @@ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/x86_64-linux" } }, + "firefox-darwin": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1778466742, + "narHash": "sha256-bA9wKcPaMNaV331epbUyuQPYHtSEfTUEaKJlpizOkrM=", + "owner": "bandithedoge", + "repo": "nixpkgs-firefox-darwin", + "rev": "878ea2595d194399abb43bb9e7456d0bde7d990e", + "type": "github" + }, + "original": { + "owner": "bandithedoge", + "repo": "nixpkgs-firefox-darwin", + "type": "github" + } + }, "firefox-gnome-theme": { "flake": false, "locked": { - "lastModified": 1776136500, - "narHash": "sha256-r0gN2brVWA351zwMV0Flmlcd6SGMvYqFbvC3DfKFM8Y=", + "lastModified": 1764873433, + "narHash": "sha256-1XPewtGMi+9wN9Ispoluxunw/RwozuTRVuuQOmxzt+A=", "owner": "rafaelmardojai", "repo": "firefox-gnome-theme", - "rev": "0f8ba203d475587f477e7ae12661bd8459e225b7", + "rev": "f7ffd917ac0d253dbd6a3bf3da06888f57c69f92", "type": "github" }, "original": { @@ -194,22 +227,6 @@ "type": "github" } }, - "flake-compat_3": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -233,7 +250,9 @@ }, "flake-parts_2": { "inputs": { - "nixpkgs-lib": "nixpkgs-lib" + "nixpkgs-lib": [ + "nixpkgs" + ] }, "locked": { "lastModified": 1777988971, @@ -252,16 +271,16 @@ "flake-parts_3": { "inputs": { "nixpkgs-lib": [ - "stylix", + "nixvim", "nixpkgs" ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", "type": "github" }, "original": { @@ -273,17 +292,16 @@ "flake-parts_4": { "inputs": { "nixpkgs-lib": [ - "system-manager", - "userborn", + "stylix", "nixpkgs" ] }, "locked": { - "lastModified": 1768135262, - "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", "type": "github" }, "original": { @@ -292,6 +310,24 @@ "type": "github" } }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "fromYaml": { "flake": false, "locked": { @@ -334,44 +370,23 @@ "url": "https://flakehub.com/f/cachix/git-hooks.nix/0.1.941" } }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "system-manager", - "userborn", - "pre-commit-hooks-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "gnome-shell": { "flake": false, "locked": { + "host": "gitlab.gnome.org", "lastModified": 1767737596, "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", "owner": "GNOME", "repo": "gnome-shell", "rev": "ef02db02bf0ff342734d525b5767814770d85b49", - "type": "github" + "type": "gitlab" }, "original": { + "host": "gitlab.gnome.org", "owner": "GNOME", + "ref": "gnome-49", "repo": "gnome-shell", - "rev": "ef02db02bf0ff342734d525b5767814770d85b49", - "type": "github" + "type": "gitlab" } }, "home-manager": { @@ -395,6 +410,34 @@ "type": "github" } }, + "ixx": { + "inputs": { + "flake-utils": [ + "nixvim", + "nuschtosSearch", + "flake-utils" + ], + "nixpkgs": [ + "nixvim", + "nuschtosSearch", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754860581, + "narHash": "sha256-EM0IE63OHxXCOpDHXaTyHIOk2cNvMCGPqLt/IdtVxgk=", + "owner": "NuschtOS", + "repo": "ixx", + "rev": "babfe85a876162c4acc9ab6fb4483df88fa1f281", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "ref": "v0.1.1", + "repo": "ixx", + "type": "github" + } + }, "microvm": { "inputs": { "nixpkgs": [ @@ -504,21 +547,6 @@ "type": "github" } }, - "nixpkgs-lib": { - "locked": { - "lastModified": 1777168982, - "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, "nixpkgs-regression": { "locked": { "lastModified": 1643052045, @@ -566,6 +594,22 @@ } }, "nixpkgs_3": { + "locked": { + "lastModified": 1746683680, + "narHash": "sha256-+5zk+UbG0+GQlKt+gIKm+OhlYvHmkAHFXvf7hl1HDeM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "16762245d811fdd74b417cc922223dc8eb741e8b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { "locked": { "lastModified": 1778430510, "narHash": "sha256-Ti+ZBvW6yrWWAg2szExVTwCd4qOJ3KlVr1tFHfyfi8Q=", @@ -581,22 +625,62 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { - "lastModified": 1777268161, - "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "lastModified": 1767799921, + "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1770107345, + "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, + "nixvim": { + "inputs": { + "flake-parts": "flake-parts_3", + "nixpkgs": [ + "nixpkgs" + ], + "nuschtosSearch": "nuschtosSearch", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1769049374, + "narHash": "sha256-h0Os2qqNyycDY1FyZgtbn28VF1ySP74/n0f+LDd8j+w=", + "owner": "nix-community", + "repo": "nixvim", + "rev": "b8f76bf5751835647538ef8784e4e6ee8deb8f95", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "nixos-25.11", + "repo": "nixvim", + "type": "github" + } + }, "nur": { "inputs": { "flake-parts": [ @@ -609,11 +693,11 @@ ] }, "locked": { - "lastModified": 1777598946, - "narHash": "sha256-X239dAGaU1+gfDj8jKH8GzlqKMcxaVfXOio+uzBOkeE=", + "lastModified": 1767886815, + "narHash": "sha256-pB2BBv6X9cVGydEV/9Y8+uGCvuYJAlsprs1v1QHjccA=", "owner": "nix-community", "repo": "NUR", - "rev": "5d55af01c0f86be583931fe99207fc56c14134b3", + "rev": "4ff84374d77ff62e2e13a46c33bfeb73590f9fef", "type": "github" }, "original": { @@ -622,49 +706,47 @@ "type": "github" } }, - "pre-commit-hooks-nix": { + "nuschtosSearch": { "inputs": { - "flake-compat": [ - "system-manager", - "userborn", - "flake-compat" - ], - "gitignore": "gitignore", + "flake-utils": "flake-utils", + "ixx": "ixx", "nixpkgs": [ - "system-manager", - "userborn", + "nixvim", "nixpkgs" ] }, "locked": { - "lastModified": 1769069492, - "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "lastModified": 1768249818, + "narHash": "sha256-ANfn5OqIxq3HONPIXZ6zuI5sLzX1sS+2qcf/Pa0kQEc=", + "owner": "NuschtOS", + "repo": "search", + "rev": "b6f77b88e9009bfde28e2130e218e5123dc66796", "type": "github" }, "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "owner": "NuschtOS", + "repo": "search", "type": "github" } }, "root": { "inputs": { "apple-silicon": "apple-silicon", + "den": "den", "determinate-nix": "determinate-nix", + "firefox-darwin": "firefox-darwin", "flake-parts": "flake-parts_2", "home-manager": "home-manager", "microvm": "microvm", "nix-darwin": "nix-darwin", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs_4", "nixpkgs-darwin": "nixpkgs-darwin", "nixpkgs-unstable": "nixpkgs-unstable", + "nixvim": "nixvim", "sops-nix": "sops-nix", "spicetify-nix": "spicetify-nix", "stylix": "stylix", - "system-manager": "system-manager" + "treefmt-nix": "treefmt-nix" } }, "sops-nix": { @@ -706,9 +788,9 @@ "spicetify-nix": { "inputs": { "nixpkgs": [ - "nixpkgs" + "nixpkgs-unstable" ], - "systems": "systems" + "systems": "systems_3" }, "locked": { "lastModified": 1778395012, @@ -731,53 +813,48 @@ "base16-helix": "base16-helix", "base16-vim": "base16-vim", "firefox-gnome-theme": "firefox-gnome-theme", - "flake-parts": "flake-parts_3", + "flake-parts": "flake-parts_4", "gnome-shell": "gnome-shell", - "nixpkgs": "nixpkgs_4", + "nixpkgs": "nixpkgs_5", "nur": "nur", - "systems": "systems_2", + "systems": "systems_4", + "tinted-foot": "tinted-foot", "tinted-kitty": "tinted-kitty", "tinted-schemes": "tinted-schemes", "tinted-tmux": "tinted-tmux", "tinted-zed": "tinted-zed" }, "locked": { - "lastModified": 1778104276, - "narHash": "sha256-/DSSnU0LLmOTG/OCgGwYpxP6+5YvxRx2g/GhI4x6aCU=", + "lastModified": 1778631076, + "narHash": "sha256-2qxjOZGrAUldMIlb2r1aru1rHeAqBsfeu0wK9u5Te1o=", "owner": "danth", "repo": "stylix", - "rev": "18ed8d270231e067fe2739998479ed5d7c659c2c", + "rev": "40522200fb9972bcbcb4cbc5bca1171e34be73bf", "type": "github" }, "original": { "owner": "danth", + "ref": "release-25.11", "repo": "stylix", "type": "github" } }, - "system-manager": { - "inputs": { - "flake-compat": "flake-compat_3", - "nixpkgs": [ - "nixpkgs" - ], - "userborn": "userborn" - }, + "systems": { "locked": { - "lastModified": 1777874990, - "narHash": "sha256-mQptVpwNFEgWRTZx6LhhxW4r1na+rwheWfgIIhcLOrE=", - "owner": "numtide", - "repo": "system-manager", - "rev": "3f1bffc59e51fc9816a1cf523e0093f11bc9bbf5", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "numtide", - "repo": "system-manager", + "owner": "nix-systems", + "repo": "default", "type": "github" } }, - "systems": { + "systems_2": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -792,7 +869,7 @@ "type": "github" } }, - "systems_2": { + "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -807,7 +884,7 @@ "type": "github" } }, - "systems_3": { + "systems_4": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -822,6 +899,23 @@ "type": "github" } }, + "tinted-foot": { + "flake": false, + "locked": { + "lastModified": 1726913040, + "narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=", + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + } + }, "tinted-kitty": { "flake": false, "locked": { @@ -841,11 +935,11 @@ "tinted-schemes": { "flake": false, "locked": { - "lastModified": 1777041405, - "narHash": "sha256-BAGZ7ObFV/9Z61OJZun7ifPyhkuHqNuW1QIhQ8LuzCo=", + "lastModified": 1767817087, + "narHash": "sha256-eGE8OYoK6HzhJt/7bOiNV2cx01IdIrHL7gXgjkHRdNo=", "owner": "tinted-theming", "repo": "schemes", - "rev": "5f868b3a338b6904c47f3833b9c411be641983a8", + "rev": "bd99656235aab343e3d597bf196df9bc67429507", "type": "github" }, "original": { @@ -857,11 +951,11 @@ "tinted-tmux": { "flake": false, "locked": { - "lastModified": 1777169200, - "narHash": "sha256-h7dDbIzP5hDr9v97w9PL6jdAgXawmj6krcH+959rqpU=", + "lastModified": 1767489635, + "narHash": "sha256-e6nnFnWXKBCJjCv4QG4bbcouJ6y3yeT70V9MofL32lU=", "owner": "tinted-theming", "repo": "tinted-tmux", - "rev": "f798c2dce44ef815bb6b8f05a82135c7942d35ac", + "rev": "3c32729ccae99be44fe8a125d20be06f8d7d8184", "type": "github" }, "original": { @@ -873,11 +967,11 @@ "tinted-zed": { "flake": false, "locked": { - "lastModified": 1777463218, - "narHash": "sha256-Bhkozqtq3BKLqWTlmKm8uAptfX4aRGI8QX3eEL54Vpc=", + "lastModified": 1767488740, + "narHash": "sha256-wVOj0qyil8m+ouSsVZcNjl5ZR+1GdOOAooAatQXHbuU=", "owner": "tinted-theming", "repo": "base16-zed", - "rev": "5768d08ed2e7944a26a958868cdb073cb8856dae", + "rev": "11abb0b282ad3786a2aae088d3a01c60916f2e40", "type": "github" }, "original": { @@ -886,32 +980,21 @@ "type": "github" } }, - "userborn": { + "treefmt-nix": { "inputs": { - "flake-compat": [ - "system-manager", - "flake-compat" - ], - "flake-parts": "flake-parts_4", - "nixpkgs": [ - "system-manager", - "nixpkgs" - ], - "pre-commit-hooks-nix": "pre-commit-hooks-nix", - "systems": "systems_3" + "nixpkgs": "nixpkgs_6" }, "locked": { - "lastModified": 1770377964, - "narHash": "sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=", - "owner": "jfroche", - "repo": "userborn", - "rev": "55c2cd7952c207a62736a5bbd9499ea73da18d24", + "lastModified": 1775636079, + "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", "type": "github" }, "original": { - "owner": "jfroche", - "ref": "system-manager", - "repo": "userborn", + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } } diff --git a/flake.nix b/flake.nix index be6aaa04..6f5d5794 100644 --- a/flake.nix +++ b/flake.nix @@ -5,9 +5,19 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs-darwin.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - flake-parts.url = "github:hercules-ci/flake-parts"; - + treefmt-nix.url = "github:numtide/treefmt-nix"; + firefox-darwin.url = "github:bandithedoge/nixpkgs-firefox-darwin"; + + nixvim = { + url = "github:nix-community/nixvim/nixos-25.11"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + nix-darwin = { url = "github:LnL7/nix-darwin/nix-darwin-25.11"; inputs.nixpkgs.follows = "nixpkgs-darwin"; @@ -25,18 +35,11 @@ determinate-nix.url = "github:DeterminateSystems/determinate"; - - - system-manager = { - url = "github:numtide/system-manager"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - stylix.url = "github:danth/stylix"; + stylix.url = "github:danth/stylix/release-25.11"; spicetify-nix = { url = "github:Gerg-L/spicetify-nix"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; }; apple-silicon = { @@ -49,126 +52,191 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + # ── Spike: aspect-oriented dendritic framework ─────────────────────── + # Scoped experiment: convert `modules/styling.nix` to a `den.aspects.*` + # definition using den's built-in `os` custom class to collapse the + # NixOS+Darwin Stylix duplication. Rest of the repo stays on vanilla + # flake-parts dendritic. + den.url = "github:denful/den"; + }; - outputs = inputs@{ flake-parts, ... }: + outputs = + inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; imports = [ ./modules + inputs.flake-parts.flakeModules.modules + inputs.treefmt-nix.flakeModule ]; - perSystem = { config, self', inputs', pkgs, system, ... }: { - # Formatter for `nix fmt` - formatter = pkgs.nixfmt; - - # Development shell available via `nix develop` - devShells.default = pkgs.mkShell { - name = "dotfiles-devshell"; - buildInputs = with pkgs; [ - git - nixfmt - sops - age - ]; - }; - - apps.install = { - type = "app"; - program = let - installScript = pkgs.writeShellApplication { - name = "install-system"; - runtimeInputs = [ - pkgs.git - pkgs.nh - pkgs.nix - ]; - text = '' - set -e - - REPO_URL="git@github.com:aspauldingcode/.dotfiles.git" - - # Determine the system config directory based on OS - if [[ "$OSTYPE" == "darwin"* ]]; then - TARGET_DIR="/etc/nix-darwin/.dotfiles" - else - TARGET_DIR="/etc/nixos" - fi - - if [ ! -d "$TARGET_DIR" ]; then - echo "Cloning $REPO_URL to $TARGET_DIR..." - sudo git clone "$REPO_URL" "$TARGET_DIR" - fi - - cd "$TARGET_DIR" - - # Ensure Applications directory exists and is writable by the current user - if [ -d "$HOME/Applications" ]; then - sudo chown "$USER" "$HOME/Applications" - sudo chmod 755 "$HOME/Applications" - else - mkdir -p "$HOME/Applications" - fi - - if [[ "$OSTYPE" == "darwin"* ]]; then - # 1. Prime Touch ID for the very first switch - if [ ! -f /etc/pam.d/sudo_local ]; then - echo "Priming native Touch ID support..." - echo "auth sufficient pam_tid.so" | sudo tee /etc/pam.d/sudo_local > /dev/null - fi - - # 2. Run the switch (this will trigger the maintenance module automatically) - nh darwin switch -H mba "$TARGET_DIR" - else - nh os switch "$TARGET_DIR" - fi - - echo "Installation complete!" - exit 0 - ''; + perSystem = + { + config, + pkgs, + ... + }: + { + # ── treefmt: unified formatter (nix fmt) ───────────────── + treefmt = { + projectRootFile = "flake.nix"; + # Keep in sync with `treefmt.toml` — `treefmt-nix` builds its + # own config from these Nix settings and ignores `treefmt.toml`, + # so excludes only listed there would be silently bypassed by + # the build-time `checks..treefmt` derivation. Most + # critically: every `sops` re-encrypt rewrites the encrypted + # envelope's metadata block (its formatting differs from + # prettier's YAML output), so any formatter touching + # `secrets/**` would break the next build after a secret edit. + settings.global.excludes = [ + "subrepos/microvm.nix/**" + "*.lock" + "result" + "secrets/**" + "*.sops" + "*.pem.sops" + ".git/**" + "*.patch" + "*.xml" + "*.xpi" + "*.json" + "flake_*.json" + ]; + programs = { + nixfmt.enable = true; # *.nix + shfmt.enable = true; # *.sh / *.bash + stylua.enable = true; # *.lua + ruff.enable = true; # *.py + prettier.enable = true; # *.{js,ts,json,css,html,md} }; - in "${installScript}/bin/install-system"; - }; - - apps.uninstall = { - type = "app"; - program = let - uninstallScript = pkgs.writeShellApplication { - name = "uninstall-system"; - runtimeInputs = [ pkgs.dialog pkgs.nix pkgs.nh pkgs.sudo ]; - text = '' - set -e - - if ! dialog --title "Uninstall Dendritic Nix" \ - --yesno "This will uninstall nix-darwin and reset the profile. Are you sure?" 10 60; then - clear - exit 0 - fi - - clear - echo "Starting uninstallation..." - - if command -v darwin-uninstaller &> /dev/null; then - sudo darwin-uninstaller - else - sudo nix --extra-experimental-features "nix-command flakes" run nix-darwin#darwin-uninstaller - fi - - sudo nix-env --profile /nix/var/nix/profiles/system --delete-generations old || true - sudo rm -rf /nix/var/nix/profiles/system* || true - sudo nix-collect-garbage -d - - echo "Uninstallation complete!" - ''; + # swiftformat has no treefmt-nix module; keep as a passthrough + settings.formatter.swiftformat = { + command = "${pkgs.swiftformat}/bin/swiftformat"; + options = [ "--stdinpath" ]; + includes = [ "*.swift" ]; }; - in "${uninstallScript}/bin/uninstall-system"; + }; + + # `nix fmt` delegates to treefmt + formatter = config.treefmt.build.wrapper; + + # Development shell available via `nix develop` + devShells.default = pkgs.mkShell { + name = "dotfiles-devshell"; + buildInputs = with pkgs; [ + git + config.treefmt.build.wrapper # treefmt + all formatters + sops + age + ]; + }; + + apps.install = { + type = "app"; + program = + let + installScript = pkgs.writeShellApplication { + name = "install-system"; + runtimeInputs = [ + pkgs.git + pkgs.nh + pkgs.nix + ]; + text = '' + set -e + + REPO_URL="git@github.com:aspauldingcode/.dotfiles.git" + + # Determine the system config directory based on OS + if [[ "$OSTYPE" == "darwin"* ]]; then + TARGET_DIR="/etc/nix-darwin/.dotfiles" + else + TARGET_DIR="/etc/nixos" + fi + + if [ ! -d "$TARGET_DIR" ]; then + echo "Cloning $REPO_URL to $TARGET_DIR..." + sudo git clone "$REPO_URL" "$TARGET_DIR" + fi + + cd "$TARGET_DIR" + + # Ensure Applications directory exists and is writable by the current user + if [ -d "$HOME/Applications" ]; then + sudo chown "$USER" "$HOME/Applications" + sudo chmod 755 "$HOME/Applications" + else + mkdir -p "$HOME/Applications" + fi + + if [[ "$OSTYPE" == "darwin"* ]]; then + # 1. Prime Touch ID for the very first switch + if [ ! -f /etc/pam.d/sudo_local ]; then + echo "Priming native Touch ID support..." + echo "auth sufficient pam_tid.so" | sudo tee /etc/pam.d/sudo_local > /dev/null + fi + + # 2. Run the switch (this will trigger the maintenance module automatically) + nh darwin switch -H mba "$TARGET_DIR" + else + nh os switch "$TARGET_DIR" + fi + + echo "Installation complete!" + exit 0 + ''; + }; + in + "${installScript}/bin/install-system"; + }; + + apps.uninstall = { + type = "app"; + program = + let + uninstallScript = pkgs.writeShellApplication { + name = "uninstall-system"; + runtimeInputs = [ + pkgs.dialog + pkgs.nix + pkgs.nh + ]; + text = '' + set -e + + if ! dialog --title "Uninstall Dendritic Nix" \ + --yesno "This will uninstall nix-darwin and reset the profile. Are you sure?" 10 60; then + clear + exit 0 + fi + + clear + echo "Starting uninstallation..." + + if command -v darwin-uninstaller &> /dev/null; then + sudo darwin-uninstaller + else + sudo nix --extra-experimental-features "nix-command flakes" run nix-darwin#darwin-uninstaller + fi + + sudo nix-env --profile /nix/var/nix/profiles/system --delete-generations old || true + sudo rm -rf /nix/var/nix/profiles/system* || true + sudo nix-collect-garbage -d + + echo "Uninstallation complete!" + ''; + }; + in + "${uninstallScript}/bin/uninstall-system"; + }; }; - }; - flake = { - # NixOS, Darwin, and Home Manager configurations will be built dynamically - # or defined in the flake-module. - }; }; -} \ No newline at end of file +} diff --git a/hosts/darwin/mba/default.nix b/hosts/darwin/mba/default.nix index 1454655a..f7802010 100644 --- a/hosts/darwin/mba/default.nix +++ b/hosts/darwin/mba/default.nix @@ -1,190 +1,203 @@ -{ inputs, pkgs, lib, ... }: +{ + inputs, + pkgs, + lib, + config, + ... +}: { nixpkgs.config.allowUnfree = true; nixpkgs.config.allowBroken = true; - imports = [ # 1. Base identity and platform inputs.determinate-nix.darwinModules.default { - nixpkgs.hostPlatform = "aarch64-darwin"; - nixpkgs.overlays = [ - inputs.self.overlays.default - ]; - system.primaryUser = "8amps"; - networking.hostName = "mba"; - system.stateVersion = 5; - system.defaults.dock.show-recents = false; - system.defaults.finder.AppleShowAllFiles = true; - system.defaults.finder.ShowPathbar = true; - system.defaults.finder.ShowStatusBar = true; - - documentation.enable = lib.mkForce false; - documentation.man.enable = lib.mkForce false; - documentation.doc.enable = lib.mkForce false; - documentation.info.enable = false; - - environment.systemPackages = [ - pkgs.socat - ]; - - nix.enable = false; - nix.settings = { - experimental-features = [ "nix-command" "flakes" ]; - warn-dirty = false; - substituters = [ "https://cache.nixos.org" "https://cache.flakehub.com" ]; - trusted-public-keys = [ - "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" - "cache.flakehub.com-3:hJuILl5sVK4iKm86JzgdXW12Y2Hwd5G07qKtHTOcDCM=" - "cache.flakehub.com-4:Asi8qIv291s0aYLyH6IOnr5Kf6+OF14WVjkE6t3xMio=" - "cache.flakehub.com-5:zB96CRlL7tiPtzA9/WKyPkp3A2vqxqgdgyTVNGShPDU=" - "cache.flakehub.com-6:W4EGFwAGgBj3he7c5fNh9NkOXw0PUVaxygCVKeuvaqU=" - "cache.flakehub.com-7:mvxJ2DZVHn/kRxlIaxYNMuDG1OvMckZu32um1TadOR8=" - "cache.flakehub.com-8:moO+OVS0mnTjBTcOUh2kYLQEd59ExzyoW1QgQ8XAARQ=" - "cache.flakehub.com-9:wChaSeTI6TeCuV/Sg2513ZIM9i0qJaYsF+lZCXg0J6o=" - "cache.flakehub.com-10:2GqeNlIp6AKp4EF2MVbE1kBOp9iBSyo0UPR9KoR0o1Y=" + config = { + nixpkgs.hostPlatform = "aarch64-darwin"; + nixpkgs.overlays = [ + inputs.self.overlays.default ]; - netrc-file = "/nix/var/determinate/netrc"; - trusted-users = [ "@wheel" "root" "8amps" ]; - }; - - security.pam.services.sudo_local.touchIdAuth = true; - security.pam.services.sudo_local.reattach = true; - - users.users."8amps" = { - name = "8amps"; - home = "/Users/8amps"; - shell = pkgs.zsh; + system.primaryUser = "8amps"; + networking.hostName = "mba"; + system.stateVersion = 5; + system.defaults.dock.show-recents = false; + system.defaults.finder.AppleShowAllFiles = true; + system.defaults.finder.ShowPathbar = true; + system.defaults.finder.ShowStatusBar = true; + dendritic.theme.variant = lib.mkDefault "dark"; + + documentation.enable = lib.mkForce false; + documentation.man.enable = lib.mkForce false; + documentation.doc.enable = lib.mkForce false; + documentation.info.enable = false; + + environment.systemPackages = [ + pkgs.socat + ]; + + nix.enable = false; + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + warn-dirty = false; + substituters = [ + "https://cache.nixos.org" + "https://cache.flakehub.com" + ]; + trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "cache.flakehub.com-3:hJuILl5sVK4iKm86JzgdXW12Y2Hwd5G07qKtHTOcDCM=" + "cache.flakehub.com-4:Asi8qIv291s0aYLyH6IOnr5Kf6+OF14WVjkE6t3xMio=" + "cache.flakehub.com-5:zB96CRlL7tiPtzA9/WKyPkp3A2vqxqgdgyTVNGShPDU=" + "cache.flakehub.com-6:W4EGFwAGgBj3he7c5fNh9NkOXw0PUVaxygCVKeuvaqU=" + "cache.flakehub.com-7:mvxJ2DZVHn/kRxlIaxYNMuDG1OvMckZu32um1TadOR8=" + "cache.flakehub.com-8:moO+OVS0mnTjBTcOUh2kYLQEd59ExzyoW1QgQ8XAARQ=" + "cache.flakehub.com-9:wChaSeTI6TeCuV/Sg2513ZIM9i0qJaYsF+lZCXg0J6o=" + "cache.flakehub.com-10:2GqeNlIp6AKp4EF2MVbE1kBOp9iBSyo0UPR9KoR0o1Y=" + ]; + netrc-file = "/nix/var/determinate/netrc"; + trusted-users = [ + "@wheel" + "root" + "8amps" + ]; + }; + + security.pam.services.sudo_local.touchIdAuth = true; + security.pam.services.sudo_local.reattach = true; + + # Disable Gatekeeper declaratively + system.activationScripts.disableGatekeeper.text = '' + echo "Disabling Gatekeeper..." + /usr/sbin/spctl --master-disable + ''; + + users.users."8amps" = { + name = "8amps"; + home = "/Users/8amps"; + shell = pkgs.zsh; + }; }; } # 2. Import Home Manager inputs.home-manager.darwinModules.home-manager - # 3. Pull in Feature Modules from the Hub (inputs.self.modules) - inputs.self.darwinModules.shell - inputs.self.darwinModules.secrets - inputs.self.darwinModules.styling - inputs.self.darwinModules.mas - inputs.self.darwinModules.wallpaper - inputs.self.darwinModules.microvm - inputs.self.darwinModules.dock - inputs.self.darwinModules.apps - inputs.self.darwinModules.python - inputs.self.darwinModules.maintenance + # 3. Pull in the merged Dendritic feature module + inputs.self.modules.darwin.dendritic # 4. Configure Home Manager { home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.backupFileExtension = lib.mkForce "backup"; + + # Stylix evaluates GNOME targets inside HM by default, which throws an + # evaluation error on nix-darwin because `services.displayManager.generic` + # doesn't exist. Disable it globally for all HM profiles. + home-manager.sharedModules = [ + { + stylix.targets.gnome.enable = lib.mkForce false; + } + { + dendritic.theme.variant = lib.mkDefault config.dendritic.theme.variant; + } + ]; home-manager.extraSpecialArgs = { inherit inputs; }; - home-manager.users."8amps" = { config, ... }: { - gtk.gtk4.theme = null; - manual.manpages.enable = false; - manual.html.enable = false; - manual.json.enable = false; - - # Wawona LaunchAgent - # launchd.agents.wawona = { - # enable = true; - # config = { - # Label = "com.aspaulding.wawona"; - # ProgramArguments = [ "${pkgs.wawona}/bin/wawona" ]; - # KeepAlive = true; - # RunAtLoad = true; - # StandardOutPath = "${config.home.homeDirectory}/.cache/wawona.log"; - # StandardErrorPath = "${config.home.homeDirectory}/.cache/wawona.err"; - # }; - # }; - - # Waypipe LaunchAgent (Host-side proxy for Wawona) - # launchd.agents.waypipe = { - # enable = true; - # config = { - # Label = "com.aspaulding.waypipe"; - # ProgramArguments = [ - # "${inputs.wawona.packages.${pkgs.stdenv.hostPlatform.system}.wawona-macos}/Applications/Wawona.app/Contents/MacOS/waypipe" - # "--display" - # "/tmp/wawona-503/wayland-0" - # "-s" - # "/etc/nix-darwin/.dotfiles/waypipe-wawona.sock" - # "client" - # ]; - # EnvironmentVariables = { - # WAYLAND_DISPLAY = "wayland-0"; - # XDG_RUNTIME_DIR = "/tmp/wawona-503"; - # }; - # KeepAlive = true; - # RunAtLoad = true; - # StandardOutPath = "${config.home.homeDirectory}/.cache/waypipe.log"; - # StandardErrorPath = "${config.home.homeDirectory}/.cache/waypipe.err"; - # }; - # }; - - # Bridge LaunchAgent: connects the VSOCK socket from vfkit to the Waypipe socket - # launchd.agents.waypipe-bridge = { - # enable = true; - # config = { - # Label = "com.aspaulding.waypipe-bridge"; - # ProgramArguments = [ - # "${pkgs.socat}/bin/socat" - # "UNIX-CONNECT:/etc/nix-darwin/.dotfiles/dendritic-vm-vsock.sock" - # "UNIX-CONNECT:/etc/nix-darwin/.dotfiles/waypipe-wawona.sock" - # ]; - # KeepAlive = { - # SuccessfulExit = false; - # }; - # RunAtLoad = true; - # StandardOutPath = "${config.home.homeDirectory}/.cache/waypipe-bridge.log"; - # StandardErrorPath = "${config.home.homeDirectory}/.cache/waypipe-bridge.err"; - # }; - # }; - - imports = [ - inputs.self.homeManagerModules.shell - inputs.self.homeManagerModules.terminal - inputs.self.homeManagerModules.editor - inputs.self.homeManagerModules.secrets - # inputs.self.homeManagerModules.styling - inputs.self.homeManagerModules.apps - inputs.self.homeManagerModules.ghostty - inputs.self.homeManagerModules.antigravity - inputs.self.homeManagerModules.cursor - inputs.self.homeManagerModules.beeper - inputs.self.homeManagerModules.python - inputs.self.homeManagerModules.jetbrains - inputs.self.homeManagerModules.wallpaper - inputs.self.homeManagerModules.spotify - inputs.self.homeManagerModules.vesktop - inputs.self.homeManagerModules.opencode - inputs.self.homeManagerModules.qt - ]; - home.username = "8amps"; - home.homeDirectory = "/Users/8amps"; - home.stateVersion = "24.11"; - - # ── App Linking ───────────────────────────────────────────── - targets.darwin.copyApps.enable = true; - targets.darwin.linkApps.enable = false; - - # ── Feature Toggles ───────────────────────────────────────── - dendritic.apps.ghostty.enable = true; - dendritic.apps.antigravity.enable = true; - dendritic.apps.cursor.enable = true; - dendritic.apps.beeper.enable = true; - dendritic.apps.jetbrains.enable = true; - dendritic.wallpaper.enable = true; - dendritic.python.enable = true; - - # programs.zsh.shellAliases = { - # microvm-run = "${inputs.self.nixosConfigurations.microvm.config.microvm.runner.vfkit}/bin/microvm-run"; - # }; - - # ───────────────────────────────────────────────────────────── - }; + home-manager.users."8amps" = + { config, ... }: + { + gtk.gtk4.theme = null; + manual.manpages.enable = false; + manual.html.enable = false; + manual.json.enable = false; + + # Wawona LaunchAgent + # launchd.agents.wawona = { + # enable = true; + # config = { + # Label = "com.aspaulding.wawona"; + # ProgramArguments = [ "${pkgs.wawona}/bin/wawona" ]; + # KeepAlive = true; + # RunAtLoad = true; + # StandardOutPath = "${config.home.homeDirectory}/.cache/wawona.log"; + # StandardErrorPath = "${config.home.homeDirectory}/.cache/wawona.err"; + # }; + # }; + + # Waypipe LaunchAgent (Host-side proxy for Wawona) + # launchd.agents.waypipe = { + # enable = true; + # config = { + # Label = "com.aspaulding.waypipe"; + # ProgramArguments = [ + # "${inputs.wawona.packages.${pkgs.stdenv.hostPlatform.system}.wawona-macos}/Applications/Wawona.app/Contents/MacOS/waypipe" + # "--display" + # "/tmp/wawona-503/wayland-0" + # "-s" + # "/etc/nix-darwin/.dotfiles/waypipe-wawona.sock" + # "client" + # ]; + # EnvironmentVariables = { + # WAYLAND_DISPLAY = "wayland-0"; + # XDG_RUNTIME_DIR = "/tmp/wawona-503"; + # }; + # KeepAlive = true; + # RunAtLoad = true; + # StandardOutPath = "${config.home.homeDirectory}/.cache/waypipe.log"; + # StandardErrorPath = "${config.home.homeDirectory}/.cache/waypipe.err"; + # }; + # }; + + # Bridge LaunchAgent: connects the VSOCK socket from vfkit to the Waypipe socket + # launchd.agents.waypipe-bridge = { + # enable = true; + # config = { + # Label = "com.aspaulding.waypipe-bridge"; + # ProgramArguments = [ + # "${pkgs.socat}/bin/socat" + # "UNIX-CONNECT:/etc/nix-darwin/.dotfiles/dendritic-vm-vsock.sock" + # "UNIX-CONNECT:/etc/nix-darwin/.dotfiles/waypipe-wawona.sock" + # ]; + # KeepAlive = { + # SuccessfulExit = false; + # }; + # RunAtLoad = true; + # StandardOutPath = "${config.home.homeDirectory}/.cache/waypipe-bridge.log"; + # StandardErrorPath = "${config.home.homeDirectory}/.cache/waypipe-bridge.err"; + # }; + # }; + + imports = [ + inputs.self.modules.homeManager.dendritic + ]; + home.username = "8amps"; + home.homeDirectory = "/Users/8amps"; + home.stateVersion = "24.11"; + + # ── App Linking ───────────────────────────────────────────── + targets.darwin.copyApps.enable = false; + targets.darwin.linkApps.enable = true; + + # ── Feature Toggles ───────────────────────────────────────── + dendritic.apps.ghostty.enable = true; + dendritic.apps.antigravity.enable = true; + dendritic.apps.cursor.enable = true; + dendritic.apps.beeper.enable = true; + dendritic.apps.jetbrains.enable = true; + dendritic.wallpaper.enable = true; + dendritic.python.enable = true; + + # programs.zsh.shellAliases = { + # microvm-run = "${inputs.self.nixosConfigurations.microvm.config.microvm.runner.vfkit}/bin/microvm-run"; + # }; + + # ───────────────────────────────────────────────────────────── + }; } # 5. Mac App Store — declarative apps via mas CLI @@ -201,9 +214,22 @@ # Installed via mas, just like Brave/Firefox extensions but # for Safari. Enable them in: Safari → Settings → Extensions safari.extensions = [ - { name = "uBlock Origin Lite"; id = 6745342698; } - { name = "SponsorBlock for Safari"; id = 1573461917; } - { name = "Dark Reader for Safari"; id = 1438243180; } + { + name = "Momentum"; + id = 1564329434; + } + { + name = "uBlock Origin Lite"; + id = 6745342698; + } + { + name = "SponsorBlock for Safari"; + id = 1573461917; + } + { + name = "Dark Reader for Safari"; + id = 1438243180; + } ]; }; } diff --git a/hosts/hm/8amps-linux/default.nix b/hosts/hm/8amps-linux/default.nix index 74eb5f15..dbe5480f 100644 --- a/hosts/hm/8amps-linux/default.nix +++ b/hosts/hm/8amps-linux/default.nix @@ -2,7 +2,6 @@ { imports = [ - inputs.self.homeManagerModules.theme { home.username = "8amps"; home.homeDirectory = "/home/8amps"; @@ -14,29 +13,17 @@ gtk.gtk4.theme = null; } - # Pull in Feature Modules from the Hub - inputs.self.homeManagerModules.shell - inputs.self.homeManagerModules.editor - inputs.self.homeManagerModules.secrets - inputs.self.homeManagerModules.styling - inputs.self.homeManagerModules.apps - inputs.self.homeManagerModules.ghostty - inputs.self.homeManagerModules.antigravity - inputs.self.homeManagerModules.python - inputs.self.homeManagerModules.wallpaper - inputs.self.homeManagerModules.spotify - inputs.self.homeManagerModules.vesktop - inputs.self.homeManagerModules.opencode - inputs.self.homeManagerModules.qt - inputs.self.homeManagerModules.linux-desktop - - # External modules + # Pull in the merged Dendritic feature module + inputs.self.modules.homeManager.dendritic { # ── Feature Toggles ───────────────────────────────────────── dendritic.apps.ghostty.enable = true; + dendritic.apps.vscode.enable = false; + dendritic.apps.cursor.enable = false; dendritic.apps.linux-desktop.enable = true; dendritic.apps.antigravity.enable = false; + dendritic.apps.jetbrains.enable = false; dendritic.python.enable = true; # ───────────────────────────────────────────────────────────── } diff --git a/hosts/nixos/mba-asahi/default.nix b/hosts/nixos/mba-asahi/default.nix index 35996eb8..1ee53066 100644 --- a/hosts/nixos/mba-asahi/default.nix +++ b/hosts/nixos/mba-asahi/default.nix @@ -1,4 +1,10 @@ -{ inputs, pkgs, lib, ... }: +{ + inputs, + pkgs, + lib, + config, + ... +}: { imports = [ @@ -15,9 +21,18 @@ shell = pkgs.zsh; }; - fileSystems."/" = { device = "/dev/disk/by-label/nixos"; fsType = "ext4"; }; - fileSystems."/boot" = { device = "/dev/disk/by-label/boot"; fsType = "vfat"; }; - fileSystems."/boot/asahi" = { device = "/dev/disk/by-label/asahi"; fsType = "vfat"; }; + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + fileSystems."/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "vfat"; + }; + fileSystems."/boot/asahi" = { + device = "/dev/disk/by-label/asahi"; + fsType = "vfat"; + }; hardware.asahi.extractPeripheralFirmware = false; } @@ -25,38 +40,28 @@ inputs.apple-silicon.nixosModules.apple-silicon-support inputs.home-manager.nixosModules.home-manager - # 3. Pull in Feature Modules from the Hub - inputs.self.nixosModules.shell - inputs.self.nixosModules.secrets - inputs.self.nixosModules.styling - inputs.self.nixosModules.linux-desktop - inputs.self.nixosModules.microvm - inputs.self.nixosModules.python + # 3. Pull in the merged Dendritic feature module + inputs.self.modules.nixos.dendritic # 4. Configure Home Manager { home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.backupFileExtension = "backup"; + home-manager.sharedModules = [ + { + dendritic.theme.variant = lib.mkDefault config.dendritic.theme.variant; + } + ]; home-manager.extraSpecialArgs = { inherit inputs; }; home-manager.users."8amps" = { imports = [ - inputs.self.homeManagerModules.shell - inputs.self.homeManagerModules.editor - inputs.self.homeManagerModules.secrets - inputs.self.homeManagerModules.styling - inputs.self.homeManagerModules.apps - inputs.self.homeManagerModules.ghostty - inputs.self.homeManagerModules.python - inputs.self.homeManagerModules.nixvim-ide - inputs.self.homeManagerModules.wallpaper - inputs.self.homeManagerModules.spotify - inputs.self.homeManagerModules.vesktop + inputs.self.modules.homeManager.dendritic ]; home.username = "8amps"; home.homeDirectory = "/home/8amps"; home.stateVersion = "24.11"; - + # ── Show Hidden Files (GTK) ────────────────────────────────── dconf.settings = { "org/gtk/settings/file-chooser" = { @@ -69,7 +74,6 @@ # ── Feature Toggles ───────────────────────────────────────── dendritic.apps.ghostty.enable = true; - dendritic.apps.nixvim-ide.enable = true; dendritic.python.enable = true; # ───────────────────────────────────────────────────────────── diff --git a/hosts/nixos/nixos-test/default.nix b/hosts/nixos/nixos-test/default.nix index 415f8f79..caa6f2c7 100644 --- a/hosts/nixos/nixos-test/default.nix +++ b/hosts/nixos/nixos-test/default.nix @@ -1,4 +1,10 @@ -{ inputs, pkgs, ... }: +{ + inputs, + pkgs, + lib, + config, + ... +}: { imports = [ @@ -8,15 +14,18 @@ nixpkgs.config.allowUnfree = true; nixpkgs.overlays = [ ]; system.stateVersion = "24.11"; - + users.users."8amps" = { isNormalUser = true; extraGroups = [ "wheel" ]; shell = pkgs.zsh; }; - + # Dummy filesystem for CI verification - fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; + fileSystems."/" = { + device = "/dev/sda1"; + fsType = "ext4"; + }; boot.loader.grub.enable = true; boot.loader.grub.device = "nodev"; boot.kernelPackages = pkgs.linuxPackages_latest; @@ -25,41 +34,30 @@ # 2. Import Home Manager inputs.home-manager.nixosModules.home-manager - # 3. Pull in Feature Modules from the Hub - inputs.self.nixosModules.shell - inputs.self.nixosModules.secrets - inputs.self.nixosModules.styling - inputs.self.nixosModules.linux-desktop - inputs.self.nixosModules.microvm - inputs.self.nixosModules.python + # 3. Pull in the merged Dendritic feature module + inputs.self.modules.nixos.dendritic # 4. Configure Home Manager { home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.backupFileExtension = "backup"; + home-manager.sharedModules = [ + { + dendritic.theme.variant = lib.mkDefault config.dendritic.theme.variant; + } + ]; home-manager.extraSpecialArgs = { inherit inputs; }; home-manager.users."8amps" = { imports = [ - inputs.self.homeManagerModules.shell - inputs.self.homeManagerModules.editor - inputs.self.homeManagerModules.secrets - inputs.self.homeManagerModules.styling - inputs.self.homeManagerModules.apps - inputs.self.homeManagerModules.ghostty - inputs.self.homeManagerModules.python - inputs.self.homeManagerModules.nixvim-ide - inputs.self.homeManagerModules.wallpaper - inputs.self.homeManagerModules.spotify - inputs.self.homeManagerModules.vesktop + inputs.self.modules.homeManager.dendritic ]; home.username = "8amps"; home.homeDirectory = "/home/8amps"; home.stateVersion = "24.11"; - + # ── Feature Toggles ───────────────────────────────────────── dendritic.apps.ghostty.enable = true; - dendritic.apps.nixvim-ide.enable = true; dendritic.python.enable = true; # ───────────────────────────────────────────────────────────── }; diff --git a/hosts/system-manager/linux-generic/default.nix b/hosts/system-manager/linux-generic/default.nix deleted file mode 100644 index fa6a29c0..00000000 --- a/hosts/system-manager/linux-generic/default.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ inputs, ... }: - -{ - imports = [ - { - config = { - nixpkgs.hostPlatform = "x86_64-linux"; - nixpkgs.config.allowUnfree = true; - - # System Manager specific setup - system-manager.allowAnyDistro = true; - }; - } - - # Pull in Feature Modules from the Hub (System Level) - inputs.self.modules.nixos.shell - inputs.self.modules.nixos.secrets - ]; -} diff --git a/metadata.json b/metadata.json index 603e7a57..d749d813 100644 --- a/metadata.json +++ b/metadata.json @@ -1 +1,1391 @@ -{"description":"Dendritic Nix Flake with flake-parts","dirtyRevision":"a017966f17ce26ec5d8887c784f826e131dfd43d-dirty","fingerprint":"14068949865f8356010eea922fe1947863ad37269707adac5d7d339c94d2065d","lastModified":1778454314,"locked":{"dirtyRev":"a017966f17ce26ec5d8887c784f826e131dfd43d-dirty","dirtyShortRev":"a017966-dirty","lastModified":1778454314,"type":"git","url":"file:///private/etc/nix-darwin/.dotfiles"},"locks":{"nodes":{"android-nixpkgs":{"inputs":{"devshell":"devshell","flake-utils":"flake-utils","nixpkgs":["wawona","nixpkgs"]},"locked":{"lastModified":1775076220,"narHash":"sha256-QlDAqxJAHakV7GYR97T4hx7trqMI08axwNkp9Db8U7Q=","owner":"tadfisher","repo":"android-nixpkgs","rev":"5585cc3ee71bdd8d9ee255523f11b920138fa688","type":"github"},"original":{"owner":"tadfisher","repo":"android-nixpkgs","rev":"5585cc3ee71bdd8d9ee255523f11b920138fa688","type":"github"}},"apple-silicon":{"inputs":{"flake-compat":"flake-compat","nixpkgs":["nixpkgs"]},"locked":{"lastModified":1778234684,"narHash":"sha256-usIHfvSt7aXvMvRGtcbsue3rA13Z+9TW/7I3WBzLqFY=","owner":"tpwrules","repo":"nixos-apple-silicon","rev":"3d7fe422ef6162154830209b9e50bf69e150cff7","type":"github"},"original":{"owner":"tpwrules","repo":"nixos-apple-silicon","type":"github"}},"base16":{"inputs":{"fromYaml":"fromYaml"},"locked":{"lastModified":1755819240,"narHash":"sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=","owner":"SenchoPens","repo":"base16.nix","rev":"75ed5e5e3fce37df22e49125181fa37899c3ccd6","type":"github"},"original":{"owner":"SenchoPens","repo":"base16.nix","type":"github"}},"base16-fish":{"flake":false,"locked":{"lastModified":1765809053,"narHash":"sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=","owner":"tomyun","repo":"base16-fish","rev":"86cbea4dca62e08fb7fd83a70e96472f92574782","type":"github"},"original":{"owner":"tomyun","repo":"base16-fish","rev":"86cbea4dca62e08fb7fd83a70e96472f92574782","type":"github"}},"base16-helix":{"flake":false,"locked":{"lastModified":1776754714,"narHash":"sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=","owner":"tinted-theming","repo":"base16-helix","rev":"4d508123037e7851ad36ebf7d9c48b0e9e1eb581","type":"github"},"original":{"owner":"tinted-theming","repo":"base16-helix","type":"github"}},"base16-vim":{"flake":false,"locked":{"lastModified":1732806396,"narHash":"sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=","owner":"tinted-theming","repo":"base16-vim","rev":"577fe8125d74ff456cf942c733a85d769afe58b7","type":"github"},"original":{"owner":"tinted-theming","repo":"base16-vim","rev":"577fe8125d74ff456cf942c733a85d769afe58b7","type":"github"}},"cachix":{"inputs":{"devenv":["wawona","crate2nix"],"flake-compat":["wawona","crate2nix"],"git-hooks":"git-hooks","nixpkgs":"nixpkgs_5"},"locked":{"lastModified":1767714506,"narHash":"sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=","owner":"cachix","repo":"cachix","rev":"894c649f0daaa38bbcfb21de64be47dfa7cd0ec9","type":"github"},"original":{"owner":"cachix","ref":"latest","repo":"cachix","type":"github"}},"cachix_2":{"inputs":{"devenv":["wawona","crate2nix","crate2nix_stable"],"flake-compat":["wawona","crate2nix","crate2nix_stable"],"git-hooks":"git-hooks_2","nixpkgs":"nixpkgs_6"},"locked":{"lastModified":1767714506,"narHash":"sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=","owner":"cachix","repo":"cachix","rev":"894c649f0daaa38bbcfb21de64be47dfa7cd0ec9","type":"github"},"original":{"owner":"cachix","ref":"latest","repo":"cachix","type":"github"}},"crate2nix":{"inputs":{"cachix":"cachix","crate2nix_stable":"crate2nix_stable","devshell":"devshell_3","flake-compat":"flake-compat_5","flake-parts":"flake-parts_6","nix-test-runner":"nix-test-runner_2","nixpkgs":"nixpkgs_8","pre-commit-hooks":"pre-commit-hooks_2"},"locked":{"lastModified":1770646848,"narHash":"sha256-0aZjR0id5glnZaKpu/nCwoLON4r5m6q6IDU06YvwT44=","owner":"nix-community","repo":"crate2nix","rev":"26b698e804dd32dc5bb1995028fef00cc87d603a","type":"github"},"original":{"owner":"nix-community","repo":"crate2nix","type":"github"}},"crate2nix_stable":{"inputs":{"cachix":"cachix_2","crate2nix_stable":["wawona","crate2nix","crate2nix_stable"],"devshell":"devshell_2","flake-compat":"flake-compat_4","flake-parts":"flake-parts_5","nix-test-runner":"nix-test-runner","nixpkgs":"nixpkgs_7","pre-commit-hooks":"pre-commit-hooks"},"locked":{"lastModified":1769627083,"narHash":"sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=","owner":"nix-community","repo":"crate2nix","rev":"7c33e664668faecf7655fa53861d7a80c9e464a2","type":"github"},"original":{"owner":"nix-community","ref":"0.15.0","repo":"crate2nix","type":"github"}},"determinate-nix":{"inputs":{"determinate-nixd-aarch64-darwin":"determinate-nixd-aarch64-darwin","determinate-nixd-aarch64-linux":"determinate-nixd-aarch64-linux","determinate-nixd-x86_64-linux":"determinate-nixd-x86_64-linux","nix":"nix","nixpkgs":"nixpkgs_2"},"locked":{"lastModified":1778179392,"narHash":"sha256-W6zorvjBYbzMNvqKIqCdpDF4rq3gj50Xximl56YM9/I=","owner":"DeterminateSystems","repo":"determinate","rev":"efd54faa68be8cd777b5c28cab11e638998a0853","type":"github"},"original":{"owner":"DeterminateSystems","repo":"determinate","type":"github"}},"determinate-nixd-aarch64-darwin":{"flake":false,"locked":{"narHash":"sha256-z4mCqKI3Qd6weuHrlfzGccJG0giym/VJhKv20ijRSs0=","type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/macOS"},"original":{"type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/macOS"}},"determinate-nixd-aarch64-linux":{"flake":false,"locked":{"narHash":"sha256-yW+VNepSRytzfanSssPMJPvwioCcmlZYaBX8++UFkAk=","type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/aarch64-linux"},"original":{"type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/aarch64-linux"}},"determinate-nixd-x86_64-linux":{"flake":false,"locked":{"narHash":"sha256-+L102C3Hhkd1GlXmRm2eLTLsZKBxEvooiQZFqQRlBf0=","type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/x86_64-linux"},"original":{"type":"file","url":"https://install.determinate.systems/determinate-nixd/tag/v3.20.0/x86_64-linux"}},"devshell":{"inputs":{"nixpkgs":["wawona","android-nixpkgs","nixpkgs"]},"locked":{"lastModified":1768818222,"narHash":"sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=","owner":"numtide","repo":"devshell","rev":"255a2b1725a20d060f566e4755dbf571bbbb5f76","type":"github"},"original":{"owner":"numtide","repo":"devshell","type":"github"}},"devshell_2":{"inputs":{"nixpkgs":["wawona","crate2nix","crate2nix_stable","nixpkgs"]},"locked":{"lastModified":1768818222,"narHash":"sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=","owner":"numtide","repo":"devshell","rev":"255a2b1725a20d060f566e4755dbf571bbbb5f76","type":"github"},"original":{"owner":"numtide","repo":"devshell","type":"github"}},"devshell_3":{"inputs":{"nixpkgs":["wawona","crate2nix","nixpkgs"]},"locked":{"lastModified":1768818222,"narHash":"sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=","owner":"numtide","repo":"devshell","rev":"255a2b1725a20d060f566e4755dbf571bbbb5f76","type":"github"},"original":{"owner":"numtide","repo":"devshell","type":"github"}},"firefox-gnome-theme":{"flake":false,"locked":{"lastModified":1776136500,"narHash":"sha256-r0gN2brVWA351zwMV0Flmlcd6SGMvYqFbvC3DfKFM8Y=","owner":"rafaelmardojai","repo":"firefox-gnome-theme","rev":"0f8ba203d475587f477e7ae12661bd8459e225b7","type":"github"},"original":{"owner":"rafaelmardojai","repo":"firefox-gnome-theme","type":"github"}},"flake-compat":{"locked":{"lastModified":1761640442,"narHash":"sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=","owner":"nix-community","repo":"flake-compat","rev":"4a56054d8ffc173222d09dad23adf4ba946c8884","type":"github"},"original":{"owner":"nix-community","repo":"flake-compat","type":"github"}},"flake-compat_2":{"flake":false,"locked":{"lastModified":1696426674,"narHash":"sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=","owner":"edolstra","repo":"flake-compat","rev":"0f9255e01c2351cc7d116c072cb317785dd33b33","type":"github"},"original":{"owner":"edolstra","repo":"flake-compat","type":"github"}},"flake-compat_3":{"flake":false,"locked":{"lastModified":1767039857,"narHash":"sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=","owner":"edolstra","repo":"flake-compat","rev":"5edf11c44bc78a0d334f6334cdaf7d60d732daab","type":"github"},"original":{"owner":"edolstra","repo":"flake-compat","type":"github"}},"flake-compat_4":{"locked":{"lastModified":1733328505,"narHash":"sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=","rev":"ff81ac966bb2cae68946d5ed5fc4994f96d0ffec","revCount":69,"type":"tarball","url":"https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"}},"flake-compat_5":{"locked":{"lastModified":1733328505,"narHash":"sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=","rev":"ff81ac966bb2cae68946d5ed5fc4994f96d0ffec","revCount":69,"type":"tarball","url":"https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"}},"flake-parts":{"inputs":{"nixpkgs-lib":["determinate-nix","nix","nixpkgs"]},"locked":{"lastModified":1748821116,"narHash":"sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=","rev":"49f0870db23e8c1ca0b5259734a02cd9e1e371a1","revCount":377,"type":"tarball","url":"https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev-49f0870db23e8c1ca0b5259734a02cd9e1e371a1/01972f28-554a-73f8-91f4-d488cc502f08/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/hercules-ci/flake-parts/0.1"}},"flake-parts_2":{"inputs":{"nixpkgs-lib":"nixpkgs-lib"},"locked":{"lastModified":1777988971,"narHash":"sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=","owner":"hercules-ci","repo":"flake-parts","rev":"0678d8986be1661af6bb555f3489f2fdfc31f6ff","type":"github"},"original":{"owner":"hercules-ci","repo":"flake-parts","type":"github"}},"flake-parts_3":{"inputs":{"nixpkgs-lib":["stylix","nixpkgs"]},"locked":{"lastModified":1775087534,"narHash":"sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=","owner":"hercules-ci","repo":"flake-parts","rev":"3107b77cd68437b9a76194f0f7f9c55f2329ca5b","type":"github"},"original":{"owner":"hercules-ci","repo":"flake-parts","type":"github"}},"flake-parts_4":{"inputs":{"nixpkgs-lib":["system-manager","userborn","nixpkgs"]},"locked":{"lastModified":1768135262,"narHash":"sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=","owner":"hercules-ci","repo":"flake-parts","rev":"80daad04eddbbf5a4d883996a73f3f542fa437ac","type":"github"},"original":{"owner":"hercules-ci","repo":"flake-parts","type":"github"}},"flake-parts_5":{"inputs":{"nixpkgs-lib":["wawona","crate2nix","crate2nix_stable","nixpkgs"]},"locked":{"lastModified":1768135262,"narHash":"sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=","owner":"hercules-ci","repo":"flake-parts","rev":"80daad04eddbbf5a4d883996a73f3f542fa437ac","type":"github"},"original":{"owner":"hercules-ci","repo":"flake-parts","type":"github"}},"flake-parts_6":{"inputs":{"nixpkgs-lib":["wawona","crate2nix","nixpkgs"]},"locked":{"lastModified":1768135262,"narHash":"sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=","owner":"hercules-ci","repo":"flake-parts","rev":"80daad04eddbbf5a4d883996a73f3f542fa437ac","type":"github"},"original":{"owner":"hercules-ci","repo":"flake-parts","type":"github"}},"flake-utils":{"inputs":{"systems":"systems_4"},"locked":{"lastModified":1731533236,"narHash":"sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=","owner":"numtide","repo":"flake-utils","rev":"11707dc2f618dd54ca8739b309ec4fc024de578b","type":"github"},"original":{"owner":"numtide","repo":"flake-utils","type":"github"}},"fromYaml":{"flake":false,"locked":{"lastModified":1731966426,"narHash":"sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=","owner":"SenchoPens","repo":"fromYaml","rev":"106af9e2f715e2d828df706c386a685698f3223b","type":"github"},"original":{"owner":"SenchoPens","repo":"fromYaml","type":"github"}},"git-hooks":{"inputs":{"flake-compat":["wawona","crate2nix","cachix","flake-compat"],"gitignore":"gitignore_2","nixpkgs":["wawona","crate2nix","cachix","nixpkgs"]},"locked":{"lastModified":1765404074,"narHash":"sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=","owner":"cachix","repo":"git-hooks.nix","rev":"2d6f58930fbcd82f6f9fd59fb6d13e37684ca529","type":"github"},"original":{"owner":"cachix","repo":"git-hooks.nix","type":"github"}},"git-hooks-nix":{"inputs":{"flake-compat":"flake-compat_2","gitignore":["determinate-nix","nix"],"nixpkgs":["determinate-nix","nix","nixpkgs"]},"locked":{"lastModified":1747372754,"narHash":"sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=","rev":"80479b6ec16fefd9c1db3ea13aeb038c60530f46","revCount":1026,"type":"tarball","url":"https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1026%2Brev-80479b6ec16fefd9c1db3ea13aeb038c60530f46/0196d79a-1b35-7b8e-a021-c894fb62163d/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/cachix/git-hooks.nix/0.1.941"}},"git-hooks_2":{"inputs":{"flake-compat":["wawona","crate2nix","crate2nix_stable","cachix","flake-compat"],"gitignore":"gitignore_3","nixpkgs":["wawona","crate2nix","crate2nix_stable","cachix","nixpkgs"]},"locked":{"lastModified":1765404074,"narHash":"sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=","owner":"cachix","repo":"git-hooks.nix","rev":"2d6f58930fbcd82f6f9fd59fb6d13e37684ca529","type":"github"},"original":{"owner":"cachix","repo":"git-hooks.nix","type":"github"}},"gitignore":{"inputs":{"nixpkgs":["system-manager","userborn","pre-commit-hooks-nix","nixpkgs"]},"locked":{"lastModified":1709087332,"narHash":"sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=","owner":"hercules-ci","repo":"gitignore.nix","rev":"637db329424fd7e46cf4185293b9cc8c88c95394","type":"github"},"original":{"owner":"hercules-ci","repo":"gitignore.nix","type":"github"}},"gitignore_2":{"inputs":{"nixpkgs":["wawona","crate2nix","cachix","git-hooks","nixpkgs"]},"locked":{"lastModified":1709087332,"narHash":"sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=","owner":"hercules-ci","repo":"gitignore.nix","rev":"637db329424fd7e46cf4185293b9cc8c88c95394","type":"github"},"original":{"owner":"hercules-ci","repo":"gitignore.nix","type":"github"}},"gitignore_3":{"inputs":{"nixpkgs":["wawona","crate2nix","crate2nix_stable","cachix","git-hooks","nixpkgs"]},"locked":{"lastModified":1709087332,"narHash":"sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=","owner":"hercules-ci","repo":"gitignore.nix","rev":"637db329424fd7e46cf4185293b9cc8c88c95394","type":"github"},"original":{"owner":"hercules-ci","repo":"gitignore.nix","type":"github"}},"gitignore_4":{"inputs":{"nixpkgs":["wawona","crate2nix","crate2nix_stable","pre-commit-hooks","nixpkgs"]},"locked":{"lastModified":1709087332,"narHash":"sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=","owner":"hercules-ci","repo":"gitignore.nix","rev":"637db329424fd7e46cf4185293b9cc8c88c95394","type":"github"},"original":{"owner":"hercules-ci","repo":"gitignore.nix","type":"github"}},"gitignore_5":{"inputs":{"nixpkgs":["wawona","crate2nix","pre-commit-hooks","nixpkgs"]},"locked":{"lastModified":1709087332,"narHash":"sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=","owner":"hercules-ci","repo":"gitignore.nix","rev":"637db329424fd7e46cf4185293b9cc8c88c95394","type":"github"},"original":{"owner":"hercules-ci","repo":"gitignore.nix","type":"github"}},"gnome-shell":{"flake":false,"locked":{"lastModified":1767737596,"narHash":"sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=","owner":"GNOME","repo":"gnome-shell","rev":"ef02db02bf0ff342734d525b5767814770d85b49","type":"github"},"original":{"owner":"GNOME","repo":"gnome-shell","rev":"ef02db02bf0ff342734d525b5767814770d85b49","type":"github"}},"home-manager":{"inputs":{"nixpkgs":["nixpkgs"]},"locked":{"lastModified":1778401693,"narHash":"sha256-OVHdCqXXUF5UdGkH+FF2ZL06OLZjj2kvP2dIUmzVWoo=","owner":"nix-community","repo":"home-manager","rev":"389b83002efc26f1145e89a6a8e6edc5a6435948","type":"github"},"original":{"owner":"nix-community","ref":"release-25.11","repo":"home-manager","type":"github"}},"nix":{"inputs":{"flake-parts":"flake-parts","git-hooks-nix":"git-hooks-nix","nixpkgs":"nixpkgs","nixpkgs-23-11":"nixpkgs-23-11","nixpkgs-regression":"nixpkgs-regression"},"locked":{"lastModified":1778177425,"narHash":"sha256-oyHvP5HDRe59opmjTrq2ED9lh+R9FrHyaCGPPNfBqWM=","rev":"f0ccb960d3ad5bff28acd9cabf8bdef885b5d52f","revCount":25858,"type":"tarball","url":"https://api.flakehub.com/f/pinned/DeterminateSystems/nix-src/3.20.0/019e03bc-3f83-7833-aba3-b691ef4956c7/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/DeterminateSystems/nix-src/%2A"}},"nix-darwin":{"inputs":{"nixpkgs":["nixpkgs-darwin"]},"locked":{"lastModified":1772129556,"narHash":"sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=","owner":"LnL7","repo":"nix-darwin","rev":"ebec37af18215214173c98cf6356d0aca24a2585","type":"github"},"original":{"owner":"LnL7","ref":"nix-darwin-25.11","repo":"nix-darwin","type":"github"}},"nix-test-runner":{"flake":false,"locked":{"lastModified":1588761593,"narHash":"sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=","owner":"stoeffel","repo":"nix-test-runner","rev":"c45d45b11ecef3eb9d834c3b6304c05c49b06ca2","type":"github"},"original":{"owner":"stoeffel","repo":"nix-test-runner","type":"github"}},"nix-test-runner_2":{"flake":false,"locked":{"lastModified":1588761593,"narHash":"sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=","owner":"stoeffel","repo":"nix-test-runner","rev":"c45d45b11ecef3eb9d834c3b6304c05c49b06ca2","type":"github"},"original":{"owner":"stoeffel","repo":"nix-test-runner","type":"github"}},"nix-xcodeenvtests":{"flake":false,"locked":{"lastModified":1570998936,"narHash":"sha256-xh1jtdqWXSYJqWB30IkB+FbwCrF9qh+bYD/kOzgOpUc=","owner":"svanderburg","repo":"nix-xcodeenvtests","rev":"ef4ef24802fa3822100ed3e1628307b20017711e","type":"github"},"original":{"owner":"svanderburg","repo":"nix-xcodeenvtests","type":"github"}},"nixpkgs":{"locked":{"lastModified":1773222311,"narHash":"sha256-BHoB/XpbqoZkVYZCfXJXfkR+GXFqwb/4zbWnOr2cRcU=","rev":"0590cd39f728e129122770c029970378a79d076a","revCount":909248,"type":"tarball","url":"https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.909248%2Brev-0590cd39f728e129122770c029970378a79d076a/019ce32b-8ace-7339-b129-cceaa8dd10c6/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/NixOS/nixpkgs/0.2511"}},"nixpkgs-23-11":{"locked":{"lastModified":1717159533,"narHash":"sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=","owner":"NixOS","repo":"nixpkgs","rev":"a62e6edd6d5e1fa0329b8653c801147986f8d446","type":"github"},"original":{"owner":"NixOS","repo":"nixpkgs","rev":"a62e6edd6d5e1fa0329b8653c801147986f8d446","type":"github"}},"nixpkgs-darwin":{"locked":{"lastModified":1778003029,"narHash":"sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=","owner":"NixOS","repo":"nixpkgs","rev":"0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5","type":"github"},"original":{"owner":"NixOS","ref":"nixos-25.11","repo":"nixpkgs","type":"github"}},"nixpkgs-lib":{"locked":{"lastModified":1777168982,"narHash":"sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=","owner":"nix-community","repo":"nixpkgs.lib","rev":"f5901329dade4a6ea039af1433fb087bd9c1fe14","type":"github"},"original":{"owner":"nix-community","repo":"nixpkgs.lib","type":"github"}},"nixpkgs-regression":{"locked":{"lastModified":1643052045,"narHash":"sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=","owner":"NixOS","repo":"nixpkgs","rev":"215d4d0fd80ca5163643b03a33fde804a29cc1e2","type":"github"},"original":{"owner":"NixOS","repo":"nixpkgs","rev":"215d4d0fd80ca5163643b03a33fde804a29cc1e2","type":"github"}},"nixpkgs_2":{"locked":{"lastModified":1777826146,"narHash":"sha256-wQ/iN5Zp5VIa3ebBibijPnLyKhor+xEbDy4d0goa9Zs=","rev":"73c703c22422b8951895a960959dbbaca7296492","revCount":991389,"type":"tarball","url":"https://api.flakehub.com/f/pinned/DeterminateSystems/nixpkgs-weekly/0.1.991389%2Brev-73c703c22422b8951895a960959dbbaca7296492/019df6c8-934b-7d40-b402-027bb5def30f/source.tar.gz"},"original":{"type":"tarball","url":"https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.1"}},"nixpkgs_3":{"locked":{"lastModified":1778003029,"narHash":"sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=","owner":"NixOS","repo":"nixpkgs","rev":"0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5","type":"github"},"original":{"owner":"NixOS","ref":"nixos-25.11","repo":"nixpkgs","type":"github"}},"nixpkgs_4":{"locked":{"lastModified":1777268161,"narHash":"sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=","owner":"NixOS","repo":"nixpkgs","rev":"1c3fe55ad329cbcb28471bb30f05c9827f724c76","type":"github"},"original":{"owner":"NixOS","ref":"nixos-unstable","repo":"nixpkgs","type":"github"}},"nixpkgs_5":{"locked":{"lastModified":1765186076,"narHash":"sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=","owner":"NixOS","repo":"nixpkgs","rev":"addf7cf5f383a3101ecfba091b98d0a1263dc9b8","type":"github"},"original":{"owner":"NixOS","ref":"nixos-unstable","repo":"nixpkgs","type":"github"}},"nixpkgs_6":{"locked":{"lastModified":1765186076,"narHash":"sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=","owner":"NixOS","repo":"nixpkgs","rev":"addf7cf5f383a3101ecfba091b98d0a1263dc9b8","type":"github"},"original":{"owner":"NixOS","ref":"nixos-unstable","repo":"nixpkgs","type":"github"}},"nixpkgs_7":{"locked":{"lastModified":1769433173,"narHash":"sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=","owner":"NixOS","repo":"nixpkgs","rev":"13b0f9e6ac78abbbb736c635d87845c4f4bee51b","type":"github"},"original":{"owner":"NixOS","ref":"nixpkgs-unstable","repo":"nixpkgs","type":"github"}},"nixpkgs_8":{"locked":{"lastModified":1769433173,"narHash":"sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=","owner":"NixOS","repo":"nixpkgs","rev":"13b0f9e6ac78abbbb736c635d87845c4f4bee51b","type":"github"},"original":{"owner":"NixOS","ref":"nixpkgs-unstable","repo":"nixpkgs","type":"github"}},"nur":{"inputs":{"flake-parts":["stylix","flake-parts"],"nixpkgs":["stylix","nixpkgs"]},"locked":{"lastModified":1777598946,"narHash":"sha256-X239dAGaU1+gfDj8jKH8GzlqKMcxaVfXOio+uzBOkeE=","owner":"nix-community","repo":"NUR","rev":"5d55af01c0f86be583931fe99207fc56c14134b3","type":"github"},"original":{"owner":"nix-community","repo":"NUR","type":"github"}},"pre-commit-hooks":{"inputs":{"flake-compat":["wawona","crate2nix","crate2nix_stable","flake-compat"],"gitignore":"gitignore_4","nixpkgs":["wawona","crate2nix","crate2nix_stable","nixpkgs"]},"locked":{"lastModified":1769069492,"narHash":"sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=","owner":"cachix","repo":"pre-commit-hooks.nix","rev":"a1ef738813b15cf8ec759bdff5761b027e3e1d23","type":"github"},"original":{"owner":"cachix","repo":"pre-commit-hooks.nix","type":"github"}},"pre-commit-hooks-nix":{"inputs":{"flake-compat":["system-manager","userborn","flake-compat"],"gitignore":"gitignore","nixpkgs":["system-manager","userborn","nixpkgs"]},"locked":{"lastModified":1769069492,"narHash":"sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=","owner":"cachix","repo":"pre-commit-hooks.nix","rev":"a1ef738813b15cf8ec759bdff5761b027e3e1d23","type":"github"},"original":{"owner":"cachix","repo":"pre-commit-hooks.nix","type":"github"}},"pre-commit-hooks_2":{"inputs":{"flake-compat":["wawona","crate2nix","flake-compat"],"gitignore":"gitignore_5","nixpkgs":["wawona","crate2nix","nixpkgs"]},"locked":{"lastModified":1769069492,"narHash":"sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=","owner":"cachix","repo":"pre-commit-hooks.nix","rev":"a1ef738813b15cf8ec759bdff5761b027e3e1d23","type":"github"},"original":{"owner":"cachix","repo":"pre-commit-hooks.nix","type":"github"}},"root":{"inputs":{"apple-silicon":"apple-silicon","determinate-nix":"determinate-nix","flake-parts":"flake-parts_2","home-manager":"home-manager","nix-darwin":"nix-darwin","nixpkgs":"nixpkgs_3","nixpkgs-darwin":"nixpkgs-darwin","sops-nix":"sops-nix","spicetify-nix":"spicetify-nix","stylix":"stylix","system-manager":"system-manager","wawona":"wawona"}},"rust-overlay":{"inputs":{"nixpkgs":["wawona","nixpkgs"]},"locked":{"lastModified":1776309239,"narHash":"sha256-XzTecca59093jBsVAE4PVAMcJO+PAYHYHBPRnOR8iWs=","owner":"oxalica","repo":"rust-overlay","rev":"3717ee024da7b0a20744f12c39b41e27cbc12f2d","type":"github"},"original":{"owner":"oxalica","repo":"rust-overlay","type":"github"}},"sops-nix":{"inputs":{"nixpkgs":["nixpkgs"]},"locked":{"lastModified":1777944972,"narHash":"sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=","owner":"Mic92","repo":"sops-nix","rev":"c591bf665727040c6cc5cb409079acb22dcce33c","type":"github"},"original":{"owner":"Mic92","repo":"sops-nix","type":"github"}},"spicetify-nix":{"inputs":{"nixpkgs":["nixpkgs"],"systems":"systems"},"locked":{"lastModified":1778395012,"narHash":"sha256-A/VRiNFQIwGp8cOC/8yNCRexFHjtFCzBwhajrkyGojo=","owner":"Gerg-L","repo":"spicetify-nix","rev":"3b4991bfc064c3361957f23141351ae2d9833234","type":"github"},"original":{"owner":"Gerg-L","repo":"spicetify-nix","type":"github"}},"stylix":{"inputs":{"base16":"base16","base16-fish":"base16-fish","base16-helix":"base16-helix","base16-vim":"base16-vim","firefox-gnome-theme":"firefox-gnome-theme","flake-parts":"flake-parts_3","gnome-shell":"gnome-shell","nixpkgs":"nixpkgs_4","nur":"nur","systems":"systems_2","tinted-kitty":"tinted-kitty","tinted-schemes":"tinted-schemes","tinted-tmux":"tinted-tmux","tinted-zed":"tinted-zed"},"locked":{"lastModified":1778104276,"narHash":"sha256-/DSSnU0LLmOTG/OCgGwYpxP6+5YvxRx2g/GhI4x6aCU=","owner":"danth","repo":"stylix","rev":"18ed8d270231e067fe2739998479ed5d7c659c2c","type":"github"},"original":{"owner":"danth","repo":"stylix","type":"github"}},"system-manager":{"inputs":{"flake-compat":"flake-compat_3","nixpkgs":["nixpkgs"],"userborn":"userborn"},"locked":{"lastModified":1777874990,"narHash":"sha256-mQptVpwNFEgWRTZx6LhhxW4r1na+rwheWfgIIhcLOrE=","owner":"numtide","repo":"system-manager","rev":"3f1bffc59e51fc9816a1cf523e0093f11bc9bbf5","type":"github"},"original":{"owner":"numtide","repo":"system-manager","type":"github"}},"systems":{"locked":{"lastModified":1681028828,"narHash":"sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=","owner":"nix-systems","repo":"default","rev":"da67096a3b9bf56a91d16901293e51ba5b49a27e","type":"github"},"original":{"owner":"nix-systems","repo":"default","type":"github"}},"systems_2":{"locked":{"lastModified":1681028828,"narHash":"sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=","owner":"nix-systems","repo":"default","rev":"da67096a3b9bf56a91d16901293e51ba5b49a27e","type":"github"},"original":{"owner":"nix-systems","repo":"default","type":"github"}},"systems_3":{"locked":{"lastModified":1681028828,"narHash":"sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=","owner":"nix-systems","repo":"default","rev":"da67096a3b9bf56a91d16901293e51ba5b49a27e","type":"github"},"original":{"owner":"nix-systems","repo":"default","type":"github"}},"systems_4":{"locked":{"lastModified":1681028828,"narHash":"sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=","owner":"nix-systems","repo":"default","rev":"da67096a3b9bf56a91d16901293e51ba5b49a27e","type":"github"},"original":{"owner":"nix-systems","repo":"default","type":"github"}},"tinted-kitty":{"flake":false,"locked":{"lastModified":1735730497,"narHash":"sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=","owner":"tinted-theming","repo":"tinted-kitty","rev":"de6f888497f2c6b2279361bfc790f164bfd0f3fa","type":"github"},"original":{"owner":"tinted-theming","repo":"tinted-kitty","type":"github"}},"tinted-schemes":{"flake":false,"locked":{"lastModified":1777041405,"narHash":"sha256-BAGZ7ObFV/9Z61OJZun7ifPyhkuHqNuW1QIhQ8LuzCo=","owner":"tinted-theming","repo":"schemes","rev":"5f868b3a338b6904c47f3833b9c411be641983a8","type":"github"},"original":{"owner":"tinted-theming","repo":"schemes","type":"github"}},"tinted-tmux":{"flake":false,"locked":{"lastModified":1777169200,"narHash":"sha256-h7dDbIzP5hDr9v97w9PL6jdAgXawmj6krcH+959rqpU=","owner":"tinted-theming","repo":"tinted-tmux","rev":"f798c2dce44ef815bb6b8f05a82135c7942d35ac","type":"github"},"original":{"owner":"tinted-theming","repo":"tinted-tmux","type":"github"}},"tinted-zed":{"flake":false,"locked":{"lastModified":1777463218,"narHash":"sha256-Bhkozqtq3BKLqWTlmKm8uAptfX4aRGI8QX3eEL54Vpc=","owner":"tinted-theming","repo":"base16-zed","rev":"5768d08ed2e7944a26a958868cdb073cb8856dae","type":"github"},"original":{"owner":"tinted-theming","repo":"base16-zed","type":"github"}},"userborn":{"inputs":{"flake-compat":["system-manager","flake-compat"],"flake-parts":"flake-parts_4","nixpkgs":["system-manager","nixpkgs"],"pre-commit-hooks-nix":"pre-commit-hooks-nix","systems":"systems_3"},"locked":{"lastModified":1770377964,"narHash":"sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=","owner":"jfroche","repo":"userborn","rev":"55c2cd7952c207a62736a5bbd9499ea73da18d24","type":"github"},"original":{"owner":"jfroche","ref":"system-manager","repo":"userborn","type":"github"}},"wawona":{"inputs":{"android-nixpkgs":"android-nixpkgs","crate2nix":"crate2nix","nix-xcodeenvtests":"nix-xcodeenvtests","nixpkgs":["nixpkgs"],"rust-overlay":"rust-overlay"},"locked":{"lastModified":1777675726,"narHash":"sha256-LVkJMUsPbauqZMLBnwV9iDqr8Tsh4Pgd2PBBxiM1BzI=","owner":"Wawona","repo":"Wawona","rev":"05e1cc1330c67aadeacb2ae242f3efeb581d567c","type":"github"},"original":{"owner":"Wawona","ref":"development","repo":"Wawona","type":"github"}}},"root":"root","version":7},"original":{"type":"git","url":"file:///private/etc/nix-darwin/.dotfiles"},"originalUrl":"git+file:///private/etc/nix-darwin/.dotfiles","resolved":{"type":"git","url":"file:///private/etc/nix-darwin/.dotfiles"},"resolvedUrl":"git+file:///private/etc/nix-darwin/.dotfiles","url":"git+file:///private/etc/nix-darwin/.dotfiles"} +{ + "description": "Dendritic Nix Flake with flake-parts", + "dirtyRevision": "a017966f17ce26ec5d8887c784f826e131dfd43d-dirty", + "fingerprint": "14068949865f8356010eea922fe1947863ad37269707adac5d7d339c94d2065d", + "lastModified": 1778454314, + "locked": { + "dirtyRev": "a017966f17ce26ec5d8887c784f826e131dfd43d-dirty", + "dirtyShortRev": "a017966-dirty", + "lastModified": 1778454314, + "type": "git", + "url": "file:///private/etc/nix-darwin/.dotfiles" + }, + "locks": { + "nodes": { + "android-nixpkgs": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils", + "nixpkgs": ["wawona", "nixpkgs"] + }, + "locked": { + "lastModified": 1775076220, + "narHash": "sha256-QlDAqxJAHakV7GYR97T4hx7trqMI08axwNkp9Db8U7Q=", + "owner": "tadfisher", + "repo": "android-nixpkgs", + "rev": "5585cc3ee71bdd8d9ee255523f11b920138fa688", + "type": "github" + }, + "original": { + "owner": "tadfisher", + "repo": "android-nixpkgs", + "rev": "5585cc3ee71bdd8d9ee255523f11b920138fa688", + "type": "github" + } + }, + "apple-silicon": { + "inputs": { "flake-compat": "flake-compat", "nixpkgs": ["nixpkgs"] }, + "locked": { + "lastModified": 1778234684, + "narHash": "sha256-usIHfvSt7aXvMvRGtcbsue3rA13Z+9TW/7I3WBzLqFY=", + "owner": "tpwrules", + "repo": "nixos-apple-silicon", + "rev": "3d7fe422ef6162154830209b9e50bf69e150cff7", + "type": "github" + }, + "original": { + "owner": "tpwrules", + "repo": "nixos-apple-silicon", + "type": "github" + } + }, + "base16": { + "inputs": { "fromYaml": "fromYaml" }, + "locked": { + "lastModified": 1755819240, + "narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=", + "owner": "SenchoPens", + "repo": "base16.nix", + "rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "base16.nix", + "type": "github" + } + }, + "base16-fish": { + "flake": false, + "locked": { + "lastModified": 1765809053, + "narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=", + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + }, + "original": { + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + } + }, + "base16-helix": { + "flake": false, + "locked": { + "lastModified": 1776754714, + "narHash": "sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=", + "owner": "tinted-theming", + "repo": "base16-helix", + "rev": "4d508123037e7851ad36ebf7d9c48b0e9e1eb581", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-helix", + "type": "github" + } + }, + "base16-vim": { + "flake": false, + "locked": { + "lastModified": 1732806396, + "narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=", + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + } + }, + "cachix": { + "inputs": { + "devenv": ["wawona", "crate2nix"], + "flake-compat": ["wawona", "crate2nix"], + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs_5" + }, + "locked": { + "lastModified": 1767714506, + "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=", + "owner": "cachix", + "repo": "cachix", + "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "cachix_2": { + "inputs": { + "devenv": ["wawona", "crate2nix", "crate2nix_stable"], + "flake-compat": ["wawona", "crate2nix", "crate2nix_stable"], + "git-hooks": "git-hooks_2", + "nixpkgs": "nixpkgs_6" + }, + "locked": { + "lastModified": 1767714506, + "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=", + "owner": "cachix", + "repo": "cachix", + "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "crate2nix": { + "inputs": { + "cachix": "cachix", + "crate2nix_stable": "crate2nix_stable", + "devshell": "devshell_3", + "flake-compat": "flake-compat_5", + "flake-parts": "flake-parts_6", + "nix-test-runner": "nix-test-runner_2", + "nixpkgs": "nixpkgs_8", + "pre-commit-hooks": "pre-commit-hooks_2" + }, + "locked": { + "lastModified": 1770646848, + "narHash": "sha256-0aZjR0id5glnZaKpu/nCwoLON4r5m6q6IDU06YvwT44=", + "owner": "nix-community", + "repo": "crate2nix", + "rev": "26b698e804dd32dc5bb1995028fef00cc87d603a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "crate2nix", + "type": "github" + } + }, + "crate2nix_stable": { + "inputs": { + "cachix": "cachix_2", + "crate2nix_stable": ["wawona", "crate2nix", "crate2nix_stable"], + "devshell": "devshell_2", + "flake-compat": "flake-compat_4", + "flake-parts": "flake-parts_5", + "nix-test-runner": "nix-test-runner", + "nixpkgs": "nixpkgs_7", + "pre-commit-hooks": "pre-commit-hooks" + }, + "locked": { + "lastModified": 1769627083, + "narHash": "sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=", + "owner": "nix-community", + "repo": "crate2nix", + "rev": "7c33e664668faecf7655fa53861d7a80c9e464a2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "0.15.0", + "repo": "crate2nix", + "type": "github" + } + }, + "determinate-nix": { + "inputs": { + "determinate-nixd-aarch64-darwin": "determinate-nixd-aarch64-darwin", + "determinate-nixd-aarch64-linux": "determinate-nixd-aarch64-linux", + "determinate-nixd-x86_64-linux": "determinate-nixd-x86_64-linux", + "nix": "nix", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1778179392, + "narHash": "sha256-W6zorvjBYbzMNvqKIqCdpDF4rq3gj50Xximl56YM9/I=", + "owner": "DeterminateSystems", + "repo": "determinate", + "rev": "efd54faa68be8cd777b5c28cab11e638998a0853", + "type": "github" + }, + "original": { + "owner": "DeterminateSystems", + "repo": "determinate", + "type": "github" + } + }, + "determinate-nixd-aarch64-darwin": { + "flake": false, + "locked": { + "narHash": "sha256-z4mCqKI3Qd6weuHrlfzGccJG0giym/VJhKv20ijRSs0=", + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/macOS" + }, + "original": { + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/macOS" + } + }, + "determinate-nixd-aarch64-linux": { + "flake": false, + "locked": { + "narHash": "sha256-yW+VNepSRytzfanSssPMJPvwioCcmlZYaBX8++UFkAk=", + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/aarch64-linux" + }, + "original": { + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/aarch64-linux" + } + }, + "determinate-nixd-x86_64-linux": { + "flake": false, + "locked": { + "narHash": "sha256-+L102C3Hhkd1GlXmRm2eLTLsZKBxEvooiQZFqQRlBf0=", + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/x86_64-linux" + }, + "original": { + "type": "file", + "url": "https://install.determinate.systems/determinate-nixd/tag/v3.20.0/x86_64-linux" + } + }, + "devshell": { + "inputs": { "nixpkgs": ["wawona", "android-nixpkgs", "nixpkgs"] }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { "owner": "numtide", "repo": "devshell", "type": "github" } + }, + "devshell_2": { + "inputs": { + "nixpkgs": ["wawona", "crate2nix", "crate2nix_stable", "nixpkgs"] + }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { "owner": "numtide", "repo": "devshell", "type": "github" } + }, + "devshell_3": { + "inputs": { "nixpkgs": ["wawona", "crate2nix", "nixpkgs"] }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { "owner": "numtide", "repo": "devshell", "type": "github" } + }, + "firefox-gnome-theme": { + "flake": false, + "locked": { + "lastModified": 1776136500, + "narHash": "sha256-r0gN2brVWA351zwMV0Flmlcd6SGMvYqFbvC3DfKFM8Y=", + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "rev": "0f8ba203d475587f477e7ae12661bd8459e225b7", + "type": "github" + }, + "original": { + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "type": "github" + } + }, + "flake-compat": { + "locked": { + "lastModified": 1761640442, + "narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=", + "owner": "nix-community", + "repo": "flake-compat", + "rev": "4a56054d8ffc173222d09dad23adf4ba946c8884", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_4": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "flake-compat_5": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "flake-parts": { + "inputs": { "nixpkgs-lib": ["determinate-nix", "nix", "nixpkgs"] }, + "locked": { + "lastModified": 1748821116, + "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=", + "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1", + "revCount": 377, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev-49f0870db23e8c1ca0b5259734a02cd9e1e371a1/01972f28-554a-73f8-91f4-d488cc502f08/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1" + } + }, + "flake-parts_2": { + "inputs": { "nixpkgs-lib": "nixpkgs-lib" }, + "locked": { + "lastModified": 1777988971, + "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { + "inputs": { "nixpkgs-lib": ["stylix", "nixpkgs"] }, + "locked": { + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_4": { + "inputs": { "nixpkgs-lib": ["system-manager", "userborn", "nixpkgs"] }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_5": { + "inputs": { + "nixpkgs-lib": ["wawona", "crate2nix", "crate2nix_stable", "nixpkgs"] + }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_6": { + "inputs": { "nixpkgs-lib": ["wawona", "crate2nix", "nixpkgs"] }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { "systems": "systems_4" }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "fromYaml": { + "flake": false, + "locked": { + "lastModified": 1731966426, + "narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=", + "owner": "SenchoPens", + "repo": "fromYaml", + "rev": "106af9e2f715e2d828df706c386a685698f3223b", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "fromYaml", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": ["wawona", "crate2nix", "cachix", "flake-compat"], + "gitignore": "gitignore_2", + "nixpkgs": ["wawona", "crate2nix", "cachix", "nixpkgs"] + }, + "locked": { + "lastModified": 1765404074, + "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "git-hooks-nix": { + "inputs": { + "flake-compat": "flake-compat_2", + "gitignore": ["determinate-nix", "nix"], + "nixpkgs": ["determinate-nix", "nix", "nixpkgs"] + }, + "locked": { + "lastModified": 1747372754, + "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", + "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", + "revCount": 1026, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1026%2Brev-80479b6ec16fefd9c1db3ea13aeb038c60530f46/0196d79a-1b35-7b8e-a021-c894fb62163d/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/cachix/git-hooks.nix/0.1.941" + } + }, + "git-hooks_2": { + "inputs": { + "flake-compat": [ + "wawona", + "crate2nix", + "crate2nix_stable", + "cachix", + "flake-compat" + ], + "gitignore": "gitignore_3", + "nixpkgs": [ + "wawona", + "crate2nix", + "crate2nix_stable", + "cachix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765404074, + "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "system-manager", + "userborn", + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_2": { + "inputs": { + "nixpkgs": ["wawona", "crate2nix", "cachix", "git-hooks", "nixpkgs"] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_3": { + "inputs": { + "nixpkgs": [ + "wawona", + "crate2nix", + "crate2nix_stable", + "cachix", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_4": { + "inputs": { + "nixpkgs": [ + "wawona", + "crate2nix", + "crate2nix_stable", + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_5": { + "inputs": { + "nixpkgs": ["wawona", "crate2nix", "pre-commit-hooks", "nixpkgs"] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gnome-shell": { + "flake": false, + "locked": { + "lastModified": 1767737596, + "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", + "owner": "GNOME", + "repo": "gnome-shell", + "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "type": "github" + }, + "original": { + "owner": "GNOME", + "repo": "gnome-shell", + "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "type": "github" + } + }, + "home-manager": { + "inputs": { "nixpkgs": ["nixpkgs"] }, + "locked": { + "lastModified": 1778401693, + "narHash": "sha256-OVHdCqXXUF5UdGkH+FF2ZL06OLZjj2kvP2dIUmzVWoo=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "389b83002efc26f1145e89a6a8e6edc5a6435948", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "home-manager", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-parts": "flake-parts", + "git-hooks-nix": "git-hooks-nix", + "nixpkgs": "nixpkgs", + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1778177425, + "narHash": "sha256-oyHvP5HDRe59opmjTrq2ED9lh+R9FrHyaCGPPNfBqWM=", + "rev": "f0ccb960d3ad5bff28acd9cabf8bdef885b5d52f", + "revCount": 25858, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/nix-src/3.20.0/019e03bc-3f83-7833-aba3-b691ef4956c7/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/nix-src/%2A" + } + }, + "nix-darwin": { + "inputs": { "nixpkgs": ["nixpkgs-darwin"] }, + "locked": { + "lastModified": 1772129556, + "narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=", + "owner": "LnL7", + "repo": "nix-darwin", + "rev": "ebec37af18215214173c98cf6356d0aca24a2585", + "type": "github" + }, + "original": { + "owner": "LnL7", + "ref": "nix-darwin-25.11", + "repo": "nix-darwin", + "type": "github" + } + }, + "nix-test-runner": { + "flake": false, + "locked": { + "lastModified": 1588761593, + "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=", + "owner": "stoeffel", + "repo": "nix-test-runner", + "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2", + "type": "github" + }, + "original": { + "owner": "stoeffel", + "repo": "nix-test-runner", + "type": "github" + } + }, + "nix-test-runner_2": { + "flake": false, + "locked": { + "lastModified": 1588761593, + "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=", + "owner": "stoeffel", + "repo": "nix-test-runner", + "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2", + "type": "github" + }, + "original": { + "owner": "stoeffel", + "repo": "nix-test-runner", + "type": "github" + } + }, + "nix-xcodeenvtests": { + "flake": false, + "locked": { + "lastModified": 1570998936, + "narHash": "sha256-xh1jtdqWXSYJqWB30IkB+FbwCrF9qh+bYD/kOzgOpUc=", + "owner": "svanderburg", + "repo": "nix-xcodeenvtests", + "rev": "ef4ef24802fa3822100ed3e1628307b20017711e", + "type": "github" + }, + "original": { + "owner": "svanderburg", + "repo": "nix-xcodeenvtests", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773222311, + "narHash": "sha256-BHoB/XpbqoZkVYZCfXJXfkR+GXFqwb/4zbWnOr2cRcU=", + "rev": "0590cd39f728e129122770c029970378a79d076a", + "revCount": 909248, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2511.909248%2Brev-0590cd39f728e129122770c029970378a79d076a/019ce32b-8ace-7339-b129-cceaa8dd10c6/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2511" + } + }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + } + }, + "nixpkgs-darwin": { + "locked": { + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1777826146, + "narHash": "sha256-wQ/iN5Zp5VIa3ebBibijPnLyKhor+xEbDy4d0goa9Zs=", + "rev": "73c703c22422b8951895a960959dbbaca7296492", + "revCount": 991389, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/nixpkgs-weekly/0.1.991389%2Brev-73c703c22422b8951895a960959dbbaca7296492/019df6c8-934b-7d40-b402-027bb5def30f/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.1" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_7": { + "locked": { + "lastModified": 1769433173, + "narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_8": { + "locked": { + "lastModified": 1769433173, + "narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": ["stylix", "flake-parts"], + "nixpkgs": ["stylix", "nixpkgs"] + }, + "locked": { + "lastModified": 1777598946, + "narHash": "sha256-X239dAGaU1+gfDj8jKH8GzlqKMcxaVfXOio+uzBOkeE=", + "owner": "nix-community", + "repo": "NUR", + "rev": "5d55af01c0f86be583931fe99207fc56c14134b3", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": [ + "wawona", + "crate2nix", + "crate2nix_stable", + "flake-compat" + ], + "gitignore": "gitignore_4", + "nixpkgs": ["wawona", "crate2nix", "crate2nix_stable", "nixpkgs"] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": ["system-manager", "userborn", "flake-compat"], + "gitignore": "gitignore", + "nixpkgs": ["system-manager", "userborn", "nixpkgs"] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "pre-commit-hooks_2": { + "inputs": { + "flake-compat": ["wawona", "crate2nix", "flake-compat"], + "gitignore": "gitignore_5", + "nixpkgs": ["wawona", "crate2nix", "nixpkgs"] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "apple-silicon": "apple-silicon", + "determinate-nix": "determinate-nix", + "flake-parts": "flake-parts_2", + "home-manager": "home-manager", + "nix-darwin": "nix-darwin", + "nixpkgs": "nixpkgs_3", + "nixpkgs-darwin": "nixpkgs-darwin", + "sops-nix": "sops-nix", + "spicetify-nix": "spicetify-nix", + "stylix": "stylix", + "system-manager": "system-manager", + "wawona": "wawona" + } + }, + "rust-overlay": { + "inputs": { "nixpkgs": ["wawona", "nixpkgs"] }, + "locked": { + "lastModified": 1776309239, + "narHash": "sha256-XzTecca59093jBsVAE4PVAMcJO+PAYHYHBPRnOR8iWs=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "3717ee024da7b0a20744f12c39b41e27cbc12f2d", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "sops-nix": { + "inputs": { "nixpkgs": ["nixpkgs"] }, + "locked": { + "lastModified": 1777944972, + "narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "c591bf665727040c6cc5cb409079acb22dcce33c", + "type": "github" + }, + "original": { "owner": "Mic92", "repo": "sops-nix", "type": "github" } + }, + "spicetify-nix": { + "inputs": { "nixpkgs": ["nixpkgs"], "systems": "systems" }, + "locked": { + "lastModified": 1778395012, + "narHash": "sha256-A/VRiNFQIwGp8cOC/8yNCRexFHjtFCzBwhajrkyGojo=", + "owner": "Gerg-L", + "repo": "spicetify-nix", + "rev": "3b4991bfc064c3361957f23141351ae2d9833234", + "type": "github" + }, + "original": { + "owner": "Gerg-L", + "repo": "spicetify-nix", + "type": "github" + } + }, + "stylix": { + "inputs": { + "base16": "base16", + "base16-fish": "base16-fish", + "base16-helix": "base16-helix", + "base16-vim": "base16-vim", + "firefox-gnome-theme": "firefox-gnome-theme", + "flake-parts": "flake-parts_3", + "gnome-shell": "gnome-shell", + "nixpkgs": "nixpkgs_4", + "nur": "nur", + "systems": "systems_2", + "tinted-kitty": "tinted-kitty", + "tinted-schemes": "tinted-schemes", + "tinted-tmux": "tinted-tmux", + "tinted-zed": "tinted-zed" + }, + "locked": { + "lastModified": 1778104276, + "narHash": "sha256-/DSSnU0LLmOTG/OCgGwYpxP6+5YvxRx2g/GhI4x6aCU=", + "owner": "danth", + "repo": "stylix", + "rev": "18ed8d270231e067fe2739998479ed5d7c659c2c", + "type": "github" + }, + "original": { "owner": "danth", "repo": "stylix", "type": "github" } + }, + "system-manager": { + "inputs": { + "flake-compat": "flake-compat_3", + "nixpkgs": ["nixpkgs"], + "userborn": "userborn" + }, + "locked": { + "lastModified": 1777874990, + "narHash": "sha256-mQptVpwNFEgWRTZx6LhhxW4r1na+rwheWfgIIhcLOrE=", + "owner": "numtide", + "repo": "system-manager", + "rev": "3f1bffc59e51fc9816a1cf523e0093f11bc9bbf5", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "system-manager", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tinted-kitty": { + "flake": false, + "locked": { + "lastModified": 1735730497, + "narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=", + "owner": "tinted-theming", + "repo": "tinted-kitty", + "rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-kitty", + "type": "github" + } + }, + "tinted-schemes": { + "flake": false, + "locked": { + "lastModified": 1777041405, + "narHash": "sha256-BAGZ7ObFV/9Z61OJZun7ifPyhkuHqNuW1QIhQ8LuzCo=", + "owner": "tinted-theming", + "repo": "schemes", + "rev": "5f868b3a338b6904c47f3833b9c411be641983a8", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "schemes", + "type": "github" + } + }, + "tinted-tmux": { + "flake": false, + "locked": { + "lastModified": 1777169200, + "narHash": "sha256-h7dDbIzP5hDr9v97w9PL6jdAgXawmj6krcH+959rqpU=", + "owner": "tinted-theming", + "repo": "tinted-tmux", + "rev": "f798c2dce44ef815bb6b8f05a82135c7942d35ac", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-tmux", + "type": "github" + } + }, + "tinted-zed": { + "flake": false, + "locked": { + "lastModified": 1777463218, + "narHash": "sha256-Bhkozqtq3BKLqWTlmKm8uAptfX4aRGI8QX3eEL54Vpc=", + "owner": "tinted-theming", + "repo": "base16-zed", + "rev": "5768d08ed2e7944a26a958868cdb073cb8856dae", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-zed", + "type": "github" + } + }, + "userborn": { + "inputs": { + "flake-compat": ["system-manager", "flake-compat"], + "flake-parts": "flake-parts_4", + "nixpkgs": ["system-manager", "nixpkgs"], + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "systems": "systems_3" + }, + "locked": { + "lastModified": 1770377964, + "narHash": "sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=", + "owner": "jfroche", + "repo": "userborn", + "rev": "55c2cd7952c207a62736a5bbd9499ea73da18d24", + "type": "github" + }, + "original": { + "owner": "jfroche", + "ref": "system-manager", + "repo": "userborn", + "type": "github" + } + }, + "wawona": { + "inputs": { + "android-nixpkgs": "android-nixpkgs", + "crate2nix": "crate2nix", + "nix-xcodeenvtests": "nix-xcodeenvtests", + "nixpkgs": ["nixpkgs"], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1777675726, + "narHash": "sha256-LVkJMUsPbauqZMLBnwV9iDqr8Tsh4Pgd2PBBxiM1BzI=", + "owner": "Wawona", + "repo": "Wawona", + "rev": "05e1cc1330c67aadeacb2ae242f3efeb581d567c", + "type": "github" + }, + "original": { + "owner": "Wawona", + "ref": "development", + "repo": "Wawona", + "type": "github" + } + } + }, + "root": "root", + "version": 7 + }, + "original": { + "type": "git", + "url": "file:///private/etc/nix-darwin/.dotfiles" + }, + "originalUrl": "git+file:///private/etc/nix-darwin/.dotfiles", + "resolved": { + "type": "git", + "url": "file:///private/etc/nix-darwin/.dotfiles" + }, + "resolvedUrl": "git+file:///private/etc/nix-darwin/.dotfiles", + "url": "git+file:///private/etc/nix-darwin/.dotfiles" +} diff --git a/modules/apps/_vscode-common.nix b/modules/apps/_vscode-common.nix index 5cea317c..d6d1d387 100644 --- a/modules/apps/_vscode-common.nix +++ b/modules/apps/_vscode-common.nix @@ -1,74 +1,115 @@ -{ pkgs, lib, ... }: +{ + pkgs, + lib, + config, + ... +}: let + fontName = config.stylix.fonts.monospace.name; + appFontSize = config.stylix.fonts.sizes.applications; + modern-pdf-preview = pkgs.vscode-utils.extensionFromVscodeMarketplace { publisher = "chocolatedesue"; name = "modern-pdf-preview"; version = "1.5.9"; sha256 = "0qwzwaynf7wb7lfaaimxlr0n1ngrc68q15mv6hdjpffp52yq7rbh"; }; + + # treefmt VSCode: unifies all formatters behind one `treefmt` call. + # https://marketplace.visualstudio.com/items?itemName=ibecker.treefmt-vscode + treefmt-vscode = pkgs.vscode-utils.extensionFromVscodeMarketplace { + publisher = "ibecker"; + name = "treefmt-vscode"; + version = "2.4.1"; + sha256 = "sha256-ZTRrZDXqK9L7E5fr5NLEa/0ZyTnFdItfytbVuh/qr94="; + }; + + # Google's official Eclipse-JDT formatter profile, fetched at build time + # from `google/styleguide` and pinned to an immutable commit so the + # `redhat.java` extension's "Format Document" always lands on the exact + # same byte-for-byte settings file. Pulling from upstream keeps the asset + # out of this repo's tree (it used to live as a 338-line XML next to + # this module, which the dendritic layout reserves for Nix modules + # only). To bump the profile, refresh both `rev` and `hash`: + # + # curl -fsSL https://api.github.com/repos/google/styleguide/commits/gh-pages \ + # | jq -r .sha # → new rev + # nix store prefetch-file --hash-type sha256 --json \ + # "https://raw.githubusercontent.com/google/styleguide//eclipse-java-google-style.xml" \ + # | jq -r .hash # → new hash (SRI) + eclipse-java-google-style = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/google/styleguide/3c5c895c68bfb108cd5d936937dc36e2dfbdbcc2/eclipse-java-google-style.xml"; + hash = "sha256-51Uku2fj/8iNXGgO11JU4HLj28y7kcSgxwjc+r8r35E="; + }; in { # ── Shared settings & extensions for VSCode, Cursor, and Antigravity ── programs.vscode = { enable = true; + # Avoid Home Manager's onChange hook that shells out to `code --list-extensions` + # during activation; recent Electron/Node builds can crash in headless mode. + mutableExtensionsDir = false; profiles.default.userSettings = { "cursor.composer.enabled" = false; "files.readonlyFromPermissions" = true; "window.titleBarStyle" = "custom"; + "window.autoDetectColorScheme" = true; + "window.autoDetectHighContrast" = true; "workbench.colorTheme" = lib.mkForce "Stylix"; - "editor.fontFamily" = lib.mkForce "'Maple Mono NF', monospace"; - "editor.fontSize" = lib.mkForce 12; + "workbench.preferredDarkColorTheme" = lib.mkForce "Stylix"; + "workbench.preferredLightColorTheme" = lib.mkForce "Stylix"; + "workbench.colorCustomizations" = { + "titleBar.activeBackground" = "#${config.lib.stylix.colors.base00}"; + "titleBar.inactiveBackground" = "#${config.lib.stylix.colors.base00}"; + "titleBar.activeForeground" = "#${config.lib.stylix.colors.base05}"; + "titleBar.inactiveForeground" = "#${config.lib.stylix.colors.base04}"; + }; + "editor.fontFamily" = lib.mkForce "'${fontName}', monospace"; + "editor.fontSize" = lib.mkForce appFontSize; "editor.fontLigatures" = "'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'cv01', 'cv02', 'cv03'"; "editor.rulers" = [ 80 ]; - "terminal.integrated.fontFamily" = "'Maple Mono NF'"; - "terminal.integrated.fontSize" = lib.mkForce 12; + "terminal.integrated.fontFamily" = "'${fontName}'"; + "terminal.integrated.fontSize" = lib.mkForce appFontSize; "window.zoomLevel" = 0; - "swiftformat.path" = "${pkgs.swiftformat}/bin/swiftformat"; - "workbench.editorAssociations" = { - "*.pdf" = "chocolatedesue.modern-pdf-preview"; - }; - "[nix]" = { - "editor.defaultFormatter" = "jnoortheen.nix-ide"; - "editor.formatOnSave" = true; - }; - "nix.enableLanguageServer" = true; - "nix.serverPath" = "${pkgs.nil}/bin/nil"; - "nix.serverSettings" = { - "nil" = { - "formatting" = { - "command" = [ "${pkgs.nixfmt}/bin/nixfmt" ]; - }; - }; - }; - "nix.formatterPath" = "${pkgs.nixfmt}/bin/nixfmt"; - "[swift]" = { - "editor.defaultFormatter" = "nicklockwood.swiftformat"; - "editor.formatOnSave" = true; - }; - "[asm]" = { - "editor.formatOnSave" = true; - }; - "[s]" = { + + # ── Workspace trust: disabled (Nix manages everything) ─────── + "security.workspace.trust.enabled" = false; + # Fork-specific toggle used by Cursor/Antigravity builds. + "workspaceValidation" = false; + "workspaceValidation.enabled" = false; + + # ── treefmt: global default formatter ──────────────────────── + "treefmt.path" = "${pkgs.treefmt}/bin/treefmt"; + "editor.defaultFormatter" = "ibecker.treefmt-vscode"; + "editor.formatOnSave" = true; + + # ── Per-language overrides that treefmt doesn't cover ──────── + # Keep typst (tinymist) and java (redhat.java) as explicit formatters. + "[typst]" = { + "editor.defaultFormatter" = "myriad-dreamin.tinymist"; "editor.formatOnSave" = true; }; "[java]" = { "editor.defaultFormatter" = "redhat.java"; "editor.formatOnSave" = true; }; - "java.format.settings.url" = "${./eclipse-java-google-style.xml}"; + "java.format.settings.url" = "${eclipse-java-google-style}"; "java.format.settings.profile" = "GoogleStyle"; "java.format.enabled" = true; - "tinymist.preview.refresh" = "onType"; - "[typst]" = { - "editor.defaultFormatter" = "myriad-dreamin.tinymist"; - "editor.formatOnSave" = true; + + # ── workbench ───────────────────────────────────────────────── + "workbench.editorAssociations" = { + "*.pdf" = "chocolatedesue.modern-pdf-preview"; }; + "tinymist.preview.refresh" = "onType"; + + # ── Clangd / C++ ───────────────────────────────────────────── "C_cpp.intelliSenseEngine" = "disabled"; }; profiles.default.extensions = with pkgs.vscode-extensions; [ bbenoist.nix - jnoortheen.nix-ide + # jnoortheen.nix-ide excluded — treefmt handles nix formatting ms-python.python charliermarsh.ruff dbaeumer.vscode-eslint @@ -82,6 +123,56 @@ in ms-vscode.cpptools modern-pdf-preview myriad-dreamin.tinymist + treefmt-vscode ]; }; + + # Force-remove extensions that must not be installed in any VSCode fork. + # This runs after the extension dirs are unlocked and before they are relocked, + # so a deleted extension cannot survive the activation cycle. + home.activation.purgeBlockedVscodeExtensions = + lib.hm.dag.entryBetween [ "lockManagedVscodeExtensionDirs" ] [ "unlockManagedVscodeExtensionDirs" ] + '' + for EXT_DIR in \ + "$HOME/.vscode/extensions" \ + "$HOME/.cursor/extensions" \ + "$HOME/.antigravity/extensions" + do + # Remove jnoortheen.nix-ide — conflicts with bbenoist.nix (already managed). + for MATCH in "$EXT_DIR"/jnoortheen.nix-ide-*; do + [ -e "$MATCH" ] && rm -rf "$MATCH" || true + done + done + ''; + + # Keep nix-managed extension directories immutable in all VSCode forks. + # Unlock before HM link checks so generation updates can apply, then + # relock after links are materialized so in-app uninstall/remove fails. + home.activation.unlockManagedVscodeExtensionDirs = lib.hm.dag.entryBefore [ "checkLinkTargets" ] '' + MANAGED_EXTENSION_DIRS=" + $HOME/.vscode/extensions + $HOME/.cursor/extensions + $HOME/.antigravity/extensions + " + + for EXT_DIR in $MANAGED_EXTENSION_DIRS; do + if [ -e "$EXT_DIR" ]; then + chmod u+rwx "$EXT_DIR" || true + fi + done + ''; + + home.activation.lockManagedVscodeExtensionDirs = lib.hm.dag.entryAfter [ "linkGeneration" ] '' + MANAGED_EXTENSION_DIRS=" + $HOME/.vscode/extensions + $HOME/.cursor/extensions + $HOME/.antigravity/extensions + " + + for EXT_DIR in $MANAGED_EXTENSION_DIRS; do + if [ -d "$EXT_DIR" ]; then + chmod a-w "$EXT_DIR" || true + fi + done + ''; } diff --git a/modules/apps/antigravity.nix b/modules/apps/antigravity.nix index 9b141c74..0fdbd3ba 100644 --- a/modules/apps/antigravity.nix +++ b/modules/apps/antigravity.nix @@ -1,20 +1,78 @@ { - flake.modules.homeManager.antigravity = { pkgs, lib, config, ... }: { - options.dendritic.apps.antigravity = { - enable = lib.mkEnableOption "Antigravity IDE"; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.apps.antigravity; + sharedSettings = config.programs.vscode.profiles.default.userSettings; + sharedExtensions = config.programs.vscode.profiles.default.extensions; + + # Build extension symlinks by reading each extension's directory + extensionFiles = lib.foldl' ( + acc: ext: + let + extPath = "${ext}/share/vscode/extensions"; + dirs = builtins.attrNames (builtins.readDir extPath); + in + acc + // (lib.listToAttrs ( + map (dir: { + name = ".antigravity/extensions/${dir}"; + value = { + source = "${extPath}/${dir}"; + }; + }) dirs + )) + ) { } sharedExtensions; + in + { + options.dendritic.apps.antigravity = { + enable = lib.mkEnableOption "Antigravity IDE"; + }; + + imports = [ ./_vscode-common.nix ]; + config = lib.mkIf cfg.enable { + home.packages = [ + (if pkgs.stdenv.isDarwin then pkgs.antigravity else pkgs.antigravity-fhs) + ]; + + # Mirror VS Code settings/extensions so Stylix + editor defaults stay in sync. + home.file = + extensionFiles + // lib.optionalAttrs pkgs.stdenv.isDarwin { + "Library/Application Support/Antigravity/User/settings.json" = { + force = true; + text = builtins.toJSON sharedSettings; + }; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + ".antigravity/User/settings.json".text = builtins.toJSON sharedSettings; + ".config/Antigravity/User/settings.json".text = builtins.toJSON sharedSettings; + }; + }; }; - imports = [ ./_vscode-common.nix ]; - config = lib.mkIf config.dendritic.apps.antigravity.enable { - home.packages = [ - (if pkgs.stdenv.isDarwin then pkgs.antigravity else pkgs.antigravity-fhs) + # Dock registration: Antigravity owns its dock entry (order 170 in `dock.nix`). + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + user = config.system.primaryUser; + antigravityEnabled = config.home-manager.users.${user}.dendritic.apps.antigravity.enable or false; + in + lib.mkIf antigravityEnabled { + dendritic.dock.apps = lib.mkOrder 170 [ + "${ + if pkgs.stdenv.isDarwin then pkgs.antigravity else pkgs.antigravity-fhs + }/Applications/Antigravity.app" ]; - - # Ensure extensions are linked for Antigravity - home.file.".antigravity/extensions/bbenoist.Nix".source = - "${pkgs.vscode-extensions.bbenoist.nix}/share/vscode/extensions/bbenoist.Nix"; - home.file.".antigravity/extensions/jnoortheen.nix-ide".source = - "${pkgs.vscode-extensions.jnoortheen.nix-ide}/share/vscode/extensions/jnoortheen.nix-ide"; }; - }; } diff --git a/modules/apps/beeper.nix b/modules/apps/beeper.nix index 250f6e44..b0f30bf0 100644 --- a/modules/apps/beeper.nix +++ b/modules/apps/beeper.nix @@ -1,601 +1,652 @@ { - flake.modules.homeManager.beeper = { - pkgs, - lib, - config, - ... - }: let - cfg = config.dendritic.apps.beeper; - colors = config.lib.stylix.colors; - - # Helper to convert hex to RGB components - # Stylix colors are like "1a1b26" - hexToRGBComponents = hex: let - r = lib.strings.substring 0 2 hex; - g = lib.strings.substring 2 2 hex; - b = lib.strings.substring 4 2 hex; - - hexToDecMap = { - "0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4; "5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9; - "a" = 10; "b" = 11; "c" = 12; "d" = 13; "e" = 14; "f" = 15; - "A" = 10; "B" = 11; "C" = 12; "D" = 13; "E" = 14; "F" = 15; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.apps.beeper; + colors = config.lib.stylix.colors; + + # Generate the ultimate Beeper CSS + beeperCSS = '' + /* + Ultimate Beeper Theme (Stylix/Base16) - Solid Edition + Managed by Nix via Home Manager. + */ + + :root { + /* ── Base16 Polarity-Aware Core ── */ + --color-bg: #${colors.base00}; + --color-fg: #${colors.base05}; + --color-primary: #${colors.base0D}; + + /* Grayscale Surface Stack (Darkest to Lighter) */ + --bg-0: #${colors.base00}; + --bg-1: #${colors.base01}; + --bg-2: #${colors.base02}; + --bg-3: #${colors.base03}; + + /* Text & Content Stack (Muted to Brightest) */ + --fg-0: #${colors.base04}; + --fg-1: #${colors.base05}; + --fg-2: #${colors.base06}; + --fg-3: #${colors.base07}; + + /* ── Beeper Variable Mapping ── */ + + /* UI Backgrounds */ + --color-background-app: var(--bg-0); + --color-background-app-weak: var(--bg-1); + --color-background-elevated: var(--bg-1); + --color-background-elevated-hover: var(--bg-2); + --color-background-grouped: var(--bg-1); + --color-background-grouped-weak: var(--color-base-gray-10); + --color-background-object: var(--bg-2); + + /* Buttons */ + --color-background-button-primary: var(--color-primary); + --color-background-button-primary-active: #${colors.base0C}; + --color-background-button-primary-disabled: var(--bg-3); + --color-background-button-secondary: var(--bg-2); + --color-background-button-secondary-active: var(--bg-3); + --color-background-button-secondary-disabled: var(--bg-1); + --color-background-button-translucent: rgba(var(--color-bg-rgb), 0.1); + --color-background-button-translucent-active: rgba(var(--color-bg-rgb), 0.15); + --color-background-button-translucent-disabled: rgba(var(--color-bg-rgb), 0.05); + + /* Sidebar */ + --color-background-sidebar: var(--bg-1); + --color-background-sidebar-opaque: var(--bg-1); + --color-background-sidebar-thread-focus: var(--bg-2); + --color-background-sidebar-thread-selected: var(--color-primary); + --color-background-sidebar-thread-selected-unfocused: var(--bg-2); + + /* Messages */ + --color-background-message-active: var(--bg-1); + --color-background-message-bubble-received: var(--bg-1); + --color-background-message-bubble-sent: var(--color-primary); + --color-background-message-bubble-linked: var(--bg-0); + + /* Inputs */ + --color-background-input: var(--bg-1); + --color-background-kbd: var(--bg-2); + + /* Header/Menu */ + --color-background-header-right: var(--bg-1); + --color-background-header-right-opaque: var(--bg-1); + --color-background-menu: var(--bg-2); + --color-background-menu-opaque: var(--bg-2); + --color-background-menu-option-hover: var(--color-primary); + + /* Borders */ + --color-border-neutrals: var(--bg-2); + --color-border-neutrals-strong: var(--bg-3); + --color-border-neutrals-weak: var(--bg-1); + --color-border-input: var(--bg-2); + --color-border-input-active: var(--bg-3); + + /* Text */ + --color-text-neutrals: var(--fg-1); + --color-text-neutrals-subtle: var(--fg-0); + --color-text-neutrals-weak: var(--fg-0); + --color-text-on-accent: var(--bg-0); + --color-text-on-accent-weak: var(--bg-1); + --color-text-translucent: var(--fg-1); + + /* Icons */ + --color-icon-neutrals: var(--fg-0); + --color-icon-neutrals-strong: var(--fg-1); + --color-icon-neutrals-subtle: var(--bg-3); + + /* Typography */ + --font-family: "${config.stylix.fonts.sansSerif.name}", system-ui, -apple-system, BlinkMacSystemFont, Twemoji, "Segoe UI", "Helvetica Neue", sans-serif; + --font-family-mono: "${config.stylix.fonts.monospace.name}", monospace; + + /* Functional Colors mapping */ + --functional-red: #${colors.base08}; + --functional-orange: #${colors.base09}; + --functional-green: #${colors.base0B}; + --functional-cyan: #${colors.base0C}; + --functional-purple: #${colors.base0E}; + + /* Matrix/Element Aliases */ + --primary-content: var(--fg-1) !important; + --secondary-content: var(--fg-0) !important; + --tertiary-content: var(--fg-0) !important; + --accent: var(--color-primary) !important; + --background: var(--bg-0) !important; + --timeline-background: var(--bg-0) !important; + --composer-background: var(--bg-1) !important; + + /* Audio Bar */ + --audio-bar-bg: var(--color-background-elevated); + --audio-bar-border: var(--color-border-neutrals); + --audio-bar-button: var(--color-icon-neutrals); + + /* Keyboard Keys */ + --key-bg: var(--color-background-elevated); + --key-border: var(--color-border-neutrals); + + /* ANSI Colors (Stylix base00-base0F) */ + --ansi-black: #${colors.base00}; + --ansi-red: #${colors.base08}; + --ansi-green: #${colors.base0B}; + --ansi-yellow: #${colors.base0A}; + --ansi-blue: #${colors.base0D}; + --ansi-magenta: #${colors.base0E}; + --ansi-cyan: #${colors.base0C}; + --ansi-white: #${colors.base05}; + --ansi-bright-black: #${colors.base03}; + --ansi-bright-red: #${colors.base08}; + --ansi-bright-green: #${colors.base0B}; + --ansi-bright-yellow: #${colors.base0A}; + --ansi-bright-blue: #${colors.base0D}; + --ansi-bright-magenta: #${colors.base0E}; + --ansi-bright-cyan: #${colors.base0C}; + --ansi-bright-white: #${colors.base07}; + + /* Layout & Spacing Variables */ + --header-height: 48px; + --filters-pane-width: 220px; + --min-sidebar-width: 280px; + --max-sidebar-width: 800px; + --threads-list-item-height: 54px; + --pinned-thread-base-size: 64px; + + --margin-50: 2px; + --margin-75: 3px; + --margin-100: 4px; + --margin-200: 6px; + --margin-300: 8px; + --margin-400: 10px; + --margin-500: 12px; + --margin-700: 16px; + --margin-900: 22px; + --margin-1000: 24px; + + --border-radius-25: 4px; + --border-radius-50: 8px; + --border-radius-100: 12px; + --border-radius-200: 20px; + --border-radius-conversation-bubble: 17px; + + /* Detailed Typography */ + --font-weight-regular: 400; + --font-weight-emphasized: 600; + --font-size-body-medium: 0.875rem; + --line-height-body-medium: 1.125rem; + --font-size-label-medium: 0.75rem; + --line-height-label-medium: 0.9375rem; + + /* Sizes & Dimensions */ + --inbox-avatar-size: 28px; + --cv-avatar-size: 28px; + --inbox-icon-size: 20px; + --inbox-search-icon-size: 15px; + --account-switcher-width: 54px; + --composer-attachment-max-height: 66px; + --message-padding-horizontal: 12px; + --message-padding-vertical: 5px; + + /* JetBrains Dark Purple inspired additions */ + --color-background-preferences-option-selected: var(--color-primary); + --color-background-commandbar-opaque: var(--color-background-sidebar); + --color-background-commandbar-command-highlighted: var(--color-primary); + --color-background-selected: var(--color-primary); + + /* ── Beeper 4.x Material You Surface Variables ────────────── + Beeper 4.x uses --color-surface (defaults to white in light + mode) for nearly every background. Override them here so the + left panel, thread list, and secondary containers inherit the + Stylix palette instead of the default browser white. */ + --color-surface: var(--bg-0); + --color-surface-bright: var(--bg-0); + --color-surface-dim: var(--bg-1); + --color-on-surface: var(--fg-1); + --color-on-surface-variant: var(--fg-0); + + /* Secondary / tertiary containers (sidebar sections, list rows) */ + --color-secondary-container: var(--bg-1); + --color-tertiary-container: var(--bg-1); + --color-container-inside-secondary-container: var(--bg-2); + --color-on-secondary-container: var(--fg-0); + --color-on-tertiary-container: var(--fg-0); + --color-primary-container: var(--bg-0); + + /* Outline / border tokens */ + --color-outline: var(--bg-3); + --color-outline-variant: var(--bg-2); + + /* Backing shade (used under avatars, activity dots, etc.) */ + --color-backing-shade: var(--bg-2); + + /* Scrim / glass overlays */ + --color-scrim: rgba(0,0,0,0.4); + --color-glass-surface: var(--bg-1); + --color-glass-container: var(--bg-1); + + /* Misc aliases that some Beeper builds still use */ + --color-bg: var(--bg-0); + } + + body { + -webkit-font-smoothing: auto; + --margin-50: 2px; + --margin-75: 3px; + --margin-100: 4px; + --margin-200: 6px; + --margin-300: 8px; + --margin-400: 10px; + --margin-500: 12px; + --margin-700: 16px; + --margin-900: 22px; + --margin-1000: 24px; + } + + /* JetBrains Dark Purple & Advanced UI Refinements */ + .command.command.highlighted .command-children { + background: var(--color-primary) !important; + } + + .panes, .compose-message-container > * { + background: var(--bg-0) !important; + backdrop-filter: none !important; + } + + .linked-message { + color: inherit !important; + } + + .sidebar-thread.isSelected > section:before { + background: var(--bg-0) !important; + } + + /* Sidebar Conversation Titles (Fixing Ghost Backgrounds) */ + .mx_RoomTile_name, + .mx_RoomTile_title, + .mx_RoomTile_messagePreview, + [class*="RoomTile_name"], + [class*="RoomTile_title"] { + background: transparent !important; + background-color: transparent !important; + color: var(--color-fg) !important; + } + + /* Global App Styles */ + body, #matrixchat, .mx_MatrixChat, .mx_RoomView, .mx_RoomView_messageList, .mx_MainSplit, .mx_EventTile_content { + background-color: var(--color-bg) !important; + background: var(--color-bg) !important; + color: var(--color-fg) !important; + } + + input, textarea, [contenteditable="true"] { + color: var(--color-fg) !important; + background-color: var(--color-surface-elevated) !important; + } + + /* ── Comprehensive Text Color Overrides ── */ + + /* Global text fallback */ + #matrixchat, .mx_MatrixChat, .mx_RoomView_messageList, .mx_MainSplit { + color: var(--color-fg) !important; + } + + /* Specific Matrix/Element/Beeper classes */ + .mx_EventTile_body, + .mx_EventTile_senderDetails, + .mx_RoomTile_name, + .mx_RoomTile_messagePreview, + .mx_GenericEventListSummary_summary, + .mx_TextualEvent, + .mx_RoomHeader_nametext, + .mx_RoomHeader_topic, + .mx_LeftPanel_section_header, + .thread-list-item .title, + .thread-list-item .preview, + .thread-list-item .timestamp, + .mx_MessageComposer_editor, + .mx_EventTile_timestamp, + .mx_RoomTile_subtitle, + .mx_SecondaryText, + .mx_MTextBody, + .mx_ReplyTile .mx_MTextBody, + .mx_EventTile_receiptSent, + .mx_EventTile_receiptSending, + .mx_RoomSubList_label, + .mx_BaseCard_header_title, + .mx_SettingsTab_heading, + .mx_SettingsFlag_label, + .mx_LabelledCheckbox_label, + .mx_Heading_h1, .mx_Heading_h2, .mx_Heading_h3, .mx_Heading_h4, + .mx_Dialog_title, + .mx_Dialog_content, + .mx_MainSplit_searchResultHeader, + .mx_SearchStatus, + .mx_GenericEventListSummary_summary, + .mx_MemberStatus_online, + .mx_MemberStatus_offline, + .mx_MemberStatus_away, + .mx_MessageActionBar_icon, + .mx_NotificationBadge, + .mx_SpaceButton_label, + .mx_GroupFilterPanel_item_label, + .mx_InviteDialog_tile_name, + .mx_UserMenu_name, + .mx_RoomView_empty_title, + .mx_RoomView_empty_text, + .mx_RoomTile_messagePreview_line, + .mx_RoomDropTarget_label, + a { + color: var(--color-fg) !important; + } + + /* Handle generic text elements within the chat area */ + .mx_RoomView_messageList div, + .mx_RoomView_messageList span, + .mx_RoomView_messageList p, + .mx_RoomView_messageList li { + color: var(--color-fg) !important; + } + + a:hover { + color: var(--color-primary) !important; + } + + /* Code Blocks & Inline Code */ + code, pre, .mx_EventTile_body code, .mx_EventTile_body pre { + background-color: var(--color-surface-elevated) !important; + color: var(--color-primary) !important; + border: 1px solid var(--color-border-strong) !important; + border-radius: 4px !important; + font-family: var(--font-family-mono) !important; + padding: 2px 4px !important; + } + + pre { + padding: 10px !important; + margin: 8px 0 !important; + display: block !important; + overflow-x: auto !important; + } + + /* Reactions */ + .mx_ReactionsRow_item { + background-color: var(--color-surface-elevated) !important; + border: 1px solid var(--color-border) !important; + border-radius: 8px !important; + color: var(--color-fg) !important; + } + + .mx_ReactionsRow_item.mx_ReactionsRow_item_selected { + background-color: var(--color-surface-active) !important; + border-color: var(--color-primary) !important; + color: var(--color-primary) !important; + } + + /* Mentions and Pills */ + .mx_Pill, .mx_Mention, .mx_UserPill { + background-color: var(--color-surface-active) !important; + color: var(--color-primary) !important; + border-radius: 4px !important; + padding: 0 4px !important; + } + + /* ── Deep Dive Component Fixes (Attribute & Structural) ── */ + + /* 1. Inbox Header & Filter Buttons */ + [aria-label="Filter"], + [aria-label="Inbox"], + [aria-label*="filter" i], + [aria-label*="inbox" i], + [role="button"][aria-label*="filter" i], + .inbox-filter, + .filter-button, + .mx_FilterButton { + color: var(--color-fg) !important; + fill: var(--color-fg) !important; /* Catch icons */ + } + + /* 2. Inbox Text & Headings */ + h1, h2, h3, h4, + [role="heading"], + .mx_RoomList_header, + .mx_LeftPanel_roomList_header, + .mx_Heading_h1, + .mx_Heading_h2 { + color: var(--color-fg) !important; + background-color: var(--color-bg) !important; + } + + /* 3. Date Labels & Timestamps (Banishing Dark/Light Confusion) */ + .mx_DateSeparator_content, + .mx_EventTile_timestamp, + .mx_MessageTimestamp, + .thread-list-item .timestamp, + [class*="DateSeparator"], + [class*="timestamp"], + [class*="date-label"] { + color: var(--color-fg) !important; + background-color: transparent !important; + opacity: 0.8 !important; + } + + /* 4. Links Fix (Flipped Colors) */ + a, .mx_Link, .mx_EventTile_body a { + color: var(--color-primary) !important; + text-decoration: underline !important; + } + + a:hover { + color: #${colors.base0C} !important; /* Brightest accent on hover */ + } + + /* 5. Message Bubbles & Replies (The "Dark-on-Dark" and "Light-on-Light" Killer) */ + + /* Received Messages */ + .mx_EventTile_received, + .mx_EventTile_received *, + .mx_EventTile_bubble, + .mx_EventTile_bubble *, + .mx_EventTile_line, + .mx_EventTile_content { + background-color: var(--color-base-gray-20) !important; + color: var(--color-fg) !important; + } + + /* Reply Tiles & Previews */ + .mx_ReplyTile, + .mx_ReplyTile *, + .mx_ReplyChain, + .mx_ReplyChain *, + .mx_EventTile_reply { + background-color: var(--color-base-gray-10) !important; + color: var(--color-fg) !important; + border-left: 2px solid var(--color-primary) !important; + } + + /* 6. Inbox / Archive Switcher & Space Panel */ + .mx_SpacePanel, + .mx_SpacePanel *, + .mx_SpaceButton, + .mx_SpaceButton *, + .mx_RoomList_header_switcher, + .mx_RoomList_header_switcher *, + [class*="SpaceButton"], + [class*="SpacePanel"], + [class*="header_switcher"] { + background-color: var(--color-background-sidebar) !important; + color: var(--color-fg) !important; + } + + .mx_SpaceButton_active, + .mx_SpaceButton_selected { + background-color: var(--color-surface-active) !important; + } + + /* 7. Generic Button & Icon Protection */ + button, [role="button"] { + color: var(--color-fg) !important; + } + + svg, [class*="icon"], [class*="Icon"] { + fill: currentColor !important; + } + + /* ── Problem Area Overrides (Titlebar, Sidebar, Composer) ── */ + + /* 1. Titlebar & Header */ + .mx_RoomHeader, + .mx_RoomHeader *, + .mx_TitleBar, + .mx_TitleBar *, + .mx_RoomHeader_wrapper, + .mx_RoomHeader_name, + .mx_RoomHeader_nametext, + .mx_RoomHeader_topic { + color: var(--color-fg) !important; + background-color: var(--color-bg) !important; + } + + /* 2. Sidebar (Message List / Room List) */ + .mx_LeftPanel, + .mx_LeftPanel *, + .thread-list, + .thread-list *, + .mx_RoomList, + .mx_RoomList *, + .mx_RoomTile, + .mx_RoomTile *, + .mx_RoomTile_name, + .mx_RoomTile_messagePreview, + .mx_RoomTile_subtitle, + .mx_LeftPanel_section_header, + .mx_LeftPanel_section_header * { + color: var(--color-fg) !important; + } + + /* Sidebar Backgrounds (Consistency) */ + .mx_LeftPanel, .thread-list, .mx_RoomList, .mx_SpacePanel { + background-color: var(--color-background-sidebar) !important; + } + + /* ── Beeper 4.x left panel / surface fallback ────────────────── + Beeper 4.x renders the left panel with background:var(--color-surface). + The :root override above handles the CSS variable, but we also + apply a direct rule to catch any element using a hardcoded white. */ + [class*="surface"], + [class*="Surface"], + [class*="leftPanel"], + [class*="LeftPanel"], + [class*="sideBar"], + [class*="sidebar"], + [class*="Sidebar"], + [class*="inbox"], + [class*="Inbox"] { + background-color: var(--bg-0) !important; + color: var(--fg-1) !important; + } + + /* 3. Chat Box (Composer / Input Area) */ + .mx_SendMessageComposer, + .mx_SendMessageComposer *, + .composer-container, + .composer-container *, + .mx_MessageComposer, + .mx_MessageComposer *, + .mx_MessageComposer_editor, + .mx_MessageComposer_editor *, + .mx_Composer_input, + .mx_Composer_input *, + [contenteditable="true"], + [contenteditable="true"] * { + color: var(--color-fg) !important; + } + + /* Composer Background Fixes */ + .mx_SendMessageComposer, + .composer-container, + .mx_MessageComposer_editor, + .mx_MessageComposer_wrapper { + background-color: var(--color-surface-elevated) !important; + background: var(--color-surface-elevated) !important; + } + + /* Specific Fix for Placeholder text in Composer */ + .mx_MessageComposer_editor:empty:before, + .mx_Composer_input:empty:before, + [data-placeholder]:empty:before { + color: var(--color-fg) !important; + opacity: 0.5 !important; + } + + /* ── Global Scrollbars ────────────────────────────────────── */ + ::-webkit-scrollbar { + width: 4px; + height: 4px; + } + ::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: 0px; /* Flat scrollbars */ + } + ::-webkit-scrollbar-thumb:hover { + background: #${colors.base03}; + } + ''; + in + { + options.dendritic.apps.beeper = { + enable = lib.mkEnableOption "Beeper Desktop theme customization"; }; - convertPair = pair: let - v1 = hexToDecMap.${lib.strings.substring 0 1 pair}; - v2 = hexToDecMap.${lib.strings.substring 1 1 pair}; - in - v1 * 16 + v2; - in "${toString (convertPair r)}, ${toString (convertPair g)}, ${toString (convertPair b)}"; - - # Generate the ultimate Beeper CSS - beeperCSS = '' - /* - Ultimate Beeper Theme (Stylix/Base16) - Solid Edition - Managed by Nix via Home Manager. - */ - - :root { - /* ── Base16 Polarity-Aware Core ── */ - --color-bg: #${colors.base00}; - --color-fg: #${colors.base05}; - --color-primary: #${colors.base0D}; - - /* Grayscale Surface Stack (Darkest to Lighter) */ - --bg-0: #${colors.base00}; - --bg-1: #${colors.base01}; - --bg-2: #${colors.base02}; - --bg-3: #${colors.base03}; - - /* Text & Content Stack (Muted to Brightest) */ - --fg-0: #${colors.base04}; - --fg-1: #${colors.base05}; - --fg-2: #${colors.base06}; - --fg-3: #${colors.base07}; - - /* ── Beeper Variable Mapping ── */ - - /* UI Backgrounds */ - --color-background-app: var(--bg-0); - --color-background-app-weak: var(--bg-1); - --color-background-elevated: var(--bg-1); - --color-background-elevated-hover: var(--bg-2); - --color-background-grouped: var(--bg-1); - --color-background-grouped-weak: var(--color-base-gray-10); - --color-background-object: var(--bg-2); - - /* Buttons */ - --color-background-button-primary: var(--color-primary); - --color-background-button-primary-active: #${colors.base0C}; - --color-background-button-primary-disabled: var(--bg-3); - --color-background-button-secondary: var(--bg-2); - --color-background-button-secondary-active: var(--bg-3); - --color-background-button-secondary-disabled: var(--bg-1); - --color-background-button-translucent: rgba(var(--color-bg-rgb), 0.1); - --color-background-button-translucent-active: rgba(var(--color-bg-rgb), 0.15); - --color-background-button-translucent-disabled: rgba(var(--color-bg-rgb), 0.05); - - /* Sidebar */ - --color-background-sidebar: var(--bg-1); - --color-background-sidebar-opaque: var(--bg-1); - --color-background-sidebar-thread-focus: var(--bg-2); - --color-background-sidebar-thread-selected: var(--color-primary); - --color-background-sidebar-thread-selected-unfocused: var(--bg-2); - - /* Messages */ - --color-background-message-active: var(--bg-1); - --color-background-message-bubble-received: var(--bg-1); - --color-background-message-bubble-sent: var(--color-primary); - --color-background-message-bubble-linked: var(--bg-0); - - /* Inputs */ - --color-background-input: var(--bg-1); - --color-background-kbd: var(--bg-2); - - /* Header/Menu */ - --color-background-header-right: var(--bg-1); - --color-background-header-right-opaque: var(--bg-1); - --color-background-menu: var(--bg-2); - --color-background-menu-opaque: var(--bg-2); - --color-background-menu-option-hover: var(--color-primary); - - /* Borders */ - --color-border-neutrals: var(--bg-2); - --color-border-neutrals-strong: var(--bg-3); - --color-border-neutrals-weak: var(--bg-1); - --color-border-input: var(--bg-2); - --color-border-input-active: var(--bg-3); - - /* Text */ - --color-text-neutrals: var(--fg-1); - --color-text-neutrals-subtle: var(--fg-0); - --color-text-neutrals-weak: var(--fg-0); - --color-text-on-accent: var(--bg-0); - --color-text-on-accent-weak: var(--bg-1); - --color-text-translucent: var(--fg-1); - - /* Icons */ - --color-icon-neutrals: var(--fg-0); - --color-icon-neutrals-strong: var(--fg-1); - --color-icon-neutrals-subtle: var(--bg-3); - - /* Typography */ - --font-family: "${config.stylix.fonts.sansSerif.name}", system-ui, -apple-system, BlinkMacSystemFont, Twemoji, "Segoe UI", "Helvetica Neue", sans-serif; - --font-family-mono: "${config.stylix.fonts.monospace.name}", monospace; - - /* Functional Colors mapping */ - --functional-red: #${colors.base08}; - --functional-orange: #${colors.base09}; - --functional-green: #${colors.base0B}; - --functional-cyan: #${colors.base0C}; - --functional-purple: #${colors.base0E}; - - /* Matrix/Element Aliases */ - --primary-content: var(--fg-1) !important; - --secondary-content: var(--fg-0) !important; - --tertiary-content: var(--fg-0) !important; - --accent: var(--color-primary) !important; - --background: var(--bg-0) !important; - --timeline-background: var(--bg-0) !important; - --composer-background: var(--bg-1) !important; - - /* Audio Bar */ - --audio-bar-bg: var(--color-background-elevated); - --audio-bar-border: var(--color-border-neutrals); - --audio-bar-button: var(--color-icon-neutrals); - - /* Keyboard Keys */ - --key-bg: var(--color-background-elevated); - --key-border: var(--color-border-neutrals); - - /* ANSI Colors (Stylix base00-base0F) */ - --ansi-black: #${colors.base00}; - --ansi-red: #${colors.base08}; - --ansi-green: #${colors.base0B}; - --ansi-yellow: #${colors.base0A}; - --ansi-blue: #${colors.base0D}; - --ansi-magenta: #${colors.base0E}; - --ansi-cyan: #${colors.base0C}; - --ansi-white: #${colors.base05}; - --ansi-bright-black: #${colors.base03}; - --ansi-bright-red: #${colors.base08}; - --ansi-bright-green: #${colors.base0B}; - --ansi-bright-yellow: #${colors.base0A}; - --ansi-bright-blue: #${colors.base0D}; - --ansi-bright-magenta: #${colors.base0E}; - --ansi-bright-cyan: #${colors.base0C}; - --ansi-bright-white: #${colors.base07}; - - /* Layout & Spacing Variables */ - --header-height: 48px; - --filters-pane-width: 220px; - --min-sidebar-width: 280px; - --max-sidebar-width: 800px; - --threads-list-item-height: 54px; - --pinned-thread-base-size: 64px; - - --margin-50: 2px; - --margin-75: 3px; - --margin-100: 4px; - --margin-200: 6px; - --margin-300: 8px; - --margin-400: 10px; - --margin-500: 12px; - --margin-700: 16px; - --margin-900: 22px; - --margin-1000: 24px; - - --border-radius-25: 4px; - --border-radius-50: 8px; - --border-radius-100: 12px; - --border-radius-200: 20px; - --border-radius-conversation-bubble: 17px; - - /* Detailed Typography */ - --font-weight-regular: 400; - --font-weight-emphasized: 600; - --font-size-body-medium: 0.875rem; - --line-height-body-medium: 1.125rem; - --font-size-label-medium: 0.75rem; - --line-height-label-medium: 0.9375rem; - - /* Sizes & Dimensions */ - --inbox-avatar-size: 28px; - --cv-avatar-size: 28px; - --inbox-icon-size: 20px; - --inbox-search-icon-size: 15px; - --account-switcher-width: 54px; - --composer-attachment-max-height: 66px; - --message-padding-horizontal: 12px; - --message-padding-vertical: 5px; - - /* JetBrains Dark Purple inspired additions */ - --color-background-preferences-option-selected: var(--color-primary); - --color-background-commandbar-opaque: var(--color-background-sidebar); - --color-background-commandbar-command-highlighted: var(--color-primary); - --color-secondary-container: var(--color-bg); - --color-tertiary-container: var(--color-background-sidebar); - --color-background-selected: var(--color-primary); - } - - body { - -webkit-font-smoothing: auto; - --margin-50: 2px; - --margin-75: 3px; - --margin-100: 4px; - --margin-200: 6px; - --margin-300: 8px; - --margin-400: 10px; - --margin-500: 12px; - --margin-700: 16px; - --margin-900: 22px; - --margin-1000: 24px; - } - - /* JetBrains Dark Purple & Advanced UI Refinements */ - .command.command.highlighted .command-children { - background: var(--color-primary) !important; - } - - .panes, .compose-message-container > * { - background: var(--bg-0) !important; - backdrop-filter: none !important; - } - - .linked-message { - color: inherit !important; - } - - .sidebar-thread.isSelected > section:before { - background: var(--bg-0) !important; - } - - /* Sidebar Conversation Titles (Fixing Ghost Backgrounds) */ - .mx_RoomTile_name, - .mx_RoomTile_title, - .mx_RoomTile_messagePreview, - [class*="RoomTile_name"], - [class*="RoomTile_title"] { - background: transparent !important; - background-color: transparent !important; - color: var(--color-fg) !important; - } - - /* Global App Styles */ - body, #matrixchat, .mx_MatrixChat, .mx_RoomView, .mx_RoomView_messageList, .mx_MainSplit, .mx_EventTile_content { - background-color: var(--color-bg) !important; - background: var(--color-bg) !important; - color: var(--color-fg) !important; - } - - input, textarea, [contenteditable="true"] { - color: var(--color-fg) !important; - background-color: var(--color-surface-elevated) !important; - } - - /* ── Comprehensive Text Color Overrides ── */ - - /* Global text fallback */ - #matrixchat, .mx_MatrixChat, .mx_RoomView_messageList, .mx_MainSplit { - color: var(--color-fg) !important; - } - - /* Specific Matrix/Element/Beeper classes */ - .mx_EventTile_body, - .mx_EventTile_senderDetails, - .mx_RoomTile_name, - .mx_RoomTile_messagePreview, - .mx_GenericEventListSummary_summary, - .mx_TextualEvent, - .mx_RoomHeader_nametext, - .mx_RoomHeader_topic, - .mx_LeftPanel_section_header, - .thread-list-item .title, - .thread-list-item .preview, - .thread-list-item .timestamp, - .mx_MessageComposer_editor, - .mx_EventTile_timestamp, - .mx_RoomTile_subtitle, - .mx_SecondaryText, - .mx_MTextBody, - .mx_ReplyTile .mx_MTextBody, - .mx_EventTile_receiptSent, - .mx_EventTile_receiptSending, - .mx_RoomSubList_label, - .mx_BaseCard_header_title, - .mx_SettingsTab_heading, - .mx_SettingsFlag_label, - .mx_LabelledCheckbox_label, - .mx_Heading_h1, .mx_Heading_h2, .mx_Heading_h3, .mx_Heading_h4, - .mx_Dialog_title, - .mx_Dialog_content, - .mx_MainSplit_searchResultHeader, - .mx_SearchStatus, - .mx_GenericEventListSummary_summary, - .mx_MemberStatus_online, - .mx_MemberStatus_offline, - .mx_MemberStatus_away, - .mx_MessageActionBar_icon, - .mx_NotificationBadge, - .mx_SpaceButton_label, - .mx_GroupFilterPanel_item_label, - .mx_InviteDialog_tile_name, - .mx_UserMenu_name, - .mx_RoomView_empty_title, - .mx_RoomView_empty_text, - .mx_RoomTile_messagePreview_line, - .mx_RoomDropTarget_label, - a { - color: var(--color-fg) !important; - } - - /* Handle generic text elements within the chat area */ - .mx_RoomView_messageList div, - .mx_RoomView_messageList span, - .mx_RoomView_messageList p, - .mx_RoomView_messageList li { - color: var(--color-fg) !important; - } - - a:hover { - color: var(--color-primary) !important; - } - - /* Code Blocks & Inline Code */ - code, pre, .mx_EventTile_body code, .mx_EventTile_body pre { - background-color: var(--color-surface-elevated) !important; - color: var(--color-primary) !important; - border: 1px solid var(--color-border-strong) !important; - border-radius: 4px !important; - font-family: var(--font-family-mono) !important; - padding: 2px 4px !important; - } - - pre { - padding: 10px !important; - margin: 8px 0 !important; - display: block !important; - overflow-x: auto !important; - } - - /* Reactions */ - .mx_ReactionsRow_item { - background-color: var(--color-surface-elevated) !important; - border: 1px solid var(--color-border) !important; - border-radius: 8px !important; - color: var(--color-fg) !important; - } - - .mx_ReactionsRow_item.mx_ReactionsRow_item_selected { - background-color: var(--color-surface-active) !important; - border-color: var(--color-primary) !important; - color: var(--color-primary) !important; - } - - /* Mentions and Pills */ - .mx_Pill, .mx_Mention, .mx_UserPill { - background-color: var(--color-surface-active) !important; - color: var(--color-primary) !important; - border-radius: 4px !important; - padding: 0 4px !important; - } - - /* ── Deep Dive Component Fixes (Attribute & Structural) ── */ - - /* 1. Inbox Header & Filter Buttons */ - [aria-label="Filter"], - [aria-label="Inbox"], - [aria-label*="filter" i], - [aria-label*="inbox" i], - [role="button"][aria-label*="filter" i], - .inbox-filter, - .filter-button, - .mx_FilterButton { - color: var(--color-fg) !important; - fill: var(--color-fg) !important; /* Catch icons */ - } - - /* 2. Inbox Text & Headings */ - h1, h2, h3, h4, - [role="heading"], - .mx_RoomList_header, - .mx_LeftPanel_roomList_header, - .mx_Heading_h1, - .mx_Heading_h2 { - color: var(--color-fg) !important; - background-color: var(--color-bg) !important; - } - - /* 3. Date Labels & Timestamps (Banishing Dark/Light Confusion) */ - .mx_DateSeparator_content, - .mx_EventTile_timestamp, - .mx_MessageTimestamp, - .thread-list-item .timestamp, - [class*="DateSeparator"], - [class*="timestamp"], - [class*="date-label"] { - color: var(--color-fg) !important; - background-color: transparent !important; - opacity: 0.8 !important; - } - - /* 4. Links Fix (Flipped Colors) */ - a, .mx_Link, .mx_EventTile_body a { - color: var(--color-primary) !important; - text-decoration: underline !important; - } - - a:hover { - color: #${colors.base0C} !important; /* Brightest accent on hover */ - } - - /* 5. Message Bubbles & Replies (The "Dark-on-Dark" and "Light-on-Light" Killer) */ - - /* Received Messages */ - .mx_EventTile_received, - .mx_EventTile_received *, - .mx_EventTile_bubble, - .mx_EventTile_bubble *, - .mx_EventTile_line, - .mx_EventTile_content { - background-color: var(--color-base-gray-20) !important; - color: var(--color-fg) !important; - } - - /* Reply Tiles & Previews */ - .mx_ReplyTile, - .mx_ReplyTile *, - .mx_ReplyChain, - .mx_ReplyChain *, - .mx_EventTile_reply { - background-color: var(--color-base-gray-10) !important; - color: var(--color-fg) !important; - border-left: 2px solid var(--color-primary) !important; - } - - /* 6. Inbox / Archive Switcher & Space Panel */ - .mx_SpacePanel, - .mx_SpacePanel *, - .mx_SpaceButton, - .mx_SpaceButton *, - .mx_RoomList_header_switcher, - .mx_RoomList_header_switcher *, - [class*="SpaceButton"], - [class*="SpacePanel"], - [class*="header_switcher"] { - background-color: var(--color-background-sidebar) !important; - color: var(--color-fg) !important; - } - - .mx_SpaceButton_active, - .mx_SpaceButton_selected { - background-color: var(--color-surface-active) !important; - } - - /* 7. Generic Button & Icon Protection */ - button, [role="button"] { - color: var(--color-fg) !important; - } - - svg, [class*="icon"], [class*="Icon"] { - fill: currentColor !important; - } - - /* ── Problem Area Overrides (Titlebar, Sidebar, Composer) ── */ - - /* 1. Titlebar & Header */ - .mx_RoomHeader, - .mx_RoomHeader *, - .mx_TitleBar, - .mx_TitleBar *, - .mx_RoomHeader_wrapper, - .mx_RoomHeader_name, - .mx_RoomHeader_nametext, - .mx_RoomHeader_topic { - color: var(--color-fg) !important; - background-color: var(--color-bg) !important; - } - - /* 2. Sidebar (Message List / Room List) */ - .mx_LeftPanel, - .mx_LeftPanel *, - .thread-list, - .thread-list *, - .mx_RoomList, - .mx_RoomList *, - .mx_RoomTile, - .mx_RoomTile *, - .mx_RoomTile_name, - .mx_RoomTile_messagePreview, - .mx_RoomTile_subtitle, - .mx_LeftPanel_section_header, - .mx_LeftPanel_section_header * { - color: var(--color-fg) !important; - } - - /* Sidebar Backgrounds (Consistency) */ - .mx_LeftPanel, .thread-list, .mx_RoomList, .mx_SpacePanel { - background-color: var(--color-background-sidebar) !important; - } - - /* 3. Chat Box (Composer / Input Area) */ - .mx_SendMessageComposer, - .mx_SendMessageComposer *, - .composer-container, - .composer-container *, - .mx_MessageComposer, - .mx_MessageComposer *, - .mx_MessageComposer_editor, - .mx_MessageComposer_editor *, - .mx_Composer_input, - .mx_Composer_input *, - [contenteditable="true"], - [contenteditable="true"] * { - color: var(--color-fg) !important; - } - - /* Composer Background Fixes */ - .mx_SendMessageComposer, - .composer-container, - .mx_MessageComposer_editor, - .mx_MessageComposer_wrapper { - background-color: var(--color-surface-elevated) !important; - background: var(--color-surface-elevated) !important; - } - - /* Specific Fix for Placeholder text in Composer */ - .mx_MessageComposer_editor:empty:before, - .mx_Composer_input:empty:before, - [data-placeholder]:empty:before { - color: var(--color-fg) !important; - opacity: 0.5 !important; - } - - /* ── Global Scrollbars ────────────────────────────────────── */ - ::-webkit-scrollbar { - width: 4px; - height: 4px; - } - ::-webkit-scrollbar-thumb { - background: var(--color-border-strong); - border-radius: 0px; /* Flat scrollbars */ - } - ::-webkit-scrollbar-thumb:hover { - background: #${colors.base03}; - } - ''; - in { - options.dendritic.apps.beeper = { - enable = lib.mkEnableOption "Beeper Desktop theme customization"; - }; - - config = lib.mkIf cfg.enable { - home.packages = [ pkgs.beeper ]; - - home.file."Library/Application Support/Beeper/custom.css" = { - text = beeperCSS; - force = true; + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.beeper ]; + + home.file = + lib.optionalAttrs pkgs.stdenv.isDarwin { + # Legacy Beeper path. + "Library/Application Support/Beeper/custom.css" = { + text = beeperCSS; + force = true; + }; + # Current desktop app path used by newer builds. + "Library/Application Support/Beeper Desktop/custom.css" = { + text = beeperCSS; + force = true; + }; + # Beeper Texts/desktop path seen in upstream theme repos. + "Library/Application Support/BeeperTexts/custom.css" = { + text = beeperCSS; + force = true; + }; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + ".config/Beeper/custom.css".text = beeperCSS; + }; + + # ── Beeper Auto-Update Prevention (macOS "uchg" Hard Fix) ────── + home.activation.disableBeeperUpdates = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + BEEPER_UPDATE_PATH="$HOME/Library/Application Support/Beeper/Update" + BEEPER_SHIPIT_PATH="$HOME/Library/Caches/com.beeper.desktop.ShipIt" + + # 1. Block the 'Update' directory + if ! /usr/bin/stat -f "%Sf" "$BEEPER_UPDATE_PATH" 2> /dev/null | grep -q uchg; then + rm -rf "$BEEPER_UPDATE_PATH" + mkdir -p "$BEEPER_UPDATE_PATH" + /usr/bin/chflags uchg "$BEEPER_UPDATE_PATH" + fi + + # 2. Block the Electron 'ShipIt' directory (where update staging happens) + if ! /usr/bin/stat -f "%Sf" "$BEEPER_SHIPIT_PATH" 2> /dev/null | grep -q uchg; then + rm -rf "$BEEPER_SHIPIT_PATH" + mkdir -p "$BEEPER_SHIPIT_PATH" + /usr/bin/chflags uchg "$BEEPER_SHIPIT_PATH" + fi + '' + ); }; - - # ── Beeper Auto-Update Prevention (macOS "uchg" Hard Fix) ────── - home.activation.disableBeeperUpdates = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - BEEPER_UPDATE_PATH="$HOME/Library/Application Support/Beeper/Update" - BEEPER_SHIPIT_PATH="$HOME/Library/Caches/com.beeper.desktop.ShipIt" - - # 1. Block the 'Update' directory - if ! /usr/bin/stat -f "%Sf" "$BEEPER_UPDATE_PATH" 2> /dev/null | grep -q uchg; then - rm -rf "$BEEPER_UPDATE_PATH" - mkdir -p "$BEEPER_UPDATE_PATH" - /usr/bin/chflags uchg "$BEEPER_UPDATE_PATH" - fi - - # 2. Block the Electron 'ShipIt' directory (where update staging happens) - if ! /usr/bin/stat -f "%Sf" "$BEEPER_SHIPIT_PATH" 2> /dev/null | grep -q uchg; then - rm -rf "$BEEPER_SHIPIT_PATH" - mkdir -p "$BEEPER_SHIPIT_PATH" - /usr/bin/chflags uchg "$BEEPER_SHIPIT_PATH" - fi - ''; }; - }; } diff --git a/modules/apps/brave.nix b/modules/apps/brave.nix new file mode 100644 index 00000000..f06b9b1a --- /dev/null +++ b/modules/apps/brave.nix @@ -0,0 +1,693 @@ +{ lib, ... }: +let + # ── Pure helpers (shared by HM + Darwin modules) ───────────────────── + # Converts a 2-char lowercase hex string to an integer (0-255). + hexDigits = { + "0" = 0; + "1" = 1; + "2" = 2; + "3" = 3; + "4" = 4; + "5" = 5; + "6" = 6; + "7" = 7; + "8" = 8; + "9" = 9; + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + }; + + hexToDec = + hex: + let + chars = lib.stringToCharacters (lib.toLower hex); + in + builtins.foldl' (acc: ch: acc * 16 + hexDigits.${ch}) 0 chars; + + hexToRgb = hex: [ + (hexToDec (builtins.substring 0 2 hex)) + (hexToDec (builtins.substring 2 2 hex)) + (hexToDec (builtins.substring 4 2 hex)) + ]; + + # Build a Chrome/Brave theme manifest.json with all base16 color slots. + # `c` is a base16 palette: { base00 = "rrggbb"; ...; base0F = "rrggbb"; } + # (lower-case hex without leading '#'). + # + # `version` is a Chromium-valid version string ("X.Y[.Z[.W]]" with each + # component 0–65535). Derive it from the palette hash via + # `mkBraveThemeVersion` so each variant's CRX has a distinct version and + # Brave's External Extensions loader installs/swaps it cleanly. + mkBraveManifest = + themeName: version: c: + builtins.toJSON { + manifest_version = 3; + name = "Stylix ${themeName}"; + version = version; + theme.colors = { + frame = hexToRgb c.base00; # titlebar active + frame_inactive = hexToRgb c.base00; # titlebar inactive + toolbar = hexToRgb c.base01; # tab strip + omnibox bg + tab_text = hexToRgb c.base06; # active tab text + tab_background_text = hexToRgb c.base04; # inactive tab text + background_tab_text = hexToRgb c.base04; # background tab text + bookmark_text = hexToRgb c.base05; # bookmark bar text + ntp_background = hexToRgb c.base00; # new tab page background + ntp_text = hexToRgb c.base05; # new tab page text + ntp_link = hexToRgb c.base0D; # new tab page links (accent) + ntp_section = hexToRgb c.base01; # NTP card/section bg + ntp_section_text = hexToRgb c.base05; # NTP card text + ntp_section_link = hexToRgb c.base0D; # NTP card links + omnibox_background = hexToRgb c.base01; # address bar background + omnibox_text = hexToRgb c.base05; # address bar text + button_background = hexToRgb c.base01; # toolbar button bg + control_background = hexToRgb c.base01; # general control bg + }; + }; + + # ── Stable constants ─────────────────────────────────────────────── + braveDefaultUpdateUrl = "https://clients2.google.com/service/update2/crx"; + + # Legacy Chromium extension IDs the activation cleanup loop wipes so + # rebuilds from older revisions of this module don't leave dead + # CRX/External Extensions artifacts behind. These come from previous + # delivery paths (CRX-signed theme key rotations, the abandoned + # `--load-extension=$HOME/.brave-stylix-theme` flow whose ID was + # derived from the $HOME path, etc.). The current delivery path is + # `--load-extension=` which derives a fresh, + # path-based ID per Stylix palette, so no current ID needs to be + # pinned here. + braveLegacyThemeExtIds = [ + "hogbnhnlblglmhabmimehnofpnafnmle" + "aepmofgifbmpldjgfgojkeiedalilblg" + "ddkjecaebecekiijgeokobnjphlglake" + "mnjggpindoocnndabpppocagnlbhbggn" + "eimadpbcbfnmbkpkfnekohlhhenbhjje" + # Last CRX-signed theme ID (pre-cleanup). Held here so a generation + # rolled forward from that revision still scrubs its on-disk + # artifacts; safe to remove once no rollback target references it. + "lnemcieeceadiagiepafbahmiojdckln" + ]; + + # Derive a Chromium-valid 4-component version string ("1.A.B.C" with each + # of A/B/C in [0, 65535]) from a content hash so light/dark variants + # produce distinct, deterministic CRX versions. Brave's External + # Extensions loader installs whichever version the JSON points at, + # regardless of whether it's higher or lower than the previously + # installed version (the file system is implicitly trusted). + mkBraveThemeVersion = + content: + let + h = builtins.hashString "sha256" content; + a = hexToDec (builtins.substring 0 4 h); + b = hexToDec (builtins.substring 4 4 h); + c = hexToDec (builtins.substring 8 4 h); + in + "1.${toString a}.${toString b}.${toString c}"; + + # Force-installed Brave extensions (uBlock Origin Lite, YT Windowed + # Fullscreen, SponsorBlock, Dark Reader, Momentum). These are pulled from + # the Chrome Web Store via `update_url` policy entries. + braveExtensions = [ + { + id = "ddkjiahejlhfcafbddmgiahcphecmpfh"; + updateUrl = braveDefaultUpdateUrl; + } # uBlock Origin Lite + { + id = "gkkmiofalnjagdcjheckamobghglpdpm"; + updateUrl = braveDefaultUpdateUrl; + } # YouTube Windowed Fullscreen + { + id = "mnjggcdmjocbbbhaepdhchncahnbgone"; + updateUrl = braveDefaultUpdateUrl; + } # SponsorBlock + { + id = "eimadpbcbfnmbkopoojfekhnkhdbieeh"; + updateUrl = braveDefaultUpdateUrl; + } # Dark Reader + { + id = "laookkfknpbbblfpciffpaejjkokdgca"; + updateUrl = braveDefaultUpdateUrl; + } # Momentum + ]; + + braveForcelist = map (ext: "${ext.id};${ext.updateUrl or braveDefaultUpdateUrl}") braveExtensions; + braveExtensionIds = map (ext: ext.id) braveExtensions; + + # When ExtensionSettings is present, ALL other extension policies + # (ExtensionInstallAllowlist, ExtensionInstallBlocklist, etc.) are + # superseded. Extensions NOT listed here would be blocked by Brave's + # default managed-mode behavior. The "*" wildcard with "allowed" sets + # the permissive default for unlisted extensions. + braveExtensionSettings = { + "*" = { + installation_mode = "allowed"; + }; + } + // builtins.listToAttrs ( + map (ext: { + name = ext.id; + value = { + installation_mode = "force_installed"; + update_url = ext.updateUrl or braveDefaultUpdateUrl; + }; + }) braveExtensions + ); + + # Convert a Stylix palette (config.lib.stylix.colors.withHashtag) to the + # lowercase-hex-without-hash format expected by `mkBraveManifest`. + paletteFromStylix = + stylixColors: lib.mapAttrs (_: value: lib.toLower (lib.removePrefix "#" value)) stylixColors; + + # ── Theme delivery rationale ──────────────────────────────────────── + # We *don't* write the theme directly into Brave's `Preferences` or + # `Secure Preferences`: those files are HMAC-tracked, and any external + # write Brave didn't make itself triggers the pref-tamper reset dialog + # ("Brave reset these settings — Extension(s)") on every launch. + # + # We *don't* use `--load-extension=` either: Chromium 137+ + # only honors that flag when Developer Mode is on in brave://extensions, + # and the corresponding enterprise policy (ExtensionDeveloperModeSettings) + # only supports Allow (0) and Disallow (1) — there is no "force-enable" + # value, so it can't be made fully declarative. + # + # Instead we pack the Stylix manifest into a signed CRX3 archive in the + # Nix store and register it as a per-profile *external extension* (drop + # `.json` with `external_crx` into the profile's `External Extensions/` + # directory). Brave treats it as a regularly-installed extension — no + # Developer Mode banner, no policy gymnastics, no manual toggling. + + # ── Home Manager Brave module (let-bound; mirrored to flake-parts + den) ── + braveHmModule = + { + pkgs, + lib, + config, + ... + }: + let + themePalette = paletteFromStylix config.lib.stylix.colors.withHashtag; + + # Version derived from the variant's palette so each light/dark flip + # produces a distinct CRX/manifest version. Brave installs whichever + # the External Extensions JSON points at — no upgrade gating. + braveThemeVersion = mkBraveThemeVersion (builtins.toJSON themePalette); + + # Manifest content is built at eval time (it has no secrets — just + # palette colors). Only the *signing* step needs the decrypted PEM, + # which happens at activation time below. + braveThemeManifestJson = mkBraveManifest config.dendritic.theme.name braveThemeVersion themePalette; + braveThemeManifestFile = pkgs.writeText "manifest.json" braveThemeManifestJson; + + # Source tree handed to the CRX packer: just a manifest.json today, + # but the layout supports adding images/icons later without touching + # the packer. + braveThemeSourceDir = pkgs.runCommand "brave-stylix-theme-src" { } '' + mkdir -p $out + cp ${braveThemeManifestFile} $out/manifest.json + ''; + + # ── Stylix theme delivery (declarative, via --load-extension) ─── + # We deliver the theme as an UNPACKED extension loaded by Brave at + # launch via `--load-extension=`. This gives the extension + # Chromium's `Location::COMMAND_LINE`, which: + # + # 1. Bypasses the side-load disable gate that External + # Extensions JSON (`Location::EXTERNAL_PREF`) hits — i.e. + # `disable_reasons: [8192]` (`DISABLE_EXTERNAL_EXTENSION`) + # and the "Brave reset these settings — Extension(s)" alert. + # 2. Bypasses Chromium's `force_installed` + `update_url` + # policy path's silent rejection of `file://` URLs (the + # policy fetcher only honors `http://` / `https://`, so any + # attempt to point at a local CRX is dead on arrival). + # 3. Doesn't require Chromium-internal signing keys, sops-managed + # PEMs, or local HTTP servers serving a gupdate XML manifest. + # + # `braveThemeSourceDir` is a content-addressed Nix derivation + # whose path changes when the Stylix palette (light vs. dark) + # changes; the Mach-O stub below bakes that exact path into the + # binary at build time. The appearance system prebuilds dark/light + # variants, each with their own wrapper baked at their respective + # palette, so an appearance flip = swap-prebuilt + restart Brave + # = new theme dir loaded. + braveLoadExtensionArg = "--load-extension=${braveThemeSourceDir}"; + + # Single source of truth for Brave command-line args. Used by: + # 1. `programs.brave.commandLineArgs` — works on Linux (HM's + # override fires for the source-built brave package). + # 2. `braveWrapperBin` below — bakes the same args into the + # Mach-O stub on Darwin, because nixpkgs Brave on Darwin + # silently no-ops `pkgs.brave.override { commandLineArgs = ...; }` + # (the prebuilt .app derivation accepts but discards the + # override argument). + braveCommandLineArgs = [ + # Suppress browser update polling/scheduling. + "--check-for-update-interval=31536000" + "--disable-updater-scheduler" + # Stylix theme delivery (see braveLoadExtensionArg comment above). + braveLoadExtensionArg + ]; + + # Compiled Mach-O stub for the Brave wrapper .app. + # macOS 16+ requires the main executable to be a Mach-O binary; + # shell scripts cause kLSNotAnApplicationErr (-10669) in Launch Services. + # + # The stub execs the HM-wrapped Brave binary and INJECTS the args + # from `braveCommandLineArgs` ahead of any user-passed args, since + # the upstream nixpkgs Darwin Brave wrapper drops them. + braveWrapperBin = + pkgs.runCommand "brave-wrapper-bin" + { + nativeBuildInputs = [ pkgs.stdenv.cc ]; + } + '' + cat > stub.c << 'EOF' + #include + #include + #include + #include + + static const char *extra_args[] = { + ${lib.concatMapStringsSep "\n " (a: ''"${a}",'') braveCommandLineArgs} + }; + static const int n_extra = (int)(sizeof(extra_args) / sizeof(extra_args[0])); + + int main(int argc, char *argv[]) { + const char *user = getenv("USER"); + if (!user) user = "unknown"; + + /* Exec the HM-wrapped Brave binary; nixpkgs's Darwin wrapper + * doesn't honor the HM `commandLineArgs` override, so we + * splice them in here ourselves. */ + char brave[512]; + snprintf(brave, sizeof(brave), + "/etc/profiles/per-user/%s/bin/brave", user); + + char **new_argv = malloc((argc + n_extra + 1) * sizeof(char *)); + if (!new_argv) return 1; + int j = 0; + new_argv[j++] = brave; + for (int i = 0; i < n_extra; i++) new_argv[j++] = (char *)extra_args[i]; + for (int i = 1; i < argc; i++) new_argv[j++] = argv[i]; + new_argv[j] = NULL; + + execv(brave, new_argv); + perror("execv brave"); + return 1; + } + EOF + $CC -o "$out" stub.c -O2 + ''; + + bravePolicyJson = builtins.toJSON { + AutoUpdateCheckPeriodMinutes = 0; + DisableAutoUpdate = true; + ComponentUpdatesEnabled = false; + # NOTE on dev mode: Chromium's `ExtensionDeveloperModeSettings` policy + # has only two values — `0 = Allow` (user CAN toggle dev mode) and + # `1 = Disallow` (user CANNOT toggle dev mode, forced off). There is + # NO "force enable" value (deliberately omitted from Chromium so an + # enterprise admin can't silently force-sideload on a workstation). + # We previously set this to 1 thinking it meant "force enable" — that + # actively DISABLED dev mode and broke `--load-extension=` loading. + # Omitting the key leaves dev mode at its default (Allow, user-toggleable). + ExtensionInstallForcelist = braveForcelist; + # ── New Tab Page footer suppression ────────────────────────── + # Chromium's NTP footer renders when EITHER `managed_ntp` is true + # (browser is managed AND `NTPFooterManagementNoticeEnabled` allows + # showing the "managed by your organization" badge) OR when the + # user pref `NewTabPage.FooterVisible` is true AND an extension is + # eligible to show attribution there. Setting both Chromium policies + # to false makes neither arm fire — the footer is fully suppressed + # without touching the (HMAC-shaped) user pref. The user-facing + # "Show footer on New Tab page" toggle becomes a no-op. + NTPFooterManagementNoticeEnabled = false; + NTPFooterExtensionAttributionEnabled = false; + # The Stylix theme is delivered via `--load-extension=` in + # the wrapper's command-line args (see `braveLoadExtensionArg`), + # not via policy — so the theme's ID does NOT appear here. The + # policy entries below are for the upstream-store extensions + # only. + ExtensionSettings = braveExtensionSettings; + }; + + in + { + options.dendritic.apps.brave = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable declarative Brave Browser with Stylix-driven theming."; + }; + }; + + config = lib.mkIf config.dendritic.apps.brave.enable { + programs.brave = { + enable = true; + # Declarative source of truth for Brave extensions. + extensions = braveExtensions; + # Note: on Darwin this is silently dropped by nixpkgs's prebuilt + # Brave wrapper; the same args are injected by `braveWrapperBin` + # via the Mach-O stub. Kept here so Linux (where the override + # DOES fire) and tooling that inspects HM config still see them. + commandLineArgs = braveCommandLineArgs; + }; + + # ── Brave wrapper .app (Darwin only) ──────────────────────────── + # Provides a separate bundle ID so the HM-managed bundle and the + # Nix-store bundle don't collide in Launch Services. + # + # Keychain compatibility note: macOS attributes Keychain ACL + # ownership to the *responsible process* identity, which for an + # `execv`-style wrapper stays bound to the wrapper's code + # signature (not the post-exec target binary). The original + # Brave (from nixpkgs and the Brave installer alike) is signed + # with `Identifier=Brave Browser`, and any existing "Brave Safe + # Storage" keychain entry's ACL accepts that designated + # requirement. We therefore force the wrapper's *signing* + # identifier to match — `--identifier "Brave Browser"` — + # regardless of CFBundleIdentifier, which can stay distinct for + # Launch Services. Without this, every flake switch re-signs + # the wrapper with a fresh ad-hoc CDHash and a non-Brave + # identifier, breaking Brave's keychain access and triggering + # the "Keychain Not Found … Reset To Defaults" dialog (which + # offers to wipe the user's entire login keychain — never click + # it). + home.activation.createBraveWrapperApp = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + BRAVE_REAL="${pkgs.brave}/Applications/Brave Browser.app" + BRAVE_WRAP="$HOME/Applications/Brave Browser.app" + + rm -rf "$BRAVE_WRAP" + mkdir -p "$BRAVE_WRAP/Contents/MacOS" + + cat > "$BRAVE_WRAP/Contents/Info.plist" << 'PLIST' + + + + + CFBundleExecutable Brave Browser + CFBundleIconFile app.icns + CFBundleIdentifier org.dendritic.BraveWrapper + CFBundleName Brave Browser + CFBundlePackageType APPL + CFBundleShortVersionString 1.0 + CFBundleVersion 1 + NSHighResolutionCapable + + + PLIST + + ln -sfn "$BRAVE_REAL/Contents/Resources" "$BRAVE_WRAP/Contents/Resources" + cp "${braveWrapperBin}" "$BRAVE_WRAP/Contents/MacOS/Brave Browser" + chmod +x "$BRAVE_WRAP/Contents/MacOS/Brave Browser" + + /usr/bin/codesign \ + --force \ + --sign - \ + --identifier "Brave Browser" \ + "$BRAVE_WRAP" 2>/dev/null || true + /usr/bin/xattr -rd com.apple.quarantine "$BRAVE_WRAP" 2>/dev/null || true + '' + ); + + # ── Brave keychain ACL repair (Darwin only) ────────────────────── + # Belt-and-suspenders to keep the "Brave Safe Storage" entry + # readable across wrapper rebuilds. macOS partitions keychain + # items by code-signing CDHash; ad-hoc signatures get a fresh + # CDHash on every codesign invocation. Even with a stable + # signing *identifier* (above), some macOS versions still gate + # on partition list. So if — and only if — the entry exists + # and is NOT currently readable by this activation context, we + # widen its partition list to accept "any apple-tool, any + # apple-signed, any unsigned, any team-id'd app". + # + # The conditional avoids the GUI keychain password prompt on + # every flake switch: when access already works, we touch + # nothing. On the one switch after a fresh re-sign breaks + # access, macOS will prompt for the login keychain password + # exactly once to authorize the widening. + home.activation.repairBraveKeychainAcl = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter + [ + "writeBoundary" + "createBraveWrapperApp" + ] + '' + if /usr/bin/security find-generic-password -s 'Brave Safe Storage' >/dev/null 2>&1; then + if ! /usr/bin/security find-generic-password -s 'Brave Safe Storage' -w >/dev/null 2>&1; then + /usr/bin/security set-generic-password-partition-list \ + -s 'Brave Safe Storage' \ + -S 'apple-tool:,apple:,unsigned:,teamid:' \ + -k login.keychain-db \ + >/dev/null 2>&1 || true + fi + fi + '' + ); + + home.activation.disableBraveUpdates = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + /usr/bin/defaults write com.brave.Browser AutoUpdate -bool false + /usr/bin/defaults write com.brave.Browser AutoUpdateCheckPeriodMinutes -int 0 + /usr/bin/defaults write com.brave.Browser DisableAutoUpdate -bool true + # Brave on macOS still uses Sparkle updater prefs for update checks/prompts. + /usr/bin/defaults write com.brave.Browser SUEnableAutomaticChecks -bool false + /usr/bin/defaults write com.brave.Browser SUAutomaticallyUpdate -bool false + /usr/bin/defaults write com.brave.Browser SUScheduledCheckInterval -int 31536000 + '' + ); + + # Pre-clean stale managed paths so HM doesn't backup-clobber on writes. + home.activation.prepareBraveManagedPaths = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryBefore [ "checkLinkTargets" ] '' + BRAVE_ROOT="$HOME/Library/Application Support/BraveSoftware/Brave-Browser" + rm -f \ + "$BRAVE_ROOT/Managed Policies/policies.json" \ + "$BRAVE_ROOT/Managed Policies/policies.json.backup" \ + "$BRAVE_ROOT/Policies/Managed/policies.json" \ + "$BRAVE_ROOT/Policies/Managed/policies.json.backup" + + # Wipe External Extensions JSONs for all force-installed CWS + # extensions — these are written by the activation script + # below, not home.file, so they need explicit cleanup before + # HM's link-target check runs. + for EXT_ID in ${lib.concatStringsSep " " braveExtensionIds}; do + rm -f \ + "$BRAVE_ROOT/External Extensions/$EXT_ID.json" \ + "$BRAVE_ROOT/External Extensions/$EXT_ID.json.backup" + done + + # Wipe legacy theme extension IDs (see `braveLegacyThemeExtIds` + # at the top of this file for context). + for EXT_ID in ${lib.concatStringsSep " " braveLegacyThemeExtIds}; do + rm -f \ + "$BRAVE_ROOT/External Extensions/$EXT_ID.json" \ + "$BRAVE_ROOT/External Extensions/$EXT_ID.json.backup" + rm -rf "$BRAVE_ROOT/Default/Extensions/$EXT_ID" + done + + # Legacy unpacked-theme directory from the abandoned + # `--load-extension=$HOME/.brave-stylix-theme` flow. Current + # delivery loads `braveThemeSourceDir` (a /nix/store path) + # instead. Safe to nuke unconditionally. + rm -rf "$HOME/.brave-stylix-theme" + '' + ); + + # ── Materialize Brave policy + force-installed extensions ───── + # Stylix theme delivery has moved to `--load-extension` baked + # into the Mach-O wrapper (see `braveLoadExtensionArg`), so we + # no longer pack a CRX, write a gupdate XML, or place an + # External Extensions JSON here. This activation block now only + # writes the upstream-store ExtensionInstallForcelist policy + # and clears stale theme-side-load artifacts on disk. + home.activation.materializeBraveStylixTheme = + lib.hm.dag.entryAfter + [ + "writeBoundary" + ] + '' + BRAVE_ROOT="${ + if pkgs.stdenv.isDarwin then + "$HOME/Library/Application Support/BraveSoftware/Brave-Browser" + else + "$HOME/.config/BraveSoftware/Brave-Browser" + }" + mkdir -p "$BRAVE_ROOT/Default" "$BRAVE_ROOT/External Extensions" + + # Clear any side-loaded artifacts from prior CRX-based + # iterations of this module. The theme is now loaded via + # --load-extension; Brave doesn't need a CRX, an External + # Extensions JSON, or a gupdate XML on disk. The legacy + # ID list `braveLegacyThemeExtIds` handles the per-ID + # `External Extensions/*.json` + `Default/Extensions/*` + # paths (those run in prepareBraveManagedPaths above); + # here we just nuke the activation-time CRX output dir. + rm -rf "${config.xdg.dataHome}/brave-stylix" + + ${lib.optionalString pkgs.stdenv.isDarwin '' + POLICY_MAIN="$BRAVE_ROOT/Managed Policies/policies.json" + POLICY_ALT="$BRAVE_ROOT/Policies/Managed/policies.json" + mkdir -p "$BRAVE_ROOT/Managed Policies" "$BRAVE_ROOT/Policies/Managed" + rm -f "$POLICY_MAIN" "$POLICY_ALT" + printf '%s\n' '${bravePolicyJson}' > "$POLICY_MAIN" + cp "$POLICY_MAIN" "$POLICY_ALT" + chmod 0644 "$POLICY_MAIN" "$POLICY_ALT" || true + + ${lib.concatMapStringsSep "\n" (ext: '' + rm -f "$BRAVE_ROOT/External Extensions/${ext.id}.json" + printf '%s\n' '{"external_update_url":"${ + ext.updateUrl or braveDefaultUpdateUrl + }"}' > "$BRAVE_ROOT/External Extensions/${ext.id}.json" + chmod 0644 "$BRAVE_ROOT/External Extensions/${ext.id}.json" || true + '') braveExtensions} + ''} + ''; + }; + }; + + # ── nix-darwin Brave module (let-bound; mirrored to flake-parts + den) ── + braveDarwinModule = + { + pkgs, + lib, + config, + ... + }: + let + user = config.system.primaryUser; + + systemPolicyJson = builtins.toJSON { + AutoUpdateCheckPeriodMinutes = 0; + DisableAutoUpdate = true; + ComponentUpdatesEnabled = false; + # NOTE on dev mode: Chromium's `ExtensionDeveloperModeSettings` + # policy has only Allow=0 / Disallow=1 (no "force enable"). The + # Stylix theme is loaded via `--load-extension` (see + # `braveLoadExtensionArg`), which is honored at Brave startup + # regardless of dev-mode UI state, so we leave this key omitted + # (default: user-togglable from `brave://extensions/`). + ExtensionInstallForcelist = braveForcelist; + # See note in bravePolicyJson: these two Chromium policies fully + # suppress the NTP footer (both the "managed by org" badge and + # extension attribution), making the user-facing "Show footer on + # New Tab page" toggle a no-op. + NTPFooterManagementNoticeEnabled = false; + NTPFooterExtensionAttributionEnabled = false; + # Mirror of `bravePolicyJson`: theme is delivered via + # --load-extension, not policy. Theme ID intentionally absent + # from ExtensionSettings. + ExtensionSettings = braveExtensionSettings; + }; + + # Variant hot-reload script invoked by darwin-appearance-sync after a + # light/dark flip. HM has just rewritten the External Extensions JSON + # to point at the new variant's CRX (different version, different + # store path). Brave reads External Extensions only at startup, so to + # see the new theme it has to be restarted — quit + relaunch is all + # this script does. No Preferences mutation, no extension cache + # tampering. + braveStylixReload = pkgs.writeShellApplication { + name = "brave-stylix-reload"; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + set -u + target_user="${user}" + uid="$(id -u "$target_user")" + home_dir="/Users/$target_user" + wrapper_app="$home_dir/Applications/Brave Browser.app" + + # Quit Brave so it re-reads External Extensions on next launch. + brave_was_running=0 + if /usr/bin/pgrep -x "Brave Browser" >/dev/null 2>&1; then + brave_was_running=1 + /bin/launchctl asuser "$uid" /usr/bin/sudo -u "$target_user" \ + /usr/bin/osascript -e 'tell application "Brave Browser" to quit' >/dev/null 2>&1 || true + i=0 + while /usr/bin/pgrep -x "Brave Browser" >/dev/null 2>&1 && [ "$i" -lt 20 ]; do + /bin/sleep 0.25 + i=$((i + 1)) + done + fi + + if [ "$brave_was_running" -eq 1 ]; then + /bin/sleep 0.4 + if [ -d "$wrapper_app" ]; then + /bin/launchctl asuser "$uid" /usr/bin/sudo -u "$target_user" \ + /usr/bin/open "$wrapper_app" >/dev/null 2>&1 || true + else + /bin/launchctl asuser "$uid" /usr/bin/sudo -u "$target_user" \ + /usr/bin/open -a "Brave Browser" >/dev/null 2>&1 || true + fi + fi + ''; + }; + in + { + options.dendritic.brave = { + reloadScript = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = "Variant-aware Brave reload script consumed by darwin-appearance-sync."; + }; + }; + + config = { + dendritic.brave.reloadScript = braveStylixReload; + + # System-level Managed Preferences plist + machine policy JSON so + # Brave shows "Managed by your organization" and accepts the unpacked + # Stylix theme without user prompts. + system.activationScripts.postActivation.text = lib.mkAfter '' + /bin/mkdir -p "/Library/Managed Preferences" + /bin/mkdir -p "/Library/Application Support/BraveSoftware/Brave-Browser/Policies/Managed" + /bin/mkdir -p "/Library/Application Support/BraveSoftware/Brave-Browser/External Extensions" + ${pkgs.python3}/bin/python3 - <<'PY' + import json + import plistlib + from pathlib import Path + + policy_path = Path("/Library/Managed Preferences/com.brave.Browser.plist") + policy_json_path = Path("/Library/Application Support/BraveSoftware/Brave-Browser/Policies/Managed/policies.json") + policy = json.loads("""${systemPolicyJson}""") + + with policy_path.open("wb") as f: + plistlib.dump(policy, f, sort_keys=True) + policy_json_path.write_text(json.dumps(policy, separators=(",", ":"))) + PY + /usr/sbin/chown root:wheel "/Library/Managed Preferences/com.brave.Browser.plist" || true + /bin/chmod 0644 "/Library/Managed Preferences/com.brave.Browser.plist" || true + /usr/sbin/chown root:wheel "/Library/Application Support/BraveSoftware/Brave-Browser/Policies/Managed/policies.json" || true + /bin/chmod 0644 "/Library/Application Support/BraveSoftware/Brave-Browser/Policies/Managed/policies.json" || true + ''; + + # Dock registration: Brave owns its dock entry (order 110 in `dock.nix`). + dendritic.dock.apps = lib.mkOrder 110 [ + "/Users/${user}/Applications/Brave Browser.app" + ]; + }; + }; +in +{ + # ── Flake-parts dendritic exports (consumed by embedded HM + Darwin users) ── + # The four darwin/nixos host files import `inputs.self.modules..dendritic` + # which merges every contribution to `flake.modules..dendritic`. The + # brave modules below get picked up via that path. + flake.modules.homeManager.dendritic = braveHmModule; + flake.modules.darwin.dendritic = braveDarwinModule; + + # ── Den aspect (future-proofing for den.homes / aspect includes) ──────── + # Same modules exposed as a named aspect so any future `den.homes.*` + # consumer or host aspect can `includes = [ config.den.aspects.brave ]` + # to pick up brave declaratively without going through the dendritic + # monolith. + den.aspects.brave = { + homeManager = braveHmModule; + darwin = braveDarwinModule; + }; +} diff --git a/modules/apps/chatgpt-cli.nix b/modules/apps/chatgpt-cli.nix new file mode 100644 index 00000000..d0acd289 --- /dev/null +++ b/modules/apps/chatgpt-cli.nix @@ -0,0 +1,38 @@ +{ + # `j178/chatgpt` (nixpkgs: `chatgpt-cli`) is a TUI ChatGPT client. It + # reads its OpenAI key from either an `OPENAI_API_KEY` env var or + # `~/.config/chatgpt/config.json`. We refuse to symlink the + # plaintext sops secret into a public config path, and we refuse to + # export `OPENAI_API_KEY` globally (every spawned process would + # inherit it). Instead we ship a thin `writeShellScriptBin` wrapper + # that lifts the key from the sops-nix runtime path *only* for the + # `chatgpt` process, then `exec`s the upstream binary. + # + # The secret is declared here so this module is self-contained — + # `sops.secrets.openai_api_key = { }` is set-merged across every + # consumer (`modules/editor.nix` declares the same value for the + # Nixvim/CodeCompanion adapter), so nothing breaks if one of the + # consumers is later removed. + flake.modules.homeManager.dendritic = + { pkgs, config, ... }: + let + keyPath = config.sops.secrets.openai_api_key.path; + chatgpt = pkgs.writeShellScriptBin "chatgpt" '' + # If the sops secret hasn't been materialised yet (e.g. the + # user is mid-bootstrap or the sops-nix launchd agent failed), + # fall through with whatever `OPENAI_API_KEY` is already in + # the environment — j178/chatgpt will surface its own + # "unauthorized" error, which is a clearer diagnostic than + # `cat: : No such file or directory` would be. + if [ -r "${keyPath}" ]; then + OPENAI_API_KEY="$(cat "${keyPath}")" + export OPENAI_API_KEY + fi + exec ${pkgs.chatgpt-cli}/bin/chatgpt "$@" + ''; + in + { + sops.secrets.openai_api_key = { }; + home.packages = [ chatgpt ]; + }; +} diff --git a/modules/apps/common.nix b/modules/apps/common.nix deleted file mode 100644 index 461c733d..00000000 --- a/modules/apps/common.nix +++ /dev/null @@ -1,125 +0,0 @@ -{ - flake.modules.homeManager.apps = - { - pkgs, - inputs, - config, - lib, - ... - }: - { - - - programs.brave = { - enable = true; - extensions = [ - { id = "ddkjecaebecekiijgeokobnjphlglake"; } # uBlock Origin Lite - { id = "gkkmiofalnjagdcjheckamobghglpdpm"; } # YouTube Windowed Fullscreen - { id = "mnjggpindoocnndabpppocagnlbhbggn"; } # SponsorBlock - { id = "eimadpbcbfnmbkpkfnekohlhhenbhjje"; } # Dark Reader - ]; - }; - programs.firefox = { - enable = true; - package = pkgs.firefox-bin; - - profiles.default = { - id = 0; - name = "default"; - settings = { - "toolkit.legacyUserProfileCustomizations.stylesheets" = true; - "browser.tabs.drawInTitlebar" = true; - "svg.context-properties.content.enabled" = true; - - # Disable Telemetry - "datareporting.healthreport.uploadEnabled" = false; - "datareporting.policy.dataSubmissionEnabled" = false; - "toolkit.telemetry.enabled" = false; - "toolkit.telemetry.unified" = false; - "toolkit.telemetry.server" = "data:,"; - "toolkit.telemetry.archive.enabled" = false; - "toolkit.telemetry.newProfilePing.enabled" = false; - "toolkit.telemetry.shutdownPingSender.enabled" = false; - "toolkit.telemetry.updatePing.enabled" = false; - "toolkit.telemetry.bhrPing.enabled" = false; - "toolkit.telemetry.firstShutdownPing.enabled" = false; - "toolkit.telemetry.coverage.opt-out" = true; - "toolkit.coverage.opt-out" = true; - "toolkit.coverage.endpoint.base" = ""; - "browser.ping-centre.telemetry" = false; - "browser.newtabpage.activity-stream.feeds.telemetry" = false; - "browser.newtabpage.activity-stream.telemetry" = false; - }; - }; - policies = { - DisableTelemetry = true; - DisableFirefoxStudies = true; - DisablePocket = true; - ExtensionSettings = { - "uBlock0@raymondhill.net" = { - installation_mode = "normal_installed"; - install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"; - }; - "youtube-windowed-fullscreen@navi-jador" = { - installation_mode = "normal_installed"; - install_url = "https://addons.mozilla.org/firefox/downloads/latest/youtube-windowed-fullscreen/latest.xpi"; - }; - "sponsorBlocker@ajay.app" = { - installation_mode = "normal_installed"; - install_url = "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi"; - }; - "addon@darkreader.org" = { - installation_mode = "normal_installed"; - install_url = "https://addons.mozilla.org/firefox/downloads/latest/darkreader/latest.xpi"; - }; - }; - }; - }; - - - - home.activation.signApps = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if [ -d "$HOME/Applications/Home Manager Apps/Firefox.app" ]; then - /usr/bin/codesign --force --deep --sign - "$HOME/Applications/Home Manager Apps/Firefox.app" - fi - if [ -d "$HOME/Applications/Home Manager Apps/Vesktop.app" ]; then - /usr/bin/codesign --force --deep --sign - "$HOME/Applications/Home Manager Apps/Vesktop.app" - fi - ''; - - home.packages = - with pkgs; - [ - # Dev tools - gh # GitHub CLI - ghidra # Reverse engineering - jdk21 # Java development - # System - fastfetch # System info - ] - ++ lib.optionals (config.dendritic.apps.jetbrains.enable or false) [ - # IDEs - jetbrains.clion - jetbrains.idea - jetbrains.rust-rover - ]; - }; - - # Dock registration: Brave owns its dock entry - flake.modules.darwin.apps = - { pkgs, inputs, ... }: - { - dendritic.dock.apps = [ - "/System/Cryptexes/App/System/Applications/Safari.app" - "${pkgs.firefox-bin}/Applications/Firefox.app" - "${pkgs.brave}/Applications/Brave Browser.app" - "${inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}.vesktop}/Applications/Vesktop.app" - "${pkgs.ghostty-bin}/Applications/Ghostty.app" - "${pkgs.jetbrains.idea}/Applications/IntelliJ IDEA.app" - "${pkgs.jetbrains.clion}/Applications/CLion.app" - "${pkgs.jetbrains.rust-rover}/Applications/RustRover.app" - "${pkgs.code-cursor}/Applications/Cursor.app" - "${pkgs.antigravity}/Applications/Antigravity.app" - ]; - }; -} diff --git a/modules/apps/cursor.nix b/modules/apps/cursor.nix index 7ccbbd71..44f27b9f 100644 --- a/modules/apps/cursor.nix +++ b/modules/apps/cursor.nix @@ -1,49 +1,80 @@ { - flake.modules.homeManager.cursor = { pkgs, lib, config, ... }: let - cfg = config.dendritic.apps.cursor; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.apps.cursor; - # ── Derive settings & extensions from Antigravity (1:1) ────── - # Antigravity uses programs.vscode, which Stylix auto-themes. - # Cursor reuses the exact same resolved settings + extensions - # so both editors are always visually identical. - sharedSettings = config.programs.vscode.profiles.default.userSettings; - sharedExtensions = config.programs.vscode.profiles.default.extensions; + # ── Derive settings & extensions from Antigravity (1:1) ────── + # Antigravity uses programs.vscode, which Stylix auto-themes. + # Cursor reuses the exact same resolved settings + extensions + # so both editors are always visually identical. + sharedSettings = config.programs.vscode.profiles.default.userSettings; + sharedExtensions = config.programs.vscode.profiles.default.extensions; - # Build extension symlinks by reading each extension's directory - extensionFiles = lib.foldl' (acc: ext: let - extPath = "${ext}/share/vscode/extensions"; - dirs = builtins.attrNames (builtins.readDir extPath); - in acc // (lib.listToAttrs (map (dir: { - name = ".cursor/extensions/${dir}"; - value = { source = "${extPath}/${dir}"; }; - }) dirs))) {} sharedExtensions; - in { - imports = [ ./_vscode-common.nix ]; - options.dendritic.apps.cursor = { - enable = lib.mkEnableOption "Cursor IDE"; - }; + # Build extension symlinks by reading each extension's directory + extensionFiles = lib.foldl' ( + acc: ext: + let + extPath = "${ext}/share/vscode/extensions"; + dirs = builtins.attrNames (builtins.readDir extPath); + in + acc + // (lib.listToAttrs ( + map (dir: { + name = ".cursor/extensions/${dir}"; + value = { + source = "${extPath}/${dir}"; + }; + }) dirs + )) + ) { } sharedExtensions; + in + { + imports = [ ./_vscode-common.nix ]; + options.dendritic.apps.cursor = { + enable = lib.mkEnableOption "Cursor IDE"; + }; - config = lib.mkIf cfg.enable { - home.packages = [ - (if pkgs.stdenv.isLinux then pkgs.code-cursor-fhs else pkgs.code-cursor) - ]; + config = lib.mkIf cfg.enable { + home.packages = [ + (if pkgs.stdenv.isLinux then pkgs.code-cursor-fhs else pkgs.code-cursor) + ]; - # Link derived extensions (symlinks) - home.file = extensionFiles // lib.optionalAttrs pkgs.stdenv.isDarwin { - "Library/Application Support/Cursor/User/settings.json" = { - force = true; - text = builtins.toJSON sharedSettings; - }; - } // lib.optionalAttrs pkgs.stdenv.isLinux { - ".cursor/User/settings.json".text = builtins.toJSON sharedSettings; + # Link derived extensions (symlinks) + home.file = + extensionFiles + // lib.optionalAttrs pkgs.stdenv.isDarwin { + "Library/Application Support/Cursor/User/settings.json" = { + force = true; + text = builtins.toJSON sharedSettings; + }; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + ".cursor/User/settings.json".text = builtins.toJSON sharedSettings; + }; }; }; - }; - # Dock registration: Cursor owns its dock entry - flake.modules.darwin.cursor = { pkgs, ... }: { - dendritic.dock.apps = [ - "${if pkgs.stdenv.isDarwin then pkgs.code-cursor else pkgs.code-cursor-fhs}/Applications/Cursor.app" - ]; - }; + # Dock registration: Cursor owns its dock entry (order 160 in `dock.nix`). + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + user = config.system.primaryUser; + cursorEnabled = config.home-manager.users.${user}.dendritic.apps.cursor.enable or false; + in + lib.mkIf cursorEnabled { + dendritic.dock.apps = lib.mkOrder 160 [ + "${if pkgs.stdenv.isDarwin then pkgs.code-cursor else pkgs.code-cursor-fhs}/Applications/Cursor.app" + ]; + }; } diff --git a/modules/apps/eclipse-java-google-style.xml b/modules/apps/eclipse-java-google-style.xml deleted file mode 100644 index 7bb6804e..00000000 --- a/modules/apps/eclipse-java-google-style.xml +++ /dev/null @@ -1,337 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/apps/fastfetch.nix b/modules/apps/fastfetch.nix new file mode 100644 index 00000000..99059abd --- /dev/null +++ b/modules/apps/fastfetch.nix @@ -0,0 +1,7 @@ +{ + flake.modules.homeManager.dendritic = + { pkgs, ... }: + { + home.packages = [ pkgs.fastfetch ]; + }; +} diff --git a/modules/apps/firefox.nix b/modules/apps/firefox.nix new file mode 100644 index 00000000..d52ce724 --- /dev/null +++ b/modules/apps/firefox.nix @@ -0,0 +1,245 @@ +{ lib, ... }: +let + # ── Home Manager Firefox module (let-bound; mirrored to flake-parts + den) ── + firefoxHmModule = + { + pkgs, + inputs, + config, + lib, + ... + }: + let + cfg = config.dendritic.apps.firefox; + in + { + options.dendritic.apps.firefox = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable declarative Firefox (HM + Darwin dock + signing)."; + }; + }; + + config = lib.mkIf cfg.enable { + # ── Write Firefox installs.ini to suppress profile chooser ─── + # Firefox can choose different install paths depending on launcher + # resolution (app bundle path, wrapped binary path, resolved exec target). + # We precompute hashes for all known path variants, keep any existing + # install hashes, and map all of them to the declarative profile. + home.activation.setupFirefoxProfile = + let + ffDir = if pkgs.stdenv.isDarwin then "Library/Application Support/Firefox" else ".mozilla/firefox"; + profilePath = "Profiles/default"; + in + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + _ffDir="$HOME/${ffDir}" + _installsIni="$_ffDir/installs.ini" + _hashesFile="$(${pkgs.coreutils}/bin/mktemp)" + mkdir -p "$_ffDir" + + _add_hash_for_path() { + _candidate="$1" + [ -n "$_candidate" ] || return 0 + ${pkgs.python3}/bin/python3 - "$_candidate" >> "$_hashesFile" <<'PY' + import hashlib + import sys + path = sys.argv[1] + print(hashlib.sha256(path.encode("utf-8")).hexdigest()[:16].upper()) + PY + } + + # Keep any existing section names so current hashes remain pinned. + if [ -f "$_installsIni" ]; then + ${pkgs.gawk}/bin/awk ' + /^\[[0-9A-F]{16}\]$/ { + print substr($0, 2, 16) + } + ' "$_installsIni" >> "$_hashesFile" || true + fi + + # Darwin launcher path (the currently observed Firefox hash is tied to + # this launch flow for this setup, so keep it as a compatibility key). + if [ -d "$HOME/Applications/Home Manager Apps/Firefox.app/Contents/MacOS" ]; then + _add_hash_for_path "$HOME/Applications/Home Manager Apps/Firefox.app/Contents/MacOS" + printf '%s\n' "83029BECFDFE6B79" >> "$_hashesFile" + fi + + if command -v firefox >/dev/null 2>&1; then + _ffBin="$(command -v firefox)" + _add_hash_for_path "$(${pkgs.coreutils}/bin/dirname "$_ffBin")" + + _ffBinReal="$(${pkgs.coreutils}/bin/readlink -f "$_ffBin" 2>/dev/null || true)" + if [ -n "$_ffBinReal" ]; then + _add_hash_for_path "$(${pkgs.coreutils}/bin/dirname "$_ffBinReal")" + fi + + # Nix firefox wrappers usually exec a store binary; include that path. + _execTarget="$(${pkgs.gawk}/bin/awk -F'"' '/^exec "/ { print $2; exit }' "$_ffBin" 2>/dev/null || true)" + if [ -n "$_execTarget" ]; then + _add_hash_for_path "$(${pkgs.coreutils}/bin/dirname "$_execTarget")" + fi + fi + + if [ -s "$_hashesFile" ]; then + { + ${pkgs.coreutils}/bin/sort -u "$_hashesFile" \ + | ${pkgs.gawk}/bin/awk ' + { + printf("[%s]\nDefault=${profilePath}\nLocked=1\n\n", $1) + } + ' + } > "$_installsIni" + fi + + rm -f "$_hashesFile" + + # Remove stale lock files left by crashes → prevents "profile inaccessible" + find "$_ffDir/Profiles" -maxdepth 2 \ + \( -name ".parentlock" -o -name "lock" \) \ + -delete 2>/dev/null || true + ''; + + programs.firefox = { + # Avoid forcing Linux Firefox builds from a Darwin builder for the embedded microvm. + # Native Linux/NixOS systems still get the declarative Firefox setup. + enable = + pkgs.stdenv.isDarwin || (pkgs.stdenv.buildPlatform.system == pkgs.stdenv.hostPlatform.system); + package = + if pkgs.stdenv.isDarwin then + pkgs.firefox-bin.overrideAttrs (old: { + src = (inputs.firefox-darwin.overlay pkgs pkgs).firefox-bin.src; + }) + else if pkgs.stdenv.hostPlatform.isAarch64 then + pkgs.firefox + else + pkgs.firefox-bin; + + profiles.default = { + id = 0; + name = "default"; + path = "default"; + isDefault = true; + settings = { + # Firefox updates are managed by nixpkgs only. + "app.update.auto" = false; + "app.update.enabled" = false; + "app.update.service.enabled" = false; + "app.update.silent" = false; + + "toolkit.legacyUserProfileCustomizations.stylesheets" = true; + "browser.tabs.drawInTitlebar" = true; + "svg.context-properties.content.enabled" = true; + + # Disable Telemetry + "datareporting.healthreport.uploadEnabled" = false; + "datareporting.policy.dataSubmissionEnabled" = false; + "toolkit.telemetry.enabled" = false; + "toolkit.telemetry.unified" = false; + "toolkit.telemetry.server" = "data:,"; + "toolkit.telemetry.archive.enabled" = false; + "toolkit.telemetry.newProfilePing.enabled" = false; + "toolkit.telemetry.shutdownPingSender.enabled" = false; + "toolkit.telemetry.updatePing.enabled" = false; + "toolkit.telemetry.bhrPing.enabled" = false; + "toolkit.telemetry.firstShutdownPing.enabled" = false; + "toolkit.telemetry.coverage.opt-out" = true; + "toolkit.coverage.opt-out" = true; + "toolkit.coverage.endpoint.base" = ""; + "browser.ping-centre.telemetry" = false; + "browser.newtabpage.activity-stream.feeds.telemetry" = false; + "browser.newtabpage.activity-stream.telemetry" = false; + }; + }; + policies = { + DisableAppUpdate = true; + DisableTelemetry = true; + DisableFirefoxStudies = true; + DisablePocket = true; + ExtensionSettings = { + "uBlock0@raymondhill.net" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"; + }; + "youtube-windowed-fullscreen@navi-jador" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/youtube-windowed-fullscreen/latest.xpi"; + }; + "sponsorBlocker@ajay.app" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi"; + }; + "addon@darkreader.org" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/darkreader/latest.xpi"; + }; + "momentum@momentumdash.com" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/momentumdash/latest.xpi"; + }; + } + # iCloud Passwords requires the macOS Keychain bridge (Apple's + # native messaging host shipped with macOS Sonoma+), so the + # extension is useful only on Darwin. Force-installed via + # Firefox enterprise policy, the same way every other extension + # in this profile is delivered. + // lib.optionalAttrs pkgs.stdenv.isDarwin { + "password-manager-firefox-extension@apple.com" = { + installation_mode = "force_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/icloud-passwords/latest.xpi"; + }; + }; + }; + }; + + # Codesign the HM-managed Firefox.app bundle on Darwin so Launch + # Services accepts it after `home-manager` regenerates the bundle. + home.activation.signFirefoxApp = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ -d "$HOME/Applications/Home Manager Apps/Firefox.app" ]; then + /usr/bin/codesign --force --deep --sign - "$HOME/Applications/Home Manager Apps/Firefox.app" + fi + '' + ); + }; + }; + + # ── nix-darwin Firefox module (let-bound; mirrored to flake-parts + den) ── + # Pins Firefox.app into the dock at order 100 when the HM toggle is on. + firefoxDarwinModule = + { + inputs, + lib, + pkgs, + config, + ... + }: + let + user = config.system.primaryUser; + firefoxEnabled = config.home-manager.users.${user}.dendritic.apps.firefox.enable or true; + firefox-signed = (inputs.firefox-darwin.overlay pkgs pkgs).firefox-bin; + in + lib.mkIf firefoxEnabled { + dendritic.dock.apps = lib.mkOrder 100 [ + "${firefox-signed}/Applications/Firefox.app" + ]; + }; +in +{ + # ── Flake-parts dendritic exports (consumed by embedded HM + Darwin users) ── + # The darwin/nixos host files import `inputs.self.modules..dendritic` + # which merges every contribution to `flake.modules..dendritic`. The + # firefox modules below get picked up via that path. + flake.modules.homeManager.dendritic = firefoxHmModule; + flake.modules.darwin.dendritic = firefoxDarwinModule; + + # ── Den aspect (future-proofing for den.homes / aspect includes) ──────── + # Same modules exposed as a named aspect so any future `den.homes.*` + # consumer or host aspect can `includes = [ config.den.aspects.firefox ]` + # to pick up firefox declaratively without going through the dendritic + # monolith. + den.aspects.firefox = { + homeManager = firefoxHmModule; + darwin = firefoxDarwinModule; + }; +} diff --git a/modules/apps/gh.nix b/modules/apps/gh.nix new file mode 100644 index 00000000..ec4671ff --- /dev/null +++ b/modules/apps/gh.nix @@ -0,0 +1,54 @@ +{ + # The GitHub CLI (`gh`) authenticates against api.github.com via, + # in priority order: `GH_TOKEN` env var → `GITHUB_TOKEN` env var → + # OS keychain entry seeded by `gh auth login` → `oauth_token` in + # `~/.config/gh/hosts.yml`. The interactive `gh auth login` flow is + # great on a single machine but fights declarative dotfiles (the + # token lives in OS-specific keychains, can't be replayed onto a + # fresh host, and silently rotates outside Git). The env-var + # override path lets us source the token from our sops-nix + # secrets store so every host built from this flake gets the + # same identity without any post-bootstrap manual steps. + # + # Same posture as `chatgpt-cli`: wrap `pkgs.gh` in a thin + # `writeShellScriptBin` that exports `GH_TOKEN` only for the + # `gh` process. We refuse to set `GH_TOKEN` globally (every + # shell child — terminals, LSP, formatters, lazygit's subshells — + # would inherit it), and we refuse to symlink the plaintext + # secret into `~/.config/gh/hosts.yml` (a world-readable home + # location). + # + # Self-contained secret declaration: `sops.secrets.gh_token = {}` + # is set-merged across consumers, so other modules can ALSO + # declare it without conflict. Update the encrypted value via + # `sops edit secrets/secrets.yaml`. + flake.modules.homeManager.dendritic = + { + pkgs, + config, + ... + }: + let + tokenPath = config.sops.secrets.gh_token.path; + gh = pkgs.writeShellScriptBin "gh" '' + # Fall through silently if the sops secret hasn't been + # materialised yet (fresh bootstrap, sops-nix launchd agent + # failed, etc.) — `gh` will print its own clearer + # "not logged in" diagnostic than a `cat: ... No such file` + # error would. + if [ -r "${tokenPath}" ]; then + GH_TOKEN="$(cat "${tokenPath}")" + export GH_TOKEN + fi + exec ${pkgs.gh}/bin/gh "$@" + ''; + in + { + sops.secrets.gh_token = { }; + # Only the wrapper goes into PATH; it Nix-references the real + # `pkgs.gh` via the `exec` line, so the underlying binary is + # still pulled into the closure but never shadowed by the + # wrapper. + home.packages = [ gh ]; + }; +} diff --git a/modules/apps/ghidra.nix b/modules/apps/ghidra.nix new file mode 100644 index 00000000..2e0733b9 --- /dev/null +++ b/modules/apps/ghidra.nix @@ -0,0 +1,7 @@ +{ + flake.modules.homeManager.dendritic = + { pkgs, ... }: + { + home.packages = [ pkgs.ghidra ]; + }; +} diff --git a/modules/apps/ghostty.nix b/modules/apps/ghostty.nix index 817df51a..54f119d5 100644 --- a/modules/apps/ghostty.nix +++ b/modules/apps/ghostty.nix @@ -1,233 +1,318 @@ { - flake.modules.homeManager.ghostty = { pkgs, lib, config, ... }: { - options.dendritic.apps.ghostty = { - enable = lib.mkEnableOption "Ghostty terminal emulator"; - fontSize = lib.mkOption { - type = lib.types.int; - default = config.stylix.fonts.sizes.terminal; - description = "Font size for Ghostty."; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + ghosttyReloadCommand = pkgs.writeShellApplication { + name = "ghostty-reload"; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + set -eu + + # Only attempt reload when Ghostty is running. + if ! /usr/bin/pgrep -x "Ghostty" >/dev/null 2>&1; then + exit 0 + fi + + # Preferred path: Ghostty supports SIGUSR2 config reload on newer builds. + # This is fast and avoids Accessibility/UI scripting dependencies. + if /usr/bin/pkill -USR2 -x "Ghostty" >/dev/null 2>&1; then + exit 0 + fi + + # Fallback for builds that don't yet support signal reload. + /usr/bin/osascript <<'APPLESCRIPT' + tell application "System Events" + if not (exists process "Ghostty") then return + tell process "Ghostty" + try + click menu item "Reload Configuration" of menu "Ghostty" of menu bar item "Ghostty" of menu bar 1 + on error + -- Fallback for menu structure differences across versions. + try + click menu item "Reload Configuration" of menu "File" of menu bar item "File" of menu bar 1 + end try + end try + end tell + end tell + APPLESCRIPT + ''; + }; + in + { + options.dendritic.apps.ghostty = { + enable = lib.mkEnableOption "Ghostty terminal emulator"; + fontSize = lib.mkOption { + type = lib.types.int; + default = config.stylix.fonts.sizes.terminal; + description = "Font size for Ghostty."; + }; }; - }; - config = lib.mkIf config.dendritic.apps.ghostty.enable { - programs.ghostty = { - enable = true; - # Automatically select the correct package for the platform - package = if pkgs.stdenv.isDarwin then pkgs.ghostty-bin else pkgs.ghostty; - - settings = { - font-size = config.dendritic.apps.ghostty.fontSize; - font-family = "Maple Mono NF"; - window-decoration = true; - macos-option-as-alt = true; - shell-integration = "detect"; - # Use absolute path for zsh to ensure it launches correctly on macOS - command = "${pkgs.zsh}/bin/zsh"; - - # ── Cursor Animation ──────────────────────────────────────── - custom-shader = "shaders/cursor_tail.glsl"; - custom-shader-animation = "always"; - - # Manually inherit colors from Stylix - background = "#${config.lib.stylix.colors.base00}"; - foreground = "#${config.lib.stylix.colors.base05}"; - cursor-color = "#${config.lib.stylix.colors.base05}"; - selection-background = "#${config.lib.stylix.colors.base02}"; - selection-foreground = "#${config.lib.stylix.colors.base05}"; - - palette = [ - "0=#${config.lib.stylix.colors.base00}" - "1=#${config.lib.stylix.colors.base08}" - "2=#${config.lib.stylix.colors.base0B}" - "3=#${config.lib.stylix.colors.base0A}" - "4=#${config.lib.stylix.colors.base0D}" - "5=#${config.lib.stylix.colors.base0E}" - "6=#${config.lib.stylix.colors.base0C}" - "7=#${config.lib.stylix.colors.base05}" - "8=#${config.lib.stylix.colors.base03}" - "9=#${config.lib.stylix.colors.base08}" - "10=#${config.lib.stylix.colors.base0B}" - "11=#${config.lib.stylix.colors.base0A}" - "12=#${config.lib.stylix.colors.base0D}" - "13=#${config.lib.stylix.colors.base0E}" - "14=#${config.lib.stylix.colors.base0C}" - "15=#${config.lib.stylix.colors.base07}" - ]; + config = lib.mkIf config.dendritic.apps.ghostty.enable { + programs.ghostty = { + enable = true; + # Automatically select the correct package for the platform + package = if pkgs.stdenv.isDarwin then pkgs.ghostty-bin else pkgs.ghostty; + + settings = { + font-size = config.dendritic.apps.ghostty.fontSize; + font-family = "Maple Mono NF"; + window-decoration = true; + # Ensure macOS titlebar tint follows terminal background (base00), + # and text contrast follows the active Stylix dark/light variant. + macos-titlebar-style = "transparent"; + window-theme = config.dendritic.theme.variant; + macos-option-as-alt = true; + shell-integration = "detect"; + # Use absolute path for zsh to ensure it launches correctly on macOS + command = "${pkgs.zsh}/bin/zsh"; + + # ── Cursor Animation ──────────────────────────────────────── + custom-shader = "shaders/cursor_tail.glsl"; + custom-shader-animation = "always"; + + # Manually inherit colors from Stylix + background = "#${config.lib.stylix.colors.base00}"; + foreground = "#${config.lib.stylix.colors.base05}"; + cursor-color = "#${config.lib.stylix.colors.base05}"; + selection-background = "#${config.lib.stylix.colors.base02}"; + selection-foreground = "#${config.lib.stylix.colors.base05}"; + + palette = [ + "0=#${config.lib.stylix.colors.base00}" + "1=#${config.lib.stylix.colors.base08}" + "2=#${config.lib.stylix.colors.base0B}" + "3=#${config.lib.stylix.colors.base0A}" + "4=#${config.lib.stylix.colors.base0D}" + "5=#${config.lib.stylix.colors.base0E}" + "6=#${config.lib.stylix.colors.base0C}" + "7=#${config.lib.stylix.colors.base05}" + "8=#${config.lib.stylix.colors.base03}" + "9=#${config.lib.stylix.colors.base08}" + "10=#${config.lib.stylix.colors.base0B}" + "11=#${config.lib.stylix.colors.base0A}" + "12=#${config.lib.stylix.colors.base0D}" + "13=#${config.lib.stylix.colors.base0E}" + "14=#${config.lib.stylix.colors.base0C}" + "15=#${config.lib.stylix.colors.base07}" + ]; + }; }; + + # ── Cursor Shader Implementation ─────────────────────────── + xdg.configFile."ghostty/shaders/cursor_tail.glsl".text = '' + // -- CONFIGURATION -- + vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5); + const float DURATION = 0.09; // in seconds + const float MAX_TRAIL_LENGTH = 0.2; + const float THRESHOLD_MIN_DISTANCE = 1.5; // min distance to show trail (units of cursor width) + const float BLUR = 2.0; // blur size in pixels (for antialiasing) + + // --- CONSTANTS for easing functions --- + const float PI = 3.14159265359; + + // EaseOutCirc + float ease(float x) { + return sqrt(1.0 - pow(x - 1.0, 2.0)); + } + + float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b) + { + vec2 d = abs(p - xy) - b; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); + } + + float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) { + vec2 e = b - a; + vec2 w = p - a; + vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0); + float segd = dot(p - proj, p - proj); + d = min(d, segd); + + float c0 = step(0.0, p.y - a.y); + float c1 = 1.0 - step(0.0, p.y - b.y); + float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x); + float allCond = c0 * c1 * c2; + float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2); + float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond)); + s *= flip; + return d; + } + + float getSdfParallelogram(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3) { + float s = 1.0; + float d = dot(p - v0, p - v0); + + d = seg(p, v0, v3, s, d); + d = seg(p, v1, v0, s, d); + d = seg(p, v2, v1, s, d); + d = seg(p, v3, v2, s, d); + + return s * sqrt(d); + } + + vec2 normalize(vec2 value, float isPosition) { + return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y; + } + + float antialising(float distance) { + return 1. - smoothstep(0., normalize(vec2(BLUR, BLUR), 0.).x, distance); + } + + float determineIfTopRightIsLeading(vec2 a, vec2 b) { + float condition1 = step(b.x, a.x) * step(a.y, b.y); // a.x < b.x && a.y > b.y + float condition2 = step(a.x, b.x) * step(b.y, a.y); // a.x > b.x && a.y < b.y + + // if neither condition is met, return 1 (else case) + return 1.0 - max(condition1, condition2); + } + + void mainImage(out vec4 fragColor, in vec2 fragCoord){ + #if !defined(WEB) + fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy); + #endif + + // normalization & setup(-1, 1 coords) + vec2 vu = normalize(fragCoord, 1.); + vec2 offsetFactor = vec2(-.5, 0.5); + + vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.)); + vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.)); + + vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor); + vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor); + + vec2 delta = centerCP - centerCC; + float lineLength = length(delta); + + float sdfCurrentCursor = getSdfRectangle(vu, centerCC, currentCursor.zw * 0.5); + + vec4 newColor = vec4(fragColor); + + float minDist = currentCursor.w * THRESHOLD_MIN_DISTANCE; + float progress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0); + if (lineLength > minDist) { + // ANIMATION logic + + float head_eased = 0.0; + float tail_eased = 0.0; + + float tail_delay_factor = MAX_TRAIL_LENGTH / lineLength; + + float isLongMove = step(MAX_TRAIL_LENGTH, lineLength); + + float head_eased_short = ease(progress); + float tail_eased_short = ease(smoothstep(tail_delay_factor, 1.0, progress)); + float head_eased_long = 1.0; + float tail_eased_long = ease(progress); + + head_eased = mix(head_eased_long, head_eased_short, isLongMove); + tail_eased = mix(tail_eased_long, tail_eased_short, isLongMove); + + // detect straight moves + vec2 delta_abs = abs(centerCC - centerCP); + float threshold = 0.001; + float isHorizontal = step(delta_abs.y, threshold); + float isVertical = step(delta_abs.x, threshold); + float isStraightMove = max(isHorizontal, isVertical); + + // -- Making the parallelogram sdf (diagonal move) -- + + // animate the TOP-LEFT corners + vec2 head_pos_tl = mix(previousCursor.xy, currentCursor.xy, head_eased); + vec2 tail_pos_tl = mix(previousCursor.xy, currentCursor.xy, tail_eased); + + float isTopRightLeading = determineIfTopRightIsLeading(currentCursor.xy, previousCursor.xy); + float isBottomLeftLeading = 1.0 - isTopRightLeading; + + // v0, v1 : "front" of the trail (head) + vec2 v0 = vec2(head_pos_tl.x + currentCursor.z * isTopRightLeading, head_pos_tl.y - currentCursor.w); + vec2 v1 = vec2(head_pos_tl.x + currentCursor.z * isBottomLeftLeading, head_pos_tl.y); + + // v2, v3: "back" of the trail (tail) + vec2 v2 = vec2(tail_pos_tl.x + currentCursor.z * isBottomLeftLeading, tail_pos_tl.y); + vec2 v3 = vec2(tail_pos_tl.x + currentCursor.z * isTopRightLeading, tail_pos_tl.y - previousCursor.w); + + float sdfTrail_diag = getSdfParallelogram(vu, v0, v1, v2, v3); + + // -- Making the rectangle sdf (straight move) -- + + vec2 head_center = mix(centerCP, centerCC, head_eased); + vec2 tail_center = mix(centerCP, centerCC, tail_eased); + + vec2 min_center = min(head_center, tail_center); + vec2 max_center = max(head_center, tail_center); + + vec2 box_size = (max_center - min_center) + currentCursor.zw; + vec2 box_center = (min_center + max_center) * 0.5; + + float sdfTrail_rect = getSdfRectangle(vu, box_center, box_size * 0.5); + + // -- FINAL SELECTING AND DRAWING -- + float sdfTrail = mix(sdfTrail_diag, sdfTrail_rect, isStraightMove); + + vec4 trail = TRAIL_COLOR; + float trailAlpha = antialising(sdfTrail); + newColor = mix(newColor, trail, trailAlpha); + + // punch hole + newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.)); + } + + fragColor = newColor; + } + ''; + + launchd.agents.ghostty-hot-reload = lib.mkIf pkgs.stdenv.isDarwin { + enable = true; + config = { + Label = "com.dendritic.ghostty-hot-reload"; + ProgramArguments = [ + "${pkgs.bash}/bin/bash" + "${ghosttyReloadCommand}/bin/ghostty-reload" + ]; + RunAtLoad = true; + WatchPaths = [ + "${config.xdg.configHome}/ghostty/config" + "${config.home.homeDirectory}/Library/Application Support/com.mitchellh.ghostty/config" + "${config.xdg.configHome}/ghostty/shaders/cursor_tail.glsl" + ]; + StandardOutPath = "${config.home.homeDirectory}/.cache/ghostty-hot-reload.log"; + StandardErrorPath = "${config.home.homeDirectory}/.cache/ghostty-hot-reload.err.log"; + }; + }; + + home.activation.syncGhosttyMacConfig = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + app_support_dir="$HOME/Library/Application Support/com.mitchellh.ghostty" + mkdir -p "$app_support_dir" + ln -snf "$HOME/.config/ghostty/config" "$app_support_dir/config" + '' + ); + + # Installs `ghostty-reload` so users can bind it in skhd/shkd/etc. + home.packages = [ + config.programs.ghostty.package + ghosttyReloadCommand + ]; }; + }; - # ── Cursor Shader Implementation ─────────────────────────── - xdg.configFile."ghostty/shaders/cursor_tail.glsl".text = '' - // -- CONFIGURATION -- - vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5); - const float DURATION = 0.09; // in seconds - const float MAX_TRAIL_LENGTH = 0.2; - const float THRESHOLD_MIN_DISTANCE = 1.5; // min distance to show trail (units of cursor width) - const float BLUR = 2.0; // blur size in pixels (for antialiasing) - - // --- CONSTANTS for easing functions --- - const float PI = 3.14159265359; - - // EaseOutCirc - float ease(float x) { - return sqrt(1.0 - pow(x - 1.0, 2.0)); - } - - float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b) - { - vec2 d = abs(p - xy) - b; - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); - } - - float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) { - vec2 e = b - a; - vec2 w = p - a; - vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0); - float segd = dot(p - proj, p - proj); - d = min(d, segd); - - float c0 = step(0.0, p.y - a.y); - float c1 = 1.0 - step(0.0, p.y - b.y); - float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x); - float allCond = c0 * c1 * c2; - float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2); - float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond)); - s *= flip; - return d; - } - - float getSdfParallelogram(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3) { - float s = 1.0; - float d = dot(p - v0, p - v0); - - d = seg(p, v0, v3, s, d); - d = seg(p, v1, v0, s, d); - d = seg(p, v2, v1, s, d); - d = seg(p, v3, v2, s, d); - - return s * sqrt(d); - } - - vec2 normalize(vec2 value, float isPosition) { - return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y; - } - - float antialising(float distance) { - return 1. - smoothstep(0., normalize(vec2(BLUR, BLUR), 0.).x, distance); - } - - float determineIfTopRightIsLeading(vec2 a, vec2 b) { - float condition1 = step(b.x, a.x) * step(a.y, b.y); // a.x < b.x && a.y > b.y - float condition2 = step(a.x, b.x) * step(b.y, a.y); // a.x > b.x && a.y < b.y - - // if neither condition is met, return 1 (else case) - return 1.0 - max(condition1, condition2); - } - - void mainImage(out vec4 fragColor, in vec2 fragCoord){ - #if !defined(WEB) - fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy); - #endif - - // normalization & setup(-1, 1 coords) - vec2 vu = normalize(fragCoord, 1.); - vec2 offsetFactor = vec2(-.5, 0.5); - - vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.)); - vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.)); - - vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor); - vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor); - - vec2 delta = centerCP - centerCC; - float lineLength = length(delta); - - float sdfCurrentCursor = getSdfRectangle(vu, centerCC, currentCursor.zw * 0.5); - - vec4 newColor = vec4(fragColor); - - float minDist = currentCursor.w * THRESHOLD_MIN_DISTANCE; - float progress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0); - if (lineLength > minDist) { - // ANIMATION logic - - float head_eased = 0.0; - float tail_eased = 0.0; - - float tail_delay_factor = MAX_TRAIL_LENGTH / lineLength; - - float isLongMove = step(MAX_TRAIL_LENGTH, lineLength); - - float head_eased_short = ease(progress); - float tail_eased_short = ease(smoothstep(tail_delay_factor, 1.0, progress)); - float head_eased_long = 1.0; - float tail_eased_long = ease(progress); - - head_eased = mix(head_eased_long, head_eased_short, isLongMove); - tail_eased = mix(tail_eased_long, tail_eased_short, isLongMove); - - // detect straight moves - vec2 delta_abs = abs(centerCC - centerCP); - float threshold = 0.001; - float isHorizontal = step(delta_abs.y, threshold); - float isVertical = step(delta_abs.x, threshold); - float isStraightMove = max(isHorizontal, isVertical); - - // -- Making the parallelogram sdf (diagonal move) -- - - // animate the TOP-LEFT corners - vec2 head_pos_tl = mix(previousCursor.xy, currentCursor.xy, head_eased); - vec2 tail_pos_tl = mix(previousCursor.xy, currentCursor.xy, tail_eased); - - float isTopRightLeading = determineIfTopRightIsLeading(currentCursor.xy, previousCursor.xy); - float isBottomLeftLeading = 1.0 - isTopRightLeading; - - // v0, v1 : "front" of the trail (head) - vec2 v0 = vec2(head_pos_tl.x + currentCursor.z * isTopRightLeading, head_pos_tl.y - currentCursor.w); - vec2 v1 = vec2(head_pos_tl.x + currentCursor.z * isBottomLeftLeading, head_pos_tl.y); - - // v2, v3: "back" of the trail (tail) - vec2 v2 = vec2(tail_pos_tl.x + currentCursor.z * isBottomLeftLeading, tail_pos_tl.y); - vec2 v3 = vec2(tail_pos_tl.x + currentCursor.z * isTopRightLeading, tail_pos_tl.y - previousCursor.w); - - float sdfTrail_diag = getSdfParallelogram(vu, v0, v1, v2, v3); - - // -- Making the rectangle sdf (straight move) -- - - vec2 head_center = mix(centerCP, centerCC, head_eased); - vec2 tail_center = mix(centerCP, centerCC, tail_eased); - - vec2 min_center = min(head_center, tail_center); - vec2 max_center = max(head_center, tail_center); - - vec2 box_size = (max_center - min_center) + currentCursor.zw; - vec2 box_center = (min_center + max_center) * 0.5; - - float sdfTrail_rect = getSdfRectangle(vu, box_center, box_size * 0.5); - - // -- FINAL SELECTING AND DRAWING -- - float sdfTrail = mix(sdfTrail_diag, sdfTrail_rect, isStraightMove); - - vec4 trail = TRAIL_COLOR; - float trailAlpha = antialising(sdfTrail); - newColor = mix(newColor, trail, trailAlpha); - - // punch hole - newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.)); - } - - fragColor = newColor; - } - ''; - - home.packages = [ config.programs.ghostty.package ]; + # Dock registration: Ghostty owns its dock entry (order 140 in `dock.nix`). + flake.modules.darwin.dendritic = + { + pkgs, + lib, + ... + }: + { + dendritic.dock.apps = lib.mkOrder 140 [ + "${if pkgs.stdenv.isDarwin then pkgs.ghostty-bin else pkgs.ghostty}/Applications/Ghostty.app" + ]; }; - }; - - # Dock registration: Ghostty owns its dock entry - flake.modules.darwin.ghostty = { pkgs, ... }: { - dendritic.dock.apps = [ - "${if pkgs.stdenv.isDarwin then pkgs.ghostty-bin else pkgs.ghostty}/Applications/Ghostty.app" - ]; - }; } diff --git a/modules/apps/java.nix b/modules/apps/java.nix new file mode 100644 index 00000000..78ecc3df --- /dev/null +++ b/modules/apps/java.nix @@ -0,0 +1,11 @@ +{ + # JDK 21 for general Java development (compilers, build tools, language + # servers). JetBrains IDEs additionally wire `programs.java` from + # `./jetbrains.nix` when that toggle is on; this module is independent so + # `java -version` works on hosts that don't enable JetBrains. + flake.modules.homeManager.dendritic = + { pkgs, ... }: + { + home.packages = [ pkgs.jdk21 ]; + }; +} diff --git a/modules/apps/jetbrains.nix b/modules/apps/jetbrains.nix index d740c1f0..511f1bd4 100644 --- a/modules/apps/jetbrains.nix +++ b/modules/apps/jetbrains.nix @@ -1,339 +1,413 @@ { - flake.modules.homeManager.jetbrains = { pkgs, lib, config, ... }: let - cfg = config.dendritic.apps.jetbrains; - c = config.lib.stylix.colors; - fontName = config.stylix.fonts.monospace.name; - fontSize = toString config.stylix.fonts.sizes.applications; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.apps.jetbrains; + c = config.lib.stylix.colors; + fontName = config.stylix.fonts.monospace.name; + fontSize = toString config.stylix.fonts.sizes.applications; + isLight = config.dendritic.theme.variant == "light"; + parentScheme = if isLight then "Default" else "Darcula"; + lafClass = + if isLight then + "com.intellij.ide.ui.laf.IntelliJLaf" + else + "com.intellij.ide.ui.laf.darcula.DarculaLaf"; + lafThemeId = if isLight then "JetBrainsLightTheme" else "Darcula"; - # ── JetBrains .icls color scheme (inherits from Darcula) ───── - iclsFile = pkgs.writeText "Stylix.icls" '' - - - Generated by Nix/Stylix - - - - - + # ── JetBrains .icls color scheme (inherits from Darcula) ───── + iclsFile = pkgs.writeText "Stylix.icls" '' + + + Generated by Nix/Stylix + + + + + - - + + - - - - + + + + - - - + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - + + + + + + + + - - - - + + + + - - + + - - - + + + - - - - - - - ''; + + + + + + + ''; - # ── Options XMLs ───────────────────────────────────────────── - colorSchemeXml = pkgs.writeText "colors.scheme.xml" '' - - - - - - ''; + # ── Options XMLs ───────────────────────────────────────────── + colorSchemeXml = pkgs.writeText "colors.scheme.xml" '' + + + + + + ''; - editorFontXml = pkgs.writeText "editor-font.xml" '' - - - - - ''; + editorFontXml = pkgs.writeText "editor-font.xml" '' + + + + + ''; - consoleFontXml = pkgs.writeText "console-font.xml" '' - - - - - ''; + consoleFontXml = pkgs.writeText "console-font.xml" '' + + + + + ''; - terminalFontXml = pkgs.writeText "terminal-font.xml" '' - - - - - ''; + terminalFontXml = pkgs.writeText "terminal-font.xml" '' + + + + + ''; - keymapXml = pkgs.writeText "keymap.xml" '' - - - - - - ''; + keymapXml = pkgs.writeText "keymap.xml" '' + + + + + + ''; - googleJavaFormatXml = pkgs.writeText "google-java-format.xml" '' - - - - - ''; + lafXml = pkgs.writeText "laf.xml" '' + + + + + + ''; - # Directories to skip (not actual IDE products) - skipDirs = "Daemon|Local|consentOptions|bl|crl"; - in { - options.dendritic.apps.jetbrains = { - enable = lib.mkEnableOption "JetBrains IDE theming via Stylix"; - }; + googleJavaFormatXml = pkgs.writeText "google-java-format.xml" '' + + + + + ''; - config = lib.mkIf cfg.enable { - # Auto-discover all JetBrains product directories and deploy theme files. - # Handles both macOS (~/Library/...) and Linux (~/.config/...) paths. - home.activation.jetbrainsTheme = lib.hm.dag.entryAfter ["writeBoundary"] '' - if [ "$OSTYPE" == "darwin"* ]; then - JB_BASE="$HOME/Library/Application Support/JetBrains" - else - JB_BASE="$HOME/.config/JetBrains" - fi + # ── Trust all projects under $HOME ──────────────────────────── + # Silences the "Trust Project?" dialog for all JetBrains IDEs. + trustedLocationsXml = pkgs.writeText "trusted-locations.xml" '' + + + + + + + ''; - if [ -d "$JB_BASE" ]; then - for product_dir in "$JB_BASE"/*/; do - [ ! -d "$product_dir" ] && continue - case "$(basename "$product_dir")" in - ${skipDirs}) continue ;; - esac + # Directories to skip (not actual IDE products) + skipDirs = "Daemon|Local|consentOptions|bl|crl"; + in + { + options.dendritic.apps.jetbrains = { + enable = lib.mkEnableOption "JetBrains IDE theming via Stylix"; + }; - # Color scheme - $DRY_RUN_CMD mkdir -p "$product_dir/colors" - $DRY_RUN_CMD cp -f "${iclsFile}" "$product_dir/colors/Stylix.icls" + config = lib.mkIf cfg.enable { + # JetBrains IDE packages, gated by this module's enable toggle so + # the HM-managed binaries match the dock entries declared in the + # Darwin half below. + home.packages = with pkgs; [ + jetbrains.clion + jetbrains.idea + jetbrains.rust-rover + ]; - # Options - $DRY_RUN_CMD mkdir -p "$product_dir/options" - $DRY_RUN_CMD cp -f "${colorSchemeXml}" "$product_dir/options/colors.scheme.xml" - $DRY_RUN_CMD cp -f "${editorFontXml}" "$product_dir/options/editor-font.xml" - $DRY_RUN_CMD cp -f "${consoleFontXml}" "$product_dir/options/console-font.xml" - $DRY_RUN_CMD cp -f "${terminalFontXml}" "$product_dir/options/terminal-font.xml" - $DRY_RUN_CMD cp -f "${keymapXml}" "$product_dir/options/keymap.xml" - $DRY_RUN_CMD cp -f "${googleJavaFormatXml}" "$product_dir/options/google-java-format.xml" - done - fi - ''; + # Auto-discover all JetBrains product directories and deploy theme files. + # Handles both macOS (~/Library/...) and Linux (~/.config/...) paths. + home.activation.jetbrainsTheme = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ "$OSTYPE" == "darwin"* ]; then + JB_BASE="$HOME/Library/Application Support/JetBrains" + else + JB_BASE="$HOME/.config/JetBrains" + fi + + if [ -d "$JB_BASE" ]; then + for product_dir in "$JB_BASE"/*/; do + [ ! -d "$product_dir" ] && continue + case "$(basename "$product_dir")" in + ${skipDirs}) continue ;; + esac - # Set JAVA_HOME so JetBrains IDEs can find the Nix-managed JDK - programs.java = { - enable = true; - package = pkgs.jdk21; + # Color scheme + $DRY_RUN_CMD mkdir -p "$product_dir/colors" + $DRY_RUN_CMD cp -f "${iclsFile}" "$product_dir/colors/Stylix.icls" + + # Options + $DRY_RUN_CMD mkdir -p "$product_dir/options" + $DRY_RUN_CMD cp -f "${colorSchemeXml}" "$product_dir/options/colors.scheme.xml" + $DRY_RUN_CMD cp -f "${editorFontXml}" "$product_dir/options/editor-font.xml" + $DRY_RUN_CMD cp -f "${consoleFontXml}" "$product_dir/options/console-font.xml" + $DRY_RUN_CMD cp -f "${terminalFontXml}" "$product_dir/options/terminal-font.xml" + $DRY_RUN_CMD cp -f "${keymapXml}" "$product_dir/options/keymap.xml" + $DRY_RUN_CMD cp -f "${lafXml}" "$product_dir/options/laf.xml" + $DRY_RUN_CMD cp -f "${googleJavaFormatXml}" "$product_dir/options/google-java-format.xml" + # Disable the Trust Project dialog + $DRY_RUN_CMD cp -f "${trustedLocationsXml}" "$product_dir/options/trusted-locations.xml" + done + fi + ''; + + # Set JAVA_HOME so JetBrains IDEs can find the Nix-managed JDK + programs.java = { + enable = true; + package = pkgs.jdk21; + }; }; }; - }; + + # Dock registration: JetBrains IDEs own their dock entries (order 150 in `dock.nix`). + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + user = config.system.primaryUser; + jetbrainsEnabled = config.home-manager.users.${user}.dendritic.apps.jetbrains.enable or false; + in + lib.mkIf jetbrainsEnabled { + dendritic.dock.apps = lib.mkOrder 150 [ + "${pkgs.jetbrains.idea}/Applications/IntelliJ IDEA.app" + "${pkgs.jetbrains.clion}/Applications/CLion.app" + "${pkgs.jetbrains.rust-rover}/Applications/RustRover.app" + ]; + }; } diff --git a/modules/apps/mas.nix b/modules/apps/mas.nix index a4ddb284..21a67acf 100644 --- a/modules/apps/mas.nix +++ b/modules/apps/mas.nix @@ -21,111 +21,128 @@ { # ── Darwin System Module ─────────────────────────────────────── # This runs at the nix-darwin level (system activation). - flake.modules.darwin.mas = { pkgs, lib, config, ... }: - let - cfg = config.dendritic.mas; + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.mas; - # Build the complete list of app IDs to install: - # 1. Named apps from the attrset (e.g. { Xcode = 497799835; }) - # 2. Safari extensions from the list - allApps = cfg.apps // (builtins.listToAttrs (map (ext: { - name = ext.name; - value = ext.id; - }) cfg.safari.extensions)); + # Build the complete list of app IDs to install: + # 1. Named apps from the attrset (e.g. { Xcode = 497799835; }) + # 2. Safari extensions from the list + allApps = + cfg.apps + // (builtins.listToAttrs ( + map (ext: { + name = ext.name; + value = ext.id; + }) cfg.safari.extensions + )); - masPackage = pkgs.mas; - masSync = import ../pkgs/_mas-sync.nix { - inherit pkgs lib masPackage allApps; - }; - in - { - # ── Options ───────────────────────────────────────────────── - options.dendritic.mas = { - enable = lib.mkEnableOption "Mac App Store management via mas CLI"; - - syncScript = lib.mkOption { - type = lib.types.package; - readOnly = true; - default = masSync; - description = "The mas-sync script package."; + masPackage = pkgs.mas; + masSync = import ../pkgs/_mas-sync.nix { + inherit + pkgs + lib + masPackage + allApps + ; }; + in + { + # ── Options ───────────────────────────────────────────────── + options.dendritic.mas = { + enable = lib.mkEnableOption "Mac App Store management via mas CLI"; - apps = lib.mkOption { - type = lib.types.attrsOf lib.types.int; - default = {}; - example = lib.literalExpression '' - { - Xcode = 497799835; - "1Password for Safari" = 1569813296; - } - ''; - description = '' - Attribute set of Mac App Store applications to install. - Keys are human-readable names (for logging), values are - the numeric App Store IDs. + syncScript = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = masSync; + description = "The mas-sync script package."; + }; - Find IDs with: `mas search ` - ''; - }; + apps = lib.mkOption { + type = lib.types.attrsOf lib.types.int; + default = { }; + example = lib.literalExpression '' + { + Xcode = 497799835; + "1Password for Safari" = 1569813296; + } + ''; + description = '' + Attribute set of Mac App Store applications to install. + Keys are human-readable names (for logging), values are + the numeric App Store IDs. - safari.extensions = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - description = "Human-readable name of the Safari extension."; - example = "uBlock Origin Lite"; - }; - id = lib.mkOption { - type = lib.types.int; - description = "Mac App Store ID of the Safari extension."; - example = 6745342698; - }; - }; - }); - default = []; - example = lib.literalExpression '' - [ - { name = "uBlock Origin Lite"; id = 6745342698; } - { name = "1Password for Safari"; id = 1569813296; } - ] - ''; - description = '' - List of Safari extensions to install from the Mac App Store. - Each entry has a `name` (for display/logging) and an `id` - (the numeric App Store ID). + Find IDs with: `mas search ` + ''; + }; - These are installed via `mas install` just like regular apps, - but are separated for semantic clarity — matching the pattern - used by `programs.brave.extensions` and Firefox's - `ExtensionSettings`. + safari.extensions = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Human-readable name of the Safari extension."; + example = "uBlock Origin Lite"; + }; + id = lib.mkOption { + type = lib.types.int; + description = "Mac App Store ID of the Safari extension."; + example = 6745342698; + }; + }; + } + ); + default = [ ]; + example = lib.literalExpression '' + [ + { name = "uBlock Origin Lite"; id = 6745342698; } + { name = "1Password for Safari"; id = 1569813296; } + ] + ''; + description = '' + List of Safari extensions to install from the Mac App Store. + Each entry has a `name` (for display/logging) and an `id` + (the numeric App Store ID). - After installation, extensions must be enabled in: - Safari → Settings → Extensions - ''; - }; + These are installed via `mas install` just like regular apps, + but are separated for semantic clarity — matching the pattern + used by `programs.brave.extensions` and Firefox's + `ExtensionSettings`. - safari.enableExtensionsOnInstall = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Attempt to enable Safari extensions automatically after - installation. This uses `pluginkit` and may require - additional permissions. Disabled by default since Safari - extensions typically need manual consent. - ''; + After installation, extensions must be enabled in: + Safari → Settings → Extensions + ''; + }; + + safari.enableExtensionsOnInstall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Attempt to enable Safari extensions automatically after + installation. This uses `pluginkit` and may require + additional permissions. Disabled by default since Safari + extensions typically need manual consent. + ''; + }; }; - }; - # ── Implementation ────────────────────────────────────────── - config = lib.mkIf cfg.enable { - # Ensure mas + the sync wrapper are available system-wide. - # The sync script is called AFTER `nh darwin switch` by the - # install script so the user sees full interactive output. - environment.systemPackages = [ - masPackage - masSync - ]; + # ── Implementation ────────────────────────────────────────── + config = lib.mkIf cfg.enable { + # Ensure mas + the sync wrapper are available system-wide. + # The sync script is called AFTER `nh darwin switch` by the + # install script so the user sees full interactive output. + environment.systemPackages = [ + masPackage + masSync + ]; + }; }; - }; } diff --git a/modules/apps/safari.nix b/modules/apps/safari.nix new file mode 100644 index 00000000..4049f9da --- /dev/null +++ b/modules/apps/safari.nix @@ -0,0 +1,11 @@ +{ + # Safari ships with macOS (Cryptex path); we just need to pin it in the + # dock. No HM half because Safari is system-managed. + flake.modules.darwin.dendritic = + { lib, ... }: + { + dendritic.dock.apps = lib.mkOrder 100 [ + "/System/Cryptexes/App/System/Applications/Safari.app" + ]; + }; +} diff --git a/modules/apps/spotify.nix b/modules/apps/spotify.nix index 8dba77ed..0961828d 100644 --- a/modules/apps/spotify.nix +++ b/modules/apps/spotify.nix @@ -1,47 +1,72 @@ { - flake.modules.homeManager.spotify = { pkgs, lib, inputs, ... }: let - spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}; - in { - imports = [ inputs.spicetify-nix.homeManagerModules.default ]; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + inputs, + ... + }: + let + spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}; + in + { + imports = [ inputs.spicetify-nix.homeManagerModules.default ]; - config = let - # Spotify is not available for aarch64-linux - isSupported = !(pkgs.stdenv.isLinux && pkgs.stdenv.isAarch64); - in lib.mkMerge [ - { stylix.targets.spicetify.enable = isSupported; } - (lib.mkIf isSupported { - programs.spicetify = { - enable = true; - spotifyPackage = pkgs.spotify; - colorScheme = lib.mkForce "Everforest"; + config = + let + # Spotify is not available for aarch64-linux + isSupported = !(pkgs.stdenv.isLinux && pkgs.stdenv.isAarch64); + in + lib.mkMerge [ + { stylix.targets.spicetify.enable = isSupported; } + (lib.mkIf isSupported { + programs.spicetify = { + enable = true; + spotifyPackage = pkgs.spotify; + # colorScheme and theme are managed by Stylix; set defaults only + # colorScheme = "Everforest"; # Stylix will override this + # theme = spicePkgs.themes.comfy; # Stylix will override this - # Using the built-in comfy theme - theme = lib.mkForce spicePkgs.themes.comfy; + enabledExtensions = with spicePkgs.extensions; [ + adblock + adblockify + hidePodcasts + shuffle + ]; - enabledExtensions = with spicePkgs.extensions; [ - adblock - adblockify - hidePodcasts - shuffle - ]; + enabledCustomApps = with spicePkgs.apps; [ + lyricsPlus + marketplace + ]; + }; + }) + (lib.mkIf pkgs.stdenv.isDarwin { + # MacOS Spotify auto-update prevention fix + home.activation.disableSpotifyUpdates = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + SPOTIFY_UPDATE_DIR=~/Library/Application\ Support/Spotify/PersistentCache/Update + if ! /usr/bin/stat -f "%Sf" "$SPOTIFY_UPDATE_DIR" 2> /dev/null | grep -q uchg; then + rm -rf "$SPOTIFY_UPDATE_DIR" + mkdir -p "$SPOTIFY_UPDATE_DIR" + /usr/bin/chflags uchg "$SPOTIFY_UPDATE_DIR" + fi + ''; + }) + ]; + }; - enabledCustomApps = with spicePkgs.apps; [ - lyricsPlus - marketplace - ]; - }; - }) - (lib.mkIf pkgs.stdenv.isDarwin { - # MacOS Spotify auto-update prevention fix - home.activation.disableSpotifyUpdates = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - SPOTIFY_UPDATE_DIR=~/Library/Application\ Support/Spotify/PersistentCache/Update - if ! /usr/bin/stat -f "%Sf" "$SPOTIFY_UPDATE_DIR" 2> /dev/null | grep -q uchg; then - rm -rf "$SPOTIFY_UPDATE_DIR" - mkdir -p "$SPOTIFY_UPDATE_DIR" - /usr/bin/chflags uchg "$SPOTIFY_UPDATE_DIR" - fi - ''; - }) - ]; - }; + # Dock registration: Spotify owns its dock entry (order 120 in `dock.nix`). + flake.modules.darwin.dendritic = + { + lib, + config, + ... + }: + let + user = config.system.primaryUser; + in + { + dendritic.dock.apps = lib.mkOrder 120 [ + "${config.home-manager.users.${user}.programs.spicetify.spicedSpotify}/Applications/Spotify.app" + ]; + }; } diff --git a/modules/apps/sway.nix b/modules/apps/sway.nix deleted file mode 100644 index 90139bc5..00000000 --- a/modules/apps/sway.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ - flake.modules.homeManager.sway = { config, pkgs, lib, ... }: { - options.dendritic.apps.sway = { - enable = lib.mkEnableOption "Sway window manager"; - }; - - config = lib.mkIf config.dendritic.apps.sway.enable { - wayland.windowManager.sway = { - enable = true; - package = null; # Use the system package - config = rec { - modifier = "Mod4"; - terminal = "${config.programs.ghostty.package}/bin/ghostty"; - keybindings = lib.mkOptionDefault { - "${modifier}+Return" = "exec ${terminal}"; - }; - }; - }; - }; - }; -} diff --git a/modules/apps/vesktop.nix b/modules/apps/vesktop.nix index e1307faf..306a33af 100644 --- a/modules/apps/vesktop.nix +++ b/modules/apps/vesktop.nix @@ -12,50 +12,264 @@ # Vesktop is supported on both Linux and Darwin (macOS). # Reference: https://github.com/nix-community/stylix/blob/master/modules/discord/vesktop.nix - flake.modules.homeManager.vesktop = { pkgs, lib, inputs, ... }: { - config = { - # ── Stylix: enable the vesktop colourscheme target ─────────── - stylix.targets.vesktop.enable = false; - - # ── Vesktop application ────────────────────────────────────── - programs.vesktop = { - enable = pkgs.stdenv.isDarwin; - package = lib.mkIf pkgs.stdenv.isDarwin (lib.mkForce inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}.vesktop); - - # Application-level settings - # Written to $XDG_CONFIG_HOME/vesktop/settings.json - settings = { - arRPC = true; # Rich Presence via arRPC - checkUpdates = false; # Nix manages the package version - hardwareAcceleration = true; - minimizeToTray = false; - tray = false; - splashTheming = true; # Stylix themes the splash screen too - staticTitle = true; - discordBranch = "stable"; - }; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + inputs, + config, + ... + }: + { + config = { + # ── Stylix: enable the vesktop colourscheme target ─────────── + stylix.targets.vesktop.enable = true; + + # Codesign the HM-managed Vesktop.app bundle on Darwin so Launch + # Services accepts it after `home-manager` regenerates the bundle. + home.activation.signVesktopApp = lib.mkIf pkgs.stdenv.isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ -d "$HOME/Applications/Home Manager Apps/Vesktop.app" ]; then + /usr/bin/codesign --force --deep --sign - "$HOME/Applications/Home Manager Apps/Vesktop.app" + fi + '' + ); + + # ── Vesktop application ────────────────────────────────────── + programs.vesktop = { + enable = pkgs.stdenv.isDarwin; + package = lib.mkIf pkgs.stdenv.isDarwin (lib.mkForce pkgs.vesktop); + + # Application-level settings + # Written to $XDG_CONFIG_HOME/vesktop/settings.json + settings = { + arRPC = true; # Rich Presence via arRPC + checkUpdates = false; # Nix manages the package version + hardwareAcceleration = true; + minimizeToTray = false; + tray = false; + splashTheming = true; # Stylix themes the splash screen too + staticTitle = true; + discordBranch = "stable"; + # Stylix: inject palette colors into the splash/loading panel + splashBackground = "#${config.lib.stylix.colors.base00}"; + splashColor = "#${config.lib.stylix.colors.base0D}"; + }; + + # Vencord plugin / theme settings + # Written to $XDG_CONFIG_HOME/vesktop/settings/settings.json + vencord.settings = { + autoUpdate = false; # Nix pins the Vencord version + autoUpdateNotification = false; + notifyAboutUpdates = false; + useQuickCss = true; # Needed to load extraQuickCss below + enabledThemes = lib.mkForce [ + "stylix.css" + "dendritic-overrides.css" + ]; - # Vencord plugin / theme settings - # Written to $XDG_CONFIG_HOME/vesktop/settings/settings.json - vencord.settings = { - autoUpdate = false; # Nix pins the Vencord version - autoUpdateNotification = false; - notifyAboutUpdates = false; - useQuickCss = true; # Needed to load extraQuickCss below - - plugins = { - MessageLogger = { - enabled = true; - ignoreSelf = true; + plugins = { + MessageLogger = { + enabled = true; + ignoreSelf = true; + }; + NoDevtoolsWarning.enabled = true; + SilentTyping.enabled = true; + FakeNitro.enabled = true; }; - NoDevtoolsWarning.enabled = true; - SilentTyping.enabled = true; - FakeNitro.enabled = true; }; - }; + # Force a stable surface/text/button contract on top of Stylix: + # - Sidebar: base01 + # - Titlebar: base00 + # - Main chat/content: base00 + # - Full text + controls mapped to base16 tokens + # Do not include ".css" in the key: HM appends it to the filename. + # If we include it here, the generated file becomes *.css.css and + # Vesktop won't load it from enabledThemes. + vencord.themes."dendritic-overrides" = '' + :root, + .theme-dark, + .theme-light, + .theme-darker, + .theme-midnight, + .visual-refresh { + --base00: #${config.lib.stylix.colors.base00}; + --base01: #${config.lib.stylix.colors.base01}; + --base02: #${config.lib.stylix.colors.base02}; + --base03: #${config.lib.stylix.colors.base03}; + --base04: #${config.lib.stylix.colors.base04}; + --base05: #${config.lib.stylix.colors.base05}; + --base06: #${config.lib.stylix.colors.base06}; + --base07: #${config.lib.stylix.colors.base07}; + --base08: #${config.lib.stylix.colors.base08}; + --base0D: #${config.lib.stylix.colors.base0D}; + + --font-primary: "Inter", "Maple Mono NF", "SF Pro Text", "Helvetica Neue", sans-serif !important; + --font-display: "Inter", "Maple Mono NF", "SF Pro Text", "Helvetica Neue", sans-serif !important; + --font-headline: "Inter", "Maple Mono NF", "SF Pro Text", "Helvetica Neue", sans-serif !important; + --font-code: "Maple Mono NF", "JetBrains Mono", ui-monospace, monospace !important; + + --background-primary: var(--base00) !important; + --background-secondary: var(--base01) !important; + --background-secondary-alt: var(--base01) !important; + --background-tertiary: var(--base01) !important; + --background-accent: var(--base02) !important; + --background-floating: var(--base01) !important; + --background-mobile-primary: var(--base00) !important; + --background-mobile-secondary: var(--base01) !important; + --chat-background-default: var(--base00) !important; + --channeltextarea-background: var(--base01) !important; + --modal-background: var(--base01) !important; + --modal-footer-background: var(--base01) !important; + + --text-normal: var(--base05) !important; + --text-default: var(--base05) !important; + --text-primary: var(--base05) !important; + --text-secondary: var(--base04) !important; + --text-muted: var(--base04) !important; + --text-muted-on-default: var(--base04) !important; + --text-low-contrast: var(--base04) !important; + --text-link: var(--base0D) !important; + --text-link-low-saturation: var(--base0C) !important; + --text-brand: var(--base0D) !important; + --text-danger: var(--base08) !important; + --text-positive: var(--base0D) !important; + --text-warning: var(--base08) !important; + --header-primary: var(--base06) !important; + --header-secondary: var(--base04) !important; + --interactive-normal: var(--base04) !important; + --interactive-hover: var(--base06) !important; + --interactive-active: var(--base07) !important; + --interactive-muted: var(--base03) !important; + --channels-default: var(--base04) !important; + --channel-icon: var(--base04) !important; + --channel-text-area-placeholder: var(--base04) !important; + --icon-primary: var(--base05) !important; + --icon-secondary: var(--base04) !important; + --icon-muted: var(--base03) !important; + --control-brand-foreground: var(--base0D) !important; + --control-brand-foreground-new: var(--base0D) !important; + + --button-danger-background: var(--base08) !important; + --button-danger-background-hover: color-mix(in srgb, var(--base08) 88%, black) !important; + --button-danger-text: var(--base00) !important; + --button-filled-brand-background: var(--base0D) !important; + --button-filled-brand-background-hover: color-mix(in srgb, var(--base0D) 88%, black) !important; + --button-filled-brand-text: var(--base00) !important; + --button-secondary-background: var(--base02) !important; + --button-secondary-background-hover: var(--base03) !important; + --button-secondary-background-active: var(--base03) !important; + --button-secondary-text: var(--base05) !important; + --button-outline-primary-text: var(--base05) !important; + --button-outline-primary-text-hover: var(--base06) !important; + --button-outline-primary-text-active: var(--base06) !important; + --redesign-button-primary-text: var(--base00) !important; + --redesign-button-secondary-text: var(--base05) !important; + --redesign-button-secondary-alt-text: var(--base05) !important; + --redesign-button-secondary-alt-pressed-text: var(--base06) !important; + --redesign-button-danger-text: var(--base00) !important; + --redesign-button-positive-text: var(--base00) !important; + --input-background: var(--base01) !important; + --input-placeholder-text: var(--base04) !important; + --profile-gradient-primary-color: var(--base01) !important; + --profile-gradient-secondary-color: var(--base01) !important; + } + + /* Sidebar surfaces */ + [class*="sidebar_"], + [class*="privateChannels_"], + [class*="guilds_"], + [class*="membersWrap_"], + [class*="panels_"] { + background-color: var(--base01) !important; + } + + /* Main chat/content surfaces */ + [class*="chat_"], + [class*="chatContent_"], + [class*="content_"], + [class*="messagesWrapper_"], + [class*="scroller_"], + [class*="container_"] { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + + /* Force text color across Discord UI text-bearing elements. */ + [class*="text_"], + [class*="name_"], + [class*="username_"], + [class*="messageContent_"], + [class*="markup_"], + [class*="title_"], + [class*="subtitle_"], + [class*="description_"], + [class*="channelName_"], + [class*="topic_"], + [class*="placeholder_"], + [class*="defaultColor_"], + [class*="contents_"], + span, + p, + h1, + h2, + h3, + h4, + h5, + h6, + label { + color: var(--base05) !important; + } + + [class*="textMuted_"], + [class*="subtext_"], + [class*="hint_"], + [class*="meta_"], + [class*="timestamp_"] { + color: var(--base04) !important; + } + + /* Titlebar */ + [class*="titleBar_"], + [class*="bar_"][class*="titleBar"], + [class*="typeWindows_"], + [class*="winButton_"] { + background-color: var(--base00) !important; + color: var(--base05) !important; + } + + /* Buttons + controls */ + button, + [role="button"], + [class*="lookFilled_"], + [class*="lookOutlined_"], + [class*="lookLink_"], + [class*="input_"], + [class*="select_"], + [class*="option_"], + [class*="item_"], + [class*="bd-select"], + [class*="bd-button"] { + color: var(--base05) !important; + } + ''; + + }; }; }; - }; + + # Dock registration: Vesktop owns its dock entry (order 130 in `dock.nix`). + flake.modules.darwin.dendritic = + { + pkgs, + lib, + ... + }: + { + dendritic.dock.apps = lib.mkOrder 130 [ + "${pkgs.vesktop}/Applications/Vesktop.app" + ]; + }; } diff --git a/modules/apps/vscode.nix b/modules/apps/vscode.nix index bf770f41..a4e6cb0b 100644 --- a/modules/apps/vscode.nix +++ b/modules/apps/vscode.nix @@ -1,22 +1,27 @@ { - flake.modules.homeManager.vscode = { pkgs, lib, config, ... }: { - options.dendritic.apps.vscode = { - enable = lib.mkEnableOption "VS Code / Cursor IDE"; - }; - - imports = [ ./_vscode-common.nix ]; - config = lib.mkIf config.dendritic.apps.vscode.enable { - programs.vscode = { - package = if pkgs.stdenv.isDarwin then pkgs.vscode else pkgs.vscode-fhs; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + { + options.dendritic.apps.vscode = { + enable = lib.mkEnableOption "VS Code / Cursor IDE"; }; - # Ensure extensions are linked - home.file.".cursor/extensions/bbenoist.Nix".source = - "${pkgs.vscode-extensions.bbenoist.nix}/share/vscode/extensions/bbenoist.Nix"; - home.file.".cursor/extensions/jnoortheen.nix-ide".source = - "${pkgs.vscode-extensions.jnoortheen.nix-ide}/share/vscode/extensions/jnoortheen.nix-ide"; + imports = [ ./_vscode-common.nix ]; + config = lib.mkIf config.dendritic.apps.vscode.enable { + programs.vscode = { + package = if pkgs.stdenv.isDarwin then pkgs.vscode else pkgs.vscode-fhs; + }; - home.packages = [ config.programs.vscode.package ]; + # Ensure extensions are linked + home.file.".cursor/extensions/bbenoist.Nix".source = + "${pkgs.vscode-extensions.bbenoist.nix}/share/vscode/extensions/bbenoist.Nix"; + + home.packages = [ config.programs.vscode.package ]; + }; }; - }; } diff --git a/modules/apps/wallpaper.nix b/modules/apps/wallpaper.nix index 4e226162..ee538f6b 100644 --- a/modules/apps/wallpaper.nix +++ b/modules/apps/wallpaper.nix @@ -8,136 +8,173 @@ # 4. Declarative macOS: Using macos-wallpaper CLI tool. # 5. Declarative Linux: Using wpaperd (daemon with TOML config). # - flake.modules.homeManager.wallpaper = { pkgs, lib, config, ... }: - let - cfg = config.dendritic.wallpaper; - isDarwin = pkgs.stdenv.isDarwin; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + let + cfg = config.dendritic.wallpaper; + isDarwin = pkgs.stdenv.isDarwin; - # ── Database Resolution ────────────────────────────────────── - rawImage = if builtins.hasAttr cfg.selected cfg.database - then cfg.database."${cfg.selected}" - else cfg.path; + # ── Database Resolution ────────────────────────────────────── + rawImage = + if builtins.hasAttr cfg.selected cfg.database then cfg.database."${cfg.selected}" else cfg.path; - # ── Stylix Theme Extraction ────────────────────────────────── - stylixTheme = if config.stylix.base16Scheme != null then - lib.removeSuffix ".yaml" (builtins.baseNameOf config.stylix.base16Scheme) - else "catppuccin"; + # ── Gowall theme name ──────────────────────────────────────── + # Defaults to the theme family configured in `theme-selection.nix` + # (exposed as `dendritic.theme.name`). If gowall lacks a matching + # theme, the user can override via `dendritic.wallpaper.gowall.theme`. + gowallTheme = if cfg.gowall.theme != "" then cfg.gowall.theme else config.dendritic.theme.name; - gowallTheme = if cfg.gowall.theme != "" then cfg.gowall.theme else ( - if lib.hasPrefix "catppuccin" stylixTheme then "catppuccin" - else if lib.hasPrefix "nord" stylixTheme then "nord" - else if lib.hasPrefix "dracula" stylixTheme then "dracula" - else if lib.hasPrefix "everforest" stylixTheme then "everforest" - else "catppuccin" - ); + # ── Gowall Pipeline ────────────────────────────────────────── + processedImage = + if cfg.gowall.enable then + pkgs.runCommand "processed-wallpaper-${cfg.selected}.png" + { + nativeBuildInputs = [ pkgs.gowall ]; + } + '' + export HOME=$TMPDIR + ${pkgs.gowall}/bin/gowall convert "${rawImage}" -t "${gowallTheme}" --output "$out" + '' + else + rawImage; + in + { + options.dendritic.wallpaper = { + enable = lib.mkEnableOption "Advanced declarative wallpaper management"; - # ── Gowall Pipeline ────────────────────────────────────────── - processedImage = if cfg.gowall.enable then - pkgs.runCommand "processed-wallpaper-${cfg.selected}.png" { - nativeBuildInputs = [ pkgs.gowall ]; - } '' - export HOME=$TMPDIR - ${pkgs.gowall}/bin/gowall convert "${rawImage}" -t "${gowallTheme}" --output "$out" - '' - else - rawImage; - in - { - options.dendritic.wallpaper = { - enable = lib.mkEnableOption "Advanced declarative wallpaper management"; - - database = lib.mkOption { - type = lib.types.attrsOf lib.types.path; - default = - let - dir = ../../wallpapers; - files = if builtins.pathExists dir then builtins.readDir dir else {}; - isImage = name: lib.any (ext: lib.hasSuffix ext name) [ ".png" ".jpg" ".jpeg" ".webp" ]; - images = lib.filterAttrs (name: type: type == "regular" && isImage name) files; - in - (lib.mapAttrs' (name: _: { - name = lib.removeSuffix ".webp" (lib.removeSuffix ".jpeg" (lib.removeSuffix ".jpg" (lib.removeSuffix ".png" name))); - value = dir + "/${name}"; - }) images) // { - "nix-dark" = pkgs.fetchurl { - url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/master/wallpapers/nix-wallpaper-simple-dark-gray.png"; - sha256 = "sha256-JaLHdBxwrphKVherDVe5fgh+3zqUtpcwuNbjwrBlAok="; + database = lib.mkOption { + type = lib.types.attrsOf lib.types.path; + default = + let + dir = ../../wallpapers; + files = if builtins.pathExists dir then builtins.readDir dir else { }; + isImage = + name: + lib.any (ext: lib.hasSuffix ext name) [ + ".png" + ".jpg" + ".jpeg" + ".webp" + ]; + images = lib.filterAttrs (name: type: type == "regular" && isImage name) files; + in + (lib.mapAttrs' (name: _: { + name = lib.removeSuffix ".webp" ( + lib.removeSuffix ".jpeg" (lib.removeSuffix ".jpg" (lib.removeSuffix ".png" name)) + ); + value = dir + "/${name}"; + }) images) + // { + "nix-dark" = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/master/wallpapers/nix-wallpaper-simple-dark-gray.png"; + sha256 = "sha256-JaLHdBxwrphKVherDVe5fgh+3zqUtpcwuNbjwrBlAok="; + }; }; - }; - description = "Automated wallpaper database from /wallpapers directory."; - }; + description = "Automated wallpaper database from /wallpapers directory."; + }; - selected = lib.mkOption { - type = lib.types.str; - default = "mountain-sunset"; - description = "Selected wallpaper from the database."; - }; + selected = lib.mkOption { + type = lib.types.str; + default = "mountain-sunset"; + description = "Selected wallpaper from the database."; + }; - path = lib.mkOption { - type = lib.types.path; - default = ../../wallpapers/mountain-sunset.png; - description = "Manual path fallback."; - }; + path = lib.mkOption { + type = lib.types.path; + default = ../../wallpapers/mountain-sunset.png; + description = "Manual path fallback."; + }; - scale = lib.mkOption { - type = lib.types.enum [ "fill" "fit" "stretch" "center" ]; - default = "fill"; - description = "Scaling mode."; - }; + scale = lib.mkOption { + type = lib.types.enum [ + "fill" + "fit" + "stretch" + "center" + ]; + default = "fill"; + description = "Scaling mode."; + }; - gowall = { - enable = lib.mkEnableOption "Enable gowall colorization"; - theme = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Gowall theme override."; + gowall = { + enable = lib.mkEnableOption "Enable gowall colorization"; + theme = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Gowall theme override."; + }; }; }; - }; - config = lib.mkIf cfg.enable { - # ── Stylix Integration ────────────────────────────────────── - stylix.image = lib.mkForce processedImage; + config = lib.mkIf cfg.enable { + # ── Stylix Integration ────────────────────────────────────── + stylix.image = lib.mkForce processedImage; - # ── Packages ──────────────────────────────────────────────── - home.packages = [ pkgs.gowall ] + # ── Packages ──────────────────────────────────────────────── + home.packages = [ + pkgs.gowall + ] ++ lib.optionals isDarwin [ pkgs.macos-wallpaper ] ++ lib.optionals (!isDarwin) [ pkgs.wpaperd ]; - # ── macOS Implementation ──────────────────────────────────── - home.activation.setWallpaper = lib.mkIf isDarwin (lib.hm.dag.entryAfter ["writeBoundary"] '' - WALLPAPER_BIN="${pkgs.macos-wallpaper}/bin/wallpaper" - if [ -x "$WALLPAPER_BIN" ]; then - echo "Setting macOS wallpaper: ${processedImage}" - $DRY_RUN_CMD "$WALLPAPER_BIN" set "${processedImage}" --scale ${cfg.scale} - fi - ''); + # ── macOS Implementation ──────────────────────────────────── + home.activation.setWallpaper = lib.mkIf isDarwin ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + WALLPAPER_BIN="${pkgs.macos-wallpaper}/bin/wallpaper" + if [ -x "$WALLPAPER_BIN" ]; then + echo "Setting macOS wallpaper: ${processedImage}" + $DRY_RUN_CMD "$WALLPAPER_BIN" set "${processedImage}" --scale ${cfg.scale} + fi + '' + ); - # ── Linux Implementation (wpaperd) ────────────────────────── - # wpaperd is highly declarative via its TOML config. - xdg.configFile."wpaperd/wallpaper.toml" = lib.mkIf (!isDarwin) { - text = '' - [*] - path = "${processedImage}" - apply-to = ["*"] - mode = "${cfg.scale}" - ''; - }; + # ── Linux Implementation (wpaperd) ────────────────────────── + # wpaperd is highly declarative via its TOML config. + xdg.configFile."wpaperd/wallpaper.toml" = lib.mkIf (!isDarwin) { + text = '' + [*] + path = "${processedImage}" + apply-to = ["*"] + mode = "${cfg.scale}" + ''; + }; - # Autostart wpaperd on Wayland/Sway - wayland.windowManager.sway.config.startup = lib.mkIf (!isDarwin) [ - { command = "${pkgs.wpaperd}/bin/wpaperd"; always = true; } - ]; + # Autostart wpaperd on Wayland/Sway + wayland.windowManager.sway.config.startup = lib.mkIf (!isDarwin) [ + { + command = "${pkgs.wpaperd}/bin/wpaperd"; + always = true; + } + ]; - # Ensure Sway background is NOT set by Sway itself to avoid conflicts - wayland.windowManager.sway.config.output."*".bg = lib.mkForce "none"; + # Ensure Sway background is NOT set by Sway itself to avoid conflicts + wayland.windowManager.sway.config.output."*".bg = lib.mkForce "none"; + }; }; - }; # ── System Modules ─────────────────────────────────────────── - flake.modules.darwin.wallpaper = { pkgs, lib, config, ... }: { - options.dendritic.wallpaper.enable = lib.mkEnableOption "Wallpaper management"; - }; - flake.modules.nixos.wallpaper = { pkgs, lib, config, ... }: { - options.dendritic.wallpaper.enable = lib.mkEnableOption "Wallpaper management"; - }; + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + { + options.dendritic.wallpaper.enable = lib.mkEnableOption "Wallpaper management"; + }; + flake.modules.nixos.dendritic = + { + pkgs, + lib, + config, + ... + }: + { + options.dendritic.wallpaper.enable = lib.mkEnableOption "Wallpaper management"; + }; } diff --git a/modules/configurations.nix b/modules/configurations.nix new file mode 100644 index 00000000..58963ab0 --- /dev/null +++ b/modules/configurations.nix @@ -0,0 +1,25 @@ +{ inputs, ... }: +{ + # ── Home Manager standalone configurations ──────────────────────────── + # System-level hosts (mba, mba-dark, mba-asahi, nixos-test) are now owned + # by `modules/host-topology-den.nix`; den auto-generates their + # `flake.{darwin,nixos}Configurations.*` outputs from `den.hosts.*`. + # + # HM-standalone configurations remain hand-rolled here for now (Phase 2 + # will migrate them to `den.homes.*` if/when we extend the framework + # ownership further). + + flake.homeConfigurations."8amps-linux" = inputs.home-manager.lib.homeManagerConfiguration { + pkgs = import inputs.nixpkgs { + system = "x86_64-linux"; + config = { + allowUnfree = true; + }; + }; + extraSpecialArgs = { inherit inputs; }; + modules = [ + inputs.stylix.homeModules.stylix + ../hosts/hm/8amps-linux + ]; + }; +} diff --git a/modules/darwin-appearance-sync.nix b/modules/darwin-appearance-sync.nix new file mode 100644 index 00000000..6110060c --- /dev/null +++ b/modules/darwin-appearance-sync.nix @@ -0,0 +1,598 @@ +{ + flake.modules.darwin.dendritic = + { + lib, + pkgs, + config, + ... + }: + let + user = config.system.primaryUser; + host = config.networking.hostName; + normalizeHex = hex: lib.toLower (lib.removePrefix "#" hex); + themePalette = lib.mapAttrs (_: value: normalizeHex value) config.lib.stylix.colors.withHashtag; + + # ── macOS Tahoe tint colors derived from Stylix blue accent ───────── + # Convert a 2-char lowercase hex string to an integer (0-255). + hexDigits = { + "0" = 0; + "1" = 1; + "2" = 2; + "3" = 3; + "4" = 4; + "5" = 5; + "6" = 6; + "7" = 7; + "8" = 8; + "9" = 9; + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + }; + hexToDec = + hex: + let + chars = lib.stringToCharacters (lib.toLower hex); + in + builtins.foldl' (acc: ch: acc * 16 + hexDigits.${ch}) 0 chars; + hexToRgb = hex: [ + (hexToDec (builtins.substring 0 2 hex)) + (hexToDec (builtins.substring 2 2 hex)) + (hexToDec (builtins.substring 4 2 hex)) + ]; + # Convert a 0-255 integer to a 4-decimal float string (e.g. 40 → "0.1569") + intToFloat = + n: + let + # Fixed-point: multiply by 10000, divide by 255, format as X.XXXX + scaled = builtins.floor (n * 10000 / 255); + whole = builtins.floor (scaled / 10000); + frac = scaled - whole * 10000; + pad4 = + s: + let + l = builtins.stringLength s; + in + if l >= 4 then s else lib.concatStrings (lib.replicate (4 - l) "0") + s; + in + "${toString whole}.${pad4 (toString frac)}"; + # Convert a 6-char hex color to a "R G B 1.00" float string for macOS defaults. + hexToTintStr = + hex: + let + rgb = hexToRgb hex; + in + "${intToFloat (builtins.elemAt rgb 0)} ${intToFloat (builtins.elemAt rgb 1)} ${intToFloat (builtins.elemAt rgb 2)} 1.00"; + + # Pre-computed Tahoe tint string from active Stylix blue accent. + tahoeTint = hexToTintStr themePalette.base0D; + + # Brave variant reload is owned by `modules/apps/brave.nix` and exposed + # as `config.dendritic.brave.reloadScript`. The script knows how to + # quit Brave, re-materialize the Stylix manifest, update the + # Preferences variant, and relaunch the wrapper app. This module just + # invokes it as part of the post-flip hook chain. + braveReloadScript = config.dendritic.brave.reloadScript; + in + { + config = { + environment.etc."dendritic-appearance-sync.sh".text = '' + #!/bin/sh + set -eu + + state_dir="/var/lib/dendritic" + lock_dir="/var/run/dendritic-appearance-sync.lock" + status_file="$state_dir/appearance-status.txt" + applied_file="$state_dir/appearance-variant" + requested_file="$state_dir/appearance-requested" + pending_file="$state_dir/appearance-pending" + dark_path_file="$state_dir/prebuilt-dark-path" + light_path_file="$state_dir/prebuilt-light-path" + dark_rev_file="$state_dir/prebuilt-dark-rev" + light_rev_file="$state_dir/prebuilt-light-rev" + restart_hints_file="$state_dir/restart-hints.txt" + log_file="/var/log/dendritic-appearance-sync.log" + err_log_file="/var/log/dendritic-appearance-sync.err.log" + source_flake_dir="/private/etc/nix-darwin/.dotfiles" + mirror_flake_dir="/private/var/lib/dendritic/flake-source" + + user="${user}" + uid="$(${pkgs.coreutils}/bin/id -u "$user")" + launchctl="/bin/launchctl" + now_cmd="/bin/date" + current_rev="unknown" + + mkdir -p "$state_dir" + chmod 755 "$state_dir" + + sync_flake_source() { + mkdir -p "$mirror_flake_dir" + /usr/bin/rsync -a --delete --delete-excluded \ + --exclude=".git/" \ + --exclude=".cache/" \ + --exclude=".cursor/" \ + --exclude="agent-tools/" \ + --exclude="agent-transcripts/" \ + --exclude="terminals/" \ + --exclude="*.sock" \ + --exclude="result" \ + "$source_flake_dir/" "$mirror_flake_dir/" + } + + detect_rev() { + rev="$(/usr/bin/git -C "$source_flake_dir" rev-parse --verify HEAD 2>/dev/null || true)" + if [ -n "$rev" ]; then + if /usr/bin/git -C "$source_flake_dir" diff --quiet --ignore-submodules=all 2>/dev/null; then + printf "%s\n" "$rev" + else + printf "%s-dirty\n" "$rev" + fi + return + fi + # Keep fallback stable when source is not a git checkout. + # A timestamp here causes false stale-cache mismatches every run. + printf "nogit\n" + } + + detect_desired() { + # Prefer GUI appearance API via osascript (most reliable). + # Fallback to defaults key if GUI query is unavailable. + mode="$("$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" /usr/bin/osascript -e 'tell application "System Events" to tell appearance preferences to get dark mode' 2>/dev/null || true)" + if printf '%s' "$mode" | /usr/bin/grep -Eiq '^(true|1)$'; then + printf "dark\n" + return + fi + if printf '%s' "$mode" | /usr/bin/grep -Eiq '^(false|0)$'; then + printf "light\n" + return + fi + + if /usr/bin/defaults read "/Users/$user/Library/Preferences/.GlobalPreferences" AppleInterfaceStyle 2>/dev/null | /usr/bin/grep -qi dark; then + printf "dark\n" + else + printf "light\n" + fi + } + + notify() { + title="$1" + message="$2" + subtitle="$3" + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" /usr/bin/osascript \ + -e "set t to \"$title\"" \ + -e "set s to \"$subtitle\"" \ + -e "set m to \"$message\"" \ + -e "try" \ + -e " display notification m with title t subtitle s" \ + -e "end try" >/dev/null 2>&1 || true + } + + write_status() { + phase="$1" + detail="$2" + desired="$3" + applied="unknown" + [ -f "$applied_file" ] && applied="$(cat "$applied_file" 2>/dev/null || true)" + + { + echo "phase=$phase" + echo "detail=$detail" + echo "desired=$desired" + echo "applied=$applied" + echo "current_rev=$current_rev" + echo "time=$("$now_cmd" -u +"%Y-%m-%dT%H:%M:%SZ")" + echo "dark_prebuilt=$( [ -f "$dark_path_file" ] && cat "$dark_path_file" || echo missing )" + echo "light_prebuilt=$( [ -f "$light_path_file" ] && cat "$light_path_file" || echo missing )" + echo "" + echo "tail_log:" + /usr/bin/tail -n 25 "$log_file" 2>/dev/null || true + echo "" + echo "tail_err_log:" + /usr/bin/tail -n 25 "$err_log_file" 2>/dev/null || true + echo "" + echo "restart_hints:" + [ -f "$restart_hints_file" ] && /bin/cat "$restart_hints_file" || true + } > "$status_file" + + chmod 644 "$status_file" + } + + resolve_prebuilt() { + mode="$1" + path_file="$dark_path_file" + rev_file="$dark_rev_file" + if [ "$mode" = "light" ]; then + path_file="$light_path_file" + rev_file="$light_rev_file" + fi + + if [ ! -f "$path_file" ] || [ ! -f "$rev_file" ]; then + return 1 + fi + + built_rev="$(cat "$rev_file" 2>/dev/null || true)" + built_path="$(cat "$path_file" 2>/dev/null || true)" + if [ -z "$built_rev" ] || [ -z "$built_path" ]; then + return 1 + fi + if [ "$built_rev" != "$current_rev" ]; then + return 1 + fi + if [ ! -x "$built_path/activate" ]; then + return 1 + fi + + printf "%s\n" "$built_path" + } + + activate_prebuilt() { + desired="$1" + prebuilt_path="$2" + tmp_out="$(/usr/bin/mktemp -t dendritic-activate-out)" + tmp_err="$(/usr/bin/mktemp -t dendritic-activate-err)" + fast_activate_flag="$state_dir/fast-activate" + + write_status "switching" "activating prebuilt profile ($prebuilt_path)" "$desired" + : > "$fast_activate_flag" + chmod 644 "$fast_activate_flag" + set +e + "$prebuilt_path/activate" >"$tmp_out" 2>"$tmp_err" + rc="$?" + set -e + /bin/rm -f "$fast_activate_flag" + /bin/cat "$tmp_out" >> "$log_file" 2>/dev/null || true + /bin/cat "$tmp_err" >> "$err_log_file" 2>/dev/null || true + /bin/rm -f "$tmp_out" "$tmp_err" + + if [ "$rc" -eq 0 ]; then + printf "%s\n" "$desired" > "$applied_file" + chmod 644 "$applied_file" + /bin/sh /etc/dendritic-appearance-reload-hooks.sh >>"$log_file" 2>>"$err_log_file" || true + /usr/bin/printf '%s\n' \ + "Applications with limited hot-reload may still need restart." \ + "Common candidates: Firefox, some Electron apps, GTK apps." > "$restart_hints_file" + chmod 644 "$restart_hints_file" + write_status "done" "activated prebuilt profile successfully" "$desired" + notify "Dendritic Appearance" "Stylix + system now in $desired mode." "Activation complete (no build)" + return 0 + fi + + [ "$rc" -eq 0 ] && rc=1 + write_status "failed" "activation failed with exit code $rc" "$desired" + notify "Dendritic Appearance" "Activation failed for $desired mode." "Open status for details" + return "$rc" + } + + acquire_lock() { + if mkdir "$lock_dir" 2>/dev/null; then + printf "%s\n" "$$" > "$lock_dir/pid" + return 0 + fi + + if [ -f "$lock_dir/pid" ]; then + old_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)" + if [ -n "$old_pid" ] && ! /bin/kill -0 "$old_pid" 2>/dev/null; then + rm -rf "$lock_dir" + if mkdir "$lock_dir" 2>/dev/null; then + printf "%s\n" "$$" > "$lock_dir/pid" + return 0 + fi + fi + fi + + return 1 + } + + cleanup() { + if [ -f "$lock_dir/pid" ] && [ "$(cat "$lock_dir/pid" 2>/dev/null || true)" = "$$" ]; then + rm -rf "$lock_dir" + fi + } + trap cleanup EXIT INT TERM + + latest="$(detect_desired)" + printf "%s\n" "$latest" > "$requested_file" + + if ! acquire_lock; then + printf "1\n" > "$pending_file" + exit 0 + fi + rm -f "$pending_file" + + while true; do + desired="$(cat "$requested_file" 2>/dev/null || detect_desired)" + applied="" + [ -f "$applied_file" ] && applied="$(cat "$applied_file" 2>/dev/null || true)" + + if [ "$desired" = "$applied" ]; then + write_status "idle" "already converged" "$desired" + break + fi + + current_rev="$(detect_rev)" + sync_flake_source + write_status "detected" "detected system appearance change" "$desired" + notify "Dendritic Appearance" "Detected $desired mode; preparing activation." "Activation-only mode (prebuilt)" + + prebuilt_path="$(resolve_prebuilt "$desired" || true)" + if [ -z "$prebuilt_path" ]; then + write_status "failed" "prebuilt profile missing/stale; run nh darwin switch to refresh dark+light prebuilds" "$desired" + notify "Dendritic Appearance" "Prebuilt profile unavailable for $desired mode." "Run nh darwin switch to refresh prebuild cache" + exit 1 + fi + + activate_prebuilt "$desired" "$prebuilt_path" || exit "$?" + + recheck="$(cat "$requested_file" 2>/dev/null || detect_desired)" + if [ "$recheck" = "$desired" ]; then + if [ -f "$pending_file" ]; then + rm -f "$pending_file" + fi + break + fi + printf "1\n" > "$pending_file" + done + ''; + + environment.etc."dendritic-appearance-reload-hooks.sh".text = '' + #!/bin/sh + # App reload hooks after fast activation. + set -eu + + user="${user}" + uid="$(${pkgs.coreutils}/bin/id -u "$user")" + launchctl="/bin/launchctl" + + # ── macOS Tahoe tinting (base0D blue accent from Stylix palette) ───── + # Tint string is pre-computed at Nix eval time from Stylix colors. + apply_tahoe_tinting() { + _tint="${tahoeTint}" + # Icon / widget / folder tint color + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleIconAppearanceTintColor -string "Other" + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleIconAppearanceCustomTintColor -string "$_tint" + # Text highlight color + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleHighlightColor -string "$_tint" + # Accent / theme color (explicit macOS blue + custom variant tint) + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleAccentColor -int 4 + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleAccentColorVariant -string "$_tint" + # Icon style = tinted, auto light/dark switch + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleIconAppearanceStyle -string "Tinted" + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/defaults write -g AppleIconAppearanceMode -string "Auto" + # Bounce UI services so the tint takes effect immediately. + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" \ + /usr/bin/killall Dock Finder SystemUIServer >/dev/null 2>&1 || true + } + + apply_tahoe_tinting + + # Force Ghostty config/theme reload after each mode switch. + # Preferred path for newer Ghostty builds. + if "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" /usr/bin/pkill -USR2 -x "Ghostty" >/dev/null 2>&1; then + : + fi + + # Fallback path for builds/environment where signal reload is unavailable. + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" /usr/bin/osascript <<'APPLESCRIPT' >/dev/null 2>&1 || true + tell application "System Events" + if not (exists process "Ghostty") then return + tell process "Ghostty" + try + click menu item "Reload Configuration" of menu "Ghostty" of menu bar item "Ghostty" of menu bar 1 + on error + try + click menu item "Reload Configuration" of menu "File" of menu bar item "File" of menu bar 1 + end try + end try + end tell + end tell + APPLESCRIPT + + # VS Code/Cursor/Antigravity: intentionally NOT auto-reloading + # editor windows on appearance flip. + # + # The previous implementation tried `Cmd+Shift+P → keystroke + # "Developer: Reload Window"` as a fallback when the menu-bar + # path failed. But VSCode-based editors don't expose a + # top-level `Developer` menu — `Reload Window` is a + # command-palette command — so the menu path ALWAYS errors + # and ALWAYS falls through to the keystroke. If the editor + # isn't actually the focused window at that instant (e.g. + # you're typing in an integrated terminal), AppleScript + # types the literal string into your shell, polluting + # prompts and breaking CLI sessions. + # + # Stylix already re-renders theme files at activation; the + # editors pick up palette changes on their next manual + # reload (Cmd+R / Developer: Reload Window from the user's + # command palette). Tradeoff: one manual keypress vs. + # garbled terminals — keep it manual. + + # Neovim now watches ~/colors.toml directly for Stylix palette changes. + + # Spotify/Spicetify: apply style changes via CLI after quitting the + # client, then reopen if it was previously running. + "$launchctl" asuser "$uid" /usr/bin/sudo -u "$user" /bin/zsh -lc ' + export PATH="/etc/profiles/per-user/'"${user}"'/bin:/run/current-system/sw/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" + spicetify_bin="${ + config.home-manager.users.${user}.programs.spicetify.spicetifyPackage + }/bin/spicetify" + spotify_app="$HOME/Applications/Home Manager Apps/Spotify.app" + was_running=0 + if /usr/bin/pgrep -x "Spotify" >/dev/null 2>&1; then + was_running=1 + /usr/bin/osascript <<'"'"'APPLESCRIPT'"'"' >/dev/null 2>&1 || true + tell application "Spotify" to quit + APPLESCRIPT + + # Wait for full shutdown before touching Spotify files. + i=0 + while /usr/bin/pgrep -x "Spotify" >/dev/null 2>&1 && [ "$i" -lt 40 ]; do + /bin/sleep 0.25 + i=$((i + 1)) + done + fi + + if [ -x "$spicetify_bin" ]; then + "$spicetify_bin" refresh --style --no-restart >/dev/null 2>&1 \ + || "$spicetify_bin" apply --no-restart >/dev/null 2>&1 \ + || "$spicetify_bin" apply >/dev/null 2>&1 \ + || true + fi + + if [ "$was_running" -eq 1 ]; then + /bin/sleep 0.5 + if [ -d "$spotify_app" ]; then + /usr/bin/open "$spotify_app" >/dev/null 2>&1 || true + else + /usr/bin/open -a "Spotify" >/dev/null 2>&1 || true + fi + fi + ' >/dev/null 2>&1 || true + + # Brave: re-materialize the Stylix manifest + variant for the + # newly active appearance, then quit and relaunch the wrapper + # app so Brave re-reads the theme on startup. The reload + # script is generated by `modules/apps/brave.nix` so the + # exact same theme pipeline runs at HM activation time AND + # at runtime — no duplicated logic. + ${braveReloadScript}/bin/brave-stylix-reload >/dev/null 2>&1 || true + + exit 0 + ''; + + environment.etc."dendritic-appearance-status.sh".text = '' + #!/bin/sh + set -eu + status_file="/var/lib/dendritic/appearance-status.txt" + log_file="/var/log/dendritic-appearance-sync.log" + err_log_file="/var/log/dendritic-appearance-sync.err.log" + + if [ -f "$status_file" ]; then + /usr/bin/open -a TextEdit "$status_file" >/dev/null 2>&1 || /usr/bin/open "$status_file" >/dev/null 2>&1 || true + fi + if [ -f "$log_file" ]; then + /usr/bin/open -a Console "$log_file" >/dev/null 2>&1 || true + fi + if [ -f "$err_log_file" ]; then + /usr/bin/open -a Console "$err_log_file" >/dev/null 2>&1 || true + fi + ''; + + launchd.daemons.dendritic-appearance-sync = { + serviceConfig = { + ProgramArguments = [ + "/bin/sh" + "/etc/dendritic-appearance-sync.sh" + ]; + RunAtLoad = true; + WatchPaths = [ "/Users/${user}/Library/Preferences/.GlobalPreferences.plist" ]; + StandardOutPath = "/var/log/dendritic-appearance-sync.log"; + StandardErrorPath = "/var/log/dendritic-appearance-sync.err.log"; + }; + }; + + # Ensure these steps run in a guaranteed activation phase. + system.activationScripts.postActivation.text = lib.mkAfter '' + mkdir -p /var/lib/dendritic + state_dir="/var/lib/dendritic" + + # Fast-activate path (triggered by appearance sync prebuilt activation): + # skip cache prebuilds and daemon kick to avoid recursive activations. + skip_prebuild=0 + if [ -f "$state_dir/fast-activate" ]; then + skip_prebuild=1 + fi + + if [ "$skip_prebuild" -eq 0 ]; then + printf '%s\n' "${config.dendritic.theme.variant}" > /var/lib/dendritic/appearance-variant + chmod 644 /var/lib/dendritic/appearance-variant + + # Prebuild both dark and light profiles during a normal switch, + # so runtime appearance toggles only need to activate prebuilt outputs. + set -eu + src_real="/private/etc/nix-darwin/.dotfiles" + src_mirror="/private/var/lib/dendritic/flake-source" + src="$src_mirror" + nix_bin="${pkgs.nix}/bin/nix" + # `darwinConfigurations.${host}` is light by default. + dark_attr="path:$src#darwinConfigurations.${host}-dark.config.system.build.toplevel" + light_attr="path:$src#darwinConfigurations.${host}.config.system.build.toplevel" + + mkdir -p "$state_dir" + mkdir -p "$src_mirror" + if /usr/bin/rsync -a --delete --delete-excluded \ + --exclude=".git/" \ + --exclude=".cache/" \ + --exclude=".cursor/" \ + --exclude="agent-tools/" \ + --exclude="agent-transcripts/" \ + --exclude="terminals/" \ + --exclude="*.sock" \ + --exclude="result" \ + "$src_real/" "$src_mirror/" \ + && dark_out="$("$nix_bin" build --no-link --print-out-paths "$dark_attr")" \ + && light_out="$("$nix_bin" build --no-link --print-out-paths "$light_attr")" + then + rev="$(/usr/bin/git -C "$src_real" rev-parse --verify HEAD 2>/dev/null || true)" + if [ -n "$rev" ]; then + if ! /usr/bin/git -C "$src_real" diff --quiet --ignore-submodules=all 2>/dev/null; then + rev="$rev-dirty" + fi + else + rev="nogit" + fi + + printf '%s\n' "$dark_out" > "$state_dir/prebuilt-dark-path" + printf '%s\n' "$light_out" > "$state_dir/prebuilt-light-path" + printf '%s\n' "$rev" > "$state_dir/prebuilt-dark-rev" + printf '%s\n' "$rev" > "$state_dir/prebuilt-light-rev" + chmod 644 \ + "$state_dir/prebuilt-dark-path" \ + "$state_dir/prebuilt-light-path" \ + "$state_dir/prebuilt-dark-rev" \ + "$state_dir/prebuilt-light-rev" + else + echo "warning: dendritic appearance prebuild failed; keeping previous prebuilt cache" >&2 + fi + + # macOS appearance is the source of truth. On every switch, request the + # current system appearance and trigger the sync daemon immediately so we + # land on the matching specialization (light/dark) after activation. + desired_variant="$( + /bin/launchctl asuser "$(${pkgs.coreutils}/bin/id -u "${user}")" \ + /usr/bin/sudo -u "${user}" \ + /usr/bin/osascript -e 'tell application "System Events" to tell appearance preferences to get dark mode' 2>/dev/null \ + | /usr/bin/awk '{print tolower($0)}' \ + | /usr/bin/awk '($0=="true" || $0=="1"){print "dark"; ok=1} ($0=="false" || $0=="0"){print "light"; ok=1} END{if(!ok) print ""}' + )" + if [ -z "$desired_variant" ]; then + if /usr/bin/defaults read "/Users/${user}/Library/Preferences/.GlobalPreferences" AppleInterfaceStyle 2>/dev/null | /usr/bin/grep -qi dark; then + desired_variant="dark" + else + desired_variant="light" + fi + fi + printf '%s\n' "$desired_variant" > "$state_dir/appearance-requested" + chmod 644 "$state_dir/appearance-requested" + + /bin/launchctl kickstart -k system/dendritic-appearance-sync >/dev/null 2>&1 || true + + # Apply Tahoe tinting immediately on activation (before the sync daemon + # finishes its first run). The hook reads appearance-variant which was + # written above, so the correct light/dark tint is used right away. + /bin/sh /etc/dendritic-appearance-reload-hooks.sh >>/var/log/dendritic-appearance-sync.log 2>&1 || true + fi + ''; + }; + }; +} diff --git a/modules/darwin-maintenance.nix b/modules/darwin-maintenance.nix index db2bd0cf..b6f9a379 100644 --- a/modules/darwin-maintenance.nix +++ b/modules/darwin-maintenance.nix @@ -6,58 +6,71 @@ # synchronized without needing a separate 'install' script. # { - flake.modules.darwin.maintenance = { pkgs, lib, config, ... }: { - - # ── Activation Scripts ────────────────────────────────────── - # These run during `darwin-rebuild switch` (activated by `nh`). - system.activationScripts.postActivation.text = '' - export PATH="/run/current-system/sw/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" - - maintenance_tasks() { - echo "──────────────────────────────────────────────────────────" - echo " System Maintenance & Synchronization" - echo "──────────────────────────────────────────────────────────" - - # 1. Clean up deprecated settings from Determinate Nix config - if [ -f /etc/nix/nix.conf ]; then - if grep -qE "^eval-cores|^lazy-trees" /etc/nix/nix.conf > /dev/null 2>&1; then - echo " 🧹 Cleaning up deprecated settings in /etc/nix/nix.conf..." - sed -i "" "s/^eval-cores/# eval-cores/" /etc/nix/nix.conf - sed -i "" "s/^lazy-trees/# lazy-trees/" /etc/nix/nix.conf - fi - fi - - # 2. Determinate Nix Maintenance - if command -v determinate-nixd > /dev/null; then - echo " ❄️ Verifying Determinate Nix status..." - STATUS_OUT=$(determinate-nixd status 2>&1 || true) - - if echo "$STATUS_OUT" | grep -qi "determinate-nixd upgrade"; then - echo " ⤓ Determinate Nix update available. Upgrading..." - determinate-nixd upgrade - fi - - if echo "$STATUS_OUT" | grep -qiE "invalid-token|Anonymous|expired|logged out|unauthorized"; then - echo " ⚠️ Action Required: FlakeHub authentication is missing or expired." - echo " Please run 'determinate-nixd login' manually." - fi - - if determinate-nixd version | grep -q "native-linux-builder"; then - echo " 🚀 Native Linux Builder: Access confirmed!" - fi - fi - - # 3. Mac App Store Sync - if [ -x ${config.dendritic.mas.syncScript}/bin/mas-sync ]; then - echo " 🍎 Synchronizing Mac App Store..." - sudo -u ${config.system.primaryUser} ${pkgs.bash}/bin/bash -c "source /etc/profile; ${config.dendritic.mas.syncScript}/bin/mas-sync" - fi - - echo "──────────────────────────────────────────────────────────" - } - - # Force output to stderr so it's more likely to be visible in 'nh' - maintenance_tasks >&2 - ''; - }; + flake.modules.darwin.dendritic = + { + pkgs, + lib, + config, + ... + }: + { + + # ── Activation Scripts ────────────────────────────────────── + # These run during `darwin-rebuild switch` (activated by `nh`). + system.activationScripts.postActivation.text = '' + export PATH="/run/current-system/sw/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" + + maintenance_tasks() { + echo "──────────────────────────────────────────────────────────" + echo " System Maintenance & Synchronization" + echo "──────────────────────────────────────────────────────────" + + if [ -f /var/lib/dendritic/fast-activate ]; then + echo " ⚡ Fast appearance activation: skipping maintenance tasks." + echo "──────────────────────────────────────────────────────────" + return 0 + fi + + # 1. Clean up deprecated settings from Determinate Nix config + if [ -f /etc/nix/nix.conf ]; then + if grep -qE "^eval-cores|^lazy-trees" /etc/nix/nix.conf > /dev/null 2>&1; then + echo " 🧹 Cleaning up deprecated settings in /etc/nix/nix.conf..." + sed -i "" "s/^eval-cores/# eval-cores/" /etc/nix/nix.conf + sed -i "" "s/^lazy-trees/# lazy-trees/" /etc/nix/nix.conf + fi + fi + + # 2. Determinate Nix Maintenance + if command -v determinate-nixd > /dev/null; then + echo " ❄️ Verifying Determinate Nix status..." + STATUS_OUT=$(determinate-nixd status 2>&1 || true) + + if echo "$STATUS_OUT" | grep -qi "determinate-nixd upgrade"; then + echo " ⤓ Determinate Nix update available. Upgrading..." + determinate-nixd upgrade + fi + + if echo "$STATUS_OUT" | grep -qiE "invalid-token|Anonymous|expired|logged out|unauthorized"; then + echo " ⚠️ Action Required: FlakeHub authentication is missing or expired." + echo " Please run 'determinate-nixd login' manually." + fi + + if determinate-nixd version | grep -q "native-linux-builder"; then + echo " 🚀 Native Linux Builder: Access confirmed!" + fi + fi + + # 3. Mac App Store Sync + if [ -x ${config.dendritic.mas.syncScript}/bin/mas-sync ]; then + echo " 🍎 Synchronizing Mac App Store..." + sudo -u ${config.system.primaryUser} ${pkgs.bash}/bin/bash -c "source /etc/profile; ${config.dendritic.mas.syncScript}/bin/mas-sync" + fi + + echo "──────────────────────────────────────────────────────────" + } + + # Force output to stderr so it's more likely to be visible in 'nh' + maintenance_tasks >&2 + ''; + }; } diff --git a/modules/default.nix b/modules/default.nix index 1034c892..a8b1424d 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,102 +1,19 @@ -{ config, lib, inputs, ... }: +{ lib, ... }: let - # Direct imports of modules - shell = import ./shell.nix; - terminal = import ./terminal.nix; - secrets = import ./secrets.nix; - styling = import ./styling.nix; - apps = import ./apps/common.nix; - cursor = import ./apps/cursor.nix; - antigravity = import ./apps/antigravity.nix; - ghostty = import ./apps/ghostty.nix; - beeper = import ./apps/beeper.nix; - jetbrains = import ./apps/jetbrains.nix; - vscode = import ./apps/vscode.nix; - spotify = import ./apps/spotify.nix; - vesktop = import ./apps/vesktop.nix; - dock = import ./dock.nix; - microvm_mod = import ./microvm.nix { inherit inputs; }; - wallpaper = import ./apps/wallpaper.nix; - mas = import ./apps/mas.nix; - python = import ./python.nix; - maintenance = import ./darwin-maintenance.nix; - editor = import ./editor.nix { inherit inputs; }; - opencode = import ./opencode_dummy.nix { inherit inputs; }; - qt = import ./qt_dummy.nix { inherit inputs; }; - linux-desktop = import ./linux-desktop.nix; + # Auto-discover every *.nix file under this directory and import it as a + # top-level flake-parts module. Files named `default.nix` (entrypoints) and + # files whose basename starts with `_` (helpers/private fragments) are + # skipped. This implements the Dendritic auto-import rule: every + # non-entrypoint .nix file is a top-level module. + isAutoImportable = + path: + let + name = baseNameOf path; + in + lib.hasSuffix ".nix" name && name != "default.nix" && !lib.hasPrefix "_" name; + + autoImports = lib.filter isAutoImportable (lib.filesystem.listFilesRecursive ./.); in { - imports = [ - ./overlays.nix - ./mobile.nix - ]; - - config = { - flake = { - nixosModules = { - shell = shell.flake.modules.nixos.shell; - wallpaper = wallpaper.flake.modules.nixos.wallpaper; - styling = styling.flake.modules.nixos.styling; - linux-desktop = linux-desktop.flake.modules.nixos.linux-desktop; - python = python.flake.modules.nixos.python; - }; - - darwinModules = { - dock = dock.flake.modules.darwin.dock; - shell = shell.flake.modules.darwin.shell; - secrets = secrets.flake.modules.darwin.secrets; - styling = styling.flake.modules.darwin.styling; - apps = apps.flake.modules.darwin.apps; - wallpaper = wallpaper.flake.modules.darwin.wallpaper; - mas = mas.flake.modules.darwin.mas; - microvm = microvm_mod.config.flake.modules.darwin.microvm; - maintenance = maintenance.flake.modules.darwin.maintenance; - python = python.flake.modules.darwin.python; - }; - - homeManagerModules = { - shell = shell.flake.modules.homeManager.shell; - terminal = terminal.flake.modules.homeManager.terminal; - editor = editor; - opencode = opencode; - qt = qt; - - secrets = secrets.flake.modules.homeManager.secrets; - styling = styling.flake.modules.homeManager.styling; - apps = apps.flake.modules.homeManager.apps; - cursor = cursor.flake.modules.homeManager.cursor; - antigravity = antigravity.flake.modules.homeManager.antigravity; - ghostty = ghostty.flake.modules.homeManager.ghostty; - beeper = beeper.flake.modules.homeManager.beeper; - jetbrains = jetbrains.flake.modules.homeManager.jetbrains; - vscode = vscode.flake.modules.homeManager.vscode; - spotify = spotify.flake.modules.homeManager.spotify; - vesktop = vesktop.flake.modules.homeManager.vesktop; - wallpaper = wallpaper.flake.modules.homeManager.wallpaper; - python = python.flake.modules.homeManager.python; - theme = import ./theme.nix; - linux-desktop = linux-desktop.flake.modules.homeManager.linux-desktop; - }; - - darwinConfigurations.mba = inputs.nix-darwin.lib.darwinSystem { - specialArgs = { inherit inputs; }; - modules = [ { nixpkgs.config.allowUnsupportedSystem = true; } ../hosts/darwin/mba ]; - }; - - nixosConfigurations = { - # microvm = microvm_mod.config.flake.nixosConfigurations.microvm; - }; - - homeConfigurations."8amps-linux" = inputs.home-manager.lib.homeManagerConfiguration { - pkgs = import inputs.nixpkgs { - system = "x86_64-linux"; - config = { - allowUnfree = true; - }; - }; - extraSpecialArgs = { inherit inputs; }; - modules = [ ../hosts/hm/8amps-linux ]; - }; - }; - }; + imports = autoImports; } diff --git a/modules/dock.nix b/modules/dock.nix index 939858d2..b4d9976a 100644 --- a/modules/dock.nix +++ b/modules/dock.nix @@ -1,53 +1,71 @@ { - flake.modules.darwin.dock = { config, lib, pkgs, ... }: { - options.dendritic.dock.apps = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = []; - description = "Ordered list of .app paths to pin in the Dock."; - }; - - config = { - # System apps appear first in the dock - dendritic.dock.apps = lib.mkBefore [ - "/System/Applications/Apps.app" - ]; + flake.modules.darwin.dendritic = + { + config, + lib, + pkgs, + ... + }: + { + options.dendritic.dock.apps = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Ordered list of .app paths to pin in the Dock."; + }; - system = { - defaults.dock = { - autohide = false; - expose-animation-duration = 0.1; - orientation = "bottom"; - show-recents = false; - showhidden = true; - wvous-bl-corner = 1; - wvous-br-corner = 1; - wvous-tl-corner = 1; - wvous-tr-corner = 1; - show-process-indicators = true; - tilesize = 40; - persistent-apps = config.dendritic.dock.apps; - }; + # Each app module registers its own dock entry via `lib.mkOrder`. + # Lower priority → further left in the dock. + # + # 0 system launchers (`dock.nix`) + # 100 Safari (`apps/safari.nix`) + Firefox (`apps/firefox.nix`) + # 110 Brave (`apps/brave.nix`) + # 120 Spotify (`apps/spotify.nix`) + # 130 Vesktop (`apps/vesktop.nix`) + # 140 Ghostty (`apps/ghostty.nix`) + # 150 JetBrains IDEs (`apps/jetbrains.nix`) + # 160 Cursor (`apps/cursor.nix`) + # 170 Antigravity (`apps/antigravity.nix`) - startup.chime = false; - defaults = { - LaunchServices.LSQuarantine = false; + config = { + # System apps appear first in the dock. + dendritic.dock.apps = lib.mkOrder 0 [ + "/System/Applications/Apps.app" + "/System/Applications/System Settings.app" + ]; - smb = { - NetBIOSName = config.networking.hostName; - ServerDescription = null; + system = { + defaults.dock = { + autohide = false; + expose-animation-duration = 0.1; + minimize-to-application = true; + orientation = "bottom"; + show-recents = false; + showhidden = true; + wvous-bl-corner = 1; + wvous-br-corner = 1; + wvous-tl-corner = 1; + wvous-tr-corner = 1; + show-process-indicators = true; + tilesize = 40; + persistent-apps = config.dendritic.dock.apps; }; - CustomUserPreferences = { - "NSGlobalDomain".ApplePersistence = false; - "com.apple.sidebarlists".systemitems.ShowAirDrop = true; + startup.chime = false; + defaults = { + LaunchServices.LSQuarantine = false; + finder.CreateDesktop = false; + + CustomUserPreferences = { + "NSGlobalDomain".ApplePersistence = false; + "com.apple.sidebarlists".systemitems.ShowAirDrop = true; + }; }; }; - }; - networking.applicationFirewall = { - allowSignedApp = true; - allowSigned = true; + networking.applicationFirewall = { + allowSignedApp = true; + allowSigned = true; + }; }; }; - }; } diff --git a/modules/editor.nix b/modules/editor.nix index dcc6f87b..f1848b8e 100644 --- a/modules/editor.nix +++ b/modules/editor.nix @@ -1,7 +1,1907 @@ -{ inputs, ... }: { - # This is a Home Manager module - options.programs.neovim.initLua = inputs.nixpkgs.lib.mkOption { - type = inputs.nixpkgs.lib.types.lines; - default = ""; - }; +{ + flake.modules.homeManager.dendritic = + { + pkgs, + inputs, + lib, + config, + ... + }: + let + isDarwin = pkgs.stdenv.isDarwin; + in + { + imports = [ inputs.nixvim.homeModules.nixvim ]; + + sops.secrets.openai_api_key = { }; + + programs.nixvim = { + enable = true; + defaultEditor = true; + viAlias = true; + vimAlias = true; + enableMan = false; # Disable man pages to fix the 'options.json' context warning + + # ── Global Options ────────────────────────────────────────── + globals.mapleader = " "; + globals.maplocalleader = ","; + + opts = { + number = true; + relativenumber = true; + shiftwidth = 2; + tabstop = 2; + expandtab = true; + smartindent = true; + wrap = false; + cursorline = true; + scrolloff = 8; + signcolumn = "yes"; + termguicolors = true; + mouse = "a"; + undofile = true; + ignorecase = true; + smartcase = true; + splitbelow = true; + splitright = true; + updatetime = 250; + timeoutlen = 300; + clipboard = "unnamedplus"; + completeopt = "menu,menuone,noselect"; + colorcolumn = "80"; + autoread = true; + sidescroll = 0; + sidescrolloff = 0; + + # Convert E37/E162 "No write since last change" hard-errors + # into an interactive `[Y]es / [N]o / [C]ancel` prompt that + # names each dirty buffer. With `'hidden'` on (Neovim's + # default), background buffers accumulate without being + # visible, so the bare error path makes `:q` and `:qa` feel + # broken whenever any hidden buffer was edited and forgotten + # — you have to hunt them down with `:ls!` and either `:w` + # or `:bd!` each one before quit succeeds. Setting + # `confirm = true` is the standard "modern editor" UX: it + # asks per dirty buffer and lets you save, discard, or + # cancel from the prompt. Equivalent to running `:q` + # commands as `:confirm q` automatically. + confirm = true; + + # Use swap files but handle them automatically via vim-autoswap + swapfile = true; + backup = false; + writebackup = false; + shortmess = "filnxtToOFc"; # Removed 'A' to let vim-autoswap detect the prompt + }; + + # ── Colorscheme: applied via mini.base16 (Stylix nixvim target disabled) ── + + # ── Treesitter ────────────────────────────────────────────── + plugins.treesitter = { + enable = true; + settings = { + highlight.enable = true; + indent.enable = true; + ensure_installed = [ + "bash" + "c" + "cpp" + "css" + "html" + "java" + "javascript" + "json" + "lua" + "markdown" + "markdown_inline" + "nix" + "python" + "rust" + "toml" + "typescript" + "tsx" + "vim" + "vimdoc" + "yaml" + "objc" + "typst" + ]; + }; + }; + + # ── LSP ───────────────────────────────────────────────────── + plugins.lsp = { + enable = true; + inlayHints = false; # Disabled globally to prevent SourceKit-LSP crashes + servers = { + # Nix + nil_ls.enable = true; + # Python + pyright.enable = true; + # TypeScript / JavaScript + ts_ls.enable = true; + # C / C++ / Objective-C + clangd.enable = true; + # Rust (handled by rustaceanvim, do NOT enable rust_analyzer here) + # Java — jdtls with DAP debug support enabled on attach + jdtls = { + enable = true; + # Pass the java-debug plugin JAR so jdtls can handle vscode.java.startDebugSession + extraOptions.init_options.bundles = [ + "${pkgs.vscode-extensions.vscjava.vscode-java-debug}/share/vscode/extensions/vscjava.vscode-java-debug/server/com.microsoft.java.debug.plugin-0.53.2.jar" + ]; + # After jdtls attaches: register DAP + auto-discover main classes + onAttach.function = '' + require('jdtls').setup_dap({ hotcodereplace = 'auto' }) + require('jdtls.dap').setup_dap_main_class_configs() + ''; + }; + # Typst + tinymist.enable = true; + # Lua + lua_ls.enable = true; + # HTML / CSS / JSON + html.enable = true; + cssls.enable = true; + jsonls.enable = true; + # YAML + yamlls.enable = true; + # Bash + bashls.enable = true; + # Assembly (ARMv7 support) + asm_lsp.enable = true; + # Swift + sourcekit = { + enable = true; + # Aggressively disable inlay hints to prevent the -32001 crash + onAttach.function = '' + client.server_capabilities.inlayHintProvider = false + ''; + }; + }; + }; + + # ── Rust (rustaceanvim handles rust-analyzer + DAP) ───────── + plugins.rustaceanvim = { + enable = true; + settings.server.default_settings = { + "rust-analyzer" = { + check.command = "clippy"; + inlayHints = { + closingBraceHints.enable = true; + parameterHints.enable = true; + typeHints.enable = true; + }; + }; + }; + }; + + # ── Completion (blink.cmp — modern, Rust-powered) ─────────── + plugins.blink-cmp = { + enable = true; + # Pin to the overlay-patched derivation. nixvim's default + # `plugins.blink-cmp.package` snapshots `pkgs.vimPlugins.blink-cmp` + # at module-import time and somehow misses our host-level + # `nixpkgs.overlays` override (likely because the nixvim flake + # carries its own evaluation seam for the plugin set). The fix + # is to re-route through the user-scope `pkgs` here, which IS + # overlaid via `home-manager.useGlobalPkgs = true`. Without + # this line the build closure pulls the upstream-broken + # `vimplugin-blink.cmp-1.8.0` and the patch in + # `modules/overlays.nix` becomes a no-op. + package = pkgs.vimPlugins.blink-cmp; + settings = { + # Nix posture for blink.cmp's fuzzy matcher. Pairs with + # the upstream `fuzzy/download/git.lua` patch applied in + # `modules/overlays.nix` (vimPlugins.blink-cmp override) — + # without that patch the Rust path never actually runs on + # Nix, regardless of these settings, because the upstream + # git probe crashes when run inside `/nix/store/...`. + # - `implementation = "prefer_rust"` — try the Rust + # .dylib that nixpkgs builds + symlinks into the + # plugin at `target/release/libblink_cmp_fuzzy.dylib`. + # If it ever fails to load, fall back to Lua without + # the noisy "[blink.cmp] Falling back to Lua…" toast + # emitted by the default "prefer_rust_with_warning" + # (whose advice — `build = 'cargo build --release'` + # in a lazy.nvim spec — has no legitimate analogue + # on Nix and would just confuse future-us). + # - `prebuilt_binaries.download = false` — blink.cmp + # otherwise tries to fetch a pre-built binary from + # GitHub releases at runtime when its version + # bookkeeping disagrees with the on-disk .dylib. Nix + # manages the binary; the runtime fetch path has no + # legitimate use here and must never happen. + fuzzy = { + implementation = "prefer_rust"; + prebuilt_binaries.download = false; + }; + # Tab accepts, Esc cancels, Enter does NOT complete + keymap = { + preset = "none"; + "" = [ + "select_and_accept" + "snippet_forward" + "fallback" + ]; + "" = [ + "snippet_backward" + "fallback" + ]; + "" = [ "fallback" ]; # Enter types a newline only, never completes + "" = [ + "hide" + "fallback" + ]; + "" = [ + "show" + "show_documentation" + "hide_documentation" + ]; + "" = [ "hide" ]; + "" = [ "select_and_accept" ]; + # Manually invoke minuet (force-fetch an OpenAI + # completion right now, regardless of the auto-trigger + # debounce). Returns a function via `make_blink_map()` + # which blink invokes for this key. + "".__raw = ''require("minuet").make_blink_map()''; + "" = [ + "select_prev" + "fallback" + ]; + "" = [ + "select_next" + "fallback" + ]; + }; + sources = { + default = [ + "lsp" + "path" + "snippets" + "buffer" + "minuet" # AI completion via OpenAI API (see plugins.minuet below) + ]; + providers.minuet = { + name = "minuet"; + module = "minuet.blink"; + # Minuet calls OpenAI; blink must not block its main + # loop on the request — let the source resolve async. + async = true; + # Should match `minuet.config.request_timeout * 1000` + # (minuet's setting is in seconds; this one is in ms). + timeout_ms = 3000; + # Bias OpenAI suggestions above LSP/buffer in the + # menu — they're the most expensive to compute, so + # surfacing them is the point. + score_offset = 50; + }; + }; + signature.enabled = true; + completion = { + documentation = { + auto_show = true; + auto_show_delay_ms = 200; + }; + # Avoid double-ghost: minuet has its own `virtualtext` + # frontend that renders Copilot-style inline previews. + # If blink ALSO renders ghost text from its top-ranked + # candidate, the two will overlap and flicker. + ghost_text.enabled = false; + # Don't prefetch on every InsertEnter — minuet runs on + # demand via blink's async source path, so prefetch + # would burn OpenAI tokens for completions the user + # never asked for. + trigger.prefetch_on_insert = false; + list.selection = { + # Don't auto-insert, just highlight — press Tab to accept + preselect = false; + auto_insert = false; + }; + menu = { + # Show keyboard hints in the completion menu border + border = "rounded"; + draw = { + columns = [ + { "__unkeyed-1" = "label"; } + { + "__unkeyed-2" = "label_description"; + gap = 1; + } + { + "__unkeyed-3" = "kind_icon"; + "__unkeyed-4" = "kind"; + gap = 1; + } + ]; + }; + }; + }; + }; + }; + + # Industry-standard code snippets + plugins.friendly-snippets.enable = true; + plugins.luasnip.enable = true; + + # ── LLM Autocomplete (Inline) — minuet-ai.nvim → OpenAI API ─ + # Replaces `copilot-lua`, which spoke to GitHub Copilot's + # proprietary endpoint (a separate paid service, NOT the + # OpenAI API). `minuet-ai.nvim` is a Neovim-native AI + # completion client that posts to OpenAI's chat-completions + # endpoint using our sops-managed `openai_api_key`. It plays + # in two surfaces: + # + # 1. As a `blink.cmp` source (wired in + # `plugins.blink-cmp.settings.sources` above) — so AI + # suggestions appear alongside LSP/buffer entries in + # blink's regular completion menu. + # 2. Via its own `virtualtext` frontend — Copilot-style + # ghost-text inline previews with `` to accept, + # `` / `` to cycle, `` to dismiss + # (matching the old copilot-lua bindings to minimise + # muscle-memory churn). + # + # Key handling: minuet wants a function returning the API + # key (it calls this on every request). We can't pass the + # raw secret in Nix (it would land in the world-readable + # /nix/store), and we won't `vim.env.OPENAI_API_KEY = ...` + # because that leaks into every nvim subprocess (LSP + # servers, formatters, terminals). Instead, an IIFE-built + # closure reads `sops.secrets.openai_api_key.path` once, + # caches the value, and returns it from the inner function + # on each call. The secret never leaves nvim's own memory. + plugins.minuet = { + enable = true; + settings = { + provider = "openai"; + provider_options.openai = { + # gpt-4o-mini: cheapest production-grade chat model, + # ~10x cheaper than gpt-4o and fast enough that + # virtualtext feels live. Swap to `gpt-4.1-mini` or + # `gpt-4o` if you want stronger completions. + model = "gpt-4o-mini"; + stream = true; + api_key.__raw = '' + (function() + local cached + return function() + if cached and cached ~= "" then return cached end + local f = io.open("${config.sops.secrets.openai_api_key.path}", "r") + if not f then return "" end + cached = (f:read("*a") or ""):gsub("%s+$", "") + f:close() + return cached + end + end)() + ''; + }; + # Cost guard: minuet calls OpenAI on every keystroke if + # auto-trigger is on. Limit to filetypes where AI + # completion actually pays off — extend this list as + # needed (e.g. add "markdown" if you want it in prose). + virtualtext = { + auto_trigger_ft = [ + "lua" + "nix" + "python" + "rust" + "go" + "typescript" + "typescriptreact" + "javascript" + "javascriptreact" + "c" + "cpp" + "java" + "swift" + "sh" + "bash" + "zsh" + ]; + keymap = { + accept = ""; + accept_line = ""; + accept_n_lines = ""; + prev = ""; + next = ""; + dismiss = ""; + }; + # Show the virtualtext only when blink's menu is NOT + # already showing a candidate — avoids visual collision. + show_on_completion_menu = false; + }; + # Throttle / debounce on chatty editing — the defaults + # (throttle=1500ms, debounce=400ms) are aggressive + # enough for most flows. Override here if you want. + request_timeout = 3; # seconds — matches blink timeout_ms above + n_completions = 1; # one suggestion per request, keeps spend down + context_window = 16000; + }; + }; + + # Formatting is intentionally delegated to project-local `treefmt`. + + # ── Linting (nvim-lint) ───────────────────────────────────── + plugins.lint = { + enable = true; + lintersByFt = { + python = [ "ruff" ]; + javascript = [ "eslint_d" ]; + typescript = [ "eslint_d" ]; + nix = [ + "statix" + "deadnix" + ]; + sh = [ "shellcheck" ]; + bash = [ "shellcheck" ]; + zsh = [ "shellcheck" ]; + swift = [ "swiftlint" ]; + }; + }; + + # ── Autocommands ────────────────────────────────────────── + autoCmd = [ + # Auto-reload buffers when files change on disk. + { + event = [ + "FocusGained" + "VimResume" + "BufEnter" + "WinEnter" + "CursorHold" + "CursorHoldI" + "TermLeave" + "TermClose" + ]; + command = "if mode() != 'c' | silent! checktime | endif"; + } + { + event = [ "FileChangedShellPost" ]; + callback.__raw = '' + function() + vim.notify("File changed on disk. Buffer reloaded.", vim.log.levels.INFO) + end + ''; + } + # Linting + { + event = [ + "BufWritePost" + "InsertLeave" + ]; + callback.__raw = '' + function() + require('lint').try_lint() + end + ''; + } + # Auto-open Neo-tree on directory + { + event = [ "VimEnter" ]; + callback.__raw = '' + function() + if vim.fn.isdirectory(vim.fn.argv(0)) == 1 then + require("neo-tree.command").execute({ action = "show" }) + end + end + ''; + } + # Auto-show diagnostics under the cursor + { + event = [ + "CursorHold" + "CursorHoldI" + ]; + callback.__raw = '' + function() + -- Close popup when moving cursor or typing + vim.diagnostic.open_float(nil, { + focus = false, + scope = "cursor", + close_events = { "CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre", "WinLeave" } + }) + end + ''; + } + ]; + + # ── Debugging (DAP) ───────────────────────────────────────── + # NOTE: dap-ui is loaded eagerly (no `lazyLoad.settings.cmd`). + # It used to be `cmd = [ "DapUI" ]`, but two consumers reference + # `require("dapui")` at init time: + # 1. The `du` keymap below (`action.__raw = "..."` + # emits the require literally into init.lua, so it runs at + # keymap-registration time, not keypress time). + # 2. The `vim.schedule` block in `extraConfigLua` that wires + # DAP auto-open/close listeners — that's `pcall(require, + # "dapui")`, so silent-fail if the plugin isn't on the + # runtimepath yet. + # Eager-loading dap-ui adds ~negligible startup cost and makes + # both code paths trivially correct. + plugins.dap-ui = { + enable = true; + settings = { + # Auto-open/close the UI when a debug session starts/ends + icons = { + expanded = "▾"; + collapsed = "▸"; + current_frame = "▸"; + }; + layouts = [ + { + elements = [ + { + id = "scopes"; + size = 0.40; + } + { + id = "breakpoints"; + size = 0.20; + } + { + id = "stacks"; + size = 0.20; + } + { + id = "watches"; + size = 0.20; + } + ]; + position = "left"; + size = 40; + } + { + elements = [ + { + id = "repl"; + size = 0.5; + } + { + id = "console"; + size = 0.5; + } + ]; + position = "bottom"; + size = 10; + } + ]; + }; + }; + plugins.dap-virtual-text = { + enable = true; + settings = { + enabled = true; + enabled_commands = true; + highlight_changed_variables = true; + highlight_new_as_changed = true; + show_stop_reason = true; + commented = false; + virt_text_pos = "eol"; # Show values at end of line + }; + }; + plugins.dap = { + enable = true; + adapters = { + executables = { + python = { + command = "${pkgs.python3Packages.debugpy}/bin/debugpy-adapter"; + }; + } + // ( + if isDarwin then + { + lldb = { + command = "lldb-dap"; # From Xcode Command Line Tools + }; + } + else + { + gdb = { + command = "${pkgs.gdb}/bin/gdb"; + args = [ "--interpreter=dap" ]; + }; + } + ); + }; + configurations = { + python = [ + { + name = "Launch file"; + type = "python"; + request = "launch"; + program.__raw = '' + function() + return vim.fn.input('Path to file: ', vim.fn.getcwd() .. '/', 'file') + end + ''; + } + ]; + c = [ + { + name = "Launch (${if isDarwin then "LLDB" else "GDB"})"; + type = if isDarwin then "lldb" else "gdb"; + request = "launch"; + program.__raw = '' + function() + return vim.fn.input('Path to file: ', vim.fn.getcwd() .. '/', 'file') + end + ''; + cwd = "\${workspaceFolder}"; + } + ]; + cpp = [ + { + name = "Launch (${if isDarwin then "LLDB" else "GDB"})"; + type = if isDarwin then "lldb" else "gdb"; + request = "launch"; + program.__raw = '' + function() + return vim.fn.input('Path to file: ', vim.fn.getcwd() .. '/', 'file') + end + ''; + cwd = "\${workspaceFolder}"; + } + ]; + rust = [ + { + name = "Launch (${if isDarwin then "LLDB" else "GDB"})"; + type = if isDarwin then "lldb" else "gdb"; + request = "launch"; + program.__raw = '' + function() + return vim.fn.input('Path to file: ', vim.fn.getcwd() .. '/target/debug/', 'file') + end + ''; + cwd = "\${workspaceFolder}"; + } + ]; + # Java configs are populated dynamically by jdtls.dap.setup_dap_main_class_configs() + # when jdtls attaches — no static entries needed here. + }; + }; + + # ── Lazy loader (lz.n) ────────────────────────────────────── + # Required for ANY `plugins..lazyLoad.settings.{cmd,ft,event,…}` + # declaration to actually do anything. nixvim's `lazyLoad.*` + # options serialize into a `lz.n`-shaped manifest and rely on + # `lz.n` being present at startup to register the stub commands + # / autocmds that trigger the real plugin load on first use. + # + # Without `plugins.lz-n.enable = true;` the lazy declarations + # become silent no-ops: nixvim still pulls the plugin into the + # pack-dir and skips its eager `setup()`, but never registers + # the stub `:Neotree` / `:Yazi` / `:Oil` / `:DiffviewOpen` / + # `:Trouble` commands either — so any keymap or function that + # runs `vim.cmd("Neotree toggle")` (e.g. our + # `dendritic_toggle_neotree_keep_focus`) fails with + # E492: Not an editor command: Neotree toggle + # and the affected plugins are effectively dead. + # + # Telescope appears to work despite the same `lazyLoad.cmd` + # declaration because something else (likely `noice.nvim` or + # `todo-comments`) transitively `require`s it during init — + # but that's incidental, not the lazy-load mechanism. + plugins.lz-n.enable = true; + + # ── Telescope ─────────────────────────────────────────────── + plugins.telescope = { + enable = true; + lazyLoad.settings.cmd = "Telescope"; + extensions = { + fzf-native.enable = true; + ui-select.enable = true; + }; + settings.defaults = { + layout_strategy = "horizontal"; + sorting_strategy = "ascending"; + layout_config.prompt_position = "top"; + }; + }; + + # ── File Browser (neo-tree) ───────────────────────────────── + plugins.neo-tree = { + enable = true; + lazyLoad.settings.cmd = "Neotree"; + settings = { + close_if_last_window = true; + filesystem = { + use_libuv_file_watcher = false; + follow_current_file.enabled = true; + hijack_netrw_behavior = "open_current"; + filtered_items.visible = true; + }; + window.position = "left"; + window.width = 35; + }; + }; + + # ── Dashboard (startify) ───────────────────────────────────── + plugins.startify = { + enable = true; + autoLoad = true; + settings = { + custom_header = [ + " ███╗ ██╗██╗██╗ ██╗██╗ ██╗██╗███╗ ███╗ " + " ████╗ ██║██║╚██╗██╔╝██║ ██║██║████╗ ████║ " + " ██╔██╗ ██║██║ ╚███╔╝ ██║ ██║██║██╔████╔██║ " + " ██║╚██╗██║██║ ██╔██╗ ╚██╗ ██╔╝██║██║╚██╔╝██║ " + " ██║ ╚████║██║██╔╝ ██╗ ╚████╔╝ ██║██║ ╚═╝ ██║ " + " ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ " + " " + " LLM-POWERED CODING ACTIVE " + ]; + session_autoload = false; + update_oldfiles = false; + }; + }; + + # ── Yazi (terminal file manager integration) ─────────────── + plugins.yazi = { + enable = true; + lazyLoad.settings.cmd = "Yazi"; + settings = { + open_for_directories = false; # Let Neo-tree handle directories for now + }; + }; + + # ── Oil (inline file editing) ─────────────────────────────── + plugins.oil = { + enable = true; + lazyLoad.settings.cmd = "Oil"; + settings = { + default_file_explorer = false; + delete_to_trash = true; + view_options.show_hidden = true; + }; + }; + + # ── Git ───────────────────────────────────────────────────── + plugins.gitsigns.enable = true; + plugins.fugitive.enable = true; + plugins.diffview = { + enable = true; + lazyLoad.settings.cmd = [ + "DiffviewOpen" + "DiffviewClose" + "DiffviewFileHistory" + ]; + }; + + # ── UI & Quality of Life ──────────────────────────────────── + plugins.lualine.enable = true; + plugins.bufferline.enable = true; + plugins.which-key.enable = true; + plugins.nvim-autopairs.enable = true; + plugins.indent-blankline.enable = true; + plugins.todo-comments.enable = true; + plugins.trouble = { + enable = true; + lazyLoad.settings.cmd = "Trouble"; + }; + plugins.noice.enable = true; + plugins.notify.enable = true; + plugins.web-devicons.enable = true; + plugins.comment.enable = true; + plugins.illuminate.enable = true; + plugins.toggleterm.enable = true; + plugins.mini = { + enable = true; + modules = { + # base16 is setup manually in extraConfigLua + surround = { }; + bufremove = { }; + }; + }; + + # ── Better Markdown & Codeblocks (matches VSCode/Cursor) ───── + plugins.render-markdown = { + enable = true; + lazyLoad.settings.ft = "markdown"; + settings = { + code = { + sign = false; + width = "block"; + right_pad = 4; + background.hl = "MarkdownCode"; + }; + heading = { + sign = false; + icons = [ + "󰲡 " + "󰲣 " + "󰲥 " + "󰲧 " + "󰲩 " + "󰲫 " + ]; + }; + }; + }; + + # ── Agentic AI Coding (CodeCompanion) ─────────────────────── + extraPlugins = with pkgs.vimPlugins; [ + nvim-jdtls + vim-autoswap # Automatically handle .swp file prompts + nvim-colorizer-lua + (pkgs.vimUtils.buildVimPlugin { + pname = "codecompanion.nvim"; + version = "latest"; + src = pkgs.fetchFromGitHub { + owner = "olimorris"; + repo = "codecompanion.nvim"; + rev = "fcfb7130f570ef2bbb52cbe9167c1999bc41029a"; + hash = "sha256-bcFT8PAFicRgPNAoxzrcAYH1wYJQ6Yu/E94H7M2DNaA="; + }; + dependencies = [ + plenary-nvim + nvim-treesitter + ]; + doCheck = false; + }) + (pkgs.vimUtils.buildVimPlugin { + pname = "eagle.nvim"; + version = "latest"; + src = pkgs.fetchzip { + url = "https://github.com/soulis-1256/eagle.nvim/archive/HEAD.tar.gz"; + sha256 = "1l131sv72mklizpa6yp8dbc52blcvcchmjmbbwm0y4bvl3rk9s0s"; + }; + doCheck = false; + }) + pkgs.vimPlugins.typst-preview-nvim + ]; + + # ── Pre-setup hook: pre-load blink.cmp.fuzzy.rust ───────────── + # Even after `modules/overlays.nix` patches the upstream throw + # bug in `fuzzy/download/git.lua`, blink.cmp's first async + # chain still queues "[blink.cmp] No fuzzy matching library + # found!" onto `nvim_echo`'s UIEnter queue. That happens at + # download/init.lua:27: + # + # if version.current.missing and + # pcall(require, 'blink.cmp.fuzzy.rust') then return end + # + # During the chain context (running synchronously from inside + # `blink-cmp.setup()` at init.lua line ~323), the `pcall(...)` + # returns false for reasons specific to Nix's load-time + # environment — by the time nvim is fully started and the + # user opens a buffer, the very same `require` succeeds, but + # by then the warning has already been queued and rendered + # via `vim.api.nvim_echo` on the `UIEnter` event. + # + # Workaround: pre-load the rust module in `extraConfigLuaPre`, + # which nixvim positions at the top of `init.lua` *before* + # any plugin setup. If the require succeeds at that earlier + # time, the result is cached in `package.loaded[...]`. Then + # when blink.cmp's chain calls `pcall(require, ...)`, Lua's + # require returns the cached module synchronously without + # re-executing the chunk — pcall returns true, the early + # return fires, no warning is queued. + extraConfigLuaPre = '' + -- Touch the rust module once up front so its dylib load, + -- cpath append, and `blink_cmp_fuzzy` registration are + -- all in `package.loaded[...]` by the time blink.cmp's + -- setup chain checks. Wrapped in pcall so a genuine + -- failure (missing dylib, wrong arch, etc.) doesn't + -- crash init — blink will still fall back cleanly to the + -- Lua matcher in that case. + pcall(require, "blink.cmp.fuzzy.rust") + ''; + + extraConfigLua = '' + -- ── Notification history + clipboard copy ────────────────── + -- noice.nvim intercepts `vim.notify` and stores every + -- notification (event = "notify") in its message manager. + -- We tap that history directly via the public API instead + -- of wrapping `vim.notify` ourselves — noice swaps the + -- global wrapper during a deferred plugin-load step that + -- happens AFTER `extraConfigLua` runs, so any wrapper we + -- installed during init.lua would be silently bypassed. + -- + -- API: `noice.message.manager.get(filter, opts)` → + -- `NoiceMessage[]`. Each message responds to `:content()` + -- (joined plain-text representation) and carries a `level` + -- string ("info"/"warn"/"error"/...) plus a `ctime` epoch. + local function dendritic_notify_history() + local ok, manager = pcall(require, "noice.message.manager") + if not ok then return {} end + return manager.get({ event = "notify" }, { history = true, sort = true }) + end + + -- One-shot: copy the most recent notification to the system + -- clipboard. Also writes to the unnamed register so `p` + -- pastes inside Neovim. + function _G.dendritic_yank_last_notification() + local msgs = dendritic_notify_history() + if #msgs == 0 then + vim.notify("No notifications in history", vim.log.levels.WARN) + return + end + local last = msgs[#msgs] + local text = last:content() + vim.fn.setreg("+", text) + vim.fn.setreg('"', text) + vim.notify("Copied last notification (" .. #text .. " chars)", vim.log.levels.INFO) + end + + -- Browse: open a vim.ui.select picker over recent + -- notifications (newest first). With telescope-ui-select + -- enabled, this becomes a Telescope picker; confirming an + -- entry copies that notification's full text to the + -- system clipboard. + function _G.dendritic_pick_notification_to_copy() + local msgs = dendritic_notify_history() + if #msgs == 0 then + vim.notify("No notifications in history", vim.log.levels.WARN) + return + end + local items = {} + for i = #msgs, 1, -1 do + table.insert(items, msgs[i]) + end + vim.ui.select(items, { + prompt = "Copy notification to clipboard:", + format_item = function(m) + local text = m:content() or "" + local first_line = (text:match("[^\n]+") or text):gsub("%s+", " ") + local lvl = tostring(m.level or "?"):upper() + local ts = type(m.ctime) == "number" and os.date("%H:%M:%S", m.ctime) or "--:--:--" + return "[" .. ts .. "] " .. lvl .. " " .. first_line:sub(1, 100) + end, + }, function(choice) + if not choice then return end + local text = choice:content() + vim.fn.setreg("+", text) + vim.fn.setreg('"', text) + vim.notify("Copied (" .. #text .. " chars)", vim.log.levels.INFO) + end) + end + + -- Use Neovim's bytecode loader for faster startup. + if vim.loader then + vim.loader.enable() + + -- Warm vim.loader's `rtp_cached` so cold lookups that + -- happen inside a fast event (`vim.in_fast_event()`) + -- find their modules. vim.loader's `get_rtp()` short- + -- circuits in fast-event context (see + -- `nvim/runtime/lua/vim/loader.lua` around line 93) and + -- returns the cached rtp without ever scanning it — + -- which means if the FIRST cache lookup occurs from a + -- fast event, `rtp_cached` is still its initial `{}`, + -- `find()` returns zero results, and Lua's standard + -- searcher path (which doesn't know about Nvim plugin + -- dirs) fails with the classic + -- cache_loader: module 'X' not found + -- no file '/nix/store/.../luajit2.1-.../share/lua/5.1/X.lua' + -- error. We've hit this with `gitsigns.async` (loaded + -- lazily inside a debounced autocmd that fires from a + -- `:highlight` command issued by `mini.base16` during + -- colorscheme apply — that's the fast event), and the + -- failure mode would repeat for any plugin that defers + -- a `require` into a fast event. + -- + -- Forcing a single `vim.loader.find('vim')` call from + -- this non-fast context populates `rtp_cached` once, + -- after which subsequent fast-event lookups reuse the + -- warm cache and resolve correctly. Pre-requiring the + -- known offender `gitsigns.async` is a redundant + -- belt-and-suspenders so its module table is also in + -- `package.loaded` by the time the debounce fires — + -- which short-circuits `require` before it even + -- consults any searcher. + pcall(vim.loader.find, 'vim') + pcall(require, 'gitsigns.async') + end + + -- ── Auto-open/close DAP UI on session start/end (VSCode-like) ── + vim.schedule(function() + local ok_dap, dap = pcall(require, "dap") + local ok_dapui, dapui = pcall(require, "dapui") + if not (ok_dap and ok_dapui) then + return + end + dapui.setup() + dap.listeners.before.attach.dapui_config = function() dapui.open() end + dap.listeners.before.launch.dapui_config = function() dapui.open() end + dap.listeners.before.event_terminated.dapui_config = function() dapui.close() end + dap.listeners.before.event_exited.dapui_config = function() dapui.close() end + end) + + -- Enable mouse hover events for eagle.nvim + vim.o.mousemoveevent = true + + -- Hot-reload Neovim config when init.lua changes on disk. + -- This catches external updates (e.g. Home Manager switches) without + -- requiring a Neovim restart. + if not vim.g.dendritic_hot_reload_initialized then + vim.g.dendritic_hot_reload_initialized = true + local init_file = vim.fn.stdpath("config") .. "/init.lua" + local palette_file = vim.fn.expand("~/colors.toml") + local init_poll = (vim.uv and vim.uv.new_fs_poll) and vim.uv.new_fs_poll() or nil + local palette_poll = (vim.uv and vim.uv.new_fs_poll) and vim.uv.new_fs_poll() or nil + local uv = vim.uv or vim.loop + local last_palette_mtime = nil + local file_mtime_by_buf = {} + + local function reload_init() + local ok, source_err = pcall(vim.cmd, "silent source " .. vim.fn.fnameescape(init_file)) + if type(_G.dendritic_reload_theme) == "function" then + pcall(_G.dendritic_reload_theme) + end + if ok then + vim.notify("Neovim config reloaded", vim.log.levels.INFO) + else + vim.notify("Neovim config reload failed: " .. tostring(source_err), vim.log.levels.ERROR) + end + end + + local function palette_mtime_key() + if not uv or type(uv.fs_stat) ~= "function" then + return nil + end + local stat = uv.fs_stat(palette_file) + if not stat or not stat.mtime then + return nil + end + return tostring(stat.mtime.sec or 0) .. ":" .. tostring(stat.mtime.nsec or 0) + end + + local function file_mtime_key(path) + if not uv or type(uv.fs_stat) ~= "function" then + return nil + end + local stat = uv.fs_stat(path) + if not stat or not stat.mtime then + return nil + end + return tostring(stat.mtime.sec or 0) .. ":" .. tostring(stat.mtime.nsec or 0) + end + + local function reload_theme_only(notify) + if type(_G.dendritic_reload_theme) == "function" then + local ok, reload_err = pcall(_G.dendritic_reload_theme) + if ok and notify then + vim.notify("Neovim theme reloaded", vim.log.levels.INFO) + elseif not ok then + vim.notify("Neovim theme reload failed: " .. tostring(reload_err), vim.log.levels.ERROR) + end + end + end + + local function track_or_reload_current_file() + if vim.fn.mode() == "c" then + return + end + local bufnr = vim.api.nvim_get_current_buf() + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + if vim.bo[bufnr].buftype ~= "" then + return + end + local path = vim.api.nvim_buf_get_name(bufnr) + if path == "" or vim.fn.filereadable(path) ~= 1 then + return + end + local current_mtime = file_mtime_key(path) + if not current_mtime then + return + end + local known_mtime = file_mtime_by_buf[bufnr] + if not known_mtime then + file_mtime_by_buf[bufnr] = current_mtime + return + end + if current_mtime ~= known_mtime then + file_mtime_by_buf[bufnr] = current_mtime + vim.cmd("silent! checktime") + end + end + + if init_poll then + init_poll:start(init_file, 1000, vim.schedule_wrap(function(err, prev, cur) + if err or not prev or not cur then + return + end + if prev.mtime.sec == cur.mtime.sec and prev.mtime.nsec == cur.mtime.nsec then + return + end + reload_init() + end)) + end + + if palette_poll and vim.fn.filereadable(palette_file) == 1 then + last_palette_mtime = palette_mtime_key() + palette_poll:start(palette_file, 500, vim.schedule_wrap(function(err, prev, cur) + if err or not prev or not cur then + return + end + if prev.mtime.sec == cur.mtime.sec and prev.mtime.nsec == cur.mtime.nsec then + return + end + last_palette_mtime = palette_mtime_key() + reload_theme_only(true) + end)) + end + + -- Home Manager switches can swap the colors.toml symlink in a way fs_poll + -- misses; this focus check catches it and keeps live sessions in sync. + vim.api.nvim_create_autocmd({ "FocusGained", "VimResume", "BufEnter" }, { + callback = function() + if vim.fn.filereadable(palette_file) ~= 1 then + return + end + local current_mtime = palette_mtime_key() + if not current_mtime then + return + end + if not last_palette_mtime then + last_palette_mtime = current_mtime + return + end + if current_mtime ~= last_palette_mtime then + last_palette_mtime = current_mtime + reload_theme_only(true) + end + end, + }) + + vim.api.nvim_create_autocmd({ + "FocusGained", + "VimResume", + "BufEnter", + "WinEnter", + "CursorHold", + "CursorHoldI", + "TermLeave", + "TermClose", + }, { + callback = track_or_reload_current_file, + }) + + vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, { + callback = function(ev) + file_mtime_by_buf[ev.buf] = nil + end, + }) + + if init_poll or palette_poll then + vim.api.nvim_create_autocmd("VimLeavePre", { + callback = function() + pcall(function() + if init_poll then + init_poll:stop() + init_poll:close() + end + if palette_poll then + palette_poll:stop() + palette_poll:close() + end + end) + end, + }) + end + end + + vim.schedule(function() + -- Setup eagle.nvim for VSCode-like mouse hover. + local ok_eagle, eagle = pcall(require, "eagle") + if ok_eagle then + eagle.setup() + end + + -- CodeCompanion setup. + -- + -- Adapter overrides must live under `adapters.http.` + -- (or `adapters.acp.`), NOT at the top level. The + -- resolver in `codecompanion/adapters/http/init.lua` looks + -- up `config.adapters.http[name]`; anything dropped at + -- `config.adapters.openai` is silently ignored and the + -- upstream default — whose `env.api_key = "OPENAI_API_KEY"` + -- is the env-var NAME, expected to be resolved via + -- `os.getenv` — gets used. Without the override the literal + -- string "OPENAI_API_KEY" ends up in the `Authorization: + -- Bearer ''${api_key}` header (because `os.getenv` returns + -- nil when we deliberately keep that env var unset), which + -- OpenAI rejects with `invalid_api_key: OPENAI_A**_KEY`. + -- + -- With the override at `adapters.http.openai`, codecompanion's + -- `utils/adapters.lua` sees the `cmd:` prefix on `api_key`, + -- runs `cat ` once per request via `vim.system`, + -- and uses stdout as the bearer token — no env-var leaks, + -- no plaintext key in the Nix store. + local ok, cc = pcall(require, "codecompanion") + if ok then + cc.setup({ + adapters = { + http = { + openai = function() + return require("codecompanion.adapters").extend("openai", { + env = { + api_key = "cmd:cat ${config.sops.secrets.openai_api_key.path}", + }, + }) + end, + }, + }, + strategies = { + chat = { adapter = "openai" }, + inline = { adapter = "openai" }, + agent = { adapter = "openai" }, + }, + }) + end + end) + + -- Universal clipboard provider for local + SSH workflows. + local function has(bin) + return vim.fn.executable(bin) == 1 + end + local function setup_universal_clipboard() + local is_ssh = (vim.env.SSH_TTY ~= nil) or (vim.env.SSH_CONNECTION ~= nil) or (vim.env.SSH_CLIENT ~= nil) + if is_ssh then + vim.g.clipboard = "osc52" + return + end + if has("pbcopy") and has("pbpaste") then + vim.g.clipboard = { + name = "pbcopy", + copy = { ["+"] = "pbcopy", ["*"] = "pbcopy" }, + paste = { ["+"] = "pbpaste", ["*"] = "pbpaste" }, + cache_enabled = 1, + } + return + end + if has("wl-copy") and has("wl-paste") then + vim.g.clipboard = { + name = "wl-clipboard", + copy = { ["+"] = "wl-copy --foreground --type text/plain", ["*"] = "wl-copy --foreground --primary --type text/plain" }, + paste = { ["+"] = "wl-paste --no-newline", ["*"] = "wl-paste --no-newline --primary" }, + cache_enabled = 1, + } + return + end + if has("xclip") then + vim.g.clipboard = { + name = "xclip", + copy = { ["+"] = "xclip -selection clipboard", ["*"] = "xclip -selection primary" }, + paste = { ["+"] = "xclip -selection clipboard -o", ["*"] = "xclip -selection primary -o" }, + cache_enabled = 1, + } + return + end + if has("xsel") then + vim.g.clipboard = { + name = "xsel", + copy = { ["+"] = "xsel --clipboard --input", ["*"] = "xsel --primary --input" }, + paste = { ["+"] = "xsel --clipboard --output", ["*"] = "xsel --primary --output" }, + cache_enabled = 1, + } + return + end + vim.g.clipboard = "osc52" + end + setup_universal_clipboard() + + -- Keep Neo-tree out of regular tab buffers. + local ok_bufferline, bufferline = pcall(require, "bufferline") + if ok_bufferline then + bufferline.setup({ + options = { + custom_filter = function(bufnr) + return vim.bo[bufnr].filetype ~= "neo-tree" + end, + offsets = { + { + filetype = "neo-tree", + text = "Explorer", + highlight = "Directory", + separator = true, + }, + }, + }, + }) + end + + _G.dendritic_toggle_neotree_keep_focus = function() + local prev_win = vim.api.nvim_get_current_win() + vim.cmd("Neotree toggle") + if vim.bo.filetype == "neo-tree" and vim.api.nvim_win_is_valid(prev_win) then + vim.api.nvim_set_current_win(prev_win) + end + end + + -- ── VS Code / Cursor 1:1 Aesthetic Refinements ──────────────── + + -- Tab / gutter / Neo-tree parity with Stylix + local fallback_palette = { + base00 = "${config.lib.stylix.colors.withHashtag.base00}", + base01 = "${config.lib.stylix.colors.withHashtag.base01}", + base02 = "${config.lib.stylix.colors.withHashtag.base02}", + base03 = "${config.lib.stylix.colors.withHashtag.base03}", + base04 = "${config.lib.stylix.colors.withHashtag.base04}", + base05 = "${config.lib.stylix.colors.withHashtag.base05}", + base06 = "${config.lib.stylix.colors.withHashtag.base06}", + base07 = "${config.lib.stylix.colors.withHashtag.base07}", + base08 = "${config.lib.stylix.colors.withHashtag.base08}", + base09 = "${config.lib.stylix.colors.withHashtag.base09}", + base0A = "${config.lib.stylix.colors.withHashtag.base0A}", + base0B = "${config.lib.stylix.colors.withHashtag.base0B}", + base0C = "${config.lib.stylix.colors.withHashtag.base0C}", + base0D = "${config.lib.stylix.colors.withHashtag.base0D}", + base0E = "${config.lib.stylix.colors.withHashtag.base0E}", + base0F = "${config.lib.stylix.colors.withHashtag.base0F}", + } + local active_palette = vim.deepcopy(fallback_palette) + local function color(name) + return active_palette[name] or fallback_palette[name] + end + + local function apply_style_overrides() + -- Enable Cursive Italics for logic flow (matches premium themes) + vim.api.nvim_set_hl(0, "Comment", { italic = true, fg = color("base03") }) + vim.api.nvim_set_hl(0, "Keyword", { italic = true }) + vim.api.nvim_set_hl(0, "Conditional", { italic = true }) + vim.api.nvim_set_hl(0, "Repeat", { italic = true }) + vim.api.nvim_set_hl(0, "Function", { italic = true, bold = true }) + vim.api.nvim_set_hl(0, "Operator", { fg = color("base05") }) -- Muted operators + + -- High-Fidelity Treesitter / LSP Semantic Token Overrides + -- This makes the syntax tree "pop" like VS Code's TextMate scopes + vim.api.nvim_set_hl(0, "@variable", { fg = color("base05") }) + vim.api.nvim_set_hl(0, "@variable.member", { fg = color("base08") }) -- Fields/Members + vim.api.nvim_set_hl(0, "@property", { fg = color("base08") }) + vim.api.nvim_set_hl(0, "@parameter", { fg = color("base09"), italic = true }) -- Parameters in italics + vim.api.nvim_set_hl(0, "@constructor", { fg = color("base0D"), bold = true }) + + -- Nix Specific Highlighting Refinements + vim.api.nvim_set_hl(0, "@variable.nix", { fg = color("base05") }) + vim.api.nvim_set_hl(0, "@function.call.nix", { fg = color("base0D") }) + + -- Clean up UI elements to match VS Code's "Flat" look + vim.api.nvim_set_hl(0, "LineNr", { fg = color("base02") }) + vim.api.nvim_set_hl(0, "CursorLineNr", { fg = color("base04"), bold = true }) + vim.api.nvim_set_hl(0, "VertSplit", { fg = color("base01"), bg = "NONE" }) + vim.api.nvim_set_hl(0, "WinSeparator", { fg = color("base01"), bg = "NONE" }) + + -- Match VSCode CodeBlock backgrounds + vim.api.nvim_set_hl(0, "MarkdownCode", { bg = color("base01") }) + vim.api.nvim_set_hl(0, "MarkdownCodeBlock", { bg = color("base01") }) + end + + local function apply_stylix_highlights() + local base00 = color("base00") + local base01 = color("base01") + local base02 = color("base02") + local base03 = color("base03") + local base05 = color("base05") + local base0D = color("base0D") + + -- Gutter must stay base00 + vim.api.nvim_set_hl(0, "SignColumn", { bg = base00 }) + vim.api.nvim_set_hl(0, "SignColumnSB", { bg = base00 }) + vim.api.nvim_set_hl(0, "FoldColumn", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "LineNr", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "LineNrAbove", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "LineNrBelow", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "CursorLineNr", { bg = base00, fg = base05, bold = true }) + vim.api.nvim_set_hl(0, "CursorLineFold", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "CursorLineSign", { bg = base00, fg = base03 }) + + -- Neo-tree sidebar/gutter + vim.api.nvim_set_hl(0, "NeoTreeNormal", { bg = base01, fg = base05 }) + vim.api.nvim_set_hl(0, "NeoTreeNormalNC", { bg = base01, fg = base05 }) + vim.api.nvim_set_hl(0, "NeoTreeSignColumn", { bg = base00, fg = base05 }) + vim.api.nvim_set_hl(0, "NeoTreeLineNr", { bg = base00, fg = base03 }) + vim.api.nvim_set_hl(0, "NeoTreeCursorLineNr", { bg = base00, fg = base05, bold = true }) + vim.api.nvim_set_hl(0, "NeoTreeEndOfBuffer", { bg = base01, fg = base01 }) + vim.api.nvim_set_hl(0, "NeoTreeWinSeparator", { bg = base01, fg = base03 }) + + -- Tabs: inactive=base01, active=base02 + vim.api.nvim_set_hl(0, "BufferLineFill", { bg = base00 }) + vim.api.nvim_set_hl(0, "BufferLineBackground", { bg = base01, fg = base03 }) + vim.api.nvim_set_hl(0, "BufferLineBufferVisible", { bg = base01, fg = base05 }) + vim.api.nvim_set_hl(0, "BufferLineBufferSelected", { bg = base02, fg = base05, bold = true }) + vim.api.nvim_set_hl(0, "BufferLineSeparator", { bg = base00, fg = base00 }) + vim.api.nvim_set_hl(0, "BufferLineSeparatorVisible", { bg = base00, fg = base00 }) + vim.api.nvim_set_hl(0, "BufferLineSeparatorSelected", { bg = base00, fg = base00 }) + vim.api.nvim_set_hl(0, "BufferLineIndicatorSelected", { bg = base02, fg = base0D }) + vim.api.nvim_set_hl(0, "TabLine", { bg = base01, fg = base03 }) + vim.api.nvim_set_hl(0, "TabLineSel", { bg = base02, fg = base05, bold = true }) + + -- Ensure filetype icon chips inherit the right tab backgrounds. + for _, group in ipairs(vim.fn.getcompletion("BufferLineDevIcon", "highlight")) do + local current = vim.api.nvim_get_hl(0, { name = group, link = false }) + local fg = current.fg and string.format("#%06x", current.fg) or base05 + local bg = group:match("Selected$") and base02 or base01 + vim.api.nvim_set_hl(0, group, { fg = fg, bg = bg }) + end + end + + local function parse_palette_from_colors_toml(path) + local file = io.open(path, "r") + if not file then + return nil + end + local palette = {} + local in_palette = false + for line in file:lines() do + local section = line:match("^%s*%[([^%]]+)%]%s*$") + if section then + in_palette = (section == "palette") + elseif in_palette then + local key, value = line:match('^%s*(base[%x][%x])%s*=%s*"(#?[%x]+)"%s*$') + if key and value then + palette[key] = value:sub(1, 1) == "#" and value or ("#" .. value) + end + end + end + file:close() + for key in pairs(fallback_palette) do + if not palette[key] then + return nil + end + end + return palette + end + + _G.dendritic_reload_theme = function() + local ok2, base16 = pcall(require, "mini.base16") + if not ok2 then + return + end + local palette_path = vim.fn.expand("~/colors.toml") + active_palette = parse_palette_from_colors_toml(palette_path) or fallback_palette + base16.setup({ palette = active_palette }) + apply_style_overrides() + apply_stylix_highlights() + end + + _G.dendritic_reload_theme() + vim.api.nvim_create_autocmd({ "ColorScheme", "VimEnter", "BufEnter", "WinEnter" }, { + callback = function() + apply_style_overrides() + apply_stylix_highlights() + end, + }) + + -- Neo-tree should not allow horizontal scrolling. + vim.api.nvim_create_autocmd("FileType", { + pattern = "neo-tree", + callback = function(ev) + local b = ev.buf + local function apply_no_hscroll() + for _, w in ipairs(vim.fn.win_findbuf(b)) do + pcall(vim.api.nvim_set_option_value, "wrap", true, { win = w }) + pcall(vim.api.nvim_set_option_value, "linebreak", true, { win = w }) + end + end + local mapopts = { buffer = b, silent = true, noremap = true, nowait = true } + for _, lhs in ipairs({ + "zh", "zl", "zH", "zL", + "", "", "", "", + "", "", + "", "", + "<2-ScrollWheelLeft>", "<2-ScrollWheelRight>", + }) do + vim.keymap.set({ "n", "x", "i" }, lhs, "", mapopts) + end + local function clamp_leftcol() + if vim.bo[b].filetype ~= "neo-tree" then + return + end + for _, w in ipairs(vim.fn.win_findbuf(b)) do + if vim.api.nvim_win_is_valid(w) then + local view = vim.api.nvim_win_call(w, vim.fn.winsaveview) + if view.leftcol ~= 0 then + view.leftcol = 0 + pcall(vim.api.nvim_win_call, w, function() + vim.fn.winrestview(view) + end) + end + end + end + end + vim.api.nvim_create_autocmd({ "WinScrolled", "CursorMoved", "BufEnter", "WinEnter" }, { + buffer = b, + callback = function() + apply_no_hscroll() + clamp_leftcol() + end, + }) + apply_no_hscroll() + clamp_leftcol() + end, + }) + + -- Highlight color literals in source files. + vim.schedule(function() + local colorizer_ok, colorizer = pcall(require, "colorizer") + if colorizer_ok then + colorizer.setup({ + "*", + css = { css = true, css_fn = true, mode = "background" }, + }) + end + end) + + -- Fancy DAP Breakpoint Icons + vim.fn.sign_define("DapBreakpoint", { text = "", texthl = "DiagnosticError", linehl = "", numhl = "" }) + vim.fn.sign_define("DapStopped", { text = "", texthl = "DiagnosticWarn", linehl = "Visual", numhl = "DiagnosticWarn" }) + + -- ── Modern Textutil Integration (RTF/DOC Editing) ───────────── + -- Transparently edit RTF, DOC, and WordML files as plain text + local rtf_group = vim.api.nvim_create_augroup("Textutil", { clear = true }) + + vim.api.nvim_create_autocmd({ "BufReadCmd" }, { + group = rtf_group, + pattern = { "*.rtf", "*.doc", "*.docx", "*.wordml" }, + callback = function(ev) + local file = ev.file + local cmd = string.format("textutil -convert txt -stdout %q", file) + local output = vim.fn.systemlist(cmd) + vim.api.nvim_buf_set_lines(0, 0, -1, false, output) + vim.api.nvim_set_option_value("modified", false, { buf = 0 }) + vim.api.nvim_set_option_value("filetype", "text", { buf = 0 }) + end, + }) + + vim.api.nvim_create_autocmd({ "BufWriteCmd" }, { + group = rtf_group, + pattern = { "*.rtf", "*.doc", "*.docx", "*.wordml" }, + callback = function(ev) + local file = ev.file + local content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), "\n") + local format = file:match("%.(%w+)$") + -- Map extensions to textutil formats + if format == "docx" or format == "doc" then format = "wordml" end + + local cmd = string.format("textutil -convert %s -stdin -output %q", format, file) + vim.fn.system(cmd, content) + vim.api.nvim_set_option_value("modified", false, { buf = 0 }) + vim.notify("Saved as " .. format, vim.log.levels.INFO) + end, + }) + ''; + + # ── Keymaps ───────────────────────────────────────────────── + keymaps = [ + # File browser + { + mode = "n"; + key = "e"; + action = "lua dendritic_toggle_neotree_keep_focus()"; + options.desc = "Toggle Neo-tree"; + } + { + mode = "n"; + key = "-"; + action = "Oil"; + options.desc = "Open Oil"; + } + + # Telescope + { + mode = "n"; + key = "sf"; + action = "Telescope find_files"; + options.desc = "Find Files"; + } + { + mode = "n"; + key = "sg"; + action = "Telescope live_grep"; + options.desc = "Live Grep"; + } + { + mode = "n"; + key = "sb"; + action = "Telescope buffers"; + options.desc = "Buffers"; + } + { + mode = "n"; + key = "sh"; + action = "Telescope help_tags"; + options.desc = "Help Tags"; + } + { + mode = "n"; + key = "sr"; + action = "Telescope oldfiles"; + options.desc = "Recent Files"; + } + { + mode = "n"; + key = "sd"; + action = "Telescope diagnostics"; + options.desc = "Diagnostics"; + } + { + mode = "n"; + key = "ss"; + action = "Telescope lsp_document_symbols"; + options.desc = "Document Symbols"; + } + { + mode = "n"; + key = "sn"; + action = "lua dendritic_pick_notification_to_copy()"; + options.desc = "Search Notifications (copy on select)"; + } + { + mode = "n"; + key = "yn"; + action = "lua dendritic_yank_last_notification()"; + options.desc = "Yank last notification to clipboard"; + } + + # LSP + { + mode = "n"; + key = "gd"; + action = "Telescope lsp_definitions"; + options.desc = "Go to Definition"; + } + { + mode = "n"; + key = "gr"; + action = "Telescope lsp_references"; + options.desc = "References"; + } + { + mode = "n"; + key = "gi"; + action = "Telescope lsp_implementations"; + options.desc = "Implementations"; + } + { + mode = "n"; + key = "K"; + action.__raw = "vim.lsp.buf.hover"; + options.desc = "Hover"; + } + { + mode = "n"; + key = "ca"; + action.__raw = "vim.lsp.buf.code_action"; + options.desc = "Code Action"; + } + { + mode = "n"; + key = "cr"; + action.__raw = "vim.lsp.buf.rename"; + options.desc = "Rename Symbol"; + } + + # Diagnostics + { + mode = "n"; + key = "xx"; + action = "Trouble diagnostics toggle"; + options.desc = "Diagnostics (Trouble)"; + } + { + mode = "n"; + key = "[d"; + action.__raw = "vim.diagnostic.goto_prev"; + options.desc = "Prev Diagnostic"; + } + { + mode = "n"; + key = "]d"; + action.__raw = "vim.diagnostic.goto_next"; + options.desc = "Next Diagnostic"; + } + + # DAP (Debug) + { + mode = "n"; + key = "db"; + action.__raw = "require('dap').toggle_breakpoint"; + options.desc = "Toggle Breakpoint"; + } + { + mode = "n"; + key = "dc"; + action.__raw = "require('dap').continue"; + options.desc = "Continue"; + } + { + mode = "n"; + key = "di"; + action.__raw = "require('dap').step_into"; + options.desc = "Step Into"; + } + { + mode = "n"; + key = "do"; + action.__raw = "require('dap').step_over"; + options.desc = "Step Over"; + } + { + mode = "n"; + key = "dO"; + action.__raw = "require('dap').step_out"; + options.desc = "Step Out"; + } + { + mode = "n"; + key = "du"; + # Wrapped in `function() ... end` so the require resolves at + # keypress time, not at keymap-registration time. Keeps the + # keymap intact even if dap-ui's lazyLoad is reintroduced. + action.__raw = "function() require('dapui').toggle() end"; + options.desc = "Toggle DAP UI"; + } + { + mode = "n"; + key = "dr"; + action.__raw = "require('dap').repl.open"; + options.desc = "Open REPL"; + } + + # Git + { + mode = "n"; + key = "gg"; + action = "Git"; + options.desc = "Git Status (Fugitive)"; + } + { + mode = "n"; + key = "gd"; + action = "DiffviewOpen"; + options.desc = "Diff View"; + } + + # Terminal + { + mode = "n"; + key = "t"; + action = "ToggleTerm"; + options.desc = "Toggle Terminal"; + } + + # AI / Agentic + { + mode = "n"; + key = "ac"; + action = "CodeCompanionChat"; + options.desc = "AI Chat"; + } + { + mode = "v"; + key = "ac"; + action = "CodeCompanionChat"; + options.desc = "AI Chat (selection)"; + } + { + mode = "n"; + key = "ai"; + action = "CodeCompanion"; + options.desc = "AI Inline"; + } + # Minuet (inline OpenAI completion) toggles. `Minuet + # virtualtext` toggles auto ghost-text; `Minuet blink + # toggle` toggles whether minuet auto-fires inside blink's + # menu. Manual fire is always available via `` in + # insert mode (bound in plugins.blink-cmp.settings.keymap). + { + mode = "n"; + key = "av"; + action = "Minuet virtualtext toggle"; + options.desc = "AI Virtualtext (Minuet) toggle"; + } + { + mode = "n"; + key = "ab"; + action = "Minuet blink toggle"; + options.desc = "AI in blink menu (Minuet) toggle"; + } + + # Yazi + { + mode = "n"; + key = "y"; + action = "Yazi"; + options.desc = "Open Yazi"; + } + + # Buffers + { + mode = "n"; + key = "bd"; + action.__raw = "require('mini.bufremove').delete"; + options.desc = "Delete Buffer"; + } + { + mode = "n"; + key = ""; + action = "bprevious"; + options.desc = "Prev Buffer"; + } + { + mode = "n"; + key = ""; + action = "bnext"; + options.desc = "Next Buffer"; + } + + # Window navigation + { + mode = "n"; + key = ""; + action = "h"; + options.desc = "Move Left"; + } + { + mode = "n"; + key = ""; + action = "j"; + options.desc = "Move Down"; + } + { + mode = "n"; + key = ""; + action = "k"; + options.desc = "Move Up"; + } + { + mode = "n"; + key = ""; + action = "l"; + options.desc = "Move Right"; + } + + # Format (project-local treefmt multiplexer) + { + mode = "n"; + key = "f"; + action = "silent !treefmtedit"; + options.desc = "Run treefmt (project)"; + } + # MicroVM + { + mode = "n"; + key = "vm"; + action = "TermExec cmd='microvm-run'"; + options.desc = "Launch MicroVM"; + } + ]; + }; + + # ── Formatter & Linter packages ───────────────────────────── + home.packages = + with pkgs; + [ + treefmt # Project formatter multiplexer + typst # Typst compiler + tinymist # Typst LSP + ruff # Python linting + # Linters + statix # Nix + shellcheck # Shell + eslint_d # JS/TS + asmfmt # Assembly formatter + asm-lsp # Assembly LSP (ARM support) + # VSCode Extensions (available in PATH/store) + vscode-extensions.bbenoist.nix + vscode-extensions.jnoortheen.nix-ide + ] + ++ lib.optionals isDarwin [ + swiftformat # Swift + swiftlint # Swift + sourcekit-lsp # Swift LSP + ] + ++ lib.optionals (!isDarwin) [ + gdb # Debugger (Linux) + xclip + xsel + wl-clipboard + ]; + + # ── Fancy-cat Configuration ────────────────────────────────── + }; } diff --git a/modules/host-topology-den.nix b/modules/host-topology-den.nix new file mode 100644 index 00000000..553bc798 --- /dev/null +++ b/modules/host-topology-den.nix @@ -0,0 +1,227 @@ +{ + inputs, + config, + lib, + ... +}: +let + # Wrap nix-darwin/nixos system builders so they pass `specialArgs.inputs`, + # matching the convention used by the existing hosts///default.nix + # files (they receive `inputs` from specialArgs in our pre-den setup). + # + # Den's default `instantiate` would call inputs.nix-darwin.lib.darwinSystem + # without specialArgs, breaking every host file that destructures `inputs` + # at module entry. + withInputs = + builder: args: + builder ( + args + // { + specialArgs = (args.specialArgs or { }) // { + inherit inputs; + }; + } + ); + darwinSystemWithInputs = withInputs inputs.nix-darwin.lib.darwinSystem; + nixosSystemWithInputs = withInputs inputs.nixpkgs.lib.nixosSystem; +in +{ + # Pull den's flake-parts integration into our flake-parts evaluation. This + # adds the `den.*` option namespace and lets us declare aspects + hosts. + # `den-aspects/styling.nix` defines `den.aspects.styling`; this file owns + # the host topology that consumes it. + imports = [ + inputs.den.flakeModule + ../den-aspects/styling.nix + ]; + + # ── Host declarations ───────────────────────────────────────────────── + # Den auto-generates `flake.{darwin,nixos}Configurations.` from + # these entries by calling each host's `instantiate` with its resolved + # `mainModule`. The resolved mainModule already includes the aspect chain + # (host's own aspect + everything in `includes`). + + den.hosts.aarch64-darwin.mba = { + instantiate = darwinSystemWithInputs; + }; + + den.hosts.aarch64-darwin.mba-dark = { + instantiate = darwinSystemWithInputs; + }; + + den.hosts.aarch64-linux.mba-asahi = { + instantiate = nixosSystemWithInputs; + }; + + den.hosts.aarch64-linux.nixos-test = { + instantiate = nixosSystemWithInputs; + }; + + den.hosts.aarch64-linux.microvm = { + instantiate = nixosSystemWithInputs; + }; + + # ── Host-aspect bindings ────────────────────────────────────────────── + # Each host's aspect: + # • includes the shared styling aspect (gets `os`-class Stylix + per-class + # extras automatically) + # • imports the existing hosts///default.nix raw module via + # the appropriate class body (`darwin` or `nixos`), preserving every + # bit of platform/identity/HM-config that already exists there + # • applies host-specific variant overrides where needed (mba-dark) + + den.aspects.mba = { + includes = [ config.den.aspects.styling ]; + darwin = { + imports = [ + { nixpkgs.config.allowUnsupportedSystem = true; } + ../hosts/darwin/mba + { dendritic.theme.variant = "light"; } + ]; + }; + }; + + den.aspects.mba-dark = { + includes = [ config.den.aspects.styling ]; + darwin = { + imports = [ + { nixpkgs.config.allowUnsupportedSystem = true; } + ../hosts/darwin/mba + { dendritic.theme.variant = lib.mkForce "dark"; } + ]; + }; + }; + + den.aspects.mba-asahi = { + includes = [ config.den.aspects.styling ]; + nixos = { + imports = [ ../hosts/nixos/mba-asahi ]; + }; + }; + + den.aspects.nixos-test = { + includes = [ config.den.aspects.styling ]; + nixos = { + imports = [ ../hosts/nixos/nixos-test ]; + }; + }; + + # The microvm host is a vfkit-hypervised aarch64-linux NixOS guest used as + # a Wayland sandbox from the Darwin host. Its body used to live in + # `modules/microvm.nix` as a hand-rolled `flake.nixosConfigurations.microvm`; + # migrating it here gives it the same aspect-resolution treatment as the + # other hosts (so it picks up `den.aspects.styling`). + den.aspects.microvm = { + includes = [ config.den.aspects.styling ]; + nixos = + { lib, pkgs, ... }: + { + imports = [ + inputs.microvm.nixosModules.microvm + inputs.home-manager.nixosModules.home-manager + inputs.determinate-nix.nixosModules.default + ({ + options.services.displayManager.generic = lib.mkOption { + type = lib.types.raw; + default = { }; + description = "Compatibility option for Stylix GNOME target."; + }; + }) + ]; + + nixpkgs.hostPlatform = "aarch64-linux"; + nixpkgs.config.allowUnfree = true; + + networking.hostName = "dendritic-vm"; + system.stateVersion = "24.11"; + + users.users."8amps" = { + isNormalUser = true; + extraGroups = [ + "wheel" + "video" + "input" + ]; + initialPassword = "nix"; + }; + services.getty.autologinUser = "8amps"; + + microvm = { + hypervisor = "vfkit"; + socket = "/Users/8amps/.local/share/microvm/dendritic-vm.sock"; + vcpu = 2; + mem = 8192; + shares = [ + { + proto = "virtiofs"; + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; + registerWithMachined = false; + vmHostPackages = inputs.nixpkgs.legacyPackages."aarch64-darwin"; + writableStoreOverlay = "/nix/.rw-store"; + volumes = [ + { + image = "/Users/8amps/.local/share/microvm/dendritic-vm.img"; + mountPoint = "/nix/.rw-store"; + size = 20480; + } + ]; + vfkit.logLevel = "info"; + }; + + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.extraSpecialArgs = { inherit inputs; }; + home-manager.users."8amps" = import ../hosts/hm/8amps-linux; + home-manager.sharedModules = [ + { + stylix.targets.neovim.enable = lib.mkForce false; + stylix.targets.neovide.enable = lib.mkForce false; + targets.genericLinux.enable = lib.mkForce false; + programs.firefox.enable = lib.mkForce false; + programs.brave.enable = lib.mkForce false; + } + ]; + + nix.settings = { + sandbox = false; + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + boot.initrd.systemd.enable = false; + boot.kernelPackages = pkgs.linuxPackages; + + environment.variables = { + WLR_RENDERER = "pixman"; + WLR_NO_HARDWARE_CURSORS = "1"; + WLR_BACKENDS = "wayland"; + }; + programs.sway.package = lib.mkForce pkgs.sway; + environment.systemPackages = [ + (lib.hiPrio ( + pkgs.writeShellScriptBin "sway" '' + export XDG_RUNTIME_DIR="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + echo "Connecting to macOS host over VSOCK port 1024... (XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR)" + mkdir -p "$XDG_RUNTIME_DIR" + exec ${pkgs.waypipe}/bin/waypipe \ + --display "$XDG_RUNTIME_DIR/wayland-1" \ + --socket vsock:2:1024 \ + server \ + -- ${pkgs.sway}/bin/sway "$@" + '' + )) + ]; + + documentation.enable = false; + documentation.nixos.enable = false; + documentation.man.enable = false; + documentation.doc.enable = false; + }; + }; + +} diff --git a/modules/linux-desktop.nix b/modules/linux-desktop.nix index 6935b969..a1c8fd6f 100644 --- a/modules/linux-desktop.nix +++ b/modules/linux-desktop.nix @@ -1,27 +1,31 @@ { - flake.modules.nixos.linux-desktop = { pkgs, ... }: { - services.displayManager.ly.enable = true; + flake.modules.nixos.dendritic = + { pkgs, ... }: + { + services.displayManager.ly.enable = true; - programs.sway = { - enable = true; - package = pkgs.swayfx; - extraPackages = with pkgs; [ - swaylock - swayidle - wl-clipboard - mako # notification daemon - alacritty # default terminal - dmenu # application launcher - ]; + programs.sway = { + enable = true; + package = pkgs.swayfx; + extraPackages = with pkgs; [ + swaylock + swayidle + wl-clipboard + mako # notification daemon + alacritty # default terminal + dmenu # application launcher + ]; + }; }; - }; - flake.modules.homeManager.linux-desktop = { pkgs, lib, ... }: { - options.dendritic.apps.linux-desktop = { - enable = lib.mkEnableOption "Linux Desktop (Sway)"; + flake.modules.homeManager.dendritic = + { pkgs, lib, ... }: + { + options.dendritic.apps.linux-desktop = { + enable = lib.mkEnableOption "Linux Desktop (Sway)"; + }; + config = { + # Home Manager specific linux desktop config (empty for now) + }; }; - config = { - # Home Manager specific linux desktop config (empty for now) - }; - }; } diff --git a/modules/microvm.nix b/modules/microvm.nix index 6263683d..cfe524ab 100644 --- a/modules/microvm.nix +++ b/modules/microvm.nix @@ -1,94 +1,29 @@ { inputs, ... }: -# UNIQUE_COMMENT_TO_FORCE_HASH_CHANGE_V1 { config = { # ── Darwin Specific Module ────────────────────────────────── - flake.modules.darwin.microvm = { pkgs, inputs, ... }: { - environment.systemPackages = [ - inputs.determinate-nix.packages.${pkgs.stdenv.hostPlatform.system}.default - # inputs.self.nixosConfigurations.microvm.config.microvm.runner.vfkit - ]; - # environment.shellAliases.microvm-run = "${inputs.self.nixosConfigurations.microvm.config.microvm.runner.vfkit}/bin/microvm-run"; - }; - - # ── The MicroVM Definition ────────────────────────────────── - flake.nixosConfigurations.microvm = inputs.nixpkgs.lib.nixosSystem { - specialArgs = { inherit inputs; }; - modules = [ - inputs.microvm.nixosModules.microvm - inputs.home-manager.nixosModules.home-manager - inputs.determinate-nix.nixosModules.default - inputs.self.nixosModules.shell - # inputs.self.nixosModules.styling - # inputs.self.nixosModules.linux-desktop - - ({ lib, pkgs, ... }: { - nixpkgs.hostPlatform = "aarch64-linux"; - nixpkgs.config.allowUnfree = true; - - networking.hostName = "dendritic-vm"; - system.stateVersion = "24.11"; - - users.users."8amps" = { - isNormalUser = true; - extraGroups = [ "wheel" "video" "input" ]; - initialPassword = "nix"; - }; - services.getty.autologinUser = "8amps"; - - microvm = { - hypervisor = "vfkit"; - socket = "/Users/8amps/.local/share/microvm/dendritic-vm.sock"; - vcpu = 2; mem = 8192; - # vsock.cid = 3; - shares = [{ proto = "virtiofs"; tag = "ro-store"; source = "/nix/store"; mountPoint = "/nix/.ro-store"; }]; - registerWithMachined = false; - vmHostPackages = inputs.nixpkgs.legacyPackages."aarch64-darwin"; - writableStoreOverlay = "/nix/.rw-store"; - volumes = [{ image = "/Users/8amps/.local/share/microvm/dendritic-vm.img"; mountPoint = "/nix/.rw-store"; size = 20480; }]; - vfkit.logLevel = "info"; - }; - - home-manager.useGlobalPkgs = true; - home-manager.useUserPackages = true; - home-manager.extraSpecialArgs = { inherit inputs; }; - home-manager.users."8amps" = import ../hosts/hm/8amps-linux; - - nix.settings = { sandbox = false; experimental-features = [ "nix-command" "flakes" ]; }; - boot.initrd.systemd.enable = false; - boot.kernelPackages = pkgs.linuxPackages; - - # Configure Sway for software rendering - environment.variables = { - WLR_RENDERER = "pixman"; - WLR_NO_HARDWARE_CURSORS = "1"; - # Allow sway to run without a seat/logind if needed (common in microvms) - WLR_BACKENDS = "wayland"; - }; - programs.sway.package = lib.mkForce pkgs.sway; - environment.systemPackages = [ - (lib.hiPrio (pkgs.writeShellScriptBin "sway" '' - # Ensure XDG_RUNTIME_DIR is set (it should be, but let's be safe) - export XDG_RUNTIME_DIR="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" - echo "Connecting to macOS host over VSOCK port 1024... (XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR)" - - # Ensure the runtime directory exists - mkdir -p "$XDG_RUNTIME_DIR" - - exec ${pkgs.waypipe}/bin/waypipe \ - --display "$XDG_RUNTIME_DIR/wayland-1" \ - --socket vsock:2:1024 \ - server \ - -- ${pkgs.sway}/bin/sway "$@" - '')) - ]; - - documentation.enable = false; - documentation.nixos.enable = false; - documentation.man.enable = false; - documentation.doc.enable = false; - }) - ]; - }; + # Provides the `microvm-run` wrapper script that builds the microvm + # runner on demand and execs it. The microvm host itself is declared + # by `modules/host-topology-den.nix` as `den.hosts.aarch64-linux.microvm`. + flake.modules.darwin.dendritic = + { pkgs, inputs, ... }: + let + microvmRunWrapper = pkgs.writeShellApplication { + name = "microvm-run"; + runtimeInputs = [ pkgs.nix ]; + text = '' + set -eu + runner="$(${pkgs.nix}/bin/nix build --no-link --print-out-paths \ + "/etc/nix-darwin/.dotfiles#nixosConfigurations.microvm.config.microvm.runner.vfkit")" + exec "$runner/bin/microvm-run" "$@" + ''; + }; + in + { + environment.systemPackages = [ + inputs.determinate-nix.packages.${pkgs.stdenv.hostPlatform.system}.default + microvmRunWrapper + ]; + }; }; } diff --git a/modules/mobile.nix b/modules/mobile.nix index d98fc060..48f5ccb7 100644 --- a/modules/mobile.nix +++ b/modules/mobile.nix @@ -1,9 +1,20 @@ { - flake.modules.homeManager.mobile = { pkgs, inputs, ... }: { - # If using ansible or similar setup for Sileo packages on a jailbroken iPhone - home.packages = with pkgs; [ - ansible - # Add other iOS automation tools here - ]; - }; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + ... + }: + { + options.dendritic.mobile = { + enable = lib.mkEnableOption "Mobile/iOS automation tooling (ansible et al.)"; + }; + + config = lib.mkIf config.dendritic.mobile.enable { + home.packages = with pkgs; [ + ansible + ]; + }; + }; } diff --git a/modules/opencode_dummy.nix b/modules/opencode_dummy.nix index 01352ae9..3e78fd8e 100644 --- a/modules/opencode_dummy.nix +++ b/modules/opencode_dummy.nix @@ -1,14 +1,17 @@ -{ inputs, ... }: { - # This is a Home Manager module - options.programs.opencode.tui = inputs.nixpkgs.lib.mkOption { - type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; - default = {}; - }; - options.programs.opencode.themes = inputs.nixpkgs.lib.mkOption { - type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; - default = {}; - }; - config = { - programs.opencode.enable = inputs.nixpkgs.lib.mkDefault false; - }; +{ + flake.modules.homeManager.dendritic = + { inputs, ... }: + { + options.programs.opencode.tui = inputs.nixpkgs.lib.mkOption { + type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; + default = { }; + }; + options.programs.opencode.themes = inputs.nixpkgs.lib.mkOption { + type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; + default = { }; + }; + config = { + programs.opencode.enable = inputs.nixpkgs.lib.mkDefault false; + }; + }; } diff --git a/modules/overlays.nix b/modules/overlays.nix index 6e73a7d4..3c333057 100644 --- a/modules/overlays.nix +++ b/modules/overlays.nix @@ -1,132 +1,210 @@ -{ config, lib, ... }: +{ + inputs, + ... +}: { - flake.overlays.default = final: prev: { - beeper = if prev.stdenv.isDarwin then - let - pname = "beeper"; - version = "4.2.770"; - - src = if prev.stdenv.hostPlatform.system == "aarch64-darwin" then - prev.fetchurl { - url = "https://beeper-desktop.download.beeper.com/builds/Beeper-${version}-arm64-mac.zip"; - hash = "sha256-zQPhAp0H3/NN2Ccr85qEbK+4sFG5iCMWJx0TcAfqpXQ="; + flake.overlays.default = + final: prev: + let + unstable = import inputs.nixpkgs-unstable { + system = prev.stdenv.hostPlatform.system; + config.allowUnfree = true; + }; + in + { + code-cursor = unstable.code-cursor; + antigravity = unstable.antigravity; + spotify = unstable.spotify; + vesktop = unstable.vesktop; + firefox = unstable.firefox; + + # ── vimPlugins.blink-cmp: patch upstream "No fuzzy matching + # library found!" false-positive on Nix ───────────────────── + # + # blink.cmp 1.8.0 (and current `main` as of 2026-05) ships a + # `lua/blink/cmp/fuzzy/download/git.lua` whose `get_tag` and + # `get_sha` functions are missing a `return` statement after + # the early-exit `resolve()` call: + # + # local repo_dir = vim.fs.root(files.root_dir, '.git') + # if not repo_dir then resolve() end -- <- missing `return` + # vim.system({ ..., vim.fs.joinpath(repo_dir, '.git'), ... }) + # + # On every other distribution the plugin lives somewhere + # inside the user's `~/.config/nvim/lazy/...` tree, which is + # almost always rooted under a `.git` somewhere up the chain, + # so `vim.fs.root(...)` returns non-nil and the missing-return + # is irrelevant. On Nix the plugin lives in + # `/nix/store/...-vimplugin-blink.cmp/`, which has no `.git` + # ancestor — `vim.fs.root(...)` returns nil, control falls + # through to `vim.fs.joinpath(nil, '.git')`, that throws, + # the async chain falls back to the Lua matcher despite the + # perfectly-functional prebuilt Rust .dylib that nixpkgs + # already symlinked into the plugin's `target/release/`. The + # `[blink.cmp] No fuzzy matching library found! …` message + # gets pushed onto `vim.api.nvim_echo`'s queue, drained on + # the next `UIEnter` event, and surfaces on every interactive + # nvim startup — a Nix-only false positive that no amount of + # `fuzzy.implementation` / `prebuilt_binaries.download` knob + # twiddling can suppress, because the warning fires inside + # the success path's first `:map` BEFORE the implementation + # config gets consulted. + # + # The fix is a 2-line `return` after each `resolve()`. We + # apply it via `substituteInPlace` in `postPatch`; both + # call sites have identical surrounding text so a single + # `--replace-fail` invocation patches them both. If a future + # upstream release fixes this itself, our `--replace-fail` + # will hard-fail at build time and we'll know to drop this + # override. + vimPlugins = prev.vimPlugins // { + blink-cmp = prev.vimPlugins.blink-cmp.overrideAttrs (old: { + postPatch = (old.postPatch or "") + '' + substituteInPlace lua/blink/cmp/fuzzy/download/git.lua \ + --replace-fail \ + 'if not repo_dir then resolve() end' \ + 'if not repo_dir then resolve(); return end' + ''; + }); + }; + + beeper = + if prev.stdenv.isDarwin then + let + pname = "beeper"; + version = "4.2.770"; + + src = + if prev.stdenv.hostPlatform.system == "aarch64-darwin" then + prev.fetchurl { + url = "https://beeper-desktop.download.beeper.com/builds/Beeper-${version}-arm64-mac.zip"; + hash = "sha256-zQPhAp0H3/NN2Ccr85qEbK+4sFG5iCMWJx0TcAfqpXQ="; + } + else if prev.stdenv.hostPlatform.system == "x86_64-darwin" then + prev.fetchurl { + url = "https://beeper-desktop.download.beeper.com/builds/Beeper-${version}-mac.zip"; + hash = "sha256-BVkOWiccjckJOw2ZyW4KIUWMLFEaGJEMmOSysTl3K38="; + } + else + throw "Unsupported macOS architecture for Beeper"; + in + prev.stdenvNoCC.mkDerivation { + inherit pname version src; + + nativeBuildInputs = [ + prev.unzip + prev.makeWrapper + ]; + + sourceRoot = "."; + + installPhase = '' + runHook preInstall + + mkdir -p $out/Applications + cp -r "Beeper Desktop.app" $out/Applications/ + + mkdir -p $out/bin + makeWrapper "$out/Applications/Beeper Desktop.app/Contents/MacOS/Beeper Desktop" $out/bin/beeper + + runHook postInstall + ''; + + meta = prev.beeper.meta // { + platforms = prev.lib.platforms.linux ++ prev.lib.platforms.darwin; + }; } - else if prev.stdenv.hostPlatform.system == "x86_64-darwin" then - prev.fetchurl { - url = "https://beeper-desktop.download.beeper.com/builds/Beeper-${version}-mac.zip"; - hash = "sha256-BVkOWiccjckJOw2ZyW4KIUWMLFEaGJEMmOSysTl3K38="; + else + unstable.beeper; + + # ── Wallpaper Tools ────────────────────────────────────────── + # Sindre Sorhus's wallpaper CLI for macOS (compiled from upstream) + macos-wallpaper = + if prev.stdenv.isDarwin then + let + # We manually provide the dependency resolution to bypass network limitations + # and we use only swift-argument-parser because we patch out SQLite. + generated = prev.swiftpm2nix.helpers ./macos-wallpaper-deps; + in + prev.stdenv.mkDerivation rec { + pname = "macos-wallpaper"; + version = "2.3.4"; + + src = prev.fetchFromGitHub { + owner = "sindresorhus"; + repo = "macos-wallpaper"; + rev = "v${version}"; + hash = "sha256-n4+NdfphU3Z4Gt9Cjv/wNgdnQbEjTJixicPTqTKIuMQ="; + }; + + nativeBuildInputs = [ + prev.swift + prev.swiftpm + ]; + buildInputs = [ prev.apple-sdk ]; + + postPatch = '' + # Patch Package.swift to remove SQLite and set swift-tools-version to 5.10 + cat > Package.swift <<'EOF' + // swift-tools-version:5.10 + import PackageDescription + + let package = Package( + name: "Wallpaper", + platforms: [ + .macOS(.v10_13) + ], + products: [ + .executable( + name: "wallpaper", + targets: ["WallpaperCLI"] + ), + .library( + name: "Wallpaper", + targets: ["Wallpaper"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0") + ], + targets: [ + .executableTarget( + name: "WallpaperCLI", + dependencies: [ + "Wallpaper", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ), + .target( + name: "Wallpaper", + dependencies: [] + ) + ] + ) + EOF + sed -i '/import SQLite/d' Sources/wallpaper/Wallpaper.swift + sed -i '/typealias Expression/d' Sources/wallpaper/Wallpaper.swift + sed -i '/private static func getFromDirectory/,/^\t}/c\ + \tprivate static func getFromDirectory(_ url: URL) throws -> URL {\n\t\treturn url\n\t}' Sources/wallpaper/Wallpaper.swift + sed -i 's/@retroactive //g' Sources/WallpaperCLI/Utilities.swift || true + sed -i 's/@retroactive //g' Sources/WallpaperCLI/Wallpaper.swift || true + ''; + + configurePhase = generated.configure; + + buildPhase = '' + export HOME=$TMPDIR + swift build --configuration release + ''; + + installPhase = '' + binPath="$(swiftpmBinPath)" + mkdir -p $out/bin + cp $binPath/wallpaper $out/bin/ + ''; } - else throw "Unsupported macOS architecture for Beeper"; - - in prev.stdenvNoCC.mkDerivation { - inherit pname version src; - - nativeBuildInputs = [ prev.unzip prev.makeWrapper ]; - - sourceRoot = "."; - - installPhase = '' - runHook preInstall - - mkdir -p $out/Applications - cp -r "Beeper Desktop.app" $out/Applications/ - - mkdir -p $out/bin - makeWrapper "$out/Applications/Beeper Desktop.app/Contents/MacOS/Beeper Desktop" $out/bin/beeper - - runHook postInstall - ''; - - meta = prev.beeper.meta // { - platforms = prev.lib.platforms.linux ++ prev.lib.platforms.darwin; - }; - } - else - prev.beeper; # use original on linux - - # ── Wallpaper Tools ────────────────────────────────────────── - # Sindre Sorhus's wallpaper CLI for macOS (compiled from upstream) - macos-wallpaper = if prev.stdenv.isDarwin then - let - # We manually provide the dependency resolution to bypass network limitations - # and we use only swift-argument-parser because we patch out SQLite. - generated = prev.swiftpm2nix.helpers ./macos-wallpaper-deps; - in - prev.stdenv.mkDerivation rec { - pname = "macos-wallpaper"; - version = "2.3.4"; - - src = prev.fetchFromGitHub { - owner = "sindresorhus"; - repo = "macos-wallpaper"; - rev = "v${version}"; - hash = "sha256-n4+NdfphU3Z4Gt9Cjv/wNgdnQbEjTJixicPTqTKIuMQ="; - }; - - nativeBuildInputs = [ prev.swift prev.swiftpm ]; - buildInputs = [ prev.apple-sdk ]; - - postPatch = '' - # Patch Package.swift to remove SQLite and set swift-tools-version to 5.10 - cat > Package.swift <<'EOF' -// swift-tools-version:5.10 -import PackageDescription - -let package = Package( - name: "Wallpaper", - platforms: [ - .macOS(.v10_13) - ], - products: [ - .executable( - name: "wallpaper", - targets: ["WallpaperCLI"] - ), - .library( - name: "Wallpaper", - targets: ["Wallpaper"] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0") - ], - targets: [ - .executableTarget( - name: "WallpaperCLI", - dependencies: [ - "Wallpaper", - .product(name: "ArgumentParser", package: "swift-argument-parser") - ] - ), - .target( - name: "Wallpaper", - dependencies: [] - ) - ] -) -EOF - sed -i '/import SQLite/d' Sources/wallpaper/Wallpaper.swift - sed -i '/typealias Expression/d' Sources/wallpaper/Wallpaper.swift - sed -i '/private static func getFromDirectory/,/^\t}/c\ -\tprivate static func getFromDirectory(_ url: URL) throws -> URL {\n\t\treturn url\n\t}' Sources/wallpaper/Wallpaper.swift - sed -i 's/@retroactive //g' Sources/WallpaperCLI/Utilities.swift || true - sed -i 's/@retroactive //g' Sources/WallpaperCLI/Wallpaper.swift || true - ''; - - configurePhase = generated.configure; - - buildPhase = '' - export HOME=$TMPDIR - swift build --configuration release - ''; - - installPhase = '' - binPath="$(swiftpmBinPath)" - mkdir -p $out/bin - cp $binPath/wallpaper $out/bin/ - ''; - } - else null; - }; + else + null; + }; } diff --git a/modules/pkgs/_fancy-cat.nix b/modules/pkgs/_fancy-cat.nix index e85b6465..f0c8535b 100644 --- a/modules/pkgs/_fancy-cat.nix +++ b/modules/pkgs/_fancy-cat.nix @@ -38,8 +38,11 @@ pkgs.stdenv.mkDerivation rec { hash = "sha256-bHMrVrS8DuTmVJrFjTzWanhCjf7wC2QBW0Lyi0Wh5Bc="; }; - nativeBuildInputs = [ pkgs.zig_0_15.hook pkgs.pkg-config ]; - + nativeBuildInputs = [ + pkgs.zig_0_15.hook + pkgs.pkg-config + ]; + buildInputs = [ pkgs.mupdf pkgs.harfbuzz @@ -50,7 +53,8 @@ pkgs.stdenv.mkDerivation rec { pkgs.gumbo pkgs.mujs pkgs.libz - ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + ] + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.apple-sdk pkgs.libiconv ]; diff --git a/modules/pkgs/_mas-sync.nix b/modules/pkgs/_mas-sync.nix index acaf64f5..c473b804 100644 --- a/modules/pkgs/_mas-sync.nix +++ b/modules/pkgs/_mas-sync.nix @@ -1,8 +1,19 @@ -{ pkgs, allApps, masPackage, lib }: +{ + pkgs, + allApps, + masPackage, + lib, +}: pkgs.writeShellScriptBin "mas-sync" '' set -euo pipefail - export PATH="${lib.makeBinPath [ masPackage pkgs.coreutils pkgs.gnugrep ]}:$PATH" + export PATH="${ + lib.makeBinPath [ + masPackage + pkgs.coreutils + pkgs.gnugrep + ] + }:$PATH" export MAS_NO_AUTO_INDEX=1 echo "" @@ -19,20 +30,22 @@ pkgs.writeShellScriptBin "mas-sync" '' echo " ⤓ Checking for Mac App Store updates..." mas upgrade || echo " ⚠️ Some updates could not be applied automatically." - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: id: '' - if echo "$INSTALLED" | grep -q "^${toString id} "; then - echo " ✓ ${name} (${toString id}) — already installed" - else - echo " ⤓ Installing ${name} (${toString id})..." - if mas purchase ${toString id}; then - echo " ✓ ${name} — installed successfully" - elif mas install ${toString id}; then - echo " ✓ ${name} — re-installed successfully" + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: id: '' + if echo "$INSTALLED" | grep -q "^${toString id} "; then + echo " ✓ ${name} (${toString id}) — already installed" else - echo " ✗ ${name} — install failed (check App Store sign-in)" >&2 + echo " ⤓ Installing ${name} (${toString id})..." + if mas purchase ${toString id}; then + echo " ✓ ${name} — installed successfully" + elif mas install ${toString id}; then + echo " ✓ ${name} — re-installed successfully" + else + echo " ✗ ${name} — install failed (check App Store sign-in)" >&2 + fi fi - fi - '') allApps)} + '') allApps + )} echo "" echo "══════════════════════════════════════════════════════════" diff --git a/modules/python.nix b/modules/python.nix index 14ba15c8..66491d04 100644 --- a/modules/python.nix +++ b/modules/python.nix @@ -1,6 +1,6 @@ { # ── Python Development Environment ──────────────────────────── - flake.modules.darwin.python = + flake.modules.darwin.dendritic = { pkgs, lib, @@ -10,13 +10,15 @@ let pythonPkg = pkgs.python3; - pythonEnv = pythonPkg.withPackages (ps: with ps; [ - pip - setuptools - wheel - ipython - six - ]); + pythonEnv = pythonPkg.withPackages ( + ps: with ps; [ + pip + setuptools + wheel + ipython + six + ] + ); in { options.dendritic.python = { @@ -48,7 +50,7 @@ }; }; - flake.modules.nixos.python = + flake.modules.nixos.dendritic = { pkgs, lib, @@ -58,13 +60,15 @@ let pythonPkg = pkgs.python3; - pythonEnv = pythonPkg.withPackages (ps: with ps; [ - pip - setuptools - wheel - ipython - six - ]); + pythonEnv = pythonPkg.withPackages ( + ps: with ps; [ + pip + setuptools + wheel + ipython + six + ] + ); in { options.dendritic.python = { @@ -96,7 +100,7 @@ }; }; - flake.modules.homeManager.python = + flake.modules.homeManager.dendritic = { pkgs, lib, @@ -106,13 +110,15 @@ let pythonPkg = pkgs.python3; - pythonEnv = pythonPkg.withPackages (ps: with ps; [ - pip - setuptools - wheel - ipython - six - ]); + pythonEnv = pythonPkg.withPackages ( + ps: with ps; [ + pip + setuptools + wheel + ipython + six + ] + ); in { options.dendritic.python = { diff --git a/modules/qt_dummy.nix b/modules/qt_dummy.nix index 59dbca6c..60d50e84 100644 --- a/modules/qt_dummy.nix +++ b/modules/qt_dummy.nix @@ -1,10 +1,13 @@ -{ inputs, ... }: { - # This is a Home Manager module - options.qt.kvantum = inputs.nixpkgs.lib.mkOption { - type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; - default = {}; - }; - config = { - qt.enable = inputs.nixpkgs.lib.mkDefault false; - }; +{ + flake.modules.homeManager.dendritic = + { inputs, ... }: + { + options.qt.kvantum = inputs.nixpkgs.lib.mkOption { + type = inputs.nixpkgs.lib.types.attrsOf inputs.nixpkgs.lib.unspecified; + default = { }; + }; + config = { + qt.enable = inputs.nixpkgs.lib.mkDefault false; + }; + }; } diff --git a/modules/secrets.nix b/modules/secrets.nix index 70fd4a30..b1837650 100644 --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -1,49 +1,118 @@ +# Dendritic sops-nix feature. +# +# Single-class HM-only export because every consumer in this repo (see +# `modules/editor.nix` openai_api_key) lives in the home-manager class. +# The dendritic pattern explicitly says "only export classes where feature +# applies" +# (docs/dendritic-nix/02-module-mechanics.md "Class-selective exporting"). +# +# If a future system-level consumer (NixOS or nix-darwin sops.secrets.* +# declaration) lands, add a sibling `flake.modules.{nixos,darwin}.dendritic` +# block here following the same shape — keep options surface in +# `dendritic.secrets.*` either way. +# +# Key strategy: ssh-to-age bridge from the user's ed25519 key. No separate +# age private key to back up; the SSH key IS the secret. { - flake.modules.nixos.secrets = { pkgs, inputs, ... }: { - imports = [ inputs.sops-nix.nixosModules.sops ]; - - environment.systemPackages = with pkgs; [ - sops - age - ]; - - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = ../secrets/secrets.yaml; # Ensure this file exists before applying - age = { - keyFile = "/var/lib/sops-nix/key.txt"; # Rotatable age key - generateKey = true; # Generate if missing - }; - }; - }; + flake.modules.homeManager.dendritic = + { + pkgs, + lib, + config, + inputs, + ... + }: + let + ageKeyDir = + if pkgs.stdenv.isDarwin then + "${config.home.homeDirectory}/Library/Application Support/sops/age" + else + "${config.xdg.configHome}/sops/age"; + ageKeyFile = "${ageKeyDir}/keys.txt"; + in + { + imports = [ inputs.sops-nix.homeManagerModules.sops ]; - flake.modules.darwin.secrets = { pkgs, inputs, ... }: { - imports = [ inputs.sops-nix.darwinModules.sops ]; + options.dendritic.secrets = { + enable = lib.mkEnableOption "sops-nix-managed secrets for this Home Manager profile" // { + default = true; + }; - environment.systemPackages = with pkgs; [ - sops - age - ]; + ageKeyPath = lib.mkOption { + type = lib.types.str; + default = "${config.home.homeDirectory}/.ssh/id_ed25519"; + description = '' + Path to the SSH ed25519 private key that sops-nix derives the + age decryption key from (via ssh-to-age). Hosts can override + this if they prefer a non-default SSH key location. + ''; + }; - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = ../secrets/secrets.yaml; - age.keyFile = "/var/lib/sops-nix/key.txt"; - }; - }; + defaultSopsFile = lib.mkOption { + type = lib.types.path; + default = ../secrets/secrets.yaml; + description = '' + Default sops-encrypted YAML file consumed by + `sops.secrets.` declarations that don't override + `sopsFile`. Hosts can point this at a host-scoped secrets + file if they need per-machine isolation. + ''; + }; - flake.modules.homeManager.secrets = { pkgs, inputs, ... }: { - imports = [ inputs.sops-nix.homeManagerModules.sops ]; + ageKeyFile = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = ageKeyFile; + description = '' + Read-only path where the activation script materializes an + age keys file derived from `ageKeyPath` (via ssh-to-age) so + the `sops` CLI can decrypt without `SOPS_AGE_*` env vars. + Linux uses `$XDG_CONFIG_HOME/sops/age/keys.txt`; Darwin uses + `~/Library/Application Support/sops/age/keys.txt` (sops's + canonical Darwin default). + ''; + }; + }; - home.packages = with pkgs; [ - sops - age - ]; + config = lib.mkIf config.dendritic.secrets.enable { + home.packages = with pkgs; [ + sops + age + ssh-to-age + ]; - sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = ../secrets/secrets.yaml; - age.keyFile = "/home/admin/.config/sops/age/keys.txt"; + # sops-nix at activation: SSH → age conversion happens internally + # against `sshKeyPaths`. This is the source of truth that NIX + # decryption uses; it does NOT depend on the CLI keys.txt below. + sops = { + defaultSopsFormat = "yaml"; + defaultSopsFile = config.dendritic.secrets.defaultSopsFile; + age.sshKeyPaths = [ config.dendritic.secrets.ageKeyPath ]; + }; + + # sops CLI side: the CLI does NOT use sshKeyPaths. By default it + # checks `SOPS_AGE_SSH_PRIVATE_KEY_FILE`, `/Users/$USER/.ssh/id_rsa` + # (hardcoded, ignores id_ed25519), then a handful of env vars. To + # make `sops edit secrets/...yaml` work out of the box with no + # shell setup, materialize the derived age private key at the + # location sops looks at by default on this platform. Idempotent; + # overwritten on every activation from the current SSH key. + home.activation.materializeSopsAgeKey = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + _sshKey="${config.dendritic.secrets.ageKeyPath}" + _ageDir=${lib.escapeShellArg ageKeyDir} + _ageFile=${lib.escapeShellArg ageKeyFile} + if [ -r "$_sshKey" ]; then + mkdir -p "$_ageDir" + chmod 0700 "$_ageDir" + ${pkgs.ssh-to-age}/bin/ssh-to-age -private-key -i "$_sshKey" -o "$_ageFile" + chmod 0600 "$_ageFile" + fi + ''; + + # Belt + suspenders: shells inherit a pointer to the keys file so + # subprocess sops invocations (GUI launchers, agents, scripts) + # find it even if Darwin's default-path probe ever changes. + home.sessionVariables.SOPS_AGE_KEY_FILE = ageKeyFile; + }; }; - }; } diff --git a/modules/shell.nix b/modules/shell-config.nix similarity index 79% rename from modules/shell.nix rename to modules/shell-config.nix index d108612d..fe2b84ce 100644 --- a/modules/shell.nix +++ b/modules/shell-config.nix @@ -1,5 +1,5 @@ { - flake.modules.nixos.shell = + flake.modules.nixos.dendritic = { pkgs, lib, @@ -22,9 +22,14 @@ // (lib.optionalAttrs (options ? environment && options.environment ? shells) { shells = [ pkgs.zsh ]; }); + + security.sudo.extraConfig = '' + # 120 min: authenticate once, then reuse for a long session. + Defaults timestamp_timeout=120 + ''; }; - flake.modules.darwin.shell = + flake.modules.darwin.dendritic = { pkgs, inputs, ... }: { programs.zsh.enable = true; @@ -43,9 +48,14 @@ sudo mkdir -p /Library/Java/JavaVirtualMachines sudo ln -sfn ${pkgs.jdk21}/Library/Java/JavaVirtualMachines/zulu-21.jdk /Library/Java/JavaVirtualMachines/nix-jdk-21.jdk ''; + + security.sudo.extraConfig = '' + # 120 min: authenticate once, then reuse for a long session. + Defaults timestamp_timeout=120 + ''; }; - flake.modules.homeManager.shell = + flake.modules.homeManager.dendritic = { pkgs, config, @@ -68,7 +78,7 @@ programs.zsh = { enable = true; - enableCompletion = true; + enableCompletion = false; # We'll manage compinit manually for micro-optimization autosuggestion.enable = true; syntaxHighlighting.enable = true; # Use zsh from nixpkgs to override macOS default @@ -97,10 +107,30 @@ initContent = lib.mkMerge [ + # ── zsh-defer (must be loaded incredibly early) ── + (lib.mkOrder 100 '' + source ${pkgs.zsh-defer}/share/zsh-defer/zsh-defer.plugin.zsh + '') + + # ── Micro-optimized compinit ── + (lib.mkOrder 200 '' + # Load completions but only compile the cache if older than 24 hours + autoload -Uz compinit + if [[ -n ''${ZDOTDIR:-$HOME}/.zcompdump(#qN.mh+24) ]]; then + compinit -C + else + compinit + # Compile to zsh native bytecode in the background + zsh-defer zcompile "''${ZDOTDIR:-$HOME}/.zcompdump" + fi + + # Load the natively compiled bytecode cache + autoload -Uz bashcompinit && bashcompinit + '') # ── fzf-tab (must load after compinit, before other plugins) ── (lib.mkOrder 550 '' - source ${pkgs.zsh-fzf-tab}/share/fzf-tab/fzf-tab.plugin.zsh + zsh-defer source ${pkgs.zsh-fzf-tab}/share/fzf-tab/fzf-tab.plugin.zsh '') # ── Core plugins & integrations ── @@ -110,16 +140,20 @@ bindkey '^[[B' history-substring-search-down # ── zsh-you-should-use ── - source ${pkgs.zsh-you-should-use}/share/zsh/plugins/you-should-use/you-should-use.plugin.zsh + zsh-defer source ${pkgs.zsh-you-should-use}/share/zsh/plugins/you-should-use/you-should-use.plugin.zsh # ── zsh-vi-mode ── - source ${pkgs.zsh-vi-mode}/share/zsh-vi-mode/zsh-vi-mode.plugin.zsh + zsh-defer source ${pkgs.zsh-vi-mode}/share/zsh-vi-mode/zsh-vi-mode.plugin.zsh # ── forgit (interactive git via fzf) ── - source ${pkgs.zsh-forgit}/share/zsh/zsh-forgit/forgit.plugin.zsh + zsh-defer source ${pkgs.zsh-forgit}/share/zsh/zsh-forgit/forgit.plugin.zsh # ── any-nix-shell (stay in zsh inside nix-shell/nix develop) ── - ${pkgs.any-nix-shell}/bin/any-nix-shell zsh --info-right | source /dev/stdin + zsh-defer eval "$(${pkgs.any-nix-shell}/bin/any-nix-shell zsh --info-right)" + + # ── fzf explicit deferred loading ── + zsh-defer source ${pkgs.fzf}/share/fzf/key-bindings.zsh + zsh-defer source ${pkgs.fzf}/share/fzf/completion.zsh # ── sudo toggle (ESC ESC) ── function _sudo_toggle() { @@ -174,7 +208,7 @@ # ── fzf (fuzzy finder with Ctrl+R, Ctrl+T, Alt+C) ── programs.fzf = { enable = true; - enableZshIntegration = true; + enableZshIntegration = false; # We defer it manually! defaultCommand = "${pkgs.fd}/bin/fd --type f --hidden --follow --exclude .git"; changeDirWidgetCommand = "${pkgs.fd}/bin/fd --type d --hidden --follow --exclude .git"; defaultOptions = [ @@ -220,7 +254,7 @@ # ── nix-index (command-not-found integration) ── programs.nix-index = { enable = true; - enableZshIntegration = true; + enableZshIntegration = false; # Too heavy to run synchronously }; programs.starship = { @@ -251,14 +285,13 @@ NH_FLAKE = (if pkgs.stdenv.isDarwin then "/etc/nix-darwin/.dotfiles#mba" else "/etc/nixos"); }; - # Yazi minimal configuration with ANSI inheritance programs.yazi = { enable = true; enableZshIntegration = true; shellWrapperName = "y"; settings = { - manager = { + mgr = { show_hidden = true; sort_by = "natural"; }; @@ -272,7 +305,7 @@ + "/mount.yazi"; }; keymap = { - manager.prepend_keymap = [ + mgr.prepend_keymap = [ { on = [ "M" ]; run = "plugin mount"; @@ -295,6 +328,7 @@ home.packages = with pkgs; [ nh + zsh-defer zsh-completions nix-zsh-completions # Tab completions for nix CLI comma # Run any nixpkgs binary without installing: , cowsay hello diff --git a/modules/sops-validation.nix b/modules/sops-validation.nix new file mode 100644 index 00000000..69596505 --- /dev/null +++ b/modules/sops-validation.nix @@ -0,0 +1,112 @@ +# Dendritic feature: `nix flake check` validates every sops-encrypted +# file in the repo is structurally a valid sops document. +# +# Why this exists: nothing currently catches "I accidentally committed a +# plaintext PEM under a .sops name" or "I forgot `sops updatekeys` after +# a recipient change and broke the MAC". Per-secret runtime decryption +# would catch it, but only at HM activation time on every machine, which +# is too late. This check runs at flake-eval time and is cheap. +# +# The check is structural, not cryptographic: +# - it does NOT decrypt (no key material needed in CI), +# - it asserts each file has a top-level `sops` metadata block with +# `mac`, `lastmodified`, and `version` fields. +# +# Add new sops-encrypted files to `sopsFiles` below as the surface grows. +{ + perSystem = + { pkgs, lib, ... }: + let + sopsFiles = [ + ../secrets/secrets.yaml + ]; + + # Interpolate each path individually so Nix string-context is + # preserved per file (each becomes a proper store-path reference + # in the resulting derivation). `toString` on a list of paths + # would flatten them into a context-free string and produce a + # noisy `builtins.derivation … without a proper context` warning. + sopsFilesArgs = lib.concatMapStringsSep " " (p: "${p}") sopsFiles; + + checker = + pkgs.writers.writePython3 "sops-parse-check" + { + libraries = [ pkgs.python3Packages.pyyaml ]; + flakeIgnore = [ + "E501" # line length: the path arg list is naturally long + "W503" # line break before binary operator (no longer recommended) + ]; + } + '' + import json + import sys + from pathlib import Path + + import yaml + + + REQUIRED_METADATA_KEYS = {"mac", "lastmodified", "version"} + + + def load_sops(path: Path): + """Parse a sops file as either JSON envelope or YAML envelope.""" + text = path.read_text() + stripped = text.lstrip() + if stripped.startswith("{"): + return json.loads(text) + return yaml.safe_load(text) + + + def validate(path: Path) -> str | None: + try: + doc = load_sops(path) + except Exception as exc: + return f"failed to parse as JSON or YAML: {exc}" + + if not isinstance(doc, dict): + return f"top-level is {type(doc).__name__}, expected mapping" + + sops_meta = doc.get("sops") + if sops_meta is None: + return "missing top-level `sops` metadata block (file is not sops-encrypted?)" + if not isinstance(sops_meta, dict): + return f"`sops` block is {type(sops_meta).__name__}, expected mapping" + + missing = REQUIRED_METADATA_KEYS - sops_meta.keys() + if missing: + return f"`sops` block missing required keys: {sorted(missing)}" + + return None + + + def main() -> int: + files = [Path(p) for p in sys.argv[1:]] + failures = [] + for f in files: + if not f.exists(): + failures.append(f"{f}: file does not exist") + continue + err = validate(f) + if err: + failures.append(f"{f}: {err}") + else: + print(f"ok: {f}") + + if failures: + print("\nsops-files-parse FAILED:", file=sys.stderr) + for line in failures: + print(f" - {line}", file=sys.stderr) + return 1 + return 0 + + + sys.exit(main()) + ''; + in + { + checks.sops-files-parse = pkgs.runCommand "sops-files-parse" { } '' + ${checker} ${sopsFilesArgs} + touch $out + ''; + }; +} diff --git a/modules/styling.nix b/modules/styling.nix deleted file mode 100644 index 3e51f63c..00000000 --- a/modules/styling.nix +++ /dev/null @@ -1,231 +0,0 @@ -{ - # ── NixOS Styling ───────────────────────────────────────────── - flake.modules.nixos.styling = { - pkgs, - inputs, - lib, - ... - }: { - imports = [ - inputs.stylix.nixosModules.stylix - ./theme.nix - ]; - - stylix = { - enable = true; - enableReleaseChecks = false; - polarity = "dark"; - - fonts = { - monospace = { - package = pkgs.maple-mono.NF; - name = "Maple Mono NF"; - }; - sansSerif = { - package = pkgs.inter; - name = "Inter"; - }; - serif = { - package = pkgs.noto-fonts; - name = "Noto Serif"; - }; - sizes = { - terminal = 12; - applications = 12; - desktop = 11; - }; - }; - - cursor = { - package = pkgs.bibata-cursors; - name = "Bibata-Modern-Ice"; - size = 24; - }; - - opacity = { - terminal = 1.0; - popups = 0.95; - }; - }; - }; - - # ── Darwin Styling (System-level) ───────────────────────────── - flake.modules.darwin.styling = { - pkgs, - inputs, - lib, - ... - }: { - imports = [ - inputs.stylix.darwinModules.stylix - ./theme.nix - ]; - - stylix = { - enable = true; - enableReleaseChecks = false; - polarity = "dark"; - - fonts = { - monospace = { - package = pkgs.maple-mono.NF; - name = "Maple Mono NF"; - }; - sansSerif = { - package = pkgs.inter; - name = "Inter"; - }; - serif = { - package = pkgs.noto-fonts; - name = "Noto Serif"; - }; - }; - }; - - # Explicitly add fonts to system-wide packages for Darwin - fonts.packages = [ - pkgs.maple-mono.NF - pkgs.inter - pkgs.noto-fonts - ]; - }; - - # ── Home Manager Styling ────────────────────────────────────── - flake.modules.homeManager.styling = { - pkgs, - lib, - config, - inputs, - ... - }: let - isDarwin = pkgs.stdenv.hostPlatform.isDarwin; - in { - imports = [inputs.stylix.homeManagerModules.stylix]; - config = { - stylix = { - # We only need to define targets here; the base theme is inherited from system - enable = true; - - targets.vscode.enable = true; - targets.ghostty.enable = true; - # targets.bat.enable = true; - # targets.fzf.enable = true; - # targets.lsd.enable = true; - # targets.btop.enable = true; - # targets.vesktop.enable = true; - targets.spicetify.enable = lib.mkForce true; - targets.qt.enable = false; - }; - - # ── Stylix-Themed Firefox UI ────────────────────────────── - programs.firefox.profiles = let - c = config.lib.stylix.colors.withHashtag; - - commonCss = '' - :root { - --base00: ${c.base00}; --base01: ${c.base01}; --base02: ${c.base02}; --base03: ${c.base03}; - --base04: ${c.base04}; --base05: ${c.base05}; --base06: ${c.base06}; --base07: ${c.base07}; - --base08: ${c.base08}; --base09: ${c.base09}; --base0A: ${c.base0A}; --base0B: ${c.base0B}; - --base0C: ${c.base0C}; --base0D: ${c.base0D}; --base0E: ${c.base0E}; --base0F: ${c.base0F}; - } - ''; - - stylixUserChrome = commonCss + '' - /* Aggressive UI Overrides */ - :root { - --lwt-accent-color: var(--base00) !important; - --lwt-text-color: var(--base05) !important; - --toolbar-bgcolor: var(--base00) !important; - --toolbar-field-background-color: var(--base01) !important; - --toolbar-field-color: var(--base05) !important; - --toolbar-field-border-color: var(--base03) !important; - --toolbar-field-focus-background-color: var(--base01) !important; - --toolbar-field-focus-color: var(--base05) !important; - --toolbar-field-focus-border-color: var(--base0D) !important; - --lwt-selected-tab-background-color: var(--base02) !important; - --lwt-tab-text-color: var(--base05) !important; - --lwt-background-tab-text-color: var(--base04) !important; - } - - #nav-bar, #TabsToolbar, #PersonalToolbar, #navigator-toolbox, #sidebar-box, #sidebar-header { - background-color: var(--base00) !important; - background-image: none !important; - color: var(--base05) !important; - border: none !important; - box-shadow: none !important; - } - - /* Tab Bar Tweaks */ - .tab-background[selected="true"] { - background-color: var(--base02) !important; - background-image: none !important; - } - - .tab-line[selected="true"] { - background-color: var(--base0D) !important; - } - - /* Context Menus */ - menupopup, panel { - --panel-background: var(--base01) !important; - --panel-color: var(--base05) !important; - --panel-border-color: var(--base03) !important; - } - - menuitem, menu { - appearance: none !important; - color: var(--base05) !important; - } - - menuitem[_moz-menuactive="true"], menu[_moz-menuactive="true"] { - background-color: var(--base02) !important; - color: var(--base0D) !important; - } - ''; - - stylixUserContent = commonCss + '' - /* Internal Pages Theme */ - @-moz-document url-prefix(about:), url-prefix(chrome:) { - :root { - --in-content-page-background: var(--base00) !important; - --in-content-page-color: var(--base05) !important; - --in-content-box-background: var(--base01) !important; - --in-content-primary-button-background: var(--base0D) !important; - } - body { - background-color: var(--base00) !important; - color: var(--base05) !important; - } - } - ''; - in { - default = { - userChrome = stylixUserChrome; - userContent = stylixUserContent; - }; - default-release = { - userChrome = stylixUserChrome; - userContent = stylixUserContent; - }; - }; - - # ── GTK Theming ───────────────────────────────────────────── - gtk = { - gtk4.theme = lib.mkForce null; # Silence evaluation warning on all platforms - enable = lib.mkDefault (!isDarwin); - gtk4.extraConfig.gtk-application-prefer-dark-theme = 1; - }; - - # ── Qt Theming (Linux only) ───────────────────────────────── - qt = lib.mkIf (!isDarwin) { - enable = lib.mkForce true; - platformTheme.name = lib.mkForce "gtk3"; - }; - - # ── Terminal env ──────────────────────────────────────────── - programs.zsh.initContent = '' - export COLORTERM=truecolor - ''; - }; - }; -} diff --git a/modules/terminal.nix b/modules/terminal.nix index c678aa6d..83d5adee 100644 --- a/modules/terminal.nix +++ b/modules/terminal.nix @@ -1,66 +1,73 @@ { - flake.modules.homeManager.terminal = { pkgs, config, lib, ... }: { - programs.tmux = { - enable = true; - shortcut = "a"; # Ctrl-a prefix - baseIndex = 1; - keyMode = "vi"; - mouse = true; - terminal = "screen-256color"; - shell = "${pkgs.zsh}/bin/zsh"; - - plugins = with pkgs.tmuxPlugins; [ - sensible - vim-tmux-navigator - yank - resurrect - continuum - tmux-which-key - fzf-tmux-url - ]; + flake.modules.homeManager.dendritic = + { + pkgs, + config, + lib, + ... + }: + { + programs.tmux = { + enable = true; + shortcut = "a"; # Ctrl-a prefix + baseIndex = 1; + keyMode = "vi"; + mouse = true; + terminal = "screen-256color"; + shell = "${pkgs.zsh}/bin/zsh"; - extraConfig = '' - # ── Ergonomics ──────────────────────────────────────────────── - # Split panes using | and - - bind | split-window -h -c "#{pane_current_path}" - bind - split-window -v -c "#{pane_current_path}" - unbind '"' - unbind % + plugins = with pkgs.tmuxPlugins; [ + sensible + vim-tmux-navigator + yank + resurrect + continuum + tmux-which-key + fzf-tmux-url + ]; - # Smart pane resizing (prefix + H, J, K, L) - bind -r H resize-pane -L 5 - bind -r J resize-pane -D 5 - bind -r K resize-pane -U 5 - bind -r L resize-pane -R 5 + extraConfig = '' + # ── Ergonomics ──────────────────────────────────────────────── + # Split panes using | and - + bind | split-window -h -c "#{pane_current_path}" + bind - split-window -v -c "#{pane_current_path}" + unbind '"' + unbind % - # Vim-like copy mode (prefix + [) - bind-key -T copy-mode-vi v send-keys -X begin-selection - bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle - bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel + # Smart pane resizing (prefix + H, J, K, L) + bind -r H resize-pane -L 5 + bind -r J resize-pane -D 5 + bind -r K resize-pane -U 5 + bind -r L resize-pane -R 5 - # ── Keybinding Hints (the "Tutorial") ──────────────────────── - # Open tmux-which-key with prefix + ? - # This provides a searchable menu of all your keybindings. - bind-key ? run-shell "tmux-which-key" + # Vim-like copy mode (prefix + [) + bind-key -T copy-mode-vi v send-keys -X begin-selection + bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle + bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel - # ── Optimizations ───────────────────────────────────────────── - set -sg escape-time 0 # No delay for escape key (crucial for vim) - set -g focus-events on # Pass focus events to apps like vim - setw -g aggressive-resize on # Useful when multiple clients are attached - set -g history-limit 50000 # More history - set -g status-interval 5 # Refresh status line more often + # ── Keybinding Hints (the "Tutorial") ──────────────────────── + # Open tmux-which-key with prefix + ? + # This provides a searchable menu of all your keybindings. + bind-key ? run-shell "tmux-which-key" - # ── Session Management ──────────────────────────────────────── - set -g @continuum-restore 'on' # Automatically restore session on start - - # ── Aesthetics ─────────────────────────────────────────────── - # Stylix targets tmux automatically, but we add some polish - set -g status-position top - set -g pane-border-status off - - # Use tmux-which-key XDG path to ensure it's writable (needed for Nix) - set -g @tmux-which-key-xdg-plugin-path "$XDG_CONFIG_HOME/tmux/plugins/tmux-which-key" - ''; + # ── Optimizations ───────────────────────────────────────────── + set -sg escape-time 0 # No delay for escape key (crucial for vim) + set -g focus-events on # Pass focus events to apps like vim + setw -g aggressive-resize on # Useful when multiple clients are attached + set -g history-limit 50000 # More history + set -g status-interval 5 # Refresh status line more often + + # ── Session Management ──────────────────────────────────────── + set -g @continuum-restore 'on' # Automatically restore session on start + + # ── Aesthetics ─────────────────────────────────────────────── + # Stylix targets tmux automatically, but we add some polish + set -g status-position top + set -g pane-border-status off + + # Use tmux-which-key XDG path to ensure it's writable (needed for Nix) + set -g @tmux-which-key-xdg-plugin-path "$XDG_CONFIG_HOME/tmux/plugins/tmux-which-key" + ''; + }; }; - }; } diff --git a/modules/theme.nix b/modules/theme.nix deleted file mode 100644 index 4ded5889..00000000 --- a/modules/theme.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ lib, ... }: { - stylix.base16Scheme = { - base00 = "2d302f"; - base01 = "434846"; - base02 = "5a605d"; - base03 = "9da8a3"; - base04 = "cad8d2"; - base05 = "e0f0ef"; - base06 = "ecf6f2"; - base07 = "fcfefd"; - base08 = "f9906f"; - base09 = "b38a61"; - base0A = "f0c239"; - base0B = "8ab361"; - base0C = "30dff3"; - base0D = "b0a4e3"; - base0E = "cca4e3"; - base0F = "ca6924"; - }; -} diff --git a/modules/treefmt-build-dep.nix b/modules/treefmt-build-dep.nix new file mode 100644 index 00000000..948dcd49 --- /dev/null +++ b/modules/treefmt-build-dep.nix @@ -0,0 +1,35 @@ +# Make every system build depend on `checks..treefmt`. +# +# Why this exists: `treefmt-nix.flakeModule` already wires up +# `checks..treefmt` as a flake check, so `nix flake check` fails +# on unformatted files. But `darwin-rebuild switch`, `nh darwin switch`, +# `nixos-rebuild switch`, `nh os switch`, and plain +# `nix build .#darwinConfigurations..config.system.build.toplevel` +# all build ONLY the toplevel derivation — never the checks. That means +# you can rebuild a system in an unformatted state, which defeats "the +# repo is always formatted" as an invariant. +# +# Fix: drop a single `/etc/.dotfiles-treefmt-check` entry whose `source` +# is the per-system treefmt check derivation. Because every +# `environment.etc..source` becomes a hard build dependency of +# `system.build.toplevel`, the toplevel now transitively depends on the +# check passing. If `treefmt --ci` finds an unformatted file, the check +# build fails, which propagates up and the system build fails too — +# exactly mirroring `nix flake check`'s behaviour but at rebuild time. +# +# Cost: one /etc symlink (~0 bytes runtime) pointing at the check's empty +# `$out` marker. +{ inputs, ... }: +let + mkTreefmtBuildDep = + { pkgs, ... }: + { + environment.etc.".dotfiles-treefmt-check" = { + source = inputs.self.checks.${pkgs.stdenv.hostPlatform.system}.treefmt; + }; + }; +in +{ + flake.modules.darwin.dendritic = mkTreefmtBuildDep; + flake.modules.nixos.dendritic = mkTreefmtBuildDep; +} diff --git a/runner_debug.sh b/runner_debug.sh index 1cbd59f3..8aadc428 100644 --- a/runner_debug.sh +++ b/runner_debug.sh @@ -7,12 +7,11 @@ if [ ! -e '/Users/8amps/.local/share/microvm/dendritic-vm.img' ]; then # Mark NOCOW chattr +C '/Users/8amps/.local/share/microvm/dendritic-vm.img' || true truncate -s 10240M '/Users/8amps/.local/share/microvm/dendritic-vm.img' - mkfs.ext4 '/Users/8amps/.local/share/microvm/dendritic-vm.img' + mkfs.ext4 '/Users/8amps/.local/share/microvm/dendritic-vm.img' fi # Open macvtap interface file descriptors runtime_args= -exec -a "microvm@dendritic-vm" /nix/store/n6r2khp10lr1vzr2chbx8ipy861s2dqi-qemu-for-vm-tests-10.2.2/bin/qemu-system-aarch64 -name dendritic-vm -M 'virt,accel=hvf:tcg,gic-version=max' -m 2047 -smp 2 -nodefaults -no-user-config -no-reboot -kernel /nix/store/rpxxxpb5i7z4xqrab5337avd6j9cqr2g-linux-6.18.24/Image -initrd /nix/store/lgdrb24s7b6d8bfgrxsl5y4l6mzg39mv-initrd-linux-6.18.24/initrd -chardev 'stdio,id=stdio,signal=off' -device virtio-rng-pci -smbios 'type=1,uuid=94cd2fa1-bc49-4534-c68c-051bd7ecfc7b' -serial chardev:stdio -cpu host -append 'console=ttyAMA0 reboot=t panic=-1 8250.nr_uarts=1 loglevel=4 lsm=landlock,yama,bpf vt.default_red=0x2d,0xf9,0x8a,0xf0,0xb0,0xcc,0x30,0xe0,0x9d,0xf9,0x8a,0xf0,0xb0,0xcc,0x30,0xfc vt.default_grn=0x30,0x90,0xb3,0xc2,0xa4,0xa4,0xdf,0xf0,0xa8,0x90,0xb3,0xc2,0xa4,0xa4,0xdf,0xfe vt.default_blu=0x2f,0x6f,0x61,0x39,0xe3,0xe3,0xf3,0xef,0xa3,0x6f,0x61,0x39,0xe3,0xe3,0xf3,0xfd init=/nix/store/7wr5nsflpng71iv0xiyaqgn2v9szxndf-nixos-system-dendritic-vm-26.05.20260427.1c3fe55/init regInfo=/nix/store/8ijdgxnfjga9r6iqkzc02h11wmv3cbai-closure-info/registration' -nographic -qmp unix:dendritic-vm.sock,server,nowait -drive 'id=vda,format=raw,file=/Users/8amps/.local/share/microvm/dendritic-vm.img,if=none,aio=threads,discard=unmap,cache=none,read-only=off' -device 'virtio-blk-pci,drive=vda' -fsdev 'local,id=fs0,path=/nix/store,security_model=none,readonly=false' -device 'virtio-9p-pci,fsdev=fs0,mount_tag=ro-store' -device 'vhost-vsock-pci,guest-cid=4' ${runtime_args:-} - +exec -a "microvm@dendritic-vm" /nix/store/n6r2khp10lr1vzr2chbx8ipy861s2dqi-qemu-for-vm-tests-10.2.2/bin/qemu-system-aarch64 -name dendritic-vm -M 'virt,accel=hvf:tcg,gic-version=max' -m 2047 -smp 2 -nodefaults -no-user-config -no-reboot -kernel /nix/store/rpxxxpb5i7z4xqrab5337avd6j9cqr2g-linux-6.18.24/Image -initrd /nix/store/lgdrb24s7b6d8bfgrxsl5y4l6mzg39mv-initrd-linux-6.18.24/initrd -chardev 'stdio,id=stdio,signal=off' -device virtio-rng-pci -smbios 'type=1,uuid=94cd2fa1-bc49-4534-c68c-051bd7ecfc7b' -serial chardev:stdio -cpu host -append 'console=ttyAMA0 reboot=t panic=-1 8250.nr_uarts=1 loglevel=4 lsm=landlock,yama,bpf vt.default_red=0x2d,0xf9,0x8a,0xf0,0xb0,0xcc,0x30,0xe0,0x9d,0xf9,0x8a,0xf0,0xb0,0xcc,0x30,0xfc vt.default_grn=0x30,0x90,0xb3,0xc2,0xa4,0xa4,0xdf,0xf0,0xa8,0x90,0xb3,0xc2,0xa4,0xa4,0xdf,0xfe vt.default_blu=0x2f,0x6f,0x61,0x39,0xe3,0xe3,0xf3,0xef,0xa3,0x6f,0x61,0x39,0xe3,0xe3,0xf3,0xfd init=/nix/store/7wr5nsflpng71iv0xiyaqgn2v9szxndf-nixos-system-dendritic-vm-26.05.20260427.1c3fe55/init regInfo=/nix/store/8ijdgxnfjga9r6iqkzc02h11wmv3cbai-closure-info/registration' -nographic -qmp unix:dendritic-vm.sock,server,nowait -drive 'id=vda,format=raw,file=/Users/8amps/.local/share/microvm/dendritic-vm.img,if=none,aio=threads,discard=unmap,cache=none,read-only=off' -device 'virtio-blk-pci,drive=vda' -fsdev 'local,id=fs0,path=/nix/store,security_model=none,readonly=false' -device 'virtio-9p-pci,fsdev=fs0,mount_tag=ro-store' -device 'vhost-vsock-pci,guest-cid=4' ${runtime_args:-} diff --git a/scripts/regenerate-hardware.sh b/scripts/regenerate-hardware.sh index 60f59f1c..8ecf00d9 100644 --- a/scripts/regenerate-hardware.sh +++ b/scripts/regenerate-hardware.sh @@ -25,12 +25,15 @@ echo "Wrote: ${TARGET_DIR}/${HOST}.nix" # Determine target output name ARCH="$(uname -m)" case "$ARCH" in - x86_64) - SYSTEM="x86_64-linux" ;; - aarch64|arm64) - SYSTEM="aarch64-linux" ;; - *) - SYSTEM="x86_64-linux" ;; +x86_64) + SYSTEM="x86_64-linux" + ;; +aarch64 | arm64) + SYSTEM="aarch64-linux" + ;; +*) + SYSTEM="x86_64-linux" + ;; esac -echo "${SYSTEM}" > "${TARGET_DIR}/${HOST}.system" -echo "To switch: sudo nixos-rebuild switch --flake ${REPO_ROOT}#${HOST}" \ No newline at end of file +echo "${SYSTEM}" >"${TARGET_DIR}/${HOST}.system" +echo "To switch: sudo nixos-rebuild switch --flake ${REPO_ROOT}#${HOST}" diff --git a/scripts/sops-updatekeys.sh b/scripts/sops-updatekeys.sh new file mode 100755 index 00000000..c962c996 --- /dev/null +++ b/scripts/sops-updatekeys.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Mass-rewrap every sops-encrypted file in this repo against the current +# recipient set in .sops.yaml. +# +# When to run: +# - after adding/removing an age recipient in .sops.yaml, +# - before retiring an old key (so files stop being encrypted to it), +# - after onboarding a new machine/user that needs decryption access. +# +# What this does NOT do: +# - generate new keys (use age-keygen or ssh-to-age), +# - update .sops.yaml itself (that's a manual edit), +# - decrypt or re-encrypt payloads (the data key gets re-wrapped, the +# underlying ciphertext payload stays unchanged). +# +# Add new sops-encrypted files to FILES below as the surface grows. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +if [ ! -f .sops.yaml ]; then + echo "fatal: .sops.yaml not found at repo root ($REPO_ROOT)" >&2 + echo " sops updatekeys needs the recipient config to rewrap." >&2 + exit 1 +fi + +if ! command -v sops >/dev/null 2>&1; then + echo "fatal: 'sops' not in PATH. Install via 'nix shell nixpkgs#sops' or" >&2 + echo " enter the repo devshell." >&2 + exit 1 +fi + +FILES=( + "secrets/secrets.yaml" +) + +failed=() +for f in "${FILES[@]}"; do + if [ ! -f "$f" ]; then + echo "skip: $f (missing)" + continue + fi + echo "==> sops updatekeys $f" + if ! sops updatekeys --yes "$f"; then + failed+=("$f") + fi +done + +if [ ${#failed[@]} -gt 0 ]; then + echo >&2 + echo "fatal: updatekeys failed for ${#failed[@]} file(s):" >&2 + printf ' - %s\n' "${failed[@]}" >&2 + exit 1 +fi + +echo +echo "Done. Commit the rewrapped files together." diff --git a/scripts/wawona-vm-bridge.sh b/scripts/wawona-vm-bridge.sh index c5ebe9a9..d79078a1 100755 --- a/scripts/wawona-vm-bridge.sh +++ b/scripts/wawona-vm-bridge.sh @@ -12,8 +12,8 @@ rm -f "$WAYPIPE_SOCKET" # Ensure Wawona runtime exists if [ ! -d "$WAWONA_RUNTIME" ]; then - echo "Error: Wawona runtime directory $WAWONA_RUNTIME not found. Is Wawona running?" - exit 1 + echo "Error: Wawona runtime directory $WAWONA_RUNTIME not found. Is Wawona running?" + exit 1 fi # Start waypipe client listening on a separate socket @@ -28,11 +28,11 @@ sleep 1 # Bridge the vfkit vsock socket to the waypipe socket echo "Bridging vsock socket to waypipe..." if [ -S "$VSOCK_SOCKET" ]; then - socat UNIX-CONNECT:"$VSOCK_SOCKET" UNIX-CONNECT:"$WAYPIPE_SOCKET" & - echo "Bridge established. Sway should now appear in Wawona." + socat UNIX-CONNECT:"$VSOCK_SOCKET" UNIX-CONNECT:"$WAYPIPE_SOCKET" & + echo "Bridge established. Sway should now appear in Wawona." else - echo "Warning: $VSOCK_SOCKET not found. Please start the MicroVM first." - echo "Once the VM starts, run: socat UNIX-CONNECT:$VSOCK_SOCKET UNIX-CONNECT:$WAYPIPE_SOCKET &" + echo "Warning: $VSOCK_SOCKET not found. Please start the MicroVM first." + echo "Once the VM starts, run: socat UNIX-CONNECT:$VSOCK_SOCKET UNIX-CONNECT:$WAYPIPE_SOCKET &" fi wait diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index 27e76d99..ca99f158 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -1 +1,17 @@ -anthropic_api_key: placeholder +openai_api_key: ENC[AES256_GCM,data:dRNuXDPVbc5KxrCKftpvFPY7AfzMe7NE+kIPPG76rQgriXvJdtrgWnV9HdIsKRCQVolEAnTtEw08WPjfixtSO4qYEQjwpeWKYnD/Oem73xt5sy75YltfzvjipYnVWSt4bjSjM10rtMqub1Sy7B/iHna+OlLEf8agfJROmrhbfmNS5X36m3LcnngL27BSiywJgva/xyWpM/FO/qqZKeFYV7K3lT0=,iv:31Oo+Vsz/qj2aJ7zG2BzW6EFfEeivrkXewRTxoUOOns=,tag:hP8QoBQLUqnB+2zcEP1Sdw==,type:str] +gh_token: ENC[AES256_GCM,data:3kbFy+pDCbBUfgP5cp+1PZYT+ew5Y071Gb9Fndtg0Ak++W55N2HxDg==,iv:mBIYFTUnhXWWWX/pWYIhaPtDlEglJi/3HE4TL6STvu4=,tag:vjTnCT5cW2bd6lX+CPf27g==,type:str] +sops: + age: + - recipient: age1alw70v2xd80yrkn2ukap264c64fa64qjq7rr4uh07amu8ahm9uzs9z485w + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvb0JOakc0bXhRaU5BWnZk + WTJ1d2hTRUZCV3o3WjJGSHJnbmRNWXJpNldZCmNmTlJnUEFoVnVIZzZvUG1Pb3ZG + ZE4yUEQ1VlVmS1FFSyt4S1g4OU1HMFkKLS0tIEovdWJzRmFVMDBzMktWNldwS0hL + ZGM4ZWhEckM1VXRVNVVJbUsxNVdMT2sKOgMXGQ//6/cDyS0ZozDNJnYoSIJyJawK + oglQoD9su3LPFSFm04gLuY+zFmnyuRBH/S4oedRjNYHIik27s0ENqQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-05-25T02:29:23Z" + mac: ENC[AES256_GCM,data:chXxUhElHCgdq0s4mNmQ3jlzpwJyZpRZVDEUpfWRKijT5eveV+xtVj174tKBQNe6ztpJ9tgn2/SAZkq2W1uKecH/Q/8SPfRCKEhAUSbmKnzY2lcYkoIX38JrMLbxg6wAdRaskStgQlQmKYNiFErQHe5/9r9lg7qSJDh93zPmurE=,iv:nFOpsBqnz6g8vXXRP2PsE8hijobKYNnusVUBulKi8rg=,tag:Sk9wmnxyL7lmIjzILiDGiQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.0 diff --git a/subrepos/microvm.nix/.github/FUNDING.yml b/subrepos/microvm.nix/.github/FUNDING.yml index 022aad92..b98b87fd 100644 --- a/subrepos/microvm.nix/.github/FUNDING.yml +++ b/subrepos/microvm.nix/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: ['astro'] +github: ["astro"] diff --git a/subrepos/microvm.nix/.github/workflows/prebuilt-stable.yml b/subrepos/microvm.nix/.github/workflows/prebuilt-stable.yml index f6b6b5d4..f1ac9a1d 100644 --- a/subrepos/microvm.nix/.github/workflows/prebuilt-stable.yml +++ b/subrepos/microvm.nix/.github/workflows/prebuilt-stable.yml @@ -3,7 +3,7 @@ name: "prebuilt-25.11" on: workflow_dispatch: # allows manual triggering schedule: - - cron: '0 0 * * *' # runs daily at 00:00 + - cron: "0 0 * * *" # runs daily at 00:00 merge_group: pull_request: push: @@ -12,15 +12,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@v31 - - uses: cachix/cachix-action@v17 - if: github.ref == 'refs/heads/main' - with: - name: microvm - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - uses: cachix/cachix-action@v17 - if: github.ref != 'refs/heads/main' - with: - name: microvm - - run: nix build -L .#prebuilt --override-input nixpkgs github:nixos/nixpkgs/release-25.11 + - uses: actions/checkout@v6 + - uses: cachix/install-nix-action@v31 + - uses: cachix/cachix-action@v17 + if: github.ref == 'refs/heads/main' + with: + name: microvm + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - uses: cachix/cachix-action@v17 + if: github.ref != 'refs/heads/main' + with: + name: microvm + - run: nix build -L .#prebuilt --override-input nixpkgs github:nixos/nixpkgs/release-25.11 diff --git a/subrepos/microvm.nix/.github/workflows/prebuilt-unstable.yml b/subrepos/microvm.nix/.github/workflows/prebuilt-unstable.yml index c514c40b..4836e7aa 100644 --- a/subrepos/microvm.nix/.github/workflows/prebuilt-unstable.yml +++ b/subrepos/microvm.nix/.github/workflows/prebuilt-unstable.yml @@ -3,7 +3,7 @@ name: "prebuilt-unstable" on: workflow_dispatch: # allows manual triggering schedule: - - cron: '0 0 * * *' # runs daily at 00:00 + - cron: "0 0 * * *" # runs daily at 00:00 merge_group: pull_request: push: @@ -12,15 +12,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@v31 - - uses: cachix/cachix-action@v17 - if: github.ref == 'refs/heads/main' - with: - name: microvm - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - uses: cachix/cachix-action@v17 - if: github.ref != 'refs/heads/main' - with: - name: microvm - - run: nix build -L .#prebuilt --override-input nixpkgs github:nixos/nixpkgs/nixos-unstable + - uses: actions/checkout@v6 + - uses: cachix/install-nix-action@v31 + - uses: cachix/cachix-action@v17 + if: github.ref == 'refs/heads/main' + with: + name: microvm + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - uses: cachix/cachix-action@v17 + if: github.ref != 'refs/heads/main' + with: + name: microvm + - run: nix build -L .#prebuilt --override-input nixpkgs github:nixos/nixpkgs/nixos-unstable diff --git a/subrepos/microvm.nix/CHANGELOG.md b/subrepos/microvm.nix/CHANGELOG.md index 7aa53df5..8d2a1d53 100644 --- a/subrepos/microvm.nix/CHANGELOG.md +++ b/subrepos/microvm.nix/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased: `main` branch -* Shell scripts to setup virtiofsd, interfaces, and PCI devices for +- Shell scripts to setup virtiofsd, interfaces, and PCI devices for pass-through have moved from systemd units in the host to the `bin/` subdirectory of MicroVM packages. That means you can actually use them from the command-line if you would like to run with virtiofsd. @@ -15,82 +15,83 @@ scripts. Switching your updated NixOS host will print a warning about which MicroVMs need updating. -* `bin/virtiofsd-run` (`microvm-virtiofsd@.service`) now starts the +- `bin/virtiofsd-run` (`microvm-virtiofsd@.service`) now starts the multiple virtiofsd instances through supervisord. -* The `microvm` module allows configuration of +- The `microvm` module allows configuration of `microvm.virtiofsd.group` and `microvm.virtiofsd.inodeFileHandles` and `microvm.virtiofsd.threadPoolSize` now. -* Add the [alioth VMM](https://github.com/google/alioth) -* Fixes for the stratovirt VMM -* New volume image files will be created with `truncate` instead of +- Add the [alioth VMM](https://github.com/google/alioth) +- Fixes for the stratovirt VMM +- New volume image files will be created with `truncate` instead of `fallocate`, saving disk space. -* Building the `microvm.storeDisk` is now faster as the system closure +- Building the `microvm.storeDisk` is now faster as the system closure is no longer first copied into the build directory. A mount namespace is setup with bubblewrap instead. mkfs.erofs can now run multi-threaded. ## 0.5.0 (2024-04-06) -* **tap interfaces** are now **multi-queue** when running with more +- **tap interfaces** are now **multi-queue** when running with more than one VCPU. Update your host! -* The `host` module enables **Kernel Samepage Merging** by default. -* **qemu** can run non-native systems by using its **Tiny Code +- The `host` module enables **Kernel Samepage Merging** by default. +- **qemu** can run non-native systems by using its **Tiny Code Generator** instead of KVM. -* **SSH deployment scripts** are added as +- **SSH deployment scripts** are added as `config.microvm.deploy.rebuild` -* **qemu** defaults to the *microvm* machine model now as it supports +- **qemu** defaults to the _microvm_ machine model now as it supports PCI, USB, and ACPI by now. Set `microvm.qemu.machine = "q35"` if this breaks for you. -* The NixOS **hardened** profile can be used by falling back to - *squashfs*. -* Runners execute the hypervisor with a process name of +- The NixOS **hardened** profile can be used by falling back to + _squashfs_. +- Runners execute the hypervisor with a process name of `microvm@$NAME` -* We no longer let `environment.noXlibs` default to `true` -* **Breaking:** the `microvm` user is no longer in the `disk` group +- We no longer let `environment.noXlibs` default to `true` +- **Breaking:** the `microvm` user is no longer in the `disk` group for security reasons. Add `users.users.microvm.extraGroups = [ - "disk" ]` to your config to restore the old behavior. +"disk" ]` to your config to restore the old behavior. ## 0.4.1 (2023-11-03) -* **cloud-hypervisor** replaces **rust-hypervisor-firmware** with +- **cloud-hypervisor** replaces **rust-hypervisor-firmware** with direct kernel+initramfs loading. -* The microvm module now optimizes the NixOS configuration for size. -* **crosvm** now supports **macvtap** interfaces. -* The option `microvm.qemu.bios` has been dropped again for simplicity +- The microvm module now optimizes the NixOS configuration for size. +- **crosvm** now supports **macvtap** interfaces. +- The option `microvm.qemu.bios` has been dropped again for simplicity reasons. **qemu** boots fast with the shipped SeaBIOS if after both SATA and the network interface option ROM (iPXE) have been disabled. -* `microvm.kernelParams` always copy `boot.kernelParams` -* **firecracker** is no longer launched through **firectl**. -* Networking example documentation has been split into multiple + +- `microvm.kernelParams` always copy `boot.kernelParams` +- **firecracker** is no longer launched through **firectl**. +- Networking example documentation has been split into multiple scenarios. -* **Vsock** support has been added for Hypervisors that connect them - to the Linux host's *AF_VSOCK*: qemu, crosvm, and kvmtool. -* Our packages and overlay include the unstable version of +- **Vsock** support has been added for Hypervisors that connect them + to the Linux host's _AF_VSOCK_: qemu, crosvm, and kvmtool. +- Our packages and overlay include the unstable version of **waypipe**, featuring **Vsock** support. -* Add support for the old command-line parameter syntax that returned +- Add support for the old command-line parameter syntax that returned with **cloud-hypervisor** 36.0. ## 0.4.0 (2023-07-09) -* Stop building a custom kernel by booting the NixOS kernel with an +- Stop building a custom kernel by booting the NixOS kernel with an initrd. -* New Hypervisor: **stratovirt** by Huawei -* Support *fully declarative* MicroVMs that are part of the host's +- New Hypervisor: **stratovirt** by Huawei +- Support _fully declarative_ MicroVMs that are part of the host's NixOS configuration. **No Flakes required!** -* We use **squashfs-tools-ng** now. -* The `microvm-console` script has been removed because pty console +- We use **squashfs-tools-ng** now. +- The `microvm-console` script has been removed because pty console setup was too cumbersome to maintain across all hypervisors. -* `microvm.storeDiskType` defaults to `"erofs"` now for higher runtime +- `microvm.storeDiskType` defaults to `"erofs"` now for higher runtime performance. ## 0.3.3 (2023-05-24) -* Support for **macvtap** network interfaces has been added. -* `boot.initrd.systemd.enable` is now supported. -* Experimental **graphics** support for qemu, and cloud-hypervisor -* **qemu**: use qboot BIOS +- Support for **macvtap** network interfaces has been added. +- `boot.initrd.systemd.enable` is now supported. +- Experimental **graphics** support for qemu, and cloud-hypervisor +- **qemu**: use qboot BIOS ## 0.3.2 (2022-12-25) diff --git a/subrepos/microvm.nix/README.md b/subrepos/microvm.nix/README.md index 52a456cc..9a004fa7 100644 --- a/subrepos/microvm.nix/README.md +++ b/subrepos/microvm.nix/README.md @@ -32,32 +32,31 @@ imperatively with the provided `microvm` command. shrunk using `microvm-balloon` - MicroVMs have a read-only root disk with either a prepopulated `/nix/store` or by mounting the host's along with an optional - writable overlay. This filesystem can be built as either *squashfs* - (smaller) or *erofs* (faster). + writable overlay. This filesystem can be built as either _squashfs_ + (smaller) or _erofs_ (faster). - You define your MicroVMs in a Nix Flake's `nixosConfigurations` section, reusing the `nixosModules` that are exported by this Flake. - MicroVMs can access stateful filesystems either on a image volume as a block device, or alternatively as a shared directory hierarchy - through *9p* or *virtiofs*. + through _9p_ or _virtiofs_. - Zero, one, or more virtual tap ethernet network interfaces can be - attached to a MicroVM. `qemu`, `kvmtool`, and `vfkit` also support *user* + attached to a MicroVM. `qemu`, `kvmtool`, and `vfkit` also support _user_ networking which requires no additional setup on the host. - For high-throughput TAP networking with `qemu`, enable `tap.vhost = true` to use vhost-net kernel acceleration (~10 Gbps vs ~1.5 Gbps without). ## Hypervisors -| Hypervisor | Language | Restrictions | -|-------------------------------------------------------------------------|----------|-------------------------------------------------------| -| [qemu](https://www.qemu.org/) | C | | -| [cloud-hypervisor](https://www.cloudhypervisor.org/) | Rust | no 9p shares | -| [firecracker](https://firecracker-microvm.github.io/) | Rust | no 9p/virtiofs shares | -| [crosvm](https://github.com/google/crosvm) | Rust | 9p shares broken | -| [kvmtool](https://github.com/kvmtool/kvmtool) | C | no virtiofs shares, no control socket | -| [stratovirt](https://github.com/openeuler-mirror/stratovirt) | Rust | no 9p/virtiofs shares, no control socket | -| [alioth](https://github.com/google/alioth) | Rust | no virtiofs shares, no control socket | -| [vfkit](https://github.com/crc-org/vfkit) | Go | macOS only, no 9p shares, no tap/bridge networking | - +| Hypervisor | Language | Restrictions | +| ------------------------------------------------------------ | -------- | -------------------------------------------------- | +| [qemu](https://www.qemu.org/) | C | | +| [cloud-hypervisor](https://www.cloudhypervisor.org/) | Rust | no 9p shares | +| [firecracker](https://firecracker-microvm.github.io/) | Rust | no 9p/virtiofs shares | +| [crosvm](https://github.com/google/crosvm) | Rust | 9p shares broken | +| [kvmtool](https://github.com/kvmtool/kvmtool) | C | no virtiofs shares, no control socket | +| [stratovirt](https://github.com/openeuler-mirror/stratovirt) | Rust | no 9p/virtiofs shares, no control socket | +| [alioth](https://github.com/google/alioth) | Rust | no virtiofs shares, no control socket | +| [vfkit](https://github.com/crc-org/vfkit) | Go | macOS only, no 9p shares, no tap/bridge networking | ## Installation @@ -105,6 +104,7 @@ MicroVMs. They listen for ssh with an empty root password. ### Experimental: run graphical applications with graphics support On Linux with cloud-hypervisor and Wayland forwarding: + ```shell nix run microvm#graphics neverball ``` diff --git a/subrepos/microvm.nix/checks/default.nix b/subrepos/microvm.nix/checks/default.nix index fe79a7ee..8c0a1322 100644 --- a/subrepos/microvm.nix/checks/default.nix +++ b/subrepos/microvm.nix/checks/default.nix @@ -1,244 +1,344 @@ -{ self, nixpkgs, system }: +{ + self, + nixpkgs, + system, +}: let inherit (nixpkgs) lib; # Platform filtering for hypervisors hypervisorsDarwinOnly = [ "vfkit" ]; - hypervisorsOnDarwin = [ "qemu" "vfkit" ]; + hypervisorsOnDarwin = [ + "qemu" + "vfkit" + ]; isDarwinOnly = hypervisor: builtins.elem hypervisor hypervisorsDarwinOnly; isDarwinSystem = s: lib.hasSuffix "-darwin" s; - hypervisorSupportsSystem = hypervisor: s: - if isDarwinSystem s - then builtins.elem hypervisor hypervisorsOnDarwin - else !(isDarwinOnly hypervisor); + hypervisorSupportsSystem = + hypervisor: s: + if isDarwinSystem s then + builtins.elem hypervisor hypervisorsOnDarwin + else + !(isDarwinOnly hypervisor); # Filter hypervisors to only those that support the current system - supportedHypervisors = builtins.filter - (hv: hypervisorSupportsSystem hv system) - self.lib.hypervisors; + supportedHypervisors = builtins.filter ( + hv: hypervisorSupportsSystem hv system + ) self.lib.hypervisors; variants = [ # hypervisor - [ { - id = "qemu"; - modules = [ { - microvm.hypervisor = "qemu"; - } ]; - } { - id = "qemu-tcg"; - modules = let - # Emulate a different guest system than the host one - guestSystem = if "${system}" == "x86_64-linux" then "aarch64-unknown-linux-gnu" - else "x86_64-linux"; - in [ - { - microvm = { - hypervisor = "qemu"; - # Force the CPU to be something else than the current - # system, and thus, emulated with qemu's Tiny Code Generator - # (TCG) - cpu = if "${system}" == "x86_64-linux" then "cortex-a53" - else "Westmere"; - }; - nixpkgs.crossSystem.config = guestSystem; - } - ]; - } { - id = "cloud-hypervisor"; - modules = [ { - microvm.hypervisor = "cloud-hypervisor"; - } ]; - } { - id = "crosvm"; - modules = [ { - microvm.hypervisor = "crosvm"; - } ]; - } { - id = "firecracker"; - modules = [ { - microvm.hypervisor = "firecracker"; - } ]; - } { - id = "kvmtool"; - modules = [ { - microvm.hypervisor = "kvmtool"; - } ]; - } { - id = "alioth"; - modules = [ { - microvm.hypervisor = "alioth"; - } ]; - } ] - # ro-store - [ { - # squashfs/erofs - id = null; - } { - # 9pfs - id = "9pstore"; - modules = [ ({ config, ... }: { - microvm = { - shares = [ { - proto = "9p"; - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - } ]; - testing.enableTest = builtins.elem config.microvm.hypervisor [ - # Hypervisors that support 9p - "qemu" "crosvm" "kvmtool" + [ + { + id = "qemu"; + modules = [ + { + microvm.hypervisor = "qemu"; + } + ]; + } + { + id = "qemu-tcg"; + modules = + let + # Emulate a different guest system than the host one + guestSystem = if "${system}" == "x86_64-linux" then "aarch64-unknown-linux-gnu" else "x86_64-linux"; + in + [ + { + microvm = { + hypervisor = "qemu"; + # Force the CPU to be something else than the current + # system, and thus, emulated with qemu's Tiny Code Generator + # (TCG) + cpu = if "${system}" == "x86_64-linux" then "cortex-a53" else "Westmere"; + }; + nixpkgs.crossSystem.config = guestSystem; + } ]; - }; - }) ]; - } ] + } + { + id = "cloud-hypervisor"; + modules = [ + { + microvm.hypervisor = "cloud-hypervisor"; + } + ]; + } + { + id = "crosvm"; + modules = [ + { + microvm.hypervisor = "crosvm"; + } + ]; + } + { + id = "firecracker"; + modules = [ + { + microvm.hypervisor = "firecracker"; + } + ]; + } + { + id = "kvmtool"; + modules = [ + { + microvm.hypervisor = "kvmtool"; + } + ]; + } + { + id = "alioth"; + modules = [ + { + microvm.hypervisor = "alioth"; + } + ]; + } + ] + # ro-store + [ + { + # squashfs/erofs + id = null; + } + { + # 9pfs + id = "9pstore"; + modules = [ + ( + { config, ... }: + { + microvm = { + shares = [ + { + proto = "9p"; + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; + testing.enableTest = builtins.elem config.microvm.hypervisor [ + # Hypervisors that support 9p + "qemu" + "crosvm" + "kvmtool" + ]; + }; + } + ) + ]; + } + ] # rw-store - [ { - # none - id = null; - } { - # overlay volume - id = "overlay"; - modules = [ ({ config, ... }: { - microvm.writableStoreOverlay = "/nix/.rw-store"; - microvm.volumes = [ { - image = "nix-store-overlay.img"; - label = "nix-store"; - mountPoint = config.microvm.writableStoreOverlay; - size = 128; - } ]; - }) ]; - } ] + [ + { + # none + id = null; + } + { + # overlay volume + id = "overlay"; + modules = [ + ( + { config, ... }: + { + microvm.writableStoreOverlay = "/nix/.rw-store"; + microvm.volumes = [ + { + image = "nix-store-overlay.img"; + label = "nix-store"; + mountPoint = config.microvm.writableStoreOverlay; + size = 128; + } + ]; + } + ) + ]; + } + ] # boot.systemd - [ { - # no - id = null; - modules = [ { - boot.initrd.systemd.enable = false; - } ]; - } { - id = "systemd"; - modules = [ { - boot.initrd.systemd.enable = true; - } ]; - } ] + [ + { + # no + id = null; + modules = [ + { + boot.initrd.systemd.enable = false; + } + ]; + } + { + id = "systemd"; + modules = [ + { + boot.initrd.systemd.enable = true; + } + ]; + } + ] # hardened profile - [ { - # no - id = null; - } { - id = "hardened"; - modules = [ ({ modulesPath, ... }: { - imports = [ "${modulesPath}/profiles/hardened.nix" ]; - }) ]; - } ] + [ + { + # no + id = null; + } + { + id = "hardened"; + modules = [ + ( + { modulesPath, ... }: + { + imports = [ "${modulesPath}/profiles/hardened.nix" ]; + } + ) + ]; + } + ] - [ { - # no - id = null; - } { - id = "credentials"; - modules = [ ({ config, pkgs, ... }: { - # This is the guest vm config - microvm = { - credentialFiles.SECRET_BOOTSTRAP_KEY = "/etc/microvm-bootstrap.secret"; - testing.enableTest = builtins.elem config.microvm.hypervisor [ - # Hypervisors that support systemd credentials - "qemu" - ]; - }; - # TODO: need to somehow have the test harness check for the success or failure of this service. - systemd.services.test-secret-availability = { - serviceConfig = { - ImportCredential = "SECRET_BOOTSTRAP_KEY"; - Restart = "no"; - }; - path = [ pkgs.gnugrep pkgs.coreutils ]; - script = '' - cat $CREDENTIALS_DIRECTORY/SECRET_BOOTSTRAP_KEY | grep -q "i am super secret" - if [ $? -ne 0 ]; then - echo "Secret not found at $CREDENTIALS_DIRECTORY/SECRET_BOOTSTRAP_KEY" - exit 1 - fi - ''; - }; - }) ]; - } ] + [ + { + # no + id = null; + } + { + id = "credentials"; + modules = [ + ( + { config, pkgs, ... }: + { + # This is the guest vm config + microvm = { + credentialFiles.SECRET_BOOTSTRAP_KEY = "/etc/microvm-bootstrap.secret"; + testing.enableTest = builtins.elem config.microvm.hypervisor [ + # Hypervisors that support systemd credentials + "qemu" + ]; + }; + # TODO: need to somehow have the test harness check for the success or failure of this service. + systemd.services.test-secret-availability = { + serviceConfig = { + ImportCredential = "SECRET_BOOTSTRAP_KEY"; + Restart = "no"; + }; + path = [ + pkgs.gnugrep + pkgs.coreutils + ]; + script = '' + cat $CREDENTIALS_DIRECTORY/SECRET_BOOTSTRAP_KEY | grep -q "i am super secret" + if [ $? -ne 0 ]; then + echo "Secret not found at $CREDENTIALS_DIRECTORY/SECRET_BOOTSTRAP_KEY" + exit 1 + fi + ''; + }; + } + ) + ]; + } + ] ]; allVariants = let - go = variants: - if variants == [] - then [] - else builtins.concatMap (head: - let - tail = go (builtins.tail variants); - in - if tail == [] - then [ [ head ] ] - else map (t: [ head ] ++ t) tail - ) (builtins.head variants); + go = + variants: + if variants == [ ] then + [ ] + else + builtins.concatMap ( + head: + let + tail = go (builtins.tail variants); + in + if tail == [ ] then [ [ head ] ] else map (t: [ head ] ++ t) tail + ) (builtins.head variants); in - go variants; + go variants; - makeTestConfigs = { modules, system, name }: - builtins.foldl' (result: variant: + makeTestConfigs = + { + modules, + system, + name, + }: + builtins.foldl' ( + result: variant: let configName = builtins.concatStringsSep "-" ( builtins.filter (s: s != null) ( - map ({ id ? null, ... }: id) variant - ++ - [ name ] - )); + map ( + { + id ? null, + ... + }: + id + ) variant + ++ [ name ] + ) + ); nixOS = nixpkgs.lib.nixosSystem { inherit system; - modules = - [ self.nixosModules.microvm - ({ lib, ... }: { - options.microvm.testing.enableTest = lib.mkOption { - type = lib.mkOptionType { - name = "bool merged all true"; - merge = loc: defs: - builtins.all (def: def.value) defs; + modules = [ + self.nixosModules.microvm + ( + { lib, ... }: + { + options.microvm.testing.enableTest = lib.mkOption { + type = lib.mkOptionType { + name = "bool merged all true"; + merge = loc: defs: builtins.all (def: def.value) defs; + }; + default = true; }; - default = true; - }; - }) ] - ++ + } + ) + ] + ++ modules + ++ builtins.concatMap ( + { + modules ? [ ], + ... + }: modules - ++ - builtins.concatMap ({ modules ? [], ... }: modules) variant; + ) variant; }; in - result - // - nixpkgs.lib.optionalAttrs nixOS.config.microvm.testing.enableTest { - ${configName} = nixOS; - } - ) {} allVariants; + result + // nixpkgs.lib.optionalAttrs nixOS.config.microvm.testing.enableTest { + ${configName} = nixOS; + } + ) { } allVariants; - args = { - inherit self nixpkgs system; - inherit makeTestConfigs; - }; + args = { + inherit self nixpkgs system; + inherit makeTestConfigs; + }; in -import ./shellcheck.nix args // -import ./microvm-command.nix args // -import ./imperative-template.nix args // -import ./startup-shutdown.nix args // -import ./shutdown-command.nix args // +import ./shellcheck.nix args +// import ./microvm-command.nix args +// import ./imperative-template.nix args +// import ./startup-shutdown.nix args +// import ./shutdown-command.nix args +// -builtins.foldl' (result: hypervisor: - let - args = { - inherit self nixpkgs system hypervisor; - }; - in - result // - import ./vm.nix args // - import ./iperf.nix args // - import ./machined.nix args -) {} supportedHypervisors + builtins.foldl' ( + result: hypervisor: + let + args = { + inherit + self + nixpkgs + system + hypervisor + ; + }; + in + result // import ./vm.nix args // import ./iperf.nix args // import ./machined.nix args + ) { } supportedHypervisors diff --git a/subrepos/microvm.nix/checks/imperative-template.nix b/subrepos/microvm.nix/checks/imperative-template.nix index f2ea1817..32b4b915 100644 --- a/subrepos/microvm.nix/checks/imperative-template.nix +++ b/subrepos/microvm.nix/checks/imperative-template.nix @@ -6,45 +6,47 @@ }: { - imperative-template = import (nixpkgs + "/nixos/tests/make-test-python.nix") (_: { - name = "imperative-template"; + imperative-template = + import (nixpkgs + "/nixos/tests/make-test-python.nix") + (_: { + name = "imperative-template"; - nodes.host = { - imports = [ self.nixosModules.host ]; - microvm.host.enable = true; - }; + nodes.host = { + imports = [ self.nixosModules.host ]; + microvm.host.enable = true; + }; - testScript = /* python */ '' - host.wait_for_unit("multi-user.target") + testScript = /* python */ '' + host.wait_for_unit("multi-user.target") - host.succeed("mkdir -p /var/lib/microvms/test/current/bin") - host.succeed("""cat > /var/lib/microvms/test/current/bin/microvm-run <<'EOF' - #!/bin/sh - trap 'exit 0' TERM INT - while true; do sleep 1; done - EOF - chmod +x /var/lib/microvms/test/current/bin/microvm-run - """) - host.succeed("""cat > /var/lib/microvms/test/current/bin/microvm-shutdown <<'EOF' - #!/bin/sh - exit 0 - EOF - chmod +x /var/lib/microvms/test/current/bin/microvm-shutdown - """) - host.succeed("chown microvm:kvm -R /var/lib/microvms/") + host.succeed("mkdir -p /var/lib/microvms/test/current/bin") + host.succeed("""cat > /var/lib/microvms/test/current/bin/microvm-run <<'EOF' + #!/bin/sh + trap 'exit 0' TERM INT + while true; do sleep 1; done + EOF + chmod +x /var/lib/microvms/test/current/bin/microvm-run + """) + host.succeed("""cat > /var/lib/microvms/test/current/bin/microvm-shutdown <<'EOF' + #!/bin/sh + exit 0 + EOF + chmod +x /var/lib/microvms/test/current/bin/microvm-shutdown + """) + host.succeed("chown microvm:kvm -R /var/lib/microvms/") - # Should work in imperative mode without microvm-register/microvm-unregister scripts. - host.succeed("systemctl start microvm@test.service") - host.wait_for_unit("microvm@test.service") + # Should work in imperative mode without microvm-register/microvm-unregister scripts. + host.succeed("systemctl start microvm@test.service") + host.wait_for_unit("microvm@test.service") - host.succeed("systemctl stop microvm@test.service") - host.wait_until_succeeds("! systemctl is-active --quiet microvm@test.service") - ''; + host.succeed("systemctl stop microvm@test.service") + host.wait_until_succeeds("! systemctl is-active --quiet microvm@test.service") + ''; - meta.timeout = 600; - }) - { - inherit system; - pkgs = nixpkgs.legacyPackages.${system}; - }; + meta.timeout = 600; + }) + { + inherit system; + pkgs = nixpkgs.legacyPackages.${system}; + }; } diff --git a/subrepos/microvm.nix/checks/iperf.nix b/subrepos/microvm.nix/checks/iperf.nix index 82694835..240bb963 100644 --- a/subrepos/microvm.nix/checks/iperf.nix +++ b/subrepos/microvm.nix/checks/iperf.nix @@ -1,69 +1,93 @@ -{ self, nixpkgs, system, hypervisor }: +{ + self, + nixpkgs, + system, + hypervisor, +}: nixpkgs.lib.optionalAttrs (builtins.elem hypervisor self.lib.hypervisorsWithNetwork) { # Run a VM with to test MicroVM virtiofsd - "vm-${hypervisor}-iperf" = import (nixpkgs + "/nixos/tests/make-test-python.nix") ({ pkgs, ... }: { - name = "vm-${hypervisor}-iperf"; - nodes.vm = { - imports = [ self.nixosModules.host ]; - microvm.vms."${hypervisor}-iperf-server".flake = nixpkgs.legacyPackages.${system}.runCommand "${hypervisor}-iperf-server.flake" { - passthru.nixosConfigurations."${hypervisor}-iperf-server" = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - self.nixosModules.microvm - { - microvm = { - inherit hypervisor; - interfaces = [ { - type = "tap"; - id = "microvm"; - mac = "00:02:00:01:01:01"; - } ]; - }; - networking.hostName = "${hypervisor}-microvm"; - networking = { - interfaces.eth0 = { - useDHCP = false; - ipv4.addresses = [ { - address = "10.0.0.1"; - prefixLength = 24; - } ]; - }; - firewall.enable = false; - }; - services.iperf3.enable = true; - } - ]; - }; - } "touch $out"; - environment.systemPackages = with pkgs; [ #with nixpkgs.legacyPackages.${system}; [ - iperf iproute2 - ]; - virtualisation = { - # larger than the defaults - memorySize = 2048; - cores = 2; - # 9P performance optimization that quelches a qemu warning - msize = 65536; - # # allow building packages - # writableStore = true; - # # keep the store paths built inside the VM across reboots - # writableStoreUseTmpfs = false; - qemu.options = [ - "-cpu" - { - "aarch64-linux" = "cortex-a72"; - "x86_64-linux" = "kvm64,+svm,+vmx"; - }.${system} - ]; + "vm-${hypervisor}-iperf" = + import (nixpkgs + "/nixos/tests/make-test-python.nix") + ( + { pkgs, ... }: + { + name = "vm-${hypervisor}-iperf"; + nodes.vm = { + imports = [ self.nixosModules.host ]; + microvm.vms."${hypervisor}-iperf-server".flake = + nixpkgs.legacyPackages.${system}.runCommand "${hypervisor}-iperf-server.flake" + { + passthru.nixosConfigurations."${hypervisor}-iperf-server" = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + self.nixosModules.microvm + { + microvm = { + inherit hypervisor; + interfaces = [ + { + type = "tap"; + id = "microvm"; + mac = "00:02:00:01:01:01"; + } + ]; + }; + networking.hostName = "${hypervisor}-microvm"; + networking = { + interfaces.eth0 = { + useDHCP = false; + ipv4.addresses = [ + { + address = "10.0.0.1"; + prefixLength = 24; + } + ]; + }; + firewall.enable = false; + }; + services.iperf3.enable = true; + } + ]; + }; + } + "touch $out"; + environment.systemPackages = with pkgs; [ + # with nixpkgs.legacyPackages.${system}; [ + iperf + iproute2 + ]; + virtualisation = { + # larger than the defaults + memorySize = 2048; + cores = 2; + # 9P performance optimization that quelches a qemu warning + msize = 65536; + # # allow building packages + # writableStore = true; + # # keep the store paths built inside the VM across reboots + # writableStoreUseTmpfs = false; + qemu.options = [ + "-cpu" + { + "aarch64-linux" = "cortex-a72"; + "x86_64-linux" = "kvm64,+svm,+vmx"; + } + .${system} + ]; + }; + }; + testScript = '' + vm.wait_for_unit("microvm@${hypervisor}-iperf-server.service", timeout = 900) + vm.succeed("ip addr add 10.0.0.2/24 dev microvm") + result = vm.wait_until_succeeds("iperf -c 10.0.0.1", timeout = 180) + print(result) + ''; + meta.timeout = 1800; + } + ) + { + inherit system; + pkgs = nixpkgs.legacyPackages.${system}; }; - }; - testScript = '' - vm.wait_for_unit("microvm@${hypervisor}-iperf-server.service", timeout = 900) - vm.succeed("ip addr add 10.0.0.2/24 dev microvm") - result = vm.wait_until_succeeds("iperf -c 10.0.0.1", timeout = 180) - print(result) - ''; - meta.timeout = 1800; - }) { inherit system; pkgs = nixpkgs.legacyPackages.${system}; }; } diff --git a/subrepos/microvm.nix/checks/machined.nix b/subrepos/microvm.nix/checks/machined.nix index 3c12b109..419f0ae5 100644 --- a/subrepos/microvm.nix/checks/machined.nix +++ b/subrepos/microvm.nix/checks/machined.nix @@ -1,4 +1,9 @@ -{ self, nixpkgs, system, hypervisor }: +{ + self, + nixpkgs, + system, + hypervisor, +}: let vmName = "machined-test"; @@ -55,11 +60,14 @@ in # Verify the machine class is 'vm' host.succeed("machinectl show '${vmName}' --property=Class | grep -q 'vm'") - ${lib.optionalString ((lib.versionAtLeast pkgs.systemd.version "259") && hypervisor != "cloud-hypervisor") '' - # On systemd >=259 RegisterMachineEx path should expose VSOCK/SSH metadata - host.succeed("machinectl show '${vmName}' --property=VSockCID | grep -q 'VSockCID=${toString vsockCid}'") - host.succeed("machinectl show '${vmName}' --property=SSHAddress | grep -q 'SSHAddress=vsock/${toString vsockCid}'") - ''} + ${lib.optionalString + ((lib.versionAtLeast pkgs.systemd.version "259") && hypervisor != "cloud-hypervisor") + '' + # On systemd >=259 RegisterMachineEx path should expose VSOCK/SSH metadata + host.succeed("machinectl show '${vmName}' --property=VSockCID | grep -q 'VSockCID=${toString vsockCid}'") + host.succeed("machinectl show '${vmName}' --property=SSHAddress | grep -q 'SSHAddress=vsock/${toString vsockCid}'") + '' + } # Terminate the VM via machinectl (sends SIGTERM to hypervisor) host.succeed("machinectl terminate '${vmName}'") diff --git a/subrepos/microvm.nix/checks/shellcheck.nix b/subrepos/microvm.nix/checks/shellcheck.nix index 49ae25f2..d6d1fd56 100644 --- a/subrepos/microvm.nix/checks/shellcheck.nix +++ b/subrepos/microvm.nix/checks/shellcheck.nix @@ -1,15 +1,23 @@ -{ self, nixpkgs, system, ... }: +{ + self, + nixpkgs, + system, + ... +}: let pkgs = nixpkgs.legacyPackages.${system}; -in { - shellcheck = pkgs.runCommand "microvm-shellcheck" { - src = self.packages.${system}.microvm; - nativeBuildInputs = [ pkgs.shellcheck ]; - } '' - shellcheck $src/bin/* - touch $out - ''; +in +{ + shellcheck = + pkgs.runCommand "microvm-shellcheck" + { + src = self.packages.${system}.microvm; + nativeBuildInputs = [ pkgs.shellcheck ]; + } + '' + shellcheck $src/bin/* + touch $out + ''; } - diff --git a/subrepos/microvm.nix/checks/shutdown-command.nix b/subrepos/microvm.nix/checks/shutdown-command.nix index 2c5ce9b1..f9caccb6 100644 --- a/subrepos/microvm.nix/checks/shutdown-command.nix +++ b/subrepos/microvm.nix/checks/shutdown-command.nix @@ -1,4 +1,9 @@ -{ nixpkgs, system, makeTestConfigs, ... }: +{ + nixpkgs, + system, + makeTestConfigs, + ... +}: let pkgs = nixpkgs.legacyPackages.${system}; @@ -7,36 +12,42 @@ let name = "shutdown-command"; inherit system; modules = [ - ({ config, lib, ... }: { - networking = { - hostName = "microvm-test"; - useDHCP = false; - }; - microvm = { - socket = "./microvm.sock"; - crosvm.pivotRoot = "/build/empty"; - testing.enableTest = config.microvm.declaredRunner.canShutdown; - }; - system.stateVersion = lib.mkDefault lib.trivial.release; - }) + ( + { config, lib, ... }: + { + networking = { + hostName = "microvm-test"; + useDHCP = false; + }; + microvm = { + socket = "./microvm.sock"; + crosvm.pivotRoot = "/build/empty"; + testing.enableTest = config.microvm.declaredRunner.canShutdown; + }; + system.stateVersion = lib.mkDefault lib.trivial.release; + } + ) ]; }; in -builtins.mapAttrs (_: nixos: - pkgs.runCommandLocal "microvm-test-shutdown-command" { - nativeBuildInputs = [ - nixos.config.microvm.declaredRunner - pkgs.p7zip - ]; - requiredSystemFeatures = [ "kvm" ]; - meta.timeout = 120; - } '' - set -m - microvm-run > $out & +builtins.mapAttrs ( + _: nixos: + pkgs.runCommandLocal "microvm-test-shutdown-command" + { + nativeBuildInputs = [ + nixos.config.microvm.declaredRunner + pkgs.p7zip + ]; + requiredSystemFeatures = [ "kvm" ]; + meta.timeout = 120; + } + '' + set -m + microvm-run > $out & - sleep 10 - echo Now shutting down - microvm-shutdown - '' + sleep 10 + echo Now shutting down + microvm-shutdown + '' ) configs diff --git a/subrepos/microvm.nix/checks/startup-shutdown.nix b/subrepos/microvm.nix/checks/startup-shutdown.nix index 08f73215..a6499485 100644 --- a/subrepos/microvm.nix/checks/startup-shutdown.nix +++ b/subrepos/microvm.nix/checks/startup-shutdown.nix @@ -1,4 +1,9 @@ -{ nixpkgs, system, makeTestConfigs, ... }: +{ + nixpkgs, + system, + makeTestConfigs, + ... +}: let pkgs = nixpkgs.legacyPackages.${system}; @@ -8,82 +13,103 @@ let inherit system; modules = [ # Run a MicroVM that immediately shuts down again - ({ config, lib, pkgs, ... }: { - networking = { - hostName = "microvm-test"; - useDHCP = false; - }; - microvm = { - volumes = [ { - image = "output.img"; - label = "output"; - mountPoint = "/output"; - size = 32; - } ]; - crosvm.pivotRoot = "/build/empty"; - }; - systemd.services.poweroff-again = { - wantedBy = [ "multi-user.target" ]; - serviceConfig.Type = "idle"; - script = - let - exit = { - qemu = "reboot"; - firecracker = "reboot"; - cloud-hypervisor = "poweroff"; - crosvm = "reboot"; - kvmtool = "reboot"; - alioth = "poweroff"; - }.${config.microvm.hypervisor}; - in '' - ${pkgs.coreutils}/bin/uname > /output/kernel-name - ${pkgs.coreutils}/bin/uname -m > /output/machine-name + ( + { + config, + lib, + pkgs, + ... + }: + { + networking = { + hostName = "microvm-test"; + useDHCP = false; + }; + microvm = { + volumes = [ + { + image = "output.img"; + label = "output"; + mountPoint = "/output"; + size = 32; + } + ]; + crosvm.pivotRoot = "/build/empty"; + }; + systemd.services.poweroff-again = { + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "idle"; + script = + let + exit = + { + qemu = "reboot"; + firecracker = "reboot"; + cloud-hypervisor = "poweroff"; + crosvm = "reboot"; + kvmtool = "reboot"; + alioth = "poweroff"; + } + .${config.microvm.hypervisor}; + in + '' + ${pkgs.coreutils}/bin/uname > /output/kernel-name + ${pkgs.coreutils}/bin/uname -m > /output/machine-name - ${exit} - ''; - }; - system.stateVersion = lib.mkDefault lib.trivial.release; - }) + ${exit} + ''; + }; + system.stateVersion = lib.mkDefault lib.trivial.release; + } + ) ]; }; in -builtins.mapAttrs (_: nixos: - pkgs.runCommandLocal "microvm-test-startup-shutdown" { - nativeBuildInputs = [ - nixos.config.microvm.declaredRunner - pkgs.p7zip - ]; - requiredSystemFeatures = [ "kvm" ]; - meta.timeout = 120; - } (let - expectedMachineName = (crossSystem: - if crossSystem == null then - expectedMachineName { config = system; } - else if crossSystem.config == "aarch64-unknown-linux-gnu" then - "aarch64" - else if crossSystem.config == "x86_64-linux" then - "x86_64" - else throw "unknown machine name (${crossSystem.config})" - ); - in '' - microvm-run +builtins.mapAttrs ( + _: nixos: + pkgs.runCommandLocal "microvm-test-startup-shutdown" + { + nativeBuildInputs = [ + nixos.config.microvm.declaredRunner + pkgs.p7zip + ]; + requiredSystemFeatures = [ "kvm" ]; + meta.timeout = 120; + } + ( + let + expectedMachineName = ( + crossSystem: + if crossSystem == null then + expectedMachineName { config = system; } + else if crossSystem.config == "aarch64-unknown-linux-gnu" then + "aarch64" + else if crossSystem.config == "x86_64-linux" then + "x86_64" + else + throw "unknown machine name (${crossSystem.config})" + ); + in + '' + microvm-run - 7z e output.img kernel-name machine-name + 7z e output.img kernel-name machine-name - EXPECTED_KERNEL_NAME="Linux" - if [ "$(cat kernel-name)" != "$EXPECTED_KERNEL_NAME" ] ; then - echo "Kernel does not match (got: $(cat kernel-name); expected: $EXPECTED_KERNEL_NAME)" - exit 1 - fi + EXPECTED_KERNEL_NAME="Linux" + if [ "$(cat kernel-name)" != "$EXPECTED_KERNEL_NAME" ] ; then + echo "Kernel does not match (got: $(cat kernel-name); expected: $EXPECTED_KERNEL_NAME)" + exit 1 + fi - EXPECTED_MACHINE_NAME="${expectedMachineName nixos.config.nixpkgs.crossSystem}" - if [ "$(cat machine-name)" != "$EXPECTED_MACHINE_NAME" ] ; then - echo "Machine does not match (got: $(cat machine-name); expected: $EXPECTED_MACHINE_NAME)" - exit 1 - fi + EXPECTED_MACHINE_NAME="${expectedMachineName nixos.config.nixpkgs.crossSystem}" + if [ "$(cat machine-name)" != "$EXPECTED_MACHINE_NAME" ] ; then + echo "Machine does not match (got: $(cat machine-name); expected: $EXPECTED_MACHINE_NAME)" + exit 1 + fi - mkdir $out - cp {kernel-name,machine-name} $out - '') + mkdir $out + cp {kernel-name,machine-name} $out + '' + ) ) configs diff --git a/subrepos/microvm.nix/checks/vm.nix b/subrepos/microvm.nix/checks/vm.nix index 0e918360..00c964b4 100644 --- a/subrepos/microvm.nix/checks/vm.nix +++ b/subrepos/microvm.nix/checks/vm.nix @@ -1,29 +1,44 @@ -{ self, nixpkgs, system, hypervisor }: +{ + self, + nixpkgs, + system, + hypervisor, +}: { # Run a VM with a MicroVM - "vm-${hypervisor}" = import (nixpkgs + "/nixos/tests/make-test-python.nix") ({ ... }: { - name = "vm-${hypervisor}"; - nodes.vm = { - imports = [ self.nixosModules.host ]; - - virtualisation.qemu.options = [ - "-cpu" + "vm-${hypervisor}" = + import (nixpkgs + "/nixos/tests/make-test-python.nix") + ( + { ... }: { - "aarch64-linux" = "cortex-a72"; - "x86_64-linux" = "kvm64,+svm,+vmx"; - }.${system} - ]; - # Must be big enough for the store overlay volume - virtualisation.diskSize = 4096; + name = "vm-${hypervisor}"; + nodes.vm = { + imports = [ self.nixosModules.host ]; + + virtualisation.qemu.options = [ + "-cpu" + { + "aarch64-linux" = "cortex-a72"; + "x86_64-linux" = "kvm64,+svm,+vmx"; + } + .${system} + ]; + # Must be big enough for the store overlay volume + virtualisation.diskSize = 4096; - environment.etc."microvm-bootstrap.secret".text = "i am super secret"; + environment.etc."microvm-bootstrap.secret".text = "i am super secret"; - microvm.vms."${system}-${hypervisor}-example".flake = self; - }; - testScript = '' - vm.wait_for_unit("microvm@${system}-${hypervisor}-example.service", timeout = 1200) - ''; - meta.timeout = 1800; - }) { inherit system; pkgs = nixpkgs.legacyPackages.${system}; }; + microvm.vms."${system}-${hypervisor}-example".flake = self; + }; + testScript = '' + vm.wait_for_unit("microvm@${system}-${hypervisor}-example.service", timeout = 1200) + ''; + meta.timeout = 1800; + } + ) + { + inherit system; + pkgs = nixpkgs.legacyPackages.${system}; + }; } diff --git a/subrepos/microvm.nix/doc/src/advanced-network.md b/subrepos/microvm.nix/doc/src/advanced-network.md index 4cc844cd..8fbc880a 100644 --- a/subrepos/microvm.nix/doc/src/advanced-network.md +++ b/subrepos/microvm.nix/doc/src/advanced-network.md @@ -47,6 +47,7 @@ opting for declarative, versioned configuration instead. Last, the TAP interfaces of MicroVMs shall be attached to this central bridge. Make sure your `matchConfig` matches just the interfaces you want! + ```nix systemd.network.networks."11-microvm" = { matchConfig.Name = "vm-*"; @@ -59,7 +60,7 @@ systemd.network.networks."11-microvm" = { IPv4 addresses are exhausted. It is a very common case that you get one public IPv4 address for your machine. The solution is to route -your internal virtual machines with *Network Address Translation*. +your internal virtual machines with _Network Address Translation_. You might not get a dedicated /64 IPv6 prefix to route to your MicroVMs. NAT works for this address family, too! diff --git a/subrepos/microvm.nix/doc/src/conventions.md b/subrepos/microvm.nix/doc/src/conventions.md index 653ac26d..56a7e32e 100644 --- a/subrepos/microvm.nix/doc/src/conventions.md +++ b/subrepos/microvm.nix/doc/src/conventions.md @@ -5,9 +5,8 @@ packages with the flake's `host` module. While the **microvm.nix** flake was designed for single-server usage, you can build different MicroVM deployments using the information on this page. - | `nixosModule.microvm` option | MicroVM package file | `nixosModules.host` systemd service | Description | -|------------------------------|----------------------------------------|-------------------------------------|-----------------------------------------------------------------------------------------------| +| ---------------------------- | -------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------- | | `microvm.hypervisor` | `bin/microvm-run` | `microvm@.service` | Start script for the main MicroVM process | | `microvm.hypervisor` | `bin/microvm-shutdown` | `microvm@.service` | Script for graceful shutdown of the MicroVM (i.e. triggering the power button) | | `microvm.interfaces.*.id` | `share/microvm/tap-interfaces` | `microvm-tap-interfaces@.service` | Names of the tap network interfaces to setup for the proper user | @@ -16,7 +15,6 @@ MicroVM deployments using the information on this page. | `microvm.shares.*.socket` | `share/microvm/virtiofs/${tag}/socket` | `microvm-virtiofsd@.service` | **virtiofsd** socket path by tag | | `microvm.systemSymlink` | `share/microvm/system` | | `config.system.build.toplevel` symlink, used for comparing versions when running `microvm -l` | - ## Generating custom operating system hypervisor packages Because a microvm.nix runner package completely defines how to run the diff --git a/subrepos/microvm.nix/doc/src/declarative.md b/subrepos/microvm.nix/doc/src/declarative.md index 1eafa9ff..8d5603b9 100644 --- a/subrepos/microvm.nix/doc/src/declarative.md +++ b/subrepos/microvm.nix/doc/src/declarative.md @@ -51,7 +51,7 @@ nixos-containers work if you are familiar with those. ## Declarative deployment -Why *deployed*? The per-MicroVM subdirectory under `/var/lib/microvms` +Why _deployed_? The per-MicroVM subdirectory under `/var/lib/microvms` is only created if it did not exist before. This behavior is intended to ensure existence of MicroVMs that are critical to operation. To update them later you will have to use the [imperative microvm diff --git a/subrepos/microvm.nix/doc/src/interfaces.md b/subrepos/microvm.nix/doc/src/interfaces.md index fcda4eb6..638ce4ff 100644 --- a/subrepos/microvm.nix/doc/src/interfaces.md +++ b/subrepos/microvm.nix/doc/src/interfaces.md @@ -2,6 +2,7 @@ Declare a MicroVM's virtual network interfaces like this in its NixOS configuration: + ```nix { microvm.interfaces = [ { @@ -80,7 +81,7 @@ microvm.binScripts.tap-up = lib.mkAfter '' ## `type = "macvtap"` -*MACVTAP* interfaces attach to a host's physical network interface, +_MACVTAP_ interfaces attach to a host's physical network interface, joining the same Ethernet segment with a separate MAC address. Before running a MicroVM interactively from a package, do the @@ -111,4 +112,4 @@ This mode lets qemu create a tap interface and attach it to a bridge. The `qemu-bridge-helper` binary needs to be setup with the proper permissions. See the `host` module for that. qemu will be run -*without* `-sandbox on` in order for this contraption to work. +_without_ `-sandbox on` in order for this contraption to work. diff --git a/subrepos/microvm.nix/doc/src/intro.md b/subrepos/microvm.nix/doc/src/intro.md index 058f44b9..60809b61 100644 --- a/subrepos/microvm.nix/doc/src/intro.md +++ b/subrepos/microvm.nix/doc/src/intro.md @@ -40,13 +40,13 @@ you know how to put a NixOS config into a `flake.nix` file. ## Just Virtual Machines? Full virtualization has been available for a long time with QEMU and -VirtualBox. The *MicroVM* machine type highlights that virtualization +VirtualBox. The _MicroVM_ machine type highlights that virtualization overhead has been reduced a lot by replacing emulated devices with -*virtio* interfaces that have been optimized for this environment. +_virtio_ interfaces that have been optimized for this environment. This Flake offers you to run your MicroVMs not only on QEMU but with other Hypervisors that have been explicitly authored for -*virtio*. Some of them are written in Rust, a programming language +_virtio_. Some of them are written in Rust, a programming language that is renowned for being safer than C. On macOS, vfkit leverages Apple's native Virtualization.framework for running Linux VMs. Note that building the guest still requires access to a Linux builder; see diff --git a/subrepos/microvm.nix/doc/src/microvm-command.md b/subrepos/microvm.nix/doc/src/microvm-command.md index 11a85f0e..f119f1a1 100644 --- a/subrepos/microvm.nix/doc/src/microvm-command.md +++ b/subrepos/microvm.nix/doc/src/microvm-command.md @@ -29,7 +29,7 @@ microvm.autostart = [ ## Update a MicroVM -*Updating* does not refresh your packages but simply rebuilds the +_Updating_ does not refresh your packages but simply rebuilds the MicroVM. Use `nix flake update` to get new package versions. ```bash diff --git a/subrepos/microvm.nix/doc/src/options.md b/subrepos/microvm.nix/doc/src/options.md index 646d3c7a..fd8a01a2 100644 --- a/subrepos/microvm.nix/doc/src/options.md +++ b/subrepos/microvm.nix/doc/src/options.md @@ -4,7 +4,7 @@ By including the `microvm` module a set of NixOS options is made available for customization. These are the most important ones: | Option | Purpose | -|--------------------------------|-----------------------------------------------------------------------------------------------------| +| ------------------------------ | --------------------------------------------------------------------------------------------------- | | `microvm.hypervisor` | Hypervisor to use by default in `microvm.declaredRunner` | | `microvm.vcpu` | Number of Virtual CPU cores | | `microvm.mem` | RAM allocation in MB | @@ -22,6 +22,5 @@ available for customization. These are the most important ones: | `microvm.storeOnDisk` | Enables the store on the boot squashfs even in the presence of a share with the host's `/nix/store` | | `microvm.writableStoreOverlay` | Optional string of the path where all writes to `/nix/store` should go to. | -See [the options declarations]( -https://github.com/microvm-nix/microvm.nix/blob/main/nixos-modules/microvm/options.nix) +See [the options declarations](https://github.com/microvm-nix/microvm.nix/blob/main/nixos-modules/microvm/options.nix) for a full reference. diff --git a/subrepos/microvm.nix/doc/src/output-options.md b/subrepos/microvm.nix/doc/src/output-options.md index ad27010e..8db18179 100644 --- a/subrepos/microvm.nix/doc/src/output-options.md +++ b/subrepos/microvm.nix/doc/src/output-options.md @@ -4,7 +4,7 @@ Hypervisor runners are provided in the `config` generated by a nixosSystem for you to use inside and outside your configuration. | Option | Purpose | -|--------------------------|-----------------------------------------------------------| +| ------------------------ | --------------------------------------------------------- | | `microvm.declaredRunner` | Runner package selected according to `microvm.hypervisor` | | `microvm.runners` | Attribute set of runner packages per known Hypervisor. | @@ -17,7 +17,7 @@ nix run .#nixosConfigurations.my-microvm.config.microvm.declaredRunner The `microvm.runners` option provides a runner for each known Hypervisor regardless of the `microvm.hypervisor` config setting. To -build *my-microvm* for Firecracker for example: +build _my-microvm_ for Firecracker for example: ```bash nix run .#nixosConfigurations.my-microvm.config.microvm.runners.firecracker diff --git a/subrepos/microvm.nix/doc/src/packages.md b/subrepos/microvm.nix/doc/src/packages.md index 9aab7e28..4872554b 100644 --- a/subrepos/microvm.nix/doc/src/packages.md +++ b/subrepos/microvm.nix/doc/src/packages.md @@ -10,6 +10,7 @@ and no virtiofsd is started. These can be worked around by relying on ## Immediately running a nixosConfiguration To run a `nixosConfiguration` off your Flake directly use: + ```bash nix run .#nixosConfigurations.my-microvm.config.microvm.declaredRunner ``` @@ -18,6 +19,7 @@ nix run .#nixosConfigurations.my-microvm.config.microvm.declaredRunner To add this runner permanently add a package like this to the outputs of your `flake.nix`: + ```nix packages.x86_64-linux.my-microvm = self.nixosConfigurations.my-microvm.config.microvm.declaredRunner; ``` diff --git a/subrepos/microvm.nix/doc/src/routed-network.md b/subrepos/microvm.nix/doc/src/routed-network.md index 04944bcf..c9e9f133 100644 --- a/subrepos/microvm.nix/doc/src/routed-network.md +++ b/subrepos/microvm.nix/doc/src/routed-network.md @@ -18,7 +18,7 @@ the bridge. ## Addressing Compared to one Ethernet where we assign a large subnet like -`10.0.0.0/24`, we will now only deal with *Host Routes* where the +`10.0.0.0/24`, we will now only deal with _Host Routes_ where the prefix length is `/32` for IPv4 and `/128` for IPv6. Note that by doing this we no longer lose precious space to a subnet's network and broadcast addresses. diff --git a/subrepos/microvm.nix/doc/src/shares.md b/subrepos/microvm.nix/doc/src/shares.md index 53a60122..4ec8d42c 100644 --- a/subrepos/microvm.nix/doc/src/shares.md +++ b/subrepos/microvm.nix/doc/src/shares.md @@ -41,7 +41,6 @@ have options -o xattr=sa -o acltype=posixacl - ## Sharing a host's `/nix/store` If a share with `source = "/nix/store"` is defined, size and build diff --git a/subrepos/microvm.nix/doc/src/simple-network.md b/subrepos/microvm.nix/doc/src/simple-network.md index 12849c2f..50d8e1a1 100644 --- a/subrepos/microvm.nix/doc/src/simple-network.md +++ b/subrepos/microvm.nix/doc/src/simple-network.md @@ -10,6 +10,7 @@ plentiful. If not, head over to the Because we already use systemd for MicroVM startup, let's pick `systemd-networkd`: + ```nix networking.useNetworkd = true; ``` diff --git a/subrepos/microvm.nix/doc/src/ssh-deploy.md b/subrepos/microvm.nix/doc/src/ssh-deploy.md index 208ee962..544385d1 100644 --- a/subrepos/microvm.nix/doc/src/ssh-deploy.md +++ b/subrepos/microvm.nix/doc/src/ssh-deploy.md @@ -19,7 +19,7 @@ Let's explore alternative ways before detailing our elaboration: ## microvm.deploy.rebuild -The *easy* interface that is named after `nixos-rebuild` combines the +The _easy_ interface that is named after `nixos-rebuild` combines the two scripts that are described below: - First, we evaluate locally and build remotely with diff --git a/subrepos/microvm.nix/doc/src/vfkit-rosetta.md b/subrepos/microvm.nix/doc/src/vfkit-rosetta.md index 0c98a161..299377cd 100644 --- a/subrepos/microvm.nix/doc/src/vfkit-rosetta.md +++ b/subrepos/microvm.nix/doc/src/vfkit-rosetta.md @@ -61,7 +61,7 @@ You can use `pkgsCross.gnu64.` to cross-compile any package from nixpkg ## Options Reference | Option | Type | Default | Description | -|-----------------------------------------|------|---------|---------------------------------| +| --------------------------------------- | ---- | ------- | ------------------------------- | | `microvm.vfkit.rosetta.enable` | bool | `false` | Enable Rosetta support | | `microvm.vfkit.rosetta.install` | bool | `false` | Auto-install Rosetta if missing | | `microvm.vfkit.rosetta.ignoreIfMissing` | bool | `false` | Continue if Rosetta unavailable | diff --git a/subrepos/microvm.nix/examples/graphics.nix b/subrepos/microvm.nix/examples/graphics.nix index ea2b9eb2..89f95553 100644 --- a/subrepos/microvm.nix/examples/graphics.nix +++ b/subrepos/microvm.nix/examples/graphics.nix @@ -1,6 +1,9 @@ -{ self, nixpkgs, system -, packages ? "" -, tapInterface ? null +{ + self, + nixpkgs, + system, + packages ? "", + tapInterface ? null, }: nixpkgs.lib.nixosSystem { @@ -10,66 +13,73 @@ nixpkgs.lib.nixosSystem { # this runs as a MicroVM self.nixosModules.microvm - ({ lib, pkgs, ... }: { - microvm = { - hypervisor = "cloud-hypervisor"; - graphics.enable = true; - interfaces = lib.optional (tapInterface != null) { - type = "tap"; - id = tapInterface; - mac = "00:00:00:00:00:02"; + ( + { lib, pkgs, ... }: + { + microvm = { + hypervisor = "cloud-hypervisor"; + graphics.enable = true; + interfaces = lib.optional (tapInterface != null) { + type = "tap"; + id = tapInterface; + mac = "00:00:00:00:00:02"; + }; }; - }; - networking.hostName = "graphical-microvm"; - system.stateVersion = lib.trivial.release; - nixpkgs.overlays = [ self.overlay ]; + networking.hostName = "graphical-microvm"; + system.stateVersion = lib.trivial.release; + nixpkgs.overlays = [ self.overlay ]; - services.getty.autologinUser = "user"; - users.users.user = { - password = ""; - group = "user"; - isNormalUser = true; - extraGroups = [ "wheel" "video" ]; - }; - users.groups.user = {}; - security.sudo = { - enable = true; - wheelNeedsPassword = false; - }; + services.getty.autologinUser = "user"; + users.users.user = { + password = ""; + group = "user"; + isNormalUser = true; + extraGroups = [ + "wheel" + "video" + ]; + }; + users.groups.user = { }; + security.sudo = { + enable = true; + wheelNeedsPassword = false; + }; - environment.sessionVariables = { - WAYLAND_DISPLAY = "wayland-1"; - DISPLAY = ":0"; - QT_QPA_PLATFORM = "wayland"; # Qt Applications - GDK_BACKEND = "wayland"; # GTK Applications - XDG_SESSION_TYPE = "wayland"; # Electron Applications - SDL_VIDEODRIVER = "wayland"; - CLUTTER_BACKEND = "wayland"; - }; + environment.sessionVariables = { + WAYLAND_DISPLAY = "wayland-1"; + DISPLAY = ":0"; + QT_QPA_PLATFORM = "wayland"; # Qt Applications + GDK_BACKEND = "wayland"; # GTK Applications + XDG_SESSION_TYPE = "wayland"; # Electron Applications + SDL_VIDEODRIVER = "wayland"; + CLUTTER_BACKEND = "wayland"; + }; - systemd.user.services.wayland-proxy = { - enable = true; - description = "Wayland Proxy"; - serviceConfig = with pkgs; { - # Environment = "WAYLAND_DISPLAY=wayland-1"; - ExecStart = "${wayland-proxy-virtwl}/bin/wayland-proxy-virtwl --virtio-gpu --x-display=0 --xwayland-binary=${xwayland}/bin/Xwayland"; - Restart = "on-failure"; - RestartSec = 5; + systemd.user.services.wayland-proxy = { + enable = true; + description = "Wayland Proxy"; + serviceConfig = with pkgs; { + # Environment = "WAYLAND_DISPLAY=wayland-1"; + ExecStart = "${wayland-proxy-virtwl}/bin/wayland-proxy-virtwl --virtio-gpu --x-display=0 --xwayland-binary=${xwayland}/bin/Xwayland"; + Restart = "on-failure"; + RestartSec = 5; + }; + wantedBy = [ "default.target" ]; }; - wantedBy = [ "default.target" ]; - }; - environment.systemPackages = with pkgs; [ - xdg-utils # Required - ] ++ map (package: - lib.attrByPath (lib.splitString "." package) (throw "Package ${package} not found in nixpkgs") pkgs - ) ( - builtins.filter (package: - package != "" - ) (lib.splitString " " packages)); + environment.systemPackages = + with pkgs; + [ + xdg-utils # Required + ] + ++ map ( + package: + lib.attrByPath (lib.splitString "." package) (throw "Package ${package} not found in nixpkgs") pkgs + ) (builtins.filter (package: package != "") (lib.splitString " " packages)); - hardware.graphics.enable = true; - }) + hardware.graphics.enable = true; + } + ) ]; } diff --git a/subrepos/microvm.nix/examples/microvms-host.nix b/subrepos/microvm.nix/examples/microvms-host.nix index 8556d555..d42ad180 100644 --- a/subrepos/microvm.nix/examples/microvms-host.nix +++ b/subrepos/microvm.nix/examples/microvms-host.nix @@ -1,5 +1,9 @@ # `nix run microvm#vm` -{ self, nixpkgs, system }: +{ + self, + nixpkgs, + system, +}: nixpkgs.lib.nixosSystem { inherit system; @@ -10,20 +14,24 @@ nixpkgs.lib.nixosSystem { # this runs as a MicroVM that nests MicroVMs self.nixosModules.microvm - ({ config, lib, ... }: + ( + { config, lib, ... }: let inherit (self.lib) hypervisors; hypervisorMacAddrs = builtins.listToAttrs ( - map (hypervisor: + map ( + hypervisor: let hash = builtins.hashString "sha256" hypervisor; c = off: builtins.substring off 2 hash; mac = "${builtins.substring 0 1 hash}2:${c 2}:${c 4}:${c 6}:${c 8}:${c 10}"; - in { + in + { name = hypervisor; value = mac; - }) hypervisors + } + ) hypervisors ); hypervisorIPv4Addrs = builtins.listToAttrs ( @@ -33,7 +41,8 @@ nixpkgs.lib.nixosSystem { }) hypervisors ); - in { + in + { networking.hostName = "microvms-host"; system.stateVersion = lib.trivial.release; users.users.root.password = ""; @@ -56,11 +65,13 @@ nixpkgs.lib.nixosSystem { # Use QEMU because nested virtualization and user networking # are required. hypervisor = "qemu"; - interfaces = [ { - type = "user"; - id = "qemu"; - mac = "02:00:00:01:01:01"; - } ]; + interfaces = [ + { + type = "user"; + id = "qemu"; + mac = "02:00:00:01:01:01"; + } + ]; }; # Nested MicroVMs (a *host* option) @@ -71,11 +82,13 @@ nixpkgs.lib.nixosSystem { microvm = { inherit hypervisor; - interfaces = [ { - type = "tap"; - id = "vm-${builtins.substring 0 12 hypervisor}"; - inherit mac; - } ]; + interfaces = [ + { + type = "tap"; + id = "vm-${builtins.substring 0 12 hypervisor}"; + inherit mac; + } + ]; }; # Just use 99-ethernet-default-dhcp.network systemd.network.enable = true; @@ -97,11 +110,14 @@ nixpkgs.lib.nixosSystem { networks.virbr0 = { matchConfig.Name = "virbr0"; - addresses = [ { - Address = "10.0.0.1/24"; - } { - Address = "fd12:3456:789a::1/64"; - } ]; + addresses = [ + { + Address = "10.0.0.1/24"; + } + { + Address = "fd12:3456:789a::1/64"; + } + ]; # Hand out IP addresses to MicroVMs. # Use `networkctl status virbr0` to see leases. networkConfig = { @@ -114,9 +130,11 @@ nixpkgs.lib.nixosSystem { Address = hypervisorIPv4Addrs.${hypervisor}; }) hypervisors; # IPv6 SLAAC - ipv6Prefixes = [ { - Prefix = "fd12:3456:789a::/64"; - } ]; + ipv6Prefixes = [ + { + Prefix = "fd12:3456:789a::/64"; + } + ]; }; networks.microvm-eth0 = { matchConfig.Name = "vm-*"; @@ -136,6 +154,7 @@ nixpkgs.lib.nixosSystem { internalInterfaces = [ "virbr0" ]; }; }; - }) + } + ) ]; } diff --git a/subrepos/microvm.nix/examples/no-flake-microvm.nix b/subrepos/microvm.nix/examples/no-flake-microvm.nix index c4ba2b83..4ba8197e 100644 --- a/subrepos/microvm.nix/examples/no-flake-microvm.nix +++ b/subrepos/microvm.nix/examples/no-flake-microvm.nix @@ -1,42 +1,51 @@ -{ pkgs ? import {} }: +{ + pkgs ? import { }, +}: let hypervisor = "cloud-hypervisor"; hypervisorsWith9p = [ "qemu" ]; - hypervisorsWithUserNet = [ "qemu" "kvmtool" ]; + hypervisorsWithUserNet = [ + "qemu" + "kvmtool" + ]; - configuration = { config, lib, ... }: { - imports = [ - ../nixos-modules/microvm - ]; - networking.hostName = "no-flake-microvm"; - users.users.root.password = ""; - services.getty.helpLine = '' - Log in as "root" with an empty password. - ''; + configuration = + { config, lib, ... }: + { + imports = [ + ../nixos-modules/microvm + ]; + networking.hostName = "no-flake-microvm"; + users.users.root.password = ""; + services.getty.helpLine = '' + Log in as "root" with an empty password. + ''; - microvm = { - inherit hypervisor; - # share the host's /nix/store if the hypervisor can do 9p - shares = lib.optional (builtins.elem hypervisor hypervisorsWith9p) { - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - }; - writableStoreOverlay = "/nix/.rw-store"; - volumes = [ { - image = "nix-store-overlay.img"; - mountPoint = config.microvm.writableStoreOverlay; - size = 2048; - } ]; - interfaces = lib.optional (builtins.elem hypervisor hypervisorsWithUserNet) { - type = "user"; - id = "qemu"; - mac = "02:00:00:01:01:01"; + microvm = { + inherit hypervisor; + # share the host's /nix/store if the hypervisor can do 9p + shares = lib.optional (builtins.elem hypervisor hypervisorsWith9p) { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + }; + writableStoreOverlay = "/nix/.rw-store"; + volumes = [ + { + image = "nix-store-overlay.img"; + mountPoint = config.microvm.writableStoreOverlay; + size = 2048; + } + ]; + interfaces = lib.optional (builtins.elem hypervisor hypervisorsWithUserNet) { + type = "user"; + id = "qemu"; + mac = "02:00:00:01:01:01"; + }; }; }; - }; nixos = pkgs.nixos configuration; diff --git a/subrepos/microvm.nix/examples/qemu-vnc.nix b/subrepos/microvm.nix/examples/qemu-vnc.nix index b5d60e1c..69842420 100644 --- a/subrepos/microvm.nix/examples/qemu-vnc.nix +++ b/subrepos/microvm.nix/examples/qemu-vnc.nix @@ -1,8 +1,9 @@ -{ self -, nixpkgs -, system -, packages ? "" -, tapInterface ? null +{ + self, + nixpkgs, + system, + packages ? "", + tapInterface ? null, }: # Before running: $ mkdir /tmp/share @@ -16,63 +17,70 @@ nixpkgs.lib.nixosSystem { # this runs as a MicroVM self.nixosModules.microvm - ({ lib, pkgs, ... }: { - microvm = { - hypervisor = "qemu"; - graphics.enable = true; - interfaces = lib.optional (tapInterface != null) { - type = "tap"; - id = tapInterface; - mac = "00:00:00:00:00:02"; + ( + { lib, pkgs, ... }: + { + microvm = { + hypervisor = "qemu"; + graphics.enable = true; + interfaces = lib.optional (tapInterface != null) { + type = "tap"; + id = tapInterface; + mac = "00:00:00:00:00:02"; + }; }; - }; - networking.hostName = "qemu-vnc"; - system.stateVersion = lib.trivial.release; + networking.hostName = "qemu-vnc"; + system.stateVersion = lib.trivial.release; - microvm.qemu.extraArgs = [ - "-vnc" ":0" - "-vga" "qxl" - # needed for mounse/keyboard input via vnc - "-device" "virtio-keyboard" - "-usb" - "-device" "usb-tablet,bus=usb-bus.0" - ]; + microvm.qemu.extraArgs = [ + "-vnc" + ":0" + "-vga" + "qxl" + # needed for mounse/keyboard input via vnc + "-device" + "virtio-keyboard" + "-usb" + "-device" + "usb-tablet,bus=usb-bus.0" + ]; - services.getty.autologinUser = "user"; - users.users.user = { - password = ""; - group = "user"; - isNormalUser = true; - extraGroups = [ "wheel" "video" ]; - }; - users.groups.user = { }; - security.sudo = { - enable = true; - wheelNeedsPassword = false; - }; + services.getty.autologinUser = "user"; + users.users.user = { + password = ""; + group = "user"; + isNormalUser = true; + extraGroups = [ + "wheel" + "video" + ]; + }; + users.groups.user = { }; + security.sudo = { + enable = true; + wheelNeedsPassword = false; + }; - services.xserver = { - enable = true; - desktopManager.xfce.enable = true; - displayManager.autoLogin.user = "user"; - }; + services.xserver = { + enable = true; + desktopManager.xfce.enable = true; + displayManager.autoLogin.user = "user"; + }; - hardware.graphics.enable = true; + hardware.graphics.enable = true; - environment.systemPackages = with pkgs; [ - xdg-utils # Required - ] ++ map - (package: - lib.attrByPath (lib.splitString "." package) (throw "Package ${package} not found in nixpkgs") pkgs - ) - ( - builtins.filter - (package: - package != "" - ) - (lib.splitString " " packages)); + environment.systemPackages = + with pkgs; + [ + xdg-utils # Required + ] + ++ map ( + package: + lib.attrByPath (lib.splitString "." package) (throw "Package ${package} not found in nixpkgs") pkgs + ) (builtins.filter (package: package != "") (lib.splitString " " packages)); - }) + } + ) ]; } diff --git a/subrepos/microvm.nix/flake-template/flake.nix b/subrepos/microvm.nix/flake-template/flake.nix index 5c220f02..66bad6b5 100644 --- a/subrepos/microvm.nix/flake-template/flake.nix +++ b/subrepos/microvm.nix/flake-template/flake.nix @@ -11,10 +11,16 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, microvm }: + outputs = + { + self, + nixpkgs, + microvm, + }: let system = "x86_64-linux"; - in { + in + { packages.${system} = { default = self.packages.${system}.my-microvm; my-microvm = self.nixosConfigurations.my-microvm.config.microvm.declaredRunner; @@ -29,20 +35,24 @@ networking.hostName = "my-microvm"; users.users.root.password = ""; microvm = { - volumes = [ { - mountPoint = "/var"; - image = "var.img"; - size = 256; - } ]; - shares = [ { - # use proto = "virtiofs" for MicroVMs that are started by systemd - proto = "9p"; - tag = "ro-store"; - # a host's /nix/store will be picked up so that no - # squashfs/erofs will be built for it. - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - } ]; + volumes = [ + { + mountPoint = "/var"; + image = "var.img"; + size = 256; + } + ]; + shares = [ + { + # use proto = "virtiofs" for MicroVMs that are started by systemd + proto = "9p"; + tag = "ro-store"; + # a host's /nix/store will be picked up so that no + # squashfs/erofs will be built for it. + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; # "qemu" has 9p built-in! hypervisor = "qemu"; diff --git a/subrepos/microvm.nix/flake.nix b/subrepos/microvm.nix/flake.nix index b1f395b9..b9cd6308 100644 --- a/subrepos/microvm.nix/flake.nix +++ b/subrepos/microvm.nix/flake.nix @@ -14,7 +14,12 @@ }; }; - outputs = { self, nixpkgs, spectrum }: + outputs = + { + self, + nixpkgs, + spectrum, + }: let forAllSystems = function: nixpkgs.lib.genAttrs systems (system: function system); systems = [ @@ -23,58 +28,68 @@ "x86_64-darwin" "aarch64-darwin" ]; - in { - apps = forAllSystems (system: + in + { + apps = forAllSystems ( + system: let pkgs = nixpkgs.legacyPackages.${system}; nixosToApp = configFile: { type = "app"; - program = "${(import configFile { - inherit self nixpkgs system; - }).config.microvm.declaredRunner}/bin/microvm-run"; + program = "${ + (import configFile { + inherit self nixpkgs system; + }).config.microvm.declaredRunner + }/bin/microvm-run"; }; - in { + in + { vm = nixosToApp ./examples/microvms-host.nix; qemu-vnc = nixosToApp ./examples/qemu-vnc.nix; graphics = { type = "app"; - program = toString (pkgs.writeShellScript "run-graphics" '' - set -e + program = toString ( + pkgs.writeShellScript "run-graphics" '' + set -e - if [ -z "$*" ]; then - echo "Usage: $0 [--tap tap0] " - exit 1 - fi + if [ -z "$*" ]; then + echo "Usage: $0 [--tap tap0] " + exit 1 + fi - if [ "$1" = "--tap" ]; then - TAP_INTERFACE="\"$2\"" - shift 2 - else - TAP_INTERFACE=null - fi + if [ "$1" = "--tap" ]; then + TAP_INTERFACE="\"$2\"" + shift 2 + else + TAP_INTERFACE=null + fi - ${pkgs.nix}/bin/nix run \ - -f ${./examples/graphics.nix} \ - config.microvm.declaredRunner \ - --arg self 'builtins.getFlake "${self}"' \ - --arg system '"${system}"' \ - --arg nixpkgs 'builtins.getFlake "${nixpkgs}"' \ - --arg packages "\"$*\"" \ - --arg tapInterface "$TAP_INTERFACE" - ''); + ${pkgs.nix}/bin/nix run \ + -f ${./examples/graphics.nix} \ + config.microvm.declaredRunner \ + --arg self 'builtins.getFlake "${self}"' \ + --arg system '"${system}"' \ + --arg nixpkgs 'builtins.getFlake "${nixpkgs}"' \ + --arg packages "\"$*\"" \ + --arg tapInterface "$TAP_INTERFACE" + '' + ); }; # Run this on your host to accept Wayland connections # on AF_VSOCK. waypipe-client = { type = "app"; - program = toString (pkgs.writeShellScript "waypipe-client" '' - exec ${pkgs.waypipe}/bin/waypipe --vsock -s 6000 client - ''); + program = toString ( + pkgs.writeShellScript "waypipe-client" '' + exec ${pkgs.waypipe}/bin/waypipe --vsock -s 6000 client + '' + ); }; } ); - packages = forAllSystems (system: + packages = forAllSystems ( + system: let pkgs = import nixpkgs { inherit system; @@ -82,46 +97,51 @@ }; inherit (pkgs) lib; - in { + in + { build-microvm = pkgs.callPackage ./pkgs/build-microvm.nix { inherit self; }; doc = pkgs.callPackage ./pkgs/doc.nix { }; microvm = pkgs.callPackage ./pkgs/microvm-command.nix { }; # all compilation-heavy packages that shall be prebuilt for a binary cache prebuilt = pkgs.buildEnv { name = "prebuilt"; - paths = with self.packages.${system}; with pkgs; [ - qemu-example - cloud-hypervisor-example - firecracker-example - crosvm-example - kvmtool-example - stratovirt-example - # alioth-example - virtiofsd - ]; + paths = + with self.packages.${system}; + with pkgs; + [ + qemu-example + cloud-hypervisor-example + firecracker-example + crosvm-example + kvmtool-example + stratovirt-example + # alioth-example + virtiofsd + ]; pathsToLink = [ "/" ]; extraOutputsToInstall = [ "dev" ]; ignoreCollisions = true; }; - } // - # wrap self.nixosConfigurations in executable packages - lib.listToAttrs ( - lib.concatMap (configName: - let - config = self.nixosConfigurations.${configName}; - packageName = lib.replaceString "${system}-" "" configName; - # Check if this config's guest system matches our current build system - # (accounting for darwin hosts building linux guests) - guestSystem = config.pkgs.stdenv.hostPlatform.system; - buildSystem = lib.replaceString "-darwin" "-linux" system; - in - lib.optional (guestSystem == buildSystem) - { - name = packageName; - value = config.config.microvm.runner.${config.config.microvm.hypervisor}; - } - ) (builtins.attrNames self.nixosConfigurations) - ) + } + // + # wrap self.nixosConfigurations in executable packages + lib.listToAttrs ( + lib.concatMap ( + configName: + let + config = self.nixosConfigurations.${configName}; + packageName = lib.replaceString "${system}-" "" configName; + # Check if this config's guest system matches our current build system + # (accounting for darwin hosts building linux guests) + guestSystem = config.pkgs.stdenv.hostPlatform.system; + buildSystem = lib.replaceString "-darwin" "-linux" system; + in + lib.optional (guestSystem == buildSystem) { + name = packageName; + value = config.config.microvm.runner.${config.config.microvm.hypervisor}; + } + ) (builtins.attrNames self.nixosConfigurations) + ) ); # Takes too much memory in `nix flake show` @@ -130,8 +150,10 @@ # ); # hydraJobs are checks - hydraJobs = forAllSystems (system: - builtins.mapAttrs (_: check: + hydraJobs = forAllSystems ( + system: + builtins.mapAttrs ( + _: check: (nixpkgs.lib.recursiveUpdate check { meta.timeout = 12 * 60 * 60; }) @@ -167,83 +189,108 @@ # currently broken: # "crosvm" ]; - hypervisorsWithUserNet = [ "qemu" "kvmtool" "vfkit" ]; + hypervisorsWithUserNet = [ + "qemu" + "kvmtool" + "vfkit" + ]; hypervisorsDarwinOnly = [ "vfkit" ]; # Hypervisors that work on darwin (qemu via HVF, vfkit natively) - hypervisorsOnDarwin = [ "qemu" "vfkit" ]; - hypervisorsWithTap = builtins.filter - # vfkit supports networking, but does not support tap - (hv: hv != "vfkit") - self.lib.hypervisorsWithNetwork; + hypervisorsOnDarwin = [ + "qemu" + "vfkit" + ]; + hypervisorsWithTap = + builtins.filter + # vfkit supports networking, but does not support tap + (hv: hv != "vfkit") + self.lib.hypervisorsWithNetwork; isDarwinOnly = hypervisor: builtins.elem hypervisor hypervisorsDarwinOnly; isDarwinSystem = system: lib.hasSuffix "-darwin" system; - hypervisorSupportsSystem = hypervisor: system: - if isDarwinSystem system - then builtins.elem hypervisor hypervisorsOnDarwin - else !(isDarwinOnly hypervisor); + hypervisorSupportsSystem = + hypervisor: system: + if isDarwinSystem system then + builtins.elem hypervisor hypervisorsOnDarwin + else + !(isDarwinOnly hypervisor); - makeExample = { system, hypervisor, config ? {} }: + makeExample = + { + system, + hypervisor, + config ? { }, + }: lib.nixosSystem { system = lib.replaceString "-darwin" "-linux" system; modules = [ self.nixosModules.microvm - ({ lib, ... }: { - system.stateVersion = lib.trivial.release; + ( + { lib, ... }: + { + system.stateVersion = lib.trivial.release; - networking.hostName = "${hypervisor}-microvm"; - services.getty.autologinUser = "root"; + networking.hostName = "${hypervisor}-microvm"; + services.getty.autologinUser = "root"; - nixpkgs.overlays = [ self.overlay ]; - microvm = { - inherit hypervisor; - # share the host's /nix/store if the hypervisor supports it - shares = - if builtins.elem hypervisor hypervisorsWith9p then [{ - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "9p"; - }] - else if hypervisor == "vfkit" then [{ - tag = "ro-store"; - source = "/nix/store"; - mountPoint = "/nix/.ro-store"; - proto = "virtiofs"; - }] - else []; - # writableStoreOverlay = "/nix/.rw-store"; - # volumes = [ { - # image = "nix-store-overlay.img"; - # mountPoint = config.microvm.writableStoreOverlay; - # size = 2048; - # } ]; - interfaces = lib.optional (builtins.elem hypervisor hypervisorsWithUserNet) { - type = "user"; - id = "qemu"; - mac = "02:00:00:01:01:01"; + nixpkgs.overlays = [ self.overlay ]; + microvm = { + inherit hypervisor; + # share the host's /nix/store if the hypervisor supports it + shares = + if builtins.elem hypervisor hypervisorsWith9p then + [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "9p"; + } + ] + else if hypervisor == "vfkit" then + [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + proto = "virtiofs"; + } + ] + else + [ ]; + # writableStoreOverlay = "/nix/.rw-store"; + # volumes = [ { + # image = "nix-store-overlay.img"; + # mountPoint = config.microvm.writableStoreOverlay; + # size = 2048; + # } ]; + interfaces = lib.optional (builtins.elem hypervisor hypervisorsWithUserNet) { + type = "user"; + id = "qemu"; + mac = "02:00:00:01:01:01"; + }; + forwardPorts = lib.optional (hypervisor == "qemu") { + host.port = 2222; + guest.port = 22; + }; + # Allow build on Darwin + vmHostPackages = lib.mkIf (lib.hasSuffix "-darwin" system) nixpkgs.legacyPackages.${system}; }; - forwardPorts = lib.optional (hypervisor == "qemu") { - host.port = 2222; - guest.port = 22; + networking.firewall.allowedTCPPorts = lib.optional (hypervisor == "qemu") 22; + services.openssh = lib.optionalAttrs (hypervisor == "qemu") { + enable = true; + settings.PermitRootLogin = "yes"; }; - # Allow build on Darwin - vmHostPackages = lib.mkIf (lib.hasSuffix "-darwin" system) - nixpkgs.legacyPackages.${system}; - }; - networking.firewall.allowedTCPPorts = lib.optional (hypervisor == "qemu") 22; - services.openssh = lib.optionalAttrs (hypervisor == "qemu") { - enable = true; - settings.PermitRootLogin = "yes"; - }; - }) + } + ) config ]; }; basicExamples = lib.flatten ( - lib.map (system: + lib.map ( + system: lib.map (hypervisor: { name = "${system}-${hypervisor}-example"; value = makeExample { inherit system hypervisor; }; @@ -253,17 +300,20 @@ ); tapExamples = lib.flatten ( - lib.map (system: + lib.map ( + system: lib.imap1 (idx: hypervisor: { name = "${system}-${hypervisor}-example-with-tap"; value = makeExample { inherit system hypervisor; config = _: { - microvm.interfaces = [ { - type = "tap"; - id = "vm-${builtins.substring 0 4 hypervisor}"; - mac = "02:00:00:01:01:0${toString idx}"; - } ]; + microvm.interfaces = [ + { + type = "tap"; + id = "vm-${builtins.substring 0 4 hypervisor}"; + mac = "02:00:00:01:01:0${toString idx}"; + } + ]; networking = { interfaces.eth0.useDHCP = true; firewall.allowedTCPPorts = [ 22 ]; @@ -274,14 +324,21 @@ }; }; }; - shouldInclude = builtins.elem hypervisor hypervisorsWithTap - && hypervisorSupportsSystem hypervisor system; + shouldInclude = + builtins.elem hypervisor hypervisorsWithTap && hypervisorSupportsSystem hypervisor system; }) self.lib.hypervisors ) systems ); included = builtins.filter (ex: ex.shouldInclude) (basicExamples ++ tapExamples); in - builtins.listToAttrs (builtins.map ({ name, value, ... }: { inherit name value; }) included); + builtins.listToAttrs ( + builtins.map ( + { name, value, ... }: + { + inherit name value; + } + ) included + ); }; } diff --git a/subrepos/microvm.nix/lib/default.nix b/subrepos/microvm.nix/lib/default.nix index 87b7fa29..48840924 100644 --- a/subrepos/microvm.nix/lib/default.nix +++ b/subrepos/microvm.nix/lib/default.nix @@ -15,24 +15,23 @@ rec { defaultFsType = "ext4"; - withDriveLetters = { volumes, storeOnDisk, ... }: + withDriveLetters = + { volumes, storeOnDisk, ... }: let - offset = - if storeOnDisk - then 1 - else 0; + offset = if storeOnDisk then 1 else 0; in - map ({ fst, snd }: - fst // { + map ( + { fst, snd }: + fst + // { letter = snd; } - ) (lib.zipLists volumes ( - lib.drop offset lib.strings.lowerChars - )); + ) (lib.zipLists volumes (lib.drop offset lib.strings.lowerChars)); buildRunner = import ./runner.nix; - makeMacvtap = { microvmConfig, hypervisorConfig }: + makeMacvtap = + { microvmConfig, hypervisorConfig }: import ./macvtap.nix { inherit microvmConfig hypervisorConfig lib; }; @@ -76,21 +75,26 @@ rec { extractOptValues ["-p" "-platform"] ["-vnc" ":0" "-usb"] => { values = []; args = ["-vnc" ":0" "-usb"]; } */ - extractOptValues = optFlag: extraArgs: + extractOptValues = + optFlag: extraArgs: let - flags = if builtins.isList optFlag then optFlag else [optFlag]; - - processArgs = args: values: acc: - if args == [] then - { values = values; args = acc; } + flags = if builtins.isList optFlag then optFlag else [ optFlag ]; + + processArgs = + args: values: acc: + if args == [ ] then + { + values = values; + args = acc; + } else if (builtins.elem (builtins.head args) flags) && (builtins.length args) > 1 then # Found one of the option flags, skip it and its value - processArgs (builtins.tail (builtins.tail args)) (values ++ [(builtins.elemAt args 1)]) acc + processArgs (builtins.tail (builtins.tail args)) (values ++ [ (builtins.elemAt args 1) ]) acc else # Not the option we're looking for, keep this element - processArgs (builtins.tail args) values (acc ++ [(builtins.head args)]); + processArgs (builtins.tail args) values (acc ++ [ (builtins.head args) ]); in - processArgs extraArgs [] []; + processArgs extraArgs [ ] [ ]; /* extractParamValue - Extract a parameter value from comma-separated key=value options @@ -109,8 +113,13 @@ rec { Example: extractParamValue "socket" "cid=5,socket=notify.vsock" => "notify.vsock" */ - extractParamValue = param: opts: - if opts == "" || opts == null then null - else let m = builtins.match ".*${param}=([^,]+).*" opts; - in if m == null then null else builtins.head m; + extractParamValue = + param: opts: + if opts == "" || opts == null then + null + else + let + m = builtins.match ".*${param}=([^,]+).*" opts; + in + if m == null then null else builtins.head m; } diff --git a/subrepos/microvm.nix/lib/macvtap.nix b/subrepos/microvm.nix/lib/macvtap.nix index d6b3fddb..5d2ff7ea 100644 --- a/subrepos/microvm.nix/lib/macvtap.nix +++ b/subrepos/microvm.nix/lib/macvtap.nix @@ -1,4 +1,8 @@ -{ microvmConfig, hypervisorConfig, lib }: +{ + microvmConfig, + hypervisorConfig, + lib, +}: let tapMultiQueue = hypervisorConfig.tapMultiQueue or false; @@ -7,33 +11,43 @@ let interfaceFdOffset = 3; - macvtapInterfaces = - builtins.concatLists ( - lib.imap0 (interfaceIndex: interface: - builtins.genList (queueIndex: - interface // { - fd = interfaceFdOffset + interfaceIndex * queueCount + queueIndex; - }) queueCount - ) ( - builtins.filter ({ type, ... }: - type == "macvtap" - ) microvmConfig.interfaces - ) - ); + macvtapInterfaces = builtins.concatLists ( + lib.imap0 ( + interfaceIndex: interface: + builtins.genList ( + queueIndex: + interface + // { + fd = interfaceFdOffset + interfaceIndex * queueCount + queueIndex; + } + ) queueCount + ) (builtins.filter ({ type, ... }: type == "macvtap") microvmConfig.interfaces) + ); -in { +in +{ openMacvtapFds = '' # Open macvtap interface file descriptors - '' + - lib.concatMapStrings ({ id, fd, ... }: '' - exec ${toString fd}<>/dev/tap$(< /sys/class/net/${id}/ifindex) - '') macvtapInterfaces; + '' + + lib.concatMapStrings ( + { id, fd, ... }: + '' + exec ${toString fd}<>/dev/tap$(< /sys/class/net/${id}/ifindex) + '' + ) macvtapInterfaces; - macvtapFds = builtins.foldl' (result: { id, fd, ... }: - result // { - ${id} = (result.${id} or []) ++ [ fd ]; - } - ) { - nextFreeFd = interfaceFdOffset + builtins.length macvtapInterfaces; - } macvtapInterfaces; + macvtapFds = + builtins.foldl' + ( + result: + { id, fd, ... }: + result + // { + ${id} = (result.${id} or [ ]) ++ [ fd ]; + } + ) + { + nextFreeFd = interfaceFdOffset + builtins.length macvtapInterfaces; + } + macvtapInterfaces; } diff --git a/subrepos/microvm.nix/lib/runner.nix b/subrepos/microvm.nix/lib/runner.nix index ad494af6..8dcb74f0 100644 --- a/subrepos/microvm.nix/lib/runner.nix +++ b/subrepos/microvm.nix/lib/runner.nix @@ -1,6 +1,7 @@ -{ pkgs -, microvmConfig -, toplevel +{ + pkgs, + microvmConfig, + toplevel, }: let @@ -8,14 +9,30 @@ let inherit (microvmConfig) hostName vmHostPackages; - inherit (import ./. { inherit lib; }) makeMacvtap withDriveLetters extractOptValues extractParamValue; + inherit (import ./. { inherit lib; }) + makeMacvtap + withDriveLetters + extractOptValues + extractParamValue + ; inherit (import ./volumes.nix { pkgs = microvmConfig.vmHostPackages; }) createVolumesScript; - inherit (makeMacvtap { - inherit microvmConfig hypervisorConfig; - }) openMacvtapFds macvtapFds; + inherit + (makeMacvtap { + inherit microvmConfig hypervisorConfig; + }) + openMacvtapFds + macvtapFds + ; hypervisorConfig = import (./runners + "/${microvmConfig.hypervisor}.nix") { - inherit pkgs microvmConfig macvtapFds withDriveLetters extractOptValues extractParamValue; + inherit + pkgs + microvmConfig + macvtapFds + withDriveLetters + extractOptValues + extractParamValue + ; }; inherit (hypervisorConfig) command canShutdown shutdownCommand; @@ -210,51 +227,67 @@ let in vmHostPackages.buildPackages.runCommand "microvm-${microvmConfig.hypervisor}-${hostName}" -{ - # for `nix run` - meta.mainProgram = "microvm-run"; - passthru = { - inherit canShutdown supportsNotifySocket tapMultiQueue; - inherit (microvmConfig) hypervisor registerWithMachined machineId; - }; -} '' - mkdir -p $out/bin - - ${lib.concatMapStrings (scriptName: '' - ln -s ${binScriptPkgs.${scriptName}} $out/bin/${scriptName} - '') (builtins.attrNames binScriptPkgs)} - - mkdir -p $out/share/microvm - ${lib.optionalString microvmConfig.systemSymlink '' - ln -s ${toplevel} $out/share/microvm/system - ''} - - echo vnet_hdr > $out/share/microvm/tap-flags - ${lib.optionalString tapMultiQueue '' - echo multi_queue >> $out/share/microvm/tap-flags - ''} - ${lib.concatMapStringsSep " " (interface: - lib.optionalString (interface.type == "tap" && interface ? id) '' - echo "${interface.id}" >> $out/share/microvm/tap-interfaces - '') microvmConfig.interfaces} - - ${lib.concatMapStringsSep " " (interface: - lib.optionalString ( - interface.type == "macvtap" && - interface ? id && - (interface.macvtap.link or null) != null && - (interface.macvtap.mode or null) != null - ) '' - echo "${builtins.concatStringsSep " " [ - interface.id - interface.mac - interface.macvtap.link - (builtins.toString interface.macvtap.mode) - ]}" >> $out/share/microvm/macvtap-interfaces - '') microvmConfig.interfaces} - - - ${lib.concatMapStrings ({ tag, socket, source, proto, ... }: + { + # for `nix run` + meta.mainProgram = "microvm-run"; + passthru = { + inherit canShutdown supportsNotifySocket tapMultiQueue; + inherit (microvmConfig) hypervisor registerWithMachined machineId; + }; + } + '' + mkdir -p $out/bin + + ${lib.concatMapStrings (scriptName: '' + ln -s ${binScriptPkgs.${scriptName}} $out/bin/${scriptName} + '') (builtins.attrNames binScriptPkgs)} + + mkdir -p $out/share/microvm + ${lib.optionalString microvmConfig.systemSymlink '' + ln -s ${toplevel} $out/share/microvm/system + ''} + + echo vnet_hdr > $out/share/microvm/tap-flags + ${lib.optionalString tapMultiQueue '' + echo multi_queue >> $out/share/microvm/tap-flags + ''} + ${lib.concatMapStringsSep " " ( + interface: + lib.optionalString (interface.type == "tap" && interface ? id) '' + echo "${interface.id}" >> $out/share/microvm/tap-interfaces + '' + ) microvmConfig.interfaces} + + ${lib.concatMapStringsSep " " ( + interface: + lib.optionalString + ( + interface.type == "macvtap" + && interface ? id + && (interface.macvtap.link or null) != null + && (interface.macvtap.mode or null) != null + ) + '' + echo "${ + builtins.concatStringsSep " " [ + interface.id + interface.mac + interface.macvtap.link + (builtins.toString interface.macvtap.mode) + ] + }" >> $out/share/microvm/macvtap-interfaces + '' + ) microvmConfig.interfaces} + + + ${lib.concatMapStrings ( + { + tag, + socket, + source, + proto, + ... + }: lib.optionalString (proto == "virtiofs") '' mkdir -p $out/share/microvm/virtiofs/${tag} echo "${socket}" > $out/share/microvm/virtiofs/${tag}/socket @@ -262,13 +295,16 @@ vmHostPackages.buildPackages.runCommand "microvm-${microvmConfig.hypervisor}-${h '' ) microvmConfig.shares} - ${lib.concatMapStrings ({ bus, path, ... }: '' - echo "${path}" >> $out/share/microvm/${bus}-devices - '') microvmConfig.devices} - - # VSOCK info for ssh access - ${lib.optionalString (microvmConfig.vsock.cid != null) '' - echo "${toString microvmConfig.vsock.cid}" > $out/share/microvm/vsock-cid - ''} - echo "${microvmConfig.hypervisor}" > $out/share/microvm/hypervisor -'' + ${lib.concatMapStrings ( + { bus, path, ... }: + '' + echo "${path}" >> $out/share/microvm/${bus}-devices + '' + ) microvmConfig.devices} + + # VSOCK info for ssh access + ${lib.optionalString (microvmConfig.vsock.cid != null) '' + echo "${toString microvmConfig.vsock.cid}" > $out/share/microvm/vsock-cid + ''} + echo "${microvmConfig.hypervisor}" > $out/share/microvm/hypervisor + '' diff --git a/subrepos/microvm.nix/lib/runners/alioth.nix b/subrepos/microvm.nix/lib/runners/alioth.nix index 0d7ed843..72b90b96 100644 --- a/subrepos/microvm.nix/lib/runners/alioth.nix +++ b/subrepos/microvm.nix/lib/runners/alioth.nix @@ -1,6 +1,7 @@ -{ pkgs -, microvmConfig -, ... +{ + pkgs, + microvmConfig, + ... }: let @@ -10,76 +11,117 @@ let inherit (microvmConfig) user - vcpu mem balloon initialBalloonMem hotplugMem hotpluggedMem interfaces volumes shares devices vsock - kernel initrdPath - storeDisk storeOnDisk credentialFiles; -in { + vcpu + mem + balloon + initialBalloonMem + hotplugMem + hotpluggedMem + interfaces + volumes + shares + devices + vsock + kernel + initrdPath + storeDisk + storeOnDisk + credentialFiles + ; +in +{ command = - if user != null - then throw "alioth will not change user" - else if balloon - then throw "balloon not implemented for alioth" - else if initialBalloonMem != 0 - then throw "initialBalloonMem not implemented for alioth" - else if hotplugMem != 0 - then throw "alioth does not support hotplugMem" - else if hotpluggedMem != 0 - then throw "alioth does not support hotpluggedMem" - else if credentialFiles != {} - then throw "alioth does not support credentialFiles" - else builtins.concatStringsSep " " ( - [ - "${aliothPkg}/bin/alioth" "run" - "--memory" "size=${toString mem}M,backend=memfd" - "--num-cpu" (toString vcpu) - "-k" (lib.escapeShellArg "${kernel}/${pkgs.stdenv.hostPlatform.linux-kernel.target}") - "-i" initrdPath - "-c" (lib.escapeShellArg "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}") - "--entropy" - ] - ++ - lib.optionals storeOnDisk [ - "--blk" (lib.escapeShellArg "path=${storeDisk},readonly=true") - ] - ++ - builtins.concatMap ({ image, serial, direct, readOnly, ... }: - lib.warnIf (serial != null) '' - Volume serial is not supported for alioth - '' - lib.warnIf direct '' - Volume direct IO is not supported for alioth - '' - [ - "--blk" - (lib.escapeShellArg "path=${image},readOnly=${ - lib.boolToString readOnly - }") - ] - ) volumes - ++ - builtins.concatMap ({ proto, socket, tag, ... }: - if proto == "virtiofs" - then [ - "--fs" (lib.escapeShellArg "vu,socket=${socket},tag=${tag}") - ] else throw "9p shares not implemented for alioth" - ) shares - ++ - builtins.concatMap ({ type, id, mac, ... }: - if type == "tap" - then [ - "--net" (lib.escapeShellArg "if_name=${id},mac=${mac},queue_pairs=${toString vcpu},mtu=1500") + if user != null then + throw "alioth will not change user" + else if balloon then + throw "balloon not implemented for alioth" + else if initialBalloonMem != 0 then + throw "initialBalloonMem not implemented for alioth" + else if hotplugMem != 0 then + throw "alioth does not support hotplugMem" + else if hotpluggedMem != 0 then + throw "alioth does not support hotpluggedMem" + else if credentialFiles != { } then + throw "alioth does not support credentialFiles" + else + builtins.concatStringsSep " " ( + [ + "${aliothPkg}/bin/alioth" + "run" + "--memory" + "size=${toString mem}M,backend=memfd" + "--num-cpu" + (toString vcpu) + "-k" + (lib.escapeShellArg "${kernel}/${pkgs.stdenv.hostPlatform.linux-kernel.target}") + "-i" + initrdPath + "-c" + (lib.escapeShellArg "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}") + "--entropy" ] - else throw "interface type ${type} is not supported by alioth" - ) interfaces - ++ - map ({ ... }: - throw "PCI/USB passthrough is not supported on alioth" - ) devices - ++ - lib.optionals (vsock.cid != null) [ - "--vsock" "vhost,cid=${toString vsock.cid}" - ] - ); + ++ lib.optionals storeOnDisk [ + "--blk" + (lib.escapeShellArg "path=${storeDisk},readonly=true") + ] + ++ builtins.concatMap ( + { + image, + serial, + direct, + readOnly, + ... + }: + lib.warnIf (serial != null) + '' + Volume serial is not supported for alioth + '' + lib.warnIf + direct + '' + Volume direct IO is not supported for alioth + '' + [ + "--blk" + (lib.escapeShellArg "path=${image},readOnly=${lib.boolToString readOnly}") + ] + ) volumes + ++ builtins.concatMap ( + { + proto, + socket, + tag, + ... + }: + if proto == "virtiofs" then + [ + "--fs" + (lib.escapeShellArg "vu,socket=${socket},tag=${tag}") + ] + else + throw "9p shares not implemented for alioth" + ) shares + ++ builtins.concatMap ( + { + type, + id, + mac, + ... + }: + if type == "tap" then + [ + "--net" + (lib.escapeShellArg "if_name=${id},mac=${mac},queue_pairs=${toString vcpu},mtu=1500") + ] + else + throw "interface type ${type} is not supported by alioth" + ) interfaces + ++ map ({ ... }: throw "PCI/USB passthrough is not supported on alioth") devices + ++ lib.optionals (vsock.cid != null) [ + "--vsock" + "vhost,cid=${toString vsock.cid}" + ] + ); # TODO: canShutdown = false; diff --git a/subrepos/microvm.nix/lib/runners/cloud-hypervisor.nix b/subrepos/microvm.nix/lib/runners/cloud-hypervisor.nix index a68d418f..46367389 100644 --- a/subrepos/microvm.nix/lib/runners/cloud-hypervisor.nix +++ b/subrepos/microvm.nix/lib/runners/cloud-hypervisor.nix @@ -1,52 +1,79 @@ -{ pkgs -, microvmConfig -, macvtapFds -, extractOptValues -, extractParamValue -, ... +{ + pkgs, + microvmConfig, + macvtapFds, + extractOptValues, + extractParamValue, + ... }: let inherit (pkgs) lib; - inherit (microvmConfig) vcpu mem balloon initialBalloonMem deflateOnOOM hotplugMem hotpluggedMem user interfaces volumes shares socket devices hugepageMem graphics storeDisk storeOnDisk kernel initrdPath credentialFiles vsock; + inherit (microvmConfig) + vcpu + mem + balloon + initialBalloonMem + deflateOnOOM + hotplugMem + hotpluggedMem + user + interfaces + volumes + shares + socket + devices + hugepageMem + graphics + storeDisk + storeOnDisk + kernel + initrdPath + credentialFiles + vsock + ; inherit (microvmConfig.cloud-hypervisor) platformOEMStrings extraArgs; # extract all the extra args that we merge with up front - processedExtraArgs = builtins.foldl' - (args: opt: (extractOptValues opt args).args) - extraArgs - ["--vsock" "--platform"]; + processedExtraArgs = builtins.foldl' (args: opt: (extractOptValues opt args).args) extraArgs [ + "--vsock" + "--platform" + ]; - hasUserConsole = (extractOptValues "--console" extraArgs).values != []; - hasUserSerial = (extractOptValues "--serial" extraArgs).values != []; + hasUserConsole = (extractOptValues "--console" extraArgs).values != [ ]; + hasUserSerial = (extractOptValues "--serial" extraArgs).values != [ ]; userSerial = lib.optionalString hasUserSerial (extractOptValues "--serial" extraArgs).values; - kernelPath = { - x86_64-linux = "${kernel.dev}/vmlinux"; - aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; - }.${pkgs.stdenv.hostPlatform.system}; + kernelPath = + { + x86_64-linux = "${kernel.dev}/vmlinux"; + aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; + } + .${pkgs.stdenv.hostPlatform.system}; kernelConsoleDefault = - if pkgs.stdenv.hostPlatform.system == "x86_64-linux" - then "earlyprintk=ttyS0 console=ttyS0" - else if pkgs.stdenv.hostPlatform.system == "aarch64-linux" - then "console=ttyAMA0" - else ""; + if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then + "earlyprintk=ttyS0 console=ttyS0" + else if pkgs.stdenv.hostPlatform.system == "aarch64-linux" then + "console=ttyAMA0" + else + ""; kernelConsole = lib.optionalString (!hasUserSerial || userSerial == "tty") kernelConsoleDefault; kernelCmdLine = "${kernelConsole} reboot=t panic=-1 ${toString microvmConfig.kernelParams}"; - userVSockOpts = (extractOptValues "--vsock" extraArgs).values; - userVSockStr = if userVSockOpts == [] then null else builtins.head userVSockOpts; + userVSockStr = if userVSockOpts == [ ] then null else builtins.head userVSockOpts; userVSockPath = extractParamValue "socket" userVSockStr; userVSockCID = extractParamValue "cid" userVSockStr; - vsockCID = if vsock.cid != null && userVSockCID != null - then throw "Cannot set `microvm.vsock.cid` and --vsock 'cid=${userVSockCID}...' via `microvm.cloud-hypervisor.extraArgs` at the same time" - else if vsock.cid != null - then vsock.cid - else userVSockCID; + vsockCID = + if vsock.cid != null && userVSockCID != null then + throw "Cannot set `microvm.vsock.cid` and --vsock 'cid=${userVSockCID}...' via `microvm.cloud-hypervisor.extraArgs` at the same time" + else if vsock.cid != null then + vsock.cid + else + userVSockCID; supportsNotifySocket = vsockCID != null; vsockPath = if userVSockPath != null then userVSockPath else "notify.vsock"; vsockOpts = @@ -60,40 +87,40 @@ let useVirtiofs = builtins.any ({ proto, ... }: proto == "virtiofs") shares; # Transform attrs to parameters in form of `key1=value1,key2=value2,[...]` - opsMapped = ops: lib.concatStringsSep "," ( - lib.mapAttrsToList (k: v: - "${k}=${v}" - ) ops - ); + opsMapped = ops: lib.concatStringsSep "," (lib.mapAttrsToList (k: v: "${k}=${v}") ops); # Attrs representing CHV mem options - memOps = opsMapped ({ - size = "${toString mem}M"; - mergeable = "on"; - # Shared memory is required for usage with virtiofsd but it - # prevents Kernel Same-page Merging. - shared = if useVirtiofs || graphics.enable then "on" else "off"; - } - # add ballooning options and override 'size' key - // lib.optionalAttrs useHotPlugMemory { - size = "${toString hotplugMem}M"; - hotplug_method = "virtio-mem"; - hotplug_size = "${toString hotplugMem}M"; - hotplugged_size = "${toString hotpluggedMem}M"; - } - # enable hugepages (shared option is ignored by CHV) - // lib.optionalAttrs hugepageMem { - hugepages = "on"; - }); + memOps = opsMapped ( + { + size = "${toString mem}M"; + mergeable = "on"; + # Shared memory is required for usage with virtiofsd but it + # prevents Kernel Same-page Merging. + shared = if useVirtiofs || graphics.enable then "on" else "off"; + } + # add ballooning options and override 'size' key + // lib.optionalAttrs useHotPlugMemory { + size = "${toString hotplugMem}M"; + hotplug_method = "virtio-mem"; + hotplug_size = "${toString hotplugMem}M"; + hotplugged_size = "${toString hotpluggedMem}M"; + } + # enable hugepages (shared option is ignored by CHV) + // lib.optionalAttrs hugepageMem { + hugepages = "on"; + } + ); - balloonOps = opsMapped ({ - size = "${toString initialBalloonMem}M"; - free_page_reporting = "on"; - } - # enable deflating memory balloon on out-of-memory - // lib.optionalAttrs deflateOnOOM { - deflate_on_oom = "on"; - }); + balloonOps = opsMapped ( + { + size = "${toString initialBalloonMem}M"; + free_page_reporting = "on"; + } + # enable deflating memory balloon on out-of-memory + // lib.optionalAttrs deflateOnOOM { + deflate_on_oom = "on"; + } + ); tapMultiQueue = vcpu > 1; @@ -104,34 +131,42 @@ let # cloud-hypervisor >= 30.0 < 36.0 temporarily replaced clap with argh hasArghSyntax = - builtins.compareVersions cloudhypervisorPkg.version "30.0" >= 0 && - builtins.compareVersions cloudhypervisorPkg.version "36.0" < 0; + builtins.compareVersions cloudhypervisorPkg.version "30.0" >= 0 + && builtins.compareVersions cloudhypervisorPkg.version "36.0" < 0; arg = - if hasArghSyntax - then switch: params: + if hasArghSyntax then + switch: params: # `--switch param0 --switch param1 ...` - builtins.concatMap (param: [ switch param ]) params - else switch: params: + builtins.concatMap (param: [ + switch + param + ]) params + else + switch: params: # `` or `--switch param0 param1 ...` - lib.optionals (params != []) ( - [ switch ] ++ params - ); + lib.optionals (params != [ ]) ([ switch ] ++ params); gpuParams = { context-types = "virgl:virgl2:cross-domain"; - displays = [ { - hidden = true; - } ]; + displays = [ + { + hidden = true; + } + ]; egl = true; vulkan = true; }; - oemStringValues = platformOEMStrings ++ lib.optional supportsNotifySocket "io.systemd.credential:vmm.notify_socket=vsock-stream:2:8888"; - oemStringOptions = lib.optional (oemStringValues != []) "oem_strings=[${lib.concatStringsSep "," oemStringValues}]"; + oemStringValues = + platformOEMStrings + ++ lib.optional supportsNotifySocket "io.systemd.credential:vmm.notify_socket=vsock-stream:2:8888"; + oemStringOptions = lib.optional ( + oemStringValues != [ ] + ) "oem_strings=[${lib.concatStringsSep "," oemStringValues}]"; platformExtracted = extractOptValues "--platform" extraArgs; extraArgsWithoutPlatform = platformExtracted.args; userPlatformOpts = platformExtracted.values; - userPlatformStr = lib.optionalString (userPlatformOpts != []) (builtins.head userPlatformOpts); + userPlatformStr = lib.optionalString (userPlatformOpts != [ ]) (builtins.head userPlatformOpts); userHasOemStrings = (extractParamValue "oem_strings" userPlatformStr) != null; platformOps = if userHasOemStrings then @@ -140,7 +175,8 @@ let lib.concatStringsSep "," (oemStringOptions ++ userPlatformOpts); cloudhypervisorPkg = microvmConfig.cloud-hypervisor.package; -in { +in +{ inherit tapMultiQueue supportsNotifySocket; preStart = '' @@ -151,7 +187,8 @@ in { rm -f '${socket}' ''} - '' + lib.optionalString supportsNotifySocket '' + '' + + lib.optionalString supportsNotifySocket '' # Ensure notify sockets are removed if cloud-hypervisor didn't exit cleanly the last time rm -f ${vsockPath} ${vsockPath}_8888 @@ -161,7 +198,8 @@ in { # shutdown of the stream, like systemd v256+ does. ${pkgs.socat}/bin/socat -T2 UNIX-LISTEN:${vsockPath}_8888,fork UNIX-SENDTO:$NOTIFY_SOCKET & fi - '' + lib.optionalString graphics.enable '' + '' + + lib.optionalString graphics.enable '' rm -f ${graphics.socket} ${pkgs.crosvm}/bin/crosvm device gpu \ --socket ${graphics.socket} \ @@ -173,108 +211,159 @@ in { done ''; - command = - if user != null - then throw "cloud-hypervisor will not change user" - else if credentialFiles != {} - then throw "cloud-hypervisor does not support credentialFiles" - else lib.escapeShellArgs ( - [ - "${cloudhypervisorPkg}/bin/cloud-hypervisor" - "--cpus" "boot=${toString vcpu}" - "--watchdog" - "--kernel" kernelPath - "--initramfs" initrdPath - "--cmdline" kernelCmdLine - "--seccomp" "true" - "--memory" memOps - "--platform" platformOps - ] - ++ - lib.optionals (!hasUserConsole) ["--console" "null"] - ++ - lib.optionals (!hasUserSerial) ["--serial" "tty"] - ++ - lib.optionals (vsockOpts != "") ["--vsock" vsockOpts] - ++ - lib.optionals graphics.enable [ - "--gpu" "socket=${graphics.socket}" - ] - ++ - lib.optionals balloon [ "--balloon" balloonOps ] - ++ - arg "--disk" ( - lib.optional storeOnDisk (opsMapped ({ - path = toString storeDisk; - readonly = "on"; - } // mqOps)) - ++ - map ({ image, serial, direct, readOnly, imageType, ... }: - opsMapped ( - { - path = toString image; - direct = - if direct - then "on" - else "off"; - readonly = - if readOnly - then "on" - else "off"; - image_type = toString imageType; - } // - lib.optionalAttrs (serial != null) { - inherit serial; - } // - mqOps + if user != null then + throw "cloud-hypervisor will not change user" + else if credentialFiles != { } then + throw "cloud-hypervisor does not support credentialFiles" + else + lib.escapeShellArgs ( + [ + "${cloudhypervisorPkg}/bin/cloud-hypervisor" + "--cpus" + "boot=${toString vcpu}" + "--watchdog" + "--kernel" + kernelPath + "--initramfs" + initrdPath + "--cmdline" + kernelCmdLine + "--seccomp" + "true" + "--memory" + memOps + "--platform" + platformOps + ] + ++ lib.optionals (!hasUserConsole) [ + "--console" + "null" + ] + ++ lib.optionals (!hasUserSerial) [ + "--serial" + "tty" + ] + ++ lib.optionals (vsockOpts != "") [ + "--vsock" + vsockOpts + ] + ++ lib.optionals graphics.enable [ + "--gpu" + "socket=${graphics.socket}" + ] + ++ lib.optionals balloon [ + "--balloon" + balloonOps + ] + ++ arg "--disk" ( + lib.optional storeOnDisk ( + opsMapped ( + { + path = toString storeDisk; + readonly = "on"; + } + // mqOps + ) ) - ) volumes + ++ map ( + { + image, + serial, + direct, + readOnly, + imageType, + ... + }: + opsMapped ( + { + path = toString image; + direct = if direct then "on" else "off"; + readonly = if readOnly then "on" else "off"; + image_type = toString imageType; + } + // lib.optionalAttrs (serial != null) { + inherit serial; + } + // mqOps + ) + ) volumes + ) + ++ arg "--fs" ( + map ( + { + proto, + socket, + tag, + ... + }: + if proto == "virtiofs" then + opsMapped { + inherit tag socket; + } + else + throw "cloud-hypervisor supports only shares that are virtiofs" + ) shares + ) + ++ lib.optionals (socket != null) [ + "--api-socket" + socket + ] + ++ arg "--net" ( + map ( + { + type, + id, + mac, + ... + }: + if type == "tap" then + opsMapped ( + { + tap = id; + inherit mac; + } + // lib.optionalAttrs tapMultiQueue { + num_queues = toString (2 * vcpu); + } + ) + else if type == "macvtap" then + opsMapped ( + { + fd = "[${lib.concatMapStringsSep "," toString macvtapFds.${id}}]"; + inherit mac; + } + // lib.optionalAttrs tapMultiQueue { + num_queues = toString (2 * vcpu); + } + ) + else + throw "Unsupported interface type ${type} for Cloud-Hypervisor" + ) interfaces + ) ) - ++ - arg "--fs" (map ({ proto, socket, tag, ... }: - if proto == "virtiofs" - then opsMapped { - inherit tag socket; - } - else throw "cloud-hypervisor supports only shares that are virtiofs" - ) shares) - ++ - lib.optionals (socket != null) [ "--api-socket" socket ] - ++ - arg "--net" (map ({ type, id, mac, ... }: - if type == "tap" - then opsMapped ({ - tap = id; - inherit mac; - } // lib.optionalAttrs tapMultiQueue { - num_queues = toString (2 * vcpu); - }) - else if type == "macvtap" - then opsMapped ({ - fd = "[${lib.concatMapStringsSep "," toString macvtapFds.${id}}]"; - inherit mac; - } // lib.optionalAttrs tapMultiQueue { - num_queues = toString (2 * vcpu); - }) - else throw "Unsupported interface type ${type} for Cloud-Hypervisor" - ) interfaces) - ) - + " " + # Move vfio-pci outside of - lib.concatStringsSep " " ( - arg "--device" ( - map ({ bus, path, ... }: { - pci = "path=/sys/bus/pci/devices/${path}"; - usb = throw "USB passthrough is not supported on cloud-hypervisor"; - }.${bus}) devices + + " " + # Move vfio-pci outside of + + lib.concatStringsSep " " ( + arg "--device" ( + map ( + { bus, path, ... }: + { + pci = "path=/sys/bus/pci/devices/${path}"; + usb = throw "USB passthrough is not supported on cloud-hypervisor"; + } + .${bus} + ) devices + ) ) - ) + " " + lib.escapeShellArgs processedExtraArgs; + + " " + + lib.escapeShellArgs processedExtraArgs; canShutdown = socket != null; shutdownCommand = - if socket != null - then '' + if socket != null then + '' api() { ${pkgs.curl}/bin/curl -s \ --unix-socket ${socket} \ @@ -285,14 +374,16 @@ in { ${pkgs.util-linux}/bin/waitpid $MAINPID '' - else throw "Cannot shutdown without socket"; + else + throw "Cannot shutdown without socket"; setBalloonScript = - if socket != null - then '' - ${cloudhypervisorPkg}/bin/ch-remote --api-socket ${socket} resize --balloon $SIZE"M" - '' - else null; + if socket != null then + '' + ${cloudhypervisorPkg}/bin/ch-remote --api-socket ${socket} resize --balloon $SIZE"M" + '' + else + null; requiresMacvtapAsFds = true; } diff --git a/subrepos/microvm.nix/lib/runners/crosvm.nix b/subrepos/microvm.nix/lib/runners/crosvm.nix index 8f729d1a..c31a762c 100644 --- a/subrepos/microvm.nix/lib/runners/crosvm.nix +++ b/subrepos/microvm.nix/lib/runners/crosvm.nix @@ -1,24 +1,43 @@ -{ pkgs -, microvmConfig -, macvtapFds -, ... +{ + pkgs, + microvmConfig, + macvtapFds, + ... }: let inherit (pkgs) lib; inherit (pkgs.stdenv.hostPlatform) system; inherit (microvmConfig) - vcpu mem balloon initialBalloonMem hotplugMem hotpluggedMem user volumes shares - socket devices vsock graphics credentialFiles - kernel initrdPath storeDisk storeOnDisk; + vcpu + mem + balloon + initialBalloonMem + hotplugMem + hotpluggedMem + user + volumes + shares + socket + devices + vsock + graphics + credentialFiles + kernel + initrdPath + storeDisk + storeOnDisk + ; inherit (microvmConfig.crosvm) pivotRoot extraArgs; crosvmPkg = microvmConfig.crosvm.package; - kernelPath = { - x86_64-linux = "${kernel.dev}/vmlinux"; - aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; - }.${system}; + kernelPath = + { + x86_64-linux = "${kernel.dev}/vmlinux"; + aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; + } + .${system}; gpuParams = { context-types = "virgl:virgl2:cross-domain"; @@ -26,7 +45,8 @@ let vulkan = true; }; -in { +in +{ preStart = '' rm -f ${socket} @@ -34,7 +54,8 @@ in { ${lib.optionalString (pivotRoot != null) '' mkdir -p ${pivotRoot} ''} - '' + lib.optionalString graphics.enable '' + '' + + lib.optionalString graphics.enable '' rm -f ${graphics.socket} ${crosvmPkg}/bin/crosvm device gpu \ --socket ${graphics.socket} \ @@ -47,127 +68,169 @@ in { ''; command = - if user != null - then throw "crosvm will not change user" - else if initialBalloonMem != 0 - then throw "crosvm does not support initialBalloonMem" - else if hotplugMem != 0 - then throw "crosvm does not support hotplugMem" - else if hotpluggedMem != 0 - then throw "crosvm does not support hotpluggedMem" - else if credentialFiles != {} - then throw "crosvm does not support credentialFiles" - else lib.escapeShellArgs ( - [ - "${crosvmPkg}/bin/crosvm" "run" - "-m" (toString mem) - "-c" (toString vcpu) - "--serial" "type=stdout,console=true,stdin=true" - "-p" "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}" - ] - ++ - lib.optional (!balloon) "--no-balloon" - ++ - lib.optionals storeOnDisk [ - "-r" storeDisk - ] - ++ - lib.optionals graphics.enable [ - "--vhost-user" "gpu,socket=${graphics.socket}" - ] - ++ - lib.optionals (builtins.compareVersions crosvmPkg.version "107.1" < 0) [ - # workarounds - "--seccomp-log-failures" - ] - ++ - lib.optionals (pivotRoot != null) [ - "--pivot-root" - pivotRoot - ] - ++ - lib.optionals (socket != null) [ - "-s" socket - ] - ++ - builtins.concatMap ({ image, direct, serial, readOnly, ... }: - [ "--block" - "${image},o_direct=${ - lib.boolToString direct - },ro=${ - lib.boolToString readOnly - }${ - lib.optionalString (serial != null) ",id=${serial}" - }" + if user != null then + throw "crosvm will not change user" + else if initialBalloonMem != 0 then + throw "crosvm does not support initialBalloonMem" + else if hotplugMem != 0 then + throw "crosvm does not support hotplugMem" + else if hotpluggedMem != 0 then + throw "crosvm does not support hotpluggedMem" + else if credentialFiles != { } then + throw "crosvm does not support credentialFiles" + else + lib.escapeShellArgs ( + [ + "${crosvmPkg}/bin/crosvm" + "run" + "-m" + (toString mem) + "-c" + (toString vcpu) + "--serial" + "type=stdout,console=true,stdin=true" + "-p" + "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}" ] - ) volumes - ++ - builtins.concatMap ({ proto, tag, source, socket, readOnly, ... }: { - "virtiofs" = [ - "--vhost-user" "type=fs,socket=${socket}" - ]; - "9p" = if readOnly then - throw "Readonly 9p share is not supported" - else [ - "--shared-dir" "${source}:${tag}:type=p9" - ]; - }.${proto}) shares - ++ - (builtins.concatMap ({ id, type, mac, ... }: [ - "--net" - (lib.concatStringsSep "," ([ - ( if type == "tap" - then "tap-name=${id}" - else if type == "macvtap" - then "tap-fd=${toString macvtapFds.${id}}" - else throw "Unsupported interface type ${type} for crosvm" - ) - "mac=${mac}" - # ] ++ lib.optionals (vcpu > 1) [ - # "vq-pairs=${toString vcpu}" - ])) - ]) microvmConfig.interfaces) - # ++ - # lib.optionals (vcpu > 1) [ - # "--net-vq-pairs" (toString vcpu) - # ] - ++ - lib.optionals (vsock.cid != null) [ - "--vsock" (toString vsock.cid) - ] - ++ - [ - "--initrd" initrdPath - kernelPath - ] - ) - + " " + # Move vfio-pci outside of - lib.concatStringsSep " " (lib.concatMap ({ bus, path, ... }: { - pci = [ "--vfio" "/sys/bus/pci/devices/${path},iommu=viommu" ]; - usb = throw "USB passthrough is not supported on crosvm"; - }.${bus}) devices) - + " " + lib.escapeShellArgs extraArgs; + ++ lib.optional (!balloon) "--no-balloon" + ++ lib.optionals storeOnDisk [ + "-r" + storeDisk + ] + ++ lib.optionals graphics.enable [ + "--vhost-user" + "gpu,socket=${graphics.socket}" + ] + ++ lib.optionals (builtins.compareVersions crosvmPkg.version "107.1" < 0) [ + # workarounds + "--seccomp-log-failures" + ] + ++ lib.optionals (pivotRoot != null) [ + "--pivot-root" + pivotRoot + ] + ++ lib.optionals (socket != null) [ + "-s" + socket + ] + ++ builtins.concatMap ( + { + image, + direct, + serial, + readOnly, + ... + }: + [ + "--block" + "${image},o_direct=${lib.boolToString direct},ro=${lib.boolToString readOnly}${ + lib.optionalString (serial != null) ",id=${serial}" + }" + ] + ) volumes + ++ builtins.concatMap ( + { + proto, + tag, + source, + socket, + readOnly, + ... + }: + { + "virtiofs" = [ + "--vhost-user" + "type=fs,socket=${socket}" + ]; + "9p" = + if readOnly then + throw "Readonly 9p share is not supported" + else + [ + "--shared-dir" + "${source}:${tag}:type=p9" + ]; + } + .${proto} + ) shares + ++ (builtins.concatMap ( + { + id, + type, + mac, + ... + }: + [ + "--net" + (lib.concatStringsSep "," ([ + ( + if type == "tap" then + "tap-name=${id}" + else if type == "macvtap" then + "tap-fd=${toString macvtapFds.${id}}" + else + throw "Unsupported interface type ${type} for crosvm" + ) + "mac=${mac}" + # ] ++ lib.optionals (vcpu > 1) [ + # "vq-pairs=${toString vcpu}" + ])) + ] + ) microvmConfig.interfaces) + # ++ + # lib.optionals (vcpu > 1) [ + # "--net-vq-pairs" (toString vcpu) + # ] + ++ lib.optionals (vsock.cid != null) [ + "--vsock" + (toString vsock.cid) + ] + ++ [ + "--initrd" + initrdPath + kernelPath + ] + ) + + " " + # Move vfio-pci outside of + + lib.concatStringsSep " " ( + lib.concatMap ( + { bus, path, ... }: + { + pci = [ + "--vfio" + "/sys/bus/pci/devices/${path},iommu=viommu" + ]; + usb = throw "USB passthrough is not supported on crosvm"; + } + .${bus} + ) devices + ) + + " " + + lib.escapeShellArgs extraArgs; canShutdown = socket != null; shutdownCommand = - if socket != null - then '' + if socket != null then + '' ${crosvmPkg}/bin/crosvm powerbtn ${socket} '' - else throw "Cannot shutdown without socket"; + else + throw "Cannot shutdown without socket"; setBalloonScript = - if socket != null - then '' - VALUE=$(( $SIZE * 1024 * 1024 )) - ${crosvmPkg}/bin/crosvm balloon $VALUE ${socket} - SIZE=$( ${crosvmPkg}/bin/crosvm balloon_stats ${socket} | \ - ${pkgs.jq}/bin/jq -r .BalloonStats.balloon_actual \ - ) - echo $(( $SIZE / 1024 / 1024 )) - '' - else null; + if socket != null then + '' + VALUE=$(( $SIZE * 1024 * 1024 )) + ${crosvmPkg}/bin/crosvm balloon $VALUE ${socket} + SIZE=$( ${crosvmPkg}/bin/crosvm balloon_stats ${socket} | \ + ${pkgs.jq}/bin/jq -r .BalloonStats.balloon_actual \ + ) + echo $(( $SIZE / 1024 / 1024 )) + '' + else + null; requiresMacvtapAsFds = true; } diff --git a/subrepos/microvm.nix/lib/runners/firecracker.nix b/subrepos/microvm.nix/lib/runners/firecracker.nix index 5d858551..e25bce84 100644 --- a/subrepos/microvm.nix/lib/runners/firecracker.nix +++ b/subrepos/microvm.nix/lib/runners/firecracker.nix @@ -1,23 +1,41 @@ -{ pkgs -, microvmConfig -, ... +{ + pkgs, + microvmConfig, + ... }: let inherit (pkgs) lib; inherit (pkgs.stdenv.hostPlatform) system; inherit (microvmConfig) - hostName user socket preStart - vcpu mem balloon initialBalloonMem hotplugMem hotpluggedMem - interfaces volumes shares devices - kernel initrdPath - storeDisk credentialFiles vsock; + hostName + user + socket + preStart + vcpu + mem + balloon + initialBalloonMem + hotplugMem + hotpluggedMem + interfaces + volumes + shares + devices + kernel + initrdPath + storeDisk + credentialFiles + vsock + ; inherit (microvmConfig.firecracker) cpu; - kernelPath = { - x86_64-linux = "${kernel.dev}/vmlinux"; - aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; - }.${system}; + kernelPath = + { + x86_64-linux = "${kernel.dev}/vmlinux"; + aarch64-linux = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; + } + .${system}; # Firecracker config, as JSON in `configFile` baseConfig = { @@ -33,33 +51,55 @@ let # Enabling simultaneous multithreading is not supported on aarch64 smt = system != "aarch64-linux"; }; - drives = [ { - drive_id = "store"; - path_on_host = storeDisk; - is_root_device = false; - is_read_only = true; - io_engine = microvmConfig.firecracker.driveIoEngine; - } ] ++ map ({ image, serial, direct, readOnly, ... }: - lib.warnIf (serial != null) '' - Volume serial is not supported for firecracker - '' - lib.warnIf direct '' - Volume direct IO is not supported for firecracker - '' { - drive_id = image; - path_on_host = image; + drives = [ + { + drive_id = "store"; + path_on_host = storeDisk; is_root_device = false; - is_read_only = readOnly; + is_read_only = true; io_engine = microvmConfig.firecracker.driveIoEngine; - }) volumes; - network-interfaces = map ({ type, id, mac, ... }: - if type == "tap" - then { - iface_id = id; - host_dev_name = id; - guest_mac = mac; } - else throw "Network interface type ${type} not implemented for Firecracker" + ] + ++ map ( + { + image, + serial, + direct, + readOnly, + ... + }: + lib.warnIf (serial != null) + '' + Volume serial is not supported for firecracker + '' + lib.warnIf + direct + '' + Volume direct IO is not supported for firecracker + '' + { + drive_id = image; + path_on_host = image; + is_root_device = false; + is_read_only = readOnly; + io_engine = microvmConfig.firecracker.driveIoEngine; + } + ) volumes; + network-interfaces = map ( + { + type, + id, + mac, + ... + }: + if type == "tap" then + { + iface_id = id; + host_dev_name = id; + guest_mac = mac; + } + else + throw "Network interface type ${type} not implemented for Firecracker" ) interfaces; vsock = if vsock.cid != null then @@ -79,35 +119,42 @@ let firecrackerPkg = microvmConfig.firecracker.package; -in { +in +{ command = - if user != null - then throw "firecracker will not change user" - else if shares != [] - then throw "9p/virtiofs shares not implemented for Firecracker" - else if devices != [] - then throw "devices passthrough not implemented for Firecracker" - else if balloon - then throw "balloon not implemented for Firecracker" - else if initialBalloonMem != 0 - then throw "initialBalloonMem not implemented for Firecracker" - else if hotplugMem != 0 - then throw "hotplugMem not implemented for Firecracker" - else if hotpluggedMem != 0 - then throw "hotpluggedMem not implemented for Firecracker" - else if credentialFiles != {} - then throw "credentialFiles are not implemented for Firecracker" - else lib.escapeShellArgs ([ - "${firecrackerPkg}/bin/firecracker" - "--config-file" configFile - "--api-sock" ( - if socket != null - then socket - else throw "Firecracker must be configured with an API socket (option microvm.socket)!" - ) - ] - ++ lib.optional (lib.versionAtLeast firecrackerPkg.version "1.13.0") "--enable-pci" - ++ microvmConfig.firecracker.extraArgs); + if user != null then + throw "firecracker will not change user" + else if shares != [ ] then + throw "9p/virtiofs shares not implemented for Firecracker" + else if devices != [ ] then + throw "devices passthrough not implemented for Firecracker" + else if balloon then + throw "balloon not implemented for Firecracker" + else if initialBalloonMem != 0 then + throw "initialBalloonMem not implemented for Firecracker" + else if hotplugMem != 0 then + throw "hotplugMem not implemented for Firecracker" + else if hotpluggedMem != 0 then + throw "hotpluggedMem not implemented for Firecracker" + else if credentialFiles != { } then + throw "credentialFiles are not implemented for Firecracker" + else + lib.escapeShellArgs ( + [ + "${firecrackerPkg}/bin/firecracker" + "--config-file" + configFile + "--api-sock" + ( + if socket != null then + socket + else + throw "Firecracker must be configured with an API socket (option microvm.socket)!" + ) + ] + ++ lib.optional (lib.versionAtLeast firecrackerPkg.version "1.13.0") "--enable-pci" + ++ microvmConfig.firecracker.extraArgs + ); preStart = '' ${preStart} @@ -123,15 +170,16 @@ in { canShutdown = socket != null; shutdownCommand = - if socket != null - then '' - ${pkgs.curl}/bin/curl -s \ - --unix-socket ${socket} \ - -X PUT http://localhost/actions \ - -d '{ "action_type": "SendCtrlAltDel" }' + if socket != null then + '' + ${pkgs.curl}/bin/curl -s \ + --unix-socket ${socket} \ + -X PUT http://localhost/actions \ + -d '{ "action_type": "SendCtrlAltDel" }' - # wait for exit - ${pkgs.socat}/bin/socat STDOUT UNIX:${socket},shut-none - '' - else throw "Cannot shutdown without socket"; + # wait for exit + ${pkgs.socat}/bin/socat STDOUT UNIX:${socket},shut-none + '' + else + throw "Cannot shutdown without socket"; } diff --git a/subrepos/microvm.nix/lib/runners/kvmtool.nix b/subrepos/microvm.nix/lib/runners/kvmtool.nix index fe6dab91..7f97d4f6 100644 --- a/subrepos/microvm.nix/lib/runners/kvmtool.nix +++ b/subrepos/microvm.nix/lib/runners/kvmtool.nix @@ -1,6 +1,7 @@ -{ pkgs -, microvmConfig -, ... +{ + pkgs, + microvmConfig, + ... }: let @@ -9,89 +10,143 @@ let kvmtoolPkg = microvmConfig.kvmtool.package; inherit (microvmConfig) - hostName preStart user - vcpu mem balloon initialBalloonMem hotplugMem hotpluggedMem interfaces volumes shares devices vsock - kernel initrdPath credentialFiles - storeDisk storeOnDisk; -in { + hostName + preStart + user + vcpu + mem + balloon + initialBalloonMem + hotplugMem + hotpluggedMem + interfaces + volumes + shares + devices + vsock + kernel + initrdPath + credentialFiles + storeDisk + storeOnDisk + ; +in +{ preStart = '' ${preStart} export HOME=$PWD ''; command = - if user != null - then throw "kvmtool will not change user" - else if initialBalloonMem != 0 - then throw "kvmtool does not support initialBalloonMem" - else if hotplugMem != 0 - then throw "kvmtool does not support hotplugMem" - else if hotpluggedMem != 0 - then throw "kvmtool does not support hotpluggedMem" - else if credentialFiles != {} - then throw "kvmtool does not support credentialFiles" - else builtins.concatStringsSep " " ( - [ - "${kvmtoolPkg}/bin/lkvm" "run" - "--name" (lib.escapeShellArg hostName) - "-m" (toString mem) - "-c" (toString vcpu) - "--console" "serial" - "--rng" - "-k" (lib.escapeShellArg "${kernel}/${pkgs.stdenv.hostPlatform.linux-kernel.target}") - "-i" initrdPath - "-p" (lib.escapeShellArg "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}") - ] - ++ - lib.optionals storeOnDisk [ - "-d" (lib.escapeShellArg "${storeDisk},ro") - ] - ++ - lib.optionals balloon [ "--balloon" ] - ++ - builtins.concatMap ({ serial, direct, readOnly, ... }: - lib.warnIf (serial != null) '' - Volume serial is not supported for kvmtool - '' - [ "-d" - (lib.escapeShellArg "image${ - lib.optionalString direct ",direct" - }${ - lib.optionalString readOnly ",ro" - }") + if user != null then + throw "kvmtool will not change user" + else if initialBalloonMem != 0 then + throw "kvmtool does not support initialBalloonMem" + else if hotplugMem != 0 then + throw "kvmtool does not support hotplugMem" + else if hotpluggedMem != 0 then + throw "kvmtool does not support hotpluggedMem" + else if credentialFiles != { } then + throw "kvmtool does not support credentialFiles" + else + builtins.concatStringsSep " " ( + [ + "${kvmtoolPkg}/bin/lkvm" + "run" + "--name" + (lib.escapeShellArg hostName) + "-m" + (toString mem) + "-c" + (toString vcpu) + "--console" + "serial" + "--rng" + "-k" + (lib.escapeShellArg "${kernel}/${pkgs.stdenv.hostPlatform.linux-kernel.target}") + "-i" + initrdPath + "-p" + (lib.escapeShellArg "console=ttyS0 reboot=k panic=1 ${toString microvmConfig.kernelParams}") ] - ) volumes - ++ - builtins.concatMap ({ proto, source, tag, readOnly, ... }: - if proto == "9p" - then if readOnly then - throw "kvmtool does not support readonly 9p share" - else [ - "--9p" (lib.escapeShellArg "${source},${tag}") - ] else throw "virtiofs shares not implemented for kvmtool" - ) shares - ++ - builtins.concatMap ({ type, id, mac, ... }: - if builtins.elem type [ "user" "tap" ] - then [ - "-n" (lib.escapeShellArg "mode=${type},tapif=${id},guest_mac=${mac}") + ++ lib.optionals storeOnDisk [ + "-d" + (lib.escapeShellArg "${storeDisk},ro") ] - else if type == "macvtap" - then [ - "-n" "mode=tap,tapif=/dev/tap$(< /sys/class/net/${id}/ifindex),guest_mac=${mac}" + ++ lib.optionals balloon [ "--balloon" ] + ++ builtins.concatMap ( + { + serial, + direct, + readOnly, + ... + }: + lib.warnIf (serial != null) + '' + Volume serial is not supported for kvmtool + '' + [ + "-d" + (lib.escapeShellArg "image${lib.optionalString direct ",direct"}${lib.optionalString readOnly ",ro"}") + ] + ) volumes + ++ builtins.concatMap ( + { + proto, + source, + tag, + readOnly, + ... + }: + if proto == "9p" then + if readOnly then + throw "kvmtool does not support readonly 9p share" + else + [ + "--9p" + (lib.escapeShellArg "${source},${tag}") + ] + else + throw "virtiofs shares not implemented for kvmtool" + ) shares + ++ builtins.concatMap ( + { + type, + id, + mac, + ... + }: + if + builtins.elem type [ + "user" + "tap" + ] + then + [ + "-n" + (lib.escapeShellArg "mode=${type},tapif=${id},guest_mac=${mac}") + ] + else if type == "macvtap" then + [ + "-n" + "mode=tap,tapif=/dev/tap$(< /sys/class/net/${id}/ifindex),guest_mac=${mac}" + ] + else + throw "interface type ${type} is not supported by kvmtool" + ) interfaces + ++ map ( + { bus, path }: + { + pci = lib.escapeShellArg "--vfio-pci=${path}"; + usb = throw "USB passthrough is not supported on kvmtool"; + } + .${bus} + ) devices + ++ lib.optionals (vsock.cid != null) [ + "--vsock" + (toString vsock.cid) ] - else throw "interface type ${type} is not supported by kvmtool" - ) interfaces - ++ - map ({ bus, path }: { - pci = lib.escapeShellArg "--vfio-pci=${path}"; - usb = throw "USB passthrough is not supported on kvmtool"; - }.${bus}) devices - ++ - lib.optionals (vsock.cid != null) [ - "--vsock" (toString vsock.cid) - ] - ); + ); # `lkvm stop` works but is not graceful. canShutdown = false; diff --git a/subrepos/microvm.nix/lib/runners/qemu.nix b/subrepos/microvm.nix/lib/runners/qemu.nix index 5d701ba2..8e26d2c4 100644 --- a/subrepos/microvm.nix/lib/runners/qemu.nix +++ b/subrepos/microvm.nix/lib/runners/qemu.nix @@ -1,8 +1,9 @@ -{ pkgs -, microvmConfig -, macvtapFds -, withDriveLetters -, ... +{ + pkgs, + microvmConfig, + macvtapFds, + withDriveLetters, + ... }: let @@ -10,410 +11,574 @@ let inherit (pkgs.stdenv.hostPlatform) system; inherit (microvmConfig) vmHostPackages; - enableLibusb = pkg: pkg.overrideAttrs (oa: { - configureFlags = oa.configureFlags ++ [ - "--enable-libusb" - ]; - buildInputs = oa.buildInputs ++ [ - vmHostPackages.libusb1 - ]; - }); - - minimizeQemuClosureSize = pkg: pkg.override (oa: { - # standin for disabling everything guilike by hand - nixosTestRunner = - if graphics.enable - then oa.nixosTestRunner or false - else true; - }); - - overrideQemu = x: lib.pipe x ( - lib.optional requireUsb enableLibusb - ++ lib.optional microvmConfig.optimize.enable minimizeQemuClosureSize - ); + enableLibusb = + pkg: + pkg.overrideAttrs (oa: { + configureFlags = oa.configureFlags ++ [ + "--enable-libusb" + ]; + buildInputs = oa.buildInputs ++ [ + vmHostPackages.libusb1 + ]; + }); + + minimizeQemuClosureSize = + pkg: + pkg.override (oa: { + # standin for disabling everything guilike by hand + nixosTestRunner = if graphics.enable then oa.nixosTestRunner or false else true; + }); + + overrideQemu = + x: + lib.pipe x ( + lib.optional requireUsb enableLibusb + ++ lib.optional microvmConfig.optimize.enable minimizeQemuClosureSize + ); qemu = overrideQemu microvmConfig.qemu.package; - aioEngine = if vmHostPackages.stdenv.hostPlatform.isLinux - then "io_uring" - else "threads"; - - inherit (microvmConfig) hostName machineId vcpu mem balloon initialBalloonMem deflateOnOOM hotplugMem hotpluggedMem user interfaces shares socket forwardPorts devices vsock graphics storeOnDisk kernel initrdPath storeDisk credentialFiles; - inherit (microvmConfig.qemu) machine extraArgs serialConsole pcieRootPorts; + aioEngine = if vmHostPackages.stdenv.hostPlatform.isLinux then "io_uring" else "threads"; + + inherit (microvmConfig) + hostName + machineId + vcpu + mem + balloon + initialBalloonMem + deflateOnOOM + hotplugMem + hotpluggedMem + user + interfaces + shares + socket + forwardPorts + devices + vsock + graphics + storeOnDisk + kernel + initrdPath + storeDisk + credentialFiles + ; + inherit (microvmConfig.qemu) + machine + extraArgs + serialConsole + pcieRootPorts + ; volumes = withDriveLetters microvmConfig; - requireUsb = - graphics.enable || - lib.any ({ bus, ... }: bus == "usb") microvmConfig.devices; + requireUsb = graphics.enable || lib.any ({ bus, ... }: bus == "usb") microvmConfig.devices; arch = builtins.head (builtins.split "-" system); cpuArgs = [ "-cpu" ( - if microvmConfig.cpu != null - then microvmConfig.cpu - else if system == "x86_64-linux" + if microvmConfig.cpu != null then + microvmConfig.cpu + else if + system == "x86_64-linux" # qemu crashes when sgx is used on microvm machines: https://gitlab.com/qemu-project/qemu/-/issues/2142 - then "host,+x2apic,-sgx" - else "host" - ) ]; + then + "host,+x2apic,-sgx" + else + "host" + ) + ]; - accel = if vmHostPackages.stdenv.hostPlatform.isLinux - then "kvm:tcg" else - if vmHostPackages.stdenv.hostPlatform.isDarwin - then "hvf:tcg" else "tcg"; + accel = + if vmHostPackages.stdenv.hostPlatform.isLinux then + "kvm:tcg" + else if vmHostPackages.stdenv.hostPlatform.isDarwin then + "hvf:tcg" + else + "tcg"; # PCI required by vfio-pci for PCI passthrough pciInDevices = lib.any ({ bus, ... }: bus == "pci") devices; - requirePci = - graphics.enable || - (! lib.hasPrefix "microvm" machine) || - shares != [] || - pciInDevices; + requirePci = graphics.enable || (!lib.hasPrefix "microvm" machine) || shares != [ ] || pciInDevices; machineOpts = - if microvmConfig.qemu.machineOpts != null - then microvmConfig.qemu.machineOpts - else { - x86_64-linux = { - inherit accel; - mem-merge = "on"; - acpi = "on"; - } // lib.optionalAttrs (machine == "microvm") { - pit = "off"; - pic = "off"; - pcie = if requirePci then "on" else "off"; - rtc = "on"; - usb = if requireUsb then "on" else "off"; - }; - aarch64-linux = { - inherit accel; - gic-version = "max"; - }; - aarch64-darwin = { - inherit accel; - }; - }.${system}; + if microvmConfig.qemu.machineOpts != null then + microvmConfig.qemu.machineOpts + else + { + x86_64-linux = { + inherit accel; + mem-merge = "on"; + acpi = "on"; + } + // lib.optionalAttrs (machine == "microvm") { + pit = "off"; + pic = "off"; + pcie = if requirePci then "on" else "off"; + rtc = "on"; + usb = if requireUsb then "on" else "off"; + }; + aarch64-linux = { + inherit accel; + gic-version = "max"; + }; + aarch64-darwin = { + inherit accel; + }; + } + .${system}; machineConfig = builtins.concatStringsSep "," ( - [ machine ] ++ - map (name: - "${name}=${machineOpts.${name}}" - ) (builtins.attrNames machineOpts) + [ machine ] ++ map (name: "${name}=${machineOpts.${name}}") (builtins.attrNames machineOpts) ); - devType = - if requirePci - then "pci" - else "device"; + devType = if requirePci then "pci" else "device"; kernelPath = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}"; - enumerate = n: xs: - if xs == [] - then [] - else [ - (builtins.head xs // { index = n; }) - ] ++ (enumerate (n + 1) (builtins.tail xs)); + enumerate = + n: xs: + if xs == [ ] then + [ ] + else + [ + (builtins.head xs // { index = n; }) + ] + ++ (enumerate (n + 1) (builtins.tail xs)); canSandbox = # Don't let qemu sandbox itself if it is going to call qemu-bridge-helper - (! lib.any ({ type, ... }: - type == "bridge" - ) microvmConfig.interfaces) && - (builtins.elem "--enable-seccomp" (qemu.configureFlags or [])); + (!lib.any ({ type, ... }: type == "bridge") microvmConfig.interfaces) + && (builtins.elem "--enable-seccomp" (qemu.configureFlags or [ ])); tapMultiQueue = vcpu > 1; useHotPlugMemory = hotplugMem > 0; - forwardingOptions = lib.concatMapStrings ({ proto, from, host, guest }: { - host = "hostfwd=${proto}:${host.address}:${toString host.port}-" + - "${guest.address}:${toString guest.port},"; - guest = "guestfwd=${proto}:${guest.address}:${toString guest.port}-" + - "cmd:${vmHostPackages.netcat}/bin/nc ${host.address} ${toString host.port},"; - }.${from}) forwardPorts; + forwardingOptions = lib.concatMapStrings ( + { + proto, + from, + host, + guest, + }: + { + host = + "hostfwd=${proto}:${host.address}:${toString host.port}-" + + "${guest.address}:${toString guest.port},"; + guest = + "guestfwd=${proto}:${guest.address}:${toString guest.port}-" + + "cmd:${vmHostPackages.netcat}/bin/nc ${host.address} ${toString host.port},"; + } + .${from} + ) forwardPorts; writeQmp = data: '' echo '${builtins.toJSON data}' ''; kernelConsole = - if !microvmConfig.qemu.serialConsole - then "" - else if system == "x86_64-linux" - then "earlyprintk=ttyS0 console=ttyS0" - else if system == "aarch64-linux" - then "console=ttyAMA0" - else ""; - - systemdCredentialStrings = lib.mapAttrsToList (name: path: "name=opt/io.systemd.credentials/${name},file=${path}" ) credentialFiles; + if !microvmConfig.qemu.serialConsole then + "" + else if system == "x86_64-linux" then + "earlyprintk=ttyS0 console=ttyS0" + else if system == "aarch64-linux" then + "console=ttyAMA0" + else + ""; + + systemdCredentialStrings = lib.mapAttrsToList ( + name: path: "name=opt/io.systemd.credentials/${name},file=${path}" + ) credentialFiles; fwCfgOptions = systemdCredentialStrings; in -lib.warnIf (mem == 2048) '' - QEMU hangs if memory is exactly 2GB - - -'' -{ - inherit tapMultiQueue; - - command = if initialBalloonMem != 0 - then throw "qemu does not support initialBalloonMem" - else lib.escapeShellArgs ( - [ - "${qemu}/bin/qemu-system-${arch}" - "-name" hostName - "-M" machineConfig - "-m" (toString mem) - "-smp" (toString vcpu) - "-nodefaults" "-no-user-config" - # qemu just hangs after shutdown, allow to exit by rebooting - "-no-reboot" - - "-kernel" "${kernelPath}" - "-initrd" initrdPath - - "-chardev" "stdio,id=stdio,signal=off" - "-device" "virtio-rng-${devType}" - ] ++ lib.optionals (machineId != null) [ - "-smbios" "type=1,uuid=${machineId}" - ] ++ - # Create PCIe root ports before vfio-pci devices that might require them - builtins.concatMap ({ id, bus, chassis, slot, addr, ... }: - [ "-device" "pcie-root-port,id=${id}${ - lib.optionalString (bus != null) ",bus=${bus}" + - lib.optionalString (chassis != null) ",chassis=${toString chassis}" + - lib.optionalString (slot != null) ",slot=${slot}" + - lib.optionalString (addr != null) ",addr=${addr}" - }" - ] - ) pcieRootPorts - ) - + " " + # Move vfio-pci outside of escapeShellArgs - lib.concatStringsSep " " (lib.concatMap ({ bus, path, qemu,... }: { - pci = [ - "-device" "vfio-pci,host=${path},multifunction=on${ - lib.optionalString (qemu.id != null) ",id=${qemu.id}" + - lib.optionalString (qemu.bus != null) ",bus=${qemu.bus}" + - # Allow to pass additional arguments to pci device - lib.optionalString (qemu.deviceExtraArgs != null) ",${qemu.deviceExtraArgs}" - }" - ]; - usb = [ - "-device" "usb-host,${path}" - ]; - }.${bus}) devices) - + " " + - lib.escapeShellArgs( - builtins.concatMap (fwCfgOption: ["-fw_cfg" fwCfgOption]) fwCfgOptions ++ - lib.optionals serialConsole [ - "-serial" "chardev:stdio" - ] ++ - lib.optionals (vmHostPackages.stdenv.hostPlatform.isLinux && microvmConfig.cpu == null) [ - "-enable-kvm" - ] ++ - cpuArgs ++ - lib.optionals (system == "x86_64-linux") [ - "-device" "i8042" - - "-append" "${kernelConsole} reboot=t panic=-1 ${toString microvmConfig.kernelParams}" - ] ++ - lib.optionals (system == "aarch64-linux") [ - "-append" "${kernelConsole} reboot=t panic=-1 ${toString microvmConfig.kernelParams}" - ] ++ - lib.optionals storeOnDisk [ - "-drive" "id=store,format=raw,read-only=on,file=${storeDisk},if=none,aio=${aioEngine}" - "-device" "virtio-blk-${devType},drive=store${lib.optionalString (devType == "pci") ",disable-legacy=on"}" - ] ++ - (if graphics.enable then ( - let - displayArgs = { - cocoa = [ - "-display" "cocoa" "-device" "virtio-gpu" - ]; - gtk = [ - "-display" "gtk,gl=on" "-device" "virtio-vga-gl" - ]; - }.${graphics.backend}; - in - displayArgs ++ [ - "-device" "qemu-xhci" - "-device" "usb-tablet" - "-device" "usb-kbd" - ] - ) else [ "-nographic" ]) ++ - lib.optionals canSandbox [ - "-sandbox" "on" - ] ++ - lib.optionals (user != null) [ "-user" user ] ++ - lib.optionals (socket != null) [ "-qmp" "unix:${socket},server,nowait" ] ++ - lib.optionals balloon [ - "-device" ("virtio-balloon,free-page-reporting=on,id=balloon0" + lib.optionalString (deflateOnOOM) ",deflate-on-oom=on") - ] ++ - lib.optionals useHotPlugMemory [ - "-object" "memory-backend-ram,id=vmem0,size=${toString hotplugMem}M" - "-device" "virtio-mem-pci,id=vm0,memdev=vmem0,requested-size=${toString hotpluggedMem}M" - ] ++ - builtins.concatMap ({ image, letter, serial, direct, readOnly, ... }: - [ "-drive" - "id=vd${letter},format=raw,file=${image},if=none,aio=${aioEngine},discard=unmap${ - lib.optionalString (direct != null) ",cache=none" - },read-only=${if readOnly then "on" else "off"}" - "-device" - "virtio-blk-${devType},drive=vd${letter}${ - lib.optionalString (serial != null) ",serial=${serial}" - }" - ] - ) volumes ++ - lib.optionals (shares != []) ( - (lib.optionals vmHostPackages.stdenv.hostPlatform.isLinux [ - "-numa" "node,memdev=mem" - "-object" "memory-backend-memfd,id=mem,size=${toString mem}M,share=on" - ]) ++ - builtins.concatMap ({ proto, index, socket, source, tag, securityModel, readOnly, ... }: { - "virtiofs" = [ - "-chardev" "socket,id=fs${toString index},path=${socket}" - "-device" "vhost-user-fs-${devType},chardev=fs${toString index},tag=${tag}" - ]; - "9p" = [ - "-fsdev" "local,id=fs${toString index},path=${source},security_model=${securityModel},readonly=${lib.boolToString readOnly}" - "-device" "virtio-9p-${devType},fsdev=fs${toString index},mount_tag=${tag}" - ]; - }.${proto}) (enumerate 0 shares) - ) - ++ - lib.warnIf ( - forwardPorts != [] && - ! builtins.any ({ type, ... }: type == "user") interfaces - ) "${hostName}: forwardPortsOptions only running with user network" ( - builtins.concatMap ({ type, id, mac, bridge, tap ? {}, ... }: [ - "-netdev" ( - lib.concatStringsSep "," ( +lib.warnIf (mem == 2048) + '' + QEMU hangs if memory is exactly 2GB + + + '' + { + inherit tapMultiQueue; + + command = + if initialBalloonMem != 0 then + throw "qemu does not support initialBalloonMem" + else + lib.escapeShellArgs ( + [ + "${qemu}/bin/qemu-system-${arch}" + "-name" + hostName + "-M" + machineConfig + "-m" + (toString mem) + "-smp" + (toString vcpu) + "-nodefaults" + "-no-user-config" + # qemu just hangs after shutdown, allow to exit by rebooting + "-no-reboot" + + "-kernel" + "${kernelPath}" + "-initrd" + initrdPath + + "-chardev" + "stdio,id=stdio,signal=off" + "-device" + "virtio-rng-${devType}" + ] + ++ lib.optionals (machineId != null) [ + "-smbios" + "type=1,uuid=${machineId}" + ] + ++ + # Create PCIe root ports before vfio-pci devices that might require them + builtins.concatMap ( + { + id, + bus, + chassis, + slot, + addr, + ... + }: + [ + "-device" + "pcie-root-port,id=${id}${ + lib.optionalString (bus != null) ",bus=${bus}" + + lib.optionalString (chassis != null) ",chassis=${toString chassis}" + + lib.optionalString (slot != null) ",slot=${slot}" + + lib.optionalString (addr != null) ",addr=${addr}" + }" + ] + ) pcieRootPorts + ) + + " " + # Move vfio-pci outside of escapeShellArgs + + lib.concatStringsSep " " ( + lib.concatMap ( + { + bus, + path, + qemu, + ... + }: + { + pci = [ + "-device" + "vfio-pci,host=${path},multifunction=on${ + lib.optionalString (qemu.id != null) ",id=${qemu.id}" + + lib.optionalString (qemu.bus != null) ",bus=${qemu.bus}" + + + # Allow to pass additional arguments to pci device + lib.optionalString (qemu.deviceExtraArgs != null) ",${qemu.deviceExtraArgs}" + }" + ]; + usb = [ + "-device" + "usb-host,${path}" + ]; + } + .${bus} + ) devices + ) + + " " + + lib.escapeShellArgs ( + builtins.concatMap (fwCfgOption: [ + "-fw_cfg" + fwCfgOption + ]) fwCfgOptions + ++ lib.optionals serialConsole [ + "-serial" + "chardev:stdio" + ] + ++ lib.optionals (vmHostPackages.stdenv.hostPlatform.isLinux && microvmConfig.cpu == null) [ + "-enable-kvm" + ] + ++ cpuArgs + ++ lib.optionals (system == "x86_64-linux") [ + "-device" + "i8042" + + "-append" + "${kernelConsole} reboot=t panic=-1 ${toString microvmConfig.kernelParams}" + ] + ++ lib.optionals (system == "aarch64-linux") [ + "-append" + "${kernelConsole} reboot=t panic=-1 ${toString microvmConfig.kernelParams}" + ] + ++ lib.optionals storeOnDisk [ + "-drive" + "id=store,format=raw,read-only=on,file=${storeDisk},if=none,aio=${aioEngine}" + "-device" + "virtio-blk-${devType},drive=store${lib.optionalString (devType == "pci") ",disable-legacy=on"}" + ] + ++ ( + if graphics.enable then + ( + let + displayArgs = + { + cocoa = [ + "-display" + "cocoa" + "-device" + "virtio-gpu" + ]; + gtk = [ + "-display" + "gtk,gl=on" + "-device" + "virtio-vga-gl" + ]; + } + .${graphics.backend}; + in + displayArgs + ++ [ + "-device" + "qemu-xhci" + "-device" + "usb-tablet" + "-device" + "usb-kbd" + ] + ) + else + [ "-nographic" ] + ) + ++ lib.optionals canSandbox [ + "-sandbox" + "on" + ] + ++ lib.optionals (user != null) [ + "-user" + user + ] + ++ lib.optionals (socket != null) [ + "-qmp" + "unix:${socket},server,nowait" + ] + ++ lib.optionals balloon [ + "-device" + ( + "virtio-balloon,free-page-reporting=on,id=balloon0" + + lib.optionalString (deflateOnOOM) ",deflate-on-oom=on" + ) + ] + ++ lib.optionals useHotPlugMemory [ + "-object" + "memory-backend-ram,id=vmem0,size=${toString hotplugMem}M" + "-device" + "virtio-mem-pci,id=vm0,memdev=vmem0,requested-size=${toString hotpluggedMem}M" + ] + ++ builtins.concatMap ( + { + image, + letter, + serial, + direct, + readOnly, + ... + }: [ - (if type == "macvtap" then "tap" else "${type}") - "id=${id}" - ] - ++ lib.optionals (type == "user" && forwardPorts != []) [ - forwardingOptions - ] - ++ lib.optionals (type == "bridge") [ - "br=${bridge}" "helper=/run/wrappers/bin/qemu-bridge-helper" - ] - ++ lib.optionals (type == "tap") [ - "ifname=${id}" - "script=no" "downscript=no" - ] - ++ lib.optionals (type == "tap" && tap.vhost or false) [ - "vhost=on" - ] - ++ lib.optionals (type == "macvtap") [ ( - let - fds = macvtapFds.${id}; - in - if builtins.length fds == 1 - then "fd=${toString (builtins.head fds)}" - else "fds=${lib.concatMapStringsSep ":" toString fds}" - ) ] - ++ lib.optionals (type == "tap" && tapMultiQueue) [ - "queues=${toString vcpu}" + "-drive" + "id=vd${letter},format=raw,file=${image},if=none,aio=${aioEngine},discard=unmap${ + lib.optionalString (direct != null) ",cache=none" + },read-only=${if readOnly then "on" else "off"}" + "-device" + "virtio-blk-${devType},drive=vd${letter}${lib.optionalString (serial != null) ",serial=${serial}"}" ] + ) volumes + ++ lib.optionals (shares != [ ]) ( + (lib.optionals vmHostPackages.stdenv.hostPlatform.isLinux [ + "-numa" + "node,memdev=mem" + "-object" + "memory-backend-memfd,id=mem,size=${toString mem}M,share=on" + ]) + ++ builtins.concatMap ( + { + proto, + index, + socket, + source, + tag, + securityModel, + readOnly, + ... + }: + { + "virtiofs" = [ + "-chardev" + "socket,id=fs${toString index},path=${socket}" + "-device" + "vhost-user-fs-${devType},chardev=fs${toString index},tag=${tag}" + ]; + "9p" = [ + "-fsdev" + "local,id=fs${toString index},path=${source},security_model=${securityModel},readonly=${lib.boolToString readOnly}" + "-device" + "virtio-9p-${devType},fsdev=fs${toString index},mount_tag=${tag}" + ]; + } + .${proto} + ) (enumerate 0 shares) ) - ) - "-device" "virtio-net-${devType},netdev=${id},mac=${mac}${ - # romfile= does not work with x86_64-linux and -M microvm - # setting or -cpu different than host - lib.optionalString ( - requirePci || - (microvmConfig.cpu == null && system != "x86_64-linux") - ) ",romfile=" - }${ - lib.optionalString (tapMultiQueue && requirePci) ",mq=on,vectors=${toString (2 * vcpu + 2)}" - }" - ]) interfaces - ) - ++ - lib.optionals requireUsb [ - "-device" "qemu-xhci" - ] - ++ - lib.optionals (vsock.cid != null) [ - "-device" - "vhost-vsock-${devType},guest-cid=${toString vsock.cid}" - ] - ++ - extraArgs - ); + ++ + lib.warnIf (forwardPorts != [ ] && !builtins.any ({ type, ... }: type == "user") interfaces) + "${hostName}: forwardPortsOptions only running with user network" + ( + builtins.concatMap ( + { + type, + id, + mac, + bridge, + tap ? { }, + ... + }: + [ + "-netdev" + (lib.concatStringsSep "," ( + [ + (if type == "macvtap" then "tap" else "${type}") + "id=${id}" + ] + ++ lib.optionals (type == "user" && forwardPorts != [ ]) [ + forwardingOptions + ] + ++ lib.optionals (type == "bridge") [ + "br=${bridge}" + "helper=/run/wrappers/bin/qemu-bridge-helper" + ] + ++ lib.optionals (type == "tap") [ + "ifname=${id}" + "script=no" + "downscript=no" + ] + ++ lib.optionals (type == "tap" && tap.vhost or false) [ + "vhost=on" + ] + ++ lib.optionals (type == "macvtap") [ + ( + let + fds = macvtapFds.${id}; + in + if builtins.length fds == 1 then + "fd=${toString (builtins.head fds)}" + else + "fds=${lib.concatMapStringsSep ":" toString fds}" + ) + ] + ++ lib.optionals (type == "tap" && tapMultiQueue) [ + "queues=${toString vcpu}" + ] + )) + "-device" + "virtio-net-${devType},netdev=${id},mac=${mac}${ + # romfile= does not work with x86_64-linux and -M microvm + # setting or -cpu different than host + lib.optionalString ( + requirePci || (microvmConfig.cpu == null && system != "x86_64-linux") + ) ",romfile=" + }${lib.optionalString (tapMultiQueue && requirePci) ",mq=on,vectors=${toString (2 * vcpu + 2)}"}" + ] + ) interfaces + ) + ++ lib.optionals requireUsb [ + "-device" + "qemu-xhci" + ] + ++ lib.optionals (vsock.cid != null) [ + "-device" + "vhost-vsock-${devType},guest-cid=${toString vsock.cid}" + ] + ++ extraArgs + ); + + canShutdown = socket != null; + + shutdownCommand = + if socket != null then + '' + # Exit gracefully if QEMU is already gone (e.g., killed by machinectl) + if [ ! -S ${socket} ]; then + exit 0 + fi + + ( + ${writeQmp { execute = "qmp_capabilities"; }} + ${writeQmp { + execute = "input-send-event"; + arguments.events = [ + { + type = "key"; + data = { + down = true; + key = { + type = "qcode"; + data = "ctrl"; + }; + }; + } + { + type = "key"; + data = { + down = true; + key = { + type = "qcode"; + data = "alt"; + }; + }; + } + { + type = "key"; + data = { + down = true; + key = { + type = "qcode"; + data = "delete"; + }; + }; + } + ]; + }} + # wait for exit + cat + ) | \ + ${vmHostPackages.socat}/bin/socat STDIO UNIX:${socket},shut-none + '' + else + throw "Cannot shutdown without socket"; + + setBalloonScript = + if socket != null then + '' + VALUE=$(( $SIZE * 1024 * 1024 )) + SIZE=$( ( + ${writeQmp { execute = "qmp_capabilities"; }} + ${writeQmp { + execute = "balloon"; + arguments.value = 987; + }} + ) | sed -e s/987/$VALUE/ | \ + ${vmHostPackages.socat}/bin/socat STDIO UNIX:${socket},shut-none | \ + tail -n 1 | \ + ${vmHostPackages.jq}/bin/jq -r .data.actual \ + ) + echo $(( $SIZE / 1024 / 1024 )) + '' + else + null; - canShutdown = socket != null; - - shutdownCommand = - if socket != null - then - '' - # Exit gracefully if QEMU is already gone (e.g., killed by machinectl) - if [ ! -S ${socket} ]; then - exit 0 - fi - - ( - ${writeQmp { execute = "qmp_capabilities"; }} - ${writeQmp { - execute = "input-send-event"; - arguments.events = [ { - type = "key"; - data = { - down = true; - key = { - type = "qcode"; - data = "ctrl"; - }; - }; - } { - type = "key"; - data = { - down = true; - key = { - type = "qcode"; - data = "alt"; - }; - }; - } { - type = "key"; - data = { - down = true; - key = { - type = "qcode"; - data = "delete"; - }; - }; - } ]; - }} - # wait for exit - cat - ) | \ - ${vmHostPackages.socat}/bin/socat STDIO UNIX:${socket},shut-none - '' - else throw "Cannot shutdown without socket"; - - setBalloonScript = - if socket != null - then '' - VALUE=$(( $SIZE * 1024 * 1024 )) - SIZE=$( ( - ${writeQmp { execute = "qmp_capabilities"; }} - ${writeQmp { execute = "balloon"; arguments.value = 987; }} - ) | sed -e s/987/$VALUE/ | \ - ${vmHostPackages.socat}/bin/socat STDIO UNIX:${socket},shut-none | \ - tail -n 1 | \ - ${vmHostPackages.jq}/bin/jq -r .data.actual \ - ) - echo $(( $SIZE / 1024 / 1024 )) - '' - else null; - - requiresMacvtapAsFds = true; -} + requiresMacvtapAsFds = true; + } diff --git a/subrepos/microvm.nix/lib/runners/stratovirt.nix b/subrepos/microvm.nix/lib/runners/stratovirt.nix index 39a5bb4b..3a6afd56 100644 --- a/subrepos/microvm.nix/lib/runners/stratovirt.nix +++ b/subrepos/microvm.nix/lib/runners/stratovirt.nix @@ -1,8 +1,9 @@ -{ pkgs -, microvmConfig -, macvtapFds -, withDriveLetters -, ... +{ + pkgs, + microvmConfig, + macvtapFds, + withDriveLetters, + ... }: let @@ -13,9 +14,23 @@ let inherit (microvmConfig) hostName - vcpu mem balloon initialBalloonMem hotplugMem hotpluggedMem interfaces shares socket forwardPorts devices - kernel initrdPath credentialFiles - storeOnDisk storeDisk; + vcpu + mem + balloon + initialBalloonMem + hotplugMem + hotpluggedMem + interfaces + shares + socket + forwardPorts + devices + kernel + initrdPath + credentialFiles + storeOnDisk + storeDisk + ; tapMultiQueue = vcpu > 1; @@ -24,182 +39,236 @@ let # PCI required by vfio-pci for PCI passthrough pciInDevices = lib.any ({ bus, ... }: bus == "pci") devices; requirePci = pciInDevices; - machine = { - x86_64-linux = - if requirePci - then throw "PCI configuration for stratovirt is non-functional" "q35" - else "microvm"; - aarch64-linux = "virt"; - }.${system}; - - console = { - x86_64-linux = "ttyS0"; - aarch64-linux = "ttyAMA0"; - }.${system}; - - devType = addr: - if requirePci - then - if addr < 32 - then "pci,bus=pcie.0,addr=0x${lib.toHexString addr}" - else throw "Too big PCI addr: ${lib.toHexString addr}" - else "device"; - - enumerate = n: xs: - if xs == [] - then [] - else [ - (builtins.head xs // { index = n; }) - ] ++ (enumerate (n + 1) (builtins.tail xs)); + machine = + { + x86_64-linux = + if requirePci then throw "PCI configuration for stratovirt is non-functional" "q35" else "microvm"; + aarch64-linux = "virt"; + } + .${system}; + + console = + { + x86_64-linux = "ttyS0"; + aarch64-linux = "ttyAMA0"; + } + .${system}; + + devType = + addr: + if requirePci then + if addr < 32 then + "pci,bus=pcie.0,addr=0x${lib.toHexString addr}" + else + throw "Too big PCI addr: ${lib.toHexString addr}" + else + "device"; + + enumerate = + n: xs: + if xs == [ ] then + [ ] + else + [ + (builtins.head xs // { index = n; }) + ] + ++ (enumerate (n + 1) (builtins.tail xs)); virtioblkOffset = 4; virtiofsOffset = virtioblkOffset + builtins.length microvmConfig.volumes; forwardPortsOptions = - let - forwardingOptions = lib.flip lib.concatMapStrings forwardPorts - ({ proto, from, host, guest }: - if from == "host" - then "hostfwd=${proto}:${host.address}:${toString host.port}-" + - "${guest.address}:${toString guest.port}," - else "guestfwd=${proto}:${guest.address}:${toString guest.port}-" + - "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}," - ); - in - [ forwardingOptions ]; + let + forwardingOptions = lib.flip lib.concatMapStrings forwardPorts ( + { + proto, + from, + host, + guest, + }: + if from == "host" then + "hostfwd=${proto}:${host.address}:${toString host.port}-" + + "${guest.address}:${toString guest.port}," + else + "guestfwd=${proto}:${guest.address}:${toString guest.port}-" + + "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}," + ); + in + [ forwardingOptions ]; writeQmp = data: '' echo '${builtins.toJSON data}' | nc -U "${socket}" ''; -in { +in +{ inherit tapMultiQueue; - command = if balloon - then throw "balloon not implemented for stratovirt" - else if initialBalloonMem != 0 - then throw "initialBalloonMem not implemented for stratovirt" - else if hotplugMem != 0 - then throw "stratovirt does not support hotplugMem" - else if hotpluggedMem != 0 - then throw "stratovirt does not support hotpluggedMem" - else if credentialFiles != {} - then throw "stratovirt does not support credentialFiles" - else lib.escapeShellArgs ( - [ - "${pkgs.expect}/bin/unbuffer" - "${stratovirtPkg}/bin/stratovirt" - "-name" hostName - "-machine" machine - "-m" (toString mem) - "-smp" (toString vcpu) - - "-kernel" "${kernel}/bzImage" - "-initrd" initrdPath - "-append" "console=${console} edd=off reboot=t panic=-1 ${toString microvmConfig.kernelParams}" - - "-serial" "stdio" - "-object" "rng-random,id=rng,filename=/dev/random" - "-device" "virtio-rng-${devType 1},rng=rng,id=rng_dev" - ] ++ - lib.optionals storeOnDisk [ - "-drive" "id=store,format=raw,readonly=on,file=${storeDisk},if=none,aio=io_uring,direct=false" - "-device" "virtio-blk-${devType 2},drive=store,id=blk_store" - ] ++ - lib.optionals (socket != null) [ "-qmp" "unix:${socket},server,nowait" ] ++ - builtins.concatMap ({ index, image, letter, serial, direct, readOnly, ... }: [ - "-drive" - "id=vd${ - letter - },format=raw,if=none,aio=io_uring,file=${ - image - },direct=${ - if direct then "on" else "off" - },readonly=${ - if readOnly then "on" else "off" - }" - "-device" - "virtio-blk-${ - devType (virtioblkOffset + index) - },drive=vd${ - letter - },id=blk_vd${ - letter - }${ - lib.optionalString (serial != null) ",serial=${serial}" - }" - ]) (enumerate 0 volumes) ++ - lib.optionals (shares != []) ( - builtins.concatMap ({ proto, index, socket, tag, ... }: { - "virtiofs" = [ - "-chardev" - "socket,id=fs${toString index},path=${socket}" + command = + if balloon then + throw "balloon not implemented for stratovirt" + else if initialBalloonMem != 0 then + throw "initialBalloonMem not implemented for stratovirt" + else if hotplugMem != 0 then + throw "stratovirt does not support hotplugMem" + else if hotpluggedMem != 0 then + throw "stratovirt does not support hotpluggedMem" + else if credentialFiles != { } then + throw "stratovirt does not support credentialFiles" + else + lib.escapeShellArgs ( + [ + "${pkgs.expect}/bin/unbuffer" + "${stratovirtPkg}/bin/stratovirt" + "-name" + hostName + "-machine" + machine + "-m" + (toString mem) + "-smp" + (toString vcpu) + + "-kernel" + "${kernel}/bzImage" + "-initrd" + initrdPath + "-append" + "console=${console} edd=off reboot=t panic=-1 ${toString microvmConfig.kernelParams}" + + "-serial" + "stdio" + "-object" + "rng-random,id=rng,filename=/dev/random" "-device" - "vhost-user-fs-${devType (virtiofsOffset + index)},chardev=fs${toString index},tag=${tag},id=fs${toString index}" - ]; - }.${proto}) (enumerate 0 shares) - ) - ++ - lib.warnIf ( - forwardPorts != [] && - ! builtins.any ({ type, ... }: type == "user") interfaces - ) "${hostName}: forwardPortsOptions only running with user network" ( - builtins.concatMap ({ type, id, mac, bridge, ... }: [ - "-netdev" ( - lib.concatStringsSep "," ( - [ - (if type == "macvtap" then "tap" else "${type}") - "id=${id}" - "queues=${toString (lib.min 16 vcpu)}" - ] - ++ lib.optionals (type == "user" && forwardPortsOptions != []) forwardPortsOptions - ++ lib.optionals (type == "bridge") [ - "br=${bridge}" "helper=/run/wrappers/bin/qemu-bridge-helper" - ] - ++ lib.optionals (type == "tap") [ - "ifname=${id}" - ] - ++ lib.optionals (type == "macvtap") [ - "fd=${toString macvtapFds.${id}}" - ] - ++ lib.optionals tapMultiQueue [ - "queues=${toString vcpu}" - ] - ) - ) - # TODO: devType (0x10 + i) - "-device" ( - lib.concatStringsSep "," [ - "virtio-net-${devType 30}" - "id=net_${id}" - "netdev=${id}" - "mac=${mac}" - "mq=${if tapMultiQueue then "on" else "off"}" + "virtio-rng-${devType 1},rng=rng,id=rng_dev" + ] + ++ lib.optionals storeOnDisk [ + "-drive" + "id=store,format=raw,readonly=on,file=${storeDisk},if=none,aio=io_uring,direct=false" + "-device" + "virtio-blk-${devType 2},drive=store,id=blk_store" + ] + ++ lib.optionals (socket != null) [ + "-qmp" + "unix:${socket},server,nowait" + ] + ++ builtins.concatMap ( + { + index, + image, + letter, + serial, + direct, + readOnly, + ... + }: + [ + "-drive" + "id=vd${letter},format=raw,if=none,aio=io_uring,file=${image},direct=${ + if direct then "on" else "off" + },readonly=${if readOnly then "on" else "off"}" + "-device" + "virtio-blk-${devType (virtioblkOffset + index)},drive=vd${letter},id=blk_vd${letter}${ + lib.optionalString (serial != null) ",serial=${serial}" + }" ] + ) (enumerate 0 volumes) + ++ lib.optionals (shares != [ ]) ( + builtins.concatMap ( + { + proto, + index, + socket, + tag, + ... + }: + { + "virtiofs" = [ + "-chardev" + "socket,id=fs${toString index},path=${socket}" + "-device" + "vhost-user-fs-${ + devType (virtiofsOffset + index) + },chardev=fs${toString index},tag=${tag},id=fs${toString index}" + ]; + } + .${proto} + ) (enumerate 0 shares) ) - ]) interfaces - ) - ++ - builtins.concatMap ({ bus, path, ... }: { - pci = [ - "-device" "vfio-pci,host=${path}" - ]; - usb = [ - "-device" "usb-host,${path}" - ]; - }.${bus}) devices - ++ - lib.optionals (lib.hasPrefix "q35" machine) [ - "-drive" "file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd,if=pflash,unit=0,readonly=true" - "-drive" "file=${pkgs.OVMF.fd}/FV/OVMF_VARS.fd,if=pflash,unit=1,readonly=true" - ] - ); + ++ + lib.warnIf (forwardPorts != [ ] && !builtins.any ({ type, ... }: type == "user") interfaces) + "${hostName}: forwardPortsOptions only running with user network" + ( + builtins.concatMap ( + { + type, + id, + mac, + bridge, + ... + }: + [ + "-netdev" + (lib.concatStringsSep "," ( + [ + (if type == "macvtap" then "tap" else "${type}") + "id=${id}" + "queues=${toString (lib.min 16 vcpu)}" + ] + ++ lib.optionals (type == "user" && forwardPortsOptions != [ ]) forwardPortsOptions + ++ lib.optionals (type == "bridge") [ + "br=${bridge}" + "helper=/run/wrappers/bin/qemu-bridge-helper" + ] + ++ lib.optionals (type == "tap") [ + "ifname=${id}" + ] + ++ lib.optionals (type == "macvtap") [ + "fd=${toString macvtapFds.${id}}" + ] + ++ lib.optionals tapMultiQueue [ + "queues=${toString vcpu}" + ] + )) + # TODO: devType (0x10 + i) + "-device" + (lib.concatStringsSep "," [ + "virtio-net-${devType 30}" + "id=net_${id}" + "netdev=${id}" + "mac=${mac}" + "mq=${if tapMultiQueue then "on" else "off"}" + ]) + ] + ) interfaces + ) + ++ builtins.concatMap ( + { bus, path, ... }: + { + pci = [ + "-device" + "vfio-pci,host=${path}" + ]; + usb = [ + "-device" + "usb-host,${path}" + ]; + } + .${bus} + ) devices + ++ lib.optionals (lib.hasPrefix "q35" machine) [ + "-drive" + "file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd,if=pflash,unit=0,readonly=true" + "-drive" + "file=${pkgs.OVMF.fd}/FV/OVMF_VARS.fd,if=pflash,unit=1,readonly=true" + ] + ); # Not supported for the `microvm` machine model canShutdown = false; shutdownCommand = - if socket != null - then + if socket != null then '' # ${writeQmp { execute = "qmp_capabilities"; }} # ${writeQmp { execute = "system_powerdown"; }} @@ -227,7 +296,8 @@ in { # wait for exit cat "${socket}" '' - else throw "Cannot shutdown without socket"; + else + throw "Cannot shutdown without socket"; requiresMacvtapAsFds = true; } diff --git a/subrepos/microvm.nix/lib/runners/vfkit.nix b/subrepos/microvm.nix/lib/runners/vfkit.nix index c89667a8..0e0c9aba 100644 --- a/subrepos/microvm.nix/lib/runners/vfkit.nix +++ b/subrepos/microvm.nix/lib/runners/vfkit.nix @@ -1,7 +1,8 @@ -{ pkgs -, microvmConfig -, withDriveLetters -, ... +{ + pkgs, + microvmConfig, + withDriveLetters, + ... }: let @@ -12,9 +13,24 @@ let vfkitPkg = microvmConfig.vfkit.package; inherit (microvmConfig) - vcpu mem user interfaces shares socket hostName - storeOnDisk storeDisk kernel initrdPath kernelParams - balloon devices credentialFiles vsock graphics; + vcpu + mem + user + interfaces + shares + socket + hostName + storeOnDisk + storeDisk + kernel + initrdPath + kernelParams + balloon + devices + credentialFiles + vsock + graphics + ; inherit (microvmConfig.vfkit) extraArgs logLevel; @@ -25,53 +41,95 @@ let kernelConsole = if graphics.enable then "tty0" else "hvc0"; - kernelCmdLine = [ "console=${kernelConsole}" "reboot=t" "panic=-1" ] ++ kernelParams; - + kernelCmdLine = [ + "console=${kernelConsole}" + "reboot=t" + "panic=-1" + ] + ++ kernelParams; - deviceArgs = + deviceArgs = [ + "--device" + "virtio-rng" + ] + ++ ( + if graphics.enable then + [ + "--device" + "virtio-gpu" + "--device" + "virtio-input,keyboard" + "--device" + "virtio-input,pointing" + ] + else + [ + "--device" + "virtio-serial,stdio" + ] + ) + ++ lib.optionals storeOnDisk [ + "--device" + "virtio-blk,path=${storeDisk},readonly" + ] + ++ (builtins.concatMap ( + { image, ... }: [ - "--device" "virtio-rng" - ] - ++ (if graphics.enable then [ - "--device" "virtio-gpu" - "--device" "virtio-input,keyboard" - "--device" "virtio-input,pointing" - ] else [ - "--device" "virtio-serial,stdio" - ]) - ++ lib.optionals storeOnDisk [ - "--device" "virtio-blk,path=${storeDisk},readonly" + "--device" + "virtio-blk,path=${image}" ] - ++ (builtins.concatMap ({ image, ... }: [ - "--device" "virtio-blk,path=${image}" - ]) volumesWithLetters) - ++ (builtins.concatMap ({ proto, source, tag, ... }: - if proto == "virtiofs" then [ - "--device" "virtio-fs,sharedDir=${source},mountTag=${tag}" + ) volumesWithLetters) + ++ (builtins.concatMap ( + { + proto, + source, + tag, + ... + }: + if proto == "virtiofs" then + [ + "--device" + "virtio-fs,sharedDir=${source},mountTag=${tag}" ] - else - throw "vfkit does not support ${proto} share. Use proto = \"virtiofs\" instead." - ) shares) - ++ (builtins.concatMap ({ type, id, mac, ... }: - if type == "user" then [ - "--device" "virtio-net,nat,mac=${mac}" + else + throw "vfkit does not support ${proto} share. Use proto = \"virtiofs\" instead." + ) shares) + ++ (builtins.concatMap ( + { + type, + id, + mac, + ... + }: + if type == "user" then + [ + "--device" + "virtio-net,nat,mac=${mac}" ] - else if type == "bridge" then - throw "vfkit bridge networking requires vmnet-helper which is not yet implemented. Use type = \"user\" for NAT networking." - else - throw "vfkit does not support ${type} networking on macOS. Use type = \"user\" for NAT networking." - ) interfaces); + else if type == "bridge" then + throw "vfkit bridge networking requires vmnet-helper which is not yet implemented. Use type = \"user\" for NAT networking." + else + throw "vfkit does not support ${type} networking on macOS. Use type = \"user\" for NAT networking." + ) interfaces); canShutdown = socket != null; allArgs = [ - "--cpus" (toString vcpu) - "--memory" (toString mem) - "--kernel" kernelPath - "--initrd" initrdPath - "--kernel-cmdline" (builtins.concatStringsSep " " kernelCmdLine) + "--cpus" + (toString vcpu) + "--memory" + (toString mem) + "--kernel" + kernelPath + "--initrd" + initrdPath + "--kernel-cmdline" + (builtins.concatStringsSep " " kernelCmdLine) + ] + ++ lib.optionals (logLevel != null) [ + "--log-level" + logLevel ] - ++ lib.optionals (logLevel != null) [ "--log-level" logLevel ] ++ lib.optionals graphics.enable [ "--gui" ] ++ deviceArgs ++ extraArgs; @@ -91,20 +149,24 @@ in check = cond: msg: if cond then throw msg else null; errors = [ (check (!vmHostPackages.stdenv.hostPlatform.isDarwin) "vfkit only works on macOS (Darwin)") - (check (vmHostPackages.stdenv.hostPlatform.isAarch64 != pkgs.stdenv.hostPlatform.isAarch64) "Architecture mismatch") + (check ( + vmHostPackages.stdenv.hostPlatform.isAarch64 != pkgs.stdenv.hostPlatform.isAarch64 + ) "Architecture mismatch") (check (user != null) "vfkit does not support changing user") (check (balloon) "vfkit does not support memory ballooning") - (check (devices != []) "vfkit does not support device passthrough") - (check (credentialFiles != {}) "vfkit does not support credentialFiles") + (check (devices != [ ]) "vfkit does not support device passthrough") + (check (credentialFiles != { }) "vfkit does not support credentialFiles") ]; valid = lib.all (e: e == null) errors; in - if !valid then lib.findFirst (e: e != null) null errors + if !valid then + lib.findFirst (e: e != null) null errors else let vfkitArgs = lib.concatStringsSep " " (map lib.escapeShellArg allArgs); in - "bash -c " + lib.escapeShellArg '' + "bash -c " + + lib.escapeShellArg '' ARGS=(${vfkitArgs}) ${lib.optionalString (socket != null) '' S=${lib.escapeShellArg socket} diff --git a/subrepos/microvm.nix/lib/volumes.nix b/subrepos/microvm.nix/lib/volumes.nix index fdef7de6..3a7e4549 100644 --- a/subrepos/microvm.nix/lib/volumes.nix +++ b/subrepos/microvm.nix/lib/volumes.nix @@ -2,69 +2,87 @@ let inherit (pkgs) lib; - inherit (import ../../lib { - inherit lib; - }) defaultFsType; + inherit + (import ../../lib { + inherit lib; + }) + defaultFsType + ; - fsTypeToUtil = fs: with pkgs; { + fsTypeToUtil = + fs: + with pkgs; + { "ext2" = e2fsprogs; "ext3" = e2fsprogs; "ext4" = e2fsprogs; "xfs" = xfsprogs; "btrfs" = btrfs-progs; "vfat" = dosfstools; - }.${fs} or (throw "Do not know how to handle ${fs}"); + } + .${fs} or (throw "Do not know how to handle ${fs}"); collectFsTypes = volumes: map (v: v.fsType) volumes; collectFsUtils = volumes: map (fsType: fsTypeToUtil fsType) (collectFsTypes volumes); in { - createVolumesScript = volumes: + createVolumesScript = + volumes: lib.optionalString (volumes != [ ]) ( lib.optionalString (lib.any (v: v.autoCreate) volumes) '' PATH=$PATH:${lib.makeBinPath ([ pkgs.coreutils ] ++ (collectFsUtils volumes))} '' + lib.concatMapStringsSep "\n" ( - { image - , label - , size ? throw "Specify a size for volume ${image} or use autoCreate = false" - , mkfsExtraArgs - , fsType ? defaultFsType - , autoCreate ? true - , ... }: + { + image, + label, + size ? throw "Specify a size for volume ${image} or use autoCreate = false", + mkfsExtraArgs, + fsType ? defaultFsType, + autoCreate ? true, + ... + }: lib.warnIf (label != null && !autoCreate) - "Volume is not automatically labeled unless autoCreate is true. Volume has to be labeled manually, otherwise it will not be identified" ( + "Volume is not automatically labeled unless autoCreate is true. Volume has to be labeled manually, otherwise it will not be identified" + ( let labelOption = - if autoCreate then ( - if builtins.elem fsType [ - "ext2" - "ext3" - "ext4" - "xfs" - "btrfs" - ] - then "-L" - else if fsType == "vfat" - then "-n" - else lib.warnIf (label != null) - "Will not label volume ${label} with filesystem type ${fsType}. Open an issue on the microvm.nix project to request a fix." - null - - ) - else null; - labelArgument = lib.optionalString (labelOption != null && label != null) "${labelOption} '${label}'"; + if autoCreate then + ( + if + builtins.elem fsType [ + "ext2" + "ext3" + "ext4" + "xfs" + "btrfs" + ] + then + "-L" + else if fsType == "vfat" then + "-n" + else + lib.warnIf (label != null) + "Will not label volume ${label} with filesystem type ${fsType}. Open an issue on the microvm.nix project to request a fix." + null + + ) + else + null; + labelArgument = lib.optionalString ( + labelOption != null && label != null + ) "${labelOption} '${label}'"; mkfsExtraArgsString = if mkfsExtraArgs != null then lib.escapeShellArgs mkfsExtraArgs else " "; in - lib.optionalString autoCreate '' - if [ ! -e '${image}' ]; then - touch '${image}' - # Mark NOCOW - chattr +C '${image}' || true - truncate -s ${toString size}M '${image}' - mkfs.${fsType} ${labelArgument} ${mkfsExtraArgsString} '${image}' - fi - '' + lib.optionalString autoCreate '' + if [ ! -e '${image}' ]; then + touch '${image}' + # Mark NOCOW + chattr +C '${image}' || true + truncate -s ${toString size}M '${image}' + mkfs.${fsType} ${labelArgument} ${mkfsExtraArgsString} '${image}' + fi + '' ) ) volumes ); diff --git a/subrepos/microvm.nix/nixos-modules/host.nix b/subrepos/microvm.nix/nixos-modules/host.nix index 3e61aff1..787a197c 100644 --- a/subrepos/microvm.nix/nixos-modules/host.nix +++ b/subrepos/microvm.nix/nixos-modules/host.nix @@ -2,10 +2,11 @@ { lib, ... }: -lib.warn '' - microvm.nix/nixos-modules/host.nix has moved to - microvm.nix/nixos-modules/host -- please update. -'' -{ - imports = [ ./host ]; -} +lib.warn + '' + microvm.nix/nixos-modules/host.nix has moved to + microvm.nix/nixos-modules/host -- please update. + '' + { + imports = [ ./host ]; + } diff --git a/subrepos/microvm.nix/nixos-modules/host/default.nix b/subrepos/microvm.nix/nixos-modules/host/default.nix index e6689306..04995275 100644 --- a/subrepos/microvm.nix/nixos-modules/host/default.nix +++ b/subrepos/microvm.nix/nixos-modules/host/default.nix @@ -18,11 +18,13 @@ in config = lib.mkIf config.microvm.host.enable { assertions = lib.concatMap (vmName: [ { - assertion = config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.flake == null; + assertion = + config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.flake == null; message = "vm ${vmName}: Fully-declarative VMs cannot also set a flake!"; } { - assertion = config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.updateFlake == null; + assertion = + config.microvm.vms.${vmName}.config != null -> config.microvm.vms.${vmName}.updateFlake == null; message = "vm ${vmName}: Fully-declarative VMs cannot set a updateFlake!"; } ]) (builtins.attrNames config.microvm.vms); @@ -49,244 +51,268 @@ in inherit group; }; - security.pam.loginLimits = [ { - domain = user; - item = "memlock"; - type = "hard"; - value = "infinity"; - } { - domain = user; - item = "memlock"; - type = "soft"; - value = "infinity"; - } ]; - - systemd.services = builtins.foldl' (result: name: result // ( - let - microvmConfig = config.microvm.vms.${name}; - inherit (microvmConfig) flake updateFlake; - isFlake = flake != null; - guestConfig = if isFlake - then flake.nixosConfigurations.${name}.config - else if microvmConfig.evaluatedConfig != null - then microvmConfig.evaluatedConfig.config - else microvmConfig.config.config; - runner = guestConfig.microvm.declaredRunner; - in - { - "install-microvm-${name}" = { - description = "Install MicroVM '${name}'"; - before = [ - "microvm@${name}.service" - "microvm-tap-interfaces@${name}.service" - "microvm-macvtap-interfaces@${name}.service" - "microvm-pci-devices@${name}.service" - "microvm-virtiofsd@${name}.service" - "microvm-set-booted@${name}.service" - ]; - partOf = [ "microvm@${name}.service" ]; - wantedBy = [ "microvms.target" ]; - # Run on every rebuild for fully-declarative MicroVMs and flake-based MicroVMs without updateFlake. - # For MicroVMs with updateFlake set, only run on initial installation. - unitConfig.ConditionPathExists = lib.mkIf (isFlake && updateFlake != null) "!${stateDir}/${name}"; - serviceConfig.Type = "oneshot"; - script = '' - mkdir -p ${stateDir}/${name} - cd ${stateDir}/${name} + security.pam.loginLimits = [ + { + domain = user; + item = "memlock"; + type = "hard"; + value = "infinity"; + } + { + domain = user; + item = "memlock"; + type = "soft"; + value = "infinity"; + } + ]; - ln -sTf ${runner} current - chown -h ${user}:${group} . current - '' - # Including the toplevel here is crucial to have the service definition - # change when the host is rebuilt and the vm definition changed. - + lib.optionalString (!isFlake) '' - ln -sTf ${guestConfig.system.build.toplevel} toplevel - '' - # Declarative deployment requires storing just the flake - + lib.optionalString isFlake '' - echo '${if updateFlake != null - then updateFlake - else flake}' > flake - chown -h ${user}:${group} flake - ''; - serviceConfig.SyslogIdentifier = "install-microvm-${name}"; - }; - "microvm@${name}" = { - # restartIfChanged is opt-out, so we have to include the definition unconditionally - serviceConfig.X-RestartIfChanged = [ "" microvmConfig.restartIfChanged ]; - path = lib.mkForce []; - # If the given declarative microvm wants to be restarted on change, - # We have to make sure this service group is restarted. To make sure - # that this service is also changed when the microvm configuration changes, - # we also have to include a trigger here. - restartTriggers = [guestConfig.system.build.toplevel]; - overrideStrategy = "asDropin"; - serviceConfig.Type = - if guestConfig.microvm.declaredRunner.supportsNotifySocket - then "notify" - else "simple"; - # Register with systemd-machined if the VM opts in - wants = lib.optionals runner.registerWithMachined [ - "systemd-machined.service" - ]; - after = lib.optionals runner.registerWithMachined [ - "systemd-machined.service" - ]; - }; - "microvm-tap-interfaces@${name}" = { - serviceConfig.X-RestartIfChanged = [ "" microvmConfig.restartIfChanged ]; - path = lib.mkForce []; - overrideStrategy = "asDropin"; - }; - "microvm-pci-devices@${name}" = { - serviceConfig.X-RestartIfChanged = [ "" microvmConfig.restartIfChanged ]; - path = lib.mkForce []; - overrideStrategy = "asDropin"; - }; - "microvm-virtiofsd@${name}" = { - serviceConfig.X-RestartIfChanged = [ "" microvmConfig.restartIfChanged ]; - path = lib.mkForce []; - overrideStrategy = "asDropin"; - }; - })) { - "microvm-tap-interfaces@" = { - description = "Setup MicroVM '%i' TAP interfaces"; - before = [ "microvm@%i.service" ]; - partOf = [ "microvm@%i.service" ]; - after = [ "network.target" "microvm-set-booted@%i.service" ]; - unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/tap-up"; - restartIfChanged = false; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "microvm-tap-interfaces@%i"; - ExecStart = "${stateDir}/%i/current/bin/tap-up"; - ExecStop = "${stateDir}/%i/booted/bin/tap-down"; - }; - }; + systemd.services = + builtins.foldl' + ( + result: name: + result + // ( + let + microvmConfig = config.microvm.vms.${name}; + inherit (microvmConfig) flake updateFlake; + isFlake = flake != null; + guestConfig = + if isFlake then + flake.nixosConfigurations.${name}.config + else if microvmConfig.evaluatedConfig != null then + microvmConfig.evaluatedConfig.config + else + microvmConfig.config.config; + runner = guestConfig.microvm.declaredRunner; + in + { + "install-microvm-${name}" = { + description = "Install MicroVM '${name}'"; + before = [ + "microvm@${name}.service" + "microvm-tap-interfaces@${name}.service" + "microvm-macvtap-interfaces@${name}.service" + "microvm-pci-devices@${name}.service" + "microvm-virtiofsd@${name}.service" + "microvm-set-booted@${name}.service" + ]; + partOf = [ "microvm@${name}.service" ]; + wantedBy = [ "microvms.target" ]; + # Run on every rebuild for fully-declarative MicroVMs and flake-based MicroVMs without updateFlake. + # For MicroVMs with updateFlake set, only run on initial installation. + unitConfig.ConditionPathExists = lib.mkIf (isFlake && updateFlake != null) "!${stateDir}/${name}"; + serviceConfig.Type = "oneshot"; + script = '' + mkdir -p ${stateDir}/${name} + cd ${stateDir}/${name} - "microvm-macvtap-interfaces@" = { - description = "Setup MicroVM '%i' MACVTAP interfaces"; - before = [ "microvm@%i.service" ]; - after = [ "microvm-set-booted@%i.service" ]; - partOf = [ "microvm@%i.service" ]; - unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/macvtap-up"; - restartIfChanged = false; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "microvm-macvtap-interfaces@%i"; - ExecStart = "${stateDir}/%i/current/bin/macvtap-up"; - ExecStop = "${stateDir}/%i/booted/bin/macvtap-down"; - }; - }; + ln -sTf ${runner} current + chown -h ${user}:${group} . current + '' + # Including the toplevel here is crucial to have the service definition + # change when the host is rebuilt and the vm definition changed. + + lib.optionalString (!isFlake) '' + ln -sTf ${guestConfig.system.build.toplevel} toplevel + '' + # Declarative deployment requires storing just the flake + + lib.optionalString isFlake '' + echo '${if updateFlake != null then updateFlake else flake}' > flake + chown -h ${user}:${group} flake + ''; + serviceConfig.SyslogIdentifier = "install-microvm-${name}"; + }; + "microvm@${name}" = { + # restartIfChanged is opt-out, so we have to include the definition unconditionally + serviceConfig.X-RestartIfChanged = [ + "" + microvmConfig.restartIfChanged + ]; + path = lib.mkForce [ ]; + # If the given declarative microvm wants to be restarted on change, + # We have to make sure this service group is restarted. To make sure + # that this service is also changed when the microvm configuration changes, + # we also have to include a trigger here. + restartTriggers = [ guestConfig.system.build.toplevel ]; + overrideStrategy = "asDropin"; + serviceConfig.Type = + if guestConfig.microvm.declaredRunner.supportsNotifySocket then "notify" else "simple"; + # Register with systemd-machined if the VM opts in + wants = lib.optionals runner.registerWithMachined [ + "systemd-machined.service" + ]; + after = lib.optionals runner.registerWithMachined [ + "systemd-machined.service" + ]; + }; + "microvm-tap-interfaces@${name}" = { + serviceConfig.X-RestartIfChanged = [ + "" + microvmConfig.restartIfChanged + ]; + path = lib.mkForce [ ]; + overrideStrategy = "asDropin"; + }; + "microvm-pci-devices@${name}" = { + serviceConfig.X-RestartIfChanged = [ + "" + microvmConfig.restartIfChanged + ]; + path = lib.mkForce [ ]; + overrideStrategy = "asDropin"; + }; + "microvm-virtiofsd@${name}" = { + serviceConfig.X-RestartIfChanged = [ + "" + microvmConfig.restartIfChanged + ]; + path = lib.mkForce [ ]; + overrideStrategy = "asDropin"; + }; + } + ) + ) + { + "microvm-tap-interfaces@" = { + description = "Setup MicroVM '%i' TAP interfaces"; + before = [ "microvm@%i.service" ]; + partOf = [ "microvm@%i.service" ]; + after = [ + "network.target" + "microvm-set-booted@%i.service" + ]; + unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/tap-up"; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "microvm-tap-interfaces@%i"; + ExecStart = "${stateDir}/%i/current/bin/tap-up"; + ExecStop = "${stateDir}/%i/booted/bin/tap-down"; + }; + }; + "microvm-macvtap-interfaces@" = { + description = "Setup MicroVM '%i' MACVTAP interfaces"; + before = [ "microvm@%i.service" ]; + after = [ "microvm-set-booted@%i.service" ]; + partOf = [ "microvm@%i.service" ]; + unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/macvtap-up"; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "microvm-macvtap-interfaces@%i"; + ExecStart = "${stateDir}/%i/current/bin/macvtap-up"; + ExecStop = "${stateDir}/%i/booted/bin/macvtap-down"; + }; + }; - "microvm-pci-devices@" = { - description = "Setup MicroVM '%i' devices for passthrough"; - before = [ "microvm@%i.service" ]; - partOf = [ "microvm@%i.service" ]; - unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/pci-setup"; - restartIfChanged = false; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "microvm-pci-devices@%i"; - ExecStart = "${stateDir}/%i/current/bin/pci-setup"; - }; - }; + "microvm-pci-devices@" = { + description = "Setup MicroVM '%i' devices for passthrough"; + before = [ "microvm@%i.service" ]; + partOf = [ "microvm@%i.service" ]; + unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/pci-setup"; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "microvm-pci-devices@%i"; + ExecStart = "${stateDir}/%i/current/bin/pci-setup"; + }; + }; - "microvm-virtiofsd@" = { - description = "VirtioFS daemons for MicroVM '%i'"; - before = [ "microvm@%i.service" ]; - after = [ "local-fs.target" "microvm-set-booted@%i.service" ]; - partOf = [ "microvm@%i.service" ]; - unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/virtiofsd-run"; - restartIfChanged = false; - serviceConfig = { - WorkingDirectory = "${stateDir}/%i"; - ExecStart = "${stateDir}/%i/current/bin/virtiofsd-run"; - LimitNOFILE = 1048576; - NotifyAccess = "all"; - PrivateTmp = "yes"; - Restart = "always"; - RestartSec = "5s"; - SyslogIdentifier = "microvm-virtiofsd@%i"; - Type = "notify"; - KillMode = "mixed"; + "microvm-virtiofsd@" = { + description = "VirtioFS daemons for MicroVM '%i'"; + before = [ "microvm@%i.service" ]; + after = [ + "local-fs.target" + "microvm-set-booted@%i.service" + ]; + partOf = [ "microvm@%i.service" ]; + unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/virtiofsd-run"; + restartIfChanged = false; + serviceConfig = { + WorkingDirectory = "${stateDir}/%i"; + ExecStart = "${stateDir}/%i/current/bin/virtiofsd-run"; + LimitNOFILE = 1048576; + NotifyAccess = "all"; + PrivateTmp = "yes"; + Restart = "always"; + RestartSec = "5s"; + SyslogIdentifier = "microvm-virtiofsd@%i"; + Type = "notify"; + KillMode = "mixed"; + }; }; - }; - "microvm-set-booted@" = { - description = "Save MicroVM '%i' booted configuration"; - before = [ "microvm@%i.service" ]; - partOf = [ "microvm@%i.service" ]; - restartIfChanged = false; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "microvm-set-booted@%i"; - WorkingDirectory = "${stateDir}/%i"; - User = user; - Group = group; - ExecStop = "${lib.getExe' pkgs.coreutils "rm"} booted"; - }; - script = '' - rm -f booted - ln -s $(readlink current) booted - ''; - }; + "microvm-set-booted@" = { + description = "Save MicroVM '%i' booted configuration"; + before = [ "microvm@%i.service" ]; + partOf = [ "microvm@%i.service" ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "microvm-set-booted@%i"; + WorkingDirectory = "${stateDir}/%i"; + User = user; + Group = group; + ExecStop = "${lib.getExe' pkgs.coreutils "rm"} booted"; + }; + script = '' + rm -f booted + ln -s $(readlink current) booted + ''; + }; - "microvm@" = { - description = "MicroVM '%i'"; - requires = [ - "microvm-tap-interfaces@%i.service" - "microvm-macvtap-interfaces@%i.service" - "microvm-pci-devices@%i.service" - "microvm-virtiofsd@%i.service" - "microvm-set-booted@%i.service" - ]; - after = [ - "network.target" - "systemd-modules-load.service" - "microvm-tap-interfaces@%i.service" - "microvm-macvtap-interfaces@%i.service" - "microvm-pci-devices@%i.service" - "microvm-virtiofsd@%i.service" - "microvm-set-booted@%i.service" - ]; - unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/microvm-run"; - restartIfChanged = false; - serviceConfig = { - Type = - if config.microvm.host.useNotifySockets - then "notify" - else "simple"; - WorkingDirectory = "${stateDir}/%i"; - ExecStart = "${stateDir}/%i/current/bin/microvm-run"; - ExecStop = "${stateDir}/%i/booted/bin/microvm-shutdown"; - ExecStartPost = [ - "+${pkgs.runtimeShell} -c 'if [ -x ${stateDir}/%i/current/bin/microvm-register ]; then ${stateDir}/%i/current/bin/microvm-register $MAINPID; fi'" - ]; - ExecStopPost = [ - "+${pkgs.runtimeShell} -c 'if [ -x ${stateDir}/%i/current/bin/microvm-unregister ]; then ${stateDir}/%i/current/bin/microvm-unregister; fi'" - ]; - TimeoutSec = config.microvm.host.startupTimeout; - Restart = "always"; - RestartSec = "5s"; - User = user; - Group = group; - SyslogIdentifier = "microvm@%i"; - LimitNOFILE = 1048576; - NotifyAccess = "all"; - LimitMEMLOCK = "infinity"; - }; - }; - } (builtins.attrNames config.microvm.vms); + "microvm@" = { + description = "MicroVM '%i'"; + requires = [ + "microvm-tap-interfaces@%i.service" + "microvm-macvtap-interfaces@%i.service" + "microvm-pci-devices@%i.service" + "microvm-virtiofsd@%i.service" + "microvm-set-booted@%i.service" + ]; + after = [ + "network.target" + "systemd-modules-load.service" + "microvm-tap-interfaces@%i.service" + "microvm-macvtap-interfaces@%i.service" + "microvm-pci-devices@%i.service" + "microvm-virtiofsd@%i.service" + "microvm-set-booted@%i.service" + ]; + unitConfig.ConditionPathExists = "${stateDir}/%i/current/bin/microvm-run"; + restartIfChanged = false; + serviceConfig = { + Type = if config.microvm.host.useNotifySockets then "notify" else "simple"; + WorkingDirectory = "${stateDir}/%i"; + ExecStart = "${stateDir}/%i/current/bin/microvm-run"; + ExecStop = "${stateDir}/%i/booted/bin/microvm-shutdown"; + ExecStartPost = [ + "+${pkgs.runtimeShell} -c 'if [ -x ${stateDir}/%i/current/bin/microvm-register ]; then ${stateDir}/%i/current/bin/microvm-register $MAINPID; fi'" + ]; + ExecStopPost = [ + "+${pkgs.runtimeShell} -c 'if [ -x ${stateDir}/%i/current/bin/microvm-unregister ]; then ${stateDir}/%i/current/bin/microvm-unregister; fi'" + ]; + TimeoutSec = config.microvm.host.startupTimeout; + Restart = "always"; + RestartSec = "5s"; + User = user; + Group = group; + SyslogIdentifier = "microvm@%i"; + LimitNOFILE = 1048576; + NotifyAccess = "all"; + LimitMEMLOCK = "infinity"; + }; + }; + } + (builtins.attrNames config.microvm.vms); - microvm.autostart = builtins.filter (vmName: - config.microvm.vms.${vmName}.autostart - ) (builtins.attrNames config.microvm.vms); + microvm.autostart = builtins.filter (vmName: config.microvm.vms.${vmName}.autostart) ( + builtins.attrNames config.microvm.vms + ); # Starts all the containers after boot systemd.targets.microvms = { wantedBy = [ "multi-user.target" ]; diff --git a/subrepos/microvm.nix/nixos-modules/host/options.nix b/subrepos/microvm.nix/nixos-modules/host/options.nix index 89c9835c..639a5d88 100644 --- a/subrepos/microvm.nix/nixos-modules/host/options.nix +++ b/subrepos/microvm.nix/nixos-modules/host/options.nix @@ -28,139 +28,160 @@ }; vms = mkOption { - type = with types; attrsOf (submodule ({ config, name, ... }: { - options = { - evaluatedConfig = mkOption { - description = '' - An already evaluated configuration of this MicroVM. - Allows supplying an already evaluated configuration or an alternative configuration evaluation function instead of NixOS's default eval-config. - ''; - default = null; - type = nullOr types.unspecified; - }; - - config = mkOption { - description = '' - A specification of the desired configuration of this MicroVM, - as a NixOS module, for building **without** a flake. - ''; - default = null; - type = nullOr (lib.mkOptionType { - name = "Toplevel NixOS config"; - merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" { - modules = - let - extraConfig = ({ lib, ... }: { - _file = "module at ${__curPos.file}:${toString __curPos.line}"; - config = { - networking.hostName = lib.mkDefault name; - }; - }); - in [ - extraConfig - ../microvm - ] ++ (map (x: x.value) defs); - prefix = [ "microvm" "vms" name "config" ]; - inherit (config) extraModules specialArgs pkgs; - system = - if config.pkgs != null then - config.pkgs.stdenv.hostPlatform.system - else - pkgs.stdenv.hostPlatform.system; - }); - }); - }; - - nixpkgs = mkOption { - type = types.path; - default = if config.pkgs != null then config.pkgs.path else pkgs.path; - defaultText = literalExpression "pkgs.path"; - description = '' - This option is only respected when `config` is - specified. - - The nixpkgs path to use for the MicroVM. Defaults to the - host's nixpkgs. - ''; - }; - - pkgs = mkOption { - type = types.nullOr types.unspecified; - default = pkgs; - defaultText = literalExpression "pkgs"; - description = '' - This option is only respected when `config` is specified. - - The package set to use for the MicroVM. Must be a - nixpkgs package set with the microvm overlay. Determines - the system of the MicroVM. - - If set to null, a new package set will be instantiated. - ''; - }; - - specialArgs = mkOption { - type = types.attrsOf types.unspecified; - default = {}; - description = '' - This option is only respected when `config` is specified. - - A set of special arguments to be passed to NixOS modules. - This will be merged into the `specialArgs` used to evaluate - the NixOS configurations. - ''; - }; - - extraModules = mkOption { - type = types.listOf types.deferredModule; - default = []; - description = '' - This option is only respected when `config` is specified. - - A list of additional NixOS modules to be merged into - the MicroVM's system configuration. - ''; - defaultText = literalExpression '' - [ - flakeInputs.some-project.nixosModules.example - flakeInputs.another-project.nixosModules.default - ] - ''; - }; - - flake = mkOption { - description = "Source flake for declarative build"; - type = nullOr path; - default = null; - defaultText = literalExpression ''flakeInputs.my-infra''; - }; - - updateFlake = mkOption { - description = "Source flakeref to store for later imperative update"; - type = nullOr str; - default = null; - defaultText = literalExpression ''"git+file:///home/user/my-infra"''; - }; - - autostart = mkOption { - description = "Add this MicroVM to config.microvm.autostart?"; - type = bool; - default = true; - }; - - restartIfChanged = mkOption { - type = types.bool; - default = config.config != null; - description = '' - Restart this MicroVM's services if the systemd units are changed, - i.e. if it has been updated by rebuilding the host. - - Defaults to true for fully-declarative MicroVMs. - ''; - }; - }; - })); - default = {}; + type = + with types; + attrsOf ( + submodule ( + { config, name, ... }: + { + options = { + evaluatedConfig = mkOption { + description = '' + An already evaluated configuration of this MicroVM. + Allows supplying an already evaluated configuration or an alternative configuration evaluation function instead of NixOS's default eval-config. + ''; + default = null; + type = nullOr types.unspecified; + }; + + config = mkOption { + description = '' + A specification of the desired configuration of this MicroVM, + as a NixOS module, for building **without** a flake. + ''; + default = null; + type = nullOr ( + lib.mkOptionType { + name = "Toplevel NixOS config"; + merge = + loc: defs: + (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" { + modules = + let + extraConfig = ( + { lib, ... }: + { + _file = "module at ${__curPos.file}:${toString __curPos.line}"; + config = { + networking.hostName = lib.mkDefault name; + }; + } + ); + in + [ + extraConfig + ../microvm + ] + ++ (map (x: x.value) defs); + prefix = [ + "microvm" + "vms" + name + "config" + ]; + inherit (config) extraModules specialArgs pkgs; + system = + if config.pkgs != null then + config.pkgs.stdenv.hostPlatform.system + else + pkgs.stdenv.hostPlatform.system; + }); + } + ); + }; + + nixpkgs = mkOption { + type = types.path; + default = if config.pkgs != null then config.pkgs.path else pkgs.path; + defaultText = literalExpression "pkgs.path"; + description = '' + This option is only respected when `config` is + specified. + + The nixpkgs path to use for the MicroVM. Defaults to the + host's nixpkgs. + ''; + }; + + pkgs = mkOption { + type = types.nullOr types.unspecified; + default = pkgs; + defaultText = literalExpression "pkgs"; + description = '' + This option is only respected when `config` is specified. + + The package set to use for the MicroVM. Must be a + nixpkgs package set with the microvm overlay. Determines + the system of the MicroVM. + + If set to null, a new package set will be instantiated. + ''; + }; + + specialArgs = mkOption { + type = types.attrsOf types.unspecified; + default = { }; + description = '' + This option is only respected when `config` is specified. + + A set of special arguments to be passed to NixOS modules. + This will be merged into the `specialArgs` used to evaluate + the NixOS configurations. + ''; + }; + + extraModules = mkOption { + type = types.listOf types.deferredModule; + default = [ ]; + description = '' + This option is only respected when `config` is specified. + + A list of additional NixOS modules to be merged into + the MicroVM's system configuration. + ''; + defaultText = literalExpression '' + [ + flakeInputs.some-project.nixosModules.example + flakeInputs.another-project.nixosModules.default + ] + ''; + }; + + flake = mkOption { + description = "Source flake for declarative build"; + type = nullOr path; + default = null; + defaultText = literalExpression "flakeInputs.my-infra"; + }; + + updateFlake = mkOption { + description = "Source flakeref to store for later imperative update"; + type = nullOr str; + default = null; + defaultText = literalExpression ''"git+file:///home/user/my-infra"''; + }; + + autostart = mkOption { + description = "Add this MicroVM to config.microvm.autostart?"; + type = bool; + default = true; + }; + + restartIfChanged = mkOption { + type = types.bool; + default = config.config != null; + description = '' + Restart this MicroVM's services if the systemd units are changed, + i.e. if it has been updated by rebuilding the host. + + Defaults to true for fully-declarative MicroVMs. + ''; + }; + }; + } + ) + ); + default = { }; description = '' The MicroVMs that shall be built declaratively with the host NixOS. ''; @@ -176,7 +197,7 @@ autostart = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = '' MicroVMs to start by default. diff --git a/subrepos/microvm.nix/nixos-modules/microvm/asserts.nix b/subrepos/microvm.nix/nixos-modules/microvm/asserts.nix index f138f94a..93b5a23a 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/asserts.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/asserts.nix @@ -11,110 +11,114 @@ lib.mkIf config.microvm.guest.enable { message = '' MicroVM ${hostName}: volume image "${(builtins.head volumes).image}" is used ${toString (builtins.length volumes)} > 1 times. ''; - }) ( - builtins.attrValues ( - builtins.groupBy ({ image, ... }: image) config.microvm.volumes - ) - ) + }) (builtins.attrValues (builtins.groupBy ({ image, ... }: image) config.microvm.volumes)) ++ - # check for duplicate interface ids - map (interfaces: { - assertion = builtins.length interfaces == 1; - message = '' - MicroVM ${hostName}: interface id "${(builtins.head interfaces).id}" is used ${toString (builtins.length interfaces)} > 1 times. - ''; - }) ( - builtins.attrValues ( - builtins.groupBy ({ id, ... }: id) config.microvm.interfaces - ) - ) - ++ - # check for bridge interfaces - map ({ id, type, bridge, ... }: - if type == "bridge" - then { - assertion = bridge != null; + # check for duplicate interface ids + map (interfaces: { + assertion = builtins.length interfaces == 1; message = '' - MicroVM ${hostName}: interface ${id} is of type "bridge" - but doesn't have a bridge to attach to defined. + MicroVM ${hostName}: interface id "${(builtins.head interfaces).id}" is used ${toString (builtins.length interfaces)} > 1 times. ''; - } - else { - assertion = bridge == null; - message = '' - MicroVM ${hostName}: interface ${id} is not of type "bridge" - and therefore shouldn't have a "bridge" option defined. - ''; - } - ) config.microvm.interfaces + }) (builtins.attrValues (builtins.groupBy ({ id, ... }: id) config.microvm.interfaces)) ++ - # check for interface name length - map ({ id, ... }: { - assertion = builtins.stringLength id <= 15; - message = '' - MicroVM ${hostName}: interface name ${id} is longer than the - the maximum length of 15 characters on Linux. - ''; - }) config.microvm.interfaces + # check for bridge interfaces + map ( + { + id, + type, + bridge, + ... + }: + if type == "bridge" then + { + assertion = bridge != null; + message = '' + MicroVM ${hostName}: interface ${id} is of type "bridge" + but doesn't have a bridge to attach to defined. + ''; + } + else + { + assertion = bridge == null; + message = '' + MicroVM ${hostName}: interface ${id} is not of type "bridge" + and therefore shouldn't have a "bridge" option defined. + ''; + } + ) config.microvm.interfaces ++ - # check for duplicate share tags - map (shares: { - assertion = builtins.length shares == 1; - message = '' - MicroVM ${hostName}: share tag "${(builtins.head shares).tag}" is used ${toString (builtins.length shares)} > 1 times. - ''; - }) ( - builtins.attrValues ( - builtins.groupBy ({ tag, ... }: tag) config.microvm.shares - ) - ) + # check for interface name length + map ( + { id, ... }: + { + assertion = builtins.stringLength id <= 15; + message = '' + MicroVM ${hostName}: interface name ${id} is longer than the + the maximum length of 15 characters on Linux. + ''; + } + ) config.microvm.interfaces ++ - # check for duplicate share sockets - map (shares: { - assertion = builtins.length shares == 1; - message = '' - MicroVM ${hostName}: share socket "${(builtins.head shares).socket}" is used ${toString (builtins.length shares)} > 1 times. - ''; - }) ( - builtins.attrValues ( - builtins.groupBy ({ socket, ... }: toString socket) ( - builtins.filter ({ proto, ... }: proto == "virtiofs") - config.microvm.shares + # check for duplicate share tags + map (shares: { + assertion = builtins.length shares == 1; + message = '' + MicroVM ${hostName}: share tag "${(builtins.head shares).tag}" is used ${toString (builtins.length shares)} > 1 times. + ''; + }) (builtins.attrValues (builtins.groupBy ({ tag, ... }: tag) config.microvm.shares)) + ++ + # check for duplicate share sockets + map + (shares: { + assertion = builtins.length shares == 1; + message = '' + MicroVM ${hostName}: share socket "${(builtins.head shares).socket}" is used ${toString (builtins.length shares)} > 1 times. + ''; + }) + ( + builtins.attrValues ( + builtins.groupBy ({ socket, ... }: toString socket) ( + builtins.filter ({ proto, ... }: proto == "virtiofs") config.microvm.shares + ) + ) ) - ) - ) ++ - # check for virtiofs shares without socket - map ({ tag, socket, ... }: { - assertion = socket != null; - message = '' - MicroVM ${hostName}: virtiofs share with tag "${tag}" is missing a `socket` path. - ''; - }) ( - builtins.filter ({ proto, ... }: proto == "virtiofs") - config.microvm.shares - ) + # check for virtiofs shares without socket + map ( + { tag, socket, ... }: + { + assertion = socket != null; + message = '' + MicroVM ${hostName}: virtiofs share with tag "${tag}" is missing a `socket` path. + ''; + } + ) (builtins.filter ({ proto, ... }: proto == "virtiofs") config.microvm.shares) ++ - # blacklist forwardPorts - [ { - assertion = - config.microvm.forwardPorts != [] -> ( - config.microvm.hypervisor == "qemu" && - builtins.any ({ type, ... }: type == "user") config.microvm.interfaces - ); - message = '' - MicroVM ${hostName}: `config.microvm.forwardPorts` works only with qemu and one network interface with `type = "user"` - ''; - } ] + # blacklist forwardPorts + [ + { + assertion = + config.microvm.forwardPorts != [ ] + -> ( + config.microvm.hypervisor == "qemu" + && builtins.any ({ type, ... }: type == "user") config.microvm.interfaces + ); + message = '' + MicroVM ${hostName}: `config.microvm.forwardPorts` works only with qemu and one network interface with `type = "user"` + ''; + } + ] ++ - # cloud-hypervisor specific asserts - lib.optionals (config.microvm.hypervisor == "cloud-hypervisor") [ { - assertion = ! (lib.any (str: lib.hasInfix "oem_strings" str) config.microvm.cloud-hypervisor.platformOEMStrings); - message = '' - MicroVM ${hostName}: `config.microvm.cloud-hypervisor.platformOEMStrings` items must not contain `oem_strings` - ''; - } ]; - + # cloud-hypervisor specific asserts + lib.optionals (config.microvm.hypervisor == "cloud-hypervisor") [ + { + assertion = + !(lib.any (str: lib.hasInfix "oem_strings" str) config.microvm.cloud-hypervisor.platformOEMStrings); + message = '' + MicroVM ${hostName}: `config.microvm.cloud-hypervisor.platformOEMStrings` items must not contain `oem_strings` + ''; + } + ]; warnings = # 32 MB is just an optimistic guess, not based on experience diff --git a/subrepos/microvm.nix/nixos-modules/microvm/boot-disk.nix b/subrepos/microvm.nix/nixos-modules/microvm/boot-disk.nix index 3be3cabc..ec3afa6e 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/boot-disk.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/boot-disk.nix @@ -1,13 +1,18 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let inherit (config.system.boot.loader) kernelFile; inherit (config.microvm) initrdPath; - kernelPath = - "${config.microvm.kernel}/${kernelFile}"; + kernelPath = "${config.microvm.kernel}/${kernelFile}"; -in { +in +{ options.microvm = with lib; { bootDisk = mkOption { type = types.path; @@ -21,41 +26,44 @@ in { }; config = lib.mkIf config.microvm.guest.enable { - microvm.bootDisk = pkgs.buildPackages.runCommandLocal "microvm-bootdisk.img" { - nativeBuildInputs = with pkgs.buildPackages; [ - parted - libguestfs - ]; - LIBGUESTFS_PATH = pkgs.buildPackages.libguestfs-appliance; - } '' - # kernel + initrd + slack, in sectors - EFI_SIZE=$(( ( ( $(stat -c %s ${kernelPath}) + $(stat -c %s ${initrdPath}) + 16 * 4096 ) / ( 2048 * 512 ) + 1 ) * 2048 )) + microvm.bootDisk = + pkgs.buildPackages.runCommandLocal "microvm-bootdisk.img" + { + nativeBuildInputs = with pkgs.buildPackages; [ + parted + libguestfs + ]; + LIBGUESTFS_PATH = pkgs.buildPackages.libguestfs-appliance; + } + '' + # kernel + initrd + slack, in sectors + EFI_SIZE=$(( ( ( $(stat -c %s ${kernelPath}) + $(stat -c %s ${initrdPath}) + 16 * 4096 ) / ( 2048 * 512 ) + 1 ) * 2048 )) - truncate -s $(( ( $EFI_SIZE + 2048 + 33 ) * 512 )) $out - echo Creating partition table - parted --script $out -- \ - mklabel gpt \ - mkpart ESP fat32 2048s $(( $EFI_SIZE + 2048 - 1 ))"s" \ - set 1 boot on + truncate -s $(( ( $EFI_SIZE + 2048 + 33 ) * 512 )) $out + echo Creating partition table + parted --script $out -- \ + mklabel gpt \ + mkpart ESP fat32 2048s $(( $EFI_SIZE + 2048 - 1 ))"s" \ + set 1 boot on - echo Creating EFI partition - export HOME=`pwd` - guestfish --add $out run \: mkfs fat /dev/sda1 - guestfs() { - guestfish --add $out --mount /dev/sda1:/ $@ - } - guestfs mkdir /loader - echo 'default *.conf' > loader.conf - guestfs copy-in loader.conf /loader/ - guestfs mkdir /loader/entries - cat > entry.conf < loader.conf + guestfs copy-in loader.conf /loader/ + guestfs mkdir /loader/entries + cat > entry.conf < "/proc/sys/net/ipv6/conf/${id}/disable_ipv6" - fi - ${lib.getExe' pkgs.iproute2 "ip"} link set '${id}' up - ${pkgs.coreutils-full}/bin/chown '${user}:${group}' /dev/tap$(< "/sys/class/net/${id}/ifindex") - '') macvtapInterfaces; + '' + + lib.concatMapStrings ( + { + id, + mac, + macvtap, + ... + }: + '' + if [ -e /sys/class/net/${id} ]; then + ${lib.getExe' pkgs.iproute2 "ip"} link delete '${id}' + fi + ${lib.getExe' pkgs.iproute2 "ip"} link add link '${macvtap.link}' name '${id}' address '${mac}' type macvtap mode '${macvtap.mode}' + ${lib.getExe' pkgs.iproute2 "ip"} link set '${id}' allmulticast on + if [ -f "/proc/sys/net/ipv6/conf/${id}/disable_ipv6" ]; then + echo 1 > "/proc/sys/net/ipv6/conf/${id}/disable_ipv6" + fi + ${lib.getExe' pkgs.iproute2 "ip"} link set '${id}' up + ${pkgs.coreutils-full}/bin/chown '${user}:${group}' /dev/tap$(< "/sys/class/net/${id}/ifindex") + '' + ) macvtapInterfaces; macvtap-down = '' set -ou pipefail - '' + lib.concatMapStrings ({ id, ... }: '' - ${lib.getExe' pkgs.iproute2 "ip"} link delete '${id}' - '') macvtapInterfaces; - } - ) ]; + '' + + lib.concatMapStrings ( + { id, ... }: + '' + ${lib.getExe' pkgs.iproute2 "ip"} link delete '${id}' + '' + ) macvtapInterfaces; + }) + ]; } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/mounts.nix b/subrepos/microvm.nix/nixos-modules/microvm/mounts.nix index b9fb462a..06ca64ae 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/mounts.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/mounts.nix @@ -3,139 +3,182 @@ let inherit (config.microvm) storeDiskType storeOnDisk writableStoreOverlay; - inherit (import ../../lib { - inherit lib; - }) defaultFsType withDriveLetters; + inherit + (import ../../lib { + inherit lib; + }) + defaultFsType + withDriveLetters + ; hostStore = builtins.head ( - builtins.filter ({ source, ... }: - source == "/nix/store" - ) config.microvm.shares + builtins.filter ({ source, ... }: source == "/nix/store") config.microvm.shares ); - roStore = - if storeOnDisk - then "/nix/.ro-store" - else hostStore.mountPoint; + roStore = if storeOnDisk then "/nix/.ro-store" else hostStore.mountPoint; roStoreDisk = - if storeOnDisk - then - if storeDiskType == "erofs" + if storeOnDisk then + if + storeDiskType == "erofs" # erofs supports filesystem labels - then "/dev/disk/by-label/nix-store" - else "/dev/vda" - else throw "No disk letter when /nix/store is not in disk"; + then + "/dev/disk/by-label/nix-store" + else + "/dev/vda" + else + throw "No disk letter when /nix/store is not in disk"; # Check if the writable store overlay is a virtiofs share - isRwStoreVirtiofsShare = builtins.any ({mountPoint, proto, ... }: - mountPoint == config.microvm.writableStoreOverlay - && proto == "virtiofs" + isRwStoreVirtiofsShare = builtins.any ( + { mountPoint, proto, ... }: mountPoint == config.microvm.writableStoreOverlay && proto == "virtiofs" ) config.microvm.shares; in lib.mkIf config.microvm.guest.enable { - fileSystems = lib.mkMerge [ ( - # built-in read-only store without overlay - lib.optionalAttrs ( - storeOnDisk && - writableStoreOverlay == null - ) { - "/nix/store" = { - device = roStoreDisk; - fsType = storeDiskType; - options = [ "x-systemd.after=systemd-modules-load.service" ]; - neededForBoot = true; - noCheck = true; - }; - } - ) ( - # host store is mounted somewhere else, - # bind-mount to the proper place - lib.optionalAttrs ( - !storeOnDisk && - config.microvm.writableStoreOverlay == null && - hostStore.mountPoint != "/nix/store" - ) { - "/nix/store" = { - device = hostStore.mountPoint; - fsType = hostStore.proto; - options = [ "ro" "bind" ]; - neededForBoot = true; - }; - } - ) ( - # built-in read-only store for the overlay - lib.optionalAttrs ( - storeOnDisk && - writableStoreOverlay != null - ) { - "/nix/.ro-store" = { - device = roStoreDisk; - fsType = storeDiskType; - options = [ "ro" "x-systemd.after=systemd-modules-load.service" ]; - neededForBoot = true; - noCheck = true; - }; - } - ) ( - # mount store with writable overlay - lib.optionalAttrs (writableStoreOverlay != null) { - "/nix/store" = { - neededForBoot = true; - overlay = { - lowerdir = [ roStore ]; - upperdir = "${writableStoreOverlay}/store"; - workdir = "${writableStoreOverlay}/work"; + fileSystems = lib.mkMerge [ + ( + # built-in read-only store without overlay + lib.optionalAttrs (storeOnDisk && writableStoreOverlay == null) { + "/nix/store" = { + device = roStoreDisk; + fsType = storeDiskType; + options = [ "x-systemd.after=systemd-modules-load.service" ]; + neededForBoot = true; + noCheck = true; }; - options = lib.mkIf isRwStoreVirtiofsShare [ "userxattr" ]; - }; - } - ) { - # a tmpfs / by default. can be overwritten. - "/" = lib.mkDefault { - device = "rootfs"; - fsType = "tmpfs"; - options = [ "size=50%,mode=0755" ]; - neededForBoot = true; - }; - } ( - # Volumes - builtins.foldl' (result: { label, mountPoint, letter, fsType ? defaultFsType, ... }: - result // lib.optionalAttrs (mountPoint != null) { - "${mountPoint}" = { - inherit fsType; - # Prioritize identifying a device by label if provided. This - # minimizes the risk of misidentifying a device. - device = if label != null then - "/dev/disk/by-label/${label}" - else - "/dev/vd${letter}"; - } // lib.optionalAttrs (mountPoint == config.microvm.writableStoreOverlay) { + } + ) + ( + # host store is mounted somewhere else, + # bind-mount to the proper place + lib.optionalAttrs + ( + !storeOnDisk && config.microvm.writableStoreOverlay == null && hostStore.mountPoint != "/nix/store" + ) + { + "/nix/store" = { + device = hostStore.mountPoint; + fsType = hostStore.proto; + options = [ + "ro" + "bind" + ]; + neededForBoot = true; + }; + } + ) + ( + # built-in read-only store for the overlay + lib.optionalAttrs (storeOnDisk && writableStoreOverlay != null) { + "/nix/.ro-store" = { + device = roStoreDisk; + fsType = storeDiskType; + options = [ + "ro" + "x-systemd.after=systemd-modules-load.service" + ]; + neededForBoot = true; + noCheck = true; + }; + } + ) + ( + # mount store with writable overlay + lib.optionalAttrs (writableStoreOverlay != null) { + "/nix/store" = { neededForBoot = true; + overlay = { + lowerdir = [ roStore ]; + upperdir = "${writableStoreOverlay}/store"; + workdir = "${writableStoreOverlay}/work"; + }; + options = lib.mkIf isRwStoreVirtiofsShare [ "userxattr" ]; }; - }) {} (withDriveLetters config.microvm) - ) ( - # 9p/virtiofs Shares - builtins.foldl' (result: { mountPoint, tag, proto, source, ... }: result // { - "${mountPoint}" = { - device = tag; - fsType = proto; - options = { - "virtiofs" = [ "defaults" "x-systemd.after=systemd-modules-load.service" ]; - "9p" = [ "trans=virtio" "version=9p2000.L" "msize=65536" "x-systemd.after=systemd-modules-load.service" ]; - }.${proto}; - } // lib.optionalAttrs (source == "/nix/store" || mountPoint == config.microvm.writableStoreOverlay) { + } + ) + { + # a tmpfs / by default. can be overwritten. + "/" = lib.mkDefault { + device = "rootfs"; + fsType = "tmpfs"; + options = [ "size=50%,mode=0755" ]; neededForBoot = true; }; - }) {} config.microvm.shares - ) ]; + } + ( + # Volumes + builtins.foldl' ( + result: + { + label, + mountPoint, + letter, + fsType ? defaultFsType, + ... + }: + result + // lib.optionalAttrs (mountPoint != null) { + "${mountPoint}" = { + inherit fsType; + # Prioritize identifying a device by label if provided. This + # minimizes the risk of misidentifying a device. + device = if label != null then "/dev/disk/by-label/${label}" else "/dev/vd${letter}"; + } + // lib.optionalAttrs (mountPoint == config.microvm.writableStoreOverlay) { + neededForBoot = true; + }; + } + ) { } (withDriveLetters config.microvm) + ) + ( + # 9p/virtiofs Shares + builtins.foldl' ( + result: + { + mountPoint, + tag, + proto, + source, + ... + }: + result + // { + "${mountPoint}" = { + device = tag; + fsType = proto; + options = + { + "virtiofs" = [ + "defaults" + "x-systemd.after=systemd-modules-load.service" + ]; + "9p" = [ + "trans=virtio" + "version=9p2000.L" + "msize=65536" + "x-systemd.after=systemd-modules-load.service" + ]; + } + .${proto}; + } + // lib.optionalAttrs (source == "/nix/store" || mountPoint == config.microvm.writableStoreOverlay) { + neededForBoot = true; + }; + } + ) { } config.microvm.shares + ) + ]; # Fix unmounting in qemu on shutdown for /nix/store - systemd.mounts = lib.mkIf (config.boot.initrd.systemd.enable && !storeOnDisk && writableStoreOverlay == null) [ { - what = "store"; - where = "/nix/store"; - overrideStrategy = "asDropin"; - unitConfig.DefaultDependencies = false; - } ]; + systemd.mounts = + lib.mkIf (config.boot.initrd.systemd.enable && !storeOnDisk && writableStoreOverlay == null) + [ + { + what = "store"; + where = "/nix/store"; + overrideStrategy = "asDropin"; + unitConfig.DefaultDependencies = false; + } + ]; } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/optimization.nix b/subrepos/microvm.nix/nixos-modules/microvm/optimization.nix index abb605d2..b3dc8468 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/optimization.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/optimization.nix @@ -1,16 +1,19 @@ # Closure size and startup time optimization for disposable use-cases -{ config, options, lib, ... }: +{ + config, + options, + lib, + ... +}: let cfg = config.microvm; - canSwitchViaSsh = - config.services.openssh.enable && - # Is the /nix/store mounted from the host? - builtins.any ({ source, ... }: - source == "/nix/store" - ) config.microvm.shares; + config.services.openssh.enable + && + # Is the /nix/store mounted from the host? + builtins.any ({ source, ... }: source == "/nix/store") config.microvm.shares; in lib.mkIf (cfg.guest.enable && cfg.optimize.enable) { @@ -27,7 +30,8 @@ lib.mkIf (cfg.guest.enable && cfg.optimize.enable) { "cloud-hypervisor" "firecracker" "stratovirt" - ]); + ] + ); tpm2.enable = lib.mkDefault false; }; kernelParams = [ diff --git a/subrepos/microvm.nix/nixos-modules/microvm/options.nix b/subrepos/microvm.nix/nixos-modules/microvm/options.nix index 1798da41..a7d2ce6f 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/options.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/options.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let self-lib = import ../../lib { inherit lib; @@ -173,26 +178,30 @@ in ''; }; - forwardPorts = mkOption { - type = types.listOf - (types.submodule { + type = types.listOf ( + types.submodule { options.from = mkOption { - type = types.enum [ "host" "guest" ]; + type = types.enum [ + "host" + "guest" + ]; default = "host"; - description = - '' - Controls the direction in which the ports are mapped: + description = '' + Controls the direction in which the ports are mapped: - - "host" means traffic from the host ports - is forwarded to the given guest port. + - "host" means traffic from the host ports + is forwarded to the given guest port. - - "guest" means traffic from the guest ports - is forwarded to the given host port. - ''; + - "guest" means traffic from the guest ports + is forwarded to the given host port. + ''; }; options.proto = mkOption { - type = types.enum [ "tcp" "udp" ]; + type = types.enum [ + "tcp" + "udp" + ]; default = "tcp"; description = "The protocol to forward."; }; @@ -214,8 +223,9 @@ in type = types.port; description = "The guest port to be mapped."; }; - }); - default = []; + } + ); + default = [ ]; example = lib.literalExpression /* nix */ '' [ # forward local port 2222 -> 22, to ssh into the VM { from = "host"; host.port = 2222; guest.port = 22; } @@ -227,201 +237,237 @@ in } ] ''; - description = - '' - When using the SLiRP user networking (default), this option allows to - forward ports to/from the host/guest. - - ::: {.warning} - If the NixOS firewall on the virtual machine is enabled, you - also have to open the guest ports to enable the traffic - between host and guest. - ::: - - ::: {.note} - Currently QEMU supports only IPv4 forwarding. - ::: - ''; + description = '' + When using the SLiRP user networking (default), this option allows to + forward ports to/from the host/guest. + + ::: {.warning} + If the NixOS firewall on the virtual machine is enabled, you + also have to open the guest ports to enable the traffic + between host and guest. + ::: + + ::: {.note} + Currently QEMU supports only IPv4 forwarding. + ::: + ''; }; volumes = mkOption { description = "Disk images"; - default = []; - type = with types; listOf (submodule { - options = { - image = mkOption { - type = str; - description = "Path to disk image on the host"; - }; - serial = mkOption { - type = nullOr str; - default = null; - description = "User-configured serial number for the disk"; - }; - direct = mkOption { - type = bool; - default = false; - description = "Whether to set O_DIRECT on the disk."; - }; - readOnly = mkOption { - type = bool; - default = false; - description = "Turn off write access"; - }; - label = mkOption { - type = nullOr str; - default = null; - description = "Label of the volume, if any. Only applicable if `autoCreate` is true; otherwise labeling of the volume must be done manually"; - }; - mountPoint = mkOption { - type = nullOr path; - description = "If and where to mount the volume inside the container"; - }; - size = mkOption { - type = int; - description = "Volume size (in MiB) if created automatically"; - }; - autoCreate = mkOption { - type = bool; - default = true; - description = "Created image on host automatically before start?"; - }; - mkfsExtraArgs = mkOption { - type = listOf str; - default = []; - description = "Set extra Filesystem creation parameters"; - }; - fsType = mkOption { - type = str; - default = "ext4"; - description = "Filesystem for automatic creation and mounting"; - }; - imageType = mkOption { - type = types.enum [ "raw" "qcow2" "vhd" "vhdx" ]; - default = "raw"; - description = '' - Format of the image (only passed to the hypervisor, does not change format of the image created if `autoCreate` is true). + default = [ ]; + type = + with types; + listOf (submodule { + options = { + image = mkOption { + type = str; + description = "Path to disk image on the host"; + }; + serial = mkOption { + type = nullOr str; + default = null; + description = "User-configured serial number for the disk"; + }; + direct = mkOption { + type = bool; + default = false; + description = "Whether to set O_DIRECT on the disk."; + }; + readOnly = mkOption { + type = bool; + default = false; + description = "Turn off write access"; + }; + label = mkOption { + type = nullOr str; + default = null; + description = "Label of the volume, if any. Only applicable if `autoCreate` is true; otherwise labeling of the volume must be done manually"; + }; + mountPoint = mkOption { + type = nullOr path; + description = "If and where to mount the volume inside the container"; + }; + size = mkOption { + type = int; + description = "Volume size (in MiB) if created automatically"; + }; + autoCreate = mkOption { + type = bool; + default = true; + description = "Created image on host automatically before start?"; + }; + mkfsExtraArgs = mkOption { + type = listOf str; + default = [ ]; + description = "Set extra Filesystem creation parameters"; + }; + fsType = mkOption { + type = str; + default = "ext4"; + description = "Filesystem for automatic creation and mounting"; + }; + imageType = mkOption { + type = types.enum [ + "raw" + "qcow2" + "vhd" + "vhdx" + ]; + default = "raw"; + description = '' + Format of the image (only passed to the hypervisor, does not change format of the image created if `autoCreate` is true). - ::: {.note} - Only supported with cloud-hypervisor. - ::: - ''; + ::: {.note} + Only supported with cloud-hypervisor. + ::: + ''; + }; }; - }; - }); + }); }; interfaces = mkOption { description = "Network interfaces"; - default = []; - type = with types; listOf (submodule { - options = { - type = mkOption { - type = enum [ "user" "tap" "macvtap" "bridge" ]; - description = '' - Interface type - ''; - }; - id = mkOption { - type = str; - description = '' - Interface name on the host - ''; - }; - macvtap.link = mkOption { - type = str; - description = '' - Attach network interface to host interface for type = "macvlan" - ''; - }; - macvtap.mode = mkOption { - type = enum ["private" "vepa" "bridge" "passthru" "source"]; - description = '' - The MACVLAN mode to use - ''; - }; - bridge = mkOption { - type = nullOr str; - default = null; - description = '' - Attach network interface to host bridge interface for type = "bridge" - ''; - }; - mac = mkOption { - type = str; - description = '' - MAC address of the guest's network interface - ''; - }; - tap.vhost = mkOption { - type = types.bool; - default = false; - description = '' - Enable vhost-net for TAP interfaces. + default = [ ]; + type = + with types; + listOf (submodule { + options = { + type = mkOption { + type = enum [ + "user" + "tap" + "macvtap" + "bridge" + ]; + description = '' + Interface type + ''; + }; + id = mkOption { + type = str; + description = '' + Interface name on the host + ''; + }; + macvtap.link = mkOption { + type = str; + description = '' + Attach network interface to host interface for type = "macvlan" + ''; + }; + macvtap.mode = mkOption { + type = enum [ + "private" + "vepa" + "bridge" + "passthru" + "source" + ]; + description = '' + The MACVLAN mode to use + ''; + }; + bridge = mkOption { + type = nullOr str; + default = null; + description = '' + Attach network interface to host bridge interface for type = "bridge" + ''; + }; + mac = mkOption { + type = str; + description = '' + MAC address of the guest's network interface + ''; + }; + tap.vhost = mkOption { + type = types.bool; + default = false; + description = '' + Enable vhost-net for TAP interfaces. - When enabled, packet processing is offloaded to the kernel's - vhost-net module instead of QEMU userspace, significantly - improving network throughput (~10 Gbps vs ~1.5 Gbps). + When enabled, packet processing is offloaded to the kernel's + vhost-net module instead of QEMU userspace, significantly + improving network throughput (~10 Gbps vs ~1.5 Gbps). - Requires the vhost_net kernel module on the host. - ''; + Requires the vhost_net kernel module on the host. + ''; + }; }; - }; - }); + }); }; shares = mkOption { description = "Shared directory trees"; - default = []; - type = with types; listOf (submodule ({ config, ... }: { - options = { - tag = mkOption { - type = str; - description = "Unique virtiofs daemon tag"; - }; - socket = mkOption { - type = nullOr str; - default = - if config.proto == "virtiofs" - then "${hostName}-virtiofs-${config.tag}.sock" - else null; - description = "Socket for communication with virtiofs daemon"; - }; - source = mkOption { - type = nonEmptyStr; - description = "Path to shared directory tree"; - }; - securityModel = mkOption { - type = enum [ "passthrough" "none" "mapped" "mapped-file" ]; - default = "none"; - description = "What security model to use for the shared directory"; - }; - mountPoint = mkOption { - type = path; - description = "Where to mount the share inside the container"; - }; - proto = mkOption { - type = enum [ "9p" "virtiofs" ]; - description = "Protocol for this share"; - default = "9p"; - }; - readOnly = mkOption { - type = bool; - description = "Turn off write access"; - default = false; - }; - cache = mkOption { - type = enum [ "auto" "always" "metadata" "never" ]; - description = "Virtiofs caching policy for the file system, ignored when 9p is used"; - default = "auto"; - }; - }; - })); + default = [ ]; + type = + with types; + listOf ( + submodule ( + { config, ... }: + { + options = { + tag = mkOption { + type = str; + description = "Unique virtiofs daemon tag"; + }; + socket = mkOption { + type = nullOr str; + default = if config.proto == "virtiofs" then "${hostName}-virtiofs-${config.tag}.sock" else null; + description = "Socket for communication with virtiofs daemon"; + }; + source = mkOption { + type = nonEmptyStr; + description = "Path to shared directory tree"; + }; + securityModel = mkOption { + type = enum [ + "passthrough" + "none" + "mapped" + "mapped-file" + ]; + default = "none"; + description = "What security model to use for the shared directory"; + }; + mountPoint = mkOption { + type = path; + description = "Where to mount the share inside the container"; + }; + proto = mkOption { + type = enum [ + "9p" + "virtiofs" + ]; + description = "Protocol for this share"; + default = "9p"; + }; + readOnly = mkOption { + type = bool; + description = "Turn off write access"; + default = false; + }; + cache = mkOption { + type = enum [ + "auto" + "always" + "metadata" + "never" + ]; + description = "Virtiofs caching policy for the file system, ignored when 9p is used"; + default = "auto"; + }; + }; + } + ) + ); }; devices = mkOption { description = "PCI/USB devices that are passed from the host to the MicroVM"; - default = []; + default = [ ]; example = literalExpression /* nix */ '' [ { bus = "pci"; @@ -436,45 +482,50 @@ in path = "vendorid=0xabcd,productid=0x0123"; } ] ''; - type = with types; listOf (submodule { - options = { - bus = mkOption { - type = enum [ "pci" "usb" ]; - description = '' - Device is either on the `pci` or the `usb` bus - ''; - }; - path = mkOption { - type = str; - description = '' - Identification of the device on its bus - ''; - }; - qemu = { - id = mkOption { - type = nullOr str; - default = null; - description = '' - QEMU device identifier (optional) - ''; - }; + type = + with types; + listOf (submodule { + options = { bus = mkOption { - type = nullOr str; - default = null; + type = enum [ + "pci" + "usb" + ]; description = '' - QEMU bus to which this device is attached (optional) + Device is either on the `pci` or the `usb` bus ''; }; - deviceExtraArgs = mkOption { - type = nullOr str; - default = null; + path = mkOption { + type = str; description = '' - Device additional arguments (optional) + Identification of the device on its bus ''; }; + qemu = { + id = mkOption { + type = nullOr str; + default = null; + description = '' + QEMU device identifier (optional) + ''; + }; + bus = mkOption { + type = nullOr str; + default = null; + description = '' + QEMU bus to which this device is attached (optional) + ''; + }; + deviceExtraArgs = mkOption { + type = nullOr str; + default = null; + description = '' + Device additional arguments (optional) + ''; + }; + }; }; - }; - }); + }); }; vsock.cid = mkOption { @@ -515,9 +566,9 @@ in default = let hash = builtins.hashString "sha256" "microvm.nix:${hostName}"; - hs = offset: len: - builtins.substring offset len hash; - in builtins.concatStringsSep "-" [ + hs = offset: len: builtins.substring offset len hash; + in + builtins.concatStringsSep "-" [ (hs 0 8) (hs 8 4) (hs 12 4) @@ -545,19 +596,19 @@ in storeOnDisk = mkOption { type = types.bool; - default = ! lib.any ({ source, ... }: - source == "/nix/store" - ) config.microvm.shares; + default = !lib.any ({ source, ... }: source == "/nix/store") config.microvm.shares; description = "Whether to boot with the storeDisk, that is, unless the host's /nix/store is a microvm.share."; }; - registerClosure = lib.mkEnableOption '' - Register system closure's store paths in Nix db. + registerClosure = + lib.mkEnableOption '' + Register system closure's store paths in Nix db. - While enabled by default, this option may be incompatible with a persistent writable store overlay. - '' // { - default = config.microvm.guest.enable; - }; + While enabled by default, this option may be incompatible with a persistent writable store overlay. + '' + // { + default = config.microvm.guest.enable; + }; writableStoreOverlay = mkOption { type = with types; nullOr str; @@ -592,7 +643,10 @@ in }; backend = mkOption { - type = types.enum [ "gtk" "cocoa" ]; + type = types.enum [ + "gtk" + "cocoa" + ]; default = if pkgs.stdenv.hostPlatform.isDarwin then "cocoa" else "gtk"; defaultText = lib.literalExpression ''if pkgs.stdenv.hostPlatform.isDarwin then "cocoa" else "gtk"''; description = '' @@ -639,7 +693,7 @@ in qemu.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = "Extra arguments to pass to qemu."; }; @@ -661,7 +715,7 @@ in For additional details see the QEMU PCI Express Guidelines: ''; - default = []; + default = [ ]; example = literalExpression /* nix */ '' [ { bus = "pcie.0"; @@ -669,55 +723,58 @@ in chassis = 0; } ] ''; - type = with types; listOf (submodule { - options = { - id = mkOption { - type = str; - description = '' - A unique identifier for this PCIe root port. - ''; - }; - bus = mkOption { - type = nullOr str; - default = null; - description = '' - The PCIe bus on which the root port will be created. - ''; - }; - chassis = mkOption { - type = nullOr int; - default = null; - description = '' - The chassis number associated with this PCIe root port. - ''; - }; - slot = mkOption { - type = nullOr str; - default = null; - description = '' - PCIe slot number. - ''; - }; - addr = mkOption { - type = nullOr str; - default = null; - description = '' - PCIe address on the parent bus. - ''; + type = + with types; + listOf (submodule { + options = { + id = mkOption { + type = str; + description = '' + A unique identifier for this PCIe root port. + ''; + }; + bus = mkOption { + type = nullOr str; + default = null; + description = '' + The PCIe bus on which the root port will be created. + ''; + }; + chassis = mkOption { + type = nullOr int; + default = null; + description = '' + The chassis number associated with this PCIe root port. + ''; + }; + slot = mkOption { + type = nullOr str; + default = null; + description = '' + PCIe slot number. + ''; + }; + addr = mkOption { + type = nullOr str; + default = null; + description = '' + PCIe address on the parent bus. + ''; + }; }; - }; - }); + }); }; qemu.package = mkOption { description = "The QEMU package to use."; type = types.package; - default = if cfg.cpu == null && cfg.vmHostPackages.stdenv.hostPlatform.isLinux then - # If no CPU is requested and the host is Linux, use qemu with KVM support (hardware-accelerated) - cfg.vmHostPackages.qemu_kvm - else - # Different CPU architectures like darwin or Non-Linux use the generic qemu package - cfg.vmHostPackages.qemu; + default = + if cfg.cpu == null && cfg.vmHostPackages.stdenv.hostPlatform.isLinux then + # If no CPU is requested and the host is Linux, use qemu with KVM support (hardware-accelerated) + cfg.vmHostPackages.qemu_kvm + else + # Different CPU architectures like darwin or Non-Linux use the generic qemu package + cfg.vmHostPackages.qemu; defaultText = lib.literalExpression '' if config.microvm.cpu == null && config.microvm.vmHostPackages.stdenv.hostPlatform.isLinux then # If no CPU is requested and the host is Linux, use qemu with KVM support (hardware-accelerated) @@ -737,7 +794,7 @@ in cloud-hypervisor.platformOEMStrings = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = '' Extra arguments to pass to cloud-hypervisor's --platform oem_strings=[] argument. @@ -754,17 +811,18 @@ in cloud-hypervisor.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = "Extra arguments to pass to cloud-hypervisor."; }; cloud-hypervisor.package = mkOption { description = "The cloud-hypervisor package to use."; type = types.package; - default = if cfg.graphics.enable then - cfg.vmHostPackages.cloud-hypervisor-graphics - else - cfg.vmHostPackages.cloud-hypervisor; + default = + if cfg.graphics.enable then + cfg.vmHostPackages.cloud-hypervisor-graphics + else + cfg.vmHostPackages.cloud-hypervisor; defaultText = lib.literalExpression '' if config.microvm.graphics.enable then config.microvm.vmHostPackages.cloud-hypervisor-graphics @@ -775,7 +833,7 @@ in crosvm.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = "Extra arguments to pass to crosvm."; }; @@ -799,14 +857,17 @@ in }; firecracker.driveIoEngine = mkOption { - type = types.enum [ "Async" "Sync" ]; + type = types.enum [ + "Async" + "Sync" + ]; default = "Async"; description = "Type of IO engine to use for Firecracker drives (disks)."; }; firecracker.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = "Extra arguments to pass to firecracker."; }; @@ -831,7 +892,7 @@ in in valueType; }; - default = {}; + default = { }; description = "Extra config to merge into Firecracker JSON configuration"; }; @@ -858,12 +919,18 @@ in vfkit.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = "Extra arguments to pass to vfkit."; }; vfkit.logLevel = mkOption { - type = with types; nullOr (enum ["debug" "info" "error"]); + type = + with types; + nullOr (enum [ + "debug" + "info" + "error" + ]); default = "info"; description = "vfkit log level."; }; @@ -916,9 +983,13 @@ in }; virtiofsd.inodeFileHandles = mkOption { - type = with types; nullOr (enum [ - "never" "prefer" "mandatory" - ]); + type = + with types; + nullOr (enum [ + "never" + "prefer" + "mandatory" + ]); default = "prefer"; description = '' When to use file handles to reference inodes instead of O_PATH file descriptors @@ -931,7 +1002,12 @@ in }; virtiofsd.threadPoolSize = mkOption { - type = with types; oneOf [ str ints.unsigned ]; + type = + with types; + oneOf [ + str + ints.unsigned + ]; default = "`nproc`"; description = '' The amounts of threads virtiofsd should spawn. This option also takes the special @@ -950,7 +1026,7 @@ in virtiofsd.extraArgs = mkOption { type = with types; listOf str; - default = []; + default = [ ]; description = '' Extra command-line switch to pass to virtiofsd. ''; @@ -960,7 +1036,7 @@ in description = "The virtiofsd package to use."; type = types.package; default = cfg.vmHostPackages.virtiofsd; - defaultText = literalExpression ''config.microvm.vmHostPackages.virtiofsd''; + defaultText = literalExpression "config.microvm.vmHostPackages.virtiofsd"; }; runner = mkOption { @@ -979,12 +1055,15 @@ in description = '' Script snippets that end up in the runner package's bin/ directory ''; - default = {}; + default = { }; type = with types; attrsOf lines; }; storeDiskType = mkOption { - type = types.enum [ "squashfs" "erofs" ]; + type = types.enum [ + "squashfs" + "erofs" + ]; description = '' Boot disk file system type: squashfs is smaller, erofs is supposed to be faster. @@ -999,16 +1078,15 @@ in Omit `"-Efragments"` and `"-Ededupe"` to enable multi-threading. ''; - default = - [ "-zlz4hc" ] - ++ - lib.optional (kernelAtLeast "5.16") "-Eztailpacking" - ++ - lib.optionals (kernelAtLeast "6.1") [ - # not implemented with multi-threading - "-Efragments" - "-Ededupe" - ]; + default = [ + "-zlz4hc" + ] + ++ lib.optional (kernelAtLeast "5.16") "-Eztailpacking" + ++ lib.optionals (kernelAtLeast "6.1") [ + # not implemented with multi-threading + "-Efragments" + "-Ededupe" + ]; defaultText = lib.literalExpression '' [ "-zlz4hc" ] ++ lib.optional (kernelAtLeast "5.16") "-Eztailpacking" @@ -1016,13 +1094,18 @@ in "-Efragments" "-Ededupe" ] - ''; + ''; }; storeDiskSquashfsFlags = mkOption { type = with types; listOf str; description = "Flags to pass to gensquashfs"; - default = [ "-c" "zstd" "-j" "$NIX_BUILD_CORES" ]; + default = [ + "-c" + "zstd" + "-j" + "$NIX_BUILD_CORES" + ]; }; systemSymlink = mkOption { @@ -1036,7 +1119,7 @@ in credentialFiles = mkOption { type = with types; attrsOf path; - default = {}; + default = { }; description = '' Key-value pairs of credential files that will be loaded into the vm using systemd's io.systemd.credential feature. ''; @@ -1049,18 +1132,22 @@ in }; imports = [ - (lib.mkRemovedOptionModule ["microvm" "balloonMem"] "The balloonMem option has been removed and replaced by the boolean option balloon") + (lib.mkRemovedOptionModule [ + "microvm" + "balloonMem" + ] "The balloonMem option has been removed and replaced by the boolean option balloon") ]; - config = lib.mkMerge [ { - microvm.qemu.machine = - lib.mkIf (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ( + config = lib.mkMerge [ + { + microvm.qemu.machine = lib.mkIf (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ( lib.mkDefault "microvm" ); - } { - microvm.qemu.machine = - lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") ( + } + { + microvm.qemu.machine = lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") ( lib.mkDefault "virt" ); - } ]; + } + ]; } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/pci-devices.nix b/subrepos/microvm.nix/nixos-modules/microvm/pci-devices.nix index cb420b70..4485879e 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/pci-devices.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/pci-devices.nix @@ -1,9 +1,12 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let - pciDevices = builtins.filter ({ bus, ... }: - bus == "pci" - ) config.microvm.devices; + pciDevices = builtins.filter ({ bus, ... }: bus == "pci") config.microvm.devices; # TODO: don't hardcode but obtain from host config user = "microvm"; @@ -11,27 +14,34 @@ let in { - microvm.binScripts.pci-setup = lib.mkIf (pciDevices != []) ('' - set -eou pipefail - ${pkgs.kmod}/bin/modprobe vfio-pci - '' + lib.concatMapStrings ({ path, ... }: '' - cd /sys/bus/pci/devices/${path} - if [ -e driver ]; then - echo ${path} > driver/unbind - fi - echo vfio-pci > driver_override - echo ${path} > /sys/bus/pci/drivers_probe - '' + - # In order to access the vfio dev the permissions must be set - # for the user/group running the VMM later. - # - # Insprired by https://www.kernel.org/doc/html/next/driver-api/vfio.html#vfio-usage-example - # - # assert we could get the IOMMU group number (=: name of VFIO dev) - '' - [[ -e iommu_group ]] || exit 1 - VFIO_DEV=$(basename $(readlink iommu_group)) - echo "Making VFIO device $VFIO_DEV accessible for user" - chown ${user}:${group} /dev/vfio/$VFIO_DEV - '') pciDevices); + microvm.binScripts.pci-setup = lib.mkIf (pciDevices != [ ]) ( + '' + set -eou pipefail + ${pkgs.kmod}/bin/modprobe vfio-pci + '' + + lib.concatMapStrings ( + { path, ... }: + '' + cd /sys/bus/pci/devices/${path} + if [ -e driver ]; then + echo ${path} > driver/unbind + fi + echo vfio-pci > driver_override + echo ${path} > /sys/bus/pci/drivers_probe + '' + + + # In order to access the vfio dev the permissions must be set + # for the user/group running the VMM later. + # + # Insprired by https://www.kernel.org/doc/html/next/driver-api/vfio.html#vfio-usage-example + # + # assert we could get the IOMMU group number (=: name of VFIO dev) + '' + [[ -e iommu_group ]] || exit 1 + VFIO_DEV=$(basename $(readlink iommu_group)) + echo "Making VFIO device $VFIO_DEV accessible for user" + chown ${user}:${group} /dev/vfio/$VFIO_DEV + '' + ) pciDevices + ); } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/rosetta.nix b/subrepos/microvm.nix/nixos-modules/microvm/rosetta.nix index 2d5f5be6..32fb8bfa 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/rosetta.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/rosetta.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.microvm.vfkit.rosetta; diff --git a/subrepos/microvm.nix/nixos-modules/microvm/ssh-deploy.nix b/subrepos/microvm.nix/nixos-modules/microvm/ssh-deploy.nix index 08ce8015..76c84325 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/ssh-deploy.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/ssh-deploy.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let hostName = config.networking.hostName or "$HOSTNAME"; @@ -23,13 +28,13 @@ let }; canSwitchViaSsh = - config.system.switch.enable && - # MicroVM must be reachable through SSH - config.services.openssh.enable && - # Is the /nix/store mounted from the host? - builtins.any ({ source, ... }: - source == "/nix/store" - ) config.microvm.shares; + config.system.switch.enable + && + # MicroVM must be reachable through SSH + config.services.openssh.enable + && + # Is the /nix/store mounted from the host? + builtins.any ({ source, ... }: source == "/nix/store") config.microvm.shares; in { @@ -138,11 +143,14 @@ in echo "Building toplevel ${paths.toplevelOut}" nix build -L --accept-flake-config --no-link \ - ${with paths; lib.concatMapStringsSep " " (drv: "'${drv}^out'") [ - nixDrv - closureInfoDrv - toplevelDrv - ]} + ${ + with paths; + lib.concatMapStringsSep " " (drv: "'${drv}^out'") [ + nixDrv + closureInfoDrv + toplevelDrv + ] + } echo "Building MicroVM runner for ${hostName}" nix build -L --accept-flake-config -o new \ "${paths.runnerDrv}^out" @@ -211,30 +219,34 @@ in '' ); - rebuild = with config.microvm.deploy; pkgs.writeShellScriptBin "microvm-rebuild" '' - set -eou pipefail + rebuild = + with config.microvm.deploy; + pkgs.writeShellScriptBin "microvm-rebuild" '' + set -eou pipefail - HOST="$1" - shift - TARGET="$1" - shift - OPTS="$@" - if [ $# -gt 0 ]; then - if [ "$1" == "--use-remote-sudo" ]; then - OPTS="$1" - shift + HOST="$1" + shift + TARGET="$1" + shift + OPTS="$@" + if [ $# -gt 0 ]; then + if [ "$1" == "--use-remote-sudo" ]; then + OPTS="$1" + shift + fi + fi + if [[ -z "$HOST" || -z "$TARGET" || $# -gt 0 ]]; then + echo "Usage: $0 root@ root@ [--use-remote-sudo] switch" + exit 1 fi - fi - if [[ -z "$HOST" || -z "$TARGET" || $# -gt 0 ]]; then - echo "Usage: $0 root@ root@ [--use-remote-sudo] switch" - exit 1 - fi - ${lib.getExe installOnHost} "$HOST" $OPTS - ${if canSwitchViaSsh - then ''${lib.getExe sshSwitch} "$TARGET" $OPTS'' - else ''ssh "$HOST" -- systemctl restart "microvm@${hostName}.service"'' - } - ''; + ${lib.getExe installOnHost} "$HOST" $OPTS + ${ + if canSwitchViaSsh then + ''${lib.getExe sshSwitch} "$TARGET" $OPTS'' + else + ''ssh "$HOST" -- systemctl restart "microvm@${hostName}.service"'' + } + ''; }; } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/store-disk.nix b/subrepos/microvm.nix/nixos-modules/microvm/store-disk.nix index 2ba9828c..5d2f831a 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/store-disk.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/store-disk.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let regInfo = pkgs.closureInfo { @@ -7,7 +12,14 @@ let erofs-utils = # Are any extended options specified? - if lib.any (with lib; flip elem ["-Ededupe" "-Efragments"]) config.microvm.storeDiskErofsFlags + if + lib.any ( + with lib; + flip elem [ + "-Ededupe" + "-Efragments" + ] + ) config.microvm.storeDiskErofsFlags then # If extended options are present, # stick to the single-threaded erofs-utils @@ -29,24 +41,25 @@ let { squashfs = "gensquashfs ${squashfsFlags} -D store --all-root -q $out"; erofs = "mkfs.erofs ${erofsFlags} -T 0 --all-root -L nix-store --mount-point=/nix/store $out store"; - }.${config.microvm.storeDiskType}; + } + .${config.microvm.storeDiskType}; writeClosure = pkgs.writeClosure or pkgs.writeReferencesToFile; storeDiskContents = writeClosure ( - [ config.system.build.toplevel ] - ++ - lib.optional config.nix.enable regInfo + [ config.system.build.toplevel ] ++ lib.optional config.nix.enable regInfo ); in { - options.microvm.storeDisk = with lib; mkOption { - type = types.path; - description = '' - Generated - ''; - }; + options.microvm.storeDisk = + with lib; + mkOption { + type = types.path; + description = '' + Generated + ''; + }; config = lib.mkMerge [ (lib.mkIf (config.microvm.guest.enable && config.microvm.storeOnDisk) { @@ -56,43 +69,45 @@ in # filesystems, so checking on that directly would result in an # infinite recursion. microvm.storeDiskType = lib.mkDefault ( - if config.security.virtualisation.flushL1DataCache == "always" - then "squashfs" - else "erofs" + if config.security.virtualisation.flushL1DataCache == "always" then "squashfs" else "erofs" ); boot.initrd.availableKernelModules = [ config.microvm.storeDiskType ]; - microvm.storeDisk = pkgs.buildPackages.runCommandLocal "microvm-store-disk.${config.microvm.storeDiskType}" { - nativeBuildInputs = [ - pkgs.buildPackages.time - pkgs.buildPackages.bubblewrap + microvm.storeDisk = + pkgs.buildPackages.runCommandLocal "microvm-store-disk.${config.microvm.storeDiskType}" { - squashfs = pkgs.buildPackages.squashfs-tools-ng; - erofs = erofs-utils; - }.${config.microvm.storeDiskType} - ]; - passthru = { - inherit regInfo; - }; - __structuredAttrs = true; - unsafeDiscardReferences.out = true; - } '' - mkdir store - BWRAP_ARGS="--dev-bind / / --chdir $(pwd)" - for d in $(sort -u ${storeDiskContents}); do - BWRAP_ARGS="$BWRAP_ARGS --ro-bind $d $(pwd)/store/$(basename $d)" - done + nativeBuildInputs = [ + pkgs.buildPackages.time + pkgs.buildPackages.bubblewrap + { + squashfs = pkgs.buildPackages.squashfs-tools-ng; + erofs = erofs-utils; + } + .${config.microvm.storeDiskType} + ]; + passthru = { + inherit regInfo; + }; + __structuredAttrs = true; + unsafeDiscardReferences.out = true; + } + '' + mkdir store + BWRAP_ARGS="--dev-bind / / --chdir $(pwd)" + for d in $(sort -u ${storeDiskContents}); do + BWRAP_ARGS="$BWRAP_ARGS --ro-bind $d $(pwd)/store/$(basename $d)" + done - echo Creating a ${config.microvm.storeDiskType} - bwrap $BWRAP_ARGS -- time ${mkfsCommand} || \ - ( - echo "Bubblewrap failed. Falling back to copying...">&2 - cp -a $(sort -u ${storeDiskContents}) store/ - time ${mkfsCommand} - ) - ''; + echo Creating a ${config.microvm.storeDiskType} + bwrap $BWRAP_ARGS -- time ${mkfsCommand} || \ + ( + echo "Bubblewrap failed. Falling back to copying...">&2 + cp -a $(sort -u ${storeDiskContents}) store/ + time ${mkfsCommand} + ) + ''; }) (lib.mkIf (config.microvm.registerClosure && config.nix.enable) { diff --git a/subrepos/microvm.nix/nixos-modules/microvm/system.nix b/subrepos/microvm.nix/nixos-modules/microvm/system.nix index 93d643bd..b9909ae2 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/system.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/system.nix @@ -1,13 +1,22 @@ -{ pkgs, lib, config, ... }: +{ + pkgs, + lib, + config, + ... +}: { config = lib.mkIf config.microvm.guest.enable { assertions = [ - {assertion = (config.microvm.writableStoreOverlay != null) -> (!config.nix.optimise.automatic && !config.nix.settings.auto-optimise-store); - message = '' - `nix.optimise.automatic` and `nix.settings.auto-optimise-store` do not work with `microvm.writableStoreOverlay`. - '';}]; - + { + assertion = + (config.microvm.writableStoreOverlay != null) + -> (!config.nix.optimise.automatic && !config.nix.settings.auto-optimise-store); + message = '' + `nix.optimise.automatic` and `nix.settings.auto-optimise-store` do not work with `microvm.writableStoreOverlay`. + ''; + } + ]; boot.loader.grub.enable = false; # boot.initrd.systemd.enable = lib.mkDefault true; @@ -18,36 +27,45 @@ "9pnet_virtio" "9p" "virtiofs" - ] ++ lib.optionals ( - pkgs.stdenv.targetPlatform.system == "x86_64-linux" && - config.microvm.hypervisor == "firecracker" - ) [ - # Keyboard controller that can receive CtrlAltDel - "i8042" - ] ++ lib.optionals (config.microvm.writableStoreOverlay != null) [ + ] + ++ + lib.optionals + (pkgs.stdenv.targetPlatform.system == "x86_64-linux" && config.microvm.hypervisor == "firecracker") + [ + # Keyboard controller that can receive CtrlAltDel + "i8042" + ] + ++ lib.optionals (config.microvm.writableStoreOverlay != null) [ "overlay" ]; - microvm.kernelParams = let - # When a store disk is used, we can drop references to the packed contents as the squashfs/erofs contains all paths. - toplevel = if config.microvm.storeOnDisk then - builtins.unsafeDiscardStringContext config.system.build.toplevel - else - config.system.build.toplevel; - in config.boot.kernelParams ++ [ - "init=${toplevel}/init" - ]; + microvm.kernelParams = + let + # When a store disk is used, we can drop references to the packed contents as the squashfs/erofs contains all paths. + toplevel = + if config.microvm.storeOnDisk then + builtins.unsafeDiscardStringContext config.system.build.toplevel + else + config.system.build.toplevel; + in + config.boot.kernelParams + ++ [ + "init=${toplevel}/init" + ]; # modules that consume boot time but have rare use-cases boot.blacklistedKernelModules = [ - "rfkill" "intel_pstate" - ] ++ lib.optional (!config.microvm.graphics.enable) "drm"; + "rfkill" + "intel_pstate" + ] + ++ lib.optional (!config.microvm.graphics.enable) "drm"; systemd = let # nix-daemon works only with a writable /nix/store enableNixDaemon = config.microvm.writableStoreOverlay != null; - in { + in + { services.nix-daemon.enable = lib.mkDefault enableNixDaemon; sockets.nix-daemon.enable = lib.mkDefault enableNixDaemon; @@ -55,7 +73,9 @@ services.mount-pstore.enable = false; # just fails in the usual usage of microvm.nix - generators = { systemd-gpt-auto-generator = "/dev/null"; }; + generators = { + systemd-gpt-auto-generator = "/dev/null"; + }; }; # Set /etc/machine-id from machineId if provided @@ -64,8 +84,8 @@ text = lib.replaceString "-" "" config.microvm.machineId + "\n"; }; # Generate hostId from machine-id like systemd would do - networking.hostId = lib.mkIf (config.microvm.machineId != null) (lib.mkDefault ( - builtins.substring 0 8 config.microvm.machineId - )); + networking.hostId = lib.mkIf (config.microvm.machineId != null) ( + lib.mkDefault (builtins.substring 0 8 config.microvm.machineId) + ); }; } diff --git a/subrepos/microvm.nix/nixos-modules/microvm/virtiofsd/default.nix b/subrepos/microvm.nix/nixos-modules/microvm/virtiofsd/default.nix index 0588ad0c..1db62c40 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/virtiofsd/default.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/virtiofsd/default.nix @@ -1,11 +1,14 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let - virtiofsShares = builtins.filter ({ proto, ... }: - proto == "virtiofs" - ) config.microvm.shares; + virtiofsShares = builtins.filter ({ proto, ... }: proto == "virtiofs") config.microvm.shares; - requiresVirtiofsd = virtiofsShares != [] && config.microvm.hypervisor != "vfkit"; + requiresVirtiofsd = virtiofsShares != [ ] && config.microvm.hypervisor != "vfkit"; inherit (pkgs.python3Packages) supervisor; supervisord = lib.getExe' supervisor "supervisord"; @@ -23,54 +26,67 @@ in "eventlistener:notify" = { command = pkgs.writers.writePython3 "supervisord-event-handler" { } ( - pkgs.replaceVars ./supervisord-event-handler.py { + pkgs.replaceVars ./supervisord-event-handler.py { # 1 for the event handler process virtiofsdCount = 1 + builtins.length virtiofsShares; } ); events = "PROCESS_STATE"; }; - } // builtins.listToAttrs ( - map ({ tag, socket, source, readOnly, cache, ... }: { - name = "program:virtiofsd-${tag}"; - value = { - stderr_syslog = true; - stdout_syslog = true; - command = pkgs.writeShellScript "virtiofsd-${tag}" '' - if [ $(id -u) = 0 ]; then - OPT_RLIMIT="--rlimit-nofile 1048576" - else - OPT_RLIMIT="" - fi - exec ${lib.getExe config.microvm.virtiofsd.package} \ - --socket-path=${lib.escapeShellArg socket} \ - ${lib.optionalString (config.microvm.virtiofsd.group != null) - "--socket-group=${config.microvm.virtiofsd.group}" - } \ - --shared-dir=${lib.escapeShellArg source} \ - $OPT_RLIMIT \ - --thread-pool-size ${toString config.microvm.virtiofsd.threadPoolSize} \ - --posix-acl --xattr \ - --cache=${cache} \ - ${lib.optionalString (config.microvm.virtiofsd.inodeFileHandles != null) - "--inode-file-handles=${config.microvm.virtiofsd.inodeFileHandles}" - } \ - ${lib.optionalString (config.microvm.hypervisor == "crosvm") - "--tag=${tag}" - } \ - ${lib.optionalString readOnly "--readonly"} \ - ${lib.concatStringsSep " " config.microvm.virtiofsd.extraArgs} - ''; - }; - }) virtiofsShares + } + // builtins.listToAttrs ( + map ( + { + tag, + socket, + source, + readOnly, + cache, + ... + }: + { + name = "program:virtiofsd-${tag}"; + value = { + stderr_syslog = true; + stdout_syslog = true; + command = pkgs.writeShellScript "virtiofsd-${tag}" '' + if [ $(id -u) = 0 ]; then + OPT_RLIMIT="--rlimit-nofile 1048576" + else + OPT_RLIMIT="" + fi + exec ${lib.getExe config.microvm.virtiofsd.package} \ + --socket-path=${lib.escapeShellArg socket} \ + ${ + lib.optionalString ( + config.microvm.virtiofsd.group != null + ) "--socket-group=${config.microvm.virtiofsd.group}" + } \ + --shared-dir=${lib.escapeShellArg source} \ + $OPT_RLIMIT \ + --thread-pool-size ${toString config.microvm.virtiofsd.threadPoolSize} \ + --posix-acl --xattr \ + --cache=${cache} \ + ${ + lib.optionalString ( + config.microvm.virtiofsd.inodeFileHandles != null + ) "--inode-file-handles=${config.microvm.virtiofsd.inodeFileHandles}" + } \ + ${lib.optionalString (config.microvm.hypervisor == "crosvm") "--tag=${tag}"} \ + ${lib.optionalString readOnly "--readonly"} \ + ${lib.concatStringsSep " " config.microvm.virtiofsd.extraArgs} + ''; + }; + } + ) virtiofsShares ); - supervisordConfigFile = - pkgs.writeText "${config.networking.hostName}-virtiofsd-supervisord.conf" ( - lib.generators.toINI {} supervisordConfig - ); + supervisordConfigFile = pkgs.writeText "${config.networking.hostName}-virtiofsd-supervisord.conf" ( + lib.generators.toINI { } supervisordConfig + ); - in '' + in + '' exec ${supervisord} --configuration ${supervisordConfigFile} "$@" ''; }; diff --git a/subrepos/microvm.nix/nixos-modules/microvm/vsock-ssh.nix b/subrepos/microvm.nix/nixos-modules/microvm/vsock-ssh.nix index 1fc07293..4764d29a 100644 --- a/subrepos/microvm.nix/nixos-modules/microvm/vsock-ssh.nix +++ b/subrepos/microvm.nix/nixos-modules/microvm/vsock-ssh.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.microvm.vsock; @@ -19,10 +24,12 @@ in }; config = lib.mkIf cfg.ssh.enable { - assertions = [{ - assertion = cfg.cid != null; - message = "microvm.vsock.ssh.enable requires microvm.vsock.cid to be set"; - }]; + assertions = [ + { + assertion = cfg.cid != null; + message = "microvm.vsock.ssh.enable requires microvm.vsock.cid to be set"; + } + ]; services.openssh.enable = true; diff --git a/subrepos/microvm.nix/pkgs/build-microvm.nix b/subrepos/microvm.nix/pkgs/build-microvm.nix index d88b20bd..8e448b22 100644 --- a/subrepos/microvm.nix/pkgs/build-microvm.nix +++ b/subrepos/microvm.nix/pkgs/build-microvm.nix @@ -1,13 +1,23 @@ # Builds a MicroVM from a flake but takes the hypervisor from the # local pkgs not from the target flake. -{ self -, lib, stdenv -, writeShellScriptBin -, coreutils, git, nix +{ + self, + lib, + stdenv, + writeShellScriptBin, + coreutils, + git, + nix, }: writeShellScriptBin "build-microvm" '' - PATH=${lib.makeBinPath [ coreutils git nix ]} + PATH=${ + lib.makeBinPath [ + coreutils + git + nix + ] + } if [ $# -lt 1 ]; then echo Usage: $0 flakeref#nixos diff --git a/subrepos/microvm.nix/pkgs/doc.nix b/subrepos/microvm.nix/pkgs/doc.nix index 17fa3500..43e6e1d0 100644 --- a/subrepos/microvm.nix/pkgs/doc.nix +++ b/subrepos/microvm.nix/pkgs/doc.nix @@ -1,51 +1,75 @@ -{ lib, runCommand, mdbook, nixosOptionsDoc }: +{ + lib, + runCommand, + mdbook, + nixosOptionsDoc, +}: let - makeOptionsDoc = module: nixosOptionsDoc { - inherit ((lib.evalModules { - modules = [ - module - ({ lib, ... }: { - config._module = { - check = false; - }; - # Hide NixOS `_module.args` from nixosOptionsDoc to remain specific to microvm.nix - options._module.args = lib.mkOption { - internal = true; - }; - }) - ]; - })) options; + makeOptionsDoc = + module: + nixosOptionsDoc { + inherit + ( + (lib.evalModules { + modules = [ + module + ( + { lib, ... }: + { + config._module = { + check = false; + }; + # Hide NixOS `_module.args` from nixosOptionsDoc to remain specific to microvm.nix + options._module.args = lib.mkOption { + internal = true; + }; + } + ) + ]; + }) + ) + options + ; - transformOptions = opt: opt // { - declarations = map (decl: - let - root = toString ../.; - declStr = toString decl; - declPath = lib.removePrefix root decl; - in - if lib.hasPrefix root declStr - # Rewrite links from ../. in the /nix/store to the source on Github - then { - name = "microvm.nix${declPath}"; - url = "https://github.com/microvm-nix/microvm.nix/tree/main${declPath}"; - } - else decl - ) opt.declarations; + transformOptions = + opt: + opt + // { + declarations = map ( + decl: + let + root = toString ../.; + declStr = toString decl; + declPath = lib.removePrefix root decl; + in + if + lib.hasPrefix root declStr + # Rewrite links from ../. in the /nix/store to the source on Github + then + { + name = "microvm.nix${declPath}"; + url = "https://github.com/microvm-nix/microvm.nix/tree/main${declPath}"; + } + else + decl + ) opt.declarations; + }; }; - }; microvmDoc = makeOptionsDoc ../nixos-modules/microvm/options.nix; hostDoc = makeOptionsDoc ../nixos-modules/host/options.nix; in -runCommand "microvm.nix-doc" { - nativeBuildInputs = [ mdbook ]; -} '' - cp -r ${../doc} doc - chmod u+w doc/src - cp ${microvmDoc.optionsCommonMark} doc/src/microvm-options.md - cp ${hostDoc.optionsCommonMark} doc/src/host-options.md - ${mdbook}/bin/mdbook build -d $out doc -'' +runCommand "microvm.nix-doc" + { + nativeBuildInputs = [ mdbook ]; + } + '' + cp -r ${../doc} doc + chmod u+w doc/src + cp ${microvmDoc.optionsCommonMark} doc/src/microvm-options.md + cp ${hostDoc.optionsCommonMark} doc/src/host-options.md + ${mdbook}/bin/mdbook build -d $out doc + '' diff --git a/subrepos/microvm.nix/pkgs/microvm-command.nix b/subrepos/microvm.nix/pkgs/microvm-command.nix index 129bd62a..90f90e4f 100644 --- a/subrepos/microvm.nix/pkgs/microvm-command.nix +++ b/subrepos/microvm.nix/pkgs/microvm-command.nix @@ -1,10 +1,11 @@ -{ lib -, git -, jq -, nix -, openssh -, writeShellScriptBin -, stateDir ? "/var/lib/microvms" +{ + lib, + git, + jq, + nix, + openssh, + writeShellScriptBin, + stateDir ? "/var/lib/microvms", }: let @@ -22,9 +23,14 @@ in writeShellScriptBin "microvm" '' set -e - PATH=${lib.makeBinPath [ - git jq nix openssh - ]}:$PATH + PATH=${ + lib.makeBinPath [ + git + jq + nix + openssh + ] + }:$PATH STATE_DIR=${stateDir} ACTION=help FLAKE=git+file:///etc/nixos diff --git a/test-eval.nix b/test-eval.nix index 5a04c8b4..315938f4 100644 --- a/test-eval.nix +++ b/test-eval.nix @@ -2,4 +2,4 @@ let inputs = builtins.getFlake (toString ./.); eval = inputs.systemConfigs.linux-generic; in - eval.config.environment.systemPackages +eval.config.environment.systemPackages diff --git a/test.nix b/test.nix deleted file mode 100644 index 7dfb5531..00000000 --- a/test.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - flake.modules.homeManager.apps = { pkgs, ... }: let - universal = with pkgs; [ - # IDEs - jetbrains.clion - jetbrains.idea - jetbrains.rider - # Browsers - firefox - brave - # Dev tools - gh # GitHub CLI - ghidra # Reverse engineering - jdk21 # Java development - # System - fastfetch # System info - ]; - - linuxSpecific = with pkgs; [ - antigravity-fhs - code-cursor-fhs - ]; - - darwinSpecific = with pkgs; [ - antigravity - code-cursor - ]; - in { - home.packages = universal ++ (if pkgs.stdenv.isLinux then linuxSpecific else darwinSpecific); - }; -} diff --git a/theme-selection.nix b/theme-selection.nix new file mode 100644 index 00000000..78c5854d --- /dev/null +++ b/theme-selection.nix @@ -0,0 +1,12 @@ +let + # Single editable line: + schemeBase = "gruvbox"; +in +{ + # Derived values consumed by modules (keep modules theme-agnostic). + name = schemeBase; + schemes = { + light = "${schemeBase}-light-hard"; + dark = "${schemeBase}-dark-hard"; + }; +} diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 00000000..55ebcf46 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,60 @@ +# treefmt.toml — unified formatter config for the dotfiles repo +# Used by `nix fmt` (via treefmt-nix) and the ibecker.treefmt-vscode extension. + +[global] +excludes = [ + "*.lock", + "result", + # sops-managed: every encrypt rewrites the envelope, so any reformat + # we do here will be silently undone (and meanwhile breaks sops's MAC). + "secrets/**", + "*.sops", + "*.pem.sops", + ".git/**", + "*.patch", + "*.xml", + "*.xpi", + "*.json", # managed programmatically; don't auto-format + "flake_*.json", +] + +# ── Nix ────────────────────────────────────────────────────────── +[formatter.nixfmt] +command = "nixfmt" +includes = ["*.nix"] + +# ── Shell ───────────────────────────────────────────────────────── +[formatter.shfmt] +command = "shfmt" +options = ["-i", "2", "-s", "-w"] +includes = ["*.sh", "*.bash", ".envrc"] + +# ── Lua ─────────────────────────────────────────────────────────── +[formatter.stylua] +command = "stylua" +includes = ["*.lua"] + +# ── Python ──────────────────────────────────────────────────────── +[formatter.ruff] +command = "ruff" +options = ["format"] +includes = ["*.py"] + +# ── Web / Markdown ──────────────────────────────────────────────── +[formatter.prettier] +command = "prettier" +options = ["--write"] +includes = [ + "*.js", + "*.ts", + "*.css", + "*.html", + "*.md", + "*.yaml", + "*.yml", +] + +# ── Swift ───────────────────────────────────────────────────────── +[formatter.swiftformat] +command = "swiftformat" +includes = ["*.swift"]