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