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.
513 lines
16 KiB
Python
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,
|
|
}
|