KiBot/kibot/kicad/config.py

283 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2020 Salvador E. Tropea
# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
KiCad configuration classes.
Reads the KiCad's configuration files.
In particular:
- kicad_common to know about the 'environment' variables
- The `sym-lib-table` files to map library aliases
Notes about coverage:
I'm excluding all the Darwin and Windows code from coverage.
I'm not even sure the values are correct.
"""
import os
import re
import sys
from io import StringIO
from glob import glob
import platform
import sysconfig
from ..gs import GS
from .. import log
from ..misc import W_NOCONFIG, W_NOKIENV, W_NOLIBS, W_NODEFSYMLIB
# Check python version to determine which version of ConfirParser to import
if sys.version_info.major >= 3:
import configparser as ConfigParser
else: # pragma: no cover (Py2)
# For future Python 2 support
import ConfigParser
logger = log.get_logger(__name__)
SYM_LIB_TABLE = 'sym-lib-table'
KICAD_COMMON = 'kicad_common'
class KiConfError(Exception):
def __init__(self, msg, file, lnum, line):
super().__init__()
self.line = lnum
self.file = file
self.msg = msg
self.code = line
def un_quote(val):
""" Remove optional quotes """
if val[0] == '"':
val.replace('\"', '"')
val = val[1:-1]
return val
def expand_env(val, env):
""" Expand KiCad environment variables """
for var in re.findall(r'\$\{(\S+)\}', val):
if var in env:
val = val.replace('${'+var+'}', env[var])
else:
logger.error('Unable to expand `{}` in `{}`'.format(var, val))
return val
class LibAlias(object):
""" An entry for the symbol libs table """
libs_re = re.compile(r'\(name\s+(\S+|"(?:[^"]|\\")+")\)\s*\(type\s+(\S+|"(?:[^"]|\\")+")\)'
r'\s*\(uri\s+(\S+|"(?:[^"]|\\")+")\)\s*\(options\s+(\S+|"(?:[^"]|\\")+")\)'
r'\s*\(descr\s+(\S+|"(?:[^"]|\\")+")\)')
def __init__(self):
super().__init__()
self.name = None
self.legacy = True
self.uri = None
self.options = None
self.descr = None
@staticmethod
def parse(options, cline, env):
m = LibAlias.libs_re.match(options)
if not m:
raise KiConfError('Malformed lib entry', SYM_LIB_TABLE, cline, options)
lib = LibAlias()
lib.name = un_quote(m.group(1))
lib.legacy = m.group(2) == 'Legacy'
lib.uri = os.path.abspath(expand_env(un_quote(m.group(3)), env))
lib.options = un_quote(m.group(4))
lib.descr = un_quote(m.group(5))
return lib
def __str__(self):
if not self.name:
return 'unnamed LibAlias'
return '`'+self.name+'` -> `'+self.uri+'`'
class KiConf(object):
""" Class to load and hold all KiCad configuration """
loaded = False
config_dir = None
dirname = None
sym_lib_dir = None
kicad_env = {}
lib_aliases = {}
def __init__(self):
assert False, "KiConf is fully static, no instances allowed"
def init(fname):
""" fname is the base project name, any extension is allowed.
So it can be the main schematic, the PCB or the project. """
if KiConf.loaded:
return
KiConf.dirname = os.path.dirname(fname)
KiConf.kicad_env['KIPRJMOD'] = KiConf.dirname
KiConf.load_kicad_common()
KiConf.load_all_lib_aliases()
KiConf.loaded = True
def find_kicad_common():
""" Looks for kicad_common config file.
Returns its name or None. """
cfg = ''
if GS.kicad_conf_path:
cfg = os.path.join(GS.kicad_conf_path, KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
logger.warning(W_NOCONFIG + 'Unable to find KiCad configuration file ({})'.format(cfg))
return None
def guess_kicad_data_dir(data_dir, env_var):
""" Tries to figure out where libraries are.
Only used if we failed to find the kicad_common file. """
dir = os.environ.get(env_var)
if dir and os.path.isdir(dir):
return dir
system = platform.system()
share = os.path.join('share', 'kicad', data_dir)
if system == 'Linux':
scheme_names = sysconfig.get_scheme_names()
# Try in local dir
if 'posix_user' in scheme_names:
dir = os.path.join(sysconfig.get_path('data', 'posix_user'), share)
if os.path.isdir(dir):
return dir
# Try at system level
if 'posix_prefix' in scheme_names:
dir = os.path.join(sysconfig.get_path('data', 'posix_prefix'), share)
if os.path.isdir(dir):
return dir
elif system == 'Darwin': # pragma: no cover (Darwin)
app_data = os.path.join('Library', 'Application Support', 'kicad', data_dir)
home = os.environ.get('HOME')
if home:
dir = os.path.join(home, app_data)
if os.path.isdir(dir):
return dir
dir = os.path.join('/', app_data)
if os.path.isdir(dir):
return dir
elif system == 'Windows': # pragma: no cover (Windows)
dir = os.path.join('C:', 'Program Files', 'KiCad', share)
if os.path.isdir(dir):
return dir
dir = os.path.join('C:', 'KiCad', share)
if os.path.isdir(dir):
return dir
username = os.environ.get('username')
dir = os.path.join('C:', 'Users', username, 'Documents', 'KiCad', data_dir)
if os.path.isdir(dir):
return dir
return None
def guess_symbol_dir():
return KiConf.guess_kicad_data_dir('library', 'KICAD_SYMBOL_DIR')
def guess_3d_dir():
return KiConf.guess_kicad_data_dir(os.path.join('modules', 'packages3d'), 'KISYS3DMOD')
def load_kicad_common():
# Try to figure out KiCad configuration file
cfg = KiConf.find_kicad_common()
if cfg and os.path.isfile(cfg):
logger.debug('Reading KiCad config from `{}`'.format(cfg))
KiConf.config_dir = os.path.dirname(cfg)
# Load the "environment variables"
with open(cfg, 'rt') as f:
buf = f.read()
io_buf = StringIO('[Default]\n'+buf)
cf = ConfigParser.RawConfigParser(allow_no_value=True)
cf.optionxform = str
if sys.version_info.major >= 3:
cf.read_file(io_buf, cfg)
else: # pragma: no cover (Py2)
cf.readfp(io_buf, cfg)
if 'EnvironmentVariables' not in cf.sections():
logger.warning(W_NOKIENV + 'KiCad config without EnvironmentVariables section')
else:
for k, v in cf.items('EnvironmentVariables'):
if GS.debug_level > 1:
logger.debug('- KiCad var: {}="{}"'.format(k, v))
KiConf.kicad_env[k] = v
if 'KICAD_SYMBOL_DIR' in KiConf.kicad_env:
KiConf.sym_lib_dir = KiConf.kicad_env['KICAD_SYMBOL_DIR']
else:
sym_dir = KiConf.guess_symbol_dir()
if sym_dir:
KiConf.kicad_env['KICAD_SYMBOL_DIR'] = sym_dir
KiConf.sym_lib_dir = sym_dir
logger.debug('Detected KICAD_SYMBOL_DIR="{}"'.format(sym_dir))
else:
logger.warning(W_NOLIBS + 'Unable to find KiCad libraries')
# If we couldn't get KISYS3DMOD from configuration and KISYS3DMOD isn't defined in the environment
# OR the 3D config dir is missing.
if (('KISYS3DMOD' not in KiConf.kicad_env and 'KISYS3DMOD' not in os.environ) or
(cfg is not None and not os.path.isdir(cfg.replace(KICAD_COMMON, '3d')))):
ki_sys_3d_mod = None
try:
ki_sys_3d_mod = KiConf.kicad_env['KISYS3DMOD']
except KeyError:
ki_sys_3d_mod = KiConf.guess_3d_dir()
if ki_sys_3d_mod is not None:
os.environ['KISYS3DMOD'] = ki_sys_3d_mod
KiConf.kicad_env['KISYS3DMOD'] = ki_sys_3d_mod
logger.debug('Exporting KISYS3DMOD="{}"'.format(ki_sys_3d_mod))
else:
logger.warning(W_NOLIBS + 'Unable to find KiCad 3D modules')
def load_lib_aliases(fname):
if not os.path.isfile(fname):
return False
logger.debug('Loading symbols lib table `{}`'.format(fname))
with open(fname, 'rt') as f:
line = f.readline().strip()
if line != '(sym_lib_table':
raise KiConfError('Symbol libs table missing signature', SYM_LIB_TABLE, 1, line)
line = f.readline()
cline = 2
while line and line[0] != ')':
m = re.match(r'\s*\(lib\s*(.*)\)', line)
if m:
alias = LibAlias.parse(m.group(1), cline, KiConf.kicad_env)
if GS.debug_level > 1:
logger.debug('- Adding lib alias '+str(alias))
KiConf.lib_aliases[alias.name] = alias
else:
raise KiConfError('Unknown symbol table entry', SYM_LIB_TABLE, cline, line)
line = f.readline()
cline += 1
return True
def load_all_lib_aliases():
# Load the default symbol libs table.
# This is the list of libraries enabled by the user.
loaded = False
if KiConf.config_dir:
conf_dir = KiConf.config_dir
if 'KICAD_CONFIG_HOME' in KiConf.kicad_env:
# KiCad 5 unintentionally allows it, is a bug, and won't be fixed:
# https://forum.kicad.info/t/kicad-config-home-inconsistencies-and-detail/26875
conf_dir = KiConf.kicad_env['KICAD_CONFIG_HOME']
logger.debug('Redirecting symbols lib table to '+conf_dir)
loaded = KiConf.load_lib_aliases(os.path.join(conf_dir, SYM_LIB_TABLE))
if not loaded:
logger.warning(W_NODEFSYMLIB + 'Missing default symbol library table')
# No default symbol libs table, try to create one
if KiConf.sym_lib_dir:
for f in glob(os.path.join(KiConf.sym_lib_dir, '*.lib')):
alias = LibAlias()
alias.name = os.path.splitext(os.path.basename(f))[0]
alias.uri = f
if GS.debug_level > 1:
logger.debug('Detected lib alias '+str(alias))
KiConf.lib_aliases[alias.name] = alias
# Load the project's table
KiConf.load_lib_aliases(os.path.join(KiConf.dirname, SYM_LIB_TABLE))
def expand_env(name):
return os.path.abspath(expand_env(un_quote(name), KiConf.kicad_env))