Kconfig Tool: Support input ENV. and config files loading

The Kconfig tool now supports loading config files and merging them
into one. The merging is basically done by the later loaded config files
overriding the first loaded config files with dependencies respected.

With this feature, the Kconfig system will be aware of the configs such
as platform settings and profile settings rather than having default
values for all configurations.

The environment variables are for the Kconfig files which refer to env.
variables, for example the PLATFORM_PATH.
Now the PLATFORM_PATH is no longer passed as one of the arguments, it's
done via the --env argument now. Of course, any other env. variables can
be appended at will.

Change-Id: Ib39e13b29def382a9546cd7680c62a4f4d321f99
Signed-off-by: Kevin Peng <kevin.peng@arm.com>
diff --git a/config/kconfig.cmake b/config/kconfig.cmake
index 6d9a0bd..44b674d 100644
--- a/config/kconfig.cmake
+++ b/config/kconfig.cmake
@@ -5,8 +5,19 @@
 #
 #-------------------------------------------------------------------------------
 
-set(ROOT_KCONFIG                ${CMAKE_SOURCE_DIR}/Kconfig)
-set(PLATFORM_KCONFIG            ${TARGET_PLATFORM_PATH}/Kconfig)
+# Load multiple config files and merge into one, generates CMake config file and config header file.
+# The first loaded configs would be overridden by later ones. That's how the "merge" works.
+# Check CONFIG_FILE_LIST for the loading order.
+# Configurations not set by any of the config files would be set to the default values in Kconfig
+# files with dependencies respected.
+# If a ".config" already exists in the output folder, then the CONFIG_FILE_LIST is ignored.
+# For more details, check the kconfig_system.rst.
+
+set(KCONFIG_OUTPUT_DIR  ${CMAKE_BINARY_DIR}/kconfig)
+
+set(DOTCONFIG_FILE      ${KCONFIG_OUTPUT_DIR}/.config)
+set(ROOT_KCONFIG        ${CMAKE_SOURCE_DIR}/Kconfig)
+set(PLATFORM_KCONFIG    ${TARGET_PLATFORM_PATH}/Kconfig)
 
 if(TFM_PROFILE)
     # Selecting TF-M profiles is not supported yet.
@@ -23,13 +34,38 @@
     endif()
 endif()
 
+# User customized config file
+if(DEFINED KCONFIG_CONFIG_FILE AND NOT EXISTS ${KCONFIG_CONFIG_FILE})
+    message(FATAL_ERROR "No such file: ${KCONFIG_CONFIG_FILE}")
+endif()
+
+# Note the order of CONFIG_FILE_LIST, as the first loaded configs would be
+# overridden by later ones.
+list(APPEND CONFIG_FILE_LIST
+            ${KCONFIG_CONFIG_FILE})
+
+# Set up ENV variables for the tfm_kconfig.py which are then consumed by Kconfig files.
+set(KCONFIG_ENV_VARS "TFM_BASE=${CMAKE_SOURCE_DIR} \
+                      TFM_VERSION=${TFM_VERSION} \
+                      PLATFORM_PATH=${TARGET_PLATFORM_PATH} \
+                      CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}")
+
+if(MENUCONFIG)
+    # Note: Currently, only GUI menuconfig can be supported with CMake integration
+    set(MENUCONFIG_ARG "-u=gui")
+else()
+    set(MENUCONFIG_ARG "")
+endif()
+
 find_package(Python3)
 
-# Call the tfm_kconfig.py
 execute_process(
     COMMAND
     ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/kconfig/tfm_kconfig.py
-    -k ${ROOT_KCONFIG} -o ${CMAKE_BINARY_DIR} -u gui -p ${TARGET_PLATFORM_PATH}
+    -k ${ROOT_KCONFIG} -o ${KCONFIG_OUTPUT_DIR}
+    --envs ${KCONFIG_ENV_VARS}
+    --config-files ${CONFIG_FILE_LIST}
+    ${MENUCONFIG_ARG}
     WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
     RESULT_VARIABLE ret
 )
@@ -38,15 +74,15 @@
     message(FATAL_ERROR "Kconfig tool failed!")
 endif()
 
-# Component configs generated by tfm_kconfig.py
-if(EXISTS "${CMAKE_BINARY_DIR}/project_config.h")
-    set(PROJECT_CONFIG_HEADER_FILE "${CMAKE_BINARY_DIR}/project_config.h"     CACHE STRING "User defined header file for TF-M config")
+if(DEFINED PROJECT_CONFIG_HEADER_FILE)
+    message(FATAL_ERROR "It is NOT supported to manually set PROJECT_CONFIG_HEADER_FILE while using Kconfig.")
 endif()
 
+# Component configs generated by tfm_kconfig.py
+set(PROJECT_CONFIG_HEADER_FILE ${KCONFIG_OUTPUT_DIR}/project_config.h CACHE FILEPATH "User defined header file for TF-M config")
+
 # Load project cmake configs generated by tfm_kconfig.py
-if(EXISTS "${CMAKE_BINARY_DIR}/project_config.cmake")
-    include("${CMAKE_BINARY_DIR}/project_config.cmake")
-endif()
+include(${KCONFIG_OUTPUT_DIR}/project_config.cmake)
 
 ####################################################################################################
 
diff --git a/docs/configuration/kconfig_system.rst b/docs/configuration/kconfig_system.rst
index f4fa257..a4b2caf 100644
--- a/docs/configuration/kconfig_system.rst
+++ b/docs/configuration/kconfig_system.rst
@@ -3,30 +3,32 @@
 ##################
 The Kconfig System
 ##################
-The Kconfig system is an additional tool for users to change configuration options of TF-M.
+The Kconfig system is an alternative tool to the CMake config system for users to change config
+options of TF-M.
 
 .. figure:: kconfig_header_file_system.png
 
 It handles dependencies and validations automatically when you change configurations so that the
 generated configuration options are always valid.
 
-It consists of `The Kconfig tool`_ and the `The Kconfig files`_.
+To use the Kconfig system, enable ``USE_KCONFIG_TOOL`` in commande line.
+And enable ``MENUCONFIG`` to launch configuration GUI.
+
+The Kconfig system consists of `The Kconfig tool`_ and the `The Kconfig files`_.
 
 ****************
 The Kconfig tool
 ****************
 The Kconfig tool is a python script based on `Kconfiglib <https://github.com/ulfalizer/Kconfiglib>`__
-to launch the menuconfig interfaces and generate the following config files:
+to generate the following config files:
 
 - CMake config file
 
   Contains CMake cache variables of building options.
-  This file should be passed to the build system via command line option ``TFM_EXTRA_CONFIG_PATH``.
 
 - Header file
 
   Contains component options in the header file system.
-  This file should be passed to the build system via the command line option ``PROJECT_CONFIG_HEADER_FILE``.
   Component options are gathered together in a seperate menu ``TF-M component configs`` in
   `The Kconfig files`_.
 
@@ -38,59 +40,74 @@
   The Kconfig tool will load it if it exists and ``.config.old`` will be created to
   save the previous configurations.
 
-How To Use
-==========
-The script takes four arguments at maximum.
+The tool supports loading multiple pre-set configuration files merging into a single one.
+The first loaded options are overridden by later ones if the config files contain duplicated
+options.
+And dependencies between config options are taken care of.
+It then launchs a configuration GUI for users to change any config options if the ``MENUCONFIG`` is
+enabled in build command line.
 
-- '-k', '--kconfig-file'
+Integration with TF-M build system
+----------------------------------
+TF-M build system includes ``kconfig.cmake`` to integrate this tool.
+It prepares the parameters for the script and invokes it to load multiple configuration files basing
+on your build setup, including but not limited to
 
-  Required. The root Kconfig file.
+  - Build type bound configurations, decided by ``CMAKE_BUILD_TYPE``
+  - Profile configurations, decided by ``TFM_PROFILE``
 
-- '-u', '--ui'
+**************************
+Customizing config options
+**************************
+By default, the Kconfig system only merges configuration files and generated the final config files.
+To customize the config options, there are several approaches.
 
-  Optional. The menuconfig interface to launch, ``gui`` or ``tui``.
-  Refer to `Menuconfig interfaces <https://github.com/ulfalizer/Kconfiglib#menuconfig-interfaces>`__
-  for interface details. Only the first two are supported.
-  If no UI is selected, the tool generates config files with default values.
+Menuconfig
+----------
+Menuconfig is the recommended approach to adjust the values of the config options because it has
+a graphic interface for you to easily change the options without worrying about dependencies.
 
-- '-o', '--output_path'
-
-  Required. The output directory to hold the generated files.
-
-- '-p', '--platform-path'
-
-  Optional. The platform specific Kconfig or defconfig files.
-
-Here is an example:
+To launch the menuconfig, you need to enable ``MENUCONFIG`` in addition to enabling
+``USE_KCONFIG_TOOL``.
 
 .. code-block:: bash
 
-  cd trusted-firmware-m
-  python3 tools/kconfig/tfm_kconfig.py -k Kconfig -o <output_path> -u tui
-
-  # If the platform path has defconfig or Kconfig, use '-p' to load them.
-  python3 tools/kconfig/tfm_kconfig.py -k Kconfig -o <output_path> -p platform/ext/target/arm/mps2/an521
-
-The script can be used as a standalone tool. You can pass the config files to
-build system via command line option ``TFM_EXTRA_CONFIG_PATH`` and
-``PROJECT_CONFIG_HEADER_FILE`` respectively, as mentioned above.
-
-.. code-block:: bash
-
-  # Pass the files generated by script.
-  <cmake build command> -DTFM_EXTRA_CONFIG_PATH=<output_path>/project_config.cmake \
-                        -DPROJECT_CONFIG_HEADER_FILE=<output_path>/project_config.h
-
-The TF-M build system has also integrated the tool.
-You only need to set ``USE_KCONFIG_TOOL`` to ``ON/TRUE/1`` in commande line and CMake will launch
-the GUI menuconfig for users to adjust configurations and automatically load the generated config
-files.
+  cmake -S . -B cmake_build -DTFM_PLATFORM=arm/mps2/an521 \
+                            -DUSE_KCONFIG_TOOL=ON \
+                            -DMENUCONFIG=ON
 
 .. note::
 
-  - Only GUI menuconfig can be launched by CMake for the time being.
-  - Due to the current limitation of the tool, you are not allowed to change the values of build
-    options that of which platforms have customized values. And there is no prompt messages either.
+  Although the Kconfiglib provides three
+  `menuconfig interfaces <https://github.com/ulfalizer/Kconfiglib#menuconfig-interfaces>`__,
+  only GUI menuconfig can be launched by CMake for the time being.
+
+Command line options
+--------------------
+The support of passing configurations via command line is kept for the Kconfig system.
+
+.. code-block:: bash
+
+  cmake -S . -B cmake_build -DTFM_PLATFORM=arm/mps2/an521 \
+                            -DUSE_KCONFIG_TOOL=ON \
+                            -DTFM_ISOLATION_LEVEL=2
+
+Kconfig file
+------------
+You can also put the frequently used config options into a Kconfig file. When you need to apply the
+config options in that file, pass it via command line option ``-DKCONFIG_CONFIG_FILE``
+
+.. code-block:: bash
+
+  cmake -S . -B cmake_build -DTFM_PLATFORM=arm/mps2/an521 \
+                            -DTFM_ISOLATION_LEVEL=2 \
+                            -DUSE_KCONFIG_TOOL=ON \
+                            -DKCONFIG_CONFIG_FILE=my_config.conf
+
+.. note::
+
+  The command line set options override the ones in the config file.
+  And you can always launch menuconfig to do the final adjustments.
 
 *****************
 The Kconfig files
@@ -103,4 +120,4 @@
 
 --------------
 
-*Copyright (c) 2022, Arm Limited. All rights reserved.*
+*Copyright (c) 2022-2023, Arm Limited. All rights reserved.*
diff --git a/tools/kconfig/tfm_kconfig.py b/tools/kconfig/tfm_kconfig.py
index 376b5df..b112e1b 100755
--- a/tools/kconfig/tfm_kconfig.py
+++ b/tools/kconfig/tfm_kconfig.py
@@ -1,5 +1,5 @@
 #-------------------------------------------------------------------------------
-# Copyright (c) 2022, Arm Limited. All rights reserved.
+# Copyright (c) 2022-2023, Arm Limited. All rights reserved.
 # SPDX-License-Identifier: BSD-3-Clause
 #
 #-------------------------------------------------------------------------------
@@ -9,7 +9,7 @@
 import os
 import re
 
-from kconfiglib import Kconfig
+from kconfiglib import Kconfig, TRI_TO_STR, BOOL, TRISTATE
 import menuconfig
 import guiconfig
 
@@ -37,6 +37,26 @@
     )
 
     parser.add_argument(
+        '--envs',
+        dest='envs',
+        default = None,
+        nargs = '*',
+        help = 'The environment variables for Kconfig files. Use absolute paths for directories.\
+                The format must be key-value pairs with "=" in the middle, for example:\
+                FOO=foo BAR=bar'
+    )
+
+    parser.add_argument(
+        '--config-files',
+        dest='config_files',
+        default = None,
+        nargs = '*',
+        help = 'The config files to be load and merge. The load order is the same as this list order,\
+                The later ones override the former ones.\
+                If .config is found in output-path, this file list is ignored.'
+    )
+
+    parser.add_argument(
         '-u', '--ui',
         dest = 'ui',
         required = False,
@@ -45,17 +65,26 @@
         help = 'Which config UI to display'
     )
 
-    parser.add_argument(
-        '-p', '--platform-path',
-        dest = 'platform_path',
-        required = False,
-        help = 'The platform path which contains specific Kconfig and defconfig files'
-    )
-
     args = parser.parse_args()
 
     return args
 
+def set_env_var(envs):
+    '''
+    The Kconfig files might use some environment variables.
+    This method sets environment variables for Kconfig files.
+    Each item in 'envs' should be in the key-value format, for example:
+    'FOO=foo BAR=bar'
+    '''
+    if envs is None:
+        return
+
+    for env in envs:
+        env_entries = env.strip('\r\n').split()
+        for _env in env_entries:
+            key, value = _env.split('=')
+            os.environ[key] = value
+
 def generate_file(dot_config):
     '''
     The .config file is the generated result from Kconfig files. It contains
@@ -66,7 +95,11 @@
     the prompt which contains in_component_label. These configs will be written
     into header file.
     '''
-    cmake_file, header_file = 'project_config.cmake', 'project_config.h'
+
+    output_dir = os.path.dirname(dot_config)
+    cmake_file = os.path.join(output_dir, 'project_config.cmake')
+    header_file = os.path.join(output_dir, 'project_config.h')
+
     in_component_options, menu_start = False, False
 
     '''
@@ -168,6 +201,43 @@
     logging.info('TF-M build configs saved to \'{}\''.format(cmake_file))
     logging.info('TF-M component configs saved to \'{}\''.format(header_file))
 
+def validate_promptless_sym(kconfig):
+    """
+    Check if any assignments to promptless symbols.
+    """
+
+    ret = True
+
+    for sym in kconfig.unique_defined_syms:
+        if sym.user_value and not any(node.prompt for node in sym.nodes):
+            logging.error('Assigning value to promptless symbol {}'.format(sym.name))
+            ret = False
+
+    return ret
+
+def validate_assigned_sym(kconfig):
+    """
+    Checks if all assigned symbols have the expected values
+    """
+
+    ret = True
+
+    for sym in kconfig.unique_defined_syms:
+        if not sym.user_value:
+            continue
+
+        if sym.type in (BOOL, TRISTATE):
+            user_val = TRI_TO_STR[sym.user_value]
+        else:
+            user_val = sym.user_value
+
+        if user_val != sym.str_value:
+            logging.error('Tried to set [{}] to <{}>, but is <{}> finally.'.format(
+                            sym.name, user_val, sym.str_value))
+            ret = False
+
+    return ret
+
 if __name__ == '__main__':
     logging.basicConfig(format='[%(filename)s] %(levelname)s: %(message)s',
                         level = logging.INFO)
@@ -175,39 +245,38 @@
     args = parse_args()
 
     # dot_config has a fixed name. Do NOT rename it.
-    dot_config = '.config'
-    def_config = ''
+    dot_config = os.path.abspath(os.path.join(args.output_path, '.config'))
     mtime_prv = 0
 
-    if args.platform_path:
-        platform_abs_path = os.path.abspath(args.platform_path)
-
-        if not os.path.exists(platform_abs_path):
-            logging.error('Platform path {} doesn\'t exist!'.format(platform_abs_path))
-            exit(1)
-
-        def_config = os.path.join(platform_abs_path, 'defconfig')
-
-        # Pass environment variable to Kconfig to load extra Kconfig file.
-        os.environ['PLATFORM_PATH'] = platform_abs_path
+    set_env_var(args.envs)
 
     # Load Kconfig file. kconfig_file is the root Kconfig file. The path is
     # input by users from the command.
     tfm_kconfig = Kconfig(args.kconfig_file)
+    tfm_kconfig.enable_undef_warnings() # Print warnings for undefined symbols when loading
+    tfm_kconfig.disable_override_warnings() # Overriding would happen when loading multiple config files
+    tfm_kconfig.disable_redun_warnings() # Redundant definitions might happen when loading multiple config files
 
     if not os.path.exists(args.output_path):
         os.mkdir(args.output_path)
 
-    # Change program execution path to the output folder path.
-    os.chdir(args.output_path)
-
     if os.path.exists(dot_config):
         # Load .config which contains the previous configurations.
+        # Input config files are ignored.
+        logging.info('.config file found, other config files are ignored.')
         mtime_prv = os.stat(dot_config).st_mtime
         logging.info(tfm_kconfig.load_config(dot_config))
-    elif os.path.exists(def_config):
-        # Load platform specific defconfig if exists.
-        logging.info(tfm_kconfig.load_config(def_config))
+    elif args.config_files is not None:
+        # Load input config files if .config is not found and write the .config file.
+        for conf in args.config_files:
+            logging.info(tfm_kconfig.load_config(conf, replace = False))
+
+        if not validate_promptless_sym(tfm_kconfig) or not validate_assigned_sym(tfm_kconfig):
+            exit(1)
+
+    # Change program execution path to the output folder path because menuconfigs do not support
+    # writing .config to arbitrary folders.
+    os.chdir(args.output_path)
 
     # UI options
     if args.ui == 'tui':
@@ -215,10 +284,14 @@
     elif args.ui == 'gui':
         guiconfig.menuconfig(tfm_kconfig)
     else:
-        # Save .config if UI is not created.
-        # The previous .config will be saved as .config.old.
         logging.info(tfm_kconfig.write_config(dot_config))
 
+    if not os.path.exists(dot_config):
+        # This could happend when the user did not "Save" the config file when using menuconfig
+        # We should abort here in such case.
+        logging.error('No .config is saved!')
+        exit(1)
+
     # Generate output files if .config has been changed.
-    if os.path.exists(dot_config) and os.stat(dot_config).st_mtime != mtime_prv:
+    if os.stat(dot_config).st_mtime != mtime_prv:
         generate_file(dot_config)