| # Copyright (c) 2012 Google Inc. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| TestGyp.py: a testing framework for GYP integration tests. |
| """ |
| |
| import collections |
| from contextlib import contextmanager |
| import itertools |
| import os |
| import re |
| import shutil |
| import stat |
| import subprocess |
| import sys |
| import tempfile |
| |
| import TestCmd |
| import TestCommon |
| from TestCommon import __all__ |
| |
| __all__.extend([ |
| 'TestGyp', |
| ]) |
| |
| |
| def remove_debug_line_numbers(contents): |
| """Function to remove the line numbers from the debug output |
| of gyp and thus reduce the extreme fragility of the stdout |
| comparison tests. |
| """ |
| lines = contents.splitlines() |
| # split each line on ":" |
| lines = [l.split(":", 3) for l in lines] |
| # join each line back together while ignoring the |
| # 3rd column which is the line number |
| lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines] |
| return "\n".join(lines) |
| |
| |
| def match_modulo_line_numbers(contents_a, contents_b): |
| """File contents matcher that ignores line numbers.""" |
| contents_a = remove_debug_line_numbers(contents_a) |
| contents_b = remove_debug_line_numbers(contents_b) |
| return TestCommon.match_exact(contents_a, contents_b) |
| |
| |
| @contextmanager |
| def LocalEnv(local_env): |
| """Context manager to provide a local OS environment.""" |
| old_env = os.environ.copy() |
| os.environ.update(local_env) |
| try: |
| yield |
| finally: |
| os.environ.clear() |
| os.environ.update(old_env) |
| |
| |
| class TestGypBase(TestCommon.TestCommon): |
| """ |
| Class for controlling end-to-end tests of gyp generators. |
| |
| Instantiating this class will create a temporary directory and |
| arrange for its destruction (via the TestCmd superclass) and |
| copy all of the non-gyptest files in the directory hierarchy of the |
| executing script. |
| |
| The default behavior is to test the 'gyp' or 'gyp.bat' file in the |
| current directory. An alternative may be specified explicitly on |
| instantiation, or by setting the TESTGYP_GYP environment variable. |
| |
| This class should be subclassed for each supported gyp generator |
| (format). Various abstract methods below define calling signatures |
| used by the test scripts to invoke builds on the generated build |
| configuration and to run executables generated by those builds. |
| """ |
| |
| formats = [] |
| build_tool = None |
| build_tool_list = [] |
| |
| _exe = TestCommon.exe_suffix |
| _obj = TestCommon.obj_suffix |
| shobj_ = TestCommon.shobj_prefix |
| _shobj = TestCommon.shobj_suffix |
| lib_ = TestCommon.lib_prefix |
| _lib = TestCommon.lib_suffix |
| dll_ = TestCommon.dll_prefix |
| _dll = TestCommon.dll_suffix |
| |
| # Constants to represent different targets. |
| ALL = '__all__' |
| DEFAULT = '__default__' |
| |
| # Constants for different target types. |
| EXECUTABLE = '__executable__' |
| STATIC_LIB = '__static_lib__' |
| SHARED_LIB = '__shared_lib__' |
| |
| def __init__(self, gyp=None, *args, **kw): |
| self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0])) |
| self.extra_args = sys.argv[1:] |
| |
| if not gyp: |
| gyp = os.environ.get('TESTGYP_GYP') |
| if not gyp: |
| if sys.platform == 'win32': |
| gyp = 'gyp.bat' |
| else: |
| gyp = 'gyp' |
| self.gyp = os.path.abspath(gyp) |
| self.no_parallel = False |
| |
| self.formats = [self.format] |
| |
| self.initialize_build_tool() |
| |
| kw.setdefault('match', TestCommon.match_exact) |
| |
| # Put test output in out/testworkarea by default. |
| # Use temporary names so there are no collisions. |
| workdir = os.path.join('out', kw.get('workdir', 'testworkarea')) |
| # Create work area if it doesn't already exist. |
| if not os.path.isdir(workdir): |
| os.makedirs(workdir) |
| |
| kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir) |
| |
| formats = kw.pop('formats', []) |
| |
| super(TestGypBase, self).__init__(*args, **kw) |
| |
| real_format = self.format.split('-')[-1] |
| excluded_formats = set([f for f in formats if f[0] == '!']) |
| included_formats = set(formats) - excluded_formats |
| if ('!'+real_format in excluded_formats or |
| included_formats and real_format not in included_formats): |
| msg = 'Invalid test for %r format; skipping test.\n' |
| self.skip_test(msg % self.format) |
| |
| self.copy_test_configuration(self.origin_cwd, self.workdir) |
| self.set_configuration(None) |
| |
| # Set $HOME so that gyp doesn't read the user's actual |
| # ~/.gyp/include.gypi file, which may contain variables |
| # and other settings that would change the output. |
| os.environ['HOME'] = self.workpath() |
| # Clear $GYP_DEFINES for the same reason. |
| if 'GYP_DEFINES' in os.environ: |
| del os.environ['GYP_DEFINES'] |
| # Override the user's language settings, which could |
| # otherwise make the output vary from what is expected. |
| os.environ['LC_ALL'] = 'C' |
| |
| def built_file_must_exist(self, name, type=None, **kw): |
| """ |
| Fails the test if the specified built file name does not exist. |
| """ |
| return self.must_exist(self.built_file_path(name, type, **kw)) |
| |
| def built_file_must_not_exist(self, name, type=None, **kw): |
| """ |
| Fails the test if the specified built file name exists. |
| """ |
| return self.must_not_exist(self.built_file_path(name, type, **kw)) |
| |
| def built_file_must_match(self, name, contents, **kw): |
| """ |
| Fails the test if the contents of the specified built file name |
| do not match the specified contents. |
| """ |
| return self.must_match(self.built_file_path(name, **kw), contents) |
| |
| def built_file_must_not_match(self, name, contents, **kw): |
| """ |
| Fails the test if the contents of the specified built file name |
| match the specified contents. |
| """ |
| return self.must_not_match(self.built_file_path(name, **kw), contents) |
| |
| def built_file_must_not_contain(self, name, contents, **kw): |
| """ |
| Fails the test if the specified built file name contains the specified |
| contents. |
| """ |
| return self.must_not_contain(self.built_file_path(name, **kw), contents) |
| |
| def copy_test_configuration(self, source_dir, dest_dir): |
| """ |
| Copies the test configuration from the specified source_dir |
| (the directory in which the test script lives) to the |
| specified dest_dir (a temporary working directory). |
| |
| This ignores all files and directories that begin with |
| the string 'gyptest', and all '.svn' subdirectories. |
| """ |
| for root, dirs, files in os.walk(source_dir): |
| if '.svn' in dirs: |
| dirs.remove('.svn') |
| dirs = [ d for d in dirs if not d.startswith('gyptest') ] |
| files = [ f for f in files if not f.startswith('gyptest') ] |
| for dirname in dirs: |
| source = os.path.join(root, dirname) |
| destination = source.replace(source_dir, dest_dir) |
| os.mkdir(destination) |
| if sys.platform != 'win32': |
| shutil.copystat(source, destination) |
| for filename in files: |
| source = os.path.join(root, filename) |
| destination = source.replace(source_dir, dest_dir) |
| shutil.copy2(source, destination) |
| |
| def initialize_build_tool(self): |
| """ |
| Initializes the .build_tool attribute. |
| |
| Searches the .build_tool_list for an executable name on the user's |
| $PATH. The first tool on the list is used as-is if nothing is found |
| on the current $PATH. |
| """ |
| for build_tool in self.build_tool_list: |
| if not build_tool: |
| continue |
| if os.path.isabs(build_tool): |
| self.build_tool = build_tool |
| return |
| build_tool = self.where_is(build_tool) |
| if build_tool: |
| self.build_tool = build_tool |
| return |
| |
| if self.build_tool_list: |
| self.build_tool = self.build_tool_list[0] |
| |
| def relocate(self, source, destination): |
| """ |
| Renames (relocates) the specified source (usually a directory) |
| to the specified destination, creating the destination directory |
| first if necessary. |
| |
| Note: Don't use this as a generic "rename" operation. In the |
| future, "relocating" parts of a GYP tree may affect the state of |
| the test to modify the behavior of later method calls. |
| """ |
| destination_dir = os.path.dirname(destination) |
| if not os.path.exists(destination_dir): |
| self.subdir(destination_dir) |
| os.rename(source, destination) |
| |
| def report_not_up_to_date(self): |
| """ |
| Reports that a build is not up-to-date. |
| |
| This provides common reporting for formats that have complicated |
| conditions for checking whether a build is up-to-date. Formats |
| that expect exact output from the command (make) can |
| just set stdout= when they call the run_build() method. |
| """ |
| print "Build is not up-to-date:" |
| print self.banner('STDOUT ') |
| print self.stdout() |
| stderr = self.stderr() |
| if stderr: |
| print self.banner('STDERR ') |
| print stderr |
| |
| def run_gyp(self, gyp_file, *args, **kw): |
| """ |
| Runs gyp against the specified gyp_file with the specified args. |
| """ |
| |
| # When running gyp, and comparing its output we use a comparitor |
| # that ignores the line numbers that gyp logs in its debug output. |
| if kw.pop('ignore_line_numbers', False): |
| kw.setdefault('match', match_modulo_line_numbers) |
| |
| # TODO: --depth=. works around Chromium-specific tree climbing. |
| depth = kw.pop('depth', '.') |
| run_args = ['--depth='+depth] |
| run_args.extend(['--format='+f for f in self.formats]); |
| run_args.append(gyp_file) |
| if self.no_parallel: |
| run_args += ['--no-parallel'] |
| # TODO: if extra_args contains a '--build' flag |
| # we really want that to only apply to the last format (self.format). |
| run_args.extend(self.extra_args) |
| run_args.extend(args) |
| return self.run(program=self.gyp, arguments=run_args, **kw) |
| |
| def run(self, *args, **kw): |
| """ |
| Executes a program by calling the superclass .run() method. |
| |
| This exists to provide a common place to filter out keyword |
| arguments implemented in this layer, without having to update |
| the tool-specific subclasses or clutter the tests themselves |
| with platform-specific code. |
| """ |
| if kw.has_key('SYMROOT'): |
| del kw['SYMROOT'] |
| super(TestGypBase, self).run(*args, **kw) |
| |
| def set_configuration(self, configuration): |
| """ |
| Sets the configuration, to be used for invoking the build |
| tool and testing potential built output. |
| """ |
| self.configuration = configuration |
| |
| def configuration_dirname(self): |
| if self.configuration: |
| return self.configuration.split('|')[0] |
| else: |
| return 'Default' |
| |
| def configuration_buildname(self): |
| if self.configuration: |
| return self.configuration |
| else: |
| return 'Default' |
| |
| # |
| # Abstract methods to be defined by format-specific subclasses. |
| # |
| |
| def build(self, gyp_file, target=None, **kw): |
| """ |
| Runs a build of the specified target against the configuration |
| generated from the specified gyp_file. |
| |
| A 'target' argument of None or the special value TestGyp.DEFAULT |
| specifies the default argument for the underlying build tool. |
| A 'target' argument of TestGyp.ALL specifies the 'all' target |
| (if any) of the underlying build tool. |
| """ |
| raise NotImplementedError |
| |
| def built_file_path(self, name, type=None, **kw): |
| """ |
| Returns a path to the specified file name, of the specified type. |
| """ |
| raise NotImplementedError |
| |
| def built_file_basename(self, name, type=None, **kw): |
| """ |
| Returns the base name of the specified file name, of the specified type. |
| |
| A bare=True keyword argument specifies that prefixes and suffixes shouldn't |
| be applied. |
| """ |
| if not kw.get('bare'): |
| if type == self.EXECUTABLE: |
| name = name + self._exe |
| elif type == self.STATIC_LIB: |
| name = self.lib_ + name + self._lib |
| elif type == self.SHARED_LIB: |
| name = self.dll_ + name + self._dll |
| return name |
| |
| def run_built_executable(self, name, *args, **kw): |
| """ |
| Runs an executable program built from a gyp-generated configuration. |
| |
| The specified name should be independent of any particular generator. |
| Subclasses should find the output executable in the appropriate |
| output build directory, tack on any necessary executable suffix, etc. |
| """ |
| raise NotImplementedError |
| |
| def up_to_date(self, gyp_file, target=None, **kw): |
| """ |
| Verifies that a build of the specified target is up to date. |
| |
| The subclass should implement this by calling build() |
| (or a reasonable equivalent), checking whatever conditions |
| will tell it the build was an "up to date" null build, and |
| failing if it isn't. |
| """ |
| raise NotImplementedError |
| |
| |
| class TestGypGypd(TestGypBase): |
| """ |
| Subclass for testing the GYP 'gypd' generator (spit out the |
| internal data structure as pretty-printed Python). |
| """ |
| format = 'gypd' |
| def __init__(self, gyp=None, *args, **kw): |
| super(TestGypGypd, self).__init__(*args, **kw) |
| # gypd implies the use of 'golden' files, so parallelizing conflicts as it |
| # causes ordering changes. |
| self.no_parallel = True |
| |
| |
| class TestGypCustom(TestGypBase): |
| """ |
| Subclass for testing the GYP with custom generator |
| """ |
| |
| def __init__(self, gyp=None, *args, **kw): |
| self.format = kw.pop("format") |
| super(TestGypCustom, self).__init__(*args, **kw) |
| |
| |
| class TestGypAndroid(TestGypBase): |
| """ |
| Subclass for testing the GYP Android makefile generator. Note that |
| build/envsetup.sh and lunch must have been run before running tests. |
| |
| TODO: This is currently an incomplete implementation. We do not support |
| run_built_executable(), so we pass only tests which do not use this. As a |
| result, support for host targets is not properly tested. |
| """ |
| format = 'android' |
| |
| # Note that we can't use mmm as the build tool because ... |
| # - it builds all targets, whereas we need to pass a target |
| # - it is a function, whereas the test runner assumes the build tool is a file |
| # Instead we use make and duplicate the logic from mmm. |
| build_tool_list = ['make'] |
| |
| # We use our custom target 'gyp_all_modules', as opposed to the 'all_modules' |
| # target used by mmm, to build only those targets which are part of the gyp |
| # target 'all'. |
| ALL = 'gyp_all_modules' |
| |
| def __init__(self, gyp=None, *args, **kw): |
| # Android requires build and test output to be inside its source tree. |
| # We use the following working directory for the test's source, but the |
| # test's build output still goes to $ANDROID_PRODUCT_OUT. |
| # Note that some tests explicitly set format='gypd' to invoke the gypd |
| # backend. This writes to the source tree, but there's no way around this. |
| kw['workdir'] = os.path.join('/tmp', 'gyptest', |
| kw.get('workdir', 'testworkarea')) |
| # We need to remove all gyp outputs from out/. Ths is because some tests |
| # don't have rules to regenerate output, so they will simply re-use stale |
| # output if present. Since the test working directory gets regenerated for |
| # each test run, this can confuse things. |
| # We don't have a list of build outputs because we don't know which |
| # dependent targets were built. Instead we delete all gyp-generated output. |
| # This may be excessive, but should be safe. |
| out_dir = os.environ['ANDROID_PRODUCT_OUT'] |
| obj_dir = os.path.join(out_dir, 'obj') |
| shutil.rmtree(os.path.join(obj_dir, 'GYP'), ignore_errors = True) |
| for x in ['EXECUTABLES', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES']: |
| for d in os.listdir(os.path.join(obj_dir, x)): |
| if d.endswith('_gyp_intermediates'): |
| shutil.rmtree(os.path.join(obj_dir, x, d), ignore_errors = True) |
| for x in [os.path.join('obj', 'lib'), os.path.join('system', 'lib')]: |
| for d in os.listdir(os.path.join(out_dir, x)): |
| if d.endswith('_gyp.so'): |
| os.remove(os.path.join(out_dir, x, d)) |
| |
| super(TestGypAndroid, self).__init__(*args, **kw) |
| |
| def target_name(self, target): |
| if target == self.ALL: |
| return self.ALL |
| # The default target is 'droid'. However, we want to use our special target |
| # to build only the gyp target 'all'. |
| if target in (None, self.DEFAULT): |
| return self.ALL |
| return target |
| |
| def build(self, gyp_file, target=None, **kw): |
| """ |
| Runs a build using the Android makefiles generated from the specified |
| gyp_file. This logic is taken from Android's mmm. |
| """ |
| arguments = kw.get('arguments', [])[:] |
| arguments.append(self.target_name(target)) |
| arguments.append('-C') |
| arguments.append(os.environ['ANDROID_BUILD_TOP']) |
| kw['arguments'] = arguments |
| chdir = kw.get('chdir', '') |
| makefile = os.path.join(self.workdir, chdir, 'GypAndroid.mk') |
| os.environ['ONE_SHOT_MAKEFILE'] = makefile |
| result = self.run(program=self.build_tool, **kw) |
| del os.environ['ONE_SHOT_MAKEFILE'] |
| return result |
| |
| def android_module(self, group, name, subdir): |
| if subdir: |
| name = '%s_%s' % (subdir, name) |
| if group == 'SHARED_LIBRARIES': |
| name = 'lib_%s' % name |
| return '%s_gyp' % name |
| |
| def intermediates_dir(self, group, module_name): |
| return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', group, |
| '%s_intermediates' % module_name) |
| |
| def built_file_path(self, name, type=None, **kw): |
| """ |
| Returns a path to the specified file name, of the specified type, |
| as built by Android. Note that we don't support the configuration |
| parameter. |
| """ |
| # Built files are in $ANDROID_PRODUCT_OUT. This requires copying logic from |
| # the Android build system. |
| if type == None: |
| return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', 'GYP', |
| 'shared_intermediates', name) |
| subdir = kw.get('subdir') |
| if type == self.EXECUTABLE: |
| # We don't install executables |
| group = 'EXECUTABLES' |
| module_name = self.android_module(group, name, subdir) |
| return os.path.join(self.intermediates_dir(group, module_name), name) |
| if type == self.STATIC_LIB: |
| group = 'STATIC_LIBRARIES' |
| module_name = self.android_module(group, name, subdir) |
| return os.path.join(self.intermediates_dir(group, module_name), |
| '%s.a' % module_name) |
| if type == self.SHARED_LIB: |
| group = 'SHARED_LIBRARIES' |
| module_name = self.android_module(group, name, subdir) |
| return os.path.join(self.intermediates_dir(group, module_name), 'LINKED', |
| '%s.so' % module_name) |
| assert False, 'Unhandled type' |
| |
| def run_built_executable(self, name, *args, **kw): |
| """ |
| Runs an executable program built from a gyp-generated configuration. |
| |
| This is not correctly implemented for Android. For now, we simply check |
| that the executable file exists. |
| """ |
| # Running executables requires a device. Even if we build for target x86, |
| # the binary is not built with the correct toolchain options to actually |
| # run on the host. |
| |
| # Copied from TestCommon.run() |
| match = kw.pop('match', self.match) |
| status = None |
| if os.path.exists(self.built_file_path(name)): |
| status = 1 |
| self._complete(None, None, None, None, status, match) |
| |
| def match_single_line(self, lines = None, expected_line = None): |
| """ |
| Checks that specified line appears in the text. |
| """ |
| for line in lines.split('\n'): |
| if line == expected_line: |
| return 1 |
| return |
| |
| def up_to_date(self, gyp_file, target=None, **kw): |
| """ |
| Verifies that a build of the specified target is up to date. |
| """ |
| kw['stdout'] = ("make: Nothing to be done for `%s'." % |
| self.target_name(target)) |
| |
| # We need to supply a custom matcher, since we don't want to depend on the |
| # exact stdout string. |
| kw['match'] = self.match_single_line |
| return self.build(gyp_file, target, **kw) |
| |
| |
| class TestGypCMake(TestGypBase): |
| """ |
| Subclass for testing the GYP CMake generator, using cmake's ninja backend. |
| """ |
| format = 'cmake' |
| build_tool_list = ['cmake'] |
| ALL = 'all' |
| |
| def cmake_build(self, gyp_file, target=None, **kw): |
| arguments = kw.get('arguments', [])[:] |
| |
| self.build_tool_list = ['cmake'] |
| self.initialize_build_tool() |
| |
| chdir = os.path.join(kw.get('chdir', '.'), |
| 'out', |
| self.configuration_dirname()) |
| kw['chdir'] = chdir |
| |
| arguments.append('-G') |
| arguments.append('Ninja') |
| |
| kw['arguments'] = arguments |
| |
| stderr = kw.get('stderr', None) |
| if stderr: |
| kw['stderr'] = stderr.split('$$$')[0] |
| |
| self.run(program=self.build_tool, **kw) |
| |
| def ninja_build(self, gyp_file, target=None, **kw): |
| arguments = kw.get('arguments', [])[:] |
| |
| self.build_tool_list = ['ninja'] |
| self.initialize_build_tool() |
| |
| # Add a -C output/path to the command line. |
| arguments.append('-C') |
| arguments.append(os.path.join('out', self.configuration_dirname())) |
| |
| if target not in (None, self.DEFAULT): |
| arguments.append(target) |
| |
| kw['arguments'] = arguments |
| |
| stderr = kw.get('stderr', None) |
| if stderr: |
| stderrs = stderr.split('$$$') |
| kw['stderr'] = stderrs[1] if len(stderrs) > 1 else '' |
| |
| return self.run(program=self.build_tool, **kw) |
| |
| def build(self, gyp_file, target=None, status=0, **kw): |
| # Two tools must be run to build, cmake and the ninja. |
| # Allow cmake to succeed when the overall expectation is to fail. |
| if status is None: |
| kw['status'] = None |
| else: |
| if not isinstance(status, collections.Iterable): status = (status,) |
| kw['status'] = list(itertools.chain((0,), status)) |
| self.cmake_build(gyp_file, target, **kw) |
| kw['status'] = status |
| self.ninja_build(gyp_file, target, **kw) |
| |
| def run_built_executable(self, name, *args, **kw): |
| # Enclosing the name in a list avoids prepending the original dir. |
| program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| if sys.platform == 'darwin': |
| configuration = self.configuration_dirname() |
| os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) |
| return self.run(program=program, *args, **kw) |
| |
| def built_file_path(self, name, type=None, **kw): |
| result = [] |
| chdir = kw.get('chdir') |
| if chdir: |
| result.append(chdir) |
| result.append('out') |
| result.append(self.configuration_dirname()) |
| if type == self.STATIC_LIB: |
| if sys.platform != 'darwin': |
| result.append('obj.target') |
| elif type == self.SHARED_LIB: |
| if sys.platform != 'darwin' and sys.platform != 'win32': |
| result.append('lib.target') |
| subdir = kw.get('subdir') |
| if subdir and type != self.SHARED_LIB: |
| result.append(subdir) |
| result.append(self.built_file_basename(name, type, **kw)) |
| return self.workpath(*result) |
| |
| def up_to_date(self, gyp_file, target=None, **kw): |
| result = self.ninja_build(gyp_file, target, **kw) |
| if not result: |
| stdout = self.stdout() |
| if 'ninja: no work to do' not in stdout: |
| self.report_not_up_to_date() |
| self.fail_test() |
| return result |
| |
| |
| class TestGypMake(TestGypBase): |
| """ |
| Subclass for testing the GYP Make generator. |
| """ |
| format = 'make' |
| build_tool_list = ['make'] |
| ALL = 'all' |
| def build(self, gyp_file, target=None, **kw): |
| """ |
| Runs a Make build using the Makefiles generated from the specified |
| gyp_file. |
| """ |
| arguments = kw.get('arguments', [])[:] |
| if self.configuration: |
| arguments.append('BUILDTYPE=' + self.configuration) |
| if target not in (None, self.DEFAULT): |
| arguments.append(target) |
| # Sub-directory builds provide per-gyp Makefiles (i.e. |
| # Makefile.gyp_filename), so use that if there is no Makefile. |
| chdir = kw.get('chdir', '') |
| if not os.path.exists(os.path.join(chdir, 'Makefile')): |
| print "NO Makefile in " + os.path.join(chdir, 'Makefile') |
| arguments.insert(0, '-f') |
| arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile') |
| kw['arguments'] = arguments |
| return self.run(program=self.build_tool, **kw) |
| def up_to_date(self, gyp_file, target=None, **kw): |
| """ |
| Verifies that a build of the specified Make target is up to date. |
| """ |
| if target in (None, self.DEFAULT): |
| message_target = 'all' |
| else: |
| message_target = target |
| kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target |
| return self.build(gyp_file, target, **kw) |
| def run_built_executable(self, name, *args, **kw): |
| """ |
| Runs an executable built by Make. |
| """ |
| configuration = self.configuration_dirname() |
| libdir = os.path.join('out', configuration, 'lib') |
| # TODO(piman): when everything is cross-compile safe, remove lib.target |
| if sys.platform == 'darwin': |
| # Mac puts target shared libraries right in the product directory. |
| configuration = self.configuration_dirname() |
| os.environ['DYLD_LIBRARY_PATH'] = ( |
| libdir + '.host:' + os.path.join('out', configuration)) |
| else: |
| os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target' |
| # Enclosing the name in a list avoids prepending the original dir. |
| program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| return self.run(program=program, *args, **kw) |
| def built_file_path(self, name, type=None, **kw): |
| """ |
| Returns a path to the specified file name, of the specified type, |
| as built by Make. |
| |
| Built files are in the subdirectory 'out/{configuration}'. |
| The default is 'out/Default'. |
| |
| A chdir= keyword argument specifies the source directory |
| relative to which the output subdirectory can be found. |
| |
| "type" values of STATIC_LIB or SHARED_LIB append the necessary |
| prefixes and suffixes to a platform-independent library base name. |
| |
| A subdir= keyword argument specifies a library subdirectory within |
| the default 'obj.target'. |
| """ |
| result = [] |
| chdir = kw.get('chdir') |
| if chdir: |
| result.append(chdir) |
| configuration = self.configuration_dirname() |
| result.extend(['out', configuration]) |
| if type == self.STATIC_LIB and sys.platform != 'darwin': |
| result.append('obj.target') |
| elif type == self.SHARED_LIB and sys.platform != 'darwin': |
| result.append('lib.target') |
| subdir = kw.get('subdir') |
| if subdir and type != self.SHARED_LIB: |
| result.append(subdir) |
| result.append(self.built_file_basename(name, type, **kw)) |
| return self.workpath(*result) |
| |
| |
| def ConvertToCygpath(path): |
| """Convert to cygwin path if we are using cygwin.""" |
| if sys.platform == 'cygwin': |
| p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE) |
| path = p.communicate()[0].strip() |
| return path |
| |
| |
| def FindMSBuildInstallation(msvs_version = 'auto'): |
| """Returns path to MSBuild for msvs_version or latest available. |
| |
| Looks in the registry to find install location of MSBuild. |
| MSBuild before v4.0 will not build c++ projects, so only use newer versions. |
| """ |
| import TestWin |
| registry = TestWin.Registry() |
| |
| msvs_to_msbuild = { |
| '2013': r'12.0', |
| '2012': r'4.0', # Really v4.0.30319 which comes with .NET 4.5. |
| '2010': r'4.0'} |
| |
| msbuild_basekey = r'HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions' |
| if not registry.KeyExists(msbuild_basekey): |
| print 'Error: could not find MSBuild base registry entry' |
| return None |
| |
| msbuild_version = None |
| if msvs_version in msvs_to_msbuild: |
| msbuild_test_version = msvs_to_msbuild[msvs_version] |
| if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version): |
| msbuild_version = msbuild_test_version |
| else: |
| print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" ' |
| 'but corresponding MSBuild "%s" was not found.' % |
| (msvs_version, msbuild_version)) |
| if not msbuild_version: |
| for msvs_version in sorted(msvs_to_msbuild, reverse=True): |
| msbuild_test_version = msvs_to_msbuild[msvs_version] |
| if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version): |
| msbuild_version = msbuild_test_version |
| break |
| if not msbuild_version: |
| print 'Error: could not find MSBuild registry entry' |
| return None |
| |
| msbuild_path = registry.GetValue(msbuild_basekey + '\\' + msbuild_version, |
| 'MSBuildToolsPath') |
| if not msbuild_path: |
| print 'Error: could not get MSBuild registry entry value' |
| return None |
| |
| return os.path.join(msbuild_path, 'MSBuild.exe') |
| |
| |
| def FindVisualStudioInstallation(): |
| """Returns appropriate values for .build_tool and .uses_msbuild fields |
| of TestGypBase for Visual Studio. |
| |
| We use the value specified by GYP_MSVS_VERSION. If not specified, we |
| search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable. |
| Failing that, we search for likely deployment paths. |
| """ |
| possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix) |
| for drive in range(ord('C'), ord('Z') + 1) |
| for suffix in ['', ' (x86)']] |
| possible_paths = { |
| '2013': r'Microsoft Visual Studio 12.0\Common7\IDE\devenv.com', |
| '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com', |
| '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com', |
| '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com', |
| '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'} |
| |
| possible_roots = [ConvertToCygpath(r) for r in possible_roots] |
| |
| msvs_version = 'auto' |
| for flag in (f for f in sys.argv if f.startswith('msvs_version=')): |
| msvs_version = flag.split('=')[-1] |
| msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version) |
| |
| if msvs_version in possible_paths: |
| # Check that the path to the specified GYP_MSVS_VERSION exists. |
| path = possible_paths[msvs_version] |
| for r in possible_roots: |
| build_tool = os.path.join(r, path) |
| if os.path.exists(build_tool): |
| uses_msbuild = msvs_version >= '2010' |
| msbuild_path = FindMSBuildInstallation(msvs_version) |
| return build_tool, uses_msbuild, msbuild_path |
| else: |
| print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" ' |
| 'but corresponding "%s" was not found.' % (msvs_version, path)) |
| # Neither GYP_MSVS_VERSION nor the path help us out. Iterate through |
| # the choices looking for a match. |
| for version in sorted(possible_paths, reverse=True): |
| path = possible_paths[version] |
| for r in possible_roots: |
| build_tool = os.path.join(r, path) |
| if os.path.exists(build_tool): |
| uses_msbuild = msvs_version >= '2010' |
| msbuild_path = FindMSBuildInstallation(msvs_version) |
| return build_tool, uses_msbuild, msbuild_path |
| print 'Error: could not find devenv' |
| sys.exit(1) |
| |
| class TestGypOnMSToolchain(TestGypBase): |
| """ |
| Common subclass for testing generators that target the Microsoft Visual |
| Studio toolchain (cl, link, dumpbin, etc.) |
| """ |
| @staticmethod |
| def _ComputeVsvarsPath(devenv_path): |
| devenv_dir = os.path.split(devenv_path)[0] |
| vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat') |
| return vsvars_path |
| |
| def initialize_build_tool(self): |
| super(TestGypOnMSToolchain, self).initialize_build_tool() |
| if sys.platform in ('win32', 'cygwin'): |
| build_tools = FindVisualStudioInstallation() |
| self.devenv_path, self.uses_msbuild, self.msbuild_path = build_tools |
| self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath( |
| self.devenv_path) |
| |
| def run_dumpbin(self, *dumpbin_args): |
| """Run the dumpbin tool with the specified arguments, and capturing and |
| returning stdout.""" |
| assert sys.platform in ('win32', 'cygwin') |
| cmd = os.environ.get('COMSPEC', 'cmd.exe') |
| arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin'] |
| arguments.extend(dumpbin_args) |
| proc = subprocess.Popen(arguments, stdout=subprocess.PIPE) |
| output = proc.communicate()[0] |
| assert not proc.returncode |
| return output |
| |
| class TestGypNinja(TestGypOnMSToolchain): |
| """ |
| Subclass for testing the GYP Ninja generator. |
| """ |
| format = 'ninja' |
| build_tool_list = ['ninja'] |
| ALL = 'all' |
| DEFAULT = 'all' |
| |
| def run_gyp(self, gyp_file, *args, **kw): |
| TestGypBase.run_gyp(self, gyp_file, *args, **kw) |
| |
| def build(self, gyp_file, target=None, **kw): |
| arguments = kw.get('arguments', [])[:] |
| |
| # Add a -C output/path to the command line. |
| arguments.append('-C') |
| arguments.append(os.path.join('out', self.configuration_dirname())) |
| |
| if target is None: |
| target = 'all' |
| arguments.append(target) |
| |
| kw['arguments'] = arguments |
| return self.run(program=self.build_tool, **kw) |
| |
| def run_built_executable(self, name, *args, **kw): |
| # Enclosing the name in a list avoids prepending the original dir. |
| program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| if sys.platform == 'darwin': |
| configuration = self.configuration_dirname() |
| os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) |
| return self.run(program=program, *args, **kw) |
| |
| def built_file_path(self, name, type=None, **kw): |
| result = [] |
| chdir = kw.get('chdir') |
| if chdir: |
| result.append(chdir) |
| result.append('out') |
| result.append(self.configuration_dirname()) |
| if type == self.STATIC_LIB: |
| if sys.platform != 'darwin': |
| result.append('obj') |
| elif type == self.SHARED_LIB: |
| if sys.platform != 'darwin' and sys.platform != 'win32': |
| result.append('lib') |
| subdir = kw.get('subdir') |
| if subdir and type != self.SHARED_LIB: |
| result.append(subdir) |
| result.append(self.built_file_basename(name, type, **kw)) |
| return self.workpath(*result) |
| |
| def up_to_date(self, gyp_file, target=None, **kw): |
| result = self.build(gyp_file, target, **kw) |
| if not result: |
| stdout = self.stdout() |
| if 'ninja: no work to do' not in stdout: |
| self.report_not_up_to_date() |
| self.fail_test() |
| return result |
| |
| |
| class TestGypMSVS(TestGypOnMSToolchain): |
| """ |
| Subclass for testing the GYP Visual Studio generator. |
| """ |
| format = 'msvs' |
| |
| u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ===' |
| up_to_date_re = re.compile(u, re.M) |
| |
| # Initial None element will indicate to our .initialize_build_tool() |
| # method below that 'devenv' was not found on %PATH%. |
| # |
| # Note: we must use devenv.com to be able to capture build output. |
| # Directly executing devenv.exe only sends output to BuildLog.htm. |
| build_tool_list = [None, 'devenv.com'] |
| |
| def initialize_build_tool(self): |
| super(TestGypMSVS, self).initialize_build_tool() |
| self.build_tool = self.devenv_path |
| |
| def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw): |
| """ |
| Runs a Visual Studio build using the configuration generated |
| from the specified gyp_file. |
| """ |
| configuration = self.configuration_buildname() |
| if clean: |
| build = '/Clean' |
| elif rebuild: |
| build = '/Rebuild' |
| else: |
| build = '/Build' |
| arguments = kw.get('arguments', [])[:] |
| arguments.extend([gyp_file.replace('.gyp', '.sln'), |
| build, configuration]) |
| # Note: the Visual Studio generator doesn't add an explicit 'all' |
| # target, so we just treat it the same as the default. |
| if target not in (None, self.ALL, self.DEFAULT): |
| arguments.extend(['/Project', target]) |
| if self.configuration: |
| arguments.extend(['/ProjectConfig', self.configuration]) |
| kw['arguments'] = arguments |
| return self.run(program=self.build_tool, **kw) |
| def up_to_date(self, gyp_file, target=None, **kw): |
| """ |
| Verifies that a build of the specified Visual Studio target is up to date. |
| |
| Beware that VS2010 will behave strangely if you build under |
| C:\USERS\yourname\AppData\Local. It will cause needless work. The ouptut |
| will be "1 succeeded and 0 up to date". MSBuild tracing reveals that: |
| "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because |
| 'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL' |
| was modified at 02/21/2011 17:03:30, which is newer than '' which was |
| modified at 01/01/0001 00:00:00. |
| |
| The workaround is to specify a workdir when instantiating the test, e.g. |
| test = TestGyp.TestGyp(workdir='workarea') |
| """ |
| result = self.build(gyp_file, target, **kw) |
| if not result: |
| stdout = self.stdout() |
| |
| m = self.up_to_date_re.search(stdout) |
| up_to_date = m and int(m.group(1)) > 0 |
| if not up_to_date: |
| self.report_not_up_to_date() |
| self.fail_test() |
| return result |
| def run_built_executable(self, name, *args, **kw): |
| """ |
| Runs an executable built by Visual Studio. |
| """ |
| configuration = self.configuration_dirname() |
| # Enclosing the name in a list avoids prepending the original dir. |
| program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| return self.run(program=program, *args, **kw) |
| def built_file_path(self, name, type=None, **kw): |
| """ |
| Returns a path to the specified file name, of the specified type, |
| as built by Visual Studio. |
| |
| Built files are in a subdirectory that matches the configuration |
| name. The default is 'Default'. |
| |
| A chdir= keyword argument specifies the source directory |
| relative to which the output subdirectory can be found. |
| |
| "type" values of STATIC_LIB or SHARED_LIB append the necessary |
| prefixes and suffixes to a platform-independent library base name. |
| """ |
| result = [] |
| chdir = kw.get('chdir') |
| if chdir: |
| result.append(chdir) |
| result.append(self.configuration_dirname()) |
| if type == self.STATIC_LIB: |
| result.append('lib') |
| result.append(self.built_file_basename(name, type, **kw)) |
| return self.workpath(*result) |
| |
| |
| class TestGypMSVSNinja(TestGypNinja): |
| """ |
| Subclass for testing the GYP Visual Studio Ninja generator. |
| """ |
| format = 'msvs-ninja' |
| |
| def initialize_build_tool(self): |
| super(TestGypMSVSNinja, self).initialize_build_tool() |
| # When using '--build', make sure ninja is first in the format list. |
| self.formats.insert(0, 'ninja') |
| |
| def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw): |
| """ |
| Runs a Visual Studio build using the configuration generated |
| from the specified gyp_file. |
| """ |
| arguments = kw.get('arguments', [])[:] |
| if target in (None, self.ALL, self.DEFAULT): |
| # Note: the Visual Studio generator doesn't add an explicit 'all' target. |
| # This will build each project. This will work if projects are hermetic, |
| # but may fail if they are not (a project may run more than once). |
| # It would be nice to supply an all.metaproj for MSBuild. |
| arguments.extend([gyp_file.replace('.gyp', '.sln')]) |
| else: |
| # MSBuild documentation claims that one can specify a sln but then build a |
| # project target like 'msbuild a.sln /t:proj:target' but this format only |
| # supports 'Clean', 'Rebuild', and 'Publish' (with none meaning Default). |
| # This limitation is due to the .sln -> .sln.metaproj conversion. |
| # The ':' is not special, 'proj:target' is a target in the metaproj. |
| arguments.extend([target+'.vcxproj']) |
| |
| if clean: |
| build = 'Clean' |
| elif rebuild: |
| build = 'Rebuild' |
| else: |
| build = 'Build' |
| arguments.extend(['/target:'+build]) |
| configuration = self.configuration_buildname() |
| config = configuration.split('|') |
| arguments.extend(['/property:Configuration='+config[0]]) |
| if len(config) > 1: |
| arguments.extend(['/property:Platform='+config[1]]) |
| arguments.extend(['/property:BuildInParallel=false']) |
| arguments.extend(['/verbosity:minimal']) |
| |
| kw['arguments'] = arguments |
| return self.run(program=self.msbuild_path, **kw) |
| |
| |
| class TestGypXcode(TestGypBase): |
| """ |
| Subclass for testing the GYP Xcode generator. |
| """ |
| format = 'xcode' |
| build_tool_list = ['xcodebuild'] |
| |
| phase_script_execution = ("\n" |
| "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n" |
| " cd /\\S+\n" |
| " /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n" |
| "(make: Nothing to be done for `all'\\.\n)?") |
| |
| strip_up_to_date_expressions = [ |
| # Various actions or rules can run even when the overall build target |
| # is up to date. Strip those phases' GYP-generated output. |
| re.compile(phase_script_execution, re.S), |
| |
| # The message from distcc_pump can trail the "BUILD SUCCEEDED" |
| # message, so strip that, too. |
| re.compile('__________Shutting down distcc-pump include server\n', re.S), |
| ] |
| |
| up_to_date_endings = ( |
| 'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1 |
| 'Check dependencies\n** BUILD SUCCEEDED **\n\n', # Xcode 3.2 |
| 'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2 |
| 'Check dependencies\n\n** BUILD SUCCEEDED **\n\n', # Xcode 5.0 |
| ) |
| |
| def build(self, gyp_file, target=None, **kw): |
| """ |
| Runs an xcodebuild using the .xcodeproj generated from the specified |
| gyp_file. |
| """ |
| # Be sure we're working with a copy of 'arguments' since we modify it. |
| # The caller may not be expecting it to be modified. |
| arguments = kw.get('arguments', [])[:] |
| arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')]) |
| if target == self.ALL: |
| arguments.append('-alltargets',) |
| elif target not in (None, self.DEFAULT): |
| arguments.extend(['-target', target]) |
| if self.configuration: |
| arguments.extend(['-configuration', self.configuration]) |
| symroot = kw.get('SYMROOT', '$SRCROOT/build') |
| if symroot: |
| arguments.append('SYMROOT='+symroot) |
| kw['arguments'] = arguments |
| |
| # Work around spurious stderr output from Xcode 4, http://crbug.com/181012 |
| match = kw.pop('match', self.match) |
| def match_filter_xcode(actual, expected): |
| if actual: |
| if not TestCmd.is_List(actual): |
| actual = actual.split('\n') |
| if not TestCmd.is_List(expected): |
| expected = expected.split('\n') |
| actual = [a for a in actual |
| if 'No recorder, buildTask: <Xcode3BuildTask:' not in a] |
| return match(actual, expected) |
| kw['match'] = match_filter_xcode |
| |
| return self.run(program=self.build_tool, **kw) |
| def up_to_date(self, gyp_file, target=None, **kw): |
| """ |
| Verifies that a build of the specified Xcode target is up to date. |
| """ |
| result = self.build(gyp_file, target, **kw) |
| if not result: |
| output = self.stdout() |
| for expression in self.strip_up_to_date_expressions: |
| output = expression.sub('', output) |
| if not output.endswith(self.up_to_date_endings): |
| self.report_not_up_to_date() |
| self.fail_test() |
| return result |
| def run_built_executable(self, name, *args, **kw): |
| """ |
| Runs an executable built by xcodebuild. |
| """ |
| configuration = self.configuration_dirname() |
| os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration) |
| # Enclosing the name in a list avoids prepending the original dir. |
| program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] |
| return self.run(program=program, *args, **kw) |
| def built_file_path(self, name, type=None, **kw): |
| """ |
| Returns a path to the specified file name, of the specified type, |
| as built by Xcode. |
| |
| Built files are in the subdirectory 'build/{configuration}'. |
| The default is 'build/Default'. |
| |
| A chdir= keyword argument specifies the source directory |
| relative to which the output subdirectory can be found. |
| |
| "type" values of STATIC_LIB or SHARED_LIB append the necessary |
| prefixes and suffixes to a platform-independent library base name. |
| """ |
| result = [] |
| chdir = kw.get('chdir') |
| if chdir: |
| result.append(chdir) |
| configuration = self.configuration_dirname() |
| result.extend(['build', configuration]) |
| result.append(self.built_file_basename(name, type, **kw)) |
| return self.workpath(*result) |
| |
| |
| format_class_list = [ |
| TestGypGypd, |
| TestGypAndroid, |
| TestGypCMake, |
| TestGypMake, |
| TestGypMSVS, |
| TestGypMSVSNinja, |
| TestGypNinja, |
| TestGypXcode, |
| ] |
| |
| def TestGyp(*args, **kw): |
| """ |
| Returns an appropriate TestGyp* instance for a specified GYP format. |
| """ |
| format = kw.pop('format', os.environ.get('TESTGYP_FORMAT')) |
| for format_class in format_class_list: |
| if format == format_class.format: |
| return format_class(*args, **kw) |
| raise Exception, "unknown format %r" % format |