✨ feat: smart Xilinx version scanning — pick newest, log all versions
_version discovery: - _scan_xilinx_root() collects ALL versions under Vitis/*/bin/ and Vivado/*/bin/ - _parse_version_tuple() semantic version comparison (2023.2 > 2018.3) - Defaults to newest version, Vitis preferred over Vivado for same version - _find_executable() search order: PATH → legacy vitis_path → scan xilinx_root → common paths _version reporting: - _get_tool_version(): tries -version flag first, falls back to path parsing - _version_from_path(): extracts version from install path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2) - Step 1 logs each tool with version: ✓ xsct: /path [v2023.2] - If multiple versions found, lists all: xsct versions found: 2023.2 (Vitis), 2018.3 (Vivado) Cross-platform: - Handles Windows path separators via Path() - Expanded common Windows install paths
This commit is contained in:
+15
-1
@@ -767,19 +767,33 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
# Check Vitis/Vivado tools
|
||||
self._log_message(" Checking Vitis/Vivado tools...")
|
||||
if self._config.xilinx_path:
|
||||
self._log_message(f" Xilinx root: {self._config.xilinx_path}")
|
||||
result = check_vitis(self._config)
|
||||
status_dict = get_tool_status_dict(result)
|
||||
|
||||
for tool_name, tool_info in status_dict["tools"].items():
|
||||
if tool_info["found"]:
|
||||
ver_info = f" [v{tool_info['version']}]" if tool_info.get("version") else ""
|
||||
alias_note = f" (via {tool_info['alias']})" if tool_info.get("alias") and tool_info["alias"] != tool_name else ""
|
||||
self._log_message(f" ✓ {tool_name}: {tool_info['path']}{alias_note}")
|
||||
self._log_message(f" ✓ {tool_name}: {tool_info['path']}{ver_info}{alias_note}")
|
||||
else:
|
||||
self._log_message(f" ✗ {tool_name}: {tool_info['error']}")
|
||||
|
||||
if result.is_ready:
|
||||
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
||||
self._log_message(" Vitis/Vivado tools available")
|
||||
# Report all found versions for diagnostics
|
||||
if self._config.xilinx_path:
|
||||
try:
|
||||
from vitis_checker import get_all_found_versions
|
||||
for t in ("xsct", "program_flash", "bootgen"):
|
||||
vers = get_all_found_versions(self._config.xilinx_path, t)
|
||||
if len(vers) > 1:
|
||||
ver_list = ", ".join(f"{v} ({p})" for v, p in vers)
|
||||
self._log_message(f" {t} versions found: {ver_list}")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self._vitis_status.configure(text="Vitis: Partial", text_color=WARNING_COLOR)
|
||||
self._log_message(f" Issues: {'; '.join(result.errors)}")
|
||||
|
||||
+182
-60
@@ -5,6 +5,12 @@ Checks for required Xilinx tools:
|
||||
- program_flash — QSPI flash programming
|
||||
- bootgen — BOOT.BIN generation (image creation only, not hardware)
|
||||
|
||||
Version selection strategy:
|
||||
- Scans all Vitis/Vivado version directories under xilinx_root
|
||||
- Picks the NEWEST version (by semantic version comparison)
|
||||
- Logs all found versions so the user can verify the choice
|
||||
- If a specific version is needed, set xilinx_path directly
|
||||
|
||||
Provides shared helpers used by flash_programmer and bitstream_programmer.
|
||||
"""
|
||||
|
||||
@@ -12,6 +18,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
@@ -51,6 +58,92 @@ TOOL_ALIASES: dict[str, list[str]] = {
|
||||
"bootgen": ["bootgen"],
|
||||
}
|
||||
|
||||
# Product subdirectories under xilinx_root to scan (in preference order)
|
||||
_PRODUCT_DIRS = ("Vitis", "Vivado")
|
||||
|
||||
|
||||
# ── Version helpers ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _parse_version_tuple(dirname: str) -> tuple[int, ...]:
|
||||
"""Parse a semantic-like version from a directory name.
|
||||
|
||||
E.g., '2023.2' → (2023, 2), '2018.3' → (2018, 3).
|
||||
Returns (0,) for unparseable names.
|
||||
|
||||
Args:
|
||||
dirname: Directory name (e.g., '2023.2', '2018.3.1').
|
||||
|
||||
Returns:
|
||||
Tuple of integers for version comparison.
|
||||
"""
|
||||
m = re.match(r"(\d{4})\.(\d+)(?:\.(\d+))?", dirname)
|
||||
if m:
|
||||
return tuple(int(g) for g in m.groups() if g is not None)
|
||||
return (0,)
|
||||
|
||||
|
||||
def _version_from_path(tool_path: str) -> str:
|
||||
"""Extract version string from a tool's install path.
|
||||
|
||||
E.g., '/data/xilinx/Vitis/2023.2/bin/xsct' → '2023.2'.
|
||||
|
||||
Args:
|
||||
tool_path: Absolute path to the tool executable.
|
||||
|
||||
Returns:
|
||||
Version string or empty string.
|
||||
"""
|
||||
parts = Path(tool_path).parts
|
||||
for i, part in enumerate(parts):
|
||||
if re.match(r"\d{4}\.\d+", part) and i > 0:
|
||||
return part
|
||||
return ""
|
||||
|
||||
|
||||
# ── Executable discovery ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _scan_xilinx_root(
|
||||
xilinx_root: str, exec_name: str, exec_suffix: str,
|
||||
) -> list[tuple[tuple[int, ...], Path, str]]:
|
||||
"""Scan xilinx_root for all installed versions of a tool.
|
||||
|
||||
Searches under Vitis/*/bin/ and Vivado/*/bin/ for the executable.
|
||||
|
||||
Args:
|
||||
xilinx_root: Xilinx root directory (e.g., /data/xilinx, C:/Xilinx).
|
||||
exec_name: Executable name without extension.
|
||||
exec_suffix: Platform suffix ('' or '.exe').
|
||||
|
||||
Returns:
|
||||
List of (version_tuple, full_path, product_name) sorted newest first.
|
||||
"""
|
||||
results: list[tuple[tuple[int, ...], Path, str]] = []
|
||||
xr = Path(xilinx_root)
|
||||
if not xr.is_dir():
|
||||
return results
|
||||
|
||||
for product in _PRODUCT_DIRS:
|
||||
pdir = xr / product
|
||||
if not pdir.is_dir():
|
||||
continue
|
||||
try:
|
||||
entries = list(pdir.iterdir())
|
||||
except OSError:
|
||||
continue
|
||||
for ver_dir in entries:
|
||||
if not ver_dir.is_dir():
|
||||
continue
|
||||
candidate = ver_dir / "bin" / f"{exec_name}{exec_suffix}"
|
||||
if candidate.is_file():
|
||||
v = _parse_version_tuple(ver_dir.name)
|
||||
if v != (0,):
|
||||
results.append((v, candidate, product))
|
||||
# Sort newest first (descending version tuple)
|
||||
results.sort(key=lambda x: x[0], reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
def _find_executable(
|
||||
exec_name: str,
|
||||
@@ -63,17 +156,17 @@ def _find_executable(
|
||||
Search order:
|
||||
1. System PATH (shutil.which)
|
||||
2. vitis_path/bin (legacy config)
|
||||
3. xilinx_root/Vitis/*/bin and Vivado/*/bin (multi-version scan)
|
||||
4. Common install paths
|
||||
3. xilinx_root/Vitis/*/bin and Vivado/*/bin — pick NEWEST version
|
||||
4. Common install paths (cross-platform)
|
||||
|
||||
Args:
|
||||
exec_name: Executable name (without extension).
|
||||
vitis_path: Legacy Vitis installation directory.
|
||||
system: Platform identifier (linux, windows, darwin).
|
||||
xilinx_root: Xilinx root directory (e.g., /data/xilinx or C:/Xilinx).
|
||||
xilinx_root: Xilinx root directory (e.g., /data/xilinx, C:/Xilinx).
|
||||
|
||||
Returns:
|
||||
Absolute path to the executable, or None.
|
||||
Absolute path to the best executable, or None.
|
||||
"""
|
||||
exec_suffix = ".exe" if system == "windows" else ""
|
||||
|
||||
@@ -88,28 +181,22 @@ def _find_executable(
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
|
||||
# 3. Scan xilinx_root for all versions (Vitis + Vivado)
|
||||
# 3. Scan xilinx_root — pick newest version across Vitis + Vivado
|
||||
if xilinx_root:
|
||||
xr = Path(xilinx_root)
|
||||
for product in ("Vitis", "Vivado"):
|
||||
pdir = xr / product
|
||||
if not pdir.is_dir():
|
||||
continue
|
||||
# Scan version directories, newest first
|
||||
versions = sorted(
|
||||
[d for d in pdir.iterdir() if d.is_dir()],
|
||||
reverse=True,
|
||||
)
|
||||
for ver_dir in versions:
|
||||
candidate = ver_dir / "bin" / f"{exec_name}{exec_suffix}"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
candidates = _scan_xilinx_root(xilinx_root, exec_name, exec_suffix)
|
||||
if candidates:
|
||||
# Preference: Vitis over Vivado if same version
|
||||
# candidates are already sorted by version desc;
|
||||
# within same version, Vitis appears first (scanned first)
|
||||
return str(candidates[0][1])
|
||||
|
||||
# 4. Common install paths (cross-platform)
|
||||
# 4. Common install paths (cross-platform fallback)
|
||||
if system == "windows":
|
||||
common = [
|
||||
f"C:/Xilinx/Vitis/2023.2/bin/{exec_name}.exe",
|
||||
f"C:/Xilinx/Vivado/2023.2/bin/{exec_name}.exe",
|
||||
f"C:/Xilinx/Vitis/2022.2/bin/{exec_name}.exe",
|
||||
f"C:/Xilinx/Vivado/2022.2/bin/{exec_name}.exe",
|
||||
]
|
||||
else:
|
||||
common = [
|
||||
@@ -126,78 +213,102 @@ def _find_executable(
|
||||
return None
|
||||
|
||||
|
||||
def get_xsct_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||
"""Find the xsct (Xilinx Software Command Line Tool) executable.
|
||||
def get_all_found_versions(
|
||||
xilinx_root: str,
|
||||
exec_name: str,
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Return all found versions of a tool under xilinx_root.
|
||||
|
||||
Useful for logging which versions are available.
|
||||
|
||||
Args:
|
||||
vitis_path: Legacy Vitis install path.
|
||||
xilinx_root: Xilinx root directory.
|
||||
exec_name: Executable name without extension.
|
||||
|
||||
Returns:
|
||||
Absolute path to xsct, or None.
|
||||
List of (version_string, product_name) sorted newest first.
|
||||
"""
|
||||
system = platform.system().lower()
|
||||
suffix = ".exe" if system == "windows" else ""
|
||||
candidates = _scan_xilinx_root(xilinx_root, exec_name, suffix)
|
||||
return [
|
||||
(Path(str(p)).parent.parent.name, product)
|
||||
for _, p, product in candidates
|
||||
]
|
||||
|
||||
|
||||
# ── Public path getters ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_xsct_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||
"""Find the xsct (Xilinx Software Command Line Tool) executable."""
|
||||
return _find_executable("xsct", vitis_path, platform.system().lower(), xilinx_root)
|
||||
|
||||
|
||||
def get_program_flash_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||
"""Find the program_flash executable.
|
||||
|
||||
Args:
|
||||
vitis_path: Legacy Vitis install path.
|
||||
xilinx_root: Xilinx root directory.
|
||||
|
||||
Returns:
|
||||
Absolute path to program_flash, or None.
|
||||
"""
|
||||
"""Find the program_flash executable."""
|
||||
return _find_executable("program_flash", vitis_path, platform.system().lower(), xilinx_root)
|
||||
|
||||
|
||||
def get_bootgen_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||
"""Find the bootgen executable.
|
||||
|
||||
Args:
|
||||
vitis_path: Legacy Vitis install path.
|
||||
xilinx_root: Xilinx root directory.
|
||||
|
||||
Returns:
|
||||
Absolute path to bootgen, or None.
|
||||
"""
|
||||
"""Find the bootgen executable."""
|
||||
return _find_executable("bootgen", vitis_path, platform.system().lower(), xilinx_root)
|
||||
|
||||
|
||||
def _get_tool_version(tool_path: str, tool_name: str) -> str:
|
||||
"""Get the version/h banner from a tool executable.
|
||||
# ── Tool version detection ───────────────────────────────────────────
|
||||
|
||||
|
||||
def _get_tool_version(tool_path: str) -> str:
|
||||
"""Get the version string from a tool executable.
|
||||
|
||||
Tries (in order):
|
||||
1. tool -version
|
||||
2. Extract from path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2)
|
||||
|
||||
Args:
|
||||
tool_path: Absolute path to the tool.
|
||||
tool_name: Canonical tool name.
|
||||
tool_path: Absolute path to the tool executable.
|
||||
|
||||
Returns:
|
||||
First line of help output (truncated to 120 chars), or empty string.
|
||||
Version string, or empty string if undetectable.
|
||||
"""
|
||||
# Try -version flag
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[tool_path, "-help"],
|
||||
[tool_path, "-version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
if output:
|
||||
return output.split("\n")[0][:120]
|
||||
# Look for a version pattern in output
|
||||
m = re.search(r"(\d{4}\.\d+(?:\.\d+)?)", output)
|
||||
if m:
|
||||
return m.group(1)
|
||||
# Return first meaningful line (truncated)
|
||||
line = output.split("\n")[0][:80]
|
||||
if line and "usage" not in line.lower():
|
||||
return line
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
pass
|
||||
return ""
|
||||
|
||||
# Fallback: extract from path
|
||||
return _version_from_path(tool_path)
|
||||
|
||||
|
||||
# ── Main check function ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def check_vitis(config: Config) -> VitisCheckResult:
|
||||
"""Check availability of all required Xilinx tools.
|
||||
|
||||
Scans all versions under xilinx_root and picks the newest.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
|
||||
Returns:
|
||||
VitisCheckResult with per-tool status.
|
||||
VitisCheckResult with per-tool status and version info.
|
||||
"""
|
||||
system = platform.system().lower()
|
||||
vitis_path = config.vitis_path if hasattr(config, 'vitis_path') else ""
|
||||
@@ -227,18 +338,26 @@ def _check_tool(
|
||||
system: str,
|
||||
xilinx_root: str = "",
|
||||
) -> ToolStatus:
|
||||
"""Check if a single tool is available."""
|
||||
"""Check if a single tool is available, picking the newest version."""
|
||||
best_path: str | None = None
|
||||
best_alias: str = ""
|
||||
|
||||
for alias in aliases:
|
||||
found = _find_executable(alias, vitis_path, system, xilinx_root)
|
||||
if found:
|
||||
version = _get_tool_version(found, tool_name)
|
||||
return ToolStatus(
|
||||
name=tool_name,
|
||||
found=True,
|
||||
path=found,
|
||||
version=version,
|
||||
alias=alias,
|
||||
)
|
||||
best_path = found
|
||||
best_alias = alias if alias != tool_name else ""
|
||||
break
|
||||
|
||||
if best_path:
|
||||
version = _get_tool_version(best_path)
|
||||
return ToolStatus(
|
||||
name=tool_name,
|
||||
found=True,
|
||||
path=best_path,
|
||||
version=version,
|
||||
alias=best_alias,
|
||||
)
|
||||
|
||||
return ToolStatus(
|
||||
name=tool_name,
|
||||
@@ -251,6 +370,9 @@ def _check_tool(
|
||||
)
|
||||
|
||||
|
||||
# ── Status serialization ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
|
||||
"""Convert a VitisCheckResult to a serializable dictionary.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user