[sdk][bazel] Give components an official presence in the build hierarchy.

A package now has access to the list of components it contains.
This is a preamble to being able to bazel-run individual components out of a given package.

Test: added tests to verify package contents, suite passes.
Bug: DX-750
Change-Id: Ia437a71e5ab102b6421ead574fafae48db3d8ad7
diff --git a/sdk/bazel/base/cc/build_defs/packageable_cc_binary.bzl b/sdk/bazel/base/cc/build_defs/cc_binary_component.bzl
similarity index 62%
rename from sdk/bazel/base/cc/build_defs/packageable_cc_binary.bzl
rename to sdk/bazel/base/cc/build_defs/cc_binary_component.bzl
index b591f04..156f665 100644
--- a/sdk/bazel/base/cc/build_defs/packageable_cc_binary.bzl
+++ b/sdk/bazel/base/cc/build_defs/cc_binary_component.bzl
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-load(":package_info.bzl", "get_aggregate_info", "PackageGeneratedInfo", "PackageLocalInfo")
+load(":package_info.bzl", "get_aggregate_info", "PackageGeneratedInfo", "PackageComponentInfo")
 
 """
 Makes a cc_binary ready for inclusion in a fuchsia_package.
@@ -42,37 +42,47 @@
     ],
 )
 
-def _packageable_cc_binary_impl(context):
-    target_files = context.attr.target[DefaultInfo].files.to_list()
-    if len(target_files) != 1:
-        fail("Packaged binary should produce a single output", attr="target")
-    output = target_files[0]
-    if output.extension != "":
-        fail("Expected executable, got: " + output.basename, attr="target")
+def _cc_binary_component_impl(context):
+    if len(context.attr.deps) != 1:
+        fail("'deps' attribute must have exactly one element.", "deps")
     return [
-        # TODO(pylaligand): remove this extra mapping once it's obsolete.
-        PackageLocalInfo(mappings = [("bin/app", output)]),
+        PackageComponentInfo(
+            name = context.attr.component_name,
+            manifest = context.file.manifest,
+        ),
     ]
 
-_packageable_cc_binary = rule(
-    implementation = _packageable_cc_binary_impl,
+_cc_binary_component = rule(
+    implementation = _cc_binary_component_impl,
     attrs = {
-        "target": attr.label(
-            doc = "The cc_binary to package",
+        "deps": attr.label_list(
+            doc = "The cc_binary for the component",
+            mandatory = True,
+            allow_empty = False,
             allow_files = False,
-            aspects = [
-                _cc_contents_aspect,
-            ],
+            aspects = [_cc_contents_aspect],
         ),
+        "component_name": attr.string(
+            doc = "The name of the component",
+            mandatory = True,
+        ),
+        "manifest": attr.label(
+            doc = "The component's manifest file (.cmx)",
+            mandatory = True,
+            allow_single_file = True,
+        )
     },
+    provides = [PackageComponentInfo],
 )
 
-def packageable_cc_binary(name, target, **kwargs):
+def cc_binary_component(name, deps, component_name, manifest, **kwargs):
     packaged_name = name + "_packaged"
 
-    _packageable_cc_binary(
+    _cc_binary_component(
         name = packaged_name,
-        target = target,
+        deps = deps,
+        component_name = component_name,
+        manifest = manifest,
         **kwargs
     )
 
diff --git a/sdk/bazel/base/common/build_defs/package.bzl b/sdk/bazel/base/common/build_defs/package.bzl
index 9a5b65f..3358d4b 100644
--- a/sdk/bazel/base/common/build_defs/package.bzl
+++ b/sdk/bazel/base/common/build_defs/package.bzl
@@ -2,7 +2,9 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-load(":package_info.bzl", "PackageAggregateInfo", "PackageGeneratedInfo", "PackageLocalInfo", "get_aggregate_info")
+load(":package_info.bzl", "PackageAggregateInfo", "PackageComponentInfo",
+     "PackageGeneratedInfo", "PackageInfo", "PackageLocalInfo",
+     "get_aggregate_info")
 
 """
 Defines a Fuchsia package
@@ -19,38 +21,36 @@
 """
 
 # The attributes along which the aspect propagates.
-# The value denotes whether the attribute represents a list of target or a
-# single target.
-_ASPECT_ATTRIBUTES = {
-    "data": True,
-    "target": False,
-    "deps": True,
-    "srcs": True,
-}
+_ASPECT_ATTRIBUTES = [
+    "data",
+    "deps",
+    "srcs",
+]
 
 def _info_impl(target, context):
+    components = []
     mappings = []
+    if PackageComponentInfo in target:
+        info = target[PackageComponentInfo]
+        components += [(info.name, info.manifest)]
     if PackageLocalInfo in target:
-        mappings = target[PackageLocalInfo].mappings
-    elif PackageGeneratedInfo in target:
-        mappings = target[PackageGeneratedInfo].mappings
+        mappings += target[PackageLocalInfo].mappings
+    if PackageGeneratedInfo in target:
+        mappings += target[PackageGeneratedInfo].mappings
     deps = []
-    for attribute, is_list in _ASPECT_ATTRIBUTES.items():
+    for attribute in _ASPECT_ATTRIBUTES:
         if hasattr(context.rule.attr, attribute):
             value = getattr(context.rule.attr, attribute)
-            if is_list:
-                deps += value
-            else:
-                deps.append(value)
+            deps += value
     return [
-        get_aggregate_info(mappings, deps),
+        get_aggregate_info(components, mappings, deps),
     ]
 
 # An aspect which turns PackageLocalInfo providers into a PackageAggregateInfo
 # provider to identify all elements which need to be included in the package.
 _info_aspect = aspect(
     implementation = _info_impl,
-    attr_aspects = _ASPECT_ATTRIBUTES.keys(),
+    attr_aspects = _ASPECT_ATTRIBUTES,
     provides = [
         PackageAggregateInfo,
     ],
@@ -63,7 +63,7 @@
 
 def _fuchsia_package_impl(context):
     # List all the files that need to be included in the package.
-    info = get_aggregate_info([], context.attr.deps)
+    info = get_aggregate_info([], [], context.attr.deps)
     manifest_file_contents = ""
     package_contents = []
 
@@ -72,16 +72,24 @@
     manifest_file = context.actions.declare_file(base + "package_manifest")
 
     content = "#!/bin/bash\n"
-    for key, file in info.contents.to_list():
+    for dest, source in info.mappings.to_list():
         # Only add file to the manifest if not empty.
-        content += "if [[ -s %s ]]; then\n" % file.path
-        content += "  echo '%s=%s' >> %s\n" % (key, file.path, manifest_file.path)
+        content += "if [[ -s %s ]]; then\n" % source.path
+        content += "  echo '%s=%s' >> %s\n" % (dest, source.path,
+                                               manifest_file.path)
         content += "fi\n"
-        package_contents.append(file)
+        package_contents.append(source)
+
+    # Add cmx file for each component.
+    for name, cmx in info.components.to_list():
+        content += "echo 'meta/%s.cmx=%s' >> %s\n" % (name, cmx.path,
+                                                      manifest_file.path)
+        package_contents.append(cmx)
 
     # Add the meta/package file to the manifest.
     meta_package = context.actions.declare_file(base + "meta/package")
-    content += "echo 'meta/package=%s' >> %s\n" % (meta_package.path, manifest_file.path)
+    content += "echo 'meta/package=%s' >> %s\n" % (meta_package.path,
+                                                   manifest_file.path)
 
     # Write the manifest file.
     manifest_script = context.actions.declare_file(base + "package_manifest.sh")
@@ -188,7 +196,14 @@
     outs = [package_archive]
 
     return [
-        DefaultInfo(files = depset(outs), runfiles = context.runfiles(files = outs)),
+        DefaultInfo(
+            files = depset(outs),
+            runfiles = context.runfiles(files = outs),
+        ),
+        PackageInfo(
+            name = context.attr.name,
+            archive = package_archive,
+        ),
     ]
 
 fuchsia_package = rule(
@@ -208,4 +223,5 @@
             cfg = "host",
         ),
     },
+    provides = [PackageInfo],
 )
diff --git a/sdk/bazel/base/common/build_defs/package_info.bzl b/sdk/bazel/base/common/build_defs/package_info.bzl
index 1c34d05..2a62bfb 100644
--- a/sdk/bazel/base/common/build_defs/package_info.bzl
+++ b/sdk/bazel/base/common/build_defs/package_info.bzl
@@ -6,6 +6,15 @@
 Some utilities to declare and aggregate package contents.
 """
 
+# Identifies a component added to a package.
+PackageComponentInfo = provider(
+    fields = {
+        "name": "name of the component",
+        "manifest": "path to the component manifest file",
+    },
+)
+
+# Represents a set of files to be added to a package.
 PackageLocalInfo = provider(
     fields = {
         "mappings": "list of (package dest, source) pairs",
@@ -21,17 +30,31 @@
     },
 )
 
+# Aggregates the information provided by the above providers.
 PackageAggregateInfo = provider(
     fields = {
-        "contents": "depset of (package dest, source) pairs",
+        "components": "depset of (name, manifest) pairs",
+        "mappings": "depset of (package dest, source) pairs",
     },
 )
 
-def get_aggregate_info(mappings, deps):
-    transitive_info = []
+def get_aggregate_info(components, mappings, deps):
+    transitive_components = []
+    transitive_mappings = []
     for dep in deps:
         if PackageAggregateInfo not in dep:
             continue
-        transitive_info.append(dep[PackageAggregateInfo].contents)
-    return PackageAggregateInfo(contents = depset(mappings,
-                                                  transitive = transitive_info))
+        transitive_components.append(dep[PackageAggregateInfo].components)
+        transitive_mappings.append(dep[PackageAggregateInfo].mappings)
+    return PackageAggregateInfo(
+        components = depset(components, transitive = transitive_components),
+        mappings = depset(mappings, transitive = transitive_mappings),
+    )
+
+# Contains information about a built Fuchsia package.
+PackageInfo = provider(
+    fields = {
+        "name": "name of the package",
+        "archive": "archive file",
+    },
+)
diff --git a/sdk/bazel/base/dart/build_defs/dart_app.bzl b/sdk/bazel/base/dart/build_defs/dart_app.bzl
index f59848f..9c21b06 100644
--- a/sdk/bazel/base/dart/build_defs/dart_app.bzl
+++ b/sdk/bazel/base/dart/build_defs/dart_app.bzl
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 load(":dart.bzl", "COMMON_COMPILE_KERNEL_ACTION_ATTRS", "compile_kernel_action")
-load(":package_info.bzl", "PackageLocalInfo")
+load(":package_info.bzl", "PackageComponentInfo", "PackageLocalInfo")
 
 # A Fuchsia Dart application
 #
@@ -34,11 +34,14 @@
         main_dilp_file = context.outputs.main_dilp,
         dilp_list_file = context.outputs.dilp_list,
     )
-    mappings["meta/%s.cmx" % component_name] = context.files.component_manifest[0]
     outs = [kernel_snapshot_file, manifest_file]
     return [
         DefaultInfo(files = depset(outs), runfiles = context.runfiles(files = outs)),
         PackageLocalInfo(mappings = mappings.items()),
+        PackageComponentInfo(
+            name = component_name,
+            manifest = context.files.component_manifest[0],
+        ),
     ]
 
 dart_app = rule(
diff --git a/sdk/bazel/base/dart/build_defs/flutter_app.bzl b/sdk/bazel/base/dart/build_defs/flutter_app.bzl
index 148a1e5..9631f88 100644
--- a/sdk/bazel/base/dart/build_defs/flutter_app.bzl
+++ b/sdk/bazel/base/dart/build_defs/flutter_app.bzl
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 load(":dart.bzl", "COMMON_COMPILE_KERNEL_ACTION_ATTRS", "compile_kernel_action")
-load(":package_info.bzl", "PackageLocalInfo")
+load(":package_info.bzl", "PackageComponentInfo", "PackageLocalInfo")
 
 # A Fuchsia Flutter application
 #
@@ -34,7 +34,6 @@
         main_dilp_file = context.outputs.main_dilp,
         dilp_list_file = context.outputs.dilp_list,
     )
-    mappings["meta/%s.cmx" % component_name] = context.files.component_manifest[0]
 
     # Package the assets.
     data_root = "data/%s/" % component_name
@@ -58,6 +57,10 @@
     return [
         DefaultInfo(files = depset(outs), runfiles = context.runfiles(files = outs)),
         PackageLocalInfo(mappings = mappings.items()),
+        PackageComponentInfo(
+            name = component_name,
+            manifest = context.files.component_manifest[0],
+        ),
     ]
 
 flutter_app = rule(
diff --git a/sdk/bazel/tests/cc/cc/BUILD b/sdk/bazel/tests/cc/cc/BUILD
index 62df7a3..35c8341 100644
--- a/sdk/bazel/tests/cc/cc/BUILD
+++ b/sdk/bazel/tests/cc/cc/BUILD
@@ -3,7 +3,9 @@
 # found in the LICENSE file.
 
 load("@fuchsia_sdk//build_defs:package.bzl", "fuchsia_package")
-load("@fuchsia_sdk//build_defs:packageable_cc_binary.bzl", "packageable_cc_binary")
+load("@fuchsia_sdk//build_defs:cc_binary_component.bzl", "cc_binary_component")
+
+load("//build_defs:verify_package.bzl", "verify_package")
 
 # Vanilla C++ program.
 cc_binary(
@@ -49,9 +51,11 @@
 )
 
 # Prepare the binary for inclusion in a package.
-packageable_cc_binary(
+cc_binary_component(
     name = "packageable",
-    target = ":pkg_dep",
+    deps = [":pkg_dep"],
+    component_name = "packageable",
+    manifest = "manifest.cmx",
 )
 
 # C++ program in a Fuchsia package.
@@ -62,6 +66,19 @@
     ],
 )
 
+# Verify that the package contains all the expected files.
+verify_package(
+    name = "package_verify",
+    package = ":package",
+    files = [
+        "bin/pkg_dep",
+        "lib/ld.so.1",
+        "lib/libshared.so",
+        "lib/libsvc.so",
+        "meta/packageable.cmx",
+    ],
+)
+
 # Test the testonly attribute.
 
 cc_test(
@@ -78,9 +95,11 @@
     ],
 )
 
-packageable_cc_binary(
+cc_binary_component(
     name = "packageable_testonly",
-    target = ":pkg_dep_test",
+    deps = [":pkg_dep_test"],
+    component_name = "packageable_testonly",
+    manifest = "manifest.cmx",
     testonly = 1,
 )
 
@@ -91,3 +110,12 @@
     ],
     testonly = 1,
 )
+
+verify_package(
+    name = "package_test_verify",
+    package = ":package_test",
+    files = [
+        "bin/pkg_dep_test",
+    ],
+    testonly = 1,
+)
diff --git a/sdk/bazel/tests/cc/cc/manifest.cmx b/sdk/bazel/tests/cc/cc/manifest.cmx
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/sdk/bazel/tests/cc/cc/manifest.cmx
@@ -0,0 +1 @@
+{}
diff --git a/sdk/bazel/tests/common/build_defs/BUILD b/sdk/bazel/tests/common/build_defs/BUILD
new file mode 100644
index 0000000..f99e954
--- /dev/null
+++ b/sdk/bazel/tests/common/build_defs/BUILD
@@ -0,0 +1,16 @@
+# 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.
+
+licenses(["notice"])
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files(
+    glob(["*.bzl"]),
+)
+
+py_binary(
+    name = "package_verifier",
+    srcs = ["package_verifier.py"],
+)
diff --git a/sdk/bazel/tests/common/build_defs/package_verifier.py b/sdk/bazel/tests/common/build_defs/package_verifier.py
new file mode 100644
index 0000000..2b660f7
--- /dev/null
+++ b/sdk/bazel/tests/common/build_defs/package_verifier.py
@@ -0,0 +1,52 @@
+# 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 os
+import sys
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--meta',
+                        help='The path to the package\'s meta directory',
+                        required=True)
+    parser.add_argument('--files',
+                        help='The list of expected files in the package',
+                        default=[],
+                        nargs='*')
+    parser.add_argument('--stamp',
+                        help='The path to the stamp file in case of success',
+                        required=True)
+    args = parser.parse_args()
+
+    all_files = []
+    # List the files in the meta directory itself.
+    for root, dirs, files in os.walk(args.meta):
+        all_files += map(
+            lambda f: os.path.relpath(os.path.join(root, f), args.meta), files)
+    # Add the files outside of the meta directory, which are listed in
+    # meta/contents.
+    with open(os.path.join(args.meta, 'meta', 'contents')) as contents_file:
+        all_files += map(lambda l: l.strip().split('=', 1)[0],
+                         contents_file.readlines())
+
+    has_errors = False
+    for file in args.files:
+        if file not in all_files:
+            print('Missing ' + file)
+            has_errors = True
+    if has_errors:
+        print('Known files:')
+        print(all_files)
+        return 1
+
+    with open(args.stamp, 'w') as stamp_file:
+        stamp_file.write('Success!')
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/sdk/bazel/tests/common/build_defs/verify_package.bzl b/sdk/bazel/tests/common/build_defs/verify_package.bzl
new file mode 100644
index 0000000..f29d3b1
--- /dev/null
+++ b/sdk/bazel/tests/common/build_defs/verify_package.bzl
@@ -0,0 +1,94 @@
+# 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.
+
+load("@fuchsia_sdk//build_defs:package_info.bzl", "PackageInfo")
+
+def _verify_package_impl(context):
+    # Unpack the package archive.
+    archive = context.attr.package[PackageInfo].archive
+    archive_dir = context.actions.declare_directory(context.attr.name)
+    context.actions.run(
+        executable = context.executable._far,
+        arguments = [
+            "extract",
+            "--archive=" + archive.path,
+            "--output=" + archive_dir.path,
+        ],
+        inputs = [
+            archive,
+        ],
+        outputs = [
+            archive_dir,
+        ],
+        mnemonic = "UnpackArchive",
+    )
+
+    # Unpack the meta.far archive.
+    meta_dir = context.actions.declare_directory(context.attr.name + "_meta")
+    context.actions.run(
+        executable = context.executable._far,
+        arguments = [
+            "extract",
+            "--archive=" + archive_dir.path + "/meta.far",
+            "--output=" + meta_dir.path,
+        ],
+        inputs = [
+            archive_dir,
+        ],
+        outputs = [
+            meta_dir,
+        ],
+        mnemonic = "UnpackMeta",
+    )
+
+    # Read meta/contents and verify that it contains the expected files.
+    success_stamp = context.actions.declare_file(context.attr.name + "_success")
+    context.actions.run(
+        executable = context.executable._verifier,
+        arguments = [
+            "--meta",
+            meta_dir.path,
+            "--stamp",
+            success_stamp.path,
+            "--files",
+        ] + context.attr.files,
+        inputs = [
+            meta_dir,
+        ],
+        outputs = [
+            success_stamp,
+        ],
+    )
+    return [
+        DefaultInfo(files = depset([success_stamp])),
+    ]
+
+verify_package = rule(
+    implementation = _verify_package_impl,
+    attrs = {
+        "package": attr.label(
+            doc = "The label of the package to verify",
+            mandatory = True,
+            allow_files = False,
+            providers = [PackageInfo],
+        ),
+        "files": attr.string_list(
+            doc = "The files expected to exist in the package",
+            mandatory = False,
+            allow_empty = True,
+        ),
+        "_far": attr.label(
+            default = Label("@fuchsia_sdk//tools:far"),
+            allow_single_file = True,
+            executable = True,
+            cfg = "host",
+        ),
+        "_verifier": attr.label(
+            default = Label("//build_defs:package_verifier"),
+            allow_files = True,
+            executable = True,
+            cfg = "host",
+        ),
+    },
+)