Guix Binary Packages: Reproducible Builds from Existing Binaries

beginner new 8 min read updated 18 Aug 2026
On this page 5

Reproducibility: Why Guix Matters for Binaries

A binary executable built from source code rarely guarantees bit-for-bit identity across different build environments. Minor variations in compiler versions, operating system patches, library paths, or even environment variables like PATH or LANG during compilation can alter the resulting binary artifact. Timestamps embedded in archives or object files also contribute to non-determinism. These subtle differences prevent independent verification that a distributed binary truly matches its advertised source.

This variability undermines trust and complicates security audits. Without a reproducible build, verifying that a distributed binary contains no unauthorized modifications or backdoors becomes challenging; one cannot simply rebuild the source and compare the hashes. It forces users to trust the integrity of the build process and environment used by the distributor, rather than relying on verifiable cryptographic proofs.

Guix addresses this through functional package management. It treats software builds as pure functions, where the output depends solely on explicitly declared inputs. Every component — source code, build tools, compilers, libraries, and even environment variables — is an explicit input to the build process. Each input is cryptographically hashed, and any change to an input results in a different hash, leading to a different output path in the Guix store.

Builds occur in isolated, hermetic environments. This means the build process cannot access anything outside its declared inputs, preventing implicit dependencies on the host system. The output of a successful Guix build is a set of files placed into a unique store path. For example:

/gnu/store/26spsx6x6y46788h1m270c5j7873p145-hello-2.12/bin/hello

This hash prefix, 26spsx6x6y46788h1m270c5j7873p145, is derived from a hash of all build inputs. If the inputs are identical, the output path and its contents will be identical.

For existing binaries, where the original source build environment is often unknown, Guix’s approach shifts. The goal is no longer to rebuild from source in the traditional sense, but to define a Guix package recipe that, when executed, reproduces the existing binary bit-for-bit. This involves carefully specifying the inputs and build steps required to generate an identical binary. This process makes the act of integrating and distributing existing binaries verifiable and consistently reproducible within the Guix framework. The resulting package’s store path then cryptographically reflects this reproducible packaging process.

Guix Environment Setup for Binary Packaging

Packaging existing binaries with Guix requires specific utilities to inspect and adapt their runtime dependencies. A standard Guix installation provides the core guix commands, but additional tools are needed for the unique challenges of binary re-packaging.

The most crucial utility for this task is patchelf. Guix places every package in its own isolated, content-addressed store path (e.g., /gnu/store/...-glibc-2.31). Pre-built binaries often link against libraries expected in conventional system paths or use hardcoded RPATHs that do not match Guix’s layout. patchelf modifies the RPATH (Run-time search PATH) entry within ELF binaries and shared libraries, directing the dynamic linker to look for dependencies in the correct Guix store paths.

To inspect the dynamic library dependencies of an existing binary, use standard ELF utilities such as readelf and objdump. readelf -d <binary-path> lists the required shared libraries and their RPATHs, while objdump -p <binary-path> provides a more verbose parse of the ELF header, including program headers. Understanding these dependencies is the first step before attempting to modify the binary or write its Guix package definition.

These utilities, including patchelf, readelf, and objdump, can be provisioned reproducibly using guix environment. This ensures your packaging environment is consistent and independent of your host system’s specific tool versions. For instance, to obtain patchelf and basic ELF inspection tools:

guix environment --ad-hoc patchelf binutils -- bash

Executing this command drops you into a shell where patchelf, readelf, and objdump are available in your PATH. This isolated environment prevents conflicts and guarantees that the tools used for packaging match specific versions.

For developing Guix package definitions (the .scm files), a text editor is sufficient. These definitions will typically reside within a custom Guix channel. To make your custom channel visible to Guix, configure ~/.config/guix/channels.scm to include its path. For example:

(cons* (channel
         (name 'my-binary-packages)
         (url "file:///path/to/your/guix-channel-repo"))
       %default-channels)

After modifying channels.scm, run guix pull to update Guix’s understanding of available packages, including those from your new channel. This setup provides the necessary foundation to begin introspecting and packaging existing binaries into the Guix environment.

Guix Binary Packaging: Step-by-Step Workflow

Integrating a pre-compiled binary into Guix’s reproducible framework requires defining its origin and runtime environment. The process begins by identifying the binary and its direct runtime dependencies. This ensures the packaged tool operates correctly within Guix’s isolated store.

First, inspect the arbitrary binary to determine its shared library requirements. For ELF executables on GNU/Linux systems, the ldd utility lists these dependencies. This output guides which Guix packages to declare as runtime inputs.

$ ldd /path/to/your-binary
        linux-vdso.so.1 (0x00007ffe3b1fe000)
        libz.so.1 => /gnu/store/...-zlib-1.2.13/lib/libz.so.1 (0x00007f3542289000)
        libc.so.6 => /gnu/store/...-glibc-2.37/lib/libc.so.6 (0x00007f354209f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f35422d3000)

Next, create a Guix package definition file, for example, my-tool-package.scm. Use simple-build-system as the build system, as no compilation is necessary. The source field uses local-file to point to the binary on your host system.

(define-public my-tool
  (package
    (name "my-tool")
    (version "1.0")
    (source (local-file "/path/to/your-binary" "your-binary"
                        #:recursive? #t
                        #:file-hash (base32 "sha256-YOUR-BINARY-HASH")))
    (build-system simple-build-system)
    (arguments
     '(#:phases
       (modify-phases %standard-phases
         (add-after 'unpack 'install-binary
           (lambda _
             (mkdir-p (string-append #$output "/bin"))
             (copy-file "your-binary" (string-append #$output "/bin/your-binary"))
             #t)))))
    (inputs '()) ; No build-time dependencies
    (propagated-inputs (list glibc zlib)) ; Example runtime dependencies
    (synopsis "A custom pre-built tool")
    (description "Packages a specific pre-built binary into Guix.")
    (home-page "https://example.com/your-tool")
    (license #f)))

The local-file origin requires a file-hash of the binary. Calculate this hash using guix hash --recursive /path/to/your-binary. Replace "sha256-YOUR-BINARY-HASH" in the package definition with the output from this command. The #:recursive? #t flag is useful if your binary comes with auxiliary files in the same directory.

The install-binary phase copies the your-binary file from the source directory into the Guix output’s /bin directory. This makes the binary accessible via the PATH when the package is used.

The propagated-inputs list declares the runtime dependencies identified by ldd. Guix ensures these packages are available in the environment when my-tool runs. Failing to list a needed dependency here will result in runtime errors like “file not found” for shared libraries.

Finally, build the package and test its functionality.

$ guix build -f my-tool-package.scm
$ guix shell my-tool -- your-binary --version

This confirms the binary runs within a Guix-managed environment, satisfying its dependencies and demonstrating its reproducible integration.

Binary Packaging Errors: How to Fix Guix Build Failures

Existing binaries often fail to run within a Guix environment because they expect a conventional filesystem hierarchy (FHS) and fixed paths for their dependencies. Guix, by contrast, places all software components into isolated, content-addressed paths within /gnu/store. This fundamental difference leads to common runtime errors, primarily related to missing libraries or inaccessible resource files.

A frequent issue manifests as the program failing to start with “file not found” errors, even when the required libraries are present in the Guix store. This typically means the binary’s runtime linker cannot locate its shared objects. Use ldd on the failing executable to inspect its dynamic library dependencies. Any line ending with “not found” indicates a missing or unlocatable library.

$ ldd /gnu/store/...-my-binary/bin/my-binary
        linux-vdso.so.1 (0x00007ffe6d7fe000)
        libfoo.so.2 => not found
        libc.so.6 => /gnu/store/...-glibc/lib/libc.so.6 (0x00007f3c18c5e000)
        # ... other libraries

To resolve missing shared library dependencies, modify the binary’s Runtime Path (RPATH) using patchelf. This tool allows you to add Guix store paths to the binary’s search list for libraries. You may also need to adjust the ELF interpreter path if the binary expects a system-specific dynamic linker instead of Guix’s wrapper.

$ patchelf --set-rpath /gnu/store/...-libfoo/lib:/gnu/store/...-libbar/lib \
           --set-interpreter /gnu/store/...-glibc/lib/ld-linux-x86-64.so.2 \
           /gnu/store/...-my-binary/bin/my-binary

While patchelf is essential for fixing library paths, it modifies the binary directly, which can be brittle if the binary is signed or expects specific checksums. An alternative for complex scenarios or when patchelf is insufficient is to create a shell wrapper script. This script can set environment variables like LD_LIBRARY_PATH or PATH before executing the original binary, providing a more flexible way to manage runtime dependencies without altering the binary itself.

Hardcoded absolute paths within the binary, pointing to configuration files, data directories, or other executables outside the Guix store, present another challenge. These typically manifest as “permission denied” or “no such file or directory” errors during execution. Diagnosing these often requires strace to observe file access attempts.

$ strace -f -o strace.log /gnu/store/...-my-binary/bin/my-binary
# ... examine strace.log for failed openat() or access() calls

Fixing hardcoded paths usually involves creating a FHS-like directory structure within the package output and using a wrapper script to make the binary aware of these locations. The wrapper can bind-mount directories or symlink expected paths into temporary locations, effectively emulating the traditional filesystem layout the binary expects. This approach, while useful, reduces the isolation benefits of Guix and should be used judiciously.

Package a Real-World Binary with Guix: A Practice Lab

This lab packages ripgrep, a command-line search tool, directly from its pre-compiled binary release. This demonstrates how to integrate existing, pre-built software into the Guix environment without compiling from source. We will use a statically linked binary to simplify dependency management for this exercise.

Begin by downloading the ripgrep static binary. We target a specific version and architecture to ensure a predictable outcome. Extract the rg executable from the archive.

wget https://github.com/BurntSushi/ripgrep/releases/download/13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz
tar -xzf ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz
mv ripgrep-13.0.0-x86_64-unknown-linux-musl/rg ./rg

Next, create a Guix package definition file, for example, ripgrep-binary.scm. This definition uses simple-build-system because we are not compiling anything; we only need to copy the pre-existing binary into the Guix store. The source field points to the rg file downloaded earlier using local-file.

(use-modules (guix packages) (guix build-system simple) (guix licenses))

(define-public ripgrep-binary-only
  (package
    (name "ripgrep-binary-only")
    (version "13.0.0")
    (source (local-file "./rg" "rg-binary" #:recursive? #f))
    (build-system simple-build-system)
    (arguments
     '(#:phases
       (modify-phases %standard-phases
         (add-after 'unpack 'install-binary
           (lambda _
             (mkdir (string-append #$output "/bin"))
             (copy-file #$source (string-append #$output "/bin/rg"))
             (set-permissions (string-append #$output "/bin/rg") #o555))))))
    (synopsis "A binary-only package for ripgrep")
    (description "This package provides the pre-compiled ripgrep binary (version 13.0.0). It is suitable for integrating pre-built software into Guix where source compilation is not the primary goal.")
    (license license:gpl3+)))

The arguments field defines a custom build phase. We add install-binary after the unpack phase. This phase creates a /bin directory within the package’s output and copies the rg executable into it. Setting permissions to 0o555 makes the binary executable.

Build the package using guix build and the package definition file.

guix build -f ripgrep-binary.scm

The command output will show the path to the newly created store item, for example: /gnu/store/...-ripgrep-binary-only-13.0.0.

To verify the package, use guix shell to enter an environment where ripgrep-binary-only is available. Then, execute rg --version.

guix shell ripgrep-binary-only -- rg --version
ripgrep 13.0.0 (rev 7a96a90033)
-SIMD -AVX (compiled)
-AVX (runtime)

This confirms the packaged binary functions correctly within the Guix environment. This method is useful for software where source code is unavailable, compilation is complex, or specific pre-built artifacts are required.