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