Skip to content

Add Nix-based Mac provisioning with nix-darwin and home-manager - #9

Open
tubone24 wants to merge 10 commits into
masterfrom
claude/ansible-to-nix-setup-R25Om
Open

Add Nix-based Mac provisioning with nix-darwin and home-manager#9
tubone24 wants to merge 10 commits into
masterfrom
claude/ansible-to-nix-setup-R25Om

Conversation

@tubone24

Copy link
Copy Markdown
Owner

Summary

This PR introduces a complete Nix-based provisioning system for macOS using nix-darwin and home-manager, replacing or complementing the existing Ansible-based setup. The configuration enables declarative management of system packages, Homebrew casks, macOS defaults, shell environments, and development tools.

Key Changes

  • Flake configuration (flake.nix): Defines the Nix flake with inputs for nixpkgs, nix-darwin, and home-manager. Supports both aarch64 and x86_64 Darwin systems with a single configuration.

  • System configuration (nix/darwin.nix): Core nix-darwin setup including Nix settings, system packages, zsh shell, hostname configuration, and font management.

  • macOS defaults (nix/macos.nix): Declarative macOS system preferences (Dock, Finder, Trackpad, Screenshot, Menu bar) converted from the existing Ansible configuration.

  • Homebrew integration (nix/homebrew.nix): Manages Homebrew taps, formulae, casks (browsers, IDEs, development tools, utilities), and Mac App Store apps through nix-darwin.

  • Home-manager modules:

    • nix/home.nix: Main home-manager configuration that imports all user-level modules
    • nix/packages.nix: System packages (AWS tools, infrastructure, utilities, databases, multimedia)
    • nix/languages.nix: Language version managers (fnm, pyenv, rbenv, rustup, asdf) and runtimes
    • nix/git.nix: Git configuration with SSH key generation and GitHub CLI tools
    • nix/shell.nix: Zsh and tmux configuration with shell integrations for version managers
    • nix/editors.nix: Neovim setup with AstroNvim and dotfiles initialization
  • Bootstrap script (setup.sh): Automated setup script that installs Nix, Homebrew, and applies the nix-darwin configuration with post-setup instructions.

  • Makefile targets: Added nix-setup, nix-setup-x86, nix-switch, and nix-update targets for easy management.

Notable Implementation Details

  • Uses home-manager.darwinModules for seamless integration with nix-darwin
  • Configures version managers (fnm, pyenv, rbenv) with shell integrations in zsh
  • Automatically generates SSH keys during home-manager activation
  • Clones AstroNvim and dotfiles repositories on first activation
  • Enables experimental Nix features (nix-command, flakes) and automatic garbage collection
  • Supports Touch ID for sudo authentication on macOS
  • Allows unfree packages for proprietary software (Docker, IntelliJ, etc.)

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms

claude added 5 commits March 23, 2026 09:44
Replace Ansible-based provisioning with a declarative Nix configuration:

- flake.nix: Main entry point with nix-darwin + home-manager integration
- nix/darwin.nix: System-level Nix settings, fonts, and Touch ID sudo
- nix/macos.nix: macOS defaults (Dock, Finder, trackpad, screenshots)
- nix/homebrew.nix: Homebrew casks for GUI apps and Mac App Store apps
- nix/packages.nix: CLI tools (awscli, jq, htop, nmap, ffmpeg, etc.)
- nix/languages.nix: Programming languages (Node/fnm, Python/pyenv, Ruby/rbenv, Go, Rust, Deno, Nim, Lua)
- nix/git.nix: Git config with SSH key generation
- nix/shell.nix: Zsh with autosuggestions, syntax highlighting, tmux, direnv
- nix/editors.nix: Neovim with AstroNvim, dotfiles setup
- nix/home.nix: Home-manager entry point
- setup.sh: Bootstrap script for fresh Mac setup
- Makefile: Added nix-setup, nix-switch, nix-update targets

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
- Replace macOS-11 (retired) with macOS-14 and macOS-15 images
- Replace Ansible+Serverspec CI with Nix package verification
- Add ci/verify-nix-packages.sh: checks all nixpkgs packages resolve
- Add ci/verify-homebrew.sh: checks all brew formulas/casks exist
- Add checks output to flake.nix for nix flake check support
- Pipeline steps: install Nix, evaluate flake, build system closure,
  verify individual nix packages, verify homebrew definitions

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
Each Azure Pipelines script step runs in a new shell, so Nix profile
sourcing from the install step is not preserved. Add ci/nix-env.sh as
a shared script that adds Nix binary paths to PATH and sources the
nix-daemon profile. All CI steps now source this before running Nix.

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
Determinate Systems installer dropped x86_64-darwin support, causing
404 errors on Azure Pipelines macOS images (Intel-based). Switch to
the official nixos.org installer with --no-daemon mode.

- azure-pipelines.yml: use official installer, enable flakes via nix.conf
- ci/nix-env.sh: prioritize ~/.nix-profile path for official installer
- setup.sh: use official installer for user-facing setup too

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
--no-daemon installs are no longer supported on Darwin/macOS as of
Nix 2.34.x. Switch to --daemon mode with --yes for non-interactive CI.

- azure-pipelines.yml: use --daemon --yes for multi-user install
- ci/nix-env.sh: prioritize /nix/var/nix/profiles/default for daemon mode
- setup.sh: use --daemon mode for user-facing setup

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Nix flake-based macOS provisioning workflow built on nix-darwin + home-manager, plus CI verification scripts and pipeline updates to validate the Nix/Homebrew definitions.

Changes:

  • Introduces flake.nix with nix-darwin + home-manager configurations for Apple Silicon and x86_64 macOS.
  • Adds nix-darwin modules for macOS defaults, Homebrew casks/formulas, and user-level home-manager setup (packages, languages, shell, git, editors).
  • Adds bootstrap (setup.sh), CI verification scripts, and updates Azure Pipelines + Makefile targets for Nix workflows.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
setup.sh Bootstraps Nix + Homebrew and applies the selected nix-darwin flake configuration.
flake.nix Defines nixpkgs/nix-darwin/home-manager inputs and darwinConfigurations outputs (aarch64 + x86_64).
nix/darwin.nix Core nix-darwin system config (Nix settings, system packages, hostname, user, fonts, TouchID sudo).
nix/macos.nix Declarative macOS defaults (Dock/Finder/Trackpad/Screenshot/Menu bar/NSGlobalDomain).
nix/homebrew.nix nix-darwin Homebrew integration: taps, brews, casks, and MAS apps.
nix/home.nix Home-manager entrypoint importing user-level modules and setting HM state/version.
nix/packages.nix Home-manager CLI/system tool package set.
nix/languages.nix Home-manager language runtimes/version managers package set.
nix/git.nix Home-manager Git configuration and activation-time SSH key generation.
nix/shell.nix Home-manager zsh/tmux/direnv configuration and shell init integrations.
nix/editors.nix Home-manager Neovim setup plus activation-time cloning of editor config and dotfiles.
ci/nix-env.sh Helper to put Nix on PATH for CI scripts.
ci/verify-nix-packages.sh CI script to verify referenced nixpkgs attributes resolve/build (dry-run).
ci/verify-homebrew.sh CI script to verify referenced Homebrew taps/formulas/casks exist.
azure-pipelines.yml Replaces Ansible/Serverspec job with Nix flake evaluation/build + verification scripts on macOS runners.
Makefile Adds Nix-oriented targets (nix-setup, nix-switch, nix-update, etc.).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nix/git.nix
home.activation.generateSshKey = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
mkdir -p "$HOME/.ssh"
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "tubo.yyyuuu@gmail.com"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generates an SSH private key with an empty passphrase (-N "") during activation. That creates an unencrypted key by default, which is a security risk on developer machines and can be surprising behavior for a provisioning run. Consider requiring an explicit opt-in (flag/env var), prompting the user, or generating keys with a passphrase (or skipping key generation entirely).

Suggested change
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "tubo.yyyuuu@gmail.com"
if [ -n "${SSH_KEY_PASSPHRASE-}" ]; then
# Generate key protected with the provided passphrase
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "$SSH_KEY_PASSPHRASE" -C "tubo.yyyuuu@gmail.com"
elif [ "${GENERATE_UNENCRYPTED_SSH_KEY:-}" = "1" ]; then
# Explicitly opted-in to generating an unencrypted key
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "tubo.yyyuuu@gmail.com"
else
echo "Skipping SSH key generation: set SSH_KEY_PASSPHRASE for an encrypted key or GENERATE_UNENCRYPTED_SSH_KEY=1 to allow an unencrypted key." >&2
fi

Copilot uses AI. Check for mistakes.
Comment thread setup.sh
# This script bootstraps Nix, nix-darwin, and applies the configuration.

HOSTNAME="${1:-my-mac}"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HOSTNAME is accepted from argv and used as the flake output selector, but this repo only defines specific darwinConfigurations (e.g., my-mac and my-mac-x86). When a user passes an unknown hostname, the error will be a Nix evaluation failure. Consider validating the argument (allowed set) and printing a clearer error message listing valid hostnames.

Suggested change
# Validate HOSTNAME against the allowed set of darwinConfigurations.
VALID_HOSTNAMES=("my-mac" "my-mac-x86")
if [[ ! " ${VALID_HOSTNAMES[*]} " =~ " ${HOSTNAME} " ]]; then
echo "Error: Unknown hostname '${HOSTNAME}'."
echo "Valid hostnames are:"
for h in "${VALID_HOSTNAMES[@]}"; do
echo " - ${h}"
done
exit 1
fi

Copilot uses AI. Check for mistakes.
Comment thread nix/shell.nix Outdated
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)" 2>/dev/null

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2>/dev/null redirection here only applies to eval, not to the pyenv virtualenv-init - command inside the $(...) substitution. If pyenv-virtualenv isn’t installed, the error output will still appear every shell startup. Consider redirecting stderr inside the command substitution and/or guarding the call based on plugin availability.

Suggested change
eval "$(pyenv virtualenv-init -)" 2>/dev/null
eval "$(pyenv virtualenv-init - 2>/dev/null)"

Copilot uses AI. Check for mistakes.
Comment thread nix/languages.nix Outdated
yarn

# Python - version manager
pyenv

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell.nix runs pyenv virtualenv-init -, but this Nix config only installs pyenv (not pyenv-virtualenv). Unless the Nix pyenv package bundles the virtualenv plugin, this will cause errors on shell startup and missing virtualenv integration. Consider adding pyenv-virtualenv to home.packages (or removing the virtualenv-init line if not needed).

Suggested change
pyenv
pyenv
pyenv-virtualenv

Copilot uses AI. Check for mistakes.
Comment thread flake.nix
Comment on lines +59 to +72
# CI checks - verify the configurations evaluate correctly
checks = let
forAllSystems = nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-darwin" ];
in forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
# Verify the flake configuration evaluates without errors
eval = pkgs.runCommand "eval-check" {} ''
echo "Flake evaluation succeeded for ${system}"
touch $out
'';
}
);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checks.eval currently doesn’t actually evaluate or build any flake outputs—it just echoes a message and touches $out. That means nix flake check will succeed even if the darwin configurations are broken. Consider making this check run a real evaluation/build (e.g., evaluating darwinConfigurations.<host>.config.system.build.toplevel or building the system derivation) for each system.

Suggested change
# CI checks - verify the configurations evaluate correctly
checks = let
forAllSystems = nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-darwin" ];
in forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
# Verify the flake configuration evaluates without errors
eval = pkgs.runCommand "eval-check" {} ''
echo "Flake evaluation succeeded for ${system}"
touch $out
'';
}
);
# CI checks - verify the configurations evaluate correctly by building
# the darwin system derivations for each supported architecture.
checks = {
aarch64-darwin = {
"${hostname}" = self.darwinConfigurations.${hostname}.system;
};
x86_64-darwin = {
"${hostname}-x86" = self.darwinConfigurations."${hostname}-x86".system;
};
};

Copilot uses AI. Check for mistakes.
Comment thread azure-pipelines.yml

. ci/nix-env.sh
echo ">>> Building nix-darwin system closure (this downloads/builds all Nix packages)..."
nix build .#darwinConfigurations.my-mac.system --no-link --print-build-logs 2>&1 | tail -50

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this step, piping nix build output to tail masks the exit status of nix build (the pipeline exit code becomes tail's). As a result, the CI step can pass even when the build fails. Avoid piping, or enable set -o pipefail and capture logs in a way that preserves the build command’s exit code (e.g., tee to a file and tail afterwards).

Suggested change
nix build .#darwinConfigurations.my-mac.system --no-link --print-build-logs 2>&1 | tail -50
nix build .#darwinConfigurations.my-mac.system --no-link --print-build-logs > nix-build.log 2>&1
build_status=$?
tail -50 nix-build.log
if [ "$build_status" -ne 0 ]; then
echo ">>> System closure build failed with exit code $build_status"
exit "$build_status"
fi

Copilot uses AI. Check for mistakes.
Comment thread flake.nix
Comment on lines +41 to +45
# Also support x86_64 Macs
darwinConfigurations."${hostname}-x86" = nix-darwin.lib.darwinSystem {
system = "x86_64-darwin";
specialArgs = { inherit inputs username hostname; };
modules = [

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The x86_64 configuration is selected with the flake output name "${hostname}-x86", but it still passes hostname = "my-mac" into specialArgs. This makes networking.hostName (set from that arg in nix/darwin.nix) disagree with the selected configuration name (my-mac-x86). Consider passing a distinct hostname (e.g., "${hostname}-x86") or deriving the hostname from the configuration key so setup.sh my-mac-x86 results in a matching system hostname.

Copilot uses AI. Check for mistakes.
Comment thread nix/shell.nix Outdated
eval "$(direnv hook zsh)"

# asdf (for Erlang/Elixir)
[ -f "$(brew --prefix asdf 2>/dev/null)/libexec/asdf.sh" ] && . "$(brew --prefix asdf 2>/dev/null)/libexec/asdf.sh"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asdf initialization sources a Homebrew-installed asdf (brew --prefix asdf), but the Nix config installs asdf-vm via home.packages (and homebrew.nix doesn’t install an asdf brew). As written, asdf won’t be initialized. Prefer sourcing the Nix-provided asdf script (or using a home-manager asdf module) instead of relying on Homebrew here.

Suggested change
[ -f "$(brew --prefix asdf 2>/dev/null)/libexec/asdf.sh" ] && . "$(brew --prefix asdf 2>/dev/null)/libexec/asdf.sh"
[ -f "${pkgs.asdf-vm}/share/asdf-vm/asdf.sh" ] && . "${pkgs.asdf-vm}/share/asdf-vm/asdf.sh"

Copilot uses AI. Check for mistakes.
Comment thread ci/verify-homebrew.sh
Comment on lines +9 to +17
check_brew_formula() {
local formula="$1"
if brew info "$formula" >/dev/null 2>&1; then
echo " [PASS] brew: ${formula}"
PASSED=$((PASSED + 1))
else
echo " [FAIL] brew: ${formula}"
FAILED=$((FAILED + 1))
fi

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script assumes brew is available and will fail immediately on environments where Homebrew isn't installed (it calls brew info / brew tap without checking). Consider adding an explicit command -v brew guard with a clear error message (or installing Homebrew as part of the script) so CI/local runs fail with a helpful diagnostic.

Copilot uses AI. Check for mistakes.
Comment thread nix/git.nix
Comment on lines +4 to +8
programs.git = {
enable = true;
userName = "tubone";
userEmail = "tubo.yyyuuu@gmail.com";

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git identity is hard-coded (userName/userEmail). If this provisioning is meant to be reusable across machines/users, this will cause incorrect commit attribution. Consider sourcing these from user-specific variables (passed via extraSpecialArgs, a separate secrets.nix ignored by git, or prompting during bootstrap) instead of committing fixed values.

Copilot uses AI. Check for mistakes.
claude added 5 commits March 24, 2026 04:25
- Remove packer from packages (no longer needed)
- Replace terraform with opentofu (terraform removed from nixpkgs due to BSL)
- Add Claude Code installation step in setup.sh via npm
- Update CI verify script accordingly

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
- Switch from npm install to native installer (curl claude.ai/install.sh)
  which requires no Node.js dependency and auto-updates
- Add tubone24/claude-code-settings plugin installation via its install.sh
- Restore Node.js install to post-setup steps (fnm install 22)

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
- Remove mycli, ruby-build, git-flow (unavailable in nixpkgs)
- Remove fnm, pyenv, rbenv, yarn from languages.nix
- Consolidate Node.js/Python/Ruby/Erlang/Elixir management under asdf-vm
- Update shell.nix: replace fnm/pyenv/rbenv init with asdf init
- Update setup.sh post-setup steps with asdf plugin commands
- Update CI verify script accordingly

https://claude.ai/code/session_01GYSaXuAahUWTqeoUWVzhms
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants