Files
SoulStream/handoff/rounds/patch-round43.cjs
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

117 lines
6.7 KiB
JavaScript

// Round forty-three: the build nobody has made. Every APK in this port is
// `assembleArm64Debug`, and the generated project turns R8 on for `release`
// and only for `release` — while tauri finds a plugin class by string, files
// every @Command under `commands[method.name]`, and lets Jackson fill every
// @InvokeArg field by field. Minification breaks exactly that, and it breaks
// it silently: the app builds, installs, launches, and then a tap answers
// "No command pickFolder found for plugin ...". So: one consumer-rules.pro
// per plugin module (registered by one Gradle line each, the shape
// tauri-android and tauri-plugin-dialog already use here — opener ships the
// file and never registers it, which is the failure mode), one app-wide
// file for what belongs to no plugin — the runtime annotation attributes, a
// -dontwarn pair for the two java.beans classes jackson-databind references
// and Android has not, and the four @Command methods declared on
// app.tauri.plugin.Plugin that no rule of tauri's own names — and a
// `--release` flag on build.sh that is off by default, so the deploy pipeline
// keeps the path it has run every twenty minutes for a month. No features, no
// UI, no Rust, no Kotlin, and neither walk is touched: the release build is a
// hand list, not a walk.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
const NL = String.fromCharCode(10);
/* Five of the writes below are whole new files, which succeed as happily the
second time as the first, while the Gradle and build.sh `rep`s would throw
somewhere in the middle and leave half a tree. Refuse a second run here,
before a byte is touched: the app-wide rules file is what this round
creates and nothing else in the repo makes it. */
if (fs.existsSync(root + 'src-tauri/android/proguard-soulstream.pro')) {
throw new Error('src-tauri/android/proguard-soulstream.pro already exists — round 43 has already run against ' + root);
}
// ── the rules that belong to the app, kept where gen/ cannot lose them ─
/* first, so the guard above covers a run that dies anywhere below it */
fs.writeFileSync(root + 'src-tauri/android/proguard-soulstream.pro', ins('ins-r43-proguard-app.txt') + NL);
// ── the rules that travel with each plugin ────────────────────────────
/* A consumer file is handed to whatever app links the module and does
nothing at all unless that app minifies, so the debug APK is untouched;
and because it lives in the module rather than in gen/android, a
regenerated project still has it. One Gradle line each. The anchor is the
whole `defaultConfig` block rather than its `minSdk` line, so it cannot
match again once the line is in — a second pass over a half-applied tree
refuses rather than writing the line twice. */
const plugins = [
['storage', 'ins-r43-consumer-storage.txt'],
['media-session', 'ins-r43-consumer-media.txt'],
['share', 'ins-r43-consumer-share.txt'],
['transfer-notice', 'ins-r43-consumer-notice.txt'],
];
for (const [name, file] of plugins) {
const dir = 'crates/tauri-plugin-' + name + '/android/';
fs.writeFileSync(root + dir + 'consumer-rules.pro', ins(file) + NL);
load(dir + 'build.gradle.kts');
rep(` defaultConfig {
minSdk = 26
}`,
` defaultConfig {
minSdk = 26
// the plugin's own R8 keep rules, handed to whatever app links this
// module — inert unless that app minifies, which only the release
// build type does
consumerProguardFiles("consumer-rules.pro")
}`);
save();
}
// ── build.sh: a release flag, off by default ──────────────────────────
load('src-tauri/android/build.sh');
/* the flag block sits between the toolchain and the cargo line, so a sibling
round appending at the end or editing the cargo line does not meet it */
rep(`source src-tauri/android/env.sh`,
`source src-tauri/android/env.sh` + NL + ins('ins-r43-build-flag.txt'));
/* $CARGO_EXTRA is assigned on BOTH branches, so `set -u` is satisfied, and
unquoted on purpose: empty expands to no words at all, which makes the
default cargo line character for character what it is today. */
rep(`cargo build --lib -p app --target aarch64-linux-android --features tauri/custom-protocol 2>&1`,
`cargo build --lib -p app --target aarch64-linux-android --features tauri/custom-protocol $CARGO_EXTRA 2>&1`);
rep(`SO=target/aarch64-linux-android/debug/libapp_lib.so`,
`SO=target/aarch64-linux-android/$PROFILE/libapp_lib.so`);
rep(`./gradlew.bat assembleArm64Debug -x rustBuildArm64Debug -q`,
`./gradlew.bat assemble$VARIANT -x rustBuild$VARIANT -q`);
rep(`cd /f/Coding/SoulStream
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
ls -la "$APK"`, ins('ins-r43-build-tail.txt'));
save();
// ── the doc ───────────────────────────────────────────────────────────
load('ANDROID.md');
/* the last sentence of the dev-flavour section, NOT the `## Verifying`
heading below it: a sibling inserting a section of its own would reach for
the heading, and this one still lands where it belongs either way */
rep(`harness and the app see one origin in both flavours.`,
`harness and the app see one origin in both flavours.` + NL + NL + ins('ins-r43-doc-release.txt'));
/* the last sentence of the file. Deliberately NOT the `## Not done yet`
heading and NOT its intro — that sentence carries a ROUND COUNT, which is
exactly what a sibling round bumps. No count anywhere in this file is
touched, because this round adds no step to any walk. */
/* one newline, not two: the bullets in that list sit flush against each
other, and a blank line between two of them turns the whole list loose */
rep(`which the tour deliberately refuses.`,
`which the tour deliberately refuses.` + NL + ins('ins-r43-doc-notdone.txt'));
save();
console.log('round 43 wired');