Skip to content
Merged
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
41 changes: 39 additions & 2 deletions resources_servers/gdpval/setup_libreoffice.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import annotations

import logging
import os
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -43,7 +44,11 @@
"libreoffice-java-common",
)

_APT_INSTALL_TIMEOUT_S = 600
# A cold install pulls ~500 MB. 600 s was enough for one run at a time, but a
# seven-campaign sweep has every leg re-pulling it concurrently and blew past
# it on 2026-09-12, taking the whole leg down. Raised with headroom rather
# than tuned to the observed failure.
_APT_INSTALL_TIMEOUT_S = 1800


def _run(cmd: list[str], *, timeout: int) -> tuple[int, str, str]:
Expand Down Expand Up @@ -74,6 +79,34 @@ def _java_runs() -> bool:
return rc == 0


def _javaldx_works() -> bool:
"""Return True iff libreoffice can actually find a JRE through `javaldx`.

This is the capability the v1-v3 early-exits kept failing to detect. Each
of them tested a proxy — `which("libreoffice")`, then `which("java")`, then
`java -version` — and each proxy passed on an image where `javaldx` still
could not load a JVM, so chart and formula documents produced no PDF.

`javaldx` is the bridge itself: it loads libjvm.so via JNI and prints the
JRE path it resolved. Running it answers the real question instead of an
adjacent one. On failure it prints "failed to launch" / "Could not find a
Java Runtime" and yields no path.
"""
exe = "/usr/lib/libreoffice/program/javaldx"
if not os.path.exists(exe):
# Ships in libreoffice-java-common, which the libreoffice metapackage
# does not depend on. Absent means the bridge was never installed.
return False
try:
rc, out, err = _run([exe], timeout=30)
except Exception:
return False
blob = f"{out}\n{err}".lower()
if "failed to launch" in blob or "could not find a java runtime" in blob:
return False
return rc == 0 and bool(out.strip())


def ensure_libreoffice() -> bool:
"""Make sure libreoffice + a *functional* JRE are present. ALWAYS run apt-install on Linux.

Expand All @@ -91,7 +124,7 @@ def ensure_libreoffice() -> bool:
but libreoffice's `javaldx` (which loads libjvm.so via JNI) still
can't find a usable JRE. Concrete signal: 10 `failed to launch
javaldx` errors at 35 rollouts on slurm 2621556.
v4 (this) — drop the early-exit entirely. ALWAYS run apt-update +
v4 — drop the early-exit entirely. ALWAYS run apt-update +
apt-install of the full package list. `apt-get install` is
idempotent on already-installed packages (a few seconds when
everything is present), and the only configuration that
Expand All @@ -111,6 +144,10 @@ def ensure_libreoffice() -> bool:
)
return False

if shutil.which("libreoffice") and _java_runs() and _javaldx_works():
LOGGER.info("libreoffice, a working JRE and javaldx are already present; skipping apt-get.")
return True

if not shutil.which("apt-get"):
LOGGER.warning("apt-get is unavailable; GDPVal preconvert will be a no-op.")
return False
Expand Down
45 changes: 44 additions & 1 deletion resources_servers/gdpval/tests/test_setup_libreoffice.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Tests for `setup_libreoffice.ensure_libreoffice`.
"""Tests for `setup.ensure_libreoffice`.

Behavior under test (v4): the function always runs `apt-get update` +
`apt-get install -y` regardless of what's already on PATH. Earlier
Expand Down Expand Up @@ -271,3 +271,46 @@ def test_java_runs_returns_true_when_java_version_exits_zero() -> None:
with patch.object(shutil, "which", return_value="/usr/bin/java"):
with patch.object(setup, "_run", return_value=(0, "", 'openjdk version "21.0.1"')):
assert setup._java_runs() is True


def test_early_exit_when_javaldx_actually_works(monkeypatch):
"""v5 skips apt only when the real bridge works, not when a proxy looks fine."""
calls = []
monkeypatch.setattr(setup.sys, "platform", "linux")
monkeypatch.setattr(setup.shutil, "which", lambda n: f"/usr/bin/{n}")
monkeypatch.setattr(setup, "_java_runs", lambda: True)
monkeypatch.setattr(setup, "_javaldx_works", lambda: True)
monkeypatch.setattr(setup, "_run", lambda cmd, **kw: calls.append(cmd) or (0, "", ""))

assert setup.ensure_libreoffice() is True
assert not any("apt-get" in c[0] for c in calls), "apt-get ran despite a working javaldx"


def test_falls_through_to_apt_when_javaldx_is_broken(monkeypatch):
"""The v1-v3 regression: libreoffice and java look fine but the bridge is dead."""
calls = []
monkeypatch.setattr(setup.sys, "platform", "linux")
monkeypatch.setattr(setup.shutil, "which", lambda n: f"/usr/bin/{n}")
monkeypatch.setattr(setup, "_java_runs", lambda: True)
monkeypatch.setattr(setup, "_javaldx_works", lambda: False)

def fake_run(cmd, **kw):
calls.append(cmd)
return (0, "LibreOffice 25.2", "")

monkeypatch.setattr(setup, "_run", fake_run)
setup.ensure_libreoffice()
assert any("apt-get" in c[0] for c in calls), "a broken javaldx must still trigger the install"


def test_javaldx_rejects_the_failure_banner(monkeypatch):
"""javaldx exits 0 while printing a failure banner; rc alone is not enough."""
monkeypatch.setattr(setup.os.path, "exists", lambda p: True)
monkeypatch.setattr(setup, "_run", lambda cmd, **kw: (0, "", "Warning: failed to launch javaldx"))
assert setup._javaldx_works() is False


def test_javaldx_absent_binary_is_not_working(monkeypatch):
"""javaldx ships in libreoffice-java-common, which libreoffice does not depend on."""
monkeypatch.setattr(setup.os.path, "exists", lambda p: False)
assert setup._javaldx_works() is False
Loading