ctexplain: first functional check-in

https://github.com/bazelbuild/bazel/pull/11511 set up basic project structure.  This PR adds minimum working functionality.

Specifically, you can run it with a build command and it reports basic stats on the build's graph.

Example:

```
$ bazel-bin/tools/ctexplain/ctexplain -b "//testapp:foo"
Collecting configured targets for //testapp:foo... done in 0.62 s.

Configurations: 3
Targets: 79
Configured targets: 92 (+16.5% vs. targets)
Targets with multiple configs: 13
```

Notes:
* Changed import structure to prefer module imports over function, class imports (style guide recommendation)
* Set up structure for injecting arbitrary analyses. Each analysis consumes the build's set of configured targets and can output whatever it wants.
* Implemented one basic analysis
* Structured code to make it easy to fork output formatters (e.g. for machine-readable output). But tried not to add speculative inheritance / boilerplate too soon

Context: [Measuring Configuration Overhead](https://docs.google.com/document/d/10ZxO2wZdKJATnYBqAm22xT1k5r4Vp6QX96TkqSUIhs0/edit).
Work towards #10613

Closes #11829.

PiperOrigin-RevId: 328325094
diff --git a/tools/ctexplain/lib.py b/tools/ctexplain/lib.py
new file mode 100644
index 0000000..1ddb8c9
--- /dev/null
+++ b/tools/ctexplain/lib.py
@@ -0,0 +1,57 @@
+# Lint as: python3
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# 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.
+"""General-purpose business logic."""
+from typing import Tuple
+
+# Do not edit this line. Copybara replaces it with PY2 migration helper..third_party.bazel.tools.ctexplain.bazel_api as bazel_api
+from tools.ctexplain.types import ConfiguredTarget
+
+
+def analyze_build(bazel: bazel_api.BazelApi, labels: Tuple[str, ...],
+                  build_flags: Tuple[str, ...]) -> Tuple[ConfiguredTarget, ...]:
+  """Gets a build invocation's configured targets.
+
+  Args:
+    bazel: API for invoking Bazel.
+    labels: The targets to build.
+    build_flags: The build flags to use.
+
+  Returns:
+    Configured targets representing the build.
+
+  Raises:
+    RuntimeError: On any invocation errors.
+  """
+  cquery_args = [f'deps({",".join(labels)})']
+  cquery_args.extend(build_flags)
+  (success, stderr, cts) = bazel.cquery(cquery_args)
+  if not success:
+    raise RuntimeError("invocation failed: " + stderr.decode("utf-8"))
+
+  # We have to do separate calls to "bazel config" to get the actual configs
+  # from their hashes.
+  hashes_to_configs = {}
+  cts_with_configs = []
+  for ct in cts:
+    # Don't use dict.setdefault because that unconditionally calls get_config
+    # as one of its parameters and that's an expensive operation to waste.
+    if ct.config_hash not in hashes_to_configs:
+      hashes_to_configs[ct.config_hash] = bazel.get_config(ct.config_hash)
+    config = hashes_to_configs[ct.config_hash]
+    cts_with_configs.append(
+        ConfiguredTarget(ct.label, config, ct.config_hash,
+                         ct.transitive_fragments))
+
+  return tuple(cts_with_configs)