packaging-binary-for-nix

📁 lihaoze123/my-skills 📅 Jan 23, 2026
1
总安装量
1
周安装量
#48806
全站排名
安装命令
npx skills add https://github.com/lihaoze123/my-skills --skill packaging-binary-for-nix

Agent 安装分布

claude-code 1

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 → gtk3
  • libglib-2.0.so.0 → glib
  • libpulse.so.0 → libpulseaudio
  • libGL.so.1 → mesa or libglvnd
  • libxkbcommon.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 installation
  • postPatch – Fix source before building
  • postFixup – 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