1# Copyright 2010 Google Inc. 2# All Rights Reserved. 3# 4# Author: Tim Haloun (thaloun@google.com) 5# Daniel Petersson (dape@google.com) 6# 7import os 8import SCons.Util 9 10class LibraryInfo: 11 """Records information on the libraries defined in a build configuration. 12 13 Attributes: 14 lib_targets: Dictionary of library target params for lookups in 15 ExtendComponent(). 16 prebuilt_libraries: Set of all prebuilt static libraries. 17 system_libraries: Set of libraries not found in the above (used to detect 18 out-of-order build rules). 19 """ 20 21 # Dictionary of LibraryInfo objects keyed by BUILD_TYPE value. 22 __library_info = {} 23 24 @staticmethod 25 def get(env): 26 """Gets the LibraryInfo object for the current build type. 27 28 Args: 29 env: The environment object. 30 31 Returns: 32 The LibraryInfo object. 33 """ 34 return LibraryInfo.__library_info.setdefault(env['BUILD_TYPE'], 35 LibraryInfo()) 36 37 def __init__(self): 38 self.lib_targets = {} 39 self.prebuilt_libraries = set() 40 self.system_libraries = set() 41 42 43def _GetLibParams(env, lib): 44 """Gets the params for the given library if it is a library target. 45 46 Returns the params that were specified when the given lib target name was 47 created, or None if no such lib target has been defined. In the None case, it 48 additionally records the negative result so as to detect out-of-order 49 dependencies for future targets. 50 51 Args: 52 env: The environment object. 53 lib: The library's name as a string. 54 55 Returns: 56 Its dictionary of params, or None. 57 """ 58 info = LibraryInfo.get(env) 59 if lib in info.lib_targets: 60 return info.lib_targets[lib] 61 else: 62 if lib not in info.prebuilt_libraries and lib not in info.system_libraries: 63 info.system_libraries.add(lib) 64 return None 65 66 67def _RecordLibParams(env, lib, params): 68 """Record the params used for a library target. 69 70 Record the params used for a library target while checking for several error 71 conditions. 72 73 Args: 74 env: The environment object. 75 lib: The library target's name as a string. 76 params: Its dictionary of params. 77 78 Raises: 79 Exception: The lib target has already been recorded, or the lib was 80 previously declared to be prebuilt, or the lib target is being defined 81 after a reverse library dependency. 82 """ 83 info = LibraryInfo.get(env) 84 if lib in info.lib_targets: 85 raise Exception('Multiple definitions of ' + lib) 86 if lib in info.prebuilt_libraries: 87 raise Exception(lib + ' already declared as a prebuilt library') 88 if lib in info.system_libraries: 89 raise Exception(lib + ' cannot be defined after its reverse library ' 90 'dependencies') 91 info.lib_targets[lib] = params 92 93 94def _IsPrebuiltLibrary(env, lib): 95 """Checks whether or not the given library is a prebuilt static library. 96 97 Returns whether or not the given library name has been declared to be a 98 prebuilt static library. In the False case, it additionally records the 99 negative result so as to detect out-of-order dependencies for future targets. 100 101 Args: 102 env: The environment object. 103 lib: The library's name as a string. 104 105 Returns: 106 True or False 107 """ 108 info = LibraryInfo.get(env) 109 if lib in info.prebuilt_libraries: 110 return True 111 else: 112 if lib not in info.lib_targets and lib not in info.system_libraries: 113 info.system_libraries.add(lib) 114 return False 115 116 117def _RecordPrebuiltLibrary(env, lib): 118 """Record that a library is a prebuilt static library. 119 120 Record that the given library name refers to a prebuilt static library while 121 checking for several error conditions. 122 123 Args: 124 env: The environment object. 125 lib: The library's name as a string. 126 127 Raises: 128 Exception: The lib has already been recorded to be prebuilt, or the lib was 129 previously declared as a target, or the lib is being declared as 130 prebuilt after a reverse library dependency. 131 """ 132 info = LibraryInfo.get(env) 133 if lib in info.prebuilt_libraries: 134 raise Exception('Multiple prebuilt declarations of ' + lib) 135 if lib in info.lib_targets: 136 raise Exception(lib + ' already defined as a target') 137 if lib in info.system_libraries: 138 raise Exception(lib + ' cannot be declared as prebuilt after its reverse ' 139 'library dependencies') 140 info.prebuilt_libraries.add(lib) 141 142 143def _GenericLibrary(env, static, **kwargs): 144 """Extends ComponentLibrary to support multiplatform builds 145 of dynamic or static libraries. 146 147 Args: 148 env: The environment object. 149 kwargs: The keyword arguments. 150 151 Returns: 152 See swtoolkit ComponentLibrary 153 """ 154 params = CombineDicts(kwargs, {'COMPONENT_STATIC': static}) 155 return ExtendComponent(env, 'ComponentLibrary', **params) 156 157 158def DeclarePrebuiltLibraries(env, libraries): 159 """Informs the build engine about external static libraries. 160 161 Informs the build engine that the given external library name(s) are prebuilt 162 static libraries, as opposed to shared libraries. 163 164 Args: 165 env: The environment object. 166 libraries: The library or libraries that are being declared as prebuilt 167 static libraries. 168 """ 169 if not SCons.Util.is_List(libraries): 170 libraries = [libraries] 171 for library in libraries: 172 _RecordPrebuiltLibrary(env, library) 173 174 175def Library(env, **kwargs): 176 """Extends ComponentLibrary to support multiplatform builds of static 177 libraries. 178 179 Args: 180 env: The current environment. 181 kwargs: The keyword arguments. 182 183 Returns: 184 See swtoolkit ComponentLibrary 185 """ 186 return _GenericLibrary(env, True, **kwargs) 187 188 189def DynamicLibrary(env, **kwargs): 190 """Extends ComponentLibrary to support multiplatform builds 191 of dynmic libraries. 192 193 Args: 194 env: The environment object. 195 kwargs: The keyword arguments. 196 197 Returns: 198 See swtoolkit ComponentLibrary 199 """ 200 return _GenericLibrary(env, False, **kwargs) 201 202 203def Object(env, **kwargs): 204 return ExtendComponent(env, 'ComponentObject', **kwargs) 205 206 207def Unittest(env, **kwargs): 208 """Extends ComponentTestProgram to support unittest built 209 for multiple platforms. 210 211 Args: 212 env: The current environment. 213 kwargs: The keyword arguments. 214 215 Returns: 216 See swtoolkit ComponentProgram. 217 """ 218 kwargs['name'] = kwargs['name'] + '_unittest' 219 220 common_test_params = { 221 'posix_cppdefines': ['GUNIT_NO_GOOGLE3', 'GTEST_HAS_RTTI=0'], 222 'libs': ['unittest_main', 'gunit'] 223 } 224 if 'explicit_libs' not in kwargs: 225 common_test_params['win_libs'] = [ 226 'advapi32', 227 'crypt32', 228 'iphlpapi', 229 'secur32', 230 'shell32', 231 'shlwapi', 232 'user32', 233 'wininet', 234 'ws2_32' 235 ] 236 common_test_params['lin_libs'] = [ 237 'crypto', 238 'pthread', 239 'ssl', 240 ] 241 242 params = CombineDicts(kwargs, common_test_params) 243 return ExtendComponent(env, 'ComponentTestProgram', **params) 244 245 246def App(env, **kwargs): 247 """Extends ComponentProgram to support executables with platform specific 248 options. 249 250 Args: 251 env: The current environment. 252 kwargs: The keyword arguments. 253 254 Returns: 255 See swtoolkit ComponentProgram. 256 """ 257 if 'explicit_libs' not in kwargs: 258 common_app_params = { 259 'win_libs': [ 260 'advapi32', 261 'crypt32', 262 'iphlpapi', 263 'secur32', 264 'shell32', 265 'shlwapi', 266 'user32', 267 'wininet', 268 'ws2_32' 269 ]} 270 params = CombineDicts(kwargs, common_app_params) 271 else: 272 params = kwargs 273 return ExtendComponent(env, 'ComponentProgram', **params) 274 275def WiX(env, **kwargs): 276 """ Extends the WiX builder 277 Args: 278 env: The current environment. 279 kwargs: The keyword arguments. 280 281 Returns: 282 The node produced by the environment's wix builder 283 """ 284 return ExtendComponent(env, 'WiX', **kwargs) 285 286def Repository(env, at, path): 287 """Maps a directory external to $MAIN_DIR to the given path so that sources 288 compiled from it end up in the correct place under $OBJ_DIR. NOT required 289 when only referring to header files. 290 291 Args: 292 env: The current environment object. 293 at: The 'mount point' within the current directory. 294 path: Path to the actual directory. 295 """ 296 env.Dir(at).addRepository(env.Dir(path)) 297 298 299def Components(*paths): 300 """Completes the directory paths with the correct file 301 names such that the directory/directory.scons name 302 convention can be used. 303 304 Args: 305 paths: The paths to complete. If it refers to an existing 306 file then it is ignored. 307 308 Returns: 309 The completed lif scons files that are needed to build talk. 310 """ 311 files = [] 312 for path in paths: 313 if os.path.isfile(path): 314 files.append(path) 315 else: 316 files.append(ExpandSconsPath(path)) 317 return files 318 319 320def ExpandSconsPath(path): 321 """Expands a directory path into the path to the 322 scons file that our build uses. 323 Ex: magiflute/plugin/common => magicflute/plugin/common/common.scons 324 325 Args: 326 path: The directory path to expand. 327 328 Returns: 329 The expanded path. 330 """ 331 return '%s/%s.scons' % (path, os.path.basename(path)) 332 333 334def ReadVersion(filename): 335 """Executes the supplied file and pulls out a version definition from it. """ 336 defs = {} 337 execfile(str(filename), defs) 338 if 'version' not in defs: 339 return '0.0.0.0' 340 version = defs['version'] 341 parts = version.split(',') 342 build = os.environ.get('GOOGLE_VERSION_BUILDNUMBER') 343 if build: 344 parts[-1] = str(build) 345 return '.'.join(parts) 346 347 348#------------------------------------------------------------------------------- 349# Helper methods for translating talk.Foo() declarations in to manipulations of 350# environmuent construction variables, including parameter parsing and merging, 351# 352def PopEntry(dictionary, key): 353 """Get the value from a dictionary by key. If the key 354 isn't in the dictionary then None is returned. If it is in 355 the dictionary the value is fetched and then is it removed 356 from the dictionary. 357 358 Args: 359 dictionary: The dictionary. 360 key: The key to get the value for. 361 Returns: 362 The value or None if the key is missing. 363 """ 364 value = None 365 if key in dictionary: 366 value = dictionary[key] 367 dictionary.pop(key) 368 return value 369 370 371def MergeAndFilterByPlatform(env, params): 372 """Take a dictionary of arguments to lists of values, and, depending on 373 which platform we are targetting, merge the lists of associated keys. 374 Merge by combining value lists like so: 375 {win_foo = [a,b], lin_foo = [c,d], foo = [e], mac_bar = [f], bar = [g] } 376 becomes {foo = [a,b,e], bar = [g]} on windows, and 377 {foo = [e], bar = [f,g]} on mac 378 379 Args: 380 env: The hammer environment which knows which platforms are active 381 params: The keyword argument dictionary. 382 Returns: 383 A new dictionary with the filtered and combined entries of params 384 """ 385 platforms = { 386 'linux': 'lin_', 387 'mac': 'mac_', 388 'posix': 'posix_', 389 'windows': 'win_', 390 } 391 active_prefixes = [ 392 platforms[x] for x in iter(platforms) if env.Bit(x) 393 ] 394 inactive_prefixes = [ 395 platforms[x] for x in iter(platforms) if not env.Bit(x) 396 ] 397 398 merged = {} 399 for arg, values in params.iteritems(): 400 inactive_platform = False 401 402 key = arg 403 404 for prefix in active_prefixes: 405 if arg.startswith(prefix): 406 key = arg[len(prefix):] 407 408 for prefix in inactive_prefixes: 409 if arg.startswith(prefix): 410 inactive_platform = True 411 412 if inactive_platform: 413 continue 414 415 AddToDict(merged, key, values) 416 417 return merged 418 419 420def MergeSettingsFromLibraryDependencies(env, params): 421 if 'libs' in params: 422 for lib in params['libs']: 423 libparams = _GetLibParams(env, lib) 424 if libparams: 425 if 'dependent_target_settings' in libparams: 426 params = CombineDicts( 427 params, 428 MergeAndFilterByPlatform( 429 env, 430 libparams['dependent_target_settings'])) 431 return params 432 433 434def ExtendComponent(env, component, **kwargs): 435 """A wrapper around a scons builder function that preprocesses and post- 436 processes its inputs and outputs. For example, it merges and filters 437 certain keyword arguments before appending them to the environments 438 construction variables. It can build signed targets and 64bit copies 439 of targets as well. 440 441 Args: 442 env: The hammer environment with which to build the target 443 component: The environment's builder function, e.g. ComponentProgram 444 kwargs: keyword arguments that are either merged, translated, and passed on 445 to the call to component, or which control execution. 446 TODO(): Document the fields, such as cppdefines->CPPDEFINES, 447 prepend_includedirs, include_talk_media_libs, etc. 448 Returns: 449 The output node returned by the call to component, or a subsequent signed 450 dependant node. 451 """ 452 env = env.Clone() 453 454 # prune parameters intended for other platforms, then merge 455 params = MergeAndFilterByPlatform(env, kwargs) 456 457 # get the 'target' field 458 name = PopEntry(params, 'name') 459 460 # get the 'packages' field and process it if present (only used for Linux). 461 packages = PopEntry(params, 'packages') 462 if packages and len(packages): 463 params = CombineDicts(params, env.GetPackageParams(packages)) 464 465 # save pristine params of lib targets for future reference 466 if 'ComponentLibrary' == component: 467 _RecordLibParams(env, name, dict(params)) 468 469 # add any dependent target settings from library dependencies 470 params = MergeSettingsFromLibraryDependencies(env, params) 471 472 # if this is a signed binary we need to make an unsigned version first 473 signed = env.Bit('windows') and PopEntry(params, 'signed') 474 if signed: 475 name = 'unsigned_' + name 476 477 # potentially exit now 478 srcs = PopEntry(params, 'srcs') 479 if not srcs or not hasattr(env, component): 480 return None 481 482 # apply any explicit dependencies 483 dependencies = PopEntry(params, 'depends') 484 if dependencies is not None: 485 env.Depends(name, dependencies) 486 487 # put the contents of params into the environment 488 # some entries are renamed then appended, others renamed then prepended 489 appends = { 490 'cppdefines' : 'CPPDEFINES', 491 'libdirs' : 'LIBPATH', 492 'link_flags' : 'LINKFLAGS', 493 'libs' : 'LIBS', 494 'FRAMEWORKS' : 'FRAMEWORKS', 495 } 496 prepends = {} 497 if env.Bit('windows'): 498 # MSVC compile flags have precedence at the beginning ... 499 prepends['ccflags'] = 'CCFLAGS' 500 else: 501 # ... while GCC compile flags have precedence at the end 502 appends['ccflags'] = 'CCFLAGS' 503 if PopEntry(params, 'prepend_includedirs'): 504 prepends['includedirs'] = 'CPPPATH' 505 else: 506 appends['includedirs'] = 'CPPPATH' 507 508 for field, var in appends.items(): 509 values = PopEntry(params, field) 510 if values is not None: 511 env.Append(**{var : values}) 512 for field, var in prepends.items(): 513 values = PopEntry(params, field) 514 if values is not None: 515 env.Prepend(**{var : values}) 516 517 # any other parameters are replaced without renaming 518 for field, value in params.items(): 519 env.Replace(**{field : value}) 520 521 if env.Bit('linux') and 'LIBS' in env: 522 libs = env['LIBS'] 523 # When using --as-needed + --start/end-group, shared libraries need to come 524 # after --end-group on the command-line because the pruning decision only 525 # considers the preceding modules and --start/end-group may cause the 526 # effective position of early static libraries on the command-line to be 527 # deferred to the point of --end-group. To effect this, we move shared libs 528 # into _LIBFLAGS, which has the --end-group as its first entry. SCons does 529 # not track dependencies on system shared libraries anyway so we lose 530 # nothing by removing them from LIBS. 531 static_libs = [lib for lib in libs if 532 _GetLibParams(env, lib) or _IsPrebuiltLibrary(env, lib)] 533 shared_libs = ['-l' + lib for lib in libs if not 534 (_GetLibParams(env, lib) or _IsPrebuiltLibrary(env, lib))] 535 env.Replace(LIBS=static_libs) 536 env.Append(_LIBFLAGS=shared_libs) 537 538 # invoke the builder function 539 builder = getattr(env, component) 540 541 node = builder(name, srcs) 542 543 if env.Bit('mac') and 'ComponentProgram' == component: 544 # Build .dSYM debug packages. This is useful even for non-stripped 545 # binaries, as the dsym utility will fetch symbols from all 546 # statically-linked libraries (the linker doesn't include them in to the 547 # final binary). 548 build_dsym = env.Command( 549 env.Dir('$STAGING_DIR/%s.dSYM' % node[0]), 550 node, 551 'mkdir -p `dirname $TARGET` && dsymutil -o $TARGET $SOURCE') 552 env.Alias('all_dsym', env.Alias('%s.dSYM' % node[0], build_dsym)) 553 554 if signed: 555 # Get the name of the built binary, then get the name of the final signed 556 # version from it. We need the output path since we don't know the file 557 # extension beforehand. 558 target = node[0].path.split('_', 1)[1] 559 # postsignprefix: If defined, postsignprefix is a string that should be 560 # prepended to the target executable. This is to provide a work around 561 # for EXEs and DLLs with the same name, which thus have PDBs with the 562 # same name. Setting postsignprefix allows the EXE and its PDB 563 # to be renamed and copied in a previous step; then the desired 564 # name of the EXE (but not PDB) is reconstructed after signing. 565 postsignprefix = PopEntry(params, 'postsignprefix') 566 if postsignprefix is not None: 567 target = postsignprefix + target 568 signed_node = env.SignedBinary( 569 source = node, 570 target = '$STAGING_DIR/' + target, 571 ) 572 env.Alias('signed_binaries', signed_node) 573 return signed_node 574 575 return node 576 577 578def AddToDict(dictionary, key, values, append=True): 579 """Merge the given key value(s) pair into a dictionary. If it contains an 580 entry with that key already, then combine by appending or prepending the 581 values as directed. Otherwise, assign a new keyvalue pair. 582 """ 583 if values is None: 584 return 585 586 if key not in dictionary: 587 dictionary[key] = values 588 return 589 590 cur = dictionary[key] 591 # TODO(dape): Make sure that there are no duplicates 592 # in the list. I can't use python set for this since 593 # the nodes that are returned by the SCONS builders 594 # are not hashable. 595 # dictionary[key] = list(set(cur).union(set(values))) 596 if append: 597 dictionary[key] = cur + values 598 else: 599 dictionary[key] = values + cur 600 601 602def CombineDicts(a, b): 603 """Unions two dictionaries of arrays/dictionaries. 604 605 Unions two dictionaries of arrays/dictionaries by combining the values of keys 606 shared between them. The original dictionaries should not be used again after 607 this call. 608 609 Args: 610 a: First dict. 611 b: Second dict. 612 613 Returns: 614 The union of a and b. 615 """ 616 c = {} 617 for key in a: 618 if key in b: 619 aval = a[key] 620 bval = b.pop(key) 621 if isinstance(aval, dict) and isinstance(bval, dict): 622 c[key] = CombineDicts(aval, bval) 623 else: 624 c[key] = aval + bval 625 else: 626 c[key] = a[key] 627 628 for key in b: 629 c[key] = b[key] 630 631 return c 632 633 634def RenameKey(d, old, new, append=True): 635 AddToDict(d, new, PopEntry(d, old), append) 636