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