blob: fc7b135817791c9c1bbd125f0f1bdab4ff6313fb [file] [log] [blame]
Ben Murdoch109988c2016-05-18 11:27:45 +01001# 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
Ben Murdochf91f0612016-11-29 16:50:11 +00001053 'Beginning test session' not in a and
1054 'Writing diagnostic log' not in a and
1055 'Logs/Test/' not in a]
Ben Murdoch109988c2016-05-18 11:27:45 +01001056 return match(actual, expected)
1057 kw['match'] = match_filter_xcode
1058
1059 return self.run(program=self.build_tool, **kw)
1060 def up_to_date(self, gyp_file, target=None, **kw):
1061 """
1062 Verifies that a build of the specified Xcode target is up to date.
1063 """
1064 result = self.build(gyp_file, target, **kw)
1065 if not result:
1066 output = self.stdout()
1067 for expression in self.strip_up_to_date_expressions:
1068 output = expression.sub('', output)
1069 if not output.endswith(self.up_to_date_endings):
1070 self.report_not_up_to_date()
1071 self.fail_test()
1072 return result
1073 def run_built_executable(self, name, *args, **kw):
1074 """
1075 Runs an executable built by xcodebuild.
1076 """
1077 configuration = self.configuration_dirname()
1078 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
1079 # Enclosing the name in a list avoids prepending the original dir.
1080 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
1081 return self.run(program=program, *args, **kw)
1082 def built_file_path(self, name, type=None, **kw):
1083 """
1084 Returns a path to the specified file name, of the specified type,
1085 as built by Xcode.
1086
1087 Built files are in the subdirectory 'build/{configuration}'.
1088 The default is 'build/Default'.
1089
1090 A chdir= keyword argument specifies the source directory
1091 relative to which the output subdirectory can be found.
1092
1093 "type" values of STATIC_LIB or SHARED_LIB append the necessary
1094 prefixes and suffixes to a platform-independent library base name.
1095 """
1096 result = []
1097 chdir = kw.get('chdir')
1098 if chdir:
1099 result.append(chdir)
1100 configuration = self.configuration_dirname()
1101 result.extend(['build', configuration])
1102 result.append(self.built_file_basename(name, type, **kw))
1103 return self.workpath(*result)
1104
1105
1106class TestGypXcodeNinja(TestGypXcode):
1107 """
1108 Subclass for testing the GYP Xcode Ninja generator.
1109 """
1110 format = 'xcode-ninja'
1111
1112 def initialize_build_tool(self):
1113 super(TestGypXcodeNinja, self).initialize_build_tool()
1114 # When using '--build', make sure ninja is first in the format list.
1115 self.formats.insert(0, 'ninja')
1116
1117 def build(self, gyp_file, target=None, **kw):
1118 """
1119 Runs an xcodebuild using the .xcodeproj generated from the specified
1120 gyp_file.
1121 """
1122 build_config = self.configuration
1123 if build_config and build_config.endswith(('-iphoneos',
1124 '-iphonesimulator')):
1125 build_config, sdk = self.configuration.split('-')
1126 kw['arguments'] = kw.get('arguments', []) + ['-sdk', sdk]
1127
1128 with self._build_configuration(build_config):
1129 return super(TestGypXcodeNinja, self).build(
1130 gyp_file.replace('.gyp', '.ninja.gyp'), target, **kw)
1131
1132 @contextmanager
1133 def _build_configuration(self, build_config):
1134 config = self.configuration
1135 self.configuration = build_config
1136 try:
1137 yield
1138 finally:
1139 self.configuration = config
1140
1141 def built_file_path(self, name, type=None, **kw):
1142 result = []
1143 chdir = kw.get('chdir')
1144 if chdir:
1145 result.append(chdir)
1146 result.append('out')
1147 result.append(self.configuration_dirname())
1148 subdir = kw.get('subdir')
1149 if subdir and type != self.SHARED_LIB:
1150 result.append(subdir)
1151 result.append(self.built_file_basename(name, type, **kw))
1152 return self.workpath(*result)
1153
1154 def up_to_date(self, gyp_file, target=None, **kw):
1155 result = self.build(gyp_file, target, **kw)
1156 if not result:
1157 stdout = self.stdout()
1158 if 'ninja: no work to do' not in stdout:
1159 self.report_not_up_to_date()
1160 self.fail_test()
1161 return result
1162
1163 def run_built_executable(self, name, *args, **kw):
1164 """
1165 Runs an executable built by xcodebuild + ninja.
1166 """
1167 configuration = self.configuration_dirname()
1168 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
1169 # Enclosing the name in a list avoids prepending the original dir.
1170 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
1171 return self.run(program=program, *args, **kw)
1172
1173
1174format_class_list = [
1175 TestGypGypd,
1176 TestGypCMake,
1177 TestGypMake,
1178 TestGypMSVS,
1179 TestGypMSVSNinja,
1180 TestGypNinja,
1181 TestGypXcode,
1182 TestGypXcodeNinja,
1183]
1184
1185def TestGyp(*args, **kw):
1186 """
1187 Returns an appropriate TestGyp* instance for a specified GYP format.
1188 """
1189 format = kw.pop('format', os.environ.get('TESTGYP_FORMAT'))
1190 for format_class in format_class_list:
1191 if format == format_class.format:
1192 return format_class(*args, **kw)
1193 raise Exception, "unknown format %r" % format