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
- 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) 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:
# 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 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 by dropping his config straight into a mobile-nixos checkout, no
flake.nix
required. - Ana, Hoverbear on making an install image from an existing NixOS configuration.
- Cole Mickens' public nix config shows how to maintain a shared repo for multiple machines with more advanced things like custom packages, modules, and secrets.
- my own nix config 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 which i haven't got around to trying.
- the NixOS Matrix space which contains chat rooms like
#nixos-on-arm:nixos.org
. - or contact me directly via the links in my about page.