2020-01-24 20:31:16 +01:00
""" QMK Doctor
2019-07-15 21:14:27 +02:00
2020-01-24 20:31:16 +01:00
Check out the user ' s QMK environment and make sure it ' s ready to compile .
2019-07-15 21:14:27 +02:00
"""
2019-08-22 08:40:24 +02:00
import platform
2020-06-18 20:37:47 +02:00
import re
2019-08-22 08:40:24 +02:00
import shutil
import subprocess
2020-01-24 20:31:16 +01:00
from pathlib import Path
2020-11-16 22:09:32 +01:00
from enum import Enum
2019-07-15 21:14:27 +02:00
from milc import cli
2020-11-28 21:02:18 +01:00
from milc . questions import yesno
2020-01-24 20:31:16 +01:00
from qmk import submodules
2020-09-24 07:16:00 +02:00
from qmk . constants import QMK_FIRMWARE
2020-03-29 14:29:44 +02:00
from qmk . commands import run
2020-01-24 20:31:16 +01:00
2020-11-16 22:09:32 +01:00
class CheckStatus ( Enum ) :
OK = 1
WARNING = 2
ERROR = 3
2020-03-08 17:21:45 +01:00
ESSENTIAL_BINARIES = {
' dfu-programmer ' : { } ,
2020-04-04 20:18:31 +02:00
' avrdude ' : { } ,
2020-03-08 17:21:45 +01:00
' dfu-util ' : { } ,
2020-04-03 01:46:26 +02:00
' avr-gcc ' : {
' version_arg ' : ' -dumpversion '
} ,
' arm-none-eabi-gcc ' : {
' version_arg ' : ' -dumpversion '
} ,
2020-03-08 17:21:45 +01:00
' bin/qmk ' : { } ,
}
2019-07-15 21:14:27 +02:00
2020-01-11 21:15:28 +01:00
2020-05-12 21:59:09 +02:00
def _udev_rule ( vid , pid = None , * args ) :
2019-11-07 19:53:03 +01:00
""" Helper function that return udev rules
"""
2020-05-12 21:59:09 +02:00
rule = " "
if pid :
2020-11-16 22:09:32 +01:00
rule = ' SUBSYSTEMS== " usb " , ATTRS {idVendor} == " %s " , ATTRS {idProduct} == " %s " , TAG+= " uaccess " ' % (
vid ,
pid ,
)
2020-05-12 21:59:09 +02:00
else :
2020-11-16 22:09:32 +01:00
rule = ' SUBSYSTEMS== " usb " , ATTRS {idVendor} == " %s " , TAG+= " uaccess " ' % vid
2020-05-12 21:59:09 +02:00
if args :
rule = ' , ' . join ( [ rule , * args ] )
return rule
def _deprecated_udev_rule ( vid , pid = None ) :
""" Helper function that return udev rules
Note : these are no longer the recommended rules , this is just used to check for them
"""
2019-11-07 19:53:03 +01:00
if pid :
return ' SUBSYSTEMS== " usb " , ATTRS {idVendor} == " %s " , ATTRS {idProduct} == " %s " , MODE:= " 0666 " ' % ( vid , pid )
else :
return ' SUBSYSTEMS== " usb " , ATTRS {idVendor} == " %s " , MODE:= " 0666 " ' % vid
2019-07-15 21:14:27 +02:00
2020-01-11 21:15:28 +01:00
2020-06-18 20:37:47 +02:00
def parse_gcc_version ( version ) :
m = re . match ( r " ( \ d+)(?: \ .( \ d+))?(?: \ .( \ d+))? " , version )
return {
' major ' : int ( m . group ( 1 ) ) ,
' minor ' : int ( m . group ( 2 ) ) if m . group ( 2 ) else 0 ,
2020-10-07 02:10:19 +02:00
' patch ' : int ( m . group ( 3 ) ) if m . group ( 3 ) else 0 ,
2020-06-18 20:37:47 +02:00
}
2020-03-08 17:21:45 +01:00
def check_arm_gcc_version ( ) :
""" Returns True if the arm-none-eabi-gcc version is not known to cause problems.
"""
if ' output ' in ESSENTIAL_BINARIES [ ' arm-none-eabi-gcc ' ] :
2020-03-28 04:09:21 +01:00
version_number = ESSENTIAL_BINARIES [ ' arm-none-eabi-gcc ' ] [ ' output ' ] . strip ( )
2020-03-08 17:21:45 +01:00
cli . log . info ( ' Found arm-none-eabi-gcc version %s ' , version_number )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK # Right now all known arm versions are ok
2020-03-08 17:21:45 +01:00
def check_avr_gcc_version ( ) :
""" Returns True if the avr-gcc version is not known to cause problems.
"""
2020-11-16 22:09:32 +01:00
rc = CheckStatus . ERROR
2020-03-08 17:21:45 +01:00
if ' output ' in ESSENTIAL_BINARIES [ ' avr-gcc ' ] :
2020-03-28 04:09:21 +01:00
version_number = ESSENTIAL_BINARIES [ ' avr-gcc ' ] [ ' output ' ] . strip ( )
2020-03-08 17:21:45 +01:00
2020-11-16 22:09:32 +01:00
cli . log . info ( ' Found avr-gcc version %s ' , version_number )
rc = CheckStatus . OK
2020-06-18 20:37:47 +02:00
parsed_version = parse_gcc_version ( version_number )
if parsed_version [ ' major ' ] > 8 :
2020-11-16 22:09:32 +01:00
cli . log . warning ( ' {fg_yellow} We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended. ' )
rc = CheckStatus . WARNING
2020-03-08 17:21:45 +01:00
2020-11-16 22:09:32 +01:00
return rc
2020-03-08 17:21:45 +01:00
2020-04-05 18:11:55 +02:00
def check_avrdude_version ( ) :
if ' output ' in ESSENTIAL_BINARIES [ ' avrdude ' ] :
last_line = ESSENTIAL_BINARIES [ ' avrdude ' ] [ ' output ' ] . split ( ' \n ' ) [ - 2 ]
version_number = last_line . split ( ) [ 2 ] [ : - 1 ]
cli . log . info ( ' Found avrdude version %s ' , version_number )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-04-05 18:11:55 +02:00
def check_dfu_util_version ( ) :
if ' output ' in ESSENTIAL_BINARIES [ ' dfu-util ' ] :
first_line = ESSENTIAL_BINARIES [ ' dfu-util ' ] [ ' output ' ] . split ( ' \n ' ) [ 0 ]
version_number = first_line . split ( ) [ 1 ]
cli . log . info ( ' Found dfu-util version %s ' , version_number )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-04-05 18:11:55 +02:00
def check_dfu_programmer_version ( ) :
if ' output ' in ESSENTIAL_BINARIES [ ' dfu-programmer ' ] :
first_line = ESSENTIAL_BINARIES [ ' dfu-programmer ' ] [ ' output ' ] . split ( ' \n ' ) [ 0 ]
version_number = first_line . split ( ) [ 1 ]
cli . log . info ( ' Found dfu-programmer version %s ' , version_number )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-04-05 18:11:55 +02:00
2020-01-24 20:31:16 +01:00
def check_binaries ( ) :
""" Iterates through ESSENTIAL_BINARIES and tests them.
"""
ok = True
2020-03-08 17:21:45 +01:00
for binary in sorted ( ESSENTIAL_BINARIES ) :
2020-01-24 20:31:16 +01:00
if not is_executable ( binary ) :
ok = False
return ok
def check_submodules ( ) :
""" Iterates through all submodules to make sure they ' re cloned and up to date.
"""
for submodule in submodules . status ( ) . values ( ) :
if submodule [ ' status ' ] is None :
2020-05-21 21:56:43 +02:00
cli . log . error ( ' Submodule %s has not yet been cloned! ' , submodule [ ' name ' ] )
2020-11-16 22:09:32 +01:00
return CheckStatus . ERROR
2020-01-24 20:31:16 +01:00
elif not submodule [ ' status ' ] :
2020-11-16 22:09:32 +01:00
cli . log . warning ( ' Submodule %s is not up to date! ' , submodule [ ' name ' ] )
return CheckStatus . WARNING
2020-01-24 20:31:16 +01:00
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-01-24 20:31:16 +01:00
def check_udev_rules ( ) :
""" Make sure the udev rules look good.
"""
2020-11-16 22:09:32 +01:00
rc = CheckStatus . OK
2020-01-24 20:31:16 +01:00
udev_dir = Path ( " /etc/udev/rules.d/ " )
desired_rules = {
2020-07-12 17:33:43 +02:00
' atmel-dfu ' : {
2020-11-16 22:09:32 +01:00
_udev_rule ( " 03eb " , " 2fef " ) , # ATmega16U2
_udev_rule ( " 03eb " , " 2ff0 " ) , # ATmega32U2
_udev_rule ( " 03eb " , " 2ff3 " ) , # ATmega16U4
_udev_rule ( " 03eb " , " 2ff4 " ) , # ATmega32U4
_udev_rule ( " 03eb " , " 2ff9 " ) , # AT90USB64
_udev_rule ( " 03eb " , " 2ffb " ) # AT90USB128
2020-07-12 17:33:43 +02:00
} ,
2020-11-16 22:09:32 +01:00
' kiibohd ' : { _udev_rule ( " 1c11 " , " b007 " ) } ,
2020-07-12 17:33:43 +02:00
' stm32 ' : {
2020-11-16 22:09:32 +01:00
_udev_rule ( " 1eaf " , " 0003 " ) , # STM32duino
_udev_rule ( " 0483 " , " df11 " ) # STM32 DFU
2020-07-12 17:33:43 +02:00
} ,
2020-11-16 22:09:32 +01:00
' bootloadhid ' : { _udev_rule ( " 16c0 " , " 05df " ) } ,
' usbasploader ' : { _udev_rule ( " 16c0 " , " 05dc " ) } ,
' massdrop ' : { _udev_rule ( " 03eb " , " 6124 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) } ,
2020-05-12 21:59:09 +02:00
' caterina ' : {
2020-09-18 21:40:25 +02:00
# Spark Fun Electronics
2020-11-16 22:09:32 +01:00
_udev_rule ( " 1b4f " , " 9203 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Pro Micro 3V3/8MHz
_udev_rule ( " 1b4f " , " 9205 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Pro Micro 5V/16MHz
_udev_rule ( " 1b4f " , " 9207 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # LilyPad 3V3/8MHz (and some Pro Micro clones)
# Pololu EleCTRONICS
_udev_rule ( " 1ffb " , " 0101 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # A-Star 32U4
2020-09-18 21:40:25 +02:00
# Arduino SA
_udev_rule ( " 2341 " , " 0036 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Leonardo
_udev_rule ( " 2341 " , " 0037 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Micro
2020-11-16 22:09:32 +01:00
# Adafruit INDUSTRIES llC
_udev_rule ( " 239a " , " 000c " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Feather 32U4
_udev_rule ( " 239a " , " 000d " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # ItsyBitsy 32U4 3V3/8MHz
_udev_rule ( " 239a " , " 000e " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # ItsyBitsy 32U4 5V/16MHz
# dog hunter ag
_udev_rule ( " 2a03 " , " 0036 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) , # Leonardo
_udev_rule ( " 2a03 " , " 0037 " , ' ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' ) # Micro
2020-05-12 21:59:09 +02:00
}
}
# These rules are no longer recommended, only use them to check for their presence.
deprecated_rules = {
2020-07-12 17:33:43 +02:00
' atmel-dfu ' : { _deprecated_udev_rule ( " 03eb " , " 2ff4 " ) , _deprecated_udev_rule ( " 03eb " , " 2ffb " ) , _deprecated_udev_rule ( " 03eb " , " 2ff0 " ) } ,
' kiibohd ' : { _deprecated_udev_rule ( " 1c11 " ) } ,
2020-05-12 21:59:09 +02:00
' stm32 ' : { _deprecated_udev_rule ( " 1eaf " , " 0003 " ) , _deprecated_udev_rule ( " 0483 " , " df11 " ) } ,
' bootloadhid ' : { _deprecated_udev_rule ( " 16c0 " , " 05df " ) } ,
2020-07-12 17:26:46 +02:00
' caterina ' : { ' ATTRS {idVendor} == " 2a03 " , ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' , ' ATTRS {idVendor} == " 2341 " , ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' } ,
2020-07-12 17:24:28 +02:00
' tmk ' : { _deprecated_udev_rule ( " feed " ) }
2020-01-24 20:31:16 +01:00
}
if udev_dir . exists ( ) :
2020-04-18 22:00:56 +02:00
udev_rules = [ rule_file for rule_file in udev_dir . glob ( ' *.rules ' ) ]
2020-01-24 20:31:16 +01:00
current_rules = set ( )
# Collect all rules from the config files
for rule_file in udev_rules :
2020-04-18 22:00:56 +02:00
for line in rule_file . read_text ( ) . split ( ' \n ' ) :
line = line . strip ( )
if not line . startswith ( " # " ) and len ( line ) :
current_rules . add ( line )
2020-01-24 20:31:16 +01:00
# Check if the desired rules are among the currently present rules
for bootloader , rules in desired_rules . items ( ) :
if not rules . issubset ( current_rules ) :
2020-05-12 21:59:09 +02:00
deprecated_rule = deprecated_rules . get ( bootloader )
if deprecated_rule and deprecated_rule . issubset ( current_rules ) :
2020-11-16 22:09:32 +01:00
cli . log . warning ( " {fg_yellow} Found old, deprecated udev rules for ' %s ' boards. The new rules on https://docs.qmk.fm/#/faq_build?id=linux-udev-rules offer better security with the same functionality. " , bootloader )
2020-01-24 20:31:16 +01:00
else :
2020-11-16 22:09:32 +01:00
# For caterina, check if ModemManager is running
if bootloader == " caterina " :
if check_modem_manager ( ) :
rc = CheckStatus . WARNING
cli . log . warning ( " {fg_yellow} Detected ModemManager without the necessary udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro. " )
rc = CheckStatus . WARNING
cli . log . warning ( " {fg_yellow} Missing or outdated udev rules for ' %s ' boards. Run ' sudo cp %s /util/udev/50-qmk.rules /etc/udev/rules.d/ ' . " , bootloader , QMK_FIRMWARE )
2020-01-24 20:31:16 +01:00
2020-11-16 22:09:32 +01:00
else :
cli . log . warning ( " {fg_yellow} ' %s ' does not exist. Skipping udev rule checking... " , udev_dir )
return rc
def check_systemd ( ) :
""" Check if it ' s a systemd system
"""
return bool ( shutil . which ( " systemctl " ) )
2020-01-24 20:31:16 +01:00
def check_modem_manager ( ) :
""" Returns True if ModemManager is running.
2020-11-16 22:09:32 +01:00
2020-01-24 20:31:16 +01:00
"""
2020-11-16 22:09:32 +01:00
if check_systemd ( ) :
2020-03-29 14:29:44 +02:00
mm_check = run ( [ " systemctl " , " --quiet " , " is-active " , " ModemManager.service " ] , timeout = 10 )
2020-01-24 20:31:16 +01:00
if mm_check . returncode == 0 :
return True
else :
2020-11-16 22:09:32 +01:00
""" (TODO): Add check for non-systemd systems
"""
return False
2020-01-24 20:31:16 +01:00
def is_executable ( command ) :
""" Returns True if command exists and can be executed.
"""
# Make sure the command is in the path.
res = shutil . which ( command )
if res is None :
cli . log . error ( " {fg_red} Can ' t find %s in your path. " , command )
return False
# Make sure the command can be executed
2020-03-31 02:41:58 +02:00
version_arg = ESSENTIAL_BINARIES [ command ] . get ( ' version_arg ' , ' --version ' )
2020-04-05 18:11:55 +02:00
check = run ( [ command , version_arg ] , stdout = subprocess . PIPE , stderr = subprocess . STDOUT , timeout = 5 , universal_newlines = True )
2020-03-28 04:09:21 +01:00
2020-03-08 17:21:45 +01:00
ESSENTIAL_BINARIES [ command ] [ ' output ' ] = check . stdout
2020-01-24 20:31:16 +01:00
if check . returncode in [ 0 , 1 ] : # Older versions of dfu-programmer exit 1
cli . log . debug ( ' Found {fg_cyan} %s ' , command )
return True
2020-04-14 16:58:00 +02:00
cli . log . error ( " {fg_red} Can ' t run ` %s %s ` " , command , version_arg )
2020-01-24 20:31:16 +01:00
return False
2020-12-19 01:42:30 +01:00
def os_tests ( ) :
""" Determine our OS and run platform specific tests
"""
platform_id = platform . platform ( ) . lower ( )
if ' darwin ' in platform_id or ' macos ' in platform_id :
return os_test_macos ( )
elif ' linux ' in platform_id :
return os_test_linux ( )
elif ' windows ' in platform_id :
return os_test_windows ( )
else :
cli . log . warning ( ' Unsupported OS detected: %s ' , platform_id )
return CheckStatus . WARNING
2020-01-24 20:31:16 +01:00
def os_test_linux ( ) :
""" Run the Linux specific tests.
"""
cli . log . info ( " Detected {fg_cyan} Linux. " )
2020-11-16 22:09:32 +01:00
return check_udev_rules ( )
2020-01-24 20:31:16 +01:00
def os_test_macos ( ) :
""" Run the Mac specific tests.
"""
cli . log . info ( " Detected {fg_cyan} macOS. " )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-01-24 20:31:16 +01:00
def os_test_windows ( ) :
""" Run the Windows specific tests.
"""
cli . log . info ( " Detected {fg_cyan} Windows. " )
2020-11-16 22:09:32 +01:00
return CheckStatus . OK
2020-01-24 20:31:16 +01:00
@cli.argument ( ' -y ' , ' --yes ' , action = ' store_true ' , arg_only = True , help = ' Answer yes to all questions. ' )
@cli.argument ( ' -n ' , ' --no ' , action = ' store_true ' , arg_only = True , help = ' Answer no to all questions. ' )
2019-09-22 22:25:33 +02:00
@cli.subcommand ( ' Basic QMK environment checks ' )
def doctor ( cli ) :
2019-07-15 21:14:27 +02:00
""" Basic QMK environment checks.
This is currently very simple , it just checks that all the expected binaries are on your system .
TODO ( unclaimed ) :
* [ ] Compile a trivial program with each compiler
"""
2019-08-22 18:38:10 +02:00
cli . log . info ( ' QMK Doctor is checking your environment. ' )
2020-12-19 01:42:30 +01:00
status = os_tests ( )
2019-08-22 18:38:10 +02:00
2020-09-24 07:16:00 +02:00
cli . log . info ( ' QMK home: {fg_cyan} %s ' , QMK_FIRMWARE )
2020-01-24 20:31:16 +01:00
# Make sure the basic CLI tools we need are available and can be executed.
bin_ok = check_binaries ( )
if not bin_ok :
if yesno ( ' Would you like to install dependencies? ' , default = True ) :
2020-03-29 14:29:44 +02:00
run ( [ ' util/qmk_install.sh ' ] )
2020-01-24 20:31:16 +01:00
bin_ok = check_binaries ( )
if bin_ok :
cli . log . info ( ' All dependencies are installed. ' )
2019-07-15 21:14:27 +02:00
else :
2020-11-16 22:09:32 +01:00
status = CheckStatus . ERROR
2020-01-24 20:31:16 +01:00
2020-03-08 17:21:45 +01:00
# Make sure the tools are at the correct version
2020-11-16 22:09:32 +01:00
ver_ok = [ ]
2020-04-05 18:11:55 +02:00
for check in ( check_arm_gcc_version , check_avr_gcc_version , check_avrdude_version , check_dfu_util_version , check_dfu_programmer_version ) :
2020-11-16 22:09:32 +01:00
ver_ok . append ( check ( ) )
if CheckStatus . ERROR in ver_ok :
status = CheckStatus . ERROR
elif CheckStatus . WARNING in ver_ok and status == CheckStatus . OK :
status = CheckStatus . WARNING
2020-03-08 17:21:45 +01:00
2020-01-24 20:31:16 +01:00
# Check out the QMK submodules
sub_ok = check_submodules ( )
2020-11-16 22:09:32 +01:00
if sub_ok == CheckStatus . OK :
2020-01-24 20:31:16 +01:00
cli . log . info ( ' Submodules are up to date. ' )
else :
if yesno ( ' Would you like to clone the submodules? ' , default = True ) :
submodules . update ( )
sub_ok = check_submodules ( )
2020-11-16 22:09:32 +01:00
if CheckStatus . ERROR in sub_ok :
status = CheckStatus . ERROR
elif CheckStatus . WARNING in sub_ok and status == CheckStatus . OK :
status = CheckStatus . WARNING
2019-07-15 21:14:27 +02:00
2019-08-22 18:38:10 +02:00
# Report a summary of our findings to the user
2020-11-16 22:09:32 +01:00
if status == CheckStatus . OK :
2019-07-15 21:14:27 +02:00
cli . log . info ( ' {fg_green} QMK is ready to go ' )
2020-11-16 22:09:32 +01:00
return 0
elif status == CheckStatus . WARNING :
cli . log . info ( ' {fg_yellow} QMK is ready to go, but minor problems were found ' )
return 1
2019-08-22 08:40:24 +02:00
else :
2020-11-16 22:09:32 +01:00
cli . log . info ( ' {fg_red} Major problems detected, please fix these problems before proceeding. ' )
cli . log . info ( ' {fg_blue} Check out the FAQ (https://docs.qmk.fm/#/faq_build) or join the QMK Discord (https://discord.gg/Uq7gcHh) for help. ' )
return 2