Nix
Integrating Development Environments Between NixOS and macOS
About six months ago, I switched to NixOS on my PC after a recommendation from my roommate. This came at a perfect time—I had completely messed up my Ubuntu desktop environment and couldn't recover it.
NixOS is a Linux distribution built around the Nix package manager, which enables declarative configurations and reproducible builds.
I quickly discovered how handy and powerful NixOS is for managing packages and configurations, so I made it the sole OS on my PC. As I grew more familiar with Nix, I realized I could install it on my MacBook running macOS too. This setup provides a nearly seamless development experience across my NixOS desktop and macOS laptop by:
- 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, enabling me to reuse shell scripts, variables, and aliases effortlessly.
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 point
This 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 essential tools for various programming tasks, including a robust Python setup for data science and machine learning, Java tools for backend development, and 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
This integrated setup has transformed my workflow. Switching between my desktop and laptop feels effortless, with consistent environments reducing setup time and debugging headaches.
That said, there are challenges: some packages require platform-specific tweaks, and initial setup can be steep for Nix newcomers. But once configured, the benefits far outweigh the effort.
If you're exploring similar cross-platform dev setups, I recommend starting with Nix flakes for better modularity. Feel free to adapt this to your needs!