From ceba0f58499b12c5bde3d48d8f54c0c6ef4b22f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:07:20 +0000 Subject: [PATCH 1/3] 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 Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz --- .github/workflows/nix.yml | 52 +++++++++++++++++ .gitignore | 2 + README.md | 1 + docs/CHANGELOG.md | 6 ++ docs/INSTALL.md | 33 +++++++++++ flake.lock | 27 +++++++++ flake.nix | 41 +++++++++++++ packaging/nix/module.nix | 42 ++++++++++++++ packaging/nix/package.nix | 117 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 321 insertions(+) create mode 100644 .github/workflows/nix.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 packaging/nix/module.nix create mode 100644 packaging/nix/package.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..e7bfe38 --- /dev/null +++ b/.github/workflows/nix.yml @@ -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 diff --git a/.gitignore b/.gitignore index 600dec9..66510cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.pdb .env /rpmbuild +/result +/result-* diff --git a/README.md b/README.md index f06c8d0..342e702 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 366eddc..df7bfdd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 425cfda..5936dd6 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -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. = 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 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..416bfe1 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..10afe56 --- /dev/null +++ b/flake.nix @@ -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); + }; +} diff --git a/packaging/nix/module.nix b/packaging/nix/module.nix new file mode 100644 index 0000000..933158b --- /dev/null +++ b/packaging/nix/module.nix @@ -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; + }; +} diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix new file mode 100644 index 0000000..19ff876 --- /dev/null +++ b/packaging/nix/package.nix @@ -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"; + }; +} From 453c439b74ae8b7e2ddc3181fe75124cc768f3d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 08:23:21 +0000 Subject: [PATCH 2/3] fix(packaging): give the Nix package TLS for WebKitGTK On NixOS the CAPTCHA window failed with "TLS support is not available": WebKitGTK's GIO had no TLS backend. With glib-networking in buildInputs, wrapGAppsHook4 puts its GIO modules on GIO_EXTRA_MODULES in every wrapper. Refs #37 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz --- packaging/nix/package.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix index 19ff876..1c121d2 100644 --- a/packaging/nix/package.nix +++ b/packaging/nix/package.nix @@ -8,6 +8,7 @@ glib, wrapGAppsHook4, fuse3, + glib-networking, gtk4, libadwaita, webkitgtk_6_0, @@ -63,6 +64,9 @@ rustPlatform.buildRustPackage { buildInputs = [ fuse3 + # TLS for WebKitGTK's GIO: without it the CAPTCHA page fails with "TLS + # support is not available". wrapGAppsHook4 adds it to GIO_EXTRA_MODULES. + glib-networking gtk4 libadwaita webkitgtk_6_0 From e4029817083aad5960854bf547de9cef67b1929b Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 11 Oct 2026 18:55:00 +0200 Subject: [PATCH 3/3] fix(packaging): ship the Adwaita icons with the Nix package GTK ships only a few stock icons itself. On a NixOS session without Adwaita installed, such as Hyprland, the Photos and Computers sidebar entries and about twenty other icons showed as missing (#37). adwaita-icon-theme in buildInputs puts it on the wrapper's XDG_DATA_DIRS. --- packaging/nix/package.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix index 1c121d2..2f4e6ed 100644 --- a/packaging/nix/package.nix +++ b/packaging/nix/package.nix @@ -3,6 +3,7 @@ { lib, rustPlatform, + adwaita-icon-theme, pkg-config, gettext, glib, @@ -67,6 +68,11 @@ rustPlatform.buildRustPackage { # TLS for WebKitGTK's GIO: without it the CAPTCHA page fails with "TLS # support is not available". wrapGAppsHook4 adds it to GIO_EXTRA_MODULES. glib-networking + # The stock icons the app asks for, such as the Photos and Computers + # sidebar entries. GTK ships only a few itself, and a session without + # Adwaita installed showed the rest as missing. wrapGAppsHook4 adds it to + # XDG_DATA_DIRS. + adwaita-icon-theme gtk4 libadwaita webkitgtk_6_0