• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

INTI-CMNB / KiBot / 30639695604

31 Jul 2026 02:19PM UTC coverage: 87.64% (-0.02%) from 87.66%
30639695604

push

github

set-soft
[Layer] Added KiPy support to all Layer members

103 of 123 new or added lines in 3 files covered. (83.74%)

91 existing lines in 3 files now uncovered.

33829 of 38600 relevant lines covered (87.64%)

8.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.56
/kibot/kiplot.py
1
# -*- coding: utf-8 -*-
2
# Copyright (c) 2020-2026 Salvador E. Tropea
3
# Copyright (c) 2020-2026 Instituto Nacional de Tecnología Industrial
4
# Copyright (c) 2018 John Beard
5
# License: AGPL-3.0
6
# Project: KiBot (formerly KiPlot)
7
# Adapted from: https://github.com/johnbeard/kiplot
8
"""
7✔
9
Main KiBot code
10
"""
11
import atexit
16✔
12
from copy import deepcopy
16✔
13
from collections import OrderedDict
16✔
14
import gzip
16✔
15
import json
16✔
16
import os
16✔
17
import re
16✔
18
from sys import path as sys_path
16✔
19
from shutil import which, copy2
16✔
20
from subprocess import run, PIPE, STDOUT, Popen, CalledProcessError
16✔
21
from glob import glob
16✔
22
from importlib.util import spec_from_file_location, module_from_spec
16✔
23

24
from .bom.columnlist import ColumnList
16✔
25
from .gs import GS
16✔
26
from .registrable import RegOutput
16✔
27
from .misc import (PLOT_ERROR, CORRUPTED_PCB, EXIT_BAD_ARGS, CORRUPTED_SCH, version_str2tuple,
16✔
28
                   EXIT_BAD_CONFIG, WRONG_INSTALL, UI_SMD, UI_VIRTUAL, TRY_INSTALL_CHECK, MOD_SMD, MOD_THROUGH_HOLE,
29
                   MOD_VIRTUAL, W_PCBNOSCH, W_NONEEDSKIP, W_WRONGCHAR, name2make, W_TIMEOUT, W_KIAUTO, W_VARSCH,
30
                   NO_SCH_FILE, NO_PCB_FILE, W_VARPCB, NO_YAML_MODULE, WRONG_ARGUMENTS, FAILED_EXECUTE, W_VALMISMATCH,
31
                   MOD_EXCLUDE_FROM_POS_FILES, MOD_EXCLUDE_FROM_BOM, MOD_BOARD_ONLY, hide_stderr, W_MAXDEPTH, DONT_STOP,
32
                   W_BADREF, try_decode_utf8, MISSING_FILES, KICAD_VERSION_9_0_1, W_NOUUIDMAP, W_SILLY, W_REPREF, KIPY_ERROR)
33
from .error import PlotError, KiPlotConfigurationError, config_error, KiPlotError
16✔
34
from .config_reader import CfgYamlReader
16✔
35
from .pre_base import BasePreFlight
16✔
36
from .dep_downloader import register_deps
16✔
37
import kibot.dep_downloader as dep_downloader
16✔
38
from .kicad.v5_sch import Schematic, SchFileError, SchError, SchematicField
16✔
39
from .kicad.v6_sch import SchematicV6, SchematicComponentV6
16✔
40
from .kicad.config import KiConfError, KiConf, expand_env
16✔
41
from . import log
16✔
42
INTERNAL_FIELDS = {'reference', 'value', 'footprint', 'datasheet', 'description'}
16✔
43

44
logger = log.get_logger()
16✔
45
# Cache to avoid running external many times to check their versions
46
script_versions = {}
16✔
47
actions_loaded = False
16✔
48
needed_imports = {}
16✔
49

50
try:
16✔
51
    import yaml
16✔
52
except ImportError:
×
53
    log.init()
×
UNCOV
54
    GS.exit_with_error(['No yaml module for Python, install python3-yaml', TRY_INSTALL_CHECK], NO_YAML_MODULE)
×
55

56

57
def cased_path(path):
16✔
58
    r = glob(re.sub(r'([^:/\\])(?=[/\\]|$)|\[', r'[\g<0>]', path))
16✔
59
    return r and r[0] or path
16✔
60

61

62
def try_register_deps(mod, name):
16✔
63
    if mod.__doc__:
16✔
64
        try:
16✔
65
            data = yaml.safe_load(mod.__doc__)
16✔
66
        except yaml.YAMLError as e:
×
UNCOV
67
            config_error([f'While loading plug-in `{name}`:', "Error loading YAML "+str(e)])
×
68
        register_deps(name, data)
16✔
69

70

71
def _import(name, path):
16✔
72
    # Python 3.4+ import mechanism
73
    spec = spec_from_file_location("kibot."+name, path)
16✔
74
    mod = module_from_spec(spec)
16✔
75
    try:
16✔
76
        spec.loader.exec_module(mod)
16✔
77
    except ImportError as e:
1✔
78
        GS.exit_with_error(('Unable to import plug-ins: '+str(e),
1✔
79
                            'Make sure you used `--no-compile` if you used pip for installation',
80
                            'Python path: '+str(sys_path)), WRONG_INSTALL)
81
    try_register_deps(mod, name)
16✔
82

83

84
def _load_actions(path, load_internals=False, progress=None):
16✔
85
    logger.debug("Importing from "+path)
16✔
86
    lst = glob(os.path.join(path, 'out_*.py')) + glob(os.path.join(path, 'pre_*.py'))
16✔
87
    lst += glob(os.path.join(path, 'var_*.py')) + glob(os.path.join(path, 'fil_*.py'))
16✔
88
    if load_internals:
16✔
89
        lst += [os.path.join(path, 'globals.py')]
16✔
90
    for p in sorted(lst):
16✔
91
        name = os.path.splitext(os.path.basename(p))[0]
16✔
92
        msg = "Importing "+name
16✔
93
        logger.debug('- '+msg)
16✔
94
        if progress:
16✔
95
            progress(msg)
1✔
96
        _import(name, p)
16✔
97

98

99
def load_actions(progress=None):
16✔
100
    """ Load all the available outputs and preflights """
101
    global actions_loaded
102
    if actions_loaded:
13✔
103
        return
5✔
104
    actions_loaded = True
13✔
105
    try_register_deps(dep_downloader, 'global')
13✔
106
    from kibot.mcpyrate import activate
13✔
107
    # activate.activate()
108
    _load_actions(os.path.abspath(os.path.dirname(__file__)), True, progress)
13✔
109
    home = os.environ.get('HOME')
13✔
110
    if home:
13✔
111
        dir = os.path.join(home, '.config', 'kiplot', 'plugins')
13✔
112
        if os.path.isdir(dir):
13✔
113
            _load_actions(dir)
4✔
114
        dir = os.path.join(home, '.config', 'kibot', 'plugins')
13✔
115
        if os.path.isdir(dir):
13✔
116
            _load_actions(dir)
4✔
117
    # de_activate in old mcpy
118
    if 'deactivate' in activate.__dict__:
13✔
119
        logger.debug('Deactivating macros')
13✔
120
        activate.deactivate()
13✔
121

122

123
def extract_errors(text):
13✔
124
    in_error = in_warning = False
13✔
125
    msg = ''
13✔
126
    for line in text.split('\n'):
13✔
127
        line += '\n'
13✔
128
        if line[0] == ' ' and (in_error or in_warning):
13✔
129
            msg += line
8✔
130
        else:
131
            if in_error:
13✔
132
                in_error = False
11✔
133
                logger.error(msg.rstrip())
11✔
134
            elif in_warning:
13✔
135
                in_warning = False
13✔
136
                logger.warning(W_KIAUTO+msg.rstrip())
13✔
137
        if line.startswith('ERROR:'):
13✔
138
            in_error = True
11✔
139
            msg = line[6:]
11✔
140
        elif line.startswith('WARNING:'):
13✔
141
            in_warning = True
13✔
142
            msg = line[8:]
13✔
143
    if in_error:
13✔
144
        in_error = False
145
        logger.error(msg.rstrip())
146
    elif in_warning:
13✔
147
        in_warning = False
148
        logger.warning(W_KIAUTO+msg.rstrip())
149

150

151
def debug_output(res):
13✔
152
    if res:
13✔
153
        logger.debug('- Output from command: '+res)
13✔
154

155

156
def _run_command(command, change_to):
13✔
157
    return run(command, check=True, stdout=PIPE, stderr=STDOUT, cwd=change_to)
13✔
158

159

160
def run_command(command, change_to=None, just_raise=False, use_x11=False, err_msg=None, err_lvl=FAILED_EXECUTE,
13✔
161
                force_en=False):
162
    logger.debug('- Executing: '+GS.pasteable_cmd(command))
13✔
163
    if change_to is not None:
13✔
164
        logger.debug('- CWD: '+change_to)
12✔
165
    old_lang = None
13✔
166
    if force_en:
13✔
167
        old_lang = os.environ.get('LANG')
12✔
168
        if old_lang:
12✔
169
            os.environ['LANG'] = 'en'
170
    try:
13✔
171
        if use_x11 and not GS.on_windows:
13✔
172
            logger.debug('Using Xvfb to run the command')
8✔
173
            from xvfbwrapper import Xvfb
8✔
174
            with Xvfb(width=640, height=480, colordepth=24):
8✔
175
                res = _run_command(command, change_to)
8✔
176
        else:
177
            res = _run_command(command, change_to)
13✔
178
    except CalledProcessError as e:
9✔
179
        if just_raise:
9✔
180
            raise
8✔
181
        if err_msg is not None:
5✔
182
            err_msg = err_msg.format(ret=e.returncode)
5✔
183
        GS.exit_with_error(err_msg, err_lvl, e)
5✔
184
    finally:
185
        if old_lang:
13✔
186
            os.environ['LANG'] = old_lang
5✔
187

188
    msg = try_decode_utf8(res.stdout, f'output from `{command[0]}` command', logger, GS.global_code_page_fallback)
13✔
189
    debug_output(msg)
13✔
190
    return msg.rstrip()
13✔
191

192

193
def exec_with_retry(cmd, exit_with=None):
13✔
194
    cmd_str = GS.pasteable_cmd(cmd)
13✔
195
    logger.debug('Executing: '+cmd_str)
13✔
196
    if GS.debug_level > 2:
13✔
197
        logger.debug('Command line: '+str(cmd))
8✔
198
    retry = 2
13✔
199
    while retry:
13✔
200
        result = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
13✔
201
        ret = result.returncode
13✔
202
        retry -= 1
13✔
203
        if ret != 16 and (ret > 0 and ret < 128 and retry):
13✔
204
            # 16 is KiCad crash
205
            logger.debug('Failed with error {}, retrying ...'.format(ret))
6✔
206
        else:
207
            extract_errors(result.stderr)
13✔
208
            err = '> '+result.stderr.replace('\n', '\n> ')
13✔
209
            logger.debug('Output from command:\n'+err)
13✔
210
            if 'Timed out' in err:
13✔
211
                logger.warning(W_TIMEOUT+'Time out detected, on slow machines or complex projects try:')
212
                logger.warning(W_TIMEOUT+'`kiauto_time_out_scale` and/or `kiauto_wait_start` global options')
213
            if exit_with is not None and ret:
13✔
214
                GS.exit_with_error(cmd[0]+' returned '+str(ret), exit_with)
3✔
215
            return ret
13✔
216

217

218
def _load_board(pcb_file, pcbnew):
13✔
219
    with hide_stderr():
13✔
220
        board = pcbnew.LoadBoard(pcb_file)
13✔
221
    if board is None:
13✔
222
        # KiCad 9 doesn't stop and returns None
223
        GS.exit_with_error(f'Error loading PCB file ({pcb_file}). Corrupted?', CORRUPTED_PCB)
4✔
224
        raise OSError
225
    return board
13✔
226

227

228
def load_board(pcb_file=None, forced=False):
13✔
229
    if GS.board is not None and not forced:
13✔
230
        # Already loaded
231
        return GS.board
13✔
232
    if not pcb_file:
13✔
233
        GS.check_pcb()
13✔
234
        pcb_file = GS.pcb_file
13✔
235
    return load_board_pn(pcb_file) if GS.pn is not None else load_board_kp(pcb_file)
13✔
236

237

238
def close_kipy_pcb():
13✔
239
    logger.error("Close")
240
    if GS.kp_pcb is not None:
241
        GS.kp_pcb.close()
242

243

244
def load_board_kp(pcb_file):
13✔
245
    kipy = GS.kp
246
    # Check if we already have server running
247
    if GS.kp_pcb is None:
248
        # No API server running, run it
249
        try:
250
            GS.kp_pcb = kipy.KiCad(client_name=f"KiBot ({os.getpid()})", headless=True, file_path=pcb_file)
251
        except Exception as e:
252
            GS.exit_with_error(f"Error starting KiCad API server: {e}", KIPY_ERROR)
253
        atexit
254
        atexit.register(close_kipy_pcb)
255
    else:
256
        # Ask to load this PCB
257
        try:
258
            GS.kp_pcb.open_document(pcb_file, kipy.proto.common.types.DocumentType.DOCTYPE_PCB)
259
        except Exception as e:
260
            GS.exit_with_error(f'Error loading PCB file ({pcb_file}): {e}', CORRUPTED_PCB)
261

262
    GS.board = board = GS.kp_pcb.get_board()
263

264
    # Verify GS.global_work_layer
265
    board.kibot_layer_ids = {board.get_layer_name(id): id for id in range(128)}
266
    if GS.global_work_layer and GS.global_work_layer not in board.kibot_layer_ids:
267
        raise KiPlotConfigurationError(f"Unknown layer used for the global `work_layer` option"
268
                                       f" (`{GS.global_work_layer}`)")
269

270
    # TODO: kipy doesn't have Get/SetProperties
271

272
    if BasePreFlight.get_option('check_zone_fills'):
273
        board.refill_zones(block=True)
274

275
    # TODO: kipy: is the dimensions workaround needed? Is covered by a test?
276

277
    logger.debug("Board loaded")
278
    return board
279

280

281
def load_board_pn(pcb_file):
13✔
282
    pcbnew = GS.pn
13✔
283
    try:
13✔
284
        board = _load_board(pcb_file, pcbnew)
13✔
285
        if GS.global_work_layer and board.GetLayerID(GS.global_work_layer) < 0:
13✔
286
            raise KiPlotConfigurationError(f"Unknown layer used for the global `work_layer` option"
287
                                           f" (`{GS.global_work_layer}`)")
288

289
        if (GS.global_invalidate_pcb_text_cache == 'yes' or GS.global_update_pcb_text_cache == 'yes'):
13✔
290
            # Workaround for unexpected KiCad behavior:
291
            # https://gitlab.com/kicad/code/kicad/-/issues/14360
292
            logger.debug('Current PCB text variables cache: {}'.format(board.GetProperties().items()))
9✔
293

294
            props = pcbnew.MAP_STRING_STRING()
9✔
295

296
            if GS.global_update_pcb_text_cache == 'yes':
9✔
297
                for k, v in GS.load_pro_variables().items():
1✔
298
                    props[k] = v
1✔
299
                logger.debug('Updating cached text variables')
1✔
300
            else:
301
                logger.debug('Removing cached text variables')
8✔
302
            board.SetProperties(props)
9✔
303
            # Save the PCB, so external tools also gets the reset, i.e. panelize, see #652
304
            GS.save_pcb(pcb_file, board)
9✔
305
            # KiCad 10 has QRs that might be generated from variable, they need a reload
306
            if GS.ki10:
9✔
307
                logger.debug('Reloading the PCB to workaround KiCad bug #14360')
3✔
308
                board = _load_board(pcb_file, pcbnew)
3✔
309
        if BasePreFlight.get_option('check_zone_fills'):
13✔
310
            GS.fill_zones(board)
8✔
311
        if GS.global_units:
13✔
312
            # In KiCad 6 "dimensions" has units.
313
            # The default value is DIM_UNITS_MODE_AUTOMATIC.
314
            # But this has a meaning only in the GUI where you have default units.
315
            # So now we have global.units and here we patch the board.
316
            if GS.kicad_version_n < KICAD_VERSION_9_0_1:
4✔
317
                UNIT_NAME_TO_INDEX = {'millimeters': pcbnew.DIM_UNITS_MODE_MILLIMETRES,
2✔
318
                                      'inches': pcbnew.DIM_UNITS_MODE_INCHES,
319
                                      'mils': pcbnew.DIM_UNITS_MODE_MILS}
320
            else:
321
                UNIT_NAME_TO_INDEX = {'millimeters': pcbnew.DIM_UNITS_MODE_MM,
2✔
322
                                      'inches': pcbnew.DIM_UNITS_MODE_INCH,
323
                                      'mils': pcbnew.DIM_UNITS_MODE_MILS}
324
            forced_units = UNIT_NAME_TO_INDEX[GS.global_units]
4✔
325
            for dr in board.GetDrawings():
4✔
326
                if dr.GetClass().startswith('PCB_DIM_') and dr.GetUnitsMode() == pcbnew.DIM_UNITS_MODE_AUTOMATIC:
4✔
327
                    dr.SetUnitsMode(forced_units)
4✔
328
                    dr.Update()
4✔
329
        if GS.ki8 and not GS.ki9:
13✔
330
            # KiCad 8.0.2 crazyness: hidden text affects scaling, even when not plotted
331
            # So a PRL can affect the plot mechanism
332
            # https://gitlab.com/kicad/code/kicad/-/issues/17958
333
            # https://gitlab.com/kicad/code/kicad/-/commit/8184ed64e732ed0812831a13ebc04bd12e8d1d19
334
            board.SetElementVisibility(pcbnew.LAYER_HIDDEN_TEXT, False)
1✔
335
        GS.board = board
13✔
336
    except OSError as e:
8✔
337
        GS.exit_with_error(['Error loading PCB file. Corrupted?', str(e)], CORRUPTED_PCB)
4✔
338
    assert board is not None
13✔
339
    logger.debug("Board loaded")
13✔
340
    return board
13✔
341

342

343
def ki_conf_error(e):
13✔
344
    GS.exit_with_error(('At line {} of `{}`: {}'.format(e.line, e.file, e.msg),
345
                        'Line content: `{}`'.format(e.code.rstrip())), EXIT_BAD_CONFIG)
346

347

348
def load_any_sch(file, project, fatal=True, extra_msg=None):
13✔
349
    if file[-9:] == 'kicad_sch':
13✔
350
        sch = SchematicV6()
13✔
351
        load_libs = False
13✔
352
    else:
353
        sch = Schematic()
354
        load_libs = True
355
    # Schematic UUID mapping
356
    # Check if we have a project
357
    mapped_uuid = None
13✔
358
    if GS.pro_file and GS.ki10:
13✔
359
        # Projects can override the SCH UUID
360
        file_base = os.path.basename(file)
3✔
361
        try:
3✔
362
            with open(GS.pro_file, 'rt') as f:
3✔
363
                data = json.load(f)
3✔
364
            for mentry in data["schematic"]["top_level_sheets"]:
3✔
365
                if mentry["filename"] == file_base:
3✔
366
                    mapped_uuid = mentry["uuid"]
3✔
367
                    logger.debug(f"Mapping schematic `{file_base}` to UUID `{mapped_uuid}` found in `{GS.pro_file}`")
3✔
368
                    break
3✔
369
        except Exception as e:
2✔
370
            # This is not fatal
371
            logger.debug(f"Failed to get the top level sheets: {e}")
2✔
372
        if mapped_uuid is None:
3✔
373
            logger.warning(W_NOUUIDMAP+"Unable to map the SCH file to an UUID")
2✔
374
    # End of schematic UUID mapping
375
    try:
13✔
376
        sch.load(file, project, mapped_uuid=mapped_uuid)
13✔
377
        if load_libs:
13✔
378
            sch.load_libs(file)
379
        if GS.debug_level > 1:
13✔
380
            logger.debug('Schematic dependencies: '+str(sch.get_files()))
12✔
381
    except SchFileError as e:
4✔
382
        if extra_msg is not None:
383
            logger.error(extra_msg)
384
        GS.exit_with_error(('At line {} of `{}`: {}'.format(e.line, e.file, e.msg),
385
                            'Line content: `{}`'.format(e.code)), CORRUPTED_SCH if fatal else DONT_STOP)
386
    except SchError as e:
4✔
387
        if extra_msg is not None:
4✔
388
            logger.error(extra_msg)
389
        GS.exit_with_error(('While loading `{}`'.format(file), str(e)), CORRUPTED_SCH if fatal else DONT_STOP)
4✔
390
    except KiConfError as e:
391
        ki_conf_error(e)
392
    return sch
13✔
393

394

395
def load_sch(sch_file=None, forced=False):
13✔
396
    if GS.sch is not None and not forced:  # Already loaded
13✔
397
        return
13✔
398
    if not sch_file:
13✔
399
        GS.check_sch()
13✔
400
        sch_file = GS.sch_file
13✔
401
    GS.sch = load_any_sch(sch_file, os.path.splitext(os.path.basename(sch_file))[0])
13✔
402

403

404
def create_component_from_footprint(m, ref, env, real_fields):
13✔
405
    c = SchematicComponentV6()
12✔
406
    c.f_ref = c.ref = ref
12✔
407
    c.name = m.GetValue()
12✔
408
    c.sheet_path_h = c.sheet_path = c.lib = ''
12✔
409
    c.project = GS.sch_basename
12✔
410
    c.id = m.m_Uuid.AsString() if hasattr(m, 'm_Uuid') else ''
12✔
411
    # Basic fields
412
    # Reference
413
    f = SchematicField()
12✔
414
    f.name = 'Reference'
12✔
415
    f.value = ref
12✔
416
    f.number = 0
12✔
417
    c.add_field(f)
12✔
418
    # Value
419
    f = SchematicField()
12✔
420
    f.name = 'Value'
12✔
421
    f.value = c.name
12✔
422
    f.number = 1
12✔
423
    c.add_field(f)
12✔
424
    # Footprint
425
    f = SchematicField()
12✔
426
    f.name = 'Footprint'
12✔
427
    lib = m.GetFPID()
12✔
428
    f.value = lib.GetUniStringLibId()
12✔
429
    f.number = 2
12✔
430
    c.add_field(f)
12✔
431
    # Datasheet
432
    f = SchematicField()
12✔
433
    f.name = 'Datasheet'
12✔
434
    f.value = real_fields.get('Datasheet', '~')
12✔
435
    f.number = 3
12✔
436
    c.add_field(f)
12✔
437
    # Other fields
438
    copy_fields(c, GS.get_fields(m), env)
12✔
439
    c._solve_fields(None)
12✔
440
    try:
12✔
441
        c.split_ref()
12✔
442
    except SchError:
3✔
443
        # Unusable ref, discard it
444
        logger.warning(f'{W_BADREF}Not including component `{ref}` in filters because it has a malformed reference '
3✔
445
                       f'@ PCB {GS.module_position(m)}')
446
        c = None
3✔
447
    return c
12✔
448

449

450
class PadProperty(object):
13✔
451
    pass
13✔
452

453

454
def expand_one_footprint_field(v, env, extra_env):
13✔
455
    new_value = v
13✔
456
    depth = 1
13✔
457
    used_extra = [False]
13✔
458
    while depth < GS.MAXDEPTH:
13✔
459
        new_value = expand_env(new_value, env, extra_env, used_extra=used_extra)
13✔
460
        if not used_extra[0]:
13✔
461
            break
13✔
462
        depth += 1
463
        if depth == GS.MAXDEPTH:
464
            logger.warning(W_MAXDEPTH+f'Too much nested variables replacements, possible loop ({v})')
465
    # Remove extra spaces as we did with the schematic values
466
    return new_value.strip()
13✔
467

468

469
def create_extra_env(fields):
13✔
470
    return {k.upper() if k.lower() in INTERNAL_FIELDS else k: v for k, v in fields.items()}
13✔
471

472

473
def copy_fields(c, real_fields, env, extra_env=None):
13✔
474
    extra_env = extra_env or create_extra_env(real_fields)
13✔
475
    expanded_fields = {k: expand_one_footprint_field(v, env, extra_env) for k, v in real_fields.items()}
13✔
476
    for name, value in real_fields.items():
13✔
477
        if c.is_field(name.lower()):
10✔
478
            # Already there
479
            old = c.get_field_value(name)
10✔
480
            if value and old != value and old != expanded_fields[name]:
10✔
481
                # Check if it was modified by a variant
482
                bkp_value = c.dfields_bkp.get(name) if c.dfields_bkp else None
4✔
483
                if not (bkp_value and value == bkp_value.value):
4✔
484
                    bkp_value = expand_one_footprint_field(bkp_value.value, env, extra_env) if bkp_value else None
4✔
485
                    if not (bkp_value and value == bkp_value):
4✔
486
                        logger.warning(f"{W_VALMISMATCH}{name} field mismatch for `{c.ref}` (SCH: `{old}` PCB: `{value}`)")
4✔
487
                if name == 'Reference':
4✔
488
                    logger.warning(f"{W_REPREF}Reference mismatch is normal for a KiKit panel with custom `renameref`")
2✔
489
                    c.set_ref(value)
2✔
490
                else:
491
                    c.set_field(name, value)
4✔
492
        else:
493
            # New one
494
            c.set_field(name, value)
9✔
495

496

497
def get_all_components(collapse=True):
13✔
498
    load_sch()
12✔
499
    comps = GS.sch.get_components(collapse=collapse)
12✔
500
    get_board_comps_data(comps)
12✔
501
    return comps
12✔
502

503

504
def get_board_comps_data(comps, kicad_variant=None):
13✔
505
    """ Add information from the PCB to the list of components from the schematic.
506
        Note that we do it every time the function is called to reset transformation filters like rot_footprint. """
507
    if not GS.pcb_file:
16✔
508
        return
10✔
509
    load_board()
16✔
510
    if GS.ki6:
16✔
511
        comps_hash = {c.sheet_full_path: c for c in comps}
16✔
512
        comps_hash_ref = {c.ref: c for c in comps}
16✔
513
    else:
UNCOV
514
        comps_hash = {c.ref: c for c in comps}
×
515
    # Reset the "has_pcb_info" flag
516
    for c in comps:
16✔
517
        c.has_pcb_info = False
16✔
518
    # Get the KiCad variables for fields
519
    KiConf.init(GS.sch_file)
16✔
520
    env = KiConf.kicad_env
16✔
521
    env.update(GS.load_pro_variables())
16✔
522
    if kicad_variant is not None:
16✔
523
        old_variant = GS.board.GetCurrentVariant()
1✔
524
        GS.board.SetCurrentVariant(kicad_variant)
1✔
525
    for m in GS.get_modules():
16✔
526
        ref = m.GetReference()
16✔
527
        # logger.error(f'{ref} {m.m_Uuid.AsString()} -> {m.GetPath().AsString()}')
528
        attrs = m.GetAttributes()
16✔
529
        if GS.ki6:
16✔
530
            # By full sheet path
531
            c = comps_hash.get(m.GetPath().AsString())
16✔
532
            if c is None:
16✔
533
                # Check if we have a component with the same reference in the schematic
534
                c = comps_hash_ref.get(ref)
6✔
535
                if c is not None:
6✔
536
                    logger.warning(W_PCBNOSCH+f"{ref} not linked to an existing schematic component, "
6✔
537
                                   f"but {ref} found in schematic, assuming this is the same")
1✔
538
        else:
539
            # By reference
UNCOV
540
            c = comps_hash.get(ref)
×
541

542
        real_fields = GS.get_fields(m)
16✔
543
        extra_env = create_extra_env(real_fields)
16✔
544

545
        if c is None:
16✔
546
            if not (attrs & MOD_BOARD_ONLY) and not ref.startswith('KiKit_'):
5✔
547
                logger.warning(W_PCBNOSCH+f'`{ref}` component in board, but not in schematic')
5✔
548
            if not GS.global_include_components_from_pcb:
5✔
549
                # v1.6.3 behavior
UNCOV
550
                logger.debugl(3, f"Not including {c.ref} ({m.m_Uuid.AsString()}) only found in PCB")
×
UNCOV
551
                continue
×
552
            # Create a component for this so we can include/exclude it using filters
553
            c = create_component_from_footprint(m, ref, env, real_fields)
5✔
554
            if c is None:
5✔
555
                continue
4✔
556
            if GS.ki6:
5✔
557
                logger.debugl(3, f"Including {c.ref} ({m.m_Uuid.AsString()}) only found in PCB")
5✔
558
            comps.append(c)
5✔
559

560
        if c.has_pcb_info:
16✔
561
            if GS.ki6:
15✔
562
                if c.ref != ref:
15✔
563
                    # This is a "feature" in KiCad, you can get a PCB only component linked to an unrelated sch component
564
                    logger.debugl(3, f"Repeated PCB {ref} {m.m_Uuid.AsString()} SCH {c.ref} {m.GetPath().AsString()}"
15✔
565
                                  " unrelated, making a new one")
566
                    c = create_component_from_footprint(m, ref, env, real_fields)
15✔
567
                    if c is None:
15✔
UNCOV
568
                        continue
×
569
                else:
570
                    # This isn't normal, but KiKit can create a panel with repeated references
571
                    # In this case we have an schematic with M components and a PCB with N*M components
572
                    # N repeated components in the PCB points to the same component in the schematic
573
                    logger.debugl(3, f"Repeated {c.ref}/{ref}")
3✔
574
                    c = c.copy()
3✔
575
                    logger.warning(W_REPREF+"Repeated component in PCB, normal for a KiKit panel")
3✔
576
            else:
577
                # When using references they can be repeated
578
                # We already got this reference and filled the PCB info, this is another copy
579
                logger.debugl(3, f"Repeated {c.ref}/{ref}")
×
UNCOV
580
                c = c.copy()
×
581
            comps.append(c)
15✔
582

583
        # Check the "Value", inform if different
584
        new_value = expand_one_footprint_field(m.GetValue(), env, extra_env)
16✔
585
        if new_value != c.value:
16✔
586
            expanded_value = expand_one_footprint_field(c.value, env, extra_env)
10✔
587
            if new_value != expanded_value:
10✔
588
                # Is different even after expanding vars
589
                # But perhaps is different because a variant changed it
590
                bkp_value = c.dfields_bkp.get('value') if c.dfields_bkp else None
10✔
591
                if not (bkp_value and new_value == bkp_value.value):
10✔
592
                    bkp_value = expand_one_footprint_field(bkp_value.value, env, extra_env) if bkp_value else None
10✔
593
                    if not (bkp_value and new_value == bkp_value):
10✔
594
                        # Ok, is different
595
                        logger.warning(f"{W_VALMISMATCH}Value field mismatch for `{ref}` (SCH: `{c.value}` "
10✔
596
                                       f"(`{expanded_value}`) PCB: `{new_value}`)")
2✔
597
        if 'Value' in real_fields:
16✔
598
            # We already computed it
599
            del real_fields['Value']
10✔
600
        c.value = new_value
16✔
601

602
        c.bottom = m.IsFlipped()
16✔
603
        c.footprint_rot = m.GetOrientationDegrees()
16✔
604
        center = GS.get_center(m)
16✔
605
        c.footprint_x = center.x
16✔
606
        c.footprint_y = center.y
16✔
607
        (c.footprint_w, c.footprint_h) = GS.get_fp_size(m)
16✔
608
        c.pad_properties = {}
16✔
609
        if GS.global_use_pcb_fields:
16✔
610
            copy_fields(c, real_fields, env, extra_env)
16✔
611
        c.has_pcb_info = True
16✔
612
        # Net
613
        net_name = set()
16✔
614
        net_class = set()
16✔
615
        for pad in m.Pads():
16✔
616
            net_name.add(pad.GetNetname())
16✔
617
            net_class.add(pad.GetNetClassName())
16✔
618
        c.net_name = ','.join(net_name)
16✔
619
        c.net_class = ','.join(net_class)
16✔
620
        if GS.ki5:
16✔
621
            # KiCad 5
UNCOV
622
            if attrs == UI_SMD:
×
UNCOV
623
                c.smd = True
×
UNCOV
624
            elif attrs == UI_VIRTUAL:
×
UNCOV
625
                c.virtual = True
×
626
            else:
UNCOV
627
                c.tht = True
×
628
        else:
629
            # KiCad 6
630
            if attrs & MOD_SMD:
16✔
631
                c.smd = True
16✔
632
            if attrs & MOD_THROUGH_HOLE:
16✔
633
                c.tht = True
16✔
634
            if attrs & MOD_VIRTUAL == MOD_VIRTUAL:
16✔
635
                c.virtual = True
15✔
636
            if attrs & MOD_EXCLUDE_FROM_POS_FILES:
16✔
637
                c.in_pos = False
15✔
638
            # The PCB contains another flag for the BoM
639
            # I guess it should be in sync, but: why should somebody want to unsync it?
640
            if attrs & MOD_EXCLUDE_FROM_BOM:
16✔
641
                c.in_bom_pcb = False
15✔
642
            if attrs & MOD_BOARD_ONLY:
16✔
643
                c.in_pcb_only = True
2✔
644
            c.pcb_id = m.m_Uuid.AsString()
16✔
645
            look_for_type = (not c.smd) and (not c.tht)
16✔
646
            for pad in m.Pads():
16✔
647
                p = PadProperty()
16✔
648
                center = pad.GetCenter()
16✔
649
                p.x = center.x
16✔
650
                p.y = center.y
16✔
651
                p.fab_property = pad.GetProperty()
16✔
652
                p.net = pad.GetNetname()
16✔
653
                p.net_class = pad.GetNetClassName()
16✔
654
                p.has_hole = pad.HasHole()
16✔
655
                name = pad.GetNumber()
16✔
656
                c.pad_properties[name] = p
16✔
657
                # Try to figure out if this is THT or SMD when not specified
658
                if look_for_type:
16✔
659
                    if p.has_hole:
15✔
660
                        # At least one THT, stop looking
661
                        c.tht = True
10✔
662
                        look_for_type = False
10✔
663
                    elif name:
15✔
664
                        # We have pad a valid pad, assume this is all SMD and keep looking
665
                        c.smd = True
5✔
666
    if kicad_variant is not None:
16✔
667
        logger.debug(f"Switching the PCB to {old_variant}")
1✔
668
        GS.board.SetCurrentVariant(old_variant)
1✔
669

670

671
def expand_comp_fields(c, env):
16✔
672
    extra_env = {f.name.upper() if f.name.lower() in INTERNAL_FIELDS else f.name: f.value for f in c.fields}
6✔
673
    # You can define "properties" for a sheet, and they become variables for all the components inside it
674
    if c.parent_sheet is not None:
6✔
675
        extra_env.update(c.parent_sheet.sheet_properties)
6✔
676
    for f in c.fields:
6✔
677
        new_value = f.value
6✔
678
        depth = 1
6✔
679
        used_extra = [False]
6✔
680
        while depth < GS.MAXDEPTH:
6✔
681
            new_value = expand_env(new_value, env, extra_env, used_extra=used_extra)
6✔
682
            if not used_extra[0]:
6✔
683
                break
6✔
684
            depth += 1
4✔
685
            if depth == GS.MAXDEPTH:
4✔
UNCOV
686
                logger.warning(W_MAXDEPTH+'Too much nested variables replacements, possible loop ({})'.format(f.value))
×
687
        if new_value != f.value:
6✔
688
            c.set_field(f.name, new_value)
5✔
689

690

691
def expand_fields(comps, dont_copy=False):
16✔
692
    if not dont_copy:
6✔
693
        new_comps = deepcopy(comps)
6✔
694
        for n_c, c in zip(new_comps, comps):
6✔
695
            n_c.original_copy = c
6✔
696
    KiConf.init(GS.sch_file)
6✔
697
    env = KiConf.kicad_env
6✔
698
    env.update(GS.load_pro_variables())
6✔
699
    for c in comps:
6✔
700
        expand_comp_fields(c, env)
6✔
701
    return comps
6✔
702

703

704
def preflight_checks(skip_pre, targets):
16✔
705
    logger.debug("Preflight checks")
16✔
706
    BasePreFlight.configure_all()
16✔
707
    if skip_pre is not None:
16✔
708
        if skip_pre == 'all':
10✔
709
            logger.debug("Skipping all preflight actions")
10✔
710
            return
10✔
711
        else:
712
            skip_list = skip_pre.split(',')
5✔
713
            for skip in skip_list:
5✔
714
                if skip == 'all':
5✔
715
                    GS.exit_with_error('All can\'t be part of a list of actions '
5✔
716
                                       'to skip. Use `--skip all`', EXIT_BAD_ARGS)
1✔
717
                else:
718
                    if not BasePreFlight.is_registered(skip):
5✔
719
                        GS.exit_with_error(f'Unknown preflight `{skip}`', EXIT_BAD_ARGS)
5✔
720
                    o_pre = BasePreFlight.get_preflight(skip)
5✔
721
                    if not o_pre:
5✔
722
                        logger.warning(W_NONEEDSKIP + '`{}` preflight is not in use, no need to skip'.format(skip))
5✔
723
                    else:
724
                        logger.debug('Skipping `{}`'.format(skip))
5✔
725
                        o_pre.disable()
5✔
726
    BasePreFlight.run_enabled(targets)
16✔
727

728

729
def get_output_dir(o_dir, obj, dry=False):
16✔
730
    # outdir is a combination of the config and output
731
    outdir = os.path.realpath(os.path.abspath(obj.expand_dirname(os.path.join(GS.out_dir, o_dir))))
16✔
732
    # Create directory if needed
733
    logger.debug("Output destination: {}".format(outdir))
16✔
734
    if not dry:
16✔
735
        os.makedirs(outdir, exist_ok=True)
16✔
736
    return outdir
16✔
737

738

739
def config_output(out, dry=False, dont_stop=False):
16✔
740
    if out._configured:
16✔
741
        return True
16✔
742
    try:
16✔
743
        # Should we load the PCB?
744
        if not dry:
16✔
745
            if out.is_pcb():
16✔
746
                load_board()
16✔
747
            if out.is_sch():
16✔
748
                load_sch()
16✔
749
        ok = True
16✔
750
        out.config(None)
16✔
751
    except (KiPlotConfigurationError, PlotError) as e:
6✔
752
        msg = "In section '"+out.name+"' ("+out.type+"): "+str(e)
6✔
753
        GS.exit_with_error(msg, DONT_STOP if dont_stop else EXIT_BAD_CONFIG)
6✔
754
        ok = False
×
755
    except SystemExit:
5✔
756
        if not dont_stop:
5✔
757
            raise
5✔
UNCOV
758
        GS.errors_ignored = True
×
UNCOV
759
        ok = False
×
760
    return ok
16✔
761

762

763
def get_output_targets(output, parent):
16✔
764
    out = RegOutput.get_output(output)
16✔
765
    if out is None:
16✔
766
        GS.exit_with_error(f'Unknown output `{output}` selected in {parent}', WRONG_ARGUMENTS)
1✔
767
    config_output(out)
15✔
768
    out_dir = get_output_dir(out.dir, out, dry=True)
15✔
769
    files_list = out.get_targets(out_dir)
15✔
770
    return files_list, out_dir, out
15✔
771

772

773
def run_output(out, dont_stop=False):
16✔
774
    if out._done:
16✔
775
        return
10✔
776
    if GS.global_set_text_variables_before_output and hasattr(out.options, 'variant'):
16✔
UNCOV
777
        pre = BasePreFlight.get_preflight('set_text_variables')
×
778
        if pre:
×
UNCOV
779
            pre._variant = out.options.variant
×
UNCOV
780
            pre.apply()
×
781
            load_board()
×
782
    GS.current_output = out.name
16✔
783
    logger.info('  '*run_output.depth+'- '+str(out))
16✔
784
    run_output.depth += 1
16✔
785
    if run_output.depth > GS.MAXDEPTH_OUTPUTS:
16✔
UNCOV
786
        config_error(f"Too many nested outputs (>{GS.MAXDEPTH_OUTPUTS}) check your configuration, "
×
787
                     "possibly an infinite loop")
788
    try:
16✔
789
        out.run(get_output_dir(out.dir, out))
16✔
790
        out._done = True
16✔
791
    except KiPlotConfigurationError as e:
8✔
792
        msg = "In section '"+out.name+"' ("+out.type+"): "+str(e)
6✔
793
        if dont_stop:
6✔
UNCOV
794
            logger.error(msg)
×
UNCOV
795
            GS.errors_ignored = True
×
796
        else:
797
            config_error(msg)
6✔
798
    except (PlotError, KiPlotError, SchError) as e:
8✔
799
        msg = "In output `"+str(out)+"`: "+str(e)
6✔
800
        GS.exit_with_error(msg, DONT_STOP if dont_stop else PLOT_ERROR)
6✔
801
    except KiConfError as e:
8✔
UNCOV
802
        ki_conf_error(e)
×
803
    except SystemExit:
8✔
804
        if not dont_stop:
8✔
805
            raise
8✔
806
        GS.errors_ignored = True
5✔
807
    run_output.depth -= 1
16✔
808

809

810
# run_output local and persistent depth
811
run_output.depth = 0
16✔
812

813

814
def configure_and_run(tree, out_dir, msg):
16✔
815
    out = RegOutput.get_class_for(tree['type'])()
4✔
816
    out.set_tree(tree)
4✔
817
    config_output(out)
4✔
818
    logger.debug(msg)
4✔
819
    out.run(out_dir)
4✔
820
    return out
4✔
821

822

823
def look_for_output(name, op_name, parent, valids):
16✔
824
    out = RegOutput.get_output(name)
9✔
825
    if out is None:
9✔
UNCOV
826
        raise KiPlotConfigurationError('Unknown output `{}` selected in {}'.format(name, parent))
×
827
    config_output(out)
9✔
828
    if out.type not in valids:
9✔
UNCOV
829
        raise KiPlotConfigurationError('`{}` must be {} type, not {}'.format(op_name, valids, out.type))
×
830
    return out
9✔
831

832

833
def _generate_outputs(targets, invert, skip_pre, cli_order, no_priority, dont_stop):
16✔
834
    logger.debug("Starting outputs for board {}".format(GS.pcb_file))
16✔
835
    # Make a list of target outputs
836
    n = len(targets)
16✔
837
    if n == 0:
16✔
838
        # No targets means all
839
        if invert:
16✔
840
            # Skip all targets
841
            logger.debug('Skipping all outputs')
5✔
842
        else:
843
            targets = [out for out in RegOutput.get_outputs() if out.run_by_default]
16✔
844
    else:
845
        # Check we got a valid list of outputs
846
        unknown = next(filter(lambda x: not RegOutput.is_output_or_group(x), targets), None)
15✔
847
        if unknown:
15✔
848
            GS.exit_with_error(f'Unknown output/group `{unknown}`', EXIT_BAD_ARGS)
5✔
849
        # Check for CLI+invert inconsistency
850
        if cli_order and invert:
15✔
UNCOV
851
            GS.exit_with_error("CLI order and invert options can't be used simultaneously", EXIT_BAD_ARGS)
×
852
        # Expand groups
853
        logger.debug('Outputs before groups expansion: {}'.format(targets))
15✔
854
        try:
15✔
855
            targets = RegOutput.solve_groups(targets, 'command line')
15✔
UNCOV
856
        except KiPlotConfigurationError as e:
×
UNCOV
857
            config_error(str(e))
×
858
        logger.debug('Outputs after groups expansion: {}'.format(targets))
15✔
859
        # Now convert the list of names into a list of output objects
860
        if cli_order:
15✔
861
            # Add them in the same order found at the command line
862
            targets = [RegOutput.get_output(name) for name in targets]
5✔
863
        else:
864
            # Add them in the declared order
865
            new_targets = []
15✔
866
            if invert:
15✔
867
                # Invert the selection
868
                for out in RegOutput.get_outputs():
5✔
869
                    if (out.name not in targets) and out.run_by_default:
5✔
870
                        new_targets.append(out)
5✔
871
                    else:
872
                        logger.debug('Skipping `{}` output'.format(out.name))
5✔
873
            else:
874
                # Normal list
875
                for out in RegOutput.get_outputs():
15✔
876
                    if out.name in targets:
15✔
877
                        new_targets.append(out)
15✔
878
                    else:
879
                        logger.debug('Skipping `{}` output'.format(out.name))
15✔
880
            targets = new_targets
15✔
881
    logger.debug('Outputs before preflights: {}'.format([t.name for t in targets]))
16✔
882
    # Run the preflights
883
    preflight_checks(skip_pre, targets)
16✔
884
    logger.debug('Outputs after preflights: {}'.format([t.name for t in targets]))
16✔
885
    if not cli_order and not no_priority:
16✔
886
        # Sort by priority
887
        targets = sorted(targets, key=lambda o: o.priority, reverse=True)
16✔
888
        logger.debug('Outputs after sorting: {}'.format([t.name for t in targets]))
16✔
889
    # Configure and run the outputs
890
    for out in targets:
16✔
891
        if GS.get_stop_flag():
16✔
UNCOV
892
            break
×
893
        if config_output(out, dont_stop=dont_stop):
16✔
894
            run_output(out, dont_stop)
16✔
895

896

897
def generate_outputs(targets, invert, skip_pre, cli_order, no_priority, dont_stop=False):
16✔
898
    setup_resources()
16✔
899
    prj = None
16✔
900
    if GS.global_restore_project:
16✔
901
        # Memorize the project content to restore it at exit
902
        prj = GS.read_pro()
5✔
903
    try:
16✔
904
        _generate_outputs(targets, invert, skip_pre, cli_order, no_priority, dont_stop)
16✔
905
    finally:
906
        # Restore the project file
907
        GS.write_pro(prj)
16✔
908

909

910
def adapt_file_name(name):
16✔
911
    if not name.startswith('/usr'):
5✔
912
        name = os.path.relpath(name)
5✔
913
    name = name.replace(' ', r'\ ')
5✔
914
    if '$' in name:
5✔
915
        logger.warning(W_WRONGCHAR+'Wrong character in file name `{}`'.format(name))
5✔
916
    return name
5✔
917

918

919
def gen_global_targets(f, pre_targets, out_targets, type):
16✔
920
    extra_targets = []
6✔
921
    pre = 'pre_'+type
6✔
922
    out = 'out_'+type
6✔
923
    all = 'all_'+type
6✔
924
    if pre_targets:
6✔
925
        f.write('{}:{}\n\n'.format(pre, pre_targets))
5✔
926
        extra_targets.append(pre)
5✔
927
    if out_targets:
6✔
928
        f.write('{}:{}\n\n'.format(out, out_targets))
5✔
929
        extra_targets.append(out)
5✔
930
    if pre_targets or out_targets:
6✔
931
        tg = ''
5✔
932
        if pre_targets:
5✔
933
            tg = ' '+pre
5✔
934
        if out_targets:
5✔
935
            tg += ' '+out
5✔
936
        f.write('{}:{}\n\n'.format(all, tg))
5✔
937
        extra_targets.append(all)
5✔
938
    return extra_targets
6✔
939

940

941
def get_pre_targets(targets, dependencies, is_pre):
16✔
942
    pcb_targets = sch_targets = ''
6✔
943
    BasePreFlight.configure_all()
6✔
944
    prefs = BasePreFlight.get_in_use_objs()
6✔
945
    try:
6✔
946
        for pre in prefs:
6✔
947
            tg = pre.get_targets()
5✔
948
            if not tg:
5✔
949
                continue
5✔
950
            name = pre.type
5✔
951
            targets[name] = [adapt_file_name(fn) for fn in tg]
5✔
952
            dependencies[name] = [adapt_file_name(fn) for fn in pre.get_dependencies()]
5✔
953
            is_pre.add(name)
5✔
954
            if pre.is_sch():
5✔
955
                sch_targets += ' '+name
5✔
956
            if pre.is_pcb():
5✔
957
                pcb_targets += ' '+name
5✔
UNCOV
958
    except KiPlotConfigurationError as e:
×
UNCOV
959
        config_error("In preflight '"+name+"': "+str(e))
×
960
    return pcb_targets, sch_targets
6✔
961

962

963
def get_out_targets(outputs, ori_names, targets, dependencies, comments, no_default):
16✔
964
    pcb_targets = sch_targets = ''
6✔
965
    try:
6✔
966
        for out in outputs:
6✔
967
            name = name2make(out.name)
5✔
968
            ori_names[name] = out.name
5✔
969
            tg = out.get_targets(out.expand_dirname(os.path.join(GS.out_dir, out.dir)))
5✔
970
            if not tg:
5✔
971
                continue
5✔
972
            targets[name] = [adapt_file_name(fn) for fn in tg]
5✔
973
            dependencies[name] = [adapt_file_name(fn) for fn in out.get_dependencies()]
5✔
974
            if out.comment:
5✔
975
                comments[name] = out.comment
5✔
976
            if not out.run_by_default:
5✔
UNCOV
977
                no_default.add(name)
×
978
            if out.is_sch():
5✔
979
                sch_targets += ' '+name
5✔
980
            if out.is_pcb():
5✔
981
                pcb_targets += ' '+name
5✔
UNCOV
982
    except KiPlotConfigurationError as e:
×
UNCOV
983
        config_error("In output '"+name+"': "+str(e))
×
984
    return pcb_targets, sch_targets
6✔
985

986

987
def generate_makefile(makefile, cfg_file, outputs, kibot_sys=False):
16✔
988
    cfg_file = os.path.relpath(cfg_file)
6✔
989
    logger.info('- Creating makefile `{}` from `{}`'.format(makefile, cfg_file))
6✔
990
    with open(makefile, 'wt') as f:
6✔
991
        f.write('#!/usr/bin/make\n')
6✔
992
        f.write('# Automatically generated by KiBot from `{}`\n'.format(cfg_file))
6✔
993
        fname = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src', 'kibot'))
6✔
994
        if kibot_sys or not os.path.isfile(fname):
6✔
995
            fname = 'kibot'
1✔
996
        f.write('KIBOT?={}\n'.format(fname))
6✔
997
        dbg = ''
6✔
998
        if GS.debug_level > 0:
6✔
999
            dbg = '-'+'v'*GS.debug_level
5✔
1000
        f.write('DEBUG?={}\n'.format(dbg))
6✔
1001
        f.write('CONFIG={}\n'.format(cfg_file))
6✔
1002
        if GS.sch_file:
6✔
1003
            f.write('SCH={}\n'.format(os.path.relpath(GS.sch_file)))
6✔
1004
        if GS.pcb_file:
6✔
1005
            f.write('PCB={}\n'.format(os.path.relpath(GS.pcb_file)))
6✔
1006
        f.write('DEST={}\n'.format(os.path.relpath(GS.out_dir)))
6✔
1007
        f.write('KIBOT_CMD=$(KIBOT) $(DEBUG) -c $(CONFIG) -e $(SCH) -b $(PCB) -d $(DEST)\n')
6✔
1008
        f.write('LOGFILE?=kibot_error.log\n')
6✔
1009
        f.write('\n')
6✔
1010
        # Configure all outputs
1011
        for out in outputs:
6✔
1012
            config_output(out)
5✔
1013
        # Get all targets and dependencies
1014
        targets = OrderedDict()
6✔
1015
        dependencies = OrderedDict()
6✔
1016
        comments = {}
6✔
1017
        ori_names = {}
6✔
1018
        is_pre = set()
6✔
1019
        no_default = set()
6✔
1020
        # Preflights
1021
        pre_pcb_targets, pre_sch_targets = get_pre_targets(targets, dependencies, is_pre)
6✔
1022
        # Outputs
1023
        out_pcb_targets, out_sch_targets = get_out_targets(outputs, ori_names, targets, dependencies, comments, no_default)
6✔
1024
        # all target
1025
        f.write('#\n# Default target\n#\n')
6✔
1026
        f.write('all: '+' '.join(filter(lambda x: x not in no_default, targets.keys()))+'\n\n')
6✔
1027
        extra_targets = ['all']
6✔
1028
        # PCB/SCH specific targets
1029
        f.write('#\n# SCH/PCB targets\n#\n')
6✔
1030
        extra_targets.extend(gen_global_targets(f, pre_sch_targets, out_sch_targets, 'sch'))
6✔
1031
        extra_targets.extend(gen_global_targets(f, pre_pcb_targets, out_pcb_targets, 'pcb'))
6✔
1032
        # Generate the output targets
1033
        f.write('#\n# Available targets (outputs)\n#\n')
6✔
1034
        for name, target in targets.items():
6✔
1035
            f.write(name+': '+' '.join(target)+'\n\n')
5✔
1036
        # Generate the output dependencies
1037
        f.write('#\n# Rules and dependencies\n#\n')
6✔
1038
        if GS.debug_enabled:
6✔
1039
            kibot_cmd = '\t$(KIBOT_CMD)'
5✔
1040
            log_action = ''
5✔
1041
        else:
1042
            kibot_cmd = '\t@$(KIBOT_CMD)'
6✔
1043
            log_action = ' 2>> $(LOGFILE)'
6✔
1044
        skip_all = ','.join(is_pre)
6✔
1045
        for name, dep in dependencies.items():
6✔
1046
            if name in comments:
5✔
1047
                f.write('# '+comments[name]+'\n')
5✔
1048
            dep.append(cfg_file)
5✔
1049
            f.write(' '.join(targets[name])+': '+' '.join(dep)+'\n')
5✔
1050
            if name in is_pre:
5✔
1051
                skip = filter(lambda n: n != name, is_pre)
5✔
1052
                f.write('{} -s {} -i{}\n\n'.format(kibot_cmd, ','.join(skip), log_action))
5✔
1053
            else:
1054
                f.write('{} -s {} "{}"{}\n\n'.format(kibot_cmd, skip_all, ori_names[name], log_action))
5✔
1055
        # Mark all outputs as PHONY
1056
        f.write('.PHONY: '+' '.join(extra_targets+list(targets.keys()))+'\n')
6✔
1057

1058

1059
def guess_ki6_sch(schematics):
16✔
1060
    schematics = list(filter(lambda x: x.endswith('.kicad_sch'), schematics))
5✔
1061
    if len(schematics) == 1:
5✔
UNCOV
1062
        return schematics[0]
×
1063
    if len(schematics) == 0:
5✔
UNCOV
1064
        return None
×
1065
    for fname in schematics:
5✔
1066
        with open(fname, 'rt') as f:
5✔
1067
            text = f.read()
5✔
1068
        if 'sheet_instances' in text:
5✔
1069
            return fname
1✔
1070
    return None
4✔
1071

1072

1073
def avoid_mixing_5_and_6(sch, kicad_sch):
16✔
UNCOV
1074
    GS.exit_with_error(['Found KiCad 5 and KiCad 6+ files, make sure the whole project uses one version',
×
1075
                        'KiCad 5:  '+os.path.basename(sch),
1076
                        'KiCad 6+: '+os.path.basename(kicad_sch)], EXIT_BAD_CONFIG)
1077

1078

1079
def solve_schematic(base_dir, a_schematic=None, a_board_file=None, config=None, sug_e=True):
16✔
1080
    schematic = a_schematic
16✔
1081
    if not schematic and a_board_file:
16✔
1082
        base = os.path.splitext(a_board_file)[0]
16✔
1083
        sch = os.path.join(base_dir, base+'.sch')
16✔
1084
        kicad_sch = os.path.join(base_dir, base+'.kicad_sch')
16✔
1085
        found_sch = os.path.isfile(sch)
16✔
1086
        found_kicad_sch = os.path.isfile(kicad_sch)
16✔
1087
        if found_sch and found_kicad_sch:
16✔
UNCOV
1088
            avoid_mixing_5_and_6(sch, kicad_sch)
×
1089
        if found_sch:
16✔
UNCOV
1090
            schematic = sch
×
1091
        elif GS.ki6 and found_kicad_sch:
16✔
1092
            schematic = kicad_sch
16✔
1093
    if not schematic:
16✔
1094
        schematics = glob(os.path.join(base_dir, '*.sch'))
16✔
1095
        if GS.ki6:
16✔
1096
            schematics += glob(os.path.join(base_dir, '*.kicad_sch'))
16✔
1097
        if len(schematics) == 1:
16✔
1098
            schematic = schematics[0]
10✔
1099
            logger.info('Using SCH file: '+os.path.relpath(schematic))
10✔
1100
        elif len(schematics) > 1:
16✔
1101
            is_silly = W_SILLY
5✔
1102
            # Look for a schematic with the same name as the config
1103
            if config:
5✔
1104
                if config[0] == '.':
5✔
1105
                    # Unhide hidden config
UNCOV
1106
                    config = config[1:]
×
1107
                # Remove any extension
1108
                last_split = None
5✔
1109
                while '.' in config and last_split != config:
5✔
1110
                    last_split = config
5✔
1111
                    config = os.path.splitext(config)[0]
5✔
1112
                # Try KiCad 5
1113
                sch = os.path.join(base_dir, config+'.sch')
5✔
1114
                found_sch = os.path.isfile(sch)
5✔
1115
                # Try KiCad 6
1116
                kicad_sch = os.path.join(base_dir, config+'.kicad_sch')
5✔
1117
                found_kicad_sch = os.path.isfile(kicad_sch)
5✔
1118
                if found_sch and found_kicad_sch:
5✔
UNCOV
1119
                    avoid_mixing_5_and_6(sch, kicad_sch)
×
1120
                if found_sch:
5✔
UNCOV
1121
                    schematic = sch
×
1122
                elif GS.ki6 and found_kicad_sch:
5✔
UNCOV
1123
                    schematic = kicad_sch
×
1124
            if not schematic:
5✔
1125
                # Look for a schematic with a PCB and/or project
1126
                for sch in schematics:
5✔
1127
                    base = os.path.splitext(sch)[0]
5✔
1128
                    if (os.path.isfile(os.path.join(base_dir, base+'.pro')) or
5✔
1129
                       os.path.isfile(os.path.join(base_dir, base+'.kicad_pro')) or
1✔
1130
                       os.path.isfile(os.path.join(base_dir, base+'.kicad_pcb'))):
1✔
1131
                        schematic = sch
5✔
1132
                        break
5✔
1133
                else:
1134
                    # No way to select one, just take the first
1135
                    is_silly = ''
5✔
1136
                    if GS.ki6:
5✔
1137
                        schematic = guess_ki6_sch(schematics)
5✔
1138
                    if not schematic:
5✔
1139
                        schematic = schematics[0]
4✔
1140
            msg = ' if you want to use another use -e option' if sug_e else ''
5✔
1141
            logger.warning(is_silly+W_VARSCH+'More than one SCH file found in `'+base_dir+'`.\n'
5✔
1142
                           '  Using '+schematic+msg+'.')
1✔
1143
    if schematic and not os.path.isfile(schematic):
16✔
1144
        GS.exit_with_error("Schematic file not found: "+schematic, NO_SCH_FILE)
5✔
1145
    if schematic:
16✔
1146
        schematic = os.path.abspath(schematic)
16✔
1147
        logger.debug('Using schematic: `{}`'.format(schematic))
16✔
1148
        logger.debug('Real schematic name: `{}`'.format(cased_path(schematic)))
16✔
1149
    else:
1150
        logger.debug('No schematic file found')
16✔
1151
    return schematic
16✔
1152

1153

1154
def check_board_file(board_file):
16✔
1155
    if board_file and not os.path.isfile(board_file):
16✔
1156
        GS.exit_with_error("Board file not found: "+board_file, NO_PCB_FILE)
5✔
1157

1158

1159
def solve_board_file(base_dir, a_board_file=None, sug_b=True):
16✔
1160
    schematic = GS.sch_file
16✔
1161
    board_file = a_board_file
16✔
1162
    if not board_file and schematic:
16✔
1163
        pcb = os.path.join(base_dir, os.path.splitext(schematic)[0]+'.kicad_pcb')
16✔
1164
        if os.path.isfile(pcb):
16✔
1165
            board_file = pcb
16✔
1166
    if not board_file:
16✔
1167
        board_files = glob(os.path.join(base_dir, '*.kicad_pcb'))
11✔
1168
        if len(board_files) == 1:
11✔
1169
            board_file = board_files[0]
5✔
1170
            logger.info('Using PCB file: '+os.path.relpath(board_file))
5✔
1171
        elif len(board_files) > 1:
11✔
1172
            board_file = board_files[0]
5✔
1173
            msg = ' if you want to use another use -b option' if sug_b else ''
5✔
1174
            logger.warning(W_VARPCB + 'More than one PCB file found in `'+base_dir+'`.\n'
5✔
1175
                           '  Using '+board_file+msg+'.')
1✔
1176
    check_board_file(board_file)
16✔
1177
    if board_file:
16✔
1178
        logger.debug('Using PCB: `{}`'.format(board_file))
16✔
1179
        logger.debug('Real PCB name: `{}`'.format(cased_path(board_file)))
16✔
1180
    else:
1181
        logger.debug('No PCB file found')
11✔
1182
    return board_file
16✔
1183

1184

1185
def solve_project_file():
16✔
1186
    if GS.pcb_file:
16✔
1187
        pro_name = GS.pcb_no_ext+GS.pro_ext
16✔
1188
        if os.path.isfile(pro_name):
16✔
1189
            return pro_name
16✔
1190
    if GS.sch_file:
16✔
1191
        pro_name = GS.sch_no_ext+GS.pro_ext
16✔
1192
        if os.path.isfile(pro_name):
16✔
1193
            return pro_name
9✔
1194
    return None
16✔
1195

1196

1197
def look_for_used_layers():
16✔
1198
    from .layer import Layer
5✔
1199
    Layer.reset()
5✔
1200
    layers = set()
5✔
1201
    components = {}
5✔
1202
    # Look inside the modules
1203
    for m in GS.get_modules():
5✔
1204
        layer = m.GetLayer()
5✔
1205
        components[layer] = components.get(layer, 0)+1
5✔
1206
        for gi in m.GraphicalItems():
5✔
1207
            layers.add(gi.GetLayer())
5✔
1208
        for pad in m.Pads():
5✔
1209
            for id in pad.GetLayerSet().Seq():
5✔
1210
                layers.add(id)
5✔
1211
    # All drawings in the PCB
1212
    for e in GS.board.GetDrawings():
5✔
1213
        layers.add(e.GetLayer())
5✔
1214
    # Zones
1215
    for e in list(GS.board.Zones()):
5✔
1216
        layers.add(e.GetLayer())
5✔
1217
    # Tracks and vias
1218
    via_type = 'VIA' if GS.ki5 else 'PCB_VIA'
5✔
1219
    for e in GS.board.GetTracks():
5✔
1220
        if e.GetClass() == via_type:
5✔
1221
            for id in e.GetLayerSet().Seq():
5✔
1222
                layers.add(id)
5✔
1223
        else:
1224
            layers.add(e.GetLayer())
5✔
1225
    # Now filter the pads and vias potential layers
1226
    declared_layers = {la._id for la in Layer.solve('all')}
5✔
1227
    layers = sorted(declared_layers.intersection(layers))
5✔
1228
    logger.debug('- Detected layers: {}'.format(layers))
5✔
1229
    layers = Layer.solve(layers)
5✔
1230
    for la in layers:
5✔
1231
        la.components = components.get(la._id, 0)
5✔
1232
    return layers
5✔
1233

1234

1235
def discover_files(dest_dir):
16✔
1236
    """ Look for schematic and PCBs at the dest_dir.
1237
        Return the name of the example file to generate. """
1238
    GS.pcb_file = None
5✔
1239
    GS.sch_file = None
5✔
1240
    # Check if we have useful files
1241
    fname = os.path.join(dest_dir, 'kibot_generated.kibot.yaml')
5✔
1242
    GS.set_sch(solve_schematic(dest_dir, sug_e=False))
5✔
1243
    GS.set_pcb(solve_board_file(dest_dir, sug_b=False))
5✔
1244
    GS.set_pro(solve_project_file())
5✔
1245
    return fname
5✔
1246

1247

1248
def load_config(plot_config):
16✔
1249
    if not plot_config:
16✔
1250
        return []
1✔
1251
    cr = CfgYamlReader()
16✔
1252
    outputs = None
16✔
1253
    try:
16✔
1254
        # The Python way ...
1255
        with gzip.open(plot_config, mode='rt') as cf_file:
16✔
1256
            try:
16✔
1257
                outputs = cr.read(cf_file)
16✔
1258
            except KiPlotConfigurationError as e:
16✔
UNCOV
1259
                config_error(str(e))
×
1260
    except OSError:
16✔
1261
        pass
16✔
1262
    if outputs is None:
16✔
1263
        with open(plot_config) as cf_file:
16✔
1264
            try:
16✔
1265
                outputs = cr.read(cf_file)
16✔
1266
            except KiPlotConfigurationError as e:
6✔
1267
                config_error(str(e))
6✔
1268
    return outputs
16✔
1269

1270

1271
def yaml_dump(f, tree):
16✔
1272
    if version_str2tuple(yaml.__version__) < (3, 14):
5✔
UNCOV
1273
        f.write(yaml.dump(tree))
×
1274
    else:
1275
        # sort_keys was introduced after 3.13
1276
        f.write(yaml.dump(tree, sort_keys=False))
5✔
1277

1278

1279
def register_xmp_import(name, definitions=None):
16✔
1280
    """ Register an import we need for an example """
1281
    global needed_imports
1282
    assert name not in needed_imports
4✔
1283
    needed_imports[name] = definitions
4✔
1284

1285

1286
def check_we_cant_use(o):
13✔
1287
    """ Check if the output doesn't have what it needs, i.e. no PCB and this is PCB related """
1288
    return ((not (o.is_pcb() and GS.pcb_file) and
5✔
1289
             not (o.is_sch() and GS.sch_file) and
1✔
1290
             not (o.is_any() and (GS.pcb_file or GS.sch_file))) or
1✔
1291
            ((o.is_pcb() and o.is_sch()) and (not GS.pcb_file or not GS.sch_file)))
1✔
1292

1293

1294
def generate_one_example(dest_dir, types):
16✔
1295
    """ Generate a example config for dest_dir """
1296
    fname = discover_files(dest_dir)
4✔
1297
    # Abort if none
1298
    if not GS.pcb_file and not GS.sch_file:
4✔
1299
        return None
1300
    # Reset the board and schematic
1301
    GS.board = None
4✔
1302
    GS.sch = None
4✔
1303
    # Create the config
1304
    with open(fname, 'wt') as f:
4✔
1305
        logger.info('- Creating {} example configuration'.format(fname))
4✔
1306
        f.write("# This is a working example.\n")
4✔
1307
        f.write("# For a more complete reference use `--example`\n")
4✔
1308
        f.write('kibot:\n  version: 1\n\n')
4✔
1309
        # Outputs
1310
        outs = RegOutput.get_registered()
4✔
1311
        # List of layers
1312
        layers = []
4✔
1313
        if GS.pcb_file:
4✔
1314
            load_board(GS.pcb_file)
4✔
1315
            layers = look_for_used_layers()
4✔
1316
        if GS.sch_file:
4✔
1317
            load_sch()
4✔
1318
        # Filter some warnings
1319
        fil = [{'number': 1007},  # No information for a component in a distributor
4✔
1320
               {'number': 1015},  # More than one component in a search for a distributor
1321
               {'number': 58},    # Missing project file
1322
               {'number': 107},   # Stencil.side auto, we always use it for the example
1323
               {'number': 143},   # This output depends on KiCad version, use `blender_export` instead
1324
               ]
1325
        glb = {'filters': fil}
4✔
1326
        yaml_dump(f, {'global': glb})
4✔
1327
        f.write('\n')
4✔
1328
        # A helper for the internal templates
1329
        global needed_imports
1330
        needed_imports = {}
4✔
1331
        # All the preflights
1332
        preflights = {}
4✔
1333
        for n in sorted(BasePreFlight.get_registered().keys()):
4✔
1334
            o = BasePreFlight.get_object_for(n)
4✔
1335
            if types and n not in types:
4✔
1336
                logger.debug('- {}, not selected (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
1337
                continue
1338
            if check_we_cant_use(o):
4✔
1339
                logger.debug('- {}, skipped (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
4✔
1340
                continue
4✔
1341
            tree = o.get_conf_examples(n, layers)
4✔
1342
            if tree:
4✔
1343
                logger.debug('- {}, generated'.format(n))
2✔
1344
                preflights.update(tree)
2✔
1345
            else:
1346
                logger.debug('- {}, nothing to do'.format(n))
4✔
1347
        # All the outputs
1348
        outputs = []
4✔
1349
        for n, cls in OrderedDict(sorted(outs.items())).items():
4✔
1350
            o = cls()
4✔
1351
            if types and n not in types:
4✔
1352
                logger.debug('- {}, not selected (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
1353
                continue
1354
            if check_we_cant_use(o):
4✔
1355
                logger.debug('- {}, skipped (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
4✔
1356
                continue
4✔
1357
            tree = cls.get_conf_examples(n, layers)
4✔
1358
            if tree:
4✔
1359
                logger.debug('- {}, generated'.format(n))
4✔
1360
                outputs.extend(tree)
4✔
1361
            else:
1362
                logger.debug('- {}, nothing to do'.format(n))
4✔
1363
        global_defaults = None
4✔
1364
        if needed_imports:
4✔
1365
            imports = []
4✔
1366
            for n, d in sorted(needed_imports.items()):
4✔
1367
                if n == 'global':
4✔
1368
                    global_defaults = d
4✔
1369
                    continue
4✔
1370
                content = {'file': n}
4✔
1371
                if d:
4✔
1372
                    content['definitions'] = d
4✔
1373
                imports.append(content)
4✔
1374
            yaml_dump(f, {'import': imports})
4✔
1375
            f.write('\n')
4✔
1376
        if preflights:
4✔
1377
            yaml_dump(f, {'preflight': preflights})
2✔
1378
            f.write('\n')
2✔
1379
        if outputs:
4✔
1380
            yaml_dump(f, {'outputs': outputs})
4✔
1381
        else:
1382
            return None
1383
        if global_defaults:
4✔
1384
            f.write('\n...\n')
4✔
1385
            yaml_dump(f, {'definitions': global_defaults})
4✔
1386
    return fname
4✔
1387

1388

1389
def reset_config():
13✔
1390
    # Outputs, groups, filters and variants
1391
    RegOutput.reset()
1✔
1392
    # Preflights
1393
    BasePreFlight.reset()
1✔
1394

1395

1396
def generate_targets(config_file):
13✔
1397
    """ Generate all possible targets for the configuration file """
1398
    # Reset the board and schematic
UNCOV
1399
    GS.board = None
×
UNCOV
1400
    GS.sch = None
×
1401
    # Reset the list of outputs and preflights
UNCOV
1402
    reset_config()
×
1403
    # Read the config file
UNCOV
1404
    cr = CfgYamlReader()
×
UNCOV
1405
    with open(config_file) as cf_file:
×
UNCOV
1406
        cr.read(cf_file)
×
1407
    # Do all the job
UNCOV
1408
    generate_outputs([], False, None, False, False, dont_stop=True)
×
1409

1410

1411
def _walk(path, depth):
16✔
1412
    """ Recursively list files and directories up to a certain depth """
1413
    depth -= 1
4✔
1414
    try:
4✔
1415
        with os.scandir(path) as p:
4✔
1416
            for entry in p:
4✔
1417
                yield entry.path
4✔
1418
                if entry.is_dir() and depth > 0:
4✔
1419
                    yield from _walk(entry.path, depth)
1420
    except Exception as e:
1421
        logger.debug(f'Skipping {path} because {e}')
1422

1423

1424
def setup_fonts(source):
13✔
1425
    if not os.path.isdir(source):
13✔
1426
        logger.debug('No font resources dir')
13✔
1427
        return
13✔
1428
    dest = os.path.expanduser('~/.fonts/')
3✔
1429
    installed = False
3✔
1430
    for f in glob(os.path.join(source, '*.ttf')) + glob(os.path.join(source, '*.otf')):
3✔
1431
        fname = os.path.basename(f)
3✔
1432
        fdest = os.path.join(dest, fname)
3✔
1433
        if os.path.isfile(fdest):
3✔
1434
            logger.debug('Font {} already installed'.format(fname))
1435
            continue
1436
        logger.info('Installing font {}'.format(fname))
3✔
1437
        if not os.path.isdir(dest):
3✔
1438
            os.makedirs(dest)
3✔
1439
        copy2(f, fdest)
3✔
1440
        installed = True
3✔
1441
    if installed:
3✔
1442
        run_command(['fc-cache'])
3✔
1443

1444

1445
def setup_colors(source):
13✔
1446
    if not os.path.isdir(source):
13✔
1447
        logger.debug('No color resources dir')
13✔
1448
        return
13✔
1449
    if not GS.kicad_conf_path:
3✔
1450
        return
1451
    dest = os.path.join(GS.kicad_conf_path, 'colors')
3✔
1452
    for f in glob(os.path.join(source, '*.json')):
3✔
1453
        fname = os.path.basename(f)
3✔
1454
        fdest = os.path.join(dest, fname)
3✔
1455
        if os.path.isfile(fdest):
3✔
1456
            logger.debug('Color {} already installed'.format(fname))
1457
            continue
1458
        logger.info('Installing color {}'.format(fname))
3✔
1459
        if not os.path.isdir(dest):
3✔
1460
            os.makedirs(dest)
1461
        copy2(f, fdest)
3✔
1462

1463

1464
def setup_resources():
13✔
1465
    if not GS.global_resources_dir:
13✔
1466
        logger.debug('No resources dir')
1467
        return
1468
    setup_fonts(os.path.join(GS.global_resources_dir, 'fonts'))
13✔
1469
    setup_colors(os.path.join(GS.global_resources_dir, 'colors'))
13✔
1470

1471

1472
def generate_examples(start_dir, dry, types):
13✔
1473
    if not start_dir:
4✔
1474
        start_dir = '.'
1475
    else:
1476
        if not os.path.isdir(start_dir):
4✔
1477
            GS.exit_with_error(f'Invalid dir {start_dir} to quick start', WRONG_ARGUMENTS)
1478
    # Set default global options
1479
    glb = GS.set_global_options_tree({})
4✔
1480
    glb.config(None)
4✔
1481
    # Install the resources
1482
    setup_resources()
4✔
1483
    # Look for candidate dirs
1484
    k_files_regex = re.compile(r'([^/]+)\.(kicad_pro|pro)$')
4✔
1485
    candidates = set()
4✔
1486
    for f in _walk(start_dir, 6):
4✔
1487
        if k_files_regex.search(f):
4✔
1488
            candidates.add(os.path.realpath(os.path.dirname(f)))
4✔
1489
    if not candidates:
4✔
1490
        GS.exit_with_error(f'No KiCad projects found in `{start_dir}`', MISSING_FILES)
1491
    # Try to generate the configs in the candidate places
1492
    confs = []
4✔
1493
    for c in sorted(candidates):
4✔
1494
        logger.info('Analyzing `{}` dir'.format(c))
4✔
1495
        res = generate_one_example(c, types)
4✔
1496
        if res:
4✔
1497
            confs.append(res)
4✔
1498
        logger.info('')
4✔
1499
    confs.sort()
4✔
1500
    # Just the configs, not the targets
1501
    if dry:
4✔
1502
        return
4✔
1503
    # Try to generate all the stuff
1504
    if GS.out_dir_in_cmd_line:
1505
        out_dir = GS.out_dir
1506
    else:
1507
        out_dir = 'Generated'
1508
    for n, c in enumerate(confs):
1509
        conf_dir = os.path.dirname(c)
1510
        if len(confs) > 1:
1511
            subdir = '%03d-%s' % (n+1, conf_dir.replace('/', ',').replace(' ', '_'))
1512
            dest = os.path.join(out_dir, subdir)
1513
        else:
1514
            dest = out_dir
1515
        GS.out_dir = dest
1516
        logger.info('Generating targets for `{}`, destination: `{}`'.format(c, dest))
1517
        os.makedirs(dest, exist_ok=True)
1518
        # Create a log file with all the debug we can
1519
        fl = log.set_file_log(os.path.join(dest, 'kibot.log'))
1520
        old_lvl = GS.debug_level
1521
        GS.debug_level = 10
1522
        # Detect the SCH and PCB again
1523
        discover_files(conf_dir)
1524
        # Generate all targets
1525
        generate_targets(c)
1526
        # Close the debug file
1527
        log.remove_file_log(fl)
1528
        GS.debug_level = old_lvl
1529
        logger.info('')
1530
    # Try to open a browser
1531
    index = os.path.join(GS.out_dir, 'index.html')
1532
    if os.environ.get('DISPLAY') and which('x-www-browser') and os.path.isfile(index):
1533
        Popen(['x-www-browser', index])
1534

1535

1536
def get_columns():
13✔
1537
    """ Create a list of valid columns """
1538
    if GS.sch:
11✔
1539
        cols = deepcopy(ColumnList.COLUMNS_DEFAULT)
11✔
1540
        return (GS.sch.get_field_names(cols), ColumnList.COLUMNS_EXTRA)
11✔
1541
    return (ColumnList.COLUMNS_DEFAULT, ColumnList.COLUMNS_EXTRA)
1✔
1542

1543

1544
# To avoid circular dependencies: Optionable needs it, but almost everything needs Optionable
1545
GS.load_board = load_board
16✔
1546
GS.load_sch = load_sch
16✔
1547
GS.exec_with_retry = exec_with_retry
16✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc