Compare commits
3 Commits
c919e160ee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e65591431c | |||
| 1d2cbda50d | |||
| 09feb899d6 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,6 +20,9 @@ android/local.properties
|
|||||||
android/.idea/
|
android/.idea/
|
||||||
*.apk
|
*.apk
|
||||||
*.aab
|
*.aab
|
||||||
|
# Staged release artifacts. The APK is covered above; this also catches the
|
||||||
|
# checksum file that sits beside it.
|
||||||
|
release/
|
||||||
# No exception for debug.keystore: an earlier "!debug.keystore" here deliberately
|
# No exception for debug.keystore: an earlier "!debug.keystore" here deliberately
|
||||||
# un-ignored it, which would have committed a signing key silently had one ever
|
# un-ignored it, which would have committed a signing key silently had one ever
|
||||||
# landed in the tree. Signing keys belong in ~/.android, not in the repo.
|
# landed in the tree. Signing keys belong in ~/.android, not in the repo.
|
||||||
|
|||||||
51
CLAUDE.md
51
CLAUDE.md
@@ -76,7 +76,7 @@ Device notes), but nothing on screen can be verified through a lockscreen and
|
|||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `npm run device:keep-awake` | Stay awake while charging. Do this at the start of a session. |
|
| `npm run device:keep-awake` | Stay awake while charging. Do this at the start of a session. |
|
||||||
| `npm run device:launch` | Check awake/unlocked, force-stop, start, wait for the page to actually parse |
|
| `npm run device:launch` | Check awake/unlocked, force-stop, start, wait for the page (debug builds) |
|
||||||
| `npm run device:awake` | Just the check — prints a loud warning if locked |
|
| `npm run device:awake` | Just the check — prints a loud warning if locked |
|
||||||
| `npm run device:sleep-normal` | Revert the stay-awake setting when finished |
|
| `npm run device:sleep-normal` | Revert the stay-awake setting when finished |
|
||||||
|
|
||||||
@@ -84,6 +84,22 @@ Device notes), but nothing on screen can be verified through a lockscreen and
|
|||||||
starts fine even when the renderer was denied. On failure it prints the
|
starts fine even when the renderer was denied. On failure it prints the
|
||||||
`AppBootManager` denial lines rather than leaving a blank window to puzzle over.
|
`AppBootManager` denial lines rather than leaving a blank window to puzzle over.
|
||||||
|
|
||||||
|
**On a release build it cannot do that, and says so.** A release APK is not
|
||||||
|
`DEBUGGABLE`, so Capacitor never calls `setWebContentsDebuggingEnabled` and
|
||||||
|
`webview_devtools_remote_<pid>` is never opened — the page simply cannot be
|
||||||
|
inspected over adb. Demanding that socket anyway made the helper report
|
||||||
|
`renderer denied` three times for a perfectly healthy app, force-stopping and
|
||||||
|
restarting it each time. It now checks that the process survived and that
|
||||||
|
nothing denied *our* renderer, then prints `page load assumed, not verified`. A
|
||||||
|
blank grey window and a working one are genuinely indistinguishable from here,
|
||||||
|
so that wording is the honest limit, not hedging.
|
||||||
|
|
||||||
|
The denial grep is filtered by package and bounded to the current launch
|
||||||
|
(`logcat -d -T`, timestamp from `adb shell "date +'%m-%d %H:%M:%S.000'"` — the
|
||||||
|
format must be one quoted argument or toybox `date` rejects it). Unfiltered, it
|
||||||
|
printed `com.xiaomi.wearable` denials from minutes earlier and sent one session
|
||||||
|
chasing a denial that had never happened to us.
|
||||||
|
|
||||||
It also reads the pid *after* starting the app. `adb install -r` kills the
|
It also reads the pid *after* starting the app. `adb install -r` kills the
|
||||||
running process, so a pid captured earlier points at a dead process and the
|
running process, so a pid captured earlier points at a dead process and the
|
||||||
devtools socket lookup fails while the app itself is healthy — that produced one
|
devtools socket lookup fails while the app itself is healthy — that produced one
|
||||||
@@ -172,9 +188,29 @@ now empty and bar styling is configured in `capacitor.config.json`
|
|||||||
**No community plugins.** Only first-party: `@capacitor/core`, `/cli`,
|
**No community plugins.** Only first-party: `@capacitor/core`, `/cli`,
|
||||||
`/android`, `/haptics`, `/preferences`, `/app`.
|
`/android`, `/haptics`, `/preferences`, `/app`.
|
||||||
|
|
||||||
**Debug signing.** The debug keystore is stable per-machine, so Phase 2's APK
|
**Debug signing, releases included.** The debug keystore is stable per-machine,
|
||||||
installs over Phase 1's without an uninstall and drill progress survives.
|
so Phase 2's APK installs over Phase 1's without an uninstall and drill progress
|
||||||
`appId` is `dev.ncode.sp404layers` and must not change for the same reason.
|
survives. `appId` is `dev.ncode.sp404layers` and must not change for the same
|
||||||
|
reason.
|
||||||
|
|
||||||
|
The `release` build type reuses that key on purpose, via the plugin's built-in
|
||||||
|
`signingConfig signingConfigs.debug`, so no keystore path or password lands in a
|
||||||
|
tracked file. This only ever ships as a sideloaded APK, never to Play — which
|
||||||
|
rejects debug keys anyway. The trade: anyone can build an APK that upgrades over
|
||||||
|
it, against which there is no key to keep safe for years and no way to strand
|
||||||
|
anyone on a version that can no longer be updated.
|
||||||
|
|
||||||
|
Verified rather than assumed: the release APK's signer digest is
|
||||||
|
`2494c983…551cc`, identical to every debug build, so it installs in place. That
|
||||||
|
matters more than it looks, because a release build is **not `DEBUGGABLE`** — the
|
||||||
|
`adb shell run-as` trick that reads `CapacitorStorage.xml` back out stops working
|
||||||
|
the moment one is installed, and there is no in-app restore to fall back on. A
|
||||||
|
signature change would therefore have cost the drill progress outright.
|
||||||
|
|
||||||
|
`npm run release` (`scripts/release.mjs`) builds it, stages
|
||||||
|
`release/404-Layers-<version>.apk` with a `.sha256` beside it, and fails loudly
|
||||||
|
if apksigner reports no certificate: an unsigned APK builds happily and only
|
||||||
|
fails at install time, on someone else's phone. `release/` is gitignored.
|
||||||
|
|
||||||
## Phase 2: the React port
|
## Phase 2: the React port
|
||||||
|
|
||||||
@@ -187,6 +223,7 @@ kit — the app is four tabs and a reducer, and none of those earn their keep.
|
|||||||
| `npm run test:watch` | Watch mode |
|
| `npm run test:watch` | Watch mode |
|
||||||
| `npm run typecheck` | `tsc --noEmit` |
|
| `npm run typecheck` | `tsc --noEmit` |
|
||||||
| `npm run extract:data` | Regenerate the combo table from `reference/` |
|
| `npm run extract:data` | Regenerate the combo table from `reference/` |
|
||||||
|
| `npm run release` | Build the signed release APK into `release/`, with checksum |
|
||||||
|
|
||||||
```
|
```
|
||||||
src/domain/
|
src/domain/
|
||||||
@@ -245,8 +282,10 @@ beats an afternoon of synthetic input.
|
|||||||
|
|
||||||
**Stay awake needs `svc power stayon true`, not `usb`.** The `usb` form writes
|
**Stay awake needs `svc power stayon true`, not `usb`.** The `usb` form writes
|
||||||
`stay_on_while_plugged_in = 2`, and this phone charges over AC (`USB powered:
|
`stay_on_while_plugged_in = 2`, and this phone charges over AC (`USB powered:
|
||||||
false`), so it silently never engages — `mStayOn` stays false. `true` writes 7
|
false`), so it silently never engages — `mStayOn` stays false. `true` sets every
|
||||||
(any source) and works. The screen timeout is separate and was 30s:
|
charging-source bit and works; measured as **15** here (AC|USB|wireless|dock),
|
||||||
|
not the 7 an earlier note claimed — 7 predates dock charging. The screen timeout
|
||||||
|
is separate and was 30s:
|
||||||
|
|
||||||
```
|
```
|
||||||
adb shell svc power stayon true
|
adb shell svc power stayon true
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -17,6 +17,25 @@ with what it does on that layer. Then it drills you on them.
|
|||||||
Left: sample mode, nothing held — three functions available. Right: the same
|
Left: sample mode, nothing held — three functions available. Right: the same
|
||||||
screen with `SHIFT` held — thirty-five, and every pad now shows what it becomes.
|
screen with `SHIFT` held — thirty-five, and every pad now shows what it becomes.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download the APK from the [latest release](https://git.jawhng.xyz/jawhng/404-Layers/releases),
|
||||||
|
open it on your phone, and allow installing from unknown sources when Android
|
||||||
|
asks. It needs **Android 7.0 or newer** and is about 3 MB.
|
||||||
|
|
||||||
|
It isn't on the Play Store, so Android will warn you it came from outside the
|
||||||
|
store — that warning is about where the file came from, not what's in it. Two
|
||||||
|
things worth knowing before you trust a stranger's APK:
|
||||||
|
|
||||||
|
- It's signed with a **debug key**, not a private one. Nothing here is worth
|
||||||
|
protecting with a key I'd have to keep safe for years, and it means a lost
|
||||||
|
keystore can never strand you on a version that can't be updated.
|
||||||
|
- It asks for `INTERNET` and `VIBRATE`. The vibration is the drill feedback;
|
||||||
|
the internet permission is Capacitor's default and the app never uses it —
|
||||||
|
there are no network calls in the source, and everything it needs is bundled.
|
||||||
|
|
||||||
|
Each release lists a `sha256` if you want to check the download.
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
| Tab | |
|
| Tab | |
|
||||||
@@ -32,8 +51,9 @@ the combos you keep missing keep coming back. Progress is kept on the device.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
It works and it's in daily use on one phone. It is **not** on any app store: it's
|
It works and it's in daily use on one phone. It is **not** on any app store:
|
||||||
a debug-signed APK, sideloaded over `adb`.
|
it's a sideloaded APK, either from the release page above or built from source
|
||||||
|
below.
|
||||||
|
|
||||||
It was built in two passes, and both are still in the repo:
|
It was built in two passes, and both are still in the repo:
|
||||||
|
|
||||||
@@ -70,6 +90,7 @@ npm run dev # http://localhost:5173
|
|||||||
|
|
||||||
| Command | |
|
| Command | |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `npm run release` | Build the signed release APK into `release/`, with its checksum |
|
||||||
| `npm test` | The domain suite |
|
| `npm test` | The domain suite |
|
||||||
| `npm run typecheck` | `tsc --noEmit` |
|
| `npm run typecheck` | `tsc --noEmit` |
|
||||||
| `npm run extract:data` | Regenerate the combo table from `reference/` |
|
| `npm run extract:data` | Regenerate the combo table from `reference/` |
|
||||||
@@ -127,17 +148,6 @@ testable, and immediately surfaced a real bug in the weighted pick.
|
|||||||
`CLAUDE.md` holds the fuller engineering notes, including a number of
|
`CLAUDE.md` holds the fuller engineering notes, including a number of
|
||||||
Android-specific traps that cost real time.
|
Android-specific traps that cost real time.
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
The look follows Teenage Engineering's EP-133 manual: flat fills, heavy outlines,
|
|
||||||
hard zero-blur shadows, one orange accent that means exactly one thing (this
|
|
||||||
control is live), and monospaced technical annotation. The panel is laid out from
|
|
||||||
the hardware — the control groups carry the same silkscreen captions as the
|
|
||||||
device, and the screen is a circular housing behind a rectangular LCD.
|
|
||||||
|
|
||||||
Type is Space Grotesk and Space Mono, self-hosted so the app works with no
|
|
||||||
network.
|
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
The combo data is derived from the SP-404MK2 reference manual (v5.50): control
|
The combo data is derived from the SP-404MK2 reference manual (v5.50): control
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ android {
|
|||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -20,6 +20,11 @@ android {
|
|||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
// Debug key on purpose: this only ever ships as a sideloaded APK, never
|
||||||
|
// to Play. Same signature as every build so far, so a release installs
|
||||||
|
// over an existing one instead of demanding an uninstall. Using the
|
||||||
|
// built-in config keeps the path and password out of this file.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,11 @@
|
|||||||
"sync:www": "bash scripts/sync-www.sh",
|
"sync:www": "bash scripts/sync-www.sh",
|
||||||
"sync": "npm run build && npx cap sync android",
|
"sync": "npm run build && npx cap sync android",
|
||||||
"apk": "npm run sync && cd android && ./gradlew assembleDebug",
|
"apk": "npm run sync && cd android && ./gradlew assembleDebug",
|
||||||
|
"apk:release": "npm run sync && cd android && ./gradlew assembleRelease",
|
||||||
"apk:phase1": "npm run sync:www && npx cap sync android && cd android && ./gradlew assembleDebug",
|
"apk:phase1": "npm run sync:www && npx cap sync android && cd android && ./gradlew assembleDebug",
|
||||||
"install:apk": "adb install -r android/app/build/outputs/apk/debug/app-debug.apk",
|
"install:apk": "adb install -r android/app/build/outputs/apk/debug/app-debug.apk",
|
||||||
|
"install:release": "adb install -r android/app/build/outputs/apk/release/app-release.apk",
|
||||||
|
"release": "node scripts/release.mjs",
|
||||||
"device:awake": "bash scripts/device.sh awake",
|
"device:awake": "bash scripts/device.sh awake",
|
||||||
"device:keep-awake": "bash scripts/device.sh keep-awake",
|
"device:keep-awake": "bash scripts/device.sh keep-awake",
|
||||||
"device:sleep-normal": "bash scripts/device.sh sleep-normal",
|
"device:sleep-normal": "bash scripts/device.sh sleep-normal",
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ ensure_awake() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keep_awake() {
|
keep_awake() {
|
||||||
"$ADB" shell svc power stayon usb >/dev/null
|
# `usb` writes stay_on_while_plugged_in=2 (USB only) and this phone charges
|
||||||
|
# over AC, so it silently never engages. `true` sets every charging-source bit:
|
||||||
|
# measured as 15 here (AC|USB|wireless|dock), not the 7 that older notes claim.
|
||||||
|
"$ADB" shell svc power stayon true >/dev/null
|
||||||
echo "stay-awake while charging: ON (revert with: scripts/device.sh sleep-normal)"
|
echo "stay-awake while charging: ON (revert with: scripts/device.sh sleep-normal)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +61,28 @@ sleep_normal() {
|
|||||||
echo "stay-awake: reverted to system default"
|
echo "stay-awake: reverted to system default"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Release builds are not DEBUGGABLE, so Capacitor never calls
|
||||||
|
# setWebContentsDebuggingEnabled and the devtools socket is never opened. Asking
|
||||||
|
# for it anyway reports "renderer denied" for a perfectly healthy app.
|
||||||
|
is_debuggable() {
|
||||||
|
"$ADB" shell dumpsys package "$PKG" | grep -q DEBUGGABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Device-side clock, for bounding logcat to this launch. The format has to be
|
||||||
|
# quoted as one argument: toybox date rejects a bare `+FMT` after `adb shell`.
|
||||||
|
device_now() {
|
||||||
|
"$ADB" shell "date +'%m-%d %H:%M:%S.000'" | tr -d '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Did the renderer get denied since $1? These lines are emitted whether or not
|
||||||
|
# the build is debuggable, and the package filter matters - other apps are
|
||||||
|
# denied constantly and their lines mean nothing here.
|
||||||
|
denied_since() {
|
||||||
|
"$ADB" logcat -d -T "$1" 2>/dev/null \
|
||||||
|
| grep -E "AppBootManager.*Caller\[$PKG\]|cr_ChildProcessConn: Failed" \
|
||||||
|
| grep -v 'Result\[true\]'
|
||||||
|
}
|
||||||
|
|
||||||
# Wait until the page is actually parsed, not merely until the activity is up:
|
# Wait until the page is actually parsed, not merely until the activity is up:
|
||||||
# the activity starts fine even when the renderer was denied.
|
# the activity starts fine even when the renderer was denied.
|
||||||
page_loaded() {
|
page_loaded() {
|
||||||
@@ -67,6 +92,19 @@ page_loaded() {
|
|||||||
# app itself is healthy - that produced one false "page never loaded".
|
# app itself is healthy - that produced one false "page never loaded".
|
||||||
pid=$("$ADB" shell pidof "$PKG" | tr -d '\r')
|
pid=$("$ADB" shell pidof "$PKG" | tr -d '\r')
|
||||||
[ -n "$pid" ] || return 1
|
[ -n "$pid" ] || return 1
|
||||||
|
|
||||||
|
# Release build: the page cannot be inspected from here at all, so prove what
|
||||||
|
# can be proved - the process survived and nothing denied its renderer. A
|
||||||
|
# blank grey window is indistinguishable from a working one over adb, which is
|
||||||
|
# why this says "assumed" rather than claiming the page loaded.
|
||||||
|
if ! is_debuggable; then
|
||||||
|
sleep 2
|
||||||
|
[ -n "$("$ADB" shell pidof "$PKG" | tr -d '\r')" ] || return 1
|
||||||
|
[ -z "$(denied_since "$1")" ] || return 1
|
||||||
|
echo "app running (pid $pid, release build: page load assumed, not verified)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
for i in $(seq 1 40); do
|
for i in $(seq 1 40); do
|
||||||
sock=$("$ADB" shell cat /proc/net/unix | grep -o "webview_devtools_remote_$pid" | head -1)
|
sock=$("$ADB" shell cat /proc/net/unix | grep -o "webview_devtools_remote_$pid" | head -1)
|
||||||
if [ -n "$sock" ]; then
|
if [ -n "$sock" ]; then
|
||||||
@@ -82,19 +120,23 @@ page_loaded() {
|
|||||||
|
|
||||||
launch() {
|
launch() {
|
||||||
ensure_awake || return 1
|
ensure_awake || return 1
|
||||||
local attempt
|
local attempt started
|
||||||
for attempt in $(seq 1 $ATTEMPTS); do
|
for attempt in $(seq 1 $ATTEMPTS); do
|
||||||
|
started=$(device_now)
|
||||||
"$ADB" shell am force-stop "$PKG"
|
"$ADB" shell am force-stop "$PKG"
|
||||||
"$ADB" shell am start -W -n "$ACT" >/dev/null
|
"$ADB" shell am start -W -n "$ACT" >/dev/null
|
||||||
if page_loaded; then
|
if page_loaded "$started"; then
|
||||||
[ "$attempt" -gt 1 ] && echo "(took $attempt attempts - post-install renderer denial)"
|
[ "$attempt" -gt 1 ] && echo "(took $attempt attempts - post-install renderer denial)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo "attempt $attempt: renderer denied, retrying" >&2
|
echo "attempt $attempt: no page, retrying" >&2
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "ERROR: page never loaded after $ATTEMPTS attempts." >&2
|
echo "ERROR: page never loaded after $ATTEMPTS attempts." >&2
|
||||||
"$ADB" logcat -d | grep -E 'AppBootManager.*Result\[false\]|ChildProcessConn: Failed' | tail -4 >&2
|
# Bounded to this launch and to our package. Unfiltered, this printed other
|
||||||
|
# apps' denials from minutes earlier and sent one debugging session chasing a
|
||||||
|
# denial that had never happened to us.
|
||||||
|
denied_since "$started" | tail -4 >&2
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
scripts/release.mjs
Normal file
44
scripts/release.mjs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Builds the release APK and stages it under release/ under a name a stranger
|
||||||
|
* can recognise, next to the checksum to verify a download against.
|
||||||
|
*
|
||||||
|
* It verifies the signature rather than trusting that the signing config took:
|
||||||
|
* an unsigned APK builds happily and only fails at install time, on someone
|
||||||
|
* else's phone.
|
||||||
|
*/
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||||
|
|
||||||
|
execFileSync("npm", ["run", "apk:release"], { cwd: root, stdio: "inherit" });
|
||||||
|
|
||||||
|
const built = join(root, "android/app/build/outputs/apk/release/app-release.apk");
|
||||||
|
const name = `404-Layers-${version}.apk`;
|
||||||
|
const out = join(root, "release", name);
|
||||||
|
|
||||||
|
mkdirSync(dirname(out), { recursive: true });
|
||||||
|
copyFileSync(built, out);
|
||||||
|
|
||||||
|
const bytes = readFileSync(out);
|
||||||
|
const sha = createHash("sha256").update(bytes).digest("hex");
|
||||||
|
writeFileSync(`${out}.sha256`, `${sha} ${name}\n`);
|
||||||
|
|
||||||
|
// build-tools is pinned in CLAUDE.md; a missing apksigner should fail loudly.
|
||||||
|
const sdk = process.env.ANDROID_HOME ?? join(process.env.HOME ?? "", "Android/Sdk");
|
||||||
|
const certs = execFileSync(join(sdk, "build-tools/36.1.0/apksigner"), ["verify", "--print-certs", out], {
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
|
||||||
|
const digest = certs.match(/certificate SHA-256 digest: (\w+)/)?.[1] ?? "";
|
||||||
|
if (digest === "") throw new Error(`apksigner reported no certificate:\n${certs}`);
|
||||||
|
|
||||||
|
console.log(`\n${name} ${(bytes.length / 1e6).toFixed(2)} MB`);
|
||||||
|
console.log(`file sha256 ${sha}`);
|
||||||
|
console.log(`cert sha256 ${digest}`);
|
||||||
|
console.log(`\nStaged at ${out}`);
|
||||||
Reference in New Issue
Block a user