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 """ 6 TestGyp.py: a testing framework for GYP integration tests. 7 """ 8 9 import os 10 import re 11 import shutil 12 import stat 13 import subprocess 14 import sys 15 import tempfile 16 17 import TestCmd 18 import TestCommon 19 from TestCommon import __all__ 20 21 __all__.extend([ 22 'TestGyp', 23 ]) 24 25 def remove_debug_line_numbers(contents): 26 """Function to remove the line numbers from the debug output 27 of gyp and thus remove the exremem fragility of the stdout 28 comparison tests. 29 """ 30 lines = contents.splitlines() 31 # split each line on ":" 32 lines = [l.split(":", 3) for l in lines] 33 # join each line back together while ignoring the 34 # 3rd column which is the line number 35 lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines] 36 return "\n".join(lines) 37 38 def match_modulo_line_numbers(contents_a, contents_b): 39 """File contents matcher that ignores line numbers.""" 40 contents_a = remove_debug_line_numbers(contents_a) 41 contents_b = remove_debug_line_numbers(contents_b) 42 return TestCommon.match_exact(contents_a, contents_b) 43 44 class TestGypBase(TestCommon.TestCommon): 45 """ 46 Class for controlling end-to-end tests of gyp generators. 47 48 Instantiating this class will create a temporary directory and 49 arrange for its destruction (via the TestCmd superclass) and 50 copy all of the non-gyptest files in the directory hierarchy of the 51 executing script. 52 53 The default behavior is to test the 'gyp' or 'gyp.bat' file in the 54 current directory. An alternative may be specified explicitly on 55 instantiation, or by setting the TESTGYP_GYP environment variable. 56 57 This class should be subclassed for each supported gyp generator 58 (format). Various abstract methods below define calling signatures 59 used by the test scripts to invoke builds on the generated build 60 configuration and to run executables generated by those builds. 61 """ 62 63 build_tool = None 64 build_tool_list = [] 65 66 _exe = TestCommon.exe_suffix 67 _obj = TestCommon.obj_suffix 68 shobj_ = TestCommon.shobj_prefix 69 _shobj = TestCommon.shobj_suffix 70 lib_ = TestCommon.lib_prefix 71 _lib = TestCommon.lib_suffix 72 dll_ = TestCommon.dll_prefix 73 _dll = TestCommon.dll_suffix 74 75 # Constants to represent different targets. 76 ALL = '__all__' 77 DEFAULT = '__default__' 78 79 # Constants for different target types. 80 EXECUTABLE = '__executable__' 81 STATIC_LIB = '__static_lib__' 82 SHARED_LIB = '__shared_lib__' 83 84 def __init__(self, gyp=None, *args, **kw): 85 self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0])) 86 self.extra_args = sys.argv[1:] 87 88 if not gyp: 89 gyp = os.environ.get('TESTGYP_GYP') 90 if not gyp: 91 if sys.platform == 'win32': 92 gyp = 'gyp.bat' 93 else: 94 gyp = 'gyp' 95 self.gyp = os.path.abspath(gyp) 96 97 self.initialize_build_tool() 98 99 kw.setdefault('match', TestCommon.match_exact) 100 101 # Put test output in out/testworkarea by default. 102 # Use temporary names so there are no collisions. 103 workdir = os.path.join('out', kw.get('workdir', 'testworkarea')) 104 # Create work area if it doesn't already exist. 105 if not os.path.isdir(workdir): 106 os.makedirs(workdir) 107 108 kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir) 109 110 formats = kw.pop('formats', []) 111 112 super(TestGypBase, self).__init__(*args, **kw) 113 114 excluded_formats = set([f for f in formats if f[0] == '!']) 115 included_formats = set(formats) - excluded_formats 116 if ('!'+self.format in excluded_formats or 117 included_formats and self.format not in included_formats): 118 msg = 'Invalid test for %r format; skipping test.\n' 119 self.skip_test(msg % self.format) 120 121 self.copy_test_configuration(self.origin_cwd, self.workdir) 122 self.set_configuration(None) 123 124 # Set $HOME so that gyp doesn't read the user's actual 125 # ~/.gyp/include.gypi file, which may contain variables 126 # and other settings that would change the output. 127 os.environ['HOME'] = self.workpath() 128 # Clear $GYP_DEFINES for the same reason. 129 if 'GYP_DEFINES' in os.environ: 130 del os.environ['GYP_DEFINES'] 131 132 def built_file_must_exist(self, name, type=None, **kw): 133 """ 134 Fails the test if the specified built file name does not exist. 135 """ 136 return self.must_exist(self.built_file_path(name, type, **kw)) 137 138 def built_file_must_not_exist(self, name, type=None, **kw): 139 """ 140 Fails the test if the specified built file name exists. 141 """ 142 return self.must_not_exist(self.built_file_path(name, type, **kw)) 143 144 def built_file_must_match(self, name, contents, **kw): 145 """ 146 Fails the test if the contents of the specified built file name 147 do not match the specified contents. 148 """ 149 return self.must_match(self.built_file_path(name, **kw), contents) 150 151 def built_file_must_not_match(self, name, contents, **kw): 152 """ 153 Fails the test if the contents of the specified built file name 154 match the specified contents. 155 """ 156 return self.must_not_match(self.built_file_path(name, **kw), contents) 157 158 def copy_test_configuration(self, source_dir, dest_dir): 159 """ 160 Copies the test configuration from the specified source_dir 161 (the directory in which the test script lives) to the 162 specified dest_dir (a temporary working directory). 163 164 This ignores all files and directories that begin with 165 the string 'gyptest', and all '.svn' subdirectories. 166 """ 167 for root, dirs, files in os.walk(source_dir): 168 if '.svn' in dirs: 169 dirs.remove('.svn') 170 dirs = [ d for d in dirs if not d.startswith('gyptest') ] 171 files = [ f for f in files if not f.startswith('gyptest') ] 172 for dirname in dirs: 173 source = os.path.join(root, dirname) 174 destination = source.replace(source_dir, dest_dir) 175 os.mkdir(destination) 176 if sys.platform != 'win32': 177 shutil.copystat(source, destination) 178 for filename in files: 179 source = os.path.join(root, filename) 180 destination = source.replace(source_dir, dest_dir) 181 shutil.copy2(source, destination) 182 183 def initialize_build_tool(self): 184 """ 185 Initializes the .build_tool attribute. 186 187 Searches the .build_tool_list for an executable name on the user's 188 $PATH. The first tool on the list is used as-is if nothing is found 189 on the current $PATH. 190 """ 191 for build_tool in self.build_tool_list: 192 if not build_tool: 193 continue 194 if os.path.isabs(build_tool): 195 self.build_tool = build_tool 196 return 197 build_tool = self.where_is(build_tool) 198 if build_tool: 199 self.build_tool = build_tool 200 return 201 202 if self.build_tool_list: 203 self.build_tool = self.build_tool_list[0] 204 205 def relocate(self, source, destination): 206 """ 207 Renames (relocates) the specified source (usually a directory) 208 to the specified destination, creating the destination directory 209 first if necessary. 210 211 Note: Don't use this as a generic "rename" operation. In the 212 future, "relocating" parts of a GYP tree may affect the state of 213 the test to modify the behavior of later method calls. 214 """ 215 destination_dir = os.path.dirname(destination) 216 if not os.path.exists(destination_dir): 217 self.subdir(destination_dir) 218 os.rename(source, destination) 219 220 def report_not_up_to_date(self): 221 """ 222 Reports that a build is not up-to-date. 223 224 This provides common reporting for formats that have complicated 225 conditions for checking whether a build is up-to-date. Formats 226 that expect exact output from the command (make) can 227 just set stdout= when they call the run_build() method. 228 """ 229 print "Build is not up-to-date:" 230 print self.banner('STDOUT ') 231 print self.stdout() 232 stderr = self.stderr() 233 if stderr: 234 print self.banner('STDERR ') 235 print stderr 236 237 def run_gyp(self, gyp_file, *args, **kw): 238 """ 239 Runs gyp against the specified gyp_file with the specified args. 240 """ 241 242 # When running gyp, and comparing its output we use a comparitor 243 # that ignores the line numbers that gyp logs in its debug output. 244 if kw.pop('ignore_line_numbers', False): 245 kw.setdefault('match', match_modulo_line_numbers) 246 247 # TODO: --depth=. works around Chromium-specific tree climbing. 248 depth = kw.pop('depth', '.') 249 run_args = ['--depth='+depth, '--format='+self.format, gyp_file] 250 run_args.extend(self.extra_args) 251 run_args.extend(args) 252 return self.run(program=self.gyp, arguments=run_args, **kw) 253 254 def run(self, *args, **kw): 255 """ 256 Executes a program by calling the superclass .run() method. 257 258 This exists to provide a common place to filter out keyword 259 arguments implemented in this layer, without having to update 260 the tool-specific subclasses or clutter the tests themselves 261 with platform-specific code. 262 """ 263 if kw.has_key('SYMROOT'): 264 del kw['SYMROOT'] 265 super(TestGypBase, self).run(*args, **kw) 266 267 def set_configuration(self, configuration): 268 """ 269 Sets the configuration, to be used for invoking the build 270 tool and testing potential built output. 271 """ 272 self.configuration = configuration 273 274 def configuration_dirname(self): 275 if self.configuration: 276 return self.configuration.split('|')[0] 277 else: 278 return 'Default' 279 280 def configuration_buildname(self): 281 if self.configuration: 282 return self.configuration 283 else: 284 return 'Default' 285 286 # 287 # Abstract methods to be defined by format-specific subclasses. 288 # 289 290 def build(self, gyp_file, target=None, **kw): 291 """ 292 Runs a build of the specified target against the configuration 293 generated from the specified gyp_file. 294 295 A 'target' argument of None or the special value TestGyp.DEFAULT 296 specifies the default argument for the underlying build tool. 297 A 'target' argument of TestGyp.ALL specifies the 'all' target 298 (if any) of the underlying build tool. 299 """ 300 raise NotImplementedError 301 302 def built_file_path(self, name, type=None, **kw): 303 """ 304 Returns a path to the specified file name, of the specified type. 305 """ 306 raise NotImplementedError 307 308 def built_file_basename(self, name, type=None, **kw): 309 """ 310 Returns the base name of the specified file name, of the specified type. 311 312 A bare=True keyword argument specifies that prefixes and suffixes shouldn't 313 be applied. 314 """ 315 if not kw.get('bare'): 316 if type == self.EXECUTABLE: 317 name = name + self._exe 318 elif type == self.STATIC_LIB: 319 name = self.lib_ + name + self._lib 320 elif type == self.SHARED_LIB: 321 name = self.dll_ + name + self._dll 322 return name 323 324 def run_built_executable(self, name, *args, **kw): 325 """ 326 Runs an executable program built from a gyp-generated configuration. 327 328 The specified name should be independent of any particular generator. 329 Subclasses should find the output executable in the appropriate 330 output build directory, tack on any necessary executable suffix, etc. 331 """ 332 raise NotImplementedError 333 334 def up_to_date(self, gyp_file, target=None, **kw): 335 """ 336 Verifies that a build of the specified target is up to date. 337 338 The subclass should implement this by calling build() 339 (or a reasonable equivalent), checking whatever conditions 340 will tell it the build was an "up to date" null build, and 341 failing if it isn't. 342 """ 343 raise NotImplementedError 344 345 346 class TestGypGypd(TestGypBase): 347 """ 348 Subclass for testing the GYP 'gypd' generator (spit out the 349 internal data structure as pretty-printed Python). 350 """ 351 format = 'gypd' 352 353 354 class TestGypCustom(TestGypBase): 355 """ 356 Subclass for testing the GYP with custom generator 357 """ 358 359 def __init__(self, gyp=None, *args, **kw): 360 self.format = kw.pop("format") 361 super(TestGypCustom, self).__init__(*args, **kw) 362 363 364 class TestGypAndroid(TestGypBase): 365 """ 366 Subclass for testing the GYP Android makefile generator. Note that 367 build/envsetup.sh and lunch must have been run before running tests. 368 369 TODO: This is currently an incomplete implementation. We do not support 370 run_built_executable(), so we pass only tests which do not use this. As a 371 result, support for host targets is not properly tested. 372 """ 373 format = 'android' 374 375 # Note that we can't use mmm as the build tool because ... 376 # - it builds all targets, whereas we need to pass a target 377 # - it is a function, whereas the test runner assumes the build tool is a file 378 # Instead we use make and duplicate the logic from mmm. 379 build_tool_list = ['make'] 380 381 # We use our custom target 'gyp_all_modules', as opposed to the 'all_modules' 382 # target used by mmm, to build only those targets which are part of the gyp 383 # target 'all'. 384 ALL = 'gyp_all_modules' 385 386 def __init__(self, gyp=None, *args, **kw): 387 # Android requires build and test output to be inside its source tree. 388 # We use the following working directory for the test's source, but the 389 # test's build output still goes to $ANDROID_PRODUCT_OUT. 390 # Note that some tests explicitly set format='gypd' to invoke the gypd 391 # backend. This writes to the source tree, but there's no way around this. 392 kw['workdir'] = os.path.join('/tmp', 'gyptest', 393 kw.get('workdir', 'testworkarea')) 394 # We need to remove all gyp outputs from out/. Ths is because some tests 395 # don't have rules to regenerate output, so they will simply re-use stale 396 # output if present. Since the test working directory gets regenerated for 397 # each test run, this can confuse things. 398 # We don't have a list of build outputs because we don't know which 399 # dependent targets were built. Instead we delete all gyp-generated output. 400 # This may be excessive, but should be safe. 401 out_dir = os.environ['ANDROID_PRODUCT_OUT'] 402 obj_dir = os.path.join(out_dir, 'obj') 403 shutil.rmtree(os.path.join(obj_dir, 'GYP'), ignore_errors = True) 404 for x in ['EXECUTABLES', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES']: 405 for d in os.listdir(os.path.join(obj_dir, x)): 406 if d.endswith('_gyp_intermediates'): 407 shutil.rmtree(os.path.join(obj_dir, x, d), ignore_errors = True) 408 for x in [os.path.join('obj', 'lib'), os.path.join('system', 'lib')]: 409 for d in os.listdir(os.path.join(out_dir, x)): 410 if d.endswith('_gyp.so'): 411 os.remove(os.path.join(out_dir, x, d)) 412 413 super(TestGypAndroid, self).__init__(*args, **kw) 414 415 def target_name(self, target): 416 if target == self.ALL: 417 return self.ALL 418 # The default target is 'droid'. However, we want to use our special target 419 # to build only the gyp target 'all'. 420 if target in (None, self.DEFAULT): 421 return self.ALL 422 return target 423 424 def build(self, gyp_file, target=None, **kw): 425 """ 426 Runs a build using the Android makefiles generated from the specified 427 gyp_file. This logic is taken from Android's mmm. 428 """ 429 arguments = kw.get('arguments', [])[:] 430 arguments.append(self.target_name(target)) 431 arguments.append('-C') 432 arguments.append(os.environ['ANDROID_BUILD_TOP']) 433 kw['arguments'] = arguments 434 chdir = kw.get('chdir', '') 435 makefile = os.path.join(self.workdir, chdir, 'GypAndroid.mk') 436 os.environ['ONE_SHOT_MAKEFILE'] = makefile 437 result = self.run(program=self.build_tool, **kw) 438 del os.environ['ONE_SHOT_MAKEFILE'] 439 return result 440 441 def android_module(self, group, name, subdir): 442 if subdir: 443 name = '%s_%s' % (subdir, name) 444 if group == 'SHARED_LIBRARIES': 445 name = 'lib_%s' % name 446 return '%s_gyp' % name 447 448 def intermediates_dir(self, group, module_name): 449 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', group, 450 '%s_intermediates' % module_name) 451 452 def built_file_path(self, name, type=None, **kw): 453 """ 454 Returns a path to the specified file name, of the specified type, 455 as built by Android. Note that we don't support the configuration 456 parameter. 457 """ 458 # Built files are in $ANDROID_PRODUCT_OUT. This requires copying logic from 459 # the Android build system. 460 if type == None: 461 return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', 'GYP', 462 'shared_intermediates', name) 463 subdir = kw.get('subdir') 464 if type == self.EXECUTABLE: 465 # We don't install executables 466 group = 'EXECUTABLES' 467 module_name = self.android_module(group, name, subdir) 468 return os.path.join(self.intermediates_dir(group, module_name), name) 469 if type == self.STATIC_LIB: 470 group = 'STATIC_LIBRARIES' 471 module_name = self.android_module(group, name, subdir) 472 return os.path.join(self.intermediates_dir(group, module_name), 473 '%s.a' % module_name) 474 if type == self.SHARED_LIB: 475 group = 'SHARED_LIBRARIES' 476 module_name = self.android_module(group, name, subdir) 477 return os.path.join(self.intermediates_dir(group, module_name), 'LINKED', 478 '%s.so' % module_name) 479 assert False, 'Unhandled type' 480 481 def run_built_executable(self, name, *args, **kw): 482 """ 483 Runs an executable program built from a gyp-generated configuration. 484 485 This is not correctly implemented for Android. For now, we simply check 486 that the executable file exists. 487 """ 488 # Running executables requires a device. Even if we build for target x86, 489 # the binary is not built with the correct toolchain options to actually 490 # run on the host. 491 492 # Copied from TestCommon.run() 493 match = kw.pop('match', self.match) 494 status = None 495 if os.path.exists(self.built_file_path(name)): 496 status = 1 497 self._complete(None, None, None, None, status, match) 498 499 def match_single_line(self, lines = None, expected_line = None): 500 """ 501 Checks that specified line appears in the text. 502 """ 503 for line in lines.split('\n'): 504 if line == expected_line: 505 return 1 506 return 507 508 def up_to_date(self, gyp_file, target=None, **kw): 509 """ 510 Verifies that a build of the specified target is up to date. 511 """ 512 kw['stdout'] = ("make: Nothing to be done for `%s'." % 513 self.target_name(target)) 514 515 # We need to supply a custom matcher, since we don't want to depend on the 516 # exact stdout string. 517 kw['match'] = self.match_single_line 518 return self.build(gyp_file, target, **kw) 519 520 class TestGypMake(TestGypBase): 521 """ 522 Subclass for testing the GYP Make generator. 523 """ 524 format = 'make' 525 build_tool_list = ['make'] 526 ALL = 'all' 527 def build(self, gyp_file, target=None, **kw): 528 """ 529 Runs a Make build using the Makefiles generated from the specified 530 gyp_file. 531 """ 532 arguments = kw.get('arguments', [])[:] 533 if self.configuration: 534 arguments.append('BUILDTYPE=' + self.configuration) 535 if target not in (None, self.DEFAULT): 536 arguments.append(target) 537 # Sub-directory builds provide per-gyp Makefiles (i.e. 538 # Makefile.gyp_filename), so use that if there is no Makefile. 539 chdir = kw.get('chdir', '') 540 if not os.path.exists(os.path.join(chdir, 'Makefile')): 541 print "NO Makefile in " + os.path.join(chdir, 'Makefile') 542 arguments.insert(0, '-f') 543 arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile') 544 kw['arguments'] = arguments 545 return self.run(program=self.build_tool, **kw) 546 def up_to_date(self, gyp_file, target=None, **kw): 547 """ 548 Verifies that a build of the specified Make target is up to date. 549 """ 550 if target in (None, self.DEFAULT): 551 message_target = 'all' 552 else: 553 message_target = target 554 kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target 555 return self.build(gyp_file, target, **kw) 556 def run_built_executable(self, name, *args, **kw): 557 """ 558 Runs an executable built by Make. 559 """ 560 configuration = self.configuration_dirname() 561 libdir = os.path.join('out', configuration, 'lib') 562 # TODO(piman): when everything is cross-compile safe, remove lib.target 563 if sys.platform == 'darwin': 564 # Mac puts target shared libraries right in the product directory. 565 configuration = self.configuration_dirname() 566 os.environ['DYLD_LIBRARY_PATH'] = ( 567 libdir + '.host:' + os.path.join('out', configuration)) 568 else: 569 os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target' 570 # Enclosing the name in a list avoids prepending the original dir. 571 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] 572 return self.run(program=program, *args, **kw) 573 def built_file_path(self, name, type=None, **kw): 574 """ 575 Returns a path to the specified file name, of the specified type, 576 as built by Make. 577 578 Built files are in the subdirectory 'out/{configuration}'. 579 The default is 'out/Default'. 580 581 A chdir= keyword argument specifies the source directory 582 relative to which the output subdirectory can be found. 583 584 "type" values of STATIC_LIB or SHARED_LIB append the necessary 585 prefixes and suffixes to a platform-independent library base name. 586 587 A subdir= keyword argument specifies a library subdirectory within 588 the default 'obj.target'. 589 """ 590 result = [] 591 chdir = kw.get('chdir') 592 if chdir: 593 result.append(chdir) 594 configuration = self.configuration_dirname() 595 result.extend(['out', configuration]) 596 if type == self.STATIC_LIB and sys.platform != 'darwin': 597 result.append('obj.target') 598 elif type == self.SHARED_LIB and sys.platform != 'darwin': 599 result.append('lib.target') 600 subdir = kw.get('subdir') 601 if subdir and type != self.SHARED_LIB: 602 result.append(subdir) 603 result.append(self.built_file_basename(name, type, **kw)) 604 return self.workpath(*result) 605 606 607 def ConvertToCygpath(path): 608 """Convert to cygwin path if we are using cygwin.""" 609 if sys.platform == 'cygwin': 610 p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE) 611 path = p.communicate()[0].strip() 612 return path 613 614 615 def FindVisualStudioInstallation(): 616 """Returns appropriate values for .build_tool and .uses_msbuild fields 617 of TestGypBase for Visual Studio. 618 619 We use the value specified by GYP_MSVS_VERSION. If not specified, we 620 search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable. 621 Failing that, we search for likely deployment paths. 622 """ 623 possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix) 624 for drive in range(ord('C'), ord('Z') + 1) 625 for suffix in ['', ' (x86)']] 626 possible_paths = { 627 '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com', 628 '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com', 629 '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com', 630 '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'} 631 632 possible_roots = [ConvertToCygpath(r) for r in possible_roots] 633 634 msvs_version = 'auto' 635 for flag in (f for f in sys.argv if f.startswith('msvs_version=')): 636 msvs_version = flag.split('=')[-1] 637 msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version) 638 639 build_tool = None 640 if msvs_version in possible_paths: 641 # Check that the path to the specified GYP_MSVS_VERSION exists. 642 path = possible_paths[msvs_version] 643 for r in possible_roots: 644 bt = os.path.join(r, path) 645 if os.path.exists(bt): 646 build_tool = bt 647 uses_msbuild = msvs_version >= '2010' 648 return build_tool, uses_msbuild 649 else: 650 print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" ' 651 'but corresponding "%s" was not found.' % (msvs_version, path)) 652 if build_tool: 653 # We found 'devenv' on the path, use that and try to guess the version. 654 for version, path in possible_paths.iteritems(): 655 if build_tool.find(path) >= 0: 656 uses_msbuild = version >= '2010' 657 return build_tool, uses_msbuild 658 else: 659 # If not, assume not MSBuild. 660 uses_msbuild = False 661 return build_tool, uses_msbuild 662 # Neither GYP_MSVS_VERSION nor the path help us out. Iterate through 663 # the choices looking for a match. 664 for version in sorted(possible_paths, reverse=True): 665 path = possible_paths[version] 666 for r in possible_roots: 667 bt = os.path.join(r, path) 668 if os.path.exists(bt): 669 build_tool = bt 670 uses_msbuild = msvs_version >= '2010' 671 return build_tool, uses_msbuild 672 print 'Error: could not find devenv' 673 sys.exit(1) 674 675 class TestGypOnMSToolchain(TestGypBase): 676 """ 677 Common subclass for testing generators that target the Microsoft Visual 678 Studio toolchain (cl, link, dumpbin, etc.) 679 """ 680 @staticmethod 681 def _ComputeVsvarsPath(devenv_path): 682 devenv_dir = os.path.split(devenv_path)[0] 683 vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat') 684 return vsvars_path 685 686 def initialize_build_tool(self): 687 super(TestGypOnMSToolchain, self).initialize_build_tool() 688 if sys.platform in ('win32', 'cygwin'): 689 self.devenv_path, self.uses_msbuild = FindVisualStudioInstallation() 690 self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath( 691 self.devenv_path) 692 693 def run_dumpbin(self, *dumpbin_args): 694 """Run the dumpbin tool with the specified arguments, and capturing and 695 returning stdout.""" 696 assert sys.platform in ('win32', 'cygwin') 697 cmd = os.environ.get('COMSPEC', 'cmd.exe') 698 arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin'] 699 arguments.extend(dumpbin_args) 700 proc = subprocess.Popen(arguments, stdout=subprocess.PIPE) 701 output = proc.communicate()[0] 702 assert not proc.returncode 703 return output 704 705 class TestGypNinja(TestGypOnMSToolchain): 706 """ 707 Subclass for testing the GYP Ninja generator. 708 """ 709 format = 'ninja' 710 build_tool_list = ['ninja'] 711 ALL = 'all' 712 DEFAULT = 'all' 713 714 def run_gyp(self, gyp_file, *args, **kw): 715 TestGypBase.run_gyp(self, gyp_file, *args, **kw) 716 717 def build(self, gyp_file, target=None, **kw): 718 arguments = kw.get('arguments', [])[:] 719 720 # Add a -C output/path to the command line. 721 arguments.append('-C') 722 arguments.append(os.path.join('out', self.configuration_dirname())) 723 724 if target is None: 725 target = 'all' 726 arguments.append(target) 727 728 kw['arguments'] = arguments 729 return self.run(program=self.build_tool, **kw) 730 731 def run_built_executable(self, name, *args, **kw): 732 # Enclosing the name in a list avoids prepending the original dir. 733 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] 734 if sys.platform == 'darwin': 735 configuration = self.configuration_dirname() 736 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) 737 return self.run(program=program, *args, **kw) 738 739 def built_file_path(self, name, type=None, **kw): 740 result = [] 741 chdir = kw.get('chdir') 742 if chdir: 743 result.append(chdir) 744 result.append('out') 745 result.append(self.configuration_dirname()) 746 if type == self.STATIC_LIB: 747 if sys.platform != 'darwin': 748 result.append('obj') 749 elif type == self.SHARED_LIB: 750 if sys.platform != 'darwin' and sys.platform != 'win32': 751 result.append('lib') 752 subdir = kw.get('subdir') 753 if subdir and type != self.SHARED_LIB: 754 result.append(subdir) 755 result.append(self.built_file_basename(name, type, **kw)) 756 return self.workpath(*result) 757 758 def up_to_date(self, gyp_file, target=None, **kw): 759 result = self.build(gyp_file, target, **kw) 760 if not result: 761 stdout = self.stdout() 762 if 'ninja: no work to do' not in stdout: 763 self.report_not_up_to_date() 764 self.fail_test() 765 return result 766 767 768 class TestGypMSVS(TestGypOnMSToolchain): 769 """ 770 Subclass for testing the GYP Visual Studio generator. 771 """ 772 format = 'msvs' 773 774 u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ===' 775 up_to_date_re = re.compile(u, re.M) 776 777 # Initial None element will indicate to our .initialize_build_tool() 778 # method below that 'devenv' was not found on %PATH%. 779 # 780 # Note: we must use devenv.com to be able to capture build output. 781 # Directly executing devenv.exe only sends output to BuildLog.htm. 782 build_tool_list = [None, 'devenv.com'] 783 784 def initialize_build_tool(self): 785 super(TestGypMSVS, self).initialize_build_tool() 786 self.build_tool = self.devenv_path 787 788 def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw): 789 """ 790 Runs a Visual Studio build using the configuration generated 791 from the specified gyp_file. 792 """ 793 configuration = self.configuration_buildname() 794 if clean: 795 build = '/Clean' 796 elif rebuild: 797 build = '/Rebuild' 798 else: 799 build = '/Build' 800 arguments = kw.get('arguments', [])[:] 801 arguments.extend([gyp_file.replace('.gyp', '.sln'), 802 build, configuration]) 803 # Note: the Visual Studio generator doesn't add an explicit 'all' 804 # target, so we just treat it the same as the default. 805 if target not in (None, self.ALL, self.DEFAULT): 806 arguments.extend(['/Project', target]) 807 if self.configuration: 808 arguments.extend(['/ProjectConfig', self.configuration]) 809 kw['arguments'] = arguments 810 return self.run(program=self.build_tool, **kw) 811 def up_to_date(self, gyp_file, target=None, **kw): 812 """ 813 Verifies that a build of the specified Visual Studio target is up to date. 814 815 Beware that VS2010 will behave strangely if you build under 816 C:\USERS\yourname\AppData\Local. It will cause needless work. The ouptut 817 will be "1 succeeded and 0 up to date". MSBuild tracing reveals that: 818 "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because 819 'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL' 820 was modified at 02/21/2011 17:03:30, which is newer than '' which was 821 modified at 01/01/0001 00:00:00. 822 823 The workaround is to specify a workdir when instantiating the test, e.g. 824 test = TestGyp.TestGyp(workdir='workarea') 825 """ 826 result = self.build(gyp_file, target, **kw) 827 if not result: 828 stdout = self.stdout() 829 830 m = self.up_to_date_re.search(stdout) 831 up_to_date = m and int(m.group(1)) > 0 832 if not up_to_date: 833 self.report_not_up_to_date() 834 self.fail_test() 835 return result 836 def run_built_executable(self, name, *args, **kw): 837 """ 838 Runs an executable built by Visual Studio. 839 """ 840 configuration = self.configuration_dirname() 841 # Enclosing the name in a list avoids prepending the original dir. 842 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] 843 return self.run(program=program, *args, **kw) 844 def built_file_path(self, name, type=None, **kw): 845 """ 846 Returns a path to the specified file name, of the specified type, 847 as built by Visual Studio. 848 849 Built files are in a subdirectory that matches the configuration 850 name. The default is 'Default'. 851 852 A chdir= keyword argument specifies the source directory 853 relative to which the output subdirectory can be found. 854 855 "type" values of STATIC_LIB or SHARED_LIB append the necessary 856 prefixes and suffixes to a platform-independent library base name. 857 """ 858 result = [] 859 chdir = kw.get('chdir') 860 if chdir: 861 result.append(chdir) 862 result.append(self.configuration_dirname()) 863 if type == self.STATIC_LIB: 864 result.append('lib') 865 result.append(self.built_file_basename(name, type, **kw)) 866 return self.workpath(*result) 867 868 869 class TestGypXcode(TestGypBase): 870 """ 871 Subclass for testing the GYP Xcode generator. 872 """ 873 format = 'xcode' 874 build_tool_list = ['xcodebuild'] 875 876 phase_script_execution = ("\n" 877 "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n" 878 " cd /\\S+\n" 879 " /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n" 880 "(make: Nothing to be done for `all'\\.\n)?") 881 882 strip_up_to_date_expressions = [ 883 # Various actions or rules can run even when the overall build target 884 # is up to date. Strip those phases' GYP-generated output. 885 re.compile(phase_script_execution, re.S), 886 887 # The message from distcc_pump can trail the "BUILD SUCCEEDED" 888 # message, so strip that, too. 889 re.compile('__________Shutting down distcc-pump include server\n', re.S), 890 ] 891 892 up_to_date_endings = ( 893 'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1 894 'Check dependencies\n** BUILD SUCCEEDED **\n\n', # Xcode 3.2 895 'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2 896 ) 897 898 def build(self, gyp_file, target=None, **kw): 899 """ 900 Runs an xcodebuild using the .xcodeproj generated from the specified 901 gyp_file. 902 """ 903 # Be sure we're working with a copy of 'arguments' since we modify it. 904 # The caller may not be expecting it to be modified. 905 arguments = kw.get('arguments', [])[:] 906 arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')]) 907 if target == self.ALL: 908 arguments.append('-alltargets',) 909 elif target not in (None, self.DEFAULT): 910 arguments.extend(['-target', target]) 911 if self.configuration: 912 arguments.extend(['-configuration', self.configuration]) 913 symroot = kw.get('SYMROOT', '$SRCROOT/build') 914 if symroot: 915 arguments.append('SYMROOT='+symroot) 916 kw['arguments'] = arguments 917 918 # Work around spurious stderr output from Xcode 4, http://crbug.com/181012 919 match = kw.pop('match', self.match) 920 def match_filter_xcode(actual, expected): 921 if actual: 922 if not TestCmd.is_List(actual): 923 actual = actual.split('\n') 924 if not TestCmd.is_List(expected): 925 expected = expected.split('\n') 926 actual = [a for a in actual 927 if 'No recorder, buildTask: <Xcode3BuildTask:' not in a] 928 return match(actual, expected) 929 kw['match'] = match_filter_xcode 930 931 return self.run(program=self.build_tool, **kw) 932 def up_to_date(self, gyp_file, target=None, **kw): 933 """ 934 Verifies that a build of the specified Xcode target is up to date. 935 """ 936 result = self.build(gyp_file, target, **kw) 937 if not result: 938 output = self.stdout() 939 for expression in self.strip_up_to_date_expressions: 940 output = expression.sub('', output) 941 if not output.endswith(self.up_to_date_endings): 942 self.report_not_up_to_date() 943 self.fail_test() 944 return result 945 def run_built_executable(self, name, *args, **kw): 946 """ 947 Runs an executable built by xcodebuild. 948 """ 949 configuration = self.configuration_dirname() 950 os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration) 951 # Enclosing the name in a list avoids prepending the original dir. 952 program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] 953 return self.run(program=program, *args, **kw) 954 def built_file_path(self, name, type=None, **kw): 955 """ 956 Returns a path to the specified file name, of the specified type, 957 as built by Xcode. 958 959 Built files are in the subdirectory 'build/{configuration}'. 960 The default is 'build/Default'. 961 962 A chdir= keyword argument specifies the source directory 963 relative to which the output subdirectory can be found. 964 965 "type" values of STATIC_LIB or SHARED_LIB append the necessary 966 prefixes and suffixes to a platform-independent library base name. 967 """ 968 result = [] 969 chdir = kw.get('chdir') 970 if chdir: 971 result.append(chdir) 972 configuration = self.configuration_dirname() 973 result.extend(['build', configuration]) 974 result.append(self.built_file_basename(name, type, **kw)) 975 return self.workpath(*result) 976 977 978 format_class_list = [ 979 TestGypGypd, 980 TestGypAndroid, 981 TestGypMake, 982 TestGypMSVS, 983 TestGypNinja, 984 TestGypXcode, 985 ] 986 987 def TestGyp(*args, **kw): 988 """ 989 Returns an appropriate TestGyp* instance for a specified GYP format. 990 """ 991 format = kw.pop('format', os.environ.get('TESTGYP_FORMAT')) 992 for format_class in format_class_list: 993 if format == format_class.format: 994 return format_class(*args, **kw) 995 raise Exception, "unknown format %r" % format 996