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
8
9# Keep a global dictionary of library target params for lookups in
10# ExtendComponent().
11_all_lib_targets = {}
12
13def _GenericLibrary(env, static, **kwargs):
14  """Extends ComponentLibrary to support multiplatform builds
15     of dynamic or static libraries.
16
17  Args:
18    env: The environment object.
19    kwargs: The keyword arguments.
20
21  Returns:
22    See swtoolkit ComponentLibrary
23  """
24  params = CombineDicts(kwargs, {'COMPONENT_STATIC': static})
25  return ExtendComponent(env, 'ComponentLibrary', **params)
26
27
28def Library(env, **kwargs):
29  """Extends ComponentLibrary to support multiplatform builds of static
30     libraries.
31
32  Args:
33    env: The current environment.
34    kwargs: The keyword arguments.
35
36  Returns:
37    See swtoolkit ComponentLibrary
38  """
39  return _GenericLibrary(env, True, **kwargs)
40
41
42def DynamicLibrary(env, **kwargs):
43  """Extends ComponentLibrary to support multiplatform builds
44     of dynmic libraries.
45
46  Args:
47    env: The environment object.
48    kwargs: The keyword arguments.
49
50  Returns:
51    See swtoolkit ComponentLibrary
52  """
53  return _GenericLibrary(env, False, **kwargs)
54
55
56def Object(env, **kwargs):
57  return ExtendComponent(env, 'ComponentObject', **kwargs)
58
59
60def Unittest(env, **kwargs):
61  """Extends ComponentTestProgram to support unittest built
62     for multiple platforms.
63
64  Args:
65    env: The current environment.
66    kwargs: The keyword arguments.
67
68  Returns:
69    See swtoolkit ComponentProgram.
70  """
71  kwargs['name'] = kwargs['name'] + '_unittest'
72
73  common_test_params = {
74    'posix_cppdefines': ['GUNIT_NO_GOOGLE3', 'GTEST_HAS_RTTI=0'],
75    'libs': ['unittest_main', 'gunit']
76  }
77  if not kwargs.has_key('explicit_libs'):
78    common_test_params['win_libs'] = [
79      'advapi32',
80      'crypt32',
81      'iphlpapi',
82      'secur32',
83      'shell32',
84      'shlwapi',
85      'user32',
86      'wininet',
87      'ws2_32'
88    ]
89    common_test_params['lin_libs'] = [
90      'crypto',
91      'pthread',
92      'ssl',
93    ]
94
95  params = CombineDicts(kwargs, common_test_params)
96  return ExtendComponent(env, 'ComponentTestProgram', **params)
97
98
99def App(env, **kwargs):
100  """Extends ComponentProgram to support executables with platform specific
101     options.
102
103  Args:
104    env: The current environment.
105    kwargs: The keyword arguments.
106
107  Returns:
108    See swtoolkit ComponentProgram.
109  """
110  if not kwargs.has_key('explicit_libs'):
111    common_app_params = {
112      'win_libs': [
113        'advapi32',
114        'crypt32',
115        'iphlpapi',
116        'secur32',
117        'shell32',
118        'shlwapi',
119        'user32',
120        'wininet',
121        'ws2_32'
122      ]}
123    params = CombineDicts(kwargs, common_app_params)
124  else:
125    params = kwargs
126  return ExtendComponent(env, 'ComponentProgram', **params)
127
128def WiX(env, **kwargs):
129  """ Extends the WiX builder
130  Args:
131    env: The current environment.
132    kwargs: The keyword arguments.
133
134  Returns:
135    The node produced by the environment's wix builder
136  """
137  return ExtendComponent(env, 'WiX', **kwargs)
138
139def Repository(env, at, path):
140  """Maps a directory external to $MAIN_DIR to the given path so that sources
141     compiled from it end up in the correct place under $OBJ_DIR.  NOT required
142     when only referring to header files.
143
144  Args:
145    env: The current environment object.
146    at: The 'mount point' within the current directory.
147    path: Path to the actual directory.
148  """
149  env.Dir(at).addRepository(env.Dir(path))
150
151
152def Components(*paths):
153  """Completes the directory paths with the correct file
154     names such that the directory/directory.scons name
155     convention can be used.
156
157  Args:
158    paths: The paths to complete. If it refers to an existing
159           file then it is ignored.
160
161  Returns:
162    The completed lif scons files that are needed to build talk.
163  """
164  files = []
165  for path in paths:
166    if os.path.isfile(path):
167      files.append(path)
168    else:
169      files.append(ExpandSconsPath(path))
170  return files
171
172
173def ExpandSconsPath(path):
174  """Expands a directory path into the path to the
175     scons file that our build uses.
176     Ex: magiflute/plugin/common => magicflute/plugin/common/common.scons
177
178  Args:
179    path: The directory path to expand.
180
181  Returns:
182    The expanded path.
183  """
184  return '%s/%s.scons' % (path, os.path.basename(path))
185
186
187def AddMediaLibs(env, **kwargs):
188  lmi_libdir = '$GOOGLE3/../googleclient/third_party/lmi/files/lib/'
189  if env.Bit('windows'):
190    if env.get('COVERAGE_ENABLED'):
191      lmi_libdir += 'win32/c_only'
192    else:
193      lmi_libdir += 'win32/Release'
194  elif env.Bit('mac'):
195    lmi_libdir += 'macos'
196  elif env.Bit('linux'):
197      lmi_libdir += 'linux/x86'
198
199
200  AddToDict(kwargs, 'libdirs', [
201    '$MAIN_DIR/third_party/gips/Libraries/',
202    lmi_libdir,
203  ])
204
205  gips_lib = ''
206  if env.Bit('windows'):
207    if env.Bit('debug'):
208      gips_lib = 'gipsvoiceenginelib_mtd'
209    else:
210      gips_lib = 'gipsvoiceenginelib_mt'
211  elif env.Bit('mac'):
212    gips_lib = 'VoiceEngine_mac_universal_gcc'
213  elif env.Bit('linux'):
214      gips_lib = 'VoiceEngine_Linux_gcc'
215
216
217  AddToDict(kwargs, 'libs', [
218    gips_lib,
219    'LmiAudioCommon',
220    'LmiClient',
221    'LmiCmcp',
222    'LmiDeviceManager',
223    'LmiH263ClientPlugIn',
224    'LmiH263CodecCommon',
225    'LmiH263Decoder',
226    'LmiH263Encoder',
227    'LmiH264ClientPlugIn',
228    'LmiH264CodecCommon',
229    'LmiH264Common',
230    'LmiH264Decoder',
231    'LmiH264Encoder',
232    'LmiIce',
233    'LmiMediaPayload',
234    'LmiOs',
235    'LmiPacketCache',
236    'LmiProtocolStack',
237    'LmiRateShaper',
238    'LmiRtp',
239    'LmiSecurity',
240    'LmiSignaling',
241    'LmiStun',
242    'LmiTransport',
243    'LmiUi',
244    'LmiUtils',
245    'LmiVideoCommon',
246    'LmiXml',
247  ])
248
249  if env.Bit('windows'):
250    AddToDict(kwargs, 'libs', [
251      'dsound',
252      'd3d9',
253      'gdi32',
254      'strmiids',
255    ])
256
257  if env.Bit('mac'):
258    AddToDict(kwargs, 'FRAMEWORKS', [
259      'AudioToolbox',
260      'AudioUnit',
261      'Cocoa',
262      'CoreAudio',
263      'CoreFoundation',
264      'IOKit',
265      'QTKit',
266      'QuickTime',
267      'QuartzCore',
268    ])
269  return kwargs
270
271
272def ReadVersion(filename):
273  """Executes the supplied file and pulls out a version definition from it. """
274  defs = {}
275  execfile(str(filename), defs)
276  if not defs.has_key('version'):
277    return '0.0.0.0'
278  version = defs['version']
279  parts = version.split(',')
280  build = os.environ.get('GOOGLE_VERSION_BUILDNUMBER')
281  if build:
282    parts[-1] = str(build)
283  return '.'.join(parts)
284
285
286#-------------------------------------------------------------------------------
287# Helper methods for translating talk.Foo() declarations in to manipulations of
288# environmuent construction variables, including parameter parsing and merging,
289#
290def GetEntry(dict, key):
291  """Get the value from a dictionary by key. If the key
292     isn't in the dictionary then None is returned. If it is in
293     the dictionaruy the value is fetched and then is it removed
294     from the dictionary.
295
296  Args:
297    key: The key to get the value for.
298    kwargs: The keyword argument dictionary.
299  Returns:
300    The value or None if the key is missing.
301  """
302  value = None
303  if dict.has_key(key):
304    value = dict[key]
305    dict.pop(key)
306
307  return value
308
309
310def MergeAndFilterByPlatform(env, params):
311  """Take a dictionary of arguments to lists of values, and, depending on
312     which platform we are targetting, merge the lists of associated keys.
313     Merge by combining value lists like so:
314       {win_foo = [a,b], lin_foo = [c,d], foo = [e], mac_bar = [f], bar = [g] }
315       becomes {foo = [a,b,e], bar = [g]} on windows, and
316       {foo = [e], bar = [f,g]} on mac
317
318  Args:
319    env: The hammer environment which knows which platforms are active
320    params: The keyword argument dictionary.
321  Returns:
322    A new dictionary with the filtered and combined entries of params
323  """
324  platforms = {
325    'linux': 'lin_',
326    'mac': 'mac_',
327    'posix': 'posix_',
328    'windows': 'win_',
329  }
330  active_prefixes = [
331    platforms[x] for x in iter(platforms) if env.Bit(x)
332  ]
333  inactive_prefixes = [
334    platforms[x] for x in iter(platforms) if not env.Bit(x)
335  ]
336
337  merged = {}
338  for arg, values in params.iteritems():
339    inactive_platform = False
340
341    key = arg
342
343    for prefix in active_prefixes:
344      if arg.startswith(prefix):
345        key = arg[len(prefix):]
346
347    for prefix in inactive_prefixes:
348      if arg.startswith(prefix):
349        inactive_platform = True
350
351    if inactive_platform:
352      continue
353
354    AddToDict(merged, key, values)
355
356  return merged
357
358# Linux can build both 32 and 64 bit on 64 bit host, but 32 bit host can
359# only build 32 bit.  For 32 bit debian installer a 32 bit host is required.
360# ChromeOS (linux) ebuild don't support 64 bit and requires 32 bit build only
361# for now.
362def Allow64BitCompile(env):
363  return (env.Bit('linux') and env.Bit('platform_arch_64bit')
364          )
365
366def MergeSettingsFromLibraryDependencies(env, params):
367  if params.has_key('libs'):
368    for lib in params['libs']:
369      if (_all_lib_targets.has_key(lib) and
370          _all_lib_targets[lib].has_key('dependent_target_settings')):
371        params = CombineDicts(
372            params,
373            MergeAndFilterByPlatform(
374                env,
375                _all_lib_targets[lib]['dependent_target_settings']))
376  return params
377
378def ExtendComponent(env, component, **kwargs):
379  """A wrapper around a scons builder function that preprocesses and post-
380     processes its inputs and outputs.  For example, it merges and filters
381     certain keyword arguments before appending them to the environments
382     construction variables.  It can build signed targets and 64bit copies
383     of targets as well.
384
385  Args:
386    env: The hammer environment with which to build the target
387    component: The environment's builder function, e.g. ComponentProgram
388    kwargs: keyword arguments that are either merged, translated, and passed on
389            to the call to component, or which control execution.
390            TODO(): Document the fields, such as cppdefines->CPPDEFINES,
391            prepend_includedirs, include_talk_media_libs, etc.
392  Returns:
393    The output node returned by the call to component, or a subsequent signed
394    dependant node.
395  """
396  env = env.Clone()
397
398  # prune parameters intended for other platforms, then merge
399  params = MergeAndFilterByPlatform(env, kwargs)
400
401  # get the 'target' field
402  name = GetEntry(params, 'name')
403
404  # save pristine params of lib targets for future reference
405  if 'ComponentLibrary' == component:
406    _all_lib_targets[name] = dict(params)
407
408  # add any dependent target settings from library dependencies
409  params = MergeSettingsFromLibraryDependencies(env, params)
410
411  # if this is a signed binary we need to make an unsigned version first
412  signed = env.Bit('windows') and GetEntry(params, 'signed')
413  if signed:
414    name = 'unsigned_' + name
415
416  # add default values
417  if GetEntry(params, 'include_talk_media_libs'):
418    params = AddMediaLibs(env, **params)
419
420  # potentially exit now
421  srcs = GetEntry(params, 'srcs')
422  if not srcs or not hasattr(env, component):
423    return None
424
425  # apply any explicit dependencies
426  dependencies = GetEntry(params, 'depends')
427  if dependencies is not None:
428    env.Depends(name, dependencies)
429
430  # put the contents of params into the environment
431  # some entries are renamed then appended, others renamed then prepended
432  appends = {
433    'cppdefines' : 'CPPDEFINES',
434    'libdirs' : 'LIBPATH',
435    'link_flags' : 'LINKFLAGS',
436    'libs' : 'LIBS',
437    'FRAMEWORKS' : 'FRAMEWORKS',
438  }
439  prepends = {}
440  if env.Bit('windows'):
441    # MSVC compile flags have precedence at the beginning ...
442    prepends['ccflags'] = 'CCFLAGS'
443  else:
444    # ... while GCC compile flags have precedence at the end
445    appends['ccflags'] = 'CCFLAGS'
446  if GetEntry(params, 'prepend_includedirs'):
447    prepends['includedirs'] = 'CPPPATH'
448  else:
449    appends['includedirs'] = 'CPPPATH'
450
451  for field, var in appends.items():
452    values = GetEntry(params, field)
453    if values is not None:
454      env.Append(**{var : values})
455  for field, var in prepends.items():
456    values = GetEntry(params, field)
457    if values is not None:
458      env.Prepend(**{var : values})
459
460  # workaround for pulse stripping link flag for unknown reason
461  if Allow64BitCompile(env):
462    env['SHLINKCOM'] = ('$SHLINK -o $TARGET -m32 $SHLINKFLAGS $SOURCES '
463                        '$_LIBDIRFLAGS $_LIBFLAGS')
464    env['LINKCOM'] = ('$LINK -o $TARGET -m32 $LINKFLAGS $SOURCES '
465                      '$_LIBDIRFLAGS $_LIBFLAGS')
466
467  # any other parameters are replaced without renaming
468  for field, value in params.items():
469    env.Replace(**{field : value})
470
471  # invoke the builder function
472  builder = getattr(env, component)
473
474  node = builder(name, srcs)
475
476  # make a parallel 64bit version if requested
477  if Allow64BitCompile(env) and GetEntry(params, 'also64bit'):
478    env_64bit = env.Clone()
479    env_64bit.FilterOut(CCFLAGS = ['-m32'], LINKFLAGS = ['-m32'])
480    env_64bit.Prepend(CCFLAGS = ['-m64', '-fPIC'], LINKFLAGS = ['-m64'])
481    name_64bit = name + '64'
482    env_64bit.Replace(OBJSUFFIX = '64' + env_64bit['OBJSUFFIX'])
483    env_64bit.Replace(SHOBJSUFFIX = '64' + env_64bit['SHOBJSUFFIX'])
484    if ('ComponentProgram' == component or
485        ('ComponentLibrary' == component and
486         env_64bit['COMPONENT_STATIC'] == False)):
487      # link 64 bit versions of libraries
488      libs = []
489      for lib in env_64bit['LIBS']:
490        if (_all_lib_targets.has_key(lib) and
491            _all_lib_targets[lib].has_key('also64bit')):
492          libs.append(lib + '64')
493        else:
494          libs.append(lib)
495      env_64bit.Replace(LIBS = libs)
496
497    env_64bit['SHLINKCOM'] = ('$SHLINK -o $TARGET -m64 $SHLINKFLAGS $SOURCES '
498                              '$_LIBDIRFLAGS $_LIBFLAGS')
499    env_64bit['LINKCOM'] = ('$LINK -o $TARGET -m64 $LINKFLAGS $SOURCES '
500                            '$_LIBDIRFLAGS $_LIBFLAGS')
501    builder = getattr(env_64bit, component)
502    nodes = [node, builder(name_64bit, srcs)]
503    return nodes
504
505  if signed:  # Note currently incompatible with 64Bit flag
506    # Get the name of the built binary, then get the name of the final signed
507    # version from it.  We need the output path since we don't know the file
508    # extension beforehand.
509    target = node[0].path.split('_', 1)[1]
510    signed_node = env.SignedBinary(
511      source = node,
512      target = '$STAGING_DIR/' + target,
513    )
514    env.Alias('signed_binaries', signed_node)
515    return signed_node
516
517  return node
518
519
520def AddToDict(dictionary, key, values, append=True):
521  """Merge the given key value(s) pair into a dictionary.  If it contains an
522     entry with that key already, then combine by appending or prepending the
523     values as directed.  Otherwise, assign a new keyvalue pair.
524  """
525  if values is None:
526    return
527
528  if not dictionary.has_key(key):
529    dictionary[key] = values
530    return
531
532  cur = dictionary[key]
533  # TODO: Make sure that there are no duplicates
534  # in the list. I can't use python set for this since
535  # the nodes that are returned by the SCONS builders
536  # are not hashable.
537  # dictionary[key] = list(set(cur).union(set(values)))
538  if append:
539    dictionary[key] = cur + values
540  else:
541    dictionary[key] = values + cur
542
543
544def CombineDicts(a, b):
545  """Unions two dictionaries by combining values of keys shared between them.
546  """
547  c = {}
548  for key in a:
549    if b.has_key(key):
550      c[key] = a[key] + b.pop(key)
551    else:
552      c[key] = a[key]
553
554  for key in b:
555    c[key] = b[key]
556
557  return c
558
559
560def RenameKey(d, old, new, append=True):
561  AddToDict(d, new, GetEntry(d, old), append)
562