| #!/usr/bin/python |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| |
| """Library containing utility functions used for Chrome-specific build tasks.""" |
| |
| |
| import functools |
| import glob |
| import logging |
| import os |
| import re |
| import shlex |
| import shutil |
| |
| from chromite.buildbot import cbuildbot_results as results_lib |
| from chromite.lib import cros_build_lib |
| from chromite.lib import osutils |
| |
| |
| # Taken from external/gyp.git/pylib. |
| def _NameValueListToDict(name_value_list): |
| """Converts Name-Value list to dictionary. |
| |
| Takes an array of strings of the form 'NAME=VALUE' and creates a dictionary |
| of the pairs. If a string is simply NAME, then the value in the dictionary |
| is set to True. If VALUE can be converted to an integer, it is. |
| """ |
| result = { } |
| for item in name_value_list: |
| tokens = item.split('=', 1) |
| if len(tokens) == 2: |
| # If we can make it an int, use that, otherwise, use the string. |
| try: |
| token_value = int(tokens[1]) |
| except ValueError: |
| token_value = tokens[1] |
| # Set the variable to the supplied value. |
| result[tokens[0]] = token_value |
| else: |
| # No value supplied, treat it as a boolean and set it. |
| result[tokens[0]] = True |
| return result |
| |
| |
| def ProcessGypDefines(defines): |
| """Validate and convert a string containing GYP_DEFINES to dictionary.""" |
| assert defines is not None |
| return _NameValueListToDict(shlex.split(defines)) |
| |
| |
| def DictToGypDefines(def_dict): |
| """Convert a dict to GYP_DEFINES format.""" |
| def_list = [] |
| for k, v in def_dict.iteritems(): |
| def_list.append("%s='%s'" % (k, v)) |
| return ' '.join(def_list) |
| |
| |
| class Conditions(object): |
| """Functions that return conditions used to construct Path objects. |
| |
| Condition functions returned by the public methods have signature |
| f(gyp_defines, staging_flags). For description of gyp_defines and |
| staging_flags see docstring for StageChromeFromBuildDir(). |
| """ |
| |
| @classmethod |
| def _GypSet(cls, flag, value, gyp_defines, _staging_flags): |
| val = gyp_defines.get(flag) |
| return val == value if value is not None else bool(val) |
| |
| @classmethod |
| def _GypNotSet(cls, flag, gyp_defines, staging_flags): |
| return not cls._GypSet(flag, None, gyp_defines, staging_flags) |
| |
| @classmethod |
| def _StagingFlagSet(cls, flag, _gyp_defines, staging_flags): |
| return flag in staging_flags |
| |
| @classmethod |
| def GypSet(cls, flag, value=None): |
| """Returns condition that tests a gyp flag is set (possibly to a value).""" |
| return functools.partial(cls._GypSet, flag, value) |
| |
| @classmethod |
| def GypNotSet(cls, flag): |
| """Returns condition that tests a gyp flag is not set.""" |
| return functools.partial(cls._GypNotSet, flag) |
| |
| @classmethod |
| def StagingFlagSet(cls, flag): |
| """Returns condition that tests a staging_flag is set.""" |
| return functools.partial(cls._StagingFlagSet, flag) |
| |
| |
| class MultipleMatchError(results_lib.StepFailure): |
| """A glob pattern matches multiple files but a non-dir dest was specified.""" |
| |
| |
| class MissingPathError(results_lib.StepFailure): |
| """An expected path is non-existant.""" |
| |
| |
| class MustNotBeDirError(results_lib.StepFailure): |
| """The specified path should not be a directory, but is.""" |
| |
| |
| class Copier(object): |
| """File/directory copier. |
| |
| Provides destination stripping and permission setting functionality. |
| """ |
| |
| DEFAULT_BLACKLIST = (r'(^|.*/)\.svn($|/.*)',) |
| |
| def __init__(self, strip_bin=None, strip_flags=None, default_mode=0o644, |
| dir_mode=0o755, exe_mode=0o755, blacklist=None): |
| """Initialization. |
| |
| Args: |
| strip_bin: Path to the program used to strip binaries. If set to None, |
| binaries will not be stripped. |
| strip_flags: A list of flags to pass to the |strip_bin| executable. |
| default_mode: Default permissions to set on files. |
| dir_mode: Mode to set for directories. |
| exe_mode: Permissions to set on executables. |
| blacklist: A list of path patterns to ignore during the copy. |
| """ |
| self.strip_bin = strip_bin |
| self.strip_flags = strip_flags |
| self.default_mode = default_mode |
| self.dir_mode = dir_mode |
| self.exe_mode = exe_mode |
| self.blacklist = blacklist |
| if self.blacklist is None: |
| self.blacklist = self.DEFAULT_BLACKLIST |
| |
| @staticmethod |
| def Log(src, dest, directory): |
| sep = ' [d] -> ' if directory else ' -> ' |
| logging.debug('%s %s %s', src, sep, dest) |
| |
| def _PathIsBlacklisted(self, path): |
| for pattern in self.blacklist: |
| if re.match(pattern, path): |
| return True |
| return False |
| |
| def _CopyFile(self, src, dest, path): |
| """Perform the copy. |
| |
| Args: |
| src: The path of the file/directory to copy. |
| dest: The exact path of the destination. Should not already exist. |
| path: The Path instance containing copy operation modifiers (such as |
| Path.exe, Path.strip, etc.) |
| """ |
| assert not os.path.isdir(src), '%s: Not expecting a directory!' % src |
| osutils.SafeMakedirs(os.path.dirname(dest), mode=self.dir_mode) |
| if path.exe and self.strip_bin and path.strip and os.path.getsize(src) > 0: |
| strip_flags = (['--strip-unneeded'] if self.strip_flags is None else |
| self.strip_flags) |
| cros_build_lib.DebugRunCommand( |
| [self.strip_bin] + strip_flags + ['-o', dest, src]) |
| shutil.copystat(src, dest) |
| else: |
| shutil.copy2(src, dest) |
| |
| mode = path.mode |
| if mode is None: |
| mode = self.exe_mode if path.exe else self.default_mode |
| os.chmod(dest, mode) |
| |
| def Copy(self, src_base, dest_base, path, strict=False, sloppy=False): |
| """Copy artifact(s) from source directory to destination. |
| |
| Args: |
| src_base: The directory to apply the src glob pattern match in. |
| dest_base: The directory to copy matched files to. |Path.dest|. |
| path: A Path instance that specifies what is to be copied. |
| strict: If set, enforce that all optional files are copied. |
| sloppy: If set, ignore when mandatory artifacts are missing. |
| |
| Returns: |
| A list of the artifacts copied. |
| """ |
| assert not (strict and sloppy), 'strict and sloppy are not compatible.' |
| copied_paths = [] |
| src = os.path.join(src_base, path.src) |
| if not src.endswith('/') and os.path.isdir(src): |
| raise MustNotBeDirError('%s must not be a directory\n' |
| 'Aborting copy...' % (src,)) |
| paths = glob.glob(src) |
| if not paths: |
| if ((strict and not path.optional) or |
| (not strict and not (path.optional or path.cond) and not sloppy)): |
| msg = ('%s does not exist and is required.\n' |
| 'You can bypass this error with --sloppy.\n' |
| 'Aborting copy...' % src) |
| raise MissingPathError(msg) |
| elif path.optional or (not strict and path.cond): |
| logging.debug('%s does not exist and is optional. Skipping.', src) |
| else: |
| logging.warn('%s does not exist and is required. Skipping anyway.', |
| src) |
| elif len(paths) > 1 and path.dest and not path.dest.endswith('/'): |
| raise MultipleMatchError( |
| 'Glob pattern %r has multiple matches, but dest %s ' |
| 'is not a directory.\n' |
| 'Aborting copy...' % (path.src, path.dest)) |
| else: |
| for p in paths: |
| rel_src = os.path.relpath(p, src_base) |
| if path.dest is None: |
| rel_dest = rel_src |
| elif path.dest.endswith('/'): |
| rel_dest = os.path.join(path.dest, os.path.basename(p)) |
| else: |
| rel_dest = path.dest |
| assert not rel_dest.endswith('/') |
| dest = os.path.join(dest_base, rel_dest) |
| |
| copied_paths.append(p) |
| self.Log(p, dest, os.path.isdir(p)) |
| if os.path.isdir(p): |
| for sub_path in osutils.DirectoryIterator(p): |
| rel_path = os.path.relpath(sub_path, p) |
| sub_dest = os.path.join(dest, rel_path) |
| if self._PathIsBlacklisted(rel_path): |
| continue |
| if sub_path.endswith('/'): |
| osutils.SafeMakedirs(sub_dest, mode=self.dir_mode) |
| else: |
| self._CopyFile(sub_path, sub_dest, path) |
| else: |
| self._CopyFile(p, dest, path) |
| |
| return copied_paths |
| |
| |
| class Path(object): |
| """Represents an artifact to be copied from build dir to staging dir.""" |
| |
| def __init__(self, src, exe=False, cond=None, dest=None, mode=None, |
| optional=False, strip=True, owner=None): |
| """Initializes the object. |
| |
| Args: |
| src: The relative path of the artifact. Can be a file or a directory. |
| Can be a glob pattern. |
| exe: Identifes the path as either being an executable or containing |
| executables. Executables may be stripped during copy, and have |
| special permissions set. We currently only support stripping of |
| specified files and glob patterns that return files. If |src| is a |
| directory or contains directories, the content of the directory will |
| not be stripped. |
| cond: A condition (see Conditions class) to test for in deciding whether |
| to process this artifact. If supplied, the artifact will be treated |
| as optional unless --strict is supplied. |
| dest: Name to give to the target file/directory. Defaults to keeping the |
| same name as the source. |
| mode: The mode to set for the matched files, and the contents of matched |
| directories. |
| optional: Whether to enforce the existence of the artifact. If unset, the |
| script errors out if the artifact does not exist. In 'sloppy' |
| mode, the Copier class treats all artifacts as optional. |
| strip: If |exe| is set, whether to strip the executable. |
| owner: The target owner for this artifact, to be used with chown on the |
| remote system. |
| """ |
| self.src = src |
| self.exe = exe |
| self.cond = cond |
| self.dest = dest |
| self.mode = mode |
| self.optional = optional |
| self.strip = strip |
| self.owner = owner |
| |
| def ShouldProcess(self, gyp_defines, staging_flags): |
| """Tests whether this artifact should be copied.""" |
| if self.cond: |
| return self.cond(gyp_defines, staging_flags) |
| return True |
| |
| |
| _DISABLE_NACL = 'disable_nacl' |
| _USE_DRM = 'use_drm' |
| |
| |
| _CHROME_INTERNAL_FLAG = 'chrome_internal' |
| _CONTENT_SHELL_FLAG = 'content_shell' |
| _HIGHDPI_FLAG = 'highdpi' |
| _PDF_FLAG = 'chrome_pdf' |
| STAGING_FLAGS = (_CHROME_INTERNAL_FLAG, _CONTENT_SHELL_FLAG, _HIGHDPI_FLAG, |
| _PDF_FLAG) |
| |
| _CHROME_SANDBOX_DEST = 'chrome-sandbox' |
| C = Conditions |
| |
| |
| _COPY_PATHS = ( |
| Path('ash_shell', |
| cond=C.GypSet(_USE_DRM)), |
| Path('aura_demo', |
| cond=C.GypSet(_USE_DRM)), |
| Path('chrome', |
| exe=True), |
| Path('chrome_sandbox', mode=0o4755, |
| dest=_CHROME_SANDBOX_DEST), |
| Path('chrome-wrapper'), |
| Path('chrome_100_percent.pak'), |
| Path('chrome_200_percent.pak', |
| cond=C.StagingFlagSet(_HIGHDPI_FLAG)), |
| Path('content_shell', |
| exe=True, |
| cond=C.StagingFlagSet(_CONTENT_SHELL_FLAG)), |
| Path('content_shell.pak', |
| cond=C.StagingFlagSet(_CONTENT_SHELL_FLAG)), |
| Path('keyboard_resources.pak'), |
| Path('lib/*.so', |
| exe=True, |
| cond=C.GypSet('component', value='shared_library')), |
| # Set as optional for backwards compatibility. |
| Path('lib/libpeerconnection.so', |
| exe=True, |
| cond=C.StagingFlagSet(_CHROME_INTERNAL_FLAG), |
| optional=True), |
| Path('libffmpegsumo.so', |
| exe=True), |
| Path('libpdf.so', |
| exe=True, |
| cond=C.StagingFlagSet(_PDF_FLAG)), |
| Path('libppGoogleNaClPluginChrome.so', |
| exe=True, |
| cond=C.GypNotSet(_DISABLE_NACL)), |
| Path('libosmesa.so', |
| exe=True, |
| optional=True), |
| # Widevine binaries are already pre-stripped. In addition, they don't |
| # play well with the binutils stripping tools, so skip stripping. |
| Path('libwidevinecdmadapter.so', |
| exe=True, |
| strip=False, |
| cond=C.StagingFlagSet(_CHROME_INTERNAL_FLAG)), |
| Path('libwidevinecdm.so', |
| exe=True, |
| strip=False, |
| cond=C.StagingFlagSet(_CHROME_INTERNAL_FLAG)), |
| Path('locales/'), |
| # Do not strip the nacl_helper_bootstrap binary because the binutils |
| # objcopy/strip mangles the ELF program headers. |
| Path('nacl_helper_bootstrap', |
| exe=True, strip=False, |
| cond=C.GypNotSet(_DISABLE_NACL)), |
| Path('nacl_irt_*.nexe', |
| cond=C.GypNotSet(_DISABLE_NACL)), |
| Path('nacl_helper', |
| exe=True, |
| optional=True, |
| cond=C.GypNotSet(_DISABLE_NACL)), |
| Path('pnacl/', |
| cond=C.GypNotSet(_DISABLE_NACL)), |
| Path('resources/'), |
| Path('resources.pak'), |
| Path('xdg-settings'), |
| Path('*.png'), |
| ) |
| |
| _COPY_PATHS_CONTENT_SHELL = ( |
| Path('chrome-sandbox', |
| exe=True, mode=0o4755, owner='root:chrome'), |
| Path('dogfood/dumpstate_uploader', exe=True), |
| Path('dogfood/fake.dmp'), |
| Path('eureka_shell', exe=True, mode=0o755, owner='chrome:chrome'), |
| Path('eureka_shell.pak', owner='chrome:chrome'), |
| Path('fake_log_report.dmp', owner='chrome:chrome'), |
| Path('libck_pe.so', owner='chrome:chrome'), |
| Path('libffmpegsumo.so', owner='chrome:chrome'), |
| Path('locales/*.pak', owner='chrome:chrome'), |
| Path('osd.conf', owner='chrome:chrome'), |
| Path('osd_images/1080p/*.png', owner='chrome:chrome'), |
| Path('osd_images/720p/*.png', owner='chrome:chrome'), |
| Path('setup/http/jquery.min.js', owner='chrome:chrome'), |
| Path('setup/http/setup.html', owner='chrome:chrome'), |
| Path('setup/http/welcome-video.mp4', owner='chrome:chrome'), |
| Path('update_engine', exe=True, mode=0o755, owner='updater:updater'), |
| Path('v2mirroring', exe=True, mode=0o755, owner='chrome:chrome'), |
| Path('watchdog/wd.dmp'), |
| ) |
| |
| def _FixPermissions(dest_base): |
| """Last minute permission fixes.""" |
| cros_build_lib.DebugRunCommand(['chmod', '-R', 'a+r', dest_base]) |
| cros_build_lib.DebugRunCommand( |
| ['find', dest_base, '-perm', '/110', '-exec', 'chmod', 'a+x', '{}', '+']) |
| |
| |
| def GetCopyPaths(content_shell=False): |
| """Returns the list of copy paths used as a filter for staging files. |
| |
| Args: |
| content_shell: Boolean on whether to return the copy paths used for a |
| content shell deployment or a chrome deployment. |
| |
| Returns: |
| The list of paths to use as a filter for staging files. |
| """ |
| if content_shell: |
| return _COPY_PATHS_CONTENT_SHELL |
| else: |
| return _COPY_PATHS |
| |
| |
| def StageChromeFromBuildDir(staging_dir, build_dir, strip_bin, strict=False, |
| sloppy=False, gyp_defines=None, staging_flags=None, |
| strip_flags=None, copy_paths=_COPY_PATHS): |
| """Populates a staging directory with necessary build artifacts. |
| |
| If |strict| is set, then we decide what to stage based on the |gyp_defines| |
| and |staging_flags| passed in. Otherwise, we stage everything that we know |
| about, that we can find. |
| |
| Args: |
| staging_dir: Path to an empty staging directory. |
| build_dir: Path to location of Chrome build artifacts. |
| strip_bin: Path to executable used for stripping binaries. |
| strict: If set, decide what to stage based on the |gyp_defines| and |
| |staging_flags| passed in, and enforce that all optional files |
| are copied. Otherwise, we stage optional files if they are |
| there, but we don't complain if they're not. |
| sloppy: Ignore when mandatory artifacts are missing. |
| gyp_defines: A dictionary (i.e., one returned by ProcessGypDefines) |
| containing GYP_DEFINES Chrome was built with. |
| staging_flags: A list of extra staging flags. Valid flags are specified in |
| STAGING_FLAGS. |
| strip_flags: A list of flags to pass to the tool used to strip binaries. |
| copy_paths: The list of paths to use as a filter for staging files. |
| """ |
| os.mkdir(os.path.join(staging_dir, 'plugins'), 0o755) |
| |
| if gyp_defines is None: |
| gyp_defines = {} |
| if staging_flags is None: |
| staging_flags = [] |
| |
| copier = Copier(strip_bin=strip_bin, strip_flags=strip_flags) |
| copied_paths = [] |
| for p in copy_paths: |
| if not strict or p.ShouldProcess(gyp_defines, staging_flags): |
| copied_paths += copier.Copy(build_dir, staging_dir, p, strict=strict, |
| sloppy=sloppy) |
| |
| if not copied_paths: |
| raise MissingPathError('Couldn\'t find anything to copy!\n' |
| 'Are you looking in the right directory?\n' |
| 'Aborting copy...') |
| |
| _FixPermissions(staging_dir) |