diff options
Diffstat (limited to 'infra/bots/recipe_modules/flavor')
-rw-r--r-- | infra/bots/recipe_modules/flavor/__init__.py | 15 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/android_flavor.py | 312 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/api.py | 126 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/cmake_flavor.py | 14 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/coverage_flavor.py | 75 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/default_flavor.py | 232 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/gn_flavor.py | 53 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/ios_flavor.py | 176 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/pdfium_flavor.py | 64 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/valgrind_flavor.py | 27 | ||||
-rw-r--r-- | infra/bots/recipe_modules/flavor/xsan_flavor.py | 75 |
11 files changed, 1169 insertions, 0 deletions
diff --git a/infra/bots/recipe_modules/flavor/__init__.py b/infra/bots/recipe_modules/flavor/__init__.py new file mode 100644 index 0000000000..ce433e047d --- /dev/null +++ b/infra/bots/recipe_modules/flavor/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +DEPS = [ + 'build/adb', + 'build/file', + 'recipe_engine/path', + 'recipe_engine/platform', + 'recipe_engine/python', + 'recipe_engine/raw_io', + 'recipe_engine/step', + 'run', + 'vars', +] diff --git a/infra/bots/recipe_modules/flavor/android_flavor.py b/infra/bots/recipe_modules/flavor/android_flavor.py new file mode 100644 index 0000000000..35e0b35b0b --- /dev/null +++ b/infra/bots/recipe_modules/flavor/android_flavor.py @@ -0,0 +1,312 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# pylint: disable=W0201 + + +import copy +import default_flavor + + +"""Android flavor utils, used for building for and running tests on Android.""" + + +class _ADBWrapper(object): + """Wrapper for the ADB recipe module. + + The ADB recipe module looks for the ADB binary at a path we don't have checked + out on our bots. This wrapper ensures that we set a custom ADB path before + attempting to use the module. + """ + def __init__(self, m, path_to_adb, serial_args, android_flavor): + self.m = m + self.m.adb.set_adb_path(path_to_adb) + self._has_root = False # This is set in install(). + self._serial_args = serial_args + self._wait_count = 0 + self._android_flavor = android_flavor + + def wait_for_device(self): + """Run 'adb wait-for-device'.""" + self._wait_count += 1 + cmd = [ + self._android_flavor.android_bin.join('adb_wait_for_device') + ] + self._serial_args + self.m.run( + self.m.step, + name='wait for device (%d)' % self._wait_count, + cmd=cmd, + env=self._android_flavor._default_env, + infra_step=True) + + cmd = [ + self._android_flavor.android_bin.join('adb_wait_for_charge'), + ] + self._serial_args + self.m.run( + self.m.step, + name='wait for charge (%d)' % self._wait_count, + cmd=cmd, + env=self._android_flavor._default_env, + infra_step=True) + + def maybe_wait_for_device(self): + """Run 'adb wait-for-device' if it hasn't already been run.""" + if self._wait_count == 0: + self.wait_for_device() + + def __call__(self, *args, **kwargs): + self.maybe_wait_for_device() + return self.m.run(self.m.adb, *args, **kwargs) + + +class AndroidFlavorUtils(default_flavor.DefaultFlavorUtils): + def __init__(self, m): + super(AndroidFlavorUtils, self).__init__(m) + self.device = self.m.vars.builder_spec['device_cfg'] + self.android_bin = self.m.vars.skia_dir.join( + 'platform_tools', 'android', 'bin') + self._android_sdk_root = self.m.vars.slave_dir.join( + 'android_sdk', 'android-sdk') + self.serial = None + self.serial_args = [] + try: + path_to_adb = self.m.step( + 'which adb', + cmd=['which', 'adb'], + stdout=self.m.raw_io.output(), + infra_step=True).stdout.rstrip() + except self.m.step.StepFailure: + path_to_adb = self.m.path.join(self._android_sdk_root, + 'platform-tools', 'adb') + self._adb = _ADBWrapper( + self.m, path_to_adb, self.serial_args, self) + self._default_env = {'ANDROID_SDK_ROOT': self._android_sdk_root, + 'ANDROID_HOME': self._android_sdk_root, + 'SKIA_ANDROID_VERBOSE_SETUP': 1} + + def step(self, name, cmd, env=None, **kwargs): + self._adb.maybe_wait_for_device() + args = [ + self.android_bin.join('android_run_skia'), + '--verbose', + '--logcat', + '-d', self.device, + ] + self.serial_args + [ + '-t', self.m.vars.configuration, + ] + env = dict(env or {}) + env.update(self._default_env) + + return self.m.run(self.m.step, name=name, cmd=args + cmd, + env=env, **kwargs) + + def compile(self, target): + """Build the given target.""" + env = dict(self._default_env) + ccache = self.m.run.ccache() + if ccache: + env['ANDROID_MAKE_CCACHE'] = ccache + + cmd = [self.android_bin.join('android_ninja'), target, '-d', self.device] + if 'Clang' in self.m.vars.builder_name: + cmd.append('--clang') + if 'GCC' in self.m.vars.builder_name: + cmd.append('--gcc') + if 'Vulkan' in self.m.vars.builder_name: + cmd.append('--vulkan') + self.m.run(self.m.step, 'build %s' % target, cmd=cmd, + env=env, cwd=self.m.path['checkout']) + + def device_path_join(self, *args): + """Like os.path.join(), but for paths on a connected Android device.""" + return '/'.join(args) + + def device_path_exists(self, path): + """Like os.path.exists(), but for paths on a connected device.""" + exists_str = 'FILE_EXISTS' + return exists_str in self._adb( + name='exists %s' % self.m.path.basename(path), + serial=self.serial, + cmd=['shell', 'if', '[', '-e', path, '];', + 'then', 'echo', exists_str + ';', 'fi'], + stdout=self.m.raw_io.output(), + infra_step=True + ).stdout + + def _remove_device_dir(self, path): + """Remove the directory on the device.""" + self._adb(name='rmdir %s' % self.m.path.basename(path), + serial=self.serial, + cmd=['shell', 'rm', '-r', path], + infra_step=True) + # Sometimes the removal fails silently. Verify that it worked. + if self.device_path_exists(path): + raise Exception('Failed to remove %s!' % path) # pragma: no cover + + def _create_device_dir(self, path): + """Create the directory on the device.""" + self._adb(name='mkdir %s' % self.m.path.basename(path), + serial=self.serial, + cmd=['shell', 'mkdir', '-p', path], + infra_step=True) + + def copy_directory_contents_to_device(self, host_dir, device_dir): + """Like shutil.copytree(), but for copying to a connected device.""" + self.m.run( + self.m.step, + name='push %s' % self.m.path.basename(host_dir), + cmd=[ + self.android_bin.join('adb_push_if_needed'), '--verbose', + ] + self.serial_args + [ + host_dir, device_dir, + ], + env=self._default_env, + infra_step=True) + + def copy_directory_contents_to_host(self, device_dir, host_dir): + """Like shutil.copytree(), but for copying from a connected device.""" + self.m.run( + self.m.step, + name='pull %s' % self.m.path.basename(device_dir), + cmd=[ + self.android_bin.join('adb_pull_if_needed'), '--verbose', + ] + self.serial_args + [ + device_dir, host_dir, + ], + env=self._default_env, + infra_step=True) + + def copy_file_to_device(self, host_path, device_path): + """Like shutil.copyfile, but for copying to a connected device.""" + self._adb(name='push %s' % self.m.path.basename(host_path), + serial=self.serial, + cmd=['push', host_path, device_path], + infra_step=True) + + def create_clean_device_dir(self, path): + """Like shutil.rmtree() + os.makedirs(), but on a connected device.""" + self._remove_device_dir(path) + self._create_device_dir(path) + + def has_root(self): + """Determine if we have root access on this device.""" + # Special case: GalaxyS3 hangs on `adb root`. Don't bother. + if 'GalaxyS3' in self.m.vars.builder_name: + return False + + # Determine if we have root access. + has_root = False + try: + output = self._adb(name='adb root', + serial=self.serial, + cmd=['root'], + stdout=self.m.raw_io.output(), + infra_step=True).stdout.rstrip() + if ('restarting adbd as root' in output or + 'adbd is already running as root' in output): + has_root = True + except self.m.step.StepFailure: # pragma: nocover + pass + # Wait for the device to reconnect. + self.m.run( + self.m.step, + name='wait', + cmd=['sleep', '10'], + infra_step=True) + self._adb.wait_for_device() + return has_root + + def install(self): + """Run device-specific installation steps.""" + device_scratch_dir = self._adb( + name='get EXTERNAL_STORAGE dir', + serial=self.serial, + cmd=['shell', 'echo', '$EXTERNAL_STORAGE'], + stdout=self.m.raw_io.output(), + infra_step=True, + ).stdout.rstrip() + prefix = self.device_path_join(device_scratch_dir, 'skiabot', 'skia_') + self.device_dirs = default_flavor.DeviceDirs( + dm_dir=prefix + 'dm', + perf_data_dir=prefix + 'perf', + resource_dir=prefix + 'resources', + images_dir=prefix + 'images', + skp_dir=prefix + 'skp/skps', + tmp_dir=prefix + 'tmp_dir') + + self._has_root = self.has_root() + self.m.run(self.m.step, + name='kill skia', + cmd=[ + self.android_bin.join('android_kill_skia'), + '--verbose', + ] + self.serial_args, + env=self._default_env, + infra_step=True) + if self._has_root: + self._adb(name='stop shell', + serial=self.serial, + cmd=['shell', 'stop'], + infra_step=True) + + # Print out battery stats. + self._adb(name='starting battery stats', + serial=self.serial, + cmd=['shell', 'dumpsys', 'batteryproperties'], + infra_step=True) + + # Print out CPU scale info. + if self._has_root: + self._adb(name='cat scaling_governor', + serial=self.serial, + cmd=['shell', 'cat', + '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'], + infra_step=True) + self._adb(name='cat cpu_freq', + serial=self.serial, + cmd=['shell', 'cat', + '/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq'], + infra_step=True) + + def cleanup_steps(self): + """Run any device-specific cleanup steps.""" + if self.m.vars.do_test_steps or self.m.vars.do_perf_steps: + self._adb(name='final battery stats', + serial=self.serial, + cmd=['shell', 'dumpsys', 'batteryproperties'], + infra_step=True) + self._adb(name='reboot', + serial=self.serial, + cmd=['reboot'], + infra_step=True) + self.m.run( + self.m.step, + name='wait for reboot', + cmd=['sleep', '10'], + infra_step=True) + self._adb.wait_for_device() + # The ADB binary conflicts with py-adb used by swarming. Kill it + # when finished to play nice. + self._adb(name='kill-server', + serial=self.serial, + cmd=['kill-server'], + infra_step=True) + + def read_file_on_device(self, path, *args, **kwargs): + """Read the given file.""" + return self._adb(name='read %s' % self.m.path.basename(path), + serial=self.serial, + cmd=['shell', 'cat', path], + stdout=self.m.raw_io.output(), + infra_step=True).stdout.rstrip() + + def remove_file_on_device(self, path, *args, **kwargs): + """Delete the given file.""" + return self._adb(name='rm %s' % self.m.path.basename(path), + serial=self.serial, + cmd=['shell', 'rm', '-f', path], + infra_step=True, + *args, + **kwargs) diff --git a/infra/bots/recipe_modules/flavor/api.py b/infra/bots/recipe_modules/flavor/api.py new file mode 100644 index 0000000000..fbfa9ba1cd --- /dev/null +++ b/infra/bots/recipe_modules/flavor/api.py @@ -0,0 +1,126 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# pylint: disable=W0201 + + +from recipe_engine import recipe_api + +from . import android_flavor +from . import cmake_flavor +from . import coverage_flavor +from . import default_flavor +from . import gn_flavor +from . import ios_flavor +from . import pdfium_flavor +from . import valgrind_flavor +from . import xsan_flavor + + +def is_android(builder_cfg): + """Determine whether the given builder is an Android builder.""" + return ('Android' in builder_cfg.get('extra_config', '') or + builder_cfg.get('os') == 'Android') + + +def is_cmake(builder_cfg): + return 'CMake' in builder_cfg.get('extra_config', '') + + +def is_gn(builder_cfg): + return 'GN' == builder_cfg.get('extra_config', '') + + +def is_ios(builder_cfg): + return ('iOS' in builder_cfg.get('extra_config', '') or + builder_cfg.get('os') == 'iOS') + + +def is_pdfium(builder_cfg): + return 'PDFium' in builder_cfg.get('extra_config', '') + + +def is_valgrind(builder_cfg): + return 'Valgrind' in builder_cfg.get('extra_config', '') + + +def is_xsan(builder_cfg): + return ('ASAN' in builder_cfg.get('extra_config', '') or + 'MSAN' in builder_cfg.get('extra_config', '') or + 'TSAN' in builder_cfg.get('extra_config', '')) + + +class SkiaFlavorApi(recipe_api.RecipeApi): + def get_flavor(self, builder_cfg): + """Return a flavor utils object specific to the given builder.""" + if is_android(builder_cfg): + return android_flavor.AndroidFlavorUtils(self.m) + elif is_cmake(builder_cfg): + return cmake_flavor.CMakeFlavorUtils(self.m) + elif is_gn(builder_cfg): + return gn_flavor.GNFlavorUtils(self.m) + elif is_ios(builder_cfg): + return ios_flavor.iOSFlavorUtils(self.m) + elif is_pdfium(builder_cfg): + return pdfium_flavor.PDFiumFlavorUtils(self.m) + elif is_valgrind(builder_cfg): + return valgrind_flavor.ValgrindFlavorUtils(self.m) + elif is_xsan(builder_cfg): + return xsan_flavor.XSanFlavorUtils(self.m) + elif builder_cfg.get('configuration') == 'Coverage': + return coverage_flavor.CoverageFlavorUtils(self.m) + else: + return default_flavor.DefaultFlavorUtils(self.m) + + def setup(self): + self._f = self.get_flavor(self.m.vars.builder_cfg) + + def step(self, name, cmd, **kwargs): + return self._f.step(name, cmd, **kwargs) + + def compile(self, target): + return self._f.compile(target) + + def copy_extra_build_products(self, swarming_out_dir): + return self._f.copy_extra_build_products(swarming_out_dir) + + @property + def out_dir(self): + return self._f.out_dir + + def device_path_join(self, *args): + return self._f.device_path_join(*args) + + def device_path_exists(self, path): + return self._f.device_path_exists(path) # pragma: no cover + + def copy_directory_contents_to_device(self, host_dir, device_dir): + return self._f.copy_directory_contents_to_device(host_dir, device_dir) + + def copy_directory_contents_to_host(self, device_dir, host_dir): + return self._f.copy_directory_contents_to_host(device_dir, host_dir) + + def copy_file_to_device(self, host_path, device_path): + return self._f.copy_file_to_device(host_path, device_path) + + def create_clean_host_dir(self, path): + return self._f.create_clean_host_dir(path) + + def create_clean_device_dir(self, path): + return self._f.create_clean_device_dir(path) + + def read_file_on_device(self, path): + return self._f.read_file_on_device(path) + + def remove_file_on_device(self, path): + return self._f.remove_file_on_device(path) + + def install(self): + rv = self._f.install() + self.device_dirs = self._f.device_dirs + return rv + + def cleanup_steps(self): + return self._f.cleanup_steps() diff --git a/infra/bots/recipe_modules/flavor/cmake_flavor.py b/infra/bots/recipe_modules/flavor/cmake_flavor.py new file mode 100644 index 0000000000..8b254eceb7 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/cmake_flavor.py @@ -0,0 +1,14 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import default_flavor + +"""CMake flavor utils, used for building Skia with CMake.""" + +class CMakeFlavorUtils(default_flavor.DefaultFlavorUtils): + def compile(self, target): + """Build Skia with CMake. Ignores `target`.""" + cmake_build = self.m.vars.skia_dir.join('cmake', 'cmake_build') + self.m.run(self.m.step, 'cmake_build', cmd=[cmake_build], + cwd=self.m.path['checkout']) diff --git a/infra/bots/recipe_modules/flavor/coverage_flavor.py b/infra/bots/recipe_modules/flavor/coverage_flavor.py new file mode 100644 index 0000000000..a3bb5828ce --- /dev/null +++ b/infra/bots/recipe_modules/flavor/coverage_flavor.py @@ -0,0 +1,75 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +import datetime +import default_flavor +import posixpath + + +"""Utils for running coverage tests.""" + + +class CoverageFlavorUtils(default_flavor.DefaultFlavorUtils): + + def step(self, name, cmd, **kwargs): + """Run the given step through coverage.""" + compile_target = 'dm' + build_cmd = [self.m.vars.skia_dir.join('tools', 'llvm_coverage_build'), + compile_target] + self.m.run(self.m.step, + 'build %s' % compile_target, + cmd=build_cmd, + cwd=self.m.path['checkout']) + + # Slice out the 'key' and 'properties' arguments to be reused. + key = [] + properties = [] + current = None + for i in xrange(0, len(cmd)): + if isinstance(cmd[i], basestring) and cmd[i] == '--key': + current = key + elif isinstance(cmd[i], basestring) and cmd[i] == '--properties': + current = properties + elif isinstance(cmd[i], basestring) and cmd[i].startswith('--'): + current = None + if current is not None: + current.append(cmd[i]) + + results_dir = self.m.vars.skia_out.join('coverage_results') + self.create_clean_host_dir(results_dir) + + # Run DM under coverage. + report_file_basename = '%s.cov' % self.m.vars.got_revision + report_file = results_dir.join(report_file_basename) + args = [ + 'python', + self.m.vars.skia_dir.join('tools', 'llvm_coverage_run.py'), + ] + cmd + ['--outResultsFile', report_file] + self.m.run(self.m.step, name=name, cmd=args, + cwd=self.m.path['checkout'], **kwargs) + + # Generate nanobench-style JSON output from the coverage report. + nanobench_json = results_dir.join('nanobench_%s.json' % ( + self.m.vars.got_revision)) + line_by_line_basename = ('coverage_by_line_%s.json' % ( + self.m.vars.got_revision)) + line_by_line = results_dir.join(line_by_line_basename) + args = [ + 'python', + self.m.vars.skia_dir.join('tools', 'parse_llvm_coverage.py'), + '--report', report_file, '--nanobench', nanobench_json, + '--linebyline', line_by_line] + args.extend(key) + args.extend(properties) + self.m.run( + self.m.step, + 'Generate Coverage Data', + cmd=args, cwd=self.m.path['checkout']) + + # Copy files from results_dir into swarming_out_dir. + for r in self.m.file.listdir('results_dir', results_dir): + self.m.file.copy( + 'Copy to swarming out', results_dir.join(r), + self.m.vars.swarming_out_dir) diff --git a/infra/bots/recipe_modules/flavor/default_flavor.py b/infra/bots/recipe_modules/flavor/default_flavor.py new file mode 100644 index 0000000000..4cdbaaa671 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/default_flavor.py @@ -0,0 +1,232 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# pylint: disable=W0201 + + +"""Default flavor utils class, used for desktop builders.""" + + +import json + + +WIN_TOOLCHAIN_DIR = 't' + + +class DeviceDirs(object): + def __init__(self, + dm_dir, + perf_data_dir, + resource_dir, + images_dir, + skp_dir, + tmp_dir): + self._dm_dir = dm_dir + self._perf_data_dir = perf_data_dir + self._resource_dir = resource_dir + self._images_dir = images_dir + self._skp_dir = skp_dir + self._tmp_dir = tmp_dir + + @property + def dm_dir(self): + """Where DM writes.""" + return self._dm_dir + + @property + def perf_data_dir(self): + return self._perf_data_dir + + @property + def resource_dir(self): + return self._resource_dir + + @property + def images_dir(self): + return self._images_dir + + @property + def skp_dir(self): + """Holds SKP files that are consumed by RenderSKPs and BenchPictures.""" + return self._skp_dir + + @property + def tmp_dir(self): + return self._tmp_dir + + +class DefaultFlavorUtils(object): + """Utilities to be used by build steps. + + The methods in this class define how certain high-level functions should + work. Each build step flavor should correspond to a subclass of + DefaultFlavorUtils which may override any of these functions as appropriate + for that flavor. + + For example, the AndroidFlavorUtils will override the functions for + copying files between the host and Android device, as well as the + 'step' function, so that commands may be run through ADB. + """ + def __init__(self, m): + self.m = m + self._chrome_path = None + self._win_toolchain_dir = self.m.vars.slave_dir.join(WIN_TOOLCHAIN_DIR) + win_toolchain_asset_path = self.m.vars.infrabots_dir.join( + 'assets', 'win_toolchain', 'VERSION') + if not self.m.path.exists(win_toolchain_asset_path): + self._win_toolchain_dir = self.m.vars.slave_dir + + + def step(self, name, cmd, **kwargs): + """Wrapper for the Step API; runs a step as appropriate for this flavor.""" + path_to_app = self.m.vars.skia_out.join( + self.m.vars.configuration, cmd[0]) + if (self.m.platform.is_linux and + 'x86_64' in self.m.vars.builder_name and + not 'TSAN' in self.m.vars.builder_name): + new_cmd = ['catchsegv', path_to_app] + else: + new_cmd = [path_to_app] + new_cmd.extend(cmd[1:]) + return self.m.run(self.m.step, + name, cmd=new_cmd, **kwargs) + + @property + def chrome_path(self): + """Path to a checkout of Chrome on this machine.""" + return self._win_toolchain_dir.join('src') + + def bootstrap_win_toolchain(self): + """Run bootstrapping script for the Windows toolchain.""" + bootstrap_script = self.m.vars.infrabots_dir.join( + 'bootstrap_win_toolchain_json.py') + win_toolchain_json = self._win_toolchain_dir.join( + 'src', 'build', 'win_toolchain.json') + self.m.python( + 'bootstrap win toolchain', + script=bootstrap_script, + args=['--win_toolchain_json', win_toolchain_json, + '--depot_tools_parent_dir', + self._win_toolchain_dir]) + + def build_command_buffer(self): + """Build command_buffer.""" + script = self.m.vars.skia_dir.join('tools', 'build_command_buffer.py') + self.m.run( + self.m.python, 'build command_buffer', + script=script, + args=['--chrome-dir', self.m.vars.checkout_root, + '--output-dir', self.out_dir, + '--chrome-build-type', self.m.vars.configuration, + '--no-sync']) + + def compile(self, target): + """Build the given target.""" + # The CHROME_PATH environment variable is needed for builders that use + # toolchains downloaded by Chrome. + env = {'CHROME_PATH': self.chrome_path} + if self.m.platform.is_win: + make_cmd = ['python', 'make.py'] + self.m.run.run_once(self.bootstrap_win_toolchain) + if 'Vulkan' in self.m.vars.builder_name: + env['VK_SDK_PATH'] = self.m.vars.slave_dir.join('win_vulkan_sdk') + else: + make_cmd = ['make'] + cmd = make_cmd + [target] + try: + self.m.run(self.m.step, 'build %s' % target, cmd=cmd, + env=env, cwd=self.m.path['checkout']) + except self.m.step.StepFailure: + if self.m.platform.is_win: + # The linker occasionally crashes on Windows. Try again. + self.m.run(self.m.step, 'build %s' % target, cmd=cmd, + env=env, cwd=self.m.path['checkout']) + else: + raise + if 'CommandBuffer' in self.m.vars.builder_name: + self.m.run.run_once(self.build_command_buffer) + + def copy_extra_build_products(self, swarming_out_dir): + """Copy extra build products to specified directory. + + Copy flavor-specific build products to swarming_out_dir for use in test and + perf steps.""" + if ("Win" in self.m.vars.builder_name and + "Vulkan" in self.m.vars.builder_name): + # This copies vulkan-1.dll that has been bundled into win_vulkan_sdk + # since version 2 See skia/api BUILD_PRODUCTS_ISOLATE_WHITELIST + self.m.run.copy_build_products( + self.m.path['slave_build'].join('win_vulkan_sdk'), + swarming_out_dir) + + @property + def out_dir(self): + """Flavor-specific out directory.""" + return self.m.vars.skia_out.join(self.m.vars.configuration) + + def device_path_join(self, *args): + """Like os.path.join(), but for paths on a connected device.""" + return self.m.path.join(*args) + + def device_path_exists(self, path): # pragma: no cover + """Like os.path.exists(), but for paths on a connected device.""" + return self.m.path.exists(path, infra_step=True) + + def copy_directory_contents_to_device(self, host_dir, device_dir): + """Like shutil.copytree(), but for copying to a connected device.""" + # For "normal" builders who don't have an attached device, we expect + # host_dir and device_dir to be the same. + if str(host_dir) != str(device_dir): + raise ValueError('For builders who do not have attached devices, copying ' + 'from host to device is undefined and only allowed if ' + 'host_path and device_path are the same (%s vs %s).' % ( + str(host_dir), str(device_dir))) # pragma: no cover + + def copy_directory_contents_to_host(self, device_dir, host_dir): + """Like shutil.copytree(), but for copying from a connected device.""" + # For "normal" builders who don't have an attached device, we expect + # host_dir and device_dir to be the same. + if str(host_dir) != str(device_dir): + raise ValueError('For builders who do not have attached devices, copying ' + 'from device to host is undefined and only allowed if ' + 'host_path and device_path are the same (%s vs %s).' % ( + str(host_dir), str(device_dir))) # pragma: no cover + + def copy_file_to_device(self, host_path, device_path): + """Like shutil.copyfile, but for copying to a connected device.""" + # For "normal" builders who don't have an attached device, we expect + # host_dir and device_dir to be the same. + if str(host_path) != str(device_path): # pragma: no cover + raise ValueError('For builders who do not have attached devices, copying ' + 'from host to device is undefined and only allowed if ' + 'host_path and device_path are the same (%s vs %s).' % ( + str(host_path), str(device_path))) + + def create_clean_device_dir(self, path): + """Like shutil.rmtree() + os.makedirs(), but on a connected device.""" + self.create_clean_host_dir(path) + + def create_clean_host_dir(self, path): + """Convenience function for creating a clean directory.""" + self.m.run.rmtree(path) + self.m.file.makedirs( + self.m.path.basename(path), path, infra_step=True) + + def install(self): + """Run device-specific installation steps.""" + self.device_dirs = DeviceDirs( + dm_dir=self.m.vars.dm_dir, + perf_data_dir=self.m.vars.perf_data_dir, + resource_dir=self.m.vars.resource_dir, + images_dir=self.m.vars.images_dir, + skp_dir=self.m.vars.local_skp_dir, + tmp_dir=self.m.vars.tmp_dir) + + def cleanup_steps(self): + """Run any device-specific cleanup steps.""" + pass + + def __repr__(self): + return '<%s object>' % self.__class__.__name__ # pragma: no cover diff --git a/infra/bots/recipe_modules/flavor/gn_flavor.py b/infra/bots/recipe_modules/flavor/gn_flavor.py new file mode 100644 index 0000000000..ea5bbb6934 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/gn_flavor.py @@ -0,0 +1,53 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import default_flavor + +"""GN flavor utils, used for building Skia with GN.""" +class GNFlavorUtils(default_flavor.DefaultFlavorUtils): + def compile(self, target): + """Build Skia with GN.""" + # Get the gn executable. + fetch_gn = self.m.vars.skia_dir.join('bin', 'fetch-gn') + self.m.run(self.m.step, 'fetch-gn', + cmd=[fetch_gn], + cwd=self.m.vars.skia_dir) + + is_debug = 'is_debug=true' + if self.m.vars.configuration != 'Debug': + is_debug = 'is_debug=false' + gn_args = [is_debug] + + is_clang = 'Clang' in self.m.vars.builder_name + is_gcc = 'GCC' in self.m.vars.builder_name + + cc, cxx = 'cc', 'c++' + if is_clang: + cc, cxx = 'clang', 'clang++' + elif is_gcc: + cc, cxx = 'gcc', 'g++' + + ccache = self.m.run.ccache() + if ccache: + cc, cxx = '%s %s' % (ccache, cc), '%s %s' % (ccache, cxx) + if is_clang: + # Stifle "argument unused during compilation: ..." warnings. + stifle = '-Qunused-arguments' + cc, cxx = '%s %s' % (cc, stifle), '%s %s' % (cxx, stifle) + + gn_args += [ 'cc="%s"' % cc, 'cxx="%s"' % cxx ] + + # Run gn gen. + gn_exe = 'gn' + if self.m.platform.is_win: + gn_exe = 'gn.exe' + gn_gen = [gn_exe, 'gen', self.out_dir, '--args=%s' % ' '.join(gn_args)] + self.m.run(self.m.step, 'gn_gen', cmd=gn_gen, + cwd=self.m.vars.skia_dir) + + # Run ninja. + ninja_cmd = ['ninja', '-C', self.out_dir] + self.m.run(self.m.step, 'compile %s' % target, + cmd=ninja_cmd, + cwd=self.m.vars.skia_dir) diff --git a/infra/bots/recipe_modules/flavor/ios_flavor.py b/infra/bots/recipe_modules/flavor/ios_flavor.py new file mode 100644 index 0000000000..d0d60f4284 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/ios_flavor.py @@ -0,0 +1,176 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# pylint: disable=W0201 + + +import copy +import default_flavor + + +"""iOS flavor utils, used for building for and running tests on iOS.""" + + +class iOSFlavorUtils(default_flavor.DefaultFlavorUtils): + def __init__(self, m): + super(iOSFlavorUtils, self).__init__(m) + self.default_env = {} + self.default_env['XCODEBUILD'] = ( + self.m.vars.slave_dir.join('xcodebuild')) + self.ios_bin = self.m.vars.skia_dir.join( + 'platform_tools', 'ios', 'bin') + + def step(self, name, cmd, **kwargs): + args = [self.ios_bin.join('ios_run_skia')] + env = {} + env.update(kwargs.pop('env', {})) + env.update(self.default_env) + # Convert 'dm' and 'nanobench' from positional arguments + # to flags, which is what iOSShell expects to select which + # one is being run. + cmd = ["--" + c if c in ['dm', 'nanobench'] else c + for c in cmd] + return self.m.run(self.m.step, name=name, cmd=args + cmd, + env=env, **kwargs) + + def compile(self, target): + """Build the given target.""" + cmd = [self.ios_bin.join('ios_ninja')] + self.m.run(self.m.step, 'build iOSShell', cmd=cmd, + cwd=self.m.path['checkout']) + + def device_path_join(self, *args): + """Like os.path.join(), but for paths on a connected iOS device.""" + return '/'.join(args) + + def device_path_exists(self, path): + """Like os.path.exists(), but for paths on a connected device.""" + return self.m.run( + self.m.step, + 'exists %s' % path, + cmd=[self.ios_bin.join('ios_path_exists'), path], + env=self.default_env, + infra_step=True, + ) # pragma: no cover + + def _remove_device_dir(self, path): + """Remove the directory on the device.""" + return self.m.run( + self.m.step, + 'rmdir %s' % path, + cmd=[self.ios_bin.join('ios_rm'), path], + env=self.default_env, + infra_step=True, + ) + + def _create_device_dir(self, path): + """Create the directory on the device.""" + return self.m.run( + self.m.step, + 'mkdir %s' % path, + cmd=[self.ios_bin.join('ios_mkdir'), path], + env=self.default_env, + infra_step=True, + ) + + def copy_directory_contents_to_device(self, host_dir, device_dir): + """Like shutil.copytree(), but for copying to a connected device.""" + return self.m.run( + self.m.step, + name='push %s to %s' % (self.m.path.basename(host_dir), + self.m.path.basename(device_dir)), + cmd=[self.ios_bin.join('ios_push_if_needed'), + host_dir, device_dir], + env=self.default_env, + infra_step=True, + ) + + def copy_directory_contents_to_host(self, device_dir, host_dir): + """Like shutil.copytree(), but for copying from a connected device.""" + self.m.run( + self.m.step, + name='pull %s' % self.m.path.basename(device_dir), + cmd=[self.ios_bin.join('ios_pull_if_needed'), + device_dir, host_dir], + env=self.default_env, + infra_step=True, + ) + + def copy_file_to_device(self, host_path, device_path): + """Like shutil.copyfile, but for copying to a connected device.""" + self.m.run( + self.m.step, + name='push %s' % host_path, + cmd=[self.ios_bin.join('ios_push_file'), host_path, device_path], + env=self.default_env, + infra_step=True, + ) # pragma: no cover + + def copy_extra_build_products(self, swarming_out_dir): + xcode_dir = self.m.path.join( + 'xcodebuild', '%s-iphoneos' % self.m.vars.configuration) + self.m.run.copy_build_products( + self.m.vars.skia_dir.join(xcode_dir), + swarming_out_dir.join(xcode_dir)) + + def create_clean_device_dir(self, path): + """Like shutil.rmtree() + os.makedirs(), but on a connected device.""" + self._remove_device_dir(path) + self._create_device_dir(path) + + def install(self): + """Run device-specific installation steps.""" + prefix = self.device_path_join('skiabot', 'skia_') + self.device_dirs = default_flavor.DeviceDirs( + dm_dir=prefix + 'dm', + perf_data_dir=prefix + 'perf', + resource_dir=prefix + 'resources', + images_dir=prefix + 'images', + skp_dir=prefix + 'skp/skps', + tmp_dir=prefix + 'tmp_dir') + + self.m.run( + self.m.step, + name='install iOSShell', + cmd=[self.ios_bin.join('ios_install')], + env=self.default_env, + infra_step=True) + + def cleanup_steps(self): + """Run any device-specific cleanup steps.""" + if self.m.vars.do_test_steps or self.m.vars.do_perf_steps: + self.m.run( + self.m.step, + name='reboot', + cmd=[self.ios_bin.join('ios_restart')], + env=self.default_env, + infra_step=True) + self.m.run( + self.m.step, + name='wait for reboot', + cmd=['sleep', '20'], + env=self.default_env, + infra_step=True) + + def read_file_on_device(self, path): + """Read the given file.""" + ret = self.m.run( + self.m.step, + name='read %s' % self.m.path.basename(path), + cmd=[self.ios_bin.join('ios_cat_file'), path], + env=self.default_env, + stdout=self.m.raw_io.output(), + infra_step=True) + return ret.stdout.rstrip() if ret.stdout else ret.stdout + + def remove_file_on_device(self, path): + """Remove the file on the device.""" + return self.m.run( + self.m.step, + 'rm %s' % path, + cmd=[self.ios_bin.join('ios_rm'), path], + env=self.default_env, + infra_step=True, + ) diff --git a/infra/bots/recipe_modules/flavor/pdfium_flavor.py b/infra/bots/recipe_modules/flavor/pdfium_flavor.py new file mode 100644 index 0000000000..ce11aeac2e --- /dev/null +++ b/infra/bots/recipe_modules/flavor/pdfium_flavor.py @@ -0,0 +1,64 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import re + +import default_flavor + + +"""PDFium flavor utils, used for building PDFium with Skia.""" + + +class PDFiumFlavorUtils(default_flavor.DefaultFlavorUtils): + + def compile(self, target): + """Build PDFium with Skia.""" + pdfium_dir = self.m.vars.checkout_root.join('pdfium') + + # Runhook to generate the gn binary in buildtools. + self.m.run( + self.m.step, + 'runhook', + cmd=['gclient', 'runhook', 'gn_linux64'], + cwd=pdfium_dir) + + # Setup gn args. + gn_args = ['pdf_use_skia=true', 'pdf_is_standalone=true', + 'clang_use_chrome_plugins=false'] + self.m.run( + self.m.step, + 'gn_gen', + cmd=['gn', 'gen', 'out/skia', '--args=%s' % ' '.join(gn_args)], + cwd=pdfium_dir, + env={'CHROMIUM_BUILDTOOLS_PATH': str(pdfium_dir.join('buildtools'))}) + + # Modify DEPS file to contain the current Skia revision. + skia_revision = self.m.vars.got_revision + deps_file = pdfium_dir.join('DEPS') + test_data = "'skia_revision': 'abc'" + + original_contents = self.m.file.read( + 'read PDFium DEPS', deps_file, test_data=test_data, infra_step=True) + + deps_skia_regexp = re.compile( + r'(?<=["\']skia_revision["\']: ["\'])([a-fA-F0-9]+)(?=["\'])', + re.MULTILINE) + patched_contents = re.sub(deps_skia_regexp, str(skia_revision), + original_contents) + self.m.file.write('write PDFium DEPs', deps_file, + patched_contents, infra_step=True) + + # gclient sync after updating DEPS. + self.m.run( + self.m.step, + 'sync_pdfium', + cmd=['gclient', 'sync'], + cwd=pdfium_dir) + + # Build PDFium. + self.m.run( + self.m.step, + 'build_pdfium', + cmd=['ninja', '-C', 'out/skia', '-j100'], + cwd=pdfium_dir) diff --git a/infra/bots/recipe_modules/flavor/valgrind_flavor.py b/infra/bots/recipe_modules/flavor/valgrind_flavor.py new file mode 100644 index 0000000000..2dea3e7ab4 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/valgrind_flavor.py @@ -0,0 +1,27 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +import default_flavor + + +"""Utils for running under Valgrind.""" + + +class ValgrindFlavorUtils(default_flavor.DefaultFlavorUtils): + def __init__(self, m): + super(ValgrindFlavorUtils, self).__init__(m) + self._suppressions_file = self.m.vars.skia_dir.join( + 'tools', 'valgrind.supp') + + def step(self, name, cmd, **kwargs): + new_cmd = ['valgrind', '--gen-suppressions=all', '--leak-check=full', + '--track-origins=yes', '--error-exitcode=1', '--num-callers=40', + '--suppressions=%s' % self._suppressions_file] + path_to_app = self.out_dir.join(cmd[0]) + new_cmd.append(path_to_app) + new_cmd.extend(cmd[1:]) + return self.m.run(self.m.step, name, cmd=new_cmd, + **kwargs) + diff --git a/infra/bots/recipe_modules/flavor/xsan_flavor.py b/infra/bots/recipe_modules/flavor/xsan_flavor.py new file mode 100644 index 0000000000..89169b77c1 --- /dev/null +++ b/infra/bots/recipe_modules/flavor/xsan_flavor.py @@ -0,0 +1,75 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +"""Utils for running under *SAN""" + + +import default_flavor + + +class XSanFlavorUtils(default_flavor.DefaultFlavorUtils): + def __init__(self, m): + super(XSanFlavorUtils, self).__init__(m) + self._sanitizer = { + # We'd love to just pass 'address,undefined' and get all the checks, but + # we're not anywhere close to being able to do that. Instead we start + # with a set of checks that we know pass or nearly pass. See here for + # more information: + # http://clang.llvm.org/docs/UsersManual.html#controlling-code-generation + 'ASAN': ('address,bool,function,integer-divide-by-zero,nonnull-attribute,' + 'null,object-size,return,returns-nonnull-attribute,shift,' + 'signed-integer-overflow,unreachable,vla-bound,vptr'), + # MSAN and TSAN can't run together with ASAN, so they're their own bots. + 'MSAN': 'memory', + 'TSAN': 'thread', + }[self.m.vars.builder_cfg['extra_config'].replace('Swarming', '')] + + def compile(self, target): + cmd = [self.m.vars.skia_dir.join('tools', 'xsan_build'), + self._sanitizer, target] + self.m.run(self.m.step, 'build %s' % target, cmd=cmd, + cwd=self.m.vars.skia_dir) + + def copy_extra_build_products(self, swarming_out_dir): + # Include msan_out if MSAN. + if 'MSAN' in self.m.vars.builder_cfg['extra_config']: + msan_out = self.m.path.join( + 'third_party', 'externals', 'llvm', 'msan_out') + self.m.file.copytree( + 'copy msan_out', + self.m.vars.skia_dir.join(msan_out), + swarming_out_dir.join(msan_out), + symlinks=True) + # Include llvm_symbolizer from the Chromium DEPS so that suppressions work + # by symbol name. + # TODO(benjaminwagner): Figure out how to add this to Skia DEPS for + # target_os 'llvm'. + self.m.file.copytree( + 'copy llvm-build', + self.m.vars.checkout_root.join('src', 'third_party', 'llvm-build'), + swarming_out_dir.join('llvm-build'), + symlinks=True) + + def step(self, name, cmd, env=None, **kwargs): + """Wrapper for the Step API; runs a step as appropriate for this flavor.""" + skia_dir = self.m.vars.skia_dir + lsan_suppressions = skia_dir.join('tools', 'lsan.supp') + tsan_suppressions = skia_dir.join('tools', 'tsan.supp') + ubsan_suppressions = skia_dir.join('tools', 'ubsan.supp') + env = dict(env or {}) + env['ASAN_OPTIONS'] = 'symbolize=1 detect_leaks=1' + env['LSAN_OPTIONS'] = ('symbolize=1 print_suppressions=1 suppressions=%s' % + lsan_suppressions) + env['TSAN_OPTIONS'] = 'suppressions=%s' % tsan_suppressions + env['UBSAN_OPTIONS'] = 'suppressions=%s' % ubsan_suppressions + self.m.vars.default_env['PATH'] = '%%(PATH)s:%s' % ( + self.m.vars.slave_dir.join('llvm-build', 'Release+Asserts', 'bin')) + env['LD_LIBRARY_PATH'] = self.m.vars.slave_dir.join( + 'third_party', 'externals', 'llvm', 'msan_out', 'lib') + + path_to_app = self.out_dir.join(cmd[0]) + new_cmd = [path_to_app] + new_cmd.extend(cmd[1:]) + return self.m.run(self.m.step, name, cmd=new_cmd, env=env, **kwargs) |