pw_env_setup: Adds CIPD Bazel rules

Adds a set of Bazel rules for fetching the cipd client and downloading
CIPD based dependencies as Bazel remote repositories.

Change-Id: Id15641be7dcac33ddd4bf17f807d8b7f197078ac
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/45140
Reviewed-by: Rob Mohr <mohrr@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
diff --git a/pw_env_setup/BUILD b/pw_env_setup/BUILD
new file mode 100644
index 0000000..32015da
--- /dev/null
+++ b/pw_env_setup/BUILD
@@ -0,0 +1,18 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+
+exports_files([
+    "py/pw_env_setup/cipd_setup/.cipd_version",
+    "py/pw_env_setup/cipd_setup/.cipd_version.digests",
+])
diff --git a/pw_env_setup/bazel/cipd_setup/BUILD b/pw_env_setup/bazel/cipd_setup/BUILD
new file mode 100644
index 0000000..e3edb0b
--- /dev/null
+++ b/pw_env_setup/bazel/cipd_setup/BUILD
@@ -0,0 +1,16 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+
+# This file is intentionally left empty, but it is required to mark this
+# directory as a package for 'cipd_rules.bzl'.
diff --git a/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
new file mode 100644
index 0000000..65c2a90
--- /dev/null
+++ b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
@@ -0,0 +1,73 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+
+load(
+    "//pw_env_setup/bazel/cipd_setup/internal:cipd_internal.bzl",
+    _cipd_client_impl = "cipd_client_impl",
+    _cipd_repository_impl = "cipd_repository_impl",
+)
+
+_cipd_client_repository = repository_rule(
+    _cipd_client_impl,
+    attrs = {
+        "_cipd_version_file": attr.label(default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/.cipd_version"),
+        "_cipd_digest_file": attr.label(default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/.cipd_version.digests"),
+    },
+    doc = """
+Fetches the cipd client.
+
+This rule should not be used directly and instead should be called via
+the cipd_client_repository macro.
+""",
+)
+
+def cipd_client_repository():
+    """Fetches the cipd client.
+
+    Fetches the cipd client to the prescribed remote repository target
+    prefix 'cipd_client'. This rule should be called before a
+    cipd_repository rule is instantiated.
+    """
+    _cipd_client_repository(
+        name = "cipd_client",
+    )
+
+cipd_repository = repository_rule(
+    _cipd_repository_impl,
+    attrs = {
+        "_cipd_client": attr.label(default = "@cipd_client//:cipd"),
+        "path": attr.string(),
+        "tag": attr.string(),
+    },
+    doc = """
+Downloads a singular CIPD dependency to the root of a remote repository.
+
+Example:
+
+    load(
+        "//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl",
+        "cipd_client_repository",
+        "cipd_repository",
+    )
+
+    # Must be called before cipd_repository
+    cipd_client_repository()
+
+    cipd_repository(
+        name = "bloaty",
+        path = "pigweed/third_party/bloaty-embedded/${os=linux,mac}-${arch=amd64}",
+        tag = "git_revision:2d87d204057b419f5290f8d38b61b9c2c5b4fb52-2",
+    )
+""",
+)
diff --git a/pw_env_setup/bazel/cipd_setup/ensure.tpl b/pw_env_setup/bazel/cipd_setup/ensure.tpl
new file mode 100644
index 0000000..07e3fd1
--- /dev/null
+++ b/pw_env_setup/bazel/cipd_setup/ensure.tpl
@@ -0,0 +1,18 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+$VerifiedPlatform linux-amd64
+$VerifiedPlatform mac-amd64
+$ParanoidMode CheckPresence
+@Subdir
+%{path} %{tag}
\ No newline at end of file
diff --git a/pw_env_setup/bazel/cipd_setup/internal/BUILD b/pw_env_setup/bazel/cipd_setup/internal/BUILD
new file mode 100644
index 0000000..d3c52e1
--- /dev/null
+++ b/pw_env_setup/bazel/cipd_setup/internal/BUILD
@@ -0,0 +1,16 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+
+# This file is intentionally left empty to mark this as a package for
+# cipd_internal.bzl
diff --git a/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
new file mode 100644
index 0000000..c328bb3
--- /dev/null
+++ b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
@@ -0,0 +1,123 @@
+# Copyright 2021 The Pigweed 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
+#
+#     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.
+
+_CIPD_HOST = "https://chrome-infra-packages.appspot.com"
+
+def platform_normalized(rctx):
+    """Normalizes the platform to match CIPDs naming system.
+
+    Args:
+        rctx: Repository context.
+
+    Returns:
+        str: Normalized string.
+    """
+
+    # Chained if else used because Bazel's rctx.os.name is not stable
+    # between different versions of windows i.e. windows 10 vs windows
+    # server.
+    if "windows" in rctx.os.name:
+        return "windows"
+    elif "linux" == rctx.os.name:
+        return "linux"
+    elif "mac os x" == rctx.os.name:
+        return "mac"
+    else:
+        fail("Could not normalize os:", rctx.os.name)
+
+def arch_normalized(rctx):
+    """Normalizes the architecture string to match CIPDs naming system.
+
+    Args:
+        rctx: Repository context.
+
+    Returns:
+        str: Normalized architecture.
+    """
+
+    # TODO(pwbug/388): Find a way to get host architecture information from a
+    # repository context.
+    return "amd64"
+
+def get_client_cipd_version(rctx):
+    """Gets the CIPD client version from the config file.
+
+    Args:
+        rctx: Repository context.
+
+    Returns:
+        str: The CIPD client version tag to use.
+    """
+    return rctx.read(rctx.attr._cipd_version_file).strip()
+
+def _platform(rctx):
+    return "{}-{}".format(platform_normalized(rctx), arch_normalized(rctx))
+
+def get_client_cipd_digest(rctx):
+    """Gets the CIPD client digest from the digest file.
+
+    Args:
+        rctx: Repository context.
+
+    Returns:
+        str: The CIPD client digest.
+    """
+    platform = _platform(rctx)
+    digest_file = rctx.read(rctx.attr._cipd_digest_file)
+    digest_lines = [
+        digest
+        for digest in digest_file.splitlines()
+        # Remove comments from version file
+        if not digest.startswith("#") and digest
+    ]
+
+    for line in digest_lines:
+        (digest_platform, digest_type, digest) = \
+            [element for element in line.split(" ") if element]
+        if digest_platform == platform:
+            if digest_type != "sha256":
+                fail("Bazel only supports sha256 type digests.")
+            return digest
+    fail("Could not find CIPD digest that matches this platform.")
+
+def cipd_client_impl(rctx):
+    platform = _platform(rctx)
+    path = "/client?platform={}&version={}".format(
+        platform,
+        get_client_cipd_version(rctx),
+    )
+    rctx.download(
+        output = "cipd",
+        url = _CIPD_HOST + path,
+        sha256 = get_client_cipd_digest(rctx),
+        executable = True,
+    )
+    rctx.file("BUILD", "exports_files([\"cipd\"])")
+
+def cipd_repository_base(rctx):
+    cipd_path = rctx.path(rctx.attr._cipd_client).basename
+    ensure_path = rctx.name + ".ensure"
+    rctx.template(
+        ensure_path,
+        Label("@pigweed//pw_env_setup/bazel/cipd_setup:ensure.tpl"),
+        {
+            "%{path}": rctx.attr.path,
+            "%{tag}": rctx.attr.tag,
+        },
+    )
+    rctx.execute([cipd_path, "ensure", "-root", ".", "-ensure-file", ensure_path])
+
+def cipd_repository_impl(rctx):
+    cipd_repository_base(rctx)
+    rctx.file("BUILD", "exports_files(glob([\"**/*\"]))")
diff --git a/pw_env_setup/docs.rst b/pw_env_setup/docs.rst
index a3216d2..b392a0c 100644
--- a/pw_env_setup/docs.rst
+++ b/pw_env_setup/docs.rst
@@ -91,6 +91,38 @@
   pw_bootstrap --args...  # See below for details about args.
   pw_finalize bootstrap "$SETUP_SH"
 
+
+Bazel Usage
+-----------
+It is possible to pull in a CIPD dependency into Bazel using WORKSPACE rules
+rather than using `bootstrap.sh`. e.g.
+
+.. code:: python
+
+  # WORKSPACE
+
+  load(
+      "@pigweed//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl",
+      "cipd_client_repository",
+      "cipd_repository",
+  )
+
+  # Must be called before cipd_repository
+  cipd_client_repository()
+
+  cipd_repository(
+      name = "bloaty",
+      path = "pigweed/third_party/bloaty-embedded/${os=linux,mac}-${arch=amd64}",
+      tag = "git_revision:2d87d204057b419f5290f8d38b61b9c2c5b4fb52-2",
+  )
+
+From here it is possible to get access to the Bloaty binaries using the
+following command.
+
+.. code:: sh
+
+  bazel run @bloaty//:bloaty -- --help
+
 User-Friendliness
 -----------------