| # 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' |
| _NATIVE_TARGET_TOOLCHAIN = 'native_target' |
| _CRASHPAD_EXECUTABLE = '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().__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(self.loader_out_directory, |
| _NATIVE_TARGET_TOOLCHAIN, |
| _CRASHPAD_EXECUTABLE) |
| crashpad_target_dst = os.path.join(self.staging_directory, |
| _CRASHPAD_EXECUTABLE) |
| 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(self.loader_out_directory, |
| _NATIVE_TARGET_TOOLCHAIN, |
| _CRASHPAD_EXECUTABLE) |
| crashpad_target_dst = os.path.join(staging_directory_loader, |
| _CRASHPAD_EXECUTABLE) |
| 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() |