Skip to content

Find LibreOffice's bundled Python inside a macOS app bundle - #139

Open
kungsamu-ldu wants to merge 1 commit into
harumiWeb:mainfrom
kungsamu-ldu:fix/macos-libreoffice-python-discovery
Open

Find LibreOffice's bundled Python inside a macOS app bundle#139
kungsamu-ldu wants to merge 1 commit into
harumiWeb:mainfrom
kungsamu-ldu:fix/macos-libreoffice-python-discovery

Conversation

@kungsamu-ldu

@kungsamu-ldu kungsamu-ldu commented Sep 12, 2026

Copy link
Copy Markdown

The bug

_soffice_program_dirs only ever looks in soffice's own directory. That holds on Windows and Linux, but a macOS app bundle keeps soffice in Contents/MacOS and the bundled Python in the sibling Contents/Resources, so discovery never finds it:

$ exstruct sample/basic/sample.xlsx --mode libreoffice
[libreoffice_unavailable] LibreOffice runtime is unavailable.
  (LibreOfficeUnavailableError('LibreOffice runtime is unavailable:
   compatible Python runtime was not found.'))
/Applications/LibreOffice.app/Contents/MacOS/soffice        <- _which_soffice finds this
/Applications/LibreOffice.app/Contents/Resources/python     <- bundled Python lives here

Homebrew compounds it: the soffice it puts on PATH is a bash wrapper that execs into the app bundle, so shutil.which resolves to a Caskroom command-wrappers directory outside the bundle entirely — hence the fallback to the standard bundle location as well.

After the change, discovery succeeds from a bare PATH lookup:

>>> p = _which_soffice()
>>> _soffice_program_dirs(p)
('/opt/homebrew/Caskroom/libreoffice/26.2.5/.homebrew-command-wrappers',
 '/Applications/LibreOffice.app/Contents/Resources')
>>> _resolve_python_path(p)
PosixPath('/Applications/LibreOffice.app/Contents/Resources/python')

Please read this before merging

This fixes discovery only — it does not make --mode libreoffice work on macOS. I want to be straight about that rather than have it look like a working path.

Once found, the bundled interpreter still cannot be spawned from outside LibreOffice. It dies instantly, and the crash report gives the reason:

exception:   {'type': 'EXC_CRASH', 'signal': 'SIGKILL (Code Signature Invalid)'}
termination: {"namespace": "CODESIGNING", "code": 4,
              "indicator": "Launch Constraint Violation"}

That is a macOS launch constraint, not a packaging accident on my machine. I ruled the alternatives out: the signature verifies (codesign -v clean), there is no com.apple.quarantine attribute, the binary is arm64 on an arm64 host, and it reproduces identically on a freshly reinstalled LibreOffice 26.2.5. soffice itself runs fine — only direct invocation of the nested Python is refused.

_system_python_candidates is the remaining fallback, but a Homebrew LibreOffice ships no uno module for a system Python, so it finds nothing either.

So: this patch turns a silent mis-detection into a correct one, which seemed worth having on its own, and the tests pin the platform layout. But if you would rather gate the mode off on darwin with a clear message than let it get one step further and fail elsewhere, say so and I will close this — or rework it that way, whichever you prefer.

Tests

tests/core/test_libreoffice_macos_paths.py builds a fake bundle under tmp_path and covers the sibling-Resources case, the Homebrew-wrapper fallback, and that non-darwin behaviour is unchanged. All three patch sys.platform, so they run anywhere.

927 tests pass (3 new), mypy --strict and ruff check clean.

Summary by CodeRabbit

  • Bug Fixes
    • Improved LibreOffice integration on macOS by reliably locating bundled Python resources.
    • Added support for LibreOffice installations using standard application bundles and Homebrew wrappers.
    • Preserved platform-specific behavior so Linux installations are unaffected.

Correction (added after further investigation)

Two things in the note above are wrong, and the second one matters a lot. Sorry for the noise — I would rather correct it in place than leave it standing.

1. Version. This reproduces on 26.8.0.3, not 26.2.5. A brew reinstall --cask libreoffice moved the install from 26.2.5 to 26.8.0.3 (and the bundled Python from 3.12 to 3.13) mid-investigation; the constraint is present in both.

2. "The mode remains unavailable there" is not true — there is a working route on macOS, and I have run it.

First, the root cause, which I could not identify before. It is a parent launch constraint that LibreOffice itself declares, slot 9 of the signature superblob (magic fade8181). Decoded DER:

{ ccat: 0, comp: 1,
  reqs: { signing-identifier: "org.libreoffice.script",
          team-identifier:    "7P5S3ZLCN7" },
  vers: 1 }

So the immediate parent must be a TDF-signed binary with identifier org.libreoffice.script — i.e. soffice. That is why codesign -d --verbose=6 shows nothing useful; at every verbosity it only prints "Has Parent Launch Constraints", and the detail has to be read out of the raw LC_CODE_SIGNATURE blob. The same constraint is on bin/python3.13, MacOS/uno, gengal, opencltest, regview and xpdfimport — but not on soffice or unopkg.

It arrived deliberately: commit 22ab2bec "mac: add parent launch-constraint to packaged framework/helpers except for unopkg" (2025-05-27), with tdf#167080 (2025-07-04, backported to libreoffice-25-2) fixing the plist so it actually enforces. Every TDF-signed release since then carries it, so the official dmg and both casks behave identically. Nothing is misconfigured on my machine.

The route that works: run the bridge inside soffice via the Python script provider, rather than exec'ing the bundled interpreter. Launch constraints gate exec, not dlopenlibpythonloaderlo.dylib dlopens libpyuno.dylib and the framework's libpython straight into soffice, so the constraint never applies.

I put _libreoffice_bridge.py in a throwaway profile's user/Scripts/python/, changed only _resolve_context() (_libreoffice_bridge.py:219-238) to uno.getComponentContext(), and left _load_document, _extract_draw_page_payload, _extract_chart_payload and _close_document (:241-355) untouched:

$ soffice --headless --nologo --norestore --nolockcheck \
    -env:UserInstallation=file://<tmp-profile> \
    'vnd.sun.star.script:exbridge.py$run_in_office?language=Python&location=user'

Full draw-page payload, ~3 s including launch — 33 shapes on sample/flowchart/sample-shape-connector.xlsx, connectors flagged, sys.executable == /Applications/LibreOffice.app/Contents/MacOS/soffice, Python 3.13.15.

One design consequence: a forwarded script URL does not execute. If a listener is already up, a second soffice ... 'vnd.sun.star.script:...' returns 0 immediately and the provider never sees a getScript (plain --convert-to forwards fine, so it is specific to script URLs). So the persistent --accept listener at libreoffice.py:593-616 would have to give way to one headless soffice per workbook — roughly 3 s per file instead of 3 s per session — with parameters passed via a request file rather than env vars.

Where that leaves this PR. The discovery fix here is still correct on its own terms, but it repairs _resolve_python_path() — the very function a proper macOS fix would delete. So I am happy for you to close this, and I would rather you did than merge something that entrenches the wrong design. If you would like the script-provider route as a real PR instead, say the word and I will put one together; if you would rather gate the mode off on darwin with a clear message, that is a smaller change and I can do that too. Your call — I do not want to guess at the architecture you want.

_soffice_program_dirs only ever looks in soffice's own directory. That
holds on Windows and Linux, but a macOS app bundle keeps soffice in
Contents/MacOS and the bundled Python in the sibling Contents/Resources,
so discovery never finds it and --mode libreoffice fails with:

    LibreOffice runtime is unavailable: compatible Python runtime was
    not found.

Homebrew compounds this: its soffice on PATH is a bash wrapper that
execs into the app bundle, so shutil.which resolves to a Caskroom
command-wrappers directory outside the bundle entirely. Hence also
falling back to the standard bundle location.

Note this fixes discovery only. On current macOS the bundled interpreter
still cannot be spawned from outside LibreOffice -- it terminates with
SIGKILL (Code Signature Invalid), namespace CODESIGNING, indicator
"Launch Constraint Violation", with a valid signature and no quarantine
attribute, on a freshly reinstalled 26.2.5. So the mode remains
unavailable there unless a system Python with a working uno module is
present, which a Homebrew install does not provide. Worth knowing before
anyone spends time on the surrounding code path; happy to drop this if
you would rather gate the mode off on darwin instead.
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 32c5153e-fdc4-4265-be03-ea346aee8a1e

📥 Commits

Reviewing files that changed from the base of the PR and between 92bc120 and 58bef80.

📒 Files selected for processing (2)
  • src/exstruct/core/libreoffice.py
  • tests/core/test_libreoffice_macos_paths.py

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change extends macOS LibreOffice discovery to include bundled Resources directories. New tests validate bundle resolution, Homebrew fallback behavior, and Linux behavior.

Changes

macOS LibreOffice bundle discovery

Layer / File(s) Summary
Bundle resource path resolution
src/exstruct/core/libreoffice.py
Adds the standard macOS LibreOffice bundle path and includes sibling Contents/Resources directories for MacOS candidates on Darwin.
Platform-specific path validation
tests/core/test_libreoffice_macos_paths.py
Tests bundled Python discovery, Homebrew wrapper fallback, and exclusion of macOS Resources paths on Linux.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 58bef

The macOS discovery change is covered for the intended bundle and Homebrew wrapper layouts without altering non-macOS resolution.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description provides detailed scope, motivation, technical limitations, and test results. However, it does not follow the repository template: it omits the required Summary and Acceptance Criteria… Rewrite the description using the repository template. Add the Summary section with scope, motivation, and related issue/spec information. Address each Acceptance Criteria item or explain why it is not applicable. Add the required Validatio…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: discovering LibreOffice's bundled Python inside a macOS app bundle.
Full details: Description check

Explanation

The description provides detailed scope, motivation, technical limitations, and test results. However, it does not follow the repository template: it omits the required Summary and Acceptance Criteria sections, does not address the listed criteria, and does not include the required validation checklist or documentation updates.

Resolution

Rewrite the description using the repository template. Add the Summary section with scope, motivation, and related issue/spec information. Address each Acceptance Criteria item or explain why it is not applicable. Add the required Validation checklist, including precommit status, changed-behavior tests, and documentation status. If the template is unrelated to this PR, update the repository template before merging.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity · 0 duplication

Metric Results
Complexity 5
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

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