diff --git a/resources_servers/gdpval/setup_libreoffice.py b/resources_servers/gdpval/setup_libreoffice.py index 05487a80db7..9571a3b5906 100644 --- a/resources_servers/gdpval/setup_libreoffice.py +++ b/resources_servers/gdpval/setup_libreoffice.py @@ -16,6 +16,7 @@ from __future__ import annotations import logging +import os import shutil import subprocess import sys @@ -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]: @@ -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. @@ -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 @@ -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 diff --git a/resources_servers/gdpval/tests/test_setup_libreoffice.py b/resources_servers/gdpval/tests/test_setup_libreoffice.py index a5546c1063c..df29777ce44 100644 --- a/resources_servers/gdpval/tests/test_setup_libreoffice.py +++ b/resources_servers/gdpval/tests/test_setup_libreoffice.py @@ -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 @@ -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