blob: 956cbb9bb0396cc67e2a2ea3eb61daf1f1e3de1c [file] [log] [blame]
# Copyright 2019 The Cobalt Authors. All Rights Reserved.
#
# 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.
"""Evergreen implementation of Starboard launcher abstraction."""
import logging
import os
import shutil
from starboard.tools import abstract_launcher
from starboard.tools import paths
_BASE_STAGING_DIRECTORY = 'evergreen_staging'
_CRASHPAD_TARGET = 'crashpad_handler'
_DEFAULT_LOADER_TARGET = 'elf_loader_sandbox'
class Launcher(abstract_launcher.AbstractLauncher):
"""Class for launching Evergreen targets on arbitrary platforms.
This Evergreen launcher leverages the platform-specific launchers to start the
Evergreen loader on a particular device. A staging directory is built that
resembles a typical install directory, with the Evergreen target and its
content included in the Evergreen loader's content. The platform-specific
launcher, given command-line switches to tell the Evergreen loader where the
Evergreen target and its content are, can run the loader without needing to
know about Evergreen per-se.
The Evergreen launcher does not require its own implementation for many
abstract launcher methods and trampolines them to the platform-specific
launcher.
"""
def __init__(self, platform, target_name, config, device_id, **kwargs):
# TODO: Remove this injection of 'detect_leaks=0' once the memory leaks when
# running executables in Evergreen mode have been resolved.
env_variables = kwargs.get('env_variables') or {}
asan_options = env_variables.get('ASAN_OPTIONS', '')
asan_options = [
opt for opt in asan_options.split(':') if 'detect_leaks' not in opt
]
asan_options.append('detect_leaks=0')
env_variables['ASAN_OPTIONS'] = ':'.join(asan_options)
kwargs['env_variables'] = env_variables
# pylint: disable=super-with-arguments
super(Launcher, self).__init__(platform, target_name, config, device_id,
**kwargs)
self.loader_platform = kwargs.get('loader_platform')
if not self.loader_platform:
raise ValueError('|loader_platform| cannot be |None|.')
self.loader_config = kwargs.get('loader_config')
if not self.loader_config:
raise ValueError('|loader_config| cannot be |None|.')
self.loader_target = kwargs.get('loader_target')
if not self.loader_target:
self.loader_target = _DEFAULT_LOADER_TARGET
self.loader_out_directory = kwargs.get('loader_out_directory')
if not self.loader_out_directory:
self.loader_out_directory = paths.BuildOutputDirectory(
self.loader_platform, self.loader_config)
self.use_compressed_library = kwargs.get('use_compressed_library')
# The relationship of loader platforms and configurations to evergreen
# platforms and configurations is many-to-many. We need a separate directory
# for each of them, i.e. linux-x64x11_debug__evergreen-x64_gold.
loader_build_name = f'{self.loader_platform}_{self.loader_config}'
target_build_name = f'{platform}_{config}'
self.combined_config = f'{loader_build_name}__{target_build_name}'
self.staging_directory = os.path.join(
os.path.dirname(self.out_directory), _BASE_STAGING_DIRECTORY,
self.combined_config)
# TODO: Figure out a place for the launcher to do this prep work that is not
# part of the constructor.
self._StageTargetsAndContents()
# Ensure the path, relative to the content of the ELF Loader, to the
# Evergreen target and its content are passed as command line switches.
library_path_value = os.path.join('app', self.target_name, 'lib',
f'lib{self.target_name}')
library_path_param = f'--evergreen_library={library_path_value}'
if self.use_compressed_library:
if self.target_name != 'cobalt':
raise ValueError(
'|use_compressed_library| only expected with |target_name| cobalt')
library_path_param += '.lz4'
else:
library_path_param += '.so'
target_command_line_params = [
library_path_param,
f'--evergreen_content=app/{self.target_name}/content'
]
if self.target_command_line_params:
target_command_line_params.extend(self.target_command_line_params)
self.launcher = abstract_launcher.LauncherFactory(
self.loader_platform,
self.loader_target,
self.loader_config,
device_id,
target_params=target_command_line_params,
output_file=self.output_file,
out_directory=self.staging_directory,
coverage_directory=self.coverage_directory,
env_variables=self.env_variables,
log_targets=False)
def Run(self):
"""Redirects to the ELF Loader platform's abstract loader implementation."""
return_code = 1
logging.info('-' * 32)
logging.info('Starting to run target: %s', self.target_name)
logging.info('=' * 32)
try:
return_code = self.launcher.Run()
except Exception: # pylint: disable=broad-except
logging.exception('Error occurred while running test.')
logging.info('-' * 32)
logging.info('Finished running target: %s', self.target_name)
logging.info('=' * 32)
return return_code
def Kill(self):
"""Redirects to the ELF Loader platform's abstract loader implementation."""
self.launcher.Kill()
def _StageTargetsAndContents(self):
"""Stage targets and their contents in our staging directory.
The existing launcher infrastructure expects a specific directory structure
regardless of whether or not we are running locally:
platform: raspi-2 raspi-2_devel
config: devel +-- install
target_name: nplb +-- nplb
+-- content
+-- nplb
This function effectively builds a directory structure that matches both of
these expectations while minimizing the hard copies made.
Note: The Linux launcher does not yet look in the install directory and
instead runs the target from the top of its out-directory. This will
be changed in the future.
"""
if os.path.exists(self.staging_directory):
shutil.rmtree(self.staging_directory)
os.makedirs(self.staging_directory)
# TODO(b/267568637): Make the Linux launcher run from the install_directory.
if 'linux' in self.loader_platform:
self._StageTargetsAndContentsLinux()
else:
self._StageTargetsAndContentsRaspi()
def _StageTargetsAndContentsLinux(self):
"""Stage targets and their contents for GN builds for Linux platforms."""
content_subdir = os.path.join('usr', 'share', 'cobalt')
# Copy loader content and binaries
loader_install_path = os.path.join(self.loader_out_directory, 'install')
loader_content_src = os.path.join(loader_install_path, content_subdir)
loader_content_dst = os.path.join(self.staging_directory, 'content')
shutil.copytree(loader_content_src, loader_content_dst)
loader_target_src = os.path.join(loader_install_path, 'bin',
self.loader_target)
loader_target_dst = os.path.join(self.staging_directory, self.loader_target)
shutil.copy(loader_target_src, loader_target_dst)
crashpad_target_src = os.path.join(loader_install_path, 'bin',
_CRASHPAD_TARGET)
crashpad_target_dst = os.path.join(self.staging_directory, _CRASHPAD_TARGET)
shutil.copy(crashpad_target_src, crashpad_target_dst)
# Copy target content and binary
target_install_path = os.path.join(self.out_directory, 'install')
target_staging_dir = os.path.join(self.staging_directory, 'content', 'app',
self.target_name)
target_content_src = os.path.join(target_install_path, content_subdir)
target_content_dst = os.path.join(target_staging_dir, 'content')
shutil.copytree(target_content_src, target_content_dst)
shlib_name = f'lib{self.target_name}'
shlib_name += '.lz4' if self.use_compressed_library else '.so'
target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
os.makedirs(os.path.join(target_staging_dir, 'lib'))
shutil.copy(target_binary_src, target_binary_dst)
def _StageTargetsAndContentsRaspi(self):
"""Stage targets and their contents for GN builds for Raspi platforms."""
# TODO(b/218889313): `content` is hardcoded on raspi and must be in the same
# directory as the binaries.
if 'raspi' in self.loader_platform:
content_subdir = 'content'
bin_subdir = ''
else:
content_subdir = os.path.join('usr', 'share', 'cobalt')
bin_subdir = 'bin'
# Copy loader content and binaries to
loader_install_path = os.path.join(self.loader_out_directory, 'install')
# raspi is a packaged platform and the loader target is the staging target.
staging_directory_loader = os.path.join(self.staging_directory, 'install',
self.loader_target)
loader_content_src = os.path.join(loader_install_path, content_subdir)
loader_content_dst = os.path.join(staging_directory_loader, 'content')
shutil.copytree(loader_content_src, loader_content_dst)
loader_binary_src = os.path.join(loader_install_path, bin_subdir)
loader_target_src = os.path.join(loader_binary_src, self.loader_target,
self.loader_target)
loader_target_dst = os.path.join(staging_directory_loader,
self.loader_target)
shutil.copy(loader_target_src, loader_target_dst)
crashpad_target_src = os.path.join(loader_binary_src, _CRASHPAD_TARGET,
_CRASHPAD_TARGET)
crashpad_target_dst = os.path.join(staging_directory_loader,
_CRASHPAD_TARGET)
shutil.copy(crashpad_target_src, crashpad_target_dst)
# Copy target content and binary
target_install_path = os.path.join(self.out_directory, 'install')
target_staging_dir = os.path.join(staging_directory_loader, 'content',
'app', self.target_name)
os.makedirs(target_staging_dir)
# TODO(b/218889313): Reset the content path for the evergreen artifacts.
content_subdir = os.path.join('usr', 'share', 'cobalt')
target_content_src = os.path.join(target_install_path, content_subdir)
target_content_dst = os.path.join(target_staging_dir, 'content')
shutil.copytree(target_content_src, target_content_dst)
shlib_name = f'lib{self.target_name}'
shlib_name += '.lz4' if self.use_compressed_library else '.so'
target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
os.makedirs(os.path.join(target_staging_dir, 'lib'))
shutil.copy(target_binary_src, target_binary_dst)
def SupportsSuspendResume(self):
return self.launcher.SupportsSuspendResume()
def SendResume(self):
return self.launcher.SendResume()
def SendSuspend(self):
return self.launcher.SendSuspend()
def SendConceal(self):
return self.launcher.SendConceal()
def SendFocus(self):
return self.launcher.SendFocus()
def SendFreeze(self):
return self.launcher.SendFreeze()
def SendStop(self):
return self.launcher.SendStop()
def SupportsDeepLink(self):
return self.launcher.SupportsDeepLink()
def SendDeepLink(self, link):
return self.launcher.SendDeepLink(link)
def GetStartupTimeout(self):
return self.launcher.GetStartupTimeout()
def GetHostAndPortGivenPort(self, port):
return self.launcher.GetHostAndPortGivenPort(port)
def GetTargetPath(self):
return self.launcher.GetTargetPath()
def GetDeviceIp(self):
return self.launcher.GetDeviceIp()
def GetDeviceOutputPath(self):
return self.launcher.GetDeviceOutputPath()