ambuild2/frontend/amb2/gen.py
author David Anderson <dvander@alliedmods.net>
Fri Feb 07 22:54:58 2014 -0800 (2014-02-07)
changeset 236 aa23c47cb040
parent 235 fd0f5f1a7a10
child 237 a3ab09e0556b
permissions -rw-r--r--
Add an originalCwd property to builders (bug 6037, r=ds).
dvander@65
     1
# vim: set ts=8 sts=2 sw=2 tw=99 et:
dvander@72
     2
#
dvander@72
     3
# This file is part of AMBuild.
dvander@72
     4
# 
dvander@72
     5
# AMBuild is free software: you can redistribute it and/or modify
dvander@72
     6
# it under the terms of the GNU General Public License as published by
dvander@72
     7
# the Free Software Foundation, either version 3 of the License, or
dvander@72
     8
# (at your option) any later version.
dvander@72
     9
# 
dvander@72
    10
# AMBuild is distributed in the hope that it will be useful,
dvander@72
    11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
dvander@72
    12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
dvander@72
    13
# GNU General Public License for more details.
dvander@72
    14
# 
dvander@72
    15
# You should have received a copy of the GNU General Public License
dvander@72
    16
# along with AMBuild. If not, see <http://www.gnu.org/licenses/>.
dvander@65
    17
import os
dvander@167
    18
from ambuild2 import util
dvander@167
    19
from ambuild2 import nodetypes
dvander@167
    20
from ambuild2.frontend.cpp import DetectCompiler
dvander@167
    21
from ambuild2.frontend import base_gen
dvander@167
    22
from ambuild2 import database
dvander@65
    23
dvander@125
    24
class CppNodes(object):
dvander@125
    25
  def __init__(self, output, debug_outputs):
dvander@125
    26
    self.binary = output
dvander@125
    27
    self.debug = debug_outputs
dvander@125
    28
dvander@65
    29
class Generator(base_gen.Generator):
dvander@236
    30
  def __init__(self, sourcePath, buildPath, originalCwd, options, args, db=None, refactoring=False):
dvander@236
    31
    super(Generator, self).__init__(sourcePath, buildPath, originalCwd, options, args)
dvander@128
    32
    self.cacheFolder = os.path.join(self.buildPath, '.ambuild2')
dvander@157
    33
    self.old_scripts_ = set()
dvander@153
    34
    self.old_folders_ = set()
dvander@159
    35
    self.old_commands_ = set()
dvander@160
    36
    self.old_groups_ = set()
danderson@175
    37
    self.rm_list_ = []
dvander@153
    38
    self.bad_outputs_ = set()
dvander@158
    39
    self.db = db
dvander@158
    40
    self.is_bootstrap = not self.db
dvander@163
    41
    self.refactoring = refactoring
dvander@65
    42
dvander@236
    43
  @classmethod
dvander@236
    44
  def FromVars(cls, vars, db, refactoring):
dvander@236
    45
    # Backwards compatibility: for an automatic reconfigure on an older build,
dvander@236
    46
    # just assume the source path is the cwd. If the AMBuildScript suddenly
dvander@236
    47
    # has decided to depend on originalCwd, then the user may have to manually
dvander@236
    48
    # run configure.py again, until we remove configure.py entirely.
dvander@236
    49
    if 'originalCwd' in vars:
dvander@236
    50
      originalCwd = vars['originalCwd']
dvander@236
    51
    else:
dvander@236
    52
      originalCwd = vars['sourcePath']
dvander@236
    53
dvander@236
    54
    gen = cls(
dvander@236
    55
      sourcePath = vars['sourcePath'],
dvander@236
    56
      buildPath = vars['buildPath'],
dvander@236
    57
      originalCwd = originalCwd,
dvander@236
    58
      options = vars['options'],
dvander@236
    59
      args = vars['args'],
dvander@236
    60
      db = db,
dvander@236
    61
      refactoring = refactoring
dvander@236
    62
    )
dvander@236
    63
    return gen
dvander@236
    64
dvander@67
    65
  def preGenerate(self):
dvander@153
    66
    if not os.path.isdir(self.cacheFolder):
dvander@153
    67
      os.mkdir(self.cacheFolder)
dvander@158
    68
    if not self.db:
dvander@225
    69
      db_path = os.path.join(self.cacheFolder, 'graph')
dvander@226
    70
      if not os.path.isfile(db_path):
dvander@226
    71
        self.db = database.CreateDatabase(db_path)
dvander@226
    72
      else:
dvander@226
    73
        self.db = database.Database(db_path)
dvander@226
    74
        self.db.connect()
dvander@153
    75
dvander@157
    76
    self.db.query_scripts(lambda id,path,stamp: self.old_scripts_.add(path))
dvander@153
    77
    self.db.query_mkdir(lambda entry: self.old_folders_.add(entry))
dvander@153
    78
    self.db.query_commands(lambda entry: self.old_commands_.add(entry))
dvander@160
    79
    self.db.query_groups(lambda entry:self.old_groups_.add(entry))
dvander@153
    80
dvander@161
    81
  def cleanup(self):
danderson@175
    82
    for path in self.rm_list_:
danderson@175
    83
      util.rm_path(path)
danderson@175
    84
dvander@161
    85
    for cmd_entry in self.old_commands_:
danderson@175
    86
      if self.refactoring:
danderson@175
    87
        util.con_err(util.ConsoleRed, 'Command removed during refactoring: \n',
danderson@175
    88
                     util.ConsoleBlue, cmd_entry.format(),
danderson@175
    89
                     util.ConsoleNormal)
danderson@175
    90
        raise Exception('Refactoring error: command removed')
dvander@161
    91
      self.db.drop_command(cmd_entry)
dvander@161
    92
dvander@161
    93
    for path in self.old_scripts_:
dvander@161
    94
      self.db.drop_script(path)
dvander@161
    95
dvander@161
    96
    for group in self.old_groups_:
dvander@161
    97
      self.db.drop_group(group)
dvander@161
    98
dvander@235
    99
    self.db.query_dead_sources(lambda e: self.db.drop_source(e))
dvander@226
   100
    self.db.query_dead_shared_outputs(lambda e: self.db.drop_output(e))
dvander@226
   101
dvander@161
   102
    class Node:
dvander@161
   103
      def __init__(self):
dvander@161
   104
        self.incoming = set()
dvander@161
   105
        self.outgoing = set()
dvander@161
   106
dvander@161
   107
    # Build a tree of dead folders.
dvander@161
   108
    tracker = {}
dvander@161
   109
    for entry in self.old_folders_:
dvander@161
   110
      if entry not in tracker:
dvander@161
   111
        tracker[entry] = Node()
dvander@161
   112
dvander@161
   113
      if entry.folder is None:
dvander@161
   114
        continue
dvander@161
   115
danderson@175
   116
      # If our parent is not a dead folder, don't create an edge. It should be
danderson@175
   117
      # impossible for a/b to be dead, a/b/c to be alive, and a/b/c/d to be
danderson@175
   118
      # dead, since a/b/c will implicitly keep a/b alive.
danderson@175
   119
      if entry.folder not in self.old_folders_:
danderson@175
   120
        continue
danderson@175
   121
dvander@161
   122
      if entry.folder not in tracker:
dvander@161
   123
        tracker[entry.folder] = Node()
dvander@161
   124
dvander@161
   125
      parent = tracker[entry.folder]
dvander@161
   126
      child = tracker[entry]
dvander@161
   127
      parent.incoming.add(entry)
dvander@161
   128
      child.outgoing.add(entry.folder)
dvander@161
   129
dvander@161
   130
    # Find the leaves. Sets start out >= 1 items. Remove them as they they
dvander@161
   131
    # are empty.
dvander@161
   132
    dead_folders = [entry for entry in self.old_folders_ if len(tracker[entry].incoming) == 0]
dvander@161
   133
    while len(dead_folders):
dvander@161
   134
      child_entry = dead_folders.pop()
dvander@161
   135
      child_node = tracker[child_entry]
dvander@161
   136
danderson@175
   137
      if self.refactoring:
danderson@175
   138
        util.con_err(util.ConsoleRed, 'Folder removed during refactoring: \n',
danderson@175
   139
                     util.ConsoleBlue, child_entry.format(),
danderson@175
   140
                     util.ConsoleNormal)
danderson@175
   141
        raise Exception('Refactoring error: command removed')
danderson@175
   142
dvander@161
   143
      self.db.drop_folder(child_entry)
dvander@161
   144
      for parent_entry in child_node.outgoing:
dvander@161
   145
        parent_node = tracker[parent_entry]
dvander@161
   146
        parent_node.incoming.remove(child_entry)
dvander@161
   147
        if not len(parent_node.incoming):
dvander@161
   148
          dead_folders.append(parent_entry)
dvander@161
   149
dvander@161
   150
  def postGenerate(self):
dvander@161
   151
    self.cleanup()
dvander@161
   152
    self.db.commit()
dvander@235
   153
    self.db.vacuum()
dvander@161
   154
    if self.is_bootstrap:
dvander@161
   155
      self.saveVars()
dvander@161
   156
      self.db.close()
dvander@161
   157
dvander@161
   158
  def saveVars(self):
dvander@161
   159
    vars = {
dvander@161
   160
      'sourcePath': self.sourcePath,
dvander@161
   161
      'buildPath': self.buildPath,
dvander@236
   162
      'originalCwd': self.originalCwd,
dvander@161
   163
      'options': self.options,
dvander@161
   164
      'args': self.args
dvander@161
   165
    }
dvander@161
   166
    with open(os.path.join(self.cacheFolder, 'vars'), 'wb') as fp:
dvander@161
   167
      util.DiskPickle(vars, fp)
dvander@161
   168
dvander@161
   169
  def getLocalFolder(self, context):
dvander@161
   170
    if type(context.localFolder_) is nodetypes.Entry or context.localFolder_ is None:
dvander@161
   171
      return context.localFolder_
dvander@161
   172
dvander@161
   173
    if len(context.buildFolder):
dvander@161
   174
      context.localFolder_ = self.generateFolder(None, context.buildFolder)
dvander@161
   175
    else:
dvander@161
   176
      context.localFolder_ = None
dvander@161
   177
    return context.localFolder_
dvander@161
   178
dvander@161
   179
  def generateFolder(self, parent, folder):
dvander@161
   180
    parent_path = ''
dvander@161
   181
    if parent:
dvander@161
   182
      parent_path = parent.path
dvander@161
   183
    path = os.path.normpath(os.path.join(parent_path, folder))
dvander@161
   184
dvander@161
   185
    if path.startswith('..'):
dvander@161
   186
      util.con_err(
dvander@161
   187
        util.ConsoleRed,
dvander@161
   188
        'Output path ',
dvander@161
   189
        util.ConsoleBlue,
dvander@161
   190
        path,
dvander@161
   191
        util.ConsoleRed,
dvander@161
   192
        ' is outside the build folder!',
dvander@161
   193
        util.ConsoleNormal
dvander@161
   194
      )
dvander@153
   195
      raise Exception('Cannot generate folders outside the build folder')
dvander@153
   196
dvander@161
   197
    # Quick check. If this folder is not in our old folder list, and it's in
danderson@175
   198
    # the DB, then we already have an entry for it that has already negotiated
danderson@175
   199
    # its parent paths.
dvander@161
   200
    old_entry = self.db.query_path(path)
danderson@175
   201
    if old_entry and self.isValidFolderEntry(old_entry):
danderson@175
   202
      return old_entry
dvander@161
   203
dvander@159
   204
    components = []
dvander@161
   205
    while folder:
dvander@161
   206
      folder, name = os.path.split(folder)
dvander@159
   207
      if not name:
dvander@159
   208
        break
dvander@159
   209
      components.append(name)
dvander@159
   210
dvander@161
   211
    path = parent_path
dvander@159
   212
    while len(components):
dvander@159
   213
      name = components.pop()
dvander@159
   214
      path = os.path.join(path, name)
dvander@159
   215
      entry = self.db.query_path(path)
dvander@159
   216
      if not entry:
dvander@164
   217
        if self.refactoring:
dvander@164
   218
          util.con_err(util.ConsoleRed, 'New folder introduced: ',
dvander@164
   219
                       util.ConsoleBlue, path,
dvander@164
   220
                       util.ConsoleNormal)
danderson@175
   221
          raise Exception('Refactoring error: new folder')
dvander@161
   222
        entry = self.db.add_folder(parent, path)
dvander@226
   223
      elif entry.type == nodetypes.Output or entry.type == nodetypes.SharedOutput:
dvander@226
   224
        if entry.type == nodetypes.Output:
dvander@226
   225
          cmd_entries = [self.db.query_command_of(entry)]
dvander@226
   226
        elif entry.type == nodetypes.SharedOutput:
dvander@226
   227
          cmd_entries = self.db.query_shared_commands_of(entry)
dvander@226
   228
dvander@226
   229
        for cmd_entry in cmd_entries:
dvander@226
   230
          if cmd_entry not in self.old_commands_:
dvander@226
   231
            util.con_err(util.ConsoleRed, 'Folder has the same path as an output file generated by:\n',
dvander@226
   232
                         util.ConsoleBlue, cmd_entry.format(),
dvander@226
   233
                         util.ConsoleNormal)
dvander@226
   234
            raise Exception('Output has been duplicated: {0}'.format(entry.path))
danderson@175
   235
danderson@175
   236
        if self.refactoring:
danderson@175
   237
          util.con_err(util.ConsoleRed, 'Path "',
danderson@175
   238
                       util.ConsoleBlue, entry.path,
danderson@175
   239
                       util.ConsoleRed, '" has changed from a file to a folder.',
danderson@175
   240
                       util.ConsoleNormal)
danderson@175
   241
          raise Exception('Refactoring error: path changed from file to folder')
danderson@175
   242
danderson@175
   243
        self.rm_list_.append(entry.path)
dvander@226
   244
        self.db.change_to_folder(entry)
danderson@175
   245
      elif entry.type == nodetypes.Mkdir:
dvander@161
   246
        # We let the same folder be generated twice, so use discard, not remove.
dvander@159
   247
        self.old_folders_.discard(entry)
danderson@175
   248
      else:
dvander@190
   249
        util.con_err(util.ConsoleRed, 'Folder has the same node signature as: ',
dvander@190
   250
                     util.ConsoleBlue, entry.format(),
dvander@190
   251
                     util.ConsoleNormal)
danderson@175
   252
        raise Exception('Output has been duplicated: {0}'.format(entry.path))
dvander@161
   253
dvander@159
   254
      parent = entry
dvander@153
   255
dvander@153
   256
    return entry
dvander@153
   257
dvander@162
   258
  def isValidFolderEntry(self, folder_entry):
dvander@162
   259
    # If it's a mkdir and it's created, we're done.
danderson@175
   260
    return folder_entry.type == nodetypes.Mkdir and folder_entry not in self.old_folders_
dvander@162
   261
dvander@162
   262
  def validateOutputFolder(self, path):
dvander@162
   263
    # Empty path is the root folder, which is null.
dvander@162
   264
    if not len(path):
dvander@162
   265
      return None
dvander@162
   266
dvander@162
   267
    # The folder must already exist.
dvander@162
   268
    folder_entry = self.db.query_path(path)
dvander@162
   269
    if not folder_entry:
dvander@162
   270
      util.con_err(util.ConsoleRed, 'Path "',
dvander@162
   271
                   util.ConsoleBlue, path,
dvander@162
   272
                   util.ConsoleRed, '" specifies a folder that does not exist.',
dvander@162
   273
                   util.ConsoleNormal)
dvander@162
   274
      raise Exception('path specifies a folder that does not exist')
dvander@162
   275
dvander@162
   276
    if self.isValidFolderEntry(folder_entry):
dvander@162
   277
      return folder_entry
dvander@162
   278
dvander@162
   279
    # If it's a folder or an output, we can give a better error message.
dvander@162
   280
    if folder_entry.type == nodetypes.Output or folder_entry.type == nodetypes.Mkdir:
dvander@162
   281
      util.con_err(util.ConsoleRed, 'Folder "',
dvander@162
   282
                   util.ConsoleBlue, folder_entry.path,
dvander@162
   283
                   util.ConsoleRed, '" was never created.',
dvander@162
   284
                   util.ConsoleNormal)
dvander@162
   285
      raise Exception('path {0} was never created', folder_entry.path)
dvander@162
   286
dvander@162
   287
    util.con_err(util.ConsoleRed, 'Attempted to use node "',
dvander@162
   288
                 util.ConsoleBlue, folder_entry.format(),
dvander@162
   289
                 util.ConsoleRed, '" as a path component.',
dvander@162
   290
                 util.ConsoleNormal)
dvander@162
   291
    raise Exception('illegal path component')
dvander@162
   292
dvander@226
   293
  def parseOutput(self, cwd_entry, path, kind):
dvander@162
   294
    if path[-1] == os.sep or path[-1] == os.altsep or path == '.' or path == '':
dvander@162
   295
      util.con_err(util.ConsoleRed, 'Path "',
dvander@162
   296
                   util.ConsoleBlue, path,
dvander@162
   297
                   util.ConsoleRed, '" looks like a folder; a folder was not expected.',
dvander@162
   298
                   util.ConsoleNormal)
dvander@162
   299
      raise Exception('Expected folder, but path has a trailing slash')
dvander@162
   300
dvander@177
   301
    path = os.path.normpath(path)
dvander@177
   302
dvander@162
   303
    path, name = os.path.split(path)
dvander@162
   304
    path = nodetypes.combine(cwd_entry, path)
dvander@162
   305
dvander@162
   306
    # We should have caught a case like 'x/' earlier.
dvander@162
   307
    assert len(name)
dvander@162
   308
dvander@177
   309
    # If we resolved that there is no prefix path, then take this to mean the
dvander@177
   310
    # root folder.
dvander@177
   311
    if path:
dvander@177
   312
      folder_entry = self.validateOutputFolder(path)
dvander@177
   313
      output_path = os.path.join(path, name)
dvander@177
   314
    else:
dvander@177
   315
      folder_entry = None
dvander@177
   316
      output_path = name
dvander@162
   317
dvander@162
   318
    entry = self.db.query_path(output_path)
dvander@162
   319
    if not entry:
dvander@163
   320
      if self.refactoring:
danderson@175
   321
        util.con_err(util.ConsoleRed, 'New output file introduced: ',
danderson@175
   322
                     util.ConsoleBlue, output_path,
dvander@163
   323
                     util.ConsoleNormal)
dvander@163
   324
        raise Exception('Refactoring error')
dvander@226
   325
      return self.db.add_output(folder_entry, output_path, kind)
dvander@162
   326
dvander@226
   327
    if entry.type == kind:
dvander@162
   328
      return entry
dvander@162
   329
dvander@226
   330
    if entry.type == nodetypes.Mkdir:
dvander@226
   331
      if entry not in self.old_folders_:
dvander@226
   332
        util.con_err(util.ConsoleRed, 'A folder is being re-used as an output file: "',
dvander@226
   333
                     util.ConsoleBlue, entry.path,
dvander@226
   334
                     util.ConsoleRed, '"',
dvander@226
   335
                     util.ConsoleNormal)
dvander@226
   336
        raise Exception('Attempted to re-use a folder as generated file')
dvander@226
   337
dvander@226
   338
      if self.refactoring:
dvander@226
   339
        util.con_err(util.ConsoleRed, 'A generated folder has changed to a generated file: ',
dvander@226
   340
                     util.ConsoleBlue, entry.path,
dvander@226
   341
                     util.ConsoleNormal)
dvander@226
   342
        raise Exception('Refactoring error')
dvander@226
   343
dvander@226
   344
      # We keep the node in old_folders_. This should be okay, since we've
dvander@226
   345
      # changed the type to Output now. This way we can stick to one folder
dvander@226
   346
      # deletion routine, since it's fairly complicated.
dvander@226
   347
    elif entry.type == nodetypes.Output:
dvander@226
   348
      # If we're asking for a shared output, make sure we can reuse this one.
dvander@226
   349
      input_cmd = self.db.query_command_of(entry)
dvander@226
   350
      if input_cmd and input_cmd not in self.old_commands_:
dvander@226
   351
        util.con_err(util.ConsoleRed, 'First defined with command: ', input_cmd.format(), util.ConsoleNormal)
dvander@226
   352
        raise Exception('Existing output cannot be a shared output: {0}'.format(entry.path))
dvander@226
   353
dvander@226
   354
      if self.refactoring:
dvander@226
   355
        util.con_err(util.ConsoleRed, 'An output has changed to a shared output: ',
dvander@226
   356
                     util.ConsoleBlue, entry.path,
dvander@226
   357
                     util.ConsoleNormal)
dvander@226
   358
        raise Exception('Refactoring error')
dvander@226
   359
    elif entry.type == nodetypes.SharedOutput:
dvander@226
   360
      input_cmds = self.db.query_shared_commands_of(entry)
dvander@226
   361
      for input_cmd in input_cmds:
dvander@226
   362
        if input_cmd not in self.old_commands_:
dvander@226
   363
          util.con_err(util.ConsoleRed, 'A shared output cannot be specified as an normal output.',
dvander@226
   364
                       util.ConsoleNormal)
dvander@226
   365
          raise Exception('Existing shared output cannot be a normal output: {0}'.format(entry.path))
dvander@226
   366
dvander@226
   367
      if self.refactoring:
dvander@226
   368
        util.con_err(util.ConsoleRed, 'A shared output has changed to a normal output: ',
dvander@226
   369
                     util.ConsoleBlue, entry.path,
dvander@226
   370
                     util.ConsoleNormal)
dvander@226
   371
        raise Exception('Refactoring error')
dvander@226
   372
    else:
danderson@175
   373
      util.con_err(util.ConsoleRed, 'An existing node has been specified as an output file: "',
dvander@162
   374
                   util.ConsoleBlue, entry.format(),
dvander@162
   375
                   util.ConsoleRed, '"',
dvander@162
   376
                   util.ConsoleNormal)
dvander@162
   377
      raise Exception('Attempted to re-use an incompatible node as an output')
dvander@162
   378
dvander@226
   379
    self.db.change_to_output(entry, kind)
dvander@162
   380
    return entry
dvander@162
   381
dvander@176
   382
  def parseInput(self, context, source):
dvander@163
   383
    if util.IsString(source):
dvander@176
   384
      if not os.path.isabs(source):
dvander@176
   385
        source = os.path.join(context.currentSourcePath, source)
dvander@177
   386
      source = os.path.normpath(source)
dvander@176
   387
dvander@162
   388
      entry = self.db.query_path(source)
dvander@162
   389
      if not entry:
dvander@162
   390
        return self.db.add_source(source)
dvander@162
   391
dvander@176
   392
      # Otherwise, we have to valid the node.
dvander@162
   393
      source = entry
dvander@162
   394
dvander@162
   395
    if source.type == nodetypes.Source or source.type == nodetypes.Output:
dvander@162
   396
      return source
dvander@162
   397
dvander@162
   398
    if source.type == nodetypes.Mkdir:
dvander@162
   399
      if source not in self.bad_outputs_:
dvander@162
   400
        util.con_err(util.ConsoleRed, 'Tried to use folder path ',
dvander@162
   401
                     util.ConsoleBlue, source.path,
dvander@162
   402
                     util.ConsoleRed, ' as a file path.',
dvander@162
   403
                     util.ConsoleNormal)
dvander@162
   404
        raise Exception('Tried to use folder path as a file path')
dvander@162
   405
dvander@162
   406
    util.con_err(util.ConsoleRed, 'Tried to use incompatible node "',
dvander@162
   407
                 util.ConsoleBlue, source.format(),
dvander@162
   408
                 util.ConsoleRed, '" as a file path.',
dvander@162
   409
                 util.ConsoleNormal)
dvander@162
   410
    raise Exception('Tried to use non-file node as a file path')
dvander@162
   411
dvander@226
   412
  def addCommand(self, context, node_type, folder, data, inputs, outputs,
dvander@226
   413
                 weak_inputs=[], shared_outputs=[]):
dvander@167
   414
    assert not folder or isinstance(folder, nodetypes.Entry)
dvander@153
   415
dvander@153
   416
    # Build the set of weak links.
dvander@153
   417
    weak_links = set()
dvander@153
   418
    for weak_input in weak_inputs:
dvander@153
   419
      assert type(weak_input) is nodetypes.Entry
dvander@153
   420
      assert weak_input.type != nodetypes.Source
dvander@153
   421
      weak_links.add(weak_input)
dvander@153
   422
dvander@153
   423
    # Build the set of strong links.
dvander@153
   424
    strong_links = set()
dvander@153
   425
    for strong_input in inputs:
dvander@176
   426
      strong_input = self.parseInput(context, strong_input)
dvander@153
   427
      strong_links.add(strong_input)
dvander@153
   428
dvander@226
   429
    # Build the list of outputs.
dvander@153
   430
    cmd_entry = None
dvander@153
   431
    output_nodes = []
dvander@153
   432
    for output in outputs:
dvander@226
   433
      output_node = self.parseOutput(folder, output, nodetypes.Output)
dvander@162
   434
      output_nodes.append(output_node)
dvander@153
   435
danderson@175
   436
      input_entry = self.db.query_command_of(output_node)
danderson@175
   437
      if not input_entry:
dvander@162
   438
        continue
dvander@153
   439
dvander@162
   440
      # Make sure this output won't be duplicated.
dvander@162
   441
      if input_entry not in self.old_commands_:
dvander@162
   442
        util.con_err(util.ConsoleRed, 'Command: ', input_entry.format(), util.ConsoleNormal)
dvander@162
   443
        raise Exception('Output has been duplicated: {0}'.format(output_node.path))
dvander@153
   444
dvander@162
   445
      if not cmd_entry:
dvander@162
   446
        cmd_entry = input_entry
dvander@153
   447
    # end for
dvander@153
   448
dvander@226
   449
    # Build the list of shared outputs.
dvander@226
   450
    shared_output_nodes = []
dvander@226
   451
    for shared_output in shared_outputs:
dvander@226
   452
      shared_output_node = self.parseOutput(folder, shared_output, nodetypes.SharedOutput)
dvander@226
   453
      shared_output_nodes.append(shared_output_node)
dvander@226
   454
dvander@226
   455
    output_links = set(output_nodes)
dvander@226
   456
    shared_links = set(shared_output_nodes)
dvander@226
   457
dvander@226
   458
    # There should be no duplicates in either output list. These error messages
dvander@226
   459
    # could be better.
dvander@226
   460
    if len(output_nodes) > len(output_links):
dvander@226
   461
      util.con_err(util.ConsoleRed, 'The output list contains duplicate files.',
dvander@226
   462
                   util.ConsoleNormal)
dvander@226
   463
      raise Exception('Shared output list contains duplicate files.')
dvander@226
   464
    if len(shared_output_nodes) > len(shared_links):
dvander@226
   465
      util.con_err(util.ConsoleRed, 'The output list contains duplicate files.',
dvander@226
   466
                   util.ConsoleNormal)
dvander@226
   467
      raise Exception('Shared output list contains duplicate files.')
dvander@226
   468
dvander@226
   469
    # The intersection of output_links and shared_links should be the empty set.
dvander@226
   470
    duplicates = output_links.intersection(shared_links)
dvander@226
   471
    if len(duplicates):
dvander@226
   472
      bad_entry = duplicates.pop()
dvander@226
   473
      util.con_err(util.ConsoleRed, 'An output has been duplicated as a shared output: ',
dvander@226
   474
                   util.ConsoleBlue, bad_entry.path,
dvander@226
   475
                   util.ConsoleNormal)
dvander@226
   476
      raise Exception('An output has been duplicated as a shared output.')
dvander@153
   477
dvander@153
   478
    if cmd_entry:
dvander@153
   479
      # Update the entry in the database.
dvander@163
   480
      self.db.update_command(cmd_entry, node_type, folder, data, self.refactoring)
dvander@153
   481
dvander@153
   482
      # Disconnect any outputs that are no longer connected to this output.
dvander@226
   483
      # It's okay to use output_links since there should never be duplicate
dvander@153
   484
      # outputs.
dvander@153
   485
      for outgoing in self.db.query_strong_outgoing(cmd_entry):
dvander@226
   486
        if outgoing not in output_links:
dvander@153
   487
          self.db.drop_strong_edge(cmd_entry, outgoing)
dvander@185
   488
          self.db.drop_output(outgoing)
dvander@153
   489
        else:
dvander@226
   490
          output_links.remove(outgoing)
dvander@226
   491
dvander@226
   492
      # Do the same for shared outputs. Since there is a many:1 relationship,
dvander@226
   493
      # we can't drop shared outputs here. We save that for a cleanup step.
dvander@226
   494
      for outgoing in self.db.query_shared_outputs(cmd_entry):
dvander@226
   495
        if outgoing not in shared_links:
dvander@226
   496
          self.db.drop_shared_output_edge(cmd_entry, outgoing)
dvander@226
   497
        else:
dvander@226
   498
          shared_links.remove(outgoing)
dvander@153
   499
dvander@153
   500
      # Remove us from the list of commands to delete.
dvander@153
   501
      self.old_commands_.remove(cmd_entry)
dvander@153
   502
    else:
dvander@226
   503
      # Note that if there are no outputs, we will always add a new command,
dvander@226
   504
      # and the old (identical) command will be deleted.
dvander@153
   505
      cmd_entry = self.db.add_command(node_type, folder, data)
dvander@153
   506
dvander@226
   507
    # Local helper function to warn about refactoring problems.
dvander@164
   508
    def refactoring_error(node):
dvander@163
   509
      util.con_err(util.ConsoleRed, 'New input introduced: ',
dvander@163
   510
                   util.ConsoleBlue, node.path + '\n',
dvander@163
   511
                   util.ConsoleRed, 'Command: ',
dvander@163
   512
                   util.ConsoleBlue, cmd_entry.format(),
dvander@163
   513
                   util.ConsoleNormal)
dvander@163
   514
      raise Exception('Refactoring error: new input introduced')
dvander@163
   515
dvander@226
   516
    if len(output_links) and self.refactoring:
dvander@226
   517
      refactoring_error(output_links.pop())
dvander@226
   518
    if len(shared_links) and self.refactoring:
dvander@226
   519
      refactoring_error(shared_links.pop())
dvander@164
   520
dvander@153
   521
    # Connect each output.
dvander@226
   522
    for output_node in output_links:
dvander@153
   523
      self.db.add_strong_edge(cmd_entry, output_node)
dvander@226
   524
    for shared_output_node in shared_links:
dvander@226
   525
      self.db.add_shared_output_edge(cmd_entry, shared_output_node)
dvander@153
   526
dvander@153
   527
    # Connect/disconnect strong inputs.
dvander@153
   528
    strong_inputs = self.db.query_strong_inputs(cmd_entry)
dvander@153
   529
    strong_added = strong_links - strong_inputs
dvander@153
   530
    strong_removed = strong_inputs - strong_links 
dvander@163
   531
dvander@164
   532
    if len(strong_added) and self.refactoring:
dvander@164
   533
      refactoring_error(strong_added.pop())
dvander@164
   534
dvander@153
   535
    for strong_input in strong_added:
dvander@153
   536
      self.db.add_strong_edge(strong_input, cmd_entry)
dvander@153
   537
    for strong_input in strong_removed:
dvander@153
   538
      self.db.drop_strong_edge(strong_input, cmd_entry)
dvander@153
   539
dvander@153
   540
    # Connect/disconnect weak inputs.
dvander@153
   541
    weak_inputs = self.db.query_weak_inputs(cmd_entry)
dvander@153
   542
    weak_added = weak_links - weak_inputs
dvander@153
   543
    weak_removed = weak_inputs - weak_links 
dvander@163
   544
dvander@164
   545
    if len(weak_added) and self.refactoring:
dvander@164
   546
      refactoring_error(weak_added.pop())
dvander@164
   547
dvander@153
   548
    for weak_input in weak_added:
dvander@153
   549
      self.db.add_weak_edge(weak_input, cmd_entry)
dvander@153
   550
    for weak_input in weak_removed:
dvander@153
   551
      self.db.drop_weak_edge(weak_input, cmd_entry)
dvander@153
   552
dvander@153
   553
    # If we got new outputs or inputs, we need to re-run the command.
dvander@226
   554
    changed = len(output_links) + len(strong_added) + len(weak_added)
dvander@153
   555
    if changed and not cmd_entry.dirty:
dvander@153
   556
      self.db.mark_dirty(cmd_entry)
dvander@153
   557
dvander@153
   558
    return cmd_entry, output_nodes
dvander@65
   559
dvander@163
   560
  def parseCxxDeps(self, context, binary, inputs, items):
dvander@163
   561
    for val in items:
dvander@163
   562
      if util.IsString(val):
dvander@163
   563
        continue
dvander@163
   564
dvander@210
   565
      if type(val) is nodetypes.Entry:
dvander@210
   566
        item = val
dvander@210
   567
      elif util.IsLambda(val.node):
dvander@163
   568
        item = val.node(context, binary)
dvander@186
   569
      elif val.node is None:
dvander@186
   570
        item = val.text
dvander@163
   571
      else:
dvander@163
   572
        item = val.node
dvander@163
   573
dvander@163
   574
      if type(item) is list:
dvander@163
   575
        inputs.extend(item)
dvander@163
   576
      else:
dvander@163
   577
        inputs.append(item)
dvander@163
   578
dvander@67
   579
  def addCxxTasks(self, cx, binary):
dvander@161
   580
    folder_node = self.generateFolder(cx.localFolder, binary.localFolder)
dvander@67
   581
dvander@163
   582
    # Find dependencies
dvander@153
   583
    inputs = []
dvander@163
   584
    self.parseCxxDeps(cx, binary, inputs, binary.compiler.linkflags)
dvander@163
   585
    self.parseCxxDeps(cx, binary, inputs, binary.compiler.postlink)
dvander@114
   586
dvander@190
   587
    for objfile in binary.objects:
dvander@67
   588
      cxxData = {
dvander@67
   589
        'argv': objfile.argv,
dvander@67
   590
        'type': binary.linker.behavior
dvander@67
   591
      }
dvander@153
   592
      cxxCmd, (cxxNode,) = self.addCommand(
dvander@162
   593
        context = cx,
dvander@162
   594
        weak_inputs = binary.compiler.sourcedeps,
dvander@162
   595
        inputs = [objfile.sourceFile],
dvander@162
   596
        outputs = [objfile.outputFile],
dvander@162
   597
        node_type = nodetypes.Cxx,
dvander@162
   598
        folder = folder_node,
dvander@227
   599
        data = cxxData,
dvander@227
   600
        shared_outputs = objfile.sharedOutputs
dvander@120
   601
      )
dvander@153
   602
      inputs.append(cxxNode)
dvander@190
   603
    for rcfile in binary.resources:
dvander@190
   604
      rcData = {
dvander@190
   605
        'cl_argv': rcfile.cl_argv,
dvander@190
   606
        'rc_argv': rcfile.rc_argv,
dvander@190
   607
      }
dvander@201
   608
      rcCmd, (preprocNode, rcNode) = self.addCommand(
dvander@190
   609
        context = cx,
dvander@190
   610
        weak_inputs = binary.compiler.sourcedeps,
dvander@190
   611
        inputs = [rcfile.sourceFile],
dvander@199
   612
        outputs = [rcfile.preprocFile, rcfile.outputFile],
dvander@190
   613
        node_type = nodetypes.Rc,
dvander@190
   614
        folder = folder_node,
dvander@190
   615
        data = rcData
dvander@190
   616
      )
dvander@190
   617
      inputs.append(rcNode)
dvander@67
   618
danderson@220
   619
    output_file, debug_file = binary.link(
dvander@162
   620
      context = cx,
dvander@162
   621
      folder = folder_node,
danderson@220
   622
      inputs = inputs
dvander@153
   623
    )
dvander@153
   624
danderson@220
   625
    return CppNodes(output_file, debug_file)
dvander@118
   626
dvander@162
   627
  def addFileOp(self, cmd, context, source, output_path):
dvander@162
   628
    # Try to detect if our output_path is actually a folder, via trailing
dvander@162
   629
    # slash or '.'/'' indicating the context folder.
dvander@162
   630
    detected_folder = None
dvander@163
   631
    if util.IsString(output_path):
dvander@162
   632
      if output_path[-1] == os.sep or output_path[-1] == os.altsep:
dvander@162
   633
        detected_folder = os.path.join(context.buildFolder, os.path.normpath(output_path))
dvander@162
   634
      elif output_path == '.' or output_path == '':
dvander@168
   635
        detected_folder = context.buildFolder
dvander@168
   636
dvander@168
   637
      # Since we're building something relative to the context folder, ensure
dvander@168
   638
      # that the context folder exists.
dvander@168
   639
      self.getLocalFolder(context)
dvander@162
   640
    else:
dvander@162
   641
      assert output_path.type != nodetypes.Source
dvander@163
   642
      local_path = os.path.relpath(output_path.path, context.buildFolder)
dvander@163
   643
      detected_folder = os.path.join(context.buildFolder, local_path)
dvander@163
   644
      detected_folder = os.path.normpath(detected_folder)
dvander@162
   645
dvander@176
   646
    source_entry = self.parseInput(context, source)
dvander@159
   647
dvander@162
   648
    # This is similar to a "cp a b/", so we append to get "b/a" as the path.
dvander@162
   649
    if detected_folder is not None:
dvander@168
   650
      base, output_path = os.path.split(source_entry.path)
dvander@168
   651
      assert len(output_path)
dvander@168
   652
dvander@168
   653
      output_folder = detected_folder
dvander@168
   654
    else:
dvander@192
   655
      output_folder = context.buildFolder
dvander@168
   656
dvander@176
   657
    output_path = nodetypes.combine(output_folder, output_path)
dvander@159
   658
dvander@168
   659
    # For copy operations, it's okay to use the path from the current folder.
dvander@193
   660
    # However, when performing symlinks, we always want an absolute path.
dvander@168
   661
    if cmd == nodetypes.Symlink:
dvander@193
   662
      if source_entry.type == nodetypes.Source:
dvander@193
   663
        source_path = source_entry.path
dvander@193
   664
      else:
dvander@193
   665
        source_path = os.path.join(context.buildPath, source_entry.path)
dvander@163
   666
    else:
dvander@168
   667
      source_path = source_entry.path
dvander@168
   668
dvander@162
   669
    # For clarity of spew, we always execute file operations in the root of
dvander@162
   670
    # the build folder. This means that no matter what context we're in,
dvander@162
   671
    # we can use absolute-ish folders and get away with it.
dvander@162
   672
    return self.addCommand(
dvander@162
   673
      context = context,
dvander@162
   674
      node_type = cmd,
dvander@162
   675
      folder = None,
dvander@168
   676
      data = (source_path, output_path),
dvander@162
   677
      inputs = [source_entry],
dvander@162
   678
      outputs = [output_path]
dvander@162
   679
    )
dvander@153
   680
dvander@157
   681
  def addSource(self, context, source_path):
dvander@153
   682
    return self.graph.addSource(source_path)
dvander@153
   683
dvander@161
   684
  def addCopy(self, context, source, output_path):
dvander@161
   685
    return self.addFileOp(nodetypes.Copy, context, source, output_path)
dvander@161
   686
dvander@157
   687
  def addSymlink(self, context, source, output_path):
dvander@153
   688
    if util.IsWindows():
dvander@153
   689
      # Windows pre-Vista does not support symlinks. Windows Vista+ supports
dvander@153
   690
      # symlinks via mklink, but it's Administrator-only by default.
dvander@161
   691
      return self.addFileOp(nodetypes.Copy, context, source, output_path)
dvander@161
   692
    return self.addFileOp(nodetypes.Symlink, context, source, output_path)
dvander@153
   693
dvander@157
   694
  def addFolder(self, context, folder):
dvander@161
   695
    return self.generateFolder(context.localFolder, folder)
dvander@153
   696
danderson@219
   697
  def addShellCommand(self,
danderson@219
   698
                      context,
danderson@219
   699
                      inputs,
danderson@219
   700
                      argv,
danderson@219
   701
                      outputs,
danderson@219
   702
                      folder=-1,
danderson@219
   703
                      dep_type=None,
dvander@226
   704
                      weak_inputs=[],
dvander@226
   705
                      shared_outputs=[]):
dvander@163
   706
    if folder is -1:
dvander@163
   707
      folder = context.localFolder
dvander@153
   708
danderson@218
   709
    if dep_type is None:
danderson@218
   710
      node_type = nodetypes.Command
danderson@218
   711
      data = argv
danderson@218
   712
    else:
danderson@218
   713
      node_type = nodetypes.Cxx
danderson@218
   714
      if dep_type not in ['gcc', 'msvc', 'sun']:
danderson@218
   715
        util.con_err(util.ConsoleRed, 'Invalid dependency spew type: ',
danderson@218
   716
                     util.ConsoleBlue, dep_type,
danderson@218
   717
                     util.ConsoleNormal)
danderson@218
   718
        raise Exception('Invalid dependency spew type')
danderson@218
   719
      data = {
danderson@218
   720
        'type': dep_type,
danderson@218
   721
        'argv': argv,
danderson@218
   722
      }
danderson@218
   723
dvander@160
   724
    return self.addCommand(
dvander@163
   725
      context = context,
danderson@218
   726
      node_type = node_type,
dvander@163
   727
      folder = folder,
danderson@218
   728
      data = data,
dvander@163
   729
      inputs = inputs,
danderson@219
   730
      outputs = outputs,
dvander@226
   731
      weak_inputs = weak_inputs,
dvander@226
   732
      shared_outputs = shared_outputs
dvander@160
   733
    )
dvander@157
   734
dvander@157
   735
  def addConfigureFile(self, context, path):
dvander@235
   736
    if not os.path.isabs(path):
dvander@235
   737
      path = os.path.join(context.currentSourcePath, path)
dvander@235
   738
    path = os.path.normpath(path)
dvander@160
   739
dvander@235
   740
    self.old_scripts_.discard(path)
dvander@235
   741
    self.db.add_or_update_script(path)
dvander@194
   742