Over-the-Air (OTA) Updater

The following robotnix configuration enables the OTA updater app.

{
    apps.updater.enable = true;
    apps.updater.url = "...";
}

The apps.updater.url setting needs to point to a URL hosting the OTA files described below.

Additionally, the buildDateTime option is set by default by the flavor, and is updated when those flavors have new releases. If you make new changes to your build that you want to be pushed by the OTA updater, you should set buildDateTime yourself, using date "+%s" to get the current time.

Building OTA updates

The OTA file and metadata can be generated as part of the releaseScript output. If you are signing builds inside Nix using the sandbox exception, robotnix additionally includes a convenient target which will build a directory containing OTA files ready to be sideloaded or served over the web. To generate the OTA directory, build the otaDir attribute (here for sunfish):

$ nix-build --arg configuration ./sunfish.nix -A otaDir -o ota-dir

The directory structure will look similar to this (arrows indicate symbolic links) with possibly different timestamps and hashes of course:

$ tree -l ota-dir
├── sunfish-ota_update-2021.02.06.16.zip -> /nix/store/wwr49all6x868f0mdl11369ybfwyir0f-sunfish-ota_update-2021.02.06.16.zip
├── sunfish-stable -> /nix/store/c1rp46m9spncanacglqs5mxk6znfs44s-sunfish-stable
└── sunfish-target_files-2021.02.06.16.zip -> /nix/store/8ys21rzjqhi2055d7bd4iwa15fv1m446-sunfish-signed_target_files-2021.02.06.16.zip

The file sunfish-ota_update-2021.02.06.16.zip can be sideloaded with adb as described in the next section.

Actually serving OTA updates over the air

Note: The Vanilla and GrapheneOS flavors use a different updater than the LineageOS flavor, therefore they might behave slightly different from one another. The instructions below should however work for both.

Essentially this boils down to just serving the otaDir build output on the web, e.g. with nginx. To receive OTA updates on the device, enable the updater and point it to the domain and possibly subdirectory that you will be serving OTA updates from:

# Device configuration
{
  apps = {
    updater.enable = true;
    updater.url = "https://example.com/android/";
  };
}

On a NixOS server, it is as easy as serving a directory at the required endpoint:

# NixOS server configuration
{
  services.nginx.enable = true;
  systemd.services.nginx.serviceConfig.ReadOnlyPaths = [ "/var/www" ];
  services.nginx.virtualHosts."example.com" = {
    forceSSL = true;
    enableACME = true;
    locations."/android/" = {
      root = "/var/www";
      tryFiles = "$uri $uri/ =404";
    };
  };
}

The directory simply contains symlinks to the store paths that were contained in the otaDir output that was built earlier. I choose to just copy the result symlink to /var/www/android. It is recommended to use a symlink or mv operation to expose the otaDir to the web server, since (if you are copying/uploading slowly) the OTA updater app on your phone might start updating before the copy/upload is complete.

$ cp --no-dereference ota-dir /var/www/android
$ tree -l /var/www
/var/www
└── android -> /nix/store/dbjcl9lwn6xif9c0fy8d2wwpn9zi4hw4-sunfish-otaDir
    ├── sunfish-ota_update-2021.02.06.16.zip -> /nix/store/wwr49all6x868f0mdl11369ybfwyir0f-sunfish-ota_update-2021.02.06.16.zip
    ├── sunfish-stable -> /nix/store/c1rp46m9spncanacglqs5mxk6znfs44s-sunfish-stable
    └── sunfish-target_files-2021.02.06.16.zip -> /nix/store/8ys21rzjqhi2055d7bd4iwa15fv1m446-sunfish-signed_target_files-2021.02.06.16.zip

Of course, this doesn't have to be located at /var/www and it's totally possible to integrate updating of the OTA directory into your other robotnix build automation. In this case it is as easy as updating the /var/www/android symlink with the new build output.

Here it was assumed that robotnix was built on the same machine that you will serve the OTA from. If that is not the case you can conveniently copy the closure to a remote host using nix copy as in

$ nix copy --to ssh://user@example.com ./ota-dir

Don't forget to add the store path of the ota-dir as a garbage collector root on the remote machine or it might be collected in the next sweep.