From 21acc16c8c8ae78683365394bb018841642b869a Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Thu, 11 Jun 2026 14:56:40 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20smart=20Xilinx=20version=20?= =?UTF-8?q?scanning=20=E2=80=94=20pick=20newest,=20log=20all=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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 --- src/gui/main_window.py | 16 ++- src/vitis_checker.py | 242 +++++++++++++++++++++++++++++++---------- 2 files changed, 197 insertions(+), 61 deletions(-) diff --git a/src/gui/main_window.py b/src/gui/main_window.py index ba3c3db..dd38e16 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -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)}") diff --git a/src/vitis_checker.py b/src/vitis_checker.py index b1e9ba0..877036b 100644 --- a/src/vitis_checker.py +++ b/src/vitis_checker.py @@ -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.