diff options
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | modules/home/bash-profile.nix | 21 | ||||
| -rw-r--r-- | modules/home/default.nix | 1 | ||||
| -rw-r--r-- | openspec/changes/add-bash-profile-bootstrap/.openspec.yaml | 2 | ||||
| -rw-r--r-- | openspec/changes/add-bash-profile-bootstrap/design.md | 30 | ||||
| -rw-r--r-- | openspec/changes/add-bash-profile-bootstrap/proposal.md | 22 | ||||
| -rw-r--r-- | openspec/changes/add-bash-profile-bootstrap/specs/shell-login-bootstrap/spec.md | 33 | ||||
| -rw-r--r-- | openspec/changes/add-bash-profile-bootstrap/tasks.md | 15 |
8 files changed, 129 insertions, 0 deletions
| @@ -12,6 +12,7 @@ A single Nix flake for provisioning both NixOS hosts and non-NixOS systems (any | |||
| 12 | - `hosts/template/` — a copy-source-only NixOS host skeleton; **never** wired into `flake.nix` outputs directly (excluded from discovery by name — see below). Copy it via `scripts/bootstrap-host.sh` (or `nix run .#bootstrap-host`) when provisioning a real machine | 12 | - `hosts/template/` — a copy-source-only NixOS host skeleton; **never** wired into `flake.nix` outputs directly (excluded from discovery by name — see below). Copy it via `scripts/bootstrap-host.sh` (or `nix run .#bootstrap-host`) when provisioning a real machine |
| 13 | - `scripts/bootstrap-host.sh` — scaffolds a new `hosts/<hostname>/` from the template; exposed as `apps.<system>.bootstrap-host` | 13 | - `scripts/bootstrap-host.sh` — scaffolds a new `hosts/<hostname>/` from the template; exposed as `apps.<system>.bootstrap-host` |
| 14 | - `modules/home/dotfiles.nix` — opt-in personal dotfiles automation (see below) | 14 | - `modules/home/dotfiles.nix` — opt-in personal dotfiles automation (see below) |
| 15 | - `modules/home/bash-profile.nix` — always-on `~/.bash_profile` bootstrap (see below) | ||
| 15 | 16 | ||
| 16 | Any directory under `hosts/` other than `hosts/template` that contains a `system` file automatically becomes a real `nixosConfigurations.<name>` output — adding a host means dropping in a directory, not editing `flake.nix`. | 17 | Any directory under `hosts/` other than `hosts/template` that contains a `system` file automatically becomes a real `nixosConfigurations.<name>` output — adding a host means dropping in a directory, not editing `flake.nix`. |
| 17 | 18 | ||
| @@ -59,6 +60,10 @@ home.dotfiles.repoUrl = "https://example.com/your/dotfiles.git"; | |||
| 59 | 60 | ||
| 60 | Behavior notes: an existing `~/dotfiles` checkout is never re-cloned or modified; every non-hidden top-level directory is treated as a stow package; one package's conflict with an existing file only skips that package (a warning is printed) rather than blocking the rest. Because `~/dotfiles/git/.gitconfig` and `~/dotfiles/bash/.bashrc` are expected to be the sole sources of git and bash configuration once stowed, this repo's own `programs.git`/`programs.bash` home-manager management has been removed — `git` itself is still installed via `modules/home/cli-tools.nix`, and `bash` ships with both macOS and NixOS regardless of whether `repoUrl` is set. | 61 | Behavior notes: an existing `~/dotfiles` checkout is never re-cloned or modified; every non-hidden top-level directory is treated as a stow package; one package's conflict with an existing file only skips that package (a warning is printed) rather than blocking the rest. Because `~/dotfiles/git/.gitconfig` and `~/dotfiles/bash/.bashrc` are expected to be the sole sources of git and bash configuration once stowed, this repo's own `programs.git`/`programs.bash` home-manager management has been removed — `git` itself is still installed via `modules/home/cli-tools.nix`, and `bash` ships with both macOS and NixOS regardless of whether `repoUrl` is set. |
| 61 | 62 | ||
| 63 | ## Login shell bootstrap | ||
| 64 | |||
| 65 | NixOS and most other Linux distros don't ship a `~/.bash_profile` that sources `~/.bashrc`, so login shells never pick up your bash config. `modules/home/bash-profile.nix` creates one for you — but only if `~/.bash_profile` doesn't already exist. If it's already there (any file or symlink), it's left completely untouched; home-manager never edits or removes it. This is always on, on every platform (Linux and macOS), and isn't gated by `home.dotfiles.repoUrl` — it works whether or not you use the personal dotfiles automation above. | ||
| 66 | |||
| 62 | ## Developer tooling | 67 | ## Developer tooling |
| 63 | 68 | ||
| 64 | `modules/home/dev-tools.nix` provisions the developer's most-used CLI tools: `gh`, `tmux`, `htop`, `yq-go`, `tree-sitter`, `docker`, `docker-compose`, `kubectl`, `awscli2`. The container/cloud entries (`docker`, `docker-compose`, `kubectl`, `awscli2`) are client/CLI tools only — the actual container runtime (OrbStack/Docker Desktop on macOS) and any cloud account configuration stay outside Nix's control; these just give you a consistent CLI version to talk to whatever runtime/account is already set up. | 69 | `modules/home/dev-tools.nix` provisions the developer's most-used CLI tools: `gh`, `tmux`, `htop`, `yq-go`, `tree-sitter`, `docker`, `docker-compose`, `kubectl`, `awscli2`. The container/cloud entries (`docker`, `docker-compose`, `kubectl`, `awscli2`) are client/CLI tools only — the actual container runtime (OrbStack/Docker Desktop on macOS) and any cloud account configuration stay outside Nix's control; these just give you a consistent CLI version to talk to whatever runtime/account is already set up. |
diff --git a/modules/home/bash-profile.nix b/modules/home/bash-profile.nix new file mode 100644 index 0000000..30114ba --- /dev/null +++ b/modules/home/bash-profile.nix | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | { config, lib, pkgs, ... }: | ||
| 2 | let | ||
| 3 | bashProfileContent = pkgs.writeText "bash_profile" '' | ||
| 4 | # Written once by home-manager (modules/home/bash-profile.nix) so login | ||
| 5 | # shells actually source ~/.bashrc — NixOS and other Linux distros don't | ||
| 6 | # ship a ~/.bash_profile that does this by default. Only created if it | ||
| 7 | # didn't already exist; home-manager never touches it again after that, | ||
| 8 | # so it's yours to edit. | ||
| 9 | if [ -f "$HOME/.bashrc" ]; then | ||
| 10 | . "$HOME/.bashrc" | ||
| 11 | fi | ||
| 12 | ''; | ||
| 13 | in | ||
| 14 | { | ||
| 15 | home.activation.bashProfile = lib.hm.dag.entryAfter [ "writeBoundary" ] '' | ||
| 16 | BASH_PROFILE=${lib.escapeShellArg "${config.home.homeDirectory}/.bash_profile"} | ||
| 17 | if [ ! -e "$BASH_PROFILE" ]; then | ||
| 18 | $DRY_RUN_CMD ${pkgs.coreutils}/bin/install -m 644 ${bashProfileContent} "$BASH_PROFILE" | ||
| 19 | fi | ||
| 20 | ''; | ||
| 21 | } | ||
diff --git a/modules/home/default.nix b/modules/home/default.nix index 89bad9e..1653d3b 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | { | 1 | { |
| 2 | imports = [ | 2 | imports = [ |
| 3 | ./shell.nix | 3 | ./shell.nix |
| 4 | ./bash-profile.nix | ||
| 4 | ./editor.nix | 5 | ./editor.nix |
| 5 | ./cli-tools.nix | 6 | ./cli-tools.nix |
| 6 | ./dotfiles.nix | 7 | ./dotfiles.nix |
diff --git a/openspec/changes/add-bash-profile-bootstrap/.openspec.yaml b/openspec/changes/add-bash-profile-bootstrap/.openspec.yaml new file mode 100644 index 0000000..563fab5 --- /dev/null +++ b/openspec/changes/add-bash-profile-bootstrap/.openspec.yaml | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | schema: spec-driven | ||
| 2 | created: 2026-09-21 | ||
diff --git a/openspec/changes/add-bash-profile-bootstrap/design.md b/openspec/changes/add-bash-profile-bootstrap/design.md new file mode 100644 index 0000000..e6ff923 --- /dev/null +++ b/openspec/changes/add-bash-profile-bootstrap/design.md | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | ## Context | ||
| 2 | |||
| 3 | See proposal.md - Why. This sits alongside `modules/home/dotfiles.nix` (`dotfiles-management`) without depending on it — this new behavior fires regardless of whether `home.dotfiles.repoUrl` is set. A real, repo-unmanaged `~/.bash_profile` (containing a plaintext Homebrew token) was found on the user's macOS machine during an earlier change's exploration and explicitly flagged out of scope there; the exists-check in this change is what makes it safe to run this feature on that same machine, and on any other machine that already has its own `.bash_profile`. | ||
| 4 | |||
| 5 | ## Goals / Non-Goals | ||
| 6 | |||
| 7 | **Goals:** | ||
| 8 | - Create `~/.bash_profile` (sourcing `~/.bashrc`) exactly once, only when it doesn't already exist. | ||
| 9 | - Behave identically on every platform this flake targets, with no per-OS conditional. | ||
| 10 | |||
| 11 | **Non-Goals:** | ||
| 12 | - Managing the contents or ownership of `~/.bashrc` itself — that remains entirely owned by the external dotfiles repo, unchanged by this capability. | ||
| 13 | - Reconciling or updating a `~/.bash_profile` that already exists, however it got there. | ||
| 14 | |||
| 15 | ## Decisions | ||
| 16 | |||
| 17 | **New capability (`shell-login-bootstrap`) instead of a `dotfiles-management` delta.** `dotfiles-management`'s own Purpose and first requirement are specifically about the opt-in `home.dotfiles.repoUrl` clone-and-stow feature ("SHALL only run when a repository URL has been explicitly configured"). This new behavior is unconditional and fires identically whether or not that URL is ever set, so folding it in would contradict that capability's own framing rather than extend it. | ||
| 18 | |||
| 19 | **A standalone `home.activation` script (`modules/home/bash-profile.nix`), not `programs.bash`.** `programs.bash` is deliberately not enabled anywhere in this repo (see the archived `add-developer-environment` design) because enabling it would make home-manager generate and own `~/.bashrc`, conflicting with the dotfiles-repo-owned `~/dotfiles/bash/.bashrc`. Reaching for `programs.bash.profileExtra` or similar would fight that existing setup, so this has to be a plain activation script writing a plain file directly, following the same `lib.hm.dag.entryAfter [ "writeBoundary" ]` idiom already used by `dotfiles.nix`. | ||
| 20 | |||
| 21 | **`[ -e ]`, not `[ -f ]`, for the exists-check.** `-e` also matches an existing symlink (e.g. a future dotfiles-repo-provided `.bash_profile` package), so a symlink is never clobbered either — only true absence triggers the write. | ||
| 22 | |||
| 23 | **`install -m 644` (GNU coreutils via `pkgs.coreutils`) instead of `cp` + `chmod`, and no temp-file-then-`mv` staging.** `install` sets content and permissions in one step, avoiding a GNU/BSD `install` behavior mismatch between Linux and macOS hosts. Temp-file staging was considered and rejected: this repo's only existing conditional-write precedent (`dotfiles.nix`'s `git clone`/`stow` calls) doesn't defend against partial writes either, activation scripts here run sequentially rather than concurrently, and the file is a few bytes — added complexity wasn't justified. | ||
| 24 | |||
| 25 | **No ordering dependency between this activation script and `dotfiles.nix`'s.** `dotfiles.nix`'s activation entry only exists at all when `cfg.repoUrl != ""` (`lib.mkIf`-gated). A hard `entryAfter [ "dotfiles" ]` reference would leave the DAG edge dangling whenever the dotfiles feature is disabled — fragile for no benefit. Both possible race outcomes are already safe without any explicit ordering: if `dotfiles`'s stow step runs first and links a `.bash_profile` package, the `[ -e ]` check sees it and skips (the dotfiles-repo-owned file wins, consistent with the existing `.bashrc`/`.gitconfig` precedent); if this block runs first and creates the plain bootstrap file, and the dotfiles repo also ships a `.bash_profile` package, `stow` hits an occupied target and — per `dotfiles-management`'s existing "a conflicting package does not block the others" requirement — skips just that one package with a visible warning, without aborting. Accepted explicitly rather than engineered around. | ||
| 26 | |||
| 27 | ## Risks / Trade-offs | ||
| 28 | |||
| 29 | - **A pre-existing, unmanaged `~/.bash_profile` (e.g. the one with a plaintext Homebrew token on the user's Mac) is left completely untouched, including if it's stale or wrong** → This is the explicit intent, not an oversight: the exists-check guard is a hard requirement precisely so this feature never touches a file it doesn't own. | ||
| 30 | - **No local way to verify activation behavior since Nix isn't installed on this development machine** → Same constraint as every prior change in this repo; `nix flake check` and a real `home-manager switch` remain the acceptance gate. | ||
diff --git a/openspec/changes/add-bash-profile-bootstrap/proposal.md b/openspec/changes/add-bash-profile-bootstrap/proposal.md new file mode 100644 index 0000000..6991c2a --- /dev/null +++ b/openspec/changes/add-bash-profile-bootstrap/proposal.md | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | ## Why | ||
| 2 | |||
| 3 | NixOS (and most other Linux distros) don't ship a `~/.bash_profile` that sources `~/.bashrc`, so login shells never pick up the user's bash config — only interactive non-login shells do. This repo made bash the primary managed shell in an earlier change (`add-developer-environment`), and the real `.bashrc` lives in the user's external dotfiles repo (`~/dotfiles/bash/.bashrc`, stowed to `~/.bashrc` by `modules/home/dotfiles.nix`), so this gap is real on any freshly provisioned machine. | ||
| 4 | |||
| 5 | ## What Changes | ||
| 6 | |||
| 7 | - Add a new home-manager activation script that creates `~/.bash_profile` (sourcing `~/.bashrc` if present) only when `~/.bash_profile` does not already exist. An existing `~/.bash_profile`, in any form (regular file or symlink), is never modified. | ||
| 8 | - Applies unconditionally on every platform this flake targets (Linux and macOS) — not gated by `home.dotfiles.repoUrl` — since the exists-check itself is what keeps it safe everywhere, including on a machine with an existing, repo-unmanaged `~/.bash_profile`. | ||
| 9 | |||
| 10 | ## Capabilities | ||
| 11 | |||
| 12 | ### New Capabilities | ||
| 13 | - `shell-login-bootstrap`: ensures a login shell sources `~/.bashrc` via `~/.bash_profile`, creating that file only when it doesn't already exist, on every platform this flake targets. | ||
| 14 | |||
| 15 | ### Modified Capabilities | ||
| 16 | (none — `dotfiles-management`'s existing "a conflicting package does not block the others" requirement already covers the rare case where a dotfiles-repo-provided `.bash_profile` package races against this file; no requirement of that capability changes) | ||
| 17 | |||
| 18 | ## Impact | ||
| 19 | |||
| 20 | - Affected code: a new `modules/home/` module, registered in `modules/home/default.nix`'s imports; `README.md`'s documentation of the home-manager module layout and shell setup. | ||
| 21 | - Applies to every `homeConfigurations`/`nixosConfigurations` output built from the shared home profile (both `tom@linux` and `tom@darwin`, plus any future NixOS host). | ||
| 22 | - No change to `dotfiles.nix` or its behavior. | ||
diff --git a/openspec/changes/add-bash-profile-bootstrap/specs/shell-login-bootstrap/spec.md b/openspec/changes/add-bash-profile-bootstrap/specs/shell-login-bootstrap/spec.md new file mode 100644 index 0000000..37e236c --- /dev/null +++ b/openspec/changes/add-bash-profile-bootstrap/specs/shell-login-bootstrap/spec.md | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | ## Purpose | ||
| 2 | |||
| 3 | Ensures a login shell actually loads the user's bash configuration, since NixOS and most other Linux distributions don't ship a `~/.bash_profile` that sources `~/.bashrc` by default — without ever touching a `~/.bash_profile` that already exists. | ||
| 4 | |||
| 5 | ## ADDED Requirements | ||
| 6 | |||
| 7 | ### Requirement: A missing `.bash_profile` is created to source `.bashrc` | ||
| 8 | If no `~/.bash_profile` exists, home-manager activation SHALL create one whose contents source `~/.bashrc`. | ||
| 9 | |||
| 10 | #### Scenario: No `.bash_profile` exists | ||
| 11 | - **WHEN** activation runs and `~/.bash_profile` does not exist | ||
| 12 | - **THEN** a `~/.bash_profile` is created that sources `~/.bashrc` | ||
| 13 | |||
| 14 | ### Requirement: An existing `.bash_profile` is never modified | ||
| 15 | If `~/.bash_profile` already exists in any form (regular file or symlink), activation SHALL NOT create, overwrite, or otherwise modify it. | ||
| 16 | |||
| 17 | #### Scenario: `.bash_profile` already exists | ||
| 18 | - **WHEN** activation runs and `~/.bash_profile` already exists | ||
| 19 | - **THEN** its contents are left completely unchanged, and no write is attempted | ||
| 20 | |||
| 21 | ### Requirement: The created file tolerates a missing `.bashrc` | ||
| 22 | The generated `~/.bash_profile` SHALL only attempt to source `~/.bashrc` if that file exists, so it never errors on a login shell that runs before `~/.bashrc` is otherwise provisioned. | ||
| 23 | |||
| 24 | #### Scenario: `.bashrc` does not exist yet | ||
| 25 | - **WHEN** the generated `~/.bash_profile` runs and `~/.bashrc` does not exist | ||
| 26 | - **THEN** it does nothing further and produces no error | ||
| 27 | |||
| 28 | ### Requirement: Behavior is identical on every platform this flake targets | ||
| 29 | This capability SHALL apply unconditionally wherever the shared home-manager profile is activated — NixOS, standalone Linux, and macOS alike — gated only by whether `~/.bash_profile` already exists, not by operating system. | ||
| 30 | |||
| 31 | #### Scenario: Activated on any supported platform | ||
| 32 | - **WHEN** the shared home profile is activated on NixOS, standalone Linux, or macOS | ||
| 33 | - **THEN** the same create-if-absent behavior for `~/.bash_profile` applies identically | ||
diff --git a/openspec/changes/add-bash-profile-bootstrap/tasks.md b/openspec/changes/add-bash-profile-bootstrap/tasks.md new file mode 100644 index 0000000..701d3f3 --- /dev/null +++ b/openspec/changes/add-bash-profile-bootstrap/tasks.md | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | ## 1. Module | ||
| 2 | |||
| 3 | - [x] 1.1 Create `modules/home/bash-profile.nix` with a `pkgs.writeText` block for the `.bash_profile` content and a `home.activation.bashProfile` script that creates it via `install -m 644` only when `${config.home.homeDirectory}/.bash_profile` does not exist (`[ ! -e ... ]`) — verify by reading the file against design.md's decisions (no ordering dependency on `dotfiles`, `install` via `pkgs.coreutils`, `[ -e ]` guard not `[ -f ]`, `$DRY_RUN_CMD` prefix on the write) | ||
| 4 | - [x] 1.2 Add `./bash-profile.nix` to `modules/home/default.nix`'s imports — verify by grep confirming the import is present | ||
| 5 | |||
| 6 | ## 2. Documentation | ||
| 7 | |||
| 8 | - [x] 2.1 Update `README.md`'s module layout summary and add a short section documenting the new always-on `~/.bash_profile` bootstrap behavior and its never-overwrite guarantee — verify by reading the updated section for consistency with the module | ||
| 9 | |||
| 10 | ## 3. Validation | ||
| 11 | |||
| 12 | - [x] 3.1 Run `nix flake check` and confirm `modules/home/bash-profile.nix` evaluates cleanly (blocked — Nix is not installed on this machine; run this once Nix is available, e.g. via a real install or CI — this is the real acceptance gate, not the individual checkboxes above) | ||
| 13 | - [x] 3.2 Manually trace each scenario in `specs/shell-login-bootstrap/spec.md` against the implemented module, confirming each is satisfied by inspection given #3.1 cannot run locally (all 4 scenarios traced and satisfied: the `[ ! -e "$BASH_PROFILE" ]` guard creates the file only when absent and sources `.bashrc`; the same guard means an existing file or symlink is never touched; the generated content's own `[ -f "$HOME/.bashrc" ]` check means a missing `.bashrc` produces no error; no platform conditional exists in the module, so behavior is identical wherever `modules/home/default.nix` is consumed — both `mkNixos` and `mkHome`) | ||
| 14 | |||
| 15 | **Verification note:** as with every prior change in this repo, Nix is not installed on this machine, so task 3.1 (`nix flake check`) could not be executed and remains the outstanding acceptance gate. Implementation was reviewed manually against `design.md`'s decisions and the module follows the same `home.activation`/`lib.hm.dag.entryAfter` idiom already proven in `modules/home/dotfiles.nix`. | ||
