/ home / blog about

Bootstrapping a NixOS Pinephone Installation

2022/06/15

there is no official, easy-to-grab NixOS image to download and flash to devices like the pinephone today. although there is a way 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

mkdir nixos-pinephone-getting-started && cd nixos-pinephone-getting-started

each section in this article corresponds to one commit in this repository in case you want to skip the commentary.

Bare-minimal Build

for our Pinephone machine, we'll let mobile-nixos do the heavy lifting. create a minimal flake.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):

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:

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 where we'll build a more usable image. at any time, consult the 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.

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:

 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:

-  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:

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

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

modules/hardware.nix:

{ ... }:
{
  ## enable the hardware rotation sensor
  hardware.sensor.iio.enable = true;

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

modules/home-manager.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:

{ ... }:
{
  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:

{ ... }:
{
  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:

$ 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.

- 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:

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):

~$ 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 :-)

Troubleshooting

these issues tend to be power-related. flash a minimal, known-good image (like postmarketOS) 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.

validate the built, but unflashed, image:

# 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.

nixos moves fast, but does occasionally break. you might need to freeze the nixpkgs to a specific commit. try cloning the repo for this post and building it directly -- the repo includes flake.lock so that it should be more reproducible.

Additional Resources: