feat: APK distribution pipeline — server hosting + Pi auto-update + /app routes

This commit is contained in:
Mathieu Moran
2026-08-15 20:52:38 -04:00
parent 5039b90bc6
commit 5a5df2b323
11 changed files with 320 additions and 16 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# One-time install of the Android APK auto-updater on the Pi.
# Run ON THE PI as the `yaris` user (needs sudo):
# cd ~/app/yaris-obd && bash deploy/install_app_updater.sh
#
# Installs a systemd timer that polls the remote server for the latest APK
# and mirrors it to static/app/ so the phone can install from
# http://10.42.0.1/app without leaving the car hotspot.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
sudo systemctl stop yaris-app-updater.timer yaris-app-updater.service 2>/dev/null || true
sudo install -m 755 "$REPO_DIR/deploy/yaris_app_updater.py" /usr/local/bin/yaris-app-updater
sudo install -m 644 "$REPO_DIR/deploy/yaris-app-updater.service" /etc/systemd/system/yaris-app-updater.service
sudo install -m 644 "$REPO_DIR/deploy/yaris-app-updater.timer" /etc/systemd/system/yaris-app-updater.timer
sudo systemctl daemon-reload
sudo systemctl enable --now yaris-app-updater.timer
sudo systemctl start yaris-app-updater.service
echo "APK updater installed. Check: systemctl status yaris-app-updater.service"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=yaris-obd Android APK mirror from the remote server
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
TimeoutStartSec=600
Environment=YARIS_SERVER_URL=https://yaris.ceaf.dev
Environment=YARIS_APP_DIR=/home/yaris/app/yaris-obd/static/app
ExecStart=/usr/local/bin/yaris-app-updater
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Periodically mirror the Android APK to the car hotspot
[Timer]
OnBootSec=2min
OnUnitActiveSec=1h
RandomizedDelaySec=5min
Unit=yaris-app-updater.service
[Install]
WantedBy=timers.target
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Mirror the latest Android APK from the remote server to the Pi's hotspot.
Polls GET /api/v1/app/latest on the server, downloads /app/yaris-obd.apk when
the sha256 differs from the local copy, verifies it, and atomically replaces
static/app/yaris-obd.apk — so the phone can install from http://10.42.0.1/app
without leaving the car WiFi.
Runs on a systemd timer (see deploy/yaris-app-updater.{service,timer}).
Stdlib only — no pip dependencies.
"""
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
SERVER_URL = os.environ.get("YARIS_SERVER_URL", "https://yaris.ceaf.dev").rstrip("/")
APP_DIR = os.environ.get("YARIS_APP_DIR", "/home/yaris/app/yaris-obd/static/app")
APK_PATH = os.path.join(APP_DIR, "yaris-obd.apk")
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def fetch_json(url, timeout=20):
with urllib.request.urlopen(url, timeout=timeout) as r:
return json.load(r)
def main():
try:
manifest = fetch_json(f"{SERVER_URL}/api/v1/app/latest")
except urllib.error.HTTPError as e:
if e.code == 404:
print("No app published on the server yet; skipping.")
return 0
print(f"latest check failed: HTTP {e.code}")
return 1
except Exception as e:
print(f"latest check failed: {e}")
return 1
sha = manifest.get("sha256")
if not sha:
print("manifest missing sha256")
return 1
if os.path.isfile(APK_PATH):
try:
if sha256_file(APK_PATH) == sha:
print(f"APK up to date (version {manifest.get('version')})")
return 0
except Exception as e:
print(f"local hash failed: {e}")
os.makedirs(APP_DIR, exist_ok=True)
tmp = APK_PATH + ".part"
try:
with urllib.request.urlopen(f"{SERVER_URL}/app/yaris-obd.apk", timeout=600) as r, open(tmp, "wb") as f:
while True:
chunk = r.read(1024 * 1024)
if not chunk:
break
f.write(chunk)
if sha256_file(tmp) != sha:
print("downloaded APK sha256 mismatch — discarding")
os.remove(tmp)
return 1
os.replace(tmp, APK_PATH)
print(f"Updated APK to version {manifest.get('version')} ({os.path.getsize(APK_PATH)} bytes)")
return 0
except Exception as e:
print(f"download failed: {e}")
if os.path.isfile(tmp):
try:
os.remove(tmp)
except Exception:
pass
return 1
if __name__ == "__main__":
sys.exit(main())