diff --git a/api/build.xml b/api/build.xml
index 48ffec3d9e7547decd505991c1c6056b56db7622..065ca4c885245f1d1b97c915f35bfa33b9e893df 100644
--- a/api/build.xml
+++ b/api/build.xml
@@ -1,3 +1,3 @@
-<project name='api' default='compile'>
+<project name='api' default='depend'>
 	<import file='../build-common.xml'/>
 </project>
diff --git a/build-common.xml b/build-common.xml
index 6d9487f8e7d643231c0ac0ecfcb0e1d6395f7056..a7e4270fee02087e17ec499c98026e07ec92c720 100644
--- a/build-common.xml
+++ b/build-common.xml
@@ -1,4 +1,4 @@
-<project name='build-common' default='compile'>
+<project name='build-common'>
 	<import file='dependencies.xml'/>
 	<dirname property='build-common.root' file='${ant.file.build-common}'/>
 	<fileset id='bundled-jars' dir='${build-common.root}/lib'>
diff --git a/components/build.xml b/components/build.xml
index 88fdea5bfad738d9d541ae7f0d4c6e93347ac50e..164882e389cd2e70e6ce9aed6a07d79ed13c633a 100644
--- a/components/build.xml
+++ b/components/build.xml
@@ -1,3 +1,3 @@
-<project name='components' default='compile'>
+<project name='components' default='depend'>
 	<import file='../build-common.xml'/>
 </project>
diff --git a/dependencies.xml b/dependencies.xml
index 1755a17fe7ad9b21fc7e3855bc3984263520b1f2..6aa196e390e4f58a2a4dc1b801e48036c30fd3e8 100644
--- a/dependencies.xml
+++ b/dependencies.xml
@@ -2,19 +2,24 @@
 	<dirname property='depend.root' file='${ant.file.dependencies}'/>
 	<target name='depend.all' depends='depend.components, depend.ui'/>
 	<target name='depend.api'>
-		<ant dir='${depend.root}/api' inheritAll='false'/>
+		<ant dir='${depend.root}/api' target='compile'
+			inheritAll='false'/>
 	</target>
 	<target name='depend.components' depends='depend.api, depend.util'>
-		<ant dir='${depend.root}/components' inheritAll='false'/>
+		<ant dir='${depend.root}/components' target='compile'
+			inheritAll='false'/>
 	</target>
 	<target name='depend.test' depends='depend.components'>
-		<ant dir='${depend.root}/test' inheritAll='false'/>
+		<ant dir='${depend.root}/test' target='compile'
+			inheritAll='false'/>
 	</target>
 	<target name='depend.ui' depends='depend.api, depend.util'>
-		<ant dir='${depend.root}/ui' inheritAll='false'/>
+		<ant dir='${depend.root}/ui' target='compile'
+			inheritAll='false'/>
 	</target>
 	<target name='depend.util'>
-		<ant dir='${depend.root}/util' inheritAll='false'/>
+		<ant dir='${depend.root}/util' target='compile'
+			inheritAll='false'/>
 	</target>
 	<target name='depend-clean.all'
 		depends='depend-clean.components, depend-clean.ui'/>
diff --git a/test/build.xml b/test/build.xml
index 5869dbdfc96cb4d8327fe136ea394245ebaab759..dfc757721352458e13b4f5ebb0b04406e0d31610 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -1,4 +1,4 @@
-<project name='test' default='compile'>
+<project name='test' default='test'>
 	<import file='../build-common.xml'/>
 	<target name='test' depends='depend'>
 		<junit haltonfailure='true' printsummary='on' showoutput='true'>
diff --git a/test/net/sf/briar/util/FileUtilsTest.java b/test/net/sf/briar/util/FileUtilsTest.java
index c30ac0a6578229e1be850504880072ade3f568b5..1b76d85705a1ed6b1bad16b045cae92ae869db4b 100644
--- a/test/net/sf/briar/util/FileUtilsTest.java
+++ b/test/net/sf/briar/util/FileUtilsTest.java
@@ -1,13 +1,16 @@
 package net.sf.briar.util;
 
 import java.io.File;
-import java.io.FileOutputStream;
+import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.PrintStream;
+import java.io.InputStream;
 import java.util.Scanner;
 
 import junit.framework.TestCase;
+import net.sf.briar.util.FileUtils.Callback;
 
+import org.jmock.Expectations;
+import org.jmock.Mockery;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -21,15 +24,20 @@ public class FileUtilsTest extends TestCase {
 		testDir.mkdirs();
 	}
 
+	@Test
+	public void testCreateTempFile() throws IOException {
+		File temp = FileUtils.createTempFile();
+		assertTrue(temp.exists());
+		assertTrue(temp.isFile());
+		assertEquals(0L, temp.length());
+		temp.delete();
+	}
+
 	@Test
 	public void testCopy() throws IOException {
 		File src = new File(testDir, "src");
 		File dest = new File(testDir, "dest");
-
-		PrintStream out = new PrintStream(new FileOutputStream(src));
-		out.print("Foo bar\r\nBar foo\r\n");
-		out.flush();
-		out.close();
+		TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n");
 		long length = src.length();
 
 		FileUtils.copy(src, dest);
@@ -42,18 +50,78 @@ public class FileUtilsTest extends TestCase {
 		assertEquals("Bar foo", in.nextLine());
 		assertFalse(in.hasNext());
 		in.close();
+	}
 
-		src.delete();
-		dest.delete();
+	@Test
+	public void testCopyFromStream() throws IOException {
+		File src = new File(testDir, "src");
+		File dest = new File(testDir, "dest");
+		TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n");
+		long length = src.length();
+		InputStream is = new FileInputStream(src);
+		is.skip(4);
+
+		FileUtils.copy(is, dest);
+
+		assertEquals(length - 4, dest.length());
+		Scanner in = new Scanner(dest);
+		assertTrue(in.hasNextLine());
+		assertEquals("bar", in.nextLine());
+		assertTrue(in.hasNextLine());
+		assertEquals("Bar foo", in.nextLine());
+		assertFalse(in.hasNext());
+		in.close();
 	}
 
-	@After
-	public void tearDown() throws IOException {
-		delete(testDir);
+	@Test
+	public void testCopyRecursively() throws IOException {
+		final File dest1 = new File(testDir, "dest/abc/def/1");
+		final File dest2 = new File(testDir, "dest/abc/def/2");
+		final File dest3 = new File(testDir, "dest/abc/3");
+		Mockery context = new Mockery();
+		final Callback callback = context.mock(Callback.class);
+		context.checking(new Expectations() {{
+			oneOf(callback).processingFile(dest1);
+			oneOf(callback).processingFile(dest2);
+			oneOf(callback).processingFile(dest3);
+		}});
+
+		copyRecursively(callback);
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testCopyRecursivelyNoCallback() throws IOException {
+		copyRecursively(null);
 	}
 
-	private static void delete(File f) throws IOException {
-		if(f.isDirectory()) for(File child : f.listFiles()) delete(child);
-		f.delete();
+	private void copyRecursively(Callback callback) throws IOException {
+		TestUtils.createFile(new File(testDir, "abc/def/1"), "one one one");
+		TestUtils.createFile(new File(testDir, "abc/def/2"), "two two two");
+		TestUtils.createFile(new File(testDir, "abc/3"), "three three three");
+
+		File dest = new File(testDir, "dest");
+		dest.mkdir();
+
+		FileUtils.copyRecursively(new File(testDir, "abc"), dest, callback);
+
+		File dest1 = new File(testDir, "dest/abc/def/1");
+		assertTrue(dest1.exists());
+		assertTrue(dest1.isFile());
+		assertEquals("one one one".length(), dest1.length());
+		File dest2 = new File(testDir, "dest/abc/def/2");
+		assertTrue(dest2.exists());
+		assertTrue(dest2.isFile());
+		assertEquals("two two two".length(), dest2.length());
+		File dest3 = new File(testDir, "dest/abc/3");
+		assertTrue(dest3.exists());
+		assertTrue(dest3.isFile());
+		assertEquals("three three three".length(), dest3.length());
+	}
+
+	@After
+	public void tearDown() throws IOException {
+		TestUtils.delete(testDir);
 	}
 }
diff --git a/test/net/sf/briar/util/StringUtilsTest.java b/test/net/sf/briar/util/StringUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..267414ba2a777d8840b982b2a68a65defa673429
--- /dev/null
+++ b/test/net/sf/briar/util/StringUtilsTest.java
@@ -0,0 +1,20 @@
+package net.sf.briar.util;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+public class StringUtilsTest extends TestCase {
+
+	@Test
+	public void testHead() {
+		String head = StringUtils.head("123456789", 5);
+		assertEquals("12345...", head);
+	}
+
+	@Test
+	public void testTail() {
+		String tail = StringUtils.tail("987654321", 5);
+		assertEquals("...54321", tail);
+	}
+}
diff --git a/test/net/sf/briar/util/TestUtils.java b/test/net/sf/briar/util/TestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bfe71fbb8a8a1d17ca8ca10a414ad15732a9ba0
--- /dev/null
+++ b/test/net/sf/briar/util/TestUtils.java
@@ -0,0 +1,22 @@
+package net.sf.briar.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+
+class TestUtils {
+
+	static void delete(File f) throws IOException {
+		if(f.isDirectory()) for(File child : f.listFiles()) delete(child);
+		f.delete();
+	}
+
+	static void createFile(File f, String s) throws IOException {
+		f.getParentFile().mkdirs();
+		PrintStream out = new PrintStream(new FileOutputStream(f));
+		out.print(s);
+		out.flush();
+		out.close();
+	}
+}
diff --git a/test/net/sf/briar/util/ZipUtilsTest.java b/test/net/sf/briar/util/ZipUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6c1d91e04f23e73a4b0b23f53b35695ebe21383
--- /dev/null
+++ b/test/net/sf/briar/util/ZipUtilsTest.java
@@ -0,0 +1,120 @@
+package net.sf.briar.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.TreeMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import junit.framework.TestCase;
+import net.sf.briar.util.ZipUtils.Callback;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ZipUtilsTest extends TestCase {
+
+	private final File testDir = new File("test.tmp");
+
+	@Before
+	public void setUp() {
+		testDir.mkdirs();
+	}
+
+	@Test
+	public void testCopyToZip() throws IOException {
+		File src = new File(testDir, "src");
+		File dest = new File(testDir, "dest");
+		TestUtils.createFile(src, "foo bar baz");
+		ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest));
+
+		ZipUtils.copyToZip("abc/def", src, zip);
+		zip.flush();
+		zip.close();
+
+		Map<String, String> expected = new TreeMap<String, String>();
+		expected.put("abc/def", "foo bar baz");
+		checkZipEntries(dest, expected);
+	}
+
+	private void checkZipEntries(File f, Map<String, String> expected)
+	throws IOException {
+		Map<String, String> found = new TreeMap<String, String>();
+		assertTrue(f.exists());
+		assertTrue(f.isFile());
+		ZipInputStream unzip = new ZipInputStream(new FileInputStream(f));
+		ZipEntry entry;
+		while((entry = unzip.getNextEntry()) != null) {
+			String name = entry.getName();
+			Scanner s = new Scanner(unzip);
+			assertTrue(s.hasNextLine());
+			String contents = s.nextLine();
+			assertFalse(s.hasNextLine());
+			unzip.closeEntry();
+			found.put(name, contents);
+		}
+		unzip.close();
+		assertEquals(expected.size(), found.size());
+		for(String name : expected.keySet()) {
+			String contents = found.get(name);
+			assertNotNull(contents);
+			assertEquals(expected.get(name), contents);
+		}
+	}
+
+	@Test
+	public void testCopyToZipRecursively() throws IOException {
+		final File src1 = new File(testDir, "abc/def/1");
+		final File src2 = new File(testDir, "abc/def/2");
+		final File src3 = new File(testDir, "abc/3");
+		Mockery context = new Mockery();
+		final Callback callback = context.mock(Callback.class);
+		context.checking(new Expectations() {{
+			oneOf(callback).processingFile(src1);
+			oneOf(callback).processingFile(src2);
+			oneOf(callback).processingFile(src3);
+		}});
+
+		copyRecursively(callback);
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testCopyToZipRecursivelyNoCallback() throws IOException {
+		copyRecursively(null);
+	}
+
+	private void copyRecursively(Callback callback) throws IOException {
+		TestUtils.createFile(new File(testDir, "abc/def/1"), "one one one");
+		TestUtils.createFile(new File(testDir, "abc/def/2"), "two two two");
+		TestUtils.createFile(new File(testDir, "abc/3"), "three three three");
+
+		File src = new File(testDir, "abc");
+		File dest = new File(testDir, "dest");
+		ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest));
+
+		ZipUtils.copyToZipRecursively("ghi", src, zip, callback);
+		zip.flush();
+		zip.close();
+
+		Map<String, String> expected = new TreeMap<String, String>();
+		expected.put("ghi/def/1", "one one one");
+		expected.put("ghi/def/2", "two two two");
+		expected.put("ghi/3", "three three three");
+		checkZipEntries(dest, expected);
+	}
+
+	@After
+	public void tearDown() throws IOException {
+		TestUtils.delete(testDir);
+	}
+}
diff --git a/ui/build.xml b/ui/build.xml
index 9190ee924bbbd369b9a609ac030ee038e59cebc5..e803799a810736b66c6dd123509c4e38292174b9 100644
--- a/ui/build.xml
+++ b/ui/build.xml
@@ -1,3 +1,3 @@
-<project name='ui' default='compile'>
+<project name='ui' default='depend'>
 	<import file='../build-common.xml'/>
 </project>
diff --git a/util/build.xml b/util/build.xml
index 87f79eb6a7fa4797fb95805538a46844ebb61dbb..1d52a27ea25d24a3be1c81a1688bb8b846fe888e 100644
--- a/util/build.xml
+++ b/util/build.xml
@@ -1,3 +1,3 @@
-<project name='util' default='compile'>
+<project name='util' default='depend'>
 	<import file='../build-common.xml'/>
 </project>
diff --git a/util/net/sf/briar/util/FileUtils.java b/util/net/sf/briar/util/FileUtils.java
index 887c8d7d93ad2bd21db235ce19b95e2e2954b683..f86d5eaa5daf2f8520913ff21ce706c039bd4bbb 100644
--- a/util/net/sf/briar/util/FileUtils.java
+++ b/util/net/sf/briar/util/FileUtils.java
@@ -67,7 +67,8 @@ public class FileUtils {
 	}
 
 	/**
-	 * Copies the source file or directory to the destination directory.
+	 * Copies the source file or directory to the destination directory. If the
+	 * callback is not null it's called once for each file created.
 	 */
 	public static void copyRecursively(File src, File dest, Callback callback)
 	throws IOException {
diff --git a/util/net/sf/briar/util/StringUtils.java b/util/net/sf/briar/util/StringUtils.java
index bcd9947c3084fd2f655ce4b23956606b3534f139..f61ab476e762e0a1bcb4ea8d4373eb7d87cbc6d4 100644
--- a/util/net/sf/briar/util/StringUtils.java
+++ b/util/net/sf/briar/util/StringUtils.java
@@ -2,11 +2,19 @@ package net.sf.briar.util;
 
 public class StringUtils {
 
+	/**
+	 * Trims the given string to the given length, returning the head and
+	 * appending "..." if the string was trimmed.
+	 */
 	public static String head(String s, int length) {
 		if(s.length() > length) return s.substring(0, length) + "...";
 		else return s;
 	}
 
+	/**
+	 * Trims the given string to the given length, returning the tail and
+	 * prepending "..." if the string was trimmed.
+	 */
 	public static String tail(String s, int length) {
 		if(s.length() > length) return "..." + s.substring(s.length() - length);
 		else return s;
diff --git a/util/net/sf/briar/util/ZipUtils.java b/util/net/sf/briar/util/ZipUtils.java
index cc5aeacd9c1aa6735d68967887264a8d1e178daf..202a9b086e930853f29910212a4b893b6dd898ca 100644
--- a/util/net/sf/briar/util/ZipUtils.java
+++ b/util/net/sf/briar/util/ZipUtils.java
@@ -11,9 +11,13 @@ import java.util.zip.ZipOutputStream;
 
 public class ZipUtils {
 
+	/**
+	 * Copies the given file to the given zip, using the given path for the
+	 * zip entry.
+	 */
 	public static void copyToZip(String path, File file, ZipOutputStream zip)
 	throws IOException {
-		assert file.isFile() : file.getAbsolutePath();
+		assert file.isFile();
 		zip.putNextEntry(new ZipEntry(path));
 		FileInputStream in = new FileInputStream(file);
 		byte[] buf = new byte[1024];
@@ -23,6 +27,12 @@ public class ZipUtils {
 		zip.closeEntry();
 	}
 
+	/**
+	 * Copies the given directory to the given zip recursively, using the
+	 * given path in place of the directory's name as the parent of all the zip
+	 * entries. If the callback is not null it's called once for each file
+	 * added.
+	 */
 	public static void copyToZipRecursively(String path, File dir,
 			ZipOutputStream zip, Callback callback) throws IOException {
 		assert dir.isDirectory();
@@ -42,6 +52,11 @@ public class ZipUtils {
 		else return path + "/" + name;
 	}
 
+	/**
+	 * Unzips the given stream to the given directory, skipping any zip entries
+	 * that don't match the given regex. If the callback is not null it's
+	 * called once for each file extracted.
+	 */
 	public static void unzipStream(InputStream in, File dir, String regex,
 			Callback callback) throws IOException {
 		String path = dir.getCanonicalPath();