Coverage for apio/commands/apio_packages.py: 77%

92 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-24 03:51 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2024 FPGAwars 

4# -- Authors 

5# -- * Jesús Arroyo (2016-2019) 

6# -- * Juan Gonzalez (obijuan) (2019-2024) 

7# -- License GPLv2 

8"""Implementation of 'apio packages' command""" 

9 

10import sys 

11from typing import Dict 

12from dataclasses import dataclass 

13import click 

14from rich.table import Table 

15from rich import box 

16from apio.common.apio_console import cout, ctable, cerror 

17from apio.common.apio_styles import INFO, BORDER, ERROR, SUCCESS 

18from apio.managers import packages 

19from apio.commands import options 

20from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand 

21from apio.apio_context import ( 

22 ApioContext, 

23 ProjectPolicy, 

24 RemoteConfigPolicy, 

25 PackagesPolicy, 

26) 

27 

28 

29@dataclass(frozen=True) 

30class RequiredPackageRow: 

31 """Information of a row of a required package.""" 

32 

33 # -- Package name 

34 name: str 

35 # -- The status column text value. 

36 status: str 

37 # -- The style to use for the row. 

38 style: str | None 

39 

40 

41def print_packages_report(apio_ctx: ApioContext) -> bool: 

42 """A common function to print the state of the packages. 

43 Returns True if the packages are OK. 

44 """ 

45 

46 # -- Scan the packages 

47 scan = packages.scan_packages(apio_ctx.packages_context) 

48 

49 # -- Shortcuts to reduce clutter. 

50 get_installed_package_info = apio_ctx.profile.get_installed_package_info 

51 get_required_package_info = apio_ctx.get_required_package_info 

52 

53 table = Table( 

54 show_header=True, 

55 show_lines=True, 

56 box=box.SQUARE, 

57 border_style=BORDER, 

58 title="Apio Packages Status", 

59 title_justify="left", 

60 padding=(0, 2), 

61 ) 

62 

63 table.add_column("PACKAGE NAME", no_wrap=True) 

64 table.add_column("VERSION", no_wrap=True) 

65 table.add_column("PLATFORM", no_wrap=True) 

66 table.add_column("DESCRIPTION", no_wrap=True) 

67 table.add_column("STATUS", no_wrap=True) 

68 

69 required_packages_rows: Dict[str, RequiredPackageRow] = {} 

70 

71 # -- Collect rows of required packages that are installed OK. 

72 for package_name in scan.installed_ok_package_names: 

73 assert package_name not in required_packages_rows 

74 required_packages_rows[package_name] = RequiredPackageRow( 

75 package_name, "OK", None 

76 ) 

77 

78 # -- Collect rows of required packages that are uninstalled. 

79 for package_name in scan.uninstalled_package_names: 79 ↛ 80line 79 didn't jump to line 80 because the loop on line 79 never started

80 assert package_name not in required_packages_rows 

81 required_packages_rows[package_name] = RequiredPackageRow( 

82 package_name, "Uninstalled", INFO 

83 ) 

84 

85 # -- Collect rows of required packages have version or platform mismatch. 

86 for package_name in scan.bad_version_package_names: 86 ↛ 87line 86 didn't jump to line 87 because the loop on line 86 never started

87 assert package_name not in required_packages_rows 

88 required_packages_rows[package_name] = RequiredPackageRow( 

89 package_name, "Mismatch", ERROR 

90 ) 

91 

92 # -- Collect rows of required packages that are broken. 

93 for package_name in scan.broken_package_names: 93 ↛ 94line 93 didn't jump to line 94 because the loop on line 93 never started

94 assert package_name not in required_packages_rows 

95 required_packages_rows[package_name] = RequiredPackageRow( 

96 package_name, "Broken", ERROR 

97 ) 

98 

99 # -- Add the required packages rows to the table, in the order that they 

100 # -- are statically defined in the remote config file. 

101 assert set(required_packages_rows.keys()) == ( 

102 apio_ctx.required_packages.keys() 

103 ) 

104 for package_name in apio_ctx.required_packages: 

105 row_info = required_packages_rows[package_name] 

106 version, platform_id = get_installed_package_info(package_name) 

107 info = get_required_package_info(package_name) 

108 description = info["description"] 

109 table.add_row( 

110 package_name, 

111 version, 

112 platform_id, 

113 description, 

114 row_info.status, 

115 style=row_info.style, 

116 ) 

117 

118 # -- Render table. 

119 cout() 

120 ctable(table) 

121 

122 # -- Define errors table. 

123 table = Table( 

124 show_header=True, 

125 show_lines=True, 

126 box=box.SQUARE, 

127 border_style=BORDER, 

128 title="Apio Packages Errors", 

129 title_justify="left", 

130 padding=(0, 2), 

131 ) 

132 

133 # -- Add columns. 

134 table.add_column("ERROR TYPE", no_wrap=True, min_width=15, style=ERROR) 

135 table.add_column("NAME", no_wrap=True, min_width=15) 

136 

137 # -- Add rows. 

138 for package_name in scan.orphan_package_names: 138 ↛ 139line 138 didn't jump to line 139 because the loop on line 138 never started

139 table.add_row("Orphan package", package_name) 

140 

141 for name in sorted(scan.orphan_dir_names): 141 ↛ 142line 141 didn't jump to line 142 because the loop on line 141 never started

142 table.add_row("Orphan dir", name) 

143 

144 for name in sorted(scan.orphan_file_names): 144 ↛ 145line 144 didn't jump to line 145 because the loop on line 144 never started

145 table.add_row("Orphan file", name) 

146 

147 # -- Render the table, unless empty. 

148 if table.row_count: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true

149 cout() 

150 ctable(table) 

151 

152 # -- Scan packages again and print a summary. 

153 packages_ok = scan.is_all_ok() 

154 

155 cout() 

156 if packages_ok: 156 ↛ 159line 156 didn't jump to line 159 because the condition on line 156 was always true

157 cout("All Apio packages are installed OK.", style=SUCCESS) 

158 else: 

159 cout( 

160 "Run 'apio packages install' to install the packages.", 

161 style=INFO, 

162 ) 

163 

164 # -- Return with the current packages status. Normally it should be 

165 # -- True for OK since we fixed and installed the packages. 

166 return packages_ok 

167 

168 

169# ------ apio packages install 

170 

171# -- Text in the rich-text format of the python rich library. 

172APIO_PACKAGES_INSTALL_HELP = """ 

173The command 'apio packages install' installs the installed Apio packages \ 

174to their latest requirements. 

175 

176Examples:[code] 

177 apio packages install # Install packages 

178 apio pack upd # Same, with shortcuts 

179 apio packages install --force # Force reinstallation from scratch 

180 apio packages install --verbose # Provide additional info[/code] 

181 

182Adding the '--force' option forces the reinstallation of existing packages; \ 

183otherwise, packages that are already installed correctly remain unchanged. 

184 

185It is highly recommended to run the 'apio packages install' once in a while \ 

186because it check the Apio remote server for the latest packages versions \ 

187which may included fixes and enhancements such as new examples that were \ 

188added to the examples package. 

189""" 

190 

191 

192@click.command( 

193 name="install", 

194 cls=ApioCommand, 

195 short_help="Install apio packages.", 

196 help=APIO_PACKAGES_INSTALL_HELP, 

197) 

198@options.force_option_gen(short_help="Force reinstallation.") 

199@options.verbose_option 

200def _install_cli( 

201 *, 

202 # Options 

203 force: bool, 

204 verbose: bool, 

205): 

206 """Implements the 'apio packages install' command.""" 

207 

208 apio_ctx = ApioContext( 

209 project_policy=ProjectPolicy.NO_PROJECT, 

210 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

211 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

212 ) 

213 

214 # cout(f"Platform id '{apio_ctx.platform_id}'") 

215 

216 # -- First thing, fix broken packages, if any. This forces fetching 

217 # -- of the latest remote config file. 

218 packages.scan_and_fix_packages(apio_ctx.packages_context) 

219 

220 # -- Install the packages, one by one. 

221 for package in apio_ctx.required_packages: 

222 packages.install_package( 

223 apio_ctx.packages_context, 

224 package_name=package, 

225 force_reinstall=force, 

226 verbose=verbose, 

227 ) 

228 

229 # -- If verbose, print a full report. 

230 if verbose: 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true

231 package_ok = print_packages_report(apio_ctx) 

232 sys.exit(0 if package_ok else 1) 

233 

234 # -- When not in verbose mode, we run a scan and print a short status. 

235 scan = packages.scan_packages(apio_ctx.packages_context) 

236 if not scan.is_all_ok(): 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 cerror("Failed to install some packages.") 

238 cout( 

239 "Run 'apio packages list' to view the packages.", 

240 style=INFO, 

241 ) 

242 sys.exit(1) 

243 

244 # -- Here when packages are ok. 

245 cout("All Apio packages are installed OK.", style=SUCCESS) 

246 

247 

248# ------ apio packages list 

249 

250# -- Text in the rich-text format of the python rich library. 

251APIO_PACKAGES_LIST_HELP = """ 

252The command 'apio packages list' lists the available and installed Apio \ 

253packages. The list of available packages depends on the operating system \ 

254you are using and may vary between operating systems. 

255 

256Examples:[code] 

257 apio packages list[/code] 

258""" 

259 

260 

261@click.command( 

262 name="list", 

263 cls=ApioCommand, 

264 short_help="List apio packages.", 

265 help=APIO_PACKAGES_LIST_HELP, 

266) 

267# @options.verbose_option 

268def _list_cli(): 

269 """Implements the 'apio packages list' command.""" 

270 

271 apio_ctx = ApioContext( 

272 project_policy=ProjectPolicy.NO_PROJECT, 

273 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

274 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

275 ) 

276 

277 # -- Print packages report. 

278 packages_ok = print_packages_report(apio_ctx) 

279 sys.exit(0 if packages_ok else 1) 

280 

281 

282# ------ apio packages (group) 

283 

284# -- Text in the rich-text format of the python rich library. 

285APIO_PACKAGES_HELP = """ 

286The command group 'apio packages' provides commands to manage the \ 

287installation of Apio packages. These are not Python packages but \ 

288Apio packages containing various tools and data essential for the \ 

289operation of Apio. 

290 

291The list of available packages depends on the operating system you are \ 

292using and may vary between different operating systems. 

293""" 

294 

295 

296# -- We have only a single group with the title 'Subcommands'. 

297SUBGROUPS = [ 

298 ApioSubgroup( 

299 "Subcommands", 

300 [ 

301 _install_cli, 

302 _list_cli, 

303 ], 

304 ) 

305] 

306 

307 

308@click.command( 

309 name="packages", 

310 cls=ApioGroup, 

311 subgroups=SUBGROUPS, 

312 short_help="Manage the apio packages.", 

313 help=APIO_PACKAGES_HELP, 

314) 

315def cli(): 

316 """Implements the 'apio packages' command group.'""" 

317 

318 # pass