Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ jobs:
if: github.event_name != 'push'
needs: [aar, instrumented]
runs-on: ubuntu-24.04
# holds the portal token and the signing key
environment: maven-central
permissions:
contents: read
packages: write
Expand Down Expand Up @@ -237,8 +239,21 @@ jobs:
echo "ODR_VERSION=${INPUT_VERSION}" >> $GITHUB_ENV
fi

- name: publish
- name: publish to github packages
working-directory: android
run: ./gradlew publishReleasePublicationToGitHubPackagesRepository -Podr.abis=
run: ./gradlew publishAllPublicationsToGitHubPackagesRepository -Podr.abis=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Uploads a deployment to the portal and leaves it there. Releasing it to
# Maven Central is a click in the portal UI, deliberately: Central never
# forgets a version, so this is the last point at which a bad artifact can
# still be dropped instead of lived with.
- name: publish to maven central
working-directory: android
run: ./gradlew publishToMavenCentral -Podr.abis=
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASS }}
61 changes: 50 additions & 11 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,34 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: setup java
uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5
with:
distribution: temurin
java-version: 21
cache: maven

- name: build
run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml verify

# Split out so the secrets sit behind an environment and are never in reach of
# a plain push build.
publish:
if: github.event_name != 'push'
runs-on: ubuntu-24.04
environment: maven-central
permissions:
contents: read
packages: write
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -34,7 +55,6 @@ jobs:
server-id: github

- name: get version
if: github.event_name != 'push'
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
Expand All @@ -44,12 +64,31 @@ jobs:
echo "REVISION=${INPUT_VERSION}" >> $GITHUB_ENV
fi

- name: build
if: github.event_name == 'push'
run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml verify

- name: publish
if: github.event_name != 'push'
- name: publish to github packages
run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml deploy -Drevision="${REVISION}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# A second setup-java: the central profile deploys to a different server
# id and needs the signing key in the runner's gpg keyring, and the step
# above has already had its turn with settings.xml.
- name: setup java for maven central
uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5
with:
distribution: temurin
java-version: 21
cache: maven
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.SIGNING_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE

# Uploads and stops, the way the AAR does — released by hand from the
# portal, because Central never forgets a version.
- name: publish to maven central
run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml deploy -Pcentral -Drevision="${REVISION}"
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASS }}
6 changes: 6 additions & 0 deletions android/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ calls it.
- **Shared test inputs stay in `../jni/testfixtures`**, which both suites
compile — so it is limited to what android API 26 offers (no `Path.of`, no
`Files.writeString`, no `String.formatted`).
- **Publishing goes to Maven Central and GitHub Packages**, via
`com.vanniktech.maven.publish` — sonatype ships no official gradle plugin for
the portal. Central's extra requirements (sources + javadoc jars, `developers`
in the POM, a PGP signature per file) drive what that block declares; do not
drop one thinking GitHub Packages is the only consumer. Signing is conditional
on a key being present so `publishToMavenLocal` works without one.
- **`native/` is build output**, never committed; the artifacts CI downloads
land in the same place. `-Podr.abis=` (empty) is what tells the build the
libraries are already there.
Expand Down
47 changes: 28 additions & 19 deletions android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,17 @@ ABIs: `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`. minSdk 26.

```gradle
repositories {
maven {
url = uri("https://maven.pkg.github.com/opendocument-app/OpenDocument.core")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
mavenCentral()
}

dependencies {
implementation "app.opendocument:odr-core-android:<version>"
}
```

GitHub Packages needs a token with `read:packages` even for public packages —
see [Publishing](#publishing) for what that rules out. Do not depend on
The same artifact is also on GitHub Packages, but prefer Central: GitHub
Packages needs a token with `read:packages` even for public packages — see
[Publishing](#publishing) for what that rules out. Do not depend on
`odr-core-java` as well: the AAR carries the same classes.

```kotlin
Expand Down Expand Up @@ -104,17 +99,31 @@ ships to — and on a current API level.

## Publishing

Releases go to GitHub Packages, like the maven jar, and only once the
Releases go to **Maven Central and GitHub Packages**, and only once the
instrumented suite has passed on every API level — an AAR that assembles and
lints is no evidence that it works on a device. That is enough for
consumers who can hold a token, and **not** enough for the one this is
ultimately built for: GitHub Packages demands `read:packages` even to read a
public artifact, and f-droid builds from source with no credentials at all.
Making the AAR the base of OpenDocument.droid therefore means publishing it to
Maven Central first, which is a separate piece of work — the `app.opendocument`
namespace has to be verified, and every artifact signed.

Until then OpenDocument.droid keeps building odrcore from the conan package
lints is no evidence that it works on a device.

Maven Central is the one that matters. GitHub Packages demands `read:packages`
even to read a public artifact, which rules out the consumer this is ultimately
built for: f-droid builds from source with no credentials at all. It stays
published there only because the maven jar is, and dropping it would break
whoever already reads it.

`app.opendocument` is an established namespace on Central — `pdf2htmlex-android`
and `wvware-android` have been there since 2024 — so this is a new artifact under
an old name, not a new namespace. Central asks for more than GitHub Packages
does, and the module supplies it: sources and javadoc jars, a POM carrying
`developers`, and a PGP signature over every file. Signing is conditional on a
key being configured, so `publishToMavenLocal` and the GitHub Packages publish
still work without one; an unsigned upload is rejected by the portal, so the
release path stays guarded either way.

Publishing **uploads a deployment to the portal and stops**. Releasing it is a
deliberate click in the portal UI, because Central never forgets a version —
that click is the last point at which a bad artifact can be dropped rather than
lived with.

For now OpenDocument.droid keeps building odrcore from the conan package
(`with_jni=True`), deploying `libodr_jni.so`, `odr-core-java.jar` and the assets
out of it. That path is unaffected by anything here, and it is the reason the
two halves cannot drift: they come out of one build.
Expand Down
88 changes: 53 additions & 35 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.spotless)
`maven-publish`
alias(libs.plugins.maven.publish)
}

// ktfmt in the kotlinlang flavor, the same formatter and version
Expand Down Expand Up @@ -143,13 +145,6 @@ android {
// library outside java.library.path; android never takes that branch
disable += "UnsafeDynamicallyLoadedCode"
}

publishing {
singleVariant("release") {
withSourcesJar()
withJavadocJar()
}
}
}

dependencies {
Expand All @@ -159,37 +154,60 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
}

publishing {
publications {
register<MavenPublication>("release") {
groupId = "app.opendocument"
artifactId = "odr-core-android"
version = odrVersion

afterEvaluate { from(components["release"]) }

pom {
name = "odr-core-android"
description =
"Android bindings for OpenDocument.core — decode office documents " +
"(ODF, OOXML, legacy MS binary, PDF, CSV, ...) and render them to HTML"
url = "https://github.com/opendocument-app/OpenDocument.core"
licenses {
license {
name = "MPL-2.0"
url = "https://www.mozilla.org/en-US/MPL/2.0/"
}
}
scm {
connection = "scm:git:https://github.com/opendocument-app/OpenDocument.core.git"
developerConnection =
"scm:git:git@github.com:opendocument-app/OpenDocument.core.git"
url = "https://github.com/opendocument-app/OpenDocument.core"
}
// Two destinations, deliberately. Maven Central is the one that matters: it is
// the only one f-droid and any other credential-less source builder can resolve
// from, and `app.opendocument` is already a live namespace there
// (`pdf2htmlex-android`, `wvware-android`). GitHub Packages stays because the
// maven jar publishes there and dropping it would break whoever already reads
// it. Central mandates what the publication below carries — sources and javadoc
// jars, a pom with developers, and a PGP signature over every file — none of
// which GitHub Packages asks for.
mavenPublishing {
configure(AndroidSingleVariantLibrary(variant = "release"))

// Uploads to the portal and stops. A human releases it from there, so a bad
// artifact is still recallable — Central is immutable once released.
publishToMavenCentral()

// Only Central demands a signature. Making it unconditional would mean no
// `publishToMavenLocal` and no GitHub Packages publish without a private
// key on hand, which is a steep price for a repository that never checks
// one. A Central upload missing its .asc files is rejected by the portal,
// so the release path is still guarded.
if (providers.gradleProperty("signingInMemoryKey").isPresent) {
signAllPublications()
}

coordinates("app.opendocument", "odr-core-android", odrVersion)

pom {
name = "odr-core-android"
description =
"Android bindings for OpenDocument.core — decode office documents " +
"(ODF, OOXML, legacy MS binary, PDF, CSV, ...) and render them to HTML"
url = "https://github.com/opendocument-app/OpenDocument.core"
licenses {
license {
name = "MPL-2.0"
url = "https://www.mozilla.org/en-US/MPL/2.0/"
}
}
developers {
developer {
id = "opendocument-app"
name = "OpenDocument.app"
url = "https://github.com/opendocument-app"
}
}
scm {
connection = "scm:git:https://github.com/opendocument-app/OpenDocument.core.git"
developerConnection = "scm:git:git@github.com:opendocument-app/OpenDocument.core.git"
url = "https://github.com/opendocument-app/OpenDocument.core"
}
}
}

publishing {
repositories {
maven {
name = "GitHubPackages"
Expand Down
5 changes: 5 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ agp = "9.3.1"
spotless = "8.8.0"
ktfmt = "0.64"

# sonatype ships no official gradle plugin for the central portal, so this is
# the community one android libraries have settled on
mavenPublish = "0.37.0"

junit = "4.13.2"
androidxTest = "1.7.0"
androidxTestJunit = "1.3.0"
Expand All @@ -19,3 +23,4 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "andro
[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
2 changes: 1 addition & 1 deletion jni/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package `app.opendocument.core`. Mirrors the surface of the python bindings
| Path | What |
|------|------|
| `CMakeLists.txt` | Builds `libodr_jni` + `odr-core-java.jar`; included from the root build via `ODR_JNI`, or standalone against an installed `odrcore`. |
| `pom.xml` | Maven distribution of the Java classes only (`app.opendocument:odr-core-java`); published to GitHub Packages on release via `.github/workflows/maven.yml`. Keep `--release`/`-Xlint` in sync with `CMAKE_JAVA_COMPILE_FLAGS`. |
| `pom.xml` | Maven distribution of the Java classes only (`app.opendocument:odr-core-java`); published to Maven Central (the `central` profile) and GitHub Packages on release via `.github/workflows/maven.yml`. Keep `--release`/`-Xlint` in sync with `CMAKE_JAVA_COMPILE_FLAGS`. |
| `src/` | JNI sources, one `jni_*` unit per public-API area; `odr_jni.hpp` (strings, exceptions, handles) and `jni_convert.hpp` (struct/POJO marshalling) are the helpers. |
| `java/app/opendocument/core/` | Java API: enums, POJOs (styles, metas, `HtmlConfig`), and handle-backed wrappers extending `NativeResource`. Also compiled as-is into the AAR (`../android`) — which is kotlin, but this stays java: `add_jar` below has no kotlin toolchain. |
| `tests/` | JUnit 5 suite, run via ctest (`odr_jni_junit`); inputs are generated inline (tmp files, zip-built minimal ODT) — no fixture files. |
Expand Down
20 changes: 9 additions & 11 deletions jni/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The native library is loaded from `java.library.path` as `odr_jni`

## Maven distribution

The Java classes are published as `app.opendocument:odr-core-java` to
The Java classes are published as `app.opendocument:odr-core-java` to Maven
Central and to
[GitHub Packages](https://github.com/orgs/opendocument-app/packages?repo_name=OpenDocument.core)
on release (`.github/workflows/maven.yml`, built from `pom.xml`). The artifact
contains **only the Java API** — consumers build the native `odr_jni` library
Expand All @@ -36,25 +37,22 @@ native library for every ABI and the runtime assets. See [`../android`](../andro

```gradle
repositories {
maven {
url = uri("https://maven.pkg.github.com/opendocument-app/OpenDocument.core")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
mavenCentral()
}

dependencies {
implementation "app.opendocument:odr-core-java:<version>"
}
```

Note: GitHub Packages requires authentication (a token with `read:packages`)
even for public packages.
Prefer Central: GitHub Packages requires authentication (a token with
`read:packages`) even for public packages.

Local build: `mvn --file jni/pom.xml verify` (produces the jar plus sources
and javadoc jars in `jni/target/`).
and javadoc jars in `jni/target/`). Central additionally wants every file PGP
signed and a POM carrying `developers`; both live in the `central` profile
(`mvn deploy -Pcentral`), kept off the default build so neither a signing key
nor a portal token is needed to build or to deploy to GitHub Packages.

## Building

Expand Down
Loading
Loading