Coverage for apio/managers/drivers.py: 50%
169 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Manage board drivers"""
11import getpass
12import os
13import shlex
14import shutil
15import subprocess
16from pathlib import Path
17from apio.utils import util
18from apio.common.apio_console import cout, cerror, cmarkdown, fatal_error
19from apio.common.apio_styles import INFO, SUCCESS, EMPH1, EMPH3
20from apio.apio_context import ApioContext
22# -- Style shortcuts
23E1 = f"[{EMPH1}]"
24E3 = f"[{EMPH3}]"
26# -- A message to print when trying to install/uninstall apio drivers
27# -- on platforms that don't require it.
28NO_DRIVERS_MSG = "No driver installation is required on this platform."
30# -- Text in the rich-text format of the python rich library.
31FTDI_INSTALL_INSTRUCTIONS_WINDOWS = f"""
32{E3}Please follow these steps:[/]
34 1. Make sure your {E1}FPGA board is connected[/] to the computer.
36 2. {E1}Accept the Zadig request[/] to make changes to your computer.
38 3. {E1}Find the Zadig window[/] on your screen. You may need to click
39 on its icon in the task bar for it to appear.
41 4. {E1}Select your FPGA board[/] from the drop down list, For example
42 'Alhambra II v1.0A - B09-335 (Interface 0)'.
44 {E3}VERY IMPORTANT - If your board appears multiple time, make sure
45 to select its 'interface 0' entry.[/]
47 5. {E1}Select the 'WinUSB' driver[/] as the target driver. For example
48 'WinUSB (v6.1.7600.16385)'.
50 6. {E1}Click 'Replace Driver'[/] and wait for a successful
51 completion, this can take a minute or two.
53 7. {E1}Close the Zadig window.[/]
55 8. {E1}Disconnect and reconnect[/] your FPGA board for the new driver
56 to take affect.
58 9. {E1}Run the command 'apio devices scan-usb'[/] and verify that
59 your board is listed.
60"""
62# -- Text in the rich-text format of the python rich library.
63FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS = f"""
64{E3}Please follow these steps:[/]
66 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer.
68 2. If asked, {E1}allow the Device Manager to make changes to your system.[/]
70 3. {E1}Find the Device Manager window.[/]
72 4. {E1}Connect the board[/] to your computer and a new entry will be added
73 to the device list (though sometimes it may be collapsed and
74 hidden).
76 5. {E1}Identify the entry of your board[/] (e.g. in the 'Universal Serial
77 Bus Devices' section).
79 {E3}NOTE: Boards with FT2232 ICs have two channels, 'interface 0'
80 and 'interface 1'. Here we care only about 'interface 0' and
81 ignore 'interface 1' if it appears as a COM port.[/]
83 6. {E1}Right click[/] on your board entry and \
84{E1}select 'Uninstall device'.[/]
86 7. If available, check the box {E1}'Delete the driver software for this
87 device'.[/]
89 8. Click the {E1}'Uninstall' button[/].
91 9. {E1}Close[/] the Device Manager window.
92"""
94# -- Text in the rich-text format of the python rich library.
95SERIAL_INSTALL_INSTRUCTIONS_WINDOWS = f"""
96{E3}Please follow these steps:[/]
98 1. Make sure your FPGA {E1}board is connected[/] to the computer.
100 2. {E1}Accept the Serial Installer request[/] to make changes to your \
101computer.
103 3. Find the Serial installer window and {E1}follow the instructions.[/]
105 4. To verify, {E1}disconnect and reconnect the board[/] and run the command
106 {E1}'apio devices scan-serial'.[/]
107"""
109# -- Text in the rich-text format of the python rich library.
110SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS = f"""
111{E3}Please follow these steps:[/]
113 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer.
115 2. If asked, {E1}allow the Device Manager to make changes[/] to your system.
117 3. {E1}Find the Device Manager window.[/]
119 4. {E1}Connect the board[/] to your computer and a new entry will be added
120 to the device list (though sometimes it may be collapsed).
122 5. {E1}Identify the entry of your board[/] (typically in the Ports section).
124 {E3} NOTE: If your board does not show up as a COM port, it may not
125 have the 'apio drivers --serial-install' applied to it.[/]
127 6. {E1}Right click[/] on your board entry \
128and {E1}select 'Uninstall device'.[/]
130 7. If available, check the box \
131{E1}'Delete the driver software for this device'.[/]
133 8. Click the {E1}'Uninstall' button.[/]
135 9. {E1}Close the Device Manager window.[/]
136"""
139class Drivers:
140 """Class for managing the board drivers"""
142 # -- The driver installation on linux consist of copying the rule files
143 # -- to the /etc/udev/rules.d folder
145 # -- FTDI source rules file paths
146 resources_dir = util.get_path_in_apio_package("resources")
147 ftdi_rules_local_path = resources_dir / "80-fpga-ftdi.rules"
149 # -- Target rule file
150 ftdi_rules_system_path = Path("/etc/udev/rules.d/80-fpga-ftdi.rules")
152 # Serial rules files paths
153 serial_rules_local_path = resources_dir / "80-fpga-serial.rules"
154 serial_rules_system_path = Path("/etc/udev/rules.d/80-fpga-serial.rules")
156 # Driver to restore: mac os
157 driver_c = ""
159 def __init__(self, apio_ctx: ApioContext) -> None:
161 self.apio_ctx = apio_ctx
163 def ftdi_install(self):
164 """Installs the FTDI driver. Function is platform dependent."""
166 if self.apio_ctx.is_linux: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true
167 self._ftdi_install_linux()
168 elif self.apio_ctx.is_darwin:
169 self._ftdi_install_darwin()
170 elif self.apio_ctx.is_windows:
171 self._ftdi_install_windows()
172 else:
173 fatal_error(
174 f"Unexpected platform type '{self.apio_ctx.platform.id}'."
175 )
177 def ftdi_uninstall(self):
178 """Uninstalls the FTDI driver. Function is platform dependent."""
179 if self.apio_ctx.is_linux: 179 ↛ 181line 179 didn't jump to line 181 because the condition on line 179 was always true
180 self._ftdi_uninstall_linux()
181 elif self.apio_ctx.is_darwin:
182 self._ftdi_uninstall_darwin()
183 elif self.apio_ctx.is_windows:
184 self._ftdi_uninstall_windows()
185 else:
186 fatal_error(f"Unexpected platform '{self.apio_ctx.platform.id}'.")
188 def serial_install(self):
189 """Installs the serial driver. Function is platform dependent."""
191 if self.apio_ctx.is_linux: 191 ↛ 193line 191 didn't jump to line 193 because the condition on line 191 was always true
192 self._serial_install_linux()
193 elif self.apio_ctx.is_darwin:
194 self._serial_install_darwin()
195 elif self.apio_ctx.is_windows:
196 self._serial_install_windows()
197 else:
198 fatal_error(f"Unexpected platform '{self.apio_ctx.platform.id}'.")
200 def serial_uninstall(self):
201 """Uninstalls the serial driver. Function is platform dependent."""
203 if self.apio_ctx.is_linux: 203 ↛ 205line 203 didn't jump to line 205 because the condition on line 203 was always true
204 self._serial_uninstall_linux()
205 elif self.apio_ctx.is_darwin:
206 self._serial_uninstall_darwin()
208 elif self.apio_ctx.is_windows:
209 self._serial_uninstall_windows()
210 else:
211 fatal_error(f"Unknown platform '{self.apio_ctx.platform.id}'.")
213 def _ftdi_install_linux(self):
214 """Drivers install on Linux. It copies the .rules file into
215 the corresponding folder."""
217 cout("Configure FTDI drivers for FPGA")
219 # -- Check if the target rules file already exists
220 if self.ftdi_rules_system_path.exists(): 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true
221 cout("Already installed", style=INFO)
222 return
224 # -- here when the driver is not already installed.
225 # -- Copy the rules file and reload udev, all in ONE sudo invocation
226 # -- (a single password prompt).
227 steps = [
228 (
229 "cp "
230 f"{shlex.quote(str(self.ftdi_rules_local_path))} "
231 f"{shlex.quote(str(self.ftdi_rules_system_path))}",
232 "install the FTDI udev rules file",
233 ),
234 ] + self._udev_reload_steps()
236 exit_code = self._sudo_steps_linux(steps)
238 if exit_code != 0: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true
239 fatal_error("Failed to install FTDI drivers.")
241 # -- All done ok.
242 cout("FTDI drivers installed", style=SUCCESS)
243 cout("Unplug and reconnect your board", style=INFO)
245 def _ftdi_uninstall_linux(self):
246 """Uninstall the FTDI drivers on linux. Returns process exist code."""
248 # -- For disabling the FTDI driver the .rules files should be
249 # -- removed from the /etc/udev/rules.d/ folder
251 # -- Remove the .rules file, if it exists
252 if not self.ftdi_rules_system_path.exists(): 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true
253 cout("Already uninstalled", style=INFO)
254 return
256 # -- Here when need to uninstall
257 cout("Revert FTDI drivers configuration")
259 # -- Remove the rules file and reload udev in ONE sudo call.
260 steps = [
261 (
262 f"rm {shlex.quote(str(self.ftdi_rules_system_path))}",
263 "remove the FTDI udev rules file",
264 ),
265 ] + self._udev_reload_steps()
267 exit_code = self._sudo_steps_linux(steps)
269 if exit_code != 0: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true
270 fatal_error("Failed to uninstall FTDI drivers")
272 cout("FTDI drivers uninstalled", style=SUCCESS)
273 cout("Unplug and reconnect your board", style=INFO)
275 def _serial_install_linux(self):
276 """Serial drivers install on Linux."""
278 cout("Configure Serial drivers for FPGA")
280 # -- Check if the target rules file already exists
281 if self.serial_rules_system_path.exists(): 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true
282 cout("Already installed", style=INFO)
283 return
285 # -- Here when need to install.
286 steps = []
288 # -- Add the user to the dialout group for having access to the
289 # -- serial port, if not a member yet.
290 group_added = self._needs_dialout_group_linux()
291 if group_added: 291 ↛ 302line 291 didn't jump to line 302 because the condition on line 291 was always true
292 steps.append(
293 (
294 "usermod -a -G dialout "
295 f"{shlex.quote(getpass.getuser())}",
296 "add the user to the dialout group",
297 )
298 )
300 # -- Copy the rules file and reload udev; everything runs in
301 # -- ONE sudo invocation (a single password prompt).
302 steps += [
303 (
304 "cp "
305 f"{shlex.quote(str(self.serial_rules_local_path))} "
306 f"{shlex.quote(str(self.serial_rules_system_path))}",
307 "install the serial udev rules file",
308 ),
309 ] + self._udev_reload_steps()
311 exit_code = self._sudo_steps_linux(steps)
313 if exit_code != 0: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true
314 fatal_error("Serial drivers installation failed.")
316 cout("Serial drivers installed", style=SUCCESS)
317 cout("Unplug and reconnect your board", style=INFO)
318 if group_added: 318 ↛ exitline 318 didn't return from function '_serial_install_linux' because the condition on line 318 was always true
319 cout(
320 "Restart your machine to install the dialout group",
321 style=INFO,
322 )
324 def _serial_uninstall_linux(self):
325 """Uninstall the serial driver on Linux."""
327 # -- Do noting if not installed.
328 if not self.serial_rules_system_path.exists(): 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true
329 cout("Already uninstalled", style=INFO)
331 # -- For disabling the serial driver the corresponding .rules file
332 # -- should be removed, it it exists
333 cout("Revert Serial drivers configuration")
335 # -- Remove the rules file and reload udev in ONE sudo call.
336 steps = [
337 (
338 f"rm {shlex.quote(str(self.serial_rules_system_path))}",
339 "remove the serial udev rules file",
340 ),
341 ] + self._udev_reload_steps()
343 exit_code = self._sudo_steps_linux(steps)
345 if exit_code != 0: 345 ↛ 346line 345 didn't jump to line 346 because the condition on line 345 was never true
346 fatal_error("Serial drivers uninstallation failed")
348 cout("Serial drivers uninstalled", style=SUCCESS)
349 cout("Unplug and reconnect your board", style=INFO)
351 # -- Exit code of the first step of a _sudo_steps_linux() script; the
352 # -- following steps use consecutive codes. High enough to not collide
353 # -- with sudo's own exit codes (1 = auth failure).
354 _FIRST_STEP_EXIT_CODE = 10
356 def _sudo_steps_linux(self, steps) -> int:
357 """Run the given root steps as a SINGLE sudo invocation, so the
358 user is prompted for the password at most once. 'steps' is a list
359 of (shell_command, action_description) tuples; each command gets a
360 distinct exit code so a failure is reported precisely (their
361 stderr also reaches the console). Returns the process exit code,
362 0 on success."""
364 cout(
365 "This one-time setup needs administrator privileges "
366 "(a single sudo prompt)",
367 style=INFO,
368 )
370 # -- Build 'cmd1 || exit 10; cmd2 || exit 11; ...'
371 script = "; ".join(
372 f"{cmd} || exit {self._FIRST_STEP_EXIT_CODE + i}"
373 for i, (cmd, _) in enumerate(steps)
374 )
376 # -- Honor a graphical askpass helper when the caller provides one
377 # -- (SUDO_ASKPASS): GUI launchers like Icestudio spawn apio without
378 # -- an interactive terminal, so sudo cannot prompt on a tty; with
379 # -- -A it asks through the helper instead (issue #899). Terminal
380 # -- users without SUDO_ASKPASS keep the classic tty prompt (-A
381 # -- without a helper would fail instead of prompting).
382 sudo_cmd = ["sudo"]
383 if os.environ.get("SUDO_ASKPASS"): 383 ↛ 384line 383 didn't jump to line 384 because the condition on line 383 was never true
384 sudo_cmd.append("-A")
386 exit_code = subprocess.call(sudo_cmd + ["sh", "-c", script])
387 if exit_code == 0: 387 ↛ 391line 387 didn't jump to line 391 because the condition on line 387 was always true
388 return 0
390 # -- Map the exit code back to the step that failed.
391 step = exit_code - self._FIRST_STEP_EXIT_CODE
392 if 0 <= step < len(steps):
393 cerror(f"Failed to {steps[step][1]}.")
394 else:
395 # -- sudo itself failed (wrong password, no sudo rights, ...)
396 cerror(
397 "Could not get administrator privileges "
398 f"(sudo failed, exit_code={exit_code}).",
399 )
400 return exit_code
402 def _udev_reload_steps(self):
403 """The root steps for reloading the udev rules, for
404 _sudo_steps_linux(). Restarting the udev daemon is NOT needed for
405 rule changes and the legacy unit name it used ('udev') only exists
406 on distros with the Debian/Ubuntu compat alias (issue #899 on
407 other distros: "Failed to restart udev.service: Unit udev.service
408 not found")."""
410 return [
411 ("udevadm control --reload-rules", "reload the udev rules"),
412 (
413 "udevadm trigger",
414 "apply the udev rules to the connected devices",
415 ),
416 ]
418 def _needs_dialout_group_linux(self):
419 """True if the user must be added to the dialout group (needed for
420 access to the serial port)."""
422 # -- Get the current groups of the user
423 groups = subprocess.check_output("groups")
425 # -- True if it does not belong to the dialout group yet.
426 return "dialout" not in groups.decode()
428 def _ftdi_install_darwin(self):
429 """Installs FTDI driver on darwin. Returns process status code."""
430 # Check homebrew
431 cout(NO_DRIVERS_MSG, style=SUCCESS)
433 def _ftdi_uninstall_darwin(self):
434 """Uninstalls FTDI driver on darwin. Returns process status code."""
435 cout(NO_DRIVERS_MSG, style=SUCCESS)
437 def _serial_install_darwin(self):
438 """Installs serial driver on darwin. Returns process status code."""
439 cout(NO_DRIVERS_MSG, style=SUCCESS)
441 def _serial_uninstall_darwin(self):
442 """Uninstalls serial driver on darwin. Returns process status code."""
443 cout(NO_DRIVERS_MSG, style=SUCCESS)
445 def _ftdi_install_windows(self):
447 # -- Get the drivers apio package base folder
448 drivers_base_dir = self.apio_ctx.get_package_dir("drivers")
450 # NOTE: Zadig documentation:
451 # https://github.com/pbatard/libwdi/wiki/Zadig?utm_source=chatgpt.com
453 # -- Path to the config file zadig.ini.
454 zadig_ini_src = drivers_base_dir / "share" / "zadig.ini"
456 # -- Execute in a tmp directory, this way we don't contaminate the
457 # -- current with zadig.ini, in case the program crashes.
458 # -- Using a fix tmp location prevents accumulation of leftover
459 # -- zadig.ini in case the are not cleaned up properly.
460 # -- We can't store zadig under _build since we don't necessarily
461 # -- run in a context of a project..
462 with util.pushd(self.apio_ctx.get_tmp_dir()):
463 # -- Bring a copy of zadig.ini
464 shutil.copyfile(zadig_ini_src, "zadig.ini")
466 # -- Zadig exe file with full path:
467 zadig_abs_path = drivers_base_dir / "bin" / "zadig.exe"
468 assert zadig_abs_path.is_absolute(), zadig_abs_path
470 # -- Show messages for the user
471 cout("", "Launching zadig.exe.")
472 cmarkdown(FTDI_INSTALL_INSTRUCTIONS_WINDOWS)
474 # -- Execute Zadig.
475 # -- We execute it using a shell, rather than by
476 # -- util.exec_command() because zadig required permissions
477 # -- elevation.
478 exit_code = subprocess.call(str(zadig_abs_path), shell=True)
480 if exit_code != 0:
481 fatal_error("Zadig failed")
483 def _ftdi_uninstall_windows(self) -> None:
485 cout("", "Launching the interactive Device Manager.")
486 cmarkdown(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS)
488 # -- We launch the device manager using os.system() rather than with
489 # -- util.exec_command() because util.exec_command() does not support
490 # -- elevation.
491 exit_code = subprocess.call("mmc devmgmt.msc", shell=True)
493 if exit_code != 0:
494 fatal_error("Device Manager invocation failed.")
496 def _serial_install_windows(self):
497 """Install serial drivers on windows."""
499 drivers_base_dir = self.apio_ctx.get_package_dir("drivers")
500 drivers_bin_dir = drivers_base_dir / "bin"
502 cout("", "Launching the interactive Serial Installer.")
503 cmarkdown(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS)
505 # -- We launch the device manager using shell rather than with
506 # -- util.exec_command() because util.exec_command() does not support
507 # -- elevation.
509 serial_installer_abs_path = drivers_bin_dir / "serial_install.exe"
510 assert serial_installer_abs_path.is_absolute, serial_installer_abs_path
512 exit_code = subprocess.call(str(serial_installer_abs_path), shell=True)
514 if exit_code != 0:
515 fatal_error("Interactive Serial Installer failed.")
517 def _serial_uninstall_windows(self):
518 """Uninstall serial drivers on Windows"""
520 cout("", "Launching the interactive Device Manager.")
521 cmarkdown(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS)
523 # -- We launch the device manager using os.system() rather than with
524 # -- util.exec_command() because util.exec_command() does not support
525 # -- elevation.
527 exit_code = subprocess.call("mmc devmgmt.msc", shell=True)
529 if exit_code != 0:
530 fatal_error("The interactive Device Manager failed.")