Coverage for tsfpga/examples/build_fpga_utils.py: 0%

88 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-08-29 20:51 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import argparse 

12from pathlib import Path 

13from shutil import copy2, make_archive 

14from typing import TYPE_CHECKING, Callable 

15 

16from hdl_registers.generator.c.header import CHeaderGenerator 

17from hdl_registers.generator.cpp.header import CppHeaderGenerator 

18from hdl_registers.generator.cpp.implementation import CppImplementationGenerator 

19from hdl_registers.generator.cpp.interface import CppInterfaceGenerator 

20from hdl_registers.generator.html.page import HtmlPageGenerator 

21from hdl_registers.generator.python.accessor import PythonAccessorGenerator 

22from hdl_registers.generator.python.pickle import PythonPickleGenerator 

23 

24from tsfpga.system_utils import create_directory, delete 

25 

26if TYPE_CHECKING: 

27 from tsfpga.build_project_list import BuildProjectList 

28 from tsfpga.module_list import ModuleList 

29 from tsfpga.vivado.project import VivadoProject 

30 

31 

32def arguments(default_temp_dir: Path) -> argparse.Namespace: 

33 """ 

34 Setup of arguments for the example build flow. 

35 

36 Arguments: 

37 default_temp_dir: Default value for output paths 

38 (can be overridden by command line argument). 

39 """ 

40 parser = argparse.ArgumentParser( 

41 "Create, synth and build an FPGA project", 

42 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 

43 ) 

44 

45 group = parser.add_mutually_exclusive_group() 

46 

47 group.add_argument("--list-only", action="store_true", help="list the available projects") 

48 

49 group.add_argument( 

50 "--generate-registers-only", 

51 action="store_true", 

52 help="only generate the register artifacts (C/C++ code, HTML, ...) for inspection", 

53 ) 

54 

55 group.add_argument("--create-only", action="store_true", help="only create projects") 

56 

57 group.add_argument("--synth-only", action="store_true", help="only synthesize projects") 

58 

59 group.add_argument( 

60 "--from-impl", 

61 action="store_true", 

62 help="run impl and onwards on an existing synthesized projects", 

63 ) 

64 

65 group.add_argument("--open", action="store_true", help="open existing projects in the GUI") 

66 

67 group.add_argument( 

68 "--collect-artifacts-only", 

69 action="store_true", 

70 help="collect artifacts of previously successful builds", 

71 ) 

72 

73 parser.add_argument( 

74 "--use-existing-project", 

75 action="store_true", 

76 help="build existing projects, or create first if they do not exist", 

77 ) 

78 

79 parser.add_argument( 

80 "--netlist-builds", 

81 action="store_true", 

82 help="use netlist build projects instead of top level build projects", 

83 ) 

84 

85 parser.add_argument( 

86 "--projects-path", 

87 type=Path, 

88 default=default_temp_dir / "projects", 

89 help="the FPGA build projects will be placed here", 

90 ) 

91 

92 parser.add_argument( 

93 "--ip-cache-path", 

94 type=Path, 

95 default=default_temp_dir / "vivado_ip_cache", 

96 help="location of Vivado IP cache", 

97 ) 

98 

99 parser.add_argument( 

100 "--output-path", 

101 type=Path, 

102 required=False, 

103 help="the output products (bit file, ...) will be placed here", 

104 ) 

105 

106 parser.add_argument( 

107 "--num-parallel-builds", type=int, default=8, help="Number of parallel builds to launch" 

108 ) 

109 

110 parser.add_argument( 

111 "--num-threads-per-build", 

112 type=int, 

113 default=4, 

114 help="number of threads for each build process", 

115 ) 

116 

117 parser.add_argument("--no-color", action="store_true", help="disable color in printouts") 

118 

119 parser.add_argument( 

120 "project_filters", 

121 nargs="*", 

122 help="filter for which projects to build. Can use wildcards. Leave empty for all.", 

123 ) 

124 

125 args = parser.parse_args() 

126 

127 assert args.use_existing_project or not args.from_impl, ( 

128 "Must set --use-existing-project when using --from-impl" 

129 ) 

130 

131 return args 

132 

133 

134def setup_and_run( # noqa: C901, PLR0911 

135 modules: ModuleList, 

136 projects: BuildProjectList, 

137 args: argparse.Namespace, 

138 collect_artifacts_function: Callable[[VivadoProject, Path], bool] | None, 

139) -> int: 

140 """ 

141 Example of a function to setup and execute build projects. 

142 As instructed by the arguments. 

143 

144 Arguments: 

145 modules: When running a register generation, registers from these 

146 modules will be included. 

147 projects: These build projects will be built. 

148 args: Command line argument namespace. 

149 collect_artifacts_function: Function pointer to a function that collects build artifacts. 

150 Will be run after a successful implementation build. 

151 The function must return ``True`` if successful and ``False`` otherwise. 

152 It will receive the ``project`` and ``output_path`` as arguments. 

153 

154 Can be ``None`` if no special artifact collection operation shall be run. 

155 Which is typically the case for synthesis-only builds such as netlist builds. 

156 

157 Return: 

158 0 if everything passed, otherwise non-zero. 

159 Can be used for system exit code. 

160 """ 

161 if args.list_only: 

162 print(projects) 

163 return 0 

164 

165 if args.generate_registers_only: 

166 # Generate register output from all modules. Note that this is not used by the 

167 # build flow or simulation flow, it is only for the user to inspect the artifacts. 

168 generate_register_artifacts( 

169 modules=modules, output_path=args.projects_path.parent / "registers" 

170 ) 

171 return 0 

172 

173 if args.open: 

174 projects.open(projects_path=args.projects_path) 

175 return 0 

176 

177 if args.collect_artifacts_only: 

178 # We have to assume that the projects exist if the user sent this argument. 

179 # The 'collect_artifacts_function' call below will probably fail if it does not. 

180 create_ok = True 

181 

182 elif args.use_existing_project: 

183 create_ok = projects.create_unless_exists( 

184 projects_path=args.projects_path, 

185 num_parallel_builds=args.num_parallel_builds, 

186 ip_cache_path=args.ip_cache_path, 

187 ) 

188 

189 else: 

190 create_ok = projects.create( 

191 projects_path=args.projects_path, 

192 num_parallel_builds=args.num_parallel_builds, 

193 ip_cache_path=args.ip_cache_path, 

194 ) 

195 

196 if not create_ok: 

197 return 1 

198 

199 if args.create_only: 

200 return 0 

201 

202 # If doing only synthesis, there are no artifacts to collect. 

203 collect_artifacts_function = ( 

204 None if (args.synth_only or args.netlist_builds) else collect_artifacts_function 

205 ) 

206 

207 if args.collect_artifacts_only: 

208 assert collect_artifacts_function is not None, "No artifact collection available" 

209 

210 for project in projects.projects: 

211 # Assign the arguments in the exact same way as within the call to 

212 # 'projects.build()' below. 

213 # Ensures that the correct output path is used in all scenarios. 

214 assert collect_artifacts_function( 

215 project=project, 

216 output_path=projects.get_build_project_output_path( 

217 project=project, projects_path=args.projects_path, output_path=args.output_path 

218 ), 

219 ) 

220 

221 return 0 

222 

223 build_ok = projects.build( 

224 projects_path=args.projects_path, 

225 num_parallel_builds=args.num_parallel_builds, 

226 num_threads_per_build=args.num_threads_per_build, 

227 output_path=args.output_path, 

228 collect_artifacts=collect_artifacts_function, 

229 synth_only=args.synth_only, 

230 from_impl=args.from_impl, 

231 ) 

232 

233 if build_ok: 

234 return 0 

235 

236 return 1 

237 

238 

239def generate_register_artifacts(modules: ModuleList, output_path: Path) -> None: 

240 """ 

241 Example of a function to generate register artifacts from the given modules. 

242 Will generate pretty much all register artifacts available. 

243 In your own build flow you might want to only generate a subset of these. 

244 

245 Arguments: 

246 modules: Registers from these modules will be included. 

247 output_path: Register artifacts will be placed here. 

248 """ 

249 print(f"Generating register artifacts in {output_path.resolve()}...") 

250 

251 # Empty the output directory so we don't have leftover old artifacts. 

252 create_directory(directory=output_path, empty=True) 

253 

254 for module in modules: 

255 if module.registers is not None: 

256 CHeaderGenerator(module.registers, output_path / "c").create() 

257 

258 CppInterfaceGenerator(module.registers, output_path / "cpp" / "include").create() 

259 CppHeaderGenerator(module.registers, output_path / "cpp" / "include").create() 

260 CppImplementationGenerator(module.registers, output_path / "cpp").create() 

261 

262 HtmlPageGenerator(module.registers, output_path / "html").create() 

263 

264 PythonPickleGenerator(module.registers, output_path / "python").create() 

265 PythonAccessorGenerator(module.registers, output_path / "python").create() 

266 

267 

268def collect_artifacts(project: VivadoProject, output_path: Path) -> bool: 

269 """ 

270 Example of a function to collect build artifacts. 

271 Will create a zip file with the bitstream, hardware definition (.xsa) and register artifacts. 

272 

273 Arguments: 

274 project: Project object that has been built, and who's artifacts shall now be collected. 

275 output_path: Path to the build output. Artifact zip will be placed here as well. 

276 

277 Return: 

278 True if everything went well. 

279 """ 

280 version = "0.0.0" 

281 release_dir = create_directory(output_path / f"{project.name}-{version}", empty=True) 

282 print(f"Creating release in {release_dir.resolve()}.zip") 

283 

284 generate_register_artifacts(modules=project.modules, output_path=release_dir / "registers") 

285 copy2(output_path / f"{project.name}.bit", release_dir) 

286 copy2(output_path / f"{project.name}.bin", release_dir) 

287 if (output_path / f"{project.name}.xsa").exists(): 

288 copy2(output_path / f"{project.name}.xsa", release_dir) 

289 

290 make_archive(str(release_dir), "zip", release_dir) 

291 

292 # Remove folder so that only zip remains 

293 delete(release_dir) 

294 

295 return True