Skip to content

Commit aa6cd2b

Browse files
committed
[nrf noup] cmake: Kconfig: strict mode
fail building uppon ineffective Kconfigs Signed-off-by: Mateusz Michalek <mateusz.michalek@nordicsemi.no>
1 parent d83f532 commit aa6cd2b

9 files changed

Lines changed: 314 additions & 15 deletions

File tree

cmake/modules/kconfig.cmake

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,49 @@ if(DEFINED FORCED_CONF_FILE)
397397
list(APPEND input_configs_flags --forced-input-configs)
398398
endif()
399399

400+
# Strict Kconfig checking: an assignment that Kconfig ends up discarding is an
401+
# error rather than a warning, but only for the configuration the application
402+
# itself owns. That is the fragments placed directly in the application
403+
# configuration directory (prj.conf and any overlay next to it) plus the symbols
404+
# given on the CMake command line as -DCONFIG_<symbol>=<value>. Both are written
405+
# for one specific build, so an assignment that does not take effect there is a
406+
# bug in the application.
407+
#
408+
# Everything else keeps warning only: board and SoC defconfigs, the application
409+
# boards/ and socs/ subdirectories, shields, snippets, fragments coming from
410+
# modules, and fragments a parent image passes to a child image. Those are
411+
# shared between several build targets, where the same assignment can be both
412+
# meaningful for one target and discarded for another.
413+
#
414+
# Script mode (package_helper.cmake) is exempt as well. It is used to resolve
415+
# the configuration without building it, most notably by twister to evaluate a
416+
# testsuite 'filter:' expression on every platform. A test that is about to be
417+
# filtered out is expected to have assignments that do not take effect there,
418+
# and the platforms that survive the filter still get the strict checks from
419+
# the real build that follows.
420+
zephyr_get(KCONFIG_STRICT SYSBUILD GLOBAL)
421+
if(NOT DEFINED KCONFIG_STRICT)
422+
# Note: set_ifndef() is not usable here, as it overwrites a variable that is
423+
# defined but false, which is exactly how the feature is turned off.
424+
set(KCONFIG_STRICT ON)
425+
endif()
426+
427+
set(kconfig_strict_args)
428+
if(KCONFIG_STRICT AND "--handwritten-input-configs" IN_LIST input_configs_flags
429+
AND NOT DEFINED CMAKE_SCRIPT_MODE_FILE)
430+
list(APPEND kconfig_strict_args --strict-flip-checks)
431+
432+
set(kconfig_strict_dirs ${APPLICATION_CONFIG_DIR} ${APPLICATION_SOURCE_DIR})
433+
list(REMOVE_DUPLICATES kconfig_strict_dirs)
434+
foreach(dir ${kconfig_strict_dirs})
435+
list(APPEND kconfig_strict_args --strict-scope-dir ${dir})
436+
endforeach()
437+
438+
if(EXTRA_KCONFIG_OPTIONS_FILE)
439+
list(APPEND kconfig_strict_args --strict-scope-file ${EXTRA_KCONFIG_OPTIONS_FILE})
440+
endif()
441+
endif()
442+
400443
cmake_path(GET AUTOCONF_H PARENT_PATH autoconf_h_path)
401444
if(NOT EXISTS ${autoconf_h_path})
402445
file(MAKE_DIRECTORY ${autoconf_h_path})
@@ -410,6 +453,7 @@ execute_process(
410453
${ZEPHYR_BASE}/scripts/kconfig/kconfig.py
411454
--zephyr-base=${ZEPHYR_BASE}
412455
${input_configs_flags}
456+
${kconfig_strict_args}
413457
${KCONFIG_ROOT}
414458
${DOTCONFIG}
415459
${AUTOCONF_H}

doc/build/kconfig/setting.rst

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,11 @@ Comments use a #:
111111
# This is a comment
112112
113113
Assignments in configuration files are only respected if the dependencies for
114-
the symbol are satisfied. A warning is printed otherwise. To figure out what
115-
the dependencies of a symbol are, use one of the :ref:`interactive
116-
configuration interfaces <menuconfig>` (you can jump directly to a symbol with
117-
:kbd:`/`), or look up the symbol in the Kconfig search page.
114+
the symbol are satisfied. A warning is printed otherwise, or an error for the
115+
fragments covered by :ref:`kconfig_strict`. To figure out what the dependencies
116+
of a symbol are, use one of the :ref:`interactive configuration interfaces
117+
<menuconfig>` (you can jump directly to a symbol with :kbd:`/`), or look up the
118+
symbol in the Kconfig search page.
118119

119120

120121
.. _initial-conf:
@@ -409,6 +410,83 @@ the choice might not be satisfied. In that case, you're setting the default
409410
selection whenever the user makes the choice visible.
410411

411412

413+
.. _kconfig_strict:
414+
415+
Strict checking of ineffective assignments
416+
==========================================
417+
418+
An assignment that Kconfig cannot honor, because the symbol's dependencies are
419+
not satisfied or because a ``select`` from another symbol wins, has no effect on
420+
the build. The value that ends up in :file:`zephyr/.config` is not the one the
421+
fragment asked for. Such an assignment is usually a leftover from an earlier
422+
configuration, or a misunderstanding of what the symbol depends on.
423+
424+
Strict checking turns those ineffective assignments into build errors instead of
425+
warnings. It is enabled by default and only applies to the configuration the
426+
application itself owns:
427+
428+
* Fragments placed directly in the :ref:`application configuration directory
429+
<application-configuration-directory>`, such as :file:`prj.conf` and any
430+
overlay fragment next to it.
431+
432+
* Symbols given on the CMake command line as ``-DCONFIG_<symbol>=<value>``.
433+
434+
Both are written for one specific build, so an assignment that does not take
435+
effect there is a bug in the application.
436+
437+
Every other fragment keeps warning only, because it is shared between several
438+
build targets and the same assignment can be meaningful for one target and
439+
discarded for another:
440+
441+
* Board and SoC :file:`*_defconfig` files, and the application
442+
:file:`boards/` and :file:`socs/` subdirectories.
443+
444+
* Shield and snippet fragments.
445+
446+
* Fragments coming from a Zephyr module.
447+
448+
* Fragments that a parent image passes to a child image in a
449+
:ref:`sysbuild <sysbuild>` build. Note that for a child image, the
450+
application configuration directory is
451+
:file:`<parent app>/sysbuild/<image>/`, so :file:`prj.conf` there is
452+
covered while :file:`<parent app>/sysbuild/<image>/boards/` is not.
453+
454+
When several fragments assign the same symbol, only the fragment that
455+
established the value Kconfig actually used is reported. A board fragment that
456+
overrides :file:`prj.conf` therefore takes over responsibility for the symbol,
457+
and the :file:`prj.conf` assignment is no longer flagged.
458+
459+
The checks are also skipped when Kconfig runs under
460+
:file:`cmake/package_helper.cmake`, which resolves the configuration without
461+
building it. Twister uses it to evaluate a testsuite ``filter:`` expression on
462+
every platform, including the ones the test is about to be filtered out on,
463+
where assignments are not expected to take effect. The platforms that survive
464+
the filter are checked by the real build that follows.
465+
466+
The error names the fragment and the line that made the assignment, and all
467+
findings for an image are reported together:
468+
469+
.. code-block:: none
470+
471+
error: /path/to/app/prj.conf:12: LOG_BUFFER_SIZE (defined at
472+
subsys/logging/Kconfig.processing:38) was assigned the value '2048' but got the
473+
value ''. Check these unsatisfied dependencies: (LOG_MODE_DEFERRED) (=n). ...
474+
475+
Fix such an error by assigning a value that Kconfig can honor, by satisfying the
476+
missing dependencies, or by dropping the assignment. If the assignment is
477+
intentionally target-dependent, move it to a fragment that is specific to the
478+
targets it applies to, such as :file:`boards/<board>.conf`.
479+
480+
To downgrade the errors back to warnings for a build, set
481+
``KCONFIG_STRICT`` to a false value:
482+
483+
.. code-block:: console
484+
485+
west build -b <board> <app> -- -DKCONFIG_STRICT=n
486+
487+
In a :ref:`sysbuild <sysbuild>` build, this applies to all images.
488+
489+
412490
More Kconfig resources
413491
======================
414492

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Variant of prj.conf for the scenarios that build without a bootloader.
2+
#
3+
# prj.conf turns on image management, which only makes sense when MCUboot owns a
4+
# second slot. Without a bootloader IMG_MANAGER stays off, and with it both
5+
# MCUMGR_GRP_IMG and the whole MCUBOOT_UTIL log level choice become unreachable,
6+
# so those assignments could never take effect here. The rest is identical to
7+
# prj.conf.
8+
9+
# Enable MCUmgr and dependencies.
10+
CONFIG_NET_BUF=y
11+
CONFIG_ZCBOR=y
12+
CONFIG_CRC=y
13+
CONFIG_MCUMGR=y
14+
CONFIG_STREAM_FLASH=y
15+
CONFIG_FLASH_MAP=y
16+
17+
# Some command handlers require a large stack.
18+
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2304
19+
CONFIG_MAIN_STACK_SIZE=2176
20+
21+
# Enable flash operations.
22+
CONFIG_FLASH=y
23+
24+
# Required by the `taskstat` command.
25+
CONFIG_THREAD_MONITOR=y
26+
27+
# Support for taskstat command
28+
CONFIG_MCUMGR_GRP_OS_TASKSTAT=y
29+
30+
# Enable statistics and statistic names.
31+
CONFIG_STATS=y
32+
CONFIG_STATS_NAMES=y
33+
34+
# Enable most core commands.
35+
CONFIG_FLASH=y
36+
# Ineffective without a bootloader: MCUMGR_GRP_IMG depends on IMG_MANAGER.
37+
# CONFIG_IMG_MANAGER=y
38+
# CONFIG_MCUMGR_GRP_IMG=y
39+
CONFIG_MCUMGR_GRP_OS=y
40+
CONFIG_MCUMGR_GRP_STAT=y
41+
42+
# Enable logging
43+
CONFIG_LOG=y
44+
# Ineffective without a bootloader: the MCUBOOT_UTIL log level choice only
45+
# exists once IMG_MANAGER pulls in MCUBOOT_BOOTUTIL_LIB.
46+
# CONFIG_MCUBOOT_UTIL_LOG_LEVEL_WRN=y
47+
48+
# Disable debug logging
49+
CONFIG_LOG_MAX_LEVEL=3

samples/subsys/mgmt/mcumgr/smp_svr/sample.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ tests:
3737
- frdm_k64f
3838
integration_platforms:
3939
- frdm_k64f
40+
# Uses prj_no_bootloader.conf instead of prj.conf: this is the only scenario
41+
# built without MCUboot, so the image-management settings prj.conf carries for
42+
# every other scenario cannot take effect here.
4043
sample.mcumgr.smp_svr.udp-dtls:
4144
extra_args:
45+
- FILE_SUFFIX="no_bootloader"
4246
- EXTRA_CONF_FILE="udp-dtls.conf"
43-
- CONFIG_IMG_MANAGER=n
44-
- SB_CONFIG_BOOTLOADER_NONE=y
4547
platform_allow:
4648
- native_sim
4749
integration_platforms:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Companion of prj_no_bootloader.conf: build the application on its own.
2+
SB_CONFIG_BOOTLOADER_NONE=y

scripts/kconfig/kconfig.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def main():
4646
if args.zephyr_base:
4747
os.environ['ZEPHYR_BASE'] = args.zephyr_base
4848

49+
strict_scope = StrictScope(args.strict_scope_dir, args.strict_scope_file) \
50+
if args.strict_flip_checks else None
51+
4952
print("Parsing " + args.kconfig_file)
5053
kconf = Kconfig(args.kconfig_file, warn_to_stderr=False,
5154
suppress_traceback=True)
@@ -88,8 +91,8 @@ def main():
8891
# Print warnings for symbols that didn't get the assigned value. Only
8992
# do this for handwritten input too, to avoid likely unhelpful warnings
9093
# when using an old configuration and updating Kconfig files.
91-
check_assigned_sym_values(kconf)
92-
check_assigned_choice_values(kconf)
94+
check_assigned_sym_values(kconf, strict_scope)
95+
check_assigned_choice_values(kconf, strict_scope)
9396

9497
if kconf.syms.get('WARN_DEPRECATED', kconf.y).tri_value == 2:
9598
check_deprecated(kconf)
@@ -132,6 +135,9 @@ def main():
132135
if error_out:
133136
err("Aborting due to Kconfig warnings")
134137

138+
if strict_scope:
139+
strict_scope.report()
140+
135141
# Write the merged configuration and the C header
136142
print(kconf.write_config(args.config_out))
137143
print(kconf.write_autoconf(args.header_out))
@@ -158,7 +164,68 @@ def check_no_promptless_assign(kconf):
158164
symbols. """ + SYM_INFO_HINT.format(sym))
159165

160166

161-
def check_assigned_sym_values(kconf):
167+
class StrictScope:
168+
# Decides which configuration fragments an ineffective assignment is an
169+
# error for, rather than just a warning.
170+
#
171+
# A fragment is in scope when it sits directly in one of 'dirs' (files in
172+
# subdirectories such as boards/ or socs/ are not in scope), or when it is
173+
# listed in 'files'. The application owns those fragments and knows the
174+
# target they are built for, so an assignment that Kconfig discards there
175+
# is a bug. Everything else - board and SoC defconfigs, shield, snippet and
176+
# module fragments, fragments belonging to another image - is shared
177+
# between targets and cannot be expected to "take" everywhere, so those
178+
# keep warning only.
179+
180+
def __init__(self, dirs, files):
181+
self.dirs = {os.path.realpath(d) for d in dirs}
182+
self.files = {os.path.realpath(f) for f in files}
183+
self.errors = []
184+
185+
def covers(self, loc):
186+
if loc is None:
187+
return False
188+
189+
path = os.path.realpath(loc[0])
190+
return path in self.files or os.path.dirname(path) in self.dirs
191+
192+
def add(self, loc, msg):
193+
# Records 'msg' as an error and returns True if 'loc' is in scope.
194+
# Returns False otherwise, leaving it to the caller to warn instead.
195+
196+
if not self.covers(loc):
197+
return False
198+
199+
self.errors.append(f"{loc[0]}:{loc[1]}: {msg}")
200+
return True
201+
202+
def report(self):
203+
# Prints every recorded error and aborts if there were any. All of them
204+
# are reported at once so that a single build lists everything that
205+
# needs fixing.
206+
207+
if not self.errors:
208+
return
209+
210+
for msg in self.errors:
211+
print("\n" + textwrap.fill("error: " + msg, 100), file=sys.stderr)
212+
213+
err(f"aborting due to {len(self.errors)} ineffective Kconfig "
214+
"assignment(s) in application-owned configuration. Assign a value "
215+
"that Kconfig can honor, satisfy the missing dependencies, or drop "
216+
"the assignment. Building with -DKCONFIG_STRICT=n downgrades this "
217+
"to a warning.")
218+
219+
220+
def report_ineffective(strict_scope, loc, msg):
221+
# Turns an ineffective assignment into an error when it comes from a
222+
# fragment in the strict scope, and into a warning otherwise.
223+
224+
if strict_scope is None or not strict_scope.add(loc, msg):
225+
warn(msg)
226+
227+
228+
def check_assigned_sym_values(kconf, strict_scope=None):
162229
# Verifies that the values assigned to symbols "took" (matches the value
163230
# the symbols actually got), printing warnings otherwise. Choice symbols
164231
# are checked separately, in check_assigned_choice_values().
@@ -197,7 +264,8 @@ def check_assigned_sym_values(kconf):
197264
msg += "Check these unsatisfied dependencies: " + \
198265
", ".join(expr_strs) + ". "
199266

200-
warn(msg + SYM_INFO_HINT.format(sym))
267+
report_ineffective(strict_scope, sym.user_loc,
268+
msg + SYM_INFO_HINT.format(sym))
201269

202270

203271
def missing_deps(sym):
@@ -225,7 +293,7 @@ def missing_deps(sym):
225293
return [dep for dep in deps if expr_value(dep) == 0]
226294

227295

228-
def check_assigned_choice_values(kconf):
296+
def check_assigned_choice_values(kconf, strict_scope=None):
229297
# Verifies that any choice symbols that were selected (by setting them to
230298
# y) ended up as the selection, printing warnings otherwise.
231299
#
@@ -242,7 +310,7 @@ def check_assigned_choice_values(kconf):
242310
if choice.user_selection and \
243311
choice.user_selection is not choice.selection:
244312

245-
warn(f"""\
313+
report_ineffective(strict_scope, choice.user_selection.user_loc, f"""\
246314
The choice symbol {choice.user_selection.name_and_loc} was selected (set =y),
247315
but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended
248316
up as the choice selection. """ + SYM_INFO_HINT.format(choice.user_selection))
@@ -364,6 +432,25 @@ def parse_args():
364432
"set specific configuration settings to a "
365433
"pre-defined value and thereby remove any user "
366434
" adjustments.")
435+
parser.add_argument("--strict-flip-checks",
436+
action="store_true",
437+
help="Treat an assignment that Kconfig does not honor "
438+
"as an error instead of a warning, for the "
439+
"fragments selected by --strict-scope-dir and "
440+
"--strict-scope-file")
441+
parser.add_argument("--strict-scope-dir",
442+
action="append",
443+
default=[],
444+
help="Directory whose configuration fragments the "
445+
"strict checks apply to. Only files directly in "
446+
"the directory are covered, not files in its "
447+
"subdirectories. May be given multiple times.")
448+
parser.add_argument("--strict-scope-file",
449+
action="append",
450+
default=[],
451+
help="Configuration fragment the strict checks apply "
452+
"to, regardless of its location. May be given "
453+
"multiple times.")
367454
parser.add_argument("--zephyr-base",
368455
help="Path to current Zephyr installation")
369456
parser.add_argument("kconfig_file",

tests/drivers/timer/nrf_grtc_timer/prj.conf

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ CONFIG_ZTEST=y
22
CONFIG_NRF_GRTC_TIMER=y
33
CONFIG_COUNTER=y
44
CONFIG_TEST_RANDOM_GENERATOR=y
5-
CONFIG_XOSHIRO_RANDOM_GENERATOR=y
6-
CONFIG_LOG_PRINTK=y
5+
# Ineffective here: XOSHIRO_RANDOM_GENERATOR depends on ENTROPY_HAS_DRIVER, which no board this
6+
# test runs on enables, so the RNG choice falls back to TIMER_RANDOM_GENERATOR.
7+
# CONFIG_XOSHIRO_RANDOM_GENERATOR=y
8+
# Ineffective here: LOG_PRINTK depends on !LOG_MODE_MINIMAL, and the minimal logging mode is what
9+
# these boards default to. Where it is available it already defaults to y.
10+
# CONFIG_LOG_PRINTK=y
711
CONFIG_CPU_LOAD=y
8-
CONFIG_CPU_LOAD_USE_COUNTER=y
12+
# Ineffective here: CPU_LOAD_USE_COUNTER depends on the zephyr,cpu-load-counter chosen node, which
13+
# most of these cores do not have. On the cores that do it already defaults to y.
14+
# CONFIG_CPU_LOAD_USE_COUNTER=y

0 commit comments

Comments
 (0)