qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

check.py (9517B)


      1 """Check for specific programs.
      2 """
      3 from enum import Enum
      4 import shutil
      5 from subprocess import DEVNULL, TimeoutExpired
      6 from tempfile import TemporaryDirectory
      7 from pathlib import Path
      8 
      9 from milc import cli
     10 from qmk import submodules
     11 from qmk.commands import find_make
     12 
     13 
     14 class CheckStatus(Enum):
     15     OK = 1
     16     WARNING = 2
     17     ERROR = 3
     18 
     19 
     20 WHICH_MAKE = Path(find_make()).name
     21 
     22 ESSENTIAL_BINARIES = {
     23     WHICH_MAKE: {},
     24     'git': {},
     25     'dos2unix': {},
     26     'diff': {},
     27     'dfu-programmer': {},
     28     'avrdude': {},
     29     'dfu-util': {},
     30     'avr-gcc': {
     31         'version_arg': '-dumpversion'
     32     },
     33     'arm-none-eabi-gcc': {
     34         'version_arg': '-dumpversion'
     35     },
     36 }
     37 
     38 
     39 def _check_make_version():
     40     last_line = ESSENTIAL_BINARIES[WHICH_MAKE]['output'].split('\n')[0]
     41     version_number = last_line.split()[2]
     42     cli.log.info('Found %s version %s', WHICH_MAKE, version_number)
     43 
     44     return CheckStatus.OK
     45 
     46 
     47 def _check_git_version():
     48     last_line = ESSENTIAL_BINARIES['git']['output'].split('\n')[0]
     49     version_number = last_line.split()[2]
     50     cli.log.info('Found git version %s', version_number)
     51 
     52     return CheckStatus.OK
     53 
     54 
     55 def _check_dos2unix_version():
     56     last_line = ESSENTIAL_BINARIES['dos2unix']['output'].split('\n')[0]
     57     version_number = last_line.split()[1]
     58     cli.log.info('Found dos2unix version %s', version_number)
     59 
     60     return CheckStatus.OK
     61 
     62 
     63 def _check_diff_version():
     64     last_line = ESSENTIAL_BINARIES['diff']['output'].split('\n')[0]
     65     if 'Apple diff' in last_line:
     66         version_number = last_line
     67     else:
     68         version_number = last_line.split()[3]
     69     cli.log.info('Found diff version %s', version_number)
     70 
     71     return CheckStatus.OK
     72 
     73 
     74 def _check_arm_gcc_version():
     75     """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
     76     """
     77     version_number = ESSENTIAL_BINARIES['arm-none-eabi-gcc']['output'].strip()
     78     cli.log.info('Found arm-none-eabi-gcc version %s', version_number)
     79 
     80     # Right now all known ARM versions are ok, so check that it can produce binaries
     81     return _check_arm_gcc_installation()
     82 
     83 
     84 def _check_arm_gcc_installation():
     85     """Returns OK if the arm-none-eabi-gcc is fully installed and can produce binaries.
     86     """
     87     with TemporaryDirectory() as temp_dir:
     88         temp_in = Path(temp_dir) / 'test.c'
     89         temp_out = Path(temp_dir) / 'test.elf'
     90 
     91         temp_in.write_text('#include <newlib.h>\nint main() { return __NEWLIB__ * __NEWLIB_MINOR__ * __NEWLIB_PATCHLEVEL__; }', encoding='utf-8')
     92 
     93         args = ['arm-none-eabi-gcc', '-mcpu=cortex-m0', '-mthumb', '-mno-thumb-interwork', '--specs=nosys.specs', '--specs=nano.specs', '-x', 'c', '-o', str(temp_out), str(temp_in)]
     94         result = cli.run(args, stdout=None, stderr=None)
     95         if result.returncode == 0:
     96             cli.log.info('Successfully compiled using arm-none-eabi-gcc')
     97         else:
     98             cli.log.error(f'Failed to compile a simple program with arm-none-eabi-gcc, return code {result.returncode}')
     99             cli.log.error(f'Command: {" ".join(args)}')
    100             return CheckStatus.ERROR
    101 
    102         args = ['arm-none-eabi-size', str(temp_out)]
    103         result = cli.run(args, stdout=None, stderr=None)
    104         if result.returncode == 0:
    105             cli.log.info('Successfully tested arm-none-eabi-binutils using arm-none-eabi-size')
    106         else:
    107             cli.log.error(f'Failed to execute arm-none-eabi-size, perhaps corrupt arm-none-eabi-binutils, return code {result.returncode}')
    108             cli.log.error(f'Command: {" ".join(args)}')
    109             return CheckStatus.ERROR
    110 
    111         return CheckStatus.OK
    112 
    113 
    114 def _check_avr_gcc_version():
    115     """Returns True if the avr-gcc version is not known to cause problems.
    116     """
    117     version_number = ESSENTIAL_BINARIES['avr-gcc']['output'].strip()
    118     cli.log.info('Found avr-gcc version %s', version_number)
    119 
    120     # Right now all known AVR versions are ok, so check that it can produce binaries
    121     return _check_avr_gcc_installation()
    122 
    123 
    124 def _check_avr_gcc_installation():
    125     """Returns OK if the avr-gcc is fully installed and can produce binaries.
    126     """
    127     with TemporaryDirectory() as temp_dir:
    128         temp_in = Path(temp_dir) / 'test.c'
    129         temp_out = Path(temp_dir) / 'test.elf'
    130 
    131         temp_in.write_text('int main() { return 0; }', encoding='utf-8')
    132 
    133         args = ['avr-gcc', '-mmcu=atmega32u4', '-x', 'c', '-o', str(temp_out), str(temp_in)]
    134         result = cli.run(args, stdout=None, stderr=None)
    135         if result.returncode == 0:
    136             cli.log.info('Successfully compiled using avr-gcc')
    137         else:
    138             cli.log.error(f'Failed to compile a simple program with avr-gcc, return code {result.returncode}')
    139             cli.log.error(f'Command: {" ".join(args)}')
    140             return CheckStatus.ERROR
    141 
    142         args = ['avr-size', str(temp_out)]
    143         result = cli.run(args, stdout=None, stderr=None)
    144         if result.returncode == 0:
    145             cli.log.info('Successfully tested avr-binutils using avr-size')
    146         else:
    147             cli.log.error(f'Failed to execute avr-size, perhaps corrupt avr-binutils, return code {result.returncode}')
    148             cli.log.error(f'Command: {" ".join(args)}')
    149             return CheckStatus.ERROR
    150 
    151         return CheckStatus.OK
    152 
    153 
    154 def _check_avrdude_version():
    155     lines = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')
    156     # avrdude version text is currently not translated, however we fall back to old behaviour of assuming a line
    157     version_line = next((line for line in lines if 'version' in line), lines[-2])
    158     version_number = version_line.split()[2][:-1]
    159     cli.log.info('Found avrdude version %s', version_number)
    160 
    161     return CheckStatus.OK
    162 
    163 
    164 def _check_dfu_util_version():
    165     first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
    166     version_number = first_line.split()[1]
    167     cli.log.info('Found dfu-util version %s', version_number)
    168 
    169     return CheckStatus.OK
    170 
    171 
    172 def _check_dfu_programmer_version():
    173     first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
    174     version_number = first_line.split()[1]
    175     cli.log.info('Found dfu-programmer version %s', version_number)
    176 
    177     return CheckStatus.OK
    178 
    179 
    180 def check_binaries():
    181     """Iterates through ESSENTIAL_BINARIES and tests them.
    182     """
    183     ok = CheckStatus.OK
    184     missing_from_path = []
    185 
    186     for binary in sorted(ESSENTIAL_BINARIES):
    187         try:
    188             if not is_in_path(binary):
    189                 ok = CheckStatus.ERROR
    190                 missing_from_path.append(binary)
    191             elif not is_executable(binary):
    192                 ok = CheckStatus.ERROR
    193         except TimeoutExpired:
    194             cli.log.debug('Timeout checking %s', binary)
    195             if ok != CheckStatus.ERROR:
    196                 ok = CheckStatus.WARNING
    197 
    198     if missing_from_path:
    199         location_noun = 'its location' if len(missing_from_path) == 1 else 'their locations'
    200         cli.log.error('{fg_red}' + ', '.join(missing_from_path) + f' may need to be installed, or {location_noun} added to your path.')
    201 
    202     return ok
    203 
    204 
    205 def check_binary_versions():
    206     """Check the versions of ESSENTIAL_BINARIES
    207     """
    208     checks = {
    209         WHICH_MAKE: _check_make_version,
    210         'git': _check_git_version,
    211         'dos2unix': _check_dos2unix_version,
    212         'diff': _check_diff_version,
    213         'arm-none-eabi-gcc': _check_arm_gcc_version,
    214         'avr-gcc': _check_avr_gcc_version,
    215         'avrdude': _check_avrdude_version,
    216         'dfu-util': _check_dfu_util_version,
    217         'dfu-programmer': _check_dfu_programmer_version,
    218     }
    219 
    220     versions = []
    221     for binary in sorted(ESSENTIAL_BINARIES):
    222         if 'output' not in ESSENTIAL_BINARIES[binary]:
    223             cli.log.warning('Unknown version for %s', binary)
    224             versions.append(CheckStatus.WARNING)
    225             continue
    226 
    227         check = checks[binary]
    228         versions.append(check())
    229     return versions
    230 
    231 
    232 def check_submodules():
    233     """Iterates through all submodules to make sure they're cloned and up to date.
    234     """
    235     for submodule in submodules.status().values():
    236         if submodule['status'] is None:
    237             return CheckStatus.ERROR
    238         elif not submodule['status']:
    239             return CheckStatus.WARNING
    240 
    241     return CheckStatus.OK
    242 
    243 
    244 def is_in_path(command):
    245     """Returns True if command is found in the path.
    246     """
    247     if shutil.which(command) is None:
    248         cli.log.error("{fg_red}Can't find %s in your path.", command)
    249         return False
    250     return True
    251 
    252 
    253 def is_executable(command):
    254     """Returns True if command can be executed.
    255     """
    256     # Make sure the command can be executed
    257     version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
    258     check = cli.run([command, version_arg], combined_output=True, stdin=DEVNULL, timeout=5)
    259 
    260     ESSENTIAL_BINARIES[command]['output'] = check.stdout
    261 
    262     if check.returncode in [0, 1]:  # Older versions of dfu-programmer exit 1
    263         cli.log.debug('Found {fg_cyan}%s', command)
    264         return True
    265 
    266     cli.log.error("{fg_red}Can't run `%s %s`", command, version_arg)
    267     return False
    268 
    269 
    270 def release_info(file='/etc/os-release'):
    271     """Parse release info to dict
    272     """
    273     ret = {}
    274     try:
    275         with open(file) as f:
    276             for line in f:
    277                 if '=' in line:
    278                     key, value = map(str.strip, line.split('=', 1))
    279                     if value.startswith('"') and value.endswith('"'):
    280                         value = value[1:-1]
    281                     ret[key] = value
    282     except (PermissionError, FileNotFoundError):
    283         pass
    284 
    285     return ret