blob: 6d57c904b6e8cfa51666a503027d55628b23cd9f [file] [log] [blame] [edit]
"""Build step class for building OTA's."""
from __future__ import absolute_import
import logging
import os
import re
import sys
from helpers import make_utils
from helpers import ninja_utils
from helpers import py_env_utils
from slave import base_step
# Taken from recommended value in autoninja
BUILD_ACCELERATION_JOB_MULTIPLIER = 40
# Allow sufficient time for builds without build acceleration to finish.
_OTA_STEP_MAX_TIME_SECONDS = 6 * 60 * 60
_MESH_TAG = '-mesh-'
_YETI_TAG = '-yeti-'
_SDM_TAG = '-sdm-'
_GALLIUM_TAG = '-gallium-'
_OPAL_TAG = '-opal-'
_CONCIERGE_TAG = '-concierge-'
_HEALTH_TAG = '-health-'
_FUCHSIA_TAG = '-fuchsia-'
_FUCHSIA_REV_TAG = '-fuchsia_rev-'
_ML_FRAMEWORK_TAG = '-ml_framework-'
_DEMO_TAG = '-demo-'
_OTA_FILES_RE = r'.*-ota-.*\..*'
_PARTNER_TAG = '-partner-'
_LEGACY_TAG = '_legacy'
_1LED_TAG = '-1led-'
_MODULAR_COMMS_TAG = '-modular_comms-'
_IOT_CAST_TAG = '-iot_cast-'
GANETI_TAG = '-ganeti-'
_OTA_ZIP_TEMPLATE = '{}-ota-{}.zip'
class OtaStep(base_step.BaseStep):
"""Build step class for building OTA's."""
def __init__(self, build_name, name='build ota', board_name=None,
extra_params=None, fct_extra_params=None, max_ota_size=None, target_name=None,
timeout_secs=_OTA_STEP_MAX_TIME_SECONDS,
**kwargs):
"""Creates a OtaStep instance.
Args:
build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
name: user-visible name of this step.
board_name: optional BOARD_NAME param for make if any.
extra_params: optional list of params to add
fct_extra_params: optional list of params to add for building fct
max_ota_size: Max size of OTA to enfoce if any enforcement required.
timeout_secs: Max execution time for OTA steps.
**kwargs: Any additional args to pass to BaseStep.
"""
base_step.BaseStep.__init__(self, name=name, timeout_secs=timeout_secs,
**kwargs)
self._build_name = build_name
self._board_name = board_name
self._extra_params = extra_params
self._fct_extra_params = fct_extra_params
self._max_ota_size = max_ota_size
self._target_name = target_name
self._enable_verbose_logging = self._properties.get(
'enable_verbose_logging', False)
self._is_mesh_build = _MESH_TAG in self._build_name
self._build_name = self._build_name.replace(_MESH_TAG, '-')
self._is_yeti_build = _YETI_TAG in self._build_name
self._build_name = self._build_name.replace(_YETI_TAG, '-')
self._is_sdm_build = _SDM_TAG in self._build_name
self._build_name = self._build_name.replace(_SDM_TAG, '-')
self._is_gallium_build = _GALLIUM_TAG in self._build_name
self._build_name = self._build_name.replace(_GALLIUM_TAG, '-')
self._is_opal_build = _OPAL_TAG in self._build_name
self._build_name = self._build_name.replace(_OPAL_TAG, '-')
self._is_concierge_build = _CONCIERGE_TAG in self._build_name
self._build_name = self._build_name.replace(_CONCIERGE_TAG, '-')
self._is_health_build = _HEALTH_TAG in self._build_name
self._build_name = self._build_name.replace(_HEALTH_TAG, '-')
self._is_fuchsia_build = _FUCHSIA_TAG in self._build_name
self._build_name = self._build_name.replace(_FUCHSIA_TAG, '-')
self._is_fuchsia_rev_build = _FUCHSIA_REV_TAG in self._build_name
self._build_name = self._build_name.replace(_FUCHSIA_REV_TAG, '-')
self._is_ml_framework_build = _ML_FRAMEWORK_TAG in self._build_name
self._build_name = self._build_name.replace(_ML_FRAMEWORK_TAG, '-')
self._is_demo_build = _DEMO_TAG in self._build_name
self._build_name = self._build_name.replace(_DEMO_TAG, '-')
self._is_ganeti_build = GANETI_TAG in self._build_name
self._build_name = self._build_name.replace(GANETI_TAG, '-')
self._is_1led_build = _1LED_TAG in self._build_name
self._build_name = self._build_name.replace(_1LED_TAG, '-')
self._is_modular_comms_build = _MODULAR_COMMS_TAG in self._build_name
self._build_name = self._build_name.replace(_MODULAR_COMMS_TAG, '-')
self._is_iot_cast_build = _IOT_CAST_TAG in self._build_name
self._build_name = self._build_name.replace(_IOT_CAST_TAG, '-')
# Partner repo is specified in properties, but the tag needs to be
# removed from the build name.
self._build_name = self._build_name.replace(_PARTNER_TAG, '-')
self._build_name = self._build_name.replace(_LEGACY_TAG, '')
def _dist_dir(self):
dist_dir = os.path.join('out/dist', self.build_number)
if self.patchset != '0':
dist_dir = os.path.join(dist_dir, str(self.patchset))
return dist_dir
def make_target_param(self, target_params=False):
"""Creates PRODUCT or TARGET params for make.
build/build test steps are using PRODUCT-<build name> notation,
TARGET_PRODUCT and TARGET_BUILD_VARIANT params required for OSS and FCT.
Args:
target_params: use TARGET style params regardless current build_system.
Returns:
array of params to be appended to make command.
"""
target_name = self._target_name
if not target_name:
target_name = self._build_name
if target_params:
product, variant = target_name.split('-')
return ['TARGET_PRODUCT={}'.format(product),
'TARGET_BUILD_VARIANT={}'.format(variant)]
return ['PRODUCT-' + target_name]
def make_command_env(self):
"""Generates env dictionary for make command."""
return self.build_accelerator.environment_variables
def _make_extra_flags(self):
"""Generates make extra params based on build type."""
flags = []
variant = self._build_name.split('-')[-1]
if variant != 'eng':
flags += ['OFFICIAL_BUILD=1']
if self._enable_verbose_logging:
flags.append('showcommands')
if self._is_mesh_build:
flags += ['USE_GWIFI=1']
if self._is_yeti_build:
flags += ['ENABLE_YETI=1', 'ENABLE_CLASSIC_YETI=0']
if self._is_sdm_build:
flags += ['ENABLE_HOSPITALITY=1', 'ENABLE_MANAGED_MODE=1']
if self._is_gallium_build:
flags += ['ENABLE_GALLIUM=1']
if self._is_opal_build:
flags += ['ENABLE_OPAL=true']
if self._is_concierge_build:
flags += ['ENABLE_SOUND_PRESENCE=1']
if self._is_health_build:
flags += ['ENABLE_HEALTH_SOUND=1', 'ENABLE_MANAGED_MODE=1']
if self._is_fuchsia_build:
flags += ['FUCHSIA_OTA=true']
if self._is_fuchsia_rev_build:
flags += ['FUCHSIA_REVERSE=true']
if self._is_ml_framework_build:
flags += ['ENABLE_ML_FRAMEWORK=true']
if self._is_demo_build:
flags += ['RETAIL_DEMO_BUILD=true']
if self._board_name:
flags += ['BOARD_NAME={}'.format(self._board_name)]
if self._is_1led_build:
flags += ['NUM_LEDS=1']
if self._is_modular_comms_build:
flags += ['ENABLE_MODULAR_COMMS=true']
if self._is_iot_cast_build:
flags += ['ENABLE_IOT_CAST=1']
if self._extra_params:
flags += self._extra_params
return flags
def _make_fct_extra_flags(self):
flags = []
if self._fct_extra_params:
flags += self._fct_extra_params
return flags
def _build_ota(self, custom_shell):
"""Builds |build_name| OTA package for the |issue| and |patchset|.
Args:
custom_shell: List containing custom shell, to be used with make.
Returns:
True if the OTA package was built successfully, False otherwise.
"""
# Clean the systemimage first to avoid bleedover on incremental builds
# from previous builds of the same product.
command = ['make',
'BUILD_NUMBER={}'.format(self.build_number),
'USER=auto',
'clean-systemimage',
'DIST_DIR={}'.format(self._dist_dir())]
command += self.build_accelerator.make_flags
quiet = False
if custom_shell:
quiet = True
# For this step, don't use PRODUCT-* because it adds a "dist" goal. Just
# perform the clean-systemimage.
command += self.make_target_param(target_params=True)
command += self._make_extra_flags()
returncode, stdout, stderr = self.exec_subprocess(
command, env=self.make_command_env(), quiet=quiet)
# We permit this step to fail because the clean-systemimage step
# is custom to Cast OS and does not exist on older release branches.
# Since CQ build logic always runs on master, regardless of the age
# of the code being built, we must maintain backwards compatibility.
if returncode != 0:
logging.warning(
'Failed to run clean-systemimage. This is normal on release branches '
'from 1.50 and earlier. If this happens on a newer branch it is an '
'error and will cause non-deterministic ota package sizes.\n'
'Return code %d, stderr:\n%s',
returncode, stderr)
command = ['make',
'--output-sync=target',
'-j{}'.format(self.get_num_jobs(
multiplier=BUILD_ACCELERATION_JOB_MULTIPLIER)),
'BUILD_NUMBER={}'.format(self.build_number),
'USER=auto',
'dist',
'DIST_DIR={}'.format(self._dist_dir()),
'DISABLE_AUTO_INSTALLCLEAN=true',]
command += self.build_accelerator.make_flags
command += self.make_target_param()
command += self._make_extra_flags()
if custom_shell:
command += custom_shell
returncode, stdout, stderr = self.exec_subprocess(
command, env=self.make_command_env(), quiet=quiet)
if custom_shell:
self._merge_build_logs()
if returncode != 0:
if custom_shell:
self._dump_error_logs(stdout, stderr)
self._error_processor(stdout, stderr)
else:
if custom_shell:
self._print_build_success()
if custom_shell:
sys.stdout.write('Complete logs can be found in build_ota.log '
'within invocation artifacts, sorted by module. Logs that are '
'generated outside of a module are logged under the "general" module.\n')
sys.stdout.flush()
return returncode == 0
def _error_processor(self, stdout, stderr):
messages = ninja_utils.ninja_failure_to_comments(stdout, max_errors=3)
for message in messages:
self.add_review({'message': message})
comments = make_utils.make_error_to_comments(stderr, self.directory)
if comments:
self.add_review({'comments': comments})
def _print_build_success(self):
sys.stdout.write('Build successful.\n')
sys.stdout.flush()
def _clear_dist(self, quiet):
"""Clears everything in the dist directory leftover from previous builds.
Args:
quiet: Suppress subprocess output.
"""
dist_dirs = self.glob_files('out/dist/*')
if dist_dirs:
self.exec_subprocess(['rm', '-rf'] + dist_dirs, quiet=quiet)
def _clear_ota_artifacts(self, quiet):
"""Removes all OTA artifacts leftover by previous builds.
Args:
quiet: Suppress subprocess output.
"""
archive_pattern = re.compile(_OTA_FILES_RE)
dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
ota_files = [f for f in dist_files if archive_pattern.match(f)]
if ota_files:
self.exec_subprocess(['rm', '-f'] + ota_files, quiet=quiet)
def _clear_symbols_artifacts(self, quiet):
"""Removes all symbols artifacts leftover by previous builds.
Args:
quiet: Suppress subprocess output.
"""
symbols_files = self.glob_files('out/target/product/*/*-symbols-*.zip')
if symbols_files:
self.exec_subprocess(['rm', '-f'] + symbols_files, quiet=quiet)
def _ota_archive_output_dir(self):
"""Return system specific archive dir."""
output_dir = self.get_gcs_dir()
if self.build_system != 'catabuilder':
output_dir = os.path.join(output_dir, 'artifacts')
if not os.path.exists(output_dir):
os.makedirs(output_dir)
return output_dir
def _archive_artifacts(self, quiet):
"""Copies the artifacts for archival.
Args:
quiet: Suppress subprocess output.
"""
output_dir = self._ota_archive_output_dir()
if self.build_system == 'catabuilder':
# Archive everything in the continuous build system.
archive_pattern = re.compile(r'.*')
else:
archive_pattern = re.compile(_OTA_FILES_RE)
dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
archive_files = [f for f in dist_files if archive_pattern.match(f)]
for f in archive_files:
self.exec_subprocess(['cp', '-r', f, output_dir], quiet=quiet)
# Save the OTA file list for later use.
ota_files = [f for f in archive_files if re.match(_OTA_FILES_RE, f)]
if ota_files:
filenames = [os.path.basename(x) for x in ota_files]
self.set_build_property('ota_files', filenames)
def _dump_error_logs(self, stdout, stderr):
"""Dumps error logs to stderr."""
product = self._build_name.split('-')[0]
error_log_files = sorted(self.glob_files(
'out/target/product/' + product + '/build_logs/*.err.txt'),
key=os.path.basename)
if error_log_files:
sys.stdout.write('The following modules failed to build:\n')
sys.stdout.flush()
for error_file in error_log_files:
with open(error_file, 'r') as err_file:
contents = err_file.read()
sys.stdout.write(os.path.basename(error_file).split('.')[0] + ':\n')
sys.stdout.flush()
sys.stderr.write(contents)
sys.stderr.flush()
else:
# No errors found, dump error logs to test.log.
sys.stderr.write(stderr)
sys.stderr.flush()
def _merge_build_logs(self):
"""Copies build logs for archival."""
product = self._build_name.split('-')[0]
build_log_files = sorted(self.glob_files(
'out/target/product/' + product + '/build_logs/*.out.txt'),
key=os.path.basename)
output_dir = self.get_gcs_dir()
if not os.path.exists(output_dir):
os.makedirs(output_dir)
build_logs_file = os.path.join(output_dir, 'build_ota.log')
with open(build_logs_file, 'w') as out_file:
for log_file in build_log_files:
with open(log_file, 'r') as in_file:
contents = in_file.read()
if contents:
out_file.write('**********************************'
'************************************\n');
out_file.write(os.path.basename(log_file).split('.')[0] + ':\n')
out_file.write('**********************************'
'************************************\n');
out_file.write(contents + '\n')
out_file.flush()
def _check_ota_size(self):
"""Checks the size of the OTA.
Returns:
Boolean if the OTA is under the max size.
"""
# TODO(gfhuang): A/B ota size check need rework.
# This as-is checks both .zip and .bin.
archive_pattern = re.compile(_OTA_FILES_RE)
dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
ota_files = [f for f in dist_files if archive_pattern.match(f)]
self._log_ota_size(ota_files)
if not self._max_ota_size:
sys.stdout.write("No max_ota_size defined for this build")
sys.stdout.flush()
return True
for ota_file in ota_files:
ota_size = os.path.getsize(ota_file)
if ota_size > self._max_ota_size:
message = 'OTA size of {} exceed max of {} for file {}'.format(
ota_size, self._max_ota_size, ota_file)
sys.stdout.write(message + '\n')
sys.stdout.flush()
self.add_review({'message': message})
return False
else:
sys.stdout.write(
"OTA file {} size: {} bytes out of {} maximum ({}%)".format(
ota_file,
ota_size,
self._max_ota_size,
round((ota_size / self._max_ota_size) * 100, 2),
))
sys.stdout.flush()
return True
def _log_ota_size(self, ota_files):
target_ota_filename = _OTA_ZIP_TEMPLATE.format(
self.get_property('buildername', default='').split('-')[0],
self.get_property('buildset'))
ota_file = next((f for f in ota_files
if os.path.basename(f) == target_ota_filename), None)
if not ota_file:
return
self.set_build_property('ota_size', os.path.getsize(ota_file))
if self._max_ota_size:
self.set_build_property('max_ota_size', self._max_ota_size)
def _make_custom_shell(self):
"""Returns a list containing a path to the custom shell"""
shell_path = os.path.abspath(os.path.join(
os.getcwd(),
'build/android-mk-module-split-shell'))
if os.path.isfile(shell_path):
return ['ANDROID_BUILD_SHELL={}'.format(shell_path)]
return []
def run(self):
"""Builds ota package.
Returns:
True iff there were no errors.
"""
custom_shell = self._make_custom_shell()
self._clear_dist(bool(custom_shell))
self._clear_ota_artifacts(bool(custom_shell))
self._clear_symbols_artifacts(bool(custom_shell))
if not self._build_ota(custom_shell):
return False
self._archive_artifacts(bool(custom_shell))
if not self._check_ota_size():
return False
return True
class OtaPropStep(base_step.BaseStep):
"""Ota Property file step class for generating repo.prop."""
def __init__(self, **kwargs):
"""Creates a OtaPropStep instance.
Args:
**kwargs: Any additional args to pass to BaseStep.
"""
base_step.BaseStep.__init__(self, name='save repo.prop', **kwargs)
def run(self):
prop_cmd = [
'repo', 'forall', '-c',
'git show -s --format="$REPO_PROJECT %H"'
]
proj_sha_pair_re = re.compile(r'^.+? [0-9a-f]{40}$', flags=re.IGNORECASE)
repo_prop = self.exec_subprocess(prop_cmd, check_output=True, quiet=True)
valid_lines = [l for l in repo_prop.split('\n')
if proj_sha_pair_re.match(l.strip())]
prop_file = os.path.join(self.get_gcs_dir(), 'repo.prop')
with open(prop_file, 'w') as f:
f.write('\n'.join(valid_lines))
return True
class OtaCameraTargetsStep(base_step.BaseStep):
"""Ota Property file step class for generating repo.prop."""
def __init__(self, product_name, variant, is_eng_build, name='camera targets check',
**kwargs):
"""Creates a OtaCameraTargets instance.
Args:
product_name: product name without variant, e.g. spencer, venus
variant: either 'eng' or 'user'
is_eng_build: if this build is eng build
name: user-visible name of this step.
**kwargs: Any additional args to pass to BaseStep.
"""
base_step.BaseStep.__init__(self, name='camera targets check', **kwargs)
self._is_eng_build = is_eng_build
self._product_name = product_name
self._variant = variant
def run(self):
eng_targets = [
'/system/bin/camera_tool',
'/system/bin/ffprobe',
'/system/bin/ffmpeg',
'/system/bin/cast_camera_hal_test'
]
general_targets = [ '/obj/lib/libcast_camera_hal.so' ]
if self._is_eng_build:
for t in eng_targets:
abs_path = f'out/target/product/{self._product_name}-{self._variant}{t}'
if not os.path.exists(abs_path):
logging.error('Missing binary %s' % abs_path)
return False
for b in general_targets:
abs_path = f'out/target/product/{self._product_name}-{self._variant}{b}'
if not os.path.exists(abs_path):
logging.error('Missing binary %s' % abs_path)
return False
return True
class OtaUsoniaDaemonStep(OtaStep):
"""Ota Property file step class for generating repo.prop."""
def __init__(self, build_name, **kwargs):
"""Creates a OtaPropStep instance.
Args:
**kwargs: Any additional args to pass to BaseStep.
"""
OtaStep.__init__(self, build_name=build_name,
name='build usonia_daemon', **kwargs)
def run(self):
"""Builds |usonia_daemon| for the |issue| and |patchset|.
Returns:
True if the usonia_daemon package is successfully built.
"""
chromium_src = 'chromium/src'
usonia_tarball = os.path.join(chromium_src,
'usonia-{}.tar.gz'.format(self.build_number))
variant = self._build_name.split('-')[-1]
command = ['chromecast/internal/usonia/package_wifi.py', '-o',
'-f', '.', self.build_number, variant]
returncode, stdout, stderr = self.exec_subprocess(
command, env=self.make_command_env(), cwd=chromium_src)
if returncode != 0:
self.exec_subprocess(['rm', '-rf', usonia_tarball])
self._error_processor(stdout, stderr)
return False
archive_dir = self._ota_archive_output_dir()
returncode, stdout, stderr = self.exec_subprocess(
['cp', '-r', usonia_tarball, archive_dir])
self.exec_subprocess(['rm', '-rf', usonia_tarball])
if returncode != 0:
self._error_processor(stdout, stderr)
return False
return True
class ThreadTelemetryServiceCrosStep(OtaStep):
"""Build step class for building Thread Telemetry Service for use in Chrome
OS."""
def __init__(self, **kwargs):
OtaStep.__init__(self, name='build thread_telemetry_service_cros', **kwargs)
def run(self):
chromium_src = 'chromium/src'
tarball = os.path.join(chromium_src,
'thread_telemetry_service-{}.tar.gz'.format(
self.build_number))
variant = self._build_name.split('-')[-1]
command = [
'chromecast/internal/device/thread/'
'build_thread_telemetry_service_cros.py',
'-o',
'-f',
variant,
'.',
self.build_number,
]
returncode, stdout, stderr = self.exec_subprocess(
command, env=self.make_command_env(), cwd=chromium_src)
if returncode != 0:
self.exec_subprocess(['rm', '-f', tarball])
self._error_processor(stdout, stderr)
return False
archive_dir = self._ota_archive_output_dir()
returncode, stdout, stderr = self.exec_subprocess(
['cp', '-r', tarball, archive_dir])
self.exec_subprocess(['rm', '-f', tarball])
if returncode != 0:
self._error_processor(stdout, stderr)
return False
return True