diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e83b4ec7..1af0f93d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,10 @@ jobs: - name: Gradle Test working-directory: zenoh-java - run: ./gradlew jvmTest --info + # CI builds against the sibling checkouts above, so it opts into the + # composite build explicitly. A release does not: it resolves + # zenoh-flat-jni from Maven Central like any other consumer. + run: ./gradlew jvmTest --info -PuseLocalFlatJni=true markdown_lint: runs-on: ubuntu-latest @@ -97,25 +100,14 @@ jobs: - name: Check whether all jobs pass run: echo '${{ toJson(needs) }}' | jq -e 'all(.result == "success")' - # Publish snapshot packages. These jobs will only be triggered when the CI is executed on main. - publish_jvm_snapshot_package: - name: Publish JVM snapshot package + # Publish snapshot packages. Only triggered when CI runs on main. + # One job, because both publications now come from a single Gradle + # invocation — see .github/workflows/publish.yml. + publish_snapshot_package: + name: Publish snapshot package if: contains(fromJSON('["refs/heads/main"]'), github.ref) needs: ci - uses: ./.github/workflows/publish-jvm.yml - permissions: - contents: read - packages: write - with: - snapshot: true - branch: ${{ github.ref_name }} - secrets: inherit - - publish_android_snapshot_package: - name: Publish Android snapshot package - if: contains(fromJSON('["refs/heads/main"]'), github.ref) - needs: ci - uses: ./.github/workflows/publish-android.yml + uses: ./.github/workflows/publish.yml permissions: contents: read packages: write diff --git a/.github/workflows/publish-jvm.yml b/.github/workflows/publish-jvm.yml deleted file mode 100644 index ce0ba5e6..00000000 --- a/.github/workflows/publish-jvm.yml +++ /dev/null @@ -1,196 +0,0 @@ -name: Publish (JVM) - -on: - workflow_call: - inputs: - snapshot: - required: true - type: boolean - description: "If the publication is for a snapshot version." - default: false - branch: - description: Target branch - type: string - required: false - maven_publish: - type: boolean - required: false - default: true - -env: - CARGO_TERM_COLOR: always - JNI_LIB_PATHS: jni-libs # Edit on the inner build.gradle.kts file as well. - -jobs: - builds: - name: Build for ${{ matrix.job.target }} on ${{ matrix.job.os }} - if: ${{ !(github.event.inputs.build == 'false') }} - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # In order to load any added target at runtime, editing the Zenoh class under jvmMain is required. - - { - target: x86_64-unknown-linux-gnu, - arch: amd64, - os: ubuntu-latest, - build-cmd: "cargo", - } - - { - target: aarch64-unknown-linux-gnu, - arch: arm64, - os: ubuntu-latest, - build-cmd: "cross", - } - - { - target: x86_64-apple-darwin, - arch: darwin, - os: macos-latest, - build-cmd: "cargo", - } - - { - target: aarch64-apple-darwin, - arch: darwin, - os: macos-latest, - build-cmd: "cargo", - } - - { - target: x86_64-pc-windows-msvc, - arch: win64, - os: windows-latest, - build-cmd: "cargo", - } - - { - target: aarch64-pc-windows-msvc, - arch: arm64, - os: windows-latest, - build-cmd: "cargo", - } - steps: - - name: Checkout source code - uses: actions/checkout@v4 - with: - ref: ${{ inputs.branch }} - - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - *-linux-gnu*) cargo +stable install cargo-deb --locked ;; - esac - - case ${{ matrix.job.target }} in - aarch64-unknown-linux-gnu) - sudo apt-get -y update - sudo apt-get -y install gcc-aarch64-linux-gnu - ;; - esac - - cargo +stable install cross --locked - - - name: Install Rust toolchain - run: | - rustup show - rustup target add ${{ matrix.job.target }} - - - name: Build - run: ${{ matrix.job.build-cmd }} build --release --bins --lib --features=${{ github.event.inputs.features}} --target=${{ matrix.job.target }} --manifest-path zenoh-jni/Cargo.toml - - - name: Packaging - id: package - shell: bash - run: | - TARGET=${{ matrix.job.target }} - MAIN_PKG_NAME="${GITHUB_WORKSPACE}/${TARGET}.zip" - - case ${TARGET} in - *linux*) - cd "zenoh-jni/target/${TARGET}/release/" - echo "Packaging ${MAIN_PKG_NAME}:" - zip ${MAIN_PKG_NAME} libzenoh_jni.so - cd - - echo "MAIN_PKG_NAME=${MAIN_PKG_NAME}" >> $GITHUB_OUTPUT - ;; - *apple*) - cd "zenoh-jni/target/${TARGET}/release/" - echo "Packaging ${MAIN_PKG_NAME}:" - zip ${MAIN_PKG_NAME} libzenoh_jni.dylib - cd - - echo "MAIN_PKG_NAME=${MAIN_PKG_NAME}" >> $GITHUB_OUTPUT - ;; - *windows*) - cd "zenoh-jni/target/${TARGET}/release/" - echo "Packaging ${MAIN_PKG_NAME}:" - 7z -y a "${MAIN_PKG_NAME}" zenoh_jni.dll - cd - - echo "MAIN_PKG_NAME=${MAIN_PKG_NAME}" >> $GITHUB_OUTPUT - ;; - esac - - - name: "Upload packages" - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.job.target }} - path: | - ${{ steps.package.outputs.MAIN_PKG_NAME }} - - publish_jvm_package: - name: Publish JVM package - needs: builds - permissions: - contents: read - packages: write - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.branch }} - - - name: Create resources destination - run: mkdir ${{env.JNI_LIB_PATHS}} - - - name: Download result of previous builds - uses: actions/download-artifact@v4 - with: - path: ${{env.JNI_LIB_PATHS}} - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 11 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Set pub mode env var - # Note: This step is intended to allow publishing snapshot packages. - # It allows to optionally append the property -PSNAPSHOT to the gradle - # publication task on the next step, resulting in the package version - # following the convention '-SNAPSHOT'. - run: | - if [[ "${{ inputs.snapshot }}" == "true" ]]; then - echo "PUB_MODE=-PSNAPSHOT" >> $GITHUB_ENV - else - echo "RELEASE=closeAndReleaseSonatypeStagingRepository" >> $GITHUB_ENV - fi - - - if: ${{ inputs.maven_publish == true }} - name: Gradle Publish JVM Package to Maven Central repository - run: | - ./gradlew publishJvmPublicationToSonatypeRepository ${{ env.RELEASE }} --info -PremotePublication=true ${{ env.PUB_MODE }} - env: - CENTRAL_SONATYPE_TOKEN_USERNAME: ${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME}} - CENTRAL_SONATYPE_TOKEN_PASSWORD: ${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }} - ORG_GPG_KEY_ID: ${{ secrets.ORG_GPG_KEY_ID }} - ORG_GPG_SUBKEY_ID: ${{ secrets.ORG_GPG_SUBKEY_ID }} - ORG_GPG_PRIVATE_KEY: ${{ secrets.ORG_GPG_PRIVATE_KEY }} - ORG_GPG_PASSPHRASE: ${{ secrets.ORG_GPG_PASSPHRASE }} - - - name: "Upload gradle problems report" - if: always() - uses: actions/upload-artifact@v4 - with: - name: problem-reports-${{ github.job }}.zip - path: ${{ github.workspace }}/build/reports/problems/ diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish.yml similarity index 58% rename from .github/workflows/publish-android.yml rename to .github/workflows/publish.yml index 380e5444..9937124f 100644 --- a/.github/workflows/publish-android.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,10 @@ -name: Publish (Android) +name: Publish + +# Both publications come from a *single* Gradle invocation, so they share one +# staging repository and one close/release: either both coordinates become public +# or neither does. That is only possible because zenoh-flat-jni is a Kotlin +# Multiplatform library — one dependency declaration resolves per target, so an +# Android-enabled build produces both publications correctly. on: workflow_call: @@ -17,18 +23,20 @@ on: required: false default: true -env: - CARGO_TERM_COLOR: always - +# The native libraries are not built here: they ship inside the +# org.eclipse.zenoh:zenoh-flat-jni artifact this SDK depends on, already +# cross-compiled and verified by that repository's release. This workflow only +# compiles Kotlin and publishes. jobs: - publish_android_package: - name: Publish Android package - runs-on: ubuntu-latest + publish_package: + name: Publish to Maven Central permissions: contents: read packages: write + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 with: ref: ${{ inputs.branch }} @@ -37,28 +45,15 @@ jobs: distribution: temurin java-version: 11 - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r26 - add-to-path: false - link-to-sdk: true - - - name: Install Rust toolchain - run: | - rustup show - rustup component add rustfmt clippy - - - name: Setup Rust toolchains - run: | - rustup target add armv7-linux-androideabi - rustup target add i686-linux-android - rustup target add aarch64-linux-android - rustup target add x86_64-linux-android - - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + # Assembles the artifact and generates its POM without uploading, so a + # `maven_publish: false` rehearsal actually proves something. Only the + # remote upload below is gated. + - name: Assemble and verify the publication + run: ./gradlew publishJvmPublicationToMavenLocal publishAndroidReleasePublicationToMavenLocal --info -Pandroid=true + - name: Set pub mode env var # Note: This step is intended to allow publishing snapshot packages. # It allows to optionally append the property -PSNAPSHOT to the gradle @@ -72,9 +67,11 @@ jobs: fi - if: ${{ inputs.maven_publish == true }} - name: Gradle Publish Android Package to Maven Central repository + name: Gradle Publish JVM Package to Maven Central repository run: | - ./gradlew publishAndroidReleasePublicationToSonatypeRepository ${{ env.RELEASE }} --info -PremotePublication=true -Pandroid=true ${{ env.PUB_MODE }} + ./gradlew publishJvmPublicationToSonatypeRepository \ + publishAndroidReleasePublicationToSonatypeRepository \ + ${{ env.RELEASE }} --info -PremotePublication=true -Pandroid=true ${{ env.PUB_MODE }} env: CENTRAL_SONATYPE_TOKEN_USERNAME: ${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME}} CENTRAL_SONATYPE_TOKEN_PASSWORD: ${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e874c778..0052018a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,9 @@ on: type: string description: Release number required: false - zenoh-version: + zenoh-flat-jni-version: type: string - description: Release number of Zenoh + description: zenoh-flat-jni release to build against (must already be on Maven Central) required: false branch: type: string @@ -67,29 +67,14 @@ jobs: env: LIVE_RUN: ${{ inputs.live-run || false }} VERSION: ${{ steps.create-release-branch.outputs.version }} - BUMP_DEPS_VERSION: ${{ inputs.zenoh-version }} - BUMP_DEPS_PATTERN: ${{ inputs.zenoh-version && 'zenoh.*' || '' }} - BUMP_DEPS_BRANCH: ${{ inputs.zenoh-version && format('release/{0}', inputs.zenoh-version) || '' }} + FLAT_JNI_VERSION: ${{ inputs.zenoh-flat-jni-version }} GIT_USER_NAME: eclipse-zenoh-bot GIT_USER_EMAIL: eclipse-zenoh-bot@users.noreply.github.com - publish-jvm: - name: Publish JVM package + publish: + name: Publish to Maven Central needs: tag - uses: ./.github/workflows/publish-jvm.yml - with: - snapshot: ${{ !(inputs.live-run || false) }} - branch: ${{ needs.tag.outputs.branch }} - maven_publish: ${{ !contains(inputs.maven_publish, 'false') }} - permissions: - contents: read - packages: write - secrets: inherit - - publish-android: - name: Publish Android package - needs: tag - uses: ./.github/workflows/publish-android.yml + uses: ./.github/workflows/publish.yml with: snapshot: ${{ !(inputs.live-run || false) }} branch: ${{ needs.tag.outputs.branch }} @@ -101,14 +86,19 @@ jobs: publish-dokka: name: Publish documentation - needs: [tag, publish-android, publish-jvm] + needs: [tag, publish] uses: ./.github/workflows/publish-dokka.yml with: live-run: ${{ inputs.live-run || false }} branch: ${{ needs.tag.outputs.branch }} + # peaceiris/actions-gh-pages pushes the generated site to the gh-pages + # branch, which the default read-only token cannot do. + permissions: + contents: write + secrets: inherit publish-github: - needs: [tag, publish-android, publish-jvm] + needs: [tag, publish] runs-on: macos-latest steps: - uses: eclipse-zenoh/ci/publish-crates-github@main diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 00000000..05e219a3 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,282 @@ +# Publishing zenoh-java + +This document describes how `zenoh-java` is built, verified, and published to +Maven Central, and how to rehearse a release without publishing one. + +It describes the pipeline as it exists in this repository. Where something is +not yet implemented or not yet exercised, it is listed under +[Known gaps](#known-gaps) rather than described as if it worked. + +If you just need to run a release, go to [Running a release](#running-a-release). +For the JVM publishing concepts — coordinates, staging, signing — see +[zenoh-flat-jni's PUBLISHING.md](https://github.com/eclipse-zenoh/zenoh-flat-jni/blob/main/PUBLISHING.md#background-if-you-do-not-work-in-the-jvm-ecosystem), +which covers them once for both repositories. + +## Contents + +- [What this repository publishes](#what-this-repository-publishes) +- [Relationship to zenoh-flat-jni](#relationship-to-zenoh-flat-jni) +- [Running a release](#running-a-release) + - [Before the first run](#before-the-first-run) + - [Rehearsal (dry run)](#rehearsal-dry-run) + - [The real release](#the-real-release) + - [After a release](#after-a-release) +- [Rehearsing before zenoh-flat-jni is released](#rehearsing-before-zenoh-flat-jni-is-released) +- [How the pipeline works](#how-the-pipeline-works) +- [Local development](#local-development) +- [Required secrets](#required-secrets) +- [Known gaps](#known-gaps) +- [Release checklist](#release-checklist) + +## What this repository publishes + +```text +org.eclipse.zenoh:zenoh-java: the JVM artifact +org.eclipse.zenoh:zenoh-java-android: the Android artifact +``` + +Both are **pure JVM/Kotlin**. This repository contains no Rust and builds no +native libraries: they arrive inside the zenoh-flat-jni artifacts, already +cross-compiled and verified by that repository's own release, and a consumer of +`zenoh-java` gets them transitively. + +`zenoh-flat-jni` is itself a Kotlin Multiplatform library, so this SDK declares +**one** dependency on its root coordinate and Gradle resolves the variant +matching each target: + +| zenoh-java artifact | resolves | which carries | +| --- | --- | --- | +| `zenoh-java` | `zenoh-flat-jni-jvm` | six desktop targets | +| `zenoh-java-android` | `zenoh-flat-jni-android` | four Android ABIs, as `jni//` | + +Nothing selects between them by hand, so a publication cannot name the wrong one. +And because one Gradle invocation produces both publications correctly, they are +uploaded into a single staging repository and released together. + +That automatic resolution applies to the *dependency*, not to this SDK itself. +zenoh-java keeps the two plain coordinates above: the release publishes the +`jvm` and `androidRelease` publications only, never the Kotlin Multiplatform +root module, so there is no module metadata for a consumer to resolve against +and an Android consumer must name `zenoh-java-android` explicitly. That is +deliberate — it keeps `org.eclipse.zenoh:zenoh-java` meaning the JVM artifact, +as it always has. It is also why the publish workflow names its two publication +tasks instead of using `publishAllPublicationsTo…`: the unpublished root +publication carries the same `zenoh-java` artifactId as the JVM one, and +publishing both would upload two different things to one coordinate. + +That is the whole reason the publishing here is simple — there is no build +matrix, no cross-compilation, and no native artifact to inspect. + +## Relationship to zenoh-flat-jni + +```text +zenoh-flat-jni: released to Maven Central + | + v + zenoh-java built against it + | + v + zenoh-java: released +``` + +**The order is not a convention, it is a constraint.** `zenoh-java` cannot be +released until the `zenoh-flat-jni` version it depends on is really on Maven +Central, because a release must not depend on a snapshot: + +- consumers do not have the Central *snapshot* repository configured, so the + dependency would simply fail to resolve for them; +- snapshots are mutable and are eventually removed, so even where it resolved it + would not stay reproducible. + +`ci/scripts/bump-and-tag.bash` refuses a `-SNAPSHOT` value outright rather than +letting that reach a published POM. + +Rehearsals are not constrained this way — see +[Rehearsing before zenoh-flat-jni is released](#rehearsing-before-zenoh-flat-jni-is-released). + +## Running a release + +Everything is driven from **Actions → Release → Run workflow** on the default +branch. The workflow creates the release branch, bumps the version, tags it, +builds and publishes. + +### Before the first run + +- **Secrets are already in place.** `CENTRAL_SONATYPE_TOKEN_*` and `ORG_GPG_*` + are organization-level secrets on `eclipse-zenoh`, inherited automatically. +- **The zenoh-flat-jni version must already be on Maven Central** for a live + run. Check before starting: + + ```bash + curl -sfI https://repo1.maven.org/maven2/org/eclipse/zenoh/zenoh-flat-jni//zenoh-flat-jni-.pom + ``` + +### Rehearsal (dry run) + +| Field | Value | +| --- | --- | +| `live-run` | **unchecked** | +| `version` | a fresh provisional number, not one already used | +| `zenoh-flat-jni-version` | leave empty for a rehearsal, or a released version | +| `maven_publish` | checked — or uncheck for the very first run | + +`live-run` and `maven_publish` behave exactly as in zenoh-flat-jni: unchecking +`live-run` publishes `-SNAPSHOT` to the **mutable** snapshot repository +and never runs `closeAndReleaseSonatypeStagingRepository`, while `maven_publish` +decides whether any upload happens at all. **A rehearsal with `maven_publish` +checked performs a real, signed upload** — into the snapshot repository — and is +the only configuration that exercises the credentials. + +`bump-and-tag.bash` tags whatever version it is handed, rehearsals included, so +never give a rehearsal the number you intend to release. + +### The real release + +| Field | Value | +| --- | --- | +| `live-run` | **checked** | +| `version` | the release number | +| `zenoh-flat-jni-version` | the zenoh-flat-jni release to build against — **must already be on Central** | +| `maven_publish` | checked | + +Supplying `zenoh-flat-jni-version` rewrites `zenohFlatJniVersion` in +`gradle.properties` and commits it, so the published POM records exactly which +binding release the SDK was built against. + +### After a release + +Confirm the coordinates resolve, then verify the dependency is right — the POM +must reference a real `zenoh-flat-jni` release: + +```bash +curl -s https://repo1.maven.org/maven2/org/eclipse/zenoh/zenoh-java//zenoh-java-.pom \ + | grep -A2 zenoh-flat-jni +``` + +## Rehearsing before zenoh-flat-jni is released + +This is the common case during the transition, and it works — only the *live* +release is blocked. + +| Rehearsal | resolves zenoh-flat-jni from | proves | +| --- | --- | --- | +| local build and tests | a sibling checkout, via `-PuseLocalFlatJni=true` | the code compiles and the tests pass | +| CI, `maven_publish` unchecked | the snapshot repository | the artifact assembles | +| CI, snapshot publication | `zenoh-flat-jni:-SNAPSHOT` | signing, credentials, a real upload | +| live release | `zenoh-flat-jni:` on Central | **blocked until that exists** | + +A snapshot may depend on a snapshot, because nothing published is permanent. So +the answer is to consume the snapshot that zenoh-flat-jni's *own* rehearsal +published — a rehearsal there with `maven_publish` enabled uploads +`zenoh-flat-jni:-SNAPSHOT` to the Central snapshot repository. + +Nothing needs editing. Name the version on the command line: + +```bash +./gradlew build -PzenohFlatJniVersion=1.9.0-rc4-SNAPSHOT +``` + +or pass the same value as the `zenoh-flat-jni-version` input to a rehearsal of +the release workflow. + +The Central snapshot repository is declared **conditionally** in +`build.gradle.kts`, and this is the part worth understanding: + +```kotlin +if (zenohFlatJniVersion.endsWith("-SNAPSHOT")) { + maven { + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + content { includeGroup("org.eclipse.zenoh") } + } +} +``` + +`includeGroup`, not `includeModule`: the dependency is declared on the root +coordinate, but what Gradle actually downloads is `zenoh-flat-jni-jvm` or +`zenoh-flat-jni-android`. Naming a single module would hide those from the +snapshot repository and the build would fail to resolve. + +It enters the resolution path only when a snapshot version was explicitly asked +for, and even then serves only that one group. A release version never ends in +`-SNAPSHOT`, so a release build cannot resolve a mutable artifact — not by +oversight, and not by someone leaving a flag set. The guarantee is structural +rather than procedural. + +While developing, prefer not to involve a repository at all — see +[Local development](#local-development). + +## How the pipeline works + +`release.yml` runs four jobs: + +1. **`tag`** — `eclipse-zenoh/ci/create-release-branch` cuts the release branch, + then `ci/scripts/bump-and-tag.bash` writes `version.txt`, optionally rewrites + `zenohFlatJniVersion` in `gradle.properties`, commits and tags. It refuses a + `-SNAPSHOT` binding version. +2. **`publish_package`** — compiles Kotlin and publishes both artifacts in one + Gradle invocation, so they share one staging repository and are released + together. No native toolchain is installed and no Rust is built; both would + be pointless here. +3. **`publish-dokka`** — regenerates the API documentation and, on a live run + only, deploys it to the `gh-pages` site README.md links to. The javadoc JAR + attached to the Maven publications does not serve that site; this job does. +4. **`publish-github`** — creates the GitHub release, on a live run only. + +Publishing goes through `io.github.gradle-nexus.publish-plugin` to the Central +Portal, signed with the organization GPG key, exactly as in zenoh-flat-jni. + +## Local development + +The default build resolves `zenoh-flat-jni` from Maven Central like any consumer. +To work against a local checkout of it: + +```bash +./gradlew build -PuseLocalFlatJni=true +``` + +That substitutes `../zenoh-flat-jni` through a Gradle composite build, so changes +there are picked up without publishing anything. Set it in your personal +`gradle.properties` if you want it always on. + +**It must never be enabled for a release.** With it on, the published artifact +would be built against whatever happens to be on the builder's disk rather than +the resolved dependency, and the POM would still claim the released version. It +is off by default and CI passes it explicitly only where it checks the sibling +out. + +## Required secrets + +| Secret | Use | +| --- | --- | +| `CENTRAL_SONATYPE_TOKEN_USERNAME` / `_PASSWORD` | Central Portal user token | +| `ORG_GPG_KEY_ID` / `_SUBKEY_ID` / `_PRIVATE_KEY` / `_PASSPHRASE` | signing | +| `BOT_TOKEN_WORKFLOW` | release branch, tag push, GitHub release | + +All are organization-level on `eclipse-zenoh`; nothing is configured per +repository. + +## Known gaps + +- **The rewritten release path has never run.** The workflows were repaired for + a repository that no longer contains Rust; no rehearsal has yet exercised + them. +- **No consumer test.** Unlike zenoh-flat-jni, nothing resolves the published + `zenoh-java` artifact from a repository and runs it before release. The tests + here run against the build's own output. +- **The Android artifact has no runtime test**, and its `ndkVersion` and NDK + setup step are retained although no native code is built here — unverified + whether the Android Gradle Plugin still needs them. +- **`zenoh-flat-jni` itself has not been released**, so the ordering constraint + above has never been satisfied for a real release. + +## Release checklist + +- [ ] The `zenoh-flat-jni` version to build against is on Maven Central. +- [ ] `version.txt` and the intended tag agree. +- [ ] A rehearsal completed under a fresh version, with publication enabled at + least once so signing and credentials were exercised. +- [ ] `useLocalFlatJni` is off — the release resolves from Central. +- [ ] The published POM references a released `zenoh-flat-jni`, not a snapshot. +- [ ] For an Android release: the Android POM references + `zenoh-flat-jni-android`, not the desktop coordinate. +- [ ] The released coordinates resolve from Maven Central. diff --git a/README.md b/README.md index f8bb048e..7390d9f9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Check the website [zenoh.io](http://zenoh.io) and the [roadmap](https://github.c This repository provides a Java compatible Kotlin binding based on the main [Zenoh implementation written in Rust](https://github.com/eclipse-zenoh/zenoh). -The code relies on the Zenoh JNI native library, which written in Rust and communicates with the Kotlin layer via the Java Native Interface (JNI). +The code relies on a native library written in Rust, communicating with the Kotlin layer through the Java Native Interface (JNI). That library is not built in this repository: it is generated and published separately as [zenoh-flat-jni](https://github.com/eclipse-zenoh/zenoh-flat-jni) and consumed here as an ordinary Maven dependency. ## Zenoh Documentation @@ -49,7 +49,7 @@ dependencyResolutionManagement { After that add to the dependencies in the app's `build.gradle.kts`: ```kotlin -implementation("org.eclipse.zenoh:zenoh-java-android:1.1.1") +implementation("org.eclipse.zenoh:zenoh-java-android:1.9.0") ``` ### Platforms @@ -90,7 +90,7 @@ dependencyResolutionManagement { After that add to the dependencies in the app's `build.gradle.kts`: ```kotlin -implementation("org.eclipse.zenoh:zenoh-java-jvm:1.1.1") +implementation("org.eclipse.zenoh:zenoh-java:1.9.0") ``` ### Platforms @@ -112,7 +112,6 @@ For the moment, the library targets the following platforms: Basically: -- Rust ([Installation guide](https://doc.rust-lang.org/cargo/getting-started/installation.html)) - Kotlin ([Installation guide](https://kotlinlang.org/docs/getting-started.html#backend)) and in case of targeting Android you'll also need: @@ -125,6 +124,12 @@ This repository ships a [Gradle wrapper](https://docs.gradle.org/current/usergui Use `./gradlew` on Unix/macOS/Linux (or `gradlew.bat` on Windows) in place of `gradle` for all commands listed below. +The native libraries are not built here — they arrive inside the +`org.eclipse.zenoh:zenoh-flat-jni` dependency. To build against a local checkout +of that repository instead of the published artifact, pass +`-PuseLocalFlatJni=true`. Releasing is documented in +[PUBLISHING.md](PUBLISHING.md). + ## JVM JVM To publish a library for a JVM project into Maven local, run @@ -133,13 +138,12 @@ To publish a library for a JVM project into Maven local, run ./gradlew publishJvmPublicationToMavenLocal ``` -This will first, trigger the compilation of Zenoh-JNI in release, and second publish the library into maven local, containing the native library -as a resource that will be loaded during runtime. - -:warning: The native library will be compiled against the default rustup target on your machine, so although it may work fine -for you on your desktop, the generated publication may not be working on another computer with a different operating system and/or a different cpu architecture. +This compiles the Kotlin and publishes the library into Maven local. No native +code is built here: the native libraries arrive inside the +`org.eclipse.zenoh:zenoh-flat-jni` dependency, already cross-compiled for every +supported desktop target, so the result is not tied to the machine that built it. -Once we have published the package, we should be able to find it under `~/.m2/repository/org/eclipse/zenoh/zenoh-java-jvm/1.1.1`. +Once we have published the package, we should be able to find it under `~/.m2/repository/org/eclipse/zenoh/zenoh-java/1.9.0`. Finally, in the gradle file of the project where you intend to use this library, add mavenLocal to the list of repositories and add zenoh-java as a dependency: @@ -150,48 +154,31 @@ repositories { } dependencies { - implementation("org.eclipse.zenoh:zenoh-java-jvm:1.1.1") + implementation("org.eclipse.zenoh:zenoh-java:1.9.0") } ``` ## Android Android -In order to use these bindings in a native Android project, what we will do is to build them as an Android NDK Library, +To use these bindings in a native Android project, build the Android publication, publishing it into Maven local for us to be able to easily import it in our project. -It is required to have the [NDK (native development kit)](https://developer.android.com/ndk) installed, since we are going to compile Zenoh JNI for multiple -android native targets. The currently used NDK version is **26.0.10792818**. -It can be set up by using Android Studio (go to `Preferences > Languages & Frameworks > Android SDK > SDK Tools`, tick `Show Package Details` and pick the right NDK version), -or alternatively it can be found [here](https://developer.android.com/ndk/downloads). - -The native platforms we are going to target are the following ones: - -- x86 -- x86_64 -- arm -- arm64 - -Therefore, if they are not yet already added to the Rust toolchain, run: - -```bash -rustup target add armv7-linux-androideabi; \ -rustup target add i686-linux-android; \ -rustup target add aarch64-linux-android; \ -rustup target add x86_64-linux-android -``` - -to install them. +The Android native libraries are **not** built here either: they arrive inside +the `org.eclipse.zenoh:zenoh-flat-jni-android` artifact, cross-compiled for +`armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64` by that repository's release. No +NDK or Rust Android target is needed to build this SDK. -So, in order to publish the library onto Maven Local, run: +To publish the library onto Maven Local, run: ```bash ./gradlew -Pandroid=true publishAndroidReleasePublicationToMavenLocal ``` This will first trigger the compilation of the Zenoh-JNI for the previously mentioned targets, and secondly will -publish the library, containing the native binaries. +publish the library. The Android native binaries are not produced here — they +come from the `zenoh-flat-jni-android` dependency. -You should now be able to see the package under `~/.m2/repository/org/eclipse/zenoh/zenoh-java-android/1.1.1`. +You should now be able to see the package under `~/.m2/repository/org/eclipse/zenoh/zenoh-java-android/1.9.0`. Finally, in the gradle file of the project where you intend to use this library, add mavenLocal to the list of repositories and add zenoh-java-android as a dependency: @@ -202,7 +189,7 @@ repositories { } dependencies { - implementation("org.eclipse.zenoh:zenoh-kotlin-android:1.1.1") + implementation("org.eclipse.zenoh:zenoh-java-android:1.9.0") } ``` @@ -231,7 +218,8 @@ To run the tests, run: ./gradlew jvmTest ``` -This will compile the native library on debug mode (if not already available) and run the tests afterward against the JVM target. +This runs the tests against the JVM target. Nothing native is compiled: the +libraries come from the `zenoh-flat-jni` dependency. ## Logging @@ -292,6 +280,6 @@ Then after that, add the dependency as usual: ```kotlin dependencies { - implementation("org.eclipse.zenoh:zenoh-java-jvm:") + implementation("org.eclipse.zenoh:zenoh-java:") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 5cf17dc0..f6d1eb5c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,6 @@ buildscript { } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") - classpath("org.mozilla.rust-android-gradle:plugin:0.9.6") classpath("com.android.tools.build:gradle:7.4.2") classpath("com.gradleup.shadow:shadow-gradle-plugin:9.0.0-beta6") } @@ -28,7 +27,6 @@ plugins { id("com.android.library") version "7.4.2" apply false id("org.jetbrains.kotlin.android") version "1.9.10" apply false id("org.jetbrains.kotlin.multiplatform") version "1.9.0" apply false - id("org.mozilla.rust-android-gradle.rust-android") version "0.9.6" apply false id("org.jetbrains.dokka-javadoc") version "2.0.0" apply false id("com.adarshr.test-logger") version "3.2.0" apply false kotlin("plugin.serialization") version "1.9.0" apply false @@ -56,9 +54,29 @@ nexusPublishing { } } +// The zenoh-flat-jni release this SDK builds against. Overridable per-invocation +// (`-PzenohFlatJniVersion=…`) so a rehearsal can point at a snapshot without +// editing anything tracked. +val zenohFlatJniVersion: String by project + subprojects { repositories { google() mavenCentral() + // A rehearsal has to build against a zenoh-flat-jni that is not released + // yet; its own rehearsal publishes -SNAPSHOT here. This + // repository enters the resolution path *only* when a snapshot version + // was explicitly asked for, and even then only for that one module — so + // a release, whose version never ends in -SNAPSHOT, cannot resolve a + // mutable artifact by accident. + if (zenohFlatJniVersion.endsWith("-SNAPSHOT")) { + maven { + name = "centralSnapshots" + url = uri("https://central.sonatype.com/repository/maven-snapshots/") + // The root module plus the platform variants Gradle resolves + // through its metadata. + content { includeGroup("org.eclipse.zenoh") } + } + } } } diff --git a/ci/scripts/bump-and-tag.bash b/ci/scripts/bump-and-tag.bash index 5b5f7ee4..e40ecfc4 100644 --- a/ci/scripts/bump-and-tag.bash +++ b/ci/scripts/bump-and-tag.bash @@ -5,58 +5,51 @@ set -xeo pipefail readonly live_run=${LIVE_RUN:-false} # Release number readonly version=${VERSION:?input VERSION is required} -# Dependencies' pattern -readonly bump_deps_pattern=${BUMP_DEPS_PATTERN:-''} -# Dependencies' version -readonly bump_deps_version=${BUMP_DEPS_VERSION:-''} -# Dependencies' git branch -readonly bump_deps_branch=${BUMP_DEPS_BRANCH:-''} # Git actor name readonly git_user_name=${GIT_USER_NAME:?input GIT_USER_NAME is required} # Git actor email readonly git_user_email=${GIT_USER_EMAIL:?input GIT_USER_EMAIL is required} - -cargo +stable install toml-cli - -# NOTE(fuzzypixelz): toml-cli doesn't yet support in-place modification -# See: https://github.com/gnprice/toml-cli?tab=readme-ov-file#writing-ish-toml-set -function toml_set_in_place() { - local tmp=$(mktemp) - toml set "$1" "$2" "$3" > "$tmp" - mv "$tmp" "$1" -} +# The zenoh-flat-jni release to build against, if it is moving with this release +readonly flat_jni_version=${FLAT_JNI_VERSION:-''} export GIT_AUTHOR_NAME=$git_user_name export GIT_AUTHOR_EMAIL=$git_user_email export GIT_COMMITTER_NAME=$git_user_name export GIT_COMMITTER_EMAIL=$git_user_email -# Bump Gradle project version +# Bump Gradle project version. There is no Cargo manifest here any more: the +# native libraries live inside the zenoh-flat-jni artifact this SDK depends on. printf '%s' "$version" > version.txt -# Propagate version change to zenoh-jni -toml_set_in_place zenoh-jni/Cargo.toml "package.version" "$version" - -git commit version.txt zenoh-jni/Cargo.toml -m "chore: Bump version to \`$version\`" - -# Select all package dependencies that match $bump_deps_pattern and bump them to $bump_deps_version -if [[ "$bump_deps_pattern" != '' ]]; then - deps=$(toml get zenoh-jni/Cargo.toml dependencies | jq -r "keys[] | select(test(\"$bump_deps_pattern\"))") - for dep in $deps; do - if [[ -n $bump_deps_version ]]; then - toml_set_in_place zenoh-jni/Cargo.toml "dependencies.$dep.version" "$bump_deps_version" - fi - - if [[ -n $bump_deps_branch ]]; then - toml_set_in_place zenoh-jni/Cargo.toml "dependencies.$dep.branch" "$bump_deps_branch" - fi - done - # Update lockfile - cargo check --manifest-path zenoh-jni/Cargo.toml - if [[ -n $bump_deps_version || -n $bump_deps_branch ]]; then - git commit zenoh-jni/Cargo.toml zenoh-jni/Cargo.lock -m "chore: Bump \`$bump_deps_pattern\` dependencies to \`$bump_deps_version\`" +git commit version.txt -m "chore: Bump version to \`$version\`" + +# Point at the zenoh-flat-jni release this SDK is built against. It must be a +# real release, never a snapshot: consumers do not have the snapshot repository +# configured, and snapshots are mutable and eventually removed. +if [[ -n "$flat_jni_version" ]]; then + # A *release* may not depend on a snapshot: consumers do not configure the + # snapshot repository, and snapshots mutate and expire. A rehearsal may — that + # is how the SDK is exercised before the binding is released at all. + case "$flat_jni_version" in + *-SNAPSHOT) + if [[ "$live_run" == "true" ]]; then + echo "error: refusing to release against a snapshot dependency ($flat_jni_version)" >&2 + exit 1 + fi + echo "note: rehearsing against snapshot $flat_jni_version" + ;; + esac + + sed -i.bak -E "s|^zenohFlatJniVersion=.*|zenohFlatJniVersion=$flat_jni_version|" gradle.properties + rm -f gradle.properties.bak + + # Only commit when it actually moved: `git commit` on an unchanged file exits + # non-zero, which under `set -e` would abort the release before tagging. + if git diff --quiet gradle.properties; then + echo "note: already building against zenoh-flat-jni $flat_jni_version" else - echo "warn: no changes have been made to any dependencies matching $bump_deps_pattern" + git diff gradle.properties + git commit gradle.properties -m "chore: Build against zenoh-flat-jni \`$flat_jni_version\`" fi fi diff --git a/gradle.properties b/gradle.properties index 5c311496..3d1a48df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,11 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true + +# The zenoh-flat-jni release this SDK is built against. Rewritten by +# ci/scripts/bump-and-tag.bash at release time; override for a rehearsal with +# -PzenohFlatJniVersion=1.9.0-rc3-SNAPSHOT. +zenohFlatJniVersion=1.9.0 + +# Substitute a sibling ../zenoh-flat-jni checkout instead of resolving the +# artifact. Never enable this for a release. +useLocalFlatJni=false diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f04b1a3..63adc44d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,10 +24,16 @@ rootProject.name = "zenoh-java" include(":zenoh-java") include(":examples") -// zenoh-flat-jni is now a separate repository (https://github.com/ZettaScaleLabs/zenoh-flat-jni) -// and is consumed as a Maven artifact: org.eclipse.zenoh:zenoh-flat-jni:VERSION -// For local development with coordinated changes, use Gradle composite build: -includeBuild("../zenoh-flat-jni") +// zenoh-flat-jni (https://github.com/eclipse-zenoh/zenoh-flat-jni) is consumed as +// an ordinary Maven artifact: org.eclipse.zenoh:zenoh-flat-jni:$zenohFlatJniVersion. +// +// For coordinated local development, `-PuseLocalFlatJni=true` substitutes a +// sibling checkout through a composite build. It is off by default and must stay +// off for a release: with it on, the published artifact would be built against +// whatever is on that developer's disk rather than the resolved dependency. +if (providers.gradleProperty("useLocalFlatJni").orNull?.toBoolean() == true) { + includeBuild("../zenoh-flat-jni") +} plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0") diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index e4e0d742..bf36f732 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -21,6 +21,7 @@ plugins { signing } +val zenohFlatJniVersion: String by project val androidEnabled = project.findProperty("android")?.toString()?.toBoolean() == true val release = project.findProperty("release")?.toString()?.toBoolean() == true @@ -59,8 +60,14 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - // Zenoh Flat JNI - includes Kotlin sources and native libraries - implementation("org.eclipse.zenoh:zenoh-flat-jni:1.9.0") + // Zenoh Flat JNI. One coordinate: it is a Kotlin Multiplatform + // library, so Gradle resolves the JVM or Android variant from + // its module metadata according to the target being built. The + // native libraries come with whichever variant is chosen. + // + // Version lives in gradle.properties so the release can bump it + // and a rehearsal can point at a snapshot. + implementation("org.eclipse.zenoh:zenoh-flat-jni:$zenohFlatJniVersion") implementation("com.google.guava:guava:33.3.1-jre") } } @@ -83,9 +90,19 @@ kotlin { } val javadocJar by tasks.registering(Jar::class) { - dependsOn("dokkaGenerate") + dependsOn("dokkaGeneratePublicationJavadoc") archiveClassifier.set("javadoc") - from("${buildDir}/dokka/html") + // Dokka's javadoc publication writes here. It used to say `dokka/html`, + // which Dokka never produces, so the published javadoc JAR contained + // nothing but a manifest. + val javadocDir = layout.buildDirectory.dir("dokka/javadoc") + from(javadocDir) + doFirst { + val dir = javadocDir.get().asFile + check(dir.isDirectory && (dir.list()?.isNotEmpty() == true)) { + "$dir is missing or empty — the javadoc JAR would ship empty" + } + } } publishing {