Merge pull request #81 from reventlov/import_ir

Switch `emboss_cc_library` to use real Starlark rules.
diff --git a/build_defs.bzl b/build_defs.bzl
index fd16ebd..1dc3de9 100644
--- a/build_defs.bzl
+++ b/build_defs.bzl
@@ -16,10 +16,16 @@
 # vim:set ft=blazebuild:
 """Build defs for Emboss.
 
-This file exports the emboss_cc_library rule, which accepts an .emb file and
-produces a corresponding C++ library.
+This file exports emboss_library, which creates an Emboss library, and
+cc_emboss_library, which creates a header file and can be used as a dep in a
+`cc_library`, `cc_binary`, or `cc_test` rule.
+
+There is also a convenience macro, `emboss_cc_library()`, which creates an
+`emboss_library` and a `cc_emboss_library` based on it.
 """
 
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
+
 def emboss_cc_library(name, srcs, deps = [], visibility = None):
     """Constructs a C++ library from an .emb file."""
     if len(srcs) != 1:
@@ -28,47 +34,204 @@
             "srcs",
         )
 
-    native.filegroup(
-        # The original .emb file must be visible to any other emboss_cc_library
-        # that specifies this emboss_cc_library in its deps.  This rule makes the
-        # original .emb available to dependent rules.
-        # TODO(bolms): As an optimization, use the precompiled IR instead of
-        # reparsing the raw .embs.
-        name = name + "__emb",
+    emboss_library(
+        name = name + "_ir",
         srcs = srcs,
-        visibility = visibility,
+        deps = [dep + "_ir" for dep in deps],
     )
 
-    native.genrule(
-        # The generated header may be used in non-cc_library rules.
-        name = name + "_header",
-        tools = [
-            # TODO(bolms): Make "emboss" driver program.
-            "@com_google_emboss//compiler/front_end:emboss_front_end",
-            "@com_google_emboss//compiler/back_end/cpp:emboss_codegen_cpp",
-        ],
-        srcs = srcs + [dep + "__emb" for dep in deps],
-        cmd = ("$(location @com_google_emboss//compiler/front_end:emboss_front_end) " +
-               "--output-ir-to-stdout " +
-               "--import-dir=. " +
-               "--import-dir='$(GENDIR)' " +
-               "$(location {}) > $(@D)/$$(basename $(OUTS) .h).ir; " +
-               "$(location @com_google_emboss//compiler/back_end/cpp:emboss_codegen_cpp) " +
-               "< $(@D)/$$(basename $(OUTS) .h).ir > " +
-               "$(OUTS); " +
-               "rm $(@D)/$$(basename $(OUTS) .h).ir").format(") $location( ".join(srcs)),
-        outs = [src + ".h" for src in srcs],
-        # This rule should only be visible to the following rule.
-        visibility = ["//visibility:private"],
-    )
-
-    native.cc_library(
+    cc_emboss_library(
         name = name,
-        hdrs = [
-            ":" + name + "_header",
-        ],
-        deps = deps + [
-            "@com_google_emboss//runtime/cpp:cpp_utils",
-        ],
+        deps = [":" + name + "_ir"],
         visibility = visibility,
     )
+
+# Full Starlark rules for emboss_library and cc_emboss_library.
+#
+# This implementation is loosely based on the proto_library and
+# cc_proto_library rules that are included with Bazel.
+
+EmbossInfo = provider(
+    doc = "Encapsulates information provided by a `emboss_library.`",
+    fields = {
+        "direct_source": "(File) The `.emb` source files from the `srcs`" +
+                         " attribute.",
+        "transitive_sources": "(depset[File]) The `.emb` files from `srcs` " +
+                              "and all `deps`.",
+        "transitive_roots": "(list[str]) The root paths for all " +
+                            "transitive_sources.",
+        "direct_ir": "(list[File]) The `.emb.ir` files generated from the " +
+                     "`srcs`.",
+        "transitive_ir": "(depset[File]) The `.emb.ir` files generated from " +
+                         "transitive_srcs.",
+    },
+)
+
+def _emboss_library_impl(ctx):
+    deps = [dep[EmbossInfo] for dep in ctx.attr.deps]
+    outs = []
+    if len(ctx.attr.srcs) != 1:
+        fail("`srcs` attribute must contain exactly one label.", attr = "srcs")
+    src = ctx.files.srcs[0]
+    out = ctx.actions.declare_file(src.basename + ".ir", sibling = src)
+    outs.append(out)
+    inputs = depset(
+        direct = [src],
+        transitive = [dep.transitive_sources for dep in deps],
+    )
+    dep_roots = depset(
+        direct = [src.root],
+        transitive = [dep.transitive_roots for dep in deps],
+    )
+    imports = ["--import-dir=" + root.path for root in dep_roots.to_list()]
+    ctx.actions.run(
+        inputs = inputs.to_list(),
+        outputs = [out],
+        arguments = [src.path, "--output-file=" + out.path] + imports,
+        executable = ctx.executable._emboss_compiler,
+    )
+    transitive_sources = depset(
+        direct = [src],
+        transitive = [dep.transitive_sources for dep in deps],
+    )
+    transitive_ir = depset(
+        direct = outs,
+        transitive = [dep.transitive_ir for dep in deps],
+    )
+    transitive_roots = depset(
+        direct = [src.root],
+        transitive = [dep.transitive_roots for dep in deps],
+    )
+    return [
+        EmbossInfo(
+            direct_source = src,
+            transitive_sources = transitive_sources,
+            transitive_roots = transitive_roots,
+            direct_ir = outs,
+            transitive_ir = transitive_ir,
+        ),
+        DefaultInfo(
+            files = depset(outs),
+        ),
+    ]
+
+emboss_library = rule(
+    _emboss_library_impl,
+    attrs = {
+        "srcs": attr.label_list(
+            allow_files = [".emb"],
+        ),
+        "deps": attr.label_list(
+            providers = [EmbossInfo],
+        ),
+        "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(),
+        "_emboss_compiler": attr.label(
+            executable = True,
+            cfg = "exec",
+            allow_files = True,
+            default = Label(
+                "@com_google_emboss//compiler/front_end:emboss_front_end",
+            ),
+        ),
+    },
+    provides = [EmbossInfo],
+)
+
+EmbossCcHeaderInfo = provider(
+    fields = {
+        "headers": "(list[File]) The `.emb.h` headers from this rule.",
+        "transitive_headers": "(list[File]) The `.emb.h` headers from this " +
+                              "rule and all dependencies.",
+    },
+    doc = "Provide cc emboss headers.",
+)
+
+def _cc_emboss_aspect_impl(target, ctx):
+    cc_toolchain = find_cpp_toolchain(ctx, mandatory = True)
+    emboss_cc_compiler = ctx.executable._emboss_cc_compiler
+    emboss_info = target[EmbossInfo]
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+        requested_features = list(ctx.features),
+        unsupported_features = list(ctx.disabled_features),
+    )
+    src = target[EmbossInfo].direct_source
+    headers = [ ctx.actions.declare_file( src.basename + ".h", sibling = src) ]
+    args = ctx.actions.args()
+    args.add("--input-file")
+    args.add_all(emboss_info.direct_ir)
+    args.add("--output-file")
+    args.add_all(headers)
+    ctx.actions.run(
+        executable = emboss_cc_compiler,
+        arguments = [args],
+        inputs = emboss_info.direct_ir,
+        outputs = headers,
+    )
+    runtime_cc_info = ctx.attr._emboss_cc_runtime[CcInfo]
+    transitive_headers = depset(
+        direct = headers,
+        transitive = [
+                         dep[EmbossCcHeaderInfo].transitive_headers
+                         for dep in ctx.rule.attr.deps
+                     ] +
+                     [runtime_cc_info.compilation_context.headers],
+    )
+    (cc_compilation_context, cc_compilation_outputs) = cc_common.compile(
+        name = ctx.label.name,
+        actions = ctx.actions,
+        feature_configuration = feature_configuration,
+        cc_toolchain = cc_toolchain,
+        public_hdrs = headers,
+        private_hdrs = transitive_headers.to_list(),
+    )
+    return [
+        CcInfo(compilation_context = cc_compilation_context),
+        EmbossCcHeaderInfo(
+            headers = depset(headers),
+            transitive_headers = transitive_headers,
+        ),
+    ]
+
+_cc_emboss_aspect = aspect(
+    implementation = _cc_emboss_aspect_impl,
+    attr_aspects = ["deps"],
+    fragments = ["cpp"],
+    required_providers = [EmbossInfo],
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = "@bazel_tools//tools/cpp:current_cc_toolchain",
+        ),
+        "_emboss_cc_compiler": attr.label(
+            executable = True,
+            cfg = "exec",
+            default = "@com_google_emboss//compiler/back_end/cpp:emboss_codegen_cpp",
+        ),
+        "_emboss_cc_runtime": attr.label(
+            default = "@com_google_emboss//runtime/cpp:cpp_utils",
+        ),
+    },
+)
+
+def _cc_emboss_library_impl(ctx):
+    if len(ctx.attr.deps) != 1:
+        fail("`deps` attribute must contain exactly one label.", attr = "deps")
+    dep = ctx.attr.deps[0]
+    return [
+        dep[CcInfo],
+        dep[EmbossInfo],
+        DefaultInfo(files = dep[EmbossCcHeaderInfo].headers),
+    ]
+
+cc_emboss_library = rule(
+    implementation = _cc_emboss_library_impl,
+    attrs = {
+        "deps": attr.label_list(
+            aspects = [_cc_emboss_aspect],
+            allow_rules = ["emboss_library"],
+            allow_files = False,
+        ),
+    },
+    provides = [CcInfo, EmbossInfo],
+)
diff --git a/compiler/back_end/cpp/BUILD b/compiler/back_end/cpp/BUILD
index fe532a1..c9e879c 100644
--- a/compiler/back_end/cpp/BUILD
+++ b/compiler/back_end/cpp/BUILD
@@ -126,6 +126,17 @@
 )
 
 emboss_cc_test(
+    name = "importer2_test",
+    srcs = [
+        "testcode/importer2_test.cc",
+    ],
+    deps = [
+        "//testdata:importer2_emboss",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+emboss_cc_test(
     name = "uint_sizes_test",
     srcs = [
         "testcode/uint_sizes_test.cc",
diff --git a/compiler/back_end/cpp/emboss_codegen_cpp.py b/compiler/back_end/cpp/emboss_codegen_cpp.py
index b1d0f72..73a1450 100644
--- a/compiler/back_end/cpp/emboss_codegen_cpp.py
+++ b/compiler/back_end/cpp/emboss_codegen_cpp.py
@@ -20,18 +20,41 @@
 
 from __future__ import print_function
 
+import argparse
 import sys
 
 from compiler.back_end.cpp import header_generator
 from compiler.util import ir_pb2
 
 
-def main(argv):
-  del argv  # Unused.
-  ir = ir_pb2.EmbossIr.from_json(sys.stdin.read())
-  print(header_generator.generate_header(ir))
+def _parse_command_line(argv):
+  """Parses the given command-line arguments."""
+  parser = argparse.ArgumentParser(description="Emboss compiler C++ back end.",
+                                   prog=argv[0])
+  parser.add_argument("--input-file",
+                      type=str,
+                      help=".emb.ir file to compile.")
+  parser.add_argument("--output-file",
+                      type=str,
+                      help="Write header to file.  If not specified, write " +
+                           "header to stdout.")
+  return parser.parse_args(argv[1:])
+
+
+def main(flags):
+  if flags.input_file:
+    with open(flags.input_file) as f:
+      ir = ir_pb2.EmbossIr.from_json(f.read())
+  else:
+    ir = ir_pb2.EmbossIr.from_json(sys.stdin.read())
+  header = header_generator.generate_header(ir)
+  if flags.output_file:
+    with open(flags.output_file, "w") as f:
+      f.write(header)
+  else:
+    print(header)
   return 0
 
 
-if __name__ == '__main__':
-  sys.exit(main(sys.argv))
+if __name__ == "__main__":
+  sys.exit(main(_parse_command_line(sys.argv)))
diff --git a/compiler/back_end/cpp/testcode/importer2_test.cc b/compiler/back_end/cpp/testcode/importer2_test.cc
new file mode 100644
index 0000000..15a24aa
--- /dev/null
+++ b/compiler/back_end/cpp/testcode/importer2_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+//     https://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.
+
+// Tests for using types that are imported from imports.
+
+#include <stdint.h>
+
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "testdata/importer2.emb.h"
+
+namespace emboss {
+namespace test {
+namespace {
+
+const ::std::uint8_t kOuter2[16] = {
+    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,  // inner
+    0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,  // inner_gen
+};
+
+TEST(Importer, CanAccessInner) {
+  auto view = Outer2View(kOuter2, sizeof kOuter2);
+  EXPECT_EQ(0x0807060504030201UL, view.outer().inner().value().Read());
+  EXPECT_EQ(0x100f0e0d0c0b0a09UL, view.outer().inner_gen().value().Read());
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace emboss
diff --git a/compiler/front_end/emboss_front_end.py b/compiler/front_end/emboss_front_end.py
index aa74eb7..ed435c2 100644
--- a/compiler/front_end/emboss_front_end.py
+++ b/compiler/front_end/emboss_front_end.py
@@ -64,6 +64,9 @@
   parser.add_argument("--output-ir-to-stdout",
                       action="store_true",
                       help="Dump serialized IR to stdout.")
+  parser.add_argument("--output-file",
+                      type=str,
+                      help="Write serialized IR to file.")
   parser.add_argument("--no-debug-show-header-lines",
                       dest="debug_show_header_lines",
                       action="store_false",
@@ -159,6 +162,9 @@
         set(module_ir.PRODUCTIONS) - main_module_debug_info.used_productions))
   if flags.output_ir_to_stdout:
     print(ir.to_json())
+  if flags.output_file:
+    with open(flags.output_file, "w") as f:
+      f.write(ir.to_json())
   return 0
 
 
diff --git a/testdata/BUILD b/testdata/BUILD
index 9c68b22..083298c 100644
--- a/testdata/BUILD
+++ b/testdata/BUILD
@@ -14,7 +14,11 @@
 
 # Shared test data for Emboss.
 
-load("//:build_defs.bzl", "emboss_cc_library")
+load("//:build_defs.bzl",
+     "emboss_cc_library",
+     "emboss_library",
+     "cc_emboss_library"
+)
 
 package(
     default_visibility = ["//:__subpackages__"],
@@ -67,13 +71,20 @@
     srcs = glob(["format/**"]),
 )
 
-emboss_cc_library(
-    name = "span_se_log_file_status_emboss",
+emboss_library(
+    name = "span_se_log_file_status_emb_ir",
     srcs = [
         "golden/span_se_log_file_status.emb",
     ],
 )
 
+cc_emboss_library(
+    name = "span_se_log_file_status_emboss",
+    deps = [
+        ":span_se_log_file_status_emb_ir",
+    ],
+)
+
 emboss_cc_library(
     name = "nested_structure_emboss",
     srcs = [
@@ -147,6 +158,16 @@
 )
 
 emboss_cc_library(
+    name = "importer2_emboss",
+    srcs = [
+        "importer2.emb",
+    ],
+    deps = [
+        ":importer_emboss",
+    ],
+)
+
+emboss_cc_library(
     name = "float_emboss",
     srcs = [
         "float.emb",
diff --git a/testdata/importer2.emb b/testdata/importer2.emb
new file mode 100644
index 0000000..0732c22
--- /dev/null
+++ b/testdata/importer2.emb
@@ -0,0 +1,30 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+#     https://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.
+
+# Test .emb to ensure that the import system works.
+#
+# The file imported_genfiles.emb is identical to imported.emb except for the
+# [(cpp) namespace] attribute; it is used to ensure that generated .embs can be
+# used by the emboss_cc_library build rule.
+
+# These imports intentionally use names that do not match the file names, as a
+# test that the file names aren't being used.
+
+import "testdata/importer.emb" as imp
+
+[(cpp) namespace: "emboss::test"]
+
+
+struct Outer2:
+  0 [+16]  imp.Outer      outer