mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:38:52 +03:00
Add gzip-aware crash upload and OTA staging build/deploy tooling.
This restores the interrupted stash work on a dedicated feature branch, including backend decoding support, Dockerized build scripts, and staging channel artifacts for end-to-end OTA validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ __pycache__
|
|||||||
desktop/session-studio/build/
|
desktop/session-studio/build/
|
||||||
build/
|
build/
|
||||||
config.php.alpine
|
config.php.alpine
|
||||||
|
out/
|
||||||
|
ota-publish/
|
||||||
|
|||||||
@@ -71,4 +71,6 @@ COPY scripts/ci-build.sh scripts/ci-build-and-publish-ota.sh scripts/generate-ot
|
|||||||
RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/ci-build-and-publish-ota.sh \
|
RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/ci-build-and-publish-ota.sh \
|
||||||
/workspace/scripts/generate-ota-v0.sh /workspace/scripts/build-native-codecs.sh
|
/workspace/scripts/generate-ota-v0.sh /workspace/scripts/build-native-codecs.sh
|
||||||
|
|
||||||
|
# Full tree is bind-mounted in CI; default command runs the pipeline.
|
||||||
|
# OTA staging publish: ./scripts/ci-build-and-publish-ota.sh (see docs/BUILD_DEPLOY.md).
|
||||||
CMD ["./scripts/ci-build.sh"]
|
CMD ["./scripts/ci-build.sh"]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.io.OutputStream;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
/** POST crash JSON to the PHP receiver. */
|
/** POST crash JSON to the PHP receiver. */
|
||||||
public final class CrashUploadClient {
|
public final class CrashUploadClient {
|
||||||
@@ -27,6 +28,14 @@ public final class CrashUploadClient {
|
|||||||
|
|
||||||
private CrashUploadClient() {}
|
private CrashUploadClient() {}
|
||||||
|
|
||||||
|
private static byte[] gzip(byte[] plain) throws java.io.IOException {
|
||||||
|
ByteArrayOutputStream buf = new ByteArrayOutputStream(Math.max(256, plain.length / 4));
|
||||||
|
try (GZIPOutputStream gz = new GZIPOutputStream(buf)) {
|
||||||
|
gz.write(plain);
|
||||||
|
}
|
||||||
|
return buf.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean upload(String uploadUrl, JSONObject report) {
|
public static boolean upload(String uploadUrl, JSONObject report) {
|
||||||
if (uploadUrl == null || uploadUrl.isEmpty() || report == null) {
|
if (uploadUrl == null || uploadUrl.isEmpty() || report == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -40,7 +49,9 @@ public final class CrashUploadClient {
|
|||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
byte[] body = report.toString().getBytes(StandardCharsets.UTF_8);
|
conn.setRequestProperty("Content-Encoding", "gzip");
|
||||||
|
byte[] json = report.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] body = gzip(json);
|
||||||
try (OutputStream out = conn.getOutputStream()) {
|
try (OutputStream out = conn.getOutputStream()) {
|
||||||
out.write(body);
|
out.write(body);
|
||||||
}
|
}
|
||||||
|
|||||||
52
docker-compose.build.yml
Normal file
52
docker-compose.build.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Android Cast — Docker build + OTA staging publish
|
||||||
|
#
|
||||||
|
# export OTA_BASE_URL=https://apps.f0xx.org
|
||||||
|
# docker compose -f docker-compose.build.yml run --rm android-build
|
||||||
|
#
|
||||||
|
# Outputs on the host: ./out/android_cast-latest.apk ./out/ota/v0/...
|
||||||
|
#
|
||||||
|
# Publish APK only from an existing build (skip compile):
|
||||||
|
# docker compose -f docker-compose.build.yml run --rm ota-publish-only
|
||||||
|
|
||||||
|
services:
|
||||||
|
android-build:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: androidcast-ci:latest
|
||||||
|
working_dir: /workspace
|
||||||
|
volumes:
|
||||||
|
- .:/workspace
|
||||||
|
- gradle-cache:/root/.gradle
|
||||||
|
environment:
|
||||||
|
OTA_BASE_URL: ${OTA_BASE_URL:-}
|
||||||
|
OTA_CHANNEL: ${OTA_CHANNEL:-staging}
|
||||||
|
GRADLE_TASK: ${GRADLE_TASK:-assembleDebug}
|
||||||
|
SKIP_NATIVE: ${SKIP_NATIVE:-0}
|
||||||
|
SKIP_TESTS: ${SKIP_TESTS:-0}
|
||||||
|
RELEASE_NOTES: ${RELEASE_NOTES:-}
|
||||||
|
OUT_DIR: /workspace/out
|
||||||
|
command: ["./scripts/ci-build-and-publish-ota.sh"]
|
||||||
|
|
||||||
|
ota-publish-only:
|
||||||
|
image: androidcast-ci:latest
|
||||||
|
working_dir: /workspace
|
||||||
|
volumes:
|
||||||
|
- .:/workspace
|
||||||
|
environment:
|
||||||
|
OTA_BASE_URL: ${OTA_BASE_URL:?set OTA_BASE_URL}
|
||||||
|
OTA_CHANNEL: ${OTA_CHANNEL:-staging}
|
||||||
|
APK_PATH: ${APK_PATH:-/workspace/app/build/outputs/apk/debug/app-debug.apk}
|
||||||
|
RELEASE_NOTES: ${RELEASE_NOTES:-}
|
||||||
|
entrypoint: ["/bin/bash", "-c"]
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
./scripts/generate-ota-v0.sh "$${APK_PATH}" "$${OTA_BASE_URL}" /workspace/out/ota-publish "$${OTA_CHANNEL}"
|
||||||
|
mkdir -p /workspace/out/ota
|
||||||
|
cp -rf /workspace/out/ota-publish/v0 /workspace/out/ota/
|
||||||
|
cp -f "$${APK_PATH}" /workspace/out/android_cast-latest.apk
|
||||||
|
echo "Published to /workspace/out/ota/v0/ota/channel/$${OTA_CHANNEL}.json"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
gradle-cache:
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- **Main process:** `CrashReporter.install()` — any uncaught Java exception (cast or idle UI) → `files/crash_reports/pending/crash_yyyyMMdd_HHmmss.json` (same stamp as `session_*.json`). Active cast session stats are flushed with `globals.reason: "crash"` when applicable.
|
- **Main process:** `CrashReporter.install()` — any uncaught Java exception (cast or idle UI) → `files/crash_reports/pending/crash_yyyyMMdd_HHmmss.json` (same stamp as `session_*.json`). Active cast session stats are flushed with `globals.reason: "crash"` when applicable.
|
||||||
- **Runtime context:** every report includes `runtime_context` (foreground activity, `CastActiveState`, last `session_*.json` name) and `scenario` (`cast_session` vs `app_runtime`).
|
- **Runtime context:** every report includes `runtime_context` (foreground activity, `CastActiveState`, last `session_*.json` name) and `scenario` (`cast_session` vs `app_runtime`).
|
||||||
- **Watcher process:** `CrashWatcherService` in `:crashwatcher` — reads `settings.json` → `crash`, uploads pending reports, ingests native stub files.
|
- **Watcher process:** `CrashWatcherService` in `:crashwatcher` — reads `settings.json` → `crash`, uploads pending reports (JSON body gzip-compressed), ingests native stub files.
|
||||||
- **Native:** `crash_hook.c` (unwind + `dladdr`) writes stubs to `crash_reports/native/`; watcher converts to schema v1 JSON.
|
- **Native:** `crash_hook.c` (unwind + `dladdr`) writes stubs to `crash_reports/native/`; watcher converts to schema v1 JSON.
|
||||||
- **User toggle:** Settings drawer → **Send anonymous crash logs** (`AppPreferences.send_anonymous_crash_logs`, default on).
|
- **User toggle:** Settings drawer → **Send anonymous crash logs** (`AppPreferences.send_anonymous_crash_logs`, default on).
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ Store in DB:
|
|||||||
|
|
||||||
## Upload API (`POST …/api/upload.php`)
|
## Upload API (`POST …/api/upload.php`)
|
||||||
|
|
||||||
|
Clients may send **`Content-Encoding: gzip`** with a gzip-compressed JSON body (same schema). The BE nginx upload `location` should use `gunzip on` + `gunzip_types application/json`; PHP accepts plain or gzip.
|
||||||
|
|
||||||
Phases:
|
Phases:
|
||||||
|
|
||||||
1. **Phase A (now):** Accept extra JSON fields; persist inside `payload_json`; console search includes `ingest.source` in blob.
|
1. **Phase A (now):** Accept extra JSON fields; persist inside `payload_json`; console search includes `ingest.source` in blob.
|
||||||
|
|||||||
14
docs/OTA.md
14
docs/OTA.md
@@ -111,10 +111,20 @@ When `ota.trusted=true` and build is debug, strict checksum/sign enforcement is
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x scripts/generate-ota-v0.sh
|
chmod +x scripts/generate-ota-v0.sh
|
||||||
./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk https://your-host:port ./ota-publish
|
./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk https://your-host:port ./ota-publish staging
|
||||||
```
|
```
|
||||||
|
|
||||||
Upload the contents of `ota-publish/` to your web root so `https://your-host/v0/ota/...` resolves.
|
Fourth argument is the **channel** file name (`stable`, `staging`, `next`, …). Upload `ota-publish/v0/` to your web root so `https://your-host/v0/ota/...` resolves.
|
||||||
|
|
||||||
|
### Docker build + staging channel
|
||||||
|
|
||||||
|
See [BUILD_DEPLOY.md](BUILD_DEPLOY.md):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OTA_BASE_URL=https://apps.f0xx.org
|
||||||
|
./scripts/docker-build-ota.sh
|
||||||
|
./scripts/deploy-ota-staging.sh # after setting OTA_DEPLOY_TARGET
|
||||||
|
```
|
||||||
|
|
||||||
## App configuration
|
## App configuration
|
||||||
|
|
||||||
|
|||||||
@@ -168,4 +168,5 @@ See [ndk/README.md](../ndk/README.md).
|
|||||||
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
|
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
|
||||||
- [OPEN_API.md](OPEN_API.md) — crash ingest triage API
|
- [OPEN_API.md](OPEN_API.md) — crash ingest triage API
|
||||||
- [OTA.md](OTA.md) — backend layout v0
|
- [OTA.md](OTA.md) — backend layout v0
|
||||||
|
- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker build, staging OTA, deploy flow
|
||||||
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
|
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
|
||||||
|
|||||||
@@ -109,6 +109,22 @@ curl -X POST http://127.0.0.1:8080/api/upload.php \
|
|||||||
-d @../sample_crash_java.json
|
-d @../sample_crash_java.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Gzip ingest** (Android sends `Content-Encoding: gzip`; plain JSON still works):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/test_gzip_upload.sh
|
||||||
|
# BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/test_gzip_upload.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
On the **BE nginx** upload `location`, enable gunzip so PHP receives plain JSON (see `nginx.vm.conf`):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
gunzip on;
|
||||||
|
gunzip_types application/json;
|
||||||
|
```
|
||||||
|
|
||||||
|
PHP also decodes gzip if the header reaches FPM (`read_crash_upload_body()` in `bootstrap.php`). After deploy, sync `public/` + `src/` and reload nginx.
|
||||||
|
|
||||||
## Production (nginx + PHP-FPM)
|
## Production (nginx + PHP-FPM)
|
||||||
|
|
||||||
**Deploy topology (FE/BE, ports, sshfs):** [docs/INFRA.md](../../../docs/INFRA.md).
|
**Deploy topology (FE/BE, ports, sshfs):** [docs/INFRA.md](../../../docs/INFRA.md).
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/app/androidcast_project/crashes/api/upload\.php$ {
|
location ~ ^/app/androidcast_project/crashes/api/upload\.php$ {
|
||||||
|
gunzip on;
|
||||||
|
gunzip_types application/json;
|
||||||
alias /var/www/androidcast_crashes/public/api/upload.php;
|
alias /var/www/androidcast_crashes/public/api/upload.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/androidcast_crashes/public/api/upload.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/androidcast_crashes/public/api/upload.php;
|
||||||
fastcgi_pass unix:/run/php-fpm/www.sock;
|
fastcgi_pass unix:/run/php-fpm/www.sock;
|
||||||
client_max_body_size 4m;
|
client_max_body_size 8m;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/app/androidcast_project/crashes/.+\.php$ {
|
location ~ ^/app/androidcast_project/crashes/.+\.php$ {
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location = /app/androidcast_project/crashes/api/upload.php {
|
location = /app/androidcast_project/crashes/api/upload.php {
|
||||||
|
# Decompress gzip uploads from Android before PHP (needs ngx_http_gunzip_module).
|
||||||
|
gunzip on;
|
||||||
|
gunzip_types application/json;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_pass unix:/run/php-fpm.socket;
|
fastcgi_pass unix:/run/php-fpm.socket;
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/api/upload.php;
|
fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend/public/api/upload.php;
|
||||||
fastcgi_param SCRIPT_NAME /app/androidcast_project/crashes/api/upload.php;
|
fastcgi_param SCRIPT_NAME /app/androidcast_project/crashes/api/upload.php;
|
||||||
fastcgi_param REQUEST_URI $request_uri;
|
fastcgi_param REQUEST_URI $request_uri;
|
||||||
client_max_body_size 4m;
|
client_max_body_size 8m;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /app/androidcast_project/crashes/ {
|
location ^~ /app/androidcast_project/crashes/ {
|
||||||
|
|||||||
@@ -13,10 +13,13 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||||
|
|
||||||
$raw = file_get_contents('php://input');
|
$raw = read_crash_upload_body();
|
||||||
if ($raw === false || $raw === '') {
|
if ($raw === null) {
|
||||||
json_out(['ok' => false, 'error' => 'empty body'], 400);
|
json_out(['ok' => false, 'error' => 'empty body'], 400);
|
||||||
}
|
}
|
||||||
|
if ($raw === false) {
|
||||||
|
json_out(['ok' => false, 'error' => 'invalid gzip body'], 400);
|
||||||
|
}
|
||||||
$data = json_decode($raw, true);
|
$data = json_decode($raw, true);
|
||||||
if (!is_array($data)) {
|
if (!is_array($data)) {
|
||||||
json_out(['ok' => false, 'error' => 'invalid json'], 400);
|
json_out(['ok' => false, 'error' => 'invalid json'], 400);
|
||||||
|
|||||||
33
examples/crash_reporter/backend/scripts/test_gzip_upload.sh
Executable file
33
examples/crash_reporter/backend/scripts/test_gzip_upload.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Smoke-test gzip crash upload (plain JSON still accepted).
|
||||||
|
# Usage: BASE=http://127.0.0.1:8080/app/androidcast_project/crashes ./scripts/test_gzip_upload.sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SAMPLE="${ROOT}/../sample_crash_java.json"
|
||||||
|
BASE="${BASE:-http://127.0.0.1:8080/app/androidcast_project/crashes}"
|
||||||
|
URL="${BASE}/api/upload.php"
|
||||||
|
RID="gzip-test-$(date +%s)"
|
||||||
|
|
||||||
|
if [ ! -f "$SAMPLE" ]; then
|
||||||
|
echo "Missing $SAMPLE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="$(mktemp)"
|
||||||
|
jq --arg rid "$RID" '.report_id = $rid' "$SAMPLE" > "$BODY"
|
||||||
|
GZIP="$(mktemp)"
|
||||||
|
gzip -c -f "$BODY" > "$GZIP"
|
||||||
|
|
||||||
|
echo "POST plain JSON..."
|
||||||
|
curl -sS -o /dev/null -w "plain:%{http_code}\n" -X POST "$URL" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary @"$BODY"
|
||||||
|
|
||||||
|
echo "POST gzip (Content-Encoding: gzip)..."
|
||||||
|
curl -sS -w "\n%{http_code}\n" -X POST "$URL" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Content-Encoding: gzip' \
|
||||||
|
--data-binary @"$GZIP"
|
||||||
|
|
||||||
|
rm -f "$BODY" "$GZIP"
|
||||||
@@ -79,6 +79,25 @@ function json_out(array $data, int $code = 200): void {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw POST body for crash ingest. Supports plain JSON or Content-Encoding: gzip
|
||||||
|
* (when nginx gunzip is off or the client talks to PHP directly).
|
||||||
|
*
|
||||||
|
* @return string|null decoded JSON text, null if empty, false if gzip decode failed
|
||||||
|
*/
|
||||||
|
function read_crash_upload_body(): string|false|null {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
if ($raw === false || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$encoding = $_SERVER['HTTP_CONTENT_ENCODING'] ?? '';
|
||||||
|
if ($encoding !== '' && stripos($encoding, 'gzip') !== false) {
|
||||||
|
$decoded = @gzdecode($raw);
|
||||||
|
return $decoded === false ? false : $decoded;
|
||||||
|
}
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
/** Short lines for expandable list preview (not full report detail). */
|
/** Short lines for expandable list preview (not full report detail). */
|
||||||
function report_brief_lines(array $row, bool $grouped): array {
|
function report_brief_lines(array $row, bool $grouped): array {
|
||||||
if ($grouped) {
|
if ($grouped) {
|
||||||
|
|||||||
4
examples/ota/v0/ota/channel/staging.json
Normal file
4
examples/ota/v0/ota/channel/staging.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"schema": "v0",
|
||||||
|
"manifestUrl": "https://apps.f0xx.org/v0/ota/00/00.01/00.01.00/android_cast_00.01.00.00_manifest.json"
|
||||||
|
}
|
||||||
32
scripts/deploy-ota-staging.sh
Executable file
32
scripts/deploy-ota-staging.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rsync ./out/ota/v0 to a remote web root (staging OTA tree).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# OTA_DEPLOY_TARGET=foxx@10.7.16.128:/var/www/localhost/htdocs/ \
|
||||||
|
# ./scripts/deploy-ota-staging.sh
|
||||||
|
#
|
||||||
|
# Dry run:
|
||||||
|
# OTA_DEPLOY_DRY_RUN=1 ./scripts/deploy-ota-staging.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SRC="${ROOT}/out/ota/v0"
|
||||||
|
TARGET="${OTA_DEPLOY_TARGET:?Set OTA_DEPLOY_TARGET=user@host:/path/to/web/root}"
|
||||||
|
|
||||||
|
if [[ ! -d "$SRC" ]]; then
|
||||||
|
echo "Missing ${SRC} — run ./scripts/docker-build-ota.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RSYNC_OPTS=(-av --delete)
|
||||||
|
if [[ "${OTA_DEPLOY_DRY_RUN:-}" == "1" ]]; then
|
||||||
|
RSYNC_OPTS+=(--dry-run -v)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Sync ${SRC}/ -> ${TARGET}/v0/"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" "${SRC}/" "${TARGET%/}/v0/"
|
||||||
|
|
||||||
|
echo "Channel pointer (after deploy):"
|
||||||
|
if [[ -f "${ROOT}/out/BUILD_INFO.json" ]]; then
|
||||||
|
grep channelUrl "${ROOT}/out/BUILD_INFO.json" || true
|
||||||
|
fi
|
||||||
39
scripts/docker-build-ota.sh
Executable file
39
scripts/docker-build-ota.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Host wrapper: build androidcast-ci image and run build + OTA staging publish.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# export OTA_BASE_URL=https://apps.f0xx.org
|
||||||
|
# ./scripts/docker-build-ota.sh
|
||||||
|
#
|
||||||
|
# Artifacts: ./out/android_cast-latest.apk ./out/ota/v0/ota/...
|
||||||
|
#
|
||||||
|
# Deploy to BE (example — adjust host/path):
|
||||||
|
# rsync -av ./out/ota/v0/ user@be:/var/www/localhost/htdocs/v0/
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
IMAGE="${ANDROIDCAST_CI_IMAGE:-androidcast-ci:latest}"
|
||||||
|
|
||||||
|
docker build -t "$IMAGE" -f "${ROOT}/Dockerfile" "${ROOT}"
|
||||||
|
|
||||||
|
mkdir -p "${ROOT}/out"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${ROOT}:/workspace" \
|
||||||
|
-w /workspace \
|
||||||
|
-e OTA_BASE_URL="${OTA_BASE_URL:-}" \
|
||||||
|
-e OTA_CHANNEL="${OTA_CHANNEL:-staging}" \
|
||||||
|
-e GRADLE_TASK="${GRADLE_TASK:-assembleDebug}" \
|
||||||
|
-e SKIP_NATIVE="${SKIP_NATIVE:-0}" \
|
||||||
|
-e SKIP_TESTS="${SKIP_TESTS:-0}" \
|
||||||
|
-e RELEASE_NOTES="${RELEASE_NOTES:-}" \
|
||||||
|
-e OUT_DIR=/workspace/out \
|
||||||
|
"$IMAGE" \
|
||||||
|
./scripts/ci-build-and-publish-ota.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. APK: ${ROOT}/out/android_cast-latest.apk"
|
||||||
|
if [[ -f "${ROOT}/out/BUILD_INFO.json" ]]; then
|
||||||
|
echo "OTA channel URL:"
|
||||||
|
sed -n 's/.*"channelUrl": "\([^"]*\)".*/\1/p' "${ROOT}/out/BUILD_INFO.json" | head -1
|
||||||
|
fi
|
||||||
@@ -4,9 +4,12 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/generate-ota-v0.sh path/to/app-release.apk https://host[:port] [out-dir] [channel]
|
# ./scripts/generate-ota-v0.sh path/to/app-release.apk https://host[:port] [out-dir] [channel]
|
||||||
#
|
#
|
||||||
|
# channel — JSON file under v0/ota/channel/ (default: stable). Use staging for QA.
|
||||||
|
# RELEASE_NOTES — optional env, embedded in manifest.
|
||||||
|
#
|
||||||
# channel — JSON under v0/ota/channel/ (stable, staging, dev, nightly, or custom)
|
# channel — JSON under v0/ota/channel/ (stable, staging, dev, nightly, or custom)
|
||||||
# Writes under out-dir (default: ./ota-publish):
|
# Writes under out-dir (default: ./ota-publish):
|
||||||
# v0/ota/channel/stable.json
|
# v0/ota/channel/<channel>.json
|
||||||
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_manifest.json
|
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_manifest.json
|
||||||
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_sign.json
|
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_sign.json
|
||||||
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB.otapkg
|
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB.otapkg
|
||||||
@@ -17,6 +20,19 @@ apk="${1:?APK path required}"
|
|||||||
host_base="${2:?Base URL required, e.g. https://192.168.1.1:8080}"
|
host_base="${2:?Base URL required, e.g. https://192.168.1.1:8080}"
|
||||||
out_dir="${3:-ota-publish}"
|
out_dir="${3:-ota-publish}"
|
||||||
channel="${4:-stable}"
|
channel="${4:-stable}"
|
||||||
|
release_notes="${RELEASE_NOTES:-}"
|
||||||
|
json_notes() {
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -n --arg n "$release_notes" '$n'
|
||||||
|
else
|
||||||
|
local esc="${release_notes//\\/\\\\}"
|
||||||
|
esc="${esc//\"/\\\"}"
|
||||||
|
esc="${esc//$'\n'/\\n}"
|
||||||
|
esc="${esc//$'\r'/}"
|
||||||
|
printf '"%s"' "$esc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
NOTES_JSON="$(json_notes)"
|
||||||
|
|
||||||
if [[ ! -f "$apk" ]]; then
|
if [[ ! -f "$apk" ]]; then
|
||||||
echo "APK not found: $apk" >&2
|
echo "APK not found: $apk" >&2
|
||||||
@@ -84,7 +100,7 @@ cat >"${rel_dir}/${manifest_name}" <<EOF
|
|||||||
"signUrl": "${sign_url}",
|
"signUrl": "${sign_url}",
|
||||||
"sizeBytes": ${apk_size},
|
"sizeBytes": ${apk_size},
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"releaseNotes": ""
|
"releaseNotes": ${NOTES_JSON}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -113,7 +129,7 @@ cat >"${rel_dir}/${manifest_name}" <<EOF
|
|||||||
"bundleSha256": "${bundle_sha256}",
|
"bundleSha256": "${bundle_sha256}",
|
||||||
"bundleSizeBytes": ${bundle_size},
|
"bundleSizeBytes": ${bundle_size},
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"releaseNotes": ""
|
"releaseNotes": ${NOTES_JSON}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user