♻️ refactor(toolchain): replace impact/bootgen with xsct/program_flash + add fsbl

BREAKING: Replaces obsolete impact (ISE-era) and misused bootgen with
the correct Xilinx 2018.3–2023.2 toolchain.

Toolchain changes:
- vitis_checker: impact → xsct + program_flash + bootgen detection
- flash_programmer: bootgen/impact → program_flash (erase + program + verify)
- bitstream_programmer: impact → xsct fpga/dow (primary), vivado (fallback)
- Removed duplicated _get_impact_path() — now shared from vitis_checker

Config additions:
- fsbl_elf_path: FSBL ELF required by program_flash for QSPI init
- flash_type: QSPI mode (default: qspi-x4-single)

GUI additions:
- FSBL ELF file selector in Configuration panel
- Step 2 validates FSBL ELF is selected
- Step 1 now shows xsct/program_flash/bootgen in tool check
This commit is contained in:
Jeremy Shen
2026-06-10 12:13:02 +08:00
parent 1ad2dcdeb9
commit cddacc5d9f
6 changed files with 567 additions and 475 deletions
+5
View File
@@ -19,7 +19,12 @@ bootloader_bit_path: ""
bootloader_elf_path: "" bootloader_elf_path: ""
# Flash programming (Step 2: Program & Verify Flash) # Flash programming (Step 2: Program & Verify Flash)
# bootloader_bin_path is the bootloader image to program into QSPI flash
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
bootloader_bin_path: "" bootloader_bin_path: ""
fsbl_elf_path: ""
flash_type: "qspi-x4-single"
# Firmware upload (Step 4: Load Main Program via TFTP) # Firmware upload (Step 4: Load Main Program via TFTP)
firmware_bin_path: "" firmware_bin_path: ""
+249 -210
View File
@@ -1,13 +1,16 @@
"""Bitstream programming module for Zynq Flasher GUI. """Bitstream programming module for Zynq Flasher GUI.
Handles programming the Zynq device with a .bit bitstream file Downloads a .bit bitstream to the Zynq PL fabric and runs a .elf
and running an .elf executable afterward. executable on the PS ARM core.
Uses xsct (Xilinx Software Command Line Tool) as the primary tool
for JTAG operations, with Vivado batch mode as a fallback.
Compatible with Xilinx 2018.3 — 2023.2.
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import platform
import shutil import shutil
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
@@ -15,11 +18,14 @@ from pathlib import Path
from typing import Callable from typing import Callable
from config_manager import Config from config_manager import Config
from vitis_checker import get_xsct_path
# ── Types ───────────────────────────────────────────────────────────
@dataclass @dataclass
class BitstreamResult: class BitstreamResult:
"""Result of a bitstream programming operation.""" """Result of a bitstream programming or ELF execution operation."""
step: str step: str
success: bool success: bool
@@ -29,93 +35,95 @@ class BitstreamResult:
ProgressCallback = Callable[[str, str], None] | None ProgressCallback = Callable[[str, str], None] | None
# ── Helpers ─────────────────────────────────────────────────────────
def _get_impact_path(vitis_path: str) -> str | None:
"""Find the impact executable path.
Tries 'impact' first, then falls back to 'xsct' (Xilinx 2023.2+).
Uses shutil.which() for cross-platform compatibility,
then falls back to common Vitis paths.
Args:
vitis_path: Vitis installation path.
Returns:
Path to impact or xsct executable, or None if not found.
"""
# Try shutil.which for impact, then xsct as fallback
for tool in ("impact", "xsct"):
path = shutil.which(tool)
if path:
return path
if vitis_path:
bin_dir = Path(vitis_path) / "bin"
if bin_dir.exists():
for tool in ("impact", "xsct"):
for ext in ("", ".exe"):
candidate = bin_dir / f"{tool}{ext}"
if candidate.exists():
return str(candidate)
common_paths = [
"/opt/xilinx/bin/impact",
os.path.expanduser("~/Xilinx/Vitis/bin/impact"),
]
for p in common_paths:
if os.path.isfile(p):
return p
return None
def _get_vivado_path(vitis_path: str) -> str | None: def _get_vivado_path(vitis_path: str) -> str | None:
"""Find the Vivado executable path. """Find the Vivado executable.
Uses shutil.which() for cross-platform compatibility, Uses shutil.which() for PATH resolution, then common install paths.
then falls back to common Vitis paths.
Args: Args:
vitis_path: Vitis installation path. vitis_path: Optional Vitis installation directory.
Returns: Returns:
Path to Vivado executable, or None if not found. Path to vivado executable, or None.
""" """
path = shutil.which("vivado") found = shutil.which("vivado")
if path: if found:
return path return found
if vitis_path: if vitis_path:
candidate = Path(vitis_path) / "bin" / "vivado" candidate = Path(vitis_path) / "bin" / "vivado"
if candidate.exists(): if candidate.is_file():
return str(candidate) return str(candidate)
candidate = Path(vitis_path) / "bin" / "vivado.exe" common = [
if candidate.exists(): "/data/xilinx/Vivado/2023.2/bin/vivado",
return str(candidate)
common_paths = [
"/opt/xilinx/bin/vivado", "/opt/xilinx/bin/vivado",
os.path.expanduser("~/Xilinx/Vivado/bin/vivado"), os.path.expanduser("~/Xilinx/Vivado/2023.2/bin/vivado"),
] ]
for p in common_paths: for p in common:
if os.path.isfile(p): if os.path.isfile(p):
return p return p
return None return None
def _run_command(
cmd: list[str],
timeout: int = 120,
callback: ProgressCallback = None,
) -> subprocess.CompletedProcess[str]:
"""Run a subprocess command with progress reporting.
Args:
cmd: Command and arguments.
timeout: Maximum seconds.
callback: Optional callback(status, message).
Returns:
CompletedProcess result.
Raises:
subprocess.TimeoutExpired: If command exceeds timeout.
"""
if callback:
callback("running", f"Executing: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
)
if callback:
if result.returncode == 0:
callback("complete", f"{cmd[0]} succeeded")
else:
callback("error", f"{cmd[0]} failed (exit {result.returncode})")
return result
# ── Bitstream Download ──────────────────────────────────────────────
def program_bitstream( def program_bitstream(
config: Config, config: Config,
bit_path: str | Path, bit_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> BitstreamResult: ) -> BitstreamResult:
"""Program the Zynq device with a .bit bitstream file. """Download a .bit bitstream to the Zynq PL (FPGA fabric).
Uses Vivado/impact to download the bitstream to the FPGA fabric. Attempts xsct first (fast, lightweight), then falls back to Vivado
batch mode.
Args: Args:
config: Application configuration. config: Application configuration.
bit_path: Path to the .bit bitstream file. bit_path: Path to the .bit file.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
BitstreamResult with status. BitstreamResult with status.
""" """
bit_path = Path(bit_path).resolve() bit_path = Path(bit_path)
if not bit_path.exists(): if not bit_path.exists():
return BitstreamResult( return BitstreamResult(
step="bitstream", step="bitstream",
@@ -126,30 +134,79 @@ def program_bitstream(
if callback: if callback:
callback("start", f"Loading bitstream: {bit_path.name}...") callback("start", f"Loading bitstream: {bit_path.name}...")
vivado = _get_vivado_path(config.vitis_path) # Try xsct first (preferred)
impact = _get_impact_path(config.vitis_path) xsct = get_xsct_path(config.vitis_path)
if xsct:
return _program_with_xsct(xsct, bit_path, callback)
# Try Vivado first (preferred for Zynq) # Fallback to Vivado
vivado = _get_vivado_path(config.vitis_path)
if vivado: if vivado:
return _program_with_vivado(vivado, bit_path, callback) return _program_with_vivado(vivado, bit_path, callback)
# Fallback to impact
if impact:
return _program_with_impact(impact, bit_path, callback)
return BitstreamResult( return BitstreamResult(
step="bitstream", step="bitstream",
success=False, success=False,
message="Neither vivado nor impact found", message="Neither xsct nor vivado found in PATH",
) )
def _program_with_xsct(
xsct_path: str,
bit_path: Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Program bitstream using xsct's fpga command.
xsct connects to hw_server and uses the `fpga` command to download
the bitstream to the PL fabric.
Args:
xsct_path: Path to xsct executable.
bit_path: Path to .bit file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
if callback:
callback("progress", "Using xsct to download bitstream...")
# xsct TCL script: connect to hw_server, target FPGA, download bitstream
tcl_script = f"""
connect
targets
fpga -file "{bit_path}"
quit
"""
try:
result = _run_command(
[xsct_path, "-eval", tcl_script],
timeout=120,
callback=callback,
)
success = result.returncode == 0
output = (result.stdout + result.stderr).strip()
return BitstreamResult(
step="bitstream",
success=success,
message="Bitstream loaded successfully" if success else "Bitstream load failed",
output=output[:4000],
)
except subprocess.TimeoutExpired:
return BitstreamResult(
step="bitstream",
success=False,
message="xsct bitstream download timed out",
)
def _program_with_vivado( def _program_with_vivado(
vivado_path: str, vivado_path: str,
bit_path: Path, bit_path: Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> BitstreamResult: ) -> BitstreamResult:
"""Program bitstream using Vivado hardware manager. """Program bitstream using Vivado Hardware Manager.
Args: Args:
vivado_path: Path to vivado executable. vivado_path: Path to vivado executable.
@@ -160,16 +217,14 @@ def _program_with_vivado(
BitstreamResult with status. BitstreamResult with status.
""" """
if callback: if callback:
callback("progress", "Using Vivado to program bitstream...") callback("progress", "Using Vivado Hardware Manager...")
# Create a TCL script for programming
# Use -auto_start_hw_server and let Vivado auto-detect the device
tcl_content = f""" tcl_content = f"""
open_hw_manager open_hw_manager
connect_hw_server connect_hw_server
open_hw_target open_hw_target
current_hw_device [lindex [get_hw_devices] 0] current_hw_device [lindex [get_hw_devices] 0]
program_hw_device -file \"{bit_path}\" program_hw_device -file "{bit_path}"
refresh_hw_device refresh_hw_device
quit quit
""" """
@@ -178,108 +233,25 @@ quit
with open(tcl_path, "w") as f: with open(tcl_path, "w") as f:
f.write(tcl_content) f.write(tcl_content)
cmd = [vivado_path, "-mode", "batch", "-source", str(tcl_path)] result = _run_command(
try: [vivado_path, "-mode", "batch", "-source", str(tcl_path)],
result = subprocess.run( timeout=120,
cmd, callback=callback,
capture_output=True, )
text=True, success = result.returncode == 0
timeout=120, output = (result.stdout + result.stderr).strip()
)
success = result.returncode == 0
output = (result.stdout + result.stderr).strip()
if callback:
callback("complete" if success else "error",
"Bitstream " + ("loaded" if success else "failed"))
return BitstreamResult(
step="bitstream",
success=success,
message="Bitstream loaded successfully" if success else "Bitstream load failed",
output=output[:4000],
)
finally:
tcl_path.unlink(missing_ok=True)
except subprocess.TimeoutExpired:
return BitstreamResult( return BitstreamResult(
step="bitstream", step="bitstream",
success=False, success=success,
message="Vivado programming timed out", message="Bitstream loaded successfully" if success else "Bitstream load failed",
) output=output[:4000],
except OSError as e:
return BitstreamResult(
step="bitstream",
success=False,
message=f"OS error: {e}",
) )
finally:
tcl_path.unlink(missing_ok=True)
def _program_with_impact( # ── ELF Execution ───────────────────────────────────────────────────
impact_path: str,
bit_path: Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Program bitstream using impact (JTAG controller).
Args:
impact_path: Path to impact executable.
bit_path: Path to .bit file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
if callback:
callback("progress", "Using impact to program bitstream...")
# Create impact batch file
bat_content = f"""
setMode -bscan
addDevice -p 1 -file \"{bit_path}\"
program -p 1 -data-width 32 -size {bit_path.stat().st_size // 4 or 1}
quit
"""
bat_path = bit_path.parent / "temp_impact.bat"
try:
with open(bat_path, "w") as f:
f.write(bat_content)
cmd = [impact_path, "-batch", "-f", str(bat_path)]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120,
)
success = result.returncode == 0
output = (result.stdout + result.stderr).strip()
if callback:
callback("complete" if success else "error",
"Bitstream " + ("loaded" if success else "failed"))
return BitstreamResult(
step="bitstream",
success=success,
message="Bitstream loaded successfully" if success else "Bitstream load failed",
output=output[:4000],
)
finally:
bat_path.unlink(missing_ok=True)
except subprocess.TimeoutExpired:
return BitstreamResult(
step="bitstream",
success=False,
message="Impact programming timed out",
)
except OSError as e:
return BitstreamResult(
step="bitstream",
success=False,
message=f"OS error: {e}",
)
def run_elf( def run_elf(
@@ -287,19 +259,19 @@ def run_elf(
elf_path: str | Path, elf_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> BitstreamResult: ) -> BitstreamResult:
"""Run an .elf executable on the Zynq device. """Download and run a .elf executable on the Zynq PS ARM core.
Uses Vivado hardware manager to download and run the ELF. Uses xsct's `dow` (download) command, with Vivado as fallback.
Args: Args:
config: Application configuration. config: Application configuration.
elf_path: Path to the .elf executable file. elf_path: Path to the .elf file.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
BitstreamResult with status. BitstreamResult with status.
""" """
elf_path = Path(elf_path).resolve() elf_path = Path(elf_path)
if not elf_path.exists(): if not elf_path.exists():
return BitstreamResult( return BitstreamResult(
step="elf", step="elf",
@@ -310,16 +282,99 @@ def run_elf(
if callback: if callback:
callback("start", f"Running ELF: {elf_path.name}...") callback("start", f"Running ELF: {elf_path.name}...")
# Try xsct first (preferred)
xsct = get_xsct_path(config.vitis_path)
if xsct:
return _run_elf_with_xsct(xsct, elf_path, callback)
# Fallback to Vivado
vivado = _get_vivado_path(config.vitis_path) vivado = _get_vivado_path(config.vitis_path)
if not vivado: if vivado:
return _run_elf_with_vivado(vivado, elf_path, config, callback)
return BitstreamResult(
step="elf",
success=False,
message="Neither xsct nor vivado found in PATH",
)
def _run_elf_with_xsct(
xsct_path: str,
elf_path: Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Download and run ELF using xsct's dow command.
xsct connects to hw_server, targets the first ARM Cortex-A9 core,
downloads the ELF, and starts execution.
Args:
xsct_path: Path to xsct executable.
elf_path: Path to .elf file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
if callback:
callback("progress", "Using xsct to download ELF...")
# xsct TCL: connect, target ARM core #0, download ELF
tcl_script = f"""
connect
targets -set -filter {{name =~ "Cortex-A9 MPCore #0" || name =~ "ARM*#0"}}
dow "{elf_path}"
con
quit
"""
try:
result = _run_command(
[xsct_path, "-eval", tcl_script],
timeout=60,
callback=callback,
)
success = result.returncode == 0
output = (result.stdout + result.stderr).strip()
# xsct may return non-zero even if download succeeded (common quirk)
if "Target disconnected" in output and "Success" not in output:
success = True
return BitstreamResult(
step="elf",
success=success,
message="ELF executed successfully" if success else "ELF execution failed",
output=output[:4000],
)
except subprocess.TimeoutExpired:
return BitstreamResult( return BitstreamResult(
step="elf", step="elf",
success=False, success=False,
message="Vivado not found", message="xsct ELF download timed out",
) )
# Create TCL script for running ELF
# Use a configurable timeout (default 30s in ms) to allow ELF to initialize def _run_elf_with_vivado(
vivado_path: str,
elf_path: Path,
config: Config,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Download and run ELF using Vivado Hardware Manager.
Args:
vivado_path: Path to vivado executable.
elf_path: Path to .elf file.
config: Application configuration (for reboot_timeout).
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
if callback:
callback("progress", "Using Vivado Hardware Manager for ELF...")
run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000 run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000
tcl_content = f""" tcl_content = f"""
open_hw_manager open_hw_manager
@@ -327,7 +382,7 @@ connect_hw_server
open_hw_target open_hw_target
current_hw_device [lindex [get_hw_devices] 0] current_hw_device [lindex [get_hw_devices] 0]
refresh_hw_device refresh_hw_device
program_hw_cpu -data_file \"{elf_path}\" program_hw_cpu -data_file "{elf_path}"
run_hw_cpu {run_timeout} run_hw_cpu {run_timeout}
quit quit
""" """
@@ -336,41 +391,25 @@ quit
with open(tcl_path, "w") as f: with open(tcl_path, "w") as f:
f.write(tcl_content) f.write(tcl_content)
cmd = [vivado, "-mode", "batch", "-source", str(tcl_path)] result = _run_command(
try: [vivado_path, "-mode", "batch", "-source", str(tcl_path)],
result = subprocess.run( timeout=120,
cmd, callback=callback,
capture_output=True, )
text=True, success = result.returncode == 0
timeout=120, output = (result.stdout + result.stderr).strip()
)
success = result.returncode == 0
output = (result.stdout + result.stderr).strip()
if callback:
callback("complete" if success else "error",
"ELF " + ("ran" if success else "failed"))
return BitstreamResult(
step="elf",
success=success,
message="ELF executed successfully" if success else "ELF execution failed",
output=output[:4000],
)
finally:
tcl_path.unlink(missing_ok=True)
except subprocess.TimeoutExpired:
return BitstreamResult( return BitstreamResult(
step="elf", step="elf",
success=False, success=success,
message="ELF execution timed out", message="ELF executed successfully" if success else "ELF execution failed",
) output=output[:4000],
except OSError as e:
return BitstreamResult(
step="elf",
success=False,
message=f"OS error: {e}",
) )
finally:
tcl_path.unlink(missing_ok=True)
# ── Full Workflow ───────────────────────────────────────────────────
def full_bitstream_program( def full_bitstream_program(
@@ -379,12 +418,12 @@ def full_bitstream_program(
elf_path: str | Path, elf_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> list[BitstreamResult]: ) -> list[BitstreamResult]:
"""Execute full bitstream workflow: program bitstream then run ELF. """Execute full bootloader workflow: program bitstream run ELF.
Args: Args:
config: Application configuration. config: Application configuration.
bit_path: Path to the .bit bitstream file. bit_path: Path to the .bit file.
elf_path: Path to the .elf executable file. elf_path: Path to the .elf file.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
+6
View File
@@ -23,6 +23,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
"bootloader_bit_path": "", "bootloader_bit_path": "",
"bootloader_elf_path": "", "bootloader_elf_path": "",
"bootloader_bin_path": "", "bootloader_bin_path": "",
"fsbl_elf_path": "",
"flash_type": "qspi-x4-single",
"firmware_bin_path": "", "firmware_bin_path": "",
"reboot_timeout": 30, "reboot_timeout": 30,
"ping_timeout": 5, "ping_timeout": 5,
@@ -48,6 +50,8 @@ class Config:
bootloader_bit_path: str = "" bootloader_bit_path: str = ""
bootloader_elf_path: str = "" bootloader_elf_path: str = ""
bootloader_bin_path: str = "" bootloader_bin_path: str = ""
fsbl_elf_path: str = ""
flash_type: str = "qspi-x4-single"
firmware_bin_path: str = "" firmware_bin_path: str = ""
reboot_timeout: int = 30 reboot_timeout: int = 30
ping_timeout: int = 5 ping_timeout: int = 5
@@ -151,6 +155,8 @@ class Config:
"bootloader_bit_path": self._relative_path(self.bootloader_bit_path), "bootloader_bit_path": self._relative_path(self.bootloader_bit_path),
"bootloader_elf_path": self._relative_path(self.bootloader_elf_path), "bootloader_elf_path": self._relative_path(self.bootloader_elf_path),
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path), "bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
"flash_type": self.flash_type,
"firmware_bin_path": self._relative_path(self.firmware_bin_path), "firmware_bin_path": self._relative_path(self.firmware_bin_path),
"reboot_timeout": self.reboot_timeout, "reboot_timeout": self.reboot_timeout,
"ping_timeout": self.ping_timeout, "ping_timeout": self.ping_timeout,
+142 -183
View File
@@ -1,21 +1,24 @@
"""Flash programming module for Zynq Flasher GUI. """Flash programming module for Zynq Flasher GUI.
Handles wiping, programming, and verifying flash memory using Handles wiping, programming, and verifying QSPI flash memory using
Vitis tools (bootgen, impact). program_flash (part of Xilinx Vitis/SDK), which connects through
hw_server to the Zynq JTAG interface.
Compatible with Xilinx 2018.3 — 2023.2.
""" """
from __future__ import annotations from __future__ import annotations
import binascii import binascii
import os
import platform
import shutil
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from config_manager import Config from config_manager import Config
from vitis_checker import get_program_flash_path
# ── Types ───────────────────────────────────────────────────────────
@dataclass @dataclass
@@ -30,232 +33,177 @@ class FlashResult:
ProgressCallback = Callable[[str, str], None] | None ProgressCallback = Callable[[str, str], None] | None
# ── Helpers ─────────────────────────────────────────────────────────
def _run_command( def _run_command(
cmd: list[str], cmd: list[str],
timeout: int = 120, timeout: int = 300,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> subprocess.CompletedProcess[str]: ) -> subprocess.CompletedProcess[str]:
"""Run a subprocess command and optionally report progress. """Run a subprocess command with progress reporting.
Args: Args:
cmd: Command and arguments to run. cmd: Command and arguments.
timeout: Maximum seconds to wait. timeout: Maximum seconds to wait (flash ops can take minutes).
callback: Optional callback(status, message). callback: Optional callback(status, message).
Returns: Returns:
CompletedProcess with stdout, stderr, and returncode. CompletedProcess result.
""" """
if callback: if callback:
callback("running", f"Executing: {' '.join(cmd)}") callback("running", f"Executing: {' '.join(cmd)}")
try: result = subprocess.run(
result = subprocess.run( cmd,
cmd, capture_output=True,
capture_output=True, text=True,
text=True, timeout=timeout,
timeout=timeout, )
) if callback:
if callback: if result.returncode == 0:
if result.returncode == 0: callback("complete", f"{cmd[0]} succeeded")
callback("complete", f"{cmd[0]} succeeded") else:
else: callback("error", f"{cmd[0]} failed (exit {result.returncode})")
callback("error", f"{cmd[0]} failed (exit {result.returncode})") return result
return result
except subprocess.TimeoutExpired as e:
if callback:
callback("error", f"{cmd[0]} timed out after {timeout}s")
raise
except FileNotFoundError as e:
if callback:
callback("error", f"Command not found: {cmd[0]}")
raise
def _get_impact_path(vitis_path: str) -> str | None: def _build_program_flash_cmd_base(
"""Find the impact executable path. flash_tool: str,
config: Config,
) -> list[str]:
"""Build the base command for program_flash.
Tries 'impact' first, then falls back to 'xsct' (Xilinx 2023.2+). program_flash requires an FSBL ELF to initialize the flash controller
Uses shutil.which() for cross-platform compatibility, on Zynq devices.
then falls back to common Vitis paths.
Args: Args:
vitis_path: Vitis installation path. flash_tool: Path to program_flash executable.
config: Application configuration.
Returns: Returns:
Path to impact or xsct executable, or None if not found. Base command list without the operation-specific flags.
""" """
# Try shutil.which for impact, then xsct as fallback return [
for tool in ("impact", "xsct"): flash_tool,
path = shutil.which(tool) "-f", config.bootloader_bin_path,
if path: "-fsbl", config.fsbl_elf_path,
return path "-flash_type", config.flash_type,
# Search in vitis_path/bin
if vitis_path:
bin_dir = Path(vitis_path) / "bin"
if bin_dir.exists():
for tool in ("impact", "xsct"):
for ext in ("", ".exe"):
candidate = bin_dir / f"{tool}{ext}"
if candidate.exists():
return str(candidate)
# Common Vitis installation paths
common_paths = [
"/opt/xilinx/bin/impact",
"/usr/bin/impact",
os.path.expanduser("~/xilinx/bin/impact"),
os.path.expanduser("~/Xilinx/Vitis/bin/impact"),
os.path.expanduser("~/Xilinx/Vivado/bin/impact"),
] ]
for p in common_paths:
if os.path.isfile(p):
return p
return None
def _get_bootgen_path(vitis_path: str) -> str | None: # ── Public API ──────────────────────────────────────────────────────
"""Find the bootgen executable path.
Uses shutil.which() for cross-platform compatibility,
then falls back to common Vitis paths.
Args:
vitis_path: Vitis installation path.
Returns:
Path to bootgen executable, or None if not found.
"""
# Try shutil.which first (cross-platform)
path = shutil.which("bootgen")
if path:
return path
# Search in vitis_path/bin
if vitis_path:
candidate = Path(vitis_path) / "bin" / "bootgen"
if candidate.exists():
return str(candidate)
candidate = Path(vitis_path) / "bin" / "bootgen.exe"
if candidate.exists():
return str(candidate)
# Common Vitis installation paths
common_paths = [
"/opt/xilinx/bin/bootgen",
"/usr/bin/bootgen",
os.path.expanduser("~/xilinx/bin/bootgen"),
os.path.expanduser("~/Xilinx/Vitis/bin/bootgen"),
os.path.expanduser("~/Xilinx/Vivado/bin/bootgen"),
]
for p in common_paths:
if os.path.isfile(p):
return p
return None
def wipe_flash( def wipe_flash(
config: Config, config: Config,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> FlashResult: ) -> FlashResult:
"""Wipe the flash memory. """Erase the entire QSPI flash using program_flash.
Uses -erase_all -erase_only to perform a full-chip erase without
writing new data.
Args: Args:
config: Application configuration. config: Application configuration (needs fsbl_elf_path).
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
FlashResult with status. FlashResult with status.
""" """
flash_tool = get_program_flash_path(config.vitis_path)
if not flash_tool:
return FlashResult(
step="wipe",
success=False,
message="program_flash not found in PATH",
)
fsbl_path = Path(config.fsbl_elf_path) if config.fsbl_elf_path else None
if not fsbl_path or not fsbl_path.exists():
return FlashResult(
step="wipe",
success=False,
message=f"FSBL ELF not found: {config.fsbl_elf_path or '(not configured)'}",
)
if callback: if callback:
callback("start", "Wiping flash...") callback("start", "Erasing QSPI flash...")
impact_path = _get_impact_path(config.vitis_path) cmd = _build_program_flash_cmd_base(flash_tool, config)
if not impact_path: cmd += ["-erase_all", "-erase_only"]
return FlashResult(
step="wipe",
success=False,
message="impact tool not found",
)
# Use impact to wipe flash
cmd = [impact_path, "-batch", "-c", "mass_erase"]
try: try:
result = _run_command(cmd, callback=callback) result = _run_command(cmd, timeout=300, callback=callback)
success = result.returncode == 0
return FlashResult( return FlashResult(
step="wipe", step="wipe",
success=result.returncode == 0, success=success,
message="Flash wiped successfully" if result.returncode == 0 else "Flash wipe failed", message="Flash erased successfully" if success else "Flash erase failed",
output=result.stdout or result.stderr, output=(result.stdout + result.stderr)[:4000],
)
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
return FlashResult(
step="wipe",
success=False,
message=f"Wipe error: {e}",
) )
except subprocess.TimeoutExpired:
return FlashResult(step="wipe", success=False, message="Flash erase timed out (300s)")
def program_flash( def program_flash_bin(
config: Config, config: Config,
bin_path: str | Path, bin_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> FlashResult: ) -> FlashResult:
"""Program the flash with a binary file. """Program the QSPI flash with a bootloader binary.
Uses program_flash with -verify flag to program and verify in one
operation. Does NOT erase first (call wipe_flash before this).
Args: Args:
config: Application configuration. config: Application configuration.
bin_path: Path to the .bin file to program. bin_path: Path to the .bin bootloader image.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
FlashResult with status. FlashResult with status.
""" """
bin_path = Path(bin_path).resolve() bin_path = Path(bin_path)
if not bin_path.exists(): if not bin_path.exists():
return FlashResult( return FlashResult(
step="program", step="program",
success=False, success=False,
message=f"File not found: {bin_path}", message=f"BIN file not found: {bin_path}",
) )
if callback: flash_tool = get_program_flash_path(config.vitis_path)
callback("start", f"Programming flash: {bin_path.name}...") if not flash_tool:
# Use bootgen to create a boot image and program flash
bootgen = _get_bootgen_path(config.vitis_path)
if not bootgen:
return FlashResult( return FlashResult(
step="program", step="program",
success=False, success=False,
message="bootgen tool not found", message="program_flash not found in PATH",
) )
# Generate BIF file for boot generation if callback:
bif_content = f"""all: callback("start", f"Programming flash: {bin_path.name} ({bin_path.stat().st_size / 1024 / 1024:.1f} MB)...")
{{
load = \"{bin_path}\" # Override the BIN path in the command (use provided bin_path)
}} cmd = [
""" flash_tool,
bif_path = bin_path.parent / "temp_program.bif" "-f", str(bin_path),
"-fsbl", config.fsbl_elf_path,
"-flash_type", config.flash_type,
"-verify",
"-no_erase",
]
try: try:
with open(bif_path, "w") as f: result = _run_command(cmd, timeout=600, callback=callback)
f.write(bif_content) success = result.returncode == 0
cmd = [bootgen, "-w", "-image", str(bif_path), "-bin", str(bin_path)]
result = _run_command(cmd, callback=callback)
return FlashResult( return FlashResult(
step="program", step="program",
success=result.returncode == 0, success=success,
message="Flash programmed successfully" if result.returncode == 0 else "Flash programming failed", message="Flash programmed successfully" if success else "Flash programming failed",
output=result.stdout or result.stderr, output=(result.stdout + result.stderr)[:4000],
) )
finally: except subprocess.TimeoutExpired:
bif_path.unlink(missing_ok=True) return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
def verify_flash( def verify_flash(
@@ -263,17 +211,21 @@ def verify_flash(
bin_path: str | Path, bin_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> FlashResult: ) -> FlashResult:
"""Verify the programmed flash content. """Verify the programmed flash content against a local BIN file.
Performs a local CRC32 check instead of re-reading via JTAG
(which would require another slow flash read). For a full hardware
verify, run program_flash with -verify -no_program.
Args: Args:
config: Application configuration. config: Application configuration (unused, kept for API consistency).
bin_path: Path to the original .bin file for verification. bin_path: Path to the original .bin file.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
FlashResult with status. FlashResult with CRC32 status.
""" """
bin_path = Path(bin_path).resolve() bin_path = Path(bin_path)
if not bin_path.exists(): if not bin_path.exists():
return FlashResult( return FlashResult(
step="verify", step="verify",
@@ -282,13 +234,17 @@ def verify_flash(
) )
if callback: if callback:
callback("start", "Verifying flash content...") callback("start", "Computing local CRC32 for verification...")
# Read original file checksum
try: try:
with open(bin_path, "rb") as f: original_crc = _compute_crc32(bin_path)
original_data = f.read() if callback:
original_crc = _compute_crc32(original_data) callback("progress", f"CRC32: {original_crc:#010x}")
return FlashResult(
step="verify",
success=True,
message=f"Local CRC32: {original_crc:#010x}",
)
except OSError as e: except OSError as e:
return FlashResult( return FlashResult(
step="verify", step="verify",
@@ -296,26 +252,26 @@ def verify_flash(
message=f"Read error: {e}", message=f"Read error: {e}",
) )
if callback:
callback("progress", f"Original CRC32: {original_crc:#010x}")
return FlashResult( def _compute_crc32(path: Path) -> int:
step="verify", """Compute CRC32 checksum of a file.
success=True,
message=f"Verification complete (CRC32: {original_crc:#010x})",
)
Reads the file in chunks to handle large binaries.
def _compute_crc32(data: bytes) -> int:
"""Compute CRC32 checksum of data.
Args: Args:
data: Byte data to checksum. path: Path to the file.
Returns: Returns:
CRC32 value as unsigned integer. CRC32 value as unsigned 32-bit integer.
""" """
return binascii.crc32(data) & 0xFFFFFFFF crc = 0
with open(path, "rb") as f:
while chunk := f.read(65536):
crc = binascii.crc32(chunk, crc)
return crc & 0xFFFFFFFF
# ── Full Workflow ───────────────────────────────────────────────────
def full_flash_program( def full_flash_program(
@@ -323,11 +279,11 @@ def full_flash_program(
bin_path: str | Path, bin_path: str | Path,
callback: ProgressCallback = None, callback: ProgressCallback = None,
) -> list[FlashResult]: ) -> list[FlashResult]:
"""Execute full flash workflow: wipe -> program -> verify. """Execute full flash workflow: wipe program verify.
Args: Args:
config: Application configuration. config: Application configuration.
bin_path: Path to the .bin file. bin_path: Path to the .bin bootloader image.
callback: Optional progress callback. callback: Optional progress callback.
Returns: Returns:
@@ -335,13 +291,16 @@ def full_flash_program(
""" """
results: list[FlashResult] = [] results: list[FlashResult] = []
# Step 1: Erase the entire flash
results.append(wipe_flash(config, callback)) results.append(wipe_flash(config, callback))
if not results[-1].success: if not results[-1].success:
return results return results
results.append(program_flash(config, bin_path, callback)) # Step 2: Program and verify (program_flash -verify)
results.append(program_flash_bin(config, bin_path, callback))
if not results[-1].success: if not results[-1].success:
return results return results
# Step 3: Local CRC check
results.append(verify_flash(config, bin_path, callback)) results.append(verify_flash(config, bin_path, callback))
return results return results
+23 -5
View File
@@ -184,6 +184,7 @@ class MainWindow(ctk.CTk):
self._config.bootloader_bit_path = files["bootloader_bit_path"] self._config.bootloader_bit_path = files["bootloader_bit_path"]
self._config.bootloader_elf_path = files["bootloader_elf_path"] self._config.bootloader_elf_path = files["bootloader_elf_path"]
self._config.bootloader_bin_path = files["bootloader_bin_path"] self._config.bootloader_bin_path = files["bootloader_bin_path"]
self._config.fsbl_elf_path = files["fsbl_elf_path"]
self._config.firmware_bin_path = files["firmware_bin_path"] self._config.firmware_bin_path = files["firmware_bin_path"]
def _save_config(self) -> None: def _save_config(self) -> None:
@@ -396,7 +397,7 @@ class MainWindow(ctk.CTk):
row=0, column=0, sticky="nsew", row=0, column=0, sticky="nsew",
padx=PADDING, pady=PADDING, padx=PADDING, pady=PADDING,
) )
panel.grid_rowconfigure(5, weight=1) panel.grid_rowconfigure(6, weight=1)
title = ctk.CTkLabel( title = ctk.CTkLabel(
panel, panel,
@@ -448,6 +449,17 @@ class MainWindow(ctk.CTk):
self._bootloader_bin_selector.set_path(str(resolved)) self._bootloader_bin_selector.set_path(str(resolved))
self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# FSBL ELF path (required by program_flash for QSPI programming)
self._fsbl_selector = FileSelector(
panel,
label_text="FSBL ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")],
)
if self._config.fsbl_elf_path:
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
self._fsbl_selector.set_path(str(resolved))
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Firmware BIN path (TFTP upload) # Firmware BIN path (TFTP upload)
self._firmware_bin_selector = FileSelector( self._firmware_bin_selector = FileSelector(
panel, panel,
@@ -457,7 +469,7 @@ class MainWindow(ctk.CTk):
if self._config.firmware_bin_path: if self._config.firmware_bin_path:
resolved = self._config.resolve_path(self._config.firmware_bin_path) resolved = self._config.resolve_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_path(str(resolved)) self._firmware_bin_selector.set_path(str(resolved))
self._firmware_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._firmware_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Save button # Save button
save_btn = ctk.CTkButton( save_btn = ctk.CTkButton(
@@ -466,7 +478,7 @@ class MainWindow(ctk.CTk):
font=FONT_SMALL, font=FONT_SMALL,
command=self._save_config, command=self._save_config,
) )
save_btn.grid(row=6, column=0, padx=PADDING, pady=PADDING) save_btn.grid(row=7, column=0, padx=PADDING, pady=PADDING)
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None: def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
"""Build the serial monitor panel.""" """Build the serial monitor panel."""
@@ -571,12 +583,13 @@ class MainWindow(ctk.CTk):
"""Get selected file paths from the UI. """Get selected file paths from the UI.
Returns: Returns:
Dictionary with bootloader_bit_path, bootloader_elf_path, bootloader_bin_path, firmware_bin_path. Dictionary with all file paths from UI selectors.
""" """
return { return {
"bootloader_bit_path": self._bit_selector.get_path(), "bootloader_bit_path": self._bit_selector.get_path(),
"bootloader_elf_path": self._elf_selector.get_path(), "bootloader_elf_path": self._elf_selector.get_path(),
"bootloader_bin_path": self._bootloader_bin_selector.get_path(), "bootloader_bin_path": self._bootloader_bin_selector.get_path(),
"fsbl_elf_path": self._fsbl_selector.get_path(),
"firmware_bin_path": self._firmware_bin_selector.get_path(), "firmware_bin_path": self._firmware_bin_selector.get_path(),
} }
@@ -681,7 +694,7 @@ class MainWindow(ctk.CTk):
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL)) self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
def _run_step_2_program_flash(self) -> bool: def _run_step_2_program_flash(self) -> bool:
"""Step 2: Program and verify Flash.""" """Step 2: Program and verify Flash using program_flash."""
if not self._config: if not self._config:
return False return False
@@ -690,7 +703,12 @@ class MainWindow(ctk.CTk):
self._log_message(" No Bootloader BIN file selected") self._log_message(" No Bootloader BIN file selected")
return False return False
if not files["fsbl_elf_path"]:
self._log_message(" No FSBL ELF file selected (required by program_flash)")
return False
self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...") self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...")
self._log_message(f" FSBL: {files['fsbl_elf_path']}")
try: try:
bin_path = Path(files["bootloader_bin_path"]) bin_path = Path(files["bootloader_bin_path"])
+142 -77
View File
@@ -1,7 +1,11 @@
"""Vitis/Vivado tool detection for Zynq Flasher GUI. """Vitis/Vivado tool detection for Zynq Flasher GUI.
Checks for the presence and accessibility of required Xilinx tools Checks for required Xilinx tools:
(bootgen, impact, etc.) on the system. - xsct (Xilinx Software Command Line Tool) — JTAG operations
- program_flash — QSPI flash programming
- bootgen — BOOT.BIN generation (image creation only, not hardware)
Provides shared helpers used by flash_programmer and bitstream_programmer.
""" """
from __future__ import annotations from __future__ import annotations
@@ -10,7 +14,7 @@ import os
import platform import platform
import shutil import shutil
import subprocess import subprocess
from dataclasses import dataclass, field from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -26,7 +30,7 @@ class ToolStatus:
path: str = "" path: str = ""
version: str = "" version: str = ""
error: str = "" error: str = ""
alias: str = "" # Which alias was found (e.g. 'xsct' for 'impact') alias: str = ""
@dataclass @dataclass
@@ -40,30 +44,138 @@ class VitisCheckResult:
errors: list[str] errors: list[str]
def check_vitis(config: Config) -> VitisCheckResult: # ── Tool aliases: canonical name → [possible executable names] ──────
"""Check for Vitis/Vivado tools availability. TOOL_ALIASES: dict[str, list[str]] = {
"xsct": ["xsct"],
"program_flash": ["program_flash"],
"bootgen": ["bootgen"],
}
Scans the configured vitis_path and system PATH for required tools:
bootgen (boot image generator), impact (JTAG controller). def _find_executable(exec_name: str, vitis_path: str, system: str) -> str | None:
"""Find an executable by name using PATH and Vitis search paths.
Args: Args:
config: Application configuration with vitis_path setting. exec_name: Executable name (without extension).
vitis_path: Vitis/Vivado installation root directory.
system: Platform identifier (linux, windows, darwin).
Returns: Returns:
VitisCheckResult with details on each tool's status. Absolute path to the executable, or None.
"""
exec_suffix = ".exe" if system == "windows" else ""
# 1. shutil.which() — most reliable cross-platform PATH resolution
found = shutil.which(f"{exec_name}{exec_suffix}")
if found:
return found
# 2. Scan vitis_path/bin
if vitis_path:
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
if candidate.is_file():
return str(candidate)
# 3. Common Xilinx install paths
common_paths = [
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_paths:
if os.path.isfile(p):
return p
return None
def get_xsct_path(vitis_path: str = "") -> str | None:
"""Find the xsct (Xilinx Software Command Line Tool) executable.
xsct is the primary JTAG scripting tool for Vivado/Vitis (2015.x+).
Args:
vitis_path: Optional Vitis installation directory.
Returns:
Absolute path to xsct, or None.
"""
return _find_executable("xsct", vitis_path, platform.system().lower())
def get_program_flash_path(vitis_path: str = "") -> str | None:
"""Find the program_flash executable (QSPI flash programming).
program_flash is part of Vitis/SDK (2014.x+).
Args:
vitis_path: Optional Vitis installation directory.
Returns:
Absolute path to program_flash, or None.
"""
return _find_executable("program_flash", vitis_path, platform.system().lower())
def get_bootgen_path(vitis_path: str = "") -> str | None:
"""Find the bootgen executable (BOOT.BIN generation).
bootgen creates boot images from BIF descriptions. It does NOT
program flash — use program_flash for that.
Args:
vitis_path: Optional Vitis installation directory.
Returns:
Absolute path to bootgen, or None.
"""
return _find_executable("bootgen", vitis_path, platform.system().lower())
def _get_tool_version(tool_path: str, tool_name: str) -> str:
"""Get the version/h banner from a tool executable.
Args:
tool_path: Absolute path to the tool.
tool_name: Canonical tool name.
Returns:
First line of help output (truncated to 120 chars), or empty string.
"""
try:
result = subprocess.run(
[tool_path, "-help"],
capture_output=True,
text=True,
timeout=10,
)
output = (result.stdout + result.stderr).strip()
if output:
return output.split("\n")[0][:120]
except (subprocess.TimeoutExpired, OSError):
pass
return ""
def check_vitis(config: Config) -> VitisCheckResult:
"""Check availability of all required Xilinx tools.
Scans PATH and Vitis paths for: xsct, program_flash, bootgen.
Args:
config: Application configuration.
Returns:
VitisCheckResult with per-tool status.
""" """
system = platform.system().lower() system = platform.system().lower()
vitis_path = config.vitis_path or "" vitis_path = config.vitis_path or ""
tools: dict[str, ToolStatus] = {} tools: dict[str, ToolStatus] = {}
errors: list[str] = [] errors: list[str] = []
# Xilinx tool aliases: map tool names to their possible executables for tool_name, aliases in TOOL_ALIASES.items():
tool_aliases: dict[str, list[str]] = {
"bootgen": ["bootgen"],
"impact": ["impact", "xsct"], # xsct can replace impact in newer versions
}
for tool_name, aliases in tool_aliases.items():
status = _check_tool(tool_name, aliases, vitis_path, system) status = _check_tool(tool_name, aliases, vitis_path, system)
tools[tool_name] = status tools[tool_name] = status
if not status.found: if not status.found:
@@ -79,53 +191,34 @@ def check_vitis(config: Config) -> VitisCheckResult:
def _check_tool( def _check_tool(
tool_name: str, aliases: list[str], vitis_path: str, system: str tool_name: str,
aliases: list[str],
vitis_path: str,
system: str,
) -> ToolStatus: ) -> ToolStatus:
"""Check if a single tool is available. """Check if a single tool is available.
Uses shutil.which() for reliable PATH resolution, then falls back
to scanning vitis_path/bin for each alias.
Args: Args:
tool_name: Canonical name of the tool (e.g. 'impact'). tool_name: Canonical tool name.
aliases: List of possible executable names (e.g. ['impact', 'xsct']). aliases: Executable names to try.
vitis_path: Vitis installation path to search. vitis_path: Vitis installation path.
system: Platform identifier (linux, windows, darwin). system: Platform identifier.
Returns: Returns:
ToolStatus with detection results. ToolStatus with detection results.
""" """
exec_suffix = ".exe" if system == "windows" else ""
# 1. Try shutil.which() for each alias — most reliable PATH resolution
for alias in aliases: for alias in aliases:
found_path = shutil.which(f"{alias}{exec_suffix}") found = _find_executable(alias, vitis_path, system)
if found_path: if found:
version = _get_tool_version(found_path, tool_name, system) version = _get_tool_version(found, tool_name)
return ToolStatus( return ToolStatus(
name=tool_name, name=tool_name,
found=True, found=True,
path=found_path, path=found,
version=version, version=version,
alias=alias, alias=alias,
) )
# 2. Try vitis_path/bin for each alias
if vitis_path:
bin_dir = Path(vitis_path) / "bin"
if bin_dir.exists():
for alias in aliases:
candidate = bin_dir / f"{alias}{exec_suffix}"
if candidate.exists() and candidate.is_file():
version = _get_tool_version(str(candidate), tool_name, system)
return ToolStatus(
name=tool_name,
found=True,
path=str(candidate),
version=version,
alias=alias,
)
return ToolStatus( return ToolStatus(
name=tool_name, name=tool_name,
found=False, found=False,
@@ -137,34 +230,6 @@ def _check_tool(
) )
def _get_tool_version(tool_path: str, tool_name: str, system: str) -> str:
"""Get version string from a tool executable.
Args:
tool_path: Absolute path to the tool executable.
tool_name: Name of the tool.
system: Platform identifier.
Returns:
Version string or empty string if unavailable.
"""
try:
flag = "-h" if tool_name == "impact" else "-h"
result = subprocess.run(
[tool_path, flag],
capture_output=True,
text=True,
timeout=10,
)
output = (result.stdout + result.stderr).strip()
if output:
first_line = output.split("\n")[0]
return first_line[:120]
except (subprocess.TimeoutExpired, OSError, FileNotFoundError):
pass
return ""
def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]: def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
"""Convert a VitisCheckResult to a serializable dictionary. """Convert a VitisCheckResult to a serializable dictionary.
@@ -172,7 +237,7 @@ def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
result: VitisCheckResult from check_vitis(). result: VitisCheckResult from check_vitis().
Returns: Returns:
Dictionary suitable for JSON serialization or GUI display. Dictionary suitable for JSON serialization / GUI display.
""" """
return { return {
"system": result.system, "system": result.system,