About six months ago, on a roommate’s recommendation, I moved my PC to NixOS. The timing was practical: I had broken my Ubuntu desktop badly enough that reinstalling felt easier than repairing it.
NixOS is a Linux distribution built around the Nix package manager, which enables declarative configurations and reproducible builds.
NixOS made package and configuration management feel less ad hoc, so I made it the main OS on my PC. Later, after getting comfortable with Nix, I installed it on my MacBook as well. The aim was a consistent development environment across NixOS and macOS:
- Using Nix to set up identical global development environments on both systems, such as specific Python interpreter or Java compiler versions.
- Defining a shared shell environment (like zsh) on both, so I can reuse shell scripts, variables, and aliases.
The full configuration for this setup is available here.
What I Aimed to Achieve
I was looking for a solution that could:
- Ensure identical package versions across different platforms.
- Declare my entire development stack as code for easy replication.
- Support advanced parallel computing setups, including OpenCL, OpenMP, and MPI.
- Allow for platform-specific configurations where necessary.
My Setup Architecture
To achieve this integration, I’ve structured my Nix configurations in a modular way. Here’s an overview of the directory layout:
nix/├── home-manager/│ ├── shared/│ │ └── programming.nix # Cross-platform packages│ ├── linux/│ │ └── home.nix # Linux-specific config│ └── darwin/│ └── home.nix # macOS-specific config├── nixos/│ └── configuration.nix # NixOS system config├── nix-darwin/│ └── configuration.nix # macOS system config└── flake.nix # Main configuration entry pointThis structure keeps shared elements centralized while allowing for OS-specific tweaks.
Shared Programming Environment
At the heart of my setup is the programming.nix file, which defines development tools that work consistently across both platforms. This ensures I have the same versions of everything, from compilers to libraries.
# Programming environment packages that work on both platforms{ config, pkgs, ... }:
{ home.packages = with pkgs; [ # Development tools git wget curl
# Programming languages and runtimes nodejs typescript (python3.withPackages (ps: with ps; [ numpy pandas scipy matplotlib scikit-learn jupyterlab ]))
# Java Development jdk21 maven gradle
# C/C++ Development gcc cmake gdb lldb pkg-config
# Parallel programming stack mpi llvmPackages.openmp opencl-headers opencl-clhpp ocl-icd clinfo
# Documentation and text processing pandoc typst
# Development environments docker ];
home.sessionVariables = { JAVA_HOME = "${pkgs.jdk21}"; CC = "${pkgs.gcc}/bin/gcc"; CXX = "${pkgs.gcc}/bin/g++"; };}This configuration pulls in the tools I use most often: Python packages for data work, Java tooling for backend work, and OpenCL/OpenMP/MPI components for parallel computing.
Integrating Homebrew with Nix on macOS
While Nix handles most of my command-line tools, I rely on Homebrew for GUI applications and certain macOS-specific packages not available in Nix. To keep everything declarative, I integrate Homebrew directly into my Nix configuration using the homebrew module in nix-darwin.
# Homebrew configuration for macOS{ config, pkgs, ... }:
{ # Homebrew declarative configuration homebrew = { enable = true;
# App installation preferences onActivation = { autoUpdate = true; # Update homebrew itself upgrade = true; # Upgrade all packages to latest versions cleanup = "zap"; # Uninstall packages not listed in config
# Additional update options (optional) extraFlags = [ "--verbose" # Show detailed output during updates ]; };
# Global homebrew settings global = { brewfile = true; # Use Brewfile for management lockfiles = false; # Don't create lock files };
# GUI Applications from your brew list casks = [ # Cloud storage and sync "baidunetdisk" "google-drive" "nutstore"
# Learning and education "eudic" "pdf-expert"
# Browsers & web clients "firefox" "google-chrome" "bilibili"
# Development tools "github" "jetbrains-toolbox" "visual-studio-code" "cursor" "kate"
# macUI #"sketchybar" see home manager "font-hack-nerd-font"
# Design and productivity "canva" "figma" "obsidian" "typora"
# Communication "qq" "wechat" "whatsapp" "microsoft-teams" "telegram-desktop"
# AI and productivity "chatgpt" "cherry-studio"
# Network and system tools "clash-verge-rev" #"stats"
# Office and productivity "zoom" "slack" "microsoft-auto-update"
# Gaming and entertainment "steam"
# Academic and research "zotero"
# Statistical computing and analysis "r" "rstudio"
# Document preparation "mactex" # Full MacTeX distribution includes TeXLive ];
# Formulae (command-line tools) - keeping empty for now since most are handled by nix brews = [ # AI and ML tools are now handled by nixpkgs ];
# Mac App Store apps (if any) masApps = { # Example: "Xcode" = 497799835; }; };}This approach keeps my macOS setup clean and reproducible. Homebrew handles the graphical apps, while Nix manages the rest, all defined in code.
Benefits and Challenges
The setup made switching between my desktop and laptop less brittle. Consistent environments reduced setup time and made configuration bugs easier to trace.
There are still trade-offs. Some packages require platform-specific tweaks, and initial setup can be steep for Nix newcomers. For my own workflow, the versioned configuration was worth that cost.
If I were starting this again, I would still begin with Nix flakes because the module boundaries are easier to keep visible.
Discussion