feat(packaging): add a Nix flake and NixOS module

Builds the four binaries with rustPlatform.buildRustPackage from
Cargo.lock, installs the desktop entries, icon, user service and
translations as the other packages do, and wraps the binaries with
exiftool and ffmpeg on PATH. The NixOS module installs the package,
pulls proton-drive.service into the graphical session with the
fusermount3 wrapper on its PATH, and enables FUSE and GNOME Keyring.

A Nix workflow builds the flake when its recipe, the lock files or the
code change.

Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
This commit is contained in:
Claude committed 2026-10-09 22:07:20 +00:00
1 parent 3e0db8cf46
commit ceba0f5849
9 files changed
+321

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
name: Nix
# Builds the flake (docs/INSTALL.md#nixos-flake) whenever its recipe, the lock
# files or the code change, so a NixOS user's `nix build` keeps working. Run it
# from the Actions tab for any branch to get a build to try.
on:
push:
paths:
- 'flake.nix'
- 'flake.lock'
- 'packaging/nix/**'
- 'Cargo.lock'
- 'Cargo.toml'
- 'crates/**'
- 'po/**'
- '.github/workflows/nix.yml'
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'packaging/nix/**'
- 'Cargo.lock'
- '.github/workflows/nix.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: nix-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check --print-build-logs --no-build
- name: Build the package
run: nix build --print-build-logs .#proton-drive-linux
- name: Smoke test
run: |
./result/bin/pdfs --version
for bin in pdfs-app pdfs-tray pdfs-prompt; do test -x "result/bin/$bin"; done
grep -q "ExecStart=$(readlink -f result)/bin/pdfs" result/lib/systemd/user/proton-drive.service
ls result/share/locale/*/LC_MESSAGES/pdfs.mo
+2
View File
@@ -3,3 +3,5 @@
*.pdb
.env
/rpmbuild
/result
/result-*
+1
View File
@@ -76,6 +76,7 @@ scriptable CLI.
| Debian, Ubuntu 24.04 or newer | `.deb` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) |
| Fedora 44 or newer | `.rpm` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) |
| Arch Linux | [`proton-drive-for-linux`](https://aur.archlinux.org/packages/proton-drive-for-linux) (also `-bin` and `-git`) in the AUR |
| NixOS | The flake in this repository: see [Install → NixOS](docs/INSTALL.md#nixos-flake) |
| Anything else | [Build from source](docs/INSTALL.md#build-from-source) |
```bash
+6
View File
@@ -11,6 +11,12 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased]
### Added
- **A Nix flake for NixOS.** `nix build github:narrrl/proton-drive-linux` builds the client, and
the flake's NixOS module installs it and runs the mount in every graphical session. See
[Install → NixOS](INSTALL.md#nixos-flake).
([#37](https://github.com/narrrl/proton-drive-linux/issues/37))
### Fixed
- **Writing to a file again soon after a partial write could fail with an I/O error.** When the
first write changed only part of a file this client had not read, a second write, or saving
+33
View File
@@ -82,6 +82,39 @@ paru -S proton-drive-for-linux # or yay, or any other AUR helper
The recipes live in [`packaging/aur/`](../packaging/aur/README.md).
### NixOS (flake)
The repository is a flake. Add it as an input and enable the module, which installs the package,
links the user service into every graphical session and turns on the `fusermount3` wrapper and
GNOME Keyring for you:
```nix
{
inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux";
outputs = { nixpkgs, proton-drive-linux, ... }: {
nixosConfigurations.<host> = nixpkgs.lib.nixosSystem {
modules = [
proton-drive-linux.nixosModules.default
{ services.proton-drive-linux.enable = true; }
];
};
};
}
```
To try it without changing your system, run the app straight from the flake:
```bash
nix run github:narrrl/proton-drive-linux#proton-drive-linux -- --help # the CLI
nix shell github:narrrl/proton-drive-linux -c pdfs-app # the desktop app
```
The mount itself needs the setuid `fusermount3` from NixOS (`programs.fuse.enable`, on by default
with the module) and a Secret Service such as GNOME Keyring or KWallet. To leave out ffmpeg and
with it video thumbnails, override the package with `withFfmpeg = false`. The recipe lives in
[`packaging/nix/`](../packaging/nix/).
### Other distributions
Unpack the `.tar.gz` into a directory on your `PATH`, then install the service unit as shown in
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1791456040,
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+41
View File
@@ -0,0 +1,41 @@
{
description = "Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app and tray";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in
{
packages = forAllSystems (pkgs: rec {
proton-drive-linux = pkgs.callPackage ./packaging/nix/package.nix { };
default = proton-drive-linux;
});
overlays.default = final: _: {
proton-drive-linux = final.callPackage ./packaging/nix/package.nix { };
};
nixosModules.default = import ./packaging/nix/module.nix self;
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.proton-drive-linux ];
packages = with pkgs; [
cargo
clippy
rustfmt
rust-analyzer
];
};
});
formatter = forAllSystems (pkgs: pkgs.nixfmt);
};
}
+42
View File
@@ -0,0 +1,42 @@
# NixOS module: installs the package and runs the mount daemon in every
# graphical session, as the other packages' user service does.
#
# services.proton-drive-linux.enable = true;
self:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.proton-drive-linux;
in
{
options.services.proton-drive-linux = {
enable = lib.mkEnableOption "the Proton Drive for Linux mount daemon, app and tray";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.proton-drive-linux;
defaultText = lib.literalExpression "proton-drive-linux.packages.\${system}.proton-drive-linux";
description = "The package to use.";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
# The unit ships in the package; this links it and pulls it into the
# graphical session, which is what `systemctl --user enable` would do.
systemd.packages = [ cfg.package ];
systemd.user.services.proton-drive = {
wantedBy = [ "graphical-session.target" ];
# The mount runs fusermount3 from PATH, and it has to be the setuid
# wrapper; the PATH NixOS gives a unit does not include it.
path = [ "/run/wrappers" ];
};
programs.fuse.enable = lib.mkDefault true;
# Session tokens are kept in the Secret Service.
services.gnome.gnome-keyring.enable = lib.mkDefault true;
};
}
+117
View File
@@ -0,0 +1,117 @@
# Nix package for the workspace, built from the checkout this file lives in.
# The flake at the repository root exposes it; `nix build` builds it.
{
lib,
rustPlatform,
pkg-config,
gettext,
glib,
wrapGAppsHook4,
fuse3,
gtk4,
libadwaita,
webkitgtk_6_0,
libsecret,
dbus,
exiftool,
ffmpeg-headless,
withFfmpeg ? true,
}:
let
cargoToml = lib.importTOML ../../Cargo.toml;
in
rustPlatform.buildRustPackage {
pname = "proton-drive-linux";
inherit (cargoToml.workspace.package) version;
# Only what the build reads, so editing docs or CI does not rebuild.
src = lib.fileset.toSource {
root = ../..;
fileset = lib.fileset.unions [
../../Cargo.toml
../../Cargo.lock
../../crates
../../po
../../LICENSE
../proton-drive.service
../io.narl.proton-drive-linux.desktop
../io.narl.proton-drive-linux-tray.desktop
../io.narl.proton-drive-linux.svg
];
};
cargoLock.lockFile = ../../Cargo.lock;
cargoBuildFlags = [
"--bin"
"pdfs"
"--bin"
"pdfs-tray"
"--bin"
"pdfs-app"
"--bin"
"pdfs-prompt"
];
nativeBuildInputs = [
pkg-config
gettext # msgfmt, for po/build.sh
glib # glib-compile-resources, for the GUI's build.rs
wrapGAppsHook4
];
buildInputs = [
fuse3
gtk4
libadwaita
webkitgtk_6_0
libsecret
dbus
];
# The app looks for its catalogs here instead of /usr/share/locale.
env.PDFS_LOCALEDIR = "${placeholder "out"}/share/locale";
# The suite mounts FUSE, talks to D-Bus and the kernel keyring, and checks
# permissions a build sandbox does not have; CI runs it on every push.
doCheck = false;
postInstall = ''
install -Dm644 packaging/io.narl.proton-drive-linux.desktop \
$out/share/applications/io.narl.proton-drive-linux.desktop
install -Dm644 packaging/io.narl.proton-drive-linux-tray.desktop \
$out/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop
install -Dm644 packaging/io.narl.proton-drive-linux.svg \
$out/share/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg
substituteInPlace $out/share/applications/io.narl.proton-drive-linux.desktop \
--replace-fail "Exec=pdfs-app" "Exec=$out/bin/pdfs-app"
substituteInPlace $out/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop \
--replace-fail "Exec=pdfs-tray" "Exec=$out/bin/pdfs-tray"
install -Dm644 packaging/proton-drive.service \
$out/lib/systemd/user/proton-drive.service
substituteInPlace $out/lib/systemd/user/proton-drive.service \
--replace-fail "ExecStart=/usr/bin/pdfs" "ExecStart=$out/bin/pdfs"
patchShebangs po/build.sh
po/build.sh $out/share/locale
'';
# Thumbnails shell out to exiftool, and to ffmpeg for video. Suffixed, so a
# user's own builds on PATH win. fusermount3 comes from the system: on NixOS
# it is the setuid wrapper in /run/wrappers/bin.
preFixup = ''
gappsWrapperArgs+=(--suffix PATH : ${
lib.makeBinPath ([ exiftool ] ++ lib.optional withFfmpeg ffmpeg-headless)
})
'';
meta = {
description = "Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app and tray";
homepage = "https://proton-drive.narl.io";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "pdfs";
};
}