diff --git a/CHANGELOG.md b/CHANGELOG.md index ac1e5e87..f44d4b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `suparts`: Adds support for KiCost's subparts feature. - `field_rename`: Used to rename schematic fields. - `var_rename_kicost`: Like `var_rename` but using KiCost mechanism. +- New KiCost variant style. +- `skip_if_no_field` and `invert` options to the regex used in the generic + filter. ### Changed - Errors and warnings from KiAuto now are printed as errors and warnings. diff --git a/README.md b/README.md index 5fc5d1ce..bcfa707e 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,8 @@ Currently the only type available is `generic`. This filter is based on regular expressions. It also provides some shortcuts for common situations. Note that matches aren't case sensitive and spaces at the beggining and the end are removed. + The internal `_mechanical` filter emulates the KiBoM behavior for default exclusions. + The internal `_kicost_dnp` filter emulates KiCost's `dnp` field. * Valid keys: - `comment`: [string=''] A comment for documentation purposes. - `config_field`: [string='Config'] Name of the field used to clasify components. @@ -301,8 +303,10 @@ Currently the only type available is `generic`. * Valid keys: - `column`: [string=''] Name of the column to apply the regular expression. - *field*: Alias for column. + - `invert`: [boolean=false] Invert the regex match result. - `regex`: [string=''] Regular expression to match. - *regexp*: Alias for regex. + - `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist. - `exclude_config`: [boolean=false] Exclude components containing a key value in the config field. Separators are applied. - `exclude_empty_val`: [boolean=false] Exclude components with empty 'Value'. @@ -320,8 +324,10 @@ Currently the only type available is `generic`. * Valid keys: - `column`: [string=''] Name of the column to apply the regular expression. - *field*: Alias for column. + - `invert`: [boolean=false] Invert the regex match result. - `regex`: [string=''] Regular expression to match. - *regexp*: Alias for regex. + - `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist. - `invert`: [boolean=false] Invert the result of the filter. - `keys`: [string|list(string)=dnf_list] [dnc_list,dnf_list] List of keys to match. The `dnf_list` and `dnc_list` internal lists can be specified as strings. @@ -369,6 +375,7 @@ Currently the only type available is `generic`. - var_rename_kicost: Var_Rename_KiCost This filter implements the kicost.VARIANT:FIELD=VALUE renamer to get FIELD=VALUE when VARIANT is in use. It applies the KiCost concept of variants (a regex to match the VARIANT). + The internal `_var_rename_kicost` filter emulates the KiCost behavior. * Valid keys: - `comment`: [string=''] A comment for documentation purposes. - `name`: [string=''] Used to identify this particular filter definition. diff --git a/kibot/fil_base.py b/kibot/fil_base.py index 391cd035..b6a10666 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -4,7 +4,8 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegFilter, Registrable, RegOutput -from .misc import IFILT_MECHANICAL, IFILT_VAR_RENAME, IFILT_ROT_FOOTPRINT, IFILT_KICOST_RENAME, DISTRIBUTORS +from .misc import (IFILT_MECHANICAL, IFILT_VAR_RENAME, IFILT_ROT_FOOTPRINT, IFILT_KICOST_RENAME, DISTRIBUTORS, + IFILT_VAR_RENAME_KICOST, IFILT_KICOST_DNP) from .error import KiPlotConfigurationError from .bom.columnlist import ColumnList from .macros import macros, document # noqa: F401 @@ -222,6 +223,14 @@ class BaseFilter(RegFilter): logger.debug('Creating internal filter: '+str(o_tree)) return o_tree + @staticmethod + def _create_var_rename_kicost(name): + o_tree = {'name': name} + o_tree['type'] = 'var_rename_kicost' + o_tree['comment'] = 'Internal default variant field renamer filter (KiCost style)' + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree + @staticmethod def _create_rot_footprint(name): o_tree = {'name': name} @@ -268,6 +277,15 @@ class BaseFilter(RegFilter): logger.debug('Creating internal filter: '+str(o_tree)) return o_tree + @staticmethod + def _create_kicost_dnp(name): + o_tree = {'name': name} + o_tree['type'] = 'generic' + o_tree['comment'] = 'Internal filter for KiCost `dnp` field' + # dnp = 0 is included, other dnp values are excluded + o_tree['exclude_any'] = [{'column': 'dnp', 'regex': r'^\s*0(\.0*)?\s*$', 'invert': True, 'skip_if_no_field': True}] + return o_tree + @staticmethod def _create_internal_filter(name): if name == IFILT_MECHANICAL: @@ -280,6 +298,10 @@ class BaseFilter(RegFilter): tree = BaseFilter._create_rot_footprint(name) elif name == IFILT_KICOST_RENAME: tree = BaseFilter._create_kicost_rename(name) + elif name == IFILT_VAR_RENAME_KICOST: + tree = BaseFilter._create_var_rename_kicost(name) + elif name == IFILT_KICOST_DNP: + tree = BaseFilter._create_kicost_dnp(name) else: return None filter = RegFilter.get_class_for(tree['type'])() @@ -299,7 +321,10 @@ class BaseFilter(RegFilter): # Nothing specified, use the default if default is None: return None - names = [default] + if isinstance(default, list): + names = default + else: + names = [default] elif isinstance(names, str): # User provided, but only one, make a list if names == '_none': diff --git a/kibot/fil_generic.py b/kibot/fil_generic.py index 54c9b58a..19dce981 100644 --- a/kibot/fil_generic.py +++ b/kibot/fil_generic.py @@ -30,7 +30,9 @@ class Generic(BaseFilter): # noqa: F821 """ Generic filter This filter is based on regular expressions. It also provides some shortcuts for common situations. - Note that matches aren't case sensitive and spaces at the beggining and the end are removed """ + Note that matches aren't case sensitive and spaces at the beggining and the end are removed. + The internal `_mechanical` filter emulates the KiBoM behavior for default exclusions. + The internal `_kicost_dnp` filter emulates KiCost's `dnp` field """ def __init__(self): super().__init__() with document: @@ -119,8 +121,14 @@ class Generic(BaseFilter): # noqa: F821 if not self.include_only: # Nothing to match against, means include all return True for reg in self.include_only: + if reg.skip_if_no_field and not c.is_field(reg.column): + # Skip the check if the field doesn't exist + continue field_value = c.get_field_value(reg.column) - if reg.regex.search(field_value): + res = reg.regex.search(field_value) + if reg.invert: + res = not res + if res: if GS.debug_level > 1: logger.debug("Including '{ref}': Field '{field}' ({value}) matched '{re}'".format( ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) @@ -134,8 +142,14 @@ class Generic(BaseFilter): # noqa: F821 if not self.exclude_any: # Nothing to match against, means don't exclude any return False for reg in self.exclude_any: + if reg.skip_if_no_field and not c.is_field(reg.column): + # Skip the check if the field doesn't exist + continue field_value = c.get_field_value(reg.column) - if reg.regex.search(field_value): + res = reg.regex.search(field_value) + if reg.invert: + res = not res + if res: if GS.debug_level > 1: logger.debug("Excluding '{ref}': Field '{field}' ({value}) matched '{re}'".format( ref=c.ref, field=reg.column, value=field_value, re=reg.regex)) diff --git a/kibot/fil_var_rename_kicost.py b/kibot/fil_var_rename_kicost.py index 03cc3f5b..40dee456 100644 --- a/kibot/fil_var_rename_kicost.py +++ b/kibot/fil_var_rename_kicost.py @@ -20,7 +20,8 @@ logger = log.get_logger(__name__) class Var_Rename_KiCost(BaseFilter): # noqa: F821 """ Var_Rename_KiCost This filter implements the kicost.VARIANT:FIELD=VALUE renamer to get FIELD=VALUE when VARIANT is in use. - It applies the KiCost concept of variants (a regex to match the VARIANT) """ + It applies the KiCost concept of variants (a regex to match the VARIANT). + The internal `_var_rename_kicost` filter emulates the KiCost behavior """ def __init__(self): super().__init__() self._is_transform = True @@ -53,7 +54,7 @@ class Var_Rename_KiCost(BaseFilter): # noqa: F821 variant = GS.variant[0] else: variant = '('+'|'.join(GS.variant)+')' - var = re.compile(variant, re.IGNORECASE) + var_re = re.compile(variant, re.IGNORECASE) for name, value in comp.get_user_fields(): name = name.strip().lower() # Remove the prefix @@ -69,13 +70,13 @@ class Var_Rename_KiCost(BaseFilter): # noqa: F821 # Successfully separated f_variant = res[0].lower() f_field = res[1].lower() - if var.match(f_variant): + if var_re.match(f_variant): # Variant matched if GS.debug_level > 2: logger.debug('ref: {} {}: {} -> {}'. format(comp.ref, f_field, comp.get_field_value(f_field), value)) comp.set_field(f_field, value) - elif self.variant_to_value and var.match(name): + elif self.variant_to_value and var_re.match(name): # The field matches the variant and the user wants to change the value if GS.debug_level > 2: logger.debug('ref: {} value: {} -> {}'.format(comp.ref, comp.value, value)) diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py index 863bcf0b..758f8164 100644 --- a/kibot/kicad/v5_sch.py +++ b/kibot/kicad/v5_sch.py @@ -835,6 +835,9 @@ class SchematicComponent(object): return self.dfields[field].value return '' + def is_field(self, field): + return field in self.dfields + def get_free_field_number(self): """ Looks for a field number that isn't currently in use """ max_num = -1 diff --git a/kibot/misc.py b/kibot/misc.py index 679844eb..c95b66d9 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -79,8 +79,10 @@ KICAD_VERSION_5_99 = 5099000 # Internal filter names IFILT_MECHANICAL = '_mechanical' IFILT_VAR_RENAME = '_var_rename' +IFILT_VAR_RENAME_KICOST = '_var_rename_kicost' IFILT_ROT_FOOTPRINT = '_rot_footprint' IFILT_KICOST_RENAME = '_kicost_rename' +IFILT_KICOST_DNP = '_kicost_dnp' # KiCad 5 GUI values for the attribute UI_THT = 0 # 1 for KiCad 6 UI_SMD = 1 # 2 for KiCad 6 diff --git a/kibot/out_base.py b/kibot/out_base.py index 261fe6ab..7003b310 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -102,6 +102,10 @@ class BoMRegex(Optionable): """ {column} """ self.regexp = None """ {regex} """ + self.skip_if_no_field = False + """ Skip this test if the field doesn't exist """ + self.invert = False + """ Invert the regex match result """ class VariantOptions(BaseOptions): diff --git a/kibot/var_base.py b/kibot/var_base.py index ee35a8eb..baabad04 100644 --- a/kibot/var_base.py +++ b/kibot/var_base.py @@ -5,7 +5,7 @@ # Project: KiBot (formerly KiPlot) from .registrable import RegVariant from .optionable import Optionable -from .fil_base import apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, BaseFilter, apply_pre_transform +from .fil_base import apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, apply_pre_transform from .macros import macros, document # noqa: F401 @@ -25,21 +25,20 @@ class BaseVariant(RegVariant): # * Filters self.pre_transform = Optionable """ [string|list(string)=''] Name of the filter to transform fields before applying other filters. - Use '_var_rename' to transform VARIANT:FIELD fields """ + Use '_var_rename' to transform VARIANT:FIELD fields. + Use '_var_rename_kicost' to transform kicost.VARIANT:FIELD fields. + Use '_kicost_rename' to apply KiCost field rename rules """ self.exclude_filter = Optionable """ [string|list(string)=''] Name of the filter to exclude components from BoM processing. Use '_mechanical' for the default KiBoM behavior """ self.dnf_filter = Optionable """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Fit'. - Use '_kibom_dnf' for the default KiBoM behavior """ + Use '_kibom_dnf' for the default KiBoM behavior. + Use '_kicost_dnp'' for the default KiCost behavior """ self.dnc_filter = Optionable """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'. Use '_kibom_dnc' for the default KiBoM behavior """ - def config(self, parent): - super().config(parent) - self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) - def filter(self, comps): # Apply all the filters comps = apply_pre_transform(comps, self.pre_transform) diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py index 99a978ef..2db21301 100644 --- a/kibot/var_ibom.py +++ b/kibot/var_ibom.py @@ -48,6 +48,7 @@ class IBoM(BaseVariant): # noqa: F821 def config(self, parent): super().config(parent) + self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILT_MECHANICAL) self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter') diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py index 634efa1d..ef289baa 100644 --- a/kibot/var_kibom.py +++ b/kibot/var_kibom.py @@ -51,6 +51,7 @@ class KiBoM(BaseVariant): # noqa: F821 else: self.variant = [] self.variant = [v.lower() for v in self.variant] + self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) # Filters priority: # 1) Defined here # 2) Delegated from the output format diff --git a/kibot/var_kicost.py b/kibot/var_kicost.py new file mode 100644 index 00000000..1517725c --- /dev/null +++ b/kibot/var_kicost.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020-2021 Salvador E. Tropea +# Copyright (c) 2020-2021 Instituto Nacional de TecnologĂ­a Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +""" +Implements the KiCost variants mechanism. +""" +import re +from .gs import GS +from .misc import IFILT_VAR_RENAME_KICOST, IFILT_KICOST_RENAME, IFILT_KICOST_DNP +from .fil_base import BaseFilter +from .macros import macros, document, variant_class # noqa: F401 +from . import log + +logger = log.get_logger(__name__) + + +@variant_class +class KiCost(BaseVariant): # noqa: F821 + """ KiCost variant style + The `variant` field (configurable) contains one or more values. + If any of these values matches the variant regex the component is included. + By default a pre-transform filter is applied to support kicost.VARIANT:FIELD and + field name aliases used by KiCost. + Also a default `dnf_filter` implements the KiCost DNP mechanism """ + def __init__(self): + super().__init__() + with document: + self.variant = '' + """ Variants to match (regex) """ + self.variant_field = 'variant' + """ Name of the field that stores board variant/s for component """ + self.separators = ',;/ ' + """ Valid separators for variants in the variant field. + Each character is a valid separator """ + + def config(self, parent): + super().config(parent) + self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', + [IFILT_VAR_RENAME_KICOST, IFILT_KICOST_RENAME], is_transform=True) + self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter') + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter', IFILT_KICOST_DNP) + self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter') + if not self.separators: + self.separators = ' ' + else: + self.separators = '['+self.separators+']' + + def filter(self, comps): + GS.variant = [self.variant] + comps = super().filter(comps) + logger.debug("Applying KiCost style variant `{}`".format(self.name)) + if not self.variant_field or not self.variant: + # No variant field or not variant regex + # Just skip the process + return comps + # Apply to all the components + var_re = re.compile(self.variant, flags=re.IGNORECASE) + for c in comps: + logger.debug("{} {} {}".format(c.ref, c.fitted, c.included)) + if not (c.fitted and c.included): + # Don't check if we already discarded it + continue + variants = c.get_field_value(self.variant_field) + if variants: + # The component belong to one or more variant + for v in re.split(self.separators, variants): + if var_re.match(v): + # Matched, remains + break + else: + # None of the variants matched + c.fitted = False + if GS.debug_level > 2: + logger.debug('ref: {} value: {} -> False'.format(c.ref, c.value)) + return comps diff --git a/tests/board_samples/kicad_5/kibom-variant_kicost.sch b/tests/board_samples/kicad_5/kibom-variant_kicost.sch new file mode 100644 index 00000000..71a31f7d --- /dev/null +++ b/tests/board_samples/kicad_5/kibom-variant_kicost.sch @@ -0,0 +1,70 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "KiBom Test Schematic" +Date "2020-03-12" +Rev "A" +Comp "https://github.com/SchrodingersGat/KiBom" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +Text Notes 510 730 0 79 ~ 0 +This schematic serves as a test-file for the KiBot export script.\nThis is the KiCost variants style test. +Text Notes 5950 2600 0 118 ~ 0 +The test tests the following \nvariants matrix:\n production test default\nC1 X\nC2 X X\nR1 X X X\nR2 X X\n +$Comp +L Device:C C1 +U 1 1 5F43BEC2 +P 1000 1700 +F 0 "C1" H 1115 1746 50 0000 L CNN +F 1 "1nF" H 1115 1655 50 0000 L CNN +F 2 "" H 1038 1550 50 0001 C CNN +F 3 "~" H 1000 1700 50 0001 C CNN +F 4 "1" H 1000 1700 50 0001 C CNN "kicost.default:dnp" +F 5 "0.0" H 1000 1700 50 0001 C CNN "kicost.test:dnp" +F 6 "" H 1000 1700 50 0001 C CNN "kicost.production:nopop" + 1 1000 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:C C2 +U 1 1 5F43CE1C +P 1450 1700 +F 0 "C2" H 1565 1746 50 0000 L CNN +F 1 "1000 pF" H 1565 1655 50 0000 L CNN +F 2 "" H 1488 1550 50 0001 C CNN +F 3 "~" H 1450 1700 50 0001 C CNN +F 4 "production,test" H 1450 1700 50 0001 C CNN "version" + 1 1450 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R1 +U 1 1 5F43D144 +P 2100 1700 +F 0 "R1" H 2170 1746 50 0000 L CNN +F 1 "1k" H 2170 1655 50 0000 L CNN +F 2 "" V 2030 1700 50 0001 C CNN +F 3 "~" H 2100 1700 50 0001 C CNN +F 4 "3k3" H 2100 1700 50 0001 C CNN "kicost.test:Value" + 1 2100 1700 + 1 0 0 -1 +$EndComp +$Comp +L Device:R R2 +U 1 1 5F43D4BB +P 2500 1700 +F 0 "R2" H 2570 1746 50 0000 L CNN +F 1 "1000" H 2570 1655 50 0000 L CNN +F 2 "" V 2430 1700 50 0001 C CNN +F 3 "~" H 2500 1700 50 0001 C CNN +F 4 "production default" H 2500 1700 50 0001 C CNN "Variant" + 1 2500 1700 + 1 0 0 -1 +$EndComp +$EndSCHEMATC diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index 7f487de7..c66e0664 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -34,6 +34,15 @@ Missing: - number_boards - XLSX/HTML colors (for real) +KiBoM Variants: +- kibom-variant_2.sch +- kibom-variant_5.sch + +IBoM Variants: +- test_int_bom_variant_t2if + kibom-variant_3.sch + int_bom_var_t2i_csv +- test_int_bom_variant_t2is + kibom-variant_3.sch + int_bom_var_t2is_csv +- kibom-variant_4.sch + For debug information use: pytest-3 --log-cli-level debug @@ -1239,6 +1248,7 @@ def test_int_bom_variant_t2b(test_dir): def test_int_bom_variant_t2c(test_dir): + """ Test KiBoM variant and field rename filter, R1 must be changed to 3k3 """ prj = 'kibom-variant_2' ctx = context.TestContextSCH(test_dir, 'test_int_bom_variant_t2c', prj, 'int_bom_var_t2c_csv', BOM_DIR) ctx.run() @@ -1287,6 +1297,7 @@ def test_int_bom_variant_t2s(test_dir): def test_int_bom_variant_t2if(test_dir): + """ IBoM variants test full """ prj = 'kibom-variant_3' ctx = context.TestContextSCH(test_dir, 'test_int_bom_variant_t2if', prj, 'int_bom_var_t2i_csv', BOM_DIR) ctx.run() @@ -1303,6 +1314,7 @@ def test_int_bom_variant_t2if(test_dir): def test_int_bom_variant_t2is(test_dir): + """ IBoM variants test simple """ prj = 'kibom-variant_3' ctx = context.TestContextSCH(test_dir, 'test_int_bom_variant_t2is', prj, 'int_bom_var_t2is_csv', BOM_DIR) ctx.run(extra_debug=True) @@ -1312,6 +1324,26 @@ def test_int_bom_variant_t2is(test_dir): ctx.clean_up(keep_project=True) +def test_int_bom_variant_t2kf(test_dir): + """ KiCost variants test full. + R1 must be changed to 3k3. + We also test the DNP mechanism. """ + prj = 'kibom-variant_kicost' + ctx = context.TestContextSCH(test_dir, 'test_int_bom_variant_t2kf', prj, 'int_bom_var_t2k_csv', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv(prj+'-bom.csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2']) + rows, header, info = ctx.load_csv(prj+'-bom_(production).csv') + check_kibom_test_netlist(rows, ref_column, 2, ['C1'], ['R1', 'R2', 'C2']) + val_column = header.index(VALUE_COLUMN_NAME) + check_value(rows, ref_column, 'R1', val_column, '1k') + rows, header, info = ctx.load_csv(prj+'-bom_(test).csv') + check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2']) + check_value(rows, ref_column, 'R1', val_column, '3k3') + ctx.clean_up() + + def test_int_bom_wrong_variant(test_dir): ctx = context.TestContextSCH(test_dir, 'test_int_bom_wrong_variant', 'links', 'int_bom_wrong_variant', '') ctx.run(EXIT_BAD_CONFIG) diff --git a/tests/yaml_samples/int_bom_var_t2k_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t2k_csv.kibot.yaml new file mode 100644 index 00000000..bc130344 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t2k_csv.kibot.yaml @@ -0,0 +1,43 @@ +# KiCost variants test +kibot: + version: 1 + +variants: + - name: 'production' + comment: 'Production variant' + type: kicost + file_id: '_(production)' + variant: production + + - name: 'test' + comment: 'Test variant' + type: kicost + file_id: '_(test)' + variant: 't.*' + + - name: 'default' + comment: 'Default variant' + type: kicost + variant: default + +outputs: + - name: 'bom_internal' + comment: "Bill of Materials in CSV format" + type: bom + dir: BoM + options: + variant: default + + - name: 'bom_internal_production' + comment: "Bill of Materials in CSV format for production" + type: bom + dir: BoM + options: + variant: production + + - name: 'bom_internal_test' + comment: "Bill of Materials in CSV format for test" + type: bom + dir: BoM + options: + variant: test