[gn] Introduce initial component() rule in GN

CF-232 #comment

TEST=manual

Change-Id: I4f11c8bd88ffdd2becd3f83c93a975247d6c6864
diff --git a/cat.py b/cat.py
new file mode 100755
index 0000000..2e0c1a8
--- /dev/null
+++ b/cat.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import sys
+
+def parse_args():
+    parser = argparse.ArgumentParser(description='Concat files.')
+    parser.add_argument('-i', action='append', dest='inputs', default=[],
+                          help='Input files', required=True)
+    parser.add_argument('-o', dest='output', help='Output file', required=True)
+    args = parser.parse_args()
+    return args
+
+def main():
+    args = parse_args()
+    with open(args.output, 'w') as outfile:
+      for fname in args.inputs:
+        with open(fname) as infile:
+            outfile.write(infile.read())
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/cmx/cmx.gni b/cmx/cmx.gni
index 59aa85f..507b47f 100644
--- a/cmx/cmx.gni
+++ b/cmx/cmx.gni
@@ -4,7 +4,7 @@
 #
 # Parameters
 #   cmx (required)
-#     This is the .cmx file that wants to be validated. 
+#     This is the .cmx file that wants to be validated.
 #
 #   deps (optional)
 template("validate_cmx") {
@@ -15,6 +15,7 @@
     forward_variables_from(invoker,
                            [
                              "deps",
+                             "public_deps",
                              "testonly",
                              "visibility",
                            ])
diff --git a/package.gni b/package.gni
index cbe4e29..264e240 100644
--- a/package.gni
+++ b/package.gni
@@ -5,6 +5,7 @@
 import("//build/assert_cmx.gni")
 import("//build/cmx/cmx.gni")
 import("//build/compiled_action.gni")
+import("//build/package/component.gni")
 import("//build/gn/packages.gni")
 import("//build/images/manifest.gni")
 import("//build/json/validate_json.gni")
@@ -158,6 +159,12 @@
 #         [boolean] (default: false) When true, the binary is runnable from the shell.
 #         Shell binaries are run in the shell namespace and are not run as components.
 #
+#   components (optional)
+#     [list of fuchsia_component targets] Defines all the components this
+#     package should include in assembled package.
+#
+#     Requires `deprecated_system_image` to be `false`.
+#
 #   tests (optional)
 #     [list of scopes] Defines the test binaries in the package. A test is
 #     typically produced by the build system and is placed in the `test/`
@@ -270,6 +277,7 @@
                              [
                                "binaries",
                                "binary",
+                               "components",
                                "data_deps",
                                "deprecated_system_image",
                                "deprecated_no_cmx",
@@ -294,6 +302,9 @@
       if (!defined(deps)) {
         deps = []
       }
+      if (!defined(components)) {
+        components = []
+      }
       if (!defined(data_deps)) {
         data_deps = []
       }
@@ -321,8 +332,13 @@
       if (defined(deprecated_no_cmx)) {
         deps += [ "${deprecated_no_cmx}:no_cmx_whitelist" ]
       }
-      assert_cmx(package_name) {
-        forward_variables_from(invoker, "*")
+      if (components == []) {
+        assert_cmx(package_name) {
+          forward_variables_from(invoker, "*")
+        }
+      }
+      foreach(component, components) {
+        deps += [ component ]
       }
       if (!defined(resources)) {
         resources = []
@@ -338,6 +354,8 @@
     if (pkg.deprecated_system_image) {
       assert(pkg.meta == [],
              "$pkg_desc deprecated_system_image incompatible with meta")
+      assert(pkg.components == [],
+             "$pkg_desc deprecated_system_image incompatible with components")
       assert(!defined(pkg.binary),
              "$pkg_desc deprecated_system_image incompatible with binary")
     } else {
@@ -358,10 +376,11 @@
     # Validate .cmx files
     foreach(meta, pkg.meta) {
       if (get_path_info(meta.dest, "extension") == "cmx") {
-        validate = "validate_" + pkg_target_name +
-                          "_" + get_path_info(meta.dest, "file")
+        validate = "validate_" + pkg_target_name + "_" +
+                   get_path_info(meta.dest, "file")
         validate_cmx(validate) {
           cmx = meta.path
+
           # the cmx file may be generated by one of this package's dependencies,
           # but we don't know which one, so depend on all package deps here.
           deps = pkg.deps
@@ -514,7 +533,7 @@
 
     # An empty package() target doesn't actually generate a package at all.
     # Conveniently, an empty system_image package has exactly that effect.
-    if (manifest_sources == []) {
+    if (manifest_sources == [] && pkg.components == []) {
       pkg.deprecated_system_image = true
     }
 
@@ -556,8 +575,6 @@
         ]
       }
 
-      # For a real package, generate the package manifest with all its
-      # dynamically linked libraries resolved.
       generate_manifest("${pkg_target_name}.manifest") {
         sources = manifest_sources + get_target_outputs(":$pkg_meta_package")
         args = manifest_args +
@@ -567,9 +584,47 @@
         public_deps = pkg.public_deps
       }
 
+      final_manifest = "${pkg_target_name}.final.manifest"
+
+      # TODO(CF-224): clean this up when gn has metadata support for templates
+      action(final_manifest) {
+        script = "//build/cat.py"
+        output_name = "${target_out_dir}/${final_manifest}"
+        outputs = [
+          output_name,
+        ]
+        visibility = [ "*" ]
+
+        pmx = "$target_out_dir/${pkg_target_name}.manifest"
+        args = [
+          "-o",
+          rebase_path(output_name),
+          "-i",
+          rebase_path("$target_out_dir/${pkg_target_name}.manifest"),
+        ]
+        inputs = [
+          pmx,
+        ]
+        deps = [
+          ":${pkg_target_name}.manifest",
+        ]
+
+        foreach(component, pkg.components) {
+          deps += [ component ]
+          component_name = get_label_info(component, "name")
+          dir = get_label_info(component, "target_out_dir")
+          manifest_file = "${dir}/${component_name}.manifest"
+          inputs += [ manifest_file ]
+          args += [
+            "-i",
+            rebase_path(manifest_file),
+          ]
+        }
+      }
+
       # Next generate a signed, sealed package file.
       pm_build_package("${pkg_target_name}.meta") {
-        manifest = ":${pkg_target_name}.manifest"
+        manifest = ":$final_manifest"
       }
 
       # Clear it so we don't put anything into the system image.
@@ -615,11 +670,10 @@
       response_file_contents = []
 
       foreach(binary, shell_binaries) {
-
         response_file_contents += [
           # TODO(CF-105): fuchsia-pkg URIs should always have a variant (add /${pkg.package_version}).
           "--uri",
-          "fuchsia-pkg://fuchsia.com/${pkg.package_name}#${binary}"
+          "fuchsia-pkg://fuchsia.com/${pkg.package_name}#${binary}",
         ]
       }
     }
diff --git a/package/component.gni b/package/component.gni
new file mode 100644
index 0000000..f2f2e09
--- /dev/null
+++ b/package/component.gni
@@ -0,0 +1,181 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/cmx/cmx.gni")
+import("//build/images/manifest.gni")
+import("//build/json/validate_json.gni")
+
+# Defines a fuchsia component.
+#
+# This template is used to define a unit of component.
+# A component always has a manifest defining that component.
+#
+# Parameters
+#
+#   name (optional)
+#     [path] Defines name of the component. Cmx destination depends on this
+#     name, so if name = foo, cmx would be copied to meta/foo.cmx in assembled
+#     package.
+#
+#     If not provided, defaults to $target_name
+#
+#   manifest (required)
+#     [path] Defines the manifest source path for this component.
+#
+#   binary (required)
+#     [path] The path to the the primary binary for the component.
+#
+#   loadable_modules (optional)
+#     [list of scopes] Defines the loadable modules in the package.  These
+#     are produced by `loadable_module()` GN targets, and are typically
+#     placed in the `lib/` directory of the assembled packaged.
+#
+#     Entries in a scope in the loadable_modules list:
+#
+#       name (required)
+#         [string] Name of the loadable_module.
+#
+#       dest (optional, default: "lib")
+#         [string] Location the lib will be placed in the package.
+#
+#   resources (optional)
+#     [list of scopes] Defines the resources for this component. A resource is a
+#     data file that may be produced by the build system, checked in to a
+#     source repository, or produced by another system that runs before the
+#     build. Resources are placed in the `data/` directory of the assembled
+#     package.
+#
+#     Entries in a scope in the resources list:
+#
+#       path (required)
+#         [path] Location of resource in source or build directory. If the
+#         resource is checked in, this will typically be specified as a
+#         path relative to the BUILD.gn file containing the `package()`
+#         target. If the resource is generated, this will typically be
+#         specified relative to `$target_gen_dir`.
+#
+#       dest (required)
+#         [path] Location the resource will be placed within `data/`.
+#
+#   deps (optional)
+#   public_deps (optional)
+#   data_deps (optional)
+#   visibility (optional)
+#     Usual GN meanings.
+#
+template("fuchsia_component") {
+  component_label = get_label_info(":$target_name", "label_with_toolchain")
+  assert(
+      current_toolchain == target_toolchain,
+      "Component $component_label rule only supported for $target_toolchain.")
+  component = {
+    forward_variables_from(invoker,
+                           [
+                             "name",
+                             "binary",
+                             "data_deps",
+                             "deps",
+                             "public_deps",
+                             "loadable_modules",
+                             "resources",
+                             "visibility",
+                             "manifest",
+                           ])
+
+    # remove this check once we support dart and flutter
+    assert(defined(binary), "Component $component_label should define binary")
+    if (!defined(deps)) {
+      deps = []
+    }
+    if (!defined(name)) {
+      name = target_name
+    }
+    if (!defined(data_deps)) {
+      data_deps = []
+    }
+    if (!defined(public_deps)) {
+      public_deps = []
+    }
+    if (!defined(loadable_modules)) {
+      loadable_modules = []
+    }
+    assert(defined(manifest),
+           "Component $component_label should define manifest file")
+    if (!defined(resources)) {
+      resources = []
+    }
+  }
+
+  validate = "validate_" + target_name + "_${component.name}.cmx"
+
+  validate_cmx(validate) {
+    cmx = component.manifest
+
+    # the cmx file may be generated by one of this component's dependencies,
+    # but we don't know which one, so depend on all package deps here.
+    deps = component.deps
+    public_deps = component.public_deps
+  }
+  component.deps += [ ":$validate" ]
+
+  # Collect the component's primary manifest.
+  component_manifest = [
+    {
+      dest = "meta/${component.name}.cmx"
+      source = rebase_path(component.manifest)
+    },
+  ]
+
+  component_manifest += [
+    {
+      dest = "bin/" + get_path_info(component.binary, "file")
+      source = rebase_path(component.binary, "", root_out_dir)
+    },
+  ]
+  foreach(module, component.loadable_modules) {
+    component_manifest += [
+      {
+        if (defined(module.dest)) {
+          dest = module.dest
+        } else {
+          dest = "lib"
+        }
+        dest += "/${module.name}"
+        source = rebase_path(module.name, "", root_out_dir)
+      },
+    ]
+  }
+  foreach(resource, component.resources) {
+    component_manifest += [
+      {
+        dest = "data/${resource.dest}"
+        source = rebase_path(resource.path)
+      },
+    ]
+  }
+
+  # Collect all the arguments describing input manifest files
+  # and all the entries we've just synthesized in `component_manifest`.
+  manifest_sources = []
+  manifest_args = []
+  foreach(entry, component_manifest) {
+    manifest_sources += [ entry.source ]
+    manifest_args += [ "--entry=${entry.dest}=${entry.source}" ]
+  }
+
+  # Generate component build manifest with all its dynamically linked libraries
+  # resolved.
+  generate_manifest("${target_name}.manifest") {
+    sources = manifest_sources
+    args = manifest_args
+    deps = component.deps
+    public_deps = component.public_deps
+  }
+
+  group(target_name) {
+    public_deps = [
+      ":${target_name}.manifest",
+    ]
+  }
+}