#+title: P’undrak’s Litterate Nix Config #+setupfile: headers #+property: header-args:emacs-lisp :lexical t :exports none :tangle no * Index Hi, I’m P’undrak (pronounced /PUN-drak/, or more exactly {{{phon(pʰynɖak̚)}}}), also known as Lucien Cartier-Tilet. If you want to know more about me, you can head to my [[https://phundrak.com/en][main website]]. This website documents my entire Linux configuration, managed through NixOS. Most of my programs are configured through Nix, following the [[https://github.com/Doc-Steve/dendritic-design-with-flake-parts][dendritic pattern]]. To give you a tl;dr, this basically means that each aspect of my configuration (be it a concept or an application) tries to only have a single source of truth. But as you can see with my Emacs configuration, some aspects can be quite extensive and require several pages to be properly organised. ** License See [[https://labs.phundrak.com/phundrak/dotfiles/src/branch/master/LICENSE.org][the repository’s license file]]. ** Note on What a Literate Configuration Is As implied by the title of this website, my configuration is a litterate one. This means that every page of this website comes from an Emacs org-mode file, and the code you see as code blocks are the actual source of my configuration. Org-mode offers to “tangle” these code blocks into full files, which can then be used as any other source file. If you visit this website’s repository, you will find the source org files alongside their resulting Nix file if any is generated by said file. For instance, this very page generates my =modules/default.nix= file, as we’ll see below. This also implies heavy usage of the [[https://orgmode.org/manual/Noweb-Reference-Syntax.html][noweb]] syntax. If you encounter some code that looks ~<>~, org-mode will replace this snippet with another code snippet declared elsewhere in my configuration. If you see some code that looks ~<>~, some generating code will run and replace this piece of text with the text generated. A quick example: #+begin_src elisp (defun hello () <> <>) #+end_src Will instead appear as #+begin_src emacs-lisp :noweb yes (defun hello () <> <>) #+end_src This is because I have the block of code below named ~generate-docstring~ which generates an output, which replaces its noweb tag. You can recognize noweb snippets generating code with the parenthesis. Often, such blocks aren’t visible in my HTML exports, but you can still see them if you open the actual org source file. #+name: generate-docstring #+begin_src emacs-lisp (concat "\"" "Print \\\"Hello World!\\\" in the minibuffer." "\"") #+end_src On the other hand, noweb snippets without parenthesis simply replace the snippet with the equivalent named code block. For instance the one below is named ~print-hello~ and is placed as-is in the target source block. #+name: print-hello #+begin_src emacs-lisp (message "Hello World!") #+end_src ** Root Nix Configuration As this configuration uses [[https://flake.parts/][flake-parts]] and [[https://flake-file.denful.dev/][flake-file]], I need a =modules/default.nix= which defines how to manage my config. For the record, I use [[https://github.com/nix-community/nh][nh]] to compile my configuration and switch to newer generations of my OS and home. This allows me to simply run =nh os switch= to upgrade my system, or =nh home switch= to upgrade my [[https://nix-community.github.io/home-manager/][home-manager]] configuration. This, therefore, requires to have [[https://nixos.wiki/wiki/flakes][flakes]] outputs in the form of =nixosConfigurations.marpa= (with =marpa= being the hostname of a machine) and =homeConfigurations.phundrak= or =homeConfigurations.phundrak@marpa= (with =phundrak= being the username of one of the users of a machine). But first things first, let’s declare the closure that will encapsulate the rest of the file: #+begin_src nix :tangle default.nix { inputs, lib, config, ... }: { <> <> config = { <> <> flake.lib = { <> <> <> }; <> <> }; } #+end_src To get our configuration to work, we need to import the modules from flake-parts and flake-file. #+name: modules-imports #+begin_src nix imports = [ inputs.flake-parts.flakeModules.modules inputs.flake-file.flakeModules.default ]; #+end_src Now, we can declare an option to make =homeConfigurations= available as an output for our flakes. #+name: options-home #+begin_src nix options.flake.homeConfigurations = lib.mkOption { type = lib.types.lazyAttrsOf lib.types.raw; default = {}; }; #+end_src *** Setting Things Up We need to declare a few inputs for our flake, thanks to flake-file. The first one is =flake-utils=, which allows me to easily declare my development shell for this repository both for my x86-64 machines and my aarch64 tablet, a PineTab 2. Then, speaking of the PineTab, I need some specific nixpkgs input, to handle a bug in the compilation of the kernel, as well as the flake =rockchip= to support said kernel. #+name: flake-inputs #+begin_src nix flake-file.inputs = { flake-utils.url = "github:numtide/flake-utils"; nixpkgsPinetab2Kernel.url = "github:nixos/nixpkgs/e73de5be04e0eff4190a1432b946d469c794e7b4"; rockchip = { url = "github:raboof/nixos-rockchip/pinetab-linux-7.0"; inputs.utils.follows = "flake-utils"; inputs.nixpkgsStable.follows = "nixpkgsStable"; inputs.nixpkgsUnstable.follows = "nixpkgsPinetab2Kernel"; }; }; #+end_src As I said, I support two architectures for my development shell, so let’s declare them. #+name: dev-arch #+begin_src nix systems = ["x86_64-linux" "aarch64-linux"]; #+end_src Now, we can declare some functions for our flake. These three functions’ role is to create the output of each machine and user as required. First, let’s create the function to get a machine’s configuration based on its name. #+name: lib-mkNixos #+begin_src nix mkNixos = system: name: { ${name} = inputs.nixpkgs.lib.nixosSystem { modules = [ config.flake.modules.nixos.${name} {nixpkgs.hostPlatform = lib.mkDefault system;} ]; }; }; #+end_src What this does is basically declare a Nix system named after the host’s name, created with its module (located in =modules/hosts/=) and the related nixpkgs with the appropriate architecture. For now, the only architecture used with this function is x86-64, but I’m not excluding the posibility to have other hosts using an ARM CPU. =mkHome= is somewhat similar: it declares a home-manager module, importing the module related to the user and the machine it will be deployed on. #+name: lib-mkHome #+begin_src nix mkHome = system: userName: hostName: { "${userName}@${hostName}" = inputs.home-manager.lib.homeManagerConfiguration { pkgs = inputs.nixpkgs.legacyPackages.${system}; extraSpecialArgs = { inherit inputs; bunBaseline = config.flake.packages.${system}.bun-baseline; }; modules = [config.flake.modules.homeManager."${userName}-${hostName}"]; }; }; #+end_src You may notice the declaration of =bunBaseline=. This is because of my Thinkpad x220; the default binary distributed for Bun uses CPU instructions that are more recent than this laptop’s CPU, which results in fatal errors trying to run it. Therefore, =bunBaseline= is here to either compile Bun on my Thinkpad with the correct instruction set, or simply use a prepackaged Bun if the host supports it. Lastly, I have a function dedicated to building NixOS on my PineTab 2. #+name: lib-mkPinetab #+begin_src nix mkPinetab = buildPlatform: variantModule: { pinetab2 = inputs.nixpkgs.lib.nixosSystem { system = "aarch64-linux"; modules = [ inputs.rockchip.nixosModules.sdImageRockchip inputs.rockchip.nixosModules.dtOverlayPCIeFix inputs.rockchip.nixosModules.noZFS config.flake.modules.nixos.pinetab2-base variantModule { rockchip.uBoot = inputs.rockchip.packages.${buildPlatform}.uBootPineTab2; boot.kernelPackages = inputs.rockchip.legacyPackages.${buildPlatform}.kernel_linux_7_0_pinetab_unstable; hardware.firmware = [inputs.rockchip.packages.aarch64-linux.bes2600]; nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (inputs.nixpkgs.lib.getName pkg) ["bes2600-firmware"]; } ]; }; }; #+end_src I won’t go into too much details here, but it mostly imports the modules required to run NixOS on the Pinetab as well as explicitly import the bes2600 firmware module to make Bluetooth and Wi-Fi available on the tablet. *** Declaring the Actual Outputs With all that being said, we can now actually declare our configurations. You can see below the table of hosts I have, with their CPU architecture, and which users are present on the system. #+name: table-hosts | Host | Architecture | Users | Comment | |----------+---------------+-----------------+------------------| | marpa | x86_64-linux | phundrak | Main workstation | | gampo | x86_64-linux | phundrak | Thinkpad x220 | | tilo | x86_64-linux | phundrak | Home Server | | elcafe | x86_64-linux | phundrak, creug | Server | | NaroMk3 | x86_64-linux | phundrak | Cloud Server | | pinetab2 | aarch64-linux | phundrak | PineTab 2 tablet | #+name: make-hosts #+begin_src emacs-lisp :exports none :var hosts=table-hosts :cache yes (mapconcat (lambda (line) (let ((hostname (car line)) (arch (nth 1 line))) (format "(config.flake.lib.mkNixos \"%s\" \"%s\")" arch hostname))) (-filter (lambda (host) (not (string= "pinetab2" (car host)))) hosts) "\n") #+end_src #+RESULTS[f5f8b3a89622e7526a83cbf722c4b7c77ff7bd86]: make-hosts : (config.flake.lib.mkNixos "x86_64-linux" "marpa") : (config.flake.lib.mkNixos "x86_64-linux" "gampo") : (config.flake.lib.mkNixos "x86_64-linux" "tilo") : (config.flake.lib.mkNixos "x86_64-linux" "elcafe") : (config.flake.lib.mkNixos "x86_64-linux" "NaroMk3") #+name: make-home #+begin_src emacs-lisp :exports none :var hosts=table-hosts :cache yes (require 's) (mapconcat (lambda (line) (let ((hostname (car line)) (arch (nth 1 line)) (users (mapcar #'s-trim (s-split "," (nth 2 line) t)))) (mapconcat (lambda (user) (format "(config.flake.lib.mkHome \"%s\" \"%s\" \"%s\")" arch user hostname)) users "\n"))) hosts "\n") #+end_src #+RESULTS[301fccb07591fffc8d7bdfd7d2958602150aa688]: make-home : (config.flake.lib.mkHome "x86_64-linux" "phundrak" "marpa") : (config.flake.lib.mkHome "x86_64-linux" "phundrak" "gampo") : (config.flake.lib.mkHome "x86_64-linux" "phundrak" "tilo") : (config.flake.lib.mkHome "x86_64-linux" "phundrak" "elcafe") : (config.flake.lib.mkHome "x86_64-linux" "creug" "elcafe") : (config.flake.lib.mkHome "x86_64-linux" "phundrak" "NaroMk3") : (config.flake.lib.mkHome "aarch64-linux" "phundrak" "pinetab2") This translates into this Nix code. #+name: configs-decl #+begin_src nix :noweb yes flake.nixosConfigurations = lib.mkMerge [ <> (config.flake.lib.mkPinetab "x86_64-linux" config.flake.modules.nixos.pinetab2-gnome) ]; flake.homeConfigurations = lib.mkMerge [ <> ]; #+end_src *** Development Shell Lastly, here is the declaration of my development shell. It really is only useful if I’m on a new machine or a machine that is not quite up to date and misses some packages I rely on to work on my dotfiles. Namely, these are =nh= (which I mentioned above), Jujutsu, =jj-cz= (a Commitizen alternative for Jujutsu I’m working on), and Git itself as a fallback. #+name: devshell #+begin_src nix perSystem = { pkgs, system, ... }: { formatter = pkgs.alejandra; devShells.default = pkgs.mkShell { buildInputs = [ pkgs.nh pkgs.jujutsu pkgs.git inputs.jj-cz.packages.${system}.default ]; }; }; #+end_src