authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 16:21:57-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 17:12:18-07:00
log2ab751892691170d86c0c09e585a9534a9c90b28
tree872ba4d224a8e387cc4887ebad8256b26a43ab7a
parent7069fb2abee3f488a947853152d7830c53c650f0
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: release builds keep their symbol tables and frame pointers

- Release executables keep their symbol tables, so backtraces, crash logs, perf, gdb and Instruments name functions - Every target builds with frame pointers The Linux linker shim strips debug info itself after linking, since zig's linker drops the symbol table along with it. Assisted-by: claude-opus-5.5

5 files changed, 36 insertions(+), 6 deletions(-)

.cargo/config.toml created+4
...@@ -0,0 +1,4 @@
1# Frame pointers let perf, Instruments and other sampling profilers walk the stack.
2# A cfg table, unlike build.rustflags, joins the flags the platform scripts give each target.
3[target.'cfg(all())']
4rustflags = ["-C", "force-frame-pointers=yes"]
crates/snowbound/linux/package.sh+1-1
...@@ -10,7 +10,7 @@ mkdir -p "$target/dist"...@@ -10,7 +10,7 @@ mkdir -p "$target/dist"
10[ $# -gt 0 ] || set -- x86_64 aarch6410[ $# -gt 0 ] || set -- x86_64 aarch64
11for arch do11for arch do
12 triple=$arch-unknown-linux-gnu12 triple=$arch-unknown-linux-gnu
13 CARGO_PROFILE_RELEASE_STRIP=symbols sh "$root/platform/linux/cargo.sh" "$arch" build \13 sh "$root/platform/linux/cargo.sh" "$arch" build \
14 --manifest-path "$root/Cargo.toml" --release -p snowbound14 --manifest-path "$root/Cargo.toml" --release -p snowbound
15 # PT_INTERP becomes PT_NULL, so the kernel starts the executable at `loader`'s entry, which15 # PT_INTERP becomes PT_NULL, so the kernel starts the executable at `loader`'s entry, which
16 # finds the system's dynamic loader, NixOS's too. zig rejects --no-dynamic-linker.16 # finds the system's dynamic loader, NixOS's too. zig rejects --no-dynamic-linker.
platform/linux/cc.sh+14-3
...@@ -1,8 +1,19 @@...@@ -1,8 +1,19 @@
1#!/bin/sh1#!/bin/sh
2# zig cc for $ZIG_TARGET, as `cargo.sh` sets it: the C compiler and linker. zig rejects2# zig cc for $ZIG_TARGET, as `cargo.sh` sets it: the C compiler and linker. zig rejects
3# rustc's --target, --no-undefined-version and -O1.3# rustc's --target, --no-undefined-version and -O1, and strips the symbol table with the debug
4# info, so the debug info goes after linking.
5strip= output= previous=
4for arg do6for arg do
5 shift7 shift
6 case $arg in --target=* | -Wl,--no-undefined-version | -Wl,-O1) ;; *) set -- "$@" "$arg" ;; esac8 [ "$previous" = -o ] && output=$arg
9 previous=$arg
10 case $arg in
11 --target=* | -Wl,--no-undefined-version | -Wl,-O1) ;;
12 -Wl,--strip-debug) strip=1 ;;
13 *) set -- "$@" "$arg" ;;
14 esac
7done15done
8exec zig cc -target "$ZIG_TARGET" "$@"16zig cc -target "$ZIG_TARGET" "$@" || exit
17[ -z "$strip" ] ||
18 "$(rustc --print sysroot)/lib/rustlib/$(rustc -vV | sed -n 's/^host: //p')/bin/rust-objcopy" \
19 --strip-debug "$output"
tools/RELEASE.md+16
...@@ -146,6 +146,22 @@ version above 2.17. The Linux executable links only libc, libm, libpthread and...@@ -146,6 +146,22 @@ version above 2.17. The Linux executable links only libc, libm, libpthread and
146libdl; Wayland, X11, xkbcommon, EGL, Vulkan, fontconfig, GStreamer and Enchant146libdl; Wayland, X11, xkbcommon, EGL, Vulkan, fontconfig, GStreamer and Enchant
147are loaded at run time.147are loaded at run time.
148148
149## Symbols and frame pointers
150
151Every executable keeps its symbol table, so backtraces, crash logs, `perf`, gdb
152and Instruments name functions; Cargo's release profile strips only debug info
153(`platform/linux/cc.sh` does that itself, as zig's linker would strip both).
154`.cargo/config.toml` builds every target with frame pointers, and Rust's
155standard library ships with them. The symbols cost about a fifth: Linux x86_64
156grows from 53 to 62 MB, Windows x86_64 from 55 to 72 MB, and the macOS app
157already carried them. Line tables would take an executable to some 250 MB, and
158a dSYM adds 34 MB zipped per Mac architecture, so neither ships; build with
159`CARGO_PROFILE_RELEASE_DEBUG=line-tables-only` for files and lines. Windows
160tools that read only PDBs see no names: the Rust targets here emit DWARF, from
161which lld's PDB keeps only global symbols. Linux's crash log gives a signal's
162frames as `snowbound(+0x1a2b3c)`, since glibc's `backtrace_symbols_fd` reads only
163dynamic symbols; `addr2line -f -e snowbound 0x1a2b3c` names them.
164
149## In the app165## In the app
150166
151`update.rs` runs one thread. With automatic checks on (Options, General) and a167`update.rs` runs one thread. With automatic checks on (Options, General) and a
tools/release.py+1-2
...@@ -188,8 +188,7 @@ def build_windows(architectures):...@@ -188,8 +188,7 @@ def build_windows(architectures):
188 Windows it supports."""188 Windows it supports."""
189 built = {}189 built = {}
190 for arch in architectures:190 for arch in architectures:
191 run(['sh', ROOT / 'platform/windows/cargo.sh', arch, 'build', '--release', '-p', 'snowbound'],191 run(['sh', ROOT / 'platform/windows/cargo.sh', arch, 'build', '--release', '-p', 'snowbound'])
192 env={**os.environ, 'CARGO_PROFILE_RELEASE_STRIP': 'symbols'})
193 built[f'windows-{arch}'] = ROOT / f'target/windows/{WINDOWS[arch]}/release/snowbound.exe'192 built[f'windows-{arch}'] = ROOT / f'target/windows/{WINDOWS[arch]}/release/snowbound.exe'
194 return built193 return built
195194