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