simfix 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- simfix/analyzer.py +121 -0
- simfix/cli.py +433 -0
- simfix/cmake.py +61 -0
- simfix/commands.py +91 -0
- simfix/compatibility.py +97 -0
- simfix/conda_environment.py +58 -0
- simfix/conda_fixer.py +145 -0
- simfix/cuda_docker.py +163 -0
- simfix/docker_runner.py +71 -0
- simfix/dockerfile.py +122 -0
- simfix/fixer.py +342 -0
- simfix/git_assets.py +109 -0
- simfix/planner.py +93 -0
- simfix/pypi.py +83 -0
- simfix/pyproject.py +103 -0
- simfix/python_requirements.py +42 -0
- simfix/repo.py +46 -0
- simfix/report.py +191 -0
- simfix/ros_docker.py +110 -0
- simfix/ros_package.py +94 -0
- simfix/setup_py.py +45 -0
- simfix/system.py +154 -0
- simfix/system_docker.py +175 -0
- simfix-0.1.0.dist-info/METADATA +286 -0
- simfix-0.1.0.dist-info/RECORD +30 -0
- simfix-0.1.0.dist-info/WHEEL +5 -0
- simfix-0.1.0.dist-info/entry_points.txt +2 -0
- simfix-0.1.0.dist-info/licenses/LICENSE +21 -0
- simfix-0.1.0.dist-info/top_level.txt +1 -0
simfix/commands.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from simfix.analyzer import RepoAnalysis
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class CommandPlan:
|
|
10
|
+
"""Suggested shell commands for installing a repository."""
|
|
11
|
+
|
|
12
|
+
title: str
|
|
13
|
+
commands: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_command_plan(analysis: RepoAnalysis) -> CommandPlan:
|
|
17
|
+
"""Create suggested installation commands from repository analysis."""
|
|
18
|
+
ecosystems = analysis.detected_ecosystems
|
|
19
|
+
repo_name = analysis.repo_path.name.replace("_", "-").lower()
|
|
20
|
+
|
|
21
|
+
if "docker" in ecosystems:
|
|
22
|
+
return CommandPlan(
|
|
23
|
+
title="Docker installation commands",
|
|
24
|
+
commands=[
|
|
25
|
+
f"docker build -t {repo_name} .",
|
|
26
|
+
f"docker run --rm -it {repo_name}",
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if "conda" in ecosystems:
|
|
31
|
+
env_name = "simfix-env"
|
|
32
|
+
if analysis.conda_environment is not None and analysis.conda_environment.name:
|
|
33
|
+
env_name = analysis.conda_environment.name
|
|
34
|
+
|
|
35
|
+
return CommandPlan(
|
|
36
|
+
title="Conda installation commands",
|
|
37
|
+
commands=[
|
|
38
|
+
"conda env create -f environment.yml",
|
|
39
|
+
f"conda activate {env_name}",
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if "ros" in ecosystems:
|
|
44
|
+
return CommandPlan(
|
|
45
|
+
title="ROS installation commands",
|
|
46
|
+
commands=[
|
|
47
|
+
"mkdir -p ~/simfix_ws/src",
|
|
48
|
+
"cd ~/simfix_ws/src",
|
|
49
|
+
"# Clone or copy the repository into this src folder",
|
|
50
|
+
"cd ~/simfix_ws",
|
|
51
|
+
"rosdep install --from-paths src --ignore-src -r -y",
|
|
52
|
+
"catkin build # or: colcon build",
|
|
53
|
+
"source devel/setup.bash # or: source install/setup.bash",
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if "python" in ecosystems:
|
|
58
|
+
commands = [
|
|
59
|
+
"python -m venv .venv",
|
|
60
|
+
"source .venv/bin/activate",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
if analysis.has_requirements_txt:
|
|
64
|
+
commands.append("python -m pip install -r requirements.txt")
|
|
65
|
+
|
|
66
|
+
if analysis.has_pyproject_toml:
|
|
67
|
+
commands.append("python -m pip install -e .")
|
|
68
|
+
|
|
69
|
+
return CommandPlan(
|
|
70
|
+
title="Python installation commands",
|
|
71
|
+
commands=commands,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if "cmake/c++" in ecosystems:
|
|
75
|
+
return CommandPlan(
|
|
76
|
+
title="CMake installation commands",
|
|
77
|
+
commands=[
|
|
78
|
+
"mkdir -p build",
|
|
79
|
+
"cd build",
|
|
80
|
+
"cmake ..",
|
|
81
|
+
"cmake --build . -j",
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return CommandPlan(
|
|
86
|
+
title="Manual installation commands",
|
|
87
|
+
commands=[
|
|
88
|
+
"# No common dependency file was detected.",
|
|
89
|
+
"# Read the README installation section manually.",
|
|
90
|
+
],
|
|
91
|
+
)
|
simfix/compatibility.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from simfix.analyzer import RepoAnalysis
|
|
4
|
+
from simfix.system import SystemInfo
|
|
5
|
+
|
|
6
|
+
ROS_UBUNTU_HINTS = {
|
|
7
|
+
"melodic": "18.04",
|
|
8
|
+
"noetic": "20.04",
|
|
9
|
+
"humble": "22.04",
|
|
10
|
+
"jazzy": "24.04",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detect_ros_distro_hint(analysis: RepoAnalysis) -> str | None:
|
|
15
|
+
"""Detect possible ROS distro hints from parsed dependency names."""
|
|
16
|
+
if analysis.ros_package_info is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
dependencies = [
|
|
20
|
+
dependency.lower() for dependency in analysis.ros_package_info.all_dependencies
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
joined_dependencies = " ".join(dependencies)
|
|
24
|
+
|
|
25
|
+
for distro in ROS_UBUNTU_HINTS:
|
|
26
|
+
if distro in joined_dependencies:
|
|
27
|
+
return distro
|
|
28
|
+
|
|
29
|
+
ros_info = analysis.ros_package_info
|
|
30
|
+
|
|
31
|
+
if ros_info.build_system == "catkin":
|
|
32
|
+
return "noetic"
|
|
33
|
+
|
|
34
|
+
if ros_info.build_system == "ament":
|
|
35
|
+
return "humble"
|
|
36
|
+
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def generate_compatibility_warnings(
|
|
41
|
+
analysis: RepoAnalysis,
|
|
42
|
+
system_info: SystemInfo,
|
|
43
|
+
) -> list[str]:
|
|
44
|
+
"""Generate compatibility warnings from repo analysis and system information."""
|
|
45
|
+
warnings: list[str] = []
|
|
46
|
+
ecosystems = analysis.detected_ecosystems
|
|
47
|
+
|
|
48
|
+
if "docker" in ecosystems and not system_info.docker_available:
|
|
49
|
+
warnings.append("Dockerfile detected, but Docker was not found on this system.")
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
"conda" in ecosystems
|
|
53
|
+
and not system_info.conda_available
|
|
54
|
+
and not system_info.mamba_available
|
|
55
|
+
):
|
|
56
|
+
warnings.append(
|
|
57
|
+
"Conda environment detected, but neither conda nor mamba was found."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
"python" in ecosystems
|
|
62
|
+
and not system_info.pip_available
|
|
63
|
+
and not system_info.uv_available
|
|
64
|
+
):
|
|
65
|
+
warnings.append("Python project detected, but neither pip nor uv was found.")
|
|
66
|
+
|
|
67
|
+
if "ros" in ecosystems and system_info.os_name != "Linux":
|
|
68
|
+
warnings.append(
|
|
69
|
+
"ROS project detected, but the current system is not Linux. "
|
|
70
|
+
"Docker or a Linux machine may be needed."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
ros_distro_hint = _detect_ros_distro_hint(analysis)
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
ros_distro_hint is not None
|
|
77
|
+
and system_info.linux_version is not None
|
|
78
|
+
and ROS_UBUNTU_HINTS[ros_distro_hint] != system_info.linux_version
|
|
79
|
+
):
|
|
80
|
+
warnings.append(
|
|
81
|
+
f"ROS {ros_distro_hint} project detected, which is commonly used with "
|
|
82
|
+
f"Ubuntu {ROS_UBUNTU_HINTS[ros_distro_hint]}, but this system appears "
|
|
83
|
+
f"to be Ubuntu {system_info.linux_version}."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if "cmake/c++" in ecosystems and system_info.os_name == "Darwin":
|
|
87
|
+
warnings.append(
|
|
88
|
+
"CMake/C++ project detected on macOS. Some simulator dependencies "
|
|
89
|
+
"may require Linux-specific packages."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if "unknown" in ecosystems:
|
|
93
|
+
warnings.append(
|
|
94
|
+
"No common dependency files were detected. Manual inspection is needed."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return warnings
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class CondaEnvironment:
|
|
12
|
+
"""Parsed conda environment information."""
|
|
13
|
+
|
|
14
|
+
name: str | None
|
|
15
|
+
conda_dependencies: list[str]
|
|
16
|
+
pip_dependencies: list[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_conda_environment(path: str | Path) -> CondaEnvironment | None:
|
|
20
|
+
"""Parse a conda environment.yml/environment.yaml file."""
|
|
21
|
+
environment_path = Path(path).expanduser().resolve()
|
|
22
|
+
|
|
23
|
+
if not environment_path.exists():
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
data = yaml.safe_load(environment_path.read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
name = data.get("name")
|
|
32
|
+
raw_dependencies = data.get("dependencies", [])
|
|
33
|
+
|
|
34
|
+
conda_dependencies: list[str] = []
|
|
35
|
+
pip_dependencies: list[str] = []
|
|
36
|
+
|
|
37
|
+
if not isinstance(raw_dependencies, list):
|
|
38
|
+
return CondaEnvironment(
|
|
39
|
+
name=name if isinstance(name, str) else None,
|
|
40
|
+
conda_dependencies=[],
|
|
41
|
+
pip_dependencies=[],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for dependency in raw_dependencies:
|
|
45
|
+
if isinstance(dependency, str):
|
|
46
|
+
conda_dependencies.append(dependency)
|
|
47
|
+
elif isinstance(dependency, dict):
|
|
48
|
+
pip_entries: Any = dependency.get("pip")
|
|
49
|
+
if isinstance(pip_entries, list):
|
|
50
|
+
pip_dependencies.extend(
|
|
51
|
+
item for item in pip_entries if isinstance(item, str)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return CondaEnvironment(
|
|
55
|
+
name=name if isinstance(name, str) else None,
|
|
56
|
+
conda_dependencies=conda_dependencies,
|
|
57
|
+
pip_dependencies=pip_dependencies,
|
|
58
|
+
)
|
simfix/conda_fixer.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from simfix.pypi import normalize_requirement_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CondaFixResult:
|
|
14
|
+
"""Result of fixing a conda environment file."""
|
|
15
|
+
|
|
16
|
+
file_path: Path
|
|
17
|
+
changed: bool
|
|
18
|
+
message: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dependency_name(dependency: str) -> str:
|
|
22
|
+
"""Return normalized dependency name without version constraints."""
|
|
23
|
+
separators = ["==", ">=", "<=", "!=", "~=", "=", ">", "<"]
|
|
24
|
+
|
|
25
|
+
for separator in separators:
|
|
26
|
+
if separator in dependency:
|
|
27
|
+
return dependency.split(separator, maxsplit=1)[0].strip().lower()
|
|
28
|
+
|
|
29
|
+
return dependency.strip().lower()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_more_specific(new_dependency: str, old_dependency: str) -> bool:
|
|
33
|
+
"""Return True if new dependency has more version information."""
|
|
34
|
+
version_symbols = ["==", ">=", "<=", "!=", "~=", "=", ">", "<"]
|
|
35
|
+
|
|
36
|
+
new_score = sum(symbol in new_dependency for symbol in version_symbols)
|
|
37
|
+
old_score = sum(symbol in old_dependency for symbol in version_symbols)
|
|
38
|
+
|
|
39
|
+
return new_score >= old_score
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _deduplicate_dependencies(dependencies: list[str]) -> list[str]:
|
|
43
|
+
"""Remove duplicate dependencies while keeping the more specific entry."""
|
|
44
|
+
normalized: dict[str, str] = {}
|
|
45
|
+
|
|
46
|
+
for dependency in dependencies:
|
|
47
|
+
name = _dependency_name(dependency)
|
|
48
|
+
|
|
49
|
+
if name not in normalized:
|
|
50
|
+
normalized[name] = dependency
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if _is_more_specific(dependency, normalized[name]):
|
|
54
|
+
normalized[name] = dependency
|
|
55
|
+
|
|
56
|
+
return list(normalized.values())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _deduplicate_pip_dependencies(dependencies: list[str]) -> list[str]:
|
|
60
|
+
"""Remove duplicate pip dependencies while keeping the more specific entry."""
|
|
61
|
+
normalized: dict[str, str] = {}
|
|
62
|
+
|
|
63
|
+
for dependency in dependencies:
|
|
64
|
+
name = normalize_requirement_name(dependency).lower()
|
|
65
|
+
|
|
66
|
+
if name not in normalized:
|
|
67
|
+
normalized[name] = dependency
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if _is_more_specific(dependency, normalized[name]):
|
|
71
|
+
normalized[name] = dependency
|
|
72
|
+
|
|
73
|
+
return list(normalized.values())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fix_conda_environment_file(repo_path: str | Path) -> CondaFixResult | None:
|
|
77
|
+
"""Fix environment.yml or environment.yaml in place."""
|
|
78
|
+
path = Path(repo_path).expanduser().resolve()
|
|
79
|
+
|
|
80
|
+
environment_path = path / "environment.yml"
|
|
81
|
+
|
|
82
|
+
if not environment_path.exists():
|
|
83
|
+
environment_path = path / "environment.yaml"
|
|
84
|
+
|
|
85
|
+
if not environment_path.exists():
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
old_text = environment_path.read_text(encoding="utf-8")
|
|
89
|
+
data: dict[str, Any] = yaml.safe_load(old_text) or {}
|
|
90
|
+
|
|
91
|
+
dependencies = data.get("dependencies")
|
|
92
|
+
|
|
93
|
+
if not isinstance(dependencies, list):
|
|
94
|
+
return CondaFixResult(
|
|
95
|
+
file_path=environment_path,
|
|
96
|
+
changed=False,
|
|
97
|
+
message="environment.yml has no valid dependencies list.",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
conda_dependencies: list[str] = []
|
|
101
|
+
pip_dependency_block: dict[str, list[str]] | None = None
|
|
102
|
+
other_entries: list[Any] = []
|
|
103
|
+
|
|
104
|
+
for dependency in dependencies:
|
|
105
|
+
if isinstance(dependency, str):
|
|
106
|
+
conda_dependencies.append(dependency)
|
|
107
|
+
elif isinstance(dependency, dict) and "pip" in dependency:
|
|
108
|
+
pip_values = dependency.get("pip", [])
|
|
109
|
+
|
|
110
|
+
if isinstance(pip_values, list):
|
|
111
|
+
pip_dependency_block = {
|
|
112
|
+
"pip": _deduplicate_pip_dependencies(
|
|
113
|
+
[str(value) for value in pip_values]
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
else:
|
|
117
|
+
other_entries.append(dependency)
|
|
118
|
+
else:
|
|
119
|
+
other_entries.append(dependency)
|
|
120
|
+
|
|
121
|
+
fixed_dependencies: list[Any] = []
|
|
122
|
+
fixed_dependencies.extend(_deduplicate_dependencies(conda_dependencies))
|
|
123
|
+
fixed_dependencies.extend(other_entries)
|
|
124
|
+
|
|
125
|
+
if pip_dependency_block is not None:
|
|
126
|
+
fixed_dependencies.append(pip_dependency_block)
|
|
127
|
+
|
|
128
|
+
data["dependencies"] = fixed_dependencies
|
|
129
|
+
|
|
130
|
+
new_text = yaml.safe_dump(
|
|
131
|
+
data,
|
|
132
|
+
sort_keys=False,
|
|
133
|
+
default_flow_style=False,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
changed = old_text != new_text
|
|
137
|
+
|
|
138
|
+
if changed:
|
|
139
|
+
environment_path.write_text(new_text, encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
return CondaFixResult(
|
|
142
|
+
file_path=environment_path,
|
|
143
|
+
changed=changed,
|
|
144
|
+
message="environment.yml cleaned successfully.",
|
|
145
|
+
)
|
simfix/cuda_docker.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from simfix.system import get_system_info
|
|
7
|
+
|
|
8
|
+
GPU_KEYWORDS = {
|
|
9
|
+
"cuda",
|
|
10
|
+
"cudnn",
|
|
11
|
+
"cupy",
|
|
12
|
+
"nvidia",
|
|
13
|
+
"nvcc",
|
|
14
|
+
"torch",
|
|
15
|
+
"tensorflow",
|
|
16
|
+
"numba.cuda",
|
|
17
|
+
"jax[cuda",
|
|
18
|
+
"onnxruntime-gpu",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class CudaDockerFixResult:
|
|
24
|
+
"""Result of creating a CUDA Dockerfile."""
|
|
25
|
+
|
|
26
|
+
file_path: Path
|
|
27
|
+
changed: bool
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_text_if_exists(path: Path) -> str:
|
|
32
|
+
"""Read text from a file if it exists."""
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
return path.read_text(encoding="utf-8", errors="ignore")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _repo_has_cuda_files(repo_path: Path) -> bool:
|
|
40
|
+
"""Return True if repository contains CUDA source files."""
|
|
41
|
+
cuda_suffixes = {".cu", ".cuh"}
|
|
42
|
+
|
|
43
|
+
for file_path in repo_path.rglob("*"):
|
|
44
|
+
if ".git" in file_path.parts:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if file_path.is_file() and file_path.suffix.lower() in cuda_suffixes:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def detect_gpu_project(repo_path: str | Path) -> bool:
|
|
54
|
+
"""Detect whether a repository likely needs GPU/CUDA support."""
|
|
55
|
+
path = Path(repo_path).expanduser().resolve()
|
|
56
|
+
|
|
57
|
+
searchable_text = "\n".join(
|
|
58
|
+
[
|
|
59
|
+
_read_text_if_exists(path / "requirements.txt"),
|
|
60
|
+
_read_text_if_exists(path / "pyproject.toml"),
|
|
61
|
+
_read_text_if_exists(path / "environment.yml"),
|
|
62
|
+
_read_text_if_exists(path / "environment.yaml"),
|
|
63
|
+
_read_text_if_exists(path / "README.md"),
|
|
64
|
+
_read_text_if_exists(path / "readme.md"),
|
|
65
|
+
_read_text_if_exists(path / "CMakeLists.txt"),
|
|
66
|
+
]
|
|
67
|
+
).lower()
|
|
68
|
+
|
|
69
|
+
if any(keyword in searchable_text for keyword in GPU_KEYWORDS):
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
return _repo_has_cuda_files(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _gpu_status_message() -> str:
|
|
76
|
+
"""Return host GPU/driver status message."""
|
|
77
|
+
system_info = get_system_info()
|
|
78
|
+
|
|
79
|
+
if system_info.nvidia_gpu_available:
|
|
80
|
+
return (
|
|
81
|
+
" NVIDIA GPU detected"
|
|
82
|
+
f" with driver {system_info.nvidia_driver_version or 'unknown'}"
|
|
83
|
+
f" and CUDA {system_info.nvidia_cuda_version or 'unknown'}."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
" No NVIDIA GPU/driver was detected on this machine. "
|
|
88
|
+
"GPU containers will need a working NVIDIA driver and "
|
|
89
|
+
"NVIDIA Container Toolkit on the host."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cuda_dockerfile(has_requirements: bool) -> str:
|
|
94
|
+
"""Return a CUDA Dockerfile."""
|
|
95
|
+
install_requirements = ""
|
|
96
|
+
|
|
97
|
+
if has_requirements:
|
|
98
|
+
install_requirements = """
|
|
99
|
+
|
|
100
|
+
COPY requirements.txt /workspace/requirements.txt
|
|
101
|
+
RUN python3 -m pip install --upgrade pip && \\
|
|
102
|
+
python3 -m pip install -r /workspace/requirements.txt
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
return f"""FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
|
|
106
|
+
|
|
107
|
+
SHELL ["/bin/bash", "-c"]
|
|
108
|
+
|
|
109
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
110
|
+
|
|
111
|
+
RUN apt-get update && apt-get install -y \\
|
|
112
|
+
build-essential \\
|
|
113
|
+
cmake \\
|
|
114
|
+
git \\
|
|
115
|
+
python3 \\
|
|
116
|
+
python3-pip \\
|
|
117
|
+
python3-venv \\
|
|
118
|
+
libgl1 \\
|
|
119
|
+
libglib2.0-0 \\
|
|
120
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
121
|
+
|
|
122
|
+
WORKDIR /workspace
|
|
123
|
+
{install_requirements}
|
|
124
|
+
COPY . /workspace
|
|
125
|
+
|
|
126
|
+
CMD ["/bin/bash"]
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_cuda_dockerfile(repo_path: str | Path) -> CudaDockerFixResult | None:
|
|
131
|
+
"""Create a CUDA Dockerfile for GPU projects.
|
|
132
|
+
|
|
133
|
+
This creates Dockerfile only when GPU/CUDA clues exist and Dockerfile is missing.
|
|
134
|
+
"""
|
|
135
|
+
path = Path(repo_path).expanduser().resolve()
|
|
136
|
+
|
|
137
|
+
if not detect_gpu_project(path):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
dockerfile_path = path / "Dockerfile"
|
|
141
|
+
|
|
142
|
+
if dockerfile_path.exists():
|
|
143
|
+
return CudaDockerFixResult(
|
|
144
|
+
file_path=dockerfile_path,
|
|
145
|
+
changed=False,
|
|
146
|
+
message=(
|
|
147
|
+
"Dockerfile already exists. SimFix did not overwrite it."
|
|
148
|
+
+ _gpu_status_message()
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
has_requirements = (path / "requirements.txt").exists()
|
|
153
|
+
|
|
154
|
+
dockerfile_path.write_text(
|
|
155
|
+
_cuda_dockerfile(has_requirements=has_requirements),
|
|
156
|
+
encoding="utf-8",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return CudaDockerFixResult(
|
|
160
|
+
file_path=dockerfile_path,
|
|
161
|
+
changed=True,
|
|
162
|
+
message="Created CUDA Dockerfile for GPU project." + _gpu_status_message(),
|
|
163
|
+
)
|
simfix/docker_runner.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from simfix.cuda_docker import detect_gpu_project
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class DockerRunFixResult:
|
|
11
|
+
"""Result of creating a Docker run helper script."""
|
|
12
|
+
|
|
13
|
+
file_path: Path
|
|
14
|
+
changed: bool
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _docker_run_script(image_name: str, use_gpu: bool) -> str:
|
|
19
|
+
"""Return a Docker build/run helper script."""
|
|
20
|
+
gpu_flag = " --gpus all" if use_gpu else ""
|
|
21
|
+
|
|
22
|
+
return f"""#!/usr/bin/env bash
|
|
23
|
+
set -e
|
|
24
|
+
|
|
25
|
+
IMAGE_NAME="{image_name}"
|
|
26
|
+
|
|
27
|
+
docker build -t "$IMAGE_NAME" .
|
|
28
|
+
|
|
29
|
+
docker run --rm -it{gpu_flag} \\
|
|
30
|
+
-v "$PWD:/workspace" \\
|
|
31
|
+
-w /workspace \\
|
|
32
|
+
"$IMAGE_NAME"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_docker_run_helper(repo_path: str | Path) -> DockerRunFixResult | None:
|
|
37
|
+
"""Create a Docker build/run helper script when Dockerfile exists."""
|
|
38
|
+
path = Path(repo_path).expanduser().resolve()
|
|
39
|
+
dockerfile_path = path / "Dockerfile"
|
|
40
|
+
|
|
41
|
+
if not dockerfile_path.exists():
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
script_path = path / "run_simfix_docker.sh"
|
|
45
|
+
|
|
46
|
+
if script_path.exists():
|
|
47
|
+
return DockerRunFixResult(
|
|
48
|
+
file_path=script_path,
|
|
49
|
+
changed=False,
|
|
50
|
+
message="Docker run helper already exists. SimFix did not overwrite it.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
image_name = f"simfix-{path.name}".lower().replace("_", "-")
|
|
54
|
+
use_gpu = detect_gpu_project(path)
|
|
55
|
+
|
|
56
|
+
script_path.write_text(
|
|
57
|
+
_docker_run_script(image_name=image_name, use_gpu=use_gpu),
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
script_path.chmod(0o755)
|
|
61
|
+
|
|
62
|
+
if use_gpu:
|
|
63
|
+
message = "Created Docker run helper with GPU support."
|
|
64
|
+
else:
|
|
65
|
+
message = "Created Docker run helper."
|
|
66
|
+
|
|
67
|
+
return DockerRunFixResult(
|
|
68
|
+
file_path=script_path,
|
|
69
|
+
changed=True,
|
|
70
|
+
message=message,
|
|
71
|
+
)
|