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
19 changes: 1 addition & 18 deletions docker/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,8 @@ RUN touch /bin/nvidia-smi && \
mkdir -p /var/run/nvidia-persistenced && \
touch /var/run/nvidia-persistenced/socket

# On arm64, temporarily install swig for the nlopt source build, keep it
# through the Isaac Lab dependency install, then purge it before the layer ends.
# This lets ./isaaclab.sh --install see nlopt as already satisfied without
# leaving swig in the final container filesystem.
RUN --mount=type=cache,target=${DOCKER_USER_HOME}/.cache/pip \
if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
apt-get update && \
apt-get install -y --no-install-recommends swig && \
${ISAACLAB_PATH}/isaaclab.sh -p -m pip install setuptools wheel numpy && \
${ISAACLAB_PATH}/isaaclab.sh -p -m pip install --no-build-isolation nlopt==2.6.2; \
fi && \
${ISAACLAB_PATH}/isaaclab.sh --install && \
if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
${ISAACLAB_PATH}/isaaclab.sh -p -c "import nlopt" && \
apt-get purge -y --auto-remove swig && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
if command -v swig >/dev/null 2>&1; then exit 1; fi; \
fi
${ISAACLAB_PATH}/isaaclab.sh --install

# HACK: Remove install of quadprog dependency
RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip uninstall -y quadprog
Expand Down
19 changes: 1 addition & 18 deletions docker/Dockerfile.curobo
Original file line number Diff line number Diff line change
Expand Up @@ -149,25 +149,8 @@ RUN rm -rf ${ISAACSIM_ROOT_PATH}/kit/python/lib/python3.12/site-packages/pip* &&
${ISAACLAB_PATH}/_isaac_sim/kit/python/bin/python3 get-pip.py && \
rm get-pip.py

# On arm64, temporarily install swig for the nlopt source build, keep it
# through the Isaac Lab dependency install, then purge it before the layer ends.
# This lets ./isaaclab.sh --install see nlopt as already satisfied without
# leaving swig in the final container filesystem.
RUN --mount=type=cache,target=${DOCKER_USER_HOME}/.cache/pip \
if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
apt-get update && \
apt-get install -y --no-install-recommends swig && \
${ISAACLAB_PATH}/isaaclab.sh -p -m pip install setuptools wheel numpy && \
${ISAACLAB_PATH}/isaaclab.sh -p -m pip install --no-build-isolation nlopt==2.6.2; \
fi && \
${ISAACLAB_PATH}/isaaclab.sh --install && \
if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
${ISAACLAB_PATH}/isaaclab.sh -p -c "import nlopt" && \
apt-get purge -y --auto-remove swig && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
if command -v swig >/dev/null 2>&1; then exit 1; fi; \
fi
${ISAACLAB_PATH}/isaaclab.sh --install

# HACK: Uninstall quadprog as it causes issues with some reinforcement learning frameworks
RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip uninstall -y quadprog
Expand Down
12 changes: 12 additions & 0 deletions source/isaaclab/changelog.d/fix-arm-nlopt-install.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Fixed
^^^^^

* Fixed ``./isaaclab.sh --install`` unnecessarily building ``nlopt==2.6.2`` on
ARM Linux (e.g. DGX Spark), where it fails with CMake 4.x. The ARM-only
``nlopt`` pre-install and its temporary ``swig`` install were removed because
``nlopt`` is only pulled in by Linux x86_64 dependencies.

* Fixed unavoidable ARM source builds such as ``egl-probe==1.0.2`` failing when
CMake 4.x rejects policy compatibility older than 3.5. The installer now
temporarily sets ``CMAKE_POLICY_VERSION_MINIMUM=3.5`` during ARM dependency
builds.
190 changes: 91 additions & 99 deletions source/isaaclab/isaaclab/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import re
import shutil
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path

import tomllib
Expand All @@ -26,6 +28,33 @@
from .misc import command_vscode_settings


@contextmanager
def _arm_cmake_policy_compatibility() -> Iterator[None]:
"""Allow legacy CMake projects to configure during ARM dependency installs.

CMake 4 removed compatibility with policy versions older than 3.5. Some
transitive ARM dependencies, including ``nlopt==2.6.2`` and
``egl-probe==1.0.2``, still declare an older minimum. CMake provides this
environment variable specifically so users can configure such unmaintained
third-party projects without patching their sources.
"""
if not is_arm():
yield
return

variable = "CMAKE_POLICY_VERSION_MINIMUM"
saved_value = os.environ.get(variable)
os.environ[variable] = "3.5"
print_info(f"ARM install sandbox: temporarily setting {variable}=3.5 for legacy dependency builds.")
try:
yield
finally:
if saved_value is None:
os.environ.pop(variable, None)
else:
os.environ[variable] = saved_value


def _install_system_deps() -> None:
"""install system dependencies"""
if is_windows():
Expand Down Expand Up @@ -169,41 +198,6 @@ def _maybe_uninstall_prebundled_torch(
)


def _maybe_preinstall_arm_nlopt(python_exe: str, pip_cmd: list[str]) -> None:
"""Pre-install ``nlopt==2.6.2`` on ARM Linux to skip the source-build fallback.

There is no aarch64 manylinux wheel for the ``nlopt 2.6.2`` version pinned
by ``isaacteleop[retargeters]``, so pip falls back to a CMake source build
that hides the host-Python ``numpy`` from its isolated build env. Mirror
the docker/Dockerfile.base arm64 step: install ``setuptools wheel numpy``
in the host Python first, then ``--no-build-isolation`` install nlopt so
later submodule installs see it as already satisfied.
"""
if is_windows() or not is_arm():
return

probe_result = run_command(
[
python_exe,
"-c",
"import importlib.metadata as metadata; import nlopt; "
"raise SystemExit(0 if metadata.version('nlopt') == '2.6.2' else 1)",
],
check=False,
capture_output=True,
text=True,
)
if probe_result.returncode == 0:
print_info("nlopt==2.6.2 is already installed on ARM.")
return

print_info("Pre-installing nlopt==2.6.2 on ARM (no-build-isolation)...")
print_info(" step 1/2: ensure setuptools/wheel/numpy are importable for the no-build-isolation backend")
run_command(pip_cmd + ["install", "setuptools", "wheel", "numpy"])
print_info(" step 2/2: install nlopt==2.6.2 with --no-build-isolation")
run_command(pip_cmd + ["install", "--no-build-isolation", "nlopt==2.6.2"])


# Dependency stack required by isaaclab.controllers.pink_ik. Pinocchio is installed
# via the cmeel ``pin`` wheel, which provides the ``pinocchio`` Python module under
# ``cmeel.prefix/lib/python3.12/site-packages/`` and registers it on sys.path via a
Expand Down Expand Up @@ -1066,70 +1060,68 @@ def append_submodules_once(package_dirs: tuple[str, ...]) -> None:
# leave new dangling symlinks in Isaac Sim's prebundles (nvbugs 6343978).
dangling_symlinks_before = _find_dangling_prebundle_symlinks()

try:
# Upgrade pip first to avoid compatibility issues (skip when using uv).
if not using_uv:
print_info("Upgrading pip...")
run_command(pip_cmd + ["install", "--upgrade", "pip"])

# Pin setuptools to avoid issues with pkg_resources removal in 82.0.0.
run_command(pip_cmd + ["install", "setuptools<82.0.0"])

# On ARM Linux pre-install nlopt to dodge its from-source build fallback.
_maybe_preinstall_arm_nlopt(python_exe, pip_cmd)

# Drop pip-installed torch if Isaac Sim's deprecated ML prebundle would shadow it.
_maybe_uninstall_prebundled_torch(python_exe, pip_cmd, using_uv, probe_env=probe_env)

# Install Isaac Sim if requested.
if install_isaacsim:
_install_isaacsim()

# Install pytorch (version based on arch).
_ensure_cuda_torch()

# Install all submodules (core set + any explicitly requested optional ones).
_install_isaaclab_submodules(submodules_to_install)

# Install requested optional submodule dependency extras.
if optional_submodule_extra_dependencies:
print_info("Installing optional submodule dependencies...")
for submodule_name, selector in optional_submodule_extra_dependencies:
_install_optional_submodule_extra_dependencies(submodule_name, selector)

# Install requested extra feature dependencies.
if extra_features:
print_info("Installing extra feature dependencies...")
for feature_name, selector in extra_features:
_install_extra_feature(feature_name, selector)

# In some rare cases, torch might not be installed properly by setup.py, add one more check here.
# Can prevent that from happening.
_ensure_cuda_torch()

# Ensure Pink IK's runtime dependencies are actually importable. The kit-bundled
# ``pin-pink`` in recent Isaac Sim images can cause transitive dependencies from
# ``pip install -e source/isaaclab`` to be silently skipped.
_ensure_pink_ik_dependencies_installed(python_exe, pip_cmd, probe_env=probe_env)

# Repoint prebundled packages in Isaac Sim to the environment's copies so
# the active venv/conda versions are always loaded regardless of PYTHONPATH
# ordering (e.g. torch+cu130 in venv vs torch+cu128 in prebundle on aarch64).
_repoint_prebundle_packages()

# Fail loud if any pip operation above broke Isaac Sim's cross-extension
# symlink farms. Prebundle deletions on their own are routine (pip
# replaces those packages in site-packages, which shadows the prebundle
# at runtime); only newly dangling symlinks break extension startup.
_assert_no_new_dangling_prebundle_symlinks(dangling_symlinks_before)

finally:
# Restore LD_PRELOAD if we cleared it.
if saved_ld_preload:
os.environ["LD_PRELOAD"] = saved_ld_preload
# Restore PYTHONPATH if we filtered it.
if saved_pythonpath is not None:
os.environ["PYTHONPATH"] = saved_pythonpath
with _arm_cmake_policy_compatibility():
try:
# Upgrade pip first to avoid compatibility issues (skip when using uv).
if not using_uv:
print_info("Upgrading pip...")
run_command(pip_cmd + ["install", "--upgrade", "pip"])

# Pin setuptools to avoid issues with pkg_resources removal in 82.0.0.
run_command(pip_cmd + ["install", "setuptools<82.0.0"])

# Drop pip-installed torch if Isaac Sim's deprecated ML prebundle would shadow it.
_maybe_uninstall_prebundled_torch(python_exe, pip_cmd, using_uv, probe_env=probe_env)

# Install Isaac Sim if requested.
if install_isaacsim:
_install_isaacsim()

# Install pytorch (version based on arch).
_ensure_cuda_torch()

# Install all submodules (core set + any explicitly requested optional ones).
_install_isaaclab_submodules(submodules_to_install)

# Install requested optional submodule dependency extras.
if optional_submodule_extra_dependencies:
print_info("Installing optional submodule dependencies...")
for submodule_name, selector in optional_submodule_extra_dependencies:
_install_optional_submodule_extra_dependencies(submodule_name, selector)

# Install requested extra feature dependencies.
if extra_features:
print_info("Installing extra feature dependencies...")
for feature_name, selector in extra_features:
_install_extra_feature(feature_name, selector)

# In some rare cases, torch might not be installed properly by setup.py, add one more check here.
# Can prevent that from happening.
_ensure_cuda_torch()

# Ensure Pink IK's runtime dependencies are actually importable. The kit-bundled
# ``pin-pink`` in recent Isaac Sim images can cause transitive dependencies from
# ``pip install -e source/isaaclab`` to be silently skipped.
_ensure_pink_ik_dependencies_installed(python_exe, pip_cmd, probe_env=probe_env)

# Repoint prebundled packages in Isaac Sim to the environment's copies so
# the active venv/conda versions are always loaded regardless of PYTHONPATH
# ordering (e.g. torch+cu130 in venv vs torch+cu128 in prebundle on aarch64).
_repoint_prebundle_packages()

# Fail loud if any pip operation above broke Isaac Sim's cross-extension
# symlink farms. Prebundle deletions on their own are routine (pip
# replaces those packages in site-packages, which shadows the prebundle
# at runtime); only newly dangling symlinks break extension startup.
_assert_no_new_dangling_prebundle_symlinks(dangling_symlinks_before)

finally:
# Restore LD_PRELOAD if we cleared it.
if saved_ld_preload:
os.environ["LD_PRELOAD"] = saved_ld_preload
# Restore PYTHONPATH if we filtered it.
if saved_pythonpath is not None:
os.environ["PYTHONPATH"] = saved_pythonpath

# Install vscode update unless we're in docker.
if not (os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")):
Expand Down
6 changes: 5 additions & 1 deletion source/isaaclab/test/cli/test_install_command_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ def test_optional_submodules_not_in_core(self):
# Functions that must be mocked to prevent actual system calls.
_PATCHES = [
f"{_INSTALL_MODULE}._install_system_deps",
f"{_INSTALL_MODULE}._arm_cmake_policy_compatibility",
f"{_INSTALL_MODULE}._install_isaaclab_submodules",
f"{_INSTALL_MODULE}._install_extra_feature",
f"{_INSTALL_MODULE}._install_optional_submodule_extra_dependencies",
f"{_INSTALL_MODULE}._install_isaacsim",
f"{_INSTALL_MODULE}._ensure_cuda_torch",
f"{_INSTALL_MODULE}._maybe_preinstall_arm_nlopt",
f"{_INSTALL_MODULE}._maybe_uninstall_prebundled_torch",
f"{_INSTALL_MODULE}._ensure_pink_ik_dependencies_installed",
f"{_INSTALL_MODULE}._repoint_prebundle_packages",
Expand Down Expand Up @@ -205,6 +205,10 @@ def _run(self, install_type: str):

return mocks

def test_wraps_dependency_installs_with_arm_cmake_compatibility(self):
mocks = self._run("all")
mocks["_arm_cmake_policy_compatibility"].assert_called_once_with()

# --- "all" ---

def test_all_installs_core_plus_optional_submodules(self):
Expand Down
39 changes: 39 additions & 0 deletions source/isaaclab/test/cli/test_install_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Isaac Sim installation methods: local _isaac_sim symlink, pip-installed isaacsim, none
"""

import os
import subprocess
from contextlib import contextmanager
from pathlib import Path
Expand Down Expand Up @@ -69,6 +70,44 @@ def _make_site_packages(
return site_pkgs


# ---------------------------------------------------------------------------
# _arm_cmake_policy_compatibility
# ---------------------------------------------------------------------------


class TestArmCmakePolicyCompatibility:
"""Tests for legacy CMake dependency builds on ARM."""

def test_sets_policy_minimum_during_arm_install(self):
"""ARM installs allow nlopt and egl-probe CMake projects to configure."""
with (
mock.patch("isaaclab.cli.commands.install.is_arm", return_value=True),
mock.patch.dict("os.environ", {"PATH": "/bin"}, clear=True),
):
with install_cmd._arm_cmake_policy_compatibility():
assert os.environ["CMAKE_POLICY_VERSION_MINIMUM"] == "3.5"
assert "CMAKE_POLICY_VERSION_MINIMUM" not in os.environ

def test_restores_existing_policy_minimum_after_arm_install(self):
"""An existing user setting is restored after the installer finishes."""
with (
mock.patch("isaaclab.cli.commands.install.is_arm", return_value=True),
mock.patch.dict("os.environ", {"CMAKE_POLICY_VERSION_MINIMUM": "3.10"}, clear=True),
):
with install_cmd._arm_cmake_policy_compatibility():
assert os.environ["CMAKE_POLICY_VERSION_MINIMUM"] == "3.5"
assert os.environ["CMAKE_POLICY_VERSION_MINIMUM"] == "3.10"

def test_leaves_environment_unchanged_outside_arm(self):
"""Non-ARM installs do not receive the compatibility setting."""
with (
mock.patch("isaaclab.cli.commands.install.is_arm", return_value=False),
mock.patch.dict("os.environ", {}, clear=True),
):
with install_cmd._arm_cmake_policy_compatibility():
assert "CMAKE_POLICY_VERSION_MINIMUM" not in os.environ


# ---------------------------------------------------------------------------
# _install_isaaclab_submodules targeted dependency upgrades
# ---------------------------------------------------------------------------
Expand Down
5 changes: 1 addition & 4 deletions source/isaaclab/test/cli/test_uv_run_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,7 @@ def test_ci_ovrtx_installs_match_source_package_requirement():
"""CI must not bypass the OVRTX version range declared by the source package."""
expected_requirement = _ovrtx_requirement_from_setup()

for workflow_path in (
".github/workflows/build.yaml",
".github/workflows/daily-compatibility.yml",
):
for workflow_path in (".github/workflows/build.yaml",):
workflow = (_repo_root() / workflow_path).read_text(encoding="utf-8")
ovrtx_install_lines = [
line.strip() for line in workflow.splitlines() if "extra-pip-packages:" in line and "ovrtx" in line
Expand Down
Loading