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
95 changes: 73 additions & 22 deletions src/polystore/imagej_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import shutil
import stat
import tempfile
import threading
import time
import urllib.request
import zipfile
from abc import ABC, abstractmethod
from collections.abc import Iterator, Mapping
from collections.abc import Iterator, Mapping, MutableMapping
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
Expand Down Expand Up @@ -419,15 +420,50 @@ def label(self) -> str:

return f"Fiji {self.imagej_version} archive {self.release_id}"

@property
def resolved_cache_root(self) -> Path:
"""Resolve the bundle cache once when configuring a process environment."""
return (
Path(self.cache_root)
if self.cache_root is not None
else self.default_cache_root()
)

@staticmethod
def default_cache_root() -> Path:
"""Return the platform-owned default for the immutable bundle cache."""
return user_cache_path("polystore") / "imagej"

@classmethod
def configure_process_environment(
cls,
*,
default_download_policy: ImageJArchiveDownloadPolicy,
environment: MutableMapping[str, str] | None = None,
) -> None:
"""Pin bundle storage before disposable caches or runtime imports.

Explicit selectors remain authoritative. This only configures the
environment inherited by children; it neither materializes a bundle
nor starts Java. Call before first access to the declared runtime.
"""
values = os.environ if environment is None else environment
root = cls.cache_root_from_environment(values)
if root is None:
root = cls.default_cache_root()
download_key = default_download_policy.allow_download_environment_key
permission = "true" if default_download_policy.allow_download else "false"
policy_values = dict(values)
policy_values.setdefault(download_key, permission)
ImageJArchiveDownloadPolicy.from_environment(policy_values)
values[cls.cache_root_environment_key] = str(root)
values.setdefault(download_key, permission)

def materialize(self) -> ImageJRuntimeLaunch:
"""Download, verify, and atomically cache this host's Fiji bundle."""

asset = FijiBundleAsset.for_current_host()
cache_parent = (
Path(self.cache_root)
if self.cache_root is not None
else user_cache_path("polystore") / "imagej"
)
cache_parent = self.resolved_cache_root
runtime_digest = hashlib.sha256(
"".join((asset.sha256, *(overlay.sha256 for overlay in self.runtime_overlays))).encode(
"ascii"
Expand Down Expand Up @@ -542,21 +578,36 @@ def require_compatible_gateway(self, gateway: Any) -> None:
)


FIJI_IMAGEJ_DISTRIBUTION = FijiArchiveDistribution(
release_id="20260718-0417",
imagej_version="2.18.0",
archive_base_url=("https://downloads.imagej.net/fiji/archive/latest/20260718-0417"),
cache_root=FijiArchiveDistribution.cache_root_from_environment(),
download_policy=ImageJArchiveDownloadPolicy.from_environment(),
runtime_overlays=(
ImageJRuntimeOverlay(
label="PyImageJ 1.7.0 Fiji bridge overlay",
url=(
"https://github.com/OpenHCSDev/PolyStore/releases/download/"
"imagej-runtime-20260718-0417-pyimagej-1.7.0/"
"pyimagej-1.7.0-fiji-overlay.zip"
def _create_fiji_distribution() -> FijiArchiveDistribution:
"""Decode process settings at first runtime access, not declaration import."""
return FijiArchiveDistribution(
release_id="20260718-0417",
imagej_version="2.18.0",
archive_base_url=("https://downloads.imagej.net/fiji/archive/latest/20260718-0417"),
cache_root=FijiArchiveDistribution.cache_root_from_environment(),
download_policy=ImageJArchiveDownloadPolicy.from_environment(),
runtime_overlays=(
ImageJRuntimeOverlay(
label="PyImageJ 1.7.0 Fiji bridge overlay",
url=(
"https://github.com/OpenHCSDev/PolyStore/releases/download/"
"imagej-runtime-20260718-0417-pyimagej-1.7.0/"
"pyimagej-1.7.0-fiji-overlay.zip"
),
sha256="210d1ebb2c1f1ede85d0e94e741a772e07cfb9233992ae4e85cd245b06890345",
),
sha256="210d1ebb2c1f1ede85d0e94e741a772e07cfb9233992ae4e85cd245b06890345",
),
),
)
)


_distribution_lock = threading.Lock()


def __getattr__(name: str) -> Any:
"""Keep declaration imports safe for pre-runtime environment configuration."""
if name != "FIJI_IMAGEJ_DISTRIBUTION":
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
with _distribution_lock:
if name not in globals():
globals()[name] = _create_fiji_distribution()
return globals()[name]
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import numpy as np
from pathlib import Path

from polystore.imagej_distribution import FijiArchiveDistribution, ImageJArchiveDownloadPolicy

FijiArchiveDistribution.configure_process_environment(
default_download_policy=ImageJArchiveDownloadPolicy(allow_download=False)
)

from polystore import FileManager
from polystore.disk import DiskBackend
from polystore.memory import MemoryBackend
Expand Down
42 changes: 42 additions & 0 deletions tests/test_imagej_cache_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,48 @@
)


def test_declaration_import_defers_process_configuration(tmp_path):
environment = dict(os.environ)
environment.pop(FijiArchiveDistribution.cache_root_environment_key, None)
environment.pop(ImageJArchiveDownloadPolicy.allow_download_environment_key, None)
environment["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src")
child = subprocess.run(
[sys.executable, "-c", '''
import os
import sys
from pathlib import Path
import polystore.imagej_distribution as declarations
assert "FIJI_IMAGEJ_DISTRIBUTION" not in declarations.__dict__
os.environ[declarations.FijiArchiveDistribution.cache_root_environment_key] = sys.argv[1]
os.environ[declarations.ImageJArchiveDownloadPolicy.allow_download_environment_key] = "false"
from polystore.imagej_distribution import FIJI_IMAGEJ_DISTRIBUTION
from polystore.imagej_runtime import FIJI_IMAGEJ_RUNTIME
assert FIJI_IMAGEJ_DISTRIBUTION.cache_root == Path(sys.argv[1])
assert not FIJI_IMAGEJ_DISTRIBUTION.download_policy.allow_download
assert FIJI_IMAGEJ_RUNTIME.distribution is FIJI_IMAGEJ_DISTRIBUTION
assert declarations.FIJI_IMAGEJ_DISTRIBUTION is FIJI_IMAGEJ_DISTRIBUTION
''', str(tmp_path / "shared")],
env=environment, capture_output=True, text=True, timeout=10,
)
assert child.returncode == 0, child.stdout + child.stderr
assert not (tmp_path / "shared").exists()


def test_invalid_process_permission_leaves_environment_unchanged(tmp_path):
values = {
FijiArchiveDistribution.cache_root_environment_key: str(tmp_path / "shared"),
ImageJArchiveDownloadPolicy.allow_download_environment_key: "yes",
}
before = dict(values)
with pytest.raises(ImageJDistributionUnavailableError, match="true or false"):
FijiArchiveDistribution.configure_process_environment(
environment=values,
default_download_policy=ImageJArchiveDownloadPolicy(allow_download=False),
)
assert values == before
assert not (tmp_path / "shared").exists()


@pytest.mark.parametrize("value, allowed", ((None, True), ("true", True), (" FALSE ", False)))
def test_download_policy_decodes_permission_without_copying_retry_defaults(value, allowed):
environment = {} if value is None else {
Expand Down
Loading