Skip to content

Commit 32c31b5

Browse files
committed
Inital scaffold for libprocessing ffi.
1 parent ebd4df6 commit 32c31b5

File tree

16 files changed

+973
-107
lines changed

16 files changed

+973
-107
lines changed

app/build.gradle.kts

Lines changed: 92 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,94 @@
11
import org.gradle.internal.jvm.Jvm
22
import org.gradle.internal.os.OperatingSystem
33
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
4+
import org.gradle.process.ExecOperations
45
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
56
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
67
import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download
78
import java.io.FileOutputStream
89
import java.util.zip.ZipEntry
910
import java.util.zip.ZipOutputStream
11+
import javax.inject.Inject
1012

1113
// TODO: Update to 2.10.20 and add hot-reloading: https://github.com/JetBrains/compose-hot-reload
1214

15+
abstract class SignResourcesTask : DefaultTask() {
16+
@get:Inject
17+
abstract val execOperations: ExecOperations
18+
19+
@get:InputDirectory
20+
abstract val resourcesPath: DirectoryProperty
21+
22+
@TaskAction
23+
fun signResources() {
24+
val resourcesDir = resourcesPath.get().asFile
25+
val jars = mutableListOf<File>()
26+
27+
// Copy Info.plist if present
28+
project.fileTree(resourcesDir)
29+
.matching { include("**/Info.plist") }
30+
.singleOrNull()
31+
?.let { file ->
32+
project.copy {
33+
from(file)
34+
into(resourcesDir)
35+
}
36+
}
37+
38+
project.fileTree(resourcesDir) {
39+
include("**/*.jar")
40+
exclude("**/*.jar.tmp/**")
41+
}.forEach { file ->
42+
val tempDir = file.parentFile.resolve("${file.name}.tmp")
43+
project.copy {
44+
from(project.zipTree(file))
45+
into(tempDir)
46+
}
47+
file.delete()
48+
jars.add(tempDir)
49+
}
50+
51+
project.fileTree(resourcesDir) {
52+
include("**/bin/**")
53+
include("**/*.jnilib")
54+
include("**/*.dylib")
55+
include("**/*aarch64*")
56+
include("**/*x86_64*")
57+
include("**/*ffmpeg*")
58+
include("**/ffmpeg*/**")
59+
exclude("jdk/**")
60+
exclude("*.jar")
61+
exclude("*.so")
62+
exclude("*.dll")
63+
}.forEach { file ->
64+
execOperations.exec {
65+
commandLine("codesign", "--timestamp", "--force", "--deep", "--options=runtime", "--sign", "Developer ID Application", file)
66+
}
67+
}
68+
69+
jars.forEach { file ->
70+
FileOutputStream(File(file.parentFile, file.nameWithoutExtension)).use { fos ->
71+
ZipOutputStream(fos).use { zos ->
72+
file.walkTopDown().forEach { fileEntry ->
73+
if (fileEntry.isFile) {
74+
val zipEntryPath = fileEntry.relativeTo(file).path
75+
val entry = ZipEntry(zipEntryPath)
76+
zos.putNextEntry(entry)
77+
fileEntry.inputStream().use { input ->
78+
input.copyTo(zos)
79+
}
80+
zos.closeEntry()
81+
}
82+
}
83+
}
84+
}
85+
file.deleteRecursively()
86+
}
87+
88+
File(resourcesDir, "Info.plist").delete()
89+
}
90+
}
91+
1392
plugins{
1493
id("java")
1594
kotlin("jvm") version libs.versions.kotlin
@@ -49,14 +128,17 @@ compose.desktop {
49128
application {
50129
mainClass = "processing.app.ProcessingKt"
51130

52-
jvmArgs(*listOf(
53-
Pair("processing.version", rootProject.version),
54-
Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE),
55-
Pair("processing.contributions.source", "https://contributions.processing.org/contribs"),
56-
Pair("processing.download.page", "https://processing.org/download/"),
57-
Pair("processing.download.latest", "https://processing.org/download/latest.txt"),
58-
Pair("processing.tutorials", "https://processing.org/tutorials/"),
59-
).map { "-D${it.first}=${it.second}" }.toTypedArray())
131+
jvmArgs(
132+
"--enable-native-access=ALL-UNNAMED", // Required for Java 25 native library access
133+
*listOf(
134+
Pair("processing.version", rootProject.version),
135+
Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE),
136+
Pair("processing.contributions.source", "https://contributions.processing.org/contribs"),
137+
Pair("processing.download.page", "https://processing.org/download/"),
138+
Pair("processing.download.latest", "https://processing.org/download/latest.txt"),
139+
Pair("processing.tutorials", "https://processing.org/tutorials/"),
140+
).map { "-D${it.first}=${it.second}" }.toTypedArray()
141+
)
60142

61143
nativeDistributions{
62144
modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting", "jdk.httpserver")
@@ -421,82 +503,14 @@ tasks.register("includeProcessingResources"){
421503
finalizedBy("signResources")
422504
}
423505

424-
tasks.register("signResources"){
506+
tasks.register<SignResourcesTask>("signResources") {
425507
onlyIf {
426508
OperatingSystem.current().isMacOsX
427509
&&
428510
compose.desktop.application.nativeDistributions.macOS.signing.sign.get()
429511
}
430512
group = "compose desktop"
431-
val resourcesPath = composeResources("")
432-
433-
// find jars in the resources directory
434-
val jars = mutableListOf<File>()
435-
doFirst{
436-
fileTree(resourcesPath)
437-
.matching { include("**/Info.plist") }
438-
.singleOrNull()
439-
?.let { file ->
440-
copy {
441-
from(file)
442-
into(resourcesPath)
443-
}
444-
}
445-
fileTree(resourcesPath) {
446-
include("**/*.jar")
447-
exclude("**/*.jar.tmp/**")
448-
}.forEach { file ->
449-
val tempDir = file.parentFile.resolve("${file.name}.tmp")
450-
copy {
451-
from(zipTree(file))
452-
into(tempDir)
453-
}
454-
file.delete()
455-
jars.add(tempDir)
456-
}
457-
fileTree(resourcesPath){
458-
include("**/bin/**")
459-
include("**/*.jnilib")
460-
include("**/*.dylib")
461-
include("**/*aarch64*")
462-
include("**/*x86_64*")
463-
include("**/*ffmpeg*")
464-
include("**/ffmpeg*/**")
465-
exclude("jdk/**")
466-
exclude("*.jar")
467-
exclude("*.so")
468-
exclude("*.dll")
469-
}.forEach{ file ->
470-
exec {
471-
commandLine("codesign", "--timestamp", "--force", "--deep","--options=runtime", "--sign", "Developer ID Application", file)
472-
}
473-
}
474-
jars.forEach { file ->
475-
FileOutputStream(File(file.parentFile, file.nameWithoutExtension)).use { fos ->
476-
ZipOutputStream(fos).use { zos ->
477-
file.walkTopDown().forEach { fileEntry ->
478-
if (fileEntry.isFile) {
479-
// Calculate the relative path for the zip entry
480-
val zipEntryPath = fileEntry.relativeTo(file).path
481-
val entry = ZipEntry(zipEntryPath)
482-
zos.putNextEntry(entry)
483-
484-
// Copy file contents to the zip
485-
fileEntry.inputStream().use { input ->
486-
input.copyTo(zos)
487-
}
488-
zos.closeEntry()
489-
}
490-
}
491-
}
492-
}
493-
494-
file.deleteRecursively()
495-
}
496-
file(composeResources("Info.plist")).delete()
497-
}
498-
499-
513+
resourcesPath.set(composeResources(""))
500514
}
501515
tasks.register("setExecutablePermissions") {
502516
description = "Sets executable permissions on binaries in Processing.app resources"

build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ plugins {
1212
// Can be deleted after the migration to Gradle is complete
1313
layout.buildDirectory = file(".build")
1414

15+
allprojects {
16+
tasks.withType<JavaCompile>().configureEach {
17+
sourceCompatibility = "24"
18+
targetCompatibility = "24"
19+
}
20+
21+
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
22+
compilerOptions {
23+
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_24)
24+
}
25+
}
26+
}
27+
1528
// Configure the dependencyUpdates task
1629
tasks {
1730
dependencyUpdates {

core/build.gradle.kts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,138 @@ dependencies {
3535
testImplementation(libs.junit)
3636
}
3737

38+
val osName = System.getProperty("os.name").lowercase()
39+
val osArch = System.getProperty("os.arch").lowercase()
40+
41+
val platformName = when {
42+
osName.contains("mac") || osName.contains("darwin") -> "macos"
43+
osName.contains("win") -> "windows"
44+
osName.contains("linux") -> "linux"
45+
else -> throw GradleException("Unsupported OS: $osName")
46+
}
47+
48+
val archName = when {
49+
osArch.contains("aarch64") || osArch.contains("arm") -> "aarch64"
50+
osArch.contains("x86_64") || osArch.contains("amd64") -> "x86_64"
51+
else -> throw GradleException("Unsupported architecture: $osArch")
52+
}
53+
54+
val libExtension = when (platformName) {
55+
"macos" -> "dylib"
56+
"windows" -> "dll"
57+
"linux" -> "so"
58+
else -> throw GradleException("Unknown platform: $platformName")
59+
}
60+
61+
val libName = when (platformName) {
62+
"windows" -> "processing.$libExtension"
63+
else -> "libprocessing.$libExtension"
64+
}
65+
66+
val platformTarget = "$platformName-$archName"
67+
val libProcessingDir = file("${project.rootDir}/libProcessing")
68+
val rustTargetDir = file("$libProcessingDir/target")
69+
val nativeOutputDir = file("${layout.buildDirectory.get()}/native/$platformTarget")
70+
71+
val cargoPath = System.getenv("CARGO_HOME")?.let { "$it/bin/cargo" }
72+
?: "${System.getProperty("user.home")}/.cargo/bin/cargo"
73+
74+
val buildRustRelease by tasks.registering(Exec::class) {
75+
group = "rust"
76+
description = "Build Rust FFI library in release mode for current platform"
77+
78+
workingDir = libProcessingDir
79+
commandLine = listOf(cargoPath, "build", "--release", "--manifest-path", "ffi/Cargo.toml")
80+
81+
inputs.files(fileTree("$libProcessingDir/ffi/src"))
82+
inputs.file("$libProcessingDir/ffi/Cargo.toml")
83+
inputs.file("$libProcessingDir/Cargo.toml")
84+
inputs.file("$libProcessingDir/ffi/build.rs")
85+
inputs.file("$libProcessingDir/ffi/cbindgen.toml")
86+
87+
outputs.file("$rustTargetDir/release/$libName")
88+
outputs.file("$libProcessingDir/ffi/include/processing.h")
89+
90+
doFirst {
91+
logger.lifecycle("Building Rust library for $platformTarget...")
92+
}
93+
}
94+
95+
val copyNativeLibs by tasks.registering(Copy::class) {
96+
group = "rust"
97+
description = "Copy processing library to build directory"
98+
99+
dependsOn(buildRustRelease)
100+
101+
from("$rustTargetDir/release") {
102+
include("libprocessing.a")
103+
include("libprocessing.$libExtension")
104+
}
105+
106+
into(nativeOutputDir)
107+
108+
doFirst {
109+
logger.lifecycle("Copying native libraries to $nativeOutputDir")
110+
}
111+
}
112+
113+
val bundleNativeLibs by tasks.registering(Copy::class) {
114+
group = "rust"
115+
description = "Bundle native library into resources"
116+
117+
dependsOn(copyNativeLibs)
118+
119+
from(nativeOutputDir)
120+
into("${sourceSets.main.get().output.resourcesDir}/native/$platformTarget")
121+
122+
doFirst {
123+
logger.lifecycle("Bundling libraries for $platformTarget into resources")
124+
}
125+
}
126+
127+
val jextractPath = "${System.getProperty("user.home")}/jextract-22/bin/jextract"
128+
val generatedJavaDir = file("${layout.buildDirectory.get()}/generated/sources/jextract/java")
129+
val headerFile = file("$libProcessingDir/ffi/include/processing.h")
130+
131+
sourceSets.main {
132+
java.srcDirs(generatedJavaDir)
133+
}
134+
135+
val generateJavaBindings by tasks.registering(Exec::class) {
136+
group = "rust"
137+
description = "Generate Java bindings from libProcessing headers using jextract"
138+
139+
dependsOn(buildRustRelease)
140+
141+
inputs.file(headerFile)
142+
143+
outputs.dir(generatedJavaDir)
144+
145+
doFirst {
146+
generatedJavaDir.mkdirs()
147+
logger.lifecycle("Generating Java bindings from $headerFile...")
148+
}
149+
150+
commandLine = listOf(
151+
jextractPath,
152+
"--output", generatedJavaDir.absolutePath,
153+
"--target-package", "processing.ffi",
154+
headerFile.absolutePath
155+
)
156+
}
157+
158+
tasks.named("compileJava") {
159+
dependsOn(generateJavaBindings)
160+
}
161+
162+
tasks.named("compileKotlin") {
163+
dependsOn(generateJavaBindings)
164+
}
165+
166+
tasks.named("processResources") {
167+
dependsOn(bundleNativeLibs)
168+
}
169+
38170
mavenPublishing{
39171
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
40172
signAllPublications()

0 commit comments

Comments
 (0)