1# Copyright (c) 2012 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6TestGyp.py:  a testing framework for GYP integration tests.
7"""
8
9import collections
10from contextlib import contextmanager
11import itertools
12import os
13import re
14import shutil
15import subprocess
16import sys
17import tempfile
18
19import TestCmd
20import TestCommon
21from TestCommon import __all__
22
23__all__.extend([
24  'TestGyp',
25])
26
27
28def remove_debug_line_numbers(contents):
29  """Function to remove the line numbers from the debug output
30  of gyp and thus reduce the extreme fragility of the stdout
31  comparison tests.
32  """
33  lines = contents.splitlines()
34  # split each line on ":"
35  lines = [l.split(":", 3) for l in lines]
36  # join each line back together while ignoring the
37  # 3rd column which is the line number
38  lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines]
39  return "\n".join(lines)
40
41
42def match_modulo_line_numbers(contents_a, contents_b):
43  """File contents matcher that ignores line numbers."""
44  contents_a = remove_debug_line_numbers(contents_a)
45  contents_b = remove_debug_line_numbers(contents_b)
46  return TestCommon.match_exact(contents_a, contents_b)
47
48
49@contextmanager
50def LocalEnv(local_env):
51  """Context manager to provide a local OS environment."""
52  old_env = os.environ.copy()
53  os.environ.update(local_env)
54  try:
55    yield
56  finally:
57    os.environ.clear()
58    os.environ.update(old_env)
59
60
61class TestGypBase(TestCommon.TestCommon):
62  """
63  Class for controlling end-to-end tests of gyp generators.
64
65  Instantiating this class will create a temporary directory and
66  arrange for its destruction (via the TestCmd superclass) and
67  copy all of the non-gyptest files in the directory hierarchy of the
68  executing script.
69
70  The default behavior is to test the 'gyp' or 'gyp.bat' file in the
71  current directory.  An alternative may be specified explicitly on
72  instantiation, or by setting the TESTGYP_GYP environment variable.
73
74  This class should be subclassed for each supported gyp generator
75  (format).  Various abstract methods below define calling signatures
76  used by the test scripts to invoke builds on the generated build
77  configuration and to run executables generated by those builds.
78  """
79
80  formats = []
81  build_tool = None
82  build_tool_list = []
83
84  _exe = TestCommon.exe_suffix
85  _obj = TestCommon.obj_suffix
86  shobj_ = TestCommon.shobj_prefix
87  _shobj = TestCommon.shobj_suffix
88  lib_ = TestCommon.lib_prefix
89  _lib = TestCommon.lib_suffix
90  dll_ = TestCommon.dll_prefix
91  _dll = TestCommon.dll_suffix
92  module_ = TestCommon.module_prefix
93  _module = TestCommon.module_suffix
94
95  # Constants to represent different targets.
96  ALL = '__all__'
97  DEFAULT = '__default__'
98
99  # Constants for different target types.
100  EXECUTABLE = '__executable__'
101  STATIC_LIB = '__static_lib__'
102  SHARED_LIB = '__shared_lib__'
103  LOADABLE_MODULE = '__loadable_module__'
104
105  def __init__(self, gyp=None, *args, **kw):
106    self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0]))
107    self.extra_args = sys.argv[1:]
108
109    if not gyp:
110      gyp = os.environ.get('TESTGYP_GYP')
111      if not gyp:
112        if sys.platform == 'win32':
113          gyp = 'gyp.bat'
114        else:
115          gyp = 'gyp'
116    self.gyp = os.path.abspath(gyp)
117    self.no_parallel = False
118
119    self.formats = [self.format]
120
121    self.initialize_build_tool()
122
123    kw.setdefault('match', TestCommon.match_exact)
124
125    # Put test output in out/testworkarea by default.
126    # Use temporary names so there are no collisions.
127    workdir = os.path.join('out', kw.get('workdir', 'testworkarea'))
128    # Create work area if it doesn't already exist.
129    if not os.path.isdir(workdir):
130      os.makedirs(workdir)
131
132    kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir)
133
134    formats = kw.pop('formats', [])
135
136    super(TestGypBase, self).__init__(*args, **kw)
137
138    real_format = self.format.split('-')[-1]
139    excluded_formats = set([f for f in formats if f[0] == '!'])
140    included_formats = set(formats) - excluded_formats
141    if ('!'+real_format in excluded_formats or
142        included_formats and real_format not in included_formats):
143      msg = 'Invalid test for %r format; skipping test.\n'
144      self.skip_test(msg % self.format)
145
146    self.copy_test_configuration(self.origin_cwd, self.workdir)
147    self.set_configuration(None)
148
149    # Set $HOME so that gyp doesn't read the user's actual
150    # ~/.gyp/include.gypi file, which may contain variables
151    # and other settings that would change the output.
152    os.environ['HOME'] = self.workpath()
153    # Clear $GYP_DEFINES for the same reason.
154    if 'GYP_DEFINES' in os.environ:
155      del os.environ['GYP_DEFINES']
156    # Override the user's language settings, which could
157    # otherwise make the output vary from what is expected.
158    os.environ['LC_ALL'] = 'C'
159
160  def built_file_must_exist(self, name, type=None, **kw):
161    """
162    Fails the test if the specified built file name does not exist.
163    """
164    return self.must_exist(self.built_file_path(name, type, **kw))
165
166  def built_file_must_not_exist(self, name, type=None, **kw):
167    """
168    Fails the test if the specified built file name exists.
169    """
170    return self.must_not_exist(self.built_file_path(name, type, **kw))
171
172  def built_file_must_match(self, name, contents, **kw):
173    """
174    Fails the test if the contents of the specified built file name
175    do not match the specified contents.
176    """
177    return self.must_match(self.built_file_path(name, **kw), contents)
178
179  def built_file_must_not_match(self, name, contents, **kw):
180    """
181    Fails the test if the contents of the specified built file name
182    match the specified contents.
183    """
184    return self.must_not_match(self.built_file_path(name, **kw), contents)
185
186  def built_file_must_not_contain(self, name, contents, **kw):
187    """
188    Fails the test if the specified built file name contains the specified
189    contents.
190    """
191    return self.must_not_contain(self.built_file_path(name, **kw), contents)
192
193  def copy_test_configuration(self, source_dir, dest_dir):
194    """
195    Copies the test configuration from the specified source_dir
196    (the directory in which the test script lives) to the
197    specified dest_dir (a temporary working directory).
198
199    This ignores all files and directories that begin with
200    the string 'gyptest', and all '.svn' subdirectories.
201    """
202    for root, dirs, files in os.walk(source_dir):
203      if '.svn' in dirs:
204        dirs.remove('.svn')
205      dirs = [ d for d in dirs if not d.startswith('gyptest') ]
206      files = [ f for f in files if not f.startswith('gyptest') ]
207      for dirname in dirs:
208        source = os.path.join(root, dirname)
209        destination = source.replace(source_dir, dest_dir)
210        os.mkdir(destination)
211        if sys.platform != 'win32':
212          shutil.copystat(source, destination)
213      for filename in files:
214        source = os.path.join(root, filename)
215        destination = source.replace(source_dir, dest_dir)
216        shutil.copy2(source, destination)
217
218    # The gyp tests are run with HOME pointing to |dest_dir| to provide an
219    # hermetic environment. Symlink login.keychain and the 'Provisioning
220    # Profiles' folder to allow codesign to access to the data required for
221    # signing binaries.
222    if sys.platform == 'darwin':
223      old_keychain = GetDefaultKeychainPath()
224      old_provisioning_profiles = os.path.join(
225          os.environ['HOME'], 'Library', 'MobileDevice',
226          'Provisioning Profiles')
227
228      new_keychain = os.path.join(dest_dir, 'Library', 'Keychains')
229      MakeDirs(new_keychain)
230      os.symlink(old_keychain, os.path.join(new_keychain, 'login.keychain'))
231
232      if os.path.exists(old_provisioning_profiles):
233        new_provisioning_profiles = os.path.join(
234            dest_dir, 'Library', 'MobileDevice')
235        MakeDirs(new_provisioning_profiles)
236        os.symlink(old_provisioning_profiles,
237            os.path.join(new_provisioning_profiles, 'Provisioning Profiles'))
238
239  def initialize_build_tool(self):
240    """
241    Initializes the .build_tool attribute.
242
243    Searches the .build_tool_list for an executable name on the user's
244    $PATH.  The first tool on the list is used as-is if nothing is found
245    on the current $PATH.
246    """
247    for build_tool in self.build_tool_list:
248      if not build_tool:
249        continue
250      if os.path.isabs(build_tool):
251        self.build_tool = build_tool
252        return
253      build_tool = self.where_is(build_tool)
254      if build_tool:
255        self.build_tool = build_tool
256        return
257
258    if self.build_tool_list:
259      self.build_tool = self.build_tool_list[0]
260
261  def relocate(self, source, destination):
262    """
263    Renames (relocates) the specified source (usually a directory)
264    to the specified destination, creating the destination directory
265    first if necessary.
266
267    Note:  Don't use this as a generic "rename" operation.  In the
268    future, "relocating" parts of a GYP tree may affect the state of
269    the test to modify the behavior of later method calls.
270    """
271    destination_dir = os.path.dirname(destination)
272    if not os.path.exists(destination_dir):
273      self.subdir(destination_dir)
274    os.rename(source, destination)
275
276  def report_not_up_to_date(self):
277    """
278    Reports that a build is not up-to-date.
279
280    This provides common reporting for formats that have complicated
281    conditions for checking whether a build is up-to-date.  Formats
282    that expect exact output from the command (make) can
283    just set stdout= when they call the run_build() method.
284    """
285    print "Build is not up-to-date:"
286    print self.banner('STDOUT ')
287    print self.stdout()
288    stderr = self.stderr()
289    if stderr:
290      print self.banner('STDERR ')
291      print stderr
292
293  def run_gyp(self, gyp_file, *args, **kw):
294    """
295    Runs gyp against the specified gyp_file with the specified args.
296    """
297
298    # When running gyp, and comparing its output we use a comparitor
299    # that ignores the line numbers that gyp logs in its debug output.
300    if kw.pop('ignore_line_numbers', False):
301      kw.setdefault('match', match_modulo_line_numbers)
302
303    # TODO:  --depth=. works around Chromium-specific tree climbing.
304    depth = kw.pop('depth', '.')
305    run_args = ['--depth='+depth]
306    run_args.extend(['--format='+f for f in self.formats]);
307    run_args.append(gyp_file)
308    if self.no_parallel:
309      run_args += ['--no-parallel']
310    # TODO: if extra_args contains a '--build' flag
311    # we really want that to only apply to the last format (self.format).
312    run_args.extend(self.extra_args)
313    # Default xcode_ninja_target_pattern to ^.*$ to fix xcode-ninja tests
314    xcode_ninja_target_pattern = kw.pop('xcode_ninja_target_pattern', '.*')
315    run_args.extend(
316      ['-G', 'xcode_ninja_target_pattern=%s' % xcode_ninja_target_pattern])
317    run_args.extend(args)
318    return self.run(program=self.gyp, arguments=run_args, **kw)
319
320  def run(self, *args, **kw):
321    """
322    Executes a program by calling the superclass .run() method.
323
324    This exists to provide a common place to filter out keyword
325    arguments implemented in this layer, without having to update
326    the tool-specific subclasses or clutter the tests themselves
327    with platform-specific code.
328    """
329    if kw.has_key('SYMROOT'):
330      del kw['SYMROOT']
331    super(TestGypBase, self).run(*args, **kw)
332
333  def set_configuration(self, configuration):
334    """
335    Sets the configuration, to be used for invoking the build
336    tool and testing potential built output.
337    """
338    self.configuration = configuration
339
340  def configuration_dirname(self):
341    if self.configuration:
342      return self.configuration.split('|')[0]
343    else:
344      return 'Default'
345
346  def configuration_buildname(self):
347    if self.configuration:
348      return self.configuration
349    else:
350      return 'Default'
351
352  #
353  # Abstract methods to be defined by format-specific subclasses.
354  #
355
356  def build(self, gyp_file, target=None, **kw):
357    """
358    Runs a build of the specified target against the configuration
359    generated from the specified gyp_file.
360
361    A 'target' argument of None or the special value TestGyp.DEFAULT
362    specifies the default argument for the underlying build tool.
363    A 'target' argument of TestGyp.ALL specifies the 'all' target
364    (if any) of the underlying build tool.
365    """
366    raise NotImplementedError
367
368  def built_file_path(self, name, type=None, **kw):
369    """
370    Returns a path to the specified file name, of the specified type.
371    """
372    raise NotImplementedError
373
374  def built_file_basename(self, name, type=None, **kw):
375    """
376    Returns the base name of the specified file name, of the specified type.
377
378    A bare=True keyword argument specifies that prefixes and suffixes shouldn't
379    be applied.
380    """
381    if not kw.get('bare'):
382      if type == self.EXECUTABLE:
383        name = name + self._exe
384      elif type == self.STATIC_LIB:
385        name = self.lib_ + name + self._lib
386      elif type == self.SHARED_LIB:
387        name = self.dll_ + name + self._dll
388      elif type == self.LOADABLE_MODULE:
389        name = self.module_ + name + self._module
390    return name
391
392  def run_built_executable(self, name, *args, **kw):
393    """
394    Runs an executable program built from a gyp-generated configuration.
395
396    The specified name should be independent of any particular generator.
397    Subclasses should find the output executable in the appropriate
398    output build directory, tack on any necessary executable suffix, etc.
399    """
400    raise NotImplementedError
401
402  def up_to_date(self, gyp_file, target=None, **kw):
403    """
404    Verifies that a build of the specified target is up to date.
405
406    The subclass should implement this by calling build()
407    (or a reasonable equivalent), checking whatever conditions
408    will tell it the build was an "up to date" null build, and
409    failing if it isn't.
410    """
411    raise NotImplementedError
412
413
414class TestGypGypd(TestGypBase):
415  """
416  Subclass for testing the GYP 'gypd' generator (spit out the
417  internal data structure as pretty-printed Python).
418  """
419  format = 'gypd'
420  def __init__(self, gyp=None, *args, **kw):
421    super(TestGypGypd, self).__init__(*args, **kw)
422    # gypd implies the use of 'golden' files, so parallelizing conflicts as it
423    # causes ordering changes.
424    self.no_parallel = True
425
426
427class TestGypCustom(TestGypBase):
428  """
429  Subclass for testing the GYP with custom generator
430  """
431
432  def __init__(self, gyp=None, *args, **kw):
433    self.format = kw.pop("format")
434    super(TestGypCustom, self).__init__(*args, **kw)
435
436
437class TestGypCMake(TestGypBase):
438  """
439  Subclass for testing the GYP CMake generator, using cmake's ninja backend.
440  """
441  format = 'cmake'
442  build_tool_list = ['cmake']
443  ALL = 'all'
444
445  def cmake_build(self, gyp_file, target=None, **kw):
446    arguments = kw.get('arguments', [])[:]
447
448    self.build_tool_list = ['cmake']
449    self.initialize_build_tool()
450
451    chdir = os.path.join(kw.get('chdir', '.'),
452                         'out',
453                         self.configuration_dirname())
454    kw['chdir'] = chdir
455
456    arguments.append('-G')
457    arguments.append('Ninja')
458
459    kw['arguments'] = arguments
460
461    stderr = kw.get('stderr', None)
462    if stderr:
463      kw['stderr'] = stderr.split('$$$')[0]
464
465    self.run(program=self.build_tool, **kw)
466
467  def ninja_build(self, gyp_file, target=None, **kw):
468    arguments = kw.get('arguments', [])[:]
469
470    self.build_tool_list = ['ninja']
471    self.initialize_build_tool()
472
473    # Add a -C output/path to the command line.
474    arguments.append('-C')
475    arguments.append(os.path.join('out', self.configuration_dirname()))
476
477    if target not in (None, self.DEFAULT):
478      arguments.append(target)
479
480    kw['arguments'] = arguments
481
482    stderr = kw.get('stderr', None)
483    if stderr:
484      stderrs = stderr.split('$$$')
485      kw['stderr'] = stderrs[1] if len(stderrs) > 1 else ''
486
487    return self.run(program=self.build_tool, **kw)
488
489  def build(self, gyp_file, target=None, status=0, **kw):
490    # Two tools must be run to build, cmake and the ninja.
491    # Allow cmake to succeed when the overall expectation is to fail.
492    if status is None:
493      kw['status'] = None
494    else:
495      if not isinstance(status, collections.Iterable): status = (status,)
496      kw['status'] = list(itertools.chain((0,), status))
497    self.cmake_build(gyp_file, target, **kw)
498    kw['status'] = status
499    self.ninja_build(gyp_file, target, **kw)
500
501  def run_built_executable(self, name, *args, **kw):
502    # Enclosing the name in a list avoids prepending the original dir.
503    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
504    if sys.platform == 'darwin':
505      configuration = self.configuration_dirname()
506      os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
507    return self.run(program=program, *args, **kw)
508
509  def built_file_path(self, name, type=None, **kw):
510    result = []
511    chdir = kw.get('chdir')
512    if chdir:
513      result.append(chdir)
514    result.append('out')
515    result.append(self.configuration_dirname())
516    if type == self.STATIC_LIB:
517      if sys.platform != 'darwin':
518        result.append('obj.target')
519    elif type == self.SHARED_LIB:
520      if sys.platform != 'darwin' and sys.platform != 'win32':
521        result.append('lib.target')
522    subdir = kw.get('subdir')
523    if subdir and type != self.SHARED_LIB:
524      result.append(subdir)
525    result.append(self.built_file_basename(name, type, **kw))
526    return self.workpath(*result)
527
528  def up_to_date(self, gyp_file, target=None, **kw):
529    result = self.ninja_build(gyp_file, target, **kw)
530    if not result:
531      stdout = self.stdout()
532      if 'ninja: no work to do' not in stdout:
533        self.report_not_up_to_date()
534        self.fail_test()
535    return result
536
537
538class TestGypMake(TestGypBase):
539  """
540  Subclass for testing the GYP Make generator.
541  """
542  format = 'make'
543  build_tool_list = ['make']
544  ALL = 'all'
545  def build(self, gyp_file, target=None, **kw):
546    """
547    Runs a Make build using the Makefiles generated from the specified
548    gyp_file.
549    """
550    arguments = kw.get('arguments', [])[:]
551    if self.configuration:
552      arguments.append('BUILDTYPE=' + self.configuration)
553    if target not in (None, self.DEFAULT):
554      arguments.append(target)
555    # Sub-directory builds provide per-gyp Makefiles (i.e.
556    # Makefile.gyp_filename), so use that if there is no Makefile.
557    chdir = kw.get('chdir', '')
558    if not os.path.exists(os.path.join(chdir, 'Makefile')):
559      print "NO Makefile in " + os.path.join(chdir, 'Makefile')
560      arguments.insert(0, '-f')
561      arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile')
562    kw['arguments'] = arguments
563    return self.run(program=self.build_tool, **kw)
564  def up_to_date(self, gyp_file, target=None, **kw):
565    """
566    Verifies that a build of the specified Make target is up to date.
567    """
568    if target in (None, self.DEFAULT):
569      message_target = 'all'
570    else:
571      message_target = target
572    kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target
573    return self.build(gyp_file, target, **kw)
574  def run_built_executable(self, name, *args, **kw):
575    """
576    Runs an executable built by Make.
577    """
578    configuration = self.configuration_dirname()
579    libdir = os.path.join('out', configuration, 'lib')
580    # TODO(piman): when everything is cross-compile safe, remove lib.target
581    if sys.platform == 'darwin':
582      # Mac puts target shared libraries right in the product directory.
583      configuration = self.configuration_dirname()
584      os.environ['DYLD_LIBRARY_PATH'] = (
585          libdir + '.host:' + os.path.join('out', configuration))
586    else:
587      os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target'
588    # Enclosing the name in a list avoids prepending the original dir.
589    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
590    return self.run(program=program, *args, **kw)
591  def built_file_path(self, name, type=None, **kw):
592    """
593    Returns a path to the specified file name, of the specified type,
594    as built by Make.
595
596    Built files are in the subdirectory 'out/{configuration}'.
597    The default is 'out/Default'.
598
599    A chdir= keyword argument specifies the source directory
600    relative to which  the output subdirectory can be found.
601
602    "type" values of STATIC_LIB or SHARED_LIB append the necessary
603    prefixes and suffixes to a platform-independent library base name.
604
605    A subdir= keyword argument specifies a library subdirectory within
606    the default 'obj.target'.
607    """
608    result = []
609    chdir = kw.get('chdir')
610    if chdir:
611      result.append(chdir)
612    configuration = self.configuration_dirname()
613    result.extend(['out', configuration])
614    if type == self.STATIC_LIB and sys.platform != 'darwin':
615      result.append('obj.target')
616    elif type == self.SHARED_LIB and sys.platform != 'darwin':
617      result.append('lib.target')
618    subdir = kw.get('subdir')
619    if subdir and type != self.SHARED_LIB:
620      result.append(subdir)
621    result.append(self.built_file_basename(name, type, **kw))
622    return self.workpath(*result)
623
624
625def ConvertToCygpath(path):
626  """Convert to cygwin path if we are using cygwin."""
627  if sys.platform == 'cygwin':
628    p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE)
629    path = p.communicate()[0].strip()
630  return path
631
632
633def MakeDirs(new_dir):
634  """A wrapper around os.makedirs() that emulates "mkdir -p"."""
635  try:
636    os.makedirs(new_dir)
637  except OSError as e:
638    if e.errno != errno.EEXIST:
639      raise
640
641def GetDefaultKeychainPath():
642  """Get the keychain path, for used before updating HOME."""
643  assert sys.platform == 'darwin'
644  # Format is:
645  # $ security default-keychain
646  #     "/Some/Path/To/default.keychain"
647  path = subprocess.check_output(['security', 'default-keychain']).strip()
648  return path[1:-1]
649
650def FindMSBuildInstallation(msvs_version = 'auto'):
651  """Returns path to MSBuild for msvs_version or latest available.
652
653  Looks in the registry to find install location of MSBuild.
654  MSBuild before v4.0 will not build c++ projects, so only use newer versions.
655  """
656  import TestWin
657  registry = TestWin.Registry()
658
659  msvs_to_msbuild = {
660      '2013': r'12.0',
661      '2012': r'4.0',  # Really v4.0.30319 which comes with .NET 4.5.
662      '2010': r'4.0'}
663
664  msbuild_basekey = r'HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions'
665  if not registry.KeyExists(msbuild_basekey):
666    print 'Error: could not find MSBuild base registry entry'
667    return None
668
669  msbuild_version = None
670  if msvs_version in msvs_to_msbuild:
671    msbuild_test_version = msvs_to_msbuild[msvs_version]
672    if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
673      msbuild_version = msbuild_test_version
674    else:
675      print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
676             'but corresponding MSBuild "%s" was not found.' %
677             (msvs_version, msbuild_version))
678  if not msbuild_version:
679    for msvs_version in sorted(msvs_to_msbuild, reverse=True):
680      msbuild_test_version = msvs_to_msbuild[msvs_version]
681      if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
682        msbuild_version = msbuild_test_version
683        break
684  if not msbuild_version:
685    print 'Error: could not find MSBuild registry entry'
686    return None
687
688  msbuild_path = registry.GetValue(msbuild_basekey + '\\' + msbuild_version,
689                                   'MSBuildToolsPath')
690  if not msbuild_path:
691    print 'Error: could not get MSBuild registry entry value'
692    return None
693
694  return os.path.join(msbuild_path, 'MSBuild.exe')
695
696
697def FindVisualStudioInstallation():
698  """Returns appropriate values for .build_tool and .uses_msbuild fields
699  of TestGypBase for Visual Studio.
700
701  We use the value specified by GYP_MSVS_VERSION.  If not specified, we
702  search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable.
703  Failing that, we search for likely deployment paths.
704  """
705  possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix)
706                    for drive in range(ord('C'), ord('Z') + 1)
707                    for suffix in ['', ' (x86)']]
708  possible_paths = {
709      '2015': r'Microsoft Visual Studio 14.0\Common7\IDE\devenv.com',
710      '2013': r'Microsoft Visual Studio 12.0\Common7\IDE\devenv.com',
711      '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com',
712      '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com',
713      '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com',
714      '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'}
715
716  possible_roots = [ConvertToCygpath(r) for r in possible_roots]
717
718  msvs_version = 'auto'
719  for flag in (f for f in sys.argv if f.startswith('msvs_version=')):
720    msvs_version = flag.split('=')[-1]
721  msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version)
722
723  if msvs_version in possible_paths:
724    # Check that the path to the specified GYP_MSVS_VERSION exists.
725    path = possible_paths[msvs_version]
726    for r in possible_roots:
727      build_tool = os.path.join(r, path)
728      if os.path.exists(build_tool):
729        uses_msbuild = msvs_version >= '2010'
730        msbuild_path = FindMSBuildInstallation(msvs_version)
731        return build_tool, uses_msbuild, msbuild_path
732    else:
733      print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
734              'but corresponding "%s" was not found.' % (msvs_version, path))
735  # Neither GYP_MSVS_VERSION nor the path help us out.  Iterate through
736  # the choices looking for a match.
737  for version in sorted(possible_paths, reverse=True):
738    path = possible_paths[version]
739    for r in possible_roots:
740      build_tool = os.path.join(r, path)
741      if os.path.exists(build_tool):
742        uses_msbuild = msvs_version >= '2010'
743        msbuild_path = FindMSBuildInstallation(msvs_version)
744        return build_tool, uses_msbuild, msbuild_path
745  print 'Error: could not find devenv'
746  sys.exit(1)
747
748class TestGypOnMSToolchain(TestGypBase):
749  """
750  Common subclass for testing generators that target the Microsoft Visual
751  Studio toolchain (cl, link, dumpbin, etc.)
752  """
753  @staticmethod
754  def _ComputeVsvarsPath(devenv_path):
755    devenv_dir = os.path.split(devenv_path)[0]
756    vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat')
757    return vsvars_path
758
759  def initialize_build_tool(self):
760    super(TestGypOnMSToolchain, self).initialize_build_tool()
761    if sys.platform in ('win32', 'cygwin'):
762      build_tools = FindVisualStudioInstallation()
763      self.devenv_path, self.uses_msbuild, self.msbuild_path = build_tools
764      self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath(
765          self.devenv_path)
766
767  def run_dumpbin(self, *dumpbin_args):
768    """Run the dumpbin tool with the specified arguments, and capturing and
769    returning stdout."""
770    assert sys.platform in ('win32', 'cygwin')
771    cmd = os.environ.get('COMSPEC', 'cmd.exe')
772    arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin']
773    arguments.extend(dumpbin_args)
774    proc = subprocess.Popen(arguments, stdout=subprocess.PIPE)
775    output = proc.communicate()[0]
776    assert not proc.returncode
777    return output
778
779class TestGypNinja(TestGypOnMSToolchain):
780  """
781  Subclass for testing the GYP Ninja generator.
782  """
783  format = 'ninja'
784  build_tool_list = ['ninja']
785  ALL = 'all'
786  DEFAULT = 'all'
787
788  def run_gyp(self, gyp_file, *args, **kw):
789    TestGypBase.run_gyp(self, gyp_file, *args, **kw)
790
791  def build(self, gyp_file, target=None, **kw):
792    arguments = kw.get('arguments', [])[:]
793
794    # Add a -C output/path to the command line.
795    arguments.append('-C')
796    arguments.append(os.path.join('out', self.configuration_dirname()))
797
798    if target is None:
799      target = 'all'
800    arguments.append(target)
801
802    kw['arguments'] = arguments
803    return self.run(program=self.build_tool, **kw)
804
805  def run_built_executable(self, name, *args, **kw):
806    # Enclosing the name in a list avoids prepending the original dir.
807    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
808    if sys.platform == 'darwin':
809      configuration = self.configuration_dirname()
810      os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
811    return self.run(program=program, *args, **kw)
812
813  def built_file_path(self, name, type=None, **kw):
814    result = []
815    chdir = kw.get('chdir')
816    if chdir:
817      result.append(chdir)
818    result.append('out')
819    result.append(self.configuration_dirname())
820    if type == self.STATIC_LIB:
821      if sys.platform != 'darwin':
822        result.append('obj')
823    elif type == self.SHARED_LIB:
824      if sys.platform != 'darwin' and sys.platform != 'win32':
825        result.append('lib')
826    subdir = kw.get('subdir')
827    if subdir and type != self.SHARED_LIB:
828      result.append(subdir)
829    result.append(self.built_file_basename(name, type, **kw))
830    return self.workpath(*result)
831
832  def up_to_date(self, gyp_file, target=None, **kw):
833    result = self.build(gyp_file, target, **kw)
834    if not result:
835      stdout = self.stdout()
836      if 'ninja: no work to do' not in stdout:
837        self.report_not_up_to_date()
838        self.fail_test()
839    return result
840
841
842class TestGypMSVS(TestGypOnMSToolchain):
843  """
844  Subclass for testing the GYP Visual Studio generator.
845  """
846  format = 'msvs'
847
848  u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ==='
849  up_to_date_re = re.compile(u, re.M)
850
851  # Initial None element will indicate to our .initialize_build_tool()
852  # method below that 'devenv' was not found on %PATH%.
853  #
854  # Note:  we must use devenv.com to be able to capture build output.
855  # Directly executing devenv.exe only sends output to BuildLog.htm.
856  build_tool_list = [None, 'devenv.com']
857
858  def initialize_build_tool(self):
859    super(TestGypMSVS, self).initialize_build_tool()
860    self.build_tool = self.devenv_path
861
862  def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
863    """
864    Runs a Visual Studio build using the configuration generated
865    from the specified gyp_file.
866    """
867    configuration = self.configuration_buildname()
868    if clean:
869      build = '/Clean'
870    elif rebuild:
871      build = '/Rebuild'
872    else:
873      build = '/Build'
874    arguments = kw.get('arguments', [])[:]
875    arguments.extend([gyp_file.replace('.gyp', '.sln'),
876                      build, configuration])
877    # Note:  the Visual Studio generator doesn't add an explicit 'all'
878    # target, so we just treat it the same as the default.
879    if target not in (None, self.ALL, self.DEFAULT):
880      arguments.extend(['/Project', target])
881    if self.configuration:
882      arguments.extend(['/ProjectConfig', self.configuration])
883    kw['arguments'] = arguments
884    return self.run(program=self.build_tool, **kw)
885  def up_to_date(self, gyp_file, target=None, **kw):
886    """
887    Verifies that a build of the specified Visual Studio target is up to date.
888
889    Beware that VS2010 will behave strangely if you build under
890    C:\USERS\yourname\AppData\Local. It will cause needless work.  The ouptut
891    will be "1 succeeded and 0 up to date".  MSBuild tracing reveals that:
892    "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because
893    'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL'
894    was modified at 02/21/2011 17:03:30, which is newer than '' which was
895    modified at 01/01/0001 00:00:00.
896
897    The workaround is to specify a workdir when instantiating the test, e.g.
898    test = TestGyp.TestGyp(workdir='workarea')
899    """
900    result = self.build(gyp_file, target, **kw)
901    if not result:
902      stdout = self.stdout()
903
904      m = self.up_to_date_re.search(stdout)
905      up_to_date = m and int(m.group(1)) > 0
906      if not up_to_date:
907        self.report_not_up_to_date()
908        self.fail_test()
909    return result
910  def run_built_executable(self, name, *args, **kw):
911    """
912    Runs an executable built by Visual Studio.
913    """
914    configuration = self.configuration_dirname()
915    # Enclosing the name in a list avoids prepending the original dir.
916    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
917    return self.run(program=program, *args, **kw)
918  def built_file_path(self, name, type=None, **kw):
919    """
920    Returns a path to the specified file name, of the specified type,
921    as built by Visual Studio.
922
923    Built files are in a subdirectory that matches the configuration
924    name.  The default is 'Default'.
925
926    A chdir= keyword argument specifies the source directory
927    relative to which  the output subdirectory can be found.
928
929    "type" values of STATIC_LIB or SHARED_LIB append the necessary
930    prefixes and suffixes to a platform-independent library base name.
931    """
932    result = []
933    chdir = kw.get('chdir')
934    if chdir:
935      result.append(chdir)
936    result.append(self.configuration_dirname())
937    if type == self.STATIC_LIB:
938      result.append('lib')
939    result.append(self.built_file_basename(name, type, **kw))
940    return self.workpath(*result)
941
942
943class TestGypMSVSNinja(TestGypNinja):
944  """
945  Subclass for testing the GYP Visual Studio Ninja generator.
946  """
947  format = 'msvs-ninja'
948
949  def initialize_build_tool(self):
950    super(TestGypMSVSNinja, self).initialize_build_tool()
951    # When using '--build', make sure ninja is first in the format list.
952    self.formats.insert(0, 'ninja')
953
954  def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
955    """
956    Runs a Visual Studio build using the configuration generated
957    from the specified gyp_file.
958    """
959    arguments = kw.get('arguments', [])[:]
960    if target in (None, self.ALL, self.DEFAULT):
961      # Note: the Visual Studio generator doesn't add an explicit 'all' target.
962      # This will build each project. This will work if projects are hermetic,
963      # but may fail if they are not (a project may run more than once).
964      # It would be nice to supply an all.metaproj for MSBuild.
965      arguments.extend([gyp_file.replace('.gyp', '.sln')])
966    else:
967      # MSBuild documentation claims that one can specify a sln but then build a
968      # project target like 'msbuild a.sln /t:proj:target' but this format only
969      # supports 'Clean', 'Rebuild', and 'Publish' (with none meaning Default).
970      # This limitation is due to the .sln -> .sln.metaproj conversion.
971      # The ':' is not special, 'proj:target' is a target in the metaproj.
972      arguments.extend([target+'.vcxproj'])
973
974    if clean:
975      build = 'Clean'
976    elif rebuild:
977      build = 'Rebuild'
978    else:
979      build = 'Build'
980    arguments.extend(['/target:'+build])
981    configuration = self.configuration_buildname()
982    config = configuration.split('|')
983    arguments.extend(['/property:Configuration='+config[0]])
984    if len(config) > 1:
985      arguments.extend(['/property:Platform='+config[1]])
986    arguments.extend(['/property:BuildInParallel=false'])
987    arguments.extend(['/verbosity:minimal'])
988
989    kw['arguments'] = arguments
990    return self.run(program=self.msbuild_path, **kw)
991
992
993class TestGypXcode(TestGypBase):
994  """
995  Subclass for testing the GYP Xcode generator.
996  """
997  format = 'xcode'
998  build_tool_list = ['xcodebuild']
999
1000  phase_script_execution = ("\n"
1001                            "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n"
1002                            "    cd /\\S+\n"
1003                            "    /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n"
1004                            "(make: Nothing to be done for `all'\\.\n)?")
1005
1006  strip_up_to_date_expressions = [
1007    # Various actions or rules can run even when the overall build target
1008    # is up to date.  Strip those phases' GYP-generated output.
1009    re.compile(phase_script_execution, re.S),
1010
1011    # The message from distcc_pump can trail the "BUILD SUCCEEDED"
1012    # message, so strip that, too.
1013    re.compile('__________Shutting down distcc-pump include server\n', re.S),
1014  ]
1015
1016  up_to_date_endings = (
1017    'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1
1018    'Check dependencies\n** BUILD SUCCEEDED **\n\n',     # Xcode 3.2
1019    'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2
1020    'Check dependencies\n\n** BUILD SUCCEEDED **\n\n',   # Xcode 5.0
1021  )
1022
1023  def build(self, gyp_file, target=None, **kw):
1024    """
1025    Runs an xcodebuild using the .xcodeproj generated from the specified
1026    gyp_file.
1027    """
1028    # Be sure we're working with a copy of 'arguments' since we modify it.
1029    # The caller may not be expecting it to be modified.
1030    arguments = kw.get('arguments', [])[:]
1031    arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')])
1032    if target == self.ALL:
1033      arguments.append('-alltargets',)
1034    elif target not in (None, self.DEFAULT):
1035      arguments.extend(['-target', target])
1036    if self.configuration:
1037      arguments.extend(['-configuration', self.configuration])
1038    symroot = kw.get('SYMROOT', '$SRCROOT/build')
1039    if symroot:
1040      arguments.append('SYMROOT='+symroot)
1041    kw['arguments'] = arguments
1042
1043    # Work around spurious stderr output from Xcode 4, http://crbug.com/181012
1044    match = kw.pop('match', self.match)
1045    def match_filter_xcode(actual, expected):
1046      if actual:
1047        if not TestCmd.is_List(actual):
1048          actual = actual.split('\n')
1049        if not TestCmd.is_List(expected):
1050          expected = expected.split('\n')
1051        actual = [a for a in actual
1052                    if 'No recorder, buildTask: <Xcode3BuildTask:' not in a and
1053                       'Beginning test session' not in a]
1054      return match(actual, expected)
1055    kw['match'] = match_filter_xcode
1056
1057    return self.run(program=self.build_tool, **kw)
1058  def up_to_date(self, gyp_file, target=None, **kw):
1059    """
1060    Verifies that a build of the specified Xcode target is up to date.
1061    """
1062    result = self.build(gyp_file, target, **kw)
1063    if not result:
1064      output = self.stdout()
1065      for expression in self.strip_up_to_date_expressions:
1066        output = expression.sub('', output)
1067      if not output.endswith(self.up_to_date_endings):
1068        self.report_not_up_to_date()
1069        self.fail_test()
1070    return result
1071  def run_built_executable(self, name, *args, **kw):
1072    """
1073    Runs an executable built by xcodebuild.
1074    """
1075    configuration = self.configuration_dirname()
1076    os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
1077    # Enclosing the name in a list avoids prepending the original dir.
1078    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
1079    return self.run(program=program, *args, **kw)
1080  def built_file_path(self, name, type=None, **kw):
1081    """
1082    Returns a path to the specified file name, of the specified type,
1083    as built by Xcode.
1084
1085    Built files are in the subdirectory 'build/{configuration}'.
1086    The default is 'build/Default'.
1087
1088    A chdir= keyword argument specifies the source directory
1089    relative to which  the output subdirectory can be found.
1090
1091    "type" values of STATIC_LIB or SHARED_LIB append the necessary
1092    prefixes and suffixes to a platform-independent library base name.
1093    """
1094    result = []
1095    chdir = kw.get('chdir')
1096    if chdir:
1097      result.append(chdir)
1098    configuration = self.configuration_dirname()
1099    result.extend(['build', configuration])
1100    result.append(self.built_file_basename(name, type, **kw))
1101    return self.workpath(*result)
1102
1103
1104class TestGypXcodeNinja(TestGypXcode):
1105  """
1106  Subclass for testing the GYP Xcode Ninja generator.
1107  """
1108  format = 'xcode-ninja'
1109
1110  def initialize_build_tool(self):
1111    super(TestGypXcodeNinja, self).initialize_build_tool()
1112    # When using '--build', make sure ninja is first in the format list.
1113    self.formats.insert(0, 'ninja')
1114
1115  def build(self, gyp_file, target=None, **kw):
1116    """
1117    Runs an xcodebuild using the .xcodeproj generated from the specified
1118    gyp_file.
1119    """
1120    build_config = self.configuration
1121    if build_config and build_config.endswith(('-iphoneos',
1122                                               '-iphonesimulator')):
1123      build_config, sdk = self.configuration.split('-')
1124      kw['arguments'] = kw.get('arguments', []) + ['-sdk', sdk]
1125
1126    with self._build_configuration(build_config):
1127      return super(TestGypXcodeNinja, self).build(
1128        gyp_file.replace('.gyp', '.ninja.gyp'), target, **kw)
1129
1130  @contextmanager
1131  def _build_configuration(self, build_config):
1132    config = self.configuration
1133    self.configuration = build_config
1134    try:
1135      yield
1136    finally:
1137      self.configuration = config
1138
1139  def built_file_path(self, name, type=None, **kw):
1140    result = []
1141    chdir = kw.get('chdir')
1142    if chdir:
1143      result.append(chdir)
1144    result.append('out')
1145    result.append(self.configuration_dirname())
1146    subdir = kw.get('subdir')
1147    if subdir and type != self.SHARED_LIB:
1148      result.append(subdir)
1149    result.append(self.built_file_basename(name, type, **kw))
1150    return self.workpath(*result)
1151
1152  def up_to_date(self, gyp_file, target=None, **kw):
1153    result = self.build(gyp_file, target, **kw)
1154    if not result:
1155      stdout = self.stdout()
1156      if 'ninja: no work to do' not in stdout:
1157        self.report_not_up_to_date()
1158        self.fail_test()
1159    return result
1160
1161  def run_built_executable(self, name, *args, **kw):
1162    """
1163    Runs an executable built by xcodebuild + ninja.
1164    """
1165    configuration = self.configuration_dirname()
1166    os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
1167    # Enclosing the name in a list avoids prepending the original dir.
1168    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
1169    return self.run(program=program, *args, **kw)
1170
1171
1172format_class_list = [
1173  TestGypGypd,
1174  TestGypCMake,
1175  TestGypMake,
1176  TestGypMSVS,
1177  TestGypMSVSNinja,
1178  TestGypNinja,
1179  TestGypXcode,
1180  TestGypXcodeNinja,
1181]
1182
1183def TestGyp(*args, **kw):
1184  """
1185  Returns an appropriate TestGyp* instance for a specified GYP format.
1186  """
1187  format = kw.pop('format', os.environ.get('TESTGYP_FORMAT'))
1188  for format_class in format_class_list:
1189    if format == format_class.format:
1190      return format_class(*args, **kw)
1191  raise Exception, "unknown format %r" % format
1192