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.
6 TestGyp.py: a testing framework for GYP integration tests.
10 from contextlib import contextmanager
22 from TestCommon import __all__
29 def remove_debug_line_numbers(contents):
30 """Function to remove the line numbers from the debug output
31 of gyp and thus reduce the extreme fragility of the stdout
34 lines = contents.splitlines()
35 # split each line on ":"
36 lines = [l.split(":", 3) for l in lines]
37 # join each line back together while ignoring the
38 # 3rd column which is the line number
39 lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines]
40 return "\n".join(lines)
43 def match_modulo_line_numbers(contents_a, contents_b):
44 """File contents matcher that ignores line numbers."""
45 contents_a = remove_debug_line_numbers(contents_a)
46 contents_b = remove_debug_line_numbers(contents_b)
47 return TestCommon.match_exact(contents_a, contents_b)
51 def LocalEnv(local_env):
52 """Context manager to provide a local OS environment."""
53 old_env = os.environ.copy()
54 os.environ.update(local_env)
59 os.environ.update(old_env)
62 class TestGypBase(TestCommon.TestCommon):
64 Class for controlling end-to-end tests of gyp generators.
66 Instantiating this class will create a temporary directory and
67 arrange for its destruction (via the TestCmd superclass) and
68 copy all of the non-gyptest files in the directory hierarchy of the
71 The default behavior is to test the 'gyp' or 'gyp.bat' file in the
72 current directory. An alternative may be specified explicitly on
73 instantiation, or by setting the TESTGYP_GYP environment variable.
75 This class should be subclassed for each supported gyp generator
76 (format). Various abstract methods below define calling signatures
77 used by the test scripts to invoke builds on the generated build
78 configuration and to run executables generated by those builds.
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
93 # Constants to represent different targets.
95 DEFAULT = '__default__'
97 # Constants for different target types.
98 EXECUTABLE = '__executable__'
99 STATIC_LIB = '__static_lib__'
100 SHARED_LIB = '__shared_lib__'
102 def __init__(self, gyp=None, *args, **kw):
103 self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0]))
104 self.extra_args = sys.argv[1:]
107 gyp = os.environ.get('TESTGYP_GYP')
109 if sys.platform == 'win32':
113 self.gyp = os.path.abspath(gyp)
114 self.no_parallel = False
116 self.initialize_build_tool()
118 kw.setdefault('match', TestCommon.match_exact)
120 # Put test output in out/testworkarea by default.
121 # Use temporary names so there are no collisions.
122 workdir = os.path.join('out', kw.get('workdir', 'testworkarea'))
123 # Create work area if it doesn't already exist.
124 if not os.path.isdir(workdir):
127 kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir)
129 formats = kw.pop('formats', [])
131 super(TestGypBase, self).__init__(*args, **kw)
133 excluded_formats = set([f for f in formats if f[0] == '!'])
134 included_formats = set(formats) - excluded_formats
135 if ('!'+self.format in excluded_formats or
136 included_formats and self.format not in included_formats):
137 msg = 'Invalid test for %r format; skipping test.\n'
138 self.skip_test(msg % self.format)
140 self.copy_test_configuration(self.origin_cwd, self.workdir)
141 self.set_configuration(None)
143 # Set $HOME so that gyp doesn't read the user's actual
144 # ~/.gyp/include.gypi file, which may contain variables
145 # and other settings that would change the output.
146 os.environ['HOME'] = self.workpath()
147 # Clear $GYP_DEFINES for the same reason.
148 if 'GYP_DEFINES' in os.environ:
149 del os.environ['GYP_DEFINES']
151 def built_file_must_exist(self, name, type=None, **kw):
153 Fails the test if the specified built file name does not exist.
155 return self.must_exist(self.built_file_path(name, type, **kw))
157 def built_file_must_not_exist(self, name, type=None, **kw):
159 Fails the test if the specified built file name exists.
161 return self.must_not_exist(self.built_file_path(name, type, **kw))
163 def built_file_must_match(self, name, contents, **kw):
165 Fails the test if the contents of the specified built file name
166 do not match the specified contents.
168 return self.must_match(self.built_file_path(name, **kw), contents)
170 def built_file_must_not_match(self, name, contents, **kw):
172 Fails the test if the contents of the specified built file name
173 match the specified contents.
175 return self.must_not_match(self.built_file_path(name, **kw), contents)
177 def built_file_must_not_contain(self, name, contents, **kw):
179 Fails the test if the specified built file name contains the specified
182 return self.must_not_contain(self.built_file_path(name, **kw), contents)
184 def copy_test_configuration(self, source_dir, dest_dir):
186 Copies the test configuration from the specified source_dir
187 (the directory in which the test script lives) to the
188 specified dest_dir (a temporary working directory).
190 This ignores all files and directories that begin with
191 the string 'gyptest', and all '.svn' subdirectories.
193 for root, dirs, files in os.walk(source_dir):
196 dirs = [ d for d in dirs if not d.startswith('gyptest') ]
197 files = [ f for f in files if not f.startswith('gyptest') ]
199 source = os.path.join(root, dirname)
200 destination = source.replace(source_dir, dest_dir)
201 os.mkdir(destination)
202 if sys.platform != 'win32':
203 shutil.copystat(source, destination)
204 for filename in files:
205 source = os.path.join(root, filename)
206 destination = source.replace(source_dir, dest_dir)
207 shutil.copy2(source, destination)
209 def initialize_build_tool(self):
211 Initializes the .build_tool attribute.
213 Searches the .build_tool_list for an executable name on the user's
214 $PATH. The first tool on the list is used as-is if nothing is found
215 on the current $PATH.
217 for build_tool in self.build_tool_list:
220 if os.path.isabs(build_tool):
221 self.build_tool = build_tool
223 build_tool = self.where_is(build_tool)
225 self.build_tool = build_tool
228 if self.build_tool_list:
229 self.build_tool = self.build_tool_list[0]
231 def relocate(self, source, destination):
233 Renames (relocates) the specified source (usually a directory)
234 to the specified destination, creating the destination directory
237 Note: Don't use this as a generic "rename" operation. In the
238 future, "relocating" parts of a GYP tree may affect the state of
239 the test to modify the behavior of later method calls.
241 destination_dir = os.path.dirname(destination)
242 if not os.path.exists(destination_dir):
243 self.subdir(destination_dir)
244 os.rename(source, destination)
246 def report_not_up_to_date(self):
248 Reports that a build is not up-to-date.
250 This provides common reporting for formats that have complicated
251 conditions for checking whether a build is up-to-date. Formats
252 that expect exact output from the command (make) can
253 just set stdout= when they call the run_build() method.
255 print "Build is not up-to-date:"
256 print self.banner('STDOUT ')
258 stderr = self.stderr()
260 print self.banner('STDERR ')
263 def run_gyp(self, gyp_file, *args, **kw):
265 Runs gyp against the specified gyp_file with the specified args.
268 # When running gyp, and comparing its output we use a comparitor
269 # that ignores the line numbers that gyp logs in its debug output.
270 if kw.pop('ignore_line_numbers', False):
271 kw.setdefault('match', match_modulo_line_numbers)
273 # TODO: --depth=. works around Chromium-specific tree climbing.
274 depth = kw.pop('depth', '.')
275 run_args = ['--depth='+depth, '--format='+self.format, gyp_file]
277 run_args += ['--no-parallel']
278 run_args.extend(self.extra_args)
279 run_args.extend(args)
280 return self.run(program=self.gyp, arguments=run_args, **kw)
282 def run(self, *args, **kw):
284 Executes a program by calling the superclass .run() method.
286 This exists to provide a common place to filter out keyword
287 arguments implemented in this layer, without having to update
288 the tool-specific subclasses or clutter the tests themselves
289 with platform-specific code.
291 if kw.has_key('SYMROOT'):
293 super(TestGypBase, self).run(*args, **kw)
295 def set_configuration(self, configuration):
297 Sets the configuration, to be used for invoking the build
298 tool and testing potential built output.
300 self.configuration = configuration
302 def configuration_dirname(self):
303 if self.configuration:
304 return self.configuration.split('|')[0]
308 def configuration_buildname(self):
309 if self.configuration:
310 return self.configuration
315 # Abstract methods to be defined by format-specific subclasses.
318 def build(self, gyp_file, target=None, **kw):
320 Runs a build of the specified target against the configuration
321 generated from the specified gyp_file.
323 A 'target' argument of None or the special value TestGyp.DEFAULT
324 specifies the default argument for the underlying build tool.
325 A 'target' argument of TestGyp.ALL specifies the 'all' target
326 (if any) of the underlying build tool.
328 raise NotImplementedError
330 def built_file_path(self, name, type=None, **kw):
332 Returns a path to the specified file name, of the specified type.
334 raise NotImplementedError
336 def built_file_basename(self, name, type=None, **kw):
338 Returns the base name of the specified file name, of the specified type.
340 A bare=True keyword argument specifies that prefixes and suffixes shouldn't
343 if not kw.get('bare'):
344 if type == self.EXECUTABLE:
345 name = name + self._exe
346 elif type == self.STATIC_LIB:
347 name = self.lib_ + name + self._lib
348 elif type == self.SHARED_LIB:
349 name = self.dll_ + name + self._dll
352 def run_built_executable(self, name, *args, **kw):
354 Runs an executable program built from a gyp-generated configuration.
356 The specified name should be independent of any particular generator.
357 Subclasses should find the output executable in the appropriate
358 output build directory, tack on any necessary executable suffix, etc.
360 raise NotImplementedError
362 def up_to_date(self, gyp_file, target=None, **kw):
364 Verifies that a build of the specified target is up to date.
366 The subclass should implement this by calling build()
367 (or a reasonable equivalent), checking whatever conditions
368 will tell it the build was an "up to date" null build, and
371 raise NotImplementedError
374 class TestGypGypd(TestGypBase):
376 Subclass for testing the GYP 'gypd' generator (spit out the
377 internal data structure as pretty-printed Python).
380 def __init__(self, gyp=None, *args, **kw):
381 super(TestGypGypd, self).__init__(*args, **kw)
382 # gypd implies the use of 'golden' files, so parallelizing conflicts as it
383 # causes ordering changes.
384 self.no_parallel = True
387 class TestGypCustom(TestGypBase):
389 Subclass for testing the GYP with custom generator
392 def __init__(self, gyp=None, *args, **kw):
393 self.format = kw.pop("format")
394 super(TestGypCustom, self).__init__(*args, **kw)
397 class TestGypAndroid(TestGypBase):
399 Subclass for testing the GYP Android makefile generator. Note that
400 build/envsetup.sh and lunch must have been run before running tests.
402 TODO: This is currently an incomplete implementation. We do not support
403 run_built_executable(), so we pass only tests which do not use this. As a
404 result, support for host targets is not properly tested.
408 # Note that we can't use mmm as the build tool because ...
409 # - it builds all targets, whereas we need to pass a target
410 # - it is a function, whereas the test runner assumes the build tool is a file
411 # Instead we use make and duplicate the logic from mmm.
412 build_tool_list = ['make']
414 # We use our custom target 'gyp_all_modules', as opposed to the 'all_modules'
415 # target used by mmm, to build only those targets which are part of the gyp
417 ALL = 'gyp_all_modules'
419 def __init__(self, gyp=None, *args, **kw):
420 # Android requires build and test output to be inside its source tree.
421 # We use the following working directory for the test's source, but the
422 # test's build output still goes to $ANDROID_PRODUCT_OUT.
423 # Note that some tests explicitly set format='gypd' to invoke the gypd
424 # backend. This writes to the source tree, but there's no way around this.
425 kw['workdir'] = os.path.join('/tmp', 'gyptest',
426 kw.get('workdir', 'testworkarea'))
427 # We need to remove all gyp outputs from out/. Ths is because some tests
428 # don't have rules to regenerate output, so they will simply re-use stale
429 # output if present. Since the test working directory gets regenerated for
430 # each test run, this can confuse things.
431 # We don't have a list of build outputs because we don't know which
432 # dependent targets were built. Instead we delete all gyp-generated output.
433 # This may be excessive, but should be safe.
434 out_dir = os.environ['ANDROID_PRODUCT_OUT']
435 obj_dir = os.path.join(out_dir, 'obj')
436 shutil.rmtree(os.path.join(obj_dir, 'GYP'), ignore_errors = True)
437 for x in ['EXECUTABLES', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES']:
438 for d in os.listdir(os.path.join(obj_dir, x)):
439 if d.endswith('_gyp_intermediates'):
440 shutil.rmtree(os.path.join(obj_dir, x, d), ignore_errors = True)
441 for x in [os.path.join('obj', 'lib'), os.path.join('system', 'lib')]:
442 for d in os.listdir(os.path.join(out_dir, x)):
443 if d.endswith('_gyp.so'):
444 os.remove(os.path.join(out_dir, x, d))
446 super(TestGypAndroid, self).__init__(*args, **kw)
448 def target_name(self, target):
449 if target == self.ALL:
451 # The default target is 'droid'. However, we want to use our special target
452 # to build only the gyp target 'all'.
453 if target in (None, self.DEFAULT):
457 def build(self, gyp_file, target=None, **kw):
459 Runs a build using the Android makefiles generated from the specified
460 gyp_file. This logic is taken from Android's mmm.
462 arguments = kw.get('arguments', [])[:]
463 arguments.append(self.target_name(target))
464 arguments.append('-C')
465 arguments.append(os.environ['ANDROID_BUILD_TOP'])
466 kw['arguments'] = arguments
467 chdir = kw.get('chdir', '')
468 makefile = os.path.join(self.workdir, chdir, 'GypAndroid.mk')
469 os.environ['ONE_SHOT_MAKEFILE'] = makefile
470 result = self.run(program=self.build_tool, **kw)
471 del os.environ['ONE_SHOT_MAKEFILE']
474 def android_module(self, group, name, subdir):
476 name = '%s_%s' % (subdir, name)
477 if group == 'SHARED_LIBRARIES':
478 name = 'lib_%s' % name
479 return '%s_gyp' % name
481 def intermediates_dir(self, group, module_name):
482 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', group,
483 '%s_intermediates' % module_name)
485 def built_file_path(self, name, type=None, **kw):
487 Returns a path to the specified file name, of the specified type,
488 as built by Android. Note that we don't support the configuration
491 # Built files are in $ANDROID_PRODUCT_OUT. This requires copying logic from
492 # the Android build system.
494 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', 'GYP',
495 'shared_intermediates', name)
496 subdir = kw.get('subdir')
497 if type == self.EXECUTABLE:
498 # We don't install executables
499 group = 'EXECUTABLES'
500 module_name = self.android_module(group, name, subdir)
501 return os.path.join(self.intermediates_dir(group, module_name), name)
502 if type == self.STATIC_LIB:
503 group = 'STATIC_LIBRARIES'
504 module_name = self.android_module(group, name, subdir)
505 return os.path.join(self.intermediates_dir(group, module_name),
506 '%s.a' % module_name)
507 if type == self.SHARED_LIB:
508 group = 'SHARED_LIBRARIES'
509 module_name = self.android_module(group, name, subdir)
510 return os.path.join(self.intermediates_dir(group, module_name), 'LINKED',
511 '%s.so' % module_name)
512 assert False, 'Unhandled type'
514 def run_built_executable(self, name, *args, **kw):
516 Runs an executable program built from a gyp-generated configuration.
518 This is not correctly implemented for Android. For now, we simply check
519 that the executable file exists.
521 # Running executables requires a device. Even if we build for target x86,
522 # the binary is not built with the correct toolchain options to actually
525 # Copied from TestCommon.run()
526 match = kw.pop('match', self.match)
528 if os.path.exists(self.built_file_path(name)):
530 self._complete(None, None, None, None, status, match)
532 def match_single_line(self, lines = None, expected_line = None):
534 Checks that specified line appears in the text.
536 for line in lines.split('\n'):
537 if line == expected_line:
541 def up_to_date(self, gyp_file, target=None, **kw):
543 Verifies that a build of the specified target is up to date.
545 kw['stdout'] = ("make: Nothing to be done for `%s'." %
546 self.target_name(target))
548 # We need to supply a custom matcher, since we don't want to depend on the
549 # exact stdout string.
550 kw['match'] = self.match_single_line
551 return self.build(gyp_file, target, **kw)
554 class TestGypCMake(TestGypBase):
556 Subclass for testing the GYP CMake generator, using cmake's ninja backend.
559 build_tool_list = ['cmake']
562 def cmake_build(self, gyp_file, target=None, **kw):
563 arguments = kw.get('arguments', [])[:]
565 self.build_tool_list = ['cmake']
566 self.initialize_build_tool()
568 chdir = os.path.join(kw.get('chdir', '.'),
570 self.configuration_dirname())
573 arguments.append('-G')
574 arguments.append('Ninja')
576 kw['arguments'] = arguments
578 stderr = kw.get('stderr', None)
580 kw['stderr'] = stderr.split('$$$')[0]
582 self.run(program=self.build_tool, **kw)
584 def ninja_build(self, gyp_file, target=None, **kw):
585 arguments = kw.get('arguments', [])[:]
587 self.build_tool_list = ['ninja']
588 self.initialize_build_tool()
590 # Add a -C output/path to the command line.
591 arguments.append('-C')
592 arguments.append(os.path.join('out', self.configuration_dirname()))
594 if target not in (None, self.DEFAULT):
595 arguments.append(target)
597 kw['arguments'] = arguments
599 stderr = kw.get('stderr', None)
601 stderrs = stderr.split('$$$')
602 kw['stderr'] = stderrs[1] if len(stderrs) > 1 else ''
604 return self.run(program=self.build_tool, **kw)
606 def build(self, gyp_file, target=None, status=0, **kw):
607 # Two tools must be run to build, cmake and the ninja.
608 # Allow cmake to succeed when the overall expectation is to fail.
612 if not isinstance(status, collections.Iterable): status = (status,)
613 kw['status'] = list(itertools.chain((0,), status))
614 self.cmake_build(gyp_file, target, **kw)
615 kw['status'] = status
616 self.ninja_build(gyp_file, target, **kw)
618 def run_built_executable(self, name, *args, **kw):
619 # Enclosing the name in a list avoids prepending the original dir.
620 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
621 if sys.platform == 'darwin':
622 configuration = self.configuration_dirname()
623 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
624 return self.run(program=program, *args, **kw)
626 def built_file_path(self, name, type=None, **kw):
628 chdir = kw.get('chdir')
632 result.append(self.configuration_dirname())
633 if type == self.STATIC_LIB:
634 if sys.platform != 'darwin':
635 result.append('obj.target')
636 elif type == self.SHARED_LIB:
637 if sys.platform != 'darwin' and sys.platform != 'win32':
638 result.append('lib.target')
639 subdir = kw.get('subdir')
640 if subdir and type != self.SHARED_LIB:
641 result.append(subdir)
642 result.append(self.built_file_basename(name, type, **kw))
643 return self.workpath(*result)
645 def up_to_date(self, gyp_file, target=None, **kw):
646 result = self.ninja_build(gyp_file, target, **kw)
648 stdout = self.stdout()
649 if 'ninja: no work to do' not in stdout:
650 self.report_not_up_to_date()
655 class TestGypMake(TestGypBase):
657 Subclass for testing the GYP Make generator.
660 build_tool_list = ['make']
662 def build(self, gyp_file, target=None, **kw):
664 Runs a Make build using the Makefiles generated from the specified
667 arguments = kw.get('arguments', [])[:]
668 if self.configuration:
669 arguments.append('BUILDTYPE=' + self.configuration)
670 if target not in (None, self.DEFAULT):
671 arguments.append(target)
672 # Sub-directory builds provide per-gyp Makefiles (i.e.
673 # Makefile.gyp_filename), so use that if there is no Makefile.
674 chdir = kw.get('chdir', '')
675 if not os.path.exists(os.path.join(chdir, 'Makefile')):
676 print "NO Makefile in " + os.path.join(chdir, 'Makefile')
677 arguments.insert(0, '-f')
678 arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile')
679 kw['arguments'] = arguments
680 return self.run(program=self.build_tool, **kw)
681 def up_to_date(self, gyp_file, target=None, **kw):
683 Verifies that a build of the specified Make target is up to date.
685 if target in (None, self.DEFAULT):
686 message_target = 'all'
688 message_target = target
689 kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target
690 return self.build(gyp_file, target, **kw)
691 def run_built_executable(self, name, *args, **kw):
693 Runs an executable built by Make.
695 configuration = self.configuration_dirname()
696 libdir = os.path.join('out', configuration, 'lib')
697 # TODO(piman): when everything is cross-compile safe, remove lib.target
698 if sys.platform == 'darwin':
699 # Mac puts target shared libraries right in the product directory.
700 configuration = self.configuration_dirname()
701 os.environ['DYLD_LIBRARY_PATH'] = (
702 libdir + '.host:' + os.path.join('out', configuration))
704 os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target'
705 # Enclosing the name in a list avoids prepending the original dir.
706 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
707 return self.run(program=program, *args, **kw)
708 def built_file_path(self, name, type=None, **kw):
710 Returns a path to the specified file name, of the specified type,
713 Built files are in the subdirectory 'out/{configuration}'.
714 The default is 'out/Default'.
716 A chdir= keyword argument specifies the source directory
717 relative to which the output subdirectory can be found.
719 "type" values of STATIC_LIB or SHARED_LIB append the necessary
720 prefixes and suffixes to a platform-independent library base name.
722 A subdir= keyword argument specifies a library subdirectory within
723 the default 'obj.target'.
726 chdir = kw.get('chdir')
729 configuration = self.configuration_dirname()
730 result.extend(['out', configuration])
731 if type == self.STATIC_LIB and sys.platform != 'darwin':
732 result.append('obj.target')
733 elif type == self.SHARED_LIB and sys.platform != 'darwin':
734 result.append('lib.target')
735 subdir = kw.get('subdir')
736 if subdir and type != self.SHARED_LIB:
737 result.append(subdir)
738 result.append(self.built_file_basename(name, type, **kw))
739 return self.workpath(*result)
742 def ConvertToCygpath(path):
743 """Convert to cygwin path if we are using cygwin."""
744 if sys.platform == 'cygwin':
745 p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE)
746 path = p.communicate()[0].strip()
750 def FindVisualStudioInstallation():
751 """Returns appropriate values for .build_tool and .uses_msbuild fields
752 of TestGypBase for Visual Studio.
754 We use the value specified by GYP_MSVS_VERSION. If not specified, we
755 search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable.
756 Failing that, we search for likely deployment paths.
758 possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix)
759 for drive in range(ord('C'), ord('Z') + 1)
760 for suffix in ['', ' (x86)']]
762 '2013': r'Microsoft Visual Studio 12.0\Common7\IDE\devenv.com',
763 '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com',
764 '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com',
765 '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com',
766 '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'}
768 possible_roots = [ConvertToCygpath(r) for r in possible_roots]
770 msvs_version = 'auto'
771 for flag in (f for f in sys.argv if f.startswith('msvs_version=')):
772 msvs_version = flag.split('=')[-1]
773 msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version)
776 if msvs_version in possible_paths:
777 # Check that the path to the specified GYP_MSVS_VERSION exists.
778 path = possible_paths[msvs_version]
779 for r in possible_roots:
780 bt = os.path.join(r, path)
781 if os.path.exists(bt):
783 uses_msbuild = msvs_version >= '2010'
784 return build_tool, uses_msbuild
786 print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
787 'but corresponding "%s" was not found.' % (msvs_version, path))
789 # We found 'devenv' on the path, use that and try to guess the version.
790 for version, path in possible_paths.iteritems():
791 if build_tool.find(path) >= 0:
792 uses_msbuild = version >= '2010'
793 return build_tool, uses_msbuild
795 # If not, assume not MSBuild.
797 return build_tool, uses_msbuild
798 # Neither GYP_MSVS_VERSION nor the path help us out. Iterate through
799 # the choices looking for a match.
800 for version in sorted(possible_paths, reverse=True):
801 path = possible_paths[version]
802 for r in possible_roots:
803 bt = os.path.join(r, path)
804 if os.path.exists(bt):
806 uses_msbuild = msvs_version >= '2010'
807 return build_tool, uses_msbuild
808 print 'Error: could not find devenv'
811 class TestGypOnMSToolchain(TestGypBase):
813 Common subclass for testing generators that target the Microsoft Visual
814 Studio toolchain (cl, link, dumpbin, etc.)
817 def _ComputeVsvarsPath(devenv_path):
818 devenv_dir = os.path.split(devenv_path)[0]
819 vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat')
822 def initialize_build_tool(self):
823 super(TestGypOnMSToolchain, self).initialize_build_tool()
824 if sys.platform in ('win32', 'cygwin'):
825 self.devenv_path, self.uses_msbuild = FindVisualStudioInstallation()
826 self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath(
829 def run_dumpbin(self, *dumpbin_args):
830 """Run the dumpbin tool with the specified arguments, and capturing and
832 assert sys.platform in ('win32', 'cygwin')
833 cmd = os.environ.get('COMSPEC', 'cmd.exe')
834 arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin']
835 arguments.extend(dumpbin_args)
836 proc = subprocess.Popen(arguments, stdout=subprocess.PIPE)
837 output = proc.communicate()[0]
838 assert not proc.returncode
841 class TestGypNinja(TestGypOnMSToolchain):
843 Subclass for testing the GYP Ninja generator.
846 build_tool_list = ['ninja']
850 def run_gyp(self, gyp_file, *args, **kw):
851 TestGypBase.run_gyp(self, gyp_file, *args, **kw)
853 def build(self, gyp_file, target=None, **kw):
854 arguments = kw.get('arguments', [])[:]
856 # Add a -C output/path to the command line.
857 arguments.append('-C')
858 arguments.append(os.path.join('out', self.configuration_dirname()))
862 arguments.append(target)
864 kw['arguments'] = arguments
865 return self.run(program=self.build_tool, **kw)
867 def run_built_executable(self, name, *args, **kw):
868 # Enclosing the name in a list avoids prepending the original dir.
869 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
870 if sys.platform == 'darwin':
871 configuration = self.configuration_dirname()
872 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
873 return self.run(program=program, *args, **kw)
875 def built_file_path(self, name, type=None, **kw):
877 chdir = kw.get('chdir')
881 result.append(self.configuration_dirname())
882 if type == self.STATIC_LIB:
883 if sys.platform != 'darwin':
885 elif type == self.SHARED_LIB:
886 if sys.platform != 'darwin' and sys.platform != 'win32':
888 subdir = kw.get('subdir')
889 if subdir and type != self.SHARED_LIB:
890 result.append(subdir)
891 result.append(self.built_file_basename(name, type, **kw))
892 return self.workpath(*result)
894 def up_to_date(self, gyp_file, target=None, **kw):
895 result = self.build(gyp_file, target, **kw)
897 stdout = self.stdout()
898 if 'ninja: no work to do' not in stdout:
899 self.report_not_up_to_date()
904 class TestGypMSVS(TestGypOnMSToolchain):
906 Subclass for testing the GYP Visual Studio generator.
910 u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ==='
911 up_to_date_re = re.compile(u, re.M)
913 # Initial None element will indicate to our .initialize_build_tool()
914 # method below that 'devenv' was not found on %PATH%.
916 # Note: we must use devenv.com to be able to capture build output.
917 # Directly executing devenv.exe only sends output to BuildLog.htm.
918 build_tool_list = [None, 'devenv.com']
920 def initialize_build_tool(self):
921 super(TestGypMSVS, self).initialize_build_tool()
922 self.build_tool = self.devenv_path
924 def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
926 Runs a Visual Studio build using the configuration generated
927 from the specified gyp_file.
929 configuration = self.configuration_buildname()
936 arguments = kw.get('arguments', [])[:]
937 arguments.extend([gyp_file.replace('.gyp', '.sln'),
938 build, configuration])
939 # Note: the Visual Studio generator doesn't add an explicit 'all'
940 # target, so we just treat it the same as the default.
941 if target not in (None, self.ALL, self.DEFAULT):
942 arguments.extend(['/Project', target])
943 if self.configuration:
944 arguments.extend(['/ProjectConfig', self.configuration])
945 kw['arguments'] = arguments
946 return self.run(program=self.build_tool, **kw)
947 def up_to_date(self, gyp_file, target=None, **kw):
949 Verifies that a build of the specified Visual Studio target is up to date.
951 Beware that VS2010 will behave strangely if you build under
952 C:\USERS\yourname\AppData\Local. It will cause needless work. The ouptut
953 will be "1 succeeded and 0 up to date". MSBuild tracing reveals that:
954 "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because
955 'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL'
956 was modified at 02/21/2011 17:03:30, which is newer than '' which was
957 modified at 01/01/0001 00:00:00.
959 The workaround is to specify a workdir when instantiating the test, e.g.
960 test = TestGyp.TestGyp(workdir='workarea')
962 result = self.build(gyp_file, target, **kw)
964 stdout = self.stdout()
966 m = self.up_to_date_re.search(stdout)
967 up_to_date = m and int(m.group(1)) > 0
969 self.report_not_up_to_date()
972 def run_built_executable(self, name, *args, **kw):
974 Runs an executable built by Visual Studio.
976 configuration = self.configuration_dirname()
977 # Enclosing the name in a list avoids prepending the original dir.
978 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
979 return self.run(program=program, *args, **kw)
980 def built_file_path(self, name, type=None, **kw):
982 Returns a path to the specified file name, of the specified type,
983 as built by Visual Studio.
985 Built files are in a subdirectory that matches the configuration
986 name. The default is 'Default'.
988 A chdir= keyword argument specifies the source directory
989 relative to which the output subdirectory can be found.
991 "type" values of STATIC_LIB or SHARED_LIB append the necessary
992 prefixes and suffixes to a platform-independent library base name.
995 chdir = kw.get('chdir')
998 result.append(self.configuration_dirname())
999 if type == self.STATIC_LIB:
1000 result.append('lib')
1001 result.append(self.built_file_basename(name, type, **kw))
1002 return self.workpath(*result)
1005 class TestGypXcode(TestGypBase):
1007 Subclass for testing the GYP Xcode generator.
1010 build_tool_list = ['xcodebuild']
1012 phase_script_execution = ("\n"
1013 "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n"
1015 " /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n"
1016 "(make: Nothing to be done for `all'\\.\n)?")
1018 strip_up_to_date_expressions = [
1019 # Various actions or rules can run even when the overall build target
1020 # is up to date. Strip those phases' GYP-generated output.
1021 re.compile(phase_script_execution, re.S),
1023 # The message from distcc_pump can trail the "BUILD SUCCEEDED"
1024 # message, so strip that, too.
1025 re.compile('__________Shutting down distcc-pump include server\n', re.S),
1028 up_to_date_endings = (
1029 'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1
1030 'Check dependencies\n** BUILD SUCCEEDED **\n\n', # Xcode 3.2
1031 'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2
1032 'Check dependencies\n\n** BUILD SUCCEEDED **\n\n', # Xcode 5.0
1035 def build(self, gyp_file, target=None, **kw):
1037 Runs an xcodebuild using the .xcodeproj generated from the specified
1040 # Be sure we're working with a copy of 'arguments' since we modify it.
1041 # The caller may not be expecting it to be modified.
1042 arguments = kw.get('arguments', [])[:]
1043 arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')])
1044 if target == self.ALL:
1045 arguments.append('-alltargets',)
1046 elif target not in (None, self.DEFAULT):
1047 arguments.extend(['-target', target])
1048 if self.configuration:
1049 arguments.extend(['-configuration', self.configuration])
1050 symroot = kw.get('SYMROOT', '$SRCROOT/build')
1052 arguments.append('SYMROOT='+symroot)
1053 kw['arguments'] = arguments
1055 # Work around spurious stderr output from Xcode 4, http://crbug.com/181012
1056 match = kw.pop('match', self.match)
1057 def match_filter_xcode(actual, expected):
1059 if not TestCmd.is_List(actual):
1060 actual = actual.split('\n')
1061 if not TestCmd.is_List(expected):
1062 expected = expected.split('\n')
1063 actual = [a for a in actual
1064 if 'No recorder, buildTask: <Xcode3BuildTask:' not in a]
1065 return match(actual, expected)
1066 kw['match'] = match_filter_xcode
1068 return self.run(program=self.build_tool, **kw)
1069 def up_to_date(self, gyp_file, target=None, **kw):
1071 Verifies that a build of the specified Xcode target is up to date.
1073 result = self.build(gyp_file, target, **kw)
1075 output = self.stdout()
1076 for expression in self.strip_up_to_date_expressions:
1077 output = expression.sub('', output)
1078 if not output.endswith(self.up_to_date_endings):
1079 self.report_not_up_to_date()
1082 def run_built_executable(self, name, *args, **kw):
1084 Runs an executable built by xcodebuild.
1086 configuration = self.configuration_dirname()
1087 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
1088 # Enclosing the name in a list avoids prepending the original dir.
1089 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
1090 return self.run(program=program, *args, **kw)
1091 def built_file_path(self, name, type=None, **kw):
1093 Returns a path to the specified file name, of the specified type,
1096 Built files are in the subdirectory 'build/{configuration}'.
1097 The default is 'build/Default'.
1099 A chdir= keyword argument specifies the source directory
1100 relative to which the output subdirectory can be found.
1102 "type" values of STATIC_LIB or SHARED_LIB append the necessary
1103 prefixes and suffixes to a platform-independent library base name.
1106 chdir = kw.get('chdir')
1108 result.append(chdir)
1109 configuration = self.configuration_dirname()
1110 result.extend(['build', configuration])
1111 result.append(self.built_file_basename(name, type, **kw))
1112 return self.workpath(*result)
1115 format_class_list = [
1125 def TestGyp(*args, **kw):
1127 Returns an appropriate TestGyp* instance for a specified GYP format.
1129 format = kw.pop('format', os.environ.get('TESTGYP_FORMAT'))
1130 for format_class in format_class_list:
1131 if format == format_class.format:
1132 return format_class(*args, **kw)
1133 raise Exception, "unknown format %r" % format