simfix 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simfix-0.1.2/simfix.egg-info → simfix-0.1.4}/PKG-INFO +1 -1
- {simfix-0.1.2 → simfix-0.1.4}/pyproject.toml +1 -1
- {simfix-0.1.2 → simfix-0.1.4}/simfix/__init__.py +1 -1
- {simfix-0.1.2 → simfix-0.1.4}/simfix/cli.py +9 -0
- simfix-0.1.4/simfix/cuda.py +133 -0
- simfix-0.1.4/simfix/recommendation_types.py +16 -0
- simfix-0.1.4/simfix/recommendations.py +320 -0
- simfix-0.1.4/simfix/ros_environment.py +106 -0
- simfix-0.1.4/simfix/system_capabilities.py +46 -0
- simfix-0.1.4/simfix/vendor_dependencies.py +134 -0
- {simfix-0.1.2 → simfix-0.1.4/simfix.egg-info}/PKG-INFO +1 -1
- {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/SOURCES.txt +5 -0
- {simfix-0.1.2 → simfix-0.1.4}/tests/test_basic.py +232 -1
- simfix-0.1.2/simfix/recommendations.py +0 -131
- {simfix-0.1.2 → simfix-0.1.4}/LICENSE +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/README.md +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/setup.cfg +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/analyzer.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/cmake.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/commands.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/compatibility.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/conda_environment.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/conda_fixer.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/cuda_docker.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/docker_runner.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/dockerfile.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/fixer.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/git_assets.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/planner.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/pypi.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/pyproject.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/python_requirements.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/repo.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/report.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/ros_docker.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/ros_package.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/setup_py.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/system.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix/system_docker.py +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/dependency_links.txt +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/entry_points.txt +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/requires.txt +0 -0
- {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/top_level.txt +0 -0
|
@@ -15,8 +15,11 @@ from simfix.planner import create_install_plan
|
|
|
15
15
|
from simfix.pypi import check_pypi_packages
|
|
16
16
|
from simfix.repo import clone_repo, is_git_url
|
|
17
17
|
from simfix.report import generate_markdown_report, write_markdown_report
|
|
18
|
+
from simfix.system_capabilities import detect_system_capabilities
|
|
18
19
|
from simfix.system import get_system_info
|
|
19
20
|
from simfix.recommendations import generate_recommendations
|
|
21
|
+
from simfix.cuda import detect_cuda_version_info
|
|
22
|
+
from simfix.ros_environment import detect_ros_environment_info
|
|
20
23
|
|
|
21
24
|
console = Console()
|
|
22
25
|
|
|
@@ -120,6 +123,9 @@ def recommendations(repo: str) -> None:
|
|
|
120
123
|
repo_recommendations = generate_recommendations(
|
|
121
124
|
dependencies=analysis.all_python_dependencies,
|
|
122
125
|
detected_ecosystems=analysis.detected_ecosystems,
|
|
126
|
+
system_capabilities=detect_system_capabilities(),
|
|
127
|
+
cuda_version_info=detect_cuda_version_info(repo_path),
|
|
128
|
+
ros_environment_info=detect_ros_environment_info(repo_path),
|
|
123
129
|
)
|
|
124
130
|
|
|
125
131
|
console.print("[bold]SimFix Recommendations[/bold]")
|
|
@@ -405,6 +411,9 @@ def doctor(
|
|
|
405
411
|
repo_recommendations = generate_recommendations(
|
|
406
412
|
dependencies=analysis.all_python_dependencies,
|
|
407
413
|
detected_ecosystems=analysis.detected_ecosystems,
|
|
414
|
+
system_capabilities=detect_system_capabilities(),
|
|
415
|
+
cuda_version_info=detect_cuda_version_info(repo_path),
|
|
416
|
+
ros_environment_info=detect_ros_environment_info(repo_path),
|
|
408
417
|
)
|
|
409
418
|
|
|
410
419
|
if repo_recommendations:
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""CUDA version detection helpers for SimFix recommendations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class CudaVersionInfo:
|
|
13
|
+
"""Detected CUDA version information."""
|
|
14
|
+
|
|
15
|
+
repo_cuda_version: tuple[int, int] | None
|
|
16
|
+
system_cuda_version: tuple[int, int] | None
|
|
17
|
+
repo_cuda_source: str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_cuda_version_info(repo_path: Path) -> CudaVersionInfo:
|
|
21
|
+
"""Detect repository CUDA requirement and system CUDA capability."""
|
|
22
|
+
repo_cuda_version, repo_cuda_source = detect_repo_cuda_version(repo_path)
|
|
23
|
+
system_cuda_version = detect_system_cuda_version()
|
|
24
|
+
|
|
25
|
+
return CudaVersionInfo(
|
|
26
|
+
repo_cuda_version=repo_cuda_version,
|
|
27
|
+
system_cuda_version=system_cuda_version,
|
|
28
|
+
repo_cuda_source=repo_cuda_source,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_repo_cuda_version(
|
|
33
|
+
repo_path: Path,
|
|
34
|
+
) -> tuple[tuple[int, int] | None, str | None]:
|
|
35
|
+
"""Detect CUDA version requested by repository files.
|
|
36
|
+
|
|
37
|
+
This is intentionally generic. It checks common simulator/deep-learning
|
|
38
|
+
signals such as Docker base images and CUDA-specific dependency names.
|
|
39
|
+
"""
|
|
40
|
+
dockerfile = repo_path / "Dockerfile"
|
|
41
|
+
if dockerfile.exists():
|
|
42
|
+
version = _detect_cuda_version_from_text(dockerfile.read_text(encoding="utf-8"))
|
|
43
|
+
if version is not None:
|
|
44
|
+
return version, "Dockerfile"
|
|
45
|
+
|
|
46
|
+
dependency_files = [
|
|
47
|
+
repo_path / "requirements.txt",
|
|
48
|
+
repo_path / "pyproject.toml",
|
|
49
|
+
repo_path / "setup.py",
|
|
50
|
+
repo_path / "environment.yml",
|
|
51
|
+
repo_path / "environment.yaml",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
for dependency_file in dependency_files:
|
|
55
|
+
if dependency_file.exists():
|
|
56
|
+
version = _detect_cuda_version_from_text(
|
|
57
|
+
dependency_file.read_text(encoding="utf-8")
|
|
58
|
+
)
|
|
59
|
+
if version is not None:
|
|
60
|
+
return version, dependency_file.name
|
|
61
|
+
|
|
62
|
+
return None, None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def detect_system_cuda_version() -> tuple[int, int] | None:
|
|
66
|
+
"""Detect CUDA version supported by the NVIDIA driver using nvidia-smi."""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["nvidia-smi"],
|
|
70
|
+
check=False,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
timeout=10,
|
|
74
|
+
)
|
|
75
|
+
except (OSError, subprocess.SubprocessError):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return _parse_nvidia_smi_cuda_version(result.stdout + result.stderr)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_cuda_version_mismatch(
|
|
82
|
+
repo_cuda_version: tuple[int, int] | None,
|
|
83
|
+
system_cuda_version: tuple[int, int] | None,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""Return True if the repo appears to require newer CUDA than the system."""
|
|
86
|
+
if repo_cuda_version is None or system_cuda_version is None:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
return repo_cuda_version > system_cuda_version
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _detect_cuda_version_from_text(text: str) -> tuple[int, int] | None:
|
|
93
|
+
"""Detect CUDA version from generic text."""
|
|
94
|
+
normalized = text.lower()
|
|
95
|
+
|
|
96
|
+
detected_versions: list[tuple[int, int]] = []
|
|
97
|
+
|
|
98
|
+
explicit_patterns = [
|
|
99
|
+
r"nvidia/cuda:(\d+)\.(\d+)",
|
|
100
|
+
r"cuda[-_]?(\d+)[\._-](\d+)",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for pattern in explicit_patterns:
|
|
104
|
+
for match in re.finditer(pattern, normalized):
|
|
105
|
+
detected_versions.append((int(match.group(1)), int(match.group(2))))
|
|
106
|
+
|
|
107
|
+
for match in re.finditer(r"cuda[-_]?(\d+)x", normalized):
|
|
108
|
+
detected_versions.append((int(match.group(1)), 0))
|
|
109
|
+
|
|
110
|
+
for match in re.finditer(r"cu(\d{2,3})", normalized):
|
|
111
|
+
detected_versions.append(_parse_compact_cuda_version(match.group(1)))
|
|
112
|
+
|
|
113
|
+
if not detected_versions:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
return max(detected_versions)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _parse_nvidia_smi_cuda_version(text: str) -> tuple[int, int] | None:
|
|
120
|
+
"""Parse CUDA version from nvidia-smi output."""
|
|
121
|
+
match = re.search(r"cuda version:\s*(\d+)\.(\d+)", text.lower())
|
|
122
|
+
if match is None:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
return int(match.group(1)), int(match.group(2))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_compact_cuda_version(version: str) -> tuple[int, int]:
|
|
129
|
+
"""Parse compact CUDA version strings such as cu118 or cu121."""
|
|
130
|
+
if len(version) == 2:
|
|
131
|
+
return int(version[0]), int(version[1])
|
|
132
|
+
|
|
133
|
+
return int(version[:-1]), int(version[-1])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared recommendation types used by SimFix."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Recommendation:
|
|
10
|
+
"""A safe recommendation for system or vendor dependency guidance."""
|
|
11
|
+
|
|
12
|
+
category: str
|
|
13
|
+
title: str
|
|
14
|
+
status: str
|
|
15
|
+
reason: str
|
|
16
|
+
suggestion: str
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# from dataclasses import dataclass
|
|
4
|
+
from simfix.system_capabilities import SystemCapabilities
|
|
5
|
+
from simfix.cuda import CudaVersionInfo, is_cuda_version_mismatch
|
|
6
|
+
from simfix.ros_environment import RosEnvironmentInfo
|
|
7
|
+
from simfix.vendor_dependencies import detect_vendor_dependency_recommendations
|
|
8
|
+
from simfix.recommendation_types import Recommendation
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_recommendations(
|
|
13
|
+
dependencies: list[str],
|
|
14
|
+
detected_ecosystems: list[str],
|
|
15
|
+
python_version: tuple[int, int] | None = None,
|
|
16
|
+
system_capabilities: SystemCapabilities | None = None,
|
|
17
|
+
cuda_version_info: CudaVersionInfo | None = None,
|
|
18
|
+
ros_environment_info: RosEnvironmentInfo | None = None,
|
|
19
|
+
) -> list[Recommendation]:
|
|
20
|
+
"""Generate safe system and vendor dependency recommendations.
|
|
21
|
+
|
|
22
|
+
This function does not install drivers, ROS, CUDA, or vendor tools.
|
|
23
|
+
It only detects likely requirements and returns guidance.
|
|
24
|
+
"""
|
|
25
|
+
recommendations: list[Recommendation] = []
|
|
26
|
+
|
|
27
|
+
normalized_dependencies = [dependency.lower() for dependency in dependencies]
|
|
28
|
+
normalized_ecosystems = [ecosystem.lower() for ecosystem in detected_ecosystems]
|
|
29
|
+
|
|
30
|
+
if python_version is None:
|
|
31
|
+
python_version = sys.version_info[:2]
|
|
32
|
+
|
|
33
|
+
has_isaacgym = any(
|
|
34
|
+
"isaacgym" in dependency for dependency in normalized_dependencies
|
|
35
|
+
)
|
|
36
|
+
has_isaacsim = any(
|
|
37
|
+
"isaacsim" in dependency or "omni.isaac" in dependency
|
|
38
|
+
for dependency in normalized_dependencies
|
|
39
|
+
)
|
|
40
|
+
has_cuda_dependency = any(
|
|
41
|
+
_has_cuda_keyword(dependency) for dependency in normalized_dependencies
|
|
42
|
+
)
|
|
43
|
+
has_ros = "ros" in normalized_ecosystems
|
|
44
|
+
|
|
45
|
+
has_old_pinned_dependency = any(
|
|
46
|
+
_has_old_pinned_dependency(dependency) for dependency in normalized_dependencies
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
recommendations.extend(
|
|
50
|
+
detect_vendor_dependency_recommendations(normalized_dependencies)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if has_isaacgym:
|
|
54
|
+
recommendations.append(
|
|
55
|
+
Recommendation(
|
|
56
|
+
category="Vendor-managed dependency",
|
|
57
|
+
title="NVIDIA Isaac Gym required",
|
|
58
|
+
status="Manual installation required",
|
|
59
|
+
reason=(
|
|
60
|
+
"The dependency 'isaacgym' was detected, but it is not "
|
|
61
|
+
"available as a normal PyPI package."
|
|
62
|
+
),
|
|
63
|
+
suggestion=(
|
|
64
|
+
"Install NVIDIA Isaac Gym manually in a compatible Linux "
|
|
65
|
+
"environment with NVIDIA GPU support. If the local machine "
|
|
66
|
+
"is not suitable, use an HPC GPU node or cloud GPU instance."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if has_isaacsim:
|
|
72
|
+
recommendations.append(
|
|
73
|
+
Recommendation(
|
|
74
|
+
category="Vendor-managed dependency",
|
|
75
|
+
title="NVIDIA Isaac Sim required",
|
|
76
|
+
status="Manual installation required",
|
|
77
|
+
reason=(
|
|
78
|
+
"Isaac Sim or omni.isaac dependencies were detected. "
|
|
79
|
+
"These are NVIDIA-managed dependencies."
|
|
80
|
+
),
|
|
81
|
+
suggestion=(
|
|
82
|
+
"Install Isaac Sim using NVIDIA's official installation "
|
|
83
|
+
"method. Use a supported system with compatible NVIDIA GPU "
|
|
84
|
+
"drivers."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if has_isaacgym or has_isaacsim or has_cuda_dependency:
|
|
90
|
+
recommendations.append(
|
|
91
|
+
Recommendation(
|
|
92
|
+
category="GPU/CUDA",
|
|
93
|
+
title="CUDA-compatible environment recommended",
|
|
94
|
+
status="Check required",
|
|
95
|
+
reason=(
|
|
96
|
+
"GPU/CUDA-related dependencies were detected in the " "repository."
|
|
97
|
+
),
|
|
98
|
+
suggestion=(
|
|
99
|
+
"Use a compatible Linux/NVIDIA GPU environment. If the "
|
|
100
|
+
"local GPU is not CUDA-compatible, consider a Docker setup "
|
|
101
|
+
"on a compatible machine, an HPC GPU node, a cloud GPU "
|
|
102
|
+
"instance, or CPU-only mode if the simulator supports it."
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if ros_environment_info is not None:
|
|
108
|
+
if ros_environment_info.project_type in {"ROS 1 / catkin", "ROS 2 / ament"}:
|
|
109
|
+
recommendations.append(
|
|
110
|
+
Recommendation(
|
|
111
|
+
category="ROS",
|
|
112
|
+
title=f"{ros_environment_info.project_type} environment detected",
|
|
113
|
+
status="Environment recommendation available",
|
|
114
|
+
reason=(
|
|
115
|
+
f"{ros_environment_info.project_type} signals were detected "
|
|
116
|
+
f"from {ros_environment_info.source}."
|
|
117
|
+
),
|
|
118
|
+
suggestion=(
|
|
119
|
+
f"Use ROS {ros_environment_info.recommended_distribution} "
|
|
120
|
+
f"with {ros_environment_info.recommended_ubuntu}, or use the "
|
|
121
|
+
f"Docker image {ros_environment_info.recommended_docker_image}."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
recommendations.append(
|
|
127
|
+
Recommendation(
|
|
128
|
+
category="ROS",
|
|
129
|
+
title="ROS project detected",
|
|
130
|
+
status="Manual environment check recommended",
|
|
131
|
+
reason=(
|
|
132
|
+
f"ROS package files were detected from "
|
|
133
|
+
f"{ros_environment_info.source}, but the ROS build style "
|
|
134
|
+
"could not be confidently classified."
|
|
135
|
+
),
|
|
136
|
+
suggestion=(
|
|
137
|
+
"Check the package.xml and build files to determine whether "
|
|
138
|
+
"the project requires ROS 1/catkin or ROS 2/ament. Use a "
|
|
139
|
+
"matching ROS Docker image or native ROS installation."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
elif has_ros:
|
|
144
|
+
recommendations.append(
|
|
145
|
+
Recommendation(
|
|
146
|
+
category="ROS",
|
|
147
|
+
title="ROS environment required",
|
|
148
|
+
status="Manual installation required",
|
|
149
|
+
reason="ROS package files were detected in the repository.",
|
|
150
|
+
suggestion=(
|
|
151
|
+
"Use a matching ROS Docker image or install the correct ROS "
|
|
152
|
+
"distribution manually."
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if has_old_pinned_dependency and python_version >= (3, 13):
|
|
158
|
+
recommendations.append(
|
|
159
|
+
Recommendation(
|
|
160
|
+
category="Python environment",
|
|
161
|
+
title="Older pinned dependencies detected",
|
|
162
|
+
status="Python version compatibility check recommended",
|
|
163
|
+
reason=(
|
|
164
|
+
"This repository contains older pinned dependencies that may "
|
|
165
|
+
f"not install cleanly on Python {python_version[0]}.{python_version[1]}."
|
|
166
|
+
),
|
|
167
|
+
suggestion=(
|
|
168
|
+
"Use Python 3.10 or 3.11 in a virtual environment for better "
|
|
169
|
+
"compatibility before installing the project dependencies."
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if has_cuda_dependency and system_capabilities is not None:
|
|
175
|
+
if (
|
|
176
|
+
system_capabilities.has_docker
|
|
177
|
+
and not system_capabilities.has_nvidia_container_runtime
|
|
178
|
+
):
|
|
179
|
+
recommendations.append(
|
|
180
|
+
Recommendation(
|
|
181
|
+
category="GPU containers",
|
|
182
|
+
title="NVIDIA Docker support not detected",
|
|
183
|
+
status="Configuration check recommended",
|
|
184
|
+
reason=(
|
|
185
|
+
"This repository appears to need GPU/CUDA support, and Docker "
|
|
186
|
+
"is available, but NVIDIA container runtime support was not detected."
|
|
187
|
+
),
|
|
188
|
+
suggestion=(
|
|
189
|
+
"GPU containers may fail with '--gpus all'. Use a compatible "
|
|
190
|
+
"Linux/NVIDIA environment with NVIDIA Container Toolkit configured, "
|
|
191
|
+
"or use an HPC/cloud GPU environment."
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if cuda_version_info is not None:
|
|
197
|
+
repo_cuda_version = cuda_version_info.repo_cuda_version
|
|
198
|
+
system_cuda_version = cuda_version_info.system_cuda_version
|
|
199
|
+
|
|
200
|
+
if repo_cuda_version is not None and system_cuda_version is None:
|
|
201
|
+
recommendations.append(
|
|
202
|
+
Recommendation(
|
|
203
|
+
category="CUDA compatibility",
|
|
204
|
+
title="System CUDA support not detected",
|
|
205
|
+
status="Compatibility unknown",
|
|
206
|
+
reason=(
|
|
207
|
+
"The repository appears to require CUDA "
|
|
208
|
+
f"{repo_cuda_version[0]}.{repo_cuda_version[1]} from "
|
|
209
|
+
f"{cuda_version_info.repo_cuda_source}, but system CUDA support "
|
|
210
|
+
"could not be detected with nvidia-smi."
|
|
211
|
+
),
|
|
212
|
+
suggestion=(
|
|
213
|
+
"Use a compatible Linux/NVIDIA GPU environment, a configured "
|
|
214
|
+
"GPU Docker setup, an HPC GPU node, or a cloud GPU instance. "
|
|
215
|
+
"If the simulator supports CPU-only mode, that may also be used "
|
|
216
|
+
"for limited testing."
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if is_cuda_version_mismatch(repo_cuda_version, system_cuda_version):
|
|
222
|
+
recommendations.append(
|
|
223
|
+
Recommendation(
|
|
224
|
+
category="CUDA compatibility",
|
|
225
|
+
title="CUDA version mismatch detected",
|
|
226
|
+
status="Version mismatch",
|
|
227
|
+
reason=(
|
|
228
|
+
"The repository appears to require CUDA "
|
|
229
|
+
f"{repo_cuda_version[0]}.{repo_cuda_version[1]} from "
|
|
230
|
+
f"{cuda_version_info.repo_cuda_source}, but the NVIDIA driver "
|
|
231
|
+
f"reports CUDA {system_cuda_version[0]}.{system_cuda_version[1]} support."
|
|
232
|
+
),
|
|
233
|
+
suggestion=(
|
|
234
|
+
"Use a CUDA version compatible with the installed NVIDIA driver, "
|
|
235
|
+
"update the NVIDIA driver if appropriate, or run the simulator on "
|
|
236
|
+
"an HPC/cloud GPU environment with compatible CUDA support."
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
return recommendations
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _has_cuda_keyword(dependency: str) -> bool:
|
|
244
|
+
"""Return True if a dependency suggests GPU/CUDA requirements."""
|
|
245
|
+
cuda_keywords = [
|
|
246
|
+
"cuda",
|
|
247
|
+
"cudnn",
|
|
248
|
+
"cupy",
|
|
249
|
+
"pytorch3d",
|
|
250
|
+
"onnxruntime-gpu",
|
|
251
|
+
"tensorflow-gpu",
|
|
252
|
+
"jax[cuda",
|
|
253
|
+
"numba.cuda",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return any(keyword in dependency for keyword in cuda_keywords)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _has_old_pinned_dependency(dependency: str) -> bool:
|
|
260
|
+
"""Return True when a dependency is pinned to an old version.
|
|
261
|
+
|
|
262
|
+
This is a conservative heuristic used for Python-version compatibility
|
|
263
|
+
recommendations. It does not modify dependencies.
|
|
264
|
+
"""
|
|
265
|
+
normalized = dependency.lower().strip()
|
|
266
|
+
|
|
267
|
+
if "==" not in normalized:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
package_name, version = normalized.split("==", maxsplit=1)
|
|
271
|
+
package_name = package_name.strip()
|
|
272
|
+
version = version.strip()
|
|
273
|
+
|
|
274
|
+
old_version_thresholds = {
|
|
275
|
+
"numpy": (1, 24),
|
|
276
|
+
"scipy": (1, 10),
|
|
277
|
+
"networkx": (3, 0),
|
|
278
|
+
"pandas": (2, 0),
|
|
279
|
+
"matplotlib": (3, 7),
|
|
280
|
+
"gym": (1, 0),
|
|
281
|
+
"urdfpy": (0, 1),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if package_name not in old_version_thresholds:
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
parsed_version = _parse_version_prefix(version)
|
|
288
|
+
if parsed_version is None:
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
return parsed_version < old_version_thresholds[package_name]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _parse_version_prefix(version: str) -> tuple[int, ...] | None:
|
|
295
|
+
"""Parse the numeric prefix of a dependency version.
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
1.23.0 -> (1, 23, 0)
|
|
299
|
+
2.2 -> (2, 2)
|
|
300
|
+
0.0.22 -> (0, 0, 22)
|
|
301
|
+
"""
|
|
302
|
+
parts: list[int] = []
|
|
303
|
+
|
|
304
|
+
for part in version.split("."):
|
|
305
|
+
number = ""
|
|
306
|
+
for character in part:
|
|
307
|
+
if character.isdigit():
|
|
308
|
+
number += character
|
|
309
|
+
else:
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
if not number:
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
parts.append(int(number))
|
|
316
|
+
|
|
317
|
+
if not parts:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
return tuple(parts)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""ROS environment detection helpers for SimFix recommendations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RosEnvironmentInfo:
|
|
11
|
+
"""Detected ROS environment information."""
|
|
12
|
+
|
|
13
|
+
project_type: str | None
|
|
14
|
+
recommended_distribution: str | None
|
|
15
|
+
recommended_ubuntu: str | None
|
|
16
|
+
recommended_docker_image: str | None
|
|
17
|
+
source: str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_ros_environment_info(repo_path: Path) -> RosEnvironmentInfo | None:
|
|
21
|
+
"""Detect ROS project style and recommend a common compatible environment.
|
|
22
|
+
|
|
23
|
+
This detection is generic and based on build-system signals, not repository
|
|
24
|
+
names. It does not install ROS or modify files.
|
|
25
|
+
"""
|
|
26
|
+
package_xml = repo_path / "package.xml"
|
|
27
|
+
cmake_lists = repo_path / "CMakeLists.txt"
|
|
28
|
+
|
|
29
|
+
package_text = _read_file(package_xml)
|
|
30
|
+
cmake_text = _read_file(cmake_lists)
|
|
31
|
+
combined_text = f"{package_text}\n{cmake_text}".lower()
|
|
32
|
+
|
|
33
|
+
if not combined_text.strip():
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
if _looks_like_ros2(combined_text):
|
|
37
|
+
return RosEnvironmentInfo(
|
|
38
|
+
project_type="ROS 2 / ament",
|
|
39
|
+
recommended_distribution="Humble",
|
|
40
|
+
recommended_ubuntu="Ubuntu 22.04",
|
|
41
|
+
recommended_docker_image="osrf/ros:humble-desktop",
|
|
42
|
+
source=_source_label(package_xml, cmake_lists),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if _looks_like_ros1(combined_text):
|
|
46
|
+
return RosEnvironmentInfo(
|
|
47
|
+
project_type="ROS 1 / catkin",
|
|
48
|
+
recommended_distribution="Noetic",
|
|
49
|
+
recommended_ubuntu="Ubuntu 20.04",
|
|
50
|
+
recommended_docker_image="osrf/ros:noetic-desktop-full",
|
|
51
|
+
source=_source_label(package_xml, cmake_lists),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if package_xml.exists():
|
|
55
|
+
return RosEnvironmentInfo(
|
|
56
|
+
project_type="ROS project",
|
|
57
|
+
recommended_distribution=None,
|
|
58
|
+
recommended_ubuntu=None,
|
|
59
|
+
recommended_docker_image=None,
|
|
60
|
+
source="package.xml",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _looks_like_ros2(text: str) -> bool:
|
|
67
|
+
ros2_markers = [
|
|
68
|
+
"ament_cmake",
|
|
69
|
+
"ament_python",
|
|
70
|
+
"ament_package",
|
|
71
|
+
"find_package(ament_cmake",
|
|
72
|
+
"<build_type>ament_cmake</build_type>",
|
|
73
|
+
"<build_type>ament_python</build_type>",
|
|
74
|
+
"rclpy",
|
|
75
|
+
"rclcpp",
|
|
76
|
+
]
|
|
77
|
+
return any(marker in text for marker in ros2_markers)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _looks_like_ros1(text: str) -> bool:
|
|
81
|
+
ros1_markers = [
|
|
82
|
+
"catkin_package",
|
|
83
|
+
"find_package(catkin",
|
|
84
|
+
"<buildtool_depend>catkin</buildtool_depend>",
|
|
85
|
+
"<build_depend>catkin</build_depend>",
|
|
86
|
+
"<depend>roscpp</depend>",
|
|
87
|
+
"<depend>rospy</depend>",
|
|
88
|
+
"roslaunch",
|
|
89
|
+
]
|
|
90
|
+
return any(marker in text for marker in ros1_markers)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_file(path: Path) -> str:
|
|
94
|
+
if not path.exists():
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
return path.read_text(encoding="utf-8")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _source_label(*paths: Path) -> str:
|
|
101
|
+
existing_paths = [path.name for path in paths if path.exists()]
|
|
102
|
+
|
|
103
|
+
if not existing_paths:
|
|
104
|
+
return "ROS project files"
|
|
105
|
+
|
|
106
|
+
return ", ".join(existing_paths)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""System capability detection used by SimFix recommendations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class SystemCapabilities:
|
|
12
|
+
"""Detected system capabilities relevant to simulator installation."""
|
|
13
|
+
|
|
14
|
+
has_docker: bool
|
|
15
|
+
has_nvidia_smi: bool
|
|
16
|
+
has_nvidia_container_runtime: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_system_capabilities() -> SystemCapabilities:
|
|
20
|
+
"""Detect system capabilities without modifying the system."""
|
|
21
|
+
return SystemCapabilities(
|
|
22
|
+
has_docker=shutil.which("docker") is not None,
|
|
23
|
+
has_nvidia_smi=shutil.which("nvidia-smi") is not None,
|
|
24
|
+
has_nvidia_container_runtime=_has_nvidia_container_runtime(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _has_nvidia_container_runtime() -> bool:
|
|
29
|
+
"""Return True if Docker appears to support NVIDIA GPU containers."""
|
|
30
|
+
if shutil.which("docker") is None:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["docker", "info", "--format", "{{json .Runtimes}}"],
|
|
36
|
+
check=False,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=10,
|
|
40
|
+
)
|
|
41
|
+
except (OSError, subprocess.SubprocessError):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
output = result.stdout.lower() + result.stderr.lower()
|
|
45
|
+
|
|
46
|
+
return "nvidia" in output
|