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