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"; + }; +}