The work! The progress!
authorMark Wooding <mdw@distorted.org.uk>
Thu, 28 Jun 2018 11:09:08 +0000 (12:09 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 28 Jun 2018 11:09:08 +0000 (12:09 +0100)
.gitignore
Makefile
app.scala [new file with mode: 0644]
jni.c
keys.scala
peers.scala
sock.scala [deleted file]
sys.scala
toy-activity.scala

index e3dc5e6..7778fc4 100644 (file)
@@ -3,3 +3,4 @@
 /COPYING
 /auto-version
 debug.keystore
+/local.mk
index 7982820..275a88f 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -53,7 +53,7 @@ CCACHE                        := $(shell \
 
 ## Where to put the object files.
 OUTDIR                  = out
-CONFIGDIR               = $(OUTDIR)/config
+CONFIGDIR               = $(OUTDIR)/config-$(hostcpu)
 
 ## The Java runtime, for some reason, hardcodes its default for
 ## `java.io.tmpdir', inviting security problems.  If the user has defined a
@@ -97,13 +97,13 @@ CFLAGS                       = -O2 -g -Wall
 LDFLAGS                         = -Wl,-z,defs
 
 ## Host toolchain.
-FLAVOURS               += host
-ENV.host                =
-CC.host                         = gcc
-CFLAGS.host             = $(CFLAGS) -fPIC
-LD.host                         = $(CC.host)
-LDFLAGS.host            = $(LDFLAGS)
-CONFIG.host             =
+FLAVOURS               += host-$(hostcpu)
+ENV.host-$(hostcpu)     =
+CC.host-$(hostcpu)      = gcc
+CFLAGS.host-$(hostcpu)  = $(CFLAGS) -fPIC
+LD.host-$(hostcpu)      = $(CC.host-$(hostcpu))
+LDFLAGS.host-$(hostcpu)         = $(LDFLAGS)
+CONFIG.host-$(hostcpu)  =
 
 ## Host JNI machinery.
 $(CONFIGDIR)/jdkdir.mk:
@@ -119,7 +119,7 @@ JDKPLAT                     := $(shell \
          (darwin) echo macosx ;; \
          (*) echo $(hostos) ;; \
        esac)
-CFLAGS.host            += -I$(JDKDIR)/include -I$(JDKDIR)/include/$(JDKPLAT)
+CFLAGS.host-$(hostcpu) += -I$(JDKDIR)/include -I$(JDKDIR)/include/$(JDKPLAT)
 
 ## Android SDK location.
 ANDROID_SDKDIR          = /usr/local/android/sdk
@@ -386,7 +386,7 @@ $(foreach a,$(APKLIBS), $(eval $(call obj-rule,$a)))
 CLEANFILES             += $(OUTDIR)/obj.*/*.o $(OUTDIR)/obj.*/*.d
 
 ## Machinery for linking.
-JNIDIR.host             = $(OUTDIR)
+JNIDIR.host-$(hostcpu)  = $(OUTDIR)/lib.host-$(hostcpu)
 JNIDIR.ndk              = $(OUTDIR)/pkg/lib/$1
 
 define apklib-rule
@@ -404,7 +404,8 @@ $(foreach f,$(FLAVOURS), \
 $(foreach a,$(APKLIBS), \
        $(eval $(call apklib-rule,$f,$a))))
 
-CLEANFILES             += $(OUTDIR)/pkg/lib/*/lib*.so $(OUTDIR)/lib*.so
+CLEANFILES             += $(OUTDIR)/pkg/lib/*/lib*.so
+CLEANFILES             += $(OUTDIR)/lib.host-$(hostcpu)/lib*.so
 
 ###--------------------------------------------------------------------------
 ### Android string resource generation.
@@ -473,7 +474,8 @@ CLASSES                     += progress:sys,util
 CLASSES                        += keys:progress,tar,sys,util
 CLASSES                        += terminal:progress,sys,util
 CLASSES                        += R
-CLASSES                        += toy-activity:R
+CLASSES                        += app:R
+CLASSES                        += toy-activity:app,R
 
 ## Building class files.
 $(STAMPDIR)/%.class-stamp: %.java
@@ -581,8 +583,9 @@ all:: debug
 clean::; rm -f $(CLEANFILES)
 realclean::; rm -f $(REALCLEANFILES)
 
-repl: $(CLASSSTAMPS) $(foreach a,$(APKLIBS),$(OUTDIR)/$a)
-       $(SCALA) -cp $(CLASSDIR) -Djava.lib.path=$(OUTDIR) -Yno-load-impl-class
+repl: $(CLASSSTAMPS) $(foreach a,$(APKLIBS),$(JNIDIR.host-$(hostcpu))/$a)
+       $(SCALA) -cp $(CLASSDIR) -Yno-load-impl-class \
+               -Djava.lib.path=$(JNIDIR.host-$(hostcpu)) \
 
 t:; : $(show)
 .PHONY: t
diff --git a/app.scala b/app.scala
new file mode 100644 (file)
index 0000000..f82846c
--- /dev/null
+++ b/app.scala
@@ -0,0 +1,141 @@
+/* -*-scala-*-
+ *
+ * Setting up the Android environment
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object app {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{File, IOException};
+
+import scala.collection.mutable.HashMap;
+
+import android.content.Context; import Context.MODE_WORLD_READABLE;
+import android.os.Build; import Build.{CPU_ABI, CPU_ABI2};
+import android.util.Log;
+
+import sys.FileImplicits._;
+
+/*----- Regular expressions for parsing the `.installed file --------------*/
+
+private final val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
+private final val RX_KEYVAL = """(?x) ^ \s*
+      ([-\w]+)
+      (?:\s+(?!=)|\s*=\s*)
+      (|\S|\S.*\S)
+      \s* $""".r;
+
+/*----- Main code ---------------------------------------------------------*/
+
+private final val TAG = "TrIPE";
+
+var root: File = null;
+
+private def install(ctx: Context, inst: HashMap[String, String]) {
+
+  /* First, figure out which ABIs are wanted on this device.  Unfortunately,
+   * the good way of doing this isn't available in our minimum API level, so
+   * we must use reflection.
+   */
+  val abis = try {
+    classOf[Build].getField("SUPPORTED_ABIS").get(null).
+      asInstanceOf[Array[String]]
+  } catch { case _: NoSuchFieldException =>
+    Array(CPU_ABI, CPU_ABI2) flatMap {
+      case null | "" => None
+      case s => Some(s)
+    }
+  }
+  Log.d(TAG, s"abis = ${abis.mkString(", ")}");
+
+  /* Clear out whatever might be there already. */
+  val bindir = root/"bin";
+  bindir.rmTree();
+  bindir.mkdir_!();
+
+  /* Now extract each of our binaries using the best available ABI. */
+  val assets = ctx.getAssets;
+  for (abi <- abis) {
+    val binsrc = s"bin/$abi";
+    for (base <- assets.list(binsrc)) {
+      val outfile = bindir/base;
+      if (!outfile.exists_!) {
+       Log.d(TAG, s"install: extract `$base' using abi `$abi'");
+       outfile.withOutput { out =>
+         closing(assets.open(s"$binsrc/$base")) { in =>
+           for ((buf, n) <- blocks(in)) out.write(buf, 0, n);
+         }
+       }
+      }
+      outfile.chmod_!(0x1ed);
+    }
+  }
+
+  /* Write out a new install file. */
+  val infofile = root/".installed";
+  val newinfofile = root/".installed.new";
+  newinfofile.withWriter { out =>
+    out.write(s"""### -*-conf-*-
+
+uuid = ${ctx.getString(R.string.auto_build_uuid)}
+""");
+  }
+  newinfofile.rename_!(infofile);
+}
+
+def setup(ctx: Context) {
+
+  /* Make our root directory and remember where it is. */
+  root = ctx.getFilesDir;
+  if (!root.isdir_!) {
+    throw new IOException("system failed to create `files' " +
+                         "(but didn't tell us)");
+  }
+
+  /* Find out which build, if any, corresponds to what's there already. */
+  val inst = HashMap[String, String]();
+  try { root/".installed" withReader { in =>
+    var lno = 1;
+    for (line <- lines(in)) {
+      line match {
+       case RX_COMMENT() => ok;
+       case RX_KEYVAL(k, v) => inst(k) = v;
+       case _ => Log.w(TAG, s".installed:$lno: ignored unparseable line");
+      }
+      lno += 1;
+    }
+  } } catch {
+    case e: IOException =>
+      Log.w(TAG, s".installed: I/O error: ${e.getMessage}");
+  }
+
+  /* If this doesn't match, then we have some work to do. */
+  if (inst.getOrElse("uuid", "<nothing>") !=
+      ctx.getString(R.string.auto_build_uuid))
+    install(ctx, inst);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}
diff --git a/jni.c b/jni.c
index 9ca3651..3841f35 100644 (file)
--- a/jni.c
+++ b/jni.c
@@ -912,6 +912,21 @@ end:
   put_cstring(jni, path, pathstr);
 }
 
+JNIEXPORT void JNIFUNC(chmod)(JNIEnv *jni, jobject cls,
+                             jobject path, jint mode)
+{
+  const char *pathstr = 0;
+
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (chmod(pathstr, mode)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed st permissions on `%s'", pathstr);
+    goto end;
+  }
+end:
+  put_cstring(jni, path, pathstr);
+}
+
 JNIEXPORT void JNIFUNC(mkfile)(JNIEnv *jni, jobject cls,
                            jobject path, jint mode)
 {
index bdbede9..9108b38 100644 (file)
@@ -123,7 +123,7 @@ private val DEFAULTS: Seq[(String, Config => String)] =
 private def parseConfig(file: File): HashMap[String, String] = {
 
   /* Build the new configuration in a temporary place. */
-  var m = HashMap[String, String]();
+  val m = HashMap[String, String]();
 
   /* Read the config file into our map. */
   file.withReader { in =>
index 76326be..c487a9e 100644 (file)
@@ -1,5 +1,32 @@
+/* -*-scala-*-
+ *
+ * The database of known peers
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
 package uk.org.distorted.tripe; package object peers {
 
+/*----- Imports -----------------------------------------------------------*/
+
 import java.io.{BufferedReader, File, FileReader, Reader};
 import java.net.{InetAddress, Inet4Address, Inet6Address,
                 UnknownHostException};
@@ -9,26 +36,31 @@ import scala.concurrent.Channel;
 import scala.util.control.Breaks;
 import scala.util.matching.Regex;
 
-val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r;
-val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r;
-val RX_ASSGN = """(?x) ^
+/*----- Handy regular expressions -----------------------------------------*/
+
+private final val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r;
+private final val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r;
+private final val RX_ASSGN = """(?x) ^
        ([^\s:=] (?: [^:=]* [^\s:=])?)
        \s* [:=] \s*
        (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
        (?: \s+ (?: [;\#].*)? )? $""".r;
-val RX_CONT = """(?x) ^ \s+
+private final val RX_CONT = """(?x) ^ \s+
        (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
        (?: \s+ (?: [;\#].*)? )? $""".r;
-val RX_REF = """(?x) \$ \( ([^)]+) \)""".r;
-val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r;
-val RX_PARENT = """(?x) [^\s,]+""".r
+private final val RX_REF = """(?x) \$ \( ([^)]+) \)""".r;
+private final val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r;
+private final val RX_PARENT = """(?x) [^\s,]+""".r
+
+/*----- Name resolution ---------------------------------------------------*/
 
-object BulkResolver {
-  val BREAK = new Breaks;
+private object BulkResolver {
+  private val BREAK = new Breaks;
 }
 
-class BulkResolver(val nthreads: Int = 8) {
-  import BulkResolver.BREAK._;
+private class BulkResolver(val nthreads: Int = 8) {
+  import BulkResolver.BREAK.{breakable, break};
+
   class Host(val name: String) {
     var a4, a6: Seq[InetAddress] = Seq.empty;
 
@@ -57,6 +89,7 @@ class BulkResolver(val nthreads: Int = 8) {
       }
     }
   }
+
   val ch = new Channel[Host];
   val map = HashMap[String, Host]();
   var preparing = true;
@@ -96,6 +129,8 @@ println(s";; prepare host `$name'");
     map(name).get(flags);
 }
 
+/*----- The peer configuration --------------------------------------------*/
+
 def fmtpath(path: Seq[String]) =
   path.reverse map { i => s"`$i'" } mkString " -> ";
 
@@ -103,10 +138,12 @@ class ConfigSyntaxError(val file: File, val lno: Int, val msg: String)
        extends Exception {
   override def getMessage(): String = s"$file:$lno: $msg";
 }
+
 class MissingConfigSection(val sect: String) extends Exception {
   override def getMessage(): String =
     s"missing configuration section `$sect'";
 }
+
 class MissingConfigItem(val sect: String, val key: String,
                        val path: Seq[(String)]) extends Exception {
   override def getMessage(): String = {
@@ -115,6 +152,7 @@ class MissingConfigItem(val sect: String, val key: String,
     else msg + s" (wanted while expanding ${fmtpath(path)})"
   }
 }
+
 class AmbiguousConfig(val key: String,
                      val v0: String, val p0: Seq[String],
                      val v1: String, val p1: Seq[String])
@@ -128,32 +166,33 @@ class ConfigCycle(val key: String, path: Seq[String]) extends Exception {
   override def getMessage(): String =
     s"found a cycle ${fmtpath(path)} looking up key `$key'";
 }
+
 class NoHostAddresses(val sect: String, val key: String, val host: String)
        extends Exception {
   override def getMessage(): String =
     s"no addresses found for `$host' (key `$key' in section `$sect')";
 }
 
-object Config {
-  sealed abstract class ConfigCacheEntry;
-  case object StillLooking extends ConfigCacheEntry;
-  case object NotFound extends ConfigCacheEntry;
-  case class Found(value: String, path: Seq[String])
-    extends ConfigCacheEntry;
-}
+private sealed abstract class ConfigCacheEntry;
+private case object StillLooking extends ConfigCacheEntry;
+private case object NotFound extends ConfigCacheEntry;
+private case class Found(value: String, path: Seq[String])
+       extends ConfigCacheEntry;
 
 class Config { conf =>
-  import Config._;
-  class Section(val name: String) {
-    val itemmap = HashMap[String, String]();
-    val cache = HashMap[String, ConfigCacheEntry]();
+
+  class Section private(val name: String) {
+    private val itemmap = HashMap[String, String]();
+    private[this] val cache = HashMap[String, ConfigCacheEntry]();
+
     override def toString: String = s"${getClass.getName}($name)";
+
     def parents: Seq[Section] =
       (itemmap.get("@inherit")
        map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList }
        getOrElse Nil);
 
-    def get_internal(key: String, path: Seq[String] = Nil):
+    private def get_internal(key: String, path: Seq[String] = Nil):
              Option[(String, Seq[String])] = {
       val incpath = name +: path;
 
@@ -198,8 +237,8 @@ class Config { conf =>
       expand(key, v0, resolve, path)
     }
 
-    def expand(key: String, value: String, resolve: Boolean,
-              path: Seq[String]): String = {
+    private def expand(key: String, value: String, resolve: Boolean,
+                      path: Seq[String]): String = {
       val v1 = RX_REF.replaceAllIn(value, { m =>
        Regex.quoteReplacement(get(m.group(1), resolve, path))
       });
@@ -233,14 +272,15 @@ class Config { conf =>
       b.result
     }
   }
-  val sectmap = new HashMap[String, Section];
+
+  private[this] val sectmap = new HashMap[String, Section];
   def sections: Iterator[Section] = sectmap.values.iterator;
   def section(name: String): Section =
     sectmap.getOrElse(name, throw new MissingConfigSection(name));
 
-  val resolver = new BulkResolver;
+  private[this] val resolver = new BulkResolver;
 
-  def parseFile(path: File): this.type = {
+  private[this] def parseFile(path: File): this.type = {
 println(s";; parse ${path.getPath}");
     withCleaner { clean =>
       val in = new FileReader(path); clean { in.close(); }
@@ -283,6 +323,7 @@ println(s";; in `${sect.name}', set `$key' to `${b.result}'");
     }
     this
   }
+
   def parse(path: File): this.type = {
     if (!path.isDirectory) parseFile(path);
     else for {
@@ -324,4 +365,6 @@ println(s";;        resolving in key `$key'...");
   }
 }
 
+/*----- That's all, folks -------------------------------------------------*/
+
 }
diff --git a/sock.scala b/sock.scala
deleted file mode 100644 (file)
index 1b52bf1..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package uk.org.distorted.tripe;
-
-import java.io.{Closeable, File, InputStream, OutputStream};
-import jni.Constants._;
-
-class Connection(path: File) extends Closeable {
-  def this(path: String) { this(new File(path)); }
-  val conn = jni.connect(path.getPath);
-  override def close() { jni.close(conn, CF_CLOSEMASK); }
-  lazy val input = new ConnectionInputStream(this);
-  lazy val output = new ConnectionOutputStream(this);
-  override protected def finalize() { super.finalize(); close(); }
-}
-
-class ConnectionInputStream(val conn: Connection) extends InputStream {
-  override def read(): Int = {
-    val buf = new Array[Byte](1);
-    val n = read(buf, 0, 1);
-    if (n < 0) -1 else buf(0)&0xff;
-  }
-  override def read(buf: Array[Byte]): Int =
-    read(buf, 0, buf.length);
-  override def read(buf: Array[Byte], start: Int, len: Int) =
-    jni.recv(conn.conn, buf, start, len);
-  override def close() { jni.close(conn.conn, CF_CLOSERD); }
-}
-
-class ConnectionOutputStream(val conn: Connection) extends OutputStream {
-  override def write(b: Int) = {
-    val buf = Array[Byte](b.toByte);
-    write(buf, 0, 1);
-  }
-  override def write(buf: Array[Byte]) { write(buf, 0, buf.length); }
-  override def write(buf: Array[Byte], start: Int, len: Int) =
-    jni.send(conn.conn, buf, start, len);
-  override def close() { jni.close(conn.conn, CF_CLOSEWR); }
-}
index c449414..43487b5 100644 (file)
--- a/sys.scala
+++ b/sys.scala
@@ -32,6 +32,7 @@ import scala.collection.mutable.{HashMap, HashSet};
 
 import java.io.{BufferedReader, BufferedWriter, Closeable, File,
                FileDescriptor, FileInputStream, FileOutputStream,
+               FileReader, FileWriter,
                InputStream, InputStreamReader,
                OutputStream, OutputStreamWriter};
 import java.nio.{ByteBuffer, CharBuffer};
@@ -409,6 +410,8 @@ def unlink(path: String) { unlink(path.toCString); }
 def rmdir(path: String) { rmdir(path.toCString); }
 @native protected def mkdir(path: CString, mode: Int);
 def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); }
+@native protected def chmod(path: CString, mode: Int);
+def chmod(path: String, mode: Int) { chmod(path.toCString, mode); }
 @native protected def mkfile(path: CString, mode: Int);
 def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); }
 @native protected def rename(from: CString, to: CString);
@@ -608,6 +611,7 @@ object FileImplicits {
     def rmdir_!() { rmdir(file.getPath); }
     def mkdir_!(mode: Int) { mkdir(file.getPath, mode); }
     def mkdir_!() { mkdir_!(0x1ff); }
+    def chmod_!(mode: Int) { chmod(file.getPath, mode); }
     def mkfile_!(mode: Int) { mkfile(file.getPath, mode); }
     def mkfile_!() { mkfile_!(0x1b6); }
     def rename_!(to: File) { rename(file.getPath, to.getPath); }
@@ -681,10 +685,8 @@ object FileImplicits {
     /* Opening files.  Again, I'm surprised this isn't here already. */
     def open(): FileInputStream = new FileInputStream(file);
     def openForOutput(): FileOutputStream = new FileOutputStream(file);
-    def reader(): BufferedReader =
-      new BufferedReader(new InputStreamReader(open()));
-    def writer(): BufferedWriter =
-      new BufferedWriter(new OutputStreamWriter(openForOutput()));
+    def reader(): BufferedReader = new BufferedReader(new FileReader(file));
+    def writer(): BufferedWriter = new BufferedWriter(new FileWriter(file));
     def withInput[T](body: FileInputStream => T): T = {
       val in = open();
       try { body(in) }
@@ -694,13 +696,15 @@ object FileImplicits {
       val out = openForOutput();
       try { body(out) } finally { out.close(); }
     }
-    def withReader[T](body: BufferedReader => T): T = withInput { in =>
-      body(new BufferedReader(new InputStreamReader(in)))
-    };
-    def withWriter[T](body: BufferedWriter => T): T = withOutput { out =>
-      val w = new BufferedWriter(new OutputStreamWriter(out));
-      /* Do this the hard way, so that we flush the `BufferedWriter'. */
-      try { body(w) } finally { w.close(); }
+    def withReader[T](body: BufferedReader => T): T = {
+      val r = reader();
+      try { body(r) }
+      finally { r.close(); }
+    }
+    def withWriter[T](body: BufferedWriter => T): T = {
+      val w = writer();
+      try { body(w) }
+      finally { w.close(); }
     }
   }
 }
index dce975c..c605ca5 100644 (file)
@@ -12,61 +12,6 @@ import android.view.View;
 
 import scala.util.control.Breaks;
 
-object Setup {
-  private final val TAG = "Setup";
-  private val BREAK = new Breaks;
-  import BREAK.{breakable, break};
-
-  def setup(ctx: Context) {
-    val bindir = ctx.getDir("bin", MODE_WORLD_READABLE);
-    val assets = ctx.getAssets;
-
-    val abis =
-      try { classOf[Build].getField("SUPPORTED_ABIS").get(null).asInstanceOf[Array[String]] }
-      catch {
-       case _: NoSuchFieldException => Array(CPU_ABI, CPU_ABI2) flatMap {
-         case null | "" => None
-         case s => Some(s)
-       }
-      };
-
-    Log.d(TAG, s"abis = ${abis.mkString(", ")}");
-    Log.d(TAG, s"assets: ${assets.list("bin").mkString(", ")}");
-
-    for (abi <- abis) {
-      val binsrc = s"bin/$abi";
-      for (base <- assets.list(binsrc)) {
-       val prog = new File(bindir, base);
-       if (!prog.exists) try {
-         Log.d(TAG, s"creating $prog...");
-         val in = assets.open(s"$binsrc/$base");
-         Log.d(TAG, "opened source...");
-         val out = new FileOutputStream(prog);
-         Log.d(TAG, "opened target...");
-         val buf = new Array[Byte](4096);
-         breakable {
-           while (true) {
-             val n = in.read(buf);
-             Log.d(TAG, s"read $n bytes...");
-             if (n <= 0) break;
-             out.write(buf, 0, n);
-           }
-         }
-         in.close();
-         out.close();
-         Log.d(TAG, "set permissions...");
-         if (!prog.setReadable(true, false) ||
-             !prog.setExecutable(true, false))
-           throw new IOException("failed to set program permissions");
-       } catch {
-         case exc: IOException =>
-           Log.wtf(TAG, "fuck, failed to create prog", exc);
-       }
-      }
-    }
-    Log.d(TAG, "all OK");
-  }
-}
 
 object ToyActivity {
   private final val TAG = "ToyActivity";
@@ -79,7 +24,7 @@ class ToyActivity extends Activity {
 
   override protected def onCreate(joy: Bundle) {
     super.onCreate(joy);
-    Setup.setup(this);
+    app.setup(this);
     setContentView(R.layout.toy);
     Log.d(TAG, s"created ${this}");
   }