Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/exstruct/core/libreoffice.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,9 @@ def _resolve_python_path(soffice_path: Path) -> Path | None:
return None


_MACOS_BUNDLE_MACOS_DIR = Path("/Applications/LibreOffice.app/Contents/MacOS")


def _soffice_program_dirs(soffice_path: Path) -> tuple[Path, ...]:
"""Return candidate LibreOffice program directories for the given ``soffice`` path."""

Expand All @@ -810,6 +813,18 @@ def _soffice_program_dirs(soffice_path: Path) -> tuple[Path, ...]:
return tuple(program_dirs)
if resolved_parent not in program_dirs:
program_dirs.append(resolved_parent)
# macOS keeps soffice in Contents/MacOS but the bundled Python in the sibling
# Contents/Resources, so the executable's own dir never finds it. Homebrew
# compounds this by putting a bash wrapper on PATH that execs into the bundle,
# leaving the resolved path outside the bundle entirely -- hence the fixed
# bundle location as a last resort.
if sys.platform == "darwin":
for parent in (*program_dirs, _MACOS_BUNDLE_MACOS_DIR):
if parent.name != "MacOS":
continue
resources_dir = parent.parent / "Resources"
if resources_dir.is_dir() and resources_dir not in program_dirs:
program_dirs.append(resources_dir)
return tuple(program_dirs)


Expand Down
63 changes: 63 additions & 0 deletions tests/core/test_libreoffice_macos_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""macOS keeps LibreOffice's bundled Python outside the soffice executable's dir."""

from __future__ import annotations

from pathlib import Path

import pytest

from exstruct.core import libreoffice


def _fake_bundle(root: Path) -> tuple[Path, Path]:
"""Build a minimal LibreOffice.app layout and return (soffice, python)."""
macos_dir = root / "LibreOffice.app" / "Contents" / "MacOS"
resources_dir = root / "LibreOffice.app" / "Contents" / "Resources"
macos_dir.mkdir(parents=True)
resources_dir.mkdir(parents=True)
soffice = macos_dir / "soffice"
python = resources_dir / "python"
soffice.write_text("#!/bin/sh\n")
python.write_text("#!/bin/sh\n")
return soffice, python


def test_program_dirs_include_the_sibling_resources_dir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(libreoffice.sys, "platform", "darwin")
soffice, python = _fake_bundle(tmp_path)

dirs = libreoffice._soffice_program_dirs(soffice)

assert python.parent in dirs
candidates = [c for d in dirs for c in libreoffice._bundled_python_candidates(d)]
assert python in candidates


def test_program_dirs_fall_back_to_the_bundle_for_a_homebrew_wrapper(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Homebrew puts a bash wrapper on PATH, so the resolved path leaves the bundle."""
monkeypatch.setattr(libreoffice.sys, "platform", "darwin")
_, python = _fake_bundle(tmp_path)
monkeypatch.setattr(
libreoffice, "_MACOS_BUNDLE_MACOS_DIR", python.parent.parent / "MacOS"
)
wrapper_dir = tmp_path / "command-wrappers"
wrapper_dir.mkdir()
wrapper = wrapper_dir / "soffice"
wrapper.write_text("#!/bin/bash\n")

dirs = libreoffice._soffice_program_dirs(wrapper)

assert python.parent in dirs


def test_program_dirs_stay_unchanged_off_macos(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(libreoffice.sys, "platform", "linux")
soffice, python = _fake_bundle(tmp_path)

assert python.parent not in libreoffice._soffice_program_dirs(soffice)
Loading