# Guix
Freedom is a state of mind
Author: SqrtMinusOne

GNU Guix is (1) a transactional package manager and (2) a GNU/Linux distribution.

My personal selling points are declarative package configuration and transactional upgrades.

References:

- Official help
- System Crafters wiki
- Pjotr Prins’ Guix notes
- Davil Wilson’s YouTube series

## Profiles

A profile is a way to group Guix packages. Amongst its advantages, profiles can be defined by manifests, which in turn can be stored in VCS.

References:

- Guix Profiles in Practice

### Activate profiles

A script to activate guix profiles. Usage:

    activate-profiles [profile1] [profile2] ...

Source: David Wilson’s config

    GREEN='\033[1;32m'
    RED='\033[1;30m'
    NC='\033[0m'
    GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles

    profiles=$*
    if [[ $# -eq 0 ]]; then
        profiles="$HOME/.config/guix/manifests/*.scm";
    fi

    for profile in $profiles; do
      # Remove the path and file extension, if any
      profileName=$(basename $profile)
      profileName="${profileName%.*}"
      profilePath="$GUIX_EXTRA_PROFILES/$profileName"
      manifestPath=$HOME/.config/guix/manifests/$profileName.scm

      if [ -f $manifestPath ]; then
        echo
        echo -e "${GREEN}Activating profile:" $manifestPath "${NC}"
        echo

        mkdir -p $profilePath
        guix package --manifest=$manifestPath --profile="$profilePath/$profileName"

        # Source the new profile
        GUIX_PROFILE="$profilePath/$profileName"
        if [ -f $GUIX_PROFILE/etc/profile ]; then
    	. "$GUIX_PROFILE"/etc/profile
        else
    	echo -e "${RED}Couldn't find profile:" $GUIX_PROFILE/etc/profile "${NC}"
        fi
      else
        echo "No profile found at path" $profilePath
      fi
    done

### Update profiles

A script to update Guix profiles. Usage:

    update-profiles [profile1] [profile2] ...

Source: once again, David Wilson’s config.

    GREEN='\033[1;32m'
    NC='\033[0m'
    GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles

    profiles=$*
    if [[ $# -eq 0 ]]; then
        profiles="$GUIX_EXTRA_PROFILES/*";
    fi

    for profile in $profiles; do
      profileName=$(basename $profile)
      profilePath=$GUIX_EXTRA_PROFILES/$profileName

      echo
      echo -e "${GREEN}Updating profile:" $profilePath "${NC}"
      echo

      guix package --profile="$profilePath/$profileName" --manifest="$HOME/.config/guix/manifests/$profileName.scm"
    done

### Run `guix package` in profile

    GUIX_EXTRA_PROFILES=$HOME/.guix-extra-profiles

    profileName=$(basename $1)
    profileName="${profileName%.*}"
    profilePath="$GUIX_EXTRA_PROFILES/$profileName"

    if [ -d $profilePath ]; then
        guix package --profile="$profilePath/$profileName" ${@:2}
    else
        echo -e "No profile found at path: " $profilePath
    fi

## Channels

Specifying additional channels.

channel-q is my Guix channel. Don’t use it at home.

References:

- nonguix channel repo
- Guix channels reference

    (cons*
     (channel
      (name 'channel-q)
      (url "file:///home/pavel/Code/channel-q"))
     (channel
      (name 'flat)
      (url "https://github.com/flatwhatson/guix-channel.git")
      (introduction
       (make-channel-introduction
        "33f86a4b48205c0dc19d7c036c85393f0766f806"
        (openpgp-fingerprint
         "736A C00E 1254 378B A982  7AF6 9DBE 8265 81B6 4490"))))
     (channel
      (name 'nonguix)
      (url "https://gitlab.com/nonguix/nonguix")
      ;; (commit "d54973e47b89fe5772a5b6e2d0c0b86acb089e27")
      (introduction
       (make-channel-introduction
        "897c1a470da759236cc11798f4e0a5f7d4d59fbc"
        (openpgp-fingerprint
         "2A39 3FFF 68F4 EF7A 3D29  12AF 6F51 20A0 22FB B2D5"))))
     (channel
      ;; What can possibly go wrong, huh
      (name 'guix-gaming-games)
      (url "https://gitlab.com/guix-gaming-channels/games.git")
      ;; Enable signature verification:
      (introduction
       (make-channel-introduction
        "c23d64f1b8cc086659f8781b27ab6c7314c5cca5"
        (openpgp-fingerprint
         "50F3 3E2E 5B0C 3D90 0424  ABE8 9BDC F497 A4BB CC7F"))))
     %default-channels)

## Systems

Configuring systems with Guix.

Yes, all my machines are named after colors I like.

### Base configuration

The base configuration is shared between all the machines.

While it’s possible to make a single `.scm` file with base configuration and load it, I noticed that it produces more cryptic error messages whenever there is an error in the base file, so I opt-in for noweb.

`guix system` invocation is as follows:

    sudo -E guix system reconfigure ~/.config/guix/systems/[system].scm

Common modules:

    (use-modules (gnu))
    (use-modules (gnu system nss))
    (use-modules (gnu packages bash))
    (use-modules ((gnu packages base) #:select (coreutils glibc)))
    (use-modules (gnu packages certs))
    (use-modules (gnu packages version-control))
    (use-modules (gnu packages vim))
    (use-modules (gnu packages gnome))
    (use-modules (gnu packages xorg))
    (use-modules (gnu packages wm))
    (use-modules (gnu packages openbox))
    (use-modules (gnu services docker))
    (use-modules (gnu services cups))
    (use-modules (gnu services virtualization))
    (use-modules (srfi srfi-1))
    (use-modules (guix channels))
    (use-modules (guix inferior))
    (use-modules (nongnu packages linux))
    (use-modules (nongnu system linux-initrd))

    (use-service-modules desktop networking ssh xorg nix)
    (use-package-modules ssh)

In principle, we could define a variable called `base-operating-system` and extend it in ancestors. However, then we would have to define mandatory fields like `host-name`, `bootloader` with dummy values. Since I’m already using noweb, there is little point.

The following code will be inserted at the top of the `operating-system` definition.

Use the full Linux kernel. I hope I’ll be able to use Libre kernel somewhere later.

Inferior in the kernel is used to avoid recompilation. It looks like I can pin these to different commits than in my `channels.scm`

    (kernel
     (let*
         ((channels
           (list (channel
    	      (name 'nonguix)
    	      (url "https://gitlab.com/nonguix/nonguix")
    	      (commit "393b8e0405f44835c498d7735a8ae9ff4682b07f"))
    	     (channel
    	      (name 'guix)
    	      (url "https://git.savannah.gnu.org/git/guix.git")
    	      (commit "4c812db049d5c9f2c438748e180f9486ad221b0a"))))
          (inferior
           (inferior-for-channels channels)))
       (first (lookup-inferior-packages inferior "linux" "5.15.12"))))
    ;; (kernel linux)
    (initrd microcode-initrd)
    (firmware (list linux-firmware))
    (locale "en_US.utf8")
    (timezone "Europe/Moscow")

US/RU keyboard layout, switch with Alt+Shift.

    (keyboard-layout (keyboard-layout "us,ru" #:options '("grp:alt_shift_toggle")))

User accounts.

    (users (cons* (user-account
    	       (name "pavel")
    	       (comment "Pavel")
    	       (group "users")
    	       (home-directory "/home/pavel")
    	       (supplementary-groups
    		'("wheel"  ;; sudo
    		  "netdev" ;; network devices
    		  "audio"
    		  "video"
    		  "input"
    		  "tty"
    		  "docker"
    		  "scanner"
    		  "libvirt"
    		  "lp")))
    	      %base-user-accounts))

Base packages, necessary right after the installation.

    (packages
     (append
      (list nss-certs
    	    git
    	i3-gaps
    	i3lock
    	openbox
    	xterm
    	    vim)
      %base-packages))

Default services for each machine:

- override the default `%desktop-services` to add OpenVPN support
- add nix service
- add docker service
- add CUPS service
- add libvirt service
- add a symlink to ELF interpreter to where most Linux binaries expect it

    (define %my-base-services
      (cons*
       (service openssh-service-type)
       (screen-locker-service i3lock "i3lock")
       (extra-special-file "/lib64/ld-linux-x86-64.so.2" (file-append glibc "/lib/ld-linux-x86-64.so.2"))
       (service nix-service-type)
       (service cups-service-type
    	    (cups-configuration
    	     (web-interface? #t)))
       (service docker-service-type)
       (service libvirt-service-type
    	    (libvirt-configuration
    	     (unix-sock-group "libvirt")
    	     (tls-port "16555")))
       (service virtlog-service-type)
       (bluetooth-service #:auto-enable? #f)
       (modify-services %desktop-services
    		    (network-manager-service-type
    		     config =>
    		     (network-manager-configuration
    		      (inherit config)
    		      (vpn-plugins (list network-manager-openvpn)))))))

### indigo

`indigo` is my desktop PC.

    <<system-common>>

    (operating-system
     <<system-base>>

     (host-name "indigo")
     (services (cons*
    	    (set-xorg-configuration
    	     (xorg-configuration
    	      (keyboard-layout keyboard-layout)))
    	    %my-base-services))

     (bootloader
      (bootloader-configuration
       (bootloader grub-efi-bootloader)
       (target "/boot/efi")
       (keyboard-layout keyboard-layout)))

     (swap-devices
      (list (uuid "3a77c542-7d24-46ff-8123-f7398d1c2677")))

     (file-systems
      (cons* (file-system
    	  (mount-point "/")
    	  (device (file-system-label "my-root"))
    	  (type "ext4"))
    	     (file-system
    	      (mount-point "/boot/efi")
    	      (device "/dev/sda1")
    	      (type "vfat"))
    	 %base-file-systems)))

### eminence

`eminence` is a HP 15s laptop.

`%backlight-udev-rule` should enable members of `video` group change the display backlight. See the relevant page at Arch Wiki.

    <<system-common>>

    (define %backlight-udev-rule
      (udev-rule
       "90-backlight.rules"
       (string-append "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
    		  "RUN+=\"/run/current-system/profile/bin/chgrp video /sys/class/backlight/%k/brightness\""
    		  "\n"
    		  "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
    		  "RUN+=\"/run/current-system/profile/bin/chmod g+w /sys/class/backlight/%k/brightness\"")))

    (operating-system
     <<system-base>>

     (host-name "eminence")
     (services (cons*
    	    (set-xorg-configuration
    	     (xorg-configuration
    	      (keyboard-layout keyboard-layout)))
    	    (modify-services %my-base-services
    			     (elogind-service-type
    			      config =>
    			      (elogind-configuration
    			       (inherit config)
    			       (handle-lid-switch-external-power 'suspend)))
    			     (udev-service-type
    			      config =>
    			      (udev-configuration
    			       (inherit config)
    			       (rules (cons %backlight-udev-rule
    					    (udev-configuration-rules config))))))))

     (bootloader
      (bootloader-configuration
       (bootloader grub-efi-bootloader)
       (target "/boot/efi")
       (keyboard-layout keyboard-layout)))

     (swap-devices
      (list (uuid "f93cf3f6-7ee7-42ec-8ee2-f3d896fdf9b5")))

     (file-systems
      (cons* (file-system
    	  (mount-point "/")
    	  (device
    	   (uuid "1d937704-bbeb-43b5-bc63-453886c426af"
    		 'ext4))
    	  (type "ext4"))
    	 (file-system
    	  (mount-point "/boot/efi")
    	  (device (uuid "0031-3784" 'fat32))
    	  (type "vfat"))
    	 %base-file-systems)))

### azure

`azure` is a Lenovo Ideapad 330 laptop.

`%backlight-udev-rule` should enable members of `video` group change the display backlight. See the relevant page at Arch Wiki.

    <<system-common>>

    (define %backlight-udev-rule
      (udev-rule
       "90-backlight.rules"
       (string-append "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
    		  "RUN+=\"/run/current-system/profile/bin/chgrp video /sys/class/backlight/%k/brightness\""
    		  "\n"
    		  "ACTION==\"add\", SUBSYSTEM==\"backlight\", "
    		  "RUN+=\"/run/current-system/profile/bin/chmod g+w /sys/class/backlight/%k/brightness\"")))

    (operating-system
     <<system-base>>

     (host-name "azure")
     (services (cons*
    	    (set-xorg-configuration
    	     (xorg-configuration
    	      (keyboard-layout keyboard-layout)))
    	    (modify-services %my-base-services
    			     (elogind-service-type config =>
    						   (elogind-configuration (inherit config)
    									  (handle-lid-switch-external-power 'suspend)))
    			     (udev-service-type config =>
    						(udev-configuration (inherit config)
    								    (rules (cons %backlight-udev-rule
    										 (udev-configuration-rules config))))))))

     (bootloader
      (bootloader-configuration
       (bootloader grub-efi-bootloader)
       (target "/boot/efi")
       (keyboard-layout keyboard-layout)))

     (swap-devices
      (list (uuid "4b2dedb3-b111-4e69-8c05-6daa2b072c76")))

     (file-systems
      (cons* (file-system
    	  (mount-point "/")
    	  (device (file-system-label "my-root"))
    	  (type "ext4"))
    	     (file-system
    	      (mount-point "/boot/efi")
    	      (device "/dev/sda1")
    	      (type "vfat"))
    	 %base-file-systems)))

## System installation

### Preparation

In my case, the provided ISO doesn’t work because of the Libre kernel.

Fortunately, David Wilson has made a repository with a toolchain to make an ISO with the full kernel. In case it won’t be an option, the nonguix repo also has instructions on how to do that.

When an ISO is there, we have to write it on a USB stick. Run `sudo fdisk -l` to get a list of disks.

The approach given in the official instruction is to create a bootable USB with `dd`:

    sudo dd of=/dev/sdxX if=<path-to-iso> status=progress && sync

However, I couldn’t make it work for some strange reason. Fortunately, `gnome-disk-utility` was able to produce a bootable USB.

### Installation

Going further, the official instructions for installation & SystemCrafters wiki entry are pretty good, so it’s not necessary to repeat them here.

### After installation

After the installation, the strategy is as follows.

Set a password for the main user (pavel). Login with openbox to get a tolerable interface because i3’s default config is horrible.

Connect to the internet.

Clone the dotfiles repo:

    mkdir Code
    cd Code
    git clone https://github.com/SqrtMinusOne/dotfiles.git

Copy the channels file and run guix pull:

    cp ~/Code/dotfiles/.config/guix/channels.scm ~/.config/guix
    guix pull

The first pull usually takes a while. After that install yadm and pull dotfiles:

    guix install yadm
    guix clone https://github.com/SqrtMinusOne/dotfiles.git

And activate the required profiles. Again, downloading & building Emacs, Starship and stuff will take a while.

Don’t forget to install `JetBrainsMono Nerd Font`.

## Misc software & notes

Category | Guix dependency | Description
--- | --- | ---
system | patchelf | A program to modify existsing ELF executables
system | glibc | A lot of stuff, including ELF interpeter and `ldd`

### VPN

Category | Guix dependency
--- | ---
system | openvpn
system | openvpn-update-resolve-conf
system | openresolv
system | vpnc

I’m not sure how to properly spin up VPN on Guix, so here is what ended I’m doing after some trial and error.

I’m using Mullvad VPN. The `~/.vpn` folder stores its OpenVPN config (`openvpn.ovpn`), modified as follows:

- paths to `ca`, `cert` and `key` are made absolute

      ca /home/pavel/.vpn/ca.crt
      cert /home/pavel/.vpn/client.crt
      key /home/pavel/.vpn/client.key

- added `auth-user-pass` with a link to login info

      auth-user-pass /home/pavel/.vpn/auth.conf

  `auth.conf` looks like this:

      login
      password

- Run openvpn-update-resolv-conf script to prevent DNS leaks. `openvpn-update-resolve-conf` originates in my channel-q.

  Edit <2022-04-07 Thu>: Looks like this doesn’t work on some connections. See the next option in that case

      setenv PATH /home/pavel/.guix-extra-profiles/system/system/bin:/home/pavel/.guix-extra-profiles/system/system/sbin:/home/pavel/.guix-extra-profiles/console/console/bin:/run/current-system/profile/bin:/run/current-system/profile/sbin

      up /home/pavel/.guix-extra-profiles/system/system/bin/update-resolv-conf.sh
      down /home/pavel/.guix-extra-profiles/system/system/bin/update-resolv-conf.sh

  `setenv PATH` is necessary because both `resolvconf` (openresolve) and `update-resolv-conf.sh` are shell scripts which need GNU coreutils and stuff, and OpenVPN clears PATH by default.

- Manually fix `etc/resolv.conf` to prevent DNS leaks

      cp /etc/resolv.conf /etc/resolv.conf-bak
      echo "nameserver 8.8.8.8" > /etc/resolv.conf

  Restore `resolv.conf`

      resolveconf -u

      up /home/pavel/bin/scripts/fix-resolve-conf
      down /home/pavel/bin/scripts/restore-resolve-conf

- run a script to fix Docker routes

      route-up /home/pavel/bin/scripts/vpn-fix-routes

  References:

  - Github issue

  The script itself:

      echo "Adding default route to $route_vpn_gateway with /0 mask..."

      IP=/run/current-system/profile/sbin/ip

      $IP route add default via $route_vpn_gateway

      echo "Removing /1 routes..."
      $IP route del 0.0.0.0/1 via $route_vpn_gateway
      $IP route del 128.0.0.0/1 via $route_vpn_gateway

#### vpn-start

As of now, CyberGhost doesn’t provide ipv6, so we have to disable it.

Mullvad seems to provide it, so the script just launches `openvpn` with `pkexec`.

    export DISPLAY=:0
    CONN=$(nmcli -f NAME con show --active | grep -Ev "(.*docker.*|NAME|br-.*|veth.*|tun.*|vnet.*|virbr.*)" | sed 's/ *$//g')

    if [ -z "$CONN" ]; then
        echo "No connection!"
        notify-send "VPN" "No connection for VPN to run"
        exit
    fi

    # if [[ "$CONN" != *"Wired"* ]]; then
    #     echo "Connection: $CONN"
    #     notify-send "VPN" "Initializing for connection: $CONN"

    #     pkexec nmcli con modify "$CONN" ipv6.method ignore
    #     nmcli connection up "$CONN"
    # fi
    pkexec openvpn --config ~/.vpn/mullvad_openvpn_linux_lv_rix/mullvad_lv_rix.conf

#### vpn-stop

Also a script to reverse the changes Also not necessary now. Just `herd stop vpn` and `sudo pkill vpn`.

    CONN=$(nmcli -f NAME con show --active | grep -Ev "(.*docker.*|NAME|br-.*|veth.*|tun.*)" | sed 's/ *$//g')
    echo "Connection: $CONN"

    pkexec nmcli con modify "$CONN" ipv6.method auto
    nmcli connection up "$CONN"

### flatpak

As for now, the easiest way to install most of proprietary software is via flatpak. See the relevant section in Desktop.org.

### conda

conda is a package manager, which I use for managing various versions of Python & Node.js.

It is packaged for GNU Guix, although the definition has its fair share of workarounds. It is almost surprising to see it work with all the C libraries and stuff. But there are still some problems.

First, it’s impossible to perform `conda init` to patch files like `.bashrc`, because the command is hell-bent on modifying `/gnu/store/`. So I do this manually, look for the `init_conda` procedures in Console.org.

Second, the base environment has `/gnu/store/...` as a root, so don’t install anything there (and don’t run `conda` with superuser rights!).

Third, by default it tries to create envronments in `/gnu/store`. It’s enough to create one environment like this to fix it:

    mkdir -p ~/.conda/envs
    conda create -p ~/.conda/envs/test

Fourth, you may need to unset `$PYTHONPATH` if you have any global packages installed, otherwise Python from anaconda will try to import them instead of the conda versions.

Finally, I also want to have an ability to use global npm. Some settings for that are located in Console.org. Here we want to unset `NPM_CONFIG_USERCONFIG` if there is npm available in the environment.

So here is a script to set up conda hooks:

    # Get writable conda envs with npm & without it
    readarray -t CONDA_ENVS_ALL <<< $(conda env list --json | jq '.envs[]')
    CONDA_ENVS_NPM=()
    CONDA_ENVS_NO_NPM=()
    for env in "${CONDA_ENVS_ALL[@]}"; do
        env="${env:1:${#env}-2}"
        if [ -w "$env" ]; then
    	if [ -f "$env/bin/npm" ]; then
    	    CONDA_ENVS_NPM+=($env)
    	else
    	    CONDA_ENVS_NO_NPM+=($env)
    	fi
        fi
    done

    for env in "${CONDA_ENVS_NPM[@]}"; do
        echo "Found npm in $env"
        mkdir -p "$env/etc/conda/activate.d"
        mkdir -p "$env/etc/conda/deactivate.d"

        echo "unset NPM_CONFIG_USERCONFIG" > "$env/etc/conda/activate.d/conda.sh"
        echo "set -e NPM_CONFIG_USERCONFIG" > "$env/etc/conda/activate.d/conda.fish"
        echo "export NPM_CONFIG_USERCONFIG=$HOME/._npmrc" > "$env/etc/conda/deactivate.d/conda.sh"
        echo "export NPM_CONFIG_USERCONFIG=$HOME/._npmrc" > "$env/etc/conda/deactivate.d/conda.fish"
    done

    for env in "${CONDA_ENVS_NO_NPM}"; do
        echo "Did not found npm in $env"
        rm -rf "$env/etc/conda/activate.d/conda.sh" || true
        rm -rf "$env/etc/conda/activate.d/conda.fish" || true
        rm -rf "$env/etc/conda/deactivate.d/conda.sh" || true
        rm -rf "$env/etc/conda/deactivate.d/conda.fish" || true
    done

### Slack

What a nonsense of a program.

I was able to launch the nix version with the following wrapper script:

    export PATH="$HOME/bin/dummies:$PATH"
    mkdir -p ~/.cache/slack
    slack -r ~/.cache/slack

Also, it requires a `lsb_release` in the PATH, so here is one:

    echo "LSB Version:    Hey. I spent an hour figuring out why Slack doesn't launch."
    echo "Distributor ID: It seems like it requires an lsb_release."
    echo "Description:    But GNU Guix doesn't have one."
    echo "Release:        42.2"
    echo "Codename:       n/a"

### virt-manager

Run the following to fix the network:

    sudo virsh net-define /run/current-system/profile/etc/libvirt/qemu/networks/default.xml
    sudo virsh net-start default
    sudo herd restart libvirtd

### wakatime-cli

Note | Description
--- | ---
TODO | Package this for Guix

Before I figure out how to package this for Guix:

- Clone the repo
- Run `go build`
- Copy the binary to the `~/bin` folder

### Manifest

    (my/format-guix-dependencies category)

System

    (specifications->manifest
     '(
       <<packages("system")>>))

Table of Contents
<Expand> <Collapse>