Skip to content
Draft
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: 52 additions & 43 deletions .github/workflows/install-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ concurrency:
permissions:
contents: read
pull-requests: read

jobs:
changes:
name: Detect Installation Test Changes
Expand All @@ -46,10 +47,8 @@ jobs:
run: |
set -euo pipefail

# Installation tests run only when paths in the patterns table change.
# Otherwise the test job skips via `if:` and reports green to branch
# protection, which is why we don't use a workflow-level `paths:`
# filter (a not-triggered required check would block the PR forever).
# This job produces a PR summary showing which paths triggered tests.
# The installation test jobs themselves always run regardless of this output.
patterns=(
$'^apps/\tStandalone apps'
$'^tools/\tBuild tooling'
Expand All @@ -59,8 +58,9 @@ jobs:
$'^VERSION$\tVersion file'
$'(^|/)pyproject\\.toml$\tPython project metadata'
$'(^|/)environment\\.ya?ml$\tConda environment file'
$'^docker/Dockerfile\\.installci\tInstall CI Dockerfiles'
)
triggered_jobs="Installation Tests"
triggered_jobs="Installation Tests (uv), Installation Tests (conda)"

render_table() {
local files="$1" entry regex desc count sample
Expand All @@ -79,77 +79,86 @@ jobs:
done
}

any_match() {
local files="$1" entry regex
for entry in "${patterns[@]}"; do
IFS=$'\t' read -r regex _ <<< "$entry"
if printf '%s\n' "$files" | grep -qE "$regex"; then
return 0
fi
done
return 1
}

decide() {
local decision="$1" reason="$2" files="${3:-}"
echo "Decision: run_install_tests=$decision ($reason)"
echo "run_install_tests=$decision" >> "$GITHUB_OUTPUT"
summarize() {
local files="${1:-}"
{
echo "## Installation test gating"
echo ""
if [ "$decision" = "true" ]; then
echo "Installation tests will **run**: $reason."
else
echo "Installation tests will be **skipped**: $reason."
fi
echo ""
echo "Triggered jobs: $triggered_jobs."
echo "Installation tests always run. Triggered jobs: $triggered_jobs."
if [ -n "$files" ]; then
echo ""
render_table "$files"
fi
} >> "$GITHUB_STEP_SUMMARY"
}

# Always run; output is used for the summary only.
echo "run_install_tests=true" >> "$GITHUB_OUTPUT"

if [ "$EVENT_NAME" != "pull_request" ]; then
decide true "non-PR event ($EVENT_NAME)"
summarize
exit 0
fi

if ! changed_files="$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')"; then
# Fail-safe: a transient API error must not block merge. Default to running.
echo "::warning::Could not list changed files; defaulting to running tests"
decide true "fail-safe (could not list changed files)"
echo "::warning::Could not list changed files"
summarize
exit 0
fi

printf '%s\n' "$changed_files"

if any_match "$changed_files"; then
decide true "relevant paths changed" "$changed_files"
else
decide false "no relevant paths changed" "$changed_files"
fi
summarize "$changed_files"

install-tests:
name: Installation Tests
name: Installation Tests (uv)
needs: [changes]
if: needs.changes.outputs.run_install_tests == 'true'
runs-on: [self-hosted, gpu]
timeout-minutes: 90
timeout-minutes: 120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Conda job timeout leaves no headroom for Docker builds

Both install-tests and install-tests-conda are set to timeout-minutes: 120. For the conda job, run_install_ci.py sets an inner Docker run timeout of 7200 s (exactly 120 minutes). Every second spent on actions/checkout, building the uv base image, and then building the conda image on top of it eats into that same 120-minute budget — meaning the actual Docker run will be cancelled by GitHub Actions before the inner timeout ever fires. In practice even a fast build (~5–10 minutes) leaves the Docker run with only ~110 minutes instead of 120, and the GitHub job is the one that kills the container without a clean JUnit report. Consider raising this to at least timeout-minutes: 150 (or 160) to provide a genuine buffer for build overhead.

steps:
- name: Checkout
uses: actions/checkout@v6 # v6
- name: Run installation tests
- name: Run uv installation tests
env:
BASE_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.base_image || 'ubuntu:24.04' }}
TEST_FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.test_filter || '' }}
run: |
RUNNER_ARGS="--base-image $BASE_IMAGE"
RUNNER_ARGS="$RUNNER_ARGS --results-dir ${{ github.workspace }}/results"
RUNNER_ARGS="$RUNNER_ARGS --results-dir ${{ github.workspace }}/results-uv"
RUNNER_ARGS="$RUNNER_ARGS --gpu"

# Always restrict to uv-marked tests so the conda suite does not run
# redundantly in this image (Dockerfile.installci has no conda).
# TEST_FILTER (-k) narrows further when provided via workflow_dispatch.
PYTEST_EXTRA_ARGS=(-sv -m uv)
if [ -n "$TEST_FILTER" ]; then
PYTEST_EXTRA_ARGS+=(-k "$TEST_FILTER")
fi

tools/run_install_ci.py docker $RUNNER_ARGS -- --tb=short "${PYTEST_EXTRA_ARGS[@]}"

install-tests-conda:
name: Installation Tests (conda)
needs: [changes]
runs-on: [self-hosted, gpu]
timeout-minutes: 160
steps:
- name: Checkout
uses: actions/checkout@v6 # v6
- name: Run conda installation tests
env:
BASE_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.base_image || 'ubuntu:24.04' }}
TEST_FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.test_filter || '' }}
run: |
RUNNER_ARGS="--conda"
RUNNER_ARGS="$RUNNER_ARGS --base-image $BASE_IMAGE"
RUNNER_ARGS="$RUNNER_ARGS --results-dir ${{ github.workspace }}/results-conda"
RUNNER_ARGS="$RUNNER_ARGS --gpu"

PYTEST_EXTRA_ARGS=(-sv)
# Always restrict to conda-marked tests. The conda image
# (Dockerfile.installci-conda) also has uv, so without this filter it
# would re-run the entire uv suite redundantly.
# TEST_FILTER (-k) narrows further when provided via workflow_dispatch.
PYTEST_EXTRA_ARGS=(-sv -m conda)
if [ -n "$TEST_FILTER" ]; then
PYTEST_EXTRA_ARGS+=(-k "$TEST_FILTER")
fi
Expand Down
59 changes: 59 additions & 0 deletions docker/Dockerfile.installci-conda
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

# Conda-enabled container for testing Isaac Lab installation scenarios.
# Built on top of Dockerfile.installci (the uv image) so that all system
# dependencies, uv, the repo copy, and pytest are already present.
# Only Miniconda is added here.
#
# The runner script (tools/run_install_ci.py) builds Dockerfile.installci
# first and then passes the resulting tag as UV_IMAGE when building this file.
#
# Manual usage (from repo root):
# # 1. Build the uv base image first:
# docker build -f docker/Dockerfile.installci -t isaaclab-installci:ubuntu-24.04 .
# # 2. Build the conda image on top:
# docker build -f docker/Dockerfile.installci-conda \
# --build-arg UV_IMAGE=isaaclab-installci:ubuntu-24.04 \
# -t isaaclab-installci-conda .
# # Run:
# docker run --rm --gpus all isaaclab-installci-conda -v --tb=short
# # Drop into a shell for debugging:
# docker run --rm -it --entrypoint bash isaaclab-installci-conda

ARG UV_IMAGE=isaaclab-installci:ubuntu-24.04
FROM ${UV_IMAGE}

LABEL description="Container for conda-based installation CI tests."

# Install Miniconda — select the correct installer for the build platform.
# $(uname -m) returns x86_64 on amd64 and aarch64 on ARM (e.g. DGX Spark),
# matching Anaconda's naming convention exactly.
ARG MINICONDA_VERSION=latest
RUN ARCH="$(uname -m)" && \
curl -fsSL "https://repo.anaconda.com/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-${ARCH}.sh" \
-o /tmp/miniconda.sh && \
bash /tmp/miniconda.sh -b -p /opt/miniconda3 && \
rm /tmp/miniconda.sh

ENV PATH="/opt/miniconda3/bin:${PATH}"
ENV CONDA_AUTO_ACTIVATE_BASE=false

# Accept Anaconda's Terms of Service for the default channels so that
# non-interactive `conda env create` calls inside tests don't hit
# CondaToSNonInteractiveError (added in conda ≥ 24.9).
RUN conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r

# Initialize conda for bash so `conda run` and activation hooks work in subprocesses
RUN conda init bash && \
conda config --set always_yes true && \
conda clean --all --yes

# Use the system Python (where pytest is installed by Dockerfile.installci) rather
# than conda's Python, which does not have pytest. Conda's bin is on PATH for the
# tests themselves (which run `conda run …` / `conda activate …` in subprocesses).
ENTRYPOINT ["/usr/bin/python", "-m", "pytest", "-c", "source/isaaclab/test/install_ci/pytest.ini", "source/isaaclab/test/install_ci"]
CMD ["-v", "--tb=short", "-m", "conda"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Added
^^^^^

* Added installation workflow tests for training smoke coverage across uv,
conda, kitless, pip, and source-based Isaac Sim installs.
* Added environment variable controls for Isaac Sim pip installation to support
version, extras, pre-release, and extra index URL overrides.
4 changes: 4 additions & 0 deletions source/isaaclab/changelog.d/usd-core-25-11.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Changed
^^^^^^^

* Updated the ``usd-core`` dependency to 25.11.0 to match Kit 110.1.1.
67 changes: 51 additions & 16 deletions source/isaaclab/isaaclab/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,54 @@ def _ensure_cuda_torch() -> None:
ISAACSIM_EXTRAS = "all"
NVIDIA_INDEX_URL = "https://pypi.nvidia.com"

# Internal NVIDIA PyPI registries used for pre-release / internal builds.
# Activated when ISAACSIM_EXTRA_INDEX_URLS is set in the environment.
_INTERNAL_ISAACSIM_INDEX_URLS = [
"https://urm.nvidia.com/artifactory/api/pypi/sw-isaacsim-pypi/simple",
"https://urm.nvidia.com/artifactory/api/pypi/ct-omniverse-pypi/simple",
]


def _install_isaacsim() -> None:
"""Install Isaac Sim pip package if not already present."""
"""Install Isaac Sim pip package if not already present.

The following environment variables override defaults and enable installation
from internal NVIDIA registries (e.g. for pre-release builds not yet on
``pypi.nvidia.com``):

``ISAACSIM_VERSION_SPEC``
pip version specifier, e.g. ``"==6.4.0"`` (default: ``">=6.0.0"``).
``ISAACSIM_EXTRAS``
pip extras string, e.g. ``"all,extscache"`` (default: ``"all"``).
``ISAACSIM_EXTRA_INDEX_URLS``
Whitespace-separated ``--extra-index-url`` values appended after the
default NVIDIA public index. The two internal NVIDIA Artifactory
registries (``sw-isaacsim-pypi`` and ``ct-omniverse-pypi``) are added
**automatically** whenever this variable is non-empty so callers only
need to supply any additional custom URLs beyond those two.
``ISAACSIM_USE_PRE``
Set to ``"1"`` (or ``"true"`` / ``"yes"``) to pass ``--pre`` to pip,
which is required for pre-release wheel versions.
"""
python_exe = extract_python_exe()
pip_cmd = get_pip_command(python_exe)

# Resolve settings — env vars win over module-level defaults.
version_spec = os.environ.get("ISAACSIM_VERSION_SPEC", ISAACSIM_VERSION_SPEC)
extras = os.environ.get("ISAACSIM_EXTRAS", ISAACSIM_EXTRAS)
use_pre = os.environ.get("ISAACSIM_USE_PRE", "").strip().lower() in ("1", "true", "yes")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: insert(0, ...) in a loop reverses the declaration order — Iterating _INTERNAL_ISAACSIM_INDEX_URLS and calling insert(0, url) each time produces [ct-omniverse, sw-isaacsim, NVIDIA] instead of [sw-isaacsim, ct-omniverse, NVIDIA].

Practically this doesn't matter (pip resolves across all indexes regardless of --extra-index-url order), but if declaration order was intentional, replace the loop with:

Suggested change
# Prepend the two internal registries so they are tried before pypi.org.
for url in reversed(_INTERNAL_ISAACSIM_INDEX_URLS):
if url not in extra_index_urls:
extra_index_urls.insert(0, url)

# Build the extra-index-url list. Always start with the public NVIDIA index.
extra_index_urls: list[str] = [NVIDIA_INDEX_URL]
extra_urls_env = os.environ.get("ISAACSIM_EXTRA_INDEX_URLS", "").split()
if extra_urls_env:
# Prepend the two internal registries so they are tried before pypi.org.
prepend_urls = [url for url in _INTERNAL_ISAACSIM_INDEX_URLS if url not in extra_index_urls]
extra_index_urls = prepend_urls + extra_index_urls
for url in extra_urls_env:
if url not in extra_index_urls:
extra_index_urls.append(url)

# Check if already installed.
result = run_command(
[python_exe, "-c", "from importlib.metadata import version; print(version('isaacsim'))"],
Expand All @@ -302,22 +344,15 @@ def _install_isaacsim() -> None:

print_info("Installing Isaac Sim...")
using_uv = pip_cmd[0] == "uv"
extra_flags = []
if using_uv:
# uv needs unsafe-best-match to resolve packages across multiple indexes
# (isaacsim is on pypi.nvidia.com, its deps are on pypi.org).
extra_flags = ["--index-strategy", "unsafe-best-match"]

run_command(
pip_cmd
+ [
"install",
f"isaacsim[{ISAACSIM_EXTRAS}]{ISAACSIM_VERSION_SPEC}",
"--extra-index-url",
NVIDIA_INDEX_URL,
]
+ extra_flags
)
pre_flags = ["--pre"] if use_pre else []
uv_flags = ["--index-strategy", "unsafe-best-match"] if using_uv else []

extra_idx_args: list[str] = []
for url in extra_index_urls:
extra_idx_args += ["--extra-index-url", url]

run_command(pip_cmd + ["install"] + pre_flags + [f"isaacsim[{extras}]{version_spec}"] + extra_idx_args + uv_flags)


# Valid Isaac Lab submodule names that can be passed to --install.
Expand Down
2 changes: 2 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ __all__ = [
"SphereLightCfg",
"spawn_rigid_body_material",
"PhysicsMaterialCfg",
"RigidBodyMaterialBaseCfg",
"RigidBodyMaterialCfg",
"spawn_from_mdl_file",
"spawn_preview_surface",
Expand Down Expand Up @@ -237,6 +238,7 @@ from .spawners import (
SphereLightCfg,
spawn_rigid_body_material,
PhysicsMaterialCfg,
RigidBodyMaterialBaseCfg,
RigidBodyMaterialCfg,
spawn_from_mdl_file,
spawn_preview_surface,
Expand Down
6 changes: 3 additions & 3 deletions source/isaaclab/isaaclab/sim/simulation_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import Any, Literal # Literal used by RenderCfg

from isaaclab.physics import PhysicsCfg
from isaaclab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialCfg
from isaaclab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialBaseCfg
from isaaclab.utils import configclass
from isaaclab.visualizers import VisualizerCfg

Expand Down Expand Up @@ -193,8 +193,8 @@ class SimulationCfg:
physics_prim_path: str = "/physicsScene"
"""The prim path where the USD PhysicsScene is created. Default is "/physicsScene"."""

physics_material: RigidBodyMaterialCfg = RigidBodyMaterialCfg()
"""Default physics material settings for rigid bodies. Default is RigidBodyMaterialCfg.
physics_material: RigidBodyMaterialBaseCfg = RigidBodyMaterialBaseCfg()
"""Default physics material settings for rigid bodies. Default is :class:`RigidBodyMaterialBaseCfg`.

The physics engine defaults to this physics material for all the rigid body prims that do not have any
physics material specified on them.
Expand Down
3 changes: 2 additions & 1 deletion source/isaaclab/isaaclab/terrains/terrain_importer_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import TYPE_CHECKING, Literal

import isaaclab.sim as sim_utils
from isaaclab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialBaseCfg
from isaaclab.utils import configclass

if TYPE_CHECKING:
Expand Down Expand Up @@ -87,7 +88,7 @@ class TerrainImporterCfg:
to the grid color of the imported ground plane.
"""

physics_material: sim_utils.RigidBodyMaterialCfg = sim_utils.RigidBodyMaterialCfg()
physics_material: RigidBodyMaterialBaseCfg = RigidBodyMaterialBaseCfg()
"""The physics material of the terrain. Defaults to a default physics material.

The material is created at the path: ``{prim_path}/physicsMaterial``.
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
]
# Adds OpenUSD dependencies based on architecture for Kit less mode.
INSTALL_REQUIRES += [
f"usd-core==25.8.0 ; ({SUPPORTED_ARCHS})",
f"usd-core==25.11.0 ; ({SUPPORTED_ARCHS})",
f"usd-exchange>=2.2 ; ({SUPPORTED_ARCHS_ARM})",
]

Expand Down
7 changes: 6 additions & 1 deletion source/isaaclab/test/install_ci/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line("markers", "docker: tests that only run inside Docker")
config.addinivalue_line("markers", "native: tests that only run natively (not in Docker)")
config.addinivalue_line("markers", "slow: tests that take a long time")
config.addinivalue_line("markers", "uv: tests that require the uv package manager")
config.addinivalue_line("markers", "uv: tests that exercise the uv installation workflow")
config.addinivalue_line("markers", "conda: tests that exercise the conda installation workflow")
config.addinivalue_line(
"markers",
"isaacsim_source: tests that use Isaac Sim pre-installed via the _isaac_sim symlink",
)

try:
config.stash[_EXECUTION_ENVIRONMENT_KEY] = _utils.detect_execution_environment()
Expand Down
Loading
Loading