563 lines
19 KiB
Python
Executable File
563 lines
19 KiB
Python
Executable File
#!/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 < (7, 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')
|
|
|