Coverage for pyEDAA/Launcher/__init__.py: 39%
145 statements
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-07 22:29 +0000
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-07 22:29 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ _ _ #
3# _ __ _ _| ____| _ \ / \ / \ | | __ _ _ _ _ __ ___| |__ ___ _ __ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | / _` | | | | '_ \ / __| '_ \ / _ \ '__| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| |__| (_| | |_| | | | | (__| | | | __/ | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_____\__,_|\__,_|_| |_|\___|_| |_|\___|_| #
7# |_| |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Stefan Unrein #
11# Patrick Lehmann #
12# #
13# License: #
14# ==================================================================================================================== #
15# Copyright 2021-2025 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-2025, Stefan Unrein"
36__license__ = "Apache License, Version 2.0"
37__version__ = "0.2.0"
38__keywords__ = ["launcher", "version selector", "amd", "xilinx", "vivado"]
40from colorama import init as colorama_init, Fore as Foreground
41from pathlib import Path
42from re import compile as re_compile
43from subprocess import Popen
44from sys import exit, argv, stdout
45from textwrap import dedent
46from time import sleep
47from typing import NoReturn, Generator, Tuple
49from pyTooling.Decorators import export
50from pyTooling.Versioning import YearReleaseVersion
53@export
54class Program:
55 """Program instance of pyEDAA.Launcher."""
57 _vivadoBatchfile = Path("bin/vivado.bat")
58 _vvglWrapperFile = Path("bin/unwrapped/win64.o/vvgl.exe")
60 _vivadoVersionPattern = re_compile(r"\d+\.\d+(\.\d+)?")
61 _versionLinePattern = re_compile(r"^<!--\s*Product\sVersion:\s+Vivado\s+v(?P<major>\d+).(?P<minor>\d+)(?:.(?P<patch>\d+))?\s+\(64-bit\)\s+-->")
63 _projectFilePath: Path
65 def __init__(self, projectFilePath: Path) -> None:
66 """Initializer.
68 :param projectFilePath: Path to the ``*.xpr`` file.
69 :raises Exception: When the given ``*.xpr`` file doesn't exist.
70 """
71 if not projectFilePath.exists(): 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 raise Exception(f"Vivado project file '{projectFilePath}' not found.") \
73 from FileNotFoundError(f"File '{projectFilePath}' not found.")
75 self._projectFilePath = projectFilePath
77 def GetVersion(self) -> YearReleaseVersion:
78 """Opens an ``*.xpr`` file and returns the Vivado version used to save this file.
80 :returns: Used Vivado version to save the given ``*.xpr`` file.
81 :raises Exception: When the version information isn't found in the file.
82 """
83 with self._projectFilePath.open("r", encoding="utf-8") as file:
84 for line in file: 84 ↛ 89line 84 didn't jump to line 89 because the loop on line 84 didn't complete
85 match = self._versionLinePattern.match(line)
86 if match is not None:
87 return YearReleaseVersion(year=int(match['major']), release=int(match['minor']))
88 else:
89 raise Exception(f"Pattern not found in '{self._projectFilePath}'.")
91 @classmethod
92 def GetVivadoVersions(cls, xilinxInstallPath: Path) -> Generator[Tuple[YearReleaseVersion, Path], None, None]:
93 """Scan a given directory for installed Vivado versions.
95 :param xilinxInstallPath: Xilinx installation directory.
96 :returns: A generator for a sequence of installed Vivado versions.
97 """
98 for directory in xilinxInstallPath.iterdir():
99 if directory.is_dir(): 99 ↛ 98line 99 didn't jump to line 98 because the condition on line 99 was always true
100 if directory.name == "Vivado":
101 for version in directory.iterdir():
102 if cls._vivadoVersionPattern.match(version.name): 102 ↛ 101line 102 didn't jump to line 101 because the condition on line 102 was always true
103 yield YearReleaseVersion.Parse(version.name), version
104 elif cls._vivadoVersionPattern.match(directory.name): 104 ↛ 98line 104 didn't jump to line 98 because the condition on line 104 was always true
105 yield YearReleaseVersion.Parse(directory.name), directory / "Vivado"
107 def StartVivado(self, vivadoInstallationPath: Path) -> None:
108 """Start the given Vivado version with an ``*.xpr`` file as parameter.
110 :param vivadoInstallationPath: Path to the Xilinx toolchain installations.
111 :param version: The Vivado version to start.
112 """
113 vvglWrapperPath = vivadoInstallationPath / self._vvglWrapperFile
114 vivadoBatchfilePath = vivadoInstallationPath / self._vivadoBatchfile
116 cmd = [str(vvglWrapperPath), str(vivadoBatchfilePath), str(self._projectFilePath)]
117 Popen(cmd, cwd=self._projectFilePath.parent)
120@export
121def printHeadline() -> None:
122 """
123 Print the programs headline.
125 .. code-block::
127 ================================================================================
128 pyEDAA.Launcher
129 ================================================================================
131 """
132 print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")
133 print(f"{Foreground.MAGENTA}{'pyEDAA.Launcher':^80}{Foreground.RESET}")
134 print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")
137@export
138def printVersion() -> None:
139 """
140 Print author(s), copyright notice, license and version.
142 .. code-block::
144 Author: Jane Doe
145 Copyright: ....
146 License: MIT
147 Version: v2.1.4
149 """
150 print(f"Author: {__author__} ({__email__})")
151 print(f"Copyright: {__copyright__}")
152 print(f"License: {__license__}")
153 print(f"Version: {__version__}")
156@export
157def printCLIOptions() -> None:
158 """
159 Print accepted CLI arguments and CLI options.
160 """
161 print(f"{Foreground.LIGHTBLUE_EX}Accepted argument:{Foreground.RESET}")
162 print(" <path to xpr file> AMD/Xilinx Vivado project file")
163 print()
164 print(f"{Foreground.LIGHTBLUE_EX}Accepted options:{Foreground.RESET}")
165 print(" --help Show a help page.")
166 print(" --version Show tool version.")
167 print(" --list List available Vivado versions.")
170@export
171def printSetup(scriptPath: Path) -> None:
172 """
173 Print how to setup pyEDAA.Launcher.
175 :param scriptPath: Path to this script.
176 """
177 print(dedent(f"""\
178 For using this {scriptPath.stem}, please associate the '*.xpr' file extension to
179 this executable.
181 {Foreground.LIGHTBLUE_EX}Setup steps:{Foreground.RESET}
182 * Copy this executable into the Xilinx installation directory.
183 Example: C:\\Xilinx\\
184 * Set '*.xpr' file association:
185 1. right-click on any existing '*.xrp' file in Windows Explorer
186 2. open with
187 3. {scriptPath}""")
188 )
191@export
192def printAvailableVivadoVersions(xilinxInstallationPath: Path) -> None:
193 """
194 Print a list of discovered Xilinx Vivado installations.
196 :param xilinxInstallationPath: Directory were Xilinx software is installed.
197 """
198 print(dedent(f"""\
199 {Foreground.LIGHTBLACK_EX}Detecting Vivado installations in Xilinx installation directory '{xilinxInstallationPath}' ...{Foreground.RESET}
201 {Foreground.LIGHTBLUE_EX}Detected Vivado versions:{Foreground.RESET}""")
202 )
203 for version, installDirectory in Program.GetVivadoVersions(xilinxInstallationPath):
204 print(f"* {Foreground.GREEN}{version}{Foreground.RESET} -> {installDirectory}")
207@export
208def waitForReturnKeyAndExit(exitCode: int = 0) -> NoReturn:
209 """
210 Ask the user to press Return. Afterwards exit the program with ``exitcode``.
212 :param exitCode: Exit code when returning to caller.
213 """
214 print()
215 print(f"{Foreground.CYAN}Press Return to exit.{Foreground.RESET}")
216 input() # wait on user interaction
217 exit(exitCode)
220@export
221def main() -> NoReturn:
222 """Entry point function.
224 It creates an instance of :class:`Program` and hands over the execution to the OOP world.
225 """
226 colorama_init()
228 scriptPath = Path(argv[0])
229 xilinxInstallationDirectory = scriptPath.parent
231 printHeadline()
232 if (argc := len(argv)) == 1:
233 printVersion()
234 print()
235 print(f"{Foreground.RED}[ERROR] No argument or option provided.{Foreground.RESET}")
236 print()
237 printSetup(scriptPath)
238 print()
239 printAvailableVivadoVersions(xilinxInstallationDirectory)
240 print()
241 printCLIOptions()
242 waitForReturnKeyAndExit(2)
243 elif argc == 2:
244 if (option := argv[1]) == "--help":
245 print(dedent(f"""\
246 {scriptPath.stem} launches the matching Vivado installation based on the Vivado
247 version used to save an '*.xpr' file""")
248 )
249 print()
250 printCLIOptions()
251 exit(0)
252 elif option == "--version":
253 printVersion()
254 exit(0)
255 elif option == "--list":
256 printAvailableVivadoVersions(xilinxInstallationDirectory)
257 exit(0)
258 else:
259 try:
260 program = Program(Path(option))
261 versionFromXPRFile = program.GetVersion()
263 for version, vivadoInstallationDirectory in program.GetVivadoVersions(xilinxInstallationDirectory):
264 if version == versionFromXPRFile:
265 print(f"Using Vivado {Foreground.GREEN}{version}{Foreground.RESET} to open '{program._projectFilePath.parent.as_posix()}/{Foreground.CYAN}{program._projectFilePath.name}{Foreground.RESET}'.")
266 print()
267 program.StartVivado(vivadoInstallationDirectory)
269 i = 3
270 print(f"Closing in {i}", end="")
271 stdout.flush()
272 for i in range(i - 1, 0, -1):
273 sleep(1)
274 print(f"\x1b[1D{i}", end="")
275 stdout.flush()
276 sleep(1)
277 print()
278 exit(0)
279 else:
280 print(dedent(f"""\
281 {Foreground.RED}[ERROR] Vivado version {versionFromXPRFile} not available.{Foreground.RESET}
283 Please start manually!""")
284 )
285 printAvailableVivadoVersions(xilinxInstallationDirectory)
286 waitForReturnKeyAndExit(2)
288 except Exception as ex:
289 print(f"{Foreground.RED}[ERROR] {ex}{Foreground.RESET}")
290 if ex.__cause__ is not None:
291 print(f"{Foreground.YELLOW}Caused by: {ex.__cause__}{Foreground.RESET}")
292 waitForReturnKeyAndExit(1)
293 else:
294 printHeadline()
295 print(f"{Foreground.RED}[ERROR] Too many arguments.{Foreground.RESET}")
296 print()
297 printCLIOptions()
298 waitForReturnKeyAndExit(2)
301# Entry point
302if __name__ == "__main__": 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true
303 main()