From a2f60eeeaf3f4feef0f7e438c531fe1ef5fe4325 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Sep 2026 18:13:41 -0400 Subject: [PATCH] chore(release): bump to 1.15.1, unblocking the AAB on AGP 9.4.0 v1.15.0 was tagged but never shipped. Its `Create Release Assets` run failed in `deploy-android` at the first packaging task: Execution failed for task ':amethyst:buildFdroidReleasePreBundle' > Entry name contains invalid characters: root/META-INF/zoomable-root:zoomable.kotlin_module so no AAB, no APK, and none of the 47 assets were produced. A `.kotlin_module` is named after the Gradle project path that produced it, colons included. 14 of the 98 modules merged into the app carry one: zoomable, Negentropy, vico, the seven coil3 artifacts, and four of ours -- Amethyst:quartz, Amethyst:commons, Amethyst:quic and Amethyst:nestsClient -- so renaming our own would not have been enough. Bisected against the two toolchain bumps this cycle, since both landed after the last good release: AGP 9.3.1 -> 9.4.0 and Kotlin 2.4.10 -> 2.4.20. With Kotlin held at 2.4.20 and AGP reverted, both flavours' bundle tasks pass, so Kotlin is not the trigger. The R8 output jar carries the identical 14 colon entries under BOTH AGP versions -- 9.4.0 added the rejection rather than the names, in JarFlinger.addJar, reached from PerModuleBundleTask.addHybridFolder. `packaging.resources.excludes` was tried first and cannot work, at either `META-INF/*.kotlin_module` or `**/*.kotlin_module`: with minification on, R8 emits the java resources itself and addHybridFolder hands JarFlinger its own predicate, so those filters are never consulted. Confirmed by deleting the R8 output and re-running rather than reading a stale intermediate -- mergeJavaResource's jar holds zero kotlin_modules while R8's holds all 98. So the entries are stripped from R8's jar in the moment before the bundle task opens it, and the jar is put back exactly as R8 left it afterwards. Two details carry their weight: - the strip is doFirst on the CONSUMER rather than doLast on R8, so a build-cache hit on R8 cannot skip it; - the restore is what keeps R8 up to date. Without it Gradle sees a modified output and re-runs R8 on every build -- measured here at ~2 min for an otherwise no-op build. With it, a second run reports minifyFdroidReleaseWithR8 UP-TO-DATE and finishes in 1s. Pinning back to 9.3.1 was the alternative and is one line away; the catalog comment records that, and says to drop the workaround when AGP fixes it. Verified from a cleaned R8 output on both flavours: buildFdroidReleasePreBundle and buildPlayReleasePreBundle both BUILD SUCCESSFUL, each reporting "Stripped 14 colon-named entries from base.jar". appCode 458 -> 459. RELEASE_NOTES_ID deliberately stays on the v1.15.0 note: RELEASE_OPS has it repointed on x.y.0 only, and 1.15.1 ships that release's contents. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VgVDQQXAg4cmzsWHoJj61k --- .claude/skills/quartz-integration/SKILL.md | 6 +- .../references/gradle-setup.md | 8 +- BUILDING.md | 6 +- README.md | 10 +-- RELEASE_OPS.md | 4 +- amethyst/build.gradle.kts | 76 +++++++++++++++++++ docs/changelog/README.md | 1 + docs/changelog/v1.15.01.md | 30 ++++++++ geode/README.md | 6 +- gradle/libs.versions.toml | 7 +- 10 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 docs/changelog/v1.15.01.md diff --git a/.claude/skills/quartz-integration/SKILL.md b/.claude/skills/quartz-integration/SKILL.md index f060a36c64..136ae31af4 100644 --- a/.claude/skills/quartz-integration/SKILL.md +++ b/.claude/skills/quartz-integration/SKILL.md @@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects. -**Published artifact**: `com.vitorpamplona.quartz:quartz:1.15.0` (Maven Central) +**Published artifact**: `com.vitorpamplona.quartz:quartz:1.15.1` (Maven Central) **Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`) **License**: MIT @@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr ```toml [versions] -quartz = "1.15.0" +quartz = "1.15.1" [libraries] quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" } @@ -41,7 +41,7 @@ kotlin { ```kotlin dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.15.0") + implementation("com.vitorpamplona.quartz:quartz:1.15.1") } ``` diff --git a/.claude/skills/quartz-integration/references/gradle-setup.md b/.claude/skills/quartz-integration/references/gradle-setup.md index 742822d673..63c34ba1ee 100644 --- a/.claude/skills/quartz-integration/references/gradle-setup.md +++ b/.claude/skills/quartz-integration/references/gradle-setup.md @@ -3,7 +3,7 @@ ## Current version ``` -com.vitorpamplona.quartz:quartz:1.15.0 +com.vitorpamplona.quartz:quartz:1.15.1 ``` Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz @@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua ```toml [versions] -quartz = "1.15.0" +quartz = "1.15.1" [libraries] quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" } @@ -55,7 +55,7 @@ kotlin { ```kotlin // build.gradle.kts (app module) dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.15.0") + implementation("com.vitorpamplona.quartz:quartz:1.15.1") } ``` @@ -70,7 +70,7 @@ plugins { } dependencies { - implementation("com.vitorpamplona.quartz:quartz:1.15.0") + implementation("com.vitorpamplona.quartz:quartz:1.15.1") // JNA needed for libsodium (NIP-44) on JVM implementation("net.java.dev.jna:jna:5.18.1") } diff --git a/BUILDING.md b/BUILDING.md index af87acd699..f354492819 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -533,7 +533,7 @@ reads an optional per-release changelog from ## Bootstrap runbook (one-time) -> **Status as of v1.15.0:** both Homebrew packages are now live upstream — the +> **Status as of v1.15.1:** both Homebrew packages are now live upstream — the > `amethyst-nostr` cask (`Homebrew/homebrew-cask`, at 1.14.0) and the `amy` > formula (`Homebrew/homebrew-core`) both answer 200 on `formulae.brew.sh`, so > `bump-homebrew.yml` finally has something to bump. **Winget is still not @@ -587,7 +587,7 @@ The token then lives only in that maintainer's shell: ```bash export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope -scripts/bump-homebrew-cask.sh v1.15.0 +scripts/bump-homebrew-cask.sh v1.15.1 ``` Create one at @@ -603,7 +603,7 @@ Same split, and it needs **no token at all**. `scripts/bump-winget.sh` drives runs fine from macOS or Linux: ```bash -scripts/bump-winget.sh v1.15.0 +scripts/bump-winget.sh v1.15.1 ``` CI (`bump-winget.yml`, `GITHUB_TOKEN` only) does the bookkeeping: downloads the diff --git a/README.md b/README.md index 11c018c51e..f6218a8750 100644 --- a/README.md +++ b/README.md @@ -328,16 +328,16 @@ repositories { Add the following line to your `commonMain` dependencies: ```gradle -implementation('com.vitorpamplona.quartz:quartz:1.15.0') +implementation('com.vitorpamplona.quartz:quartz:1.15.1') ``` Variations to each platform are also available: ```gradle -implementation('com.vitorpamplona.quartz:quartz-android:1.15.0') -implementation('com.vitorpamplona.quartz:quartz-jvm:1.15.0') -implementation('com.vitorpamplona.quartz:quartz-iosarm64:1.15.0') -implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1.15.0') +implementation('com.vitorpamplona.quartz:quartz-android:1.15.1') +implementation('com.vitorpamplona.quartz:quartz-jvm:1.15.1') +implementation('com.vitorpamplona.quartz:quartz-iosarm64:1.15.1') +implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1.15.1') ``` Check versions on [MavenCentral](https://central.sonatype.com/search?q=com.vitorpamplona.quartz) diff --git a/RELEASE_OPS.md b/RELEASE_OPS.md index 7ca75fc9f1..7b1fd6cf8b 100644 --- a/RELEASE_OPS.md +++ b/RELEASE_OPS.md @@ -240,9 +240,9 @@ readable by anyone with push access here), so a maintainer runs the last step: ```bash # after merging the sync PRs export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope -scripts/bump-homebrew-cask.sh v1.15.0 +scripts/bump-homebrew-cask.sh v1.15.1 -scripts/bump-winget.sh v1.15.0 # no token — uses your `gh` auth +scripts/bump-winget.sh v1.15.1 # no token — uses your `gh` auth ``` Both scripts re-verify the published artifact's sha256 before submitting, and diff --git a/amethyst/build.gradle.kts b/amethyst/build.gradle.kts index 4b9583cc82..77010ea9fe 100644 --- a/amethyst/build.gradle.kts +++ b/amethyst/build.gradle.kts @@ -1,3 +1,7 @@ +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -611,3 +615,75 @@ dependencies { implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.extensions) } + +// AGP 9.4.0's PerModuleBundleTask refuses to write an AAB entry whose name contains a colon: +// +// Entry name contains invalid characters: root/META-INF/zoomable-root:zoomable.kotlin_module +// +// A .kotlin_module is named after the Gradle project path that produced it, colons included, and +// 14 of the 98 merged into this app carry one -- zoomable, Negentropy, vico, seven coil3 artifacts +// and four of ours. The entries are identical under 9.3.1, which writes them without complaint, so +// 9.4.0 added the rejection rather than the names. +// +// packaging.resources.excludes cannot remove them: with minification on, R8 emits the java +// resources and PerModuleBundleTask.addHybridFolder hands JarFlinger its own predicate, so those +// filters are never consulted. Nothing in the AAB reads a .kotlin_module either -- it exists for +// the Kotlin compiler to resolve top-level declarations across modules at COMPILE time. +// +// So drop them from R8's java-res jar in the moment before the bundle task opens it, then put the +// jar back exactly as R8 left it. The strip is doFirst on the CONSUMER rather than doLast on R8, so +// a build-cache hit on R8 cannot skip it; the restore is what keeps R8 up to date next build -- +// without it Gradle sees a modified output and re-runs R8 every time, which measured ~2 min a build +// here for no work. +fun stripColonNamedEntries(jar: File): Int { + val offenders = ZipFile(jar).use { zip -> zip.entries().toList().count { ':' in it.name } } + if (offenders == 0) return 0 + + val rewritten = File(jar.parentFile, "${jar.name}.stripped") + ZipFile(jar).use { zip -> + ZipOutputStream(rewritten.outputStream().buffered()).use { out -> + zip.entries().asSequence().filterNot { ':' in it.name }.forEach { entry -> + out.putNextEntry(ZipEntry(entry.name)) + zip.getInputStream(entry).use { it.copyTo(out) } + out.closeEntry() + } + } + } + rewritten.copyTo(jar, overwrite = true) + rewritten.delete() + return offenders +} + +androidComponents.onVariants { variant -> + val variantName = variant.name + val capitalized = variantName.replaceFirstChar { it.uppercase() } + tasks.matching { it.name == "build${capitalized}PreBundle" }.configureEach { + val javaResDir = layout.buildDirectory.dir("intermediates/merged_java_res/$variantName") + val backups = mutableMapOf() + + doFirst { + javaResDir.get().asFile + .walkTopDown() + .filter { it.isFile && it.extension == "jar" } + .forEach { jar -> + val backup = File(jar.parentFile, "${jar.name}.orig") + jar.copyTo(backup, overwrite = true) + val dropped = stripColonNamedEntries(jar) + if (dropped > 0) { + backups[jar] = backup + logger.lifecycle("Stripped $dropped colon-named entries from ${jar.name}") + } else { + backup.delete() + } + } + } + + doLast { + backups.forEach { (jar, backup) -> + backup.copyTo(jar, overwrite = true) + backup.delete() + } + backups.clear() + } + } +} diff --git a/docs/changelog/README.md b/docs/changelog/README.md index 8396285f58..61cc4fa258 100644 --- a/docs/changelog/README.md +++ b/docs/changelog/README.md @@ -2,6 +2,7 @@ Release notes for Amethyst, one file per version. Files are named with zero-padded version numbers so they sort correctly in any file browser. Use [`TEMPLATE.md`](TEMPLATE.md) as the starting point for the next release. +- [v1.15.1 — Release Build Fix](v1.15.01.md) - [v1.15.0 — Marmot Updates, a Advanced Search Box, and Books, DVM Updates](v1.15.00.md) - [v1.14.0 — Highlights, Relay Login, and a Much Faster Start](v1.14.00.md) - [v1.13.1 — Follow-up Fixes for Buzz, Concord and the Keyboard](v1.13.01.md) diff --git a/docs/changelog/v1.15.01.md b/docs/changelog/v1.15.01.md new file mode 100644 index 0000000000..904215956e --- /dev/null +++ b/docs/changelog/v1.15.01.md @@ -0,0 +1,30 @@ +# v1.15.1: Release Build Fix + +v1.15.0 was tagged but never shipped: its `Create Release Assets` run failed in +`deploy-android` before producing a single artifact. This release carries the +whole of [v1.15.0](v1.15.00.md) plus the fix for that failure — read those notes +for what actually changed in the app. + +## Build + +- Strips colon-named `.kotlin_module` entries out of R8's java-resource jar + before the app bundle is written. AGP 9.4.0 refuses them: + + ``` + Execution failed for task ':amethyst:buildFdroidReleasePreBundle' + > Entry name contains invalid characters: root/META-INF/zoomable-root:zoomable.kotlin_module + ``` + + A `.kotlin_module` is named after the Gradle project path that produced it, + colons included, and 14 of the 98 modules merged into the app carry one — + zoomable, Negentropy, vico, seven coil3 artifacts, and four of ours + (`Amethyst:quartz`, `:commons`, `:quic`, `:nestsClient`). The entries are + byte-identical under AGP 9.3.1, which writes the bundle without complaint, so + 9.4.0 added the rejection rather than the names. + + `packaging.resources.excludes` cannot remove them — with minification on, R8 + emits the java resources and `PerModuleBundleTask` hands `JarFlinger` its own + predicate, so those filters are never consulted. Nothing reads a + `.kotlin_module` out of an installed app either; the file exists for the + Kotlin compiler to resolve top-level declarations across modules at compile + time. diff --git a/geode/README.md b/geode/README.md index cd5031e6ea..1e8a8d8bbc 100644 --- a/geode/README.md +++ b/geode/README.md @@ -32,7 +32,7 @@ docker run -d --name geode -p 7447:7447 \ ``` with `in_memory = false` and `file = "/var/lib/geode/events.db"` in your -`geode.toml`. Pin a version (`:1.15.0`) instead of `:latest` for reproducible +`geode.toml`. Pin a version (`:1.15.1`) instead of `:latest` for reproducible deploys. To build the image yourself, from the repo root: ```bash @@ -47,7 +47,7 @@ it — a minimal JRE is bundled, so no system Java is required. It installs to `/opt/geode/` with the launcher at `/opt/geode/bin/geode`. ```bash -sudo dpkg -i geode-1.15.0-linux-x64.deb # or: sudo rpm -i geode-1.15.0-linux-x64.rpm +sudo dpkg -i geode-1.15.1-linux-x64.deb # or: sudo rpm -i geode-1.15.1-linux-x64.rpm ``` To run it as a managed service, wire up the shipped systemd unit @@ -69,7 +69,7 @@ formula: [`packaging/homebrew/geode-relay.rb`](packaging/homebrew/geode-relay.rb Download `geode---.tar.gz`, unpack, and run: ```bash -tar xzf geode-1.15.0-linux-x64.tar.gz +tar xzf geode-1.15.1-linux-x64.tar.gz ./geode/bin/geode --config geode/share/geode/config.example.toml ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 904ecf238e..7def77e9aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ # Amethyst app version — single source of truth consumed by both Android (amethyst/) # and Desktop (desktopApp/). `appCode` is the Android versionCode: a monotonic # integer that must increment on every release, even when `app` is unchanged. -app = "1.15.0" -appCode = "458" +app = "1.15.1" +appCode = "459" accompanistAdaptive = "0.37.3" # Pinned: 0.3.0 turns CacheMap.entries/keys/values into DeprecationLevel.ERROR (and # throws UnsupportedOperationException at runtime), which breaks quartz's appleMain @@ -13,6 +13,9 @@ accompanistAdaptive = "0.37.3" cachemapVersion = "0.2.4" composeMultiplatform = "1.12.0" activityCompose = "1.13.0" +# 9.4.0's PerModuleBundleTask rejects an AAB entry whose name contains a colon, which every +# .kotlin_module named after a Gradle project path has. Worked around in amethyst/build.gradle.kts +# (stripColonNamedEntries) rather than by pinning to 9.3.1; drop that block if AGP fixes it. agp = "9.4.0" android-compileSdk = "37" android-minSdk = "26"