1260 lines
44 KiB
Python
1260 lines
44 KiB
Python
# 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.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
import errno
|
|
import itertools
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from contextlib import contextmanager
|
|
|
|
from six.moves import collections_abc
|
|
|
|
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
|
|
module_ = TestCommon.module_prefix
|
|
_module = TestCommon.module_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__'
|
|
LOADABLE_MODULE = '__loadable_module__'
|
|
|
|
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)
|
|
|
|
# The gyp tests are run with HOME pointing to |dest_dir| to provide an
|
|
# hermetic environment. Symlink login.keychain and the 'Provisioning
|
|
# Profiles' folder to allow codesign to access to the data required for
|
|
# signing binaries.
|
|
if sys.platform == 'darwin':
|
|
old_keychain = GetDefaultKeychainPath()
|
|
old_provisioning_profiles = os.path.join(
|
|
os.environ['HOME'], 'Library', 'MobileDevice',
|
|
'Provisioning Profiles')
|
|
|
|
new_keychain = os.path.join(dest_dir, 'Library', 'Keychains')
|
|
MakeDirs(new_keychain)
|
|
os.symlink(old_keychain, os.path.join(new_keychain, 'login.keychain'))
|
|
|
|
if os.path.exists(old_provisioning_profiles):
|
|
new_provisioning_profiles = os.path.join(
|
|
dest_dir, 'Library', 'MobileDevice')
|
|
MakeDirs(new_provisioning_profiles)
|
|
os.symlink(old_provisioning_profiles,
|
|
os.path.join(new_provisioning_profiles, 'Provisioning Profiles'))
|
|
|
|
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)
|
|
# Default xcode_ninja_target_pattern to ^.*$ to fix xcode-ninja tests
|
|
xcode_ninja_target_pattern = kw.pop('xcode_ninja_target_pattern', '.*')
|
|
if self is TestGypXcodeNinja:
|
|
run_args.extend(
|
|
['-G', 'xcode_ninja_target_pattern=%s' % xcode_ninja_target_pattern])
|
|
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 'SYMROOT' in kw:
|
|
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
|
|
elif type == self.LOADABLE_MODULE:
|
|
name = self.module_ + name + self._module
|
|
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 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_abc.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 MakeDirs(new_dir):
|
|
"""A wrapper around os.makedirs() that emulates "mkdir -p"."""
|
|
try:
|
|
os.makedirs(new_dir)
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise
|
|
|
|
def GetDefaultKeychainPath():
|
|
"""Get the keychain path, for used before updating HOME."""
|
|
assert sys.platform == 'darwin'
|
|
# Format is:
|
|
# $ security default-keychain
|
|
# "/Some/Path/To/default.keychain"
|
|
path = subprocess.check_output(['security', 'default-keychain']).decode(
|
|
'utf-8', 'ignore').strip()
|
|
return path[1:-1]
|
|
|
|
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.
|
|
"""
|
|
override_build_tool = os.environ.get('GYP_BUILD_TOOL')
|
|
if override_build_tool:
|
|
return override_build_tool, True, override_build_tool
|
|
|
|
possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix)
|
|
for drive in range(ord('C'), ord('Z') + 1)
|
|
for suffix in ['', ' (x86)']]
|
|
possible_paths = {
|
|
'2017': r'Microsoft Visual Studio\2017',
|
|
'2015': r'Microsoft Visual Studio 14.0\Common7\IDE\devenv.com',
|
|
'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 ['2017', 'auto']:
|
|
msbuild_exes = []
|
|
try:
|
|
path = possible_paths['2017']
|
|
for r in possible_roots:
|
|
build_tool = os.path.join(r, path)
|
|
if os.path.exists(build_tool):
|
|
break;
|
|
else:
|
|
build_tool = None
|
|
if not build_tool:
|
|
args1 = ['reg', 'query',
|
|
'HKLM\Software\Microsoft\VisualStudio\SxS\VS7',
|
|
'/v', '15.0', '/reg:32']
|
|
build_tool = subprocess.check_output(args1).decode(
|
|
'utf-8', 'ignore').strip().split(b'\r\n').pop().split(b' ').pop()
|
|
build_tool = build_tool.decode('utf-8')
|
|
if build_tool:
|
|
args2 = ['cmd.exe', '/d', '/c',
|
|
'cd', '/d', build_tool,
|
|
'&', 'dir', '/b', '/s', 'msbuild.exe']
|
|
msbuild_exes = subprocess.check_output(args2).strip().split(b'\r\n')
|
|
msbuild_exes = [m.decode('utf-8') for m in msbuild_exes]
|
|
if len(msbuild_exes):
|
|
msbuild_Path = os.path.join(build_tool, msbuild_exes[0])
|
|
if os.path.exists(msbuild_Path):
|
|
os.environ['GYP_MSVS_VERSION'] = '2017'
|
|
os.environ['GYP_BUILD_TOOL'] = msbuild_Path
|
|
return msbuild_Path, True, msbuild_Path
|
|
except Exception as e:
|
|
pass
|
|
|
|
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]
|
|
|
|
# Check for location of Community install (in VS2017, at least).
|
|
vcvars_path = os.path.join(devenv_path, '..', '..', '..', '..', 'VC',
|
|
'Auxiliary', 'Build', 'vcvars32.bat')
|
|
if os.path.exists(vcvars_path):
|
|
return os.path.abspath(vcvars_path)
|
|
|
|
vsvars_path = os.path.join(devenv_path, '..', '..', 'Tools',
|
|
'vsvars32.bat')
|
|
return os.path.abspath(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].decode('utf-8', 'ignore')
|
|
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.
|
|
"""
|
|
if '15.0' in self.build_tool:
|
|
configuration = '/p:Configuration=' + (
|
|
self.configuration or self.configuration_buildname())
|
|
build = '/t'
|
|
if target not in (None, self.ALL, self.DEFAULT):
|
|
build += ':' + target
|
|
if clean:
|
|
build += ':Clean'
|
|
elif rebuild:
|
|
build += ':Rebuild'
|
|
elif ':' not in build:
|
|
build += ':Build'
|
|
arguments = kw.get('arguments', [])[:]
|
|
arguments.extend([gyp_file.replace('.gyp', '.sln'),
|
|
build, configuration])
|
|
else:
|
|
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):
|
|
r"""
|
|
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 and
|
|
'Beginning test session' not in a and
|
|
'Writing diagnostic log' not in a and
|
|
'Logs/Test/' 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)
|
|
|
|
|
|
class TestGypXcodeNinja(TestGypXcode):
|
|
"""
|
|
Subclass for testing the GYP Xcode Ninja generator.
|
|
"""
|
|
format = 'xcode-ninja'
|
|
|
|
def initialize_build_tool(self):
|
|
super(TestGypXcodeNinja, 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, **kw):
|
|
"""
|
|
Runs an xcodebuild using the .xcodeproj generated from the specified
|
|
gyp_file.
|
|
"""
|
|
build_config = self.configuration
|
|
if build_config and build_config.endswith(('-iphoneos',
|
|
'-iphonesimulator')):
|
|
build_config, sdk = self.configuration.split('-')
|
|
kw['arguments'] = kw.get('arguments', []) + ['-sdk', sdk]
|
|
|
|
with self._build_configuration(build_config):
|
|
return super(TestGypXcodeNinja, self).build(
|
|
gyp_file.replace('.gyp', '.ninja.gyp'), target, **kw)
|
|
|
|
@contextmanager
|
|
def _build_configuration(self, build_config):
|
|
config = self.configuration
|
|
self.configuration = build_config
|
|
try:
|
|
yield
|
|
finally:
|
|
self.configuration = config
|
|
|
|
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())
|
|
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
|
|
|
|
def run_built_executable(self, name, *args, **kw):
|
|
"""
|
|
Runs an executable built by xcodebuild + ninja.
|
|
"""
|
|
configuration = self.configuration_dirname()
|
|
os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', 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)
|
|
|
|
|
|
format_class_list = [
|
|
TestGypGypd,
|
|
TestGypCMake,
|
|
TestGypMake,
|
|
TestGypMSVS,
|
|
TestGypMSVSNinja,
|
|
TestGypNinja,
|
|
TestGypXcode,
|
|
TestGypXcodeNinja,
|
|
]
|
|
|
|
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)
|