Compare commits

..

9 Commits

8 changed files with 456 additions and 14 deletions

228
README.md Normal file
View File

@@ -0,0 +1,228 @@
# PinePods Nix Package
A Nix/NixOS package for [PinePods](https://github.com/madeofpendletonwool/PinePods), a self-hosted podcast manager. This packages the PinePods desktop client (Tauri v2) for NixOS.
> **Note:** PinePods is a client/server application. You will need a running PinePods server instance to use this package. See the [PinePods documentation](https://github.com/madeofpendletonwool/PinePods) for server setup instructions.
---
## Requirements
- NixOS
- A running PinePods server (self-hosted)
---
## Installation
### Option 1: NixOS Module via Flake (recommended)
Add PinePods as a flake input in your system `flake.nix`:
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
pinepods.url = "git+https://git.briannelson.dev/brian/PinePods-nix";
pinepods.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, pinepods, ... }: {
nixosConfigurations.mymachine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
pinepods.nixosModules.pinepods
{
services.pinepods = {
enable = true;
# Your server address
server = "https://pinepods.example.com";
};
}
];
};
};
}
```
Then rebuild your system:
```bash
sudo nixos-rebuild switch
```
---
### Option 2: NixOS Module without Flakes
Clone this repository:
```bash
git clone https://git.briannelson.dev/brian/PinePods-nix.git
```
Add the module to your `/etc/nixos/configuration.nix`:
```nix
{ config, pkgs, ... }:
{
imports = [
/path/to/pinepods-nix/pinepods-module.nix
];
services.pinepods = {
enable = true;
# Your server address
server = "https://pinepods.example.com";
};
}
```
Then rebuild your system:
```bash
sudo nixos-rebuild switch
```
---
### Option 3: Install to user profile with nix-env
Clone this repository:
```bash
git clone https://git.briannelson.dev/brian/PinePods-nix.git
cd pinepods-nix
```
Install to your user profile:
```bash
nix-env -f default.nix -iA pinepods
```
This is persistent across reboots. To uninstall:
```bash
nix-env -e pinepods
```
> **Note:** With this option the `server` URL cannot be pre-configured. You will need to enter it manually in the app on first launch.
---
## Configuration
### Module Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `services.pinepods.enable` | bool | `false` | Enable the PinePods desktop client |
| `services.pinepods.server` | string | `""` | URL of your PinePods server (e.g. `https://pinepods.example.com`) |
When `server` is set, the app will navigate directly to your server on launch. If left empty, the app will open to a blank page and you will need to navigate to your server manually in the address bar.
---
## Project Structure
```
pinepods-nix/
├── flake.nix # Nix flake definition
├── default.nix # Package definition entry point (non-flake)
├── pinepods.nix # Main package derivation
├── pinepods-module.nix # NixOS module definition
├── patch-lib.py # Build-time patch for server URL support
├── example-usage-flake.nix # Example flake for users
├── Cargo.lock # Cargo lockfile for src-tauri (Tauri binary)
└── Cargo.frontend.lock # Cargo lockfile for web/ (Yew/WASM frontend)
```
---
## How It Works
PinePods is a multi-component application. This package builds two components from source:
**Frontend (pinepods-frontend):** The Yew/WASM web frontend is compiled using `trunk` targeting `wasm32-unknown-unknown`. The output is a static `dist/` directory containing HTML, WASM, and CSS assets.
**Desktop client (pinepods):** The Tauri v2 desktop binary is compiled with `cargo`, with the pre-built frontend assets embedded directly into the binary via the `custom-protocol` feature. At build time, `tauri.conf.json` is patched to remove the `devUrl` so Tauri serves the embedded assets instead of connecting to a development server. The Rust source is also patched to read the `PINEPODS_SERVER` environment variable and navigate the app window to that URL on launch.
**Runtime dependencies** are provided via `wrapProgram`:
- WebKit2GTK 4.1 (Tauri v2 requirement)
- GStreamer + plugins (audio playback)
- libayatana-appindicator (system tray icon)
- GLib/GTK3/Cairo stack
---
## Building from Source
To build manually without installing:
```bash
git clone https://git.briannelson.dev/brian/PinePods-nix.git
cd pinepods-nix
nix-build -A pinepods
./result/bin/pinepods
```
To use the dev shell with all build tools available:
```bash
nix develop
```
---
## Updating to a New Version of PinePods
*This project is still a WIP so I have not been able to test these step until a new version of PinePods gets released*
To update to a new PinePods release:
1. Update the `version` and `sha256` in `pinepods.nix`:
```nix
version = "0.8.3"; # new version
sha256 = "..."; # run nix-build and paste the correct hash from the error
```
2. Replace the Cargo lockfiles with the new versions:
```bash
curl -L https://github.com/madeofpendletonwool/PinePods/archive/refs/tags/0.8.3.tar.gz | tar xz
cp PinePods-0.8.3/web/Cargo.lock ./Cargo.frontend.lock
cp PinePods-0.8.3/web/src-tauri/Cargo.lock ./Cargo.lock
```
3. Check if the `wasm-bindgen` version changed:
```bash
grep "wasm-bindgen" PinePods-0.8.3/web/Cargo.toml
```
If it changed, update `wasm-bindgen-cli` in `default.nix` and `TRUNK_TOOLS_WASM_BINDGEN` in `pinepods.nix` accordingly.
4. Rebuild and test:
```bash
nix-build -A pinepods
./result/bin/pinepods
```
---
## Troubleshooting
**White screen on launch**
Make sure `WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1` is set. This is handled automatically by the package wrapper.
**No audio playback**
GStreamer plugins are included but if audio still doesn't work, check that your system has the required codec support:
```bash
gst-inspect-1.0 autoaudiosink
gst-inspect-1.0 appsink
```
**Login doesn't persist between sessions**
This is a known limitation of the WebKit localStorage implementation. As a workaround, set `services.pinepods.server` in your NixOS configuration so the app always navigates to your server on launch.
**Tray icon shows generic icon**
The icon is installed under the app's reverse-DNS identifier `com.gooseberrydevelopment.pinepods`. Make sure your icon theme cache is up to date:
```bash
gtk-update-icon-cache
```
---
## License
This Nix packaging is provided as-is. PinePods itself is licensed under GPL-3.0. See the [PinePods repository](https://github.com/madeofpendletonwool/PinePods) for details.

View File

@@ -1,11 +1,13 @@
let let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.11"; nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.11";
pkgs = import nixpkgs { config = {}; overlays = []; }; pkgs = import nixpkgs { config = {}; overlays = []; };
in in
{ serverUrl ? "" }:
{ {
pinepods = pkgs.callPackage ./pinepods.nix { pinepods = pkgs.callPackage ./pinepods.nix {
# Pin wasm-bindgen-cli to exactly the version PinePods requires
wasm-bindgen-cli = pkgs.wasm-bindgen-cli_0_2_105; wasm-bindgen-cli = pkgs.wasm-bindgen-cli_0_2_105;
inherit (pkgs) binaryen tailwindcss_3 libayatana-appindicator gst_all_1; inherit (pkgs) binaryen tailwindcss_3 libayatana-appindicator gst_all_1 python3;
inherit serverUrl;
}; };
pinepods-frontend = (pkgs.callPackage ./pinepods.nix { }).frontend; }
}

34
example-useage-flake.nix Normal file
View File

@@ -0,0 +1,34 @@
{
description = "Example NixOS configuration using the PinePods flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
pinepods.url = "git+https://git.briannelson.dev/brian/pinepods-nix";
# If you want to ensure pinepods uses the same nixpkgs as your system
pinepods.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, pinepods, ... }: {
nixosConfigurations.mymachine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# Import the PinePods NixOS module
pinepods.nixosModules.pinepods
({ config, pkgs, ... }: {
# Enable PinePods and point it at your server
services.pinepods = {
enable = true;
server = "https://pinepods.example.com";
};
# The rest of your normal NixOS configuration goes here...
# networking.hostName = "mymachine";
# users.users.youruser = { ... };
# etc.
})
];
};
};
}

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1772822230,
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"owner": "NixOs",
"repo": "nixpkgs",
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"type": "github"
},
"original": {
"owner": "NixOs",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

66
flake.nix Normal file
View File

@@ -0,0 +1,66 @@
{
description = "Pinepods desktop client - self-hosted podcast manager";
inputs = {
nixpkgs.url = "github:NixOs/nixpkgs/nixos-25.11";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; config = {}; overlays = []; };
makePinepods = { serverUrl ? "" }:
pkgs.callPackage ./pinepods.nix {
wasm-bindgen-cli = pkgs.wasm-bindgen-cli_0_2_105;
inherit (pkgs) binaryen tailwindcss_3 libayatana-appindicator gst_all_1 python3;
inherit serverUrl;
};
in
{
packages.${system} = {
pinepods = makePinepods { };
default = makePinepods { };
};
nixosModules.pinepods = { config, lib, pkgs, ... }:
let
cfg = config.services.pinepods;
in
{
options.services.pinepods = {
enable = lib.mkEnableOption "PinePods podcast manager";
server = lib.mkOption {
type = lib.types.str;
default = "";
example = "https://pinepods.example.com";
description = ''
The URL oof your self-hosted PinePods server.
The app will navigate directly to this address on launch.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
(makePinepods { serverUrl = cfg.server; })
];
};
};
nixosModules.default = self.nixosModules.pinepods;
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [
trunk
wasm-bindgen-cli_0_2_105
cargo
rustc
llvmPackages.lld
binaryen
tailwindcss_3
];
};
};
}

28
patch-lib.py Normal file
View File

@@ -0,0 +1,28 @@
import sys
target = '.run(tauri::generate_context!())'
replacement = '''.setup(|app| {
if let Ok(server_url) = std::env::var("PINEPODS_SERVER") {
if !server_url.is_empty() {
use tauri::Manager;
let window = app.get_webview_window("main").unwrap();
window.navigate(server_url.parse().unwrap()).unwrap();
}
}
Ok(())
})
.run(tauri::generate_context!())'''
with open(sys.argv[1], 'r') as f:
content = f.read()
if target not in content:
print(f"ERROR: Could not find target string in {sys.argv[1]}")
sys.exit(1)
content = content.replace(target, replacement)
with open(sys.argv[1], 'w') as f:
f.write(content)
print("Successfully patched lib.rs")

24
pinepods-module.nix Normal file
View File

@@ -0,0 +1,24 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.pinepods;
pinepods-pkg = (import "${toString ./.}/default.nix" {
serverUrl = cfg.server;
}).pinepods;
in
{
options.services.pinepods = {
enable = lib.mkEnableOption "PinePods podcast manager";
server = lib.mkOption {
type = lib.types.str;
default = "";
example = "https://pinepods.example.com";
description = "The URL of your PinePods server.";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pinepods-pkg ];
};
}

View File

@@ -26,6 +26,8 @@
, makeWrapper , makeWrapper
, libayatana-appindicator , libayatana-appindicator
, gst_all_1 , gst_all_1
, python3
, serverUrl ? ""
}: }:
let let
@@ -96,6 +98,7 @@ rustPlatform.buildRustPackage {
pkg-config pkg-config
wrapGAppsHook3 wrapGAppsHook3
makeWrapper makeWrapper
python3
]; ];
buildInputs = [ buildInputs = [
@@ -120,7 +123,7 @@ rustPlatform.buildRustPackage {
gst_all_1.gst-plugins-good gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly gst_all_1.gst-plugins-ugly
gst_all_1.gst-libav # for audio codec support gst_all_1.gst-libav
]; ];
OPENSSL_NO_VENDOR = "1"; OPENSSL_NO_VENDOR = "1";
@@ -129,11 +132,10 @@ rustPlatform.buildRustPackage {
preBuild = '' preBuild = ''
chmod -R u+w $NIX_BUILD_TOP/source chmod -R u+w $NIX_BUILD_TOP/source
ln -s ${frontend} $NIX_BUILD_TOP/source/web/dist ln -s ${frontend} $NIX_BUILD_TOP/source/web/dist
# Remove devUrl so Tauri uses frontendDist instead of dev server
sed -i '/"devUrl"/d' $NIX_BUILD_TOP/source/web/src-tauri/tauri.conf.json sed -i '/"devUrl"/d' $NIX_BUILD_TOP/source/web/src-tauri/tauri.conf.json
echo "=== tauri.conf.json ==="
cat $NIX_BUILD_TOP/source/web/src-tauri/tauri.conf.json | head -10 # Patch lib.rs to read PINEPODS_SERVER env var and navigate to it on startup
python3 ${./patch-lib.py} $NIX_BUILD_TOP/source/web/src-tauri/src/lib.rs
''; '';
installPhase = '' installPhase = ''
@@ -141,26 +143,57 @@ rustPlatform.buildRustPackage {
install -Dm755 target/x86_64-unknown-linux-gnu/release/app $out/bin/pinepods install -Dm755 target/x86_64-unknown-linux-gnu/release/app $out/bin/pinepods
# Desktop entry
if [ -f ../../pinepods.desktop ]; then if [ -f ../../pinepods.desktop ]; then
install -Dm644 ../../pinepods.desktop $out/share/applications/pinepods.desktop install -Dm644 ../../pinepods.desktop \
$out/share/applications/pinepods.desktop
fi fi
for size in 32x32 64x64 128x128 256x256; do # Icons
for size in 32x32 128x128 256x256; do
if [ -f icons/''${size}.png ]; then if [ -f icons/''${size}.png ]; then
install -Dm644 icons/''${size}.png \ install -Dm644 icons/''${size}.png \
$out/share/icons/hicolor/''${size}/apps/pinepods.png $out/share/icons/hicolor/''${size}/apps/pinepods.png
fi fi
done done
# Tray icon install as the app icon so the taskbar uses it
if [ -f icons/icon.png ]; then
install -Dm644 icons/icon.png \
$out/share/icons/hicolor/256x256/apps/com.gooseberrydevelopment.pinepods.png
fi
# AppStream metadata
if [ -f com.gooseberrydevelopment.pinepods.metainfo.xml ]; then
install -Dm644 com.gooseberrydevelopment.pinepods.metainfo.xml \
$out/share/metainfo/com.gooseberrydevelopment.pinepods.metainfo.xml
fi
# Create desktop entry
mkdir -p $out/share/applications
cat > $out/share/applications/pinepods.desktop << EOF
[Desktop Entry]
Name=Pinepods
Comment=A self-hosted podcast manager
Exec=pinepods
Icon=com.gooseberrydevelopment.pinepods
Type=Application
Categories=Audio;Music;
StartupNotify=true
StartupWMClass=Pinepods
EOF
runHook postInstall runHook postInstall
''; '';
postFixup = '' postFixup = ''
wrapProgram $out/bin/pinepods \ wrapProgram $out/bin/pinepods \
--set WEBKIT_DISABLE_COMPOSITING_MODE 1 \ --set WEBKIT_DISABLE_COMPOSITING_MODE 1 \
--set WEBKIT_FORCE_SANDBOX 0 \ --set WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS 1 \
${lib.optionalString (serverUrl != "") "--set PINEPODS_SERVER \"${serverUrl}\""} \
--prefix LD_LIBRARY_PATH : "${libayatana-appindicator}/lib" \ --prefix LD_LIBRARY_PATH : "${libayatana-appindicator}/lib" \
--prefix GST_PLUGIN_SYSTEM_PATH_1_0 : "${gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-ugly}/lib/gstreamer-1.0:${gst_all_1.gst-libav}/lib/gstreamer-1.0" \ --prefix GST_PLUGIN_SYSTEM_PATH_1_0 : "${gst_all_1.gstreamer}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0:${gst_all_1.gst-plugins-ugly}/lib/gstreamer-1.0:${gst_all_1.gst-libav}/lib/gstreamer-1.0" \
--prefix XDG_DATA_DIRS : "$out/share" \
--prefix XDG_DATA_DIRS : "${gsettings-desktop-schemas}/share/gsettings-schemas/${gsettings-desktop-schemas.name}:${gtk3}/share/gsettings-schemas/${gtk3.name}" \ --prefix XDG_DATA_DIRS : "${gsettings-desktop-schemas}/share/gsettings-schemas/${gsettings-desktop-schemas.name}:${gtk3}/share/gsettings-schemas/${gtk3.name}" \
--prefix GIO_EXTRA_MODULES : "${glib-networking}/lib/gio/modules" --prefix GIO_EXTRA_MODULES : "${glib-networking}/lib/gio/modules"
''; '';