My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

secrets: add forgejo user API token

+164 -59
+126
home/default.nix
··· 101 101 manual.html.enable = false; 102 102 manual.json.enable = false; 103 103 104 + # ── Developer directory scaffold ─────────────────────────────────────────── 105 + # ~/Developer/Git — clones from github.com/ewanc26 (excl. nix) and any 106 + # Forgejo repos not mirrored from GitHub. 107 + # ~/Developer/Local — private repos synced to Forgejo only. 108 + home.activation.developerDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' 109 + for dir in Git Local; do 110 + target="$HOME/Developer/$dir" 111 + if [ ! -d "$target" ]; then 112 + $DRY_RUN_CMD mkdir -p "$target" 113 + echo "developer: created $target" 114 + fi 115 + done 116 + ''; 117 + 104 118 # ── nix-config git repo ──────────────────────────────────────────────────── 105 119 # Ensures ~/.config/nix-config is always a git repo with the correct remotes. 106 120 # Server hosts only use origin (GitHub) — Tangled is for desktop hosts only. ··· 143 157 fi 144 158 ''; 145 159 160 + # ── Developer directories ────────────────────────────────────────────────── 161 + # Creates ~/Developer/Git and ~/Developer/Local on all hosts. 162 + # ~/Developer/Git — GitHub repos (ewanc26, minus nix) + non-mirror Forgejo repos. 163 + # ~/Developer/Local — private Forgejo repos (requires userApiTokenFile to be set). 164 + # Repos are cloned via SSH on first activation; existing dirs are never touched. 165 + home.activation.developerDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' 166 + $DRY_RUN_CMD mkdir -p "$HOME/Developer/Git" 167 + $DRY_RUN_CMD mkdir -p "$HOME/Developer/Local" 168 + 169 + # ── GitHub ──────────────────────────────────────────────────────────────── 170 + if ${pkgs.curl}/bin/curl --silent --max-time 5 --output /dev/null "https://github.com"; then 171 + page=1 172 + while true; do 173 + repos=$(${pkgs.curl}/bin/curl --silent \ 174 + "https://api.github.com/users/${cfg.user.githubUsername}/repos?per_page=100&page=$page" \ 175 + | ${pkgs.jq}/bin/jq -r '.[].name // empty') 176 + [ -z "$repos" ] && break 177 + for repo in $repos; do 178 + [ "$repo" = "nix" ] && continue 179 + if [ ! -d "$HOME/Developer/Git/$repo" ]; then 180 + echo "developer: cloning github:${cfg.user.githubUsername}/$repo" 181 + $DRY_RUN_CMD ${pkgs.git}/bin/git clone \ 182 + "git@github.com:${cfg.user.githubUsername}/$repo.git" \ 183 + "$HOME/Developer/Git/$repo" 184 + fi 185 + done 186 + page=$((page + 1)) 187 + done 188 + else 189 + echo "developer: github unreachable, skipping GitHub clones" 190 + fi 191 + 192 + # ── Forgejo ─────────────────────────────────────────────────────────────── 193 + if ${pkgs.curl}/bin/curl --silent --max-time 5 --output /dev/null "https://${cfg.forgejo.hostname}"; then 194 + forgejo_token_arg="" 195 + ${lib.optionalString (cfg.forgejo.userApiTokenFile != null) '' 196 + forgejo_token_arg="&token=$(cat "${cfg.forgejo.userApiTokenFile}")" 197 + ''} 198 + 199 + page=1 200 + while true; do 201 + page_data=$(${pkgs.curl}/bin/curl --silent \ 202 + "https://${cfg.forgejo.hostname}/api/v1/repos/search?limit=50&page=$page$forgejo_token_arg") 203 + count=$(echo "$page_data" | ${pkgs.jq}/bin/jq '.data | length') 204 + [ "$count" = "0" ] && break 205 + 206 + # Non-mirror public repos -> Developer/Git 207 + while IFS= read -r name; do 208 + if [ ! -d "$HOME/Developer/Git/$name" ]; then 209 + echo "developer: cloning forgejo:${cfg.user.username}/$name" 210 + $DRY_RUN_CMD ${pkgs.git}/bin/git clone \ 211 + "git@${cfg.forgejo.hostname}:${cfg.user.username}/$name.git" \ 212 + "$HOME/Developer/Git/$name" 213 + fi 214 + done < <(echo "$page_data" \ 215 + | ${pkgs.jq}/bin/jq -r '.data[] | select(.mirror == false and .private == false) | .name') 216 + 217 + ${lib.optionalString (cfg.forgejo.userApiTokenFile != null) '' 218 + # Non-mirror private repos -> Developer/Local (token required) 219 + while IFS= read -r name; do 220 + if [ ! -d "$HOME/Developer/Local/$name" ]; then 221 + echo "developer: cloning forgejo:${cfg.user.username}/$name (private)" 222 + $DRY_RUN_CMD ${pkgs.git}/bin/git clone \ 223 + "git@${cfg.forgejo.hostname}:${cfg.user.username}/$name.git" \ 224 + "$HOME/Developer/Local/$name" 225 + fi 226 + done < <(echo "$page_data" \ 227 + | ${pkgs.jq}/bin/jq -r '.data[] | select(.mirror == false and .private == true) | .name') 228 + ''} 229 + 230 + page=$((page + 1)) 231 + done 232 + else 233 + echo "developer: forgejo unreachable, skipping Forgejo clones" 234 + fi 235 + ''; 236 + 146 237 # ── Nextcloud desktop client ───────────────────────────────────────────── 147 238 # Both activation scripts below patch nextcloud.cfg in-place so that 148 239 # credentials/tokens already written by the client are preserved. ··· 161 252 '' 162 253 ); 163 254 255 + # Ensure Nextcloud-synced dirs exist directly at $HOME. 256 + # On macOS, Pictures is excluded — Photos Library.photoslibrary is TCC-protected. 257 + # Any legacy symlinks pointing to ~/Nextcloud/* are replaced with real dirs. 258 + home.activation.nextcloudFolderLinks = lib.mkIf cfg.isDesktop ( 259 + lib.hm.dag.entryAfter [ "writeBoundary" ] '' 260 + if ! ${pkgs.curl}/bin/curl --silent --max-time 5 --output /dev/null "https://${cfg.nextcloud.hostname}"; then 261 + echo "nextcloud: server unreachable, skipping folder setup" 262 + else 263 + dirs="Desktop Downloads Documents Obsidian Archives Pictures" 264 + ${lib.optionalString isDarwin ''dirs="Desktop Downloads Documents Obsidian Archives"''} 265 + 266 + for dir in $dirs; do 267 + target="$HOME/$dir" 268 + 269 + # Replace legacy symlink (from old ~/Nextcloud/$dir setup) with a real dir. 270 + if [ -L "$target" ]; then 271 + echo "nextcloud: removing legacy symlink $target" 272 + $DRY_RUN_CMD rm "$target" 273 + fi 274 + 275 + if [ ! -d "$target" ]; then 276 + $DRY_RUN_CMD mkdir -p "$target" 277 + echo "nextcloud: created $target" 278 + fi 279 + done 280 + fi 281 + '' 282 + ); 283 + 164 284 # Allow syncing files of any size (client default blocks files over ~500 MB). 165 285 home.activation.nextcloudMaxSize = lib.mkIf cfg.isDesktop ( 166 286 lib.hm.dag.entryAfter [ "writeBoundary" ] '' ··· 213 333 # Tell the home-manager sops module to decrypt using the host's SSH ed25519 214 334 # key as an age key — same source as the system-level sops in common.nix. 215 335 sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; 336 + 337 + sops.secrets."forgejo-user-token" = { 338 + sopsFile = ../secrets/forgejo-user-token; 339 + format = "binary"; 340 + path = "${config.home.homeDirectory}/.config/forgejo-user-token"; 341 + }; 216 342 }
hooks/pre-commit
+5 -1
hosts/laptop/default.nix
··· 19 19 20 20 myConfig.isDesktop = true; 21 21 myConfig.gaming.enable = true; 22 + myConfig.forgejo.userApiTokenFile = "/home/${config.myConfig.user.username}/.config/forgejo-user-token"; 22 23 23 24 networking.hostName = "laptop"; 24 25 ··· 65 66 User = cfg.user.username; 66 67 WorkingDirectory = "/home/${cfg.user.username}/.config/nix-config"; 67 68 }; 68 - path = with pkgs; [ git nix ]; 69 + path = with pkgs; [ 70 + git 71 + nix 72 + ]; 69 73 script = '' 70 74 if [ -x .git/hooks/pre-commit ]; then 71 75 .git/hooks/pre-commit || exit 1
+12 -4
hosts/macmini/default.nix
··· 20 20 system.primaryUser = cfg.user.username; 21 21 22 22 myConfig.isDesktop = true; 23 + myConfig.forgejo.userApiTokenFile = "/Users/${config.myConfig.user.username}/.config/forgejo-user-token"; 23 24 24 25 networking = { 25 26 hostName = "macmini"; 26 27 computerName = "MacMini"; 27 28 }; 28 - 29 29 30 30 # ── External disk (CT2000X9SSD9, APFS container on disk4s2) ─────────────── 31 31 # See docs/time-machine.md for first-time setup instructions. ··· 34 34 # Tailscale — auto-start at login so SSH ProxyCommand never fails on boot. 35 35 launchd.user.agents."com.tailscale.tailscaled-launcher" = { 36 36 serviceConfig = { 37 - ProgramArguments = [ "/usr/bin/open" "-a" "/Applications/Tailscale.app" ]; 37 + ProgramArguments = [ 38 + "/usr/bin/open" 39 + "-a" 40 + "/Applications/Tailscale.app" 41 + ]; 38 42 RunAtLoad = true; 39 43 KeepAlive = false; 40 44 }; ··· 45 49 # Launch it automatically at login via a launchd user agent instead. 46 50 launchd.user.agents."com.rileytestut.AltServer-launcher" = { 47 51 serviceConfig = { 48 - ProgramArguments = [ "/usr/bin/open" "-a" "/Applications/AltServer.app" ]; 52 + ProgramArguments = [ 53 + "/usr/bin/open" 54 + "-a" 55 + "/Applications/AltServer.app" 56 + ]; 49 57 RunAtLoad = true; 50 - KeepAlive = false; # one-shot: open the app then exit 58 + KeepAlive = false; # one-shot: open the app then exit 51 59 }; 52 60 }; 53 61
+11 -54
modules/darwin/common.nix
··· 2 2 { 3 3 config, 4 4 lib, 5 - pkgs, 6 5 ... 7 6 }: 8 7 let ··· 11 10 { 12 11 programs.zsh.enable = true; 13 12 14 - nix = { 15 - # Explicit nix management via nix-darwin. 16 - # NOTE: Set `nix.enable = false` if you use Determinate Nix, which manages 17 - # the nix daemon itself and conflicts with nix-darwin's native management. 18 - enable = true; 19 - package = pkgs.nix; 20 - 21 - settings = { 22 - experimental-features = [ 23 - "nix-command" 24 - "flakes" 25 - ]; 26 - 27 - # IMPORTANT: Disable store optimisation on macOS. 28 - # `auto-optimise-store = true` triggers a kernel bug on macOS that causes 29 - # build failures: https://github.com/NixOS/nix/issues/7273 30 - # Use `nix store optimise` manually when needed instead. 31 - auto-optimise-store = false; 32 - 33 - # Allow the primary user to use trusted nix operations (e.g. adding 34 - # substituters) without requiring root. 35 - trusted-users = [ 36 - "root" 37 - cfg.user.username 38 - ]; 39 - 40 - # Storage pressure management — keep the Nix store from ballooning on 41 - # a 256 GB disk. Nix will trigger GC automatically when free space on 42 - # the store volume drops below min-free, stopping once max-free is reached. 43 - # Values are in bytes: 5 GiB min-free, 10 GiB max-free. 44 - min-free = 5368709120; # 5 GiB 45 - max-free = 10737418240; # 10 GiB 46 - 47 - # Do not retain build-time inputs or derivations after a successful build. 48 - # These are only needed for `nix develop` / `nix-shell` workflows; keeping 49 - # them on a space-constrained machine is not worth it. 50 - keep-outputs = false; 51 - keep-derivations = false; 52 - }; 53 - 54 - # Automatic garbage collection (macOS launchd schedule) 55 - gc = { 56 - automatic = true; 57 - interval = { 58 - Weekday = 0; 59 - Hour = 2; 60 - Minute = 0; 61 - }; # Every Sunday at 02:00 62 - # Keep only the last 14 days of generations on the space-constrained 63 - # 256 GB Mac. Linux hosts retain 30 days (set in modules/common.nix). 64 - options = "--delete-older-than 14d"; 65 - }; 66 - }; 13 + # NOTE: Set `nix.enable = false` if you use Determinate Nix, which manages 14 + # the nix daemon itself and conflicts with nix-darwin's native management. 15 + nix.enable = false; 67 16 68 17 # NOTE: system.autoUpgrade does not exist in nix-darwin. 69 18 # Run manually: darwin-rebuild switch --flake ~/.config/nix-config#macmini ··· 72 21 # nix-darwin only executes hardcoded script names, so we append to postActivation 73 22 # rather than using a custom name (which would be silently ignored). 74 23 system.activationScripts.postActivation.text = lib.mkAfter '' 24 + # Clean up stale nix-darwin launchd services left over from nix.enable = true. 25 + for svc in org.nixos.nix-daemon org.nixos.nix-gc; do 26 + if /bin/launchctl list "$svc" &>/dev/null; then 27 + /bin/launchctl bootout system "$svc" 2>/dev/null || true 28 + echo "postActivation: booted out stale service $svc" 29 + fi 30 + done 31 + 75 32 REPO="/Users/${cfg.user.username}/.config/nix-config" 76 33 if [ -d "$REPO/.git" ]; then 77 34 ln -sf "$REPO/hooks/pre-commit" "$REPO/.git/hooks/pre-commit"
+10
modules/options.nix
··· 31 31 type = str; 32 32 default = "ewan"; 33 33 }; 34 + githubUsername = mkOption { 35 + type = str; 36 + default = "ewanc26"; 37 + description = "GitHub username for cloning repos into ~/Developer/Git."; 38 + }; 34 39 fullName = mkOption { 35 40 type = str; 36 41 default = "Ewan Croft"; ··· 475 480 disableRegistration = mkOption { 476 481 type = bool; 477 482 default = true; 483 + }; 484 + userApiTokenFile = mkOption { 485 + type = nullStr; 486 + default = null; 487 + description = "Path to a file containing a Forgejo user API token. Used to list private repos for ~/Developer/Local."; 478 488 }; 479 489 }; 480 490