Running NixOS on the Librem 5
03 November 2023I set up NixOS on my new phone, following a NixCon 2023 talk by Sophie Tauchert.
Introduction
So, I got a used Purism Librem 5. Bought it used with the grand idea of actually doing something useful: contributing to the mobile GNU/Linux ecosystem.
This was definitely inspired by Sophie Tauchert's talk at NixCon 2023, where she goes over her work and the current status. I thought, since she's already upstreamed that work to nixos-hardware, it should be easy enough to reproduce, right?
NixOS seems a good match for a mobile phone. In particular, the ability to easily rollback to a previous known-good system configuration is a key feature. Traditionally this is done at the OS-level with an A/B partitioning system, but with NixOS this works for the entire system configuration. Theoretically, should I break things while I'm tinkering, it's easy to return to a known good state.
Something I personally find important in my smartphone is stability. While I've dabbled plenty in custom ROMs, I've mostly stuck to well-supported and less flavourful distributions such LineageOS and its predecessor CyanogenMod. Ultimately I've never felt comfortable leaving my phone in an unusable state while tinkering with it, which has caused me plenty of stress while modding my phones.
Less recently, I purchased another used Android smartphone to replace my LG G3 which had served me well for almost a decade at that point. With it, I could still meet my daily needs in a smartphone, but its age was starting to show. It had undergone a motherboard replacement, consumed a handful of (removable!) batteries, and still functions normally to this day. However it struggled with some of the more intensive applications - in particular Organic Maps, it struggled to survive a full day's usage, and most importantly, custom ROM support had dried up, no longer being officially supported by LineageOS.
While I was tempted to dabble in the Android ROM development scene and bring it back to life, that seemed like a lot of work for a phone I'd most likely not use again. I can hardly hack on my daily driver, right? On top of that, I'm aware of what a mess Android distributions are. Kernel forks, non-mainlined drivers, proprietary firmware blobs. It takes a small army of volunteers to maintain all these devices - with a lot of redundant effort across ROM development groups - to keep these old devices running long after their manufacturers have dropped support.
I'm immensely grateful for all these volunteers (especially HardStyl3r, who supported the LG G3 across several versions of LineageOS), but I can't help viewing the Android ecosystem as a whole with a degree of fatalism. Google has made some attempts at improving the software support situation for Android devices, e.g. with Project Treble, but in the years since, device manufacturers haven't improved their support guarantees much. If, like myself, you choose to go through the rigmarole of granting yourself root access to fully control your device, you'll often find yourself locked out of automatic updates anyway. Being effectively punished for wanting to truly own my phone does not engender feelings of satisfaction.
This is all while ignoring the hulking elephant in the room that is Google itself, whose motives and priorities are usually misaligned with my own.
So, enter the extreme option: mobile GNU/Linux. I find this much more appealing, especially given I already use this on my desktops every day. Less proprietary, more Free, and independent from Google. Mobile GNU/Linux has picked up some momentum more recently, with manufacturers such as PINE64 and Purism releasing smartphone with (mostly) open hardware with (almost) mainline Kernel support. Minimise the work needed for each new device - now that seems like a more sustainable approach.
And so, as is commonly the case with me, I opted to act out of principle rather than practicality.
My somewhat recent delve into Nix and NixOS has given me the impression that it's a much better way to build software and GNU/Linux distributions. Theoretically, there's nothing I depend upon a smartphone for that couldn't run on GNU/Linux. Well, with the exception of my banking, but I'd happily drop my bank for one that allows me to access my account through a (relatively) more open platform like the Web.
Starting point
The phone arrived with Purism's PureOS distribution installed, version 10 (Byzantium). Full-disk encryption was enabled, with a pretty unlock screen on boot. Judging from the few megabytes of free space before the real partitions, u-boot was already installed (more on that later).
It all seemed to be working normally, though I didn't have a spare activated SIM at hand to test telephony. A deactivated one I had lying around did get picked up, but of course couldn't connect to the network. It did briefly show 3G in the status bar, which sounded promising. The kill switch also appeared to work correctly: disabling and then re-enabling the switch made the mobile network grey out and then reconnect respectively. Unfortunately, when I remove the SIM tray the phone consistently rebooted, which I suspect is an intentional hardware feature. I ordered a new SIM from a cheap UK provider and left testing telephony for later.
In Sophie's talk, she mentions the work she upstreamed into nixos-hardware, so that's where I started.
The first step mentions setting up Jumpdrive, a small system you can flash to the phone to expose its storage over USB. Given that the phone already had PureOS installed and functioning on it, I opted to skip this for now, as I could potentially install NixOS over SSH with nixos-anywhere, discussed in another NixCon 2023 talk. Fingers crossed I don't have to get into recovery mode at some point.
The next step was to install u-boot - I suppose a different version than was
already installed? nixos-hardware does not expose these packages as a
regular Flake would, with the instructions using the traditional nix-build
instead of Flake-y nix build
. Not knowing how to enable cross compilation with
nix-build
, I decided to make a quick and dirty Flake in my checkout of the
repository:
{
;
;
}
I built it on my x86-64 desktop with aarch64 binfmt emulation enabled, it didn't take too long.
The result is a script which writes the built image to the root of the main disk. To get this on the device, I simply copied the script and the referenced image over WiFi onto the device, then executed it:
$ sudo TARGET=. ./u-boot-install-librem5 /dev/mmcblk0
There was some output from dd
which all indicated success. After a bit of
fretful procrastination, I finally rebooted the device and... was pleasantly met
with the full-disk encryption prompt as normal. Success!
Building a configuration
First things first: I needed a NixOS system configuration for the phone.
I started off with the suggested approach of enabling Phosh, but quickly stumbled once I realised I had to deal with full-disk encryption, which I hadn't used before on NixOS. This is definitely a hard requirement for a smartphone personally - anyone grabbing my phone should not have immediate trivial access to it contents. The regular cryptsetup method suits me just fine, I won't worry about more in-depth systems like boot verification for now.
On NixOS, cryptsetup is normally controlled with
boot.initrd.luks.devices,
but I was unsure of how to actually input the decryption password. I was also
unsure if simply plugging in a USB-C keyboard would work; my main keyboard does
use USB-C, but it requires the hid_apple
kernel module to be enabled in the
initial ramdisk. It does work in the existing PureOS installation, so it was
probably excessive caution. In any case, having to plug in a keyboard to boot a
phone is not ideal, so I continued researching entirely sure I dug around on
PureOS and discovered that it's using Plymouth with an on-screen keyboard
extension, osk-sdl. An attempted
package patch for nixpkgs was
discarded, with Samuel
Dionne-Riel mentioning the Mobile NixOS project had
rejected
osk-sdl.
Of course, I'd completely forgotten that the Mobile NixOS project existed. Its main goals are providing a touchscreen-based boot process, and to automatically sort out any device quirks for you. The latter is already provided by Sophie's nixos-hardware module, but the former sounds like a great solution to me.
Mobile NixOS doesn't currently have any support for the Librem 5, but I thought it shouldn't be too difficult to port it. My first evaluable system derivation is chronicled in 275174d3. I don't think I quite got the hardware config right, but I wanted to get something building first. The configuration used nixpkgs unstable, as the Mobile NixOS docs stated:
Note: Mobile NixOS is only expected to build succesfully against the unstable branch of Nixpkgs
$ nix build .#nixosConfigurations.maia.config.system.build.toplevel
The first build took quite a while, with over 300 dependencies to be built.
After getting through about 1000, it finally fell over on gdm
, the GNOME
desktop manager. At this point my only solution seemed to be updating my nixpkgs
pins. But knowing the Mobile NixOS project existed, and aarch64 was supported by
NixOS, there was surely some nixpkgs commit where all these dependencies where
already in the public Nix cache. And so to hydra I went, eventually finding the
Mobile NixOS project, and found a recent build that
evaluated the Phosh example
successfulyl. In the
inputs tab I found the
nixpkgs commit used: 5e4c2ad.
So I updated my nixpkgs-unstable pin accordingly in 544a1ea6:
$ nix flake lock --override-input nixpkgs-unstable github:nixos/nixpkgs/5e4c2ada4fcd54b99d56d7bd62f384511a7e2593 --update-input nixpkgs-unstable
I then hit another evaluation error:
$ nix build .#nixosConfigurations.maia.config.system.build.toplevel
error: systemdStage1 cannot be found in pkgs
This confounded me for quite a while. Especially since I could find and build
this in the pkgs
attribute of the derivation:
nix-repl> :b outputs.nixosConfigurations.maia.pkgs.systemdStage1
:b outputs.nixosConfigurations.maia.pkgs.systemdStage1
The only useful information the trace (--show-trace
) gave me was that this was
a dependency of plymouth
. After some unsuccessful attempts at debugging (the
module system remains as opaque as ever) I eventually realised this was a
problem with the NixOS system definition. It was being defined with the stable
nixpkgs flake, but I was overriding pkgs
to use the imported unstable nixpkgs
flake. I fixed the problem in
236b756c
by using the unstable nixpkgs lib
instead, leaving myself a note to research
how nixpkgs works in NixOS configurations.
Starting the build again, the dependencies were substituted from the cache instead of built locally! But then it hit the kernel, which I knew was going to be a little tough.
Building the kernel
The nixos-hardware repo contains a package for Purism's kernel fork for the Librem 5, so that's probably the kernel package I need to use.
This is actually the first time I've tried to build Linux itself properly,
aside from when I first tried to put NixOS on a Raspberry Pi. That first
attempt, I ran into disk space - in reality, /tmp
space - due to the sheer
quantity of code being compiled. In particular, I remember attempting to compile
several GPU drivers - things that I definitely don't need on an ARM SOC. On top
of that, Nix is compiling using an aarch64 emulator, which slows building down
tremendously.
Last time I hit the disk space issues I also recall researching and discovering
that you can direct Nix where to put the build directory, which would be a
workaround. It was something like TEMP
, TEMPFS
, TMPFS
. I try it out for a
few builds but nothing seems to work - the build directories are generated in
/tmp
regardless.
So I first tried starting a build just to check it wouldn't immediately fail. It started building successfully, but slowly. At this point I figured I should turn on compilation caching for the derivation, in case I run into the same disk space issue. It's a bit confusing, but following the wiki and a discourse thread, I eventually got it working in ea9246cd.
Even with the compilation cache the build speed doesn't seem to be much improved, even when interrupting and re-running the start of the build. But hey, it's something, and I figure it may come in handy as a mess with the package.
The next thing I want to figure out is how to turn off those drivers I
definitely don't need. Time to give nix develop
a whirl:
$ nix develop .#nixosConfigurations.maia.config.system.build.kernel
$ mkdir /tmp/kernel
$ unpackPhase
unpacking source archive /nix/store/hsqvhjlchdc93mbmsaxjb7sm9vs3cks7-source
source root is source
$ cd ./source
$ patchPhase
applying patch /nix/store/23728y7zgh1jb55kpwxv5qnjbq0ykca6-randstruct-provide-seed-5.19.patch
patching file scripts/gen-randstruct-seed.sh
substituteStream(): WARNING: pattern '/bin/pwd' doesn't match anything in file 'Makefile'
substituteStream(): WARNING: pattern '/bin/pwd' doesn't match anything in file 'tools/scripts/Makefile.include'
patching script interpreter paths in scripts/ld-version.sh
scripts/ld-version.sh: interpreter directive changed from "#!/bin/sh" to "/nix/store/4qp96hwq3wqkqhv99m76g2rp8sdjcl4v-bash-5.2-p15/bin/sh"
patching script interpreter paths in scripts
...
arch/arm64/boot/install.sh: interpreter directive changed from "#!/bin/sh" to "/nix/store/4qp96hwq3wqkqhv99m76g2rp8sdjcl4v-bash-5.2-p15/bin/sh"
$ configurePhase
no configure script, doing nothing
$ buildPhase
build flags: -j12 SHELL=/bin/bash O=\$\(buildRoot\) CC=/nix/store/ahrxi3smaz2k98dsilzfjh5kbqya4bil-ccache-links-wrapper-4.8.3/bin/cc HOSTCC=/nix/store/ahrxi3smaz2k98dsilzfjh5kbqya4bil-ccache-links-wrapper-4.8.3/bin/cc HOSTLD=/nix/store/azdignpplcnd1gw87a3zb8nyabqnfqi9-binutils-wrapper-2.40/bin/ld ARCH=arm64 KBUILD_BUILD_VERSION=1-NixOS Image vmlinux modules dtbs DTC_FLAGS=-@
***
*** Configuration file ".config" not found!
***
*** Please run some configurator (e.g. "make oldconfig" or
*** "make menuconfig" or "make xconfig").
***
Makefile:770: include/config/auto.conf.cmd: No such file or directory
make: *** [Makefile:779: .config] Error 1
Okay, the build falls over immediately, meaning I've missed a step. The
configuraPhase
doing nothing is suspicious, as normally the sequence of
unpack, patch, configure, build should Just Work. The fact that configurePhase
bailed is suspicious. As I've never built the kernel manually before, I need to
understand the build process before I can proceed.
I start by investigating the regular derivation with nix edit nixpkgs#linux
.
This opens up pkgs/os-specific/linux/kernel/mainline.nix
, which looks like
some unhelpful boilerplate. But the directory looks about right, so I go up a
level and inspect some of the neighbouring files. In generic.nix
I find more
useful derivation derivation. It's more complex than most I've seen, which makes
sense for the complexity of the kernel. There's a lot relating to
configuration, which involves importing an auxiliary derivation in
./manual-config.nix
.
It's strange that there's extra patches like this in the generic derivation, yet it doesn't seem to be applied in my source directory:
{
# ...
postPatch = kernel.postPatch + ''
# Patch kconfig to print "###" after every question so that
# generate-config.pl from the generic builder can answer them.
sed -e '/fflush(stdout);/i\printf("###");' -i scripts/kconfig/conf.c
'';
}
The Librem kernel derivation specifically uses the buildLinux
function, so I
go hunting for it. nix edit nixpkgs#buildLinux
doesn't find
anything. A simply search for buildLinux =
finds...
{
buildLinux = attrs: callPackage ../os-specific/linux/kernel/generic.nix attrs;
}
So I'm looking in the right place I guess.
I look at the build log of my previous attempt:
$ nix log /nix/store/7m0x5qgxw9g2xzbhvrn87izp0grnhpdx-linux-6.4.14-librem5.drv
@nix { "action": "setPhase", "phase": "unpackPhase" }
unpacking sources
unpacking source archive /nix/store/hsqvhjlchdc93mbmsaxjb7sm9vs3cks7-source
source root is source
@nix { "action": "setPhase", "phase": "patchPhase" }
patching sources
...
@nix { "action": "setPhase", "phase": "updateAutotoolsGnuConfigScriptsPhase" }
updateAutotoolsGnuConfigScriptsPhase
@nix { "action": "setPhase", "phase": "configurePhase" }
configuring
...
There's a curious build phase I've never seen before. I run it in my
source tree and get no output, same as the log. But configurePhase
still says
no configure script, doing nothing
, what gives?
configurePhase
has the log message at the end:
Which means that configureScript
must be undefined, and sure enough it is. But
where should it come from, and what should it be defined to.
For sanity I try a different approach, running nix develop .#nixosConfigurations.maia.pkgs.linuxPackages_librem5.kernel
instead. No dice,
configurePhase
has the same output. Curiously this and
.#nixosConfigurations.maia.config.system.build.kernel
are different
derivations, I'm not sure why.
At this point I give in and search up how others use Nix to hack on the kernel.
I find this NixOS & Flakes
Book
page with a sample flake. The key difference with their approach is in their use
of linuxPackages_thead.kernel.dev
, the dev
attribute of the derivation. I
try nix develop ~/.config/nix#nixosConfigurations.maia.config.system.build.kernel.dev
but
there's no difference in running configurePhase
.
Another thing that's confusing me is the different between the configurePhase
function and $configurePhase
variable:
$ echo $configurePhase
runHook preConfigure mkdir build export buildRoot="$(pwd)/build" echo "manual-config configurePhase buildRoot=$buildRoot pwd=$PWD" if [ -f "$buildRoot/.config" ]; then echo "Could not link $buildRoot/.config : file exists" exit 1 fi ln -sv /nix/store/4hvv1ywdp5k7p0mb4sd9azhywgqpb8cv-linux-config-6.4.14-librem5 $buildRoot/.config # reads the existing .config file and prompts the user for options in # the current kernel source that are not found in the file. make $makeFlags "${makeFlagsArray[@]}" oldconfig runHook postConfigure make $makeFlags "${makeFlagsArray[@]}" prepare actualModDirVersion="$(cat $buildRoot/include/config/kernel.release)" if [ "$actualModDirVersion" != "6.4.14-librem5" ]; then echo "Error: modDirVersion 6.4.14-librem5 specified in the Nix expression is wrong, it should be: $actualModDirVersion" exit 1 fi buildFlagsArray+=("KBUILD_BUILD_TIMESTAMP=$(date -u -d @$SOURCE_DATE_EPOCH)") cd $buildRoot
The variable contains that manual-config
line that is in the derivation, but
running it produces no output if I run $configurePhase
. (set -x; $configurePhase)
shows a bunch of flags being built before an exit 0
.
This is driving me a bit mad. Let's try building the kernel the traditional way.
Building the kernel, take 2
Some searching brings me to the Kernel docs which link to this "admin guide". The file path and title are a bit misleading, but the breadcrumb says The Linux kernel user’s and administrator’s guide, which is a bit more accurate.
This guide soon directs you to build menuconfig
, a tool for configuring the
kernel. It quickly falls over due to ncurses
being unavailable.
$ make O=build/kernel menuconfig
GEN Makefile
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/mconf.o
In file included from /tmp/kernel/source/scripts/kconfig/mconf.c:23:
/tmp/kernel/source/scripts/kconfig/lxdialog/dialog.h:19:10: fatal error: ncurses.h: No such file or directory
19 | #include <ncurses.h>
| ^~~~~~~~~~~
compilation terminated.
make[2]: *** [/tmp/kernel/source/scripts/Makefile.host:131: scripts/kconfig/mconf.o] Error 1
make[1]: *** [/tmp/kernel/source/Makefile:692: menuconfig] Error 2
make: *** [Makefile:226: __sub-make] Error 2
Which means I need to have ncurses
as a shell input. It's rather difficult to
make an impromptu shell including a particular package for development, so I
think I have to bodge one into my Flake. I end up overriding my existing
override for the kernel package to add the build input:
{
# ...
cached_linuxPackages_librem5 = (super.linuxPackages_librem5.kernel.override {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
}).overrideAttrs(old: {
buildInputs = [super.ncurses];
});
}
Nastier, but easier. I nix develop
into the configuration attribute and now
the build proceeds further, but fails with missing symbol errors:
$ make O=build/kernel menuconfig
GEN Makefile
HOSTCC scripts/kconfig/mconf.o
HOSTCC scripts/kconfig/lxdialog/checklist.o
HOSTCC scripts/kconfig/lxdialog/inputbox.o
HOSTCC scripts/kconfig/lxdialog/menubox.o
HOSTCC scripts/kconfig/lxdialog/textbox.o
HOSTCC scripts/kconfig/lxdialog/util.o
HOSTCC scripts/kconfig/lxdialog/yesno.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/mconf
/nix/store/as2izmyw2771n7p60nlm0f6a84kmnyin-binutils-2.40/bin/ld: scripts/kconfig/mconf.o: in function `show_help':
mconf.c:(.text+0xa18): undefined reference to `stdscr'
/nix/store/as2izmyw2771n7p60nlm0f6a84kmnyin-binutils-2.40/bin/ld: mconf.c:(.text+0xa20): undefined reference to `stdscr'
...
I took a brief diversion to test whether this is an issue with cross-compiling by testing out building the kernel on the native x86-64 architecture. I added the kernel to my Flake's packages like so:
{
# ...
linuxPackages_librem5 = pkgs.cached_linuxPackages_librem5;
}
I can't even open the dev shell however, because the config dependency fails to build:
$ nix develop ~/.config/nix#linuxPackages_librem5
warning: Git tree '/home/william/.config' is dirty
error: builder for '/nix/store/m2wx8d43gnqhjg9k4khwpdvy97224gfj-linux-config-6.4.14-librem5.drv' failed with exit code 2;
last 10 log lines:
> HOSTCC scripts/kconfig/symbol.o
> HOSTCC scripts/kconfig/util.o
> HOSTLD scripts/kconfig/conf
> ***
> *** Can't find default configuration "arch/x86/configs/librem5_defconfig"!
> ***
> make[2]: *** [../scripts/kconfig/Makefile:94: librem5_defconfig] Error 1
> make[1]: *** [/build/source/Makefile:692: librem5_defconfig] Error 2
> make: *** [Makefile:226: __sub-make] Error 2
> make: Leaving directory '/build/source'
For full logs, run 'nix log /nix/store/m2wx8d43gnqhjg9k4khwpdvy97224gfj-linux-config-6.4.14-librem5.drv'.
error: 1 dependencies of derivation '/nix/store/s05nyymk5bsf7gmxfzl1mbas4y7m23lr-linux-6.4.14-librem5-env.drv' failed to build
Clearly the configuration file is only specified for the aarch64 architecture,
and sure enough I find it under arch/arm64/configs/librem5_defconfig
. This
does complicate things, if I want to build this package I'll have to patch the
package. I tried to extend postPatch
in my overlay:
{
# ...
cached_linuxPackages_librem5 = (super.linuxPackages_librem5.kernel.override {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
}).overrideAttrs(old: {
buildInputs = [super.ncurses];
postPatch = old.postPatch + ''
cp arch/arm64/configs/librem5_defconfig arch/x86/configs/
'';
});
}
But this failed the same as before. Apparently this isn't enough to override the config derivation.
I do a bit more reading and digging around, and find out that these config
values are "tristate", being either y
(yes, on), no
(no, off), or m
(module, as in loaded separately at runtime as a kernel module). I find the
appropriate config in drivers/gpu/drm/amd/amdgpu/Kconfig
: DRM_AMDGPU
. So I
want to set CONFIG_DRM_AMDGPU=n
somehow.
At this point I noticed something in the generic Linux derivation:
{
# ...
# Adds dependencies needed to edit the config:
# nix-shell '<nixpkgs>' -A linux.configEnv --command 'make nconfig'
configEnv = kernel.overrideAttrs (old: {
nativeBuildInputs = old.nativeBuildInputs or [] ++ (with buildPackages; [
pkg-config ncurses
]);
});
}
This... sounds like exactly what I need. I couldn't find it in the
system.build.kernel
attribute, but it is in the package proper:
$ nix develop ~/.config/nix#nixosConfigurations.maia.config.system.build.kernel.configEnv
$ make V=1 O=build/kernel nconfig
Huzzah! I get a beautiful ncurses GUI.
In this GUI I can load the Librem 5 config file, and navigating to Device Drivers > Graphics support
I do indeed see the AMDGPU and Radeon drivers
enabled. I try saving my configuration to compare it to the existing one, but
wherever nconfig is saving them, it isn't in the working directory. Turns out it
was in the build directory, because I was running it with make
. The new config
is very different to the existing one, being more verbose and thus less
diffable. But I do find this in it:
# CONFIG_DRM_AMDGPU is not set
I guess it's off by default? I don't really get why it's being built then. On
another note, I find the NixOS package has the argument structuredExtraConfig
,
which lets you specify config overrides. Handy. I think I'll give that a shot
and give up on building the kernel manually.
{
# ...
cached_linuxPackages_librem5 = super.linuxPackages_librem5.kernel.override {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
structuredExtraConfig = {
CONFIG_DRM_AMDGPU = "n";
CONFIG_DRM_RADEON = "n";
CONFIG_DRM_NOUVEAU = "n";
};
};
}
Okay, let's give building another shot... once I figure out how to change the build directory.
Changing the build directory
Going off my previous knowledge, I search for TMPDIR
in the NixOS Matrix rooms
and find this
message
in the NixOS ARM Matrix room. Turns out I was right, that was the environment
variable I needed, but the key thing I missed was that this doesn't affect
builds run through the Nix daemon. This includes building as my unprivileged
user with Nix installed in multi-user mode.
A workaround therefore, is to run the build as root. Not ideal, but it'll do for now:
$ sudo TMPDIR=/data/random/nix nix build ~/.config/nix#nixosConfigurations.maia.config.system.build.kernel
$ ls /data/random/nix
nix-build-linux-6.4.14-librem5.drv-0
Finally! Now, time to wait for the build to complete.
Halfway through I dig into the build directory and find the config file under
/source/build/.config
. That the AMDGPU etc. options are still set to m
, hmm.
I inspect the Librem buildLinux
call and see that they don't have the
CONFIG_
prefix. Whoops. Time to try again. Hopefully that build cache works.
{
# ...
cached_linuxPackages_librem5 = super.linuxPackages_librem5.kernel.override {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
structuredExtraConfig = {
DRM_AMDGPU = "n";
DRM_RADEON = "n";
DRM_NOUVEAU = "n";
};
};
}
I try again but the config file is wrong, again.
$ nix eval ~/.config/nix#nixosConfigurations.maia.config.system.build.kernel.structuredExtraConfig
warning: Git tree '/home/william/.config' is dirty
{ CMA_SIZE_MBYTES = { _type = "override"; content = { freeform = "320"; optional = false; }; priority = 50; }; }
This is clearly wrong - somehow my override isn't being applied.
I faff around a bunch with builtins.trace
, during which I find stdenv
is
being overridden correctly. I eventually notice some weirdness around the Librem
kernel derivation involving overrides, and particularly this comment:
structuredExtraConfig = with lib.kernel; {
# buildLinux overrides this and defaults to 32, so go back to the value defined librem5_defconfig
# this is required for millipixels to take photos, otherwise the VIDIOC_REQ_BUFS ioctl returns ENOMEM
CMA_SIZE_MBYTES = lib.mkForce (freeform "320");
};
So they had to perform some hijinks to force an option value. Thankfully they left in a workaround:
{ lib
, buildLinux
, fetchFromGitLab
, ...
} @ args:
buildLinux (args
// rec {
defconfig = "librem5_defconfig";
version = "6.4.14-librem5";
modDirVersion = version;
src = fetchFromGitLab {
domain = "source.puri.sm";
owner = "Librem5";
repo = "linux";
rev = "pureos/6.4.14pureos1";
hash = "sha256-PzRG6czWLMahklceuaWGK1QJ+m9FAKDa/m1jp87h62k=";
};
kernelPatches = [ ];
structuredExtraConfig = with lib.kernel; {
# buildLinux overrides this and defaults to 32, so go back to the value defined librem5_defconfig
# this is required for millipixels to take photos, otherwise the VIDIOC_REQ_BUFS ioctl returns ENOMEM
CMA_SIZE_MBYTES = lib.mkForce (freeform "320");
};
}
// args.argsOverride or { })
I try using argsOverride
instead:
{
# ...
cached_linuxPackages_librem5 = (super.linuxPackages_librem5.kernel.override (old: {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
argsOverride.structuredExtraConfig = builtins.trace "structuredExtraConfig" (with super.lib.kernel; {
DRM_AMDGPU = no;
DRM_RADEON = no;
DRM_NOUVEAU = no;
});
}));
}
And it works! The configuration rebuilds itself, then the full kernel build begins proper. It's noticeably slower than before. Inspecting the compiler cache, I can see a tiny number of hits:
$ nix run nixpkgs#ccache -- -s -d /data/random/ccache/
Cacheable calls: 1037 / 1766 (58.72%)
Hits: 10 / 1037 ( 0.96%)
Direct: 10 / 10 (100.0%)
Preprocessed: 0 / 10 ( 0.00%)
Misses: 1027 / 1037 (99.04%)
Uncacheable calls: 729 / 1766 (41.28%)
Local storage:
Cache size (GiB): 3.7 / 5.0 (74.64%)
Hits: 10 / 1037 ( 0.96%)
Misses: 1027 / 1037 (99.04%)
A shame, but fingers crossed the build finishes this time.
The first attempt failed because my root disk filled up while the result was
being copied to the Nix store. After a nix store gc
and cleanup, it succeeds!
Thankfully the compilation cache sped things up quite a bit on the second
attempt.
Now that I have a built system configuration, time to figure out how to install it.
Installing
The nixos-hardware README suggests flashing Jumpdrive to mount the internal storage on another device, but I'd prefer to avoid flashing if possible.
Trying out nixos-anywhere and disko
So I go back and look into nixos-anywhere a bit more deeply. The quickstart guide looks pretty simple: build a NixOS configuration, then give it an SSH connection and it does all the rest.
Of course, it wasn't going to be that simple. I already had a building
configuration, so I tried the nixos-anywhere
command immediately. This first
attempt was met with
error: flake 'git+file:///home/william/.config?dir=nix' does not provide attribute 'packages.x86_64-linux.nixosConfigurations."maia".config.system.build.diskoScript', 'legacyPackages.x86_64-linux.nixosConfigurations."maia".config.system.build.diskoScript' or 'nixosConfigurations."maia".config.system.build.diskoScript'
So clearly I'd missed a step. And indeed an earlier step mentions including the disko module for automatic disk partitioning. Problem is, the phone's disk was already reasonably partitioned, so I'd really prefer if I could skip the partitioning step.
After a bit of digging around I failed to find this as an option, so I gave it
and gave disko a shot. I do quite like the idea of declarative partitioning, but
it's rather disconcerting off the bat - when exactly does the partitioning take
place? What if I add more partitions in the future - will it reformat everything
automatically? A bit of reading around suggests it's only done with the
nixos-anywhere
command or manually with some generated
scripts.
Okay, fine, let's convert the existing configuration.
purism@pureos:~$ sudo fdisk -l
Disk /dev/mmcblk0: 29.12 GiB, 31268536320 bytes, 61071360 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x2c38c7d5
Device Boot Start End Sectors Size Id Type
/dev/mmcblk0p1 * 10240 962559 952320 465M 83 Linux
/dev/mmcblk0p2 962560 61071326 60108767 28.7G 83 Linux
Disk /dev/mmcblk0boot0: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/mmcblk0boot1: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/mtdblock0: 192 KiB, 196608 bytes, 384 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/mtdblock1: 1.81 MiB, 1900544 bytes, 3712 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/mapper/crypt_root: 28.65 GiB, 30758911488 bytes, 60075999 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/zram0: 1.45 GiB, 1555038208 bytes, 379648 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
What I ended up with is in b5707ab6. As expected, figuring out the edge cases was a bit weird. Not knowing too much about disk partitioning made things harder to work out.
For the LUKS setup, there were plenty of examples to look at, but I couldn't
quite figure out how to set the cipher for the disk declaratively. Turns out the
extraFormatArgs
option lets you do that. I really missed having something like
the NixOS options search to explore the
option. Tab completion in the nix repl
really doesn't cut it, especially with
heavy usage of submodule option types in the disko module. The lack of option
documented is something the disko project is aware
of,
but the examples don't quite cover this case. Mental note: explore a Nix
language server as mentioned in this NixCon 2023
talk.
The other thing I found difficult to work out was the DOS mode (aka. MBR/BIOS
mode?), as the fdisk
output includes Disklabel type: dos
. I do recall this when installing Arch
Linux manually - GPT is the more modern thing relating to the UEFI boot mode.
Searching through the disko source, I sometimes saw msdos
besides gpt
. So I
thought I simply had to change the type: "gpt"
in the main disk attrset to
msdos
. That failed to build, so I eventually found this "compat"
example
for supporting both boot modes. This looks similar to the existing partition
scheme, with a traditional boot EFI partition.
The boot partition notably leaves some free space before it, 10240
bytes, for
the MBR and u-boot I expect. In the disko example, it leaves just one
MB. The Librem README suggests 2MB, but I double it to 4MB to be on the safe
side.
I also found this "standalone" example which uses BIOS mode only, but this is quite different from the existing scheme, so I ignored it. It did lead me to research the compat mode however.
I then built the diskoScript
attribute directly to test that out, but ran into
a ShellCheck build error. Good to see they're checking their scripts, but
apparently I've run into something which the project doesn't test for. I cloned
the project locally, fixed it, and locked my flake to use the local checkout
instead as a quick workaround. I later raised a quick
patch, which was very
promptly merged.
The nixos-anywhere guide mentions the --build-vm
option for testing out the
configuration. I did try running this with nixos-anywhere --flake .#maia --vm-test
. Unfortunately it to a very long time, and I eventually aborted it.
It seems to involve booting NixOS several times, I assume once with an initial
NixOS image to partition the disk, then again once everything's installed. The
configuration being an emulated aarch64
definitely contributed to it being so
slow.
I'm still not sure if I got the partitioning right, since I couldn't build the VM.
Failed attempt:
...
+ /root/kexec/kexec/kexec --load /root/kexec/kexec/bzImage --kexec-syscall-auto --initrd=/root/kexec/kexec/initrd --no-checks --command-line init=/nix/store/bn1085m7ssvj31a065krrhn827sxz2fq-nixos-system-nixos-23.05pre-git/init console=tty0 console=ttyAMA0,115200 console=ttyS0,115200 loglevel=4
Can't open (/proc/kcore).
Warning, can't get the VA_BITS from kcore
Can't open (/proc/kcore).
kexec_load failed: Function not implemented
entry = 0x575e46a0 flags = 0xb70000
nr_segments = 4
segment[0].buf = 0xffff7aa17100
segment[0].bufsz = 0x379d200
segment[0].mem = 0x41000000
segment[0].memsz = 0x3860000
segment[1].buf = 0xffff67c83170
segment[1].bufsz = 0x12d7571c
segment[1].mem = 0x44860000
segment[1].memsz = 0x12d76000
segment[2].buf = 0xffff67c67190
segment[2].bufsz = 0xda37
segment[2].mem = 0x575d6000
segment[2].memsz = 0xe000
segment[3].buf = 0xffff67c621b0
segment[3].bufsz = 0x3558
segment[3].mem = 0x575e4000
segment[3].memsz = 0x4000
+ echo kexec failed, dumping dmesg
...
After the fact I find an (as of writing) open PR to explicitly skip the disko partitioning, which I think is a step in the right direction.
With Jumpdrive
Initially tried building it myself following the repository's instructions. But
I quickly ran into issues when compiling the arm-trusted-provider dependency of
u-boot.
These (as far as I can tell) incorrect errors about dereferencing likely-zero
pointers, but these were hard-coded addresses which definitely weren't 0.
I overrode build flags in the Makefile, setting CFLAGS=-Wno-error
. But it
failed anyway, with an error I couldn't understand.
So I tried replacing it with the u-boot from the package in the nixos-hardware
repo, moving things around to more or less reproduce the layout the uuu
script
expected.
I had to try a different USB-C cable before uuu
detected the device. And also
sudo, I think. This seemed to work until it got to an unzip
command to extract
the kernel. After several attempts, I give up and consider my bodge a failure,
possibly due to the version of u-boot I flashed. Then I realised Jumpdrive
already provides pre-built archives for the Librem
5, so I just
used that. And it actually worked this time.
Thankfully Jumpdrive doesn't overwrite the internal partitions, it simply uploads and boots a tiny GNU/Linux operating system, exposing the internal storage over USB without touching it.
Cool, the internal storage of the device now shows up as /dev/sdc
or
/dev/sdd
as expected. I run the regular nixos-install
command to install the
built configuration:
$ nix build .#nixosConfigurations.maia.config.system.build.toplevel --out-link configurations/maia/result
$ sudo cryptsetup open /dev/sdc2 phone_root
...
$ sudo mount /dev/mapper/phone_root /mnt
$ sudo mount /dev/sdc1 /mnt/boot
$ sudo nixos-install -v --system ./configurations/maia/result --root /mnt
...
Warning: do not know how to make this configuration bootable; please enable a boot loader.
/nix/var/nix/profiles/system/sw/bin/bash: line 10: umount: command not found
Guess I messed up the bootloader config.
It was quite confusing to figure out what I should put into /boot
. It's not
done automatically by nixos-install
. I tried enabling GRUB with
boot.loader.grub.enable = true
, but that rendered the device unbootable.
There's a lot of mention of the special stage-1 for Mobile NixOS, but little mention of how to actually install it. E.g. going off the Pinephone README, the options mentioned are 1) flashing the installer image, and 2) flashing a full disk image. There's a mention of flashing only the boot partition, but the link is dead.
I find this issue which
mentions a boot-partition
attribute. That attribute no longer exists, but I
find similar ones by poking into the system configuration:
$ nix build .#nixosConfigurations.maia.config.mobile.outputs.u-boot.boot-partition
warning: Git tree '/home/william/.config' is dirty
error: cannot coerce null to a string
at /nix/store/fkf6sxbwp0phwbrlw7n8bj8s5lpl6y3k-source/modules/system-types/u-boot/default.nix:13:18:
12| kernel = stage-0.mobile.boot.stage-1.kernel.package;
13| kernel_file = "${kernel}/${if kernel ? file then kernel.file else pkgs.stdenv.hostPlatform.linux-kernel.target}";
| ^
14| inherit (config.mobile.generatedFilesystems) rootfs;
(use '--show-trace' to show detailed
Promising, but the kernel is missing. This must be set especially for the device's mobile config. I find it in the Pinephone config, so I must have to do this manually too:
mobile.boot.stage-1.kernel.package = pkgs.cached_linuxPackages_librem5;
And now the image builds. An ext4 image, fine. I write it with the suggested command:
Moment of truth - I reboot, and... it works! I see a boot log! And eventually, a decryption prompt!
Sadly the touchscreen keyboard doesn't work. Or a keyboard for that matter, not sure why that's the case. The prompt acts as if I pressed enter when I press the power button, funny.
Digging into the boot log through the low-tech method of recording it with another phone, I see lots of errors. Right at the start is a bit suspect:
Running Tasks::Environment...
/proc/mounts: _get_sysfs_dir fopen failed: No such file or directory
That's followed by a bunch of modprobe errors, including core modules like
devpts
, devtmpfs
, proc
, tmpfs
, sysfs
, dm_mod
. All of the errors say
not found in directory /lib/modules/6.4.14-librem5
. Quite strange, sounds like
no modules are available?
I tried comparing the kernel configuration in Mobile NixOS to the config for my kernel. I noticed some possibly related config options, so added them to my kernel's config overrides:
cached_linuxPackages_librem5 = (super.linuxPackages_librem5.kernel.override (old: {
stdenv = self.ccacheStdenv;
buildPackages = super.buildPackages // {
stdenv = self.ccacheStdenv;
};
argsOverride.structuredExtraConfig = with super.lib.kernel; {
DRM_AMDGPU = no;
DRM_RADEON = no;
DRM_NOUVEAU = no;
KERNFS = yes;
SYSFS = yes;
TMPFS = yes;
};
}));
And after a long kernel recompilation, I flashed the boot image again, and... no change.
Clearly something else was going wrong. So I assume kernel modules live in the initial ramdisk, so I tried building that separately and extracting it. After a bunch of reading of the mobile-nixos source, I eventually found the attribute for it:
$ nix build .#nixosConfigurations.maia.config.system.build.initialRamdisk
$ file result/initrd
result/initrd: gzip compressed data, max compression, from Unix, original size modulo 2^32 43448320
I extracted this file, which produced this a CPIO archive, and doing some quick searching found out how to extract it:
$ file /tmp/initrd
/tmp/initrd: ASCII cpio archive (SVR4 with no CRC)
$ cpio -idm -F /tmp/initrd
84850 blocks
$ tree -L 2
.
├── applets
│ ├── boot-error.mrb -> /nix/store/w6jryprm0r9q64c9jb91zbzj94dkpc2h-boot-error.mrb/libexec/boot-error.mrb
│ ├── boot-selection.mrb -> /nix/store/4rsrp0flb19jvm3wzm8m50xmvkl6khbf-boot-recovery-menu.mrb/libexec/boot-recovery-menu.mrb
│ └── boot-splash.mrb -> /nix/store/wni188k1gghr8rf4c1r6lp9gpayzd2mw-boot-splash.mrb/libexec/boot-splash.mrb
├── bin
│ └── sh -> /nix/store/6rpafppbl0y5i7j0j3xszy173iqx7isg-extra-utils-purism-librem5-extra-utils/bin/sh
├── dev
├── etc
│ ├── boot
│ ├── logo.svg -> /nix/store/y9nh2w9bqnl2h9fwdv5wci8hqpc2wj70-logo.white.svg
│ ├── udev
│ └── X11 -> /nix/store/cng1lrqy9d11x2w81fdvv4dpkwvf76cb-minimalX11Config
├── init -> /nix/store/sxf72hnc9kqrrw5ndz12cqz4cqakz3h0-init-wrapper
├── init.mrb -> /nix/store/azq5psyan11i8ly2n9r5racdh6hs5m5n-mobile-nixos-init-0.1.0/libexec/init.mrb
├── lib
│ ├── firmware -> /nix/store/fkvh8bmwyccylc540vvyk2h00kfjphbd-firmware/lib/firmware
│ └── modules -> /nix/store/bjkk524f7d3yphibd9kdd8z0w04acirc-null-modules/lib/modules
├── loader -> /nix/store/6rpafppbl0y5i7j0j3xszy173iqx7isg-extra-utils-purism-librem5-extra-utils/bin/loader
├── nix
│ └── store
├── proc
└── sys
So, lib/modules
sounds like the cause of the problem. It points to
...-null-modules/
, which both sounds wrong and is empty. Some more digging in
the mobile-nixos sources, I found the null-modules
thing is related to the
mobile.boot.stage-1.kernel.modular
option, which for some reason is false
by
default. If I set it to true
, the build log suggests some modules are actually
being built. I also try mobile.boot.stage-1.useNixOSKernel = true;
, since I
am, after all using a kernel from the regular NixOS configuration.
Unfortunately, the same errors occur, and the touchscreen still isn't usable.
However... plugging in a USB-C keyboard actually lights up the keyboard and notification light. I think the USB drivers are working now, but I still can't type. I try a phone dock, but it's the same story. But I give it one last shot, typing with the dock as the phone boots, and it works! I enter the encryption key and it proceeds to NixOS stage 2. Things look good until it gets to the systemd "Show Plymouth Boot Screen" service, which seems to get stuck . Then it leaves me with the following:
Cannot open access to console, the root account is locked.
See sulogin(8) man page for more details.
Still, this is promising. I'd like to at least get the phone booting, even if the touchscreen unlock isn't currently working.
During the boot process, I see an error when systemd tried to mount /boot
:
couldn't mount because of unspported optional features (40)
Turns out it's because the filesystem from the generated boot partition is ext4,
not ext2. Whoops. Amended, re-ran nixos-install
, and finally... it boots to
the UI!
It looks very similar to how it looked on PureOS. I swipe up to the unlock
prompt... and realise I never set the password. One final reflash to Jumpdrive,
nixos-enter
, passwd
, and a reboot, and finally I can unlock the device into
the Phosh desktop.
Fixing the touchscreen in stage 1
The touchscreen not working for the unlock prompt was a rather large usability issue - I didn't want to keep plugging in a USB keyboard and hoping that the appropriate keyboard driver would actually be loaded in time.
I assumed this was a case of the necessary kernel module not being loaded in the initramfs.
In some initial research, I came across the Multitouch
displays page on the
ArchWiki. This one mentions the hid_multitouch
module, but this one isn't
loaded on the booted device, according to lsmod
. The
Touchscreen page mentions
cat
ing the /dev/inputs/event*
files while touching the screen until you find
the one for the touchscreen. Unfortunately I forgot how these special files work
and used tail -f
on them, and confused myself by finding that none of them
produced output.
Thankfully I had more luck with the other method, reading
/proc/bus/input/devices
. None of the entries have "touch" in the name, but
this one stuck out with the obscure name and the fact is has a mouse output:
I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="EP0700M09"
P: Phys=
S: Sysfs=/devices/platform/soc@0/30800000.bus/30a40000.i2c/i2c-2/2-0038/input/input5
U: Uniq=
H: Handlers=event5 mouse1
B: PROP=2
B: EV=b
B: KEY=400 0 0 0 0 0
B: ABS=260800000000003
A quick search for EP0700M09 brought up a
schematic from the "Emerging Display
Technologies Corp", so this must be the display. And indeed cat /dev/input/event5
did show events while touching the screen.
And finally, after searching "linux find module for device", this
answer led to:
$ udevadm info -a -n /dev/input/event5|ag DRIVERS
DRIVERS==""
DRIVERS=="edt_ft5x06"
DRIVERS==""
DRIVERS=="imx-i2c"
DRIVERS==""
DRIVERS==""
DRIVERS==""
edt
= Emerging Display Technologies? That sounds about right.
I added edt_ft5x06
to boot.initrd.kernelModules
and rebooted, and found I
could finally type on the on-screen keyboard!
First impressions of Mobile NixOS
I didn't have to go through the fancy setup that PureOS had - unlocking immediately showed the Phosh UI. Said UI looks very similar to the PureOS version, I guess not much has changed in recent updates.
There's notably fewer apps preinstalled, really the bare minimum. A dialer, texts, browser (Epiphany), terminal, and camera app (Megapixels).
I also installed a few more applications from the GNOME core suite with my home manager configuration.
I'll intersperse this section with some pretty screenshots.
Mobile features
Obviously one of the primary functions of a phone is texting and calling, so I wanted to test those out. At this point I'd received a SIM from one of the "cheaper" pay-as-you-go providers in the UK. Annoyingly the phone once again rebooted when I inserted the SIM card - this may be a hardware "feature" in the end. However after rebooting, I was quite surprised to almost immediately receive a text from the provider with registration instructions - that meant the modem automatically registered itself as you'd expect. Interestingly the settings defaulted to 2G & 3G only, and I had to manually enable 2G, 3G, and 4G. Reception seemed reasonable for being indoors.
Mobile data took a bit more work, requiring me to manually set up the Access Point Names, by searching up my provider's details on another computer. After setting up the APNs, the 4G icon popped up and I could load websites at what seemed like reasonable speeds. I didn't spend too long testing it because of the overpriced mobile data usage costs.
I then tested sending a text to my primary phone, which worked perfectly fine. Phone calls were less functional, I couldn't hear my voice on the Librem nor my Android phone, no matter what audio devices I selected on the Librem. Interestingly if I selected the Analog Output - Modem device, opened the regular GNOME sound test menu, and clicked the front left speaker, I heard it on the other side of the call! That seems a bit backwards, but whatever. I didn't want to spend too long on the initial test.
Quick aside/rand about UK providers: that single text and a sub 3 minute call cost me a whole 78 pence! That's 10p for the text, and 25p per minute. I'd decided to put just £10 on a pay-as-you-go SIM because I didn't expect to be using it enough to justify a £10/mo contract, but I'm wondering just how long that'll last now. Phone carriers continue to be complete rip-offs, I see. I'll probably look for a better value PAYG SIM after I've exhausted this credit.
Phosh
Phosh's UI borrows heavily from Android et al., which is perfectly reasonable, and it looks pretty slick by itself. It feels smooth to swipe around, which is naturally an important feature for a touchscreen interface. I think it looks pretty slick too, and it even makes use of your GTK theme, allowing for some customisability.
One thing really irks me with Phosh's unlock screen, through no fault of its own. Android has conditioned me to expect the "enter" button in the bottom right of the keypad. However, in Phosh this is where the backspace button lives, and the unlock button instead lives at the bottom of the screen. I would love if this were configurable for poor Android users like myself.
Another issue I've noticed is that you can swipe down on the keypad to return to the lock screen, which I've accidentally triggered multiple times while typing in my passcode. A simple solution would be adding an element behind the keypad which consumes touch inputs, and have the keypad buttons themselves consume the swipe inputs.
An interesting choice was to combine the recent apps with the app drawer. I don't particularly disagree with this design, but I think it'll take some time for me to get used to it. There are some minor things that break my expectations, one of them being swiping down on an app does not bring it into the foreground, it instead hides the drawer to reveal the app that was previously open.
Another fun one is the bottom bar that indicates that you swipe up to open the drawer again. This is pretty handy, and they've included a little button in the corner to toggle the keyboard, which may be necessary for less touch-compatible applications. An oversight is that the keyboard button isn't available in the drawer itself, so if you search for applications, you can't close the keyboard again.
The notifications / quick options panel also looks pretty slick and heavily inspired by Android. Quick settings are a must-have for phones in my opinion, and it's a nice touch to include the GNOME audio device selector. Audio device selection can be finicky on Android - on my previous Lineage OS phone it was easily accessible by expanding the volume indicator, but Samsung in their infinite wisdom decided to implement this as a separate application, opened from the quick panel. Just that second it takes to cold launch irks me.
There are some fairly simple oversights as of writing. One such oversight is long pressing on the quick options to open the related menu in settings. However, doing so 1) doesn't hide the app drawer if it's open already, and 2) doesn't focus the settings application if it's not already focused.
While the audio device selector is super handy, it doesn't reset when you close the notification panel, so it will be open when you slide the notification panel down again. I imagine most users won't like having its state persisted like this.
Popups are something Phosh hasn't quite worked out. Of course, these are designed for a desktop, where having windows overlap is commonplace. But this model doesn't work for a window manager which expects only one window to be visible at a time. How this currently works is that the popup simply displays on top of the current window, and it's just luck whether that particular menu will fit on the screen. The popup displays as its own entry in the "open app list", so you can swipe it away to close it if you can't reach a close button. The first issue is the fundamental behaviour of popups. If we consider Android, a popup menu will take over the whole screen, dimming what's in the background. To close it, one can usually tap outside of the popup, or use the back button / gesture. That back button is missing in Phosh; the bottom bar only has a swipe up to access the app drawer and window list, and a keyboard button in the bottom right.
The issue of overflowing menus is a much larger one to tackle. A lot of menus
are designed with desktop aspect ratios in mind, and certainly not for 200%
resolution scaling on such small screens.
A lot of effort has been put into making some GNOME applications - particularly
the settings - reactive to thinner screens. In some places like the display
settings, this reactivity is buggy, in this case the menu seems to be fighting
its own layout and jumping around every repaint.
Other menus have been left completely neglected, such as the WiFi connection
settings. This one uses a tabbed layout to separate the separate sub-menus. A
generic solution might perhaps be automatically replacing this tabbed menu with
a vertical sub-menu system, where each tab is its own menu entry. Similarly,
instead of a sub-menu, the tab entries could expand vertically to reveal its
sub-menu.
But another problem with this connection settings menu is the multi-column
layout used to spread options horizontally. Making this layout dynamic won't be
trivial.
I've never worked with GTK to this level, let alone GNOME in particular, so I can't say just how easy these things would be to implement. But I'll give it a try anyway. A lot of people have put a lot of hard work into this project already, and I'd like to help if possible. Given how the window system is the very fundamental to the user experience of a touchscreen device, getting it right will do wonders for the usability of mobile GNU/Linux. Expectations have been set by the Android and iOS operating systems, which have both seen decades of development in touchscreen UX. While there are many valid complaints to make about Apple and Google's design choices in places, it's safe to say that they are both setting the bar, and it's been set quite high. Ultimately, we've all been using these phone operating for years now, so we're all quite accustomed to their patterns. It's probably best to start with the familiar before making opportunistic improvements where appropriate.
Epiphany
The app initially refused to launch, dying immediately. Running it through the
terminal, it complained about not being able to write to ~/.local/state
. For
some reason, (possibly from nixos-install?) this was owned by root. I chown -R
ed the directory to resolve that.
Epiphany looks notably better than the PureOS version. The UI looks very much inspired by Chrome on Android (though I haven't used that in forever). It even has Firefox sync support! I tried logging in, but I got a keyring setup prompt, followed by some error relating to the keyring. Funnily enough, the second password prompt to confirm the password had the "Store passwords unencrypted?" text.
The GNOME swipe from the left to go back gesture comes quite handy here, but it does trigger accidentally while browsing quite often. It should probably be restricted to a thin trigger zone on the side in these sorts of applications.
Annoyingly swipe gestures are used inconsistently, e.g. in the tab list menu, I'd expect to be able to swipe away tabs, but only the close button works.
I'd also prefer if the URL bar was on the bottom, as is the modern trend in browsers. That is much more accessible to my fingers, given how tall modern phones are. I'd like to go to a new site without awkwardly stretching my hand, or using a second one.
Camera
Mobile NixOS installs megapixels by default. Unfortunately this app also crashes on launch, but it's a less transient error. It complains about not being able to find a config file with the phone's model in the name. Clearly the app is specifically configured for each device, and you can find each config file here. I'll have to do some tinkering and create one for the Librem 5, then I can submit a patch for it.
Other apps
Apps like Calendar, Activity Monitor, and the standard GNOME text editor are curiously displayed as non-mobile applications, and look different than on PureOS. Compared to this showcase back in 2022, on NixOS these apps are notably missing a mobile-style bottom navigation bar. That suggests to me that Librem have forked these apps to adjust the UI for phone use. I'll have to look at packaging these for NixOS.
The Alarm application also has a good mobile interface, but unfortunately it doesn't work well as an alarm clock. It doesn't appear to set the RTC wakeup alarm, so the phone doesn't wake up to sound the alarm. There's an open ticket for this which is 3 years old as of writing.
NixOS problems
A weird issue I've been grappling with is some lingering garbage collection
roots pointing to /proc
for processes that no longer exist, I suspect from
previous boots. Nix doesn't seem to clean those up no matter what I do,
and I've yet to work out how to fix this. If unresolved, it's a big issue on the
phone given the limited storage space (32GB), of which the Nix store is taking
up 7GB. There's effectively two full systems in there, one of which is
completely unnecessary.
Next steps
I'd like to start by making it easier for others to get to this point. I didn't quite know how easy this was going to be from the start, and I certainly complicated things for myself with the nixos-anywhere tangent. However, I think there are some opportunistic improvements to documentation, particularly:
- How to override the Nix build directory with
TMPDIR
- Getting started with Mobile NixOS
- The current Device Porting Guide page is pretty bare-bones.
- How to first flash the phone. This issue suggests using the Mobile NixOS installer image by booting it from the SD card, but as far as I can tell that's not possible on the Librem 5.
After that I'd like to try adding support for the Librem 5 in the Mobile NixOS project. It seems like a more appropriate home for it, instead of the nixos-hardware repository. I don't know quite how it'll work to reproduce Sophie's work elsewhere.
I also need to work out some more things in Mobile NixOS, such as how kernel upgrades and recovery works. Surely there's a smarter system than re-writing the boot partition each time.
After that, I think I'll try resolving some of the problems I described in the first impressions. Starting with the easier ones, of course.
I hope I won't have to do much compiling, or figure out a faster method. Emulated compilation is painfully slow, and a compilation cache doesn't always help.
Anyway, we'll see how it goes. I hope to make some meaningful contributions to bring the dream of a feasible GNU/Linux phone a reality.