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

91 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 02:38 +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 install' to install 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 installed the packages. 

165 return packages_ok 

166 

167 

168# ------ apio packages install 

169 

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

171APIO_PACKAGES_INSTALL_HELP = """ 

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

173to their latest requirements. 

174 

175Examples:[code] 

176 apio packages install # Install packages 

177 apio pack upd # Same, with shortcuts 

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

179 apio packages install --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 install' 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="install", 

193 cls=ApioCommand, 

194 short_help="Install apio packages.", 

195 help=APIO_PACKAGES_INSTALL_HELP, 

196) 

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

198@options.verbose_option 

199def _install_cli( 

200 *, 

201 # Options 

202 force: bool, 

203 verbose: bool, 

204): 

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

206 

207 apio_ctx = ApioContext( 

208 project_policy=ProjectPolicy.NO_PROJECT, 

209 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

210 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

211 ) 

212 

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

214 

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

216 # -- of the latest remote config file. 

217 packages.scan_and_fix_packages(apio_ctx.packages_context) 

218 

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

220 for package in apio_ctx.required_packages: 

221 packages.install_package( 

222 apio_ctx.packages_context, 

223 package_name=package, 

224 force_reinstall=force, 

225 verbose=verbose, 

226 ) 

227 

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

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

230 package_ok = print_packages_report(apio_ctx) 

231 sys.exit(0 if package_ok else 1) 

232 

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

234 scan = packages.scan_packages(apio_ctx.packages_context) 

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

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

237 cout( 

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

239 style=INFO, 

240 ) 

241 sys.exit(1) 

242 

243 # -- Here when packages are ok. 

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

245 

246 

247# ------ apio packages list 

248 

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

250APIO_PACKAGES_LIST_HELP = """ 

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

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

253you are using and may vary between operating systems. 

254 

255Examples:[code] 

256 apio packages list[/code] 

257""" 

258 

259 

260@click.command( 

261 name="list", 

262 cls=ApioCommand, 

263 short_help="List apio packages.", 

264 help=APIO_PACKAGES_LIST_HELP, 

265) 

266# @options.verbose_option 

267def _list_cli(): 

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

269 

270 apio_ctx = ApioContext( 

271 project_policy=ProjectPolicy.NO_PROJECT, 

272 remote_config_policy=RemoteConfigPolicy.GET_FRESH, 

273 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

274 ) 

275 

276 # -- Print packages report. 

277 packages_ok = print_packages_report(apio_ctx) 

278 sys.exit(0 if packages_ok else 1) 

279 

280 

281# ------ apio packages (group) 

282 

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

284APIO_PACKAGES_HELP = """ 

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

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

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

288operation of Apio. 

289 

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

291using and may vary between different operating systems. 

292""" 

293 

294 

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

296SUBGROUPS = [ 

297 ApioSubgroup( 

298 "Subcommands", 

299 [ 

300 _install_cli, 

301 _list_cli, 

302 ], 

303 ) 

304] 

305 

306 

307@click.command( 

308 name="packages", 

309 cls=ApioGroup, 

310 subgroups=SUBGROUPS, 

311 short_help="Manage the apio packages.", 

312 help=APIO_PACKAGES_HELP, 

313) 

314def cli(): 

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

316 

317 # pass