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

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" 

42 

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 

51 

52from pyTooling.Decorators import export 

53from pyTooling.Versioning import YearReleaseVersion 

54 

55 

56@export 

57class Program: 

58 """Program instance of pyEDAA.Launcher.""" 

59 

60 _vivadoBatchfile = Path("bin/vivado.bat") 

61 _vvglWrapperFile = Path("bin/unwrapped/win64.o/vvgl.exe") 

62 

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+-->") 

65 

66 _projectFilePath: Path 

67 

68 def __init__(self, projectFilePath: Path) -> None: 

69 """Initializer. 

70 

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.") 

77 

78 self._projectFilePath = projectFilePath 

79 

80 def GetVersion(self) -> YearReleaseVersion: 

81 """Opens an ``*.xpr`` file and returns the Vivado version used to save this file. 

82 

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}'.") 

93 

94 @classmethod 

95 def GetVivadoVersions(cls, xilinxInstallPath: Path) -> Generator[Tuple[YearReleaseVersion, Path], None, None]: 

96 """Scan a given directory for installed Vivado versions. 

97 

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" 

109 

110 def StartVivado(self, vivadoInstallationPath: Path) -> None: 

111 """Start the given Vivado version with an ``*.xpr`` file as parameter. 

112 

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 

118 

119 cmd = [str(vvglWrapperPath), str(vivadoBatchfilePath), str(self._projectFilePath)] 

120 Popen(cmd, cwd=self._projectFilePath.parent) 

121 

122 

123@export 

124def printHeadline() -> None: 

125 """ 

126 Print the programs headline. 

127 

128 .. code-block:: 

129 

130 ================================================================================ 

131 pyEDAA.Launcher 

132 ================================================================================ 

133 

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}") 

138 

139 

140@export 

141def printVersion() -> None: 

142 """ 

143 Print author(s), copyright notice, license and version. 

144 

145 .. code-block:: 

146 

147 Author: Jane Doe 

148 Copyright: .... 

149 License: MIT 

150 Version: v2.1.4 

151 

152 """ 

153 print(f"Author: {__author__} ({__email__})") 

154 print(f"Copyright: {__copyright__}") 

155 print(f"License: {__license__}") 

156 print(f"Version: {__version__}") 

157 

158 

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.") 

171 

172 

173@export 

174def printSetup(scriptPath: Path) -> None: 

175 """ 

176 Print how to setup pyEDAA.Launcher. 

177 

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. 

183 

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 ) 

192 

193 

194@export 

195def printAvailableVivadoVersions(xilinxInstallationPath: Path) -> None: 

196 """ 

197 Print a list of discovered Xilinx Vivado installations. 

198 

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} 

203 

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}") 

208 

209 

210@export 

211def waitForReturnKeyAndExit(exitCode: int = 0) -> NoReturn: 

212 """ 

213 Ask the user to press Return. Afterwards exit the program with ``exitcode``. 

214 

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) 

221 

222 

223@export 

224def main() -> NoReturn: 

225 """Entry point function. 

226 

227 It creates an instance of :class:`Program` and hands over the execution to the OOP world. 

228 """ 

229 colorama_init() 

230 

231 scriptPath = Path(argv[0]) 

232 xilinxInstallationDirectory = scriptPath.parent 

233 

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() 

265 

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) 

271 

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} 

285 

286 Please start manually!""") 

287 ) 

288 printAvailableVivadoVersions(xilinxInstallationDirectory) 

289 waitForReturnKeyAndExit(2) 

290 

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) 

302 

303 

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()