Skip to content

fix(linux): give Ghostscript a clean library path in the frozen build - #211

Merged
nelsonduarte merged 1 commit into
mainfrom
fix/linux-frozen-subprocess-env
Oct 17, 2026
Merged

nelsonduarte merged 1 commit into
mainfrom
fix/linux-frozen-subprocess-env

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

Step 1 of keeping the frozen Linux build's bundled libraries away from the system tools it starts. This step covers Ghostscript in Compress.

On Linux the PyInstaller one-file bootloader prepends its extraction directory (sys._MEIPASS, /tmp/_MEI...) to LD_LIBRARY_PATH and keeps the previous value, if there was one, in LD_LIBRARY_PATH_ORIG. Every child inherits that, so the system gs started by Compress resolved its shared libraries against the bundle first. The new module docstring records the measurement: with the v1.15.1 PDFApps-Linux.tar.gz on Ubuntu 26.04 the system gs 10.06.0 loaded the bundle's libstdc++.so.6, exited 1 with "GLIBCXX_3.4.32 not found", and Compress dropped its Ghostscript pass without a message.

Changes

  • app/_subprocess_env.py (new, no Qt and no app.* imports): system_tool_env(environ=None, *, frozen=None, platform=None, meipass=None).
    • Returns None (inherit unchanged) outside a frozen build, on any platform other than Linux, with no _MEIPASS, or when there is nothing to undo.
    • Otherwise returns a copy of the environment with the bundle taken out of LD_LIBRARY_PATH: when the first entry is inside _MEIPASS and LD_LIBRARY_PATH_ORIG exists, the value from before launch is put back, even when empty; otherwise every entry inside _MEIPASS is dropped, and the variable is removed when nothing is left. LD_LIBRARY_PATH_ORIG is removed in both cases.
    • A sibling directory such as /tmp/_MEIabcd is not treated as inside /tmp/_MEIabc. Neither the input mapping nor os.environ is modified.
  • app/_gs_paths.py: an environ argument goes through _gs_lib_dirs and _derive_gs_lib_dirs down to _gs_version, so the gs --version probe runs with the same environment as the gs child. A probe run with the inherited LD_LIBRARY_PATH would fail and stay cached for the session.
  • app/utils.py (_compress_pdf, pass A): base_env = system_tool_env(), then GS_LIB on top of it with _gs_subprocess_env(gs, environ=base_env), falling back to base_env when there is no GS_LIB to add. The imports of both helpers move out of the try block, so an ImportError is no longer swallowed by its except Exception.

Tests

  • tests/test_subprocess_env.py (new): platforms, restore from _ORIG, sibling directories, path normalisation, no mutation, defaults read from sys at call time, and an import allowlist for the module.
  • tests/test_gs_lib_paths.py: the version probe inherits by default and otherwise gets the child's environment; pass A builds GS_LIB on the system-tool environment and still passes that environment when there is no GS_LIB; a missing helper module raises instead of being swallowed.
  • Full suite at verification time: 1057 passed.

Not in this PR (later steps)

  • the tesseract calls;
  • xdg-open in app/base.py;
  • CONTRIBUTING;
  • the relaunch on a language change, which hands the old _MEI directory on in LD_LIBRARY_PATH_ORIG;
  • the test for an empty meipass.

🤖 Generated with Claude Code

On Linux the PyInstaller one-file bootloader prepends its extraction
directory (sys._MEIPASS, /tmp/_MEI...) to LD_LIBRARY_PATH and keeps the
previous value, if there was one, in LD_LIBRARY_PATH_ORIG. Every child
inherits that, so the system gs started by Compress resolved its shared
libraries against the bundle first. The new module docstring records the
measurement: with the v1.15.1 PDFApps-Linux.tar.gz on Ubuntu 26.04 the
system gs 10.06.0 loaded the bundle's libstdc++.so.6, exited 1 with
"GLIBCXX_3.4.32 not found", and Compress dropped its Ghostscript pass
without a message.

app/_subprocess_env.py (new, no Qt and no app.* imports) adds
system_tool_env(environ=None, *, frozen=None, platform=None,
meipass=None). It returns None (inherit unchanged) outside a frozen
build, on any platform other than Linux, with no _MEIPASS, or when there
is nothing to undo. Otherwise it returns a copy of the environment with
the bundle taken out of LD_LIBRARY_PATH: when the first entry is inside
_MEIPASS and LD_LIBRARY_PATH_ORIG exists, the value from before launch
is put back, even when empty; otherwise every entry inside _MEIPASS is
dropped and the variable removed when nothing is left. LD_LIBRARY_PATH_ORIG
is removed in both cases. A sibling directory such as /tmp/_MEIabcd is
not treated as inside /tmp/_MEIabc. Neither the input mapping nor
os.environ is modified.

app/_gs_paths.py threads an environ argument through _gs_lib_dirs and
_derive_gs_lib_dirs down to _gs_version, so the "gs --version" probe runs
with the same environment as the gs child; a probe run with the
inherited LD_LIBRARY_PATH would fail and stay cached for the session.

app/utils.py (_compress_pdf, pass A) builds the gs environment in two
layers: base_env = system_tool_env(), then GS_LIB on top of it with
_gs_subprocess_env(gs, environ=base_env), falling back to base_env when
there is no GS_LIB to add. The imports of both helpers move out of the
try block, so an ImportError is no longer swallowed by its except
Exception and pass A cannot be skipped silently for that reason.

Tests: tests/test_subprocess_env.py covers the helper (platforms,
restore from _ORIG, sibling directories, path normalisation, no
mutation, defaults read from sys at call time, and an import allowlist).
tests/test_gs_lib_paths.py checks that the version probe inherits by
default and otherwise gets the child's environment, that pass A builds
GS_LIB on the system-tool environment and still passes that environment
when there is no GS_LIB, and that a missing helper module raises instead
of being swallowed. Full suite at verification time: 1057 passed.

This is step 1. Still to do: the tesseract calls, xdg-open in base.py,
CONTRIBUTING, the relaunch on a language change (which hands the old
_MEI directory on in LD_LIBRARY_PATH_ORIG), and the test for an empty
meipass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nelsonduarte
nelsonduarte merged commit 8cf4bf0 into main Oct 17, 2026
5 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