blob: c8bf2d50962afab3d7672437babff763de0ce912 [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
from starboard.tools import port_symlink
_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)
# 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.
self.combined_config = '{}_{}__{}_{}'.format(self.loader_platform,
self.loader_config, platform,
config)
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.
target_command_line_params = [
'--evergreen_library=app/{}/lib/lib{}.so'.format(
self.target_name, self.target_name),
'--evergreen_content=app/{}/content'.format(self.target_name)
]
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)
if os.path.exists(os.path.join(self.loader_out_directory, 'install')):
# TODO: Make the Linux launcher run from the install_directory
if 'linux' in self.loader_platform:
self._StageTargetsAndContentsGnLinux()
else:
self._StageTargetsAndContentsGnRaspi()
else:
self._StageTargetsAndContentsGyp()
def _StageTargetsAndContentsGnLinux(self):
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 = 'lib{}.so'.format(self.target_name)
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 _StageTargetsAndContentsGnRaspi(self):
# 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)
os.makedirs(crashpad_target_dst)
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 = 'lib{}.so'.format(self.target_name)
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 _StageTargetsAndContentsGyp(self):
# <outpath>/deploy/elf_loader_sandbox
staging_directory_loader = os.path.join(self.staging_directory, 'deploy',
self.loader_target)
# <outpath>/deploy/elf_loader_sandbox/content/app/nplb/
staging_directory_evergreen = os.path.join(staging_directory_loader,
'content', 'app',
self.target_name)
# Make a hard copy of the ELF Loader's install_directory in the location
# specified by |staging_directory_loader|. A symbolic link here would cause
# future symbolic links to fall through to the original out-directories.
shutil.copytree(
os.path.join(self.loader_out_directory, 'deploy', self.loader_target),
staging_directory_loader)
shutil.copy(
os.path.join(self.loader_out_directory, 'deploy', _CRASHPAD_TARGET,
_CRASHPAD_TARGET), staging_directory_loader)
port_symlink.MakeSymLink(
os.path.join(self.out_directory, 'deploy', self.target_name),
staging_directory_evergreen)
# TODO: Make the Linux launcher run from the install_directory, no longer
# create these symlinks, and remove the NOTE from the docstring.
port_symlink.MakeSymLink(
os.path.join(staging_directory_loader, self.loader_target),
os.path.join(self.staging_directory, self.loader_target))
port_symlink.MakeSymLink(
os.path.join(staging_directory_loader, _CRASHPAD_TARGET),
os.path.join(self.staging_directory, _CRASHPAD_TARGET))
port_symlink.MakeSymLink(
os.path.join(staging_directory_loader, 'content'),
os.path.join(self.staging_directory, 'content'))
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()