packaging-binary-for-nix
npx skills add https://github.com/lihaoze123/my-skills --skill packaging-binary-for-nix
Agent 安装分布
Skill 文档
Packaging Binary Distributions for Nix
Overview
Extract and patch binary packages within Nix builds for reproducibility. Core principle: Source from original archive directly, never from pre-extracted directories.
When to Use
Use when:
- Converting binary packages (.deb, .rpm, .tar.gz, .zip) to Nix derivations
- Packaging proprietary/closed-source software distributed as binaries
- Electron/GUI apps show “library not found” errors
- User provides pre-extracted binary contents
- Binary distributions need library path fixes
Don’t use for:
- Software available in nixpkgs
- Source-based packages (use standard derivation)
- AppImages (use appimage-run or extract and patch)
Essential Pattern
For .deb packages:
{ pkgs }:
pkgs.stdenv.mkDerivation rec {
pname = "appname";
version = "1.0.0";
# â
Source the archive directly
src = ./AppName-${version}-linux-amd64.deb;
# â
autoPatchelfHook fixes library paths automatically
nativeBuildInputs = with pkgs; [
autoPatchelfHook
dpkg # for .deb extraction
];
# â
Runtime library dependencies
buildInputs = with pkgs; [
stdenv.cc.cc.lib
glib
gtk3
# Add libraries based on ldd output
];
# â
Extract during build (.deb format)
unpackPhase = ''
ar x $src
tar xf data.tar.xz
'';
# For .tar.gz/.tar.bz2: stdenv auto-detects
# For .zip: nativeBuildInputs = [ unzip ];
# For .rpm: nativeBuildInputs = [ rpm, cpio ];
installPhase = ''
mkdir -p $out
cp -r opt/AppName/* $out/
# Fix desktop file paths if exists
if [ -f usr/share/applications/app.desktop ]; then
mkdir -p $out/share/applications
cp usr/share/applications/app.desktop $out/share/applications/
substituteInPlace $out/share/applications/app.desktop \
--replace-fail "/opt/AppName" "$out"
fi
'';
}
Archive Format Examples
RPM packages:
nativeBuildInputs = [ autoPatchelfHook rpm cpio ];
unpackPhase = ''
rpm2cpio $src | cpio -idmv
'';
Tar.gz/tar.bz2/tar.xz:
# stdenv auto-detects these formats
src = ./app-${version}.tar.gz;
# unpackPhase not needed
ZIP archives:
nativeBuildInputs = [ autoPatchelfHook unzip ];
unpackPhase = ''
unzip $src
'';
Plain directory (already extracted):
# â Don't do this - not reproducible
src = ./extracted-app;
# â
Instead: create tarball first
# tar czf app.tar.gz extracted-app/
# Then: src = ./app.tar.gz;
Quick Reference
| Task | Solution |
|---|---|
| Local archive | src = ./package-${version}.tar.gz (relative path) |
| Remote archive | src = fetchurl { url = "..."; hash = "sha256-..."; } |
| Extract .deb | ar x $src && tar xf data.tar.xz in unpackPhase + dpkg |
| Extract .rpm | rpm2cpio $src | cpio -idmv in unpackPhase + rpm, cpio |
| Extract .tar.gz | Auto-detected by stdenv |
| Extract .zip | Add unzip to nativeBuildInputs |
| Fix libraries | Add autoPatchelfHook to nativeBuildInputs |
| Find missing libs | Run binary, check errors, add to buildInputs |
| Wrapper scripts | Use makeWrapper in nativeBuildInputs |
| Version sync | Use ${version} in filename: src = ./app-${version}.tar.gz |
Dependencies: The Three Categories
digraph dependencies {
"What is this?" [shape=diamond];
"When needed?" [shape=diamond];
"nativeBuildInputs" [shape=box];
"buildInputs" [shape=box];
"propagatedBuildInputs" [shape=box];
"What is this?" -> "When needed?";
"When needed?" -> "nativeBuildInputs" [label="build-time tool"];
"When needed?" -> "buildInputs" [label="runtime library"];
"When needed?" -> "propagatedBuildInputs" [label="users need it"];
"nativeBuildInputs" -> "dpkg\nautoPatchelfHook\nmakeWrapper" [style=dashed];
"buildInputs" -> "gtk3\nglib\nlibpulseaudio" [style=dashed];
}
nativeBuildInputs: Tools for building (dpkg, autoPatchelfHook, makeWrapper) buildInputs: Libraries the app links against (gtk3, glib, mesa) propagatedBuildInputs: Rarely needed for .deb packaging
Source File Options
For local archives (development/testing):
src = ./app-${version}.tar.gz; # Relative path in same directory
src = ./app-${version}.deb; # Works for any archive format
For distributed packages:
src = fetchurl {
url = "https://example.com/releases/app-${version}.tar.gz";
hash = "sha256-AAAA..."; # Get with: nix-hash --type sha256 --flat archive
};
Never use absolute paths – they break on other machines.
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
src = ./extracted/ |
Not reproducible, breaks on other machines | src = ./app.tar.gz |
src = /home/user/app.tar.gz |
Absolute path breaks portability | src = ./app.tar.gz (relative) |
src = /home/user/app.tar.gz in fetchurl |
Still an absolute local path | Use ./app.tar.gz or real URL |
| Missing autoPatchelfHook | Binary can’t find libraries | Add to nativeBuildInputs |
| Libraries in nativeBuildInputs | Wrong category – they’re runtime deps | Move to buildInputs |
| Hardcoded version in filename | Must update 2 places when upgrading | Use src = ./app-${version}.tar.gz |
| Wrong extractor for format | .zip fails, .rpm fails with tar | Check Quick Reference for format |
Red Flags – STOP
If you catch yourself thinking:
- “User already extracted it, use that directory” â NO, source from original archive
- “Absolute path works for me locally” â Breaks for others, use relative
- “Just add more libraries until it works” â Find actual dependencies with
ldd - “Quick local test, absolute path is fine” â Bad habits stick, do it right
- “Mixed extraction (some pre-extracted)” â Extract everything in unpackPhase
All of these mean: Use original archive as source, extract in build.
Finding Missing Libraries
# After building
result/bin/appname # Run and check errors
# Or check with ldd
ldd result/opt/AppName/appname
# Look for "not found" libraries
# Add corresponding Nix packages to buildInputs
Common library mappings:
libgtk-3.so.0âgtk3libglib-2.0.so.0âgliblibpulse.so.0âlibpulseaudiolibGL.so.1âmesaorlibglvndlibxkbcommon.so.0âlibxkbcommon(NOT xorg.libxkbcommon)
Version Management
# â
Good: version variable used in filename
pkgs.stdenv.mkDerivation rec {
pname = "myapp";
version = "1.2.3";
src = ./myapp-${version}-linux-x86_64.tar.gz;
}
# â Bad: version hardcoded separately
pkgs.stdenv.mkDerivation rec {
pname = "myapp";
version = "1.2.3";
src = ./myapp-1.2.2-linux-x86_64.tar.gz; # Mismatch!
}
Electron Apps: Extra Considerations
Electron apps often need:
buildInputs = with pkgs; [
# Base
stdenv.cc.cc.lib
# GTK/GUI
glib gtk3 cairo pango gdk-pixbuf
# X11
xorg.libX11 xorg.libXcomposite xorg.libXdamage
xorg.libXext xorg.libXfixes xorg.libXrandr
# System
dbus nspr nss cups libdrm mesa
alsa-lib libpulseaudio
];
Add GPU flags in wrapper:
makeWrapper $out/opt/App/app $out/bin/app \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath buildInputs}" \
--add-flags "--disable-gpu-sandbox"
Advanced: Binaries That Resist Patching
Some proprietary software detects modifications and refuses to run (e.g., license checks, integrity validation). For these cases, autoPatchelfHook won’t work.
Solution: FHS environment with Bubblewrap
Create a standard Linux filesystem hierarchy in a container:
{ buildFHSEnv }:
buildFHSEnv {
name = "myapp";
targetPkgs = pkgs: with pkgs; [
# All runtime dependencies
glib gtk3 openssl
];
runScript = "${extracted}/bin/myapp";
}
Or use steam-run for quick testing (includes many common libraries):
steam-run ./myapp
When to use:
- Binary detects modifications (license/DRM checks)
- autoPatchelfHook breaks functionality
- Quick prototyping before proper packaging
Trade-off: Larger closure size, less reproducible than autoPatchelfHook.
Build Phases and Hooks
The standard environment runs 7 phases. You can customize any phase:
stdenv.mkDerivation {
# ...
# Before unpacking
preUnpack = ''
echo "About to extract..."
'';
# After patching, before configure
postPatch = ''
# Fix hardcoded paths
substituteInPlace Makefile \
--replace "/usr/bin" "$out/bin"
'';
# After installation
postInstall = ''
# Wrap binary with runtime dependencies
wrapProgram $out/bin/myapp \
--prefix PATH : ${lib.makeBinPath [ ffmpeg ]} \
--set MY_VAR "value"
'';
}
Phase order: unpack â patch â configure â build â check â install â fixup
Common hooks:
preInstall/postInstall– Modify installationpostPatch– Fix source before buildingpostFixup– Final touches after automatic fixup
Wrapper Programs: wrapProgram
When binaries need specific environment variables or PATH entries:
nativeBuildInputs = [ makeWrapper ];
postInstall = ''
wrapProgram $out/bin/myapp \
--prefix PATH : "${lib.makeBinPath [ ffmpeg imagemagick ]}" \
--prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ vulkan-loader ]}" \
--set QT_QPA_PLATFORM "xcb" \
--add-flags "--disable-telemetry"
'';
Common use cases:
- Add tools to PATH (ffmpeg, imagemagick)
- Set environment variables (QT_, GTK_)
- Add default flags
- Extend LD_LIBRARY_PATH for dynamic loading
Real-World Impact
Without this pattern:
- Package works on your machine only
- Breaks when shared or used in flakes
- Manual extraction required before every build
- Version mismatches go unnoticed
- Format-specific extraction errors
With this pattern:
- Fully reproducible across machines
- Works in flakes, NixOS configs, nix-env
- Automatic extraction on every build
- Version changes = single line edit
- Handles .deb, .rpm, .tar.gz, .zip consistently