qtwebengine-linker-shebang-fix¶
Overlays
A nixpkgs overlay that fixes a qtwebengine 6.11+ build failure inside the Nix
build sandbox.
The problem¶
Building qtwebengine (directly, or transitively via anything that pulls it in
— Plasma, qutebrowser, pyside6, …) fails during linking:
/build/.../linker_ulimit.sh: /bin/bash: bad interpreter: No such file or directory
... exited with code 126
cmake generates build/linker_ulimit.sh with a hardcoded #!/bin/bash
shebang. The Nix build sandbox has no /bin/bash, so the moment the link step
tries to run that script it dies with exit 126 ("bad interpreter").
Why the obvious fixes don't work¶
patchShebangsinpatchPhaseis too early.linker_ulimit.shdoes not exist yet — cmake only emits it after configure runs. ApatchShebangscall inpatchPhase/postPatchmatches the.sh.intemplate at best, and nothing at worst.patchShebangscan't befind -exec'd. It's a shell function, not a binary, so you can't discover the generated file later and hand it topatchShebangsviafind -exec. You have to rewrite the shebang line yourself withsed.
So the overlay rewrites the shebang in two phases:
| Phase | Target | When it exists |
|---|---|---|
postPatch |
linker_ulimit.sh.in template |
before cmake configure |
preBuild |
generated linker_ulimit.sh |
after cmake configure |
Each phase does a sed -i "1s|^#!.*bash.*|#!$(command -v bash)|" on the first
line only, pointing the shebang at the sandbox's real bash.
The subtle trap: two paths to qtwebengine¶
qtwebengine is reached in more than one way, and a naive overrideScope
patch only fixes one of them:
- Top-level
qt6.qtwebengine— used by Plasma,qutebrowser, etc.qt6.overrideScope patchQtwebenginecovers this. - Rebound scope
(qt6.override { python3 = ...; }).qtwebengine—pyside6(and anything that rebinds Qt to a specific Python) callsqt6.override, which produces a fresh package set that youroverrideScopenever touched.
To catch both, the overlay wraps qt6.override so every overridden scope is
also run through overrideScope patchQtwebengine:
If you only patch the top level, pyside6 (and friends) will still fail to
build with the same exit-126 error.
Usage¶
Add the overlay to your nixpkgs overlay list.
NixOS module:
Flake / plain import <nixpkgs>:
pkgs = import nixpkgs {
inherit system;
overlays = [ (import ./overlays/qtwebengine-linker-shebang-fix) ];
};
No options to configure — it's a pure overlay.
Caveats¶
- This is a workaround for a specific upstream packaging quirk. Once
nixpkgs (or Qt upstream) makes the generated linker script sandbox-safe, the
overlay becomes a harmless no-op — the
sedsimply matches nothing. Drop it when you confirmqtwebenginebuilds clean without it. - The shebang match is deliberately loose (
^#!.*bash.*) so it survives minor changes to how cmake writes the line. If a future version renames the script or drops thebashshebang entirely, revisit thefind -namepatterns. - Version-observed on
qtwebengine6.11.0; the pattern applies to any release that emitslinker_ulimit.shwith an absolute-path bash shebang.
Source¶
overlays/qtwebengine-linker-shebang-fix/default.nix
# qtwebengine-linker-shebang-fix
#
# Nixpkgs overlay that fixes a qtwebengine 6.11+ build failure inside the Nix
# sandbox: cmake generates `build/linker_ulimit.sh` with a `#!/bin/bash`
# shebang, but the sandbox has no `/bin/bash`, so the link step dies with
# exit 126 ("bad interpreter").
#
# Why not `patchShebangs`?
# - The offending script does not exist until *after* cmake configure, so a
# `patchShebangs` in `patchPhase` runs too early and matches nothing.
# - `patchShebangs` is a shell function, so it cannot be `find -exec`'d over
# files discovered later. We use `sed` on the shebang line instead.
#
# The fix rewrites the shebang in BOTH places the file appears:
# - postPatch: the `linker_ulimit.sh.in` template (before configure).
# - preBuild: the generated `linker_ulimit.sh` copy (after configure).
#
# The subtle part — patch BOTH ways qtwebengine is reached:
# - top-level `qt6.qtwebengine` (e.g. plasma, qutebrowser).
# - `(qt6.override { ... }).qtwebengine`, which pyside6 / a Python rebind
# produces. `overrideScope` only reaches the first, so we also wrap
# `qt6.override` to route the overridden scope through `overrideScope`.
#
# Usage — add to nixpkgs.overlays:
#
# nixpkgs.overlays = [ (import ./overlays/qtwebengine-linker-shebang-fix) ];
#
# or in a flake:
#
# pkgs = import nixpkgs {
# inherit system;
# overlays = [ (import ./overlays/qtwebengine-linker-shebang-fix) ];
# };
final: prev:
let
patchQtwebengine = qfinal: qprev: {
qtwebengine = qprev.qtwebengine.overrideAttrs (old: {
# Fix the template before cmake configure copies it.
postPatch = (old.postPatch or "") + ''
for f in $(find . -type f \( -name 'linker_ulimit.sh' -o -name 'linker_ulimit.sh.in' \) 2>/dev/null); do
echo "[qtwebengine-overlay] patching shebang in $f (postPatch)"
sed -i "1s|^#!.*bash.*|#!$(command -v bash)|" "$f"
done
'';
# Fix the generated copy that only exists after configure.
preBuild = (old.preBuild or "") + ''
for f in $(find . -name linker_ulimit.sh -type f 2>/dev/null); do
echo "[qtwebengine-overlay] patching shebang in $f (preBuild)"
sed -i "1s|^#!.*bash.*|#!$(command -v bash)|" "$f"
done
'';
});
};
applyPatch = q: q.overrideScope patchQtwebengine;
in
{
qt6 =
let
patched = applyPatch prev.qt6;
in
patched
// {
# Route overridden scopes (e.g. pyside6's `qt6.override { python3 = ...; }`)
# through the same patch, since `overrideScope` above does not reach them.
override = args: applyPatch (prev.qt6.override args);
};
}