diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index fe9c6cd..ab26021 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -23,26 +23,39 @@ jobs: filename: codemp.dll - runner: macos-14 target: darwin-arm64 - filename: codemp.dylib + filename: libcodemp.dylib steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + - run: cargo build --release --features=java + - uses: actions/upload-artifact@v4 + with: + name: codemp-java-${{ matrix.platform.target }} + path: target/release/${{ matrix.platform.filename }} + + publish: + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: dist/java/artifacts + pattern: codemp-java-* + merge-multiple: true + - run: tree + working-directory: dist/java - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' - uses: gradle/actions/setup-gradle@v4 with: - gradle-version: "8.10" # Quotes required to prevent YAML converting to number - - run: gradle build + gradle-version: "8.10" working-directory: dist/java - - uses: actions/upload-artifact@v4 - with: - name: codemp-java-${{ matrix.platform.target }} - path: dist/java/build/libs - run: gradle publish working-directory: dist/java env: @@ -50,3 +63,4 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_CENTRAL_GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_CENTRAL_GPG_PASSWORD }} + diff --git a/.gitignore b/.gitignore index 5a6fc9f..dc39325 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ dist/java/.gradle/ dist/java/.project dist/java/.settings/ dist/java/bin/ +dist/java/artifacts/ # intellij insists on creating the wrapper every time even if it's not strictly necessary dist/java/gradle/ diff --git a/dist/README.md b/dist/README.md index 317e696..66a935b 100644 --- a/dist/README.md +++ b/dist/README.md @@ -33,14 +33,20 @@ Thus, we also provide pre-made Java glue code, wrapping all native calls and def The Java bindings have no known major quirk. However, here are a list of facts that are useful to know when developing with these: -* Memory management is entirely delegated to the JVM's garbage collector. - * A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually. +* Memory management is entirely delegated to the JVM's garbage collector using the `Cleaner` API. + * Because of this, we require Java 11 as minimum version: `Cleaner` was added in version 9. This should not be a problem, as IDEs tend to run on recent versions, but if there is actual demand for it we may add a Java 8-friendly version using `Object.finalize()` (which is deprecated in modern JDKs). * Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`. * `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug. ### Using -`codemp` **will be available soon** as an artifact on [Maven Central](https://mvnrepository.com) +`codemp` is available on [Maven Central](https://central.sonatype.com/artifact/mp.code/codemp), with each officially supported OS as an archive classifier. ### Building -This is a [Gradle](https://gradle.org/) project: building requires having both Gradle and Cargo installed, as well as the JDK (any non-abandoned version). -Once you have all the requirements, building is as simple as running `gradle build`: the output is going to be a JAR under `build/libs`, which you can import into your classpath with your IDE of choice. +> [!NOTE] +> The following instructions assume `dist/java` as current working directory. + +This is a [Gradle](https://gradle.org/) project, so you must have install `gradle` (as well as JDK 11 or higher) in order to build it. +- You can build a JAR without bundling the native library with `gradle build`. +- Otherwise, you can compile the project for your current OS and create a JAR that bundles the resulting binary with `gradle nativeBuild`; do note that this second way of building also requires Cargo and the relevant Rust toolchain. + +In both cases, the output is going to be a JAR under `build/libs`, which you can import into your classpath with your IDE of choice. diff --git a/dist/java/build.gradle b/dist/java/build.gradle index 240dacc..7615946 100644 --- a/dist/java/build.gradle +++ b/dist/java/build.gradle @@ -1,14 +1,104 @@ plugins { id 'java-library' - id "com.vanniktech.maven.publish" version "0.29.0" + id "com.vanniktech.maven.publish.base" version "0.30.0" id 'com.google.osdetector' version '1.7.3' } group = 'mp.code' version = '0.7.3' +tasks.register('windowsJar', Jar) { + outputs.upToDateWhen { false } + archiveClassifier = 'windows-x86_64' + from sourceSets.main.runtimeClasspath + from('artifacts') { + include('*.dll') + into('natives/') + } + doFirst { + if(!(new File('artifacts/codemp.dll').exists())) { + throw new GradleException("The required file does not exist!") + } + } +} + +tasks.register('macosJar', Jar) { + outputs.upToDateWhen { false } + archiveClassifier = 'osx-aarch_64' + from sourceSets.main.runtimeClasspath + from('artifacts') { + include('*.dylib') + into('natives/') + } + doFirst { + if(!(new File('artifacts/libcodemp.dylib').exists())) { + throw new GradleException("The required file does not exist!") + } + } +} + +tasks.register('linuxJar', Jar) { + outputs.upToDateWhen { false } + archiveClassifier = 'linux-x86_64' + from sourceSets.main.runtimeClasspath + from('artifacts') { + include('*.so') + into('natives/') + } + doFirst { + if(!(new File('artifacts/libcodemp.so').exists())) { + throw new GradleException("The required file does not exist! Maybe you need to `cargo build` the main library first?") + } + } +} + +tasks.register('multiplatformJar', Jar) { + outputs.upToDateWhen { false } + archiveClassifier = 'all' + from sourceSets.main.runtimeClasspath + from('artifacts') { + include('*') + into('natives/') + } +} + +configurations { + windowsJar { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation, runtimeOnly + } + linuxJar { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation, runtimeOnly + } + macosJar { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation, runtimeOnly + } + multiplatformJar { + canBeConsumed = true + canBeResolved = false + extendsFrom implementation, runtimeOnly + } +} + java { sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11 + withSourcesJar() + withJavadocJar() +} + +artifacts { + archives jar + archives sourcesJar + archives javadocJar + windowsJar(windowsJar) + macosJar(macosJar) + linuxJar(linuxJar) + multiplatformJar(multiplatformJar) } repositories { @@ -29,17 +119,17 @@ tasks.register('cargoBuild', Exec) { commandLine 'cargo', 'build', '--release', '--features=java' } -jar.archiveClassifier = osdetector.classifier - def rustDir = projectDir.toPath() .parent .parent .resolve('target') .resolve('release') .toFile() -processResources { + +tasks.register('nativeBuild', Jar) { + archiveClassifier = osdetector.classifier dependsOn cargoBuild - outputs.upToDateWhen { false } // no caching + from sourceSets.main.runtimeClasspath from(rustDir) { include('*.dll') include('*.so') @@ -48,9 +138,23 @@ processResources { } } +publishing { + publications { + mavenJava(MavenPublication) { + artifact jar + artifact sourcesJar + artifact javadocJar + artifact windowsJar + artifact linuxJar + artifact macosJar + artifact multiplatformJar + } + } +} + import com.vanniktech.maven.publish.SonatypeHost mavenPublishing { - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) signAllPublications() coordinates(project.group, rootProject.name, project.version) diff --git a/dist/java/src/mp/code/BufferController.java b/dist/java/src/mp/code/BufferController.java index 99570fa..fe1becd 100644 --- a/dist/java/src/mp/code/BufferController.java +++ b/dist/java/src/mp/code/BufferController.java @@ -70,6 +70,7 @@ public final class BufferController { /** * Tries to send a {@link TextChange} update. + * @param change the update to send * @throws ControllerException if the controller was stopped */ public void send(TextChange change) throws ControllerException { @@ -81,6 +82,8 @@ public final class BufferController { /** * Registers a callback to be invoked whenever a {@link BufferUpdate} occurs. * This will not work unless a Java thread has been dedicated to the event loop. + * @param cb a {@link Consumer} that receives the controller when the change occurs; + * you should probably spawn a new thread in here, to avoid deadlocking * @see Extensions#drive(boolean) */ public void callback(Consumer cb) { diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index 6c2cae1..2a0f5ee 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -43,14 +43,15 @@ public final class CursorController { return recv(this.ptr); } - private static native void send(long self, Selection cursor) throws ControllerException; + private static native void send(long self, Selection selection) throws ControllerException; /** * Tries to send a {@link Selection} update. + * @param selection the update to send * @throws ControllerException if the controller was stopped */ - public void send(Selection cursor) throws ControllerException { - send(this.ptr, cursor); + public void send(Selection selection) throws ControllerException { + send(this.ptr, selection); } private static native void callback(long self, Consumer cb); @@ -58,6 +59,8 @@ public final class CursorController { /** * Registers a callback to be invoked whenever a {@link Cursor} update occurs. * This will not work unless a Java thread has been dedicated to the event loop. + * @param cb a {@link Consumer} that receives the controller when the change occurs; + * you should probably spawn a new thread in here, to avoid deadlocking * @see Extensions#drive(boolean) */ public void callback(Consumer cb) { diff --git a/dist/java/src/mp/code/Extensions.java b/dist/java/src/mp/code/Extensions.java index ff4a1b7..327e0f4 100644 --- a/dist/java/src/mp/code/Extensions.java +++ b/dist/java/src/mp/code/Extensions.java @@ -33,7 +33,7 @@ public final class Extensions { *

* You may alternatively call this with true, in a separate and dedicated Java thread; * it will remain active in the background and act as event loop. Assign it like this: - *

new Thread(() -> Extensions.drive(true)).start();

+ *

new Thread(() -> Extensions.drive(true)).start();

* @param block true if it should use the current thread */ public static native void drive(boolean block); diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index b359516..116f24a 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -198,6 +198,8 @@ public final class Workspace { /** * Registers a callback to be invoked whenever a new {@link Event} is ready to be received. * This will not work unless a Java thread has been dedicated to the event loop. + * @param cb a {@link Consumer} that receives the controller when the change occurs; + * you should probably spawn a new thread in here, to avoid deadlocking * @see Extensions#drive(boolean) */ public void callback(Consumer cb) { diff --git a/dist/java/src/mp/code/data/TextChange.java b/dist/java/src/mp/code/data/TextChange.java index 023869f..6d1879e 100644 --- a/dist/java/src/mp/code/data/TextChange.java +++ b/dist/java/src/mp/code/data/TextChange.java @@ -3,9 +3,6 @@ package mp.code.data; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; -import mp.code.Extensions; - -import java.util.OptionalLong; /** * A data class holding information about a text change. diff --git a/dist/java/src/mp/code/exceptions/ControllerException.java b/dist/java/src/mp/code/exceptions/ControllerException.java index 2061478..f5462d9 100644 --- a/dist/java/src/mp/code/exceptions/ControllerException.java +++ b/dist/java/src/mp/code/exceptions/ControllerException.java @@ -2,8 +2,8 @@ package mp.code.exceptions; /** * An exception that may occur when a {@link mp.code.BufferController} or - * a {@link mp.code.CursorController} perform an illegal operation. - * It may also occur as a result of {@link mp.code.Workspace#event()}. + * a {@link mp.code.CursorController} or {@link mp.code.Workspace} (in the + * receiver part) perform an illegal operation. */ public abstract class ControllerException extends Exception {