Engine implementation of conversations: #3808

This introduces Geary.Conversations into the API, which scans a Folder and arranges the
messages into threaded conversations.
This commit is contained in:
Jim Nelson 2011-10-21 17:04:33 -07:00
parent 8ab948bce4
commit cd0b926d57
13 changed files with 731 additions and 27 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ build/
/geary /geary
/console /console
/norman /norman
/theseus

View file

@ -0,0 +1,53 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A utility class for building async-based command-line programs. Users should subclass MainAsync
* and provide an exec_async() method. It can return an int as the program's exit code. It can
* also throw an exception which is caught by MainAsync and printed to stderr.
*
* Then, in main(), create the subclasses object and call its exec() method, returning its int
* from main as the program's exit code. (If exec_async() throws an exception, main will return
* EXCEPTION_EXIT_CODE, which is 255.) Thus, main() should look something like this:
*
* int main(string[] args) {
* return new MyMainAsync(args).exec();
* }
*/
public abstract class MainAsync : Object {
public const int EXCEPTION_EXIT_CODE = 255;
public string[] args;
private MainLoop main_loop = new MainLoop();
private int ec = 0;
public MainAsync(string[] args) {
this.args = args;
}
public int exec() {
exec_async.begin(on_exec_completed);
main_loop.run();
return ec;
}
private void on_exec_completed(Object? source, AsyncResult result) {
try {
ec = exec_async.end(result);
} catch (Error err) {
stderr.printf("%s\n", err.message);
ec = EXCEPTION_EXIT_CODE;
}
main_loop.quit();
}
protected abstract async int exec_async() throws Error;
}

View file

@ -0,0 +1,73 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public interface Geary.ConversationNode : Object {
/**
* Returns the Email represented by this ConversationNode. If the Email is available
* in the remote or local Folder and as loaded into the Conversations object either by a manual
* scan or through monitoring the Folder, then that Email will be returned (with all the fields
* specified in the Conversation's constructor available). If, however, the Email was merely
* referred to by another Email in the Conversation but is unavailable, then this will return
* null (it's a placeholder).
*/
public abstract Geary.Email? get_email();
}
public abstract class Geary.Conversation : Object {
protected Conversation() {
}
/**
* Returns a ConversationNode that is the origin of the entire conversation.
*
* Returns null if the Conversation has been disconnected from the master Conversations holder
* (probably due to it being destroyed).
*/
public abstract Geary.ConversationNode? get_origin();
/**
* Returns the ConversationNode that the supplied node is in reply to (i.e. its parent).
*
* Returns null if the node has no parent (it's the origin), is not part of the conversation,
* or the conversation has been disconnected (see get_origin()).
*/
public abstract Geary.ConversationNode? get_in_reply_to(Geary.ConversationNode node);
/**
* Returns a Collection of ConversationNodes that replied to the supplied node (i.e. its
* children).
*
* Returns null if the node has no replies, is not part of the conversation, or if the
* conversation has been disconnected (see get_origin()).
*/
public abstract Gee.Collection<Geary.ConversationNode>? get_replies(Geary.ConversationNode node);
/**
* Returns all ConversationNodes in the conversation, which can then be sorted by the caller's
* own requirements.
*
*/
public virtual Gee.Collection<Geary.ConversationNode>? get_pool() {
Gee.HashSet<ConversationNode> pool = new Gee.HashSet<ConversationNode>();
gather(pool, get_origin());
return (pool.size > 0) ? pool : null;
}
private void gather(Gee.Set<ConversationNode> pool, ConversationNode? current) {
if (current == null)
return;
pool.add(current);
Gee.Collection<Geary.ConversationNode>? children = get_replies(current);
if (children != null) {
foreach (Geary.ConversationNode child in children)
gather(pool, child);
}
}
}

View file

@ -0,0 +1,457 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Conversations : Object {
/**
* These are the fields Conversations require to thread emails together. These fields will
* be retrieved irregardless of the Field parameter passed to the constructor.
*/
public const Geary.Email.Field REQUIRED_FIELDS = Geary.Email.Field.REFERENCES;
private class Node : Object, ConversationNode {
public RFC822.MessageID? node_id { get; private set; }
public Geary.Email? email { get; set; }
public RFC822.MessageID? parent_id { get; set; default = null; }
public ImplConversation? conversation { get; set; default = null; }
private Gee.Set<RFC822.MessageID>? children_ids = null;
public Node(RFC822.MessageID? node_id, Geary.Email? email) {
this.node_id = node_id;
this.email = email;
}
public void add_child(RFC822.MessageID child_id) {
if (children_ids == null) {
children_ids = new Gee.HashSet<RFC822.MessageID>(Geary.Hashable.hash_func,
Geary.Equalable.equal_func);
}
children_ids.add(child_id);
}
public Gee.Set<RFC822.MessageID>? get_children() {
return (children_ids != null) ? children_ids.read_only_view : null;
}
public Geary.Email? get_email() {
return email;
}
}
private class SingleConversationNode : Object, ConversationNode {
public Geary.Email email;
public SingleConversationNode(Geary.Email email) {
this.email = email;
}
public Geary.Email? get_email() {
return email;
}
}
private class ImplConversation : Conversation {
public weak Geary.Conversations? owner;
public RFC822.MessageID? origin;
public SingleConversationNode? single_node;
public ImplConversation(Geary.Conversations owner, ConversationNode origin_node) {
this.owner = owner;
// Rather than keep a reference to the origin node (creating a cross-reference from it
// to the conversation), keep only the Message-ID, unless it's a SingleConversationNode,
// which can't be addressed by a Message-ID (it has none)
single_node = origin_node as SingleConversationNode;
if (single_node != null) {
origin = null;
} else {
origin = ((Node) origin_node).node_id;
assert(origin != null);
}
}
public override Geary.ConversationNode? get_origin() {
if (owner == null)
return null;
if (origin == null) {
assert(single_node != null);
return single_node;
}
Node? node = owner.get_node(origin);
if (node == null)
return null;
// Since the ImplConversation holds the Message-ID of the origin, this should always
// be true. Other accessors return null instead because it's possible the caller is
// passing in ConversationNodes for another Conversation, and would rather warn than
// assert on that case.
assert(node.conversation == this);
return node;
}
public override Geary.ConversationNode? get_in_reply_to(Geary.ConversationNode cnode) {
if (owner == null)
return null;
Node? node = cnode as Node;
if (node == null)
return null;
if (node.conversation != this) {
warning("Conversation node %s not in conversation", node.node_id.to_string());
return null;
}
if (node.parent_id == null)
return null;
Node? parent = owner.get_node(node.parent_id);
assert(parent != null);
if (parent.conversation != this) {
warning("Parent of conversation node %s not in conversation", node.node_id.to_string());
return null;
}
return parent;
}
public override Gee.Collection<Geary.ConversationNode>? get_replies(
Geary.ConversationNode cnode) {
Node? node = cnode as Node;
if (node == null)
return null;
if (node.conversation != this) {
warning("Conversation node %s not in conversation", node.node_id.to_string());
return null;
}
if (owner == null)
return null;
Gee.Set<RFC822.MessageID>? children_ids = node.get_children();
if (children_ids == null || children_ids.size == 0)
return null;
Gee.Set<Geary.ConversationNode> child_nodes = new Gee.HashSet<Geary.ConversationNode>();
foreach (RFC822.MessageID child_id in children_ids) {
Node? child_node = owner.get_node(child_id);
assert(child_node != null);
// assert on this because the sub-nodes are maintained by the current node
assert(child_node.conversation == this);
child_nodes.add(child_node);
}
return child_nodes;
}
}
public Geary.Folder folder { get; private set; }
private Geary.Email.Field required_fields;
private Gee.Map<Geary.RFC822.MessageID, Node> id_map = new Gee.HashMap<Geary.RFC822.MessageID,
Node>(Geary.Hashable.hash_func, Geary.Equalable.equal_func);
private Gee.Set<ImplConversation> conversations = new Gee.HashSet<ImplConversation>();
public virtual signal void scan_started(int low, int count) {
}
public virtual signal void scan_error(Error err) {
}
public virtual signal void scan_completed() {
}
public virtual signal void conversations_added(Gee.Collection<Conversation> conversations) {
}
public virtual signal void conversation_appended(Conversation conversation,
Gee.Collection<Geary.Email> email) {
}
public virtual signal void updated_placeholders(Conversation conversation,
Gee.Collection<Geary.Email> email) {
}
public Conversations(Geary.Folder folder, Geary.Email.Field required_fields) {
this.folder = folder;
this.required_fields = required_fields | REQUIRED_FIELDS;
}
~Conversations() {
// Manually detach all the weak refs in the Conversation objects
foreach (ImplConversation conversation in conversations)
conversation.owner = null;
}
protected virtual void notify_scan_started(int low, int count) {
scan_started(low, count);
}
protected virtual void notify_scan_error(Error err) {
scan_error(err);
}
protected virtual void notify_scan_completed() {
scan_completed();
}
protected virtual void notify_conversations_added(Gee.Collection<Conversation> conversations) {
conversations_added(conversations);
}
protected virtual void notify_conversation_appended(Conversation conversation,
Gee.Collection<Geary.Email> email) {
conversation_appended(conversation, email);
}
protected virtual void notify_updated_placeholders(Conversation conversation,
Gee.Collection<Geary.Email> email) {
updated_placeholders(conversation, email);
}
public Gee.Collection<Conversation> get_conversations() {
return conversations.read_only_view;
}
public async void load_async(int low, int count, Geary.Folder.ListFlags flags,
Cancellable? cancellable) throws Error {
notify_scan_started(low, count);
try {
Gee.List<Email>? list = yield folder.list_email_async(low, count, required_fields, flags);
on_email_listed(list, null);
if (list != null)
on_email_listed(null, null);
} catch (Error err) {
on_email_listed(null, err);
}
}
public void lazy_load(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable)
throws Error {
notify_scan_started(low, count);
folder.lazy_list_email(low, count, required_fields, flags, on_email_listed, cancellable);
}
private void on_email_listed(Gee.List<Geary.Email>? emails, Error? err) {
if (err != null)
notify_scan_error(err);
// check for completion
if (emails == null) {
notify_scan_completed();
return;
}
process_email(emails);
}
private void process_email(Gee.List<Geary.Email> emails) {
// take each email and toss it into the node pool (also adding its ancestors along the way)
// and track what signalable events need to be reported: new Conversations, Emails updating
// placeholders, and Conversation objects having a new Email added to them
Gee.HashSet<Conversation> new_conversations = new Gee.HashSet<Conversation>();
Gee.MultiMap<Conversation, Geary.Email> appended_conversations = new Gee.HashMultiMap<
Conversation, Geary.Email>();
Gee.MultiMap<Conversation, Geary.Email> updated_placeholders = new Gee.HashMultiMap<
Conversation, Geary.Email>();
foreach (Geary.Email email in emails) {
// Right now, all threading is done with Message-IDs (no parsing of subject lines, etc.)
// If a message doesn't have a Message-ID, it's treated as its own conversation that
// cannot be referenced through the node pool (but is available through the
// conversations list)
if (email.message_id == null) {
debug("Email %s: No Message-ID", email.to_string());
bool added = new_conversations.add(
new ImplConversation(this, new SingleConversationNode(email)));
assert(added);
continue;
}
// this email is authoritative ... although it might be an old node that's now being
// filled in with a retrieved email, any other fields that may already be filled in will
// be replaced by this node
// see if a Node already exists for this email (it's possible that earlier processed
// emails refer to this one)
Node? node = get_node(email.message_id);
if (node != null) {
if (node.email != null) {
message("Duplicate email found while threading: %s vs. %s", node.email.to_string(),
email.to_string());
}
// even with duplicates, this new email is considered authoritative
// (Note that if the node's conversation is null then it's been added this loop,
// in which case it's not an updated placeholder but part of a new conversation)
node.email = email;
if (node.conversation != null)
updated_placeholders.set(node.conversation, email);
} else {
node = add_node(new Node(email.message_id, email));
}
// build lineage of this email
Gee.ArrayList<RFC822.MessageID> ancestors = new Gee.ArrayList<RFC822.MessageID>();
// References list the email trail back to its source
if (email.references != null && email.references.list != null)
ancestors.add_all(email.references.list);
// RFC822 requires the In-Reply-To Message-ID be prepended to the References list, but
// this ensures that's the case
if (email.in_reply_to != null) {
if (ancestors.size == 0 || !ancestors.last().equals(email.in_reply_to))
ancestors.add(email.in_reply_to);
}
// track whether this email has been reported as appended to a conversation, as the
// MultiMap will add as many of the same value as you throw at it
ImplConversation? found_conversation = node.conversation;
// Watch for loops
Gee.HashSet<RFC822.MessageID> seen = new Gee.HashSet<RFC822.MessageID>(Hashable.hash_func,
Equalable.equal_func);
seen.add(node.node_id);
// Walk ancestor IDs, creating nodes if necessary and chaining them together
// NOTE: References are stored from earliest to latest, but we're walking the opposite
// direction
Node current_node = node;
Node? ancestor_node = null;
for (int ctr = ancestors.size - 1; ctr >= 0; ctr--) {
RFC822.MessageID ancestor_id = ancestors[ctr];
if (seen.contains(ancestor_id)) {
warning("Loop detected in conversation: %s seen twice", ancestor_id.to_string());
continue;
}
seen.add(ancestor_id);
// create if necessary
ancestor_node = get_node(ancestor_id);
if (ancestor_node == null)
ancestor_node = add_node(new Node(ancestor_id, null));
// if current_node is in a conversation and its parent_id is null, that means
// it's the origin of a conversation, in which case making it a child of ancestor
// is potentially creating a loop
bool is_origin = (current_node.conversation != null && current_node.parent_id == null);
// This watches for emails with contradictory References paths and new loops;
// essentially, first email encountered wins when assigning parentage
if (!is_origin && (current_node.parent_id == null || current_node.parent_id.equals(ancestor_id))) {
// link up only if
current_node.parent_id = ancestor_id;
ancestor_node.add_child(current_node.node_id);
// See if chaining up uncovers an existing conversation
if (found_conversation == null)
found_conversation = ancestor_node.conversation;
} else if (!is_origin) {
warning("Email %s parent already assigned to %s, %s is orphaned",
current_node.node_id.to_string(), current_node.parent_id.to_string(),
ancestor_id.to_string());
} else {
warning("Email %s already origin of conversation, %s is orphaned",
current_node.node_id.to_string(), ancestor_id.to_string());
}
// move up the chain
current_node = ancestor_node;
}
// if found a conversation, mark all in chain as part of that conversation and note
// that this email was appended to the conversation, otherwise create a new one and
// note that as well
if (found_conversation != null) {
appended_conversations.set(found_conversation, email);
} else {
found_conversation = new ImplConversation(this, current_node);
bool added = new_conversations.add(found_conversation);
assert(added);
}
assign_conversation(current_node, found_conversation);
}
// Go through all the emails and verify they've all been marked as part of a conversation
// TODO: Make this optional at compile time (essentially a giant and expensive assertion)
foreach (Geary.Email email in emails) {
if (email.message_id == null)
continue;
Node? node = get_node(email.message_id);
assert(node != null);
assert(node.conversation != null);
}
// Save and signal the new conversations
if (new_conversations.size > 0) {
conversations.add_all(new_conversations);
notify_conversations_added(new_conversations);
}
// fire signals for other changes
foreach (Conversation conversation in appended_conversations.get_all_keys())
notify_conversation_appended(conversation, appended_conversations.get(conversation));
foreach (Conversation conversation in updated_placeholders.get_all_keys())
notify_updated_placeholders(conversation, updated_placeholders.get(conversation));
}
private Node add_node(Node node) {
assert(node.node_id != null);
// add to id_map (all Nodes are referenceable by their Message-ID)
if (id_map.has_key(node.node_id)) {
debug("WARNING: Replacing node in conversation model with new node of same Message-ID %s",
node.node_id.to_string());
}
id_map.set(node.node_id, node);
return node;
}
private inline Node? get_node(RFC822.MessageID node_id) {
return id_map.get(node_id);
}
private void assign_conversation(Node node, ImplConversation conversation) {
if (node.conversation != null && node.conversation != conversation)
warning("Reassigning node %s to another conversation", node.node_id.to_string());
node.conversation = conversation;
Gee.Collection<RFC822.MessageID>? children = node.get_children();
if (children != null) {
foreach (RFC822.MessageID child_id in children) {
Node? child = get_node(child_id);
assert(child != null);
assign_conversation(child, conversation);
}
}
}
}

View file

@ -380,7 +380,8 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
} }
} }
email.set_full_references(message_id, in_reply_to, references); if (fields.require(Geary.Email.Field.REFERENCES))
email.set_full_references(message_id, in_reply_to, references);
} }
} }

View file

@ -275,15 +275,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
if (remote_position < local_low) { if (remote_position < local_low) {
debug("do_replay_remove_message: Not removing message at %d from local store, not present", debug("do_replay_remove_message: Not removing message at %d from local store, not present",
remote_position); remote_position);
} else {
remote_count = new_remote_count; // Adjust remote position to local position
yield local_folder.remove_email_async((remote_position - local_low) + 1);
return;
} }
// Adjust remote position to local position
yield local_folder.remove_email_async((remote_position - local_low) + 1);
// save new remote count // save new remote count
remote_count = new_remote_count; remote_count = new_remote_count;
@ -445,10 +441,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
} }
} }
if (!found) { if (!found)
debug("Need email at %d in %s", position, to_string());
needed_by_position += position; needed_by_position += position;
}
} }
if (needed_by_position.length == 0) { if (needed_by_position.length == 0) {

View file

@ -14,7 +14,9 @@ public interface Geary.RFC822.MessageData : Geary.Common.MessageData {
} }
public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC822.MessageData, public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC822.MessageData,
Geary.Equalable { Geary.Equalable, Geary.Hashable {
private uint hash = 0;
public MessageID(string value) { public MessageID(string value) {
base (value); base (value);
} }
@ -27,31 +29,36 @@ public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC8
if (this == message_id) if (this == message_id)
return true; return true;
if (to_hash() != message_id.to_hash())
return false;
return value == message_id.value; return value == message_id.value;
} }
public uint to_hash() {
return (hash != 0) ? hash : (hash = str_hash(value));
}
} }
/**
* A Message-ID list stores its IDs from earliest to latest.
*/
public class Geary.RFC822.MessageIDList : Geary.Common.StringMessageData, Geary.RFC822.MessageData { public class Geary.RFC822.MessageIDList : Geary.Common.StringMessageData, Geary.RFC822.MessageData {
private Gee.List<MessageID>? list = null; public Gee.List<MessageID>? list { get; private set; }
public MessageIDList(string value) { public MessageIDList(string value) {
base (value); base (value);
}
public Gee.List<MessageID> decoded() {
if (list != null)
return list;
list = new Gee.ArrayList<MessageID>(Equalable.equal_func); string[] ids = value.split_set(" \n\r\t");
string[] ids = value.split(" ");
foreach (string id in ids) { foreach (string id in ids) {
id = id.strip(); id = id.strip();
if (!String.is_empty(id)) if (!String.is_empty(id)) {
if (list == null)
list = new Gee.ArrayList<MessageID>();
list.add(new MessageID(id)); list.add(new MessageID(id));
}
} }
return list;
} }
} }

View file

@ -102,9 +102,11 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
id.uid.to_string(), to_string()); id.uid.to_string(), to_string());
} }
// TODO: Also check by Message-ID (and perhaps other EmailProperties) to link an existing
// message in the database to this Folder
message_id = yield message_table.create_async(transaction, message_id = yield message_table.create_async(transaction,
new MessageRow.from_email(message_table, email), new MessageRow.from_email(message_table, email), cancellable);
cancellable);
// create the message location in the location lookup table using its UID for the ordering // create the message location in the location lookup table using its UID for the ordering
// (which fulfills the requirements for the ordering column) // (which fulfills the requirements for the ordering column)

View file

@ -118,7 +118,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
if (row.fields.is_any_set(Geary.Email.Field.REFERENCES)) { if (row.fields.is_any_set(Geary.Email.Field.REFERENCES)) {
query = locked.prepare( query = locked.prepare(
"UPDATE MessageTable SET message_id=?, in_reply_to=?, reference_ids = ? WHERE id=?"); "UPDATE MessageTable SET message_id=?, in_reply_to=?, reference_ids=? WHERE id=?");
query.bind_string(0, row.message_id); query.bind_string(0, row.message_id);
query.bind_string(1, row.in_reply_to); query.bind_string(1, row.in_reply_to);
query.bind_string(2, row.references); query.bind_string(2, row.references);

87
src/theseus/theseus.vala Normal file
View file

@ -0,0 +1,87 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
class TheseusAsync : MainAsync {
public string username;
public string password;
public string folder;
public TheseusAsync(string[] args, string username, string password, string? folder = null) {
base (args);
this.username = username;
this.password = password;
this.folder = folder ?? "INBOX";
}
protected override async int exec_async() throws Error {
Geary.Account account = Geary.Engine.open(new Geary.Credentials(username, password),
File.new_for_path(Environment.get_user_data_dir()).get_child("geary"),
File.new_for_path(Environment.get_current_dir()));
Geary.Folder inbox = yield account.fetch_folder_async(new Geary.FolderRoot(folder, null, true));
yield inbox.open_async(true);
Geary.Conversations threads = new Geary.Conversations(inbox, Geary.Email.Field.ENVELOPE);
yield threads.load_async(-1, -1, Geary.Folder.ListFlags.NONE, null);
yield inbox.close_async();
foreach (Geary.Conversation conversation in threads.get_conversations()) {
print_thread(conversation, conversation.get_origin(), 0);
stdout.printf("\n");
}
return 0;
}
private void print_thread(Geary.Conversation conversation, Geary.ConversationNode node, int level) {
for (int ctr = 0; ctr < level; ctr++)
stdout.printf(" ");
print_email(node);
Gee.Collection<Geary.ConversationNode>? children = conversation.get_replies(node);
if (children != null) {
foreach (Geary.ConversationNode child_node in children)
print_thread(conversation, child_node, level + 1);
}
}
private void print_email(Geary.ConversationNode node) {
Geary.Email? email = node.get_email();
if (email == null)
stdout.printf("(no message available)\n");
else
stdout.printf("%s\n", get_details(email));
}
private string get_details(Geary.Email email) {
StringBuilder builder = new StringBuilder();
if (email.subject != null)
builder.append_printf("%.40s ", email.subject.value);
if (email.from != null)
builder.append_printf("(%.20s) ", email.from.to_string());
if (email.date != null)
builder.append_printf("[%s] ", email.date.to_string());
return builder.str;
}
}
int main(string[] args) {
if (args.length != 3 && args.length != 4) {
stderr.printf("usage: theseus <username> <password> [folder]\n");
return 1;
}
return new TheseusAsync(args, args[1], args[2], (args.length == 4) ? args[3] : null).exec();
}

21
src/theseus/wscript_build Normal file
View file

@ -0,0 +1,21 @@
#! /usr/bin/env python
# encoding: utf-8
#
# Copyright 2011 Yorba Foundation
theseus_src = [
'theseus.vala'
]
theseus_uselib = 'GEE GTK GLIB'
theseus_packages = [ 'gee-1.0', 'glib-2.0' ]
bld.program(
target = 'theseus',
vapi_dirs = '../../vapi',
threading = True,
uselib = theseus_uselib + ' ' + bld.engine_uselib + ' ' + bld.common_uselib,
packages = theseus_packages + bld.engine_packages + bld.common_packages,
source = theseus_src + bld.common_src + bld.engine_src
)

View file

@ -6,6 +6,7 @@
def build(bld): def build(bld):
bld.common_src = [ bld.common_src = [
'../common/common-arrays.vala', '../common/common-arrays.vala',
'../common/common-async.vala',
'../common/common-date.vala', '../common/common-date.vala',
'../common/common-intl.vala', '../common/common-intl.vala',
'../common/common-yorba-application.vala' '../common/common-yorba-application.vala'
@ -17,6 +18,8 @@ def build(bld):
bld.engine_src = [ bld.engine_src = [
'../engine/api/geary-account.vala', '../engine/api/geary-account.vala',
'../engine/api/geary-composed-email.vala', '../engine/api/geary-composed-email.vala',
'../engine/api/geary-conversation.vala',
'../engine/api/geary-conversations.vala',
'../engine/api/geary-credentials.vala', '../engine/api/geary-credentials.vala',
'../engine/api/geary-email-identifier.vala', '../engine/api/geary-email-identifier.vala',
'../engine/api/geary-email-location.vala', '../engine/api/geary-email-location.vala',
@ -152,4 +155,5 @@ def build(bld):
bld.recurse('client') bld.recurse('client')
bld.recurse('console') bld.recurse('console')
bld.recurse('norman') bld.recurse('norman')
bld.recurse('theseus')

View file

@ -113,6 +113,7 @@ def post_build(bld):
geary_path = 'build/src/client/geary' geary_path = 'build/src/client/geary'
console_path = 'build/src/console/console' console_path = 'build/src/console/console'
norman_path = 'build/src/norman/norman' norman_path = 'build/src/norman/norman'
theseus_path = 'build/src/theseus/theseus'
if os.path.isfile(geary_path) : if os.path.isfile(geary_path) :
shutil.copy2(geary_path, 'geary') shutil.copy2(geary_path, 'geary')
@ -123,6 +124,9 @@ def post_build(bld):
if os.path.isfile(norman_path) : if os.path.isfile(norman_path) :
shutil.copy2(norman_path, 'norman') shutil.copy2(norman_path, 'norman')
if os.path.isfile(theseus_path) :
shutil.copy2(theseus_path, 'theseus')
# Compile schemas for local (non-intall) build. # Compile schemas for local (non-intall) build.
client_build_path = 'build/src/client' client_build_path = 'build/src/client'
shutil.copy2('src/client/org.yorba.geary.gschema.xml', client_build_path) shutil.copy2('src/client/org.yorba.geary.gschema.xml', client_build_path)