Source code for unisim.backend.isaacsim.dependencies
"""Runtime discovery for the external IsaacSim 5.1 / IsaacLab worker."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from unisim.backend.subprocess_ipc.runtime import WorkerRuntime, build_worker_environment
from unisim.optional import OptionalDependencyError
ENV_HOME = "UNISIM_ISAACSIM_HOME"
ENV_PYTHON = "UNISIM_ISAACSIM_PYTHON"
_SETUP_HINT = (
"Create the dedicated IsaacSim/IsaacLab worker environment under "
"UNISIM_ISAACSIM_HOME (see docs/en/support-matrix.md)."
)
[docs]
class IsaacSimDependencyError(OptionalDependencyError):
"""Raised with an actionable setup hint when the worker runtime is absent."""
[docs]
@dataclass(frozen=True)
class IsaacSimRuntime(WorkerRuntime):
"""Resolved on-disk locations for the Python 3.11 IsaacSim worker."""
isaaclab_source: Path | None = None
[docs]
def default_isaacsim_home() -> Path:
value = os.environ.get(ENV_HOME)
return Path(value or "~/.cache/unisim/isaacsim").expanduser()
def _candidate_paths(home: Path) -> IsaacSimRuntime:
venv = home / "venv"
return IsaacSimRuntime(
python=venv / "bin" / "python",
package_path=venv / "lib" / "python3.11" / "site-packages",
lib_path=venv / "lib",
bin_path=venv / "bin",
isaaclab_source=home / "IsaacLab" / "source",
)
def _runtime_from_override(python: Path, home: Path) -> IsaacSimRuntime:
"""Build a runtime from an explicitly supplied interpreter.
``UNISIM_ISAACSIM_PYTHON`` is intentionally a complete escape hatch for
installations that do not use the helper script's directory layout. If
the interpreter looks like ``<venv>/bin/python``, we opportunistically
discover its sibling library/site-packages directories; otherwise those
paths remain unset and the interpreter's own environment is trusted.
"""
bin_path = python.parent if python.parent.is_dir() else None
venv_root = python.parent.parent if python.parent.name in {"bin", "Scripts"} else None
package_path: Path | None = None
lib_path: Path | None = None
if venv_root is not None:
candidate_lib = venv_root / "lib"
if candidate_lib.is_dir():
lib_path = candidate_lib
site_candidates = sorted(candidate_lib.glob("python*/site-packages"))
if site_candidates:
package_path = site_candidates[0]
source_candidates = [home / "IsaacLab" / "source"]
if venv_root is not None:
source_candidates.append(venv_root.parent / "IsaacLab" / "source")
isaaclab_source = next((path for path in source_candidates if path.is_dir()), None)
return IsaacSimRuntime(
python=python,
package_path=package_path,
lib_path=lib_path,
bin_path=bin_path,
isaaclab_source=isaaclab_source,
)
[docs]
def resolve_isaacsim_runtime(python_override: str | None = None) -> IsaacSimRuntime:
"""Resolve the interpreter and package/source layout without importing Kit."""
home = default_isaacsim_home()
candidate = _candidate_paths(home)
override = python_override or os.environ.get(ENV_PYTHON)
if override:
python = Path(override).expanduser()
if not python.is_file():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the Python 3.11 worker interpreter at "
f"{python}. {_SETUP_HINT}"
)
# An explicit interpreter may belong to a completely custom layout;
# do not reject it merely because the default helper-tree paths are
# absent. The worker will report import errors with its full
# traceback if that environment is not actually IsaacSim-capable.
return _runtime_from_override(python, home)
python = candidate.python
if not python.is_file():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the Python 3.11 worker interpreter at "
f"{python}. {_SETUP_HINT}"
)
# The venv may use an editable IsaacLab install or a regular wheel. A
# package directory is still required as a cheap, import-free sanity check.
if candidate.package_path is not None and not candidate.package_path.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the worker site-packages directory at "
f"{candidate.package_path}. {_SETUP_HINT}"
)
if candidate.isaaclab_source is not None and not candidate.isaaclab_source.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the IsaacLab source directory at "
f"{candidate.isaaclab_source}. {_SETUP_HINT}"
)
if candidate.lib_path is not None and not candidate.lib_path.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the worker library directory at "
f"{candidate.lib_path}. {_SETUP_HINT}"
)
return IsaacSimRuntime(
python=python,
package_path=candidate.package_path,
lib_path=candidate.lib_path,
bin_path=candidate.bin_path,
isaaclab_source=candidate.isaaclab_source,
)
[docs]
def isaacsim_runtime_available() -> bool:
try:
resolve_isaacsim_runtime()
except IsaacSimDependencyError:
return False
return True
[docs]
def build_worker_env(runtime: IsaacSimRuntime) -> dict[str, str]:
"""Build an environment that prefers the pinned Kit/venv libraries."""
env = build_worker_environment(runtime)
# A host ``PYTHONHOME`` would redirect the Python 3.11 worker's standard
# library even when PYTHONPATH is constructed below.
env.pop("PYTHONHOME", None)
env.setdefault("OMNI_KIT_ACCEPT_EULA", "1")
# Do not add the host site-packages root here. It would shadow the Python
# 3.11 worker's compiled dependencies with host-wheel binaries. worker.py
# installs a name-scoped importer for the host UniSim owner modules instead.
python_paths = []
if runtime.isaaclab_source is not None:
python_paths.append(runtime.isaaclab_source)
# The host PYTHONPATH can contain binaries compiled for another Python
# version. The worker gets host UniSim modules through its name-scoped
# importer, so only the resolved IsaacLab source root is importable from
# PYTHONPATH.
env["PYTHONPATH"] = os.pathsep.join(str(path) for path in python_paths)
return env
__all__ = [
"ENV_HOME",
"ENV_PYTHON",
"IsaacSimDependencyError",
"IsaacSimRuntime",
"build_worker_env",
"default_isaacsim_home",
"isaacsim_runtime_available",
"resolve_isaacsim_runtime",
]