KiBot/kibot/__main__.py

332 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2020 Salvador E. Tropea
# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2018 John Beard
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Adapted from: https://github.com/johnbeard/kiplot
"""KiBot: KiCad automation tool for documents generation
Usage:
kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE]
[-q | -v...] [-i] [-g DEF]... [TARGET...]
kibot [-v...] [-c PLOT_CONFIG] --list
kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example
kibot [-v...] --help-filters
kibot [-v...] --help-list-outputs
kibot [-v...] --help-output=HELP_OUTPUT
kibot [-v...] --help-outputs
kibot [-v...] --help-preflights
kibot -h | --help
kibot --version
Arguments:
TARGET Outputs to generate, default is all
Options:
-h, --help Show this help message and exit
-b BOARD, --board-file BOARD The PCB .kicad-pcb board file
-c CONFIG, --plot-config CONFIG The plotting config file to use
-d OUT_DIR, --out-dir OUT_DIR The output directory [default: .]
-e SCHEMA, --schematic SCHEMA The schematic file (.sch)
-g DEF, --global-redef DEF Overwrite a global value (VAR=VAL)
--help-filters List supported filters and details
--help-list-outputs List supported outputs
--help-output HELP_OUTPUT Help for this particular output
--help-outputs List supported outputs and details
--help-preflights List supported preflights and details
-i, --invert-sel Generate the outputs not listed as targets
-l, --list List available outputs (in the config file)
-p, --copy-options Copy plot options from the PCB file
-P, --copy-and-expand As -p but expand the list of layers
-q, --quiet Remove information logs
-s PRE, --skip-pre PRE Skip preflights, comma separated or `all`
-v, --verbose Show debugging information
-V, --version Show program's version number and exit
-x, --example Create an example configuration file.
"""
__author__ = 'Salvador E. Tropea, John Beard'
__copyright__ = 'Copyright 2018-2020, Salvador E. Tropea/INTI/John Beard'
__credits__ = ['Salvador E. Tropea', 'John Beard']
__license__ = 'GPL v3+'
__email__ = 'set@ieee.org'
__url__ = 'https://github.com/INTI-CMNB/KiBot/'
__status__ = 'stable'
__version__ = '0.7.0'
import os
import sys
import stat
import gzip
import locale
from glob import glob
from logging import DEBUG
# Import log first to set the domain
from . import log
log.set_domain('kibot')
logger = log.init()
from .gs import (GS)
from .kiplot import (generate_outputs, load_actions, config_output)
from .pre_base import (BasePreFlight)
from .config_reader import (CfgYamlReader, print_outputs_help, print_output_help, print_preflights_help, create_example,
print_filters_help)
from .misc import (NO_PCB_FILE, NO_SCH_FILE, EXIT_BAD_ARGS, W_VARSCH, W_VARCFG, W_VARPCB, W_PYCACHE)
from .docopt import docopt
has_macro = [
'layer',
'drill_marks',
'fil_base',
'fil_generic',
'globals',
'out_any_drill',
'out_any_layer',
'out_base',
'out_bom',
'out_dxf',
'out_excellon',
'out_gerb_drill',
'out_gerber',
'out_hpgl',
'out_ibom',
'out_kibom',
'out_pcbdraw',
'out_pdf',
'out_pdf_pcb_print',
'out_pdf_sch_print',
'out_position',
'out_ps',
'out_sch_variant',
'out_step',
'out_svg',
'out_svg_sch_print',
'pre_check_zone_fills',
'pre_drc',
'pre_erc',
'pre_filters',
'pre_ignore_unconnected',
'pre_update_xml',
'var_base',
'var_kibom',
]
def list_pre_and_outs(logger, outputs):
logger.info('Available actions:\n')
pf = BasePreFlight.get_in_use_objs()
if len(pf):
logger.info('Pre-flight:')
for c in pf:
logger.info('- '+str(c))
if len(outputs):
logger.info('Outputs:')
for o in outputs:
config_output(o)
logger.info('- '+str(o))
def solve_schematic(a_schematic, a_board_file):
schematic = a_schematic
if not schematic and a_board_file:
sch = os.path.splitext(a_board_file)[0]+'.sch'
if os.path.isfile(sch):
schematic = sch
if not schematic:
schematics = glob('*.sch')
if len(schematics) == 1:
schematic = schematics[0]
logger.info('Using SCH file: '+schematic)
elif len(schematics) > 1:
# Look for a schematic with a PCB
boards = glob('*.kicad_pcb')
boards_schs = [x[:-9]+'sch' for x in boards]
for sch in schematics:
if sch in boards_schs:
schematic = sch
break
if schematic is None:
schematic = schematics[0]
logger.warning(W_VARSCH + 'More than one SCH file found in current directory.\n'
' Using '+schematic+' if you want to use another use -e option.')
if schematic and not os.path.isfile(schematic):
logger.error("Schematic file not found: "+schematic)
sys.exit(NO_SCH_FILE)
if schematic:
schematic = os.path.abspath(schematic)
logger.debug('Using schematic: `{}`'.format(schematic))
else:
logger.debug('No schematic file found')
return schematic
def solve_config(a_plot_config):
plot_config = a_plot_config
if not plot_config:
plot_configs = glob('*.kibot.yaml')+glob('*.kiplot.yaml')
if len(plot_configs) == 1:
plot_config = plot_configs[0]
logger.info('Using config file: '+plot_config)
elif len(plot_configs) > 1:
plot_config = plot_configs[0]
logger.warning(W_VARCFG + 'More than one config file found in current directory.\n'
' Using '+plot_config+' if you want to use another use -c option.')
else:
logger.error('No config file found (*.kibot.yaml), use -c to specify one.')
sys.exit(EXIT_BAD_ARGS)
if not os.path.isfile(plot_config):
logger.error("Plot config file not found: "+plot_config)
sys.exit(EXIT_BAD_ARGS)
logger.debug('Using configuration file: `{}`'.format(plot_config))
return plot_config
def check_board_file(board_file):
if board_file and not os.path.isfile(board_file):
logger.error("Board file not found: "+board_file)
sys.exit(NO_PCB_FILE)
def solve_board_file(schematic, a_board_file):
board_file = a_board_file
if not board_file and schematic:
pcb = os.path.splitext(schematic)[0]+'.kicad_pcb'
if os.path.isfile(pcb):
board_file = pcb
if not board_file:
board_files = glob('*.kicad_pcb')
if len(board_files) == 1:
board_file = board_files[0]
logger.info('Using PCB file: '+board_file)
elif len(board_files) > 1:
board_file = board_files[0]
logger.warning(W_VARPCB + 'More than one PCB file found in current directory.\n'
' Using '+board_file+' if you want to use another use -b option.')
check_board_file(board_file)
if board_file:
logger.debug('Using PCB: `{}`'.format(board_file))
else:
logger.debug('No PCB file found')
return board_file
def set_locale():
""" Try to set the locale for all the cataegories.
If it fails try with LC_NUMERIC (the one we need for tests). """
try:
locale.setlocale(locale.LC_ALL, '')
return
except locale.Error:
pass
try:
locale.setlocale(locale.LC_NUMERIC, '')
return
except locale.Error:
pass
def clean_cache():
""" Files that expands macros can't be pre-compiled """
here = os.path.abspath(os.path.dirname(__file__))
cache = os.path.join(here, '__pycache__')
logger.debug('Python cache dir: '+cache)
try:
if os.path.isdir(cache):
for f in has_macro:
fnames = glob(os.path.join(cache, f+'.*'))
for fname in fnames:
if os.path.isfile(fname):
# Don't remove read-only files
r = os.stat(fname)
if stat.S_IMODE(r.st_mode) & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH):
logger.debug('Removing '+fname)
os.remove(fname)
else:
raise PermissionError()
except PermissionError:
logger.warning(W_PYCACHE + 'Wrong installation, avoid creating Python cache files\n'
'If you are using `pip` to install use the `--no-compile` option:\n'
'$ pip install --no-compile kibot\n')
def main():
set_locale()
ver = 'KiBot '+__version__+' - '+__copyright__+' - License: '+__license__
args = docopt(__doc__, version=ver, options_first=True)
# Set the specified verbosity
log.set_verbosity(logger, args.verbose, args.quiet)
GS.debug_enabled = logger.getEffectiveLevel() <= DEBUG
GS.debug_level = args.verbose
# Parse global overwrite options
for redef in args.global_redef:
if '=' not in redef:
logger.error('Malformed global-redef option, must be VARIABLE=VALUE ({})'.format(redef))
sys.exit(EXIT_BAD_ARGS)
var = redef.split('=')[0]
GS.global_from_cli[var] = redef[len(var)+1:]
clean_cache()
# Output dir: relative to CWD (absolute path overrides)
GS.out_dir = os.path.join(os.getcwd(), args.out_dir)
# Load output and preflight plugins
load_actions()
if args.help_outputs or args.help_list_outputs:
print_outputs_help(details=args.help_outputs)
sys.exit(0)
if args.help_output:
print_output_help(args.help_output)
sys.exit(0)
if args.help_preflights:
print_preflights_help()
sys.exit(0)
if args.help_filters:
print_filters_help()
sys.exit(0)
if args.example:
check_board_file(args.board_file)
if args.copy_options and not args.board_file:
logger.error('Asked to copy options but no PCB specified.')
sys.exit(EXIT_BAD_ARGS)
create_example(args.board_file, GS.out_dir, args.copy_options, args.copy_and_expand)
sys.exit(0)
# Determine the YAML file
plot_config = solve_config(args.plot_config)
# Read the config file
cr = CfgYamlReader()
outputs = None
try:
# The Python way ...
with gzip.open(plot_config) as cf_file:
outputs = cr.read(cf_file)
except OSError:
pass
if outputs is None:
with open(plot_config) as cf_file:
outputs = cr.read(cf_file)
# Is just list the available targets?
if args.list:
list_pre_and_outs(logger, outputs)
sys.exit(0)
# Determine the SCH file
GS.set_sch(solve_schematic(args.schematic, args.board_file))
# Determine the PCB file
GS.set_pcb(solve_board_file(GS.sch_file, args.board_file))
# Do all the job (pre-flight + outputs)
generate_outputs(outputs, args.target, args.invert_sel, args.skip_pre)
# Print total warnings
logger.log_totals()
if __name__ == "__main__":
main() # pragma: no cover