Clean and vacuum unused source nodes after reconfiguring; also don't add nodes for configure files.
1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
3 # This file is part of AMBuild.
5 # AMBuild is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # AMBuild is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with AMBuild. If not, see <http://www.gnu.org/licenses/>.
20 from ambuild2 import util
21 from ambuild2 import nodetypes
22 from ambuild2.nodetypes import Entry
25 GroupPrefix = '//group/./'
27 def CreateDatabase(path):
28 cn = sqlite3.connect(path)
30 "create table if not exists nodes( \
31 id integer primary key autoincrement, \
32 type varchar(4) not null, \
33 stamp real not null default 0.0, \
34 dirty int not null default 0, \
40 # The edge table stores links that are specified by the build scripts;
41 # this table is essentially immutable (except for reconfigures).
42 "create table if not exists edges( \
43 outgoing int not null, \
44 incoming int not null, \
45 unique (outgoing, incoming) \
48 # The weak edge table stores links that are specified by build scripts,
49 # but only to enforce ordering. They do not propagate damage or updates.
50 "create table if not exists weak_edges( \
51 outgoing int not null, \
52 incoming int not null, \
53 unique (outgoing, incoming) \
56 # The dynamic edge table stores edges that are discovered as a result of
57 # executing a command; for example, a |cp *| or C++ #includes.
58 "create table if not exists dynamic_edges( \
59 outgoing int not null, \
60 incoming int not null, \
61 unique (outgoing, incoming) \
64 # List of nodes which trigger a reconfigure.
65 "create table if not exists reconfigure( \
66 stamp real not null default 0.0, \
70 # Extra key/values we might randomly want.
71 "create table if not exists vars( \
72 key varchar(255) primary key not null, \
76 "insert into vars (key, val) values ('db_version', '3')",
78 "create index if not exists outgoing_edge on edges(outgoing)",
79 "create index if not exists incoming_edge on edges(incoming)",
80 "create index if not exists weak_outgoing_edge on weak_edges(outgoing)",
81 "create index if not exists weak_incoming_edge on weak_edges(incoming)",
82 "create index if not exists dyn_outgoing_edge on dynamic_edges(outgoing)",
83 "create index if not exists dyn_incoming_edge on dynamic_edges(incoming)",
85 # The shared output table.
86 "create table if not exists shared_outputs( \
87 outgoing int not null, \
88 incoming int not null, \
89 unique (outgoing, incoming) \
91 "create index if not exists sho_outgoing_edge on shared_outputs(outgoing)",
92 "create index if not exists sho_incoming_edge on shared_outputs(incoming)",
102 class Database(object):
103 def __init__(self, path):
106 self.node_cache_ = {}
107 self.path_cache_ = {}
111 self.cn = sqlite3.connect(self.path)
112 self.cn.execute("PRAGMA journal_mode = WAL;")
124 def __exit__(self, type, value, traceback):
130 def flush_caches(self):
131 self.node_cache_ = {}
132 self.path_cache_ = {}
134 def check_upgrade(self):
136 query = "select val from vars where key = 'db_version'"
137 cursor = self.cn.execute(query)
138 row = cursor.fetchone()
140 raise Exception('Database seems to be misconfigured - cannot read version')
141 version = int(row[0])
146 if version == latest_version:
148 if version > latest_version:
149 raise Exception('Your database version is too new!')
153 'Note: upgrading database from version {0} to {1}'.format(version, latest_version),
158 version = self.upgrade_to_v2()
161 version = self.upgrade_to_v3()
163 def upgrade_to_v2(self):
165 "create table if not exists vars( \
166 key varchar(255) primary key not null, \
170 "drop table if exists nodestmp",
171 "drop table if exists nodesold",
173 "create table nodestmp( \
174 id integer primary key autoincrement, \
175 type varchar(4) not null, \
176 stamp real not null default 0.0, \
177 dirty int not null default 0, \
183 for query in queries:
184 self.cn.execute(query)
186 # Migrate all rows in nodes to nodestmp.
188 select id, type, stamp, dirty, path, folder, data
194 (id, type, stamp, dirty, path, folder, data)
196 (?, ?, ?, ?, ?, ?, ?)
198 for row in self.cn.execute(query):
199 self.cn.execute(ins_query, row)
201 self.cn.execute("alter table nodes rename to nodesold")
202 self.cn.execute("alter table nodestmp rename to nodes")
203 self.cn.execute("drop table nodesold")
204 self.cn.execute("insert or replace into vars (key, val) values ('db_version', ?)", (2,))
208 def upgrade_to_v3(self):
210 "create table if not exists shared_outputs( \
211 outgoing int not null, \
212 incoming int not null, \
213 unique (outgoing, incoming) \
215 "create index if not exists sho_outgoing_edge on shared_outputs(outgoing)",
216 "create index if not exists sho_incoming_edge on shared_outputs(incoming)",
218 for query in queries:
219 self.cn.execute(query)
220 self.cn.execute("insert or replace into vars (key, val) values ('db_version', ?)", (3,))
224 def add_folder(self, parent, path):
225 assert path not in self.path_cache_
226 assert not os.path.isabs(path)
227 assert os.path.normpath(path) == path
229 return self.add_file(nodetypes.Mkdir, path, parent)
231 def add_output(self, folder_entry, path, kind = nodetypes.Output):
232 assert path not in self.path_cache_
233 assert not os.path.isabs(path)
234 assert not folder_entry or os.path.split(path)[0] == folder_entry.path
235 assert kind == nodetypes.Output or kind == nodetypes.SharedOutput
237 return self.add_file(kind, path, folder_entry)
239 def find_or_add_source(self, path):
240 node = self.query_path(path)
242 assert node.type == nodetypes.Source
245 return self.add_source(path)
247 def add_source(self, path):
248 assert path not in self.path_cache_
249 assert os.path.isabs(path)
251 return self.add_file(nodetypes.Source, path)
253 def add_file(self, type, path, folder_entry = None):
255 folder_id = folder_entry.id
259 query = "insert into nodes (type, path, folder) values (?, ?, ?)"
261 cursor = self.cn.execute(query, (type, path, folder_id))
262 row = (type, 0, 1, path, folder_entry, None)
263 return self.import_node(
268 def find_group(self, name):
269 path = GroupPrefix + name
270 return self.query_path(path)
272 def add_group(self, name):
273 path = GroupPrefix + name
274 return self.add_file(nodetypes.Group, path, False)
276 def update_command(self, entry, type, folder, data, refactoring):
280 blob = util.BlobType(util.CompatPickle(data))
282 if entry.type == type and entry.folder == folder and entry.blob == data:
286 util.con_err(util.ConsoleRed, 'Command changed! \n',
287 util.ConsoleRed, 'Old: ',
288 util.ConsoleBlue, entry.format(),
291 entry.folder = folder
293 util.con_err(util.ConsoleRed, 'New: ',
294 util.ConsoleBlue, entry.format(),
296 raise Exception('Refactoring error: command changed')
301 folder_id = folder.id
312 self.cn.execute(query, (type, folder_id, blob, 1, entry.id))
314 entry.folder = folder
319 def add_command(self, type, folder, data):
323 blob = util.BlobType(util.CompatPickle(data))
327 folder_id = folder.id
329 query = "insert into nodes (type, folder, data, dirty) values (?, ?, ?, ?)"
330 cursor = self.cn.execute(query, (type, folder_id, blob, 1))
341 self.node_cache_[entry.id] = entry
344 def add_weak_edge(self, from_entry, to_entry):
345 query = "insert into weak_edges (outgoing, incoming) values (?, ?)"
346 self.cn.execute(query, (to_entry.id, from_entry.id))
347 if to_entry.weak_inputs is not None:
348 to_entry.weak_inputs.add(from_entry)
350 def add_strong_edge(self, from_entry, to_entry):
351 query = "insert into edges (outgoing, incoming) values (?, ?)"
352 self.cn.execute(query, (to_entry.id, from_entry.id))
353 if to_entry.strong_inputs is not None:
354 to_entry.strong_inputs.add(from_entry)
355 if from_entry.outgoing is not None:
356 from_entry.outgoing.add(to_entry)
358 def add_dynamic_edge(self, from_entry, to_entry):
359 query = "insert into dynamic_edges (outgoing, incoming) values (?, ?)"
360 self.cn.execute(query, (to_entry.id, from_entry.id))
361 if to_entry.dynamic_inputs is not None:
362 to_entry.dynamic_inputs.add(from_entry)
363 if from_entry.outgoing is not None:
364 from_entry.outgoing.add(to_entry)
366 def add_shared_output_edge(self, from_entry, to_entry):
367 # These don't factor into the DAG in any meaningful way, so we don't
368 # cache the results or put them into edge lists.
369 query = "insert into shared_outputs (outgoing, incoming) values (?, ?)"
370 self.cn.execute(query, (to_entry.id, from_entry.id))
372 def drop_dynamic_edge(self, from_entry, to_entry):
373 query = "delete from dynamic_edges where outgoing = ? and incoming = ?"
374 self.cn.execute(query, (to_entry.id, from_entry.id))
375 if to_entry.dynamic_inputs is not None:
376 to_entry.dynamic_inputs.remove(from_entry)
377 if from_entry.outgoing is not None:
378 from_entry.outgoing.remove(to_entry)
380 def drop_weak_edge(self, from_entry, to_entry):
381 query = "delete from weak_edges where outgoing = ? and incoming = ?"
382 self.cn.execute(query, (to_entry.id, from_entry.id))
383 if to_entry.weak_inputs is not None:
384 to_entry.weak_inputs.remove(from_entry)
386 def drop_strong_edge(self, from_entry, to_entry):
387 query = "delete from edges where outgoing = ? and incoming = ?"
388 self.cn.execute(query, (to_entry.id, from_entry.id))
389 if to_entry.strong_inputs is not None:
390 to_entry.strong_inputs.remove(from_entry)
391 if from_entry.outgoing is not None:
392 from_entry.outgoing.remove(to_entry)
394 def drop_shared_output_edge(self, from_entry, to_entry):
395 query = "delete from shared_outputs where outgoing = ? and incoming = ?"
396 self.cn.execute(query, (to_entry.id, from_entry.id))
398 def query_node(self, id):
399 if id in self.node_cache_:
400 return self.node_cache_[id]
402 query = "select type, stamp, dirty, path, folder, data from nodes where id = ?"
403 cursor = self.cn.execute(query, (id,))
404 return self.import_node(id, cursor.fetchone())
406 def query_path(self, path):
407 if path in self.path_cache_:
408 return self.path_cache_[path]
411 select type, stamp, dirty, path, folder, data, id
415 cursor = self.cn.execute(query, (path,))
416 row = cursor.fetchone()
420 return self.import_node(row[6], row)
422 def import_node(self, id, row):
423 assert id not in self.node_cache_
427 elif type(row[4]) is nodetypes.Entry:
430 folder = self.query_node(row[4])
434 blob = util.Unpickle(row[5])
443 self.node_cache_[id] = node
445 assert node.path not in self.path_cache_
446 self.path_cache_[node.path] = node
449 def query_strong_outgoing(self, node):
452 query = "select outgoing from edges where incoming = ?"
453 for outgoing_id, in self.cn.execute(query, (node.id,)):
454 entry = self.query_node(outgoing_id)
458 # Find the list of shared outputs this command generates.
459 def query_shared_outputs(self, node):
462 query = "select outgoing from shared_outputs where incoming = ?"
463 for outgoing_id, in self.cn.execute(query, (node.id,)):
464 entry = self.query_node(outgoing_id)
468 def query_outgoing(self, node):
469 if node.outgoing is not None:
472 node.outgoing = set()
474 query = "select outgoing from edges where incoming = ?"
475 for outgoing_id, in self.cn.execute(query, (node.id,)):
476 entry = self.query_node(outgoing_id)
477 node.outgoing.add(entry)
479 query = "select outgoing from dynamic_edges where incoming = ?"
480 for outgoing_id, in self.cn.execute(query, (node.id,)):
481 entry = self.query_node(outgoing_id)
482 node.outgoing.add(entry)
486 def query_weak_inputs(self, node):
487 if node.weak_inputs is not None:
488 return node.weak_inputs
490 query = "select incoming from weak_edges where outgoing = ?"
491 node.weak_inputs = set()
492 for incoming_id, in self.cn.execute(query, (node.id,)):
493 incoming = self.query_node(incoming_id)
494 node.weak_inputs.add(incoming)
496 return node.weak_inputs
498 def query_strong_inputs(self, node):
499 if node.strong_inputs is not None:
500 return node.strong_inputs
502 query = "select incoming from edges where outgoing = ?"
503 node.strong_inputs = set()
504 for incoming_id, in self.cn.execute(query, (node.id,)):
505 incoming = self.query_node(incoming_id)
506 node.strong_inputs.add(incoming)
508 return node.strong_inputs
510 def query_command_of(self, node):
511 assert node.type == nodetypes.Output
512 incoming = self.query_strong_inputs(node)
514 assert len(incoming) <= 1
515 if not len(incoming):
518 cmd_entry = list(incoming)[0]
519 assert cmd_entry.isCommand()
523 def query_shared_commands_of(self, node):
524 query = "select incoming from shared_outputs where outgoing = ?"
526 for row in self.cn.execute(query, (node.id,)):
527 commands.append(self.query_node(row[0]))
530 def query_dynamic_inputs(self, node):
531 if node.dynamic_inputs is not None:
532 return node.dynamic_inputs
534 query = "select incoming from dynamic_edges where outgoing = ?"
535 node.dynamic_inputs = set()
536 for incoming_id, in self.cn.execute(query, (node.id,)):
537 incoming = self.query_node(incoming_id)
538 node.dynamic_inputs.add(incoming)
540 return node.dynamic_inputs
542 def mark_dirty(self, entry):
543 query = "update nodes set dirty = 1 where id = ?"
544 self.cn.execute(query, (entry.id,))
545 entry.dirty |= nodetypes.KnownDirty
547 def unmark_dirty(self, entry, stamp=None):
548 query = "update nodes set dirty = 0, stamp = ? where id = ?"
550 if entry.isCommand():
554 stamp = os.path.getmtime(entry.path)
556 traceback.print_exc()
559 'Could not unmark file as dirty; leaving dirty.',
564 self.cn.execute(query, (stamp, entry.id))
568 # Query all mkdir nodes.
569 def query_mkdir(self, aggregate):
571 select type, stamp, dirty, path, folder, data, id
575 for row in self.cn.execute(query):
577 node = self.import_node(id, row)
580 # Intended to be called before any nodes are imported.
581 def query_known_dirty(self, aggregate):
583 select type, stamp, dirty, path, folder, data, id
588 for row in self.cn.execute(query):
590 node = self.import_node(id, row)
593 # Query all nodes that are not dirty, but need to be checked. Intended to
594 # be called after query_dirty, and returns a mutually exclusive list.
595 def query_maybe_dirty(self, aggregate):
597 select type, stamp, dirty, path, folder, data, id
600 and (type == 'src' or type == 'out' or type == 'cpa')
602 for row in self.cn.execute(query):
604 node = self.import_node(id, row)
607 # Find the list of commands that generate this shared output.
608 def query_commands(self, aggregate):
610 select type, stamp, dirty, path, folder, data, id
612 where (type != 'src' and
618 for row in self.cn.execute(query):
620 node = self.import_node(id, row)
623 # Note that this does not update any caches. It should only be called
625 def drop_links(self, entry):
626 query = "delete from edges where incoming = ? or outgoing = ?"
627 self.cn.execute(query, (entry.id, entry.id))
629 query = "delete from dynamic_edges where incoming = ? or outgoing = ?"
630 self.cn.execute(query, (entry.id, entry.id))
632 query = "delete from weak_edges where incoming = ? or outgoing = ?"
633 self.cn.execute(query, (entry.id, entry.id))
635 def drop_entry(self, entry):
636 self.drop_links(entry)
638 query = "delete from nodes where id = ?"
639 self.cn.execute(query, (entry.id,))
641 del self.node_cache_[entry.id]
643 def drop_folder(self, entry):
644 assert entry.type in [nodetypes.Mkdir, nodetypes.Output, nodetypes.SharedOutput]
645 assert not os.path.isabs(entry.path)
647 if os.path.exists(entry.path):
649 util.ConsoleHeader, 'Removing old folder: ',
650 util.ConsoleBlue, '{0}'.format(entry.path),
655 except OSError as exn:
656 if exn.errno != errno.ENOENT:
657 util.con_err(util.ConsoleRed, 'Could not remove folder: ',
658 util.ConsoleBlue, '{0}'.format(entry.path),
659 util.ConsoleNormal, '\n',
660 util.ConsoleRed, '{0}'.format(exn),
664 cursor = self.cn.execute("select count(*) from nodes where folder = ?", (entry.id,))
665 amount = cursor.fetchone()[0]
667 util.con_err(util.ConsoleRed, 'Folder id ',
668 util.ConsoleBlue, '{0} '.format(entry.id),
669 util.ConsoleRed, 'is about to be deleted, but is still in use as a folder!',
671 raise Exception('folder still in use!')
673 # If the node transitioned to an entry, don't delete its node.
674 if entry.type == nodetypes.Mkdir:
675 self.drop_entry(entry)
677 def drop_source(self, output):
678 assert output.type == nodetypes.Source
679 self.drop_entry(output)
681 def drop_output(self, output):
682 assert output.type == nodetypes.Output or output.type == nodetypes.SharedOutput
683 util.rm_path(output.path)
684 self.drop_entry(output)
686 def drop_command(self, cmd_entry):
687 for output in self.query_outgoing(cmd_entry):
688 # Commands should never have dynamic outgoing edges, FWIW.
689 assert output.type == nodetypes.Output
690 self.drop_output(output)
691 self.cn.execute("delete from shared_outputs where incoming = ?", (cmd_entry.id,))
692 self.drop_entry(cmd_entry)
694 def add_or_update_script(self, path):
695 stamp = os.path.getmtime(path)
696 query = "insert or replace into reconfigure (path, stamp) values (?, ?)"
697 self.cn.execute(query, (path, stamp))
699 def query_scripts(self, aggregate):
700 query = "select rowid, path, stamp from reconfigure"
701 for rowid, path, stamp in self.cn.execute(query):
702 aggregate(rowid, path, stamp)
704 def query_groups(self, aggregate):
706 select type, stamp, dirty, path, folder, data, id
710 for row in self.cn.execute(query):
712 entry = self.import_node(id, row)
715 def query_dead_sources(self, aggregate):
719 and id not in (select incoming from edges)
720 and id not in (select incoming from dynamic_edges)
721 and id not in (select incoming from weak_edges)
722 """.format(nodetypes.Source)
724 for row in self.cn.execute(query):
725 entry = self.query_node(row[0])
728 def query_dead_shared_outputs(self, aggregate):
732 and id not in (select outgoing from shared_outputs)
733 """.format(nodetypes.SharedOutput)
735 for row in self.cn.execute(query):
736 entry = self.query_node(row[0])
739 def drop_script(self, path):
740 self.cn.execute("delete from reconfigure where path = ?", (path,))
742 def drop_group(self, group):
743 self.drop_entry(group)
745 def change_to_folder(self, entry):
746 assert entry.type == nodetypes.Output or entry.type == nodetypes.SharedOutput
747 self.drop_links(entry)
748 self.cn.execute("update nodes set type = 'mkd' where id = ?", (entry.id,))
749 entry.type = nodetypes.Mkdir
751 def change_to_output(self, entry, kind):
752 assert entry.type in [nodetypes.Mkdir, nodetypes.Output, nodetypes.SharedOutput]
753 self.drop_links(entry)
754 self.cn.execute("update nodes set type = ? where id = ?", (kind, entry.id))
758 self.cn.execute("vacuum")
760 def printGraph(self):
761 # Find all mkdir nodes.
762 query = "select path from nodes where type = 'mkd'"
763 for path, in self.cn.execute(query):
764 print(' : mkdir \"' + path + '\"')
765 # Find all other nodes that have no outgoing edges.
766 query = "select id from nodes where id not in (select incoming from edges) and type != 'mkd'"
767 for id, in self.cn.execute(query):
768 node = self.query_node(id)
769 self.printGraphNode(node, 0)
771 def printGraphNode(self, node, indent):
772 print((' ' * indent) + ' - ' + node.format())
774 for incoming in self.query_strong_inputs(node):
775 self.printGraphNode(incoming, indent + 1)
776 for incoming in self.query_dynamic_inputs(node):
777 self.printGraphNode(incoming, indent + 1)