Skip to content

chore(release): 2.7.6 -- separate debug and release build configurations - #65

Merged
LESdylan merged 1 commit into
developfrom
chore/release-2.7.6
Aug 23, 2026
Merged

chore(release): 2.7.6 -- separate debug and release build configurations#65
LESdylan merged 1 commit into
developfrom
chore/release-2.7.6

Conversation

@LESdylan

Copy link
Copy Markdown
Member

Cuts v2.7.6 and separates the build you develop against from the build you ship.

Build configurations

make now takes MODE=debug|release|relwithdebinfo:

MODE flags for
debug (default) -O0 -g3 -ggdb, ASan+LSan, libc malloc developing
release -O3 -DNDEBUG, LTO, --gc-sections, no -g, no sanitizer shipping
relwithdebinfo -O2 -g -DNDEBUG, no sanitizer, no LTO bugs that only appear optimized

Measured on this machine:

mode size debug data ASan syms
release 462 KB 807 B 0
relwithdebinfo 3.0 MB 2.6 MB 0
debug 5.6 MB 2.2 MB 154

Nothing is stripped after the fact — release carries no debug information because release never compiles -g in. The 807 bytes that remain are a single compilation unit, libgcc's crtfastmath.c, pulled in by the pre-existing -ffast-math and shipped by the distro already built with -g. None of our 493 translation units contribute any debug info to release. No sanitizer, assertion, symbol or dependency was removed from the debug build to get there.

OPT=1 — which the release workflow, the platform matrix, both install targets, the Docker build and make bench all pass — still means exactly MODE=release, byte for byte, and a test pins that.

Two real defects found while writing the test

OPT=1 disagreed with itself. SAFE defaulted off ifdef OPT, so the two spellings of one configuration picked different allocators and produced different binaries. SAFE now keys off MODE.

Build modes shared an object tree. OBJ_DIR also keyed on ifdef OPT, covering the benchmark build alone and leaving MODE=release and MODE=relwithdebinfo parked in the debug tree. make rebuilds on a changed prerequisite, never on a changed flag, so a plain make MODE=release after a debug build fed the linker ASan-instrumented objects under a link line with no -fsanitize:

func_retire.o: undefined reference to `__asan_report_load4'

make re hid it. Objects now live in build/obj-<mode>-<allocator>.

Tests

tests/build_modes_test.py — 20 checks reading make flags, which resolves a configuration without compiling, so it costs nothing and cannot be fooled by a stale object tree. 3 of its checks fail against the pre-fix OBJ_DIR, verified red-then-green.

tests/link_closure_test.py now globs build/obj* and picks the newest tree rather than the first name it recognised — which is what its docstring always claimed, and what a fixed list could not keep doing once modes were added.

Both are picked up automatically by tests/pty_suite.sh (it globs tests/*.py), so no CI wiring was needed.

Also in this release

67508ad (already on develop) — make user-install now leaves hellish on PATH rather than only on disk. Release notes updated to cover it, since it ships under this tag.

Verified locally

  • make norm — clean
  • golden suite — 3790/3790
  • pty suite — 30 ok / 0 failed / 4 skipped
  • build_modes_test.py 20/20, user_install_path_test.py 22/22

Development instrumentation and shipping optimization are different jobs
and one set of flags cannot do both. The Makefile had two configurations
selected by `ifdef OPT` and no name for either, so the middle case — an
optimized build you can still attach a debugger to — did not exist, and a
5.6 MB `build/bin/hellish` read as the shipped binary when it never was.

Three named configurations, chosen with MODE=:

  debug (default)   -O0 -g3 -ggdb, ASan+LSan, libc malloc     develop
  release           -O3 -DNDEBUG, LTO, --gc-sections, no -g   ship
  relwithdebinfo    -O2 -g -DNDEBUG, no sanitizer, no LTO     optimized bugs

462 KB / 3.0 MB / 5.6 MB respectively. Nothing is stripped afterwards:
release carries no debug information because release never compiles -g in,
which is why strip recovers ~1 KB from it — 807 bytes of libgcc
crtfastmath.c that -ffast-math pulls in already compiled with -g. None of
it is ours. No sanitizer, assertion, symbol or dependency was removed from
the debug build to get there.

OPT=1 stays working and now resolves to exactly MODE=release, byte for
byte. It used to disagree with itself: SAFE defaulted off `ifdef OPT`, so
the two spellings of one configuration picked different allocators and
produced different binaries. SAFE now keys off MODE.

Fixes a real defect found while writing the test: OBJ_DIR also keyed on
`ifdef OPT`, which covered the OPT benchmark build alone and left
MODE=release and MODE=relwithdebinfo parked in the debug tree. make
rebuilds on a changed prerequisite, never on a changed flag, so a plain
`make MODE=release` after a debug build fed the linker ASan-instrumented
objects under a link line with no -fsanitize:

    func_retire.o: undefined reference to `__asan_report_load4'

`make re` hid it. Objects now live in build/obj-<mode>-<allocator>.

tests/build_modes_test.py pins all of it — 20 checks read `make flags`,
which resolves a configuration without compiling, so the test costs
nothing and cannot be fooled by a stale object tree. 3 of its checks fail
against the pre-fix OBJ_DIR. tests/link_closure_test.py now globs
build/obj* and picks the newest tree rather than the first name it
recognised, which is what its docstring always claimed.

Also corrects the update-freshness fix to reference issue #64; 2.7.5 cited
the number GitHub had assigned to the pull request.

make norm clean · golden suite 3790/3790 · pty suite green
@LESdylan
LESdylan merged commit 023b785 into develop Aug 23, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant