[Config] More robust sym-lib-table parser
- Now loads on-the-fly (not needed for KiCad v6+) - Uses s-expressions parser, no regex - Can recreate the global table for KiCad v6+
This commit is contained in:
parent
baf1471be8
commit
74a27b3036
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020-2022 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial
|
||||
# Copyright (c) 2020-2023 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
|
||||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
"""
|
||||
|
|
@ -24,9 +24,13 @@ import re
|
|||
from shutil import copy2
|
||||
import sys
|
||||
import sysconfig
|
||||
from ..error import KiPlotConfigurationError
|
||||
from ..gs import GS
|
||||
from .. import log
|
||||
from ..misc import W_NOCONFIG, W_NOKIENV, W_NOLIBS, W_NODEFSYMLIB, MISSING_WKS, W_MAXDEPTH, W_3DRESVER
|
||||
from ..misc import (W_NOCONFIG, W_NOKIENV, W_NOLIBS, W_NODEFSYMLIB, MISSING_WKS, W_MAXDEPTH, W_3DRESVER, W_LIBTVERSION,
|
||||
W_LIBTUNK)
|
||||
from .sexpdata import load, SExpData
|
||||
from .sexp_helpers import _check_is_symbol_list, _check_integer, _check_relaxed
|
||||
|
||||
# Check python version to determine which version of ConfirParser to import
|
||||
if sys.version_info.major >= 3:
|
||||
|
|
@ -37,8 +41,10 @@ else: # pragma: no cover (Py2)
|
|||
|
||||
logger = log.get_logger()
|
||||
SYM_LIB_TABLE = 'sym-lib-table'
|
||||
FP_LIB_TABLE = 'fp-lib-table'
|
||||
KICAD_COMMON = 'kicad_common'
|
||||
MAXDEPTH = 20
|
||||
SUP_VERSION = 7
|
||||
reported = set()
|
||||
|
||||
|
||||
|
|
@ -106,10 +112,6 @@ def expand_env(val, env, extra_env, used_extra=None):
|
|||
|
||||
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
|
||||
|
|
@ -119,17 +121,23 @@ class LibAlias(object):
|
|||
self.descr = None
|
||||
|
||||
@staticmethod
|
||||
def parse(options, cline, env, extra_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, extra_env))
|
||||
lib.options = un_quote(m.group(4))
|
||||
lib.descr = un_quote(m.group(5))
|
||||
return lib
|
||||
def parse(items, env, extra_env):
|
||||
s = LibAlias()
|
||||
for i in items[1:]:
|
||||
i_type = _check_is_symbol_list(i)
|
||||
if i_type == 'name':
|
||||
s.name = _check_relaxed(i, 1, i_type)
|
||||
elif i_type == 'type':
|
||||
s.type = _check_relaxed(i, 1, i_type)
|
||||
elif i_type == 'uri':
|
||||
s.uri = os.path.abspath(expand_env(_check_relaxed(i, 1, i_type), env, extra_env))
|
||||
elif i_type == 'options':
|
||||
s.options = _check_relaxed(i, 1, i_type)
|
||||
elif i_type == 'descr':
|
||||
s.descr = _check_relaxed(i, 1, i_type)
|
||||
else:
|
||||
logger.warning(W_LIBTUNK+'Unknown lib table attribute `{}`'.format(i))
|
||||
return s
|
||||
|
||||
def __str__(self):
|
||||
if not self.name:
|
||||
|
|
@ -148,7 +156,8 @@ class KiConf(object):
|
|||
models_3d_dir = None
|
||||
party_3rd_dir = None
|
||||
kicad_env = {}
|
||||
lib_aliases = {}
|
||||
lib_aliases = None
|
||||
fp_aliases = None
|
||||
aliases_3D = {}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -162,9 +171,13 @@ class KiConf(object):
|
|||
KiConf.dirname = os.path.dirname(fname)
|
||||
KiConf.kicad_env['KIPRJMOD'] = KiConf.dirname
|
||||
KiConf.load_kicad_common()
|
||||
KiConf.load_all_lib_aliases()
|
||||
KiConf.load_3d_aliases()
|
||||
KiConf.loaded = True
|
||||
# Loaded on demand, here to debug
|
||||
# KiConf.get_sym_lib_aliases()
|
||||
# logger.error(KiConf.lib_aliases)
|
||||
# KiConf.get_fp_lib_aliases()
|
||||
# logger.error(KiConf.fp_aliases)
|
||||
|
||||
def find_kicad_common():
|
||||
""" Looks for kicad_common config file.
|
||||
|
|
@ -431,42 +444,41 @@ class KiConf(object):
|
|||
os.environ[k] = v
|
||||
logger.debug('Exporting {}="{}"'.format(k, v))
|
||||
|
||||
def load_lib_aliases(fname):
|
||||
def load_lib_aliases(fname, lib_aliases):
|
||||
if not os.path.isfile(fname):
|
||||
return False
|
||||
logger.debug('Loading symbols lib table `{}`'.format(fname))
|
||||
version = 0
|
||||
with open(fname, 'rt') as f:
|
||||
line = f.readline().strip()
|
||||
if line != '(sym_lib_table':
|
||||
raise KiConfError('Symbol libs table missing signature', fname, 1, line)
|
||||
line = f.readline()
|
||||
cline = 2
|
||||
lib_regex = re.compile(r'\(lib\s*(.*)\)')
|
||||
ver_regex = re.compile(r'\(version\s*(.*)\)')
|
||||
while line and line[0] != ')':
|
||||
line = line.strip()
|
||||
m = lib_regex.match(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:
|
||||
m = ver_regex.match(line)
|
||||
if m:
|
||||
version = int(m.group(1))
|
||||
logger.debug('Symbols library table version {}'.format(version))
|
||||
else:
|
||||
raise KiConfError('Unknown symbol table entry', fname, cline, line)
|
||||
line = f.readline()
|
||||
cline += 1
|
||||
error = None
|
||||
try:
|
||||
table = load(f)[0]
|
||||
except SExpData as e:
|
||||
error = str(e)
|
||||
if error:
|
||||
raise KiPlotConfigurationError('Error loading `{}`: {}'.format(fname, error))
|
||||
if not isinstance(table, list) or (table[0].value() != 'sym_lib_table' and table[0].value() != 'fp_lib_table'):
|
||||
raise KiPlotConfigurationError('Error loading `{}`: not a library table'.format(fname))
|
||||
for e in table[1:]:
|
||||
e_type = _check_is_symbol_list(e)
|
||||
if e_type == 'version':
|
||||
version = _check_integer(e, 1, e_type)
|
||||
if version > SUP_VERSION:
|
||||
logger.warning(W_LIBTVERSION+"Unsupported lib table version, loading could fail")
|
||||
elif e_type == 'lib':
|
||||
alias = LibAlias.parse(e, KiConf.kicad_env, {})
|
||||
if GS.debug_level > 1:
|
||||
logger.debug('- Adding lib alias '+str(alias))
|
||||
lib_aliases[alias.name] = alias
|
||||
else:
|
||||
logger.warning(W_LIBTUNK+"Unknown lib table entry `{}`".format(e_type))
|
||||
return True
|
||||
|
||||
def load_all_lib_aliases():
|
||||
def load_all_lib_aliases(table_name, sys_dir, pattern):
|
||||
# Load the default symbol libs table.
|
||||
# This is the list of libraries enabled by the user.
|
||||
loaded = False
|
||||
lib_aliases = {}
|
||||
if KiConf.config_dir:
|
||||
conf_dir = KiConf.config_dir
|
||||
if 'KICAD_CONFIG_HOME' in KiConf.kicad_env:
|
||||
|
|
@ -474,22 +486,39 @@ class KiConf(object):
|
|||
# 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))
|
||||
loaded = KiConf.load_lib_aliases(os.path.join(conf_dir, table_name), lib_aliases)
|
||||
if not loaded and 'KICAD_TEMPLATE_DIR' in KiConf.kicad_env:
|
||||
loaded = KiConf.load_lib_aliases(os.path.join(KiConf.kicad_env['KICAD_TEMPLATE_DIR'], SYM_LIB_TABLE))
|
||||
loaded = KiConf.load_lib_aliases(os.path.join(KiConf.kicad_env['KICAD_TEMPLATE_DIR'], table_name), lib_aliases)
|
||||
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')):
|
||||
logger.error(os.path.join(sys_dir, pattern))
|
||||
for f in glob(os.path.join(sys_dir, pattern)):
|
||||
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
|
||||
lib_aliases[alias.name] = alias
|
||||
# Load the project's table
|
||||
KiConf.load_lib_aliases(os.path.join(KiConf.dirname, SYM_LIB_TABLE))
|
||||
KiConf.load_lib_aliases(os.path.join(KiConf.dirname, table_name), lib_aliases)
|
||||
return lib_aliases
|
||||
|
||||
def get_sym_lib_aliases(fname=None):
|
||||
if KiConf.lib_aliases is None:
|
||||
fname |= GS.sch_file
|
||||
KiConf.init(fname)
|
||||
pattern = '*.kicad_sym' if GS.ki6 else '*.lib'
|
||||
KiConf.lib_aliases = KiConf.load_all_lib_aliases(SYM_LIB_TABLE, KiConf.sym_lib_dir, pattern)
|
||||
return KiConf.lib_aliases
|
||||
|
||||
def get_fp_lib_aliases(fname=None):
|
||||
if KiConf.fp_aliases is None:
|
||||
fname |= GS.pcb_file
|
||||
KiConf.init(fname)
|
||||
KiConf.fp_aliases = KiConf.load_all_lib_aliases(FP_LIB_TABLE, KiConf.footprint_dir, '*.pretty')
|
||||
return KiConf.fp_aliases
|
||||
|
||||
def load_3d_aliases():
|
||||
if not KiConf.config_dir:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from datetime import datetime
|
|||
from copy import deepcopy
|
||||
from collections import OrderedDict
|
||||
from .config import KiConf, un_quote
|
||||
from .error import SchError, SchFileError, SchLibError
|
||||
from ..gs import GS
|
||||
from ..misc import (W_BADPOLI, W_POLICOORDS, W_BADSQUARE, W_BADCIRCLE, W_BADARC, W_BADTEXT, W_BADPIN, W_BADCOMP, W_BADDRAW,
|
||||
W_UNKDCM, W_UNKAR, W_ARNOPATH, W_ARNOREF, W_MISCFLD, W_EXTRASPC, W_NOLIB, W_INCPOS, W_NOANNO, W_MISSLIB,
|
||||
|
|
@ -26,24 +27,6 @@ from .. import log
|
|||
logger = log.get_logger()
|
||||
|
||||
|
||||
class SchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SchFileError(SchError):
|
||||
def __init__(self, msg, code, reader):
|
||||
super().__init__()
|
||||
self.line = reader.line
|
||||
self.file = reader.file
|
||||
self.msg = msg
|
||||
self.code = code
|
||||
|
||||
|
||||
class SchLibError(SchFileError):
|
||||
def __init__(self, msg, code, reader):
|
||||
super().__init__(msg, code, reader)
|
||||
|
||||
|
||||
class LineReader(object):
|
||||
def __init__(self, f, file):
|
||||
super().__init__()
|
||||
|
|
@ -1695,10 +1678,10 @@ class Schematic(object):
|
|||
logger.debug('Filling desc for {}:{} `{}`'.format(c.lib, c.name, c.desc))
|
||||
|
||||
def load_libs(self, fname):
|
||||
KiConf.init(fname)
|
||||
aliases = KiConf.get_sym_lib_aliases(fname)
|
||||
# Try to find the library paths
|
||||
for k in self.libs.keys():
|
||||
alias = KiConf.lib_aliases.get(k)
|
||||
alias = aliases.get(k)
|
||||
if k and alias:
|
||||
self.libs[k] = alias.uri
|
||||
if GS.debug_level > 1:
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@ from collections import OrderedDict
|
|||
from ..gs import GS
|
||||
from .. import log
|
||||
from ..misc import W_NOLIB, W_UNKFLD, W_MISSCMP
|
||||
from .v5_sch import SchError, SchematicComponent, Schematic
|
||||
from .error import SchError
|
||||
from .sexpdata import load, SExpData, Symbol, dumps, Sep
|
||||
from .sexp_helpers import (_check_is_symbol_list, _check_len, _check_len_total, _check_symbol, _check_hide, _check_integer,
|
||||
_check_float, _check_str, _check_symbol_value, _check_symbol_float, _check_symbol_int,
|
||||
_check_symbol_str, _get_offset, _get_yes_no, _get_at, _get_size, _get_xy, _get_points)
|
||||
from .v5_sch import SchematicComponent, Schematic
|
||||
|
||||
logger = log.get_logger()
|
||||
CROSSED_LIB = 'kibot_crossed'
|
||||
|
|
@ -28,143 +32,6 @@ SHEET_FILE = {'Sheet file', 'Sheetfile'}
|
|||
SHEET_NAME = {'Sheet name', 'Sheetname'}
|
||||
|
||||
|
||||
def _check_is_symbol_list(e, allow_orphan_symbol=()):
|
||||
# Each entry is a list
|
||||
if not isinstance(e, list):
|
||||
if isinstance(e, Symbol):
|
||||
name = e.value()
|
||||
if name in allow_orphan_symbol:
|
||||
return name
|
||||
raise SchError('Orphan symbol `{}`'.format(e.value()))
|
||||
else:
|
||||
raise SchError('Orphan data `{}`'.format(e))
|
||||
# The first element is a symbol
|
||||
if not isinstance(e[0], Symbol):
|
||||
raise SchError('Orphan data `{}`'.format(e[0]))
|
||||
return e[0].value()
|
||||
|
||||
|
||||
def _check_len(items, pos, name):
|
||||
if len(items) < pos+1:
|
||||
raise SchError('Missing argument {} in `{}`'.format(pos, name))
|
||||
return items[pos]
|
||||
|
||||
|
||||
def _check_len_total(items, num, name):
|
||||
if len(items) != num:
|
||||
raise SchError('Wrong number of attributes for {} `{}`'.format(name, items))
|
||||
|
||||
|
||||
def _check_symbol(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, Symbol):
|
||||
raise SchError('{} is not a Symbol `{}`'.format(name, value))
|
||||
return value.value()
|
||||
|
||||
|
||||
def _check_hide(items, pos, name):
|
||||
value = _check_symbol(items, pos, name + ' hide')
|
||||
if value != 'hide':
|
||||
raise SchError('Found Symbol `{}` when `hide` expected'.format(value))
|
||||
return True
|
||||
|
||||
|
||||
def _check_integer(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, int):
|
||||
raise SchError('{} is not an integer `{}`'.format(name, value))
|
||||
return value
|
||||
|
||||
|
||||
def _check_float(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, (float, int)):
|
||||
raise SchError('{} is not a float `{}`'.format(name, value))
|
||||
return value
|
||||
|
||||
|
||||
def _check_str(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, str):
|
||||
raise SchError('{} is not a string `{}`'.format(name, value))
|
||||
return value
|
||||
|
||||
|
||||
def _check_relaxed(items, pos, name):
|
||||
value = _check_len(items, pos, name)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, Symbol):
|
||||
return value.value()
|
||||
if isinstance(value, (float, int)):
|
||||
return str(value)
|
||||
raise SchError('{} is not a string, Symbol or number `{}`'.format(name, value))
|
||||
|
||||
|
||||
def _check_symbol_value(items, pos, name, sym):
|
||||
value = _check_len(items, pos, name)
|
||||
if not isinstance(value, list) or not isinstance(value[0], Symbol) or value[0].value() != sym:
|
||||
raise SchError('Missing `{}` in `{}`'.format(sym, name))
|
||||
return value
|
||||
|
||||
|
||||
def _check_symbol_float(items, pos, name, sym):
|
||||
name += ' ' + sym
|
||||
values = _check_symbol_value(items, pos, name, sym)
|
||||
return _check_float(values, 1, name)
|
||||
|
||||
|
||||
def _check_symbol_int(items, pos, name, sym):
|
||||
name += ' ' + sym
|
||||
values = _check_symbol_value(items, pos, name, sym)
|
||||
return _check_integer(values, 1, name)
|
||||
|
||||
|
||||
def _check_symbol_str(items, pos, name, sym):
|
||||
name += ' ' + sym
|
||||
values = _check_symbol_value(items, pos, name, sym)
|
||||
return _check_str(values, 1, name)
|
||||
|
||||
|
||||
def _get_offset(items, pos, name):
|
||||
value = _check_symbol_value(items, pos, name, 'offset')
|
||||
return _check_float(value, 1, 'offset')
|
||||
|
||||
|
||||
def _get_yes_no(items, pos, name):
|
||||
sym = _check_symbol(items, pos, name)
|
||||
return sym == 'yes'
|
||||
|
||||
|
||||
def _get_id(items, pos, name):
|
||||
value = _check_symbol_value(items, pos, name, 'id')
|
||||
return _check_integer(value, 1, 'id')
|
||||
|
||||
|
||||
def _get_at(items, pos, name):
|
||||
value = _check_symbol_value(items, pos, name, 'at')
|
||||
angle = 0
|
||||
if len(value) > 3:
|
||||
angle = _check_float(value, 3, 'at angle')
|
||||
return _check_float(value, 1, 'at x'), _check_float(value, 2, 'at y'), angle
|
||||
|
||||
|
||||
def _get_size(items, pos, name):
|
||||
value = _check_symbol_value(items, pos, name, 'size')
|
||||
return _get_xy(value)
|
||||
|
||||
|
||||
class Point(object):
|
||||
def __init__(self, items):
|
||||
super().__init__()
|
||||
self.x = _check_float(items, 1, 'x coord')
|
||||
self.y = _check_float(items, 2, 'y coord')
|
||||
|
||||
@staticmethod
|
||||
def parse(items):
|
||||
return Point(items)
|
||||
|
||||
|
||||
class PointXY(object):
|
||||
def __init__(self, x, y):
|
||||
super().__init__()
|
||||
|
|
@ -210,23 +77,6 @@ class Box(object):
|
|||
self.y2 = max(self.y2, b.y2)
|
||||
|
||||
|
||||
def _get_xy(items):
|
||||
if len(items) != 3:
|
||||
raise SchError('Point definition with wrong args (`{}`)'.format(items))
|
||||
return Point.parse(items)
|
||||
|
||||
|
||||
def _get_points(items):
|
||||
points = []
|
||||
for i in items[1:]:
|
||||
i_type = _check_is_symbol_list(i)
|
||||
if i_type == 'xy':
|
||||
points.append(_get_xy(i))
|
||||
else:
|
||||
raise SchError('Unknown points attribute `{}`'.format(i))
|
||||
return points
|
||||
|
||||
|
||||
class FontEffects(object):
|
||||
""" Class used to describe text attributes """
|
||||
def __init__(self):
|
||||
|
|
|
|||
|
|
@ -259,6 +259,8 @@ W_BADPCB3DSTK = '(W115) '
|
|||
W_EEDA3D = '(W116) '
|
||||
W_MICROVIAS = '(W117) '
|
||||
W_BLINDVIAS = '(W118) '
|
||||
W_LIBTVERSION = '(W119) '
|
||||
W_LIBTUNK = '(W120) '
|
||||
# Somehow arbitrary, the colors are real, but can be different
|
||||
PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"}
|
||||
PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e",
|
||||
|
|
|
|||
Loading…
Reference in New Issue