From 0afecb4b0e4cff5be2673553163379cb95578460 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Sun, 23 Aug 2026 17:44:25 -0300 Subject: [PATCH] feat(nix): add Linux Home Manager profile --- .github/workflows/ci.yml | 2 +- README.md | 53 ++++++++++++----------- fish/conf.d/10-core.fish | 5 ++- fish/tests/w2-core.fish | 5 +++ flake.nix | 6 +++ nix/home.nix | 65 ++++++++++++++++------------ scripts/nix-preflight.sh | 30 +++++++------ scripts/test-nix-preflight.sh | 80 +++++++++++++++++++++++++++-------- 8 files changed, 161 insertions(+), 85 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30bfa50..8a10fd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: - name: Check installer symlink safety run: ./scripts/check-install-symlinks.sh - - name: Check pure Nix preflight contract + - name: Check Nix Home Manager preflight contracts run: bash scripts/test-nix-preflight.sh - name: Validate TOML configs diff --git a/README.md b/README.md index 4cfdf10..b8077c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # dotfiles -Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de AI CLI y herramientas de desarrollo para macOS. +Configuración local extraída de `cortex`: terminal, shell, prompt, helpers de AI CLI y herramientas de desarrollo para macOS, con un primer perfil Home Manager para Linux headless. ## CI @@ -54,41 +54,44 @@ El instalador macOS: 5. Intenta arrancar/recargar `sketchybar`, `yabai` y `skhd` sin cortar la instalación si macOS requiere permisos 6. Crea `local/env.zsh` desde el template -## Límite Nix + Home Manager (W1) +## Home Manager: límites y validación -W1 agrega solamente una base de Flake y Home Manager al repositorio. No instala Nix, no genera `flake.lock`, no activa Home Manager y no cambia ningún archivo del host. +La Flake declara configuraciones puras para macOS y Linux como parte de [cortex-dotfiles #58](https://github.com/barbatdev/cortex-dotfiles/issues/58). Esta unidad no instala Nix, no activa Home Manager, no cambia el login shell y no modifica archivos del host. `flake.lock` ya está versionado y las comprobaciones no lo escriben. -### Verificación segura ahora +### Camino rápido para Linux headless -```bash -bash scripts/nix-preflight.sh -bash scripts/test-nix-preflight.sh -bash -n scripts/nix-preflight.sh scripts/test-nix-preflight.sh -``` +Objetivo: Ubuntu 24.04 `x86_64` con el perfil `homeConfigurations.jbarbat-linux`. -El preflight es de solo lectura y sin red: usa `--offline` y `--no-write-lock-file`. En una máquina sin Nix termina con estado no cero de forma esperada, pero no cambia el host. Su contrato de readiness exige macOS `arm64` o `x86_64`, un usuario y `HOME` válidos, Nix disponible y los inputs bloqueados ya disponibles localmente. Una configuración Fish existente es una advertencia, no un bloqueo: W1 no la administra. +1. Use un checkout limpio; no actualice un checkout remoto existente en el lugar. +2. Instale Nix fuera de este repositorio y verifique sus inputs ya bloqueados localmente. +3. Ejecute las comprobaciones de solo lectura: -### Ownership y próximo bootstrap + ```bash + bash scripts/nix-preflight.sh + bash scripts/test-nix-preflight.sh + bash -n scripts/nix-preflight.sh scripts/test-nix-preflight.sh + ``` -| Área | Owner en W1 | -| --- | --- | -| `/opt/homebrew/bin/fish` | Homebrew actual | -| Symlinks, servicios y fuentes actuales | `install.sh` | -| Zsh y Ghostty | Configuración actual, sin cambios | -| `~/.config/fish/conf.d/99-local.fish` | Host; reservado para secretos/estado privado futuro, no creado ni gestionado por Nix | -| `flake.nix` y `nix/home.nix` | Base inactiva de Home Manager | +4. Espere la verificación independiente antes de cualquier activación. La instalación, `home-manager switch`, `nix run ... switch`, `chsh` y servicios quedan fuera de esta entrega. -El bootstrap queda explícitamente diferido a un work unit revisado. Después de instalar Nix por fuera de este repositorio, ese trabajo podrá generar el lock con: +El preflight selecciona solamente `Darwin arm64` o `Linux x86_64`, usa `--offline` y `--no-write-lock-file`. En una máquina sin Nix termina con estado no cero de forma esperada y sin cambios. Rechaza sistemas o arquitecturas no soportados. -```bash -nix --extra-experimental-features 'nix-command flakes' flake lock -``` +### Configuraciones y ownership + +| Área | Decisión | +| --- | --- | +| macOS | `homeConfigurations.jbarbat` conserva `aarch64-darwin`, `/Users/jbarbat` y la compatibilidad existente. | +| Linux | La configuración pura para Linux es `homeConfigurations.jbarbat-linux`: usa `x86_64-linux`, `/home/jbarbat` y declara solo `fish` y `starship` como runtime mínimo. | +| Fish compartido | Home Manager mapea cada archivo declarado de forma explícita; no gestiona el directorio completo, historial ni variables Fish. | +| Override privado | `~/.config/fish/conf.d/99-local.fish` sigue siendo del host, queda fuera del store y no se lee ni crea. | +| Links no declarados | Los links o archivos previos sin declarar permanecen bajo ownership del host; esta entrega no elimina nada. | +| Zsh | Permanece disponible como rollback; no se cambia el shell de login. | -Ese comando resuelve inputs y modifica `flake.lock`; por eso no es parte de W1. También quedan diferidos cualquier `home-manager switch`, `nix run ... switch`, instalación de paquetes, cambio de shell, `chsh`, servicios o extensiones Fish fuera del core W2. +Linux excluye screenshots y clipboard (`_screenshots_*`, `_time_ago`, `ss`, `last`, `ssd`, `imgclip` y `ccclip`). No instala backends gráficos ni gestiona funciones macOS. También están fuera de alcance Yabai, scripting addition/SIP/Dock, skhd, SketchyBar, Karabiner, tmux, Ghostty, herramientas X11/Wayland y flujos de permisos o servicios macOS. -La configuración pura de W1 es `homeConfigurations.jbarbat`: declara `username = "jbarbat"`, `homeDirectory = "/Users/jbarbat"` y `system = "aarch64-darwin"` de forma explícita y revisable. Una futura activación debe seleccionar ese target sin derivar valores de la máquina en tiempo de evaluación. +### Rollback -Para volver atrás de W1 basta quitar `flake.nix`, `nix/`, `scripts/nix-preflight.sh`, `scripts/test-nix-preflight.sh` y esta sección. No hay estado de host que revertir porque W1 no activó nada. +Antes de una activación no existe estado de host que revertir. Después de una futura activación, el límite de rollback es el perfil `homeConfigurations.jbarbat-linux` y los archivos Fish explícitamente declarados; Zsh y `99-local.fish` permanecen fuera de ese límite. No elimine overrides privados ni links no declarados. ## Fish core profile (W2) diff --git a/fish/conf.d/10-core.fish b/fish/conf.d/10-core.fish index 2ea1e16..0e28632 100644 --- a/fish/conf.d/10-core.fish +++ b/fish/conf.d/10-core.fish @@ -57,7 +57,10 @@ alias .. 'cd ..' alias ... 'cd ../..' alias .... 'cd ../../..' alias c clear -alias o open + +if test (uname) = Darwin + alias o open +end if command -q eza alias ll 'eza -la --icons --git' diff --git a/fish/tests/w2-core.fish b/fish/tests/w2-core.fish index 6eafc96..85b8214 100644 --- a/fish/tests/w2-core.fish +++ b/fish/tests/w2-core.fish @@ -82,6 +82,11 @@ set -l aliases (run_fish -c 'functions gs; functions gpl' | string collect) string match -q '*git status*' "$aliases"; or fail 'gs alias' string match -q '*git pull*' "$aliases"; or fail 'gpl alias' +printf '#!/bin/sh\nprintf "%s\\n" Linux\n' >"$bin/uname" +chmod +x "$bin/uname" +set -l linux_open_alias (run_fish -c 'functions -q o; and echo defined; or echo absent') +assert_equal "$linux_open_alias" absent 'Linux Fish startup excludes the macOS open alias' + printf '#!/bin/sh\n[ "$1" = init ] && printf "set -gx W2_STARSHIP_READY 1\\n"\n' >"$bin/starship" chmod +x "$bin/starship" set -l noninteractive (run_fish -c 'set -q W2_STARSHIP_READY; and echo ready; or echo skipped') diff --git a/flake.nix b/flake.nix index 9983641..1499e89 100644 --- a/flake.nix +++ b/flake.nix @@ -26,5 +26,11 @@ homeDirectory = "/Users/jbarbat"; system = "aarch64-darwin"; }; + + homeConfigurations.jbarbat-linux = mkHomeConfiguration { + username = "jbarbat"; + homeDirectory = "/home/jbarbat"; + system = "x86_64-linux"; + }; }; } diff --git a/nix/home.nix b/nix/home.nix index c4b0cb9..777379d 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -1,23 +1,7 @@ -{ username, homeDirectory, system, ... }: +{ username, homeDirectory, system, pkgs, lib, ... }: -{ - assertions = [ - { - assertion = username != "" && homeDirectory != ""; - message = "homeConfigurations.jbarbat must provide explicit non-empty username and homeDirectory values."; - } - ]; - - home = { - username = username; - homeDirectory = homeDirectory; - stateVersion = "24.11"; - }; - - programs.home-manager.enable = true; - - xdg.configFile = { - "nix/nix.conf".text = "experimental-features = nix-command flakes\n"; +let + sharedFishFiles = { "fish/conf.d/10-core.fish".source = ../fish/conf.d/10-core.fish; "fish/functions/_go_dev_dir.fish".source = ../fish/functions/_go_dev_dir.fish; "fish/functions/_go_first_existing_dir.fish".source = ../fish/functions/_go_first_existing_dir.fish; @@ -67,13 +51,6 @@ "fish/functions/sshc.fish".source = ../fish/functions/sshc.fish; "fish/functions/sshx.fish".source = ../fish/functions/sshx.fish; "fish/functions/sshx-doctor.fish".source = ../fish/functions/sshx-doctor.fish; - "fish/functions/_screenshots_dir.fish".source = ../fish/functions/_screenshots_dir.fish; - "fish/functions/_screenshot_files.fish".source = ../fish/functions/_screenshot_files.fish; - "fish/functions/_time_ago.fish".source = ../fish/functions/_time_ago.fish; - "fish/functions/ss.fish".source = ../fish/functions/ss.fish; - "fish/functions/last.fish".source = ../fish/functions/last.fish; - "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; - "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; "fish/functions/_cortex_resolve_target.fish".source = ../fish/functions/_cortex_resolve_target.fish; "fish/functions/_cortex_run_agent.fish".source = ../fish/functions/_cortex_run_agent.fish; "fish/functions/cc.fish".source = ../fish/functions/cc.fish; @@ -81,6 +58,42 @@ "fish/functions/ocb.fish".source = ../fish/functions/ocb.fish; "fish/functions/ccx.fish".source = ../fish/functions/ccx.fish; "fish/functions/ccd.fish".source = ../fish/functions/ccd.fish; + }; + + darwinFishFiles = { + "fish/functions/_screenshots_dir.fish".source = ../fish/functions/_screenshots_dir.fish; + "fish/functions/_screenshot_files.fish".source = ../fish/functions/_screenshot_files.fish; + "fish/functions/_time_ago.fish".source = ../fish/functions/_time_ago.fish; + "fish/functions/ss.fish".source = ../fish/functions/ss.fish; + "fish/functions/last.fish".source = ../fish/functions/last.fish; + "fish/functions/ssd.fish".source = ../fish/functions/ssd.fish; + "fish/functions/imgclip.fish".source = ../fish/functions/imgclip.fish; "fish/functions/ccclip.fish".source = ../fish/functions/ccclip.fish; }; +in +{ + assertions = [ + { + assertion = username != "" && homeDirectory != ""; + message = "Home Manager configurations must provide explicit non-empty username and homeDirectory values."; + } + { + assertion = system == "aarch64-darwin" || system == "x86_64-linux"; + message = "Only aarch64-darwin and x86_64-linux Home Manager configurations are supported."; + } + ]; + + home = { + username = username; + homeDirectory = homeDirectory; + stateVersion = "24.11"; + packages = lib.optionals pkgs.stdenv.isLinux [ pkgs.fish pkgs.starship ]; + }; + + programs.home-manager.enable = true; + + xdg.configFile = { + "nix/nix.conf".text = "experimental-features = nix-command flakes\n"; + } + // sharedFishFiles // lib.optionalAttrs (!pkgs.stdenv.isLinux) darwinFishFiles; } diff --git a/scripts/nix-preflight.sh b/scripts/nix-preflight.sh index 5adc9b4..d2c9a57 100755 --- a/scripts/nix-preflight.sh +++ b/scripts/nix-preflight.sh @@ -34,18 +34,18 @@ else fail "HOME must be an existing absolute directory; received: $home_directory" fi -if [[ "$host_os" == "Darwin" ]]; then - pass 'Host platform: Darwin.' -else - fail "W1 currently targets macOS; detected platform: $host_os" -fi - -case "$host_architecture" in -arm64 | x86_64) - pass "Supported macOS architecture: $host_architecture" +home_configuration='' +case "$host_os:$host_architecture" in +Darwin:arm64) + home_configuration='jbarbat' + pass 'Host platform: Darwin arm64 (homeConfigurations.jbarbat).' + ;; +Linux:x86_64) + home_configuration='jbarbat-linux' + pass 'Host platform: Linux x86_64 (homeConfigurations.jbarbat-linux).' ;; *) - fail "Unsupported macOS architecture for W1: $host_architecture" + fail "Unsupported platform: $host_os $host_architecture. Supported targets are Darwin arm64 and Linux x86_64." ;; esac @@ -63,10 +63,12 @@ else if nix --extra-experimental-features 'nix-command flakes' flake metadata --no-write-lock-file --offline "$repo_root" >/dev/null 2>&1; then pass 'Flake features and the local lock/input metadata are ready for read-only evaluation.' - if nix --extra-experimental-features 'nix-command flakes' eval --no-write-lock-file --offline --raw "$repo_root#homeConfigurations.jbarbat.activationPackage.drvPath" >/dev/null 2>&1; then - pass 'Pure Home Manager configuration jbarbat is available through the flake input.' - else - fail 'Pure Home Manager configuration jbarbat cannot be evaluated locally; generate flake.lock during the future bootstrap, then retry.' + if [[ -n "$home_configuration" ]]; then + if nix --extra-experimental-features 'nix-command flakes' eval --no-write-lock-file --offline --raw "$repo_root#homeConfigurations.$home_configuration.activationPackage.drvPath" >/dev/null 2>&1; then + pass "Pure Home Manager configuration $home_configuration is available through the flake input." + else + fail "Pure Home Manager configuration $home_configuration cannot be evaluated locally; generate flake.lock during the future bootstrap, then retry." + fi fi else fail 'Flake metadata is not available offline; generate flake.lock during the future bootstrap, then retry.' diff --git a/scripts/test-nix-preflight.sh b/scripts/test-nix-preflight.sh index ca5f5b1..10143e7 100755 --- a/scripts/test-nix-preflight.sh +++ b/scripts/test-nix-preflight.sh @@ -27,31 +27,53 @@ assert_not_contains() { fi } -assert_pure_w1_configuration() { +assert_pure_home_manager_configurations() { assert_contains ' mkHomeConfiguration = { username, homeDirectory, system }:' "$repo_root/flake.nix" assert_contains ' homeConfigurations.jbarbat = mkHomeConfiguration {' "$repo_root/flake.nix" assert_contains ' username = "jbarbat";' "$repo_root/flake.nix" assert_contains ' homeDirectory = "/Users/jbarbat";' "$repo_root/flake.nix" assert_contains ' system = "aarch64-darwin";' "$repo_root/flake.nix" - assert_contains '{ username, homeDirectory, system, ... }:' "$repo_root/nix/home.nix" - assert_contains ' - name: Check pure Nix preflight contract' "$repo_root/.github/workflows/ci.yml" - grep -Fq 'La configuración pura de W1 es `homeConfigurations.jbarbat`:' "$repo_root/README.md" || - fail 'README does not document the pure W1 configuration' - - for forbidden in 'home.packages' 'programs.fish' 'services.' 'home.activation'; do - assert_not_contains "$forbidden" "$repo_root/nix/home.nix" + assert_contains ' homeConfigurations.jbarbat-linux = mkHomeConfiguration {' "$repo_root/flake.nix" + assert_contains ' homeDirectory = "/home/jbarbat";' "$repo_root/flake.nix" + assert_contains ' system = "x86_64-linux";' "$repo_root/flake.nix" + assert_contains '{ username, homeDirectory, system, pkgs, lib, ... }:' "$repo_root/nix/home.nix" + assert_contains ' sharedFishFiles = {' "$repo_root/nix/home.nix" + assert_contains ' darwinFishFiles = {' "$repo_root/nix/home.nix" + assert_contains ' packages = lib.optionals pkgs.stdenv.isLinux [ pkgs.fish pkgs.starship ];' "$repo_root/nix/home.nix" + assert_contains ' // sharedFishFiles // lib.optionalAttrs (!pkgs.stdenv.isLinux) darwinFishFiles;' "$repo_root/nix/home.nix" + assert_contains ' - name: Check Nix Home Manager preflight contracts' "$repo_root/.github/workflows/ci.yml" + grep -Fq 'La configuración pura para Linux es `homeConfigurations.jbarbat-linux`:' "$repo_root/README.md" || + fail 'README does not document the Linux Home Manager configuration' + + for forbidden in 'builtins.currentSystem' 'builtins.getEnv'; do + assert_not_contains "$forbidden" "$repo_root/flake.nix" done - assert_not_contains 'builtins.currentSystem' "$repo_root/flake.nix" - assert_not_contains 'builtins.getEnv' "$repo_root/flake.nix" - for file in "$repo_root/scripts/nix-preflight.sh" "$repo_root/.github/workflows/ci.yml" "$repo_root/README.md"; do assert_not_contains '--impure' "$file" done } +assert_linux_fish_exclusions() { + local shared_sources + local darwin_sources + + shared_sources="$(sed -n '/ sharedFishFiles = {/,/ };/p' "$repo_root/nix/home.nix")" + darwin_sources="$(sed -n '/ darwinFishFiles = {/,/ };/p' "$repo_root/nix/home.nix")" + + for source in '_screenshots_dir.fish' '_screenshot_files.fish' '_time_ago.fish' 'ss.fish' 'last.fish' 'ssd.fish' 'imgclip.fish' 'ccclip.fish'; do + if grep -Fq -- "$source" <<<"$shared_sources"; then + fail "Linux Fish ownership must exclude $source" + fi + grep -Fq -- "$source" <<<"$darwin_sources" || + fail "Darwin Fish ownership must retain $source" + done +} + make_fake_commands() { local bin_dir="$1" + local fake_os="$2" + local fake_architecture="$3" mkdir -p "$bin_dir" @@ -62,11 +84,11 @@ if [ "$1" = "-un" ]; then fi EOF - cat >"$bin_dir/uname" <<'EOF' + cat >"$bin_dir/uname" <"$output" 2>&1 } -assert_pure_w1_configuration +assert_pure_home_manager_configurations +assert_linux_fish_exclusions missing_nix_home="$tmp_dir/missing-nix-home" missing_nix_bin="$tmp_dir/missing-nix-bin" missing_nix_output="$tmp_dir/missing-nix.out" mkdir -p "$missing_nix_home" "$missing_nix_bin" -make_fake_commands "$missing_nix_bin" +make_fake_commands "$missing_nix_bin" Darwin arm64 if run_preflight "$missing_nix_home" "$missing_nix_bin" "$missing_nix_output"; then fail 'preflight unexpectedly passed without nix' @@ -99,7 +122,7 @@ ready_home="$tmp_dir/ready-home" ready_bin="$tmp_dir/ready-bin" ready_output="$tmp_dir/ready.out" mkdir -p "$ready_home" "$ready_bin" -make_fake_commands "$ready_bin" +make_fake_commands "$ready_bin" Darwin arm64 cat >"$ready_bin/nix" <<'EOF' #!/bin/sh printf '%s\n' "$*" >>"$NIX_FAKE_LOG" @@ -118,6 +141,27 @@ if grep -F -- '--impure' "$tmp_dir/nix.log" >/dev/null; then fail 'preflight evaluated the configuration impurely' fi +linux_home="$tmp_dir/linux-home" +linux_bin="$tmp_dir/linux-bin" +linux_output="$tmp_dir/linux.out" +mkdir -p "$linux_home" "$linux_bin" +make_fake_commands "$linux_bin" Linux x86_64 +cp "$ready_bin/nix" "$linux_bin/nix" +NIX_FAKE_LOG="$tmp_dir/linux-nix.log" run_preflight "$linux_home" "$linux_bin" "$linux_output" +assert_contains 'PASS Host platform: Linux x86_64 (homeConfigurations.jbarbat-linux).' "$linux_output" +assert_contains 'PASS Pure Home Manager configuration jbarbat-linux is available through the flake input.' "$linux_output" +grep -F -- '#homeConfigurations.jbarbat-linux.activationPackage.drvPath' "$tmp_dir/linux-nix.log" >/dev/null || fail 'preflight did not evaluate the Linux configuration' + +unsupported_home="$tmp_dir/unsupported-home" +unsupported_bin="$tmp_dir/unsupported-bin" +unsupported_output="$tmp_dir/unsupported.out" +mkdir -p "$unsupported_home" "$unsupported_bin" +make_fake_commands "$unsupported_bin" Linux aarch64 +if run_preflight "$unsupported_home" "$unsupported_bin" "$unsupported_output"; then + fail 'preflight unexpectedly passed on an unsupported Linux architecture' +fi +assert_contains 'FAIL Unsupported platform: Linux aarch64. Supported targets are Darwin arm64 and Linux x86_64.' "$unsupported_output" + mkdir -p "$ready_home/.config/fish/conf.d" printf '%s\n' '# host-owned local configuration' >"$ready_home/.config/fish/conf.d/99-local.fish" collision_output="$tmp_dir/collision.out"