Skip to content

Keep build-tree details out of native gems - #16

Merged
excid3 merged 1 commit into
mainfrom
native-gem-hygiene
Sep 30, 2026
Merged

excid3 merged 1 commit into
mainfrom
native-gem-hygiene

Conversation

@excid3

@excid3 excid3 commented Sep 30, 2026

Copy link
Copy Markdown
Member

Found by deploying fresh Rails apps (3.2 to 8.0) on the published builds of every series. All 17 deploys passed, but three things leak from the build into gems compiled on the server.

1. Gems that build but can't load (static libruby)

gem install strscan -v 3.1.8 on 2.4.10, 2.5.9, 2.6.10 and 2.7.8:

symbol lookup error: .../strscan.so: undefined symbol: rb_deprecate_constant

mkmf links have_func probes against libruby-static.a, where Ruby's internal (hidden-visibility) functions are ordinary linkable globals. The probe succeeds, the gem calls the function, and the ruby executable doesn't export it. A fresh bundle of Rails 6.1 on 2.5.9 hits this through mail -> net-imap -> strscan.

Fix: after install, merge the archive into a single relocatable object and objcopy --localize-hidden it. Only the exported API stays linkable, as with a shared libruby.

2. Build paths in rbconfig and run paths

  • LDFLAGS/DLDFLAGS carried -Wl,-rpath,/work/.build/.../deps/ncurses/lib (-Wl,-R on older series), which went into every native gem.
  • configure_args kept --with-openssl-dir=/work/.build/... etc., which mkmf's dir_config turns into -I/-L/run path for gems such as puma. They now point at the relocated prefix, where those headers and static libraries actually are.
  • The shipped ruby and extensions had the same run paths; removed with patchelf (present in the manylinux images).

3. terminfo

The bundled ncurses searched a terminfo database under the build directory: Cannot read termcap database; using dumb terminal settings. on every readline load. It now reads /etc/terminfo:/lib/terminfo:/usr/share/terminfo.

Checks

check_no_build_paths! fails the build if rbconfig or any binary's run path names the build's dependency tree or install prefix, or if libruby-static.a still has global hidden symbols.

Testing

Built locally for arm64: 1.8.7-p374, 2.1.10, 2.5.9, 2.7.8, 3.1.7 (no YJIT) and 3.2.0, 3.4.11, 4.0.7 (YJIT). On Ubuntu 24.04 with each:

  • no termcap warning, readline loads; no /work/ in rbconfig; no run path on shipped binaries
  • strscan 3.1.8 compiles and loads (2.5.9 and later)
  • puma compiles against the bundled OpenSSL via the rewritten --with-openssl-dir, no dynamic libssl dependency, no exported SSL symbols
  • pg from source + TLS Postgres + HTTPS in both require orders: all eight pass

Not changed (same in the published builds): 4.0.7's bin/ruby has an /./lib RPATH and rbconfig keeps a RUST_LIB makefile variable pointing at the build tree.

After merge

libexec/package.rb is in every fingerprint, and this one does change every build: dispatch Release New Versions with replace_all.

Three things a gem compiled against these Rubies picked up from the build:

- mkmf links have_func probes against libruby-static.a, where Ruby's internal
  functions are linkable, so a probe for one succeeds and the gem then fails to
  load: strscan 3.1.8 on Ruby 2.4-2.7 with 'undefined symbol:
  rb_deprecate_constant'. Merge the archive into one object and localize its
  hidden symbols, leaving the exported API, as a shared libruby would.
- rbconfig's LDFLAGS and configure_args named the build's dependency
  directories, so gems got -I, -L and a run path into /work/.build. Scrub the
  run path flags, point --with-*-dir at the relocated prefix (where those
  headers and libraries now are), and remove the same run paths from the
  shipped binaries.
- The bundled ncurses looked for terminfo under the build directory, so every
  readline load printed 'Cannot read termcap database'. Use the system's.

The installation test now fails if rbconfig or a binary's run path names the
build tree, or if the static libruby still has linkable hidden symbols.
@excid3
excid3 merged commit d725f12 into main Sep 30, 2026
7 checks passed
@excid3
excid3 deleted the native-gem-hygiene branch September 30, 2026 16:19
excid3 added a commit that referenced this pull request Sep 30, 2026
Move the Rails table out of the end-of-life section and extend it to the
supported series, add a Native gems section for what #16 arranged (static
libruby's linkable symbols, rbconfig paths, terminfo), and put the sections
for people using the builds ahead of the ones for building them.
excid3 added a commit that referenced this pull request Sep 30, 2026
…g files

Merging libruby-static.a into one object (#16) made every mkmf probe link the
whole of Ruby with its debug info: ~900 MB of linker memory per have_func on
3.4, which got four-way parallel gem installs OOM-killed in a 2 GB container,
and a killed link reads as 'function missing' (json 3.0.2 then failed to
compile). Nothing debugs through this archive; stripped, a probe takes ~50 MB
(the published 2.17 builds took ~270 MB) and the archive shrinks from 156 MB to
15 MB. The installation test fails if it has debug sections again.

The pkg-config files copied from the dependencies and Ruby's ruby-X.pc named the
build tree; point them at ${pcfiledir}/../.. like the rest, and check it.
Also blank rbconfig's RUST_LIB, a build-tree path only Ruby's own build uses.
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