blob: 3858b6c77fd8c7887f0bd99b8b081fcef4f34717 [file] [log] [blame] [edit]
"""This module defines the cipd module extension.
This extension hermetically installs a CIPD package as an external Bazel
repository.
Use `download_http` to download via CIPD's HTTP endpoint. This reduces overhead
when authentication is not required. Use download_cipd when authentication is
required, eg. internal packages.
Files in the CIPD package can be added as dependencies to other Bazel targets in two ways: either
individually via a label such as "@my_cipd_pkg//:path/to/file", or by adding
"@my_cipd_pkg//:all_files" as a dependency, which is a filegroup that includes the entire contents
of the CIPD package. The contents of the generated BUILD.bazel file which facilitates this are
configurable, e.g. multiple smaller packages.
Note: Any files with spaces in their names cannot be used by Bazel and are thus excluded
from the generated Bazel rules.
If a Bazel target adds a CIPD package as a dependency, its contents will appear under the runfiles
directory. Example:
```
# MODULE.bazel
cipd.download_http(
name = "git_linux-amd64",
cipd_package = "infra/3pp/tools/git/linux-amd64",
sha256 = "36cb96051827d6a3f6f59c5461996fe9490d997bcd2b351687d87dcd4a9b40fa",
tag = "version:2.29.2.chromium.6",
)
# BUILD.bazel
go_library(
name = "git_util.go",
srcs = ["git_util.go"],
data = ["@git_linux-amd64//:all_files"],
...
)
# git_util.go
import (
"path/filepath"
"go.skia.org/infra/bazel/go/bazel"
)
func FindGitBinary() string {
return filepath.Join(bazel.RunfilesDir(), "+cipd+git_amd64_linux/bin/git")
}
```
Note that runfile generation is disabled on Windows by default, and must be enabled with
--enable_runfiles[2] for the above mechanism to work.
[1] https://bazel.build/docs/configurable-attributes
[2] https://bazel.build/reference/command-line-reference#flag--enable_runfiles
"""
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
def _fail_if_nonzero_status(exec_result, msg):
if exec_result.return_code != 0:
fail("%s\nExit code: %d\nStdout:\n%s\nStderr:\n%s\n" % (
msg,
exec_result.return_code,
exec_result.stdout,
exec_result.stderr,
))
def _postinstall_script(repository_ctx, script_name, script_content):
repository_ctx.report_progress("Executing postinstall script...")
repository_ctx.file(
script_name,
content = script_content,
executable = True,
)
exec_result = repository_ctx.execute(
[repository_ctx.path(script_name)],
quiet = repository_ctx.attr.quiet,
)
_fail_if_nonzero_status(exec_result, "Failed to run postinstall script.")
repository_ctx.delete(repository_ctx.path(script_name))
_DEFAULT_BUILD_FILE_CONTENT = """
# To add a specific file inside this CIPD package as a dependency, use a label such as
# @my_cipd_pkg//:path/to/file.
# The exclude pattern prevents files with spaces in their names from tripping up Bazel.
exports_files(glob(include=["**/*"], exclude=["**/* *"]))
# Convenience filegroup to add all files in this CIPD package as dependencies.
filegroup(
name = "all_files",
# The exclude pattern prevents files with spaces in their names from tripping up Bazel.
srcs = glob(include=["**/*"], exclude=["**/* *"]),
visibility = ["//visibility:public"],
)
"""
_CIPD_CLIENT_PLATFORM_TO_DIGEST = {
"linux-amd64": "1b84a075031772cf05b1a3e0ef2119e96d0492869f971ca72bfdb240ed7cbc45",
"linux-arm64": "ee6327a8ab86646a664b140d0861147da7d256cbe57df7fa111d1953ccf7b6b4",
"windows-amd64": "e4567b664ee67eb198fbf44ad10801c57b726dd683da3d37e16b90d403cb00a3",
"windows-arm64": "ae7068c429aec3e36ebafcb58c5db878af866994c8a9c56b9dbb8e27445c9c7e",
"mac-amd64": "6bced14c6896592acd2ca5a0675f4523cc4d0f0ec8152df589c7825e5d3a477e",
"mac-arm64": "96635d202dd2427b1121fca3164abb90cfad91cfae7e114c33dda0e0057b843d",
}
_CIPD_CLIENT_VERSION = "git_revision:32d4a01e34b863fa38d75af646fb7e4492218f08"
def _cipd_install_impl(repository_ctx):
os = "linux"
is_windows = False
is_posix = True
if "windows" in repository_ctx.os.name.lower():
is_windows = True
is_posix = False
os = "windows"
elif "mac" in repository_ctx.os.name.lower():
os = "mac"
# Download the CIPD client.
platform = "%s-%s" % (os, repository_ctx.os.arch)
cipd_digest = _CIPD_CLIENT_PLATFORM_TO_DIGEST[platform]
repository_ctx.download_and_extract(
url = _cipd_http_urls("infra/tools/cipd/%s" % platform, _CIPD_CLIENT_VERSION, cipd_digest),
sha256 = cipd_digest,
type = "zip",
)
# Initialize the CIPD root.
cipd_client = repository_ctx.path("cipd.exe" if is_windows else "cipd")
exec_result = repository_ctx.execute(
[cipd_client, "init", "--force"],
quiet = repository_ctx.attr.quiet,
)
_fail_if_nonzero_status(exec_result, "Failed to initialize CIPD root.")
# Install the CIPD package.
repository_ctx.report_progress("Installing CIPD package...")
exec_result = repository_ctx.execute(
[
cipd_client,
"install",
repository_ctx.attr.package,
repository_ctx.attr.version,
"-root",
".",
"-log-level=debug",
],
quiet = repository_ctx.attr.quiet,
)
_fail_if_nonzero_status(exec_result, "Failed to fetch CIPD package.")
# Generate BUILD.bazel file.
build_file_content = repository_ctx.attr.build_file_content
if not build_file_content:
build_file_content = _DEFAULT_BUILD_FILE_CONTENT
repository_ctx.file("BUILD.bazel", content = build_file_content)
# Optionally run the postinstall script if one was given.
if is_posix and repository_ctx.attr.postinstall_cmds_posix:
_postinstall_script(
repository_ctx,
"postinstall.sh",
"\n".join(repository_ctx.attr.postinstall_cmds_posix),
)
if is_windows and repository_ctx.attr.postinstall_cmds_win:
_postinstall_script(
repository_ctx,
# The .bat extension is needed under Windows, or the OS won't execute the script.
"postinstall.bat",
"\n".join(repository_ctx.attr.postinstall_cmds_win),
)
_cipd_install = repository_rule(
implementation = _cipd_install_impl,
attrs = {
"package": attr.string(
doc = """CIPD package name, e.g. "infra/3pp/tools/git/linux-amd64".""",
mandatory = True,
),
"version": attr.string(
doc = """CIPD package version, e.g. "version:2.29.2.chromium.6".""",
mandatory = True,
),
"sha256": attr.string(
doc = "sha256 digest of the package.",
mandatory = True,
),
"build_file_content": attr.string(
doc = """If set, will be used as the content of the BUILD.bazel file. Otherwise, a
default BUILD.bazel file will be created with an all_files target.""",
),
"postinstall_cmds_posix": attr.string_list(
doc = """Post-install commands to execute. Ignored if Bazel is running on a
non-POSIX OS. Optional.""",
),
"postinstall_cmds_win": attr.string_list(
doc = """Post-install commands to execute. Ignored if Bazel is not running on
Windows. Optional.""",
),
"quiet": attr.bool(
default = True,
doc = "Whether stdout and stderr should be printed to the terminal for debugging.",
),
},
doc = "Hermetically installs a CIPD package as an external Bazel repository.",
)
# Contents of a BUILD file which export all files in all subdirectories.
_all_cipd_files = """
# To add a specific file inside this CIPD package as a dependency, use a label such as
# @my_cipd_pkg//:path/to/file.
# The exclude pattern prevents files with spaces in their names from tripping up Bazel.
exports_files(glob(include=["**/*"], exclude=["**/* *"]))
# Convenience filegroup to add all files in this CIPD package as dependencies.
filegroup(
name = "all_files",
# The exclude pattern prevents files with spaces in their names from tripping up Bazel.
srcs = glob(include=["**/*"], exclude=["**/* *"]),
visibility = ["//visibility:public"],
)
"""
_export_single_file = """
exports_files(
["%s"],
visibility = ["//visibility:public"]
)
"""
_common_attrs = {
"name": attr.string(),
"cipd_package": attr.string(),
"build_file_content": attr.string(),
"export_single_file": attr.string(),
"postinstall_cmds_posix": attr.string_list(),
"postinstall_cmds_win": attr.string_list(),
"sha256": attr.string(),
"platform_to_sha256": attr.string_dict(),
"tag": attr.string(),
}
_download_http = tag_class(attrs = _common_attrs)
_download_cipd = tag_class(attrs = _common_attrs)
def _get_build_file_content(build_file_content, export_single_file):
if build_file_content:
return build_file_content
elif export_single_file:
return _export_single_file % export_single_file
return _all_cipd_files
def _cipd_http_urls(package, tag, sha256):
cipd_url = "https://chrome-infra-packages.appspot.com/dl/{package}/+/{tag}".format(
package = package,
tag = tag,
)
mirror_url = "https://cdn.skia.org/bazel/{sha256}.zip".format(sha256 = sha256)
return [cipd_url, mirror_url]
def _download_package_http(name, cipd_package, tag, sha256, build_file_content = None, export_single_file = None, postinstall_cmds_posix = None, postinstall_cmds_win = None):
urls = _cipd_http_urls(cipd_package, tag, sha256)
http_archive(
name = name,
build_file_content = _get_build_file_content(build_file_content, export_single_file),
sha256 = sha256,
urls = urls,
patch_cmds = postinstall_cmds_posix,
patch_cmds_win = postinstall_cmds_win,
type = "zip",
)
def _cipd_impl(ctx):
direct_deps = []
for mod in ctx.modules:
for package in mod.tags.download_http:
if package.platform_to_sha256:
for platform, sha256 in package.platform_to_sha256.items():
name = package.name + "_" + platform
_download_package_http(
name = name,
cipd_package = package.cipd_package.replace("${platform}", platform),
tag = package.tag,
build_file_content = package.build_file_content,
export_single_file = package.export_single_file,
postinstall_cmds_posix = package.postinstall_cmds_posix,
postinstall_cmds_win = package.postinstall_cmds_win,
sha256 = sha256,
)
direct_deps.append(name)
else:
_download_package_http(
name = package.name,
cipd_package = package.cipd_package,
tag = package.tag,
build_file_content = package.build_file_content,
export_single_file = package.export_single_file,
postinstall_cmds_posix = package.postinstall_cmds_posix,
postinstall_cmds_win = package.postinstall_cmds_win,
sha256 = package.sha256,
)
direct_deps.append(package.name)
for package in mod.tags.download_cipd:
if package.platform_to_sha256:
for platform, sha256 in package.platform_to_sha256.items():
name = package.name + "_" + platform
_cipd_install(
name = name,
package = package.cipd_package.replace("${platform}", platform),
version = package.tag,
build_file_content = _get_build_file_content(package.build_file_content, package.export_single_file),
postinstall_cmds_posix = package.postinstall_cmds_posix,
postinstall_cmds_win = package.postinstall_cmds_win,
sha256 = sha256,
)
direct_deps.append(name)
else:
_cipd_install(
name = package.name,
package = package.cipd_package,
version = package.tag,
build_file_content = _get_build_file_content(package.build_file_content, package.export_single_file),
postinstall_cmds_posix = package.postinstall_cmds_posix,
postinstall_cmds_win = package.postinstall_cmds_win,
sha256 = package.sha256,
)
direct_deps.append(package.name)
# https://bazel.build/rules/lib/builtins/module_ctx#extension_metadata
return ctx.extension_metadata(
# By specifying the direct dependencies, bazel mod tidy will automatically
# update the use_repo call to add or remove dependencies to the list.
root_module_direct_deps = direct_deps,
root_module_direct_dev_deps = [],
# By setting this line, we are telling Bazel that the generated rules are
# hermetic all on their own. This *is* the case because we are downloading
# from CIPD by tag and verifying the sha256 sum.
# This is a big deal because it means our autorollers can update only the MODULE.bazel
# file and don't have to update anything in the MODULE.bazel.lock file after.
reproducible = True,
)
cipd = module_extension(
doc = """Bzlmod extension used to download CIPD packages.""",
implementation = _cipd_impl,
tag_classes = {
"download_http": _download_http,
"download_cipd": _download_cipd,
},
)
def _cipd_deps_impl(_ctx):
pass
cipd_deps = module_extension(
doc = """Install deps needed to download CIPD packages.""",
implementation = _cipd_deps_impl,
)