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

INTI-CMNB / KiBot / 5378203845

pending completion
5378203845

push

github-actions

set-soft
[Position][Added] quote_all option

- To quote all columns in the CSV output

Closes #456

7 of 7 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

21892 of 24887 relevant lines covered (87.97%)

5.59 hits per line

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

88.09
/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 = {}
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, definitions=None):
10✔
878
    """ Register an import we need for an example """
879
    global needed_imports
880
    assert name not in needed_imports
3✔
881
    needed_imports[name] = definitions
3✔
882

883

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

958

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

973

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

983

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

1004

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

1023

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

1031

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

1095

1096
# To avoid circular dependencies: Optionable needs it, but almost everything needs Optionable
1097
GS.load_board = load_board
10✔
1098
GS.load_sch = load_sch
10✔
1099
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