Skip to content

build: force -fno-lto for the bundled libopus build#21

Closed
UMCEKO wants to merge 2 commits into
DoumanAsh:masterfrom
UMCEKO:fix-slim-lto-objects
Closed

build: force -fno-lto for the bundled libopus build#21
UMCEKO wants to merge 2 commits into
DoumanAsh:masterfrom
UMCEKO:fix-slim-lto-objects

Conversation

@UMCEKO
Copy link
Copy Markdown

@UMCEKO UMCEKO commented May 1, 2026

Summary

When opusic-sys is built with the bundled feature (default) and the
ambient CFLAGS contain -flto* — common on distros that enable LTO by
default for package builds, e.g. Arch's makepkg with OPTIONS=(lto)
appending LTOFLAGS="-flto=auto", Gentoo's LTOFLAGS, etc. — the
final link of any binary that pulls opusic-sys transitively fails
with:

rust-lld: error: undefined symbol: opus_decoder_destroy
rust-lld: error: undefined symbol: opus_decode
rust-lld: error: undefined symbol: opus_decoder_create

even though libopus.a is built and bundled into libopusic_sys.rlib.

Root cause

With -flto in CFLAGS, GCC produces slim LTO objects when
compiling the libopus sources:

  • .text is empty (size 0).
  • The actual code lives in .gnu.lto_* sections.
  • Each object carries a __gnu_lto_slim marker.

rustc bundles those .o files into the rlib (cargo's default +bundle
modifier on cargo:rustc-link-lib=static=opus). When a downstream
binary links, rust-lld processes the rlib without an LTO plugin loaded,
sees no native code in .text, and the symbols look undefined.

readelf -SW opus_decoder.c.o on a failing build:

[ 1] .text             PROGBITS … size 000000      <- empty
[ 4] .gnu.lto_.profile PROGBITS …
[ 5] .gnu.lto_.icf     PROGBITS …
…
.symtab: __gnu_lto_slim    OBJECT GLOBAL DEFAULT COM

The bundled libopus.a is consumed by rustc, not a C linker, so doing
LTO at the C level gains nothing in this build path — it only ever
breaks it.

Fix

Append -fno-lto to the cmake C flags. gcc/clang honor the last
-flto* argument on the command line, so this overrides any inherited
-flto. It is a no-op for compilers that don't recognize it.

After the fix, opus_decoder.c.o has real native code in
.text.opus_decoder_create and the link succeeds.

Reproducer

On Arch (or any host with -flto* in CFLAGS), build any crate that
links opusic-sys transitively (e.g. symphonia-adapter-libopus):

LDFLAGS="-Wl,-O1 -Wl,--as-needed -Wl,-z,relro -Wl,-z,now" \
CFLAGS="-O3 -fno-plt -flto=auto" \
cargo build --release

Without this patch: rust-lld: error: undefined symbol: opus_decoder_*.
With this patch: links cleanly. Verified locally against
kopuz (uses symphonia-adapter-libopus → opusic-sys) on
rustc 1.95.0 with rust-lld.

Test plan

  • Build fails on Arch with OPTIONS=(lto) before the patch (reproduced)
  • Build succeeds on Arch with OPTIONS=(lto) after the patch (verified end-to-end binary contains opus_decoder_create)
  • readelf confirms post-fix .o files are normal ELF, no .gnu.lto_* sections, no __gnu_lto_slim marker
  • CI: existing tests/version.rs should still pass (compiler had no -flto in its baseline CFLAGS, so no behavioral change expected)

The bundled `libopus.a` is consumed by rustc (objects bundled into the
rlib via cargo's `+bundle` modifier), not by a C linker, so C-level LTO
gains nothing here. When ambient CFLAGS contain `-flto` (e.g. Arch's
`makepkg` with `OPTIONS=(lto)`, Gentoo `LTOFLAGS`, distros enabling it
by default), GCC produces "slim LTO" objects: empty `.text`, code only
in `.gnu.lto_*` sections, marked with `__gnu_lto_slim`. rust-lld then
links the rlib without an LTO plugin and reports `opus_decoder_create`,
`opus_decoder_destroy`, and `opus_decode` as undefined.

Repro on Arch with `makepkg`-set CFLAGS containing `-flto=auto`:

    rust-lld: error: undefined symbol: opus_decoder_destroy
        >>> referenced by symphonia_adapter_libopus...

Append `-fno-lto` to the cmake C flags so libopus is always built with
real native code regardless of inherited flags. gcc/clang honor the
last `-flto*` argument, so this overrides any earlier `-flto`, and is a
no-op for compilers that don't recognize it.
@DoumanAsh
Copy link
Copy Markdown
Owner

Thanks, I wasn't aware of it
On fedora it built just fine for me 😓

Can you please add - build.rs here in order to trigger build
https://github.com/DoumanAsh/opusic-sys/blob/master/.github/workflows/rust.yml#L12
https://github.com/DoumanAsh/opusic-sys/blob/master/.github/workflows/rust.yml#L22

@DoumanAsh
Copy link
Copy Markdown
Owner

DoumanAsh commented May 2, 2026

@UMCEKO Can you please also test build on your system with cmake flag CMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF ?

Nevermind, it does nothing when off
I will do some direct modifications to CMake to make this custom CFLAGs only on gcc/clang just in case

@DoumanAsh
Copy link
Copy Markdown
Owner

DoumanAsh commented May 2, 2026

@UMCEKO I created PR with a more fine grained solution to add flag only when it is requested (I'm pretty sure MSVC can complain if you try to add unknown flags)
#22

P.s. for whatever reason I cannot seem to reproduce it on Fedora

@UMCEKO
Copy link
Copy Markdown
Author

UMCEKO commented May 2, 2026

#22 looks great — guarding on -flto in CFLAGS/CXXFLAGS is cleaner than my unconditional flag, and the decoder test is a nice catch (would've surfaced this in CI from the start). happy to close this in favor of yours.

re: not reproducing on Fedora — fwiw Fedora's default %build_cflags doesn't include -flto, that's why. Arch flips it on via OPTIONS=(lto) in /etc/makepkg.conf which appends -flto=auto, and that's the trigger. anyone on Fedora who passes CFLAGS=-flto explicitly should hit the same thing.

@DoumanAsh
Copy link
Copy Markdown
Owner

re: not reproducing on Fedora — fwiw Fedora's default %build_cflags doesn't include -flto, that's why. Arch flips it on via OPTIONS=(lto) in /etc/makepkg.conf which appends -flto=auto, and that's the trigger. anyone on Fedora who passes CFLAGS=-flto explicitly should hit the same thing.

No, what I mean is that I set LDFLAGS and CFLAGS as yours, but cargo test --release links with these symbols with no issues...

I think it would be the best to make dedicated docker image with one of the systems where you encounter issues so that I can add workflow to test for this situation

But for now, I would appreciate if you could test #22 so that I merge it and release new version of opusic-sys today to unblock you.
Later I will work on creating dedicated workflow to test this condition

@DoumanAsh
Copy link
Copy Markdown
Owner

Fixed in #22

@DoumanAsh DoumanAsh closed this May 2, 2026
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.

2 participants