Skip to content

Latest commit

 

History

50 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dotfiles

Multi-OS config for moha, managed with Nix flakes. One repo, four hosts:

Host OS Manages Flake output
nixos-btw NixOS system + user nixosConfigurations.nixos-btw
macos macOS (M1, 2020) system + user darwinConfigurations.macos
arch-btw Arch Linux (any distro) user only homeConfigurations."moha@arch-btw"
ubuntu-btw Ubuntu/Debian, headless user only homeConfigurations."moha@ubuntu-btw"
ubuntu-btw-gui Ubuntu/Debian, with display user only homeConfigurations."moha@ubuntu-btw-gui"

All of them share the same home/home.nix (packages, shell, symlinked app configs). Only the system layer differs per OS: hosts/nixos-btw/configuration.nix (NixOS) and hosts/macos/darwin-configuration.nix (nix-darwin). Arch and Ubuntu have no system module — they aren't NixOS, so only home-manager (user-level) applies there.

Architectures

Every standalone (non-NixOS) host is emitted twice, once per architecture: moha@<host> is x86_64-linux and moha@<host>-aarch64 is aarch64-linux. Two names are needed because homeConfigurations is a plain attrset and a flake can't read the builder's architecture in pure eval (builtins.currentSystem is off limits). Nothing has to be typed by hand though — Taskfile.yml and scripts/bootstrap-ubuntu.sh both derive the suffix from uname -m, so task ubuntu:switch picks the right output on an Intel VM and on an ARM one alike.

Headless vs. desktop

home/home.nix takes a gui flag (passed from flake.nix via extraSpecialArgs, default true). Setting it to false — as ubuntu-btw does — drops Xorg, oxwm, ghostty, firefox, gparted, rofi and the fonts, and skips the Mesa LD_LIBRARY_PATH workaround. On a box with no display those are a few hundred MB that can never be used, and the LD_LIBRARY_PATH one is worse than useless there: it's inherited by every process in the login session, where it can shadow the distro's own glibc/libstdc++ for system binaries.

Layout

dotfiles/
├── flake.nix                          # every host output lives here
├── home/home.nix                      # shared: packages, shell, xdg symlinks
├── hosts/
│   ├── nixos-btw/configuration.nix    # NixOS system config
│   │   └── hardware-configuration.nix # machine-specific, generated by nixos-generate-config
│   └── macos/darwin-configuration.nix # nix-darwin system config
├── config/                            # app configs, symlinked into ~/.config by home.nix
│   ├── nvim/
│   └── oxwm/                          # Linux only, skipped on Darwin
├── scripts/
│   └── bootstrap-ubuntu.sh            # one-shot Ubuntu setup, see below
└── Taskfile.yml                       # `task <name>` shortcuts, see below

home/home.nix symlinks config/* into ~/.config/* via mkOutOfStoreSymlink, pointing at ~/dotfiles/config/.... The repo must be cloned to exactly ~/dotfiles on every host, or the symlinks break.

Prerequisites

Install Nix first (all platforms), with flakes enabled:

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

Or, if go-task is already available on the machine (e.g. installed via pacman/brew before Nix): task nix:install runs the same command. On Ubuntu/Debian, skip this — scripts/bootstrap-ubuntu.sh installs Nix itself, after the apt packages the installer depends on.

On macOS, install Xcode Command Line Tools before that: xcode-select --install.

go-task is used to shorten the commands below. It's optional — the raw nix/nixos-rebuild/darwin-rebuild/home-manager commands work directly too. Install with nix profile install nixpkgs#go-task or via your package manager.

Usage

git clone <this-repo-url> ~/dotfiles
cd ~/dotfiles
task --list        # see everything below, with descriptions

NixOS (nixos-btw)

task nixos:switch    # apply now
task nixos:boot       # apply on next boot only
task nixos:test        # apply for this session, revert on reboot

Includes KVM/QEMU virtualization (virtualisation.libvirtd, virt-manager, SPICE USB redirection). moha is in the libvirtd/kvm groups — log out and back in (or newgrp libvirtd) after the first switch for group membership to take effect, then launch virt-manager to create VMs.

macOS (macos, M1)

First run on a fresh machine — create a user account named exactly moha during the macOS setup assistant, since home.username and users.users.moha.home are hardcoded to that name. Install Homebrew itself manually before the first switch — nix-darwin manages the casks below, not the brew binary:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Then:

task macos:bootstrap   # first ever switch, before darwin-rebuild is on PATH
task macos:switch       # every switch after that

Homebrew casks: hosts/macos/darwin-configuration.nix installs ghostty and karabiner-elements via Homebrew instead of nixpkgs. ghostty has no aarch64-darwin build in nixpkgs at all. karabiner-elements does build, but it's only the Settings UI — missing the DriverKit system extension and launchd daemons that do the actual key remapping (and that make it show up under System Settings → Login Items & Extensions → Allow in the Background). The cask runs the real upstream .pkg installer instead, which sets both up correctly.

Docker: macOS has no Linux kernel for dockerd to run on, so home/home.nix installs colima (a lightweight Linux VM) alongside docker-client (CLI only). Start the VM once per boot, then use docker exactly as on Linux:

colima start

Arch Linux (arch-btw) — user config only

No system module: this only manages packages and dotfiles for moha, via standalone home-manager. Works on any non-NixOS Linux distro with Nix installed, not just Arch.

task arch:bootstrap    # first ever switch
task arch:switch         # every switch after that

Both run with -b backup: standalone home-manager has no backupFileExtension option (that's only available through the NixOS/nix-darwin integration modules), so pre-existing dotfiles (~/.bashrc, etc.) would otherwise make the switch fail on conflict — they get renamed to *.backup instead.

There's no display manager on plain Arch by default. home/home.nix installs xorg-server + xinit from Nix (no pacman needed) along with a ~/.xinitrc that execs oxwm, and auto-runs startx on tty1 login via ~/.bash_profile/~/.zprofile — so logging in on tty1 drops straight into the GUI. Running oxwm directly from a TTY instead of through startx fails with CannotOpenDisplay — it needs a running X server first.

One-time manual step required: Nix's xorg-server ships with no input driver, so the keyboard/mouse get enumerated but never actually respond (X logs No input driver specified, ignoring this device for each one — easy to mistake for a full freeze, since everything else, like oxwm's status bar, keeps rendering fine). home/home.nix installs the matching xf86-input-libinput driver and points ~/.xserverrc at it via -modulepath (not root-restricted), but the InputClass rule that tells Xorg to actually use that driver has to live under /etc/X11/xorg.conf.d-configdir only accepts absolute paths as root, so this one file can't be home-manager-managed:

sudo mkdir -p /etc/X11/xorg.conf.d
sudo tee /etc/X11/xorg.conf.d/40-libinput.conf > /dev/null <<'EOF'
Section "InputClass"
        Identifier "libinput pointer catchall"
        MatchIsPointer "on"
        MatchDevicePath "/dev/input/event*"
        Driver "libinput"
EndSection

Section "InputClass"
        Identifier "libinput keyboard catchall"
        MatchIsKeyboard "on"
        MatchDevicePath "/dev/input/event*"
        Driver "libinput"
EndSection
EOF

For a real display manager instead of the tty1-autostart hack, install ly (via pacman) — home/home.nix skips the autostart trick once it detects one, either at /etc/ly/config.ini or as an enabled /etc/systemd/system/display-manager.service (which is how gdm3, sddm and lightdm register themselves):

sudo pacman -S ly
sudo systemctl enable ly.service
sudo systemctl disable getty@tty1.service

Docker: standalone home-manager only manages moha's user packages — it can't enable a system-level systemd service, so dockerd has to come from pacman instead (unlike NixOS, which gets it via virtualisation.docker.enable in hosts/nixos-btw/configuration.nix):

sudo pacman -S docker
sudo systemctl enable --now docker.service
sudo usermod -aG docker moha   # log out and back in (or `newgrp docker`) after

Ubuntu / Debian (ubuntu-btw, ubuntu-btw-gui) — user config only

Same standalone home-manager setup as Arch, with a bootstrap script that also handles the two things Arch's instructions leave to pacman: the handful of apt packages Nix's own installer needs, and the system services home-manager can't touch. One command on a brand-new VM, on either architecture:

git clone https://github.com/moha-bekh/dotfiles ~/dotfiles
~/dotfiles/scripts/bootstrap-ubuntu.sh          # headless VM
~/dotfiles/scripts/bootstrap-ubuntu.sh --gui --docker --zsh

Or, with nothing cloned yet:

curl -fsSL https://raw.githubusercontent.com/moha-bekh/dotfiles/main/scripts/bootstrap-ubuntu.sh | bash

It is idempotent — every step checks whether it already ran — so re-running it after a failure or a change of flags is fine. In order, it:

  1. refuses to continue as root or as a user other than moha (both are hardcoded in home/home.nix, and failing here beats failing halfway through a switch);
  2. resolves the flake target from uname -m and the --gui flag, e.g. .#moha@ubuntu-btw-aarch64;
  3. apt installs only what Nix itself needs first: curl, xz-utils (not in a minimal Ubuntu image, and the installer downloads a .tar.xz), ca-certificates, git. Everything else deliberately comes from Nix — that's what keeps the hosts identical;
  4. clones the repo to ~/dotfiles if it isn't there;
  5. removes a ~/.configdotfiles/config symlink if one exists (see below);
  6. installs Nix via the Determinate Systems installer if absent, then sources nix-daemon.sh — the installer only patches login shell files, so nix is not on PATH in the already-running shell;
  7. runs the home-manager switch;
  8. with --docker, installs docker.io + docker-compose-v2 from apt, enables the service and adds you to the docker group. This can't come from the flake: standalone home-manager manages moha's user packages only and cannot enable a system-level systemd unit — same reason Arch gets Docker from pacman, while NixOS gets it from virtualisation.docker.enable;
  9. with --gui, writes /etc/X11/xorg.conf.d/40-libinput.conf — the one root-owned file behind the Arch section's "one-time manual step", so on Ubuntu it isn't manual;
  10. with --zsh, adds ~/.nix-profile/bin/zsh to /etc/shells (chsh rejects anything absent from it) and makes it the login shell.

Then, for every update after that:

task ubuntu:switch        # headless
task ubuntu:switch:gui    # desktop

Don't symlink ~/.config to config/ wholesale. It looks like a shortcut to the same result and isn't: every application on the machine then gets write access to the repo (so ~/.config/foo/state.json shows up in git status), the macOS- and Arch-only configs become visible on Ubuntu too, home-manager writes its symlinks into the repo, and nothing installs the packages those configs drive. The flake links each app directory individually via mkOutOfStoreSymlink instead, which is why the switch is what gives you the full config and not just the files.

Ubuntu Desktop already has a display manager (gdm3), so --gui does not add the tty1 startx autostart that Arch gets — home/home.nix detects the enabled display-manager.service and skips it. To actually land in oxwm rather than GNOME, pick it from the session list at the login screen; that needs a .desktop session file under /usr/share/xsessions, which is root-owned and therefore outside this repo. The startx-on-tty1 path works unchanged on Ubuntu Server.

Maintenance

task check     # nix flake check --no-build — evaluates the NixOS/darwin outputs
task update    # bump nixpkgs / home-manager / nix-darwin, re-lock

Run task check after editing home/home.nix or any host file, before switching — it catches eval errors without building anything. Note that it only covers the flake's standard outputs: homeConfigurations isn't one of them, so the standalone hosts aren't checked by it. Evaluate those explicitly:

nix eval .#homeConfigurations --apply builtins.attrNames        # list every name
for h in ubuntu-btw ubuntu-btw-aarch64 ubuntu-btw-gui-aarch64; do
  nix eval --raw ".#homeConfigurations.\"moha@$h\".activationPackage.drvPath"
done

Notes

  • system.stateVersion (NixOS) and system.stateVersion (nix-darwin) are set once at first install and should never be bumped afterward — they pin on-disk data format compatibility, not the Nixpkgs version in use.
  • The oxwm and picom config symlinks only apply on Linux (lib.optionalAttrs pkgs.stdenv.isLinux in home/home.nix) — macOS has no X11 window manager. The packages are gated more tightly still, on gui && isLinux, so the headless ubuntu-btw host gets neither.
  • Language servers come from Nix, not mason.nvim. home/home.nix installs clang-tools (clangd), gopls, zls, vtsls, rust-analyzer and nil, and config/nvim/lua/plugins/lsp.lua marks each one mason = false plus sets mason's PATH = "append" so those win over any copy mason already downloaded. Two things force this: mason has no aarch64-linux clangd at all (The current platform is unsupported. — so C/C++ has no LSP on an ARM VM), and mason's prebuilt binaries are linked against /lib64/ld-linux, which doesn't exist on NixOS. Servers nixpkgs isn't asked for here (lua_ls, marksman, terraform-ls, the ansible/docker ones) still come from mason.
  • rust-analyzer is driven by rustaceanvim, not nvim-lspconfig — LazyVim's lang.rust extra sets rust_analyzer = { enabled = false } on purpose and rustaceanvim picks the binary up from $PATH. That's why it isn't in the mason = false list.
  • zls reports parse errors but not type errors out of the box: semantic Zig diagnostics need enable_build_on_save, which makes zls run zig build on every save. Hover, completion and go-to-definition work regardless.
  • config/git/.gitconfig sets interactive.diffFilter = delta, so delta is in the shared package list — without it git add -p fails on any host that didn't happen to have it from pacman/brew. unzip is there for the same reason: LazyVim's mason.nvim shells out to it to unpack LSP servers.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages