1#!/usr/bin/python
2
3# Copyright (c) 2009 Google Inc. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import gyp
8import gyp.common
9import gyp.SCons as SCons
10import os.path
11import pprint
12import re
13
14
15# TODO:  remove when we delete the last WriteList() call in this module
16WriteList = SCons.WriteList
17
18
19generator_default_variables = {
20    'EXECUTABLE_PREFIX': '',
21    'EXECUTABLE_SUFFIX': '',
22    'STATIC_LIB_PREFIX': '${LIBPREFIX}',
23    'SHARED_LIB_PREFIX': '${SHLIBPREFIX}',
24    'STATIC_LIB_SUFFIX': '${LIBSUFFIX}',
25    'SHARED_LIB_SUFFIX': '${SHLIBSUFFIX}',
26    'INTERMEDIATE_DIR': '${INTERMEDIATE_DIR}',
27    'SHARED_INTERMEDIATE_DIR': '${SHARED_INTERMEDIATE_DIR}',
28    'OS': 'linux',
29    'PRODUCT_DIR': '$TOP_BUILDDIR',
30    'SHARED_LIB_DIR': '$LIB_DIR',
31    'LIB_DIR': '$LIB_DIR',
32    'RULE_INPUT_ROOT': '${SOURCE.filebase}',
33    'RULE_INPUT_EXT': '${SOURCE.suffix}',
34    'RULE_INPUT_NAME': '${SOURCE.file}',
35    'RULE_INPUT_PATH': '${SOURCE.abspath}',
36    'CONFIGURATION_NAME': '${CONFIG_NAME}',
37}
38
39# Tell GYP how to process the input for us.
40generator_handles_variants = True
41generator_wants_absolute_build_file_paths = True
42
43
44def FixPath(path, prefix):
45  if not os.path.isabs(path) and not path[0] == '$':
46    path = prefix + path
47  return path
48
49
50header = """\
51# This file is generated; do not edit.
52"""
53
54
55_alias_template = """
56if GetOption('verbose'):
57  _action = Action([%(action)s])
58else:
59  _action = Action([%(action)s], %(message)s)
60_outputs = env.Alias(
61  ['_%(target_name)s_action'],
62  %(inputs)s,
63  _action
64)
65env.AlwaysBuild(_outputs)
66"""
67
68_run_as_template = """
69if GetOption('verbose'):
70  _action = Action([%(action)s])
71else:
72  _action = Action([%(action)s], %(message)s)
73"""
74
75_run_as_template_suffix = """
76_run_as_target = env.Alias('run_%(target_name)s', target_files, _action)
77env.Requires(_run_as_target, [
78    Alias('%(target_name)s'),
79])
80env.AlwaysBuild(_run_as_target)
81"""
82
83_command_template = """
84if GetOption('verbose'):
85  _action = Action([%(action)s])
86else:
87  _action = Action([%(action)s], %(message)s)
88_outputs = env.Command(
89  %(outputs)s,
90  %(inputs)s,
91  _action
92)
93"""
94
95# This is copied from the default SCons action, updated to handle symlinks.
96_copy_action_template = """
97import shutil
98import SCons.Action
99
100def _copy_files_or_dirs_or_symlinks(dest, src):
101  SCons.Node.FS.invalidate_node_memos(dest)
102  if SCons.Util.is_List(src) and os.path.isdir(dest):
103    for file in src:
104      shutil.copy2(file, dest)
105    return 0
106  elif os.path.islink(src):
107    linkto = os.readlink(src)
108    os.symlink(linkto, dest)
109    return 0
110  elif os.path.isfile(src):
111    return shutil.copy2(src, dest)
112  else:
113    return shutil.copytree(src, dest, 1)
114
115def _copy_files_or_dirs_or_symlinks_str(dest, src):
116  return 'Copying %s to %s ...' % (src, dest)
117
118GYPCopy = SCons.Action.ActionFactory(_copy_files_or_dirs_or_symlinks,
119                                     _copy_files_or_dirs_or_symlinks_str,
120                                     convert=str)
121"""
122
123_rule_template = """
124%(name)s_additional_inputs = %(inputs)s
125%(name)s_outputs = %(outputs)s
126def %(name)s_emitter(target, source, env):
127  return (%(name)s_outputs, source + %(name)s_additional_inputs)
128if GetOption('verbose'):
129  %(name)s_action = Action([%(action)s])
130else:
131  %(name)s_action = Action([%(action)s], %(message)s)
132env['BUILDERS']['%(name)s'] = Builder(action=%(name)s_action,
133                                      emitter=%(name)s_emitter)
134
135_outputs = []
136_processed_input_files = []
137for infile in input_files:
138  if (type(infile) == type('')
139      and not os.path.isabs(infile)
140      and not infile[0] == '$'):
141    infile = %(src_dir)r + infile
142  if str(infile).endswith('.%(extension)s'):
143    _generated = env.%(name)s(infile)
144    env.Precious(_generated)
145    _outputs.append(_generated)
146    %(process_outputs_as_sources_line)s
147  else:
148    _processed_input_files.append(infile)
149prerequisites.extend(_outputs)
150input_files = _processed_input_files
151"""
152
153_spawn_hack = """
154import re
155import SCons.Platform.posix
156needs_shell = re.compile('["\\'><!^&]')
157def gyp_spawn(sh, escape, cmd, args, env):
158  def strip_scons_quotes(arg):
159    if arg[0] == '"' and arg[-1] == '"':
160      return arg[1:-1]
161    return arg
162  stripped_args = [strip_scons_quotes(a) for a in args]
163  if needs_shell.search(' '.join(stripped_args)):
164    return SCons.Platform.posix.exec_spawnvpe([sh, '-c', ' '.join(args)], env)
165  else:
166    return SCons.Platform.posix.exec_spawnvpe(stripped_args, env)
167"""
168
169
170def EscapeShellArgument(s):
171  """Quotes an argument so that it will be interpreted literally by a POSIX
172     shell. Taken from
173     http://stackoverflow.com/questions/35817/whats-the-best-way-to-escape-ossystem-calls-in-python
174     """
175  return "'" + s.replace("'", "'\\''") + "'"
176
177
178def InvertNaiveSConsQuoting(s):
179  """SCons tries to "help" with quoting by naively putting double-quotes around
180     command-line arguments containing space or tab, which is broken for all
181     but trivial cases, so we undo it. (See quote_spaces() in Subst.py)"""
182  if ' ' in s or '\t' in s:
183    # Then SCons will put double-quotes around this, so add our own quotes
184    # to close its quotes at the beginning and end.
185    s = '"' + s + '"'
186  return s
187
188
189def EscapeSConsVariableExpansion(s):
190  """SCons has its own variable expansion syntax using $. We must escape it for
191    strings to be interpreted literally. For some reason this requires four
192    dollar signs, not two, even without the shell involved."""
193  return s.replace('$', '$$$$')
194
195
196def EscapeCppDefine(s):
197  """Escapes a CPP define so that it will reach the compiler unaltered."""
198  s = EscapeShellArgument(s)
199  s = InvertNaiveSConsQuoting(s)
200  s = EscapeSConsVariableExpansion(s)
201  return s
202
203
204def GenerateConfig(fp, config, indent='', src_dir=''):
205  """
206  Generates SCons dictionary items for a gyp configuration.
207
208  This provides the main translation between the (lower-case) gyp settings
209  keywords and the (upper-case) SCons construction variables.
210  """
211  var_mapping = {
212      'ASFLAGS' : 'asflags',
213      'CCFLAGS' : 'cflags',
214      'CFLAGS' : 'cflags_c',
215      'CXXFLAGS' : 'cflags_cc',
216      'CPPDEFINES' : 'defines',
217      'CPPPATH' : 'include_dirs',
218      # Add the ldflags value to $LINKFLAGS, but not $SHLINKFLAGS.
219      # SCons defines $SHLINKFLAGS to incorporate $LINKFLAGS, so
220      # listing both here would case 'ldflags' to get appended to
221      # both, and then have it show up twice on the command line.
222      'LINKFLAGS' : 'ldflags',
223  }
224  postamble='\n%s],\n' % indent
225  for scons_var in sorted(var_mapping.keys()):
226      gyp_var = var_mapping[scons_var]
227      value = config.get(gyp_var)
228      if value:
229        if gyp_var in ('defines',):
230          value = [EscapeCppDefine(v) for v in value]
231        if gyp_var in ('include_dirs',):
232          if src_dir and not src_dir.endswith('/'):
233            src_dir += '/'
234          result = []
235          for v in value:
236            v = FixPath(v, src_dir)
237            # Force SCons to evaluate the CPPPATH directories at
238            # SConscript-read time, so delayed evaluation of $SRC_DIR
239            # doesn't point it to the --generator-output= directory.
240            result.append('env.Dir(%r)' % v)
241          value = result
242        else:
243          value = map(repr, value)
244        WriteList(fp,
245                  value,
246                  prefix=indent,
247                  preamble='%s%s = [\n    ' % (indent, scons_var),
248                  postamble=postamble)
249
250
251def GenerateSConscript(output_filename, spec, build_file, build_file_data):
252  """
253  Generates a SConscript file for a specific target.
254
255  This generates a SConscript file suitable for building any or all of
256  the target's configurations.
257
258  A SConscript file may be called multiple times to generate targets for
259  multiple configurations.  Consequently, it needs to be ready to build
260  the target for any requested configuration, and therefore contains
261  information about the settings for all configurations (generated into
262  the SConscript file at gyp configuration time) as well as logic for
263  selecting (at SCons build time) the specific configuration being built.
264
265  The general outline of a generated SConscript file is:
266
267    --  Header
268
269    --  Import 'env'.  This contains a $CONFIG_NAME construction
270        variable that specifies what configuration to build
271        (e.g. Debug, Release).
272
273    --  Configurations.  This is a dictionary with settings for
274        the different configurations (Debug, Release) under which this
275        target can be built.  The values in the dictionary are themselves
276        dictionaries specifying what construction variables should added
277        to the local copy of the imported construction environment
278        (Append), should be removed (FilterOut), and should outright
279        replace the imported values (Replace).
280
281    --  Clone the imported construction environment and update
282        with the proper configuration settings.
283
284    --  Initialize the lists of the targets' input files and prerequisites.
285
286    --  Target-specific actions and rules.  These come after the
287        input file and prerequisite initializations because the
288        outputs of the actions and rules may affect the input file
289        list (process_outputs_as_sources) and get added to the list of
290        prerequisites (so that they're guaranteed to be executed before
291        building the target).
292
293    --  Call the Builder for the target itself.
294
295    --  Arrange for any copies to be made into installation directories.
296
297    --  Set up the {name} Alias (phony Node) for the target as the
298        primary handle for building all of the target's pieces.
299
300    --  Use env.Require() to make sure the prerequisites (explicitly
301        specified, but also including the actions and rules) are built
302        before the target itself.
303
304    --  Return the {name} Alias to the calling SConstruct file
305        so it can be added to the list of default targets.
306  """
307  scons_target = SCons.Target(spec)
308
309  gyp_dir = os.path.dirname(output_filename)
310  if not gyp_dir:
311      gyp_dir = '.'
312  gyp_dir = os.path.abspath(gyp_dir)
313
314  output_dir = os.path.dirname(output_filename)
315  src_dir = build_file_data['_DEPTH']
316  src_dir_rel = gyp.common.RelativePath(src_dir, output_dir)
317  subdir = gyp.common.RelativePath(os.path.dirname(build_file), src_dir)
318  src_subdir = '$SRC_DIR/' + subdir
319  src_subdir_ = src_subdir + '/'
320
321  component_name = os.path.splitext(os.path.basename(build_file))[0]
322  target_name = spec['target_name']
323
324  if not os.path.exists(gyp_dir):
325    os.makedirs(gyp_dir)
326  fp = open(output_filename, 'w')
327  fp.write(header)
328
329  fp.write('\nimport os\n')
330  fp.write('\nImport("env")\n')
331
332  #
333  fp.write('\n')
334  fp.write('env = env.Clone(COMPONENT_NAME=%s,\n' % repr(component_name))
335  fp.write('                TARGET_NAME=%s)\n' % repr(target_name))
336
337  #
338  for config in spec['configurations'].itervalues():
339    if config.get('scons_line_length'):
340      fp.write(_spawn_hack)
341      break
342
343  #
344  indent = ' ' * 12
345  fp.write('\n')
346  fp.write('configurations = {\n')
347  for config_name, config in spec['configurations'].iteritems():
348    fp.write('    \'%s\' : {\n' % config_name)
349
350    fp.write('        \'Append\' : dict(\n')
351    GenerateConfig(fp, config, indent, src_subdir)
352    libraries = spec.get('libraries')
353    if libraries:
354      WriteList(fp,
355                map(repr, libraries),
356                prefix=indent,
357                preamble='%sLIBS = [\n    ' % indent,
358                postamble='\n%s],\n' % indent)
359    fp.write('        ),\n')
360
361    fp.write('        \'FilterOut\' : dict(\n' )
362    for key, var in config.get('scons_remove', {}).iteritems():
363      fp.write('             %s = %s,\n' % (key, repr(var)))
364    fp.write('        ),\n')
365
366    fp.write('        \'Replace\' : dict(\n' )
367    scons_settings = config.get('scons_variable_settings', {})
368    for key in sorted(scons_settings.keys()):
369      val = pprint.pformat(scons_settings[key])
370      fp.write('             %s = %s,\n' % (key, val))
371    if 'c++' in spec.get('link_languages', []):
372      fp.write('             %s = %s,\n' % ('LINK', repr('$CXX')))
373    if config.get('scons_line_length'):
374      fp.write('             SPAWN = gyp_spawn,\n')
375    fp.write('        ),\n')
376
377    fp.write('        \'ImportExternal\' : [\n' )
378    for var in config.get('scons_import_variables', []):
379      fp.write('             %s,\n' % repr(var))
380    fp.write('        ],\n')
381
382    fp.write('        \'PropagateExternal\' : [\n' )
383    for var in config.get('scons_propagate_variables', []):
384      fp.write('             %s,\n' % repr(var))
385    fp.write('        ],\n')
386
387    fp.write('    },\n')
388  fp.write('}\n')
389
390  fp.write('\n'
391           'config = configurations[env[\'CONFIG_NAME\']]\n'
392           'env.Append(**config[\'Append\'])\n'
393           'env.FilterOut(**config[\'FilterOut\'])\n'
394           'env.Replace(**config[\'Replace\'])\n')
395
396  fp.write('\n'
397           '# Scons forces -fPIC for SHCCFLAGS on some platforms.\n'
398           '# Disable that so we can control it from cflags in gyp.\n'
399           '# Note that Scons itself is inconsistent with its -fPIC\n'
400           '# setting. SHCCFLAGS forces -fPIC, and SHCFLAGS does not.\n'
401           '# This will make SHCCFLAGS consistent with SHCFLAGS.\n'
402           'env[\'SHCCFLAGS\'] = [\'$CCFLAGS\']\n')
403
404  fp.write('\n'
405           'for _var in config[\'ImportExternal\']:\n'
406           '  if _var in ARGUMENTS:\n'
407           '    env[_var] = ARGUMENTS[_var]\n'
408           '  elif _var in os.environ:\n'
409           '    env[_var] = os.environ[_var]\n'
410           'for _var in config[\'PropagateExternal\']:\n'
411           '  if _var in ARGUMENTS:\n'
412           '    env[_var] = ARGUMENTS[_var]\n'
413           '  elif _var in os.environ:\n'
414           '    env[\'ENV\'][_var] = os.environ[_var]\n')
415
416  fp.write('\n'
417           "env['ENV']['LD_LIBRARY_PATH'] = env.subst('$LIB_DIR')\n")
418
419  #
420  #fp.write("\nif env.has_key('CPPPATH'):\n")
421  #fp.write("  env['CPPPATH'] = map(env.Dir, env['CPPPATH'])\n")
422
423  variants = spec.get('variants', {})
424  for setting in sorted(variants.keys()):
425    if_fmt = 'if ARGUMENTS.get(%s) not in (None, \'0\'):\n'
426    fp.write('\n')
427    fp.write(if_fmt % repr(setting.upper()))
428    fp.write('  env.AppendUnique(\n')
429    GenerateConfig(fp, variants[setting], indent, src_subdir)
430    fp.write('  )\n')
431
432  #
433  scons_target.write_input_files(fp)
434
435  fp.write('\n')
436  fp.write('target_files = []\n')
437  prerequisites = spec.get('scons_prerequisites', [])
438  fp.write('prerequisites = %s\n' % pprint.pformat(prerequisites))
439
440  actions = spec.get('actions', [])
441  for action in actions:
442    a = ['cd', src_subdir, '&&'] + action['action']
443    message = action.get('message')
444    if message:
445      message = repr(message)
446    inputs = [FixPath(f, src_subdir_) for f in action.get('inputs', [])]
447    outputs = [FixPath(f, src_subdir_) for f in action.get('outputs', [])]
448    if outputs:
449      template = _command_template
450    else:
451      template = _alias_template
452    fp.write(template % {
453                 'inputs' : pprint.pformat(inputs),
454                 'outputs' : pprint.pformat(outputs),
455                 'action' : pprint.pformat(a),
456                 'message' : message,
457                 'target_name': target_name,
458             })
459    if int(action.get('process_outputs_as_sources', 0)):
460      fp.write('input_files.extend(_outputs)\n')
461    fp.write('prerequisites.extend(_outputs)\n')
462    fp.write('target_files.extend(_outputs)\n')
463
464  rules = spec.get('rules', [])
465  for rule in rules:
466    name = rule['rule_name']
467    a = ['cd', src_subdir, '&&'] + rule['action']
468    message = rule.get('message')
469    if message:
470        message = repr(message)
471    if int(rule.get('process_outputs_as_sources', 0)):
472      poas_line = '_processed_input_files.extend(_generated)'
473    else:
474      poas_line = '_processed_input_files.append(infile)'
475    inputs = [FixPath(f, src_subdir_) for f in rule.get('inputs', [])]
476    outputs = [FixPath(f, src_subdir_) for f in rule.get('outputs', [])]
477    fp.write(_rule_template % {
478                 'inputs' : pprint.pformat(inputs),
479                 'outputs' : pprint.pformat(outputs),
480                 'action' : pprint.pformat(a),
481                 'extension' : rule['extension'],
482                 'name' : name,
483                 'message' : message,
484                 'process_outputs_as_sources_line' : poas_line,
485                 'src_dir' : src_subdir_,
486             })
487
488  scons_target.write_target(fp, src_subdir)
489
490  copies = spec.get('copies', [])
491  if copies:
492    fp.write(_copy_action_template)
493  for copy in copies:
494    destdir = None
495    files = None
496    try:
497      destdir = copy['destination']
498    except KeyError, e:
499      gyp.common.ExceptionAppend(
500        e,
501        "Required 'destination' key missing for 'copies' in %s." % build_file)
502      raise
503    try:
504      files = copy['files']
505    except KeyError, e:
506      gyp.common.ExceptionAppend(
507        e, "Required 'files' key missing for 'copies' in %s." % build_file)
508      raise
509    if not files:
510      # TODO:  should probably add a (suppressible) warning;
511      # a null file list may be unintentional.
512      continue
513    if not destdir:
514      raise Exception(
515        "Required 'destination' key is empty for 'copies' in %s." % build_file)
516
517    fmt = ('\n'
518           '_outputs = env.Command(%s,\n'
519           '    %s,\n'
520           '    GYPCopy(\'$TARGET\', \'$SOURCE\'))\n')
521    for f in copy['files']:
522      # Remove trailing separators so basename() acts like Unix basename and
523      # always returns the last element, whether a file or dir. Without this,
524      # only the contents, not the directory itself, are copied (and nothing
525      # might be copied if dest already exists, since scons thinks nothing needs
526      # to be done).
527      dest = os.path.join(destdir, os.path.basename(f.rstrip(os.sep)))
528      f = FixPath(f, src_subdir_)
529      dest = FixPath(dest, src_subdir_)
530      fp.write(fmt % (repr(dest), repr(f)))
531      fp.write('target_files.extend(_outputs)\n')
532
533  run_as = spec.get('run_as')
534  if run_as:
535    action = run_as.get('action', [])
536    working_directory = run_as.get('working_directory')
537    if not working_directory:
538      working_directory = gyp_dir
539    else:
540      if not os.path.isabs(working_directory):
541        working_directory = os.path.normpath(os.path.join(gyp_dir,
542                                                          working_directory))
543    if run_as.get('environment'):
544      for (key, val) in run_as.get('environment').iteritems():
545        action = ['%s="%s"' % (key, val)] + action
546    action = ['cd', '"%s"' % working_directory, '&&'] + action
547    fp.write(_run_as_template % {
548      'action' : pprint.pformat(action),
549      'message' : run_as.get('message', ''),
550    })
551
552  fmt = "\ngyp_target = env.Alias('%s', target_files)\n"
553  fp.write(fmt % target_name)
554
555  dependencies = spec.get('scons_dependencies', [])
556  if dependencies:
557    WriteList(fp, dependencies, preamble='dependencies = [\n    ',
558                                postamble='\n]\n')
559    fp.write('env.Requires(target_files, dependencies)\n')
560    fp.write('env.Requires(gyp_target, dependencies)\n')
561    fp.write('for prerequisite in prerequisites:\n')
562    fp.write('  env.Requires(prerequisite, dependencies)\n')
563  fp.write('env.Requires(gyp_target, prerequisites)\n')
564
565  if run_as:
566    fp.write(_run_as_template_suffix % {
567      'target_name': target_name,
568    })
569
570  fp.write('Return("gyp_target")\n')
571
572  fp.close()
573
574
575#############################################################################
576# TEMPLATE BEGIN
577
578_wrapper_template = """\
579
580__doc__ = '''
581Wrapper configuration for building this entire "solution,"
582including all the specific targets in various *.scons files.
583'''
584
585import os
586import sys
587
588import SCons.Environment
589import SCons.Util
590
591def GetProcessorCount():
592  '''
593  Detects the number of CPUs on the system. Adapted form:
594  http://codeliberates.blogspot.com/2008/05/detecting-cpuscores-in-python.html
595  '''
596  # Linux, Unix and Mac OS X:
597  if hasattr(os, 'sysconf'):
598    if os.sysconf_names.has_key('SC_NPROCESSORS_ONLN'):
599      # Linux and Unix or Mac OS X with python >= 2.5:
600      return os.sysconf('SC_NPROCESSORS_ONLN')
601    else:  # Mac OS X with Python < 2.5:
602      return int(os.popen2("sysctl -n hw.ncpu")[1].read())
603  # Windows:
604  if os.environ.has_key('NUMBER_OF_PROCESSORS'):
605    return max(int(os.environ.get('NUMBER_OF_PROCESSORS', '1')), 1)
606  return 1  # Default
607
608# Support PROGRESS= to show progress in different ways.
609p = ARGUMENTS.get('PROGRESS')
610if p == 'spinner':
611  Progress(['/\\r', '|\\r', '\\\\\\r', '-\\r'],
612           interval=5,
613           file=open('/dev/tty', 'w'))
614elif p == 'name':
615  Progress('$TARGET\\r', overwrite=True, file=open('/dev/tty', 'w'))
616
617# Set the default -j value based on the number of processors.
618SetOption('num_jobs', GetProcessorCount() + 1)
619
620# Have SCons use its cached dependency information.
621SetOption('implicit_cache', 1)
622
623# Only re-calculate MD5 checksums if a timestamp has changed.
624Decider('MD5-timestamp')
625
626# Since we set the -j value by default, suppress SCons warnings about being
627# unable to support parallel build on versions of Python with no threading.
628default_warnings = ['no-no-parallel-support']
629SetOption('warn', default_warnings + GetOption('warn'))
630
631AddOption('--mode', nargs=1, dest='conf_list', default=[],
632          action='append', help='Configuration to build.')
633
634AddOption('--verbose', dest='verbose', default=False,
635          action='store_true', help='Verbose command-line output.')
636
637
638#
639sconscript_file_map = %(sconscript_files)s
640
641class LoadTarget:
642  '''
643  Class for deciding if a given target sconscript is to be included
644  based on a list of included target names, optionally prefixed with '-'
645  to exclude a target name.
646  '''
647  def __init__(self, load):
648    '''
649    Initialize a class with a list of names for possible loading.
650
651    Arguments:
652      load:  list of elements in the LOAD= specification
653    '''
654    self.included = set([c for c in load if not c.startswith('-')])
655    self.excluded = set([c[1:] for c in load if c.startswith('-')])
656
657    if not self.included:
658      self.included = set(['all'])
659
660  def __call__(self, target):
661    '''
662    Returns True if the specified target's sconscript file should be
663    loaded, based on the initialized included and excluded lists.
664    '''
665    return (target in self.included or
666            ('all' in self.included and not target in self.excluded))
667
668if 'LOAD' in ARGUMENTS:
669  load = ARGUMENTS['LOAD'].split(',')
670else:
671  load = []
672load_target = LoadTarget(load)
673
674sconscript_files = []
675for target, sconscript in sconscript_file_map.iteritems():
676  if load_target(target):
677    sconscript_files.append(sconscript)
678
679
680target_alias_list= []
681
682conf_list = GetOption('conf_list')
683if conf_list:
684    # In case the same --mode= value was specified multiple times.
685    conf_list = list(set(conf_list))
686else:
687    conf_list = [%(default_configuration)r]
688
689sconsbuild_dir = Dir(%(sconsbuild_dir)s)
690
691
692def FilterOut(self, **kw):
693  kw = SCons.Environment.copy_non_reserved_keywords(kw)
694  for key, val in kw.items():
695    envval = self.get(key, None)
696    if envval is None:
697      # No existing variable in the environment, so nothing to delete.
698      continue
699
700    for vremove in val:
701      # Use while not if, so we can handle duplicates.
702      while vremove in envval:
703        envval.remove(vremove)
704
705    self[key] = envval
706
707    # TODO(sgk): SCons.Environment.Append() has much more logic to deal
708    # with various types of values.  We should handle all those cases in here
709    # too.  (If variable is a dict, etc.)
710
711
712non_compilable_suffixes = {
713    'LINUX' : set([
714        '.bdic',
715        '.css',
716        '.dat',
717        '.fragment',
718        '.gperf',
719        '.h',
720        '.hh',
721        '.hpp',
722        '.html',
723        '.hxx',
724        '.idl',
725        '.in',
726        '.in0',
727        '.in1',
728        '.js',
729        '.mk',
730        '.rc',
731        '.sigs',
732        '',
733    ]),
734    'WINDOWS' : set([
735        '.h',
736        '.hh',
737        '.hpp',
738        '.dat',
739        '.idl',
740        '.in',
741        '.in0',
742        '.in1',
743    ]),
744}
745
746def compilable(env, file):
747  base, ext = os.path.splitext(str(file))
748  if ext in non_compilable_suffixes[env['TARGET_PLATFORM']]:
749    return False
750  return True
751
752def compilable_files(env, sources):
753  return [x for x in sources if compilable(env, x)]
754
755def GypProgram(env, target, source, *args, **kw):
756  source = compilable_files(env, source)
757  result = env.Program(target, source, *args, **kw)
758  if env.get('INCREMENTAL'):
759    env.Precious(result)
760  return result
761
762def GypTestProgram(env, target, source, *args, **kw):
763  source = compilable_files(env, source)
764  result = env.Program(target, source, *args, **kw)
765  if env.get('INCREMENTAL'):
766    env.Precious(*result)
767  return result
768
769def GypLibrary(env, target, source, *args, **kw):
770  source = compilable_files(env, source)
771  result = env.Library(target, source, *args, **kw)
772  return result
773
774def GypLoadableModule(env, target, source, *args, **kw):
775  source = compilable_files(env, source)
776  result = env.LoadableModule(target, source, *args, **kw)
777  return result
778
779def GypStaticLibrary(env, target, source, *args, **kw):
780  source = compilable_files(env, source)
781  result = env.StaticLibrary(target, source, *args, **kw)
782  return result
783
784def GypSharedLibrary(env, target, source, *args, **kw):
785  source = compilable_files(env, source)
786  result = env.SharedLibrary(target, source, *args, **kw)
787  if env.get('INCREMENTAL'):
788    env.Precious(result)
789  return result
790
791def add_gyp_methods(env):
792  env.AddMethod(GypProgram)
793  env.AddMethod(GypTestProgram)
794  env.AddMethod(GypLibrary)
795  env.AddMethod(GypLoadableModule)
796  env.AddMethod(GypStaticLibrary)
797  env.AddMethod(GypSharedLibrary)
798
799  env.AddMethod(FilterOut)
800
801  env.AddMethod(compilable)
802
803
804base_env = Environment(
805    tools = %(scons_tools)s,
806    INTERMEDIATE_DIR='$OBJ_DIR/${COMPONENT_NAME}/_${TARGET_NAME}_intermediate',
807    LIB_DIR='$TOP_BUILDDIR/lib',
808    OBJ_DIR='$TOP_BUILDDIR/obj',
809    SCONSBUILD_DIR=sconsbuild_dir.abspath,
810    SHARED_INTERMEDIATE_DIR='$OBJ_DIR/_global_intermediate',
811    SRC_DIR=Dir(%(src_dir)r),
812    TARGET_PLATFORM='LINUX',
813    TOP_BUILDDIR='$SCONSBUILD_DIR/$CONFIG_NAME',
814    LIBPATH=['$LIB_DIR'],
815)
816
817if not GetOption('verbose'):
818  base_env.SetDefault(
819      ARCOMSTR='Creating library $TARGET',
820      ASCOMSTR='Assembling $TARGET',
821      CCCOMSTR='Compiling $TARGET',
822      CONCATSOURCECOMSTR='ConcatSource $TARGET',
823      CXXCOMSTR='Compiling $TARGET',
824      LDMODULECOMSTR='Building loadable module $TARGET',
825      LINKCOMSTR='Linking $TARGET',
826      MANIFESTCOMSTR='Updating manifest for $TARGET',
827      MIDLCOMSTR='Compiling IDL $TARGET',
828      PCHCOMSTR='Precompiling $TARGET',
829      RANLIBCOMSTR='Indexing $TARGET',
830      RCCOMSTR='Compiling resource $TARGET',
831      SHCCCOMSTR='Compiling $TARGET',
832      SHCXXCOMSTR='Compiling $TARGET',
833      SHLINKCOMSTR='Linking $TARGET',
834      SHMANIFESTCOMSTR='Updating manifest for $TARGET',
835  )
836
837add_gyp_methods(base_env)
838
839for conf in conf_list:
840  env = base_env.Clone(CONFIG_NAME=conf)
841  SConsignFile(env.File('$TOP_BUILDDIR/.sconsign').abspath)
842  for sconscript in sconscript_files:
843    target_alias = env.SConscript(sconscript, exports=['env'])
844    if target_alias:
845      target_alias_list.extend(target_alias)
846
847Default(Alias('all', target_alias_list))
848
849help_fmt = '''
850Usage: hammer [SCONS_OPTIONS] [VARIABLES] [TARGET] ...
851
852Local command-line build options:
853  --mode=CONFIG             Configuration to build:
854                              --mode=Debug [default]
855                              --mode=Release
856  --verbose                 Print actual executed command lines.
857
858Supported command-line build variables:
859  LOAD=[module,...]         Comma-separated list of components to load in the
860                              dependency graph ('-' prefix excludes)
861  PROGRESS=type             Display a progress indicator:
862                              name:  print each evaluated target name
863                              spinner:  print a spinner every 5 targets
864
865The following TARGET names can also be used as LOAD= module names:
866
867%%s
868'''
869
870if GetOption('help'):
871  def columnar_text(items, width=78, indent=2, sep=2):
872    result = []
873    colwidth = max(map(len, items)) + sep
874    cols = (width - indent) / colwidth
875    if cols < 1:
876      cols = 1
877    rows = (len(items) + cols - 1) / cols
878    indent = '%%*s' %% (indent, '')
879    sep = indent
880    for row in xrange(0, rows):
881      result.append(sep)
882      for i in xrange(row, len(items), rows):
883        result.append('%%-*s' %% (colwidth, items[i]))
884      sep = '\\n' + indent
885    result.append('\\n')
886    return ''.join(result)
887
888  load_list = set(sconscript_file_map.keys())
889  target_aliases = set(map(str, target_alias_list))
890
891  common = load_list and target_aliases
892  load_only = load_list - common
893  target_only = target_aliases - common
894  help_text = [help_fmt %% columnar_text(sorted(list(common)))]
895  if target_only:
896    fmt = "The following are additional TARGET names:\\n\\n%%s\\n"
897    help_text.append(fmt %% columnar_text(sorted(list(target_only))))
898  if load_only:
899    fmt = "The following are additional LOAD= module names:\\n\\n%%s\\n"
900    help_text.append(fmt %% columnar_text(sorted(list(load_only))))
901  Help(''.join(help_text))
902"""
903
904# TEMPLATE END
905#############################################################################
906
907
908def GenerateSConscriptWrapper(build_file, build_file_data, name,
909                              output_filename, sconscript_files,
910                              default_configuration):
911  """
912  Generates the "wrapper" SConscript file (analogous to the Visual Studio
913  solution) that calls all the individual target SConscript files.
914  """
915  output_dir = os.path.dirname(output_filename)
916  src_dir = build_file_data['_DEPTH']
917  src_dir_rel = gyp.common.RelativePath(src_dir, output_dir)
918  if not src_dir_rel:
919    src_dir_rel = '.'
920  scons_settings = build_file_data.get('scons_settings', {})
921  sconsbuild_dir = scons_settings.get('sconsbuild_dir', '#')
922  scons_tools = scons_settings.get('tools', ['default'])
923
924  sconscript_file_lines = ['dict(']
925  for target in sorted(sconscript_files.keys()):
926    sconscript = sconscript_files[target]
927    sconscript_file_lines.append('    %s = %r,' % (target, sconscript))
928  sconscript_file_lines.append(')')
929
930  fp = open(output_filename, 'w')
931  fp.write(header)
932  fp.write(_wrapper_template % {
933               'default_configuration' : default_configuration,
934               'name' : name,
935               'scons_tools' : repr(scons_tools),
936               'sconsbuild_dir' : repr(sconsbuild_dir),
937               'sconscript_files' : '\n'.join(sconscript_file_lines),
938               'src_dir' : src_dir_rel,
939           })
940  fp.close()
941
942  # Generate the SConstruct file that invokes the wrapper SConscript.
943  dir, fname = os.path.split(output_filename)
944  SConstruct = os.path.join(dir, 'SConstruct')
945  fp = open(SConstruct, 'w')
946  fp.write(header)
947  fp.write('SConscript(%s)\n' % repr(fname))
948  fp.close()
949
950
951def TargetFilename(target, build_file=None, output_suffix=''):
952  """Returns the .scons file name for the specified target.
953  """
954  if build_file is None:
955    build_file, target = gyp.common.ParseQualifiedTarget(target)[:2]
956  output_file = os.path.join(os.path.dirname(build_file),
957                             target + output_suffix + '.scons')
958  return output_file
959
960
961def GenerateOutput(target_list, target_dicts, data, params):
962  """
963  Generates all the output files for the specified targets.
964  """
965  options = params['options']
966
967  if options.generator_output:
968    def output_path(filename):
969      return filename.replace(params['cwd'], options.generator_output)
970  else:
971    def output_path(filename):
972      return filename
973
974  default_configuration = None
975
976  for qualified_target in target_list:
977    spec = target_dicts[qualified_target]
978    if spec['toolset'] != 'target':
979      raise Exception(
980          'Multiple toolsets not supported in scons build (target %s)' %
981          qualified_target)
982    scons_target = SCons.Target(spec)
983    if scons_target.is_ignored:
984      continue
985
986    # TODO:  assumes the default_configuration of the first target
987    # non-Default target is the correct default for all targets.
988    # Need a better model for handle variation between targets.
989    if (not default_configuration and
990        spec['default_configuration'] != 'Default'):
991      default_configuration = spec['default_configuration']
992
993    build_file, target = gyp.common.ParseQualifiedTarget(qualified_target)[:2]
994    output_file = TargetFilename(target, build_file, options.suffix)
995    if options.generator_output:
996      output_file = output_path(output_file)
997
998    if not spec.has_key('libraries'):
999      spec['libraries'] = []
1000
1001    # Add dependent static library targets to the 'libraries' value.
1002    deps = spec.get('dependencies', [])
1003    spec['scons_dependencies'] = []
1004    for d in deps:
1005      td = target_dicts[d]
1006      target_name = td['target_name']
1007      spec['scons_dependencies'].append("Alias('%s')" % target_name)
1008      if td['type'] in ('static_library', 'shared_library'):
1009        libname = td.get('product_name', target_name)
1010        spec['libraries'].append('lib' + libname)
1011      if td['type'] == 'loadable_module':
1012        prereqs = spec.get('scons_prerequisites', [])
1013        # TODO:  parameterize with <(SHARED_LIBRARY_*) variables?
1014        td_target = SCons.Target(td)
1015        td_target.target_prefix = '${SHLIBPREFIX}'
1016        td_target.target_suffix = '${SHLIBSUFFIX}'
1017
1018    GenerateSConscript(output_file, spec, build_file, data[build_file])
1019
1020  if not default_configuration:
1021    default_configuration = 'Default'
1022
1023  for build_file in sorted(data.keys()):
1024    path, ext = os.path.splitext(build_file)
1025    if ext != '.gyp':
1026      continue
1027    output_dir, basename = os.path.split(path)
1028    output_filename  = path + '_main' + options.suffix + '.scons'
1029
1030    all_targets = gyp.common.AllTargets(target_list, target_dicts, build_file)
1031    sconscript_files = {}
1032    for t in all_targets:
1033      scons_target = SCons.Target(target_dicts[t])
1034      if scons_target.is_ignored:
1035        continue
1036      bf, target = gyp.common.ParseQualifiedTarget(t)[:2]
1037      target_filename = TargetFilename(target, bf, options.suffix)
1038      tpath = gyp.common.RelativePath(target_filename, output_dir)
1039      sconscript_files[target] = tpath
1040
1041    output_filename = output_path(output_filename)
1042    if sconscript_files:
1043      GenerateSConscriptWrapper(build_file, data[build_file], basename,
1044                                output_filename, sconscript_files,
1045                                default_configuration)
1046