[nanvix] E: Add cc-wrapper for -shared vs exe link mode#687
Open
esaurez wants to merge 1 commit into
Open
Conversation
Adds a small bash wrapper that sits in front of the real `i686-nanvix-gcc` and `i686-nanvix-g++` driver binaries in the toolchain-python docker image. The wrapper detects whether the invocation is producing an executable or a shared library and applies the correct linker flags for each case.
This mirrors the well-established `emcc` pattern used by emscripten and Pyodide, where the compiler-driver wrapper is what knows the difference between main-module and side-module builds, so consumers (cpython's Makefile.nanvix, future numpy / scipy / lxml meson builds, etc.) can use a single `LDFLAGS` env var without needing to encode the exe-vs-shared distinction themselves.
## Why this exists
`Makefile.nanvix` sets a single `LDFLAGS` on `./configure` that contains executable-specific linker flags:
- `-T <sysroot>/lib/user.ld` — executable layout script.
- `-no-pie` — disable PIE for executables.
- `-Wl,--no-dynamic-linker` — executable-only marker.
- `-Wl,--export-dynamic` — populate the main executable's `.dynsym`.
cpython propagates that same `LDFLAGS` to BOTH the main `python.elf` link AND every extension-module `.so` link via `PY_LDFLAGS`. For `.so` outputs these exe-only flags are wrong:
- `-T user.ld` tells `ld` to use an executable layout. When applied to a `-shared` link, `ld` treats the output as an exe and rejects undefined symbols, even those that should resolve at `dlopen()` time against the main exe's `.dynsym` (the Python C API symbols every extension references).
- `-no-pie` is incompatible with `-shared` (shared libraries must be PIC).
- `-Wl,--no-dynamic-linker` is meaningless for `.so`.
- `-Wl,--export-dynamic` is exe-only.
There is no clean place in cpython 3.12's `Modules/Setup` system to express "use this LDFLAGS for the exe link and that LDFLAGS for shared modules". cpython expects a single `LDFLAGS` shared between both.
The cleanest fix is to install a compiler-driver wrapper that does the split transparently: forwards exe builds unchanged, and strips exe-only flags + adds `-fPIC` for shared builds. That way `LDFLAGS` stays the same for everyone and the wrapper does the right thing per-invocation.
## What changed in `.nanvix/docker/`
- **New file `cc-wrapper.sh`** (~110 lines bash). Detects compile-only mode (`-c` / `-S` / `-E`), exe-link mode (no `-shared`), or shared-link mode (`-shared` present). Compile-only and exe modes forward to the real binary unchanged via `exec "$real_bin" "$@"`. Shared mode iterates the args and:
- Strips `-T <script>` (both `-T <script>` and `-T<script>` forms), `-no-pie`, `-Wl,--no-dynamic-linker`, `-Wl,--export-dynamic`, `-Wl,-T,<script>`, and bare `*.ld` argument files.
- Adds `-fPIC` if not already present.
- Uses a bash `filtered=()` array (not a string) so argv boundaries and quoting are preserved across the rewrite — `exec "$real_bin" "${filtered[@]}"` is argv-preserving for arguments containing spaces, empty strings, or glob metacharacters.
- **Dockerfile updated** to install the wrapper. The script is copied to `/opt/nanvix/bin/i686-nanvix-cc-wrapper.sh`, made executable, and the real `i686-nanvix-gcc` / `i686-nanvix-g++` binaries are renamed to `<name>.real`. Symlinks `<name>` -> `i686-nanvix-cc-wrapper.sh` are then installed. The wrapper picks the right `.real` binary based on `argv[0]`.
The install pattern is defensive: if the upstream base image already has a wrapper symlinked (some local-build flavours of `toolchain-python` carry an earlier wrapper), the symlink is removed and replaced — but only after asserting that the matching `<name>.real` file already exists, so a missing `.real` aborts the install rather than stranding the toolchain. If the real binary has not yet been renamed, it is moved to `<name>.real` in the same step.
## Validation
Built the image locally with the additions and smoke-tested all three wrapper modes against a trivial `int main(){return 0;}`:
- **Test 1 (exe link, no `-shared`):** wrapper transparent; real gcc handles normally.
- **Test 2 (simple `-shared`):** wrapper detects shared mode, adds `-fPIC`, link succeeds.
- **Test 3 (`-shared` plus the toxic exe-only flags `-T <script>` `-no-pie` `-Wl,--no-dynamic-linker`):** wrapper strips the toxic flags and the link succeeds with no spurious "cannot create executables" error.
Build-side end-to-end smoke test with cpython: applied a Phase-0-style change to `Modules/Setup.local` adding `*shared*\narray arraymodule.c`. With the wrapper in place, `array.cpython-312-i686-nanvix.so` (~190 KB) is produced and installed at the canonical `lib/python3.12/lib-dynload/` location; `PyInit_array` no longer appears in `python.elf`. Full `./z build` succeeds.
## What this unblocks
This wrapper is the foundational prerequisite for the broader `.a` -> `.so` migration documented in `nanvix-todo/cpython-static-to-shared-migration.md`. Every subsequent phase of that plan (peeling stdlib extension modules off `python.elf` and shipping them as `.so` files loaded via dlopen, exactly as upstream CPython works) depends on this wrapper being in place. Without it, every per-module conversion would need a per-call LDFLAGS hack.
## Backward compatibility
This change is purely additive at the image level — it adds the wrapper script and rewrites the gcc/g++ entry points to point at it. Existing build paths that don't pass `-shared` see no behavioural change (the wrapper falls through to the real binary unchanged). Existing `.so` build paths that DID work before (e.g., when LDFLAGS happened not to include `-T user.ld`) continue to work because the wrapper's strip-then-add-fPIC produces a strict superset of what bare `gcc -shared` would have done.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a compiler-driver wrapper to the Nanvix toolchain-python Docker image so the same LDFLAGS can be used for both executable links and -shared extension-module links, while automatically stripping executable-only flags for .so builds.
Changes:
- Add a bash
cc-wrapper.shthat detects-sharedlinks and rewrites argv to remove exe-only linker flags and inject-fPIC. - Update the Nanvix Docker image to install the wrapper and interpose it in front of
i686-nanvix-gcc/i686-nanvix-g++by renaming the real drivers to*.realand symlinking the tool names to the wrapper.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| .nanvix/docker/Dockerfile | Installs the wrapper into the image and rewires gcc/g++ entrypoints to go through it. |
| .nanvix/docker/cc-wrapper.sh | Implements -shared vs exe/compile-only mode detection and argument filtering for shared-library links. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+54
to
+57
| # Find the real binary by appending `.real` to argv[0]'s basename. | ||
| self_dir="$(dirname "$0")" | ||
| self_name="$(basename "$0")" | ||
| real_bin="${self_dir}/${self_name}.real" |
Comment on lines
+16
to
+18
| # Install the cc-wrapper. This wrapper sits in front of the real | ||
| # `i686-nanvix-gcc` / `i686-nanvix-g++` driver binaries and detects | ||
| # whether the invocation is producing an executable or a shared library. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[nanvix] E: Add cc-wrapper for -shared vs exe link mode
Adds a small bash wrapper that sits in front of the real
i686-nanvix-gccandi686-nanvix-g++driver binaries in the toolchain-python docker image. The wrapper detects whether the invocation is producing an executable or a shared library and applies the correct linker flags for each case.This mirrors the well-established
emccpattern used by emscripten and Pyodide, where the compiler-driver wrapper is what knows the difference between main-module and side-module builds, so consumers (cpython's Makefile.nanvix, future numpy / scipy / lxml meson builds, etc.) can use a singleLDFLAGSenv var without needing to encode the exe-vs-shared distinction themselves.Why this exists
Makefile.nanvixsets a singleLDFLAGSon./configurethat contains executable-specific linker flags:-T <sysroot>/lib/user.ld— executable layout script.-no-pie— disable PIE for executables.-Wl,--no-dynamic-linker— executable-only marker.-Wl,--export-dynamic— populate the main executable's.dynsym.cpython propagates that same
LDFLAGSto BOTH the mainpython.elflink AND every extension-module.solink viaPY_LDFLAGS. For.sooutputs these exe-only flags are wrong:-T user.ldtellsldto use an executable layout. When applied to a-sharedlink,ldtreats the output as an exe and rejects undefined symbols, even those that should resolve atdlopen()time against the main exe's.dynsym(the Python C API symbols every extension references).-no-pieis incompatible with-shared(shared libraries must be PIC).-Wl,--no-dynamic-linkeris meaningless for.so.-Wl,--export-dynamicis exe-only.There is no clean place in cpython 3.12's
Modules/Setupsystem to express "use this LDFLAGS for the exe link and that LDFLAGS for shared modules". cpython expects a singleLDFLAGSshared between both.The cleanest fix is to install a compiler-driver wrapper that does the split transparently: forwards exe builds unchanged, and strips exe-only flags + adds
-fPICfor shared builds. That wayLDFLAGSstays the same for everyone and the wrapper does the right thing per-invocation.What changed in
.nanvix/docker/cc-wrapper.sh(~110 lines bash). Detects compile-only mode (-c/-S/-E), exe-link mode (no-shared), or shared-link mode (-sharedpresent). Compile-only and exe modes forward to the real binary unchanged viaexec "$real_bin" "$@". Shared mode iterates the args and:-T <script>(both-T <script>and-T<script>forms),-no-pie,-Wl,--no-dynamic-linker,-Wl,--export-dynamic,-Wl,-T,<script>, and bare*.ldargument files.-fPICif not already present.filtered=()array (not a string) so argv boundaries and quoting are preserved across the rewrite —exec "$real_bin" "${filtered[@]}"is argv-preserving for arguments containing spaces, empty strings, or glob metacharacters./opt/nanvix/bin/i686-nanvix-cc-wrapper.sh, made executable, and the reali686-nanvix-gcc/i686-nanvix-g++binaries are renamed to<name>.real. Symlinks<name>->i686-nanvix-cc-wrapper.share then installed. The wrapper picks the right.realbinary based onargv[0].The install pattern is defensive: if the upstream base image already has a wrapper symlinked (some local-build flavours of
toolchain-pythoncarry an earlier wrapper), the symlink is removed and replaced — but only after asserting that the matching<name>.realfile already exists, so a missing.realaborts the install rather than stranding the toolchain. If the real binary has not yet been renamed, it is moved to<name>.realin the same step.Validation
Built the image locally with the additions and smoke-tested all three wrapper modes against a trivial
int main(){return 0;}:-shared): wrapper transparent; real gcc handles normally.-shared): wrapper detects shared mode, adds-fPIC, link succeeds.-sharedplus the toxic exe-only flags-T <script>-no-pie-Wl,--no-dynamic-linker): wrapper strips the toxic flags and the link succeeds with no spurious "cannot create executables" error.Build-side end-to-end smoke test with cpython: applied a Phase-0-style change to
Modules/Setup.localadding*shared*\narray arraymodule.c. With the wrapper in place,array.cpython-312-i686-nanvix.so(~190 KB) is produced and installed at the canonicallib/python3.12/lib-dynload/location;PyInit_arrayno longer appears inpython.elf. Full./z buildsucceeds.What this unblocks
This wrapper is the foundational prerequisite for the broader
.a->.somigration documented innanvix-todo/cpython-static-to-shared-migration.md. Every subsequent phase of that plan (peeling stdlib extension modules offpython.elfand shipping them as.sofiles loaded via dlopen, exactly as upstream CPython works) depends on this wrapper being in place. Without it, every per-module conversion would need a per-call LDFLAGS hack.Backward compatibility
This change is purely additive at the image level — it adds the wrapper script and rewrites the gcc/g++ entry points to point at it. Existing build paths that don't pass
-sharedsee no behavioural change (the wrapper falls through to the real binary unchanged). Existing.sobuild paths that DID work before (e.g., when LDFLAGS happened not to include-T user.ld) continue to work because the wrapper's strip-then-add-fPIC produces a strict superset of what baregcc -sharedwould have done.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com