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

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"] 

39 

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 

48 

49from pyTooling.Decorators import export 

50from pyTooling.Versioning import YearReleaseVersion 

51 

52 

53@export 

54class Program: 

55 """Program instance of pyEDAA.Launcher.""" 

56 

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

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

59 

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

62 

63 _projectFilePath: Path 

64 

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

66 """Initializer. 

67 

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

74 

75 self._projectFilePath = projectFilePath 

76 

77 def GetVersion(self) -> YearReleaseVersion: 

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

79 

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

90 

91 @classmethod 

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

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

94 

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" 

106 

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

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

109 

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 

115 

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

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

118 

119 

120@export 

121def printHeadline() -> None: 

122 """ 

123 Print the programs headline. 

124 

125 .. code-block:: 

126 

127 ================================================================================ 

128 pyEDAA.Launcher 

129 ================================================================================ 

130 

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

135 

136 

137@export 

138def printVersion() -> None: 

139 """ 

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

141 

142 .. code-block:: 

143 

144 Author: Jane Doe 

145 Copyright: .... 

146 License: MIT 

147 Version: v2.1.4 

148 

149 """ 

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

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

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

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

154 

155 

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

168 

169 

170@export 

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

172 """ 

173 Print how to setup pyEDAA.Launcher. 

174 

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. 

180 

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 ) 

189 

190 

191@export 

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

193 """ 

194 Print a list of discovered Xilinx Vivado installations. 

195 

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} 

200 

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

205 

206 

207@export 

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

209 """ 

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

211 

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) 

218 

219 

220@export 

221def main() -> NoReturn: 

222 """Entry point function. 

223 

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

225 """ 

226 colorama_init() 

227 

228 scriptPath = Path(argv[0]) 

229 xilinxInstallationDirectory = scriptPath.parent 

230 

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

262 

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) 

268 

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} 

282 

283 Please start manually!""") 

284 ) 

285 printAvailableVivadoVersions(xilinxInstallationDirectory) 

286 waitForReturnKeyAndExit(2) 

287 

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) 

299 

300 

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