#!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (c) 2022 Salvador E. Tropea # Copyright (c) 2022 Instituto Nacional de TecnologĂ­a Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) # # This is the installation checker, should help people to detect installation issues and install needed tools import argparse from contextlib import contextmanager import importlib import json import os import platform import re from shutil import which import site import subprocess import sys deps = '@json_dep@' # Dirs to look for plugins kicad_plugins_dirs = [] NOT_AVAIL = 'Not available' UNKNOWN = '*UNKNOWN*' if sys.stdout.isatty(): CSI = '\033[' RED = CSI+str(31)+'m' GREEN = CSI+str(32)+'m' YELLOW = CSI+str(33)+'m' YELLOW2 = CSI+str(93)+'m' RESET = CSI+str(39)+'m' BRIGHT = CSI+";1;4"+'m' NORMAL = CSI+'0'+'m' else: RED = GREEN = YELLOW = YELLOW2 = RESET = BRIGHT = NORMAL = '' last_ok = False last_cmd = None tests_ok = True tests_msg = '' is_x86 = is_64 = is_linux = False ver_re = re.compile(r'(\d+)\.(\d+)(?:\.(\d+))?(?:[\.-](\d+))?') def check_tool_binary_python(name): base = os.path.join(site.USER_BASE, 'bin') full_name = os.path.join(base, name) if not os.path.isfile(full_name) or not os.access(full_name, os.X_OK): return None return full_name def check_tool_binary_local(name): home = os.environ.get('HOME') or os.environ.get('username') if home is None: return None home_bin = os.path.join(home, '.local', 'share', 'kibot', 'bin') full_name = os.path.join(home_bin, name) if not os.path.isfile(full_name) or not os.access(full_name, os.X_OK): return None return full_name def look_for_command(command): cmd_full = which(command) if not cmd_full: cmd_full = check_tool_binary_python(command) if not cmd_full: cmd_full = check_tool_binary_local(command) return cmd_full def run_command(cmd, only_first_line=False, pre_ver_text=None, no_err_2=False): global last_ok global last_cmd cmd_full = look_for_command(cmd[0]) if not cmd_full: last_ok = False return NOT_AVAIL cmd[0] = cmd_full last_cmd = None try: res_run = subprocess.run(cmd, check=True, capture_output=True) last_cmd = cmd[0] except FileNotFoundError as e: last_ok = False return NOT_AVAIL except subprocess.CalledProcessError as e: if e.returncode != 2 or not no_err_2: print('Failed to run %s, error %d' % (cmd[0], e.returncode)) if e.output: print('Output from command: '+e.output.decode()) last_ok = False return UNKNOWN res = res_run.stdout.decode().strip() if len(res) == 0: res = res_run.stderr.decode().strip() if only_first_line: res = res.split('\n')[0] pre_vers = (cmd[0]+' version ', cmd[0]+' ', pre_ver_text) for pre_ver in pre_vers: if pre_ver and res.startswith(pre_ver): res = res[len(pre_ver):] last_ok = True return res def simple_run_command(cmd): res = run_command(cmd) sev, ver = check_version(res, [{'mandatory': True, 'output': 'global', 'version': None}], no_ver=True) return do_color(res, sev, version=ver) def search_as_plugin(cmd, names): """ If a command isn't in the path look for it in the KiCad plugins """ if which(cmd) is not None: return which(cmd) for dir in kicad_plugins_dirs: for name in names: fname = os.path.join(dir, name, cmd) # print('Trying '+fname) if os.path.isfile(fname): # print('Using `{}` for `{}` ({})'.format(fname, cmd, name)) return fname return cmd @contextmanager def hide_stderr(): """ Low level stderr suppression, used to hide KiCad bugs. """ newstderr = os.dup(2) devnull = os.open(os.devnull, os.O_WRONLY) os.dup2(devnull, 2) os.close(devnull) yield os.dup2(newstderr, 2) def do_int(v): return int(v) if v is not None else 0 def check_tests(tests): global tests_ok global tests_msg for t in tests: cmd = t['command'] cmd_full = look_for_command(cmd[0]) if cmd_full is None: tests_ok = False tests_msg = 'Missing test tool `{}`'.format(cmd[0]) return True cmd[0] = cmd_full try: cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) except FileNotFoundError as e: tests_ok = False tests_msg = 'Missing test tool `{}`'.format(cmd[0]) return True except subprocess.CalledProcessError as e: tests_ok = False tests_msg = 'Failed to run %s, error %d' % (cmd[0], e.returncode) if e.output: tests_msg += '\nOutput from command: '+e.output.decode() return True res = cmd_output.decode().strip() if not re.search(t['search'], res): tests_ok = False tests_msg = t['error'] return True return False def check_version(version, roles, no_ver=False, tests=None): res = ver_re.search(version) if res: ver = list(map(do_int, res.groups())) else: ver = [0, 0, 0] not_avail = version == NOT_AVAIL or version == UNKNOWN global tests_ok tests_ok = True if tests and res and not not_avail: not_avail = check_tests(tests) severity = 0 for r in roles: mandatory = r['mandatory'] glb = r['output'] == 'global' this_sever = 0 max_version = r.get('max_version') if not_avail or (r['version'] and ver < r['version']) or (max_version and ver >= max_version): if mandatory: this_sever = 4 if glb else 3 else: this_sever = 2 if glb else 1 severity = max(severity, this_sever) r['sev'] = this_sever return severity, ver def sev2color(severity): if severity == 4: return RED elif severity == 3: return YELLOW2 elif severity: return YELLOW else: return GREEN def do_color(msg, severity, version=None): if version is not None and version != [0, 0, 0]: if len(version) == 4 and version[3] == 0: version = version[:-1] ver_str = '.'.join(map(str, version)) if ver_str != msg: msg = ver_str+' ('+msg+')' return sev2color(severity)+msg+RESET def error(msg): print(sev2color(4)+'**> '+msg+RESET) def do_bright(msg): return BRIGHT+msg+NORMAL def global2human(name): return '`'+name+'`' if name != 'global' else 'general use' def show_roles(roles): needed = [] optional = [] for r in roles: if r['mandatory']: needed.append(r) else: optional.append(r) r['output'] = global2human(r['output']) if needed: if len(needed) == 1: color = sev2color(needed[0]['sev']) name = needed[0]['output'] if name == 'general use': print(color+' - Mandatory') else: print(color+' - Mandatory for '+name) else: need_s = sorted(needed, key=lambda x: x['output']) print(RESET+' - Mandatory for: '+', '.join([sev2color(f['sev'])+f['output']+RESET for f in need_s])) if optional: if len(optional) == 1: o = optional[0] desc = o['desc'][0].lower()+o['desc'][1:] print(sev2color(o['sev'])+' - Optional to {} for {}'.format(desc, o['output'])) else: print(RESET+' - Optional to:') for o in optional: ver = '' if o['version']: ver = ' (v'+'.'.join(map(str, o['version']))+')' print(sev2color(o['sev'])+' - {} for {}{}'.format(o['desc'], o['output'], ver)) def python_module(severity, name, deb_package, roles, arch): if not severity: return print(sev2color(severity)+'* Python module `{}` not installed, too old or incompatible'.format(name)) if debian_support: if deb_package is None: deb_package = 'python3-'+name print(' Install the `{0}` package, i.e.: `sudo apt-get install {0}`'.format(deb_package)) elif arch_support: if arch is None: arch = 'python-'+name print(' Install the `{0}` package, i.e.: `sudo pacman -S {0}`'.format(arch)) elif pip_ok: print(' run `{} install {}` as root,'.format(pip_command, name)) print(' or run `{} install --user {}` as a regular user'.format(pip_command, name)) else: print(' Install the Package Installer for Python (pip) and run this script again') show_roles(roles) print(RESET) def binary_tool(severity, name, url, url_down, deb_package, deb, extra_deb, roles, downloader, comments, arch, extra_arch): if not severity: return print(sev2color(severity)+'* {} not installed or too old'.format(name)) if deb and debian_support: if deb_package is None: deb_package = name.lower() print(' Install the `{0}` package, i.e.: `sudo apt-get install {0}`'.format(deb_package)) if extra_deb: print(' You should also install the following packages: '+', '.join(extra_deb)) elif arch and arch_support: if arch.endswith('(AUR)'): print(' Install the `{0}` package, i.e.: `sudo yay -S {0}`'.format(arch[:-5])) else: print(' Install the `{0}` package, i.e.: `sudo pacman -S {0}`'.format(arch)) if extra_arch: print(' You should also install the following packages: '+', '.join(extra_arch)) else: print(' Visit: '+url) if url_down: print(' Download it from: '+url_down) for c in comments: print(' '+c) if isinstance(downloader, str): if downloader == 'pytool' and not pip_ok: print(' Please install the Python Installer (pip).') else: print(' This tool might be automatically downloaded by KiBot.') show_roles(roles) print(RESET) parser = argparse.ArgumentParser(description='KiBot installation checker') parser.add_argument('--show-paths', '-p', help='Show paths to tools', action='store_true') args = parser.parse_args() # Force iBoM to avoid the use of graphical stuff os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = 'True' # ###################################################################################################################### # Core tools # ###################################################################################################################### print('KiBot installation checker\n') print(do_bright('Core:')) # Operating system system = platform.system() if system == 'Linux': linux_version = simple_run_command(['uname', '-a']) print('Linux: '+linux_version) if args.show_paths: print(' '+str(which('uname'))) os_ok = True is_x86 = 'x86' in linux_version is_64 = ('x86_64' in linux_version) or ('amd64' in linux_version) is_linux = True else: print(system) os_ok = False # Python version if sys.version_info >= (3, 6): py_ok = True sev = 0 else: py_ok = False sev = 4 print('Python: '+do_color(sys.version.replace('\n', ' '), sev)) if args.show_paths: print(' '+os.__file__) print(' '+str(which('python3'))) # KiCad home = None try: import pcbnew kicad_ok = True # Fill the plug-in locations # TODO: Windows? MacOSX? kicad_share_path = '/usr/share/kicad' if hasattr(pcbnew, 'GetKicadConfigPath'): with hide_stderr(): kicad_conf_path = pcbnew.GetKicadConfigPath() elif hasattr(pcbnew, 'GetSettingsManager'): kicad_conf_path = pcbnew.GetSettingsManager().GetUserSettingsPath() else: kicad_conf_path = None # /usr/share/kicad/* kicad_plugins_dirs.append(os.path.join(kicad_share_path, 'scripting')) kicad_plugins_dirs.append(os.path.join(kicad_share_path, 'scripting', 'plugins')) kicad_plugins_dirs.append(os.path.join(kicad_share_path, '3rdparty', 'plugins')) # KiCad 6.0 PCM # ~/.config/kicad/* if kicad_conf_path: kicad_plugins_dirs.append(os.path.join(kicad_conf_path, 'scripting')) kicad_plugins_dirs.append(os.path.join(kicad_conf_path, 'scripting', 'plugins')) # ~/.kicad_plugins and ~/.kicad if 'HOME' in os.environ: home = os.environ['HOME'] kicad_plugins_dirs.append(os.path.join(home, '.kicad_plugins')) kicad_plugins_dirs.append(os.path.join(home, '.kicad', 'scripting')) kicad_plugins_dirs.append(os.path.join(home, '.kicad', 'scripting', 'plugins')) except FileNotFoundError: kicad_ok = False kicad_version = (0, 0, 0) if kicad_ok: try: version = pcbnew.GetBuildVersion() # KiCad version m = re.search(r'(\d+)\.(\d+)\.(\d+)', version) if m is None: error("Unable to detect KiCad version, got: `{}`".format(version)) else: kicad_version = (int(m.group(1)), int(m.group(2)), int(m.group(3))) if kicad_version[0] >= 6 and home: # KiCad 6.0 PCM ver_dir = str(kicad_version[0])+'.'+str(kicad_version[1]) local_share = os.path.join(home, '.local', 'share', 'kicad', ver_dir) kicad_plugins_dirs.append(os.path.join(local_share, 'scripting')) kicad_plugins_dirs.append(os.path.join(local_share, 'scripting', 'plugins')) kicad_plugins_dirs.append(os.path.join(local_share, '3rdparty', 'plugins')) # KiCad 6.0 PCM except: version = 'Older than 5.1.6' else: version = NOT_AVAIL if kicad_version >= (5, 1, 6) and kicad_version < (8, 99): sev = 0 else: sev = 4 print('KiCad: '+do_color(version, sev)) if args.show_paths and kicad_ok: print(' '+pcbnew.__file__) print(' '+str(which('kicad'))) # KiBot version try: import kibot.__main__ version = kibot.__main__.__version__ kibot_ok = True sev = 0 except: version = NOT_AVAIL kibot_ok = False sev = 4 print('Kibot: '+do_color(version, sev)) if args.show_paths and kibot_ok: print(' '+kibot.__main__.__file__) print(' '+str(which('kibot'))) if kibot_ok and which('kibot') is None: print(sev2color(4)+'* KiBot is installed but not available in your PATH') import kibot if '/lib/' in kibot.__file__: v = re.sub(r'\/lib\/.*', '/bin/kibot', kibot.__file__) if os.path.isfile(v): print(' Try adding `{}` to your PATH'.format(v[:-5])) print(' I.e.: export PATH=$PATH:'+v[:-5]) sys.exit(1) dependencies = json.loads(deps) print(do_bright('\nModules:')) for name, d in dependencies.items(): if not d['is_python']: continue mod = None try: mod = importlib.import_module(d['module_name']) if hasattr(mod, '__version__'): version = mod.__version__ else: version = 'Ok' except: version = NOT_AVAIL sev, ver = check_version(version, d['role']) d['sev'] = sev version = version.split('\n')[0] print(name+': '+do_color(version, sev, version=ver)) if args.show_paths and mod: print(' '+mod.__file__) print(do_bright('\nTools:')) for name, d in dependencies.items(): if d['is_python']: continue command = d['command'] if d['is_kicad_plugin']: command = search_as_plugin(command, d['plugin_dirs']) if d['no_cmd_line_version']: version = 'Ok ({})'.format(command) if which(command) is not None else NOT_AVAIL else: cmd = [command, d['help_option']] if d['is_kicad_plugin'] and os.path.isfile(command): # There is no point in adding the interpreter if the script isn't there # It will just confuse run_command thinking we need to check Python cmd.insert(0, 'python3') version = run_command(cmd, no_err_2=d['no_cmd_line_version_old']) sev, ver = check_version(version, d['role'], tests=d['tests']) d['sev'] = sev version = version.split('\n')[0] pypi_name = d['pypi_name'] if pypi_name and pypi_name.lower() != name.lower(): name += ' ({})'.format(pypi_name) print(name+': '+do_color(version, sev, version=ver)) if not tests_ok: print('- '+do_color(tests_msg, sev)) if args.show_paths and last_cmd: print(' '+last_cmd) # ###################################################################################################################### # Recommendations # ###################################################################################################################### print() debian_support = False if which('apt-get'): debian_support = True arch_support = False if which('pacman'): arch_support = True pip_ok = False if which('pip3'): pip_ok = True pip_command = 'pip3' elif which('pip'): pip_ok = True pip_command = 'pip' if not os_ok: print(sev2color(4)+'* KiBot is currently tested under Linux') if system == 'Darwin': print(' MacOSX should be supported for KiCad 6.x') elif system == 'Windows': print(' Windows may work with some limitations for KiCad 6.x') print(' Consider using a docker image, Windows docker can run Linux images (using virtualization)') print(' You can also try WSL (Windows Subsystem for Linux)') else: print(' What OS are you using? Is KiCad available for it?') print(' Please consult: https://github.com/INTI-CMNB/KiBot/issues') print(RESET) if not py_ok: print(sev2color(4)+'* Install Python 3.6 or newer') print(RESET) if not kicad_ok: print(sev2color(4)+'* Install KiCad 5.1.6 or newer') if debian_support: print(' Try `apt-get install kicad` as root') else: print(' Download it from: https://www.kicad.org/download/') print(RESET) if not kibot_ok: print(sev2color(4)+'* Install KiBot!') if debian_support: print(' Follow the instructions here: https://set-soft.github.io/debian/') elif pip_ok: print(' run `{} install --no-compile kibot` as root,'.format(pip_command)) print(' or run `{} install --user --no-compile kibot` as a regular user'.format(pip_command)) else: print(' Install the Package Installer for Python (pip) and run this script again') print(RESET) for name, d in dependencies.items(): if d['is_python']: python_module(d['sev'], d['pypi_name'], d['deb_package'], d['role'], d['arch']) else: binary_tool(d['sev'], d['name'], d['url'], d['url_down'], d['deb_package'], d['in_debian'], d['extra_deb'], d['role'], d['downloader_str'], d['comments'], d['arch'], d['extra_arch']) if sys.stdout.isatty(): labels = ('ok', 'optional for an output', 'optional for general use', 'mandatory for an output', 'mandatory for general use') text = ', '.join([sev2color(c)+l+RESET for c, l in enumerate(labels)]) print(do_bright('\nColor reference:')+' '+text) print('\nDid this help? Please consider commenting it on https://github.com/INTI-CMNB/KiBot/discussions/categories/kibot-check')