New expansion patterns and out_dir from global section.

- Also added expansion in out_dir
- Related to #116
This commit is contained in:
Salvador E. Tropea 2021-11-30 19:12:14 -03:00
parent fadb255781
commit 95135c8c56
18 changed files with 141 additions and 68 deletions

View File

@ -25,9 +25,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Generic filter: options to match if a field is/isn't defined.
- Excellon drill: added `route_mode_for_oval_holes` option.
- Default global `dir` option.
- Global option to specify `out_dir` (like -d command line option)
- Pattern to expand the variant name: %V
- 3D view render
- SCH PDF Print: monochrome and no frame options.
- New expansion patterns:
- **%g** the `file_id` of the global variant.
- **%G** the `name` of the global variant.
- **%bc**, **%bd**, **%bf**, **%bF**, **%bp** and **%br** board data
- **%sc**, **%sd**, **%sf**, **%sF**, **%sp** and **%sr** schematic data
- Now patterns are also expanded in the out_dir name.
### Changed
- Internal BoM: now components with different Tolerance, Voltage, Current

View File

@ -204,6 +204,8 @@ The pattern uses the following expansions:
- **%D** date the script was started.
- **%f** original pcb/sch file name without extension.
- **%F** original pcb/sch file name without extension. Including the directory part of the name.
- **%g** the `file_id` of the global variant.
- **%G** the `name` of the global variant.
- **%i** a contextual ID, depends on the output type.
- **%p** pcb/sch title from pcb metadata.
- **%r** revision from pcb/sch metadata.
@ -221,6 +223,11 @@ global:
output: '%f_rev_%r-%i.%x'
```
Note that the following patterns: **%c**, **%d**, **%f**, **%F**, **%p** and **%r** depends on the context.
If you use them for an output related to the PCB these values will be obtained from the PCB.
If you need to force the origin of the data you can use **%bX** for the PCB and **%sX** for the schematic, where
**X** is the pattern to expand.
#### Default *dir* option
The default `dir` value for any output is `.`. You can change it here.

View File

@ -183,6 +183,8 @@ The pattern uses the following expansions:
- **%D** date the script was started.
- **%f** original pcb/sch file name without extension.
- **%F** original pcb/sch file name without extension. Including the directory part of the name.
- **%g** the `file_id` of the global variant.
- **%G** the `name` of the global variant.
- **%i** a contextual ID, depends on the output type.
- **%p** pcb/sch title from pcb metadata.
- **%r** revision from pcb/sch metadata.
@ -200,6 +202,11 @@ global:
output: '%f_rev_%r-%i.%x'
```
Note that the following patterns: **%c**, **%d**, **%f**, **%F**, **%p** and **%r** depends on the context.
If you use them for an output related to the PCB these values will be obtained from the PCB.
If you need to force the origin of the data you can use **%bX** for the PCB and **%sX** for the schematic, where
**X** is the pattern to expand.
#### Default *dir* option
The default `dir` value for any output is `.`. You can change it here.

View File

@ -285,6 +285,7 @@ def detect_kicad():
def main():
set_locale()
ver = 'KiBot '+__version__+' - '+__copyright__+' - License: '+__license__
GS.out_dir_in_cmd_line = '-d' in sys.argv or '--out-dir' in sys.argv
args = docopt(__doc__, version=ver, options_first=True)
# Set the specified verbosity

View File

@ -3,6 +3,7 @@
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
from .gs import GS
from .macros import macros, document # noqa: F401
from .pre_filters import FiltersOptions
@ -19,6 +20,8 @@ class Globals(FiltersOptions):
""" Default pattern for output file names """
self.dir = ''
""" Default pattern for the output directories """
self.out_dir = ''
""" Base output dir, same as command line `--out-dir` """
self.variant = ''
""" Default variant to apply to all outputs """
self.kiauto_wait_start = 0
@ -50,6 +53,8 @@ class Globals(FiltersOptions):
logger.warning(W_MUSTBEINT+'kiauto_wait_start must be integer, truncating to '+str(GS.global_kiauto_wait_start))
GS.global_kiauto_time_out_scale = self.set_global(GS.global_kiauto_time_out_scale, self.kiauto_time_out_scale,
'kiauto_time_out_scale')
if not GS.out_dir_in_cmd_line and self.out_dir:
GS.out_dir = os.path.join(os.getcwd(), self.out_dir)
set_filters(self.unparsed)

View File

@ -29,6 +29,7 @@ class GS(object):
sch_basename = None # file
# Main output dir
out_dir = None
out_dir_in_cmd_line = False
filter_file = None
board = None
sch = None
@ -142,3 +143,13 @@ class GS(object):
if not GS.sch_file:
logger.error('No SCH file found (*.sch), use -e to specify one.')
exit(EXIT_BAD_ARGS)
@staticmethod
def load_board():
""" Will be repplaced by kiplot.py """
assert False
@staticmethod
def load_sch():
""" Will be repplaced by kiplot.py """
assert False

View File

@ -312,9 +312,9 @@ def preflight_checks(skip_pre):
BasePreFlight.run_enabled()
def get_output_dir(o_dir, dry=False):
def get_output_dir(o_dir, obj, dry=False):
# outdir is a combination of the config and output
outdir = os.path.abspath(os.path.join(GS.out_dir, o_dir))
outdir = os.path.abspath(obj.expand_dirname(os.path.join(GS.out_dir, o_dir)))
# Create directory if needed
logger.debug("Output destination: {}".format(outdir))
if not dry and not os.path.exists(outdir):
@ -338,8 +338,7 @@ def config_output(out, dry=False):
def run_output(out):
GS.current_output = out.name
try:
out_dir = out.expand_dirname(out.dir)
out.run(get_output_dir(out_dir))
out.run(get_output_dir(out.dir, out))
out._done = True
except PlotError as e:
logger.error("In output `"+str(out)+"`: "+str(e))
@ -426,7 +425,7 @@ def get_out_targets(outputs, ori_names, targets, dependencies, comments):
for out in outputs:
name = name2make(out.name)
ori_names[name] = out.name
tg = out.get_targets(os.path.join(GS.out_dir, out.expand_dirname(out.dir)))
tg = out.get_targets(out.expand_dirname(os.path.join(GS.out_dir, out.dir)))
if not tg:
continue
targets[name] = [adapt_file_name(fn) for fn in tg]
@ -509,3 +508,8 @@ def generate_makefile(makefile, cfg_file, outputs, kibot_sys=False):
f.write('{} -s all {}{}\n\n'.format(kibot_cmd, ori_names[name], log_action))
# Mark all outputs as PHONY
f.write('.PHONY: '+' '.join(extra_targets+list(targets.keys()))+'\n')
# To avoid circular dependencies: Optionable needs it, but almost everything needs Optionable
GS.load_board = load_board
GS.load_sch = load_sch

View File

@ -208,62 +208,93 @@ class Optionable(object):
return GS.solved_global_variant.name
return ''
def expand_filename_pcb(self, name):
def expand_filename_common(self, name):
""" Expansions common to the PCB and Schematic """
# PCB expansions, explicit
if GS.board and '%b' in name:
name = name.replace('%bc', GS.pcb_comp)
name = name.replace('%bd', GS.pcb_date)
name = name.replace('%bF', GS.pcb_no_ext)
name = name.replace('%bf', GS.pcb_basename)
name = name.replace('%bp', GS.pcb_title)
name = name.replace('%br', GS.pcb_rev)
name = name.replace('%D', GS.today)
if GS.solved_global_variant:
name = name.replace('%g', GS.solved_global_variant.file_id)
name = name.replace('%G', GS.solved_global_variant.name)
# Schematic expansions, explicit
if GS.sch and '%s' in name:
name = name.replace('%sc', GS.sch_comp)
name = name.replace('%sd', GS.sch_date)
name = name.replace('%sF', GS.sch_no_ext)
name = name.replace('%sf', GS.sch_basename)
name = name.replace('%sp', GS.sch_title)
name = name.replace('%sr', GS.sch_rev)
name = name.replace('%T', GS.time)
if self:
name = name.replace('%i', self._expand_id)
name = name.replace('%v', self._find_variant())
name = name.replace('%V', self._find_variant_name())
name = name.replace('%x', self._expand_ext)
return name
def expand_filename_both(self, name, is_sch=True):
""" Expands %* values in filenames.
Uses data from the PCB. """
if GS.debug_level > 3:
parent = None
if hasattr(self, '_parent'):
if self and hasattr(self, '_parent'):
parent = self._parent
logger.debug('Expanding `{}` in PCB context for {} parent: {}'.format(name, self, parent))
if GS.board:
# Determine if we need to expand SCH and/or PCB related data
has_dep_exp = any(map(lambda x: x in name, ['%c', '%d', '%F', '%f', '%p', '%r']))
do_sch = is_sch and has_dep_exp
# logger.error(name + ' is_sch ' +str(is_sch)+" "+ str(do_sch))
# raise
do_pcb = not is_sch and has_dep_exp
# Load the needed data
if GS.pcb_file and (do_pcb or '%b' in name):
if GS.board is None:
GS.load_board()
GS.load_pcb_title_block()
# Do the replacements
if GS.sch_file and (do_sch or '%s' in name):
if GS.sch is None:
GS.load_sch()
GS.load_sch_title_block()
# This member can be called with a preflight object
name = Optionable.expand_filename_common(self, name)
if GS.board and do_pcb:
name = name.replace('%c', GS.pcb_comp)
name = name.replace('%d', GS.pcb_date)
name = name.replace('%D', GS.today)
name = name.replace('%F', GS.pcb_no_ext)
name = name.replace('%f', GS.pcb_basename)
name = name.replace('%i', self._expand_id)
name = name.replace('%p', GS.pcb_title)
name = name.replace('%r', GS.pcb_rev)
name = name.replace('%T', GS.time)
name = name.replace('%v', self._find_variant() if self else '')
name = name.replace('%V', self._find_variant_name() if self else '')
name = name.replace('%x', self._expand_ext)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
if GS.sch and do_sch:
name = name.replace('%c', GS.sch_comp)
name = name.replace('%d', GS.sch_date)
name = name.replace('%F', GS.sch_no_ext)
name = name.replace('%f', GS.sch_basename)
name = name.replace('%p', GS.sch_title)
name = name.replace('%r', GS.sch_rev)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
if GS.debug_level > 3:
logger.debug('Expanded `{}`'.format(name))
return name
def expand_filename_pcb(self, name):
""" Expands %* values in filenames.
Uses data from the PCB. """
# This member can be called with a preflight object
return Optionable.expand_filename_both(self, name, is_sch=False)
def expand_filename_sch(self, name):
""" Expands %* values in filenames.
Uses data from the SCH. """
if GS.debug_level > 3:
logger.debug('Expanding `{}` in sch context for {}'.format(name, self))
if GS.sch_file:
GS.load_sch_title_block()
# Do the replacements
name = name.replace('%c', GS.sch_comp)
name = name.replace('%d', GS.sch_date)
name = name.replace('%D', GS.today)
name = name.replace('%F', GS.sch_no_ext)
name = name.replace('%f', GS.sch_basename)
name = name.replace('%i', self._expand_id)
name = name.replace('%p', GS.sch_title)
name = name.replace('%r', GS.sch_rev)
name = name.replace('%T', GS.time)
name = name.replace('%v', self._find_variant() if self else '')
name = name.replace('%V', self._find_variant_name() if self else '')
name = name.replace('%x', self._expand_ext)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
if GS.debug_level > 3:
logger.debug('Expanded `{}`'.format(name))
return name
# This member can be called with a preflight object
return Optionable.expand_filename_both(self, name)
@staticmethod
def force_list(val):
@ -310,10 +341,7 @@ class BaseOptions(Optionable):
cur_ext = self._expand_ext
self._expand_id = id
self._expand_ext = ext
if self._parent._sch_related:
name = self.expand_filename_sch(name)
else:
name = self.expand_filename_pcb(name)
name = self.expand_filename_both(name, is_sch=self._parent._sch_related)
res = os.path.abspath(os.path.join(dir, name))
self._expand_id = cur_id
self._expand_ext = cur_ext

View File

@ -78,15 +78,10 @@ class BaseOutput(RegOutput):
self.options.config(self)
def expand_dirname(self, out_dir):
if self._sch_related:
return self.options.expand_filename_sch(out_dir)
return self.options.expand_filename_pcb(out_dir)
return self.options.expand_filename_both(out_dir, is_sch=self._sch_related)
def expand_filename(self, out_dir, name):
if self._sch_related:
name = self.options.expand_filename_sch(name)
else:
name = self.options.expand_filename_pcb(name)
name = self.options.expand_filename_both(name, is_sch=self._sch_related)
return os.path.abspath(os.path.join(out_dir, name))
def run(self, output_dir):

View File

@ -118,6 +118,7 @@ class CompressOptions(BaseOptions):
def get_files(self, output, no_out_run=False):
output_real = os.path.realpath(output)
files = OrderedDict()
out_dir = self.expand_filename_sch(GS.out_dir)
for f in self.files:
# Get the list of candidates
files_list = None
@ -125,7 +126,7 @@ class CompressOptions(BaseOptions):
for out in GS.outputs:
if out.name == f.from_output:
config_output(out)
files_list = out.get_targets(get_output_dir(out.expand_dirname(out.dir), dry=True))
files_list = out.get_targets(get_output_dir(out.dir, out, dry=True))
break
if files_list is None:
logger.error('Unknown output `{}` selected in {}'.format(f.from_output, self._parent))
@ -142,7 +143,7 @@ class CompressOptions(BaseOptions):
logger.error('Unable to generate `{}` from {}'.format(file, out))
exit(INTERNAL_ERROR)
else:
files_list = glob.iglob(os.path.join(GS.out_dir, f.source), recursive=True)
files_list = glob.iglob(os.path.join(out_dir, f.source), recursive=True)
# Filter and adapt them
for fname in filter(re.compile(f.filter).match, files_list):
fname_real = os.path.realpath(fname)
@ -154,7 +155,7 @@ class CompressOptions(BaseOptions):
if f.dest:
dest = os.path.join(f.dest, os.path.basename(fname))
else:
dest = os.path.relpath(dest, GS.out_dir)
dest = os.path.relpath(dest, out_dir)
files[fname_real] = dest
return files
@ -162,7 +163,7 @@ class CompressOptions(BaseOptions):
return [self._parent.expand_filename(out_dir, self.output)]
def get_dependencies(self):
output = self.get_targets(GS.out_dir)[0]
output = self.get_targets(self.expand_filename_sch(GS.out_dir))[0]
files = self.get_files(output, no_out_run=True)
return files.keys()

View File

@ -352,7 +352,7 @@ class KiBoMOptions(BaseOptions):
self.conf = 'bom.ini'
else:
# A configuration
conf = os.path.abspath(os.path.join(GS.out_dir, CONFIG_FILENAME))
conf = os.path.abspath(os.path.join(self.expand_filename_sch(GS.out_dir), CONFIG_FILENAME))
self.conf.save(conf)
self.conf = conf
self._expand_ext = self.format.lower()

View File

@ -131,7 +131,7 @@ class PDF_Pcb_PrintOptions(VariantOptions):
logger.error(CMD_PCBNEW_PRINT_LAYERS+' returned %d', ret)
exit(PDF_PCB_PRINT)
if video_remove:
video_name = os.path.join(GS.out_dir, 'pcbnew_export_screencast.ogv')
video_name = os.path.join(self.expand_filename_pcb(GS.out_dir), 'pcbnew_export_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)

View File

@ -131,7 +131,7 @@ class Render3DOptions(Base3DOptions):
logger.error(CMD_PCBNEW_3D+' returned %d', ret)
exit(RENDER_3D_ERR)
if video_remove:
video_name = os.path.join(GS.out_dir, 'pcbnew_3d_view_screencast.ogv')
video_name = os.path.join(self.expand_filename_pcb(GS.out_dir), 'pcbnew_3d_view_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)

View File

@ -5,6 +5,7 @@
# Project: KiBot (formerly KiPlot)
from .gs import GS
from .registrable import Registrable
from .optionable import Optionable
from .log import get_logger
logger = get_logger(__name__)
@ -22,6 +23,8 @@ class BasePreFlight(Registrable):
self._sch_related = False
self._pcb_related = False
self._enabled = True
self._expand_id = ''
self._expand_ext = ''
@staticmethod
def add_preflight(o_pre):
@ -105,6 +108,9 @@ class BasePreFlight(Registrable):
""" Returns a list of targets generated by this preflight """
return []
def expand_dirname(self, out_dir):
return Optionable.expand_filename_both(self, out_dir, is_sch=self._sch_related)
def _find_variant(self):
return ''

View File

@ -34,7 +34,7 @@ class Run_DRC(BasePreFlight): # noqa: F821
load_board()
out_pattern = GS.global_output if GS.global_output is not None else GS.def_global_output
name = Optionable.expand_filename_pcb(self, out_pattern)
return [os.path.abspath(os.path.join(GS.out_dir, name))]
return [os.path.abspath(os.path.join(Optionable.expand_filename_pcb(self, GS.out_dir), name))]
def run(self):
check_script(CMD_PCBNEW_RUN_DRC, URL_PCBNEW_RUN_DRC, '1.4.0')
@ -45,13 +45,13 @@ class Run_DRC(BasePreFlight): # noqa: F821
cmd.extend(['-f', GS.filter_file])
if BasePreFlight.get_option('ignore_unconnected'): # noqa: F821
cmd.append('-i')
cmd.extend([GS.pcb_file, GS.out_dir])
cmd.extend([GS.pcb_file, Optionable.expand_filename_pcb(self, GS.out_dir)])
# If we are in verbose mode enable debug in the child
cmd, video_remove = add_extra_options(cmd)
logger.info('- Running the DRC')
ret = exec_with_retry(cmd)
if video_remove:
video_name = os.path.join(GS.out_dir, 'pcbnew_run_drc_screencast.ogv')
video_name = os.path.join(Optionable.expand_filename_pcb(self, GS.out_dir), 'pcbnew_run_drc_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)
if ret:

View File

@ -34,7 +34,7 @@ class Run_ERC(BasePreFlight): # noqa: F821
load_sch()
out_pattern = GS.global_output if GS.global_output is not None else GS.def_global_output
name = Optionable.expand_filename_sch(self, out_pattern)
return [os.path.abspath(os.path.join(GS.out_dir, name))]
return [os.path.abspath(os.path.join(Optionable.expand_filename_sch(self, GS.out_dir), name))]
def run(self):
check_eeschema_do()
@ -47,13 +47,13 @@ class Run_ERC(BasePreFlight): # noqa: F821
cmd.append('-w')
if GS.filter_file:
cmd.extend(['-f', GS.filter_file])
cmd.extend([GS.sch_file, GS.out_dir])
cmd.extend([GS.sch_file, Optionable.expand_filename_sch(self, GS.out_dir)])
# If we are in verbose mode enable debug in the child
cmd, video_remove = add_extra_options(cmd)
logger.info('- Running the ERC')
ret = exec_with_retry(cmd)
if video_remove:
video_name = os.path.join(GS.out_dir, 'run_erc_eeschema_screencast.ogv')
video_name = os.path.join(Optionable.expand_filename_sch(self, GS.out_dir), 'run_erc_eeschema_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)
if ret:

View File

@ -96,7 +96,7 @@ class Filters(BasePreFlight): # noqa: F821
def apply(self):
# Create the filters file
if self._value:
o_dir = get_output_dir('')
o_dir = get_output_dir('', self)
GS.filter_file = os.path.join(o_dir, 'kibot_errors.filter')
with open(GS.filter_file, 'w') as f:
f.write(self._value)

View File

@ -8,6 +8,7 @@ from sys import (exit)
from .macros import macros, pre_class # noqa: F401
from .error import (KiPlotConfigurationError)
from .gs import (GS)
from .optionable import Optionable
from .kiplot import check_eeschema_do, exec_with_retry, add_extra_options
from .misc import (CMD_EESCHEMA_DO, BOM_ERROR)
from .log import (get_logger)
@ -33,7 +34,7 @@ class Update_XML(BasePreFlight): # noqa: F821
def run(self):
check_eeschema_do()
cmd = [CMD_EESCHEMA_DO, 'bom_xml', GS.sch_file, GS.out_dir]
cmd = [CMD_EESCHEMA_DO, 'bom_xml', GS.sch_file, Optionable.expand_filename_sch(self, GS.out_dir)]
# If we are in verbose mode enable debug in the child
cmd, video_remove = add_extra_options(cmd)
logger.info('- Updating BoM in XML format')
@ -42,6 +43,6 @@ class Update_XML(BasePreFlight): # noqa: F821
logger.error('Failed to update the BoM, error %d', ret)
exit(BOM_ERROR)
if video_remove:
video_name = os.path.join(GS.out_dir, 'bom_xml_eeschema_screencast.ogv')
video_name = os.path.join(Optionable.expand_filename_sch(self, GS.out_dir), 'bom_xml_eeschema_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)