Convert aos over to flatbuffers

Everything builds, and all the tests pass.  I suspect that some entries
are missing from the config files, but those will be found pretty
quickly on startup.

There is no logging or live introspection of queue messages.

Change-Id: I496ee01ed68f202c7851bed7e8786cee30df29f5
diff --git a/frc971/downloader/BUILD b/frc971/downloader/BUILD
new file mode 100644
index 0000000..1aab653
--- /dev/null
+++ b/frc971/downloader/BUILD
@@ -0,0 +1,12 @@
+py_binary(
+    name = "downloader",
+    srcs = [
+        "downloader.py",
+    ],
+    data = [
+        "@rsync",
+        "@ssh",
+        "@ssh//:scp",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/frc971/downloader/downloader.bzl b/frc971/downloader/downloader.bzl
new file mode 100644
index 0000000..a5d1bc1
--- /dev/null
+++ b/frc971/downloader/downloader.bzl
@@ -0,0 +1,119 @@
+def _aos_downloader_impl(ctx):
+    all_files = ctx.files.srcs + ctx.files.start_srcs + [ctx.outputs._startlist]
+    ctx.file_action(
+        output = ctx.outputs.executable,
+        executable = True,
+        content = "\n".join([
+            "#!/bin/bash",
+            "set -e",
+            'cd "${BASH_SOURCE[0]}.runfiles/%s"' % ctx.workspace_name,
+        ] + ['%s %s --dirs %s -- %s "$@"' % (
+            ctx.executable._downloader.short_path,
+            " ".join([src.short_path for src in d.downloader_srcs]),
+            d.downloader_dir,
+            ctx.attr.default_target,
+        ) for d in ctx.attr.dirs] + [
+            'exec %s %s -- %s "$@"' % (
+                ctx.executable._downloader.short_path,
+                " ".join([src.short_path for src in all_files]),
+                ctx.attr.default_target,
+            ),
+        ]),
+    )
+
+    ctx.file_action(
+        output = ctx.outputs._startlist,
+        content = "\n".join([f.basename for f in ctx.files.start_srcs]) + "\n",
+    )
+
+    to_download = [ctx.outputs._startlist]
+    to_download += all_files
+    for d in ctx.attr.dirs:
+        to_download += d.downloader_srcs
+
+    return struct(
+        runfiles = ctx.runfiles(
+            files = to_download + ctx.files._downloader,
+            transitive_files = ctx.attr._downloader.default_runfiles.files,
+            collect_data = True,
+            collect_default = True,
+        ),
+        files = depset([ctx.outputs.executable]),
+    )
+
+def _aos_downloader_dir_impl(ctx):
+    return struct(
+        downloader_dir = ctx.attr.dir,
+        downloader_srcs = ctx.files.srcs,
+    )
+
+"""Creates a binary which downloads code to a robot.
+
+Running this with `bazel run` will actually download everything.
+
+This also generates a start_list.txt file with the names of binaries to start.
+
+Attrs:
+  srcs: The files to download. They currently all get shoved into one folder.
+  dirs: A list of aos_downloader_dirs to download too.
+  start_srcs: Like srcs, except they also get put into start_list.txt.
+  default_target: The default host to download to. If not specified, defaults to
+                  roboRIO-971.local.
+"""
+
+aos_downloader = rule(
+    attrs = {
+        "_downloader": attr.label(
+            executable = True,
+            cfg = "host",
+            default = Label("//frc971/downloader"),
+        ),
+        "start_srcs": attr.label_list(
+            mandatory = True,
+            allow_files = True,
+        ),
+        "srcs": attr.label_list(
+            mandatory = True,
+            allow_files = True,
+        ),
+        "dirs": attr.label_list(
+            mandatory = False,
+            providers = [
+                "downloader_dir",
+                "downloader_srcs",
+            ],
+        ),
+        "default_target": attr.string(
+            default = "roboRIO-971-frc.local",
+        ),
+    },
+    executable = True,
+    outputs = {
+        "_startlist": "%{name}.start_list.dir/start_list.txt",
+    },
+    implementation = _aos_downloader_impl,
+)
+
+"""Downloads files to a specific directory.
+
+This rule does nothing by itself. Use it by adding to the dirs attribute of an
+aos_downloader rule.
+
+Attrs:
+  srcs: The files to download. They all go in the same directory.
+  dir: The directory (relative to the standard download directory) to put all
+       the files in.
+"""
+
+aos_downloader_dir = rule(
+    attrs = {
+        "srcs": attr.label_list(
+            mandatory = True,
+            allow_files = True,
+        ),
+        "dir": attr.string(
+            mandatory = True,
+        ),
+    },
+    implementation = _aos_downloader_dir_impl,
+)
diff --git a/frc971/downloader/downloader.py b/frc971/downloader/downloader.py
new file mode 100644
index 0000000..e49ee5d
--- /dev/null
+++ b/frc971/downloader/downloader.py
@@ -0,0 +1,94 @@
+# This file is run by shell scripts generated by the aos_downloader Skylark
+# macro. Everything before the first -- is a hard-coded list of files to
+# download.
+
+from __future__ import print_function
+
+import sys
+import subprocess
+import re
+import os
+
+
+def install(ssh_target, pkg):
+    """Installs a package from NI on the ssh target."""
+    print("Installing", pkg)
+    PKG_URL = "http://download.ni.com/ni-linux-rt/feeds/2015/arm/ipk/cortexa9-vfpv3/" + pkg
+    subprocess.check_call(["wget", PKG_URL, "-O", pkg])
+    try:
+        subprocess.check_call([
+            "external/ssh/usr/bin/scp", "-S", "external/ssh/usr/bin/ssh", pkg,
+            ssh_target + ":/tmp/" + pkg
+        ])
+        subprocess.check_call([
+            "external/ssh/usr/bin/ssh", ssh_target, "opkg", "install",
+            "/tmp/" + pkg
+        ])
+        subprocess.check_call(
+            ["external/ssh/usr/bin/ssh", ssh_target, "rm", "/tmp/" + pkg])
+    finally:
+        subprocess.check_call(["rm", pkg])
+
+
+def main(argv):
+    args = argv[argv.index("--") + 1:]
+
+    relative_dir = ""
+    recursive = False
+
+    if "--dirs" in argv:
+        dirs_index = argv.index("--dirs")
+        srcs = argv[1:dirs_index]
+        relative_dir = argv[dirs_index + 1]
+        recursive = True
+    else:
+        srcs = argv[1:argv.index("--")]
+
+    ROBORIO_TARGET_DIR = "/home/admin/robot_code"
+    ROBORIO_USER = "admin"
+
+    target_dir = ROBORIO_TARGET_DIR
+    user = ROBORIO_USER
+    destination = args[-1]
+
+    result = re.match("(?:([^:@]+)@)?([^:@]+)(?::([^:@]+))?", destination)
+    if not result:
+        print(
+            "Not sure how to parse destination \"%s\"!" % destination,
+            file=sys.stderr)
+        return 1
+    if result.group(1):
+        user = result.group(1)
+    hostname = result.group(2)
+    if result.group(3):
+        target_dir = result.group(3)
+
+    ssh_target = "%s@%s" % (user, hostname)
+
+    rsync_cmd = ([
+        "external/rsync/usr/bin/rsync", "-e", "external/ssh/usr/bin/ssh", "-c",
+        "-v", "-z", "--copy-links"
+    ] + srcs + ["%s:%s/%s" % (ssh_target, target_dir, relative_dir)])
+    try:
+        subprocess.check_call(rsync_cmd)
+    except subprocess.CalledProcessError as e:
+        if e.returncode == 127:
+            print("Unconfigured roboRIO, installing rsync.")
+            install(ssh_target, "libattr1_2.4.47-r0.36_cortexa9-vfpv3.ipk")
+            install(ssh_target, "libacl1_2.2.52-r0.36_cortexa9-vfpv3.ipk")
+            install(ssh_target, "rsync_3.1.0-r0.7_cortexa9-vfpv3.ipk")
+            subprocess.check_call(rsync_cmd)
+        else:
+            raise e
+
+    if not recursive:
+        subprocess.check_call(
+            ("external/ssh/usr/bin/ssh", ssh_target, "&&".join([
+                "chmod u+s %s/starter_exe" % target_dir,
+                "echo \'Done moving new executables into place\'",
+                "bash -c \'sync && sync && sync\'",
+            ])))
+
+
+if __name__ == "__main__":
+    main(sys.argv)