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:
2026-06-11 14:56:40 +08:00
parent 475042c85c
commit 21acc16c8c
2 changed files with 197 additions and 61 deletions
+15 -1
View File
@@ -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)}")
+177 -55
View File
@@ -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,17 +338,25 @@ 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)
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=found,
path=best_path,
version=version,
alias=alias,
alias=best_alias,
)
return ToolStatus(
@@ -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.