KiBot/kiplot/kicad/config.py

275 lines
10 KiB
Python

"""
KiCad configuration classes.
Notes about coverage:
I'm excluding all the Darwin and Windows code from coverage.
I don't know how to test it on GitHub CI/CD.
"""
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
# 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
# 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):
pass
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 'empty 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()
def find_kicad_common():
""" Looks for kicad_common config file.
Returns its name or None. """
# User option has the higher priority
user_set = os.environ.get('KICAD_CONFIG_HOME')
if user_set:
cfg = os.path.join(user_set, KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
# XDG option is second
xdg_set = os.environ.get('XDG_CONFIG_HOME')
if xdg_set:
cfg = os.path.join(xdg_set, 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
# Others depends on the OS
system = platform.system()
if system == 'Linux':
# Linux: ~/.config/kicad/
home = os.environ.get('HOME')
if not home:
logger.warning('Environment variable `HOME` not defined, using `/`')
home = '/'
cfg = os.path.join(home, '.config', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
elif system == 'Darwin': # pragma: no cover
# MacOSX: ~/Library/Preferences/kicad/
home = os.environ.get('HOME')
if not home:
logger.warning('Environment variable `HOME` not defined, using `/`')
home = '/'
cfg = os.path.join(home, 'Library', 'Preferences', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
elif system == 'Windows': # pragma: no cover
# Windows: C:\Users\username\AppData\Roaming\kicad
# or C:\Documents and Settings\username\Application Data\kicad
username = os.environ.get('username')
if not username:
logger.warning('Unable to determine current user')
return None
cfg = os.path.join('C:', 'Users', username, 'AppData', 'Roaming', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
cfg = os.path.join('C:', 'Documents and Settings', username, 'Application Data', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
else:
logger.warning('Unsupported system `{}`'.format(system))
return None
logger.warning('Unable to find KiCad configuration file ({})'.format(cfg))
return None
def guess_symbol_dir():
""" Tries to figure out where libraries are.
Only used if we failed to find the kicad_common file. """
dir = os.environ.get('KICAD_SYMBOL_DIR')
if dir and os.path.isdir(dir):
return dir
system = platform.system()
share = os.path.join('share', 'kicad', 'library')
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
app_data = os.path.join('Library', 'Application Support', 'kicad', 'library')
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
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', 'library')
if os.path.isdir(dir):
return dir
return None
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
cf.readfp(io_buf, cfg)
if 'EnvironmentVariables' not in cf.sections():
logger.warning('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('Unable to find KiCad libraries')
def load_lib_aliases(fname):
if not os.path.isfile(fname):
return
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
def load_all_lib_aliases():
# Load the default symbol libs table.
# This is the list of libraries enabled by the user.
if KiConf.config_dir:
KiConf.load_lib_aliases(os.path.join(KiConf.config_dir, SYM_LIB_TABLE))
else:
logger.warning('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))