Code pre-generation support (#23763)

* Start implementing a pregenerator

* Start moving pregenerate logic into a separate directory

* Start adding some ability to figure out pregeneration output locations

* Pregeneration for bridge seems to work

* Add missing files

* Restyle

* Better log level logic

* Pregeneration of java also exists

* Allow pregeneration of cpp app data. Full codegen usage is now enabled

* Move sdk root around

* Make things run

* Restyle

* Parallel code pregen - can do pregen in 1.08 seconds on my machine

* Restyle

* Make sure pregen folders are split

* Pregeneration compile when using GN works

* Restyle

* Direct pregen usage works now

* Restyle

* Minor sort

* Add support for both pregen and no pregen

* Restyle

* Fix pregen dir output logic

* Support pregenerated directory in build_examples.py

* Fix gn build logic

* Somewhat simpler code for parallel vs serial codegen

* Use imap_unordered to not care about actual parallel generation order

* Also fix java jni codegen with pregenerated data

* Fix java compilation deps: java codegen uses data model files

* NRF now can use pregen folder

* Allow telink to also use a pregen dir

* Better shell escape, make mbedos cmake flags work with pregen dir

* Restyle

* Add pregen support for esp32 as well

* Add a test for esp32 pregeneration support

* Also test pregen support in linux builds (to check gn builds)

* Remove unused file

* Fix spelling

* Fix esp32 compilation - gn arguments need to be passed from cmake

* Fix some define forwarding logic in codegen

* Make sure java build config (which includes header paths) is set as a config and applies to generated sources

* java build config should apply to all sources, not just transitively

* Restyle

* Replace codege with codegen.

* Fix naming typo

* Update text for build steps

* Update spacing logic to make the code cleaner. The nospace args is odd
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 276b1f2..4a3ce85 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -229,12 +229,28 @@
               run: |
                   ./scripts/run_in_build_env.sh \
                     "./scripts/build/build_examples.py --no-log-timestamps \
-                       --target linux-x64-all-clusters-ipv6only-clang \
-                       --target linux-x64-chip-tool-ipv6only-clang \
                        --target linux-x64-minmdns-ipv6only-clang \
                        --target linux-x64-rpc-console \
                        build \
                     "
+            - name: Create a pre-generate directory and ensure compile-time codegen would fail
+              run: |
+                  ./scripts/run_in_build_env.sh "./scripts/codepregen.py ./zzz_pregenerated"
+                  mv scripts/codegen.py scripts/codegen.py.renamed
+            - name: Build using build_examples.py (pregen)
+              timeout-minutes: 60
+              run: |
+                  ./scripts/run_in_build_env.sh \
+                    "./scripts/build/build_examples.py --no-log-timestamps \
+                       --target linux-x64-all-clusters-ipv6only-clang \
+                       --target linux-x64-chip-tool-ipv6only-clang \
+                       --pregen-dir ./zzz_pregenerated \
+                       build \
+                    "
+            - name: Undo code pre-generation changes (make compile time codegen work again)
+              run: |
+                  rm -rf ./zzz_pregenerated
+                  mv scripts/codegen.py.renamed scripts/codegen.py
             - name: Run fake linux tests with build_examples
               timeout-minutes: 15
               run: |
diff --git a/.github/workflows/examples-esp32.yaml b/.github/workflows/examples-esp32.yaml
index 6755e3f..9299ac3 100644
--- a/.github/workflows/examples-esp32.yaml
+++ b/.github/workflows/examples-esp32.yaml
@@ -71,11 +71,29 @@
                      "./scripts/build/build_examples.py \
                         --enable-flashbundle \
                         --target esp32-m5stack-all-clusters \
-                        --target esp32-m5stack-all-clusters-minimal \
-                        --target esp32-m5stack-all-clusters-rpc-ipv6only \
                         build \
                         --copy-artifacts-to out/artifacts \
                      "
+            - name: Prepare code pregen and ensure compile time pregen not possible
+              run: |
+                  ./scripts/run_in_build_env.sh "./scripts/codepregen.py ./zzz_pregenerated"
+                  mv scripts/codegen.py scripts/codegen.py.renamed
+            - name: Build some M5Stack variations with pregen
+              timeout-minutes: 60
+              run: |
+                  ./scripts/run_in_build_env.sh \
+                     "./scripts/build/build_examples.py \
+                        --enable-flashbundle \
+                        --target esp32-m5stack-all-clusters-minimal \
+                        --target esp32-m5stack-all-clusters-rpc-ipv6only \
+                        --pregen-dir ./zzz_pregenerated \
+                        build \
+                        --copy-artifacts-to out/artifacts \
+                     "
+            - name: Undo code pregeneration changes
+              run: |
+                  rm -rf ./zzz_pregenerated
+                  mv scripts/codegen.py.renamed scripts/codegen.py
             - name: Prepare bloat report
               run: |
                   .environment/pigweed-venv/bin/python3 scripts/tools/memory/gh_sizes.py \
diff --git a/build/chip/chip_codegen.cmake b/build/chip/chip_codegen.cmake
index 17e3844..885d189 100644
--- a/build/chip/chip_codegen.cmake
+++ b/build/chip/chip_codegen.cmake
@@ -31,40 +31,71 @@
          ${ARGN}
     )
 
-    set(GEN_FOLDER "${CMAKE_BINARY_DIR}/gen/${TARGET_NAME}/${ARG_GENERATOR}")
+    set(CHIP_CODEGEN_PREGEN_DIR "" CACHE PATH "Pre-generated directory to use instead of compile-time code generation.")
 
-    string(REPLACE ";" "\n" OUTPUT_AS_NEWLINES "${ARG_OUTPUTS}")
+    if ("${CHIP_CODEGEN_PREGEN_DIR}" STREQUAL "")
+        set(GEN_FOLDER "${CMAKE_BINARY_DIR}/gen/${TARGET_NAME}/${ARG_GENERATOR}")
 
-    file(MAKE_DIRECTORY "${GEN_FOLDER}")
-    file(GENERATE
-        OUTPUT "${GEN_FOLDER}/expected.outputs"
-        CONTENT "${OUTPUT_AS_NEWLINES}"
-    )
+        string(REPLACE ";" "\n" OUTPUT_AS_NEWLINES "${ARG_OUTPUTS}")
+
+        file(MAKE_DIRECTORY "${GEN_FOLDER}")
+        file(GENERATE
+            OUTPUT "${GEN_FOLDER}/expected.outputs"
+            CONTENT "${OUTPUT_AS_NEWLINES}"
+        )
 
 
-    set(OUT_NAMES)
-    foreach(NAME IN LISTS ARG_OUTPUTS)
-        list(APPEND OUT_NAMES "${GEN_FOLDER}/${NAME}")
-    endforeach()
+        set(OUT_NAMES)
+        foreach(NAME IN LISTS ARG_OUTPUTS)
+            list(APPEND OUT_NAMES "${GEN_FOLDER}/${NAME}")
+        endforeach()
 
-    # Python is expected to be in the path
-    #
-    # find_package(Python3 REQUIRED)
-    add_custom_command(
-        OUTPUT ${OUT_NAMES}
-        COMMAND "${CHIP_ROOT}/scripts/codegen.py"
-        ARGS "--generator" "${ARG_GENERATOR}"
-             "--output-dir" "${GEN_FOLDER}"
-             "--expected-outputs" "${GEN_FOLDER}/expected.outputs"
-             "${ARG_INPUT}"
-        DEPENDS
-            "${ARG_INPUT}"
-        VERBATIM
-    )
+        # Python is expected to be in the path
+        #
+        # find_package(Python3 REQUIRED)
+        add_custom_command(
+            OUTPUT ${OUT_NAMES}
+            COMMAND "${CHIP_ROOT}/scripts/codegen.py"
+            ARGS "--generator" "${ARG_GENERATOR}"
+                 "--output-dir" "${GEN_FOLDER}"
+                 "--expected-outputs" "${GEN_FOLDER}/expected.outputs"
+                 "${ARG_INPUT}"
+            DEPENDS
+                "${ARG_INPUT}"
+            VERBATIM
+        )
 
-    add_custom_target(${TARGET_NAME} DEPENDS "${OUT_NAMES}")
+        add_custom_target(${TARGET_NAME} DEPENDS "${OUT_NAMES}")
 
-    # Forward outputs to the parent
-    set(${ARG_OUTPUT_FILES} "${OUT_NAMES}" PARENT_SCOPE)
-    set(${ARG_OUTPUT_PATH} "${GEN_FOLDER}" PARENT_SCOPE)
+        # Forward outputs to the parent
+        set(${ARG_OUTPUT_FILES} "${OUT_NAMES}" PARENT_SCOPE)
+        set(${ARG_OUTPUT_PATH} "${GEN_FOLDER}" PARENT_SCOPE)
+    else()
+        # Gets a path such as:
+        #    examples/lock-app/lock-common/lock-app.matter
+        file(RELATIVE_PATH MATTER_FILE_PATH "${CHIP_ROOT}" ${ARG_INPUT})
+
+        # Removes the trailing file extension to get something like:
+        #    examples/lock-app/lock-common/lock-app
+        string(REGEX REPLACE "\.matter$" "" CODEGEN_DIR_PATH "${MATTER_FILE_PATH}")
+
+
+        # Build the final location within the pregen directory
+        set(GEN_FOLDER "${CHIP_CODEGEN_PREGEN_DIR}/${CODEGEN_DIR_PATH}/codegen/${ARG_GENERATOR}")
+
+        # TODO: build a fake target of ${TARGET_NAME}
+
+        # Here we have ${CHIP_CODEGEN_PREGEN_DIR}
+        set(OUT_NAMES)
+        foreach(NAME IN LISTS ARG_OUTPUTS)
+            list(APPEND OUT_NAMES "${GEN_FOLDER}/${NAME}")
+        endforeach()
+
+
+        set(${ARG_OUTPUT_FILES} "${OUT_NAMES}" PARENT_SCOPE)
+        set(${ARG_OUTPUT_PATH} "${GEN_FOLDER}" PARENT_SCOPE)
+
+        # allow adding dependencies to a phony target since no codegen is done
+        add_custom_target(${TARGET_NAME})
+    endif()
 endfunction()
diff --git a/build/chip/chip_codegen.gni b/build/chip/chip_codegen.gni
index bab7993..fd5d69c 100644
--- a/build/chip/chip_codegen.gni
+++ b/build/chip/chip_codegen.gni
@@ -18,41 +18,15 @@
 
 import("$dir_pw_build/python.gni")
 
-# Defines a target that runs code generation based on
-# scripts/codegen.py
+declare_args() {
+  # Location where code has been pre-generated
+  chip_code_pre_generated_directory = ""
+}
+
+# Code generation that will happen at build time.
 #
-# Arguments:
-#   input
-#     The ".matter" file to use to start the code generation
 #
-#   generator
-#     Name of the generator to use (e.g. java, cpp-app)
-#
-#   outputs
-#     Explicit names of the expected outputs. Enforced to validate that
-#     expected outputs are generated when processing input files.
-#
-# NOTE: content of "outputs" is verified to match the output of codegen.py
-#       exactly. It is not inferred on purpose, to make build-rules explicit
-#       and verifiable (even though codege.py can at runtime report its outputs)
-#
-#       To find the list of generated files, you can run codegen.py with the
-#       "--name-only" argument
-#
-# Example usage:
-#
-#  chip_codegen("java-jni-generate") {
-#    input = "controller-clusters.matter"
-#    generator = "java"
-#
-#    outputs = [
-#       "jni/IdentifyClient-ReadImpl.cpp",
-#       "jni/IdentifyClient-InvokeSubscribeImpl.cpp",
-#       # ... more to follow
-#    ]
-#  }
-#
-template("chip_codegen") {
+template("_chip_build_time_codegen") {
   _name = target_name
   _generator = invoker.generator
 
@@ -60,7 +34,7 @@
     include_dirs = [ target_gen_dir ]
   }
 
-  pw_python_action(_name) {
+  pw_python_action("${_name}_codegen") {
     script = "${chip_root}/scripts/codegen.py"
 
     _idl_file = invoker.input
@@ -79,7 +53,6 @@
     ]
 
     deps = [ "${chip_root}/scripts/idl" ]
-    public_configs = [ ":${_name}_config" ]
 
     inputs = [
       _idl_file,
@@ -92,4 +65,119 @@
       outputs += [ "${target_gen_dir}/${name}" ]
     }
   }
+
+  source_set(_name) {
+    sources = []
+    foreach(name, invoker.outputs) {
+      sources += [ "${target_gen_dir}/${name}" ]
+    }
+
+    public_configs = [ ":${_name}_config" ]
+
+    if (defined(invoker.public_configs)) {
+      public_configs += invoker.public_configs
+    }
+
+    forward_variables_from(invoker, [ "deps" ])
+
+    if (!defined(deps)) {
+      deps = []
+    }
+    deps += [ ":${_name}_codegen" ]
+  }
+}
+
+# Defines a target that runs code generation based on
+# scripts/codegen.py
+#
+# Arguments:
+#   input
+#     The ".matter" file to use to start the code generation
+#
+#   generator
+#     Name of the generator to use (e.g. java, cpp-app)
+#
+#   outputs
+#     Explicit names of the expected outputs. Enforced to validate that
+#     expected outputs are generated when processing input files.
+#
+#   deps, public_configs
+#     Forwarded to the resulting source set
+#
+# Command line parameters:
+#
+#  chip_code_pre_generated_directory:
+#     - If this is set, generation will NOT happen at compile time but rather
+#       the code generation is assumed to have already happened and reside in
+#       the given location.
+#     - The TOP LEVEL directory is assumed to be given. Actual location for
+#       individual generators is expected to be of the form
+#       <top_dir>/<matter_path>/<generator>
+#
+# NOTE: content of "outputs" is verified to match the output of codegen.py
+#       exactly. It is not inferred on purpose, to make build-rules explicit
+#       and verifiable (even though codegen.py can at runtime report its outputs)
+#
+#       To find the list of generated files, you can run codegen.py with the
+#       "--name-only" argument
+#
+# NOTE:
+#   the result of the target_name WILL BE a `source_set`. Treat it as such.
+#
+# Example usage:
+#
+#  chip_codegen("java-jni-generate") {
+#    input = "controller-clusters.matter"
+#    generator = "java"
+#
+#    outputs = [
+#       "jni/IdentifyClient-ReadImpl.cpp",
+#       "jni/IdentifyClient-InvokeSubscribeImpl.cpp",
+#       # ... more to follow
+#    ]
+#  }
+#
+template("chip_codegen") {
+  if (chip_code_pre_generated_directory == "") {
+    _chip_build_time_codegen(target_name) {
+      forward_variables_from(invoker,
+                             [
+                               "deps",
+                               "generator",
+                               "input",
+                               "outputs",
+                               "public_configs",
+                             ])
+    }
+  } else {
+    _name = target_name
+
+    # This contstructs a path like:
+    #  FROM all-clusters-app.matter (inside examples/all-clusters-app/all-clusters-common/)
+    #  USING "cpp-app" for generator:
+    #    => ${pregen_dir}/examples/all-clusters-app/all-clusters-common/all-clusters-app/codegen/cpp-app
+    _generation_dir =
+        chip_code_pre_generated_directory + "/" +
+        string_replace(rebase_path(invoker.input, chip_root), ".matter", "") +
+        "/codegen/" + invoker.generator
+
+    config("${_name}_config") {
+      include_dirs = [ "${_generation_dir}" ]
+    }
+
+    source_set(_name) {
+      public_configs = [ ":${_name}_config" ]
+
+      if (defined(invoker.public_configs)) {
+        public_configs += invoker.public_configs
+      }
+
+      forward_variables_from(invoker, [ "deps" ])
+
+      sources = []
+      foreach(name, invoker.outputs) {
+        sources += [ "${_generation_dir}/${name}" ]
+      }
+    }
+  }
 }
diff --git a/config/esp32/components/chip/CMakeLists.txt b/config/esp32/components/chip/CMakeLists.txt
index 44f9238..d7cb69a 100644
--- a/config/esp32/components/chip/CMakeLists.txt
+++ b/config/esp32/components/chip/CMakeLists.txt
@@ -101,6 +101,10 @@
     chip_gn_arg_append("chip_inet_config_enable_ipv4"        "false")
 endif()
 
+if(CHIP_CODEGEN_PREGEN_DIR)
+    chip_gn_arg_append("chip_code_pre_generated_directory"  "\"${CHIP_CODEGEN_PREGEN_DIR}\"")
+endif()
+
 if(CONFIG_ENABLE_PW_RPC)
     string(APPEND chip_gn_args "import(\"//build_overrides/pigweed.gni\")\n")
     chip_gn_arg_append("remove_default_configs"             "[\"//third_party/connectedhomeip/third_party/pigweed/repo/pw_build:toolchain_cpp_standard\"]")
diff --git a/docs/code_generation.md b/docs/code_generation.md
index 51a615f..051a215 100644
--- a/docs/code_generation.md
+++ b/docs/code_generation.md
@@ -176,12 +176,32 @@
 
 ### `*.matter` code generation
 
-Currently `*.matter` code generation is done at compile time.
+`*.matter` code generation can be done either at compile time or it can use
+pre-generated output.
 
 Rules for how `codegen.py` is invoked and how includes/sources are set are
 defined at:
 
 -   `src/app/chip_data_model.cmake`
--   `build/chip/esp32/esp32_codegen.cmake` (support for 2-pass cmake builds used
-    by the Espressif `idf.py` build system)
 -   `src/app/chip_data_model.gni`
+
+Additionally, `build/chip/esp32/esp32_codegen.cmake` adds processing support for
+the 2-pass cmake builds used by the Espressif `idf.py` build system.
+
+## Pre-generation
+
+Code pre-generation can be used:
+
+-   when compile-time code generation is not desirable. This may be for
+    importing into build systems that do not have the pre-requisites to run code
+    generation at build time or to save the code generation time at the expense
+    of running code generation for every possible zap/generation type
+-   To check changes in generated code across versions, beyond the comparisons
+    of golden image tests in `scripts/idl/tests`
+
+The script to trigger code pre-generation is `scripts/code_pregenerate.py` and
+requires the pre-generation output directory as an argument
+
+```bash
+scripts/code_pregenerate.py ${OUTPUT_DIRECTORY:-./zzz_pregenerated/}
+```
diff --git a/scripts/build/build_examples.py b/scripts/build/build_examples.py
index 5792bfa..780241f 100755
--- a/scripts/build/build_examples.py
+++ b/scripts/build/build_examples.py
@@ -88,6 +88,11 @@
     type=click.Path(file_okay=False, resolve_path=True),
     help='Prefix for the generated file output.')
 @click.option(
+    '--pregen-dir',
+    default=None,
+    type=click.Path(file_okay=False, resolve_path=True),
+    help='Directory where generated files have been pre-generated.')
+@click.option(
     '--clean',
     default=False,
     is_flag=True,
@@ -114,7 +119,7 @@
         'for using ccache when building examples.'))
 @click.pass_context
 def main(context, log_level, target, repo,
-         out_prefix, clean, dry_run, dry_run_output, enable_flashbundle,
+         out_prefix, pregen_dir, clean, dry_run, dry_run_output, enable_flashbundle,
          no_log_timestamps, pw_command_launcher):
     # Ensures somewhat pretty logging of what is going on
     log_fmt = '%(asctime)s %(levelname)-7s %(message)s'
@@ -143,6 +148,7 @@
     context.obj.SetupBuilders(targets=requested_targets, options=BuilderOptions(
         enable_flashbundle=enable_flashbundle,
         pw_command_launcher=pw_command_launcher,
+        pregen_dir=pregen_dir,
     ))
 
     if clean:
diff --git a/scripts/build/builders/builder.py b/scripts/build/builders/builder.py
index 99bc925..dc420fc 100644
--- a/scripts/build/builders/builder.py
+++ b/scripts/build/builders/builder.py
@@ -24,9 +24,13 @@
 class BuilderOptions:
     # Enable flashbundle generation stage
     enable_flashbundle: bool = False
+
     # Allow to wrap default build command
     pw_command_launcher: str = None
 
+    # Locations where files are pre-generated
+    pregen_dir: str = None
+
 
 class Builder(ABC):
     """Generic builder base class for CHIP.
diff --git a/scripts/build/builders/esp32.py b/scripts/build/builders/esp32.py
index d84e8f5..54c47a4 100644
--- a/scripts/build/builders/esp32.py
+++ b/scripts/build/builders/esp32.py
@@ -176,11 +176,19 @@
             self._Execute(
                 ['bash', '-c', 'echo -e "\\nCONFIG_DISABLE_IPV4=y\\n" >>%s' % shlex.quote(defaults_out)])
 
-        cmd = "\nexport SDKCONFIG_DEFAULTS={defaults}\nidf.py -C {example_path} -B {out} reconfigure".format(
-            defaults=shlex.quote(defaults_out),
-            example_path=self.ExamplePath,
-            out=shlex.quote(self.output_dir)
-        )
+        cmake_flags = []
+
+        if self.options.pregen_dir:
+            cmake_flags.append(
+                f"-DCHIP_CODEGEN_PREGEN_DIR={shlex.quote(self.options.pregen_dir)}")
+
+        cmake_args = ['-C', self.ExamplePath, '-B',
+                      shlex.quote(self.output_dir)] + cmake_flags
+
+        cmake_args = " ".join(cmake_args)
+        defaults = shlex.quote(defaults_out)
+
+        cmd = f"\nexport SDKCONFIG_DEFAULTS={defaults}\nidf.py {cmake_args} reconfigure"
 
         # This will do a 'cmake reconfigure' which will create ninja files without rebuilding
         self._IdfEnvExecute(cmd)
diff --git a/scripts/build/builders/gn.py b/scripts/build/builders/gn.py
index bacd87d..9e859f6 100644
--- a/scripts/build/builders/gn.py
+++ b/scripts/build/builders/gn.py
@@ -61,8 +61,13 @@
         ]
 
         extra_args = []
+
         if self.options.pw_command_launcher:
             extra_args.append('pw_command_launcher="%s"' % self.options.pw_command_launcher)
+
+        if self.options.pregen_dir:
+            extra_args.append('chip_code_pre_generated_directory="%s"' % self.options.pregen_dir)
+
         extra_args.extend(self.GnBuildArgs() or [])
         if extra_args:
             cmd += ['--args=%s' % ' '.join(extra_args)]
diff --git a/scripts/build/builders/mbed.py b/scripts/build/builders/mbed.py
index 73fc802..46b3ead 100644
--- a/scripts/build/builders/mbed.py
+++ b/scripts/build/builders/mbed.py
@@ -121,14 +121,16 @@
                            '--mbed-os-path', self.mbed_os_path,
                            ], title='Generating config ' + self.identifier)
 
-            self._Execute(['cmake', '-S', shlex.quote(self.ExamplePath), '-B', shlex.quote(self.output_dir), '-GNinja',
-                           '-DCMAKE_BUILD_TYPE={}'.format(
-                               self.profile.ProfileName.lower()),
-                           '-DMBED_OS_PATH={}'.format(
-                               shlex.quote(self.mbed_os_path)),
-                           '-DMBED_OS_POSIX_SOCKET_PATH={}'.format(
-                               shlex.quote(self.mbed_os_posix_socket_path)),
-                           ], title='Generating ' + self.identifier)
+            flags = []
+            flags.append(f"-DMBED_OS_PATH={shlex.quote(self.mbed_os_path)}")
+            flags.append(f"-DMBED_OS_PATH={shlex.quote(self.mbed_os_path)}")
+            flags.append(f"-DMBED_OS_POSIX_SOCKET_PATH={shlex.quote(self.mbed_os_posix_socket_path)}")
+
+            if self.options.pregen_dir:
+                flags.append(f"-DCHIP_CODEGEN_PREGEN_DIR={shlex.quote(self.options.pregen_dir)}")
+
+            self._Execute(['cmake', '-S', shlex.quote(self.ExamplePath), '-B', shlex.quote(self.output_dir),
+                          '-GNinja'] + flags, title='Generating ' + self.identifier)
 
     def _build(self):
         # Remove old artifacts to force linking
diff --git a/scripts/build/builders/nrf.py b/scripts/build/builders/nrf.py
index 03298cc..0037a38 100644
--- a/scripts/build/builders/nrf.py
+++ b/scripts/build/builders/nrf.py
@@ -179,6 +179,9 @@
             if self.board == NrfBoard.NRF52840DONGLE and self.app != NrfApp.ALL_CLUSTERS and self.app != NrfApp.ALL_CLUSTERS_MINIMAL:
                 flags.append("-DCONF_FILE=prj_no_dfu.conf")
 
+            if self.options.pregen_dir:
+                flags.append(f"-DCHIP_CODEGEN_PREGEN_DIR={shlex.quote(self.options.pregen_dir)}")
+
             build_flags = " -- " + " ".join(flags) if len(flags) > 0 else ""
 
             cmd = '''
diff --git a/scripts/build/builders/telink.py b/scripts/build/builders/telink.py
index a85ec7c..32a9bf8 100644
--- a/scripts/build/builders/telink.py
+++ b/scripts/build/builders/telink.py
@@ -100,15 +100,21 @@
         if os.path.exists(self.output_dir):
             return
 
+        flags = []
+        if self.options.pregen_dir:
+            flags.append(f"-DCHIP_CODEGEN_PREGEN_DIR={shlex.quote(self.options.pregen_dir)}")
+
+        build_flags = " -- " + " ".join(flags) if len(flags) > 0 else ""
+
         cmd = self.get_cmd_prefixes()
         cmd += '''
 source "$ZEPHYR_BASE/zephyr-env.sh";
-west build --cmake-only -d {outdir} -b {board} {sourcedir}
+west build --cmake-only -d {outdir} -b {board} {sourcedir}{build_flags}
         '''.format(
-            outdir=shlex.quote(
-                self.output_dir), board=self.board.GnArgName(), sourcedir=shlex.quote(
-                os.path.join(
-                    self.root, 'examples', self.app.ExampleName(), 'telink'))).strip()
+            outdir=shlex.quote(self.output_dir),
+            board=self.board.GnArgName(),
+            sourcedir=shlex.quote(os.path.join(self.root, 'examples', self.app.ExampleName(), 'telink')),
+            build_flags=build_flags).strip()
 
         self._Execute(['bash', '-c', cmd],
                       title='Generating ' + self.identifier)
diff --git a/scripts/codepregen.py b/scripts/codepregen.py
new file mode 100755
index 0000000..13ca9a6
--- /dev/null
+++ b/scripts/codepregen.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2022 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+import logging
+import multiprocessing
+import itertools
+import enum
+import os
+import sys
+
+
+try:
+    from pregenerate import FindPregenerationTargets
+except:
+    import os
+    sys.path.append(os.path.abspath(os.path.dirname(__file__)))
+    from pregenerate import FindPregenerationTargets
+
+try:
+    import coloredlogs
+    _has_coloredlogs = True
+except:
+    _has_coloredlogs = False
+
+# Supported log levels, mapping string values required for argument
+# parsing into logging constants
+__LOG_LEVELS__ = {
+    'debug': logging.DEBUG,
+    'info': logging.INFO,
+    'warn': logging.WARN,
+    'fatal': logging.FATAL,
+}
+
+
+def _ParallelGenerateOne(arg):
+    """
+    Helper method to be passed to multiprocessing parallel generation of
+    items.
+    """
+    arg[0].Generate(arg[1])
+
+
+@click.command()
+@click.option(
+    '--log-level',
+    default='INFO',
+    type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False),
+    help='Determines the verbosity of script output')
+@click.option(
+    '--parallel/--no-parallel',
+    default=True,
+    help='Do parallel/multiprocessing codegen.')
+@click.option(
+    '--sdk-root',
+    default=None,
+    help='Path to the SDK root (where .zap/.matter files exist).')
+@click.argument('output_dir')
+def main(log_level, parallel, sdk_root, output_dir):
+    if _has_coloredlogs:
+        coloredlogs.install(level=__LOG_LEVELS__[
+                            log_level], fmt='%(asctime)s %(levelname)-7s %(message)s')
+    else:
+        logging.basicConfig(
+            level=__LOG_LEVELS__[log_level],
+            format='%(asctime)s %(levelname)-7s %(message)s',
+            datefmt='%Y-%m-%d %H:%M:%S'
+        )
+
+    if not sdk_root:
+        sdk_root = os.path.join(os.path.dirname(
+            os.path.realpath(__file__)), '..')
+
+    sdk_root = os.path.abspath(sdk_root)
+
+    if not output_dir:
+        raise Exception("Missing output directory")
+
+    output_dir = os.path.abspath(output_dir)
+
+    logging.info(f"Pre-generating {sdk_root} data into {output_dir}")
+
+    if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+
+    targets = FindPregenerationTargets(sdk_root)
+
+    if parallel:
+        target_and_dir = zip(targets, itertools.repeat(output_dir))
+        with multiprocessing.Pool() as pool:
+            for _ in pool.imap_unordered(_ParallelGenerateOne, target_and_dir):
+                pass
+    else:
+        for target in targets:
+            target.Generate(output_dir)
+
+    logging.info("Done")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/pregenerate/__init__.py b/scripts/pregenerate/__init__.py
new file mode 100644
index 0000000..eab3205
--- /dev/null
+++ b/scripts/pregenerate/__init__.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2022 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import logging
+import os
+
+from typing import Iterator
+
+from .types import InputIdlFile, IdlFileType
+
+
+from .pregenerators import CodegenJavaPregenerator
+from .pregenerators import CodegenBridgePregenerator
+from .pregenerators import CodegenCppAppPregenerator
+
+
+def FindAllIdls(sdk_root: str) -> Iterator[InputIdlFile]:
+    relevant_subdirs = [
+        'examples',  # all example apps
+        'src',      # realistically only controller/data_model
+    ]
+
+    while sdk_root.endswith('/'):
+        sdk_root = sdk_root[:-1]
+    sdk_root_length = len(sdk_root)
+
+    for subdir_name in relevant_subdirs:
+        top_directory_name = os.path.join(sdk_root, subdir_name)
+        logging.debug(f"Searching {top_directory_name}")
+        for root, dirs, files in os.walk(top_directory_name):
+            for file in files:
+                if file.endswith('.zap'):
+                    yield InputIdlFile(file_type=IdlFileType.ZAP,
+                                       relative_path=os.path.join(root[sdk_root_length+1:], file))
+                if file.endswith('.matter'):
+                    yield InputIdlFile(file_type=IdlFileType.MATTER,
+                                       relative_path=os.path.join(root[sdk_root_length+1:], file))
+
+
+def FindPregenerationTargets(sdk_root: str):
+    """Finds all relevand pre-generation targets in the given
+       SDK root.
+
+       Pre-generation targets are based on zap and matter files with options
+       on what rules to pregenerate and how.
+    """
+
+    generators = [
+        CodegenBridgePregenerator(sdk_root),
+        CodegenJavaPregenerator(sdk_root),
+        CodegenCppAppPregenerator(sdk_root),
+    ]
+
+    for idl in FindAllIdls(sdk_root):
+        for generator in generators:
+            if generator.Accept(idl):
+                yield generator.CreateTarget(idl)
diff --git a/scripts/pregenerate/pregenerators.py b/scripts/pregenerate/pregenerators.py
new file mode 100644
index 0000000..3d6658d
--- /dev/null
+++ b/scripts/pregenerate/pregenerators.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2022 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+import shlex
+import subprocess
+
+from .types import InputIdlFile, IdlFileType
+
+CODEGEN_PY_PATH = os.path.join(os.path.dirname(__file__), '..', 'codegen.py')
+
+
+class CodegenTarget:
+    """A target that uses `scripts/codegen.py` to generate files."""
+
+    def __init__(self, idl: InputIdlFile, generator: str, sdk_root: str):
+        self.idl = idl
+        self.generator = generator
+        self.sdk_root = sdk_root
+
+        if idl.file_type != IdlFileType.MATTER:
+            raise Exception(f"Can only code generate for `*.matter` input files, not for {idl}")
+
+    def Generate(self, output_root: str):
+        '''Runs codegen.py to generate in the specified directory'''
+
+        output_dir = os.path.join(output_root, self.idl.pregen_subdir, self.generator)
+
+        logging.info(f"Generating: {self.generator}:{self.idl.relative_path} into {output_dir}")
+
+        cmd = [
+            CODEGEN_PY_PATH,
+            '--log-level', 'fatal',
+            '--generator', self.generator,
+            '--output-dir', output_dir,
+            os.path.join(self.sdk_root, self.idl.relative_path)
+        ]
+
+        logging.debug(f"Executing {cmd}")
+        subprocess.check_call(cmd)
+
+
+class CodegenBridgePregenerator:
+    """Pregeneration logic for "bridge" codegen.py outputs"""
+
+    def __init__(self, sdk_root):
+        self.sdk_root = sdk_root
+
+    def Accept(self, idl: InputIdlFile):
+        # Bridge is highly specific, a single path is acceptable for dynamic
+        # bridge codegen
+        return idl.relative_path == "examples/dynamic-bridge-app/bridge-common/bridge-app.matter"
+
+    def CreateTarget(self, idl: InputIdlFile):
+        return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="bridge")
+
+
+class CodegenJavaPregenerator:
+    """Pregeneration logic for "java" codegen.py outputs"""
+
+    def __init__(self, sdk_root):
+        self.sdk_root = sdk_root
+
+    def Accept(self, idl: InputIdlFile):
+        # Java is highly specific, a single path is acceptable for dynamic
+        # bridge codegen
+        return idl.relative_path == "src/controller/data_model/controller-clusters.matter"
+
+    def CreateTarget(self, idl: InputIdlFile):
+        return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="java")
+
+
+class CodegenCppAppPregenerator:
+    """Pregeneration logic for "cpp-app" codegen.py outputs"""
+
+    def __init__(self, sdk_root):
+        self.sdk_root = sdk_root
+
+    def Accept(self, idl: InputIdlFile):
+        if idl.file_type != IdlFileType.MATTER:
+            return False
+
+        # we should not be checked for these, but verify just in case
+        if '/tests/' in idl.relative_path:
+            return False
+
+        return True
+
+    def CreateTarget(self, idl: InputIdlFile):
+        return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-app")
diff --git a/scripts/pregenerate/types.py b/scripts/pregenerate/types.py
new file mode 100644
index 0000000..3d8c0bc
--- /dev/null
+++ b/scripts/pregenerate/types.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2022 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from dataclasses import dataclass
+from enum import Enum, auto
+
+
+class IdlFileType(Enum):
+    ZAP = auto()
+    MATTER = auto()
+
+
+@dataclass
+class InputIdlFile:
+    file_type: IdlFileType
+    relative_path: str
+
+    @property
+    def pregen_subdir(self):
+        '''
+        Returns the relative path inside the pregenerate directory where
+        data for this IDL file should be pregenerated.
+        '''
+        top_dir = os.path.splitext(self.relative_path)[0]
+
+        if self.file_type == IdlFileType.MATTER:
+            return os.path.join(top_dir, "codegen")
+        elif self.file_type == IdlFileType.ZAP:
+            return os.path.join(top_dir, "zap")
+        else:
+            raise Exception("Unknown file type for self")
diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni
index 68aabd7..ecab5a9 100644
--- a/src/app/chip_data_model.gni
+++ b/src/app/chip_data_model.gni
@@ -51,15 +51,6 @@
     _idl = string_replace(invoker.zap_file, ".zap", ".matter")
   }
 
-  chip_codegen("${_data_model_name}_codegen") {
-    input = _idl
-    generator = "cpp-app"
-    outputs = [
-      "app/PluginApplicationCallbacks.h",
-      "app/callback-stub.cpp",
-    ]
-  }
-
   config("${_data_model_name}_config") {
     include_dirs = []
 
@@ -68,6 +59,24 @@
     }
   }
 
+  chip_codegen("${_data_model_name}_codegen") {
+    input = _idl
+    generator = "cpp-app"
+
+    outputs = [
+      "app/PluginApplicationCallbacks.h",
+      "app/callback-stub.cpp",
+    ]
+
+    public_configs = [ ":${_data_model_name}_config" ]
+
+    if (!defined(deps)) {
+      deps = []
+    }
+
+    deps += [ "${chip_root}/src/app/common:cluster-objects" ]
+  }
+
   _use_default_im_dispatch = !defined(invoker.use_default_im_dispatch) ||
                              invoker.use_default_im_dispatch
 
@@ -84,7 +93,10 @@
       sources = []
     }
 
-    sources += get_target_outputs(":${_data_model_name}_codegen")
+    if (!defined(deps)) {
+      deps = []
+    }
+    deps += [ ":${_data_model_name}_codegen" ]
 
     sources += [
       "${_app_root}/clusters/barrier-control-server/barrier-control-server.h",
diff --git a/src/controller/data_model/BUILD.gn b/src/controller/data_model/BUILD.gn
index 011669d..95b0e6c 100644
--- a/src/controller/data_model/BUILD.gn
+++ b/src/controller/data_model/BUILD.gn
@@ -31,6 +31,12 @@
 }
 
 if (current_os == "android" || build_java_matter_controller) {
+  config("java-build-config") {
+    if (build_java_matter_controller) {
+      include_dirs = java_matter_controller_dependent_paths
+    }
+  }
+
   chip_codegen("java-jni-generate") {
     input = "controller-clusters.matter"
     generator = "java"
@@ -167,12 +173,21 @@
       "jni/UnitTestingClient-ReadImpl.cpp",
       "jni/UnitTestingClient-InvokeSubscribeImpl.cpp",
     ]
+
+    deps = [
+      ":data_model",
+      "${chip_root}/src/platform:platform_buildconfig",
+    ]
+
+    public_configs = [ ":java-build-config" ]
   }
 
   source_set("java-jni-sources") {
-    sources = get_target_outputs(":java-jni-generate")
+    public_configs = [
+      ":java-build-config",
+      "${chip_root}/src:includes",
+    ]
 
-    public_configs = [ "${chip_root}/src:includes" ]
     deps = [
       ":data_model",
       ":java-jni-generate",
@@ -182,7 +197,6 @@
     ]
 
     if (build_java_matter_controller) {
-      include_dirs = java_matter_controller_dependent_paths
       deps += [ "${chip_root}/src/platform/Linux" ]
     } else {
       deps += [ "${chip_root}/src/platform/android" ]