On the difficulty of reliably getting debug info

One thing which is surprisingly hard for me in nixpkgs is reliably getting debug symbols. I’d like to start a discussion on whether this could be improved.

First, I’ll explain where the difficulties seem to lie to me as a relative newcomer (1.5 years) to the Nix ecosystem:

  1. The nixpkgs configuration side is confusing, I’d like to say arcane: separateDebugInfo, enableDebugging, dontStrip, etc. I think I mostly understand it now, but I must be in the minority.

  2. Too many packages built by Hydra don’t get built with debug symbols (like libstdc++)—I assume mostly because of storage space reasons instead of build time reasons? This means that if I want to debug anything in nixpkgs that depends on libstdc++ and have the debug symbols for libstdc++, I need to rebuild almost the world.

  3. Writing derivations to reliably get debug info is just hard because upstream build scripts can do arbitrary things.

What I’ve done to tackle it for my own use

I’ve wasted too many days of my life figuring out how to make things have debug info, so I wrote a helper that can be used to assert at build time that there is debug info. It works like this:

verifyDebugInfo {
  pkg = pkgs.hello;  # a derivation that should have debug info (build fails if it doesn't)
  globs = [ "$out/lib/*.so" ];
  files = [ "$out/bin/hello" ];
}

Here’s an actual snippet from my config to get sway and some dependencies to have debug symbols:

sway =
  let
    wlroots-debug = pkgs.mylib.verifyDebugInfo {
      pkg = pkgs.wlroots.overrideAttrs {
        mesonBuildType = "debugoptimized";
        separateDebugInfo = true;
      };
      globs = [ "$out/lib/*.so" ];
    };
    sway-debug = pkgs.mylib.verifyDebugInfo {
      pkg = (
        (pkgs.sway-unwrapped.overrideAttrs {
          mesonBuildType = "debugoptimized";
          separateDebugInfo = true;
        }).override
          {
            wlroots = wlroots-debug;
          }
      );
      globs = [ "$out/bin/sway*" ];
    };
Implementation
# A Nix expression that adds a postFixup hook to verify that specified files
# contain debug information. It supports both explicit file paths and glob
# patterns.

# Usage:
# 
#   Call this function with a derivation and an attrset containing:
#     - pkg: the derivation to verify
#     - files: a list of explicit file paths to check
#     - globs: a list of glob patterns to expand and check
#
# Returns: the derivation with an added postFixup hook that verifies debug info.
# 
# Example:
#   let
#     verifyDebugInfo = import ./lib/verifyDebugInfo.nix { lib = lib; elfutils = pkgs.elfutils; };
#   in
#   verifyDebugInfo pkg { files = [ "$out/bin/mybinary" ]; globs = [ "$out/lib/*.so" ]; }

{ lib, elfutils }:

{
  pkg,
  files ? [ ],
  globs ? [ ],
}:

pkg.overrideAttrs (oldAttrs: {
  postFixup = (oldAttrs.postFixup or "") + ''
    (
      echo "verifyDebugInfo: Checking ${pkg.name}"

      # Determine the debug output path (if it exists)
      # Nix sets $debugOutput variable if separateDebugInfo = true,
      # or we can try to guess via the $debug environment variable.
      DEBUG_OUT="''${debug:-}"

      check_debug() {
        local f="$1"
        [ -f "$f" ] || return 0

        # Skip non-ELF files
        if ! head -c 4 "$f" | grep -q 'ELF'; then
          echo "verifyDebugInfo: SKIP (not ELF): $f"
          return 0
        fi

        # 1. Check for inline debug info (fat binary)
        if ${elfutils}/bin/eu-readelf -S "$f" | grep -q '\.debug_info'; then
          echo "verifyDebugInfo: OK (Inline): $f"
          return 0
        fi

        # Check for split debug info via build ID
        local build_id
        build_id=$(${elfutils}/bin/eu-readelf -n "$f" \
          | grep "Build ID:" \
          | sed 's/.*Build ID: \([0-9a-f]*\).*/\1/')

        if [ -n "$build_id" ] && [ -n "$DEBUG_OUT" ]; then
          # Nixpkgs stores debug info at: <debug-out>/lib/debug/.build-id/xx/yyyy.debug
          local c1="''${build_id:0:2}"
          local c2="''${build_id:2}"
          local debug_file="$DEBUG_OUT/lib/debug/.build-id/$c1/$c2.debug"

          if [ -f "$debug_file" ]; then
             echo "verifyDebugInfo: OK (Split/BuildID): $f -> .../$c1/$c2.debug"
             return 0
          else
             echo "verifyDebugInfo: ERROR: $f has Build ID $build_id, but debug file is missing!" >&2
             echo "    Expected: $debug_file" >&2
             return 1
          fi
        fi

        echo "verifyDebugInfo: ERROR: $f lacks debug info!" >&2
        echo "    - No inline .debug_info" >&2
        echo "    - No separate debug file found in $DEBUG_OUT (via build ID)" >&2
        return 1
      }

      # we only exit at end
      failed=0

      # 1. Check explicit files
      for raw_file in ${lib.escapeShellArgs files}; do
        eval "file=\"$raw_file\""
        if [ ! -e "$file" ]; then
          echo "verifyDebugInfo: ERROR: Explicit path '$file' missing!" >&2; exit 1
        fi
        check_debug "$file" || failed=1
      done

      # 2. Check globs
      for raw_pattern in ${lib.escapeShellArgs globs}; do
        eval "pattern=\"$raw_pattern\""
        shopt -s nullglob
        expanded_files=( $pattern )
        shopt -u nullglob

        if [ ''${#expanded_files[@]} -eq 0 ]; then
          echo "verifyDebugInfo: ERROR: Glob '$raw_pattern' matched nothing!" >&2; exit 1
        fi

        for file in "''${expanded_files[@]}"; do
          check_debug "$file" || failed=1
        done
      done

      if [ $failed -ne 0 ]; then
        echo "verifyDebugInfo: FAILED"
        exit 1
      fi

      echo "verifyDebugInfo: OK"
    )
  '';
})

Thoughts/proposals

There’s a few things on my wishlist.

1. nixpkgs configuration side confusing

This point is tricky; a lot of that is legacy, and it probably can mostly be improved by documentation.

2. Hydra building too little with debug symbols

This, I suspect (but do not know) is a conscious saving decision. Having said that, I wonder if we could at least build some more of the very basic libraries like libstdc++ with debug symbols.

Besides that, I have one radical proposal. If I guess right and the major issue is storage cost, could we have Hydra build the derivations with split debug symbols and then just throw the debug symbol part away? Rationale: Due to how nix works, anything that changes the derivation hash is a massive pain. This way it would at least be possible to build the split debug symbols yourself and just use them with whatever is in nixpkgs—i.e. you’d need to rebuild libstdc++, but not every derivation that depends on libstdc++.

Now, I’m not sure if there’s some reason why that doesn’t work because builds are not reproducible (but I understand nix tries hard to use same build ids).

# On the reliability of getting debug symbols

This is where I’d actually love something like my verifyDebugInfo get wider use. I have a couple of very initial ideas of how this could look like:

  • Derivations can opt into being checked by either setting
    • verifyDebugInfo = true → all ELFs in $out should have debug info when built with debug info
    • verifyDebugInfo = { files = ...; globs = ... } → opt in for specific files in $out
    • verifyDebugInfo = { exclude = ...; } → general opt in with exclusions
  • What this does depends on whether the derivation is built with some mechanism that asserts it wants debug info (it could be an env variable set by things like enableDebugging, although I dislike how that conflates debug info and no optimizations):
    • If built with debug info: fail the build if the debug infos are not there
    • If built without: Help maintain the fields by checking that the named files/globs are present and match something, but do not require debug info.

In any case for backwards compatibility this needs to clearly be opt in for derivations. And, I don’t really know if people care enough; sometimes I get the feeling I’m one of approximately three people in the world using debug symbols in nixpkgs, as evidenced by the state of libstdc++? :stuck_out_tongue:


Any thoughts?

7 Likes

Correct. At least for some packages the debug info is significantly larger than the binary package itself. And cache.nixos.org already is suffering from being too larger. Maybe after we have garbage-collection workflows for it, we could build all debug symbols and then just collect them early (after several months?)

1 Like

At least some of the past discussions around this should be in here or linked:

This has always been my big contradictory feeling with NixOS: on the one hand, designed & built based on ideas, concepts and principles of software development, used a lot by the same kind of folks, but in practice making it really hard to utilize one of the most fundamental tools of software development: a debugger.

Still haven’t made my peace with this contradiction and still not giving up on one day being able to have debug symbols throughout my whole OS stack - but for now I keep longing for being able to finally provide proper backtraces for KDE/Plasma etc

4 Likes

This isn’t about principles but about what’s practical with the current state we have.

The S3 size is currently on the order of 1000 TB I think. In this respect we’re at a huge technical disadvantage to all distros I know about. Estimates are that with debug info the size of packages multiplies, so we can’t afford to just enable it for everything. First we need some technical improvements, but they’re not easy and not without other down-sides (and haven’t progressed much over years).

2 Likes

Sure, I’m aware of the practical limitations and the primary reasons why we don’t have universal debug symbols…

…this was more about voicing a personal frustration where my idea of what it should look like collides with reality.

I think there’s probably a middle-ground we could try to strike here. Enabling debug info for specific base stdenv packages such as libstdc++ would relieve some of the rebuild-the-world pain points, and hopefully without incurring too much storage cost.

Certainly. We do enable it for some packages. I’d say feel free to file pull requests for others that have good ratio of usefulness vs. extra size (e.g. popular libraries).

1 Like