+++
title = "Bootstrapping a NixOS Pinephone Installation"
date = 2022-06-15
description = "a flake-based quickstart for nixos-22.05"
+++

there is no official, easy-to-grab NixOS image to download and flash to devices like the pinephone today. although there is [a way](https://news.ycombinator.com/item?id=30010178) to do that via the hydra build cache, it's a bit tortured and the images built in automation don't have a user. the bootstrapping process would require manually editing `/etc/fstab` (from a different machine) to add a user and then logging in with a USB-C keyboard to setup ssh or a desktop -- neither user-friendly nor easily reproducible.

so let's bootstrap from an existing Nix install. first, provision a machine (either a NixOS installation or something with the `nix` binary installed). the architecture doesn't matter -- i'll assume it's x86_64 and show you how to cross-compile. create a new directory for the work
```sh
mkdir nixos-pinephone-getting-started && cd nixos-pinephone-getting-started
```
each section in this article corresponds to one commit in [this](https://git.uninsane.org/colin/nixos-pinephone-getting-started) repository in case you want to skip the commentary.

## Bare-minimal Build

for our Pinephone machine, we'll let [mobile-nixos](https://github.com/NixOS/mobile-nixos/) do the heavy lifting. create a minimal `flake.nix`:

```nix
{
  inputs = {
    # nixpkgs.url = "nixpkgs/nixos-22.05";
    nixpkgs.url = "nixpkgs/dfd82985c273aac6eced03625f454b334daae2e8";
    mobile-nixos = {
      # url = "github:nixos/mobile-nixos";
      url = "github:nixos/mobile-nixos/efbe2c3c5409c868309ae0770852638e623690b5";
      flake = false;
    };
  };

  outputs = { self, nixpkgs, mobile-nixos }: {
    pinephone-img = (nixpkgs.lib.nixosSystem {
      system = "aarch64-linux";
      modules = [
        (import "${mobile-nixos}/lib/configuration.nix" {
          device = "pine64-pinephone";
        })
        ({ ... }: {
          nixpkgs.config.allowUnfree = true;
        })
      ];
    }).config.mobile.outputs.u-boot.disk-image;
  };
}
```

i've frozen the mobile-nixos commit here because they're in the process of switching from U-Boot to towboot -- the pinned commit is among the last which support U-Boot, which this guide assumes. pinning `nixpkgs` may be paranoia: try switching to `nixpkgs/nixos-<release>` after you're bootstrapped.

if your host machine isn't aarch64, you'll have to enable cross compiling. the trivial way is emulation (i.e. qemu), though it could make the build take a full afternoon depending on how much of your system is available through nixcache.

on the host box, add this somewhere in your config (e.g. `/etc/nixos/configuration.nix`):

```nix
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
```

if you added this emulation, rebuild your host machine to enable it (`nixos-rebuild --flake /etc/nixos switch`).

then build the image with:
```sh
nixos-pinephone-getting-started$ nix build './#pinephone-img'
```

the resulting image is effectively the same as what Hydra spits out in the automation: no users, and no way to login. if you're brand new to the Pinephone, i recommend flashing the image and booting as a sanity check. otherwise, skip to [_Making the Image More Usable_](#making-the-image-more-usable) where we'll build a more usable image. at any time, consult the [_Troubleshooting_](#troubleshooting) section if you hit something unexpected.

### Flashing the Image
you might be tempted to flash this to the eMMC instead of an SD card, thinking that the former will be more reliable storage. it's not: my eMMC began to fail within 20 write cycles. i highly recommend you use an SD card for this.

```sh
sudo dd if=$(readlink ./result) of=/dev/sdb bs=4M oflag=direct conv=sync status=progress
```

`oflag=direct` makes the progress bar actually usable -- otherwise it'll just show how much data has been passed to the kernel -- and `conv=sync` seems to deal better with low quality SD cards (it feels like paranoia, but if i `fsck -f` the result it sometimes shows corruption without this flag).

insert the card to the Pinephone and boot it. the SD slot takes precedent over the eMMC in its boot sequence, so nothing more is needed. you should see the power indicator turn red (indicating that U-Boot is active), then yellow (u-boot has run the mobile-nixos boot script), then green (NixOS stage 1). you'll see a mobile-NixOS splash screen, it'll take a minute to validate the fs, and then boot into stage 2 where you'll be stuck at a login prompt.

## Making the Image More Usable
we'll want to rebuild the image and include a user, desktop environment, and some basic applications. i tried Phosh, Plasma Mobile, and Gnome: of these, Phosh works the best OOTB _by far_ (Plasma Mobile is _slow_ and crash-prone, Gnome lacks an on-screen keyboard outside the core apps and its navigation is less tailored to phones).

we'll use home-manager to make user setup a bit nicer. add that to `flake.nix`:

```diff
 inputs = {
   nixpkgs.url = "nixpkgs/dfd82985c273aac6eced03625f454b334daae2e8";
   mobile-nixos = {
     url = "github:nixos/mobile-nixos/efbe2c3c5409c868309ae0770852638e623690b5";
     flake = false;
   };
+  home-manager.url = "github:nix-community/home-manager/release-22.05";
 };
```

add a module for this pinephone build:
```diff
-  outputs = { self, nixpkgs, mobile-nixos }: {
+  outputs = { self, nixpkgs, mobile-nixos, home-manager }: {
     pinephone-img = (pkgs-mobile.lib.nixosSystem {
       system = "aarch64-linux";
+      specialArgs = { inherit home-manager; };
       modules = [
         (import "${mobile-nixos}/lib/configuration.nix" {
           device = "pine64-pinephone";
         })
-        ({ ... }: {
-          nixpkgs.config.allowUnfree = true;
-        })
+        ./modules/default.nix
      ];
    }).config.mobile.outputs.u-boot.disk-image;
    ...
```

and then populate `modules/default.nix`. you could just throw everything in here, but it's more readable (and reusable -- in case you want to share this config across machines) if you split it up:

```nix
{ ... }:
{
  imports = [
    ./hardware.nix
    ./home-manager.nix
    ./phosh.nix
    ./users.nix
  ];

  system.stateVersion = "22.05";
  nixpkgs.config.allowUnfree = true;
}
```

`modules/hardware.nix`:
```nix
{ ... }:
{
  ## enable the hardware rotation sensor
  hardware.sensor.iio.enable = true;

  hardware.opengl.enable = true;
  hardware.opengl.driSupport = true;
}
```

`modules/home-manager.nix`:
```nix
{ pkgs, home-manager, ... }:
{
  imports = [
    home-manager.nixosModule
  ];

  home-manager.useGlobalPkgs = true;
  home-manager.useUserPackages = true;

  home-manager.users.colin = {
    home.stateVersion = "21.11";
    home.username = "colin";
    home.homeDirectory = "/home/colin";

    programs = {
      home-manager.enable = true;
      firefox.enable = true;
      git.enable = true;
    };

    # a few useful packages to start with
    home.packages = with pkgs; [
      # useful CLI/admin tools to have during setup
      fatresize
      gptfdisk
      networkmanager
      sudo
      vim
      wget

      # it's good to have a variety of terminals (x11, Qt, GTK) to handle more failures
      xterm
      plasma5Packages.konsole
      gnome.gnome-terminal
    ];
  };
}
```

`modules/phosh.nix`:
```nix
{ ... }:
{
  services.xserver.desktopManager.phosh = {
    enable = true;
    user = "colin";
    group = "users";
  };

  environment.variables = {
    # Qt apps won't always start unless this env var is set
    QT_QPA_PLATFORM = "wayland";
  };
}
```

`modules/users.nix`:
```nix
{ ... }:
{
  users.mutableUsers = false;

  users.users.colin = {
    isNormalUser = true;
    home = "/home/colin";
    uid = 1000;
    # make this numeric so that you can enter it in the phosh lockscreen.
    # DON'T leave this empty: not all greeters support passwordless users.
    initialPassword = "147147";
    extraGroups = [ "wheel" ];
  };

  security.sudo = {
    enable = true;
    wheelNeedsPassword = false;
  };

  services.openssh = {
    enable = true;
    permitRootLogin = "no";
    passwordAuthentication = true;
  };
}
```

build and flash the image as before. this is the final image we'll flash, so before you eject the card resize the rootfs to make use of the full device. do NOT use `fdisk` or `parted` for this. it disrupts some on-disk structures required by the bootloader (this won't be a worry after mobile-nixos officially switches to tow-boot). instead, use the ncurses frontend to fdisk: `cfdisk`:

```sh
$ sudo cfdisk /dev/sdb
scroll to /dev/sdb4
> Resize
  leave at default (28.8G)
> Write
  type "yes"
> Quit
```

eject the card and boot the phone, login, and toy around with it. open the display settings and mess with the scale (it defaults to 200%). i find 150% is best, particularly so that Firefox doesn't overflow.

## Building Generations on the Device
we don't want to re-flash the device _every_ time we change something. let's update the flake to allow on-device updates.

```diff
- outputs = { self, nixpkgs, mobile-nixos, home-manager }: {
-   pinephone-img = (nixpkgs.lib.nixosSystem {
+ outputs = { self, nixpkgs, mobile-nixos, home-manager }: rec {
+   nixosConfigurations.pinephone = (nixpkgs.lib.nixosSystem {
      system = "aarch64-linux";
      modules = [
        (import "${mobile-nixos}/lib/configuration.nix" {
          device = "pine64-pinephone";
        })
       ./pinephone.nix
      ];
-   }).config.mobile.outputs.u-boot.disk-image;
+   });
+   pinephone-img = nixosConfigurations.pinephone.config.mobile.outputs.u-boot.disk-image;
    nixosConfigurations.host = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      ...
```

you may need network access for this. unless you connect a USB-C keyboard, the graphical network manager might not be usable. in that case, open a terminal and run:
```sh
sudo nmcli device wifi connect <wifiname> password <password>
```

copy your flake over to the phone at `/etc/nixos/flake.nix`. i use git for this. on the phone (or over ssh -- try `ssh nixos` from the same LAN or use `ip addr` to find its IP):
```sh
~$ git clone https://git.uninsane.org/colin/nixos-pinephone-getting-started.git
~$ sudo rmdir /etc/nixos && sudo mv nixos-pinephone-getting-started /etc/nixos
~$ cd /etc/nixos
/etc/nixos$ sudo git config --global --add safe.directory /etc/nixos
/etc/nixos$ sudo nixos-rebuild --flake "./#pinephone" switch
```

validate this with a reboot, and you should be golden! i recommend mirroring your `/etc/nixos` folder to at least one other place: a github repo, `rsync` to a desktop, `duplicity` to cloud storage, etc. that way if you ever brick your phone you can restore from your latest config using the `dd` approach. managing generations and rolling back to a good one doesn't seem quite as easy to do on a phone.

happy nixing :-)

![](booted-running-firefox.jpg)


## Troubleshooting

- **device is unbootable or unflashable (eMMC isn't exposed as a /dev node over USB):**

these issues tend to be power-related. flash a minimal, known-good image (like [postmarketOS](https://images.postmarketos.org/pinephone/)) to the SD card and boot with the battery and USB charger plugged in. wait until the battery gets to a good charge and resume.

if it's still not working, try holding the reset button underneat the back cover of the phone for 10s.

- **fs appears to be corrupted:**

validate the built, but unflashed, image:
```sh
# create a loopback device from our .img file:
# ./result should be a symlink to /nix/store/<hash>-pine64-pinephone_full-disk-image.img
$ sudo losetup -Pf $(readlink ./result)
# check the rootfs partition (partition #4):
# you might see a single, inconsequential error about "Padding at end of inode bitmap is not set."
$ sudo fsck -f /dev/loop0p4
# close the loopback device:
$ sudo losetup -d /dev/loop0
```

then flash the image using the dd flags i show in the article. while the SD card is still attached to the host, validate it with  `sudo fsck -f /dev/sdb4`. 

finally, the eMMC has a dozen or so write cycles: prefer an SD card.

- **the nix config won't build:**

nixos moves fast, but does occasionally break. you might need to freeze the nixpkgs to a specific commit. try cloning the [repo](https://git.uninsane.org/colin/nixos-pinephone-getting-started) for this post and building it directly -- the repo includes `flake.lock` so that it should be more reproducible.


## Additional Resources:

- Tom Fitzhenry [building a minimal Pinephone image](https://git.sr.ht/~tomf/notes/tree/master/item/pinephone-nixos-getting-started.md) by dropping his config straight into a mobile-nixos checkout, no `flake.nix` required.
- Ana, Hoverbear on [making an install image](https://hoverbear.org/blog/nix-flake-live-media/) from an existing NixOS configuration.
- Cole Mickens' [public nix config](https://github.com/colemickens/nixcfg) shows how to maintain a shared repo for multiple machines with more advanced things like custom packages, modules, and secrets.
- [my own nix config](https://git.uninsane.org/colin/nix-files/) which targets multiple machines with one of these operating a nix cache to speed up pinephone builds.
- Pavel Makhov showing a better way to [distribute builds across machines](https://eno.space/blog//2021/08/nixos-on-underpowered-devices) which i haven't got around to trying.
- the [NixOS Matrix space](https://matrix.to/#/#community:nixos.org) which contains chat rooms like `#nixos-on-arm:nixos.org`.
- or contact me directly via the links in my [about](/about) page.
