Coverage for apio/managers/drivers.py: 20%

171 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 01:55 +0000

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

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

3# -- (C) 2016-2019 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6"""Manage board drivers""" 

7 

8 

9import getpass 

10import os 

11import shlex 

12import shutil 

13import subprocess 

14from pathlib import Path 

15from apio.utils import util 

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

17from apio.common.apio_styles import INFO, SUCCESS, EMPH1, EMPH3 

18from apio.apio_context import ApioContext 

19 

20 

21# -- Style shortcuts 

22E1 = f"[{EMPH1}]" 

23E3 = f"[{EMPH3}]" 

24 

25# -- A message to print when trying to install/uninstall apio drivers 

26# -- on platforms that don't require it. 

27NO_DRIVERS_MSG = "No driver installation is required on this platform." 

28 

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

30FTDI_INSTALL_INSTRUCTIONS_WINDOWS = f""" 

31{E3}Please follow these steps:[/] 

32 

33 1. Make sure your {E1}FPGA board is connected[/] to the computer. 

34 

35 2. {E1}Accept the Zadig request[/] to make changes to your computer. 

36 

37 3. {E1}Find the Zadig window[/] on your screen. You may need to click 

38 on its icon in the task bar for it to appear. 

39 

40 4. {E1}Select your FPGA board[/] from the drop down list, For example 

41 'Alhambra II v1.0A - B09-335 (Interface 0)'. 

42 

43 {E3}VERY IMPORTANT - If your board appears multiple time, make sure 

44 to select its 'interface 0' entry.[/] 

45 

46 5. {E1}Select the 'WinUSB' driver[/] as the target driver. For example 

47 'WinUSB (v6.1.7600.16385)'. 

48 

49 6. {E1}Click 'Replace Driver'[/] and wait for a successful 

50 completion, this can take a minute or two. 

51 

52 7. {E1}Close the Zadig window.[/] 

53 

54 8. {E1}Disconnect and reconnect[/] your FPGA board for the new driver 

55 to take affect. 

56 

57 9. {E1}Run the command 'apio devices scan-usb'[/] and verify that 

58 your board is listed. 

59""" 

60 

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

62FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS = f""" 

63{E3}Please follow these steps:[/] 

64 

65 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer. 

66 

67 2. If asked, {E1}allow the Device Manager to make changes to your system.[/] 

68 

69 3. {E1}Find the Device Manager window.[/] 

70 

71 4. {E1}Connect the board[/] to your computer and a new entry will be added 

72 to the device list (though sometimes it may be collapsed and 

73 hidden). 

74 

75 5. {E1}Identify the entry of your board[/] (e.g. in the 'Universal Serial 

76 Bus Devices' section). 

77 

78 {E3}NOTE: Boards with FT2232 ICs have two channels, 'interface 0' 

79 and 'interface 1'. Here we care only about 'interface 0' and 

80 ignore 'interface 1' if it appears as a COM port.[/] 

81 

82 6. {E1}Right click[/] on your board entry and \ 

83{E1}select 'Uninstall device'.[/] 

84 

85 7. If available, check the box {E1}'Delete the driver software for this 

86 device'.[/] 

87 

88 8. Click the {E1}'Uninstall' button[/]. 

89 

90 9. {E1}Close[/] the Device Manager window. 

91""" 

92 

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

94SERIAL_INSTALL_INSTRUCTIONS_WINDOWS = f""" 

95{E3}Please follow these steps:[/] 

96 

97 1. Make sure your FPGA {E1}board is connected[/] to the computer. 

98 

99 2. {E1}Accept the Serial Installer request[/] to make changes to your \ 

100computer. 

101 

102 3. Find the Serial installer window and {E1}follow the instructions.[/] 

103 

104 4. To verify, {E1}disconnect and reconnect the board[/] and run the command 

105 {E1}'apio devices scan-serial'.[/] 

106""" 

107 

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

109SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS = f""" 

110{E3}Please follow these steps:[/] 

111 

112 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer. 

113 

114 2. If asked, {E1}allow the Device Manager to make changes[/] to your system. 

115 

116 3. {E1}Find the Device Manager window.[/] 

117 

118 4. {E1}Connect the board[/] to your computer and a new entry will be added 

119 to the device list (though sometimes it may be collapsed). 

120 

121 5. {E1}Identify the entry of your board[/] (typically in the Ports section). 

122 

123 {E3} NOTE: If your board does not show up as a COM port, it may not 

124 have the 'apio drivers --serial-install' applied to it.[/] 

125 

126 6. {E1}Right click[/] on your board entry \ 

127and {E1}select 'Uninstall device'.[/] 

128 

129 7. If available, check the box \ 

130{E1}'Delete the driver software for this device'.[/] 

131 

132 8. Click the {E1}'Uninstall' button.[/] 

133 

134 9. {E1}Close the Device Manager window.[/] 

135""" 

136 

137 

138class Drivers: 

139 """Class for managing the board drivers""" 

140 

141 # -- The driver installation on linux consist of copying the rule files 

142 # -- to the /etc/udev/rules.d folder 

143 

144 # -- FTDI source rules file paths 

145 resources_dir = util.get_path_in_apio_package("resources") 

146 ftdi_rules_local_path = resources_dir / "80-fpga-ftdi.rules" 

147 

148 # -- Target rule file 

149 ftdi_rules_system_path = Path("/etc/udev/rules.d/80-fpga-ftdi.rules") 

150 

151 # Serial rules files paths 

152 serial_rules_local_path = resources_dir / "80-fpga-serial.rules" 

153 serial_rules_system_path = Path("/etc/udev/rules.d/80-fpga-serial.rules") 

154 

155 # Driver to restore: mac os 

156 driver_c = "" 

157 

158 def __init__(self, apio_ctx: ApioContext) -> None: 

159 

160 self.apio_ctx = apio_ctx 

161 

162 def ftdi_install(self) -> int: 

163 """Installs the FTDI driver. Function is platform dependent. 

164 Returns a process exit code. 

165 """ 

166 

167 if self.apio_ctx.is_linux: 

168 return self._ftdi_install_linux() 

169 

170 if self.apio_ctx.is_darwin: 

171 return self._ftdi_install_darwin() 

172 

173 if self.apio_ctx.is_windows: 

174 return self._ftdi_install_windows() 

175 

176 cerror(f"Unknown platform type '{self.apio_ctx.platform_id}'.") 

177 return 1 

178 

179 def ftdi_uninstall(self) -> int: 

180 """Uninstalls the FTDI driver. Function is platform dependent. 

181 Returns a process exit code. 

182 """ 

183 if self.apio_ctx.is_linux: 

184 return self._ftdi_uninstall_linux() 

185 

186 if self.apio_ctx.is_darwin: 

187 return self._ftdi_uninstall_darwin() 

188 

189 if self.apio_ctx.is_windows: 

190 return self._ftdi_uninstall_windows() 

191 

192 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

193 return 1 

194 

195 def serial_install(self) -> int: 

196 """Installs the serial driver. Function is platform dependent. 

197 Returns a process exit code. 

198 """ 

199 

200 if self.apio_ctx.is_linux: 

201 return self._serial_install_linux() 

202 

203 if self.apio_ctx.is_darwin: 

204 return self._serial_install_darwin() 

205 

206 if self.apio_ctx.is_windows: 

207 return self._serial_install_windows() 

208 

209 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

210 return 1 

211 

212 def serial_uninstall(self) -> int: 

213 """Uninstalls the serial driver. Function is platform dependent. 

214 Returns a process exit code. 

215 """ 

216 if self.apio_ctx.is_linux: 

217 return self._serial_uninstall_linux() 

218 

219 if self.apio_ctx.is_darwin: 

220 return self._serial_uninstall_darwin() 

221 

222 if self.apio_ctx.is_windows: 

223 return self._serial_uninstall_windows() 

224 

225 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

226 return 1 

227 

228 def _ftdi_install_linux(self) -> int: 

229 """Drivers install on Linux. It copies the .rules file into 

230 the corresponding folder. Return process exit code.""" 

231 

232 cout("Configure FTDI drivers for FPGA") 

233 

234 # -- Check if the target rules file already exists 

235 if not self.ftdi_rules_system_path.exists(): 

236 

237 # -- Copy the rules file and reload udev, all in ONE sudo 

238 # -- invocation (a single password prompt). 

239 steps = [ 

240 ( 

241 "cp " 

242 f"{shlex.quote(str(self.ftdi_rules_local_path))} " 

243 f"{shlex.quote(str(self.ftdi_rules_system_path))}", 

244 "install the FTDI udev rules file", 

245 ), 

246 ] + self._udev_reload_steps() 

247 exit_code = self._sudo_steps_linux(steps) 

248 if exit_code != 0: 

249 return exit_code 

250 

251 cout("FTDI drivers installed", style=SUCCESS) 

252 cout("Unplug and reconnect your board", style=INFO) 

253 else: 

254 cout("Already installed", style=INFO) 

255 

256 return 0 

257 

258 def _ftdi_uninstall_linux(self): 

259 """Uninstall the FTDI drivers on linux. Returns process exist code.""" 

260 

261 # -- For disabling the FTDI driver the .rules files should be 

262 # -- removed from the /etc/udev/rules.d/ folder 

263 

264 # -- Remove the .rules file, if it exists 

265 if self.ftdi_rules_system_path.exists(): 

266 cout("Revert FTDI drivers configuration") 

267 

268 # -- Remove the rules file and reload udev in ONE sudo call. 

269 steps = [ 

270 ( 

271 f"rm {shlex.quote(str(self.ftdi_rules_system_path))}", 

272 "remove the FTDI udev rules file", 

273 ), 

274 ] + self._udev_reload_steps() 

275 exit_code = self._sudo_steps_linux(steps) 

276 if exit_code != 0: 

277 return exit_code 

278 

279 cout("FTDI drivers uninstalled", style=SUCCESS) 

280 cout("Unplug and reconnect your board", style=INFO) 

281 else: 

282 cout("Already uninstalled", style=INFO) 

283 

284 return 0 

285 

286 def _serial_install_linux(self): 

287 """Serial drivers install on Linux. Returns process exit code.""" 

288 

289 cout("Configure Serial drivers for FPGA") 

290 

291 # -- Check if the target rules file already exists 

292 if not self.serial_rules_system_path.exists(): 

293 steps = [] 

294 

295 # -- Add the user to the dialout group for having access to the 

296 # -- serial port, if not a member yet. 

297 group_added = self._needs_dialout_group_linux() 

298 if group_added: 

299 steps.append( 

300 ( 

301 "usermod -a -G dialout " 

302 f"{shlex.quote(getpass.getuser())}", 

303 "add the user to the dialout group", 

304 ) 

305 ) 

306 

307 # -- Copy the rules file and reload udev; everything runs in 

308 # -- ONE sudo invocation (a single password prompt). 

309 steps += [ 

310 ( 

311 "cp " 

312 f"{shlex.quote(str(self.serial_rules_local_path))} " 

313 f"{shlex.quote(str(self.serial_rules_system_path))}", 

314 "install the serial udev rules file", 

315 ), 

316 ] + self._udev_reload_steps() 

317 exit_code = self._sudo_steps_linux(steps) 

318 if exit_code != 0: 

319 return exit_code 

320 

321 cout("Serial drivers installed", style=SUCCESS) 

322 cout("Unplug and reconnect your board", style=INFO) 

323 if group_added: 

324 cout( 

325 "Restart your machine to install the dialout group", 

326 style=INFO, 

327 ) 

328 else: 

329 cout("Already installed", style=INFO) 

330 

331 return 0 

332 

333 def _serial_uninstall_linux(self) -> int: 

334 """Uninstall the serial driver on Linux. Return process exit code.""" 

335 

336 # -- For disabling the serial driver the corresponding .rules file 

337 # -- should be removed, it it exists 

338 if self.serial_rules_system_path.exists(): 

339 cout("Revert Serial drivers configuration") 

340 

341 # -- Remove the rules file and reload udev in ONE sudo call. 

342 steps = [ 

343 ( 

344 f"rm {shlex.quote(str(self.serial_rules_system_path))}", 

345 "remove the serial udev rules file", 

346 ), 

347 ] + self._udev_reload_steps() 

348 exit_code = self._sudo_steps_linux(steps) 

349 if exit_code != 0: 

350 return exit_code 

351 

352 cout("Serial drivers uninstalled", style=SUCCESS) 

353 cout("Unplug and reconnect your board", style=INFO) 

354 else: 

355 cout("Already uninstalled", style=INFO) 

356 

357 return 0 

358 

359 # -- Exit code of the first step of a _sudo_steps_linux() script; the 

360 # -- following steps use consecutive codes. High enough to not collide 

361 # -- with sudo's own exit codes (1 = auth failure). 

362 _FIRST_STEP_EXIT_CODE = 10 

363 

364 def _sudo_steps_linux(self, steps) -> int: 

365 """Run the given root steps as a SINGLE sudo invocation, so the 

366 user is prompted for the password at most once. 'steps' is a list 

367 of (shell_command, action_description) tuples; each command gets a 

368 distinct exit code so a failure is reported precisely (their 

369 stderr also reaches the console). Returns the process exit code, 

370 0 on success.""" 

371 

372 cout( 

373 "This one-time setup needs administrator privileges " 

374 "(a single sudo prompt)", 

375 style=INFO, 

376 ) 

377 

378 # -- Build 'cmd1 || exit 10; cmd2 || exit 11; ...' 

379 script = "; ".join( 

380 f"{cmd} || exit {self._FIRST_STEP_EXIT_CODE + i}" 

381 for i, (cmd, _) in enumerate(steps) 

382 ) 

383 

384 # -- Honor a graphical askpass helper when the caller provides one 

385 # -- (SUDO_ASKPASS): GUI launchers like Icestudio spawn apio without 

386 # -- an interactive terminal, so sudo cannot prompt on a tty; with 

387 # -- -A it asks through the helper instead (issue #899). Terminal 

388 # -- users without SUDO_ASKPASS keep the classic tty prompt (-A 

389 # -- without a helper would fail instead of prompting). 

390 sudo_cmd = ["sudo"] 

391 if os.environ.get("SUDO_ASKPASS"): 

392 sudo_cmd.append("-A") 

393 

394 exit_code = subprocess.call(sudo_cmd + ["sh", "-c", script]) 

395 if exit_code == 0: 

396 return 0 

397 

398 # -- Map the exit code back to the step that failed. 

399 step = exit_code - self._FIRST_STEP_EXIT_CODE 

400 if 0 <= step < len(steps): 

401 cerror(f"Failed to {steps[step][1]}.") 

402 else: 

403 # -- sudo itself failed (wrong password, no sudo rights, ...) 

404 cerror("Could not get administrator privileges (sudo failed).") 

405 return exit_code 

406 

407 def _udev_reload_steps(self): 

408 """The root steps for reloading the udev rules, for 

409 _sudo_steps_linux(). Restarting the udev daemon is NOT needed for 

410 rule changes and the legacy unit name it used ('udev') only exists 

411 on distros with the Debian/Ubuntu compat alias (issue #899 on 

412 other distros: "Failed to restart udev.service: Unit udev.service 

413 not found").""" 

414 

415 return [ 

416 ("udevadm control --reload-rules", "reload the udev rules"), 

417 ( 

418 "udevadm trigger", 

419 "apply the udev rules to the connected devices", 

420 ), 

421 ] 

422 

423 def _needs_dialout_group_linux(self): 

424 """True if the user must be added to the dialout group (needed for 

425 access to the serial port).""" 

426 

427 # -- Get the current groups of the user 

428 groups = subprocess.check_output("groups") 

429 

430 # -- True if it does not belong to the dialout group yet. 

431 return "dialout" not in groups.decode() 

432 

433 def _ftdi_install_darwin(self) -> int: 

434 """Installs FTDI driver on darwin. Returns process status code.""" 

435 # Check homebrew 

436 cout(NO_DRIVERS_MSG, style=SUCCESS) 

437 return 0 

438 

439 def _ftdi_uninstall_darwin(self): 

440 """Uninstalls FTDI driver on darwin. Returns process status code.""" 

441 cout(NO_DRIVERS_MSG, style=SUCCESS) 

442 return 0 

443 

444 def _serial_install_darwin(self): 

445 """Installs serial driver on darwin. Returns process status code.""" 

446 cout(NO_DRIVERS_MSG, style=SUCCESS) 

447 return 0 

448 

449 def _serial_uninstall_darwin(self): 

450 """Uninstalls serial driver on darwin. Returns process status code.""" 

451 cout(NO_DRIVERS_MSG, style=SUCCESS) 

452 return 0 

453 

454 def _ftdi_install_windows(self) -> int: 

455 

456 # -- Get the drivers apio package base folder 

457 drivers_base_dir = self.apio_ctx.get_package_dir("drivers") 

458 

459 # NOTE: Zadig documentation: 

460 # https://github.com/pbatard/libwdi/wiki/Zadig?utm_source=chatgpt.com 

461 

462 # -- Path to the config file zadig.ini. 

463 zadig_ini_src = drivers_base_dir / "share" / "zadig.ini" 

464 

465 # -- Execute in a tmp directory, this way we don't contaminate the 

466 # -- current with zadig.ini, in case the program crashes. 

467 # -- Using a fix tmp location prevents accumulation of leftover 

468 # -- zadig.ini in case the are not cleaned up properly. 

469 # -- We can't store zadig under _build since we don't necessarily 

470 # -- run in a context of a project.. 

471 with util.pushd(self.apio_ctx.get_tmp_dir()): 

472 # -- Bring a copy of zadig.ini 

473 shutil.copyfile(zadig_ini_src, "zadig.ini") 

474 

475 # -- Zadig exe file with full path: 

476 zadig_exe = drivers_base_dir / "bin" / "zadig.exe" 

477 

478 # -- Show messages for the user 

479 cout("", "Launching zadig.exe.") 

480 cmarkdown(FTDI_INSTALL_INSTRUCTIONS_WINDOWS) 

481 

482 # -- Execute zadig! 

483 # -- We execute it using os.system() rather than by 

484 # -- util.exec_command() because zadig required permissions 

485 # -- elevation. 

486 exit_code = os.system(str(zadig_exe)) 

487 

488 # -- All done. 

489 return exit_code 

490 

491 def _ftdi_uninstall_windows(self) -> int: 

492 # -- Check that the required packages exist. 

493 # packages.install_missing_packages_on_the_fly( 

494 # self.apio_ctx.packages_context 

495 # ) 

496 

497 cout("", "Launching the interactive Device Manager.") 

498 cmarkdown(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS) 

499 

500 # -- We launch the device manager using os.system() rather than with 

501 # -- util.exec_command() because util.exec_command() does not support 

502 # -- elevation. 

503 exit_code = os.system("mmc devmgmt.msc") 

504 return exit_code 

505 

506 def _serial_install_windows(self) -> int: 

507 

508 drivers_base_dir = self.apio_ctx.get_package_dir("drivers") 

509 drivers_bin_dir = drivers_base_dir / "bin" 

510 

511 cout("", "Launching the interactive Serial Installer.") 

512 cmarkdown(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS) 

513 

514 # -- We launch the device manager using os.system() rather than with 

515 # -- util.exec_command() because util.exec_command() does not support 

516 # -- elevation. 

517 exit_code = os.system( 

518 str(Path(drivers_bin_dir) / "serial_install.exe") 

519 ) 

520 

521 return exit_code 

522 

523 def _serial_uninstall_windows(self) -> int: 

524 

525 cout("", "Launching the interactive Device Manager.") 

526 cmarkdown(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS) 

527 

528 # -- We launch the device manager using os.system() rather than with 

529 # -- util.exec_command() because util.exec_command() does not support 

530 # -- elevation. 

531 exit_code = os.system("mmc devmgmt.msc") 

532 return exit_code