Have downloader rsync everything at once for speed

This speeds up the copy *significantly*.  The extra SSH calls were
adding up to a lot of extra cost.  3 seconds for a NOP download!

Do this by building up a folder that looks like what the target should
look like, and then rsync that all in 1 go.

While we are here, rename .stripped to not have the extension.  It makes
the commands harder to remember.

Change-Id: I50e50c90421e049f48af453c8113d7c2204c7774
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/aos/starter/starter.sh b/aos/starter/starter.sh
index 68d567a..2c8494e 100755
--- a/aos/starter/starter.sh
+++ b/aos/starter/starter.sh
@@ -17,5 +17,5 @@
 cd "${ROBOT_CODE}"
 export PATH="${PATH}:${ROBOT_CODE}"
 while true; do
-	starterd.stripped 2>&1
+	starterd 2>&1
 done
diff --git a/frc971/downloader/downloader.bzl b/frc971/downloader/downloader.bzl
index e1b535e..9eb7e32 100644
--- a/frc971/downloader/downloader.bzl
+++ b/frc971/downloader/downloader.bzl
@@ -1,5 +1,12 @@
 def _aos_downloader_impl(ctx):
     all_files = ctx.files.srcs + ctx.files.start_srcs + [ctx.outputs._startlist]
+    target_files = []
+
+    # downloader looks for : in the inputs and uses the part after the : as
+    # the directory to copy to.
+    for d in ctx.attr.dirs:
+        target_files += [src.short_path + ":" + d.downloader_dir for src in d.downloader_srcs]
+
     ctx.actions.write(
         output = ctx.outputs.executable,
         is_executable = True,
@@ -7,16 +14,12 @@
             "#!/bin/bash",
             "set -e",
             'cd "${BASH_SOURCE[0]}.runfiles/%s"' % ctx.workspace_name,
-        ] + ['%s --dir %s --target "$@" --type %s %s' % (
-            ctx.executable._downloader.short_path,
-            d.downloader_dir,
-            ctx.attr.target_type,
-            " ".join([src.short_path for src in d.downloader_srcs]),
-        ) for d in ctx.attr.dirs] + [
-            'exec %s --target "$@" --type %s %s' % (
+        ] + [
+            'exec %s --target "$@" --type %s %s %s' % (
                 ctx.executable._downloader.short_path,
                 ctx.attr.target_type,
                 " ".join([src.short_path for src in all_files]),
+                " ".join(target_files),
             ),
         ]),
     )
diff --git a/frc971/downloader/downloader.py b/frc971/downloader/downloader.py
index 31155dd..dc14df1 100644
--- a/frc971/downloader/downloader.py
+++ b/frc971/downloader/downloader.py
@@ -6,9 +6,12 @@
 
 import argparse
 import sys
+from tempfile import TemporaryDirectory
 import subprocess
 import re
+import stat
 import os
+import shutil
 
 
 def install(ssh_target, pkg, ssh_path, scp_path):
@@ -40,20 +43,10 @@
         required=True,
         help="Target type for deployment")
     parser.add_argument(
-        "--dir",
-        type=str,
-        help="Directory within robot_code to copy the files to.")
-    parser.add_argument(
         "srcs", type=str, nargs='+', help="List of files to copy over")
     args = parser.parse_args(argv[1:])
 
-    relative_dir = ""
-    recursive = False
-
     srcs = args.srcs
-    if args.dir is not None:
-        relative_dir = args.dir
-        recursive = True
 
     destination = args.target
 
@@ -83,46 +76,74 @@
     ssh_path = "external/ssh/ssh"
     scp_path = "external/ssh/scp"
 
-    rsync_cmd = ([
-        "external/rsync/usr/bin/rsync", "-e", ssh_path, "-c", "-v", "-z",
-        "--perms", "--copy-links"
-    ] + srcs)
+    # Since rsync is pretty fixed in what it can do, build up a temporary
+    # directory with the exact contents we want the target to have.  This
+    # is faster than multiple SSH connections.
+    with TemporaryDirectory() as temp_dir:
+        pwd = os.getcwd()
+        # Bazel gives us the same file twice, so dedup here rather than
+        # in starlark
+        copied = set()
+        for s in srcs:
+            if ":" in s:
+                folder = os.path.join(temp_dir, s[s.find(":") + 1:])
+                os.makedirs(folder, exist_ok=True)
+                s = os.path.join(pwd, s[:s.find(":")])
+                destination = os.path.join(folder, os.path.basename(s))
+            else:
+                s = os.path.join(pwd, s)
+                destination = os.path.join(temp_dir, os.path.basename(s))
 
-    # If there is only 1 file to transfer, we would overwrite the destination
-    # folder.  In that case, specify the full path to the target.
-    if len(srcs) == 1:
-        rsync_cmd += [
-            "%s:%s/%s/%s" % (ssh_target, target_dir, relative_dir, srcs[0])
-        ]
-    else:
-        rsync_cmd += ["%s:%s/%s" % (ssh_target, target_dir, relative_dir)]
+            if s in copied:
+                continue
+            copied.add(s)
+            if s.endswith(".stripped"):
+                destination = destination[:destination.find(".stripped")]
+            shutil.copy2(s, destination)
+        # Make sure the folder that gets created on the roboRIO has open
+        # permissions or the executables won't be visible to init.
+        os.chmod(temp_dir, 0o775)
+        # Starter needs to be SUID so we transition from lvuser to admin.
+        os.chmod(os.path.join(temp_dir, "starterd"), 0o775 | stat.S_ISUID)
 
-    try:
-        subprocess.check_call(rsync_cmd)
-    except subprocess.CalledProcessError as e:
-        if e.returncode == 127 or e.returncode == 12:
-            print("Unconfigured roboRIO, installing rsync.")
-            install(ssh_target, "libattr1_2.4.47-r0.36_cortexa9-vfpv3.ipk",
-                    ssh_path, scp_path)
-            install(ssh_target, "libacl1_2.2.52-r0.36_cortexa9-vfpv3.ipk",
-                    ssh_path, scp_path)
-            install(ssh_target, "rsync_3.1.0-r0.7_cortexa9-vfpv3.ipk",
-                    ssh_path, scp_path)
-            subprocess.check_call(rsync_cmd)
-        elif e.returncode == 11:
-            # Directory wasn't created, make it and try again.  This keeps the happy path fast.
-            subprocess.check_call(
-                [ssh_path, ssh_target, "mkdir", "-p", target_dir])
-            subprocess.check_call(rsync_cmd)
+        rsync_cmd = ([
+            "external/rsync/usr/bin/rsync",
+            "-e",
+            ssh_path,
+            "-c",
+            "-r",
+            "-v",
+            "--perms",
+            "-l",
+            temp_dir + "/",
+        ])
+
+        # If there is only 1 file to transfer, we would overwrite the destination
+        # folder.  In that case, specify the full path to the target.
+        if len(srcs) == 1:
+            rsync_cmd += ["%s:%s/%s" % (ssh_target, target_dir, srcs[0])]
         else:
-            raise e
+            rsync_cmd += ["%s:%s" % (ssh_target, target_dir)]
 
-    if not recursive:
-        subprocess.check_call((ssh_path, ssh_target, "&&".join([
-            "chmod u+s %s/starterd.stripped" % target_dir,
-            "echo \'Done moving new executables into place\'",
-            "bash -c \'sync\'",
-        ])))
+        try:
+            subprocess.check_call(rsync_cmd)
+        except subprocess.CalledProcessError as e:
+            if e.returncode == 127 or e.returncode == 12:
+                print("Unconfigured roboRIO, installing rsync.")
+                install(ssh_target, "libattr1_2.4.47-r0.36_cortexa9-vfpv3.ipk",
+                        ssh_path, scp_path)
+                install(ssh_target, "libacl1_2.2.52-r0.36_cortexa9-vfpv3.ipk",
+                        ssh_path, scp_path)
+                install(ssh_target, "rsync_3.1.0-r0.7_cortexa9-vfpv3.ipk",
+                        ssh_path, scp_path)
+                subprocess.check_call(rsync_cmd)
+            elif e.returncode == 11:
+                # Directory wasn't created, make it and try again.  This keeps the happy path fast.
+                subprocess.check_call(
+                    [ssh_path, ssh_target, "mkdir", "-p", target_dir])
+                subprocess.check_call(rsync_cmd)
+            else:
+                raise e
 
 
 if __name__ == "__main__":
diff --git a/y2019/y2019.json b/y2019/y2019.json
index 719925c..5b34951 100644
--- a/y2019/y2019.json
+++ b/y2019/y2019.json
@@ -44,35 +44,35 @@
   "applications": [
     {
       "name": "drivetrain",
-      "executable_name": "drivetrain.stripped"
+      "executable_name": "drivetrain"
     },
     {
       "name": "trajectory_generator",
-      "executable_name": "trajectory_generator.stripped"
+      "executable_name": "trajectory_generator"
     },
     {
       "name": "superstructure",
-      "executable_name": "superstructure.stripped"
+      "executable_name": "superstructure"
     },
     {
       "name": "server",
-      "executable_name": "server.stripped"
+      "executable_name": "server"
     },
     {
       "name": "logger_main",
-      "executable_name": "logger_main.stripped"
+      "executable_name": "logger_main"
     },
     {
       "name": "joystick_reader",
-      "executable_name": "joystick_reader.stripped"
+      "executable_name": "joystick_reader"
     },
     {
       "name": "autonomous_action",
-      "executable_name": "autonomous_action.stripped"
+      "executable_name": "autonomous_action"
     },
     {
       "name": "wpilib_interface",
-      "executable_name": "wpilib_interface.stripped"
+      "executable_name": "wpilib_interface"
     }
   ],
   "imports": [
diff --git a/y2020/y2020_pi_template.json b/y2020/y2020_pi_template.json
index 8b14bef..759cfee 100644
--- a/y2020/y2020_pi_template.json
+++ b/y2020/y2020_pi_template.json
@@ -126,28 +126,28 @@
   "applications": [
     {
       "name": "message_bridge_client",
-      "executable_name": "message_bridge_client.stripped",
+      "executable_name": "message_bridge_client",
       "nodes": [
         "pi{{ NUM }}"
       ]
     },
     {
       "name": "message_bridge_server",
-      "executable_name": "message_bridge_server.stripped",
+      "executable_name": "message_bridge_server",
       "nodes": [
         "pi{{ NUM }}"
       ]
     },
     {
       "name": "web_proxy",
-      "executable_name": "web_proxy_main.stripped",
+      "executable_name": "web_proxy_main",
       "nodes": [
         "pi{{ NUM }}"
       ]
     },
     {
       "name": "camera_reader",
-      "executable_name": "camera_reader.stripped",
+      "executable_name": "camera_reader",
       "nodes": [
         "pi{{ NUM }}"
       ]
diff --git a/y2020/y2020_roborio.json b/y2020/y2020_roborio.json
index 67916b6..4cdd035 100644
--- a/y2020/y2020_roborio.json
+++ b/y2020/y2020_roborio.json
@@ -332,70 +332,70 @@
   "applications": [
     {
       "name": "drivetrain",
-      "executable_name": "drivetrain.stripped",
+      "executable_name": "drivetrain",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "trajectory_generator",
-      "executable_name": "trajectory_generator.stripped",
+      "executable_name": "trajectory_generator",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "superstructure",
-      "executable_name": "superstructure.stripped",
+      "executable_name": "superstructure",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "joystick_reader",
-      "executable_name": "joystick_reader.stripped",
+      "executable_name": "joystick_reader",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "wpilib_interface",
-      "executable_name": "wpilib_interface.stripped",
+      "executable_name": "wpilib_interface",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "autonomous_action",
-      "executable_name": "autonomous_action.stripped",
+      "executable_name": "autonomous_action",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "web_proxy",
-      "executable_name": "web_proxy_main.stripped",
+      "executable_name": "web_proxy_main",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "message_bridge_client",
-      "executable_name": "message_bridge_client.stripped",
+      "executable_name": "message_bridge_client",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "message_bridge_server",
-      "executable_name": "message_bridge_server.stripped",
+      "executable_name": "message_bridge_server",
       "nodes": [
         "roborio"
       ]
     },
     {
       "name": "logger",
-      "executable_name": "logger_main.stripped",
+      "executable_name": "logger_main",
       "nodes": [
         "roborio"
       ]