scripts: support compile_commands.json in gen_app_partitions.py

Fixes: #40590

This commit updates gen_app_partitions.py to include only files present
in the current build by extracting the information from the CMake
generated `compile_commands.json` file.

This ensures that object files in sub-projects, such as `empty_cpu0`,
will not be considered by the script.

Using the compile_commands.json instead of walking the whole build tree
for finding object files also improves performance:

Time of executing `gen_app_partitions.py` (Old):
__________________________
Executed in  480.06 millis
   usr time  425.83 millis
   sys time   49.55 millis

Time of executing `gen_app_partitions.py` (New):
________________________________________________________
Executed in   76.22 millis
   usr time   49.00 millis
   sys time   24.59 millis

Signed-off-by: Torsten Rasmussen <Torsten.Rasmussen@nordicsemi.no>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f0786d3..4b69bc5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -880,7 +880,7 @@
     OUTPUT ${APP_SMEM_UNALIGNED_LD} ${APP_SMEM_PINNED_UNALIGNED_LD}
     COMMAND ${PYTHON_EXECUTABLE}
     ${ZEPHYR_BASE}/scripts/gen_app_partitions.py
-    -d ${OBJ_FILE_DIR}
+    -f ${CMAKE_BINARY_DIR}/compile_commands.json
     -o ${APP_SMEM_UNALIGNED_LD}
     $<$<BOOL:${APP_SMEM_PINNED_UNALIGNED_LD}>:--pinoutput=${APP_SMEM_PINNED_UNALIGNED_LD}>
     ${APP_SMEM_PINNED_PARTITION_LIST_ARG}
diff --git a/cmake/app/boilerplate.cmake b/cmake/app/boilerplate.cmake
index f77fbc0..979ec6d 100644
--- a/cmake/app/boilerplate.cmake
+++ b/cmake/app/boilerplate.cmake
@@ -558,6 +558,14 @@
 set(SOC_TOOLCHAIN_NAME ${CONFIG_SOC_TOOLCHAIN_NAME})
 set(SOC_FAMILY ${CONFIG_SOC_FAMILY})
 
+# For the gen_app_partitions.py to work correctly, we must ensure that
+# all targets exports their compile commands to fetch object files.
+# We enable it unconditionally, as this is also useful for several IDEs
+set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE CACHE BOOL
+    "Export CMake compile commands. Used by gen_app_partitions.py script"
+    FORCE
+)
+
 if("${SOC_SERIES}" STREQUAL "")
   set(SOC_PATH ${SOC_NAME})
 else()
diff --git a/scripts/gen_app_partitions.py b/scripts/gen_app_partitions.py
index e38d7b2..2017011 100644
--- a/scripts/gen_app_partitions.py
+++ b/scripts/gen_app_partitions.py
@@ -34,6 +34,7 @@
 
 import sys
 import argparse
+import json
 import os
 import re
 from collections import OrderedDict
@@ -149,6 +150,26 @@
                     find_obj_file_partitions(fullname, partitions)
 
 
+def parse_compile_command_file(partitions):
+    # Iterate over all entries to find object files.
+    # Thereafter process each object file to find partitions
+    object_pattern = re.compile(r'-o\s+(\S*)')
+    with open(args.compile_commands_file, 'rb') as f:
+        commands = json.load(f)
+        for command in commands:
+            build_dir = command.get('directory')
+            compile_command = command.get('command')
+            compile_arg = object_pattern.search(compile_command)
+            obj_file = None if compile_arg is None else compile_arg.group(1)
+            if obj_file:
+                fullname = os.path.join(build_dir, obj_file)
+                # Because of issue #40635, then not all objects referenced by
+                # the compile_commands.json file may be available, therefore
+                # only include existing files.
+                if os.path.exists(fullname):
+                    find_obj_file_partitions(fullname, partitions)
+
+
 def parse_elf_file(partitions):
     with open(args.elf, 'rb') as f:
         try:
@@ -216,6 +237,8 @@
                         help="Root build directory")
     parser.add_argument("-e", "--elf", required=False, default=None,
                         help="ELF file")
+    parser.add_argument("-f", "--compile-commands-file", required=False,
+                        default=None, help="CMake compile commands file")
     parser.add_argument("-o", "--output", required=False,
                         help="Output ld file")
     parser.add_argument("-v", "--verbose", action="count", default=0,
@@ -237,6 +260,8 @@
 
     if args.directory is not None:
         parse_obj_files(partitions)
+    if args.compile_commands_file is not None:
+        parse_compile_command_file(partitions)
     elif args.elf is not None:
         parse_elf_file(partitions)
     else: