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
import shutil
import subprocess
2020-01-24 20:31:16 +01:00
from pathlib import Path
2019-07-15 21:14:27 +02:00
from milc import cli
2020-01-24 20:31:16 +01:00
from qmk import submodules
from qmk . questions import yesno
ESSENTIAL_BINARIES = [ ' dfu-programmer ' , ' avrdude ' , ' dfu-util ' , ' avr-gcc ' , ' arm-none-eabi-gcc ' , ' bin/qmk ' ]
ESSENTIAL_SUBMODULES = [ ' lib/chibios ' , ' lib/lufa ' ]
2019-07-15 21:14:27 +02:00
2020-01-11 21:15:28 +01:00
def _udev_rule ( vid , pid = None ) :
2019-11-07 19:53:03 +01:00
""" Helper function that return udev rules
"""
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-01-24 20:31:16 +01:00
def check_binaries ( ) :
""" Iterates through ESSENTIAL_BINARIES and tests them.
"""
ok = True
for binary in ESSENTIAL_BINARIES :
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.
"""
ok = True
for submodule in submodules . status ( ) . values ( ) :
if submodule [ ' status ' ] is None :
if submodule [ ' name ' ] in ESSENTIAL_SUBMODULES :
cli . log . error ( ' Submodule %s has not yet been cloned! ' , submodule [ ' name ' ] )
ok = False
else :
cli . log . warn ( ' Submodule %s is not available. ' , submodule [ ' name ' ] )
elif not submodule [ ' status ' ] :
if submodule [ ' name ' ] in ESSENTIAL_SUBMODULES :
cli . log . warn ( ' Submodule %s is not up to date! ' )
return ok
def check_udev_rules ( ) :
""" Make sure the udev rules look good.
"""
ok = True
udev_dir = Path ( " /etc/udev/rules.d/ " )
desired_rules = {
' dfu ' : { _udev_rule ( " 03eb " , " 2ff4 " ) , _udev_rule ( " 03eb " , " 2ffb " ) , _udev_rule ( " 03eb " , " 2ff0 " ) } ,
' tmk ' : { _udev_rule ( " feed " ) } ,
' input_club ' : { _udev_rule ( " 1c11 " ) } ,
' stm32 ' : { _udev_rule ( " 1eaf " , " 0003 " ) , _udev_rule ( " 0483 " , " df11 " ) } ,
' caterina ' : { ' ATTRS {idVendor} == " 2a03 " , ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' , ' ATTRS {idVendor} == " 2341 " , ENV {ID_MM_DEVICE_IGNORE} = " 1 " ' } ,
}
if udev_dir . exists ( ) :
udev_rules = [ str ( rule_file ) for rule_file in udev_dir . glob ( ' *.rules ' ) ]
current_rules = set ( )
# Collect all rules from the config files
for rule_file in udev_rules :
with open ( rule_file , " r " ) as fd :
for line in fd . readlines ( ) :
line = line . strip ( )
if not line . startswith ( " # " ) and len ( line ) :
current_rules . add ( line )
# Check if the desired rules are among the currently present rules
for bootloader , rules in desired_rules . items ( ) :
if not rules . issubset ( current_rules ) :
# If the rules for catalina are not present, check if ModemManager is running
if bootloader == " caterina " :
if check_modem_manager ( ) :
ok = False
cli . log . warn ( " {bg_yellow} Detected ModemManager without udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro. " )
else :
cli . log . warn ( " {bg_yellow} Missing udev rules for ' %s ' boards. You ' ll need to use `sudo` in order to flash them. " , bootloader )
return ok
def check_modem_manager ( ) :
""" Returns True if ModemManager is running.
"""
if shutil . which ( " systemctl " ) :
mm_check = subprocess . run ( [ " systemctl " , " --quiet " , " is-active " , " ModemManager.service " ] , timeout = 10 )
if mm_check . returncode == 0 :
return True
else :
cli . log . warn ( " Can ' t find systemctl to check for ModemManager. " )
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
check = subprocess . run ( [ command , ' --version ' ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE , timeout = 5 )
if check . returncode in [ 0 , 1 ] : # Older versions of dfu-programmer exit 1
cli . log . debug ( ' Found {fg_cyan} %s ' , command )
return True
cli . log . error ( " {fg_red} Can ' t run ` %s --version` " , command )
return False
def os_test_linux ( ) :
""" Run the Linux specific tests.
"""
cli . log . info ( " Detected {fg_cyan} Linux. " )
ok = True
if not check_udev_rules ( ) :
ok = False
return ok
def os_test_macos ( ) :
""" Run the Mac specific tests.
"""
cli . log . info ( " Detected {fg_cyan} macOS. " )
return True
def os_test_windows ( ) :
""" Run the Windows specific tests.
"""
cli . log . info ( " Detected {fg_cyan} Windows. " )
return True
@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. ' )
2019-07-15 21:14:27 +02:00
ok = True
2019-08-22 18:38:10 +02:00
# Determine our OS and run platform specific tests
2020-01-28 22:21:00 +01:00
OS = platform . platform ( ) . lower ( ) # noqa (N806), uppercase name is ok in this instance
2019-08-22 18:38:10 +02:00
2020-02-17 12:18:49 +01:00
if ' darwin ' or ' macos ' in OS :
2020-01-24 20:31:16 +01:00
if not os_test_macos ( ) :
ok = False
2020-01-28 22:21:00 +01:00
elif ' linux ' in OS :
2020-01-24 20:31:16 +01:00
if not os_test_linux ( ) :
ok = False
2020-01-28 22:21:00 +01:00
elif ' windows ' in OS :
2020-01-24 20:31:16 +01:00
if not os_test_windows ( ) :
ok = False
else :
cli . log . error ( ' Unsupported OS detected: %s ' , OS )
ok = False
2019-08-22 18:38:10 +02:00
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 ) :
subprocess . run ( [ ' util/qmk_install.sh ' ] )
bin_ok = check_binaries ( )
if bin_ok :
cli . log . info ( ' All dependencies are installed. ' )
2019-07-15 21:14:27 +02:00
else :
2020-01-24 20:31:16 +01:00
ok = False
# Check out the QMK submodules
sub_ok = check_submodules ( )
if sub_ok :
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 ( )
if not sub_ok :
ok = False
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
2019-07-15 21:14:27 +02:00
if ok :
cli . log . info ( ' {fg_green} QMK is ready to go ' )
2019-08-22 08:40:24 +02:00
else :
cli . log . info ( ' {fg_yellow} Problems detected, please fix these problems before proceeding. ' )
2020-01-24 20:31:16 +01:00
# FIXME(skullydazed/unclaimed): Link to a document about troubleshooting, or discord or something