Skip to content

feat(maven): publish both artifacts to maven central - #630

Open
andiwand wants to merge 4 commits into
mainfrom
feat/maven-central
Open

feat(maven): publish both artifacts to maven central#630
andiwand wants to merge 4 commits into
mainfrom
feat/maven-central

Conversation

@andiwand

Copy link
Copy Markdown
Member

Stacked on #629.

GitHub Packages demands read:packages even to read a public artifact. f-droid builds from source with no credentials at all, so it cannot resolve either artifact from there — and being consumable by droid, which f-droid builds, is the whole reason the AAR exists. #629 says this needs Maven Central "first, which is a separate piece of work — the app.opendocument namespace has to be verified, and every artifact signed." Half of that turned out to be already done.

app.opendocument is not a new namespace

The org has published android AARs to Maven Central before, under exactly this group:

artifact latest published
app.opendocument:pdf2htmlex-android 0.18.26 2024-08-25
app.opendocument:wvware-android 1.2.11 2024-08-26

Both are on repo1.maven.org today, and droid consumed them as ordinary mavenCentral() dependencies before they were archived. So this is a new artifact under an established name. What is genuinely outstanding is credentials, not verification — see below.

What this changes

The AAR goes through com.vanniktech.maven.publish. Sonatype ships no official gradle plugin for the Central Portal, and OSSRH — which pdf2htmlEX-Android and wvware-android deployed to — is gone, so their gradle-nexus/publish-plugin + s01.oss.sonatype.org setup cannot simply be copied. vanniktech is what android libraries have settled on. It renames the publication releasemaven, so the workflow now asks for the name-independent publishAllPublicationsToGitHubPackagesRepository.

The jar keeps its hand-written pom and gains a central profile. central-publishing-maven-plugin takes over the deploy lifecycle wholesale, so one mvn deploy cannot serve both destinations; the default stays GitHub Packages and -Pcentral is the Central path. maven.yml splits publishing into its own job so the secrets sit behind an environment, the shape android.yml already has.

Both keep publishing to GitHub Packages. Nothing that reads it today breaks.

Central asks for more than GitHub Packages does, and both artifacts now supply it:

  • sources and javadoc jars — the jar already had them, the AAR now does;
  • a POM carrying developersneither had one, so neither would have been accepted as it stood;
  • a PGP signature over every file.

Signing is conditional on a key being configured. Unconditional (signAllPublications() on its own) fails publishToMavenLocal with "no configured signatory", which would mean nobody could publish locally or to GitHub Packages without a private key on hand — a steep price for two destinations that never check one. The portal rejects an unsigned upload, so the release path stays guarded.

Both publishes upload a deployment and stop. Releasing is a deliberate click in the portal UI: Central never forgets a version, and that click is the last point at which a bad artifact can be dropped rather than lived with.

Verification

Locally, against a throwaway PGP key: the AAR publishes .aar, .pom, .module, sources and javadoc jars, each with a detached .asc that verifies against the key; the POM carries name, description, url, licenses, developers and scm; the AAR still contains its assets and prebuilt ABIs. With no key set it publishes the same artifacts unsigned. mvn verify and mvn validate -Pcentral both pass, and the flattened pom keeps developers.

Not verified: an actual upload to the portal, which needs credentials this repo does not have yet.

Before this can release

The maven-central environment has to exist with four secrets:

secret what
MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD Central Portal user token, generated at central.sonatype.com — not portal login credentials
SIGNING_KEY armored PGP private key
SIGNING_PASS its passphrase

The archived repos hold a working SIGNING_KEY/SIGNING_PASS pair in their own OpenDocumentAppRelease environment, so the signing key may just need copying. The Sonatype credentials do not carry over: those are OSSRH-era, and the Portal issues its own tokens. Worth confirming who holds the Portal account that owns the app.opendocument namespace, since these repos came from an outside author.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QqTNzXTxzfeCA2w1ErQzax

andiwand and others added 4 commits July 28, 2026 07:20
Two android-only failure modes shipped in a row: #628, a compile error in
`jni/src` that only the NDK sees, and #621, a `java.lang.ref.Cleaner` that
exists on the JDK and not on android's class library. Both were found by
OpenDocument.droid, because it was the only thing that ever cross compiled
this, and the build_test matrix's single android entry only ever configured
conan for one ABI — it never built the bindings, let alone ran them.

`android/` closes that: a gradle library module that packages the JNI
bindings as `app.opendocument:odr-core-android`, and `.github/workflows/
android.yml`, which on every push cross compiles all four ABIs, lints
`../jni/java` against minSdk 26, assembles the AAR, and runs an instrumented
suite on API 26 (the floor OpenDocument.droid ships to) and on API 36.

The AAR carries `classes.jar` (the same `../jni/java` the maven jar is built
from, plus `OdrAndroid`), `libodr_jni.so` and `libc++_shared.so` per ABI, the
renderer's CSS/JS and the libmagic database as assets, and a consumer proguard
rule keeping the classes JNI resolves by name. `OdrAndroid.init(context)`
unpacks the assets into no-backup storage, keyed by `Odr.commitHash()`, and
registers them with `GlobalParams` — an APK holds them as assets, and the
renderer wants files.

`build_native.py` drives conan + cmake per ABI into `native/prebuilt`, where
the gradle build reads them; CI runs it once per ABI as a separate job and
hands the results to the AAR and emulator jobs as artifacts.

This is a second packaging of the same build, not a new path: OpenDocument.droid
keeps consuming the conan package (`with_jni=True`), which needs no credentials,
as f-droid requires. The AAR is for consumers that just want a dependency.

Along the way:

* The four `android-<arch>` conan profiles replace `android-35-x86_64` and
  share one `android.jinja` body. API level drops 35 -> 26 to match the java
  floor (it is baked into the target triple, so a higher one produces
  libraries older devices refuse to load) and the NDK toolchain path is
  resolved per host, so the profiles work on macOS too. `scripts/conan_lock`
  locks each of them against the linux build profile.
* `jni/CMakeLists.txt` builds `odr_jni` without a JDK when targeting android:
  the NDK sysroot ships `jni.h` and the symbols come from the runtime. A JDK
  found there still builds the jar, which is what the conan package deploys
  next to the library; the junit suite stays host-only.
* `TestFiles` moves to `jni/testfixtures`, compiled by both suites, so it is
  limited to what API 26 offers — `String.formatted`, `Path.of` and
  `Files.writeString` are gone from it.
* `Color.toString` formats with `Locale.ROOT`; a device set to a locale with
  its own digits rendered it unparseable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqTNzXTxzfeCA2w1ErQzax
OpenDocument.droid is kotlin throughout, and it is the consumer this AAR is
built for, so the module it will depend on should not be the odd one out. The
two things this module writes itself move over: `OdrAndroid` and the
instrumented suite.

What it *borrows* stays java, and has to. `../jni/java` and
`../jni/testfixtures` are compiled by CMake's `add_jar` for the maven jar and
the host junit suite, neither of which has a kotlin toolchain — so the java API
is unchanged, and `src/androidTest` compiles kotlin against the java
`TestFiles` in the same package, which resolves the package-private members
fine.

The public surface keeps its java shape: `OdrAndroid` is an `object` with
`@JvmStatic init`, so `OdrAndroid.init(context)` is still a static call, and
`@Throws(IOException::class)` keeps the exception checked for a java caller.

Kotlin earns its place mostly by deleting code: `File.deleteRecursively`,
`InputStream.copyTo` and `use` replace three hand-rolled helpers in
`OdrAndroid`, and `readBytes()` replaces the buffer loop in `HttpServerTest`.
Net -76 lines across the six files, with the same tests.

BREAKING CHANGE: `odr-core-android` now declares a `kotlin-stdlib` dependency
(2.2.10, AGP 9's built-in kotlin). The POM had none before, which is what
`android.builtInKotlin=false` was protecting; for OpenDocument.droid it costs
nothing, and it is the price of the module being kotlin at all. Nothing about
`odr-core-java` changes — that jar is still pure java with no dependencies.

Also adds spotless + ktfmt 0.64 in the kotlinlang flavor, the same formatter
and version OpenDocument.droid runs, scoped to this module's own kotlin so an
android build never rewrites the maven jar's sources. `spotlessCheck` runs in
the format workflow, which needs neither the NDK nor conan.

Verified on an API 31 emulator: 16/16 instrumented tests pass, lint is clean
against minSdk 26, and the release AAR assembles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqTNzXTxzfeCA2w1ErQzax
… on the device suite

`.gitattributes` declares `* text eol=lf`, so committing the wrapper jar
normalized the two CR bytes out of it: the blob is 48460 bytes where the
jar gradle publishes is 48462, and `setup-gradle`'s wrapper validation
rejects the checkout outright. Declare `*.jar binary` and renormalize;
the blob now hashes to gradle's published
497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7.

Publishing moves out of `aar` into its own job needing both `aar` and
`instrumented`. As siblings off `native`, `aar` could push a release to
GitHub Packages before — or despite — the emulator suite, which is the
one check that sees what a device sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqTNzXTxzfeCA2w1ErQzax
GitHub Packages demands `read:packages` even to read a public artifact, so
f-droid — which builds from source with no credentials — cannot resolve
either artifact from it. That is the whole reason the AAR exists, so it
has to be somewhere credential-less. `app.opendocument` is already a live
namespace on Central (`pdf2htmlex-android`, `wvware-android`, both since
2024), so this is a new artifact under an old name.

The AAR goes through `com.vanniktech.maven.publish`; sonatype ships no
official gradle plugin for the portal and OSSRH, which the org's earlier
android libraries deployed to, is gone. The publication is renamed
`release` -> `maven` by the plugin, hence the workflow now asks for
`publishAllPublicationsToGitHubPackagesRepository`. The jar keeps its
hand-written pom and gets a `central` profile, because
`central-publishing-maven-plugin` takes over the deploy lifecycle
wholesale and the two destinations cannot be driven by one invocation.

Central asks for more than GitHub Packages: sources and javadoc jars, a
pom carrying `developers` (neither had one), and a PGP signature per
file. Signing is conditional on a key being configured — unconditional
would mean no `publishToMavenLocal` and no GitHub Packages publish
without a private key on hand, and the portal rejects an unsigned upload
anyway. Both publishes upload and stop; releasing is a click, because
Central never forgets a version.

Verified locally against a throwaway PGP key: the AAR publishes with
.aar/.pom/.module plus sources and javadoc, each with a valid detached
.asc, and still publishes unsigned when no key is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqTNzXTxzfeCA2w1ErQzax
Base automatically changed from feat/android-aar to main July 28, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant