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

INTI-CMNB / KiBot / 5244767857

pending completion
5244767857

push

github-actions

set-soft
[Tests][Added] References for the current stable nightly

- It is quite incompatible with the 7.0.5 release

21898 of 24891 relevant lines covered (87.98%)

5.59 hits per line

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

88.08
/kibot/kiplot.py
1
# -*- coding: utf-8 -*-
2
# Copyright (c) 2020-2023 Salvador E. Tropea
3
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
4
# Copyright (c) 2018 John Beard
5
# License: GPL-3.0
6
# Project: KiBot (formerly KiPlot)
7
# Adapted from: https://github.com/johnbeard/kiplot
8
"""
10✔
9
Main KiBot code
10
"""
11

12
import os
10✔
13
import re
10✔
14
from sys import exit
10✔
15
from sys import path as sys_path
10✔
16
import shlex
10✔
17
from shutil import which, copy2
10✔
18
from subprocess import run, PIPE, STDOUT, Popen, CalledProcessError
10✔
19
from glob import glob
10✔
20
from importlib.util import spec_from_file_location, module_from_spec
10✔
21
from collections import OrderedDict
10✔
22

23
from .gs import GS
10✔
24
from .registrable import RegOutput
10✔
25
from .misc import (PLOT_ERROR, CORRUPTED_PCB, EXIT_BAD_ARGS, CORRUPTED_SCH, version_str2tuple,
10✔
26
                   EXIT_BAD_CONFIG, WRONG_INSTALL, UI_SMD, UI_VIRTUAL, TRY_INSTALL_CHECK, MOD_SMD, MOD_THROUGH_HOLE,
27
                   MOD_VIRTUAL, W_PCBNOSCH, W_NONEEDSKIP, W_WRONGCHAR, name2make, W_TIMEOUT, W_KIAUTO, W_VARSCH,
28
                   NO_SCH_FILE, NO_PCB_FILE, W_VARPCB, NO_YAML_MODULE, WRONG_ARGUMENTS, FAILED_EXECUTE,
29
                   MOD_EXCLUDE_FROM_POS_FILES, MOD_EXCLUDE_FROM_BOM, MOD_BOARD_ONLY, hide_stderr)
30
from .error import PlotError, KiPlotConfigurationError, config_error
10✔
31
from .config_reader import CfgYamlReader
10✔
32
from .pre_base import BasePreFlight
10✔
33
from .dep_downloader import register_deps
10✔
34
import kibot.dep_downloader as dep_downloader
10✔
35
from .kicad.v5_sch import Schematic, SchFileError, SchError
10✔
36
from .kicad.v6_sch import SchematicV6
10✔
37
from .kicad.config import KiConfError
10✔
38
from . import log
10✔
39

40
logger = log.get_logger()
10✔
41
# Cache to avoid running external many times to check their versions
42
script_versions = {}
10✔
43
actions_loaded = False
10✔
44
needed_imports = set()
10✔
45

46
try:
10✔
47
    import yaml
10✔
48
except ImportError:
×
49
    log.init()
×
50
    logger.error('No yaml module for Python, install python3-yaml')
×
51
    logger.error(TRY_INSTALL_CHECK)
×
52
    exit(NO_YAML_MODULE)
×
53

54

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

59

60
def try_register_deps(mod, name):
10✔
61
    if mod.__doc__:
10✔
62
        try:
10✔
63
            data = yaml.safe_load(mod.__doc__)
10✔
64
        except yaml.YAMLError as e:
×
65
            logger.error('While loading plug-in `{}`:'.format(name))
×
66
            config_error("Error loading YAML "+str(e))
×
67
        register_deps(name, data)
10✔
68

69

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

82

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

94

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

118

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

146

147
def debug_output(res):
10✔
148
    if res.stdout:
9✔
149
        logger.debug('- Output from command: '+res.stdout.decode())
9✔
150

151

152
def _run_command(command, change_to):
10✔
153
    return run(command, check=True, stdout=PIPE, stderr=STDOUT, cwd=change_to)
9✔
154

155

156
def run_command(command, change_to=None, just_raise=False, use_x11=False):
10✔
157
    logger.debug('Executing: '+shlex.join(command))
9✔
158
    if change_to is not None:
9✔
159
        logger.debug('- CWD: '+change_to)
9✔
160
    try:
9✔
161
        if use_x11 and not GS.on_windows:
9✔
162
            logger.debug('Using Xvfb to run the command')
4✔
163
            from xvfbwrapper import Xvfb
4✔
164
            with Xvfb(width=640, height=480, colordepth=24):
4✔
165
                res = _run_command(command, change_to)
4✔
166
        else:
167
            res = _run_command(command, change_to)
9✔
168
    except CalledProcessError as e:
9✔
169
        if just_raise:
9✔
170
            raise
9✔
171
        logger.error('Running {} returned {}'.format(e.cmd, e.returncode))
×
172
        debug_output(e)
×
173
        exit(FAILED_EXECUTE)
×
174
    debug_output(res)
9✔
175
    return res.stdout.decode().rstrip()
9✔
176

177

178
def exec_with_retry(cmd, exit_with=None):
10✔
179
    cmd_str = shlex.join(cmd)
9✔
180
    logger.debug('Executing: '+cmd_str)
9✔
181
    if GS.debug_level > 2:
9✔
182
        logger.debug('Command line: '+str(cmd))
9✔
183
    retry = 2
9✔
184
    while retry:
9✔
185
        result = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
9✔
186
        ret = result.returncode
9✔
187
        retry -= 1
9✔
188
        if ret != 16 and (ret > 0 and ret < 128 and retry):
9✔
189
            # 16 is KiCad crash
190
            logger.debug('Failed with error {}, retrying ...'.format(ret))
4✔
191
        else:
192
            extract_errors(result.stderr)
9✔
193
            err = '> '+result.stderr.replace('\n', '\n> ')
9✔
194
            logger.debug('Output from command:\n'+err)
9✔
195
            if 'Timed out' in err:
9✔
196
                logger.warning(W_TIMEOUT+'Time out detected, on slow machines or complex projects try:')
1✔
197
                logger.warning(W_TIMEOUT+'`kiauto_time_out_scale` and/or `kiauto_wait_start` global options')
1✔
198
            if exit_with is not None and ret:
9✔
199
                logger.error(cmd[0]+' returned %d', ret)
3✔
200
                exit(exit_with)
3✔
201
            return ret
9✔
202

203

204
def load_board(pcb_file=None, forced=False):
10✔
205
    if GS.board is not None and not forced:
10✔
206
        # Already loaded
207
        return GS.board
10✔
208
    import pcbnew
10✔
209
    if not pcb_file:
10✔
210
        GS.check_pcb()
10✔
211
        pcb_file = GS.pcb_file
10✔
212
    try:
10✔
213
        with hide_stderr():
10✔
214
            board = pcbnew.LoadBoard(pcb_file)
10✔
215
        if GS.global_invalidate_pcb_text_cache == 'yes' and GS.ki6:
10✔
216
            logger.debug('Current PCB text variables cache: {}'.format(board.GetProperties().items()))
2✔
217
            logger.debug('Removing cached text variables')
2✔
218
            board.SetProperties(pcbnew.MAP_STRING_STRING())
2✔
219
        if BasePreFlight.get_option('check_zone_fills'):
10✔
220
            GS.fill_zones(board)
6✔
221
        if GS.global_units and GS.ki6:
10✔
222
            # In KiCad 6 "dimensions" has units.
223
            # The default value is DIM_UNITS_MODE_AUTOMATIC.
224
            # But this has a meaning only in the GUI where you have default units.
225
            # So now we have global.units and here we patch the board.
226
            UNIT_NAME_TO_INDEX = {'millimeters': pcbnew.DIM_UNITS_MODE_MILLIMETRES,
2✔
227
                                  'inches': pcbnew.DIM_UNITS_MODE_INCHES,
228
                                  'mils': pcbnew.DIM_UNITS_MODE_MILS}
229
            forced_units = UNIT_NAME_TO_INDEX[GS.global_units]
2✔
230
            for dr in board.GetDrawings():
2✔
231
                if dr.GetClass().startswith('PCB_DIM_') and dr.GetUnitsMode() == pcbnew.DIM_UNITS_MODE_AUTOMATIC:
2✔
232
                    dr.SetUnitsMode(forced_units)
2✔
233
                    dr.Update()
2✔
234
        GS.board = board
10✔
235
    except OSError as e:
3✔
236
        logger.error('Error loading PCB file. Corrupted?')
3✔
237
        logger.error(e)
3✔
238
        exit(CORRUPTED_PCB)
3✔
239
    assert board is not None
10✔
240
    logger.debug("Board loaded")
10✔
241
    return board
10✔
242

243

244
def ki_conf_error(e):
10✔
245
    GS.exit_with_error(('At line {} of `{}`: {}'.format(e.line, e.file, e.msg),
×
246
                        'Line content: `{}`'.format(e.code.rstrip())), EXIT_BAD_CONFIG)
247

248

249
def load_any_sch(file, project):
10✔
250
    if file[-9:] == 'kicad_sch':
10✔
251
        sch = SchematicV6()
6✔
252
        load_libs = False
6✔
253
    else:
254
        sch = Schematic()
4✔
255
        load_libs = True
4✔
256
    try:
10✔
257
        sch.load(file, project)
10✔
258
        if load_libs:
10✔
259
            sch.load_libs(file)
4✔
260
        if GS.debug_level > 1:
10✔
261
            logger.debug('Schematic dependencies: '+str(sch.get_files()))
9✔
262
    except SchFileError as e:
3✔
263
        GS.exit_with_error(('At line {} of `{}`: {}'.format(e.line, e.file, e.msg),
1✔
264
                            'Line content: `{}`'.format(e.code)), CORRUPTED_SCH)
265
    except SchError as e:
2✔
266
        GS.exit_with_error(('While loading `{}`'.format(file), str(e)), CORRUPTED_SCH)
2✔
267
    except KiConfError as e:
×
268
        ki_conf_error(e)
×
269
    return sch
10✔
270

271

272
def load_sch():
10✔
273
    if GS.sch:  # Already loaded
10✔
274
        return
10✔
275
    GS.check_sch()
10✔
276
    GS.sch = load_any_sch(GS.sch_file, GS.sch_basename)
10✔
277

278

279
def get_board_comps_data(comps):
10✔
280
    """ Add information from the PCB to the list of components from the schematic.
281
        Note that we do it every time the function is called to reset transformation filters like rot_footprint. """
282
    if not GS.pcb_file:
10✔
283
        return
6✔
284
    load_board()
10✔
285
    # Each reference could be more than one sub-units
286
    # So this hash is ref -> [List of units]
287
    comps_hash = {}
10✔
288
    for c in comps:
10✔
289
        cur_list = comps_hash.get(c.ref, [])
10✔
290
        cur_list.append(c)
10✔
291
        comps_hash[c.ref] = cur_list
10✔
292
    for m in GS.get_modules():
10✔
293
        ref = m.GetReference()
10✔
294
        attrs = m.GetAttributes()
10✔
295
        if ref not in comps_hash:
10✔
296
            if not (attrs & MOD_BOARD_ONLY):
9✔
297
                logger.warning(W_PCBNOSCH + '`{}` component in board, but not in schematic'.format(ref))
9✔
298
            continue
9✔
299
        for c in comps_hash[ref]:
10✔
300
            c.bottom = m.IsFlipped()
10✔
301
            c.footprint_rot = m.GetOrientationDegrees()
10✔
302
            center = GS.get_center(m)
10✔
303
            c.footprint_x = center.x
10✔
304
            c.footprint_y = center.y
10✔
305
            (c.footprint_w, c.footprint_h) = GS.get_fp_size(m)
10✔
306
            c.has_pcb_info = True
10✔
307
            if GS.ki5:
10✔
308
                # KiCad 5
309
                if attrs == UI_SMD:
4✔
310
                    c.smd = True
4✔
311
                elif attrs == UI_VIRTUAL:
4✔
312
                    c.virtual = True
3✔
313
                else:
314
                    c.tht = True
4✔
315
            else:
316
                # KiCad 6
317
                if attrs & MOD_SMD:
6✔
318
                    c.smd = True
6✔
319
                if attrs & MOD_THROUGH_HOLE:
6✔
320
                    c.tht = True
6✔
321
                if attrs & MOD_VIRTUAL == MOD_VIRTUAL:
6✔
322
                    c.virtual = True
6✔
323
                if attrs & MOD_EXCLUDE_FROM_POS_FILES:
6✔
324
                    c.in_pos = False
6✔
325
                # The PCB contains another flag for the BoM
326
                # I guess it should be in sync, but: why should somebody want to unsync it?
327
                if attrs & MOD_EXCLUDE_FROM_BOM:
6✔
328
                    c.in_bom_pcb = False
6✔
329
                if attrs & MOD_BOARD_ONLY:
6✔
330
                    c.in_pcb_only = True
×
331

332

333
def preflight_checks(skip_pre, targets):
10✔
334
    logger.debug("Preflight checks")
10✔
335

336
    if skip_pre is not None:
10✔
337
        if skip_pre == 'all':
6✔
338
            logger.debug("Skipping all preflight actions")
6✔
339
            return
6✔
340
        else:
341
            skip_list = skip_pre.split(',')
3✔
342
            for skip in skip_list:
3✔
343
                if skip == 'all':
3✔
344
                    logger.error('All can\'t be part of a list of actions '
3✔
345
                                 'to skip. Use `--skip all`')
346
                    exit(EXIT_BAD_ARGS)
3✔
347
                else:
348
                    if not BasePreFlight.is_registered(skip):
3✔
349
                        logger.error('Unknown preflight `{}`'.format(skip))
3✔
350
                        exit(EXIT_BAD_ARGS)
3✔
351
                    o_pre = BasePreFlight.get_preflight(skip)
3✔
352
                    if not o_pre:
3✔
353
                        logger.warning(W_NONEEDSKIP + '`{}` preflight is not in use, no need to skip'.format(skip))
3✔
354
                    else:
355
                        logger.debug('Skipping `{}`'.format(skip))
3✔
356
                        o_pre.disable()
3✔
357
    BasePreFlight.run_enabled(targets)
10✔
358

359

360
def get_output_dir(o_dir, obj, dry=False):
10✔
361
    # outdir is a combination of the config and output
362
    outdir = os.path.abspath(obj.expand_dirname(os.path.join(GS.out_dir, o_dir)))
10✔
363
    # Create directory if needed
364
    logger.debug("Output destination: {}".format(outdir))
10✔
365
    if not dry and not os.path.exists(outdir):
10✔
366
        os.makedirs(outdir)
10✔
367
    return outdir
10✔
368

369

370
def config_output(out, dry=False, dont_stop=False):
10✔
371
    if out._configured:
10✔
372
        return True
9✔
373
    # Should we load the PCB?
374
    if not dry:
10✔
375
        if out.is_pcb():
10✔
376
            load_board()
10✔
377
        if out.is_sch():
10✔
378
            load_sch()
10✔
379
    ok = True
10✔
380
    try:
10✔
381
        out.config(None)
10✔
382
    except KiPlotConfigurationError as e:
4✔
383
        msg = "In section '"+out.name+"' ("+out.type+"): "+str(e)
4✔
384
        if dont_stop:
4✔
385
            logger.error(msg)
×
386
        else:
387
            config_error(msg)
4✔
388
        ok = False
4✔
389
    except SystemExit:
1✔
390
        if not dont_stop:
1✔
391
            raise
1✔
392
        ok = False
×
393
    return ok
10✔
394

395

396
def get_output_targets(output, parent):
10✔
397
    out = RegOutput.get_output(output)
9✔
398
    if out is None:
9✔
399
        logger.error('Unknown output `{}` selected in {}'.format(output, parent))
1✔
400
        exit(WRONG_ARGUMENTS)
1✔
401
    config_output(out)
8✔
402
    out_dir = get_output_dir(out.dir, out, dry=True)
8✔
403
    files_list = out.get_targets(out_dir)
8✔
404
    return files_list, out_dir, out
8✔
405

406

407
def run_output(out, dont_stop=False):
10✔
408
    if out._done:
10✔
409
        return
3✔
410
    if GS.global_set_text_variables_before_output and hasattr(out.options, 'variant'):
10✔
411
        pre = BasePreFlight.get_preflight('set_text_variables')
×
412
        if pre:
×
413
            pre._variant = out.options.variant
×
414
            pre.apply()
×
415
            load_board()
×
416
    GS.current_output = out.name
10✔
417
    try:
10✔
418
        out.run(get_output_dir(out.dir, out))
10✔
419
        out._done = True
10✔
420
    except PlotError as e:
7✔
421
        logger.error("In output `"+str(out)+"`: "+str(e))
4✔
422
        if not dont_stop:
4✔
423
            exit(PLOT_ERROR)
4✔
424
    except KiPlotConfigurationError as e:
7✔
425
        msg = "In section '"+out.name+"' ("+out.type+"): "+str(e)
4✔
426
        if dont_stop:
4✔
427
            logger.error(msg)
×
428
        else:
429
            config_error(msg)
4✔
430
    except KiConfError as e:
7✔
431
        ki_conf_error(e)
×
432
    except SystemExit:
7✔
433
        if not dont_stop:
7✔
434
            raise
7✔
435

436

437
def configure_and_run(tree, out_dir, msg):
10✔
438
    out = RegOutput.get_class_for(tree['type'])()
×
439
    out.set_tree(tree)
×
440
    config_output(out)
×
441
    logger.debug(' - Creating the PCB3D ...')
×
442
    out.run(out_dir)
×
443

444

445
def look_for_output(name, op_name, parent, valids):
10✔
446
    out = RegOutput.get_output(name)
3✔
447
    if out is None:
3✔
448
        raise KiPlotConfigurationError('Unknown output `{}` selected in {}'.format(name, parent))
×
449
    config_output(out)
3✔
450
    if out.type not in valids:
3✔
451
        raise KiPlotConfigurationError('`{}` must be {} type, not {}'.format(op_name, valids, out.type))
×
452
    return out
3✔
453

454

455
def _generate_outputs(outputs, targets, invert, skip_pre, cli_order, no_priority, dont_stop):
10✔
456
    logger.debug("Starting outputs for board {}".format(GS.pcb_file))
10✔
457
    # Make a list of target outputs
458
    n = len(targets)
10✔
459
    if n == 0:
10✔
460
        # No targets means all
461
        if invert:
10✔
462
            # Skip all targets
463
            logger.debug('Skipping all outputs')
3✔
464
        else:
465
            targets = [out for out in RegOutput.get_outputs() if out.run_by_default]
10✔
466
    else:
467
        # Check we got a valid list of outputs
468
        unknown = next(filter(lambda x: not RegOutput.is_output_or_group(x), targets), None)
9✔
469
        if unknown:
9✔
470
            logger.error('Unknown output/group `{}`'.format(unknown))
3✔
471
            exit(EXIT_BAD_ARGS)
3✔
472
        # Check for CLI+invert inconsistency
473
        if cli_order and invert:
9✔
474
            logger.error("CLI order and invert options can't be used simultaneously")
×
475
            exit(EXIT_BAD_ARGS)
×
476
        # Expand groups
477
        logger.debug('Outputs before groups expansion: {}'.format(targets))
9✔
478
        try:
9✔
479
            targets = RegOutput.solve_groups(targets, 'command line')
9✔
480
        except KiPlotConfigurationError as e:
×
481
            config_error(str(e))
×
482
        logger.debug('Outputs after groups expansion: {}'.format(targets))
9✔
483
        # Now convert the list of names into a list of output objects
484
        if cli_order:
9✔
485
            # Add them in the same order found at the command line
486
            targets = [RegOutput.get_output(name) for name in targets]
3✔
487
        else:
488
            # Add them in the declared order
489
            new_targets = []
9✔
490
            if invert:
9✔
491
                # Invert the selection
492
                for out in RegOutput.get_outputs():
3✔
493
                    if (out.name not in targets) and out.run_by_default:
3✔
494
                        new_targets.append(out)
3✔
495
                    else:
496
                        logger.debug('Skipping `{}` output'.format(out.name))
3✔
497
            else:
498
                # Normal list
499
                for out in RegOutput.get_outputs():
9✔
500
                    if out.name in targets:
9✔
501
                        new_targets.append(out)
9✔
502
                    else:
503
                        logger.debug('Skipping `{}` output'.format(out.name))
9✔
504
            targets = new_targets
9✔
505
    logger.debug('Outputs before preflights: {}'.format([t.name for t in targets]))
10✔
506
    # Run the preflights
507
    preflight_checks(skip_pre, targets)
10✔
508
    logger.debug('Outputs after preflights: {}'.format([t.name for t in targets]))
10✔
509
    if not cli_order and not no_priority:
10✔
510
        # Sort by priority
511
        targets = sorted(targets, key=lambda o: o.priority, reverse=True)
10✔
512
        logger.debug('Outputs after sorting: {}'.format([t.name for t in targets]))
10✔
513
    # Configure and run the outputs
514
    for out in targets:
10✔
515
        if config_output(out, dont_stop=dont_stop):
10✔
516
            logger.info('- '+str(out))
10✔
517
            run_output(out, dont_stop)
10✔
518

519

520
def generate_outputs(outputs, targets, invert, skip_pre, cli_order, no_priority, dont_stop=False):
10✔
521
    setup_resources()
10✔
522
    prj = None
10✔
523
    if GS.global_restore_project:
10✔
524
        # Memorize the project content to restore it at exit
525
        prj = GS.read_pro()
2✔
526
    try:
10✔
527
        _generate_outputs(outputs, targets, invert, skip_pre, cli_order, no_priority, dont_stop)
10✔
528
    finally:
529
        # Restore the project file
530
        GS.write_pro(prj)
10✔
531

532

533
def adapt_file_name(name):
10✔
534
    if not name.startswith('/usr'):
3✔
535
        name = os.path.relpath(name)
3✔
536
    name = name.replace(' ', r'\ ')
3✔
537
    if '$' in name:
3✔
538
        logger.warning(W_WRONGCHAR+'Wrong character in file name `{}`'.format(name))
3✔
539
    return name
3✔
540

541

542
def gen_global_targets(f, pre_targets, out_targets, type):
10✔
543
    extra_targets = []
4✔
544
    pre = 'pre_'+type
4✔
545
    out = 'out_'+type
4✔
546
    all = 'all_'+type
4✔
547
    if pre_targets:
4✔
548
        f.write('{}:{}\n\n'.format(pre, pre_targets))
3✔
549
        extra_targets.append(pre)
3✔
550
    if out_targets:
4✔
551
        f.write('{}:{}\n\n'.format(out, out_targets))
3✔
552
        extra_targets.append(out)
3✔
553
    if pre_targets or out_targets:
4✔
554
        tg = ''
3✔
555
        if pre_targets:
3✔
556
            tg = ' '+pre
3✔
557
        if out_targets:
3✔
558
            tg += ' '+out
3✔
559
        f.write('{}:{}\n\n'.format(all, tg))
3✔
560
        extra_targets.append(all)
3✔
561
    return extra_targets
4✔
562

563

564
def get_pre_targets(targets, dependencies, is_pre):
10✔
565
    pcb_targets = sch_targets = ''
4✔
566
    prefs = BasePreFlight.get_in_use_objs()
4✔
567
    try:
4✔
568
        for pre in prefs:
4✔
569
            tg = pre.get_targets()
3✔
570
            if not tg:
3✔
571
                continue
3✔
572
            name = pre._name
3✔
573
            targets[name] = [adapt_file_name(fn) for fn in tg]
3✔
574
            dependencies[name] = [adapt_file_name(fn) for fn in pre.get_dependencies()]
3✔
575
            is_pre.add(name)
3✔
576
            if pre.is_sch():
3✔
577
                sch_targets += ' '+name
3✔
578
            if pre.is_pcb():
3✔
579
                pcb_targets += ' '+name
3✔
580
    except KiPlotConfigurationError as e:
×
581
        config_error("In preflight '"+name+"': "+str(e))
×
582
    return pcb_targets, sch_targets
4✔
583

584

585
def get_out_targets(outputs, ori_names, targets, dependencies, comments, no_default):
10✔
586
    pcb_targets = sch_targets = ''
4✔
587
    try:
4✔
588
        for out in outputs:
4✔
589
            name = name2make(out.name)
3✔
590
            ori_names[name] = out.name
3✔
591
            tg = out.get_targets(out.expand_dirname(os.path.join(GS.out_dir, out.dir)))
3✔
592
            if not tg:
3✔
593
                continue
3✔
594
            targets[name] = [adapt_file_name(fn) for fn in tg]
3✔
595
            dependencies[name] = [adapt_file_name(fn) for fn in out.get_dependencies()]
3✔
596
            if out.comment:
3✔
597
                comments[name] = out.comment
3✔
598
            if not out.run_by_default:
3✔
599
                no_default.add(name)
×
600
            if out.is_sch():
3✔
601
                sch_targets += ' '+name
3✔
602
            if out.is_pcb():
3✔
603
                pcb_targets += ' '+name
3✔
604
    except KiPlotConfigurationError as e:
×
605
        config_error("In output '"+name+"': "+str(e))
×
606
    return pcb_targets, sch_targets
4✔
607

608

609
def generate_makefile(makefile, cfg_file, outputs, kibot_sys=False):
10✔
610
    cfg_file = os.path.relpath(cfg_file)
4✔
611
    logger.info('- Creating makefile `{}` from `{}`'.format(makefile, cfg_file))
4✔
612
    with open(makefile, 'wt') as f:
4✔
613
        f.write('#!/usr/bin/make\n')
4✔
614
        f.write('# Automatically generated by KiBot from `{}`\n'.format(cfg_file))
4✔
615
        fname = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src', 'kibot'))
4✔
616
        if kibot_sys or not os.path.isfile(fname):
4✔
617
            fname = 'kibot'
1✔
618
        f.write('KIBOT?={}\n'.format(fname))
4✔
619
        dbg = ''
4✔
620
        if GS.debug_level > 0:
4✔
621
            dbg = '-'+'v'*GS.debug_level
3✔
622
        f.write('DEBUG?={}\n'.format(dbg))
4✔
623
        f.write('CONFIG={}\n'.format(cfg_file))
4✔
624
        if GS.sch_file:
4✔
625
            f.write('SCH={}\n'.format(os.path.relpath(GS.sch_file)))
4✔
626
        if GS.pcb_file:
4✔
627
            f.write('PCB={}\n'.format(os.path.relpath(GS.pcb_file)))
4✔
628
        f.write('DEST={}\n'.format(os.path.relpath(GS.out_dir)))
4✔
629
        f.write('KIBOT_CMD=$(KIBOT) $(DEBUG) -c $(CONFIG) -e $(SCH) -b $(PCB) -d $(DEST)\n')
4✔
630
        f.write('LOGFILE?=kibot_error.log\n')
4✔
631
        f.write('\n')
4✔
632
        # Configure all outputs
633
        for out in outputs:
4✔
634
            config_output(out)
3✔
635
        # Get all targets and dependencies
636
        targets = OrderedDict()
4✔
637
        dependencies = OrderedDict()
4✔
638
        comments = {}
4✔
639
        ori_names = {}
4✔
640
        is_pre = set()
4✔
641
        no_default = set()
4✔
642
        # Preflights
643
        pre_pcb_targets, pre_sch_targets = get_pre_targets(targets, dependencies, is_pre)
4✔
644
        # Outputs
645
        out_pcb_targets, out_sch_targets = get_out_targets(outputs, ori_names, targets, dependencies, comments, no_default)
4✔
646
        # all target
647
        f.write('#\n# Default target\n#\n')
4✔
648
        f.write('all: '+' '.join(filter(lambda x: x not in no_default, targets.keys()))+'\n\n')
4✔
649
        extra_targets = ['all']
4✔
650
        # PCB/SCH specific targets
651
        f.write('#\n# SCH/PCB targets\n#\n')
4✔
652
        extra_targets.extend(gen_global_targets(f, pre_sch_targets, out_sch_targets, 'sch'))
4✔
653
        extra_targets.extend(gen_global_targets(f, pre_pcb_targets, out_pcb_targets, 'pcb'))
4✔
654
        # Generate the output targets
655
        f.write('#\n# Available targets (outputs)\n#\n')
4✔
656
        for name, target in targets.items():
4✔
657
            f.write(name+': '+' '.join(target)+'\n\n')
3✔
658
        # Generate the output dependencies
659
        f.write('#\n# Rules and dependencies\n#\n')
4✔
660
        if GS.debug_enabled:
4✔
661
            kibot_cmd = '\t$(KIBOT_CMD)'
3✔
662
            log_action = ''
3✔
663
        else:
664
            kibot_cmd = '\t@$(KIBOT_CMD)'
4✔
665
            log_action = ' 2>> $(LOGFILE)'
4✔
666
        skip_all = ','.join(is_pre)
4✔
667
        for name, dep in dependencies.items():
4✔
668
            if name in comments:
3✔
669
                f.write('# '+comments[name]+'\n')
3✔
670
            dep.append(cfg_file)
3✔
671
            f.write(' '.join(targets[name])+': '+' '.join(dep)+'\n')
3✔
672
            if name in is_pre:
3✔
673
                skip = filter(lambda n: n != name, is_pre)
3✔
674
                f.write('{} -s {} -i{}\n\n'.format(kibot_cmd, ','.join(skip), log_action))
3✔
675
            else:
676
                f.write('{} -s {} "{}"{}\n\n'.format(kibot_cmd, skip_all, ori_names[name], log_action))
3✔
677
        # Mark all outputs as PHONY
678
        f.write('.PHONY: '+' '.join(extra_targets+list(targets.keys()))+'\n')
4✔
679

680

681
def guess_ki6_sch(schematics):
10✔
682
    schematics = list(filter(lambda x: x.endswith('.kicad_sch'), schematics))
2✔
683
    if len(schematics) == 1:
2✔
684
        return schematics[0]
×
685
    if len(schematics) == 0:
2✔
686
        return None
×
687
    for fname in schematics:
2✔
688
        with open(fname, 'rt') as f:
2✔
689
            text = f.read()
2✔
690
        if 'sheet_instances' in text:
2✔
691
            return fname
×
692
    return None
2✔
693

694

695
def avoid_mixing_5_and_6():
10✔
696
    logger.error('Found KiCad 5 and KiCad 6 files, make sure the whole project uses one version')
×
697
    exit(EXIT_BAD_CONFIG)
×
698

699

700
def solve_schematic(base_dir, a_schematic=None, a_board_file=None, config=None, sug_e=True):
10✔
701
    schematic = a_schematic
10✔
702
    if not schematic and a_board_file:
10✔
703
        base = os.path.splitext(a_board_file)[0]
10✔
704
        sch = os.path.join(base_dir, base+'.sch')
10✔
705
        kicad_sch = os.path.join(base_dir, base+'.kicad_sch')
10✔
706
        found_sch = os.path.isfile(sch)
10✔
707
        found_kicad_sch = os.path.isfile(kicad_sch)
10✔
708
        if found_sch and found_kicad_sch:
10✔
709
            avoid_mixing_5_and_6()
×
710
        if found_sch:
10✔
711
            schematic = sch
4✔
712
        elif GS.ki6 and found_kicad_sch:
10✔
713
            schematic = kicad_sch
6✔
714
    if not schematic:
10✔
715
        schematics = glob(os.path.join(base_dir, '*.sch'))
10✔
716
        if GS.ki6:
10✔
717
            schematics += glob(os.path.join(base_dir, '*.kicad_sch'))
6✔
718
        if len(schematics) == 1:
10✔
719
            schematic = schematics[0]
6✔
720
            logger.info('Using SCH file: '+os.path.relpath(schematic))
6✔
721
        elif len(schematics) > 1:
10✔
722
            # Look for a schematic with the same name as the config
723
            if config:
3✔
724
                if config[0] == '.':
3✔
725
                    # Unhide hidden config
726
                    config = config[1:]
×
727
                # Remove any extension
728
                last_split = None
3✔
729
                while '.' in config and last_split != config:
3✔
730
                    last_split = config
3✔
731
                    config = os.path.splitext(config)[0]
3✔
732
                # Try KiCad 5
733
                sch = os.path.join(base_dir, config+'.sch')
3✔
734
                found_sch = os.path.isfile(sch)
3✔
735
                # Try KiCad 6
736
                kicad_sch = os.path.join(base_dir, config+'.kicad_sch')
3✔
737
                found_kicad_sch = os.path.isfile(kicad_sch)
3✔
738
                if found_sch and found_kicad_sch:
3✔
739
                    avoid_mixing_5_and_6()
×
740
                if found_sch:
3✔
741
                    schematic = sch
×
742
                elif GS.ki6 and found_kicad_sch:
3✔
743
                    schematic = kicad_sch
×
744
            if not schematic:
3✔
745
                # Look for a schematic with a PCB and/or project
746
                for sch in schematics:
3✔
747
                    base = os.path.splitext(sch)[0]
3✔
748
                    if (os.path.isfile(os.path.join(base_dir, base+'.pro')) or
3✔
749
                       os.path.isfile(os.path.join(base_dir, base+'.kicad_pro')) or
750
                       os.path.isfile(os.path.join(base_dir, base+'.kicad_pcb'))):
751
                        schematic = sch
3✔
752
                        break
3✔
753
                else:
754
                    # No way to select one, just take the first
755
                    if GS.ki6:
3✔
756
                        schematic = guess_ki6_sch(schematics)
2✔
757
                    if not schematic:
3✔
758
                        schematic = schematics[0]
3✔
759
            msg = ' if you want to use another use -e option' if sug_e else ''
3✔
760
            logger.warning(W_VARSCH + 'More than one SCH file found in `'+base_dir+'`.\n'
3✔
761
                           '  Using '+schematic+msg+'.')
762
    if schematic and not os.path.isfile(schematic):
10✔
763
        logger.error("Schematic file not found: "+schematic)
3✔
764
        exit(NO_SCH_FILE)
3✔
765
    if schematic:
10✔
766
        schematic = os.path.abspath(schematic)
10✔
767
        logger.debug('Using schematic: `{}`'.format(schematic))
10✔
768
        logger.debug('Real schematic name: `{}`'.format(cased_path(schematic)))
10✔
769
    else:
770
        logger.debug('No schematic file found')
10✔
771
    return schematic
10✔
772

773

774
def check_board_file(board_file):
10✔
775
    if board_file and not os.path.isfile(board_file):
10✔
776
        logger.error("Board file not found: "+board_file)
3✔
777
        exit(NO_PCB_FILE)
3✔
778

779

780
def solve_board_file(base_dir, a_board_file=None, sug_b=True):
10✔
781
    schematic = GS.sch_file
10✔
782
    board_file = a_board_file
10✔
783
    if not board_file and schematic:
10✔
784
        pcb = os.path.join(base_dir, os.path.splitext(schematic)[0]+'.kicad_pcb')
10✔
785
        if os.path.isfile(pcb):
10✔
786
            board_file = pcb
10✔
787
    if not board_file:
10✔
788
        board_files = glob(os.path.join(base_dir, '*.kicad_pcb'))
6✔
789
        if len(board_files) == 1:
6✔
790
            board_file = board_files[0]
3✔
791
            logger.info('Using PCB file: '+os.path.relpath(board_file))
3✔
792
        elif len(board_files) > 1:
6✔
793
            board_file = board_files[0]
3✔
794
            msg = ' if you want to use another use -b option' if sug_b else ''
3✔
795
            logger.warning(W_VARPCB + 'More than one PCB file found in `'+base_dir+'`.\n'
3✔
796
                           '  Using '+board_file+msg+'.')
797
    check_board_file(board_file)
10✔
798
    if board_file:
10✔
799
        logger.debug('Using PCB: `{}`'.format(board_file))
10✔
800
        logger.debug('Real PCB name: `{}`'.format(cased_path(board_file)))
10✔
801
    else:
802
        logger.debug('No PCB file found')
6✔
803
    return board_file
10✔
804

805

806
def solve_project_file():
10✔
807
    if GS.pcb_file:
10✔
808
        pro_name = GS.pcb_no_ext+GS.pro_ext
10✔
809
        if os.path.isfile(pro_name):
10✔
810
            return pro_name
9✔
811
    if GS.sch_file:
10✔
812
        pro_name = GS.sch_no_ext+GS.pro_ext
10✔
813
        if os.path.isfile(pro_name):
10✔
814
            return pro_name
3✔
815
    return None
10✔
816

817

818
def look_for_used_layers():
10✔
819
    from .layer import Layer
3✔
820
    Layer.reset()
3✔
821
    layers = set()
3✔
822
    components = {}
3✔
823
    # Look inside the modules
824
    for m in GS.get_modules():
3✔
825
        layer = m.GetLayer()
3✔
826
        components[layer] = components.get(layer, 0)+1
3✔
827
        for gi in m.GraphicalItems():
3✔
828
            layers.add(gi.GetLayer())
3✔
829
        for pad in m.Pads():
3✔
830
            for id in pad.GetLayerSet().Seq():
3✔
831
                layers.add(id)
3✔
832
    # All drawings in the PCB
833
    for e in GS.board.GetDrawings():
3✔
834
        layers.add(e.GetLayer())
3✔
835
    # Zones
836
    for e in list(GS.board.Zones()):
3✔
837
        layers.add(e.GetLayer())
3✔
838
    # Tracks and vias
839
    via_type = 'VIA' if GS.ki5 else 'PCB_VIA'
3✔
840
    for e in GS.board.GetTracks():
3✔
841
        if e.GetClass() == via_type:
3✔
842
            for id in e.GetLayerSet().Seq():
3✔
843
                layers.add(id)
3✔
844
        else:
845
            layers.add(e.GetLayer())
3✔
846
    # Now filter the pads and vias potential layers
847
    declared_layers = {la._id for la in Layer.solve('all')}
3✔
848
    layers = sorted(declared_layers.intersection(layers))
3✔
849
    logger.debug('- Detected layers: {}'.format(layers))
3✔
850
    layers = Layer.solve(layers)
3✔
851
    for la in layers:
3✔
852
        la.components = components.get(la._id, 0)
3✔
853
    return layers
3✔
854

855

856
def discover_files(dest_dir):
10✔
857
    """ Look for schematic and PCBs at the dest_dir.
858
        Return the name of the example file to generate. """
859
    GS.pcb_file = None
3✔
860
    GS.sch_file = None
3✔
861
    # Check if we have useful files
862
    fname = os.path.join(dest_dir, 'kibot_generated.kibot.yaml')
3✔
863
    GS.set_sch(solve_schematic(dest_dir, sug_e=False))
3✔
864
    GS.set_pcb(solve_board_file(dest_dir, sug_b=False))
3✔
865
    GS.set_pro(solve_project_file())
3✔
866
    return fname
3✔
867

868

869
def yaml_dump(f, tree):
10✔
870
    if version_str2tuple(yaml.__version__) < (3, 14):
3✔
871
        f.write(yaml.dump(tree))
×
872
    else:
873
        # sort_keys was introduced after 3.13
874
        f.write(yaml.dump(tree, sort_keys=False))
3✔
875

876

877
def register_xmp_import(name):
10✔
878
    """ Register an import we need for an example """
879
    global needed_imports
880
    needed_imports.add(name)
2✔
881

882

883
def generate_one_example(dest_dir, types):
10✔
884
    """ Generate a example config for dest_dir """
885
    fname = discover_files(dest_dir)
3✔
886
    # Abort if none
887
    if not GS.pcb_file and not GS.sch_file:
3✔
888
        return None
×
889
    # Reset the board and schematic
890
    GS.board = None
3✔
891
    GS.sch = None
3✔
892
    # Create the config
893
    with open(fname, 'wt') as f:
3✔
894
        logger.info('- Creating {} example configuration'.format(fname))
3✔
895
        f.write("# This is a working example.\n")
3✔
896
        f.write("# For a more complete reference use `--example`\n")
3✔
897
        f.write('kibot:\n  version: 1\n\n')
3✔
898
        # Outputs
899
        outs = RegOutput.get_registered()
3✔
900
        # List of layers
901
        layers = []
3✔
902
        if GS.pcb_file:
3✔
903
            load_board(GS.pcb_file)
3✔
904
            layers = look_for_used_layers()
3✔
905
        if GS.sch_file:
3✔
906
            load_sch()
3✔
907
        # Filter some warnings
908
        fil = [{'number': 1007},  # No information for a component in a distributor
3✔
909
               {'number': 1015},  # More than one component in a search for a distributor
910
               {'number': 58},    # Missing project file
911
               ]
912
        glb = {'filters': fil}
3✔
913
        yaml_dump(f, {'global': glb})
3✔
914
        f.write('\n')
3✔
915
        # A helper for the internal templates
916
        global needed_imports
917
        needed_imports = set()
3✔
918
        # All the outputs
919
        outputs = []
3✔
920
        for n, cls in OrderedDict(sorted(outs.items())).items():
3✔
921
            o = cls()
3✔
922
            if types and n not in types:
3✔
923
                logger.debug('- {}, not selected (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
×
924
                continue
×
925
            if ((not(o.is_pcb() and GS.pcb_file) and not(o.is_sch() and GS.sch_file)) or
3✔
926
               ((o.is_pcb() and o.is_sch()) and (not GS.pcb_file or not GS.sch_file))):
927
                logger.debug('- {}, skipped (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch()))
3✔
928
                continue
3✔
929
            # Look for templates
930
            tpls = glob(os.path.join(GS.get_resource_path('config_templates'), n, '*.kibot.yaml'))
3✔
931
            if tpls:
3✔
932
                # Load the templates
933
                tpl_names = tpls
3✔
934
                tpls = []
3✔
935
                for t in tpl_names:
3✔
936
                    tree = yaml.safe_load(open(t))
3✔
937
                    tpls.append(tree['outputs'])
3✔
938
                    imps = tree.get('import')
3✔
939
                    if imps:
3✔
940
                        needed_imports.update([imp['file'] for imp in imps])
3✔
941
            tree = cls.get_conf_examples(n, layers, tpls)
3✔
942
            if tree:
3✔
943
                logger.debug('- {}, generated'.format(n))
3✔
944
                if tpls:
3✔
945
                    logger.debug(' - Templates: {}'.format(tpl_names))
3✔
946
                outputs.extend(tree)
3✔
947
            else:
948
                logger.debug('- {}, nothing to do'.format(n))
3✔
949
        if needed_imports:
3✔
950
            imports = [{'file': man} for man in sorted(needed_imports)]
3✔
951
            yaml_dump(f, {'import': imports})
3✔
952
            f.write('\n')
3✔
953
        if outputs:
3✔
954
            yaml_dump(f, {'outputs': outputs})
3✔
955
        else:
956
            return None
×
957
    return fname
3✔
958

959

960
def generate_targets(config_file):
10✔
961
    """ Generate all possible targets for the configuration file """
962
    # Reset the board and schematic
963
    GS.board = None
×
964
    GS.sch = None
×
965
    # Reset the list of outputs
966
    RegOutput.reset()
×
967
    # Read the config file
968
    cr = CfgYamlReader()
×
969
    with open(config_file) as cf_file:
×
970
        outputs = cr.read(cf_file)
×
971
    # Do all the job
972
    generate_outputs(outputs, [], False, None, False, False, dont_stop=True)
×
973

974

975
def _walk(path, depth):
10✔
976
    """ Recursively list files and directories up to a certain depth """
977
    depth -= 1
3✔
978
    with os.scandir(path) as p:
3✔
979
        for entry in p:
3✔
980
            yield entry.path
3✔
981
            if entry.is_dir() and depth > 0:
3✔
982
                yield from _walk(entry.path, depth)
×
983

984

985
def setup_fonts(source):
10✔
986
    if not os.path.isdir(source):
10✔
987
        logger.debug('No font resources dir')
10✔
988
        return
10✔
989
    dest = os.path.expanduser('~/.fonts/')
1✔
990
    installed = False
1✔
991
    for f in glob(os.path.join(source, '*.ttf')):
1✔
992
        fname = os.path.basename(f)
1✔
993
        fdest = os.path.join(dest, fname)
1✔
994
        if os.path.isfile(fdest):
1✔
995
            logger.debug('Font {} already installed'.format(fname))
×
996
            continue
×
997
        logger.info('Installing font {}'.format(fname))
1✔
998
        if not os.path.isdir(dest):
1✔
999
            os.makedirs(dest)
1✔
1000
        copy2(f, fdest)
1✔
1001
        installed = True
1✔
1002
    if installed:
1✔
1003
        run_command(['fc-cache'])
1✔
1004

1005

1006
def setup_colors(source):
10✔
1007
    if not os.path.isdir(source):
10✔
1008
        logger.debug('No color resources dir')
10✔
1009
        return
10✔
1010
    if not GS.kicad_conf_path:
1✔
1011
        return
×
1012
    dest = os.path.join(GS.kicad_conf_path, 'colors')
1✔
1013
    for f in glob(os.path.join(source, '*.json')):
1✔
1014
        fname = os.path.basename(f)
1✔
1015
        fdest = os.path.join(dest, fname)
1✔
1016
        if os.path.isfile(fdest):
1✔
1017
            logger.debug('Color {} already installed'.format(fname))
×
1018
            continue
×
1019
        logger.info('Installing color {}'.format(fname))
1✔
1020
        if not os.path.isdir(dest):
1✔
1021
            os.makedirs(dest)
×
1022
        copy2(f, fdest)
1✔
1023

1024

1025
def setup_resources():
10✔
1026
    if not GS.global_resources_dir:
10✔
1027
        logger.debug('No resources dir')
×
1028
        return
×
1029
    setup_fonts(os.path.join(GS.global_resources_dir, 'fonts'))
10✔
1030
    setup_colors(os.path.join(GS.global_resources_dir, 'colors'))
10✔
1031

1032

1033
def generate_examples(start_dir, dry, types):
10✔
1034
    if not start_dir:
3✔
1035
        start_dir = '.'
×
1036
    else:
1037
        if not os.path.isdir(start_dir):
3✔
1038
            logger.error('Invalid dir {} to quick start'.format(start_dir))
×
1039
            exit(WRONG_ARGUMENTS)
×
1040
    # Set default global options
1041
    glb = GS.class_for_global_opts()
3✔
1042
    glb.set_tree({})
3✔
1043
    glb.config(None)
3✔
1044
    # Install the resources
1045
    setup_resources()
3✔
1046
    # Look for candidate dirs
1047
    k_files_regex = re.compile(r'([^/]+)\.(kicad_pcb|kicad_sch|sch)$')
3✔
1048
    candidates = set()
3✔
1049
    for f in _walk(start_dir, 6):
3✔
1050
        if k_files_regex.search(f):
3✔
1051
            candidates.add(os.path.dirname(f))
3✔
1052
    # Try to generate the configs in the candidate places
1053
    confs = []
3✔
1054
    for c in sorted(candidates):
3✔
1055
        logger.info('Analyzing `{}` dir'.format(c))
3✔
1056
        res = generate_one_example(c, types)
3✔
1057
        if res:
3✔
1058
            confs.append(res)
3✔
1059
        logger.info('')
3✔
1060
    confs.sort()
3✔
1061
    # Just the configs, not the targets
1062
    if dry:
3✔
1063
        return
3✔
1064
    # Try to generate all the stuff
1065
    if GS.out_dir_in_cmd_line:
×
1066
        out_dir = GS.out_dir
×
1067
    else:
1068
        out_dir = 'Generated'
×
1069
    for n, c in enumerate(confs):
×
1070
        conf_dir = os.path.dirname(c)
×
1071
        if len(confs) > 1:
×
1072
            subdir = '%03d-%s' % (n+1, conf_dir.replace('/', ',').replace(' ', '_'))
×
1073
            dest = os.path.join(out_dir, subdir)
×
1074
        else:
1075
            dest = out_dir
×
1076
        GS.out_dir = dest
×
1077
        logger.info('Generating targets for `{}`, destination: `{}`'.format(c, dest))
×
1078
        os.makedirs(dest, exist_ok=True)
×
1079
        # Create a log file with all the debug we can
1080
        fl = log.set_file_log(os.path.join(dest, 'kibot.log'))
×
1081
        old_lvl = GS.debug_level
×
1082
        GS.debug_level = 10
×
1083
        # Detect the SCH and PCB again
1084
        discover_files(conf_dir)
×
1085
        # Generate all targets
1086
        generate_targets(c)
×
1087
        # Close the debug file
1088
        log.remove_file_log(fl)
×
1089
        GS.debug_level = old_lvl
×
1090
        logger.info('')
×
1091
    # Try to open a browser
1092
    index = os.path.join(GS.out_dir, 'index.html')
×
1093
    if os.environ.get('DISPLAY') and which('x-www-browser') and os.path.isfile(index):
×
1094
        Popen(['x-www-browser', index])
×
1095

1096

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

© 2025 Coveralls, Inc