pyEDAA.Launcher

pyEDAA/Launcher/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# ==================================================================================================================== #
#              _____ ____    _        _      _                           _                                             #
#  _ __  _   _| ____|  _ \  / \      / \    | |    __ _ _   _ _ __   ___| |__   ___ _ __                               #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |   / _` | | | | '_ \ / __| '_ \ / _ \ '__|                              #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _| |__| (_| | |_| | | | | (__| | | |  __/ |                                 #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_____\__,_|\__,_|_| |_|\___|_| |_|\___|_|                                 #
# |_|    |___/                                                                                                         #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Stefan Unrein                                                                                                      #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2021-2025 Stefan Unrein - Endingen, Germany                                                                #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""Start the correct Vivado Version based on version in `*.xpr`file."""
__author__ =    "Stefan Unrein, Patrick Lehmann"
__email__ =     "paebbels@gmail.com"
__copyright__ = "2021-2025, Stefan Unrein"
__license__ =   "Apache License, Version 2.0"
__version__ =   "0.2.0"
__keywords__ =  ["launcher", "version selector", "amd", "xilinx", "vivado"]

from colorama   import init as colorama_init, Fore as Foreground
from pathlib    import Path
from re         import compile as re_compile
from subprocess import Popen
from sys        import exit, argv, stdout
from textwrap   import dedent
from time       import sleep
from typing     import NoReturn, Generator, Tuple

from pyTooling.Decorators import export
from pyTooling.Versioning import YearReleaseVersion


@export
class Program:
	"""Program instance of pyEDAA.Launcher."""

	_vivadoBatchfile = Path("bin/vivado.bat")
	_vvglWrapperFile = Path("bin/unwrapped/win64.o/vvgl.exe")

	_vivadoVersionPattern = re_compile(r"\d+\.\d+(\.\d+)?")
	_versionLinePattern =   re_compile(r"^<!--\s*Product\sVersion:\s+Vivado\s+v(?P<major>\d+).(?P<minor>\d+)(?:.(?P<patch>\d+))?\s+\(64-bit\)\s+-->")

	_projectFilePath: Path

	def __init__(self, projectFilePath: Path) -> None:
		"""Initializer.

		:param projectFilePath: Path to the ``*.xpr`` file.
		:raises Exception:      When the given ``*.xpr`` file doesn't exist.
		"""
		if not projectFilePath.exists():
			raise Exception(f"Vivado project file '{projectFilePath}' not found.") \
				from FileNotFoundError(f"File '{projectFilePath}' not found.")

		self._projectFilePath = projectFilePath

	def GetVersion(self) -> YearReleaseVersion:
		"""Opens an ``*.xpr`` file and returns the Vivado version used to save this file.

		:returns:          Used Vivado version to save the given ``*.xpr`` file.
		:raises Exception: When the version information isn't found in the file.
		"""
		with self._projectFilePath.open("r", encoding="utf-8") as file:
			for line in file:
				match = self._versionLinePattern.match(line)
				if match is not None:
					return YearReleaseVersion(year=int(match['major']), release=int(match['minor']))
			else:
				raise Exception(f"Pattern not found in '{self._projectFilePath}'.")

	@classmethod
	def GetVivadoVersions(cls, xilinxInstallPath: Path) -> Generator[Tuple[YearReleaseVersion, Path], None, None]:
		"""Scan a given directory for installed Vivado versions.

		:param xilinxInstallPath: Xilinx installation directory.
		:returns:                 A generator for a sequence of installed Vivado versions.
		"""
		for directory in xilinxInstallPath.iterdir():
			if directory.is_dir():
				if directory.name == "Vivado":
					for version in directory.iterdir():
						if cls._vivadoVersionPattern.match(version.name):
							yield YearReleaseVersion.Parse(version.name), version
				elif cls._vivadoVersionPattern.match(directory.name):
					yield YearReleaseVersion.Parse(directory.name), directory / "Vivado"

	def StartVivado(self, vivadoInstallationPath: Path) -> None:
		"""Start the given Vivado version with an ``*.xpr`` file as parameter.

		:param vivadoInstallationPath: Path to the Xilinx toolchain installations.
		:param version: The Vivado version to start.
		"""
		vvglWrapperPath =     vivadoInstallationPath / self._vvglWrapperFile
		vivadoBatchfilePath = vivadoInstallationPath / self._vivadoBatchfile

		cmd = [str(vvglWrapperPath), str(vivadoBatchfilePath), str(self._projectFilePath)]
		Popen(cmd, cwd=self._projectFilePath.parent)


@export
def printHeadline() -> None:
	"""
	Print the programs headline.

	.. code-block::

	   ================================================================================
	                                   pyEDAA.Launcher
	   ================================================================================

	"""
	print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")
	print(f"{Foreground.MAGENTA}{'pyEDAA.Launcher':^80}{Foreground.RESET}")
	print(f"{Foreground.MAGENTA}{'=' * 80}{Foreground.RESET}")


@export
def printVersion() -> None:
	"""
	Print author(s), copyright notice, license and version.

	.. code-block::

	   Author:    Jane Doe
	   Copyright: ....
	   License:   MIT
	   Version:   v2.1.4

	"""
	print(f"Author:    {__author__} ({__email__})")
	print(f"Copyright: {__copyright__}")
	print(f"License:   {__license__}")
	print(f"Version:   {__version__}")


@export
def printCLIOptions() -> None:
	"""
	Print accepted CLI arguments and CLI options.
	"""
	print(f"{Foreground.LIGHTBLUE_EX}Accepted argument:{Foreground.RESET}")
	print("  <path to xpr file>   AMD/Xilinx Vivado project file")
	print()
	print(f"{Foreground.LIGHTBLUE_EX}Accepted options:{Foreground.RESET}")
	print("  --help               Show a help page.")
	print("  --version            Show tool version.")
	print("  --list               List available Vivado versions.")


@export
def printSetup(scriptPath: Path) -> None:
	"""
	Print how to setup pyEDAA.Launcher.

	:param scriptPath: Path to this script.
	"""
	print(dedent(f"""\
		For using this {scriptPath.stem}, please associate the '*.xpr' file extension to
		this executable.

		{Foreground.LIGHTBLUE_EX}Setup steps:{Foreground.RESET}
		* Copy this executable into the Xilinx installation directory.
		  Example: C:\\Xilinx\\
		* Set '*.xpr' file association:
		  1. right-click on any existing '*.xrp' file in Windows Explorer
		  2. open with
		  3. {scriptPath}""")
		)


@export
def printAvailableVivadoVersions(xilinxInstallationPath: Path) -> None:
	"""
	Print a list of discovered Xilinx Vivado installations.

	:param xilinxInstallationPath: Directory were Xilinx software is installed.
	"""
	print(dedent(f"""\
		{Foreground.LIGHTBLACK_EX}Detecting Vivado installations in Xilinx installation directory '{xilinxInstallationPath}' ...{Foreground.RESET}

		{Foreground.LIGHTBLUE_EX}Detected Vivado versions:{Foreground.RESET}""")
	)
	for version, installDirectory in Program.GetVivadoVersions(xilinxInstallationPath):
		print(f"* {Foreground.GREEN}{version}{Foreground.RESET}  -> {installDirectory}")


@export
def waitForReturnKeyAndExit(exitCode: int = 0) -> NoReturn:
	"""
	Ask the user to press Return. Afterwards exit the program with ``exitcode``.

	:param exitCode: Exit code when returning to caller.
	"""
	print()
	print(f"{Foreground.CYAN}Press Return to exit.{Foreground.RESET}")
	input()  # wait on user interaction
	exit(exitCode)


@export
def main() -> NoReturn:
	"""Entry point function.

	It creates an instance of :class:`Program` and hands over the execution to the OOP world.
	"""
	colorama_init()

	scriptPath = Path(argv[0])
	xilinxInstallationDirectory = scriptPath.parent

	printHeadline()
	if (argc := len(argv)) == 1:
		printVersion()
		print()
		print(f"{Foreground.RED}[ERROR] No argument or option provided.{Foreground.RESET}")
		print()
		printSetup(scriptPath)
		print()
		printAvailableVivadoVersions(xilinxInstallationDirectory)
		print()
		printCLIOptions()
		waitForReturnKeyAndExit(2)
	elif argc == 2:
		if (option := argv[1]) == "--help":
			print(dedent(f"""\
				{scriptPath.stem} launches the matching Vivado installation based on the Vivado
				version used to save an '*.xpr' file""")
						)
			print()
			printCLIOptions()
			exit(0)
		elif option == "--version":
			printVersion()
			exit(0)
		elif option == "--list":
			printAvailableVivadoVersions(xilinxInstallationDirectory)
			exit(0)
		else:
			try:
				program = Program(Path(option))
				versionFromXPRFile = program.GetVersion()

				for version, vivadoInstallationDirectory in program.GetVivadoVersions(xilinxInstallationDirectory):
					if version == versionFromXPRFile:
						print(f"Using Vivado {Foreground.GREEN}{version}{Foreground.RESET} to open '{program._projectFilePath.parent.as_posix()}/{Foreground.CYAN}{program._projectFilePath.name}{Foreground.RESET}'.")
						print()
						program.StartVivado(vivadoInstallationDirectory)

						i = 3
						print(f"Closing in {i}", end="")
						stdout.flush()
						for i in range(i - 1, 0, -1):
							sleep(1)
							print(f"\x1b[1D{i}", end="")
							stdout.flush()
						sleep(1)
						print()
						exit(0)
				else:
					print(dedent(f"""\
						{Foreground.RED}[ERROR] Vivado version {versionFromXPRFile} not available.{Foreground.RESET}

						Please start manually!""")
					)
					printAvailableVivadoVersions(xilinxInstallationDirectory)
					waitForReturnKeyAndExit(2)

			except Exception as ex:
				print(f"{Foreground.RED}[ERROR]    {ex}{Foreground.RESET}")
				if ex.__cause__ is not None:
					print(f"{Foreground.YELLOW}Caused by: {ex.__cause__}{Foreground.RESET}")
				waitForReturnKeyAndExit(1)
	else:
		printHeadline()
		print(f"{Foreground.RED}[ERROR] Too many arguments.{Foreground.RESET}")
		print()
		printCLIOptions()
		waitForReturnKeyAndExit(2)


# Entry point
if __name__ == "__main__":
	main()