Files
Zynq_Flasher/src/vitis_checker.py
T
yuysh 799da5b2f3 fix: Windows common paths try .bat before .exe (Xilinx tools are .bat)
Vitis/Vivado on Windows uses .bat wrappers, not .exe. The hardcoded
fallback paths in _find_executable step 4 only looked for .exe files.
Also expanded to try both 2023.2 and 2022.2 + Vitis/Vivado combos.
2026-06-11 17:34:36 +08:00

513 lines
16 KiB
Python

"""Vitis/Vivado tool detection for Zynq Flasher GUI.
Checks for required Xilinx tools:
- xsct (Xilinx Software Command Line Tool) — JTAG operations
- 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.
"""
from __future__ import annotations
import os
import platform
import re
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from config_manager import Config
@dataclass
class ToolStatus:
"""Status of a single Vitis/Vivado tool."""
name: str
found: bool
path: str = ""
version: str = ""
error: str = ""
alias: str = ""
@dataclass
class VitisCheckResult:
"""Result of a complete Vitis/Vivado environment check."""
system: str
vitis_path: str
tools: dict[str, ToolStatus]
is_ready: bool
errors: list[str]
# ── Tool aliases: canonical name → [possible executable names] ──────
TOOL_ALIASES: dict[str, list[str]] = {
"xsct": ["xsct"],
"program_flash": ["program_flash"],
"bootgen": ["bootgen"],
"hw_server": ["hw_server"],
}
# 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.
On Windows also checks .bat wrappers in addition to .exe.
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
# On Windows, also try .bat extension (Xilinx tools use .bat wrappers)
suffixes = [exec_suffix]
if exec_suffix == ".exe":
suffixes.append(".bat")
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
for sfx in suffixes:
candidate = ver_dir / "bin" / f"{exec_name}{sfx}"
if candidate.is_file():
v = _parse_version_tuple(ver_dir.name)
if v != (0,):
results.append((v, candidate, product))
break # Found for this version dir
# Sort newest first (descending version tuple)
results.sort(key=lambda x: x[0], reverse=True)
return results
def _find_executable(
exec_name: str,
vitis_path: str,
system: str,
xilinx_root: str = "",
) -> str | None:
"""Find an executable by name using PATH, Vitis, and Xilinx root.
Search order:
1. System PATH (shutil.which)
2. vitis_path/bin (legacy config)
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, C:/Xilinx).
Returns:
Absolute path to the best executable, or None.
"""
exec_suffix = ".exe" if system == "windows" else ""
# 1. System PATH
found = shutil.which(f"{exec_name}{exec_suffix}")
if found:
return found
# 2. Legacy vitis_path/bin
if vitis_path:
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
if candidate.is_file():
return str(candidate)
# 3. Scan xilinx_root — pick newest version across Vitis + Vivado
if xilinx_root:
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 fallback)
if system == "windows":
common: list[str] = []
for sfx in (".bat", ".exe"):
for ver in ("2023.2", "2022.2"):
for prod in ("Vitis", "Vivado"):
common.append(f"C:/Xilinx/{prod}/{ver}/bin/{exec_name}{sfx}")
else:
common = [
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
f"/data/xilinx/Vivado/2023.2/bin/{exec_name}",
f"/opt/xilinx/bin/{exec_name}",
os.path.expanduser(f"~/Xilinx/Vitis/2023.2/bin/{exec_name}"),
os.path.expanduser(f"~/Xilinx/Vivado/2023.2/bin/{exec_name}"),
]
for p in common:
if os.path.isfile(p):
return p
return None
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:
xilinx_root: Xilinx root directory.
exec_name: Executable name without extension.
Returns:
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
]
# ── Environment setup ────────────────────────────────────────────────
def _find_settings_script(tool_path: str) -> str | None:
"""Find the settings64 script for a Xilinx tool's version directory.
Walks up from the tool's bin/ directory looking for
settings64.sh (Linux) or settings64.bat (Windows).
Args:
tool_path: Absolute path to a Xilinx tool executable.
Returns:
Path to settings64 script, or None if not found.
"""
p = Path(tool_path).resolve()
for parent in p.parents:
for name in ("settings64.sh", "settings64.bat"):
candidate = parent / name
if candidate.is_file():
return str(candidate)
return None
def build_tool_command(tool_path: str, *args: str) -> list[str]:
"""Build a subprocess-safe command with Xilinx environment setup.
On Linux: sources settings64.sh before running the tool.
On Windows: .bat wrappers handle environment internally.
Args:
tool_path: Path to the Xilinx tool executable.
*args: Arguments to pass to the tool.
Returns:
Command list suitable for subprocess.run().
"""
import shlex
system = platform.system().lower()
# Windows: .bat wrappers set up their own environment
if system == "windows":
return [tool_path, *args]
# Linux: find settings64.sh and source it before running
settings = _find_settings_script(tool_path)
if not settings:
return [tool_path, *args]
quoted = " ".join(shlex.quote(a) for a in [tool_path, *args])
return ["bash", "-c", f"source {shlex.quote(settings)} && {quoted}"]
# ── 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."""
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."""
return _find_executable("bootgen", vitis_path, platform.system().lower(), xilinx_root)
def get_vivado_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the Vivado executable (JTAG operations via hw_server).
Args:
vitis_path: Legacy Vitis installation directory.
xilinx_root: Xilinx root directory.
Returns:
Path to vivado executable, or None.
"""
return _find_executable("vivado", vitis_path, platform.system().lower(), xilinx_root)
def get_hw_server_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the hw_server executable (Xilinx hardware server).
hw_server must be running before Vivado/xsct can connect to the
JTAG chain. This finder searches the same Vitis/Vivado bin dirs.
Args:
vitis_path: Legacy Vitis installation directory.
xilinx_root: Xilinx root directory.
Returns:
Path to hw_server executable, or None.
"""
return _find_executable(
"hw_server", vitis_path, platform.system().lower(), xilinx_root
)
# ── 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 → parse version from output
2. Extract from path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2)
Args:
tool_path: Absolute path to the tool executable.
Returns:
Version string, or empty string if undetectable.
"""
# Try -version flag
try:
result = subprocess.run(
[tool_path, "-version"],
capture_output=True,
text=True,
timeout=10,
)
output = (result.stdout + result.stderr).strip()
if output:
# Scan all lines for a version pattern (skip batch echo noise)
for raw_line in output.splitlines():
line = raw_line.strip()
if not line:
continue
# Skip Windows batch echo noise
if _is_batch_noise(line):
continue
m = re.search(r"(\d{4}\.\d+(?:\.\d+)?)", line)
if m:
return m.group(1)
except (subprocess.TimeoutExpired, OSError):
pass
# Fallback: extract from path
return _version_from_path(tool_path)
def _is_batch_noise(line: str) -> bool:
"""Check if a line is Windows batch script noise.
Args:
line: A line of output text.
Returns:
True if the line looks like batch echo/status noise.
"""
noise_patterns = [
r"^ECHO\s+(is\s+(on|off)|处于)", # ECHO is on/off (en+zh)
r"^[A-Z]:\\[^>]*>", # Prompt line like C:\path>
]
return any(re.search(p, line, re.IGNORECASE) for p in noise_patterns)
# ── 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 and version info.
"""
system = platform.system().lower()
vitis_path = config.vitis_path if hasattr(config, 'vitis_path') else ""
xilinx_root = config.xilinx_path if hasattr(config, 'xilinx_path') else ""
tools: dict[str, ToolStatus] = {}
errors: list[str] = []
for tool_name, aliases in TOOL_ALIASES.items():
status = _check_tool(tool_name, aliases, vitis_path, system, xilinx_root)
tools[tool_name] = status
if not status.found:
errors.append(f"{tool_name}: {status.error or 'not found'}")
return VitisCheckResult(
system=system,
vitis_path=xilinx_root or vitis_path,
tools=tools,
is_ready=len(errors) == 0,
errors=errors,
)
def _check_tool(
tool_name: str,
aliases: list[str],
vitis_path: str,
system: str,
xilinx_root: str = "",
) -> ToolStatus:
"""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:
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,
found=False,
error=(
f"Could not locate {', '.join(aliases)} in PATH"
+ (f" or {xilinx_root}" if xilinx_root else "")
+ (f" or {vitis_path}" if vitis_path else "")
),
)
# ── Status serialization ─────────────────────────────────────────────
def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
"""Convert a VitisCheckResult to a serializable dictionary.
Args:
result: VitisCheckResult from check_vitis().
Returns:
Dictionary suitable for JSON serialization / GUI display.
"""
return {
"system": result.system,
"vitis_path": result.vitis_path,
"is_ready": result.is_ready,
"tools": {
name: {
"found": status.found,
"path": status.path,
"version": status.version,
"error": status.error,
"alias": status.alias,
}
for name, status in result.tools.items()
},
"errors": result.errors,
}