Coverage for pyEDAA/Launcher/__init__.py: 40%
147 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-13 18:05 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-13 18:05 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ | | __ _ _ _ _ __ ___| |__ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | / _` | | | | '_ \ / __| '_ \ / _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| |__| (_| | |_| | | | | (__| | | | __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_____\__,_|\__,_|_| |_|\___|_| |_|\___|_| #
7# |_| |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Stefan Unrein #
11# Patrick Lehmann #
12# #
13# License: #
14# ==================================================================================================================== #
15# Copyright 2021-2026 Stefan Unrein - Endingen, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""Start the correct Vivado Version based on version in `*.xpr`file."""
33__author__ = "Stefan Unrein, Patrick Lehmann"
34__email__ = "Paebbels@gmail.com"
35__copyright__ = "2021-2026, Stefan Unrein"
36__license__ = "Apache License, Version 2.0"
37__version__ = "0.2.3"
38__keywords__ = ["launcher", "version selector", "amd", "xilinx", "vivado"]
39__project_url__ = "https://github.com/edaa-org/pyEDAA.Launcher"
40__documentation_url__ = "https://edaa-org.github.io/pyEDAA.Launcher"
41__issue_tracker_url__ = "https://GitHub.com/edaa-org/pyEDAA.Launcher/issues"
43from colorama import init as colorama_init, Fore as Foreground
44from pathlib import Path
45from re import compile as re_compile
46from subprocess import Popen
47from sys import exit, argv, stdout
48from textwrap import dedent
49from time import sleep
50from typing import NoReturn, Generator, Tuple
52from pyTooling.Decorators import export
53from pyTooling.Versioning import YearReleaseVersion
56@export
57class Program:
58 """Program instance of pyEDAA.Launcher."""
60 _vivadoBatchfile = Path("bin/vivado.bat")
61 _vvglWrapperFile = Path("bin/unwrapped/win64.o/vvgl.exe")
63 _vivadoVersionPattern = re_compile(r"\d+\.\d+(\.\d+)?")
64 _versionLinePattern = re_compile(r"^<!--\s*Product\sVersion:\s+Vivado\s+v(?P<major>\d+).(?P<minor>\d+)(?:.(?P<patch>\d+))?\s+\(64-bit\)\s+-->")
66 _projectFilePath: Path
68 def __init__(self, projectFilePath: Path) -> None:
69 """Initializer.
71 :param projectFilePath: Path to the ``*.xpr`` file.
72 :raises Exception: When the given ``*.xpr`` file doesn't exist.
73 """
74 if not projectFilePath.exists(): 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 raise Exception(f"Vivado project file '{projectFilePath}' not found.") \
76 from FileNotFoundError(f"File '{projectFilePath}' not found.")
78 self._projectFilePath = projectFilePath
80 def GetVersion(self) -> YearReleaseVersion:
81 """Opens an ``*.xpr`` file and returns the Vivado version used to save this file.
83 :returns: Used Vivado version to save the given ``*.xpr`` file.
84 :raises Exception: When the version information isn't found in the file.
85 """
86 with self._projectFilePath.open("r", encoding="utf-8") as file:
87 for line in file: 87 ↛ 92line 87 didn't jump to line 92 because the loop on line 87 didn't complete
88 match = self._versionLinePattern.match(line)
89 if match is not None:
90 return YearReleaseVersion(year=int(match['major']), release=int(match['minor']))
91 else:
92 raise Exception(f"Pattern not found in '{self._projectFilePath}'.")
94 @classmethod
95 def GetVivadoVersions(cls, xilinxInstallPath: Path) -> Generator[Tuple[YearReleaseVersion, Path], None, None]:
96 """Scan a given directory for installed Vivado versions.
98 :param xilinxInstallPath: Xilinx installation directory.
99 :returns: A generator for a sequence of installed Vivado versions.
100 """
101 for directory in xilinxInstallPath.iterdir():
102 if directory.is_dir(): 102 ↛ 101line 102 didn't jump to line 101 because the condition on line 102 was always true
103 if directory.name == "Vivado":
104 for version in directory.iterdir():
105 if cls._vivadoVersionPattern.match(version.name): 105 ↛ 104line 105 didn't jump to line 104 because the condition on line 105 was always true
106 yield YearReleaseVersion.Parse(version.name), version
107 elif cls._vivadoVersionPattern.match(directory.name): 107 ↛ 101line 107 didn't jump to line 101 because the condition on line 107 was always true
108 yield YearReleaseVersion.Parse(directory.name), directory / "Vivado"
110 def StartVivado(self, vivadoInstallationPath: Path) -> None:
111 """Start the given Vivado version with an ``*.xpr`` file as parameter.
113 :param vivadoInstallationPath: Path to the Xilinx toolchain installations.
114 :param version: The Vivado version to start.
115 """
116 vvglWrapperPath = vivadoInstallationPath / self._vvglWrapperFile
117 vivadoBatchfilePath = vivadoInstallationPath / self._vivadoBatchfile
119 cmd = [str(vvglWrapperPath), str(vivadoBatchfilePath), str(self._projectFilePath)]
120 Popen(cmd, cwd=self._projectFilePath.parent)
123@export
124def printHeadline() -> None:
125 """
126 Print the programs headline.
128 .. code-block::
130 ================================================================================
131 pyEDAA.Launcher
132 ================================================================================
134 """
135 print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")
136 print(f"{Foreground.MAGENTA}{'pyEDAA.Launcher':^80}{Foreground.RESET}")
137 print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")
140@export
141def printVersion() -> None:
142 """
143 Print author(s), copyright notice, license and version.
145 .. code-block::
147 Author: Jane Doe
148 Copyright: ....
149 License: MIT
150 Version: v2.1.4
152 """
153 print(f"Author: {__author__} ({__email__})")
154 print(f"Copyright: {__copyright__}")
155 print(f"License: {__license__}")
156 print(f"Version: {__version__}")
159@export
160def printCLIOptions() -> None:
161 """
162 Print accepted CLI arguments and CLI options.
163 """
164 print(f"{Foreground.LIGHTBLUE_EX}Accepted argument:{Foreground.RESET}")
165 print(" <path to xpr file> AMD/Xilinx Vivado project file")
166 print()
167 print(f"{Foreground.LIGHTBLUE_EX}Accepted options:{Foreground.RESET}")
168 print(" --help Show a help page.")
169 print(" --version Show tool version.")
170 print(" --list List available Vivado versions.")
173@export
174def printSetup(scriptPath: Path) -> None:
175 """
176 Print how to setup pyEDAA.Launcher.
178 :param scriptPath: Path to this script.
179 """
180 print(dedent(f"""\
181 For using this {scriptPath.stem}, please associate the '*.xpr' file extension to
182 this executable.
184 {Foreground.LIGHTBLUE_EX}Setup steps:{Foreground.RESET}
185 * Copy this executable into the Xilinx installation directory.
186 Example: C:\\Xilinx\\
187 * Set '*.xpr' file association:
188 1. right-click on any existing '*.xrp' file in Windows Explorer
189 2. open with
190 3. {scriptPath}""")
191 )
194@export
195def printAvailableVivadoVersions(xilinxInstallationPath: Path) -> None:
196 """
197 Print a list of discovered Xilinx Vivado installations.
199 :param xilinxInstallationPath: Directory were Xilinx software is installed.
200 """
201 print(dedent(f"""\
202 {Foreground.LIGHTBLACK_EX}Detecting Vivado installations in Xilinx installation directory '{xilinxInstallationPath}' ...{Foreground.RESET}
204 {Foreground.LIGHTBLUE_EX}Detected Vivado versions:{Foreground.RESET}""")
205 )
206 for version, installDirectory in Program.GetVivadoVersions(xilinxInstallationPath):
207 print(f"* {Foreground.GREEN}{version}{Foreground.RESET} -> {installDirectory}")
210@export
211def waitForReturnKeyAndExit(exitCode: int = 0) -> NoReturn:
212 """
213 Ask the user to press Return. Afterwards exit the program with ``exitcode``.
215 :param exitCode: Exit code when returning to caller.
216 """
217 print()
218 print(f"{Foreground.CYAN}Press Return to exit.{Foreground.RESET}")
219 input() # wait on user interaction
220 exit(exitCode)
223@export
224def main() -> NoReturn:
225 """Entry point function.
227 It creates an instance of :class:`Program` and hands over the execution to the OOP world.
228 """
229 colorama_init()
231 scriptPath = Path(argv[0])
232 xilinxInstallationDirectory = scriptPath.parent
234 printHeadline()
235 if (argc := len(argv)) == 1:
236 printVersion()
237 print()
238 print(f"{Foreground.RED}[ERROR] No argument or option provided.{Foreground.RESET}")
239 print()
240 printSetup(scriptPath)
241 print()
242 printAvailableVivadoVersions(xilinxInstallationDirectory)
243 print()
244 printCLIOptions()
245 waitForReturnKeyAndExit(2)
246 elif argc == 2:
247 if (option := argv[1]) == "--help":
248 print(dedent(f"""\
249 {scriptPath.stem} launches the matching Vivado installation based on the Vivado
250 version used to save an '*.xpr' file""")
251 )
252 print()
253 printCLIOptions()
254 exit(0)
255 elif option == "--version":
256 printVersion()
257 exit(0)
258 elif option == "--list":
259 printAvailableVivadoVersions(xilinxInstallationDirectory)
260 exit(0)
261 else:
262 try:
263 program = Program(Path(option))
264 versionFromXPRFile = program.GetVersion()
266 for version, vivadoInstallationDirectory in program.GetVivadoVersions(xilinxInstallationDirectory):
267 if version == versionFromXPRFile:
268 print(f"Using Vivado {Foreground.GREEN}{version}{Foreground.RESET} to open '{program._projectFilePath.parent.as_posix()}/{Foreground.CYAN}{program._projectFilePath.name}{Foreground.RESET}'.")
269 print()
270 program.StartVivado(vivadoInstallationDirectory)
272 i = 3
273 print(f"Closing in {i}", end="")
274 stdout.flush()
275 for i in range(i - 1, 0, -1):
276 sleep(1)
277 print(f"\x1b[1D{i}", end="")
278 stdout.flush()
279 sleep(1)
280 print()
281 exit(0)
282 else:
283 print(dedent(f"""\
284 {Foreground.RED}[ERROR] Vivado version {versionFromXPRFile} not available.{Foreground.RESET}
286 Please start manually!""")
287 )
288 printAvailableVivadoVersions(xilinxInstallationDirectory)
289 waitForReturnKeyAndExit(2)
291 except Exception as ex:
292 print(f"{Foreground.RED}[ERROR] {ex}{Foreground.RESET}")
293 if ex.__cause__ is not None:
294 print(f"{Foreground.YELLOW}Caused by: {ex.__cause__}{Foreground.RESET}")
295 waitForReturnKeyAndExit(1)
296 else:
297 printHeadline()
298 print(f"{Foreground.RED}[ERROR] Too many arguments.{Foreground.RESET}")
299 print()
300 printCLIOptions()
301 waitForReturnKeyAndExit(2)
304# Entry point
305if __name__ == "__main__": 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 main()