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