Skip to content

Commit de97162

Browse files
authored
Merge pull request #266 from espressif/feat/add_support_of_negation_config_rules
feat: add support of negation config rules
2 parents 5b7f1b7 + 9b7b5d5 commit de97162

7 files changed

Lines changed: 166 additions & 7 deletions

File tree

docs/en/explanations/config_rules.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ To define a Config Rule, use the following format: ``[SDKCONFIG_FILEPATTERN]=[CO
5252
- ``SDKCONFIG_FILEPATTERN``: This can be a file name to match a `sdkconfig file <#sdkconfig-files>`_ or a pattern with one wildcard (``*``) character to match multiple `sdkconfig files`_.
5353
- ``CONFIG_NAME``: The name of the corresponding build configuration. This value can be skipped if the wildcard value is to be used.
5454

55+
To exclude specific `sdkconfig files`_, use the negation rule format: ``![SDKCONFIG_FILEPATTERN]``.
56+
57+
- ``SDKCONFIG_FILEPATTERN``: The format is the same as in the normal Config Rule.
58+
- Negation rules are applied after all inclusion rules, so the order of negation rules does not matter.
59+
5560
The config rules and the corresponding matched `sdkconfig files`_ for the example project are as follows:
5661

5762
.. list-table:: Config Rules
@@ -85,6 +90,30 @@ The config rules and the corresponding matched `sdkconfig files`_ for the exampl
8590
- - ``sdkconfig.ci.foo``
8691
- ``sdkconfig.ci.bar``
8792

93+
- - - ``sdkconfig.ci.*=``
94+
- ``!sdkconfig.ci.test``
95+
- - ``foo``
96+
- ``bar``
97+
- The negation rule excludes the ``sdkconfig.ci.test`` file.
98+
- - ``sdkconfig.ci.foo``
99+
- ``sdkconfig.ci.bar``
100+
101+
- - - ``sdkconfig.ci.*=``
102+
- ``!sdkconfig.ci.test*``
103+
- - ``foo``
104+
- ``bar``
105+
- The negation rule excludes the files matching the negation wildcard pattern like ``sdkconfig.ci.test`` or ``sdkconfig.ci.test_debug``.
106+
- - ``sdkconfig.ci.foo``
107+
- ``sdkconfig.ci.bar``
108+
109+
- - - ``!sdkconfig.ci.test*``
110+
- ``sdkconfig.ci.*=``
111+
- - ``foo``
112+
- ``bar``
113+
- Same as the previous example, but the negation rule is applied first.
114+
- - ``sdkconfig.ci.foo``
115+
- ``sdkconfig.ci.bar``
116+
88117
****************
89118
Override Order
90119
****************

docs/en/explanations/find.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ The output would be:
8989
(cmake) App test-1, target esp32, sdkconfig /tmp/test/examples/test-1/sdkconfig.ci.foo, build in test-1/build
9090
(cmake) App test-1, target esp32, sdkconfig /tmp/test/examples/test-1/sdkconfig.defaults, build in test-1/build
9191
92+
To drop specific sdkconfigs, add a negation rule:
93+
94+
.. code:: shell
95+
96+
idf-build-apps find -p test-1 --target esp32 --config "sdkconfig.ci.*=" "!sdkconfig.ci.bar"
97+
98+
The ``bar`` configuration is omitted, only ``foo`` remains.
99+
92100
.. _find-placeholders:
93101

94102
****************************

docs/en/references/config_file.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Here's a simple example of a configuration file:
4242
# config rules
4343
config = [
4444
"sdkconfig.*=",
45+
"!sdkconfig.test",
4546
"=default",
4647
]
4748
@@ -62,6 +63,7 @@ Here's a simple example of a configuration file:
6263
# config rules
6364
config = [
6465
"sdkconfig.*=",
66+
"!sdkconfig.test",
6567
"=default",
6668
]
6769
@@ -73,7 +75,7 @@ Running ``idf-build-apps build`` with the above configuration is equivalent to t
7375
--paths components examples \
7476
--target esp32 \
7577
--recursive \
76-
--config-rules "sdkconfig.*=" "=default" \
78+
--config-rules "sdkconfig.*=" "!sdkconfig.test" "=default" \
7779
--build-dir "build_@t_@w"
7880
7981
`TOML <https://toml.io/en/>`__ supports native data types. In order to get the config name and type of the corresponding CLI option, you may refer to the help messages by using ``idf-build-apps find -h`` or ``idf-build-apps build -h``.

idf_build_apps/args.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,9 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
565565
'Optional NAME is the name of the configuration. '
566566
'if not specified, the filename is used as the name. '
567567
'FILEPATTERN is the filename of the sdkconfig file with a single wildcard character (*). '
568-
'The NAME is the value matched by the wildcard',
568+
'The NAME is the value matched by the wildcard. '
569+
'Prefix a rule with ! to exclude matching files from the results (e.g. !sdkconfig.ci.test). '
570+
'Negation rules are applied globally after all positive rules',
569571
validation_alias=AliasChoices('config_rules', 'config_rules_str', 'config'),
570572
default=None,
571573
)

idf_build_apps/finder.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def _get_apps_from_path(
4646
sdkconfig_paths_matched = False
4747

4848
for rule in config_rules:
49+
if rule.negated:
50+
continue
51+
4952
if not rule.file_name:
5053
default_config_name = rule.config_name
5154
continue
@@ -71,6 +74,17 @@ def _get_apps_from_path(
7174

7275
app_configs.append((sdkconfig_path, config_name))
7376

77+
# Apply negation rules
78+
negated_paths: t.Set[str] = set()
79+
for rule in config_rules:
80+
if not rule.negated:
81+
continue
82+
for matched in Path(path).glob(rule.file_name):
83+
negated_paths.add(str(matched.resolve()))
84+
85+
if negated_paths:
86+
app_configs = [(p, n) for p, n in app_configs if p not in negated_paths]
87+
7488
# no config rules matched, use default app
7589
if not sdkconfig_paths_matched:
7690
app_configs.append((None, default_config_name))

idf_build_apps/utils.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,40 @@
2020

2121

2222
class ConfigRule:
23-
def __init__(self, file_name: str, config_name: str = '') -> None:
23+
def __init__(self, file_name: str, config_name: str = '', negated: bool = False) -> None:
2424
"""
2525
ConfigRule represents the sdkconfig file and the config name.
2626
2727
For example:
2828
2929
- filename='', config_name='default' - represents the default app configuration, and gives it a name
3030
'default'
31-
- filename='sdkconfig.*', config_name=None - represents the set of configurations, names match the wildcard
31+
- filename='sdkconfig.*', config_name='' - represents the set of configurations, names match the wildcard
3232
value
33+
- filename='sdkconfig.ci.test', negated=True - represents an exclusion rule, files matching this pattern
34+
will be excluded from the build configurations
3335
3436
:param file_name: name of the sdkconfig file fragment, optionally with a single wildcard ('*' character).
3537
can also be empty to indicate that the default configuration of the app should be used
36-
:param config_name: name of the corresponding build configuration, or None if the value of wildcard is to be
37-
used
38+
:param config_name: name of the corresponding build configuration, or empty string if the value of the
39+
wildcard is to be used
40+
:param negated: if True, this rule excludes matching files instead of including them
3841
"""
3942
self.file_name = file_name
4043
self.config_name = config_name
44+
self.negated = negated
4145

4246

4347
def config_rules_from_str(rule_strings: t.Optional[t.List[str]]) -> t.List[ConfigRule]:
4448
"""
4549
Helper function to convert strings like 'file_name=config_name' into `ConfigRule` objects
4650
51+
Supports the following formats:
52+
53+
- ``file_name=config_name`` - include the sdkconfig file with the given config name
54+
- ``file_pattern=`` - include all sdkconfig files matching the pattern, config name is derived from the wildcard
55+
- ``!file_name`` or ``!file_pattern`` - exclude matching sdkconfig files from the results
56+
4757
:param rule_strings: list of rules as strings or a single rule string
4858
:return: list of ConfigRules
4959
"""
@@ -52,10 +62,20 @@ def config_rules_from_str(rule_strings: t.Optional[t.List[str]]) -> t.List[Confi
5262

5363
rules = []
5464
for rule_str in to_list(rule_strings):
65+
if rule_str.startswith('!'):
66+
negated_str = rule_str[1:].strip()
67+
if '=' in negated_str:
68+
raise InvalidInput(f'Negation rules must not have a config name: {rule_str}')
69+
if not negated_str:
70+
raise InvalidInput(f'Negation rules must contain a non-empty file name or pattern: {rule_str}')
71+
rules.append(ConfigRule(negated_str, negated=True))
72+
continue
73+
5574
items = rule_str.split('=', 2)
5675
rules.append(ConfigRule(items[0], items[1] if len(items) == 2 else ''))
5776
# '' is the default config, sort this one to the front
58-
return sorted(rules, key=lambda x: x.file_name)
77+
# negated rules are sorted to the end since they are applied after positive rules
78+
return sorted(rules, key=lambda x: (x.negated, x.file_name))
5979

6080

6181
def get_parallel_start_stop(total: int, parallel_count: int, parallel_index: int) -> t.Tuple[int, int]:

tests/test_finder.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,90 @@ def test_env_var(self, tmp_path, monkeypatch):
723723
monkeypatch.delenv('TEST_ENV_VAR')
724724

725725

726+
@pytest.mark.parametrize(
727+
'sdkconfig_files, config_rules, expected_config_names',
728+
[
729+
# basic negation
730+
(
731+
['sdkconfig.ci.foo', 'sdkconfig.ci.bar', 'sdkconfig.ci.test'],
732+
['sdkconfig.ci.*=', '!sdkconfig.ci.test'],
733+
['bar', 'foo'],
734+
),
735+
# negation with wildcard
736+
(
737+
['sdkconfig.ci.foo', 'sdkconfig.ci.test_debug', 'sdkconfig.ci.test_release'],
738+
['sdkconfig.ci.*=', '!sdkconfig.ci.test*'],
739+
['foo'],
740+
),
741+
# multiple negations
742+
(
743+
['sdkconfig.ci.foo', 'sdkconfig.ci.bar', 'sdkconfig.ci.test', 'sdkconfig.ci.debug'],
744+
['sdkconfig.ci.*=', '!sdkconfig.ci.test', '!sdkconfig.ci.debug'],
745+
['bar', 'foo'],
746+
),
747+
# negation-only (no positive match) -> default config
748+
(
749+
['sdkconfig.ci.test'],
750+
['!sdkconfig.ci.test'],
751+
[''],
752+
),
753+
# negation with named config
754+
(
755+
['sdkconfig.ci', 'sdkconfig.ci.foo', 'sdkconfig.ci.bar', 'sdkconfig.ci.test'],
756+
['sdkconfig.ci.*=', 'sdkconfig.ci=default', '!sdkconfig.ci.test'],
757+
['bar', 'default', 'foo'],
758+
),
759+
# order independent (negation before positive rule)
760+
(
761+
['sdkconfig.ci.foo', 'sdkconfig.ci.test'],
762+
['!sdkconfig.ci.test', 'sdkconfig.ci.*='],
763+
['foo'],
764+
),
765+
# negation of non-existent file has no effect
766+
(
767+
['sdkconfig.ci.foo', 'sdkconfig.ci.bar'],
768+
['sdkconfig.ci.*=', '!sdkconfig.ci.nonexistent'],
769+
['bar', 'foo'],
770+
),
771+
# negation with whitespaces
772+
(
773+
['sdkconfig.ci.foo', 'sdkconfig.ci.test'],
774+
['sdkconfig.ci.*=', '! sdkconfig.ci.test '],
775+
['foo'],
776+
),
777+
],
778+
)
779+
def test_config_rules_negation(tmp_path, sdkconfig_files, config_rules, expected_config_names):
780+
create_project('test1', tmp_path)
781+
for f in sdkconfig_files:
782+
(tmp_path / 'test1' / f).touch()
783+
784+
apps = find_apps(
785+
str(tmp_path / 'test1'),
786+
'esp32',
787+
recursive=True,
788+
config_rules_str=config_rules,
789+
)
790+
assert len(apps) == len(expected_config_names)
791+
assert sorted([app.config_name for app in apps]) == sorted(expected_config_names)
792+
793+
794+
def test_config_rules_negation_invalid_format():
795+
from idf_build_apps.utils import InvalidInput
796+
from idf_build_apps.utils import config_rules_from_str
797+
798+
with pytest.raises(InvalidInput, match='Negation rules must not have a config name'):
799+
config_rules_from_str(['!sdkconfig.ci.test=myname'])
800+
801+
802+
def test_config_rules_negation_empty_pattern():
803+
from idf_build_apps.utils import InvalidInput
804+
from idf_build_apps.utils import config_rules_from_str
805+
806+
with pytest.raises(InvalidInput, match='Negation rules must contain a non-empty file name or pattern'):
807+
config_rules_from_str(['!'])
808+
809+
726810
@pytest.mark.parametrize(
727811
'exclude_list, apps_count',
728812
[

0 commit comments

Comments
 (0)