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

91 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +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 

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[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 description = get_required_package_info(package_name)["description"] 

108 table.add_row( 

109 package_name, 

110 version, 

111 platform_id, 

112 description, 

113 row_info.status, 

114 style=row_info.style, 

115 ) 

116 

117 # -- Render table. 

118 cout() 

119 ctable(table) 

120 

121 # -- Define errors table. 

122 table = Table( 

123 show_header=True, 

124 show_lines=True, 

125 box=box.SQUARE, 

126 border_style=BORDER, 

127 title="Apio Packages Errors", 

128 title_justify="left", 

129 padding=(0, 2), 

130 ) 

131 

132 # -- Add columns. 

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

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

135 

136 # -- Add rows. 

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

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

139 

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

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

142 

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

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

145 

146 # -- Render the table, unless empty. 

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

148 cout() 

149 ctable(table) 

150 

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

152 packages_ok = scan.is_all_ok() 

153 

154 cout() 

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

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

157 else: 

158 cout( 

159 "Run 'apio packages update' to update the packages.", 

160 style=INFO, 

161 ) 

162 

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

164 # -- True for OK since we fixed and updated the packages. 

165 return packages_ok 

166 

167 

168# ------ apio packages update 

169 

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

171APIO_PACKAGES_UPDATE_HELP = """ 

172The command 'apio packages update' updates the installed Apio packages \ 

173to their latest requirements. 

174 

175Examples:[code] 

176 apio packages update # Update packages 

177 apio pack upd # Same, with shortcuts 

178 apio packages update --force # Force reinstallation from scratch 

179 apio packages update --verbose # Provide additional info[/code] 

180 

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

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

183 

184It is highly recommended to run the 'apio packages update' once in a while \ 

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

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

187added to the examples package. 

188""" 

189 

190 

191@click.command( 

192 name="update", 

193 cls=ApioCommand, 

194 short_help="Update apio packages.", 

195 help=APIO_PACKAGES_UPDATE_HELP, 

196) 

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

198@options.verbose_option 

199def _update_cli( 

200 # Options 

201 force: bool, 

202 verbose: bool, 

203): 

204 """Implements the 'apio packages update' command.""" 

205 

206 apio_ctx = ApioContext( 

207 project_policy=ProjectPolicy.NO_PROJECT, 

208 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

209 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

210 ) 

211 

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

213 

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

215 # -- of the latest remote config file. 

216 packages.scan_and_fix_packages(apio_ctx.packages_context) 

217 

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

219 for package in apio_ctx.required_packages: 

220 packages.install_package( 

221 apio_ctx.packages_context, 

222 package_name=package, 

223 force_reinstall=force, 

224 verbose=verbose, 

225 ) 

226 

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

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

229 package_ok = print_packages_report(apio_ctx) 

230 sys.exit(0 if package_ok else 1) 

231 

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

233 scan = packages.scan_packages(apio_ctx.packages_context) 

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

235 cerror("Failed to update some packages.") 

236 cout( 

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

238 style=INFO, 

239 ) 

240 sys.exit(1) 

241 

242 # -- Here when packages are ok. 

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

244 

245 

246# ------ apio packages list 

247 

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

249APIO_PACKAGES_LIST_HELP = """ 

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

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

252you are using and may vary between operating systems. 

253 

254Examples:[code] 

255 apio packages list[/code] 

256""" 

257 

258 

259@click.command( 

260 name="list", 

261 cls=ApioCommand, 

262 short_help="List apio packages.", 

263 help=APIO_PACKAGES_LIST_HELP, 

264) 

265# @options.verbose_option 

266def _list_cli(): 

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

268 

269 apio_ctx = ApioContext( 

270 project_policy=ProjectPolicy.NO_PROJECT, 

271 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

272 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

273 ) 

274 

275 # -- Print packages report. 

276 packages_ok = print_packages_report(apio_ctx) 

277 sys.exit(0 if packages_ok else 1) 

278 

279 

280# ------ apio packages (group) 

281 

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

283APIO_PACKAGES_HELP = """ 

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

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

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

287operation of Apio. 

288 

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

290using and may vary between different operating systems. 

291""" 

292 

293 

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

295SUBGROUPS = [ 

296 ApioSubgroup( 

297 "Subcommands", 

298 [ 

299 _update_cli, 

300 _list_cli, 

301 ], 

302 ) 

303] 

304 

305 

306@click.command( 

307 name="packages", 

308 cls=ApioGroup, 

309 subgroups=SUBGROUPS, 

310 short_help="Manage the apio packages.", 

311 help=APIO_PACKAGES_HELP, 

312) 

313def cli(): 

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

315 

316 # pass