Unverified Commit ab9d8f7b authored by Keiven C's avatar Keiven C Committed by GitHub
Browse files

refactor: refactor to be tree-based code + feat: framework check (#2799)


Signed-off-by: default avatarKeiven Chang <keivenchang@users.noreply.github.com>
Co-authored-by: default avatarKeiven Chang <keivenchang@users.noreply.github.com>
parent 3c7c1d64
...@@ -100,7 +100,7 @@ if ! grep -q "# Unset empty tokens" ~/.bashrc; then ...@@ -100,7 +100,7 @@ if ! grep -q "# Unset empty tokens" ~/.bashrc; then
echo '[ -z "$SSH_AUTH_SOCK" ] && unset SSH_AUTH_SOCK' >> ~/.bashrc echo '[ -z "$SSH_AUTH_SOCK" ] && unset SSH_AUTH_SOCK' >> ~/.bashrc
fi fi
$HOME/dynamo/deploy/dynamo_check.py --import-check-only $HOME/dynamo/deploy/dynamo_check.py
{ set +x; } 2>/dev/null { set +x; } 2>/dev/null
......
...@@ -3,57 +3,74 @@ ...@@ -3,57 +3,74 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
""" """
dynamo package checker, Python import tester, and usage guide. Dynamo System Information Checker
Combines version checking, import testing, and usage examples into a single tool. A comprehensive diagnostic tool that displays system configuration and Dynamo project status
Features dynamic component discovery and comprehensive troubleshooting guidance. in a hierarchical tree format. This script checks for:
Usage: - System resources (OS, CPU, memory, GPU)
dynamo_check.py # Run all checks - Development tools (Cargo/Rust, Maturin, Python)
dynamo_check.py --import-check-only # Only test imports - LLM frameworks (vllm, sglang, tensorrt_llm)
dynamo_check.py --examples # Only show examples - Dynamo runtime and framework components
dynamo_check.py --try-pythonpath # Test imports with workspace paths - Installation status and component availability
dynamo_check.py --help # Show help
The output uses status indicators:
Outputs: - ✅ Component found and working
System info (hostname: jensen-linux): - ❌ Component missing or error
├─ OS: Ubuntu 24.04.1 LTS (Noble Numbat) (Linux 6.11.0-28-generic x86_64); Memory: 30.9/125.5 GiB; Cores: 32 - ⚠️ Warning condition
├─ NVIDIA GPU: NVIDIA RTX 6000 Ada Generation (driver 570.133.07, CUDA 12.8); Power: 28.20/300.00 W; Memory: 2/49140 MiB - ❓ Component not found (for optional items)
├─ Cargo (/usr/local/cargo/bin/cargo, cargo 1.87.0 (99624be96 2025-05-06))
├─ Cargo home directory: $HOME/dynamo/.build/.cargo (CARGO_HOME is set) Exit codes:
└─ Cargo target directory: $HOME/dynamo/.build/target (CARGO_TARGET_DIR is set) - 0: All critical components are present
├─ Debug: $HOME/dynamo/.build/target/debug (modified: 2025-08-14 16:47:13 PDT) - 1: One or more errors detected (❌ status)
├─ Release: $HOME/dynamo/.build/target/release (modified: 2025-08-14 15:38:39 PDT)
└─ Binary: $HOME/dynamo/.build/target/debug/libdynamo_llm_capi.so (modified: 2025-08-14 16:45:31 PDT) Example output:
├─ Maturin (/opt/dynamo/venv/bin/maturin, maturin 1.9.3)
├─ Python: 3.12.3 (/opt/dynamo/venv/bin/python3) System info (hostname=jensen-linux, IP=10.111.122.133)
├─ Torch: 2.7.1+cu126 (✅torch.cuda.is_available()) ├─ OS Ubuntu 24.04.1 LTS (Noble Numbat) (Linux 6.11.0-28-generic x86_64), Memory=26.7/125.5 GiB, Cores=32
└─ PYTHONPATH: /home/ubuntu/dynamo/components/planner/src ├─ ✅ NVIDIA GPU NVIDIA RTX 6000 Ada Generation, driver 570.133.07, CUDA 12.8, Power=26.14/300.00 W, Memory=289/49140 MiB
└─ Dynamo ($HOME/dynamo, SHA: b0d4499f2a8c, Date: 2025-08-18 11:55:00 PDT): ├─ ✅ Cargo /usr/local/cargo/bin/cargo, cargo 1.89.0 (c24e10642 2025-06-23)
└─ Runtime components (ai-dynamo-runtime 0.4.0): │ ├─ cargo home directory $HOME/dynamo/.build/.cargo (CARGO_HOME is set)
├─ /opt/dynamo/venv/lib/python3.12/site-packages/ai_dynamo_runtime-0.4.0.dist-info (created: 2025-08-14 16:47:15 PDT) │ └─ cargo target directory $HOME/dynamo/.build/target (CARGO_TARGET_DIR is set)
├─ /opt/dynamo/venv/lib/python3.12/site-packages/ai_dynamo_runtime.pth (modified: 2025-08-14 16:47:15 PDT) │ ├─ Debug $HOME/dynamo/.build/target/debug, modified=2025-08-30 16:26:49 PDT
└─ Points to: $HOME/dynamo/lib/bindings/python/src │ ├─ Release $HOME/dynamo/.build/target/release, modified=2025-08-30 18:21:12 PDT
├─ ✅ dynamo._core $HOME/dynamo/lib/bindings/python/src/dynamo/_core.cpython-312-x86_64-linux-gnu.so (modified: 2025-08-14 16:47:15 PDT) │ └─ Binary $HOME/dynamo/.build/target/debug/libdynamo_llm_capi.so, modified=2025-08-30 16:25:37 PDT
├─ ✅ dynamo.nixl_connect $HOME/dynamo/lib/bindings/python/src/dynamo/nixl_connect/__init__.py ├─ ✅ Maturin /opt/dynamo/venv/bin/maturin, maturin 1.9.3
├─ ✅ dynamo.llm $HOME/dynamo/lib/bindings/python/src/dynamo/llm/__init__.py ├─ ✅ Python 3.12.3, /opt/dynamo/venv/bin/python
└─ ✅ dynamo.runtime $HOME/dynamo/lib/bindings/python/src/dynamo/runtime/__init__.py │ ├─ ✅ PyTorch 2.7.1+cu128, ✅torch.cuda.is_available
└─ Framework components (ai-dynamo 0.4.0): │ └─ PYTHONPATH $HOME/dynamo/components/frontend/src:$HOME/dynamo/components/planner/src:$HOME/dynamo/components/backends/vllm/src:$HOME/dynamo/components/backends/sglang/src:$HOME/dynamo/components/backends/trtllm/src:$HOME/dynamo/components/backends/llama_cpp/src:$HOME/dynamo/components/backends/mocker/src
├─ /opt/dynamo/venv/lib/python3.12/site-packages/ai_dynamo-0.4.0.dist-info (created: 2025-08-14 16:47:16 PDT) ├─ 🤖Framework
├─ /opt/dynamo/venv/lib/python3.12/site-packages/_ai_dynamo.pth (modified: 2025-08-14 16:47:16 PDT) │ ├─ ✅ vllm 0.10.1.1, module=/opt/vllm/vllm/__init__.py, exec=/opt/dynamo/venv/bin/vllm
└─ Points to: $HOME/dynamo/components/backends/vllm/src │ ├─ ❓ sglang -
│ └─ ❓ tensorrt_llm -
└─ Dynamo $HOME/dynamo, SHA: a03d29066, Date: 2025-08-30 16:22:29 PDT
├─ ✅ Runtime components ai-dynamo-runtime 0.4.1
│ ├─ /opt/dynamo/venv/lib/python3.12/site-packages/ai_dynamo_runtime-0.4.1.dist-info created=2025-08-30 19:14:29 PDT
│ ├─ /opt/dynamo/venv/lib/python3.12/site-packages/ai_dynamo_runtime.pth modified=2025-08-30 19:14:29 PDT
│ │ └─ → $HOME/dynamo/lib/bindings/python/src
│ ├─ ✅ dynamo._core $HOME/dynamo/lib/bindings/python/src/dynamo/_core.cpython-312-x86_64-linux-gnu.so, modified=2025-08-30 19:14:29 PDT
│ ├─ ✅ dynamo.logits_processing $HOME/dynamo/lib/bindings/python/src/dynamo/logits_processing/__init__.py
│ ├─ ✅ dynamo.nixl_connect $HOME/dynamo/lib/bindings/python/src/dynamo/nixl_connect/__init__.py
│ ├─ ✅ dynamo.llm $HOME/dynamo/lib/bindings/python/src/dynamo/llm/__init__.py
│ └─ ✅ dynamo.runtime $HOME/dynamo/lib/bindings/python/src/dynamo/runtime/__init__.py
└─ ✅ Framework components ai-dynamo (via PYTHONPATH)
├─ ✅ dynamo.frontend $HOME/dynamo/components/frontend/src/dynamo/frontend/__init__.py ├─ ✅ dynamo.frontend $HOME/dynamo/components/frontend/src/dynamo/frontend/__init__.py
├─ ✅ dynamo.planner $HOME/dynamo/components/planner/src/dynamo/planner/__init__.py ├─ ✅ dynamo.llama_cpp $HOME/dynamo/components/backends/llama_cpp/src/dynamo/llama_cpp/__init__.py
├─ ✅ dynamo.mocker $HOME/dynamo/components/backends/mocker/src/dynamo/mocker/__init__.py ├─ ✅ dynamo.mocker $HOME/dynamo/components/backends/mocker/src/dynamo/mocker/__init__.py
├─ ✅ dynamo.trtllm $HOME/dynamo/components/backends/trtllm/src/dynamo/trtllm/__init__.py ├─ ✅ dynamo.planner $HOME/dynamo/components/planner/src/dynamo/planner/__init__.py
├─ ✅ dynamo.vllm $HOME/dynamo/components/backends/vllm/src/dynamo/vllm/__init__.py
├─ ✅ dynamo.sglang $HOME/dynamo/components/backends/sglang/src/dynamo/sglang/__init__.py ├─ ✅ dynamo.sglang $HOME/dynamo/components/backends/sglang/src/dynamo/sglang/__init__.py
└─ ✅ dynamo.llama_cpp $HOME/dynamo/components/backends/llama_cpp/src/dynamo/llama_cpp/__init__.py ├─ ✅ dynamo.trtllm $HOME/dynamo/components/backends/trtllm/src/dynamo/trtllm/__init__.py
└─ ✅ dynamo.vllm $HOME/dynamo/components/backends/vllm/src/dynamo/vllm/__init__.py
Usage:
python dynamo_check.py [--fast]
Options:
--fast Skip directory size calculations for faster output
""" """
import argparse
import datetime import datetime
import importlib.metadata import glob
import json import json
import logging import logging
import os import os
...@@ -61,248 +78,212 @@ import platform ...@@ -61,248 +78,212 @@ import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from zoneinfo import ZoneInfo
class NVIDIAGPUDetector: class NodeStatus(Enum):
"""Handles NVIDIA GPU detection and information gathering.""" """Status of a tree node"""
def find_nvidia_smi(self) -> Optional[str]: OK = "ok" # ✅ Success/available
"""Find nvidia-smi executable.""" ERROR = "error" # ❌ Error/not found
nvsmi = shutil.which("nvidia-smi") WARNING = "warn" # ⚠️ Warning
if not nvsmi: INFO = "info" # No symbol, just information
for candidate in [ NONE = "none" # No status indicator
"/usr/bin/nvidia-smi", UNKNOWN = "unknown" # ❓ Unknown/not found
"/usr/local/bin/nvidia-smi",
"/usr/local/nvidia/bin/nvidia-smi",
]:
if os.path.exists(candidate) and os.access(candidate, os.X_OK):
return candidate
return nvsmi
def get_nvidia_gpu_names(self, nvsmi: str) -> Tuple[List[str], bool]:
"""Get list of NVIDIA GPU names and whether nvidia-smi succeeded.
Returns: @dataclass
Tuple of (gpu_names_list, nvidia_smi_succeeded) class NodeInfo:
""" """Base class for all information nodes in the tree structure"""
try:
proc = subprocess.run(
[nvsmi, "-L"], capture_output=True, text=True, timeout=10
)
if proc.returncode == 0:
names = []
if proc.stdout:
for line in proc.stdout.splitlines():
line = line.strip()
# Example: "GPU 0: NVIDIA A100-SXM4-40GB (UUID: GPU-...)"
if ":" in line:
part = line.split(":", 1)[1].strip()
# Take up to first parenthesis for clean model name
name_only = part.split("(")[0].strip()
names.append(name_only)
return names, True
else:
# Collect and surface error details (e.g. "Failed to initialize NVML: Unknown Error")
errors: List[str] = []
if proc.stderr:
for line in proc.stderr.splitlines():
line = line.strip()
if line:
errors.append(line)
if not errors and proc.stdout:
for line in proc.stdout.splitlines():
line = line.strip()
if line:
errors.append(line)
if errors:
# Return the first error line to display concisely upstream
return [errors[0]], False
return [], False
except Exception:
return [], False
def get_nvidia_driver_cuda_versions(self, nvsmi: str) -> Tuple[str, str]: # Core properties
"""Get NVIDIA driver and CUDA versions. label: str # Main text/description
desc: Optional[str] = None # Primary value/description
status: NodeStatus = NodeStatus.NONE # Status indicator
Returns: # Additional metadata as key-value pairs
Tuple of (driver_version, cuda_version) metadata: Dict[str, str] = field(default_factory=dict)
"""
driver, cuda = "?", "?"
try:
# Try query method first
proc = subprocess.run(
[
nvsmi,
"--query-gpu=driver_version,cuda_version",
"--format=csv,noheader",
],
capture_output=True,
text=True,
timeout=10,
)
if proc.returncode == 0 and proc.stdout.strip():
parts = proc.stdout.strip().splitlines()[0].split(",")
if len(parts) >= 1:
driver = parts[0].strip()
if len(parts) >= 2:
cuda = parts[1].strip()
else:
# Fallback: parse banner using regex instead of structured query
#
# Why regex fallback instead of command line query:
# 1. Compatibility: Some older nvidia-smi versions don't support
# --query-gpu with cuda_version field
# 2. Robustness: The banner output is more stable across different
# nvidia-smi versions and driver releases
# 3. Error handling: If the structured query fails (e.g., due to
# driver issues, permission problems, or unsupported fields),
# the banner parsing provides a reliable alternative
# 4. Case variations: Different nvidia-smi versions may output
# "Driver Version" vs "driver version" vs "DRIVER VERSION"
proc = subprocess.run(
[nvsmi], capture_output=True, text=True, timeout=10
)
if proc.returncode == 0 and proc.stdout:
import re
m = re.search( # Tree structure
r"Driver Version:\s*([0-9.]+)", proc.stdout, re.IGNORECASE children: List["NodeInfo"] = field(default_factory=list)
)
if m:
driver = m.group(1)
m = re.search(
r"CUDA Version:\s*([0-9.]+)", proc.stdout, re.IGNORECASE
)
if m:
cuda = m.group(1)
except Exception:
pass
return driver, cuda
def get_nvidia_power_memory_all(self, nvsmi: str, gpu_count: int) -> List[str]: # Display control
"""Get NVIDIA GPU power and memory info for all GPUs. show_symbol: bool = True # Whether to show status symbol
Returns: def add_child(self, child: "NodeInfo") -> "NodeInfo":
List of formatted strings for each GPU """Add a child node and return it for chaining"""
""" self.children.append(child)
try: return child
proc = subprocess.run(
[
nvsmi,
"--query-gpu=power.draw,power.limit,memory.used,memory.total",
"--format=csv,noheader,nounits",
],
capture_output=True,
text=True,
timeout=10,
)
if proc.returncode != 0 or not proc.stdout.strip():
return [""] * gpu_count
lines = proc.stdout.strip().splitlines() def add_metadata(self, key: str, value: str) -> "NodeInfo":
gpu_infos = [] """Add metadata key-value pair"""
self.metadata[key] = value
return self
for i, line in enumerate(lines[:gpu_count]): # Limit to expected GPU count def render(
parts = line.split(",") self, prefix: str = "", is_last: bool = True, is_root: bool = True
if len(parts) < 4: ) -> List[str]:
gpu_infos.append("") """Render the tree node and its children as a list of strings"""
continue lines = []
power_draw = parts[0].strip() if parts[0].strip() else "?" # Determine the connector
power_limit = parts[1].strip() if parts[1].strip() else "?" if not is_root:
mem_used = parts[2].strip() if parts[2].strip() else "?" connector = "└─" if is_last else "├─"
mem_total = parts[3].strip() if parts[3].strip() else "?" current_prefix = prefix + connector + " "
else:
current_prefix = ""
# Build the line content
line_parts = []
# Add status symbol
if self.show_symbol and self.status != NodeStatus.NONE:
if self.status == NodeStatus.OK:
line_parts.append("✅")
elif self.status == NodeStatus.ERROR:
line_parts.append("❌")
elif self.status == NodeStatus.WARNING:
line_parts.append("⚠️")
elif self.status == NodeStatus.UNKNOWN:
line_parts.append("❓")
# Add label and value
if self.desc:
line_parts.append(f"{self.label} {self.desc}")
else:
line_parts.append(self.label)
# Add metadata inline - consistent format for all
if self.metadata:
metadata_items = []
for k, v in self.metadata.items():
# Format all metadata consistently as "key=value"
metadata_items.append(f"{k}={v}")
if metadata_items:
# Use consistent separator (comma) for all metadata
metadata_str = ", ".join(metadata_items)
line_parts[-1] += f", {metadata_str}"
# Construct the full line
line_content = " ".join(line_parts)
if current_prefix or line_content:
lines.append(current_prefix + line_content)
# Render children
for i, child in enumerate(self.children):
is_last_child = i == len(self.children) - 1
if is_root:
child_prefix = ""
else:
child_prefix = prefix + (" " if is_last else "│ ")
lines.extend(child.render(child_prefix, is_last_child, False))
info_parts = [] return lines
if power_draw != "?" or power_limit != "?":
info_parts.append(f"Power: {power_draw}/{power_limit} W")
if mem_used != "?" and mem_total != "?": def print_tree(self) -> None:
# Add warning symbol if GPU memory usage is 90% or higher """Print the tree to console"""
warning_symbol = "" for line in self.render():
try: print(line)
mem_usage_percent = (float(mem_used) / float(mem_total)) * 100
warning_symbol = " ⚠️" if mem_usage_percent >= 90 else ""
except (ValueError, ZeroDivisionError):
pass
info_parts.append(
f"Memory: {mem_used}/{mem_total} MiB{warning_symbol}"
)
gpu_infos.append("; " + "; ".join(info_parts) if info_parts else "") def has_errors(self) -> bool:
"""Check if this node or any of its children have errors"""
# Check if this node has an error
if self.status == NodeStatus.ERROR:
return True
# Fill remaining slots if we got fewer results than expected # Recursively check all children
while len(gpu_infos) < gpu_count: for child in self.children:
gpu_infos.append("") if child.has_errors():
return True
return gpu_infos return False
except Exception:
return [""] * gpu_count
def get_gpu_info(self) -> Tuple[List[str], Optional[str], Optional[str]]: def _replace_home_with_var(self, path: str) -> str:
"""Get NVIDIA GPU information. """Replace home directory with $HOME in path."""
home = os.path.expanduser("~")
if path.startswith(home):
return path.replace(home, "$HOME", 1)
return path
Returns: def _format_timestamp_pdt(self, timestamp: float) -> str:
Tuple of (gpu_lines_list, driver_version, cuda_version) """Format timestamp as PDT time string."""
""" dt_utc = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
nvsmi = self.find_nvidia_smi() # Convert to PDT (UTC-7)
if not nvsmi: dt_pdt = dt_utc - datetime.timedelta(hours=7)
return ["❌ NVIDIA GPU: nvidia-smi not found"], None, None return dt_pdt.strftime("%Y-%m-%d %H:%M:%S PDT")
names_or_errors, nvsmi_succeeded = self.get_nvidia_gpu_names(nvsmi)
if not nvsmi_succeeded: class SystemInfo(NodeInfo):
# If error details were captured, display them directly """Root node for system information"""
if names_or_errors:
return [f"❌ NVIDIA GPU: {names_or_errors[0]}"], None, None def __init__(self, hostname: Optional[str] = None, fast_mode: bool = False):
return ["❌ NVIDIA GPU: nvidia-smi failed"], None, None self.fast_mode = fast_mode
if hostname is None:
driver, cuda = self.get_nvidia_driver_cuda_versions(nvsmi) hostname = platform.node()
# Format GPU lines # Get IP address
names = names_or_errors ip_address = self._get_ip_address()
if not names:
# Treat zero GPUs as an error condition
return (
[f"❌ NVIDIA GPU: not detected (driver {driver}, CUDA {cuda})"],
driver,
cuda,
)
if len(names) == 1: # Format label with hostname and IP
# Single GPU - keep compact format if ip_address:
power_mem_infos = self.get_nvidia_power_memory_all(nvsmi, 1) label = f"System info (hostname={hostname}, IP={ip_address})"
gpu_line = f"NVIDIA GPU: {names[0]} (driver {driver}, CUDA {cuda}){power_mem_infos[0]}"
return [gpu_line], driver, cuda
else: else:
# Multiple GPUs - show each individually label = f"System info (hostname={hostname})"
power_mem_infos = self.get_nvidia_power_memory_all(nvsmi, len(names))
gpu_lines = [] super().__init__(label=label, status=NodeStatus.INFO)
for i, name in enumerate(names):
power_mem_info = power_mem_infos[i] if i < len(power_mem_infos) else "" # Suppress Prometheus endpoint warnings from planner module
gpu_line = f"NVIDIA GPU {i}: {name} (driver {driver}, CUDA {cuda}){power_mem_info}"
gpu_lines.append(gpu_line)
return gpu_lines, driver, cuda
class DynamoChecker:
"""Comprehensive dynamo package checker."""
def __init__(self, workspace_dir: Optional[str] = None) -> None:
# If a path is provided, use it directly; otherwise discover
self.workspace_dir = (
os.path.abspath(workspace_dir) if workspace_dir else self._find_workspace()
)
self.results: Dict[str, Any] = {}
self._suppress_planner_warnings() self._suppress_planner_warnings()
# Collect warnings that should be printed later (after specific headers)
self._deferred_messages: List[str] = [] # Collect and add all system information
# Initialize NVIDIA GPU detector # Add OS info
self.gpu_detector = NVIDIAGPUDetector() self.add_child(OSInfo())
# Track whether GPU issues were detected (nvidia-smi failure or zero GPUs)
self._gpu_error: bool = False # Add GPU info
gpu_info = GPUInfo()
# Always add GPU info so we can see errors like "nvidia-smi not found"
self.add_child(gpu_info)
# Add Cargo (always show, even if not found)
self.add_child(CargoInfo(fast_mode=self.fast_mode))
# Add Maturin (Python-Rust build tool)
self.add_child(MaturinInfo())
# Add Python info
self.add_child(PythonInfo())
# Add Framework info (vllm, sglang, tensorrt_llm)
self.add_child(FrameworkInfo())
# Add Dynamo workspace info (always show, even if not found)
self.add_child(DynamoInfo(fast_mode=self.fast_mode))
def _get_ip_address(self) -> Optional[str]:
"""Get the primary IP address of the system."""
try:
import socket
# Get hostname
hostname = socket.gethostname()
# Get IP address
ip_address = socket.gethostbyname(hostname)
# Filter out localhost
if ip_address.startswith("127."):
# Try to get external IP by connecting to a public DNS
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# Connect to Google DNS (doesn't actually send data)
s.connect(("8.8.8.8", 80))
ip_address = s.getsockname()[0]
finally:
s.close()
return ip_address
except Exception:
return None
def _suppress_planner_warnings(self) -> None: def _suppress_planner_warnings(self) -> None:
"""Suppress Prometheus endpoint warnings from planner module during import testing.""" """Suppress Prometheus endpoint warnings from planner module during import testing."""
...@@ -310,1316 +291,1371 @@ class DynamoChecker: ...@@ -310,1316 +291,1371 @@ class DynamoChecker:
# outside of a Kubernetes cluster. Suppress this for cleaner output. # outside of a Kubernetes cluster. Suppress this for cleaner output.
planner_logger = logging.getLogger("dynamo.planner.defaults") planner_logger = logging.getLogger("dynamo.planner.defaults")
planner_logger.setLevel(logging.ERROR) planner_logger.setLevel(logging.ERROR)
# Also suppress the defaults._get_default_prometheus_endpoint logger
defaults_logger = logging.getLogger("defaults._get_default_prometheus_endpoint")
defaults_logger.setLevel(logging.ERROR)
# ====================================================================
# WORKSPACE AND COMPONENT DISCOVERY
# ====================================================================
def _find_workspace(self) -> str:
"""Find dynamo workspace directory.
Returns:
Path to workspace directory or empty string if not found
Example: '.' (if current dir), '/home/ubuntu/dynamo', '/workspace', or ''
Note: Checks local path first, then common locations. Validates by looking for README.md file.
"""
candidates = [
".", # Current directory (local path)
os.path.expanduser("~/dynamo"),
"/workspace",
"/home/ubuntu/dynamo",
]
for candidate in candidates: class OSInfo(NodeInfo):
if self._is_dynamo_workspace(candidate): """Operating system information"""
# Always return absolute path for consistent $HOME replacement
return os.path.abspath(candidate)
return ""
def _is_dynamo_workspace(self, path: str) -> bool: def __init__(self):
"""Check if a directory is a dynamo workspace by looking for characteristic files/directories. # Collect OS information
uname = platform.uname()
Args: # Try to get distribution info
path: Directory path to check distro = ""
version = ""
try:
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
for line in f:
if line.startswith("NAME="):
distro = line.split("=", 1)[1].strip().strip('"')
elif line.startswith("VERSION="):
version = line.split("=", 1)[1].strip().strip('"')
except Exception:
pass
Returns: # Get memory info
True if directory appears to be a dynamo workspace mem_used_gb = None
mem_total_gb = None
try:
with open("/proc/meminfo", "r") as f:
meminfo = {}
for line in f:
if ":" in line:
k, v = line.split(":", 1)
meminfo[k.strip()] = v.strip()
Note: Checks for multiple indicators like README.md, components/, lib/bindings/, lib/runtime/, Cargo.toml, etc. if "MemTotal" in meminfo and "MemAvailable" in meminfo:
""" total_kb = float(meminfo["MemTotal"].split()[0])
if not os.path.exists(path): avail_kb = float(meminfo["MemAvailable"].split()[0])
return False mem_used_gb = (total_kb - avail_kb) / (1024 * 1024)
mem_total_gb = total_kb / (1024 * 1024)
except Exception:
pass
# Check for characteristic dynamo workspace files and directories # Get CPU cores
indicators = [ cores = os.cpu_count()
"README.md",
"components",
"lib/bindings/python",
"lib/runtime",
"Cargo.toml",
]
# Require at least 3 indicators to be confident it's a dynamo workspace # Build the value string
found_indicators = 0 if distro:
for indicator in indicators: value = f"{distro} {version} ({uname.system} {uname.release} {uname.machine})".strip()
if os.path.exists(os.path.join(path, indicator)): else:
found_indicators += 1 value = f"{uname.system} {uname.release} {uname.machine}"
return found_indicators >= 4 super().__init__(label="OS", desc=value, status=NodeStatus.INFO)
def _discover_runtime_components(self) -> List[str]: # Add memory and cores as metadata
"""Discover ai-dynamo-runtime components from filesystem. if mem_used_gb is not None and mem_total_gb is not None:
self.add_metadata("Memory", f"{mem_used_gb:.1f}/{mem_total_gb:.1f} GiB")
if mem_total_gb > 0 and (mem_used_gb / mem_total_gb) >= 0.9:
self.status = NodeStatus.WARNING
if cores:
self.add_metadata("Cores", str(cores))
Returns:
List of runtime component module names
Example: ['dynamo._core', 'dynamo.nixl_connect', 'dynamo.llm', 'dynamo.runtime']
Note: Always includes 'dynamo._core' (compiled Rust module), then scans class GPUInfo(NodeInfo):
lib/bindings/python/src/dynamo/ for additional components. """NVIDIA GPU information"""
"""
components = ["dynamo._core"] # Always include compiled Rust module
if not self.workspace_dir: def __init__(self):
return components # Find nvidia-smi executable (check multiple paths)
nvidia_smi = shutil.which("nvidia-smi")
if not nvidia_smi:
# Check common paths if `which` fails
for candidate in [
"/usr/bin/nvidia-smi",
"/usr/local/bin/nvidia-smi",
"/usr/local/nvidia/bin/nvidia-smi",
]:
if os.path.exists(candidate) and os.access(candidate, os.X_OK):
nvidia_smi = candidate
break
# Scan runtime components (llm, runtime, nixl_connect, etc.) if not nvidia_smi:
# Examples: lib/bindings/python/src/dynamo/{llm,runtime,nixl_connect}/__init__.py super().__init__(
runtime_path = f"{self.workspace_dir}/lib/bindings/python/src/dynamo" label="NVIDIA GPU", desc="nvidia-smi not found", status=NodeStatus.ERROR
if not os.path.exists(runtime_path):
print(
f"⚠️ Warning: Runtime components directory not found: {runtime_path}"
) )
return components return
for item in os.listdir(runtime_path):
item_path = os.path.join(runtime_path, item)
if os.path.isdir(item_path) and os.path.exists(f"{item_path}/__init__.py"):
components.append(f"dynamo.{item}")
return components try:
# Get GPU list
result = subprocess.run(
[nvidia_smi, "-L"], capture_output=True, text=True, timeout=10
)
def _discover_framework_components(self) -> List[str]: if result.returncode != 0:
"""Discover ai-dynamo framework components from filesystem. # Capture error details from stderr or stdout
error_msg = "nvidia-smi failed"
if result.stderr and result.stderr.strip():
# Get first line of error for concise display
error_lines = result.stderr.strip().splitlines()
if error_lines:
error_msg = error_lines[0].strip()
# Make NVML error more user-friendly
if "Failed to initialize NVML" in error_msg:
error_msg = (
"No NVIDIA GPU detected (NVML initialization failed)"
)
elif result.stdout and result.stdout.strip():
error_lines = result.stdout.strip().splitlines()
if error_lines:
error_msg = error_lines[0].strip()
# Make NVML error more user-friendly
if "Failed to initialize NVML" in error_msg:
error_msg = (
"No NVIDIA GPU detected (NVML initialization failed)"
)
super().__init__(
label="NVIDIA GPU", desc=error_msg, status=NodeStatus.ERROR
)
return
Returns: # Parse GPU names
List of framework component module names gpu_names = []
Example: ['dynamo.frontend', 'dynamo.planner', 'dynamo.vllm', 'dynamo.sglang', 'dynamo.llama_cpp'] lines = result.stdout.strip().splitlines()
for line in lines:
# Example: "GPU 0: NVIDIA A100-SXM4-40GB (UUID: GPU-...)"
if ":" in line:
gpu_name = line.split(":", 1)[1].split("(")[0].strip()
gpu_names.append(gpu_name)
# Check for zero GPUs
if not gpu_names:
# Get driver and CUDA even for zero GPUs
driver, cuda = self._get_driver_cuda_versions(nvidia_smi)
driver_cuda_str = ""
if driver or cuda:
parts = []
if driver:
parts.append(f"driver {driver}")
if cuda:
parts.append(f"CUDA {cuda}")
driver_cuda_str = f", {', '.join(parts)}"
super().__init__(
label="NVIDIA GPU",
desc=f"not detected{driver_cuda_str}",
status=NodeStatus.ERROR,
)
return
Note: Scans components/ and components/backends/ directories for modules with __init__.py files. # Get driver and CUDA versions
""" driver, cuda = self._get_driver_cuda_versions(nvidia_smi)
components: List[str] = []
# Handle single vs multiple GPUs
if len(gpu_names) == 1:
# Single GPU - compact format
value = gpu_names[0]
if driver or cuda:
driver_cuda = []
if driver:
driver_cuda.append(f"driver {driver}")
if cuda:
driver_cuda.append(f"CUDA {cuda}")
value += f", {', '.join(driver_cuda)}"
super().__init__(label="NVIDIA GPU", desc=value, status=NodeStatus.OK)
# Add power and memory metadata for single GPU
self._add_power_memory_info(nvidia_smi, 0)
else:
# Multiple GPUs - show count in main label
value = f"{len(gpu_names)} GPUs"
if driver or cuda:
driver_cuda = []
if driver:
driver_cuda.append(f"driver {driver}")
if cuda:
driver_cuda.append(f"CUDA {cuda}")
value += f", {', '.join(driver_cuda)}"
super().__init__(label="NVIDIA GPU", desc=value, status=NodeStatus.OK)
# Add each GPU as a child node
for i, name in enumerate(gpu_names):
gpu_child = NodeInfo(
label=f"GPU {i}", desc=name, status=NodeStatus.OK
)
# Add power and memory for this specific GPU
power_mem = self._get_power_memory_string(nvidia_smi, i)
if power_mem:
gpu_child.add_metadata("Stats", power_mem)
self.add_child(gpu_child)
if not self.workspace_dir: except Exception:
return components super().__init__(
label="NVIDIA GPU", desc="detection failed", status=NodeStatus.ERROR
)
# Scan direct components (frontend, planner, etc.) def _get_driver_cuda_versions(
# Examples: components/{frontend,planner}/src/dynamo/{frontend,planner}/__init__.py self, nvidia_smi: str
comp_path = f"{self.workspace_dir}/components" ) -> Tuple[Optional[str], Optional[str]]:
if os.path.exists(comp_path): """Get NVIDIA driver and CUDA versions using query method."""
for item in os.listdir(comp_path): driver, cuda = None, None
item_path = os.path.join(comp_path, item) try:
if os.path.isdir(item_path) and os.path.exists( # Use query method for more reliable detection
f"{item_path}/src/dynamo/{item}/__init__.py" result = subprocess.run(
): [nvidia_smi, "--query-gpu=driver_version", "--format=csv,noheader"],
components.append(f"dynamo.{item}") capture_output=True,
else: text=True,
# Defer this message to print under the Dynamo header for alignment timeout=10,
self._deferred_messages.append(
f"⚠️ Warning: Components directory not found: {self._replace_home_with_var(comp_path)}"
) )
if result.returncode == 0 and result.stdout.strip():
driver = result.stdout.strip().splitlines()[0].strip()
# Scan backend components (vllm, sglang, etc.) # Try to get CUDA version from nvidia-smi output
# Examples: components/backends/{vllm,sglang,llama_cpp}/src/dynamo/{vllm,sglang,llama_cpp}/__init__.py result = subprocess.run(
backend_path = f"{self.workspace_dir}/components/backends" [nvidia_smi], capture_output=True, text=True, timeout=10
if os.path.exists(backend_path):
for item in os.listdir(backend_path):
item_path = os.path.join(backend_path, item)
if os.path.isdir(item_path) and os.path.exists(
f"{item_path}/src/dynamo/{item}/__init__.py"
):
components.append(f"dynamo.{item}")
else:
# Defer this message to print under the Dynamo header for alignment
self._deferred_messages.append(
f"⚠️ Warning: Backend components directory not found: {self._replace_home_with_var(backend_path)}"
) )
if result.returncode == 0:
import re
return components m = re.search(r"CUDA Version:\s*([0-9.]+)", result.stdout)
if m:
cuda = m.group(1)
except Exception:
pass
return driver, cuda
def _replace_home_with_var(self, path: str) -> str: def _add_power_memory_info(self, nvidia_smi: str, gpu_index: int = 0):
"""Replace user's home directory in path with $HOME. """Add power and memory metadata for a specific GPU."""
power_mem = self._get_power_memory_string(nvidia_smi, gpu_index)
if power_mem:
# Split into Power and Memory parts
if "; " in power_mem:
parts = power_mem.split("; ")
for part in parts:
if part.startswith("Power:"):
self.add_metadata("Power", part.replace("Power: ", ""))
elif part.startswith("Memory:"):
self.add_metadata("Memory", part.replace("Memory: ", ""))
def _get_power_memory_string(
self, nvidia_smi: str, gpu_index: int = 0
) -> Optional[str]:
"""Get power and memory info string for a specific GPU."""
try:
result = subprocess.run(
[
nvidia_smi,
"--query-gpu=power.draw,power.limit,memory.used,memory.total",
"--format=csv,noheader,nounits",
],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
lines = result.stdout.strip().splitlines()
if gpu_index < len(lines):
parts = lines[gpu_index].split(",")
if len(parts) >= 4:
power_draw = parts[0].strip()
power_limit = parts[1].strip()
mem_used = parts[2].strip()
mem_total = parts[3].strip()
Args: info_parts = []
path: File system path or colon-separated paths (for PYTHONPATH) if power_draw and power_limit:
info_parts.append(f"Power: {power_draw}/{power_limit} W")
Returns: if mem_used and mem_total:
Path with home directory replaced by $HOME if applicable # Add warning if memory usage is 90% or higher
Example: '/home/ubuntu/dynamo/...' -> '$HOME/dynamo/...' warning = ""
Example: '/home/ubuntu/dynamo/a:/home/ubuntu/dynamo/b' -> '$HOME/dynamo/a:$HOME/dynamo/b'
"""
home_dir = os.path.expanduser("~")
try: try:
# Replace all occurrences for colon-separated paths like PYTHONPATH if float(mem_used) / float(mem_total) >= 0.9:
return path.replace(home_dir, "$HOME") warning = " ⚠️"
except Exception: except Exception:
return path pass
info_parts.append(
f"Memory: {mem_used}/{mem_total} MiB{warning}"
)
def _format_timestamp_pdt(self, timestamp: float) -> str: if info_parts:
"""Format a timestamp in PDT timezone. return "; ".join(info_parts)
except Exception:
pass
return None
Args:
timestamp: Unix timestamp
Returns: class CargoInfo(NodeInfo):
Formatted timestamp string in PDT or local timezone """Cargo tool information"""
Example: '2025-08-10 22:22:52 PDT'
"""
try:
# Use zoneinfo (standard library in Python 3.9+)
pdt = ZoneInfo("America/Los_Angeles")
dt = datetime.datetime.fromtimestamp(timestamp, tz=pdt)
return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
except Exception:
# Fallback to manual PDT offset approximation
# PDT is UTC-7, so subtract 7 hours from UTC
dt_utc = datetime.datetime.utcfromtimestamp(timestamp)
dt_pdt = dt_utc - datetime.timedelta(hours=7)
return dt_pdt.strftime("%Y-%m-%d %H:%M:%S PDT")
def _get_cargo_info(self) -> Tuple[Optional[str], Optional[str]]: def __init__(self, fast_mode: bool = False):
"""Get cargo target directory and cargo home directory. self.fast_mode = fast_mode
cargo_path = shutil.which("cargo")
cargo_version = None
Returns: # Get cargo version
Tuple of (target_directory, cargo_home) or (None, None) if cargo not available if cargo_path:
Example: ('/home/ubuntu/dynamo/.build/target', '/home/ubuntu/.cargo')
"""
# First check if cargo is available
try: try:
subprocess.run( result = subprocess.run(
["cargo", "--version"], capture_output=True, text=True, timeout=5 ["cargo", "--version"], capture_output=True, text=True, timeout=5
) )
except (FileNotFoundError, subprocess.TimeoutExpired): if result.returncode == 0:
# Do not print here; caller will render a nicely aligned warning cargo_version = result.stdout.strip()
return None, None except Exception:
pass
if not cargo_path and not cargo_version:
super().__init__(
label="Cargo",
desc="not found, install Rust toolchain to see cargo target directory",
status=NodeStatus.ERROR,
)
return
# Initialize with cargo path and version
value = ""
if cargo_path:
value = cargo_path
if cargo_version:
value += f", {cargo_version}" if value else cargo_version
super().__init__(label="Cargo", desc=value, status=NodeStatus.OK)
# Get cargo home directory # Get cargo home directory
cargo_home = os.environ.get("CARGO_HOME") cargo_home = os.environ.get("CARGO_HOME")
if not cargo_home: if not cargo_home:
cargo_home = os.path.expanduser("~/.cargo") cargo_home = os.path.expanduser("~/.cargo")
if cargo_home and os.path.exists(cargo_home):
cargo_home_env = os.environ.get("CARGO_HOME")
display_cargo_home = self._replace_home_with_var(cargo_home)
home_value = display_cargo_home
if cargo_home_env:
home_value += " (CARGO_HOME is set)"
home_node = NodeInfo(
label="cargo home directory", desc=home_value, status=NodeStatus.INFO
)
self.add_child(home_node)
# Get cargo target directory # Get cargo target directory
target_directory = None cargo_target = self._get_cargo_target_directory()
if cargo_target and os.path.exists(cargo_target):
cargo_target_env = os.environ.get("CARGO_TARGET_DIR")
display_cargo_target = self._replace_home_with_var(cargo_target)
# Calculate total directory size (skip if fast mode)
size_str = ""
if not self.fast_mode:
total_size_gb = self._get_directory_size_gb(cargo_target)
size_str = (
f", {total_size_gb:.1f} GB" if total_size_gb is not None else ""
)
target_value = display_cargo_target + size_str
if cargo_target_env:
target_value += " (CARGO_TARGET_DIR is set)"
target_node = NodeInfo(
label="cargo target directory",
desc=target_value,
status=NodeStatus.INFO,
)
self.add_child(target_node)
# Add debug/release/binary info as children of target directory
self._add_build_info(target_node, cargo_target)
def _get_directory_size_gb(self, directory: str) -> Optional[float]:
"""Get the size of a directory in GB."""
try: try:
# Run cargo metadata command to get target directory # Use du command to get directory size in bytes
result = subprocess.run( result = subprocess.run(
["cargo", "metadata", "--format-version=1", "--no-deps"], ["du", "-sb", directory], capture_output=True, text=True, timeout=30
capture_output=True,
text=True,
timeout=10,
cwd=self.workspace_dir
if (self.workspace_dir and os.path.isdir(self.workspace_dir))
else None,
) )
if result.returncode == 0:
# Parse output: "size_in_bytes\tdirectory_path"
size_bytes = int(result.stdout.split()[0])
# Convert to GB
size_gb = size_bytes / (1024**3)
return size_gb
except Exception:
pass
return None
def _get_cargo_target_directory(self) -> Optional[str]:
"""Get cargo target directory using cargo metadata."""
try:
# Use DynamoInfo's static method to find workspace
workspace_dir = DynamoInfo.find_workspace()
# Run cargo metadata command to get target directory
cmd_args = ["cargo", "metadata", "--format-version=1", "--no-deps"]
kwargs: Dict[str, Any] = {
"capture_output": True,
"text": True,
"timeout": 10,
}
# Add cwd if workspace_dir was found
if workspace_dir and os.path.isdir(workspace_dir):
kwargs["cwd"] = workspace_dir
result = subprocess.run(cmd_args, **kwargs)
if result.returncode == 0: if result.returncode == 0:
# Parse JSON output to extract target_directory # Parse JSON output to extract target_directory
metadata = json.loads(result.stdout) metadata = json.loads(result.stdout)
target_directory = metadata.get("target_directory") return metadata.get("target_directory")
except ( except Exception:
subprocess.TimeoutExpired,
subprocess.CalledProcessError,
FileNotFoundError,
json.JSONDecodeError,
):
# cargo metadata failed or JSON parsing failed
pass pass
return None
return target_directory, cargo_home def _add_build_info(self, parent_node: NodeInfo, cargo_target: str):
"""Add debug/release/binary information as children of target directory."""
def _get_git_info(self, workspace_dir: str) -> Tuple[Optional[str], Optional[str]]: debug_dir = os.path.join(cargo_target, "debug")
"""Get git commit SHA and date for the workspace. release_dir = os.path.join(cargo_target, "release")
Args: # Check debug directory
workspace_dir: Path to the workspace directory if os.path.exists(debug_dir):
display_debug = self._replace_home_with_var(debug_dir)
debug_value = display_debug
Returns: # Add size (skip if fast mode)
Tuple of (short_sha, commit_date) or (None, None) if not a git repo if not self.fast_mode:
Example: ('a1b2c3d4e5f6', '2025-08-14 16:45:31 PDT') debug_size_gb = self._get_directory_size_gb(debug_dir)
""" if debug_size_gb is not None:
if not workspace_dir or not os.path.exists(workspace_dir): debug_value += f", {debug_size_gb:.1f} GB"
return None, None
try: try:
# Get the longer SHA (12 characters) debug_mtime = os.path.getmtime(debug_dir)
sha_result = subprocess.run( debug_time = self._format_timestamp_pdt(debug_mtime)
["git", "rev-parse", "--short=12", "HEAD"], debug_value += f", modified={debug_time}"
cwd=workspace_dir, except Exception:
capture_output=True, debug_value += " (unable to read timestamp)"
text=True,
timeout=5,
)
if sha_result.returncode != 0:
return None, None
short_sha = sha_result.stdout.strip()
# Get the commit timestamp debug_node = NodeInfo(
date_result = subprocess.run( label="Debug", desc=debug_value, status=NodeStatus.INFO
["git", "show", "-s", "--format=%ct", "HEAD"],
cwd=workspace_dir,
capture_output=True,
text=True,
timeout=5,
) )
if date_result.returncode != 0: parent_node.add_child(debug_node)
return None, None
# Convert timestamp to PST/PDT # Check release directory
timestamp = int(date_result.stdout.strip()) if os.path.exists(release_dir):
commit_date = self._format_timestamp_pdt(timestamp) display_release = self._replace_home_with_var(release_dir)
release_value = display_release
return short_sha, commit_date # Add size (skip if fast mode)
except (FileNotFoundError, subprocess.TimeoutExpired, Exception): if not self.fast_mode:
return None, None release_size_gb = self._get_directory_size_gb(release_dir)
if release_size_gb is not None:
release_value += f", {release_size_gb:.1f} GB"
def _print_system_info(self) -> bool:
"""Print concise system information as a top-level section.
Tree structure:
System info (hostname: ...):
├─ OS: ...
├─ NVIDIA GPU: ...
├─ Cargo: ...
├─ Maturin: ...
└─ Python: ...
├─ Torch: ...
└─ PYTHONPATH: ...
"""
# OS info
distro = ""
version = ""
try: try:
os_release_path = "/etc/os-release" release_mtime = os.path.getmtime(release_dir)
if os.path.exists(os_release_path): release_time = self._format_timestamp_pdt(release_mtime)
with open(os_release_path, "r") as f: release_value += f", modified={release_time}"
data = f.read()
name = ""
ver = ""
for line in data.splitlines():
if line.startswith("NAME=") and not name:
name = line.split("=", 1)[1].strip().strip('"')
elif line.startswith("VERSION=") and not ver:
ver = line.split("=", 1)[1].strip().strip('"')
distro = name
version = ver
except Exception: except Exception:
pass release_value += " (unable to read timestamp)"
uname = platform.uname() release_node = NodeInfo(
# Memory (used/total) and CPU cores label="Release", desc=release_value, status=NodeStatus.INFO
mem_used_gib = None )
mem_total_gib = None parent_node.add_child(release_node)
# Find *.so file
so_file = self._find_so_file(cargo_target)
if so_file:
display_so = self._replace_home_with_var(so_file)
so_value = display_so
# Add file size (skip if fast mode)
if not self.fast_mode:
try: try:
meminfo = {} file_size_bytes = os.path.getsize(so_file)
with open("/proc/meminfo", "r") as f: file_size_mb = file_size_bytes / (1024**2)
for line in f: so_value += f", {file_size_mb:.1f} MB"
if ":" in line:
k, v = line.split(":", 1)
meminfo[k.strip()] = v.strip()
if "MemTotal" in meminfo and "MemAvailable" in meminfo:
# Values are in kB
total_kib = float(meminfo["MemTotal"].split()[0])
avail_kib = float(meminfo["MemAvailable"].split()[0])
used_kib = max(total_kib - avail_kib, 0.0)
mem_total_gib = total_kib / (1024.0 * 1024.0)
mem_used_gib = used_kib / (1024.0 * 1024.0)
except Exception: except Exception:
pass pass
cores = os.cpu_count() or 0 try:
so_mtime = os.path.getmtime(so_file)
so_time = self._format_timestamp_pdt(so_mtime)
so_value += f", modified={so_time}"
except Exception:
so_value += " (unable to read timestamp)"
if distro: binary_node = NodeInfo(
base_linux = f"OS: {distro} {version} ({uname.system} {uname.release} {uname.machine})".strip() label="Binary", desc=so_value, status=NodeStatus.INFO
else:
base_linux = (
f"OS: {uname.system} {uname.release} {uname.version} ({uname.machine})"
) )
parent_node.add_child(binary_node)
extras = [] def _find_so_file(self, target_directory: str) -> Optional[str]:
if mem_used_gib is not None and mem_total_gib is not None: """Find the compiled *.so file in target directory."""
if mem_total_gib > 0: # Check common locations for .so files
mem_usage_percent = (mem_used_gib / mem_total_gib) * 100 search_dirs = [
warning_symbol = " ⚠️" if mem_usage_percent >= 90 else "" os.path.join(target_directory, "debug"),
else: os.path.join(target_directory, "release"),
warning_symbol = "" target_directory,
extras.append( ]
f"Memory: {mem_used_gib:.1f}/{mem_total_gib:.1f} GiB{warning_symbol}"
) for search_dir in search_dirs:
if cores: if not os.path.exists(search_dir):
extras.append(f"Cores: {cores}") continue
linux_line = base_linux if not extras else base_linux + "; " + "; ".join(extras)
# Defer printing until we have all three lines; we print as a tree below
# GPU info
(
gpu_lines,
gpu_driver_version,
gpu_cuda_version,
) = self.gpu_detector.get_gpu_info()
# Python info
py_ver = platform.python_version()
py_exec = sys.executable or "python"
py_path_env = os.environ.get("PYTHONPATH")
py_path_str = py_path_env if py_path_env else "unset"
python_line = f"Python: {py_ver} ({py_exec})"
if not os.path.exists(py_exec):
python_line = "❌ Python: not found"
# PyTorch info
torch_version: Optional[str] = None
torch_cuda_available: Optional[bool] = None
try:
import importlib
torch = importlib.import_module("torch") # type: ignore # Walk through directory looking for .so files
try: try:
torch_version = getattr(torch, "__version__", None) # type: ignore[attr-defined] for root, dirs, files in os.walk(search_dir):
# Check CUDA availability through PyTorch for file in files:
if hasattr(torch, "cuda"): if file.endswith(".so"):
torch_cuda_available = torch.cuda.is_available() # type: ignore[attr-defined] return os.path.join(root, file)
except Exception: # Don't recurse too deep
torch_version = None if root.count(os.sep) - search_dir.count(os.sep) > 2:
torch_cuda_available = None dirs[:] = [] # Stop recursion
except Exception: except Exception:
# torch not installed
pass pass
# Extra lines for additional system info return None
extra_lines: List[str] = []
class MaturinInfo(NodeInfo):
"""Maturin tool information (Python-Rust build tool)"""
def __init__(self):
maturin_path = shutil.which("maturin")
if not maturin_path:
super().__init__(label="Maturin", desc="not found", status=NodeStatus.ERROR)
# Add installation hint as a child node
install_hint = NodeInfo(
label="Install with",
desc="uv pip install maturin[patchelf]",
status=NodeStatus.INFO,
)
self.add_child(install_hint)
return
# Detect cargo binary path and version for heading
cargo_path = shutil.which("cargo")
cargo_version = None
try: try:
proc = subprocess.run( result = subprocess.run(
["cargo", "--version"], capture_output=True, text=True, timeout=5 ["maturin", "--version"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
version = result.stdout.strip()
# Include the maturin binary path like Cargo and Git do
display_maturin_path = self._replace_home_with_var(maturin_path)
super().__init__(
label="Maturin",
desc=f"{display_maturin_path}, {version}",
status=NodeStatus.OK,
) )
if proc.returncode == 0 and proc.stdout: return
cargo_version = proc.stdout.strip()
except Exception: except Exception:
pass pass
cargo_target, cargo_home = self._get_cargo_info() super().__init__(label="Maturin", desc="not found", status=NodeStatus.ERROR)
has_cargo = bool(cargo_path or cargo_home or cargo_target)
# Build system info output
hostname = platform.node()
system_output = [f"System info (hostname: {hostname}):", f"├─ {linux_line}"]
# Add GPU lines - handle single or multiple GPUs class PythonInfo(NodeInfo):
if len(gpu_lines) == 1: """Python installation information"""
system_output.append(f"├─ {gpu_lines[0]}")
else:
for i, gpu_line in enumerate(gpu_lines):
# All GPUs use ├─ (more system info follows)
system_output.append(f"├─ {gpu_line}")
print("\n".join(system_output))
# CUDA line removed - driver/CUDA versions already shown in NVIDIA GPU line
# Extra lines (e.g., CUDA memory clear status)
for i, line in enumerate(extra_lines):
# If cargo follows after extra lines, use mid symbol; else close on last
is_last_extra = i == len(extra_lines) - 1
symbol = "├─" if (has_cargo or not is_last_extra) else "└─"
print(f"{symbol} {line}")
# If no extra lines, and no cargo, close the system info section
if not extra_lines and not has_cargo:
# System info is complete, Dynamo Environment follows
pass
# Cargo Info block
if has_cargo:
cargo_heading = "Cargo ("
if cargo_path:
cargo_heading += f"{cargo_path}"
else:
cargo_heading += "cargo not found"
if cargo_version:
cargo_heading += f", {cargo_version}"
cargo_heading += ")"
# Cargo heading is not the last top-level child (Dynamo Environment follows) def __init__(self):
print(f"├─ {cargo_heading}") py_version = platform.python_version()
py_exec = sys.executable or "python"
display_py_exec = self._replace_home_with_var(py_exec)
# Under cargo heading, indent nested details super().__init__(
if cargo_home: label="Python",
cargo_home_env = os.environ.get("CARGO_HOME") desc=f"{py_version}, {display_py_exec}",
display_cargo_home = self._replace_home_with_var(cargo_home) status=NodeStatus.OK if os.path.exists(py_exec) else NodeStatus.ERROR,
if cargo_home_env:
print(
f" ├─ Cargo home directory: {display_cargo_home} (CARGO_HOME is set)"
) )
else:
# If there's also a target below, keep mid connector, else close # Check for PyTorch (optional)
print( try:
f" {'├─' if cargo_target else '└─'} Cargo home directory: {display_cargo_home}" torch = __import__("torch")
version = getattr(torch, "__version__", "installed")
# Check CUDA availability
cuda_status = None
if hasattr(torch, "cuda"):
try:
cuda_available = torch.cuda.is_available()
cuda_status = (
"✅torch.cuda.is_available"
if cuda_available
else "❌torch.cuda.is_available"
) )
except Exception:
pass
if cargo_target: # Get installation path
cargo_target_env = os.environ.get("CARGO_TARGET_DIR") install_path = None
display_cargo_target = self._replace_home_with_var(cargo_target) if hasattr(torch, "__file__") and torch.__file__:
target_msg = ( file_path = torch.__file__
f" └─ Cargo target directory: {display_cargo_target} (CARGO_TARGET_DIR is set)" if "site-packages" in file_path:
if cargo_target_env parts = file_path.split(os.sep)
else f" └─ Cargo target directory: {display_cargo_target}" for i, part in enumerate(parts):
if part == "site-packages":
install_path = os.sep.join(parts[: i + 1])
break
elif file_path:
install_path = os.path.dirname(file_path)
if install_path:
install_path = self._replace_home_with_var(install_path)
package_info = PythonPackageInfo(
package_name="PyTorch",
version=version,
cuda_status=cuda_status,
install_path=install_path,
is_framework=False,
) )
print(target_msg) self.add_child(package_info)
except ImportError:
pass # PyTorch is optional, don't show if not installed
# Nested details under Cargo target directory # Add PYTHONPATH
debug_dir = os.path.join(cargo_target, "debug") pythonpath = os.environ.get("PYTHONPATH", "")
release_dir = os.path.join(cargo_target, "release") self.add_child(PythonPathInfo(pythonpath))
debug_exists = os.path.exists(debug_dir)
release_exists = os.path.exists(release_dir)
# Find *.so file class FrameworkInfo(NodeInfo):
so_file = self._find_so_file(cargo_target) """LLM Framework information"""
has_so_file = so_file is not None
if debug_exists: def __init__(self):
symbol = "├─" if release_exists or has_so_file else "└─" super().__init__(label="🤖Framework", status=NodeStatus.INFO)
display_debug_dir = self._replace_home_with_var(debug_dir)
try:
debug_mtime = os.path.getmtime(debug_dir)
debug_time = self._format_timestamp_pdt(debug_mtime)
print(
f" {symbol} Debug: {display_debug_dir} (modified: {debug_time})"
)
except OSError:
print(
f" {symbol} Debug: {display_debug_dir} (unable to read timestamp)"
)
if release_exists: # Check for framework packages (mandatory to show)
symbol = "├─" if has_so_file else "└─" frameworks_to_check = [
display_release_dir = self._replace_home_with_var(release_dir) ("vllm", "vLLM"),
try: ("sglang", "Sglang"),
release_mtime = os.path.getmtime(release_dir) ("tensorrt_llm", "tensorRT LLM"),
release_time = self._format_timestamp_pdt(release_mtime) ]
print(
f" {symbol} Release: {display_release_dir} (modified: {release_time})"
)
except OSError:
print(
f" {symbol} Release: {display_release_dir} (unable to read timestamp)"
)
if has_so_file and so_file is not None: for module_name, display_name in frameworks_to_check:
display_so_file = self._replace_home_with_var(so_file) # Special handling for TensorRT-LLM to avoid NVML crashes
if module_name == "tensorrt_llm":
# Check if it's installed in system packages first
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
system_packages = [
f"/usr/local/lib/python{python_version}/dist-packages",
f"/usr/lib/python{python_version}/dist-packages",
]
found_in_system = False
for pkg_path in system_packages:
if os.path.exists(pkg_path):
tensorrt_dirs = [
d for d in os.listdir(pkg_path) if "tensorrt_llm" in d
]
if tensorrt_dirs:
found_in_system = True
# Try to get version safely
try: try:
so_mtime = os.path.getmtime(so_file) result = subprocess.run(
so_time = self._format_timestamp_pdt(so_mtime) [
print( sys.executable,
f" └─ Binary: {display_so_file} (modified: {so_time})" "-c",
"import tensorrt_llm; print(tensorrt_llm.__version__)",
],
capture_output=True,
text=True,
timeout=10,
) )
except OSError: if result.returncode == 0:
print( version = result.stdout.strip()
f" └─ Binary: {display_so_file} (unable to read timestamp)" package_info = PythonPackageInfo(
package_name=display_name,
version=version,
module_path=f"{pkg_path}/tensorrt_llm/__init__.py",
is_framework=True,
is_installed=True,
) )
else: else:
# Cargo not found: show as a top-level sibling; Dynamo follows, so use mid connector package_info = PythonPackageInfo(
print( package_name=display_name,
"├─ ❌ Cargo: not found (install Rust toolchain to see cargo target directory)" version=f"Found in {pkg_path} but not importable",
is_framework=True,
is_installed=True,
)
self.add_child(package_info)
break
except (
subprocess.TimeoutExpired,
subprocess.CalledProcessError,
):
package_info = PythonPackageInfo(
package_name=display_name,
version=f"Found in {pkg_path} but not importable",
is_framework=True,
is_installed=True,
)
self.add_child(package_info)
break
if not found_in_system:
package_info = PythonPackageInfo(
package_name=display_name,
version="-",
is_framework=True,
is_installed=False,
) )
self.add_child(package_info)
continue
# Maturin check (Python-Rust build tool) # Regular import for other frameworks
maturin_path = shutil.which("maturin")
maturin_version = None
try: try:
proc = subprocess.run( module = __import__(module_name)
["maturin", "--version"], capture_output=True, text=True, timeout=5 version = getattr(module, "__version__", "installed")
# Get module path
module_path = None
if hasattr(module, "__file__") and module.__file__:
module_path = self._replace_home_with_var(module.__file__)
# Get executable path
exec_path = None
exec_path_raw = shutil.which(module_name)
if exec_path_raw:
exec_path = self._replace_home_with_var(exec_path_raw)
package_info = PythonPackageInfo(
package_name=display_name,
version=version,
module_path=module_path,
exec_path=exec_path,
is_framework=True,
is_installed=True,
) )
if proc.returncode == 0 and proc.stdout: self.add_child(package_info)
maturin_version = proc.stdout.strip() except (ImportError, Exception):
except Exception: # Framework not installed - show with "-"
pass package_info = PythonPackageInfo(
package_name=display_name,
version="-",
is_framework=True,
is_installed=False,
)
self.add_child(package_info)
has_maturin = bool(maturin_path or maturin_version)
if has_maturin: class PythonPackageInfo(NodeInfo):
maturin_heading = "Maturin (" """Python package information"""
if maturin_path:
maturin_heading += f"{maturin_path}" def __init__(
else: self,
maturin_heading += "maturin not found" package_name: str,
if maturin_version: version: str,
maturin_heading += f", {maturin_version}" cuda_status: Optional[str] = None,
maturin_heading += ")" module_path: Optional[str] = None,
print(f"├─ {maturin_heading}") exec_path: Optional[str] = None,
install_path: Optional[str] = None,
is_framework: bool = False,
is_installed: bool = True,
):
# Build display value
display_value = version
# Determine status based on whether package is installed
if not is_installed or version == "-":
# Framework not found - show with "-" and use UNKNOWN status for ❓ symbol
display_value = "-"
status = NodeStatus.UNKNOWN # Show ❓ for not found frameworks
else: else:
print("├─ ❌ Maturin: not found") status = NodeStatus.OK
print(" Install with: uv pip install maturin[patchelf]")
# Add CUDA status for PyTorch
# Python line (moved here to appear after Maturin, before Dynamo) if cuda_status:
# Determine if more top-level entries come after Python display_value = f"{version}, {cuda_status}"
more_after_python = bool(has_cargo) # Don't add install path for PyTorch with CUDA status
print(f"{'├─' if more_after_python else '└─'} {python_line}") # For frameworks, add module and exec paths
elif is_framework and (module_path or exec_path):
# Torch version as a child under Python (before PYTHONPATH) parts = [version]
if torch_version: if module_path:
cuda_status = "" parts.append(f"module={module_path}")
if torch_cuda_available is not None: if exec_path:
cuda_status = ( parts.append(f"exec={exec_path}")
" (✅torch.cuda.is_available())" display_value = ", ".join(parts)
if torch_cuda_available # For regular packages, add install path
else " (❌torch.cuda.is_available())" elif install_path:
) display_value = f"{version} ({install_path})"
print(" ├─ Torch: " + str(torch_version) + cuda_status)
super().__init__(label=package_name, desc=display_value, status=status)
class PythonPathInfo(NodeInfo):
"""PYTHONPATH environment variable information"""
def __init__(self, pythonpath: str):
if pythonpath:
# Split by colon and replace home in each path
paths = pythonpath.split(":")
display_paths = [self._replace_home_with_var(p) for p in paths]
display_pythonpath = ":".join(display_paths)
status = NodeStatus.INFO
else: else:
# Show as a child under Python display_pythonpath = "not set"
print(" ├─ ❌ Torch: not installed") status = NodeStatus.WARNING # Show warning when PYTHONPATH is not set
# PYTHONPATH as the last child under Python super().__init__(label="PYTHONPATH", desc=display_pythonpath, status=status)
print(f" └─ PYTHONPATH: {py_path_str}")
# Determine if any errors were printed in system info
system_errors_found = False class DynamoRuntimeInfo(NodeInfo):
if isinstance(python_line, str) and python_line.startswith("❌"): """Dynamo runtime components information"""
system_errors_found = True
if not has_cargo: def __init__(self, workspace_dir: str, fast_mode: bool = False):
system_errors_found = True self.fast_mode = fast_mode
# Mark GPU error based on lines printed; treat as error for overall status as well # Try to get package version
import importlib.metadata
try: try:
self._gpu_error = any( version = importlib.metadata.version("ai-dynamo-runtime")
isinstance(line, str) and line.startswith("❌") for line in gpu_lines runtime_value = f"ai-dynamo-runtime {version}"
) is_installed = True
if self._gpu_error:
system_errors_found = True
except Exception: except Exception:
pass runtime_value = "ai-dynamo-runtime - Not installed"
return system_errors_found is_installed = False
def _find_so_file(self, target_directory: str) -> Optional[str]: super().__init__(
"""Find the compiled *.so file in target directory or Python bindings. label="Runtime components",
desc=runtime_value,
status=NodeStatus.INFO, # Will update based on components found
)
Args: # Add package info if installed
target_directory: Path to cargo target directory if is_installed:
# Add dist-info directory
dist_info = self._find_dist_info()
if dist_info:
self.add_child(dist_info)
Returns: # Add .pth file
Path to *.so file or None if not found pth_file = self._find_pth_file()
Example: '/home/ubuntu/dynamo/target/debug/libdynamo_core.so' if pth_file:
""" self.add_child(pth_file)
if not target_directory or not os.path.exists(target_directory):
return None # Discover runtime components from source
components = self._discover_runtime_components(workspace_dir)
# Find where each component actually is and add them
if components:
# Calculate max width for alignment
max_len = max(len(comp) for comp in components)
# Look for *.so files in debug and release directories components_found = False
for profile in ["debug", "release"]: for component in components:
profile_dir = os.path.join(target_directory, profile)
if os.path.exists(profile_dir):
try: try:
for root, dirs, files in os.walk(profile_dir): # Try to import to find actual location
for file in files: module = __import__(component, fromlist=[""])
if file.endswith(".so"): module_path = getattr(module, "__file__", None)
return os.path.join(root, file)
except OSError:
continue
# Also check Python bindings directory for installed *.so if module_path:
if self.workspace_dir: # Add timestamp for .so files
bindings_dir = f"{self.workspace_dir}/lib/bindings/python/src/dynamo" timestamp_str = ""
if os.path.exists(bindings_dir): if module_path.endswith(".so"):
try: try:
for root, dirs, files in os.walk(bindings_dir): stat = os.stat(module_path)
for file in files: timestamp = self._format_timestamp_pdt(stat.st_mtime)
if file.endswith(".so") and "_core" in file: timestamp_str = f", modified={timestamp}"
return os.path.join(root, file) except Exception:
except OSError:
pass pass
return None display_path = self._replace_home_with_var(module_path)
padded_name = f"{component:<{max_len}}"
module_node = NodeInfo(
label=f"✅ {padded_name}",
desc=f"{display_path}{timestamp_str}",
status=NodeStatus.NONE,
)
self.add_child(module_node)
components_found = True
except ImportError as e:
# Module not importable - show as error
padded_name = f"{component:<{max_len}}"
error_msg = str(e) if str(e) else "Import failed"
module_node = NodeInfo(
label=padded_name, desc=error_msg, status=NodeStatus.ERROR
)
self.add_child(module_node)
# Don't set components_found to True for failed imports
# Update status and value based on whether we found components
if components_found:
self.status = NodeStatus.OK
# If not installed but components work via PYTHONPATH, update the message
if not is_installed:
self.desc = "ai-dynamo-runtime (via PYTHONPATH)"
else:
self.status = NodeStatus.ERROR
else:
# No components discovered at all
self.status = NodeStatus.ERROR
def _get_cargo_build_profile(self, target_directory: str) -> Optional[str]: # Final check: if no children at all (no components found), ensure it's an error
"""Determine which cargo build profile (debug/release) was used most recently. if not self.children:
self.status = NodeStatus.ERROR
Args: def _discover_runtime_components(self, workspace_dir: str) -> list:
target_directory: Path to cargo target directory """Discover ai-dynamo-runtime components from filesystem.
Returns: Returns:
'debug', 'release', 'debug/release', or None if cannot determine List of runtime component module names
Example: 'debug' Example: ['dynamo._core', 'dynamo.nixl_connect', 'dynamo.llm', 'dynamo.runtime']
"""
# First check environment variables that indicate current build profile
profile_env = os.environ.get("PROFILE")
if profile_env:
if profile_env == "dev":
return "debug"
elif profile_env == "release":
return "release"
# Check OPT_LEVEL as secondary indicator
opt_level = os.environ.get("OPT_LEVEL")
if opt_level:
if opt_level == "0":
return "debug"
elif opt_level in ["2", "3"]:
return "release"
# Fall back to filesystem inspection
if not target_directory or not os.path.exists(target_directory):
return None
debug_dir = os.path.join(target_directory, "debug") Note: Always includes 'dynamo._core' (compiled Rust module), then scans
release_dir = os.path.join(target_directory, "release") lib/bindings/python/src/dynamo/ for additional components.
"""
components = ["dynamo._core"] # Always include compiled Rust module
debug_exists = os.path.exists(debug_dir) if not workspace_dir:
release_exists = os.path.exists(release_dir) return components
if not debug_exists and not release_exists: # Scan runtime components (llm, runtime, nixl_connect, etc.)
return None runtime_path = os.path.join(workspace_dir, "lib/bindings/python/src/dynamo")
elif debug_exists and not release_exists: if not os.path.exists(runtime_path):
return "debug" return components
elif release_exists and not debug_exists:
return "release"
else:
# Both exist, check which was modified more recently
try:
debug_mtime = os.path.getmtime(debug_dir)
release_mtime = os.path.getmtime(release_dir)
if ( for item in os.listdir(runtime_path):
abs(debug_mtime - release_mtime) < 1.0 item_path = os.path.join(runtime_path, item)
): # Same timestamp (within 1 second) if os.path.isdir(item_path) and os.path.exists(
return "debug/release" # Both available, runtime choice depends on invocation os.path.join(item_path, "__init__.py")
else: ):
return "release" if release_mtime > debug_mtime else "debug" components.append(f"dynamo.{item}")
except OSError:
return None
def _setup_pythonpath(self) -> None: return components
"""Set up PYTHONPATH for component imports."""
if not self.workspace_dir:
return
paths = [] def _find_dist_info(self) -> Optional[NodeInfo]:
"""Find the dist-info directory for ai-dynamo-runtime."""
import site
# Collect component source paths for site_dir in site.getsitepackages():
comp_path = f"{self.workspace_dir}/components" pattern = os.path.join(site_dir, "ai_dynamo_runtime*.dist-info")
if os.path.exists(comp_path): matches = glob.glob(pattern)
for item in os.listdir(comp_path): if matches:
src_path = f"{comp_path}/{item}/src" path = matches[0]
if os.path.exists(src_path): display_path = self._replace_home_with_var(path)
paths.append(src_path) try:
else: stat = os.stat(path)
print( timestamp = self._format_timestamp_pdt(stat.st_ctime)
f"⚠️ Warning: Components directory not found for PYTHONPATH setup: {comp_path}" return NodeInfo(
label=display_path,
desc=f"created={timestamp}",
status=NodeStatus.INFO,
) )
except Exception:
return NodeInfo(label=display_path, status=NodeStatus.INFO)
return None
# Collect backend source paths def _find_pth_file(self) -> Optional[NodeInfo]:
backend_path = f"{self.workspace_dir}/components/backends" """Find the .pth file for ai-dynamo-runtime."""
if os.path.exists(backend_path): import site
for item in os.listdir(backend_path):
src_path = f"{backend_path}/{item}/src" for site_dir in site.getsitepackages():
if os.path.exists(src_path): pth_path = os.path.join(site_dir, "ai_dynamo_runtime.pth")
paths.append(src_path) if os.path.exists(pth_path):
else: display_path = self._replace_home_with_var(pth_path)
print( try:
f"⚠️ Warning: Backend components directory not found for PYTHONPATH setup: {backend_path}" stat = os.stat(pth_path)
timestamp = self._format_timestamp_pdt(stat.st_mtime)
node = NodeInfo(
label=display_path,
desc=f"modified={timestamp}",
status=NodeStatus.INFO,
) )
# Update sys.path for current process # Read where it points to
if paths: with open(pth_path, "r") as f:
# Add paths to sys.path for immediate effect on imports content = f.read().strip()
for path in paths: if content:
if path not in sys.path: display_content = self._replace_home_with_var(content)
sys.path.insert(0, path) # Insert at beginning for priority points_to = NodeInfo(
label="→", desc=display_content, status=NodeStatus.INFO
# Show what PYTHONPATH would be (for manual shell setup)
pythonpath_value = ":".join(paths)
current_path = os.environ.get("PYTHONPATH", "")
if current_path:
pythonpath_value = f"{pythonpath_value}:{current_path}"
print(
f"""Below are the results if you export PYTHONPATH="{pythonpath_value}":
({len(paths)} workspace component paths found)"""
) )
for path in paths: node.add_child(points_to)
print(f" • {path}")
print()
else:
print("⚠️ Warning: No component source paths found for PYTHONPATH setup")
# ==================================================================== return node
# IMPORT TESTING except Exception:
# ==================================================================== return NodeInfo(label=display_path, status=NodeStatus.INFO)
return None
def _test_component_group(
self,
components: List[str],
package_name: str,
group_name: str,
max_width: int,
site_packages: str,
collect_failures: bool = False,
package_info: Optional[Dict[str, Any]] = None,
sub_indent: str = " ",
) -> Tuple[Dict[str, str], List[str]]:
"""Test a group of components for a given package.
Args:
components: List of component names to test
Example: ['dynamo._core', 'dynamo.llm', 'dynamo.runtime']
package_name: Name of the package to get version from
Example: 'ai-dynamo-runtime'
group_name: Display name for the group
Example: 'Runtime components'
max_width: Maximum width for component name alignment
Example: 20
site_packages: Path to site-packages directory
Example: '/opt/dynamo/venv/lib/python3.12/site-packages'
collect_failures: Whether to collect failed component names
Example: True (for framework components), False (for runtime)
Returns: class DynamoFrameworkInfo(NodeInfo):
Tuple of (results dict, list of failed components) """Dynamo framework components information"""
Example: ({'dynamo._core': '✅ Success', 'dynamo.llm': '❌ Failed: No module named dynamo.llm'},
['dynamo.llm']) def __init__(self, workspace_dir: str, fast_mode: bool = False):
self.fast_mode = fast_mode
Output printed to console: # Try to get package version
Dynamo Environment ($HOME/dynamo): import importlib.metadata
└─ Runtime components (ai-dynamo-runtime 0.4.0):
├─ /opt/dynamo/venv/lib/.../ai_dynamo_runtime-0.4.0.dist-info (created: 2025-08-12 14:17:34 PDT)
├─ ✅ dynamo._core /opt/dynamo/venv/lib/.../dynamo/_core.cpython-312-x86_64-linux-gnu.so
└─ ❌ dynamo.llm No module named 'dynamo.llm'
"""
results = {}
failures = []
# Print header with version info
try: try:
version = importlib.metadata.version(package_name) version = importlib.metadata.version("ai-dynamo")
header = f"{group_name} ({package_name} {version}):" framework_value = f"ai-dynamo {version}"
except importlib.metadata.PackageNotFoundError: is_installed = True
header = f"{group_name} ({package_name} - Not installed):"
except Exception: except Exception:
header = f"{group_name} ({package_name}):" framework_value = "ai-dynamo - Not installed"
is_installed = False
print(header)
super().__init__(
# Determine if package info should use ├─ or └─ based on whether there are components label="Framework components",
has_components = len(components) > 0 desc=framework_value,
package_symbol = "├─" if has_components else "└─" status=NodeStatus.INFO, # Will update based on components found
# Print package info as subitem of component group (only if found)
if package_info:
package_path = package_info.get("path", "")
package_created = package_info.get("created", "")
display_path = self._replace_home_with_var(package_path)
if package_created:
print(
f"{sub_indent}{package_symbol} {display_path} (created: {package_created})"
)
else:
print(f"{sub_indent}{package_symbol} {display_path}")
# Show .pth files if they exist (editable installs) - at same level as package info
pth_files = package_info.get("pth_files", [])
for i, pth_file in enumerate(pth_files):
is_last_pth = i == len(pth_files) - 1
pth_symbol = "└─" if (is_last_pth and not has_components) else "├─"
display_pth_path = self._replace_home_with_var(pth_file["path"])
display_points_to = self._replace_home_with_var(pth_file["points_to"])
print(
f"{sub_indent}{pth_symbol} {display_pth_path} (modified: {pth_file['modified']})"
) )
print(f"{sub_indent} └─ Points to: {display_points_to}")
# Don't print anything for "Not found" - just skip it
# Test each component as subitems of the package # Add package info if installed
for i, component in enumerate(components): if is_installed:
# Determine tree symbol - last component gets └─, others get ├─, with proper indentation (deeper nesting) import glob
is_last = i == len(components) - 1 import site
tree_symbol = f"{sub_indent}{'└─' if is_last else '├─'}"
for site_dir in site.getsitepackages():
# Look specifically for ai_dynamo (not ai_dynamo_runtime)
dist_pattern = os.path.join(site_dir, "ai_dynamo-*.dist-info")
matches = glob.glob(dist_pattern)
if matches:
path = matches[0]
display_path = self._replace_home_with_var(path)
try: try:
module = __import__(component, fromlist=[""]) stat = os.stat(path)
results[component] = "✅ Success" timestamp = self._format_timestamp_pdt(stat.st_ctime)
# Get module path for location info dist_node = NodeInfo(
module_path = getattr(module, "__file__", "built-in") label=display_path,
if module_path and module_path != "built-in": desc=f"created={timestamp}",
# Only show timestamps for generated files (*.so, *.pth, etc.), not __init__.py status=NodeStatus.INFO,
timestamp_str = "" )
show_timestamp = False self.add_child(dist_node)
except Exception:
dist_node = NodeInfo(label=display_path, status=NodeStatus.INFO)
self.add_child(dist_node)
break
# Check if this is a generated file we want to show timestamps for # Discover framework components from source
if any( components = self._discover_framework_components(workspace_dir)
module_path.endswith(ext)
for ext in [".so", ".pth", ".dll", ".dylib"] # Find where each component actually is and add them
): if components:
show_timestamp = True # Sort components for consistent output
components.sort()
if show_timestamp: # Calculate max width for alignment
max_len = max(len(comp) for comp in components)
components_found = False
for component in components:
try: try:
if os.path.exists(module_path): # Try to import to find actual location
mtime = os.path.getmtime(module_path) module = __import__(component, fromlist=[""])
timestamp_str = ( module_path = getattr(module, "__file__", None)
f" (modified: {self._format_timestamp_pdt(mtime)})"
)
except OSError:
pass
if self.workspace_dir and module_path.startswith( if module_path:
self.workspace_dir
):
# From workspace source - show absolute path with $HOME replacement
display_path = self._replace_home_with_var(module_path)
if show_timestamp:
print(
f"{tree_symbol}{component:<{max_width}} {display_path}{timestamp_str}"
)
else:
print(
f"{tree_symbol}{component:<{max_width}} {display_path}"
)
elif site_packages and module_path.startswith(site_packages):
# From installed package - show path with $HOME replacement
display_path = self._replace_home_with_var(module_path) display_path = self._replace_home_with_var(module_path)
if show_timestamp: padded_name = f"{component:<{max_len}}"
print( component_node = NodeInfo(
f"{tree_symbol}{component:<{max_width}} {display_path}{timestamp_str}" label=f"✅ {padded_name}",
desc=display_path,
status=NodeStatus.NONE,
) )
else: self.add_child(component_node)
print( components_found = True
f"{tree_symbol}{component:<{max_width}} {display_path}" except ImportError as e:
) # Module not importable - show as error
else: padded_name = f"{component:<{max_len}}"
# Other location - show path with $HOME replacement error_msg = str(e) if str(e) else "Import failed"
display_path = self._replace_home_with_var(module_path) component_node = NodeInfo(
if show_timestamp: label=padded_name, desc=error_msg, status=NodeStatus.ERROR
print(
f"{tree_symbol}{component:<{max_width}} {display_path}{timestamp_str}"
) )
self.add_child(component_node)
# Don't set components_found to True for failed imports
# Update status and value based on whether we found components
if components_found:
self.status = NodeStatus.OK
# If not installed but components work via PYTHONPATH, update the message
if not is_installed:
self.desc = "ai-dynamo (via PYTHONPATH)"
else: else:
print( self.status = NodeStatus.ERROR
f"{tree_symbol}{component:<{max_width}} {display_path}"
)
else: else:
built_in_suffix = ( # No components discovered at all
" (built-in)" self.status = NodeStatus.ERROR
if group_name.lower().startswith("framework")
else " built-in"
)
print(f"{tree_symbol}{component:<{max_width}}{built_in_suffix}")
except ImportError as e:
results[component] = f"❌ Failed: {e}"
print(f"{tree_symbol}{component:<{max_width}} {e}")
if collect_failures:
failures.append(component)
return results, failures def _discover_framework_components(self, workspace_dir: str) -> list:
"""Discover ai-dynamo framework components from filesystem.
def _get_package_info(self, package_name: str) -> Dict[str, Any]:
"""Get package installation information including .pth files.
Args:
package_name: Name of the package (e.g., 'ai-dynamo-runtime')
Returns: Returns:
Dict with 'path', 'created', and optionally 'pth_files' keys List of framework component module names
""" Example: ['dynamo.frontend', 'dynamo.planner', 'dynamo.vllm', 'dynamo.sglang', 'dynamo.llama_cpp']
import site
site_packages_dirs = site.getsitepackages()
if hasattr(site, "getusersitepackages"):
site_packages_dirs.append(site.getusersitepackages())
result: Dict[str, Any] = {} Note: Scans components/ and components/backends/ directories for modules with __init__.py files.
pth_files: List[Dict[str, str]] = [] """
components: List[str] = []
for site_dir in site_packages_dirs: if not workspace_dir:
if not os.path.exists(site_dir): return components
continue
try: # Scan components directory (frontend, planner, etc.)
for file in os.listdir(site_dir): components_path = os.path.join(workspace_dir, "components")
# Look for .dist-info directories that exactly match the package name if os.path.exists(components_path):
if file.endswith(".dist-info"): for item in os.listdir(components_path):
# Extract package name from .dist-info directory name item_path = os.path.join(components_path, item)
dist_name = file.replace(".dist-info", "") if os.path.isdir(item_path):
# Handle version suffixes (e.g., ai_dynamo_runtime-0.4.0 -> ai_dynamo_runtime) # Check for dynamo module in src
base_name = ( module_path = os.path.join(
dist_name.split("-")[0] if "-" in dist_name else dist_name item_path, "src", "dynamo", item, "__init__.py"
) )
expected_name = package_name.replace("-", "_") if os.path.exists(module_path):
components.append(f"dynamo.{item}")
if base_name == expected_name: # Scan backends directory (vllm, sglang, trtllm, etc.)
dist_info_path = os.path.join(site_dir, file) backends_path = os.path.join(workspace_dir, "components", "backends")
if os.path.isdir(dist_info_path): if os.path.exists(backends_path):
try: for item in os.listdir(backends_path):
ctime = os.path.getctime(dist_info_path) item_path = os.path.join(backends_path, item)
created_time = self._format_timestamp_pdt(ctime) if os.path.isdir(item_path):
result.update( # Check for dynamo module in src
{ module_path = os.path.join(
"path": dist_info_path, item_path, "src", "dynamo", item, "__init__.py"
"created": created_time,
}
)
except OSError:
result.update({"path": dist_info_path})
# Look for .pth files that match this specific package
if file.endswith(".pth"):
# Match .pth files to specific packages
pth_matches_package = False
if package_name == "ai-dynamo-runtime":
# Look for ai_dynamo_runtime.pth or similar
if (
"ai_dynamo_runtime" in file.lower()
or file.lower().startswith("ai_dynamo_runtime")
):
pth_matches_package = True
elif package_name == "ai-dynamo":
# Look for _ai_dynamo.pth or ai_dynamo.pth (but not ai_dynamo_runtime.pth)
if (
"ai_dynamo" in file.lower()
or "_ai_dynamo" in file.lower()
) and "runtime" not in file.lower():
pth_matches_package = True
if pth_matches_package:
pth_path = os.path.join(site_dir, file)
try:
mtime = os.path.getmtime(pth_path)
# Read the content to see what path it adds
with open(pth_path, "r") as f:
content = f.read().strip()
pth_files.append(
{
"path": pth_path,
"modified": self._format_timestamp_pdt(mtime),
"points_to": content,
}
) )
except OSError: if os.path.exists(module_path):
pass components.append(f"dynamo.{item}")
except OSError:
continue
if pth_files:
result["pth_files"] = pth_files
return result
def test_imports(self) -> Dict[str, str]: return components
"""Test imports for all discovered components.
Returns:
Dictionary mapping component names to their import status
Example: {
'dynamo._core': '✅ Success',
'dynamo.llm': '✅ Success',
'dynamo.runtime': '✅ Success',
'dynamo.frontend': '❌ Failed: No module named dynamo.frontend',
'dynamo.planner': '✅ Success'
}
Console output example: class DynamoInfo(NodeInfo):
Dynamo Environment ($HOME/dynamo): """Dynamo workspace information"""
└─ Runtime components (ai-dynamo-runtime 0.4.0):
├─ /opt/dynamo/venv/lib/.../ai_dynamo_runtime-0.4.0.dist-info (created: 2025-08-12 14:17:34 PDT)
├─ /opt/dynamo/venv/lib/.../ai_dynamo_runtime.pth (modified: 2025-08-12 14:17:34 PDT)
└─ Points to: $HOME/dynamo/lib/bindings/python/src
├─ ✅ dynamo._core /opt/dynamo/venv/lib/.../dynamo/_core.cpython-312-x86_64-linux-gnu.so
└─ ✅ dynamo.llm /opt/dynamo/venv/lib/.../dynamo/llm/__init__.py
└─ Framework components (ai-dynamo - Not installed):
├─ ✅ dynamo.frontend /opt/dynamo/venv/lib/.../dynamo/frontend/__init__.py
└─ ❌ dynamo.missing No module named 'dynamo.missing'
"""
results = {}
# Print system info at top-level, before Dynamo Environment def __init__(self, fast_mode: bool = False):
system_errors = self._print_system_info() self.fast_mode = fast_mode
# Then print main environment header as a subtree under System info # Find workspace directory
if ( workspace_dir = DynamoInfo.find_workspace()
self.workspace_dir
and os.path.exists(self.workspace_dir)
and self._is_dynamo_workspace(self.workspace_dir)
):
workspace_path = os.path.abspath(self.workspace_dir)
display_workspace = self._replace_home_with_var(workspace_path)
# Get git info if not workspace_dir:
sha, date = self._get_git_info(self.workspace_dir) # Show error when workspace is not found
if sha and date: super().__init__(
print(f"└─ Dynamo ({display_workspace}, SHA: {sha}, Date: {date}):") label="Dynamo",
else: desc="workspace not found - cannot detect Runtime and Framework components",
print(f"└─ Dynamo ({display_workspace}):") status=NodeStatus.ERROR,
# Backend components directory warning after the Dynamo line
backend_path = f"{self.workspace_dir}/components/backends"
if not os.path.exists(backend_path):
print(
f" ⚠️ Warning: Backend components directory not found: {self._replace_home_with_var(backend_path)}"
) )
# If there were deferred messages (e.g., invalid --path), show them here under Dynamo # Add helpful information about where we looked
for message in self._deferred_messages: search_paths = NodeInfo(
print(f" {message}") label="Searched in",
else: desc="current dir, ~/dynamo, DYNAMO_HOME, /workspace",
# If a user provided an invalid --path, reflect that, otherwise generic not found status=NodeStatus.INFO,
if self.workspace_dir and not os.path.exists(self.workspace_dir):
print(f"└─ Dynamo ({self._replace_home_with_var(self.workspace_dir)}):")
print(" ❌ Workspace path does not exist")
elif self.workspace_dir and not self._is_dynamo_workspace(
self.workspace_dir
):
# Still try to get git info even if it's not a valid workspace
sha, date = self._get_git_info(self.workspace_dir)
if sha and date:
print(
f"└─ Dynamo ({self._replace_home_with_var(self.workspace_dir)}, SHA: {sha}, Date: {date}):"
) )
else: self.add_child(search_paths)
print( hint = NodeInfo(
f"└─ Dynamo ({self._replace_home_with_var(self.workspace_dir)}):" label="Hint",
desc="Run from a Dynamo workspace directory or set DYNAMO_HOME",
status=NodeStatus.INFO,
) )
print(" ❌ Invalid dynamo workspace (missing expected files)") self.add_child(hint)
return
# Get git info
sha, date = self._get_git_info(workspace_dir)
# Build main label
display_workspace = self._replace_home_with_var(workspace_dir)
if sha and date:
value = f"{display_workspace}, SHA: {sha}, Date: {date}"
else: else:
print("└─ Dynamo (workspace not found):") value = display_workspace
# Discover all components super().__init__(label="Dynamo", desc=value, status=NodeStatus.INFO)
runtime_components = self._discover_runtime_components()
framework_components = self._discover_framework_components()
# Calculate max width for alignment across ALL components # Always add runtime components
all_components = runtime_components + framework_components runtime_info = DynamoRuntimeInfo(workspace_dir, fast_mode=self.fast_mode)
max_width = max(len(comp) for comp in all_components) if all_components else 0 self.add_child(runtime_info)
# Get site-packages path for comparison # Always add framework components
import site framework_info = DynamoFrameworkInfo(workspace_dir, fast_mode=self.fast_mode)
self.add_child(framework_info)
site_packages = site.getsitepackages()[0] if site.getsitepackages() else "" def _get_git_info(self, workspace_dir: str) -> Tuple[Optional[str], Optional[str]]:
"""Get git SHA and date for the workspace."""
# Get package information for headers try:
runtime_package_info = self._get_package_info("ai-dynamo-runtime") # Get short SHA
framework_package_info = self._get_package_info("ai-dynamo") result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
# Test runtime components (as subitem of Dynamo Environment, indented; components printed below group header) capture_output=True,
runtime_results, _ = self._test_component_group( text=True,
runtime_components, cwd=workspace_dir,
"ai-dynamo-runtime", timeout=5,
" └─ Runtime components",
max_width,
site_packages,
collect_failures=False,
package_info=runtime_package_info,
sub_indent=" ",
)
results.update(runtime_results)
# Test framework components (as subitem of Dynamo Environment, indented; components printed below group header)
framework_results, framework_failures = self._test_component_group(
framework_components,
"ai-dynamo",
" └─ Framework components",
max_width,
site_packages,
collect_failures=True,
package_info=framework_package_info,
sub_indent=" ",
) )
results.update(framework_results) sha = result.stdout.strip() if result.returncode == 0 else None
# Cargo information is printed under System info # Get commit date
result = subprocess.run(
# Show PYTHONPATH recommendation if any framework components failed (moved to end) ["git", "show", "-s", "--format=%ci", "HEAD"],
if framework_failures and self.workspace_dir: capture_output=True,
pythonpath = self._get_pythonpath() text=True,
if pythonpath: cwd=workspace_dir,
# Apply $HOME replacement to PYTHONPATH for consistency timeout=5,
display_pythonpath = self._replace_home_with_var(pythonpath)
self._show_build_options(display_pythonpath)
# Exit with non-zero status if any errors detected
# Treat Python or Cargo failures from system info, and invalid path, as failures.
any_failures = (
system_errors
or any(msg.startswith("❌ Error:") for msg in self._deferred_messages)
or bool(framework_failures)
) )
# Store whether errors occurred for overall run if result.returncode == 0 and result.stdout.strip():
self.results["had_errors"] = any_failures # Convert to PDT format
date_str = result.stdout.strip()
# Parse and format as PDT
try:
# Parse the git date (format: 2025-08-30 23:22:29 +0000)
import datetime as dt_module
# Split off timezone info
date_part = date_str.rsplit(" ", 1)[0]
dt = dt_module.datetime.strptime(date_part, "%Y-%m-%d %H:%M:%S")
# Convert to PDT (UTC-7)
dt_pdt = dt - dt_module.timedelta(hours=7)
date = dt_pdt.strftime("%Y-%m-%d %H:%M:%S PDT")
except Exception:
date = date_str
else:
date = None
return results return sha, date
except Exception:
return None, None
def _show_build_options(self, display_pythonpath: Optional[str] = None) -> None: @staticmethod
"""Show usage/build guidance including PYTHONPATH export. def find_workspace() -> Optional[str]:
"""Find dynamo workspace directory."""
candidates = []
Args: # Check DYNAMO_HOME environment variable first
display_pythonpath: Optional precomputed PYTHONPATH string with $HOME replacement dynamo_home = os.environ.get("DYNAMO_HOME")
""" if dynamo_home:
# Compute display_pythonpath if not provided candidates.append(dynamo_home)
if not display_pythonpath:
if self.workspace_dir:
pythonpath = self._get_pythonpath()
display_pythonpath = (
self._replace_home_with_var(pythonpath)
if pythonpath
else "$HOME/dynamo/components/*/src"
)
else:
display_pythonpath = "$HOME/dynamo/components/*/src"
# Single source of truth for the export command # Then check common locations
print( candidates.extend(
f'\nSet PYTHONPATH for development:\nexport PYTHONPATH="{display_pythonpath}"\n' [
".", # Current directory
os.path.expanduser("~/dynamo"),
"/workspace",
]
) )
# ==================================================================== for candidate in candidates:
# USAGE EXAMPLES AND GUIDANCE if DynamoInfo.is_dynamo_workspace(candidate):
# ==================================================================== return os.path.abspath(candidate)
return None
def _get_pythonpath(self) -> str: @staticmethod
"""Generate PYTHONPATH recommendation string. def is_dynamo_workspace(path: str) -> bool:
"""Check if directory is a dynamo workspace."""
if not os.path.exists(path):
return False
Returns: # Check for indicators of a dynamo workspace
Colon-separated string of component source paths indicators = [
Example: '/home/ubuntu/dynamo/components/frontend/src:/home/ubuntu/dynamo/components/planner/src:/home/ubuntu/dynamo/components/backends/vllm/src' "README.md",
"components",
"lib/bindings/python",
"lib/runtime",
"Cargo.toml",
]
Note: Scans workspace for all component src directories and joins them for PYTHONPATH usage. # Require at least 3 indicators to be confident
found = 0
for indicator in indicators:
check_path = os.path.join(path, indicator)
if os.path.exists(check_path):
found += 1
return found >= 3
def has_framework_errors(tree: NodeInfo) -> bool:
"""Check if there are framework component errors in the tree"""
# Find the Dynamo node
for child in tree.children:
if child.label and "Dynamo" in child.label:
# Find the Framework components node
for dynamo_child in child.children:
if dynamo_child.label and "Framework components" in dynamo_child.label:
# Use the has_errors() method to check the entire subtree
return dynamo_child.has_errors()
return False
def show_pythonpath_recommendation():
"""Show PYTHONPATH recommendation for fixing import errors.
Generates and displays the recommended PYTHONPATH based on discovered
component source paths in the workspace.
""" """
paths = [] paths = []
if not self.workspace_dir:
return "" # Try to find workspace directory
workspace_dir = None
candidates = [
os.getcwd(),
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
os.environ.get("DYNAMO_HOME", ""),
os.path.expanduser("~/dynamo"),
]
for candidate in candidates:
if os.path.exists(os.path.join(candidate, "lib/bindings/python/src/dynamo")):
workspace_dir = os.path.abspath(candidate)
break
if not workspace_dir:
return
# Collect all component source paths # Collect all component source paths
comp_path = f"{self.workspace_dir}/components" comp_path = os.path.join(workspace_dir, "components")
if os.path.exists(comp_path): if os.path.exists(comp_path):
for item in os.listdir(comp_path): for item in os.listdir(comp_path):
src_path = f"{comp_path}/{item}/src" if item == "backends":
continue # Handle backends separately
src_path = os.path.join(comp_path, item, "src")
if os.path.exists(src_path): if os.path.exists(src_path):
paths.append(src_path) paths.append(src_path)
# Collect all backend source paths # Collect all backend source paths
backend_path = f"{self.workspace_dir}/components/backends" backend_path = os.path.join(workspace_dir, "components", "backends")
if os.path.exists(backend_path): if os.path.exists(backend_path):
for item in os.listdir(backend_path): for item in os.listdir(backend_path):
src_path = f"{backend_path}/{item}/src" src_path = os.path.join(backend_path, item, "src")
if os.path.exists(src_path): if os.path.exists(src_path):
paths.append(src_path) paths.append(src_path)
return ":".join(paths) # Also add runtime path
runtime_path = os.path.join(workspace_dir, "lib/bindings/python/src")
if os.path.exists(runtime_path):
paths.insert(0, runtime_path) # Add at beginning for priority
# ==================================================================== if paths:
# MAIN ORCHESTRATION pythonpath = ":".join(paths)
# ==================================================================== # Replace home directory with $HOME
home = os.path.expanduser("~")
def run_all(self): if home in pythonpath:
"""Run comprehensive check with all functionality. pythonpath = pythonpath.replace(home, "$HOME")
Performs complete dynamo package validation including: print(f'\nSet PYTHONPATH for development:\nexport PYTHONPATH="{pythonpath}"\n')
- Component discovery and import testing
- Usage examples and troubleshooting guidance
- Summary of results
Console output: terse, tree-formatted sections
"""
# Terse mode: no banner or separators
# Execute all checks (package versions now shown in import testing headers)
self.results["imports"] = self.test_imports()
# Check if there were any import failures
import_results = self.results.get("imports", {})
has_failures = any(result.startswith("❌") for result in import_results.values())
# Provide guidance (show only if all checks succeed and no errors flagged)
had_errors_flag = bool(self.results.get("had_errors"))
if not has_failures and not had_errors_flag:
self._show_build_options()
# If any errors found, exit with status 1
had_errors = bool(self.results.get("had_errors"))
if had_errors:
sys.exit(1)
def main():
"""Main function - collect and display system information"""
import argparse
import sys
def main() -> None: # Parse command line arguments
"""Main function with command line argument parsing.""" parser = argparse.ArgumentParser(
parser = argparse.ArgumentParser(description="Comprehensive dynamo package checker") description="Display system information for Dynamo project"
parser.add_argument(
"--import-check-only", action="store_true", help="Only test imports"
)
parser.add_argument("--examples", action="store_true", help="Only show examples")
parser.add_argument(
"--build-options",
action="store_true",
help="Show build options for missing framework components",
) )
parser.add_argument( parser.add_argument(
"--try-pythonpath", "-f",
"--fast",
action="store_true", action="store_true",
help="Test imports with workspace component source directories in sys.path", help="Skip size calculations for faster output",
) )
parser.add_argument(
"--path",
type=str,
default=None,
help="Explicit path to dynamo workspace; if set, bypass workspace auto-discovery",
)
args = parser.parse_args() args = parser.parse_args()
checker = DynamoChecker(workspace_dir=args.path)
# If --path is provided, validate it; do not exit early, but record error to display and for exit code
if args.path:
abs_path = os.path.abspath(args.path)
if (not os.path.exists(abs_path)) or (
not checker._is_dynamo_workspace(abs_path)
):
checker._deferred_messages.append(
f"❌ Error: invalid workspace path: {abs_path}"
)
# Set up sys.path if requested # Simply create a SystemInfo instance - it collects everything in its constructor
if args.try_pythonpath: tree = SystemInfo(fast_mode=args.fast)
checker._setup_pythonpath() tree.print_tree()
# Check if there are framework component errors and show PYTHONPATH recommendation
if has_framework_errors(tree):
show_pythonpath_recommendation()
if args.import_check_only: # Exit with non-zero status if there are any errors
checker.test_imports() if tree.has_errors():
# Exit code handled inside run; reflect errors if set
had_errors = bool(checker.results.get("had_errors"))
if had_errors:
sys.exit(1) sys.exit(1)
# If examples are also requested and imports succeeded, show them
if args.examples:
checker._show_build_options()
# If build options are also requested, show them
if args.build_options:
if checker.workspace_dir:
pythonpath = checker._get_pythonpath()
if pythonpath:
display_pythonpath = checker._replace_home_with_var(pythonpath)
checker._show_build_options(display_pythonpath)
else:
print("❌ Error: Could not determine PYTHONPATH for build options")
else:
print("❌ Error: No dynamo workspace found for build options")
elif args.build_options:
# Show build options directly
if checker.workspace_dir:
pythonpath = checker._get_pythonpath()
if pythonpath:
display_pythonpath = checker._replace_home_with_var(pythonpath)
checker._show_build_options(display_pythonpath)
else:
print("❌ Error: Could not determine PYTHONPATH for build options")
else:
print("❌ Error: No dynamo workspace found for build options")
elif args.examples:
# Only show examples, no system info or environment header
checker._show_build_options()
else: else:
checker.run_all() sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment