Handy/flake.nix
COGnITO (mode: carbon) cf284b1700
feat(nix): NixOS integration — ALSA_PLUGIN_DIR, nixosModule, home-manager module (#1025)
- Set ALSA_PLUGIN_DIR to PipeWire's alsa-lib path in package wrapper
  (NixOS store paths aren't in ALSA's default plugin search)
- Add nixosModules.default: udev rule for /dev/uinput, systemPackages
- Add homeManagerModules.default: systemd user service for autostart

Co-authored-by: zitongcharliedeng <zitongcharliedeng@users.noreply.github.com>
2026-03-16 20:47:32 +08:00

281 lines
9.7 KiB
Nix

{
description = "Handy - A free, open source, and extensible speech-to-text application that works completely offline";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# bun2nix: generates per-package Nix fetchurl expressions from bun.lock,
# replacing the old FOD approach where a single hash covered the entire
# node_modules directory (that hash would break on bun version changes).
# See: https://github.com/nix-community/bun2nix
bun2nix = {
url = "github:nix-community/bun2nix/2.0.8";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
bun2nix,
}:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
# Read version from Cargo.toml
cargoToml = fromTOML (builtins.readFile ./src-tauri/Cargo.toml);
version = cargoToml.package.version;
in
{
packages = forAllSystems (
system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ bun2nix.overlays.default ];
};
lib = pkgs.lib;
in
{
handy = pkgs.rustPlatform.buildRustPackage {
pname = "handy";
inherit version;
src = self;
cargoRoot = "src-tauri";
cargoLock = {
lockFile = ./src-tauri/Cargo.lock;
# Automatically fetch git dependencies using builtins.fetchGit.
# This eliminates the need for manual outputHashes that had to be
# updated every time a git dependency changed in Cargo.lock.
# Safe for standalone flakes (not allowed in nixpkgs, it is needed something like crate2nix).
allowBuiltinFetchGit = true;
};
postPatch = ''
${pkgs.jq}/bin/jq 'del(.build.beforeBuildCommand) | .bundle.createUpdaterArtifacts = false' \
src-tauri/tauri.conf.json > $TMPDIR/tauri.conf.json
cp $TMPDIR/tauri.conf.json src-tauri/tauri.conf.json
# Strip postinstall hook it runs check-nix-deps.ts which is only
# needed during local development, not inside the Nix sandbox.
${pkgs.jq}/bin/jq 'del(.scripts.postinstall)' \
package.json > $TMPDIR/package.json
cp $TMPDIR/package.json package.json
# Point libappindicator-sys to the Nix store path
substituteInPlace \
$cargoDepsCopy/libappindicator-sys-*/src/lib.rs \
--replace-fail \
"libayatana-appindicator3.so.1" \
"${pkgs.libayatana-appindicator}/lib/libayatana-appindicator3.so.1"
# Disable cbindgen in ferrous-opencc (calls cargo metadata which fails in sandbox)
# Upstream removed this call in v0.3.1+
substituteInPlace $cargoDepsCopy/ferrous-opencc-0.2.3/build.rs \
--replace-fail '.expect("Unable to generate bindings")' '.ok();'
substituteInPlace $cargoDepsCopy/ferrous-opencc-0.2.3/build.rs \
--replace-fail '.write_to_file("opencc.h");' '// skipped'
'';
# Bun dependencies: fetched per-package using hashes from .nix/bun.nix.
# This file is auto-generated by `bunx bun2nix -o .nix/bun.nix` and
# kept in sync via the postinstall hook in package.json.
# To regenerate manually: bun scripts/check-nix-deps.ts
bunDeps = pkgs.bun2nix.fetchBunDeps {
bunNix = ./.nix/bun.nix;
};
nativeBuildInputs = with pkgs; [
cargo-tauri.hook
pkg-config
wrapGAppsHook4
bun
# pkgs.bun2nix (from overlay), not the flake input — `with pkgs;`
# doesn't shadow function arguments in Nix.
pkgs.bun2nix.hook # Sets up node_modules from pre-fetched bun cache
jq
cmake
llvmPackages.libclang
shaderc
];
preBuild = ''
# bun2nix.hook has already set up node_modules from pre-fetched cache.
# Build the frontend with bun (tsc + vite).
export HOME=$TMPDIR
bun run build
'';
# Tests require runtime resources (audio devices, model files, GPU/Vulkan)
# not available in the Nix build sandbox
doCheck = false;
# The tauri hook's installPhase expects target/ in cwd, but our
# cargoRoot puts it under src-tauri/. Override to extract the DEB.
installPhase = ''
runHook preInstall
mkdir -p $out
cd src-tauri
mv target/${pkgs.stdenv.hostPlatform.rust.rustcTarget}/release/bundle/deb/*/data/usr/* $out/
runHook postInstall
'';
buildInputs = with pkgs; [
webkitgtk_4_1
gtk3
glib
glib-networking
libsoup_3
alsa-lib
onnxruntime
libayatana-appindicator
libevdev
libx11
libxtst
gtk-layer-shell
openssl
vulkan-loader
vulkan-headers
shaderc
# Required for WebKitGTK audio/video
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly
];
env = {
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
BINDGEN_EXTRA_CLANG_ARGS = "-isystem ${pkgs.llvmPackages.libclang.lib}/lib/clang/${lib.getVersion pkgs.llvmPackages.libclang}/include -isystem ${pkgs.glibc.dev}/include";
ORT_LIB_LOCATION = "${pkgs.onnxruntime}/lib";
OPENSSL_NO_VENDOR = "1";
# Tell Gstreamer where to find plugins
GST_PLUGIN_SYSTEM_PATH_1_0 = "${pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" (
with pkgs.gst_all_1;
[
gstreamer
gst-plugins-base
gst-plugins-good
gst-plugins-bad
gst-plugins-ugly
]
)}";
};
preFixup = ''
gappsWrapperArgs+=(
--set WEBKIT_DISABLE_DMABUF_RENDERER 1
--set ALSA_PLUGIN_DIR "${pkgs.pipewire}/lib/alsa-lib:${pkgs.alsa-plugins}/lib/alsa-lib"
--prefix LD_LIBRARY_PATH : "${
lib.makeLibraryPath [
pkgs.vulkan-loader
pkgs.onnxruntime
]
}"
)
'';
meta = {
description = "A free, open source, and extensible speech-to-text application that works completely offline";
homepage = "https://github.com/cjpais/Handy";
license = lib.licenses.mit;
mainProgram = "handy";
platforms = supportedSystems;
};
};
default = self.packages.${system}.handy;
}
);
# NixOS module for system-level integration (udev, input group)
nixosModules.default =
{ lib, pkgs, ... }:
{
imports = [ ./nix/module.nix ];
programs.handy.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.handy;
};
# Home-manager module for per-user service
homeManagerModules.default =
{ lib, pkgs, ... }:
{
imports = [ ./nix/hm-module.nix ];
services.handy.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.handy;
};
# Development shell for building from source
devShells = forAllSystems (
system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
# Rust
rustc
cargo
rust-analyzer
clippy
# Frontend
nodejs
bun
# Tauri CLI
cargo-tauri
# Native deps
pkg-config
openssl
alsa-lib
libsoup_3
webkitgtk_4_1
gtk3
gtk-layer-shell
glib
libxtst
libevdev
llvmPackages.libclang
cmake
vulkan-headers
vulkan-loader
shaderc
libappindicator
];
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath [ pkgs.libappindicator ]}";
GST_PLUGIN_SYSTEM_PATH_1_0 = "${pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" (
with pkgs.gst_all_1;
[
gstreamer
gst-plugins-base
gst-plugins-good
gst-plugins-bad
gst-plugins-ugly
]
)}";
# Same as wrapGAppsHook4
XDG_DATA_DIRS = "${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:${pkgs.hicolor-icon-theme}/share";
shellHook = ''
echo "Handy development environment"
bun install
echo "Run 'bun run tauri dev' to start"
'';
};
}
);
};
}