blob: 80148745bbd4a96797186a429d1bab7af91693dc [file] [log] [blame]
# Copyright (C) 2022 The Android Open Source Project
#
# 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.
_BUILD_NUM_ENV_VAR = "KLEAF_DOWNLOAD_BUILD_NUMBER_MAP"
def _sanitize_repo_name(x):
"""Sanitize x so it can be used as a repository name.
Replacing invalid characters (those not in `[A-Za-z0-9-_.]`) with `_`.
"""
ret = ""
for c in x.elems():
if not c.isalnum() and not c in "-_.":
c = "_"
ret += c
return ret
def _parse_env(repository_ctx, var_name, expected_key):
"""
Given that the environment variable named by `var_name` is set to the following:
```
key=value[,key=value,...]
```
Return a list of values, where key matches `expected_key`. If there
are multiple matches, the first one is returned. If there is no match,
return `None`.
For example:
```
MYVAR="myrepo=x,myrepo2=y" bazel ...
```
Then `_parse_env(repository_ctx, "MYVAR", "myrepo")` returns `"x"`
"""
for pair in repository_ctx.os.environ.get(var_name, "").split(","):
pair = pair.strip()
if not pair:
continue
tup = pair.split("=", 1)
if len(tup) != 2:
fail("Unrecognized token in {}, must be key=value:\n{}".format(var_name, pair))
key, value = tup
if key == expected_key:
return value
return None
_ARTIFACT_URL_FMT = "https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds/{build_number}/{target}/attempts/latest/artifacts/{filename}/url?redirect=true"
def _download_artifact_repo_impl(repository_ctx):
workspace_file = """workspace(name = "{}")
""".format(repository_ctx.name)
repository_ctx.file("WORKSPACE.bazel", workspace_file, executable = False)
build_number = _get_build_number(repository_ctx)
if not build_number:
_handle_no_build_number(repository_ctx)
else:
_download_from_build_number(repository_ctx, build_number)
def _get_build_number(repository_ctx):
"""Gets the value of build number, setting defaults if necessary."""
build_number = _parse_env(repository_ctx, _BUILD_NUM_ENV_VAR, repository_ctx.attr.parent_repo)
if not build_number:
build_number = repository_ctx.attr.build_number
return build_number
def _handle_no_build_number(repository_ctx):
"""Handles the case where the build number cannot be found."""
SAMPLE_BUILD_NUMBER = "8077484"
if repository_ctx.attr.parent_repo == "gki_prebuilts":
msg = """
ERROR: {parent_repo}: No build_number specified. Fix by specifying `--use_prebuilt_gki=<build_number>"`, e.g.
bazel build --use_prebuilt_gki={build_number} @{parent_repo}//{filename}
""".format(
filename = repository_ctx.attr.filename,
parent_repo = repository_ctx.attr.parent_repo,
build_number = SAMPLE_BUILD_NUMBER,
)
else:
msg = """
ERROR: {parent_repo}: No build_number specified.
Fix by one of the following:
- Specify `build_number` attribute in {parent_repo}
- Specify `--action_env={build_num_var}="{parent_repo}=<build_number>"`, e.g.
bazel build \\
--action_env={build_num_var}="{parent_repo}={build_number}" \\
@{parent_repo}//{filename}
""".format(
filename = repository_ctx.attr.filename,
parent_repo = repository_ctx.attr.parent_repo,
build_number = SAMPLE_BUILD_NUMBER,
build_num_var = _BUILD_NUM_ENV_VAR,
)
build_file = """
load("@//build/kernel/kleaf:fail.bzl", "fail_rule")
fail_rule(
name = "file",
message = \"\"\"{}\"\"\"
)
""".format(msg)
repository_ctx.file("file/BUILD.bazel", build_file, executable = False)
def _download_from_build_number(repository_ctx, build_number):
# Download the requested file
urls = [_ARTIFACT_URL_FMT.format(
build_number = build_number,
target = repository_ctx.attr.target,
filename = repository_ctx.attr.filename,
)]
download_path = repository_ctx.path("file/{}".format(repository_ctx.attr.filename))
download_info = repository_ctx.download(
url = urls,
output = download_path,
sha256 = repository_ctx.attr.sha256,
allow_fail = repository_ctx.attr.allow_fail,
)
# Define the filegroup to contain the file.
# If failing and it is allowed, set filegroup to empty
if not download_info.success and repository_ctx.attr.allow_fail:
srcs = ""
else:
srcs = '"{}"'.format(repository_ctx.attr.filename)
build_file = """filegroup(
name="file",
srcs=[{srcs}],
visibility=["@{parent_repo}//{filename}:__pkg__"],
)
""".format(
srcs = srcs,
filename = repository_ctx.attr.filename,
parent_repo = repository_ctx.attr.parent_repo,
)
repository_ctx.file("file/BUILD.bazel", build_file, executable = False)
_download_artifact_repo = repository_rule(
implementation = _download_artifact_repo_impl,
attrs = {
"build_number": attr.string(),
"parent_repo": attr.string(doc = "Name of the parent `download_artifacts_repo`"),
"filename": attr.string(),
"target": attr.string(doc = "Name of target on [ci.android.com](http://ci.android.com), e.g. `kernel_aarch64`"),
"sha256": attr.string(default = ""),
"allow_fail": attr.bool(),
},
environ = [
_BUILD_NUM_ENV_VAR,
],
)
def _alias_repo_impl(repository_ctx):
workspace_file = """workspace(name = "{}")
""".format(repository_ctx.name)
repository_ctx.file("WORKSPACE.bazel", workspace_file, executable = False)
for filename, actual in repository_ctx.attr.aliases.items():
build_file = """alias(name="{filename}", actual="{actual}", visibility=["//visibility:public"])
""".format(filename = filename, actual = actual)
repository_ctx.file("{}/BUILD.bazel".format(filename), build_file, executable = False)
_alias_repo = repository_rule(
implementation = _alias_repo_impl,
attrs = {
"aliases": attr.string_dict(),
},
)
def download_artifacts_repo(
name,
target,
files = None,
optional_files = None,
build_number = None):
"""Create a [repository](https://docs.bazel.build/versions/main/build-ref.html#repositories) that contains artifacts downloaded from [ci.android.com](http://ci.android.com).
For each item `file` in `files`, the label `@{name}//{file}` can refer to the downloaded file.
For example:
```
download_artifacts_repo(
name = "gki_prebuilts",
target = "kernel_aarch64",
build_number = "9359437"
files = ["vmlinux"],
optional_files = ["abi_symbollist"],
)
```
You may refer to the file with the label `@gki_prebuilts//vmlinux`, etc.
To refer to all downloaded files, you may use `@gki_prebuilts//...`
You may leave the build_number empty. If so, you must override the build number at build time.
See below.
For the repo `gki_prebuilts`, you may override the build number with `--use_prebuilt_gki`,
e.g.
```
bazel build --use_prebuilt_gki=8078291 @gki_prebuilts//vmlinux
```
Args:
name: name of the repository.
target: build target on [ci.android.com](http://ci.android.com)
build_number: build number on [ci.android.com](http://ci.android.com)
files: One of the following:
- If a list, this is a list of file names on [ci.android.com](http://ci.android.com).
- If a dict, keys are file names on [ci.android.com](http://ci.android.com), and values
are corresponding SHA256 hash.
optional_files: Same as `files`, but it is optional. If the file is not in the given
build, it will not be downloaded, and the label (e.g. `@gki_prebuilts//abi_symbollist`)
points to an empty filegroup.
"""
if type(files) == type([]):
files = {filename: None for filename in files}
elif files == None:
files = {}
if type(optional_files) == type([]):
optional_files = {filename: None for filename in optional_files}
elif optional_files == None:
optional_files = {}
for files_dict, allow_fail in ((files, False), (optional_files, True)):
for filename, sha256 in files_dict.items():
# Need a repo for each file because repository_ctx.download is blocking. Defining multiple
# repos allows downloading in parallel.
# e.g. @gki_prebuilts_vmlinux
_download_artifact_repo(
name = name + "_" + _sanitize_repo_name(filename),
parent_repo = name,
filename = filename,
build_number = build_number,
target = target,
sha256 = sha256,
allow_fail = allow_fail,
)
# Define a repo named @gki_prebuilts that contains aliases to individual files, e.g.
# @gki_prebuilts//vmlinux
_alias_repo(
name = name,
aliases = {
filename: "@" + name + "_" + _sanitize_repo_name(filename) + "//file"
for filename in (list(files.keys()) + list(optional_files.keys()))
},
)