blob: ffcb7b86600939edb8d2e5c7244dc1902be253be [file] [edit]
#!/usr/bin/env python3
#
# 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.
"""The script to upload generated artifacts from build server to CAS."""
import argparse
import glob
import logging
import os
import random
import re
import subprocess
import time
import cas_metrics_pb2 # type: ignore
from uploader import ArtifactConfig
from uploader import CasInfo
from uploader import Uploader
VERSION = '1.5'
CAS_UPLOADER_PREBUILT_PATH = 'tools/tradefederation/prebuilts/'
CAS_UPLOADER_PATH = 'tools/content_addressed_storage/prebuilts/'
CAS_UPLOADER_BIN = 'casuploader'
LOG_PATH = 'logs/cas_uploader.log'
CAS_METRICS_PATH = 'logs/cas_metrics.pb'
METRICS_PATH = 'logs/artifact_metrics.json'
MAX_WORKERS_LOWER_BOUND = 2
MAX_WORKERS_UPPER_BOUND = 6
MAX_WORKERS = random.randint(MAX_WORKERS_LOWER_BOUND, MAX_WORKERS_UPPER_BOUND)
# Configurations of artifacts to upload to CAS.
#
# Override the preset artifacts with the "--artifacts" flag. Examples:
# 1. To upload img files with path relative to DIST_DIR (no '/**/'), override source_path with:
# --artifacts 'img=./*-img-*zip'
# 2. To upload img files as chunked version only:
# --artifacts 'img=*-img-*zip' standard=False chunk=True
# 3. To also upload json files in logs folder:
# --artifacts 'new=./log/*.json'
# 4. To not update img files:
# --artifacts 'img='
#
ARTIFACTS = {
# test_suite targets
'android-catbox': ArtifactConfig('android-catbox.zip', True, exclude_filters=['android-catbox/jdk/.*']),
'android-csuite': ArtifactConfig('android-csuite.zip', True, exclude_filters=['android-csuite/jdk/.*']),
'android-cts': ArtifactConfig('android-cts.zip', True, exclude_filters=['android-cts/jdk/.*']),
'android-gcatbox': ArtifactConfig('android-gcatbox.zip', True, exclude_filters=['android-gcatbox/jdk/.*']),
'android-gts': ArtifactConfig('android-gts.zip', True, exclude_filters=['android-gts/jdk/.*']),
'android-mcts': ArtifactConfig('android-mcts.zip', True),
'android-mts': ArtifactConfig('android-mts.zip', True, exclude_filters=['android-mts/jdk/.*']),
'android-pts': ArtifactConfig('android-pts.zip', True, exclude_filters=['android-pts/jdk/.*']),
'android-sts': ArtifactConfig('android-sts.zip', True),
'android-tvts': ArtifactConfig('android-tvts.zip', True, exclude_filters=['android-tvts/jdk/.*']),
'android-vts': ArtifactConfig('android-vts.zip', True),
'android-wts': ArtifactConfig('android-wts.zip', True, exclude_filters=['android-wts/jdk/.*']),
'art-host-tests': ArtifactConfig('art-host-tests.zip', True),
'bazel-test-suite': ArtifactConfig('bazel-test-suite.zip', True),
'host-unit-tests': ArtifactConfig('host-unit-tests.zip', True),
'general-tests': ArtifactConfig('general-tests.zip', True),
'general-tests_configs': ArtifactConfig('general-tests_configs.zip', True),
'general-tests_host-shared-libs': ArtifactConfig('general-tests_host-shared-libs.zip', True),
'tradefed': ArtifactConfig('tradefed.zip', True),
'google-tradefed': ArtifactConfig('google-tradefed.zip', True),
'robolectric-tests': ArtifactConfig('robolectric-tests.zip', True),
'ravenwood-tests': ArtifactConfig('ravenwood-tests.zip', True),
'test_mappings': ArtifactConfig('test_mappings.zip', True),
# Mainline artifacts
'apex': ArtifactConfig('*.apex', False),
'apk': ArtifactConfig('*.apk', False),
# Device target artifacts
'androidTest': ArtifactConfig('androidTest.zip', True),
'device-tests': ArtifactConfig('device-tests.zip', True),
'device-tests_configs': ArtifactConfig('device-tests_configs.zip', True),
'device-tests_host-shared-libs': ArtifactConfig('device-tests_host-shared-libs.zip', True),
'performance-tests': ArtifactConfig('performance-tests.zip', True),
'device-platinum-tests': ArtifactConfig('device-platinum-tests.zip', True),
'device-platinum-tests_configs': ArtifactConfig('device-platinum-tests_configs.zip', True),
'device-platinum-tests_host-shared-libs': ArtifactConfig('device-platinum-tests_host-shared-libs.zip', True),
'camera-hal-tests': ArtifactConfig('camera-hal-tests.zip', True),
'camera-hal-tests_configs': ArtifactConfig('camera-hal-tests_configs.zip', True),
'camera-hal-tests_host-shared-libs': ArtifactConfig('camera-hal-tests_host-shared-libs.zip', True),
'device-pixel-tests': ArtifactConfig('device-pixel-tests.zip', True),
'device-pixel-tests_configs': ArtifactConfig('device-pixel-tests_configs.zip', True),
'device-pixel-tests_host-shared-libs': ArtifactConfig('device-pixel-tests_host-shared-libs.zip', True),
'automotive-tests': ArtifactConfig('automotive-tests.zip', True),
'automotive-general-tests': ArtifactConfig('automotive-general-tests', True),
'automotive-sdv-tests': ArtifactConfig('automotive-sdv-tests', True),
'automotive-sdv-tests_configs': ArtifactConfig('automotive-sdv-tests_configs', True),
'tests': ArtifactConfig('*-tests-*zip', True),
'continuous_instrumentation_tests': ArtifactConfig('*-continuous_instrumentation_tests-*zip', True),
'continuous_instrumentation_metric_tests': ArtifactConfig('*-continuous_instrumentation_metric_tests-*zip', True),
'continuous_native_tests': ArtifactConfig('*-continuous_native_tests-', True),
'cvd-host_package': ArtifactConfig('cvd-host_package.tar.gz', False),
'bootloader': ArtifactConfig('bootloader.img', False),
'radio': ArtifactConfig('radio.img', False),
'target_files': ArtifactConfig('*-target_files-*zip', True),
'img': ArtifactConfig('*-img-*zip', False, chunk=True, chunk_dir=True)
}
# Artifacts will be uploaded if the config name is set in arguments `--experiment_artifacts`.
# These configs are usually used to upload artifacts in partial branches/targets for experiment
# purpose.
# A sample entry:
# "device_image_target_files": ArtifactConfig('*-target_files-*.zip', True)
EXPERIMENT_ARTIFACT_CONFIGS = {
"device_image_proguard_dict": ArtifactConfig('*-proguard-dict-*.zip', False, True, True),
}
def _init_cas_info() -> CasInfo:
client_path = _get_client()
return CasInfo(
_get_env_var('RBE_instance', check=True),
_get_env_var('RBE_service', check=True),
client_path,
_get_client_version(client_path)
)
def _get_client() -> str:
if CAS_UPLOADER_PREBUILT_PATH in os.path.abspath(__file__):
return _get_prebuilt_client()
bin_path = os.path.join(CAS_UPLOADER_PATH, CAS_UPLOADER_BIN)
if os.path.isfile(bin_path):
logging.info('Using client at %s', bin_path)
return bin_path
return _get_prebuilt_client()
def _get_prebuilt_client() -> str:
client = glob.glob(CAS_UPLOADER_PREBUILT_PATH + '**/' + CAS_UPLOADER_BIN, recursive=True)
if not client:
raise ValueError('Could not find casuploader binary')
logging.info('Using client at %s', client[0])
return client[0]
def _get_client_version(client_path: str) -> int:
"""Get the version of CAS client in turple format."""
version_output = ''
try:
version_output = subprocess.check_output([client_path, '-version']).decode('utf-8').strip()
matched = re.findall(r'version: (\d+\.\d+)', version_output)
if not matched:
logging.warning('Failed to parse CAS client version. Output: %s', version_output)
return (0, 0)
version = tuple(map(int, matched[0].split('.')))
logging.info('CAS client version is %s', version)
return version
# pylint: disable=broad-exception-caught
except Exception as e:
# pylint: enable=broad-exception-caught
logging.warning('Failed to get CAS client version. Output: %s. Error %s', version_output, e)
return (0, 0)
def _get_env_var(key: str, default=None, check=False):
value = os.environ.get(key, default)
if check and not value:
raise ValueError(f'Error: the environment variable {key} is not set')
return value
def _get_artifact_property(values: list[str], property:str, default:bool) -> bool:
for value in values:
# Example:
# --artifacts 'img=*-img-*zip chunk=True'
# --artifacts 'img=*-img-*zip standard=False chunk'
if value.startswith(property):
tokens=value.split('=', maxsplit=1)
if tokens[0] != property:
continue
if len(tokens) == 1 or tokens[1] in {'T', 't', 'True', 'true'}:
return True
return False
return default
def _override_artifacts(args):
for override in args.artifacts:
# Example:
# --artifacts 'img=./*-img-*zip unzip chunk=False'
tokens=override.split('=', maxsplit=1)
if len(tokens) == 1:
logging.warning("Artifact override - ignored (invalid): %s", override)
continue
name = tokens[0]
artifact = ARTIFACTS[name] if name in ARTIFACTS else None
if not tokens[1]: # Delete an artifact ('name=')
if artifact:
logging.info("Artifact delete: %s", override)
del ARTIFACTS[name]
else:
logging.warning("Artifact delete - ignored (name not found): %s", override)
return
values = tokens[1].split()
source_path = values[0]
if artifact:
logging.info("Artifact override: %s", override)
artifact.source_path = source_path
else:
logging.info("Artifact add new: %s", override)
artifact = ArtifactConfig(source_path, False)
if len(values) > 1:
artifact.unzip = _get_artifact_property(values[1:], "unzip", artifact.unzip)
artifact.standard = _get_artifact_property(values[1:], "standard", artifact.standard)
artifact.chunk = _get_artifact_property(values[1:], "chunk", artifact.chunk)
artifact.chunk_dir = _get_artifact_property(values[1:], "chunk_dir", artifact.chunk_dir)
logging.info("Artifact: %s", artifact)
ARTIFACTS[name] = artifact
def _parse_additional_artifacts(args) -> list[ArtifactConfig]:
additional_artifacts = []
for config in args.experiment_artifacts:
if config not in EXPERIMENT_ARTIFACT_CONFIGS:
logging.warning('Ignore invalid experiment_artifacts: %s', config)
else:
additional_artifacts.append(EXPERIMENT_ARTIFACT_CONFIGS[config])
logging.info(
'Added experiment artifact from arguments %s',
EXPERIMENT_ARTIFACT_CONFIGS[config].source_path,
)
return additional_artifacts
def main():
"""Uploads the specified artifacts to CAS."""
parser = argparse.ArgumentParser()
parser.add_argument(
'--experiment_artifacts',
required=False,
action='append',
default=[],
help='Name of configuration which artifact to upload',
)
parser.add_argument(
'--artifacts',
required=False,
action='append',
default=[],
help='Override preset artifacts, e.g. "--artifacts \'img=./*-img-*zip unzip chunk=False\'"',
)
parser.add_argument(
'--list',
action='store_true',
help='List preset artifacts and exit',
)
parser.add_argument(
'--dryrun',
action='store_true',
help='List files to upload and exit',
)
dist_dir = _get_env_var('DIST_DIR', check=True)
log_file = os.path.join(dist_dir, LOG_PATH)
print('content_uploader.py will export logs to:', log_file)
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s',
filename=log_file,
)
logging.info('Content uploader version: %s', VERSION)
logging.info('Environment variables of running server: %s', os.environ)
try:
start = time.time()
args = parser.parse_args()
_override_artifacts(args)
additional_artifacts = _parse_additional_artifacts(args)
if args.list:
for name, artifact in ARTIFACTS.items():
print(f"{name:<30}={artifact}")
cas_info = _init_cas_info()
cas_metrics = cas_metrics_pb2.CasMetrics()
Uploader(cas_info, log_file).upload(
list(ARTIFACTS.values()) + additional_artifacts,
dist_dir, cas_metrics, MAX_WORKERS, args.dryrun)
elapsed = time.time() - start
logging.info('Total time of uploading build artifacts to CAS: %d seconds',
elapsed)
cas_metrics.time_ms = int(elapsed * 1000)
cas_metrics.client_version = '.'.join([str(num) for num in cas_info.client_version])
cas_metrics.uploader_version = VERSION
cas_metrics.max_workers = MAX_WORKERS
serialized_metrics = cas_metrics.SerializeToString()
if serialized_metrics:
cas_metrics_file = os.path.join(dist_dir, CAS_METRICS_PATH)
with open(cas_metrics_file, "wb") as file:
file.write(serialized_metrics)
logging.info('Output cas metrics to: %s', cas_metrics_file)
except ValueError as e:
logging.exception("Unexpected error: %s", e)
if __name__ == '__main__':
main()