[Tizen] Create TPK flashbundle with Tizen GN SDK (#18298)

diff --git a/build_overrides/tizen.gni b/build_overrides/tizen.gni
index c98c799..34914ce 100644
--- a/build_overrides/tizen.gni
+++ b/build_overrides/tizen.gni
@@ -13,6 +13,9 @@
 # limitations under the License.
 
 declare_args() {
-  # Root directory for tizen.
+  # Root directory for Tizen.
   tizen_root = "//config/tizen/chip-gn/platform"
+
+  # Root directory for Tizen SDK build support.
+  tizen_sdk_build_root = "//third_party/tizen"
 }
diff --git a/examples/build_overrides/tizen.gni b/examples/build_overrides/tizen.gni
index 29a2771..110a271 100644
--- a/examples/build_overrides/tizen.gni
+++ b/examples/build_overrides/tizen.gni
@@ -13,6 +13,9 @@
 # limitations under the License.
 
 declare_args() {
-  # Root directory for tizen.
+  # Root directory for Tizen.
   tizen_root = "//third_party/connectedhomeip/config/tizen/chip-gn/platform"
+
+  # Root directory for Tizen SDK build support.
+  tizen_sdk_build_root = "//third_party/connectedhomeip/third_party/tizen"
 }
diff --git a/examples/lighting-app/tizen/BUILD.gn b/examples/lighting-app/tizen/BUILD.gn
index e43e377..b45574d 100644
--- a/examples/lighting-app/tizen/BUILD.gn
+++ b/examples/lighting-app/tizen/BUILD.gn
@@ -13,9 +13,11 @@
 # limitations under the License.
 
 import("//build_overrides/chip.gni")
+import("//build_overrides/tizen.gni")
 
 import("${chip_root}/build/chip/tools.gni")
 import("${chip_root}/src/app/common_flags.gni")
+import("${tizen_sdk_build_root}/tizen_sdk.gni")
 
 assert(chip_build_tools)
 
@@ -41,10 +43,20 @@
   output_dir = root_out_dir
 }
 
+tizen_sdk_package("chip-lighting-app:tpk") {
+  deps = [ ":chip-lighting-app" ]
+  manifest = rebase_path("tizen-manifest.xml")
+  sign_security_profile = "CHIP"
+}
+
 group("tizen") {
   deps = [ ":chip-lighting-app" ]
 }
 
+group("tizen:tpk") {
+  deps = [ ":chip-lighting-app:tpk" ]
+}
+
 group("default") {
   deps = [ ":tizen" ]
 }
diff --git a/scripts/build/builders/tizen.py b/scripts/build/builders/tizen.py
index 077de00..efaf444 100644
--- a/scripts/build/builders/tizen.py
+++ b/scripts/build/builders/tizen.py
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
 import os
-import shutil
 from enum import Enum, auto
 from xml.etree import ElementTree as ET
 
@@ -41,9 +41,6 @@
     def PackageVersion(self):
         return self.manifest.get('version')
 
-    def PackageExecName(self):
-        return self.manifest.find('{*}service-application').get('exec')
-
     def parse_manifest(self, manifest: str):
         self.manifest = ET.parse(manifest).getroot()
 
@@ -112,48 +109,9 @@
         ]
 
     def _generate_flashbundle(self):
-
-        self.tizen_tpk_dir = os.path.join(self.output_dir, "tpk")
-        self.tizen_out_dir = os.path.join(self.tizen_tpk_dir, "out")
-
-        # Create dirrectory where the TPK package file will be created
-        os.makedirs(self.tizen_out_dir, exist_ok=True)
-
-        # Create a dummy project definition file, so the Tizen Studio CLI
-        # will recognize given directory as a Tizen project.
-        prj_def_file = os.path.join(self.tizen_tpk_dir, "project_def.prop")
-        with open(prj_def_file, "w") as f:
-            f.writelines(l + "\n" for l in [
-                "# Generated by the build script. DO NOT EDIT!",
-                "APPNAME = %s" % self.app.AppName(),
-                "type = app",
-            ])
-
-        # Create a dummy project file, so the Tizen Studio CLI will not
-        # complain about invalid XPath (this file is not used anyway...)
-        prj_file = os.path.join(self.tizen_tpk_dir, ".project")
-        with open(prj_file, "w") as f:
-            f.writelines(l + "\n" for l in [
-                "<!-- Generated by the build script. DO NOT EDIT! -->",
-                "<projectDescription></projectDescription>",
-            ])
-
-        # Copy Tizen manifest file into the dummy Tizen project.
-        shutil.copy(
-            os.path.join(self.root, "tizen-manifest.xml"),
-            self.tizen_tpk_dir)
-
-        # Create link with the application executable in the TPK package
-        # directory. This linked file will be used by the Tizen Studio CLI
-        # when creating TPK package.
-        exec = os.path.join(self.tizen_out_dir, self.app.PackageExecName())
-        if not os.path.exists(exec):
-            os.symlink(os.path.join(self.output_dir, self.app.AppName()),
-                       exec)
-
-        self._Execute([self.tizen_sdk_cli, 'package', '--type', 'tpk',
-                       '--sign', 'CHIP', '--', self.tizen_out_dir],
-                      title='Generating TPK file for ' + self.identifier)
+        logging.info('Packaging %s', self.output_dir)
+        cmd = ['ninja', '-C', self.output_dir, self.app.AppName() + ':tpk']
+        self._Execute(cmd, title='Packaging ' + self.identifier)
 
     def build_outputs(self):
         return {
@@ -166,5 +124,5 @@
     def flashbundle(self):
         tpk = f'{self.app.PackageName()}-{self.app.PackageVersion()}.tpk'
         return {
-            tpk: os.path.join(self.tizen_out_dir, tpk),
+            tpk: os.path.join(self.output_dir, 'package', 'out', tpk),
         }
diff --git a/third_party/tizen/tizen_manifest_parser.py b/third_party/tizen/tizen_manifest_parser.py
new file mode 100755
index 0000000..3266a4a
--- /dev/null
+++ b/third_party/tizen/tizen_manifest_parser.py
@@ -0,0 +1,73 @@
+#!/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 argparse
+import json
+from xml.etree import ElementTree as ET
+
+
+class TizenManifest:
+
+    def __init__(self, manifest):
+        self.manifest = ET.parse(manifest).getroot()
+
+    def _get_application(self, apptype):
+        return self.manifest.find("{*}" + apptype)
+
+    def get_package_name(self):
+        return self.manifest.get("package")
+
+    def get_package_version(self):
+        return self.manifest.get("version")
+
+    def get_service_exec(self):
+        app = self._get_application("service-application")
+        return app.get("exec", "") if app else ""
+
+    def get_ui_exec(self):
+        app = self._get_application("ui-application")
+        return app.get("exec", "") if app else ""
+
+    def get_watch_exec(self):
+        app = self._get_application("watch-application")
+        return app.get("exec", "") if app else ""
+
+    def get_widget_exec(self):
+        app = self._get_application("widget-application")
+        return app.get("exec", "") if app else ""
+
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser(
+        description="Tool for extracting data from Tizen XML manifest file")
+    parser.add_argument('MANIFEST', help="Tizen manifest XML file")
+
+    args = parser.parse_args()
+    manifest = TizenManifest(args.MANIFEST)
+
+    print(json.dumps({
+        'package': {
+            'name': manifest.get_package_name(),
+            'version': manifest.get_package_version(),
+        },
+        'apps': {
+            'service': manifest.get_service_exec(),
+            'ui': manifest.get_ui_exec(),
+            'watch': manifest.get_watch_exec(),
+            'widget': manifest.get_widget_exec(),
+        }
+    }))
diff --git a/third_party/tizen/tizen_sdk.gni b/third_party/tizen/tizen_sdk.gni
new file mode 100644
index 0000000..dc17cf7
--- /dev/null
+++ b/third_party/tizen/tizen_sdk.gni
@@ -0,0 +1,118 @@
+# Copyright (c) 2020 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("//build_overrides/build.gni")
+import("//build_overrides/chip.gni")
+import("//build_overrides/tizen.gni")
+
+import("${build_root}/config/tizen/config.gni")
+
+tizen_manifest_parser = rebase_path("tizen_manifest_parser.py")
+
+template("tizen_sdk") {
+  forward_variables_from(invoker,
+                         [
+                           "project_build_dir",
+                           "project_app_name",
+                         ])
+
+  if (!defined(project_app_name)) {
+    project_app_name = "tizen-app"
+  }
+
+  # Create a dummy project definition file, so the Tizen Studio CLI
+  # will recognize our build directory as a Tizen project.
+  write_file("${project_build_dir}/project_def.prop",
+             [
+               "# Generated by the GN script. DO NOT EDIT!",
+               "APPNAME = " + project_app_name,
+               "type = app",
+             ])
+
+  # Create a dummy project file, so the Tizen Studio CLI will not
+  # complain about invalid XPath (this file is not used anyway...)
+  write_file("${project_build_dir}/.project",
+             [
+               "<!-- Generated by the build script. DO NOT EDIT! -->",
+               "<projectDescription></projectDescription>",
+             ])
+
+  action(target_name) {
+    forward_variables_from(invoker,
+                           [
+                             "deps",
+                             "outputs",
+                           ])
+    script = "${build_root}/gn_run_binary.py"
+    args = [ "${tizen_sdk_root}/tools/ide/bin/tizen" ] + invoker.args
+  }
+}
+
+template("tizen_sdk_package") {
+  # Output directory where packaging will occur. We need a separate directory
+  # for this, because Tizen Studio CLI scans "res" (resources), "shared" and
+  # "lib" directories for items to pack. In our case it could include in the
+  # TPK package libraries available in ${root_build_dir}/lib directory.
+  tizen_package_dir = "${root_build_dir}/package"
+  tizen_package_out_dir = "${tizen_package_dir}/out"
+
+  assert(defined(invoker.manifest),
+         "It is required to specify Tizen `manifest` XML file.")
+  assert(defined(invoker.sign_security_profile),
+         "It is required to specify a `sign_security_profile` which " +
+             "should be used for signing TPK package.")
+
+  # Extract data from Tizen XML manifest.
+  manifest = exec_script(tizen_manifest_parser, [ invoker.manifest ], "json")
+  manifest_package = manifest["package"]
+  manifest_apps = manifest["apps"]
+
+  # Copy Tizen manifest from the source directory.
+  copy("${target_name}:manifest") {
+    sources = [ invoker.manifest ]
+    outputs = [ "${tizen_package_dir}/{{source_file_part}}" ]
+    deps = invoker.deps
+  }
+
+  # List of dependencies for Tizen Studio CLI packager.
+  dependencies = [ ":${target_name}:manifest" ]
+
+  # Copy executable(s) to temporary output directory. This action is required,
+  # because Tizen Studio CLI expects particular directory layout - it is not
+  # possible to specify input files manually.
+  if (manifest_apps["service"] != "") {
+    dependencies += [ ":${target_name}:app:service" ]
+    copy("${target_name}:app:service") {
+      sources = [ root_build_dir + "/" + manifest_apps["service"] ]
+      outputs = [ "${tizen_package_out_dir}/{{source_file_part}}" ]
+      deps = invoker.deps
+    }
+  }
+
+  tpk = manifest_package["name"] + "-" + manifest_package["version"] + ".tpk"
+  tizen_sdk(target_name) {
+    deps = invoker.deps + dependencies
+    outputs = [ "${tizen_package_out_dir}/" + tpk ]
+    project_build_dir = tizen_package_dir
+    args = [
+      "package",
+      "--type",
+      "tpk",
+      "--sign",
+      invoker.sign_security_profile,
+      "--",
+      tizen_package_out_dir,
+    ]
+  }
+}