diff --git a/.gitignore b/.gitignore index 67057f942..4532993a0 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,19 @@ gen-external-apklibs hs_err_pid* replay_pid* +# Maven ignores +.kotlin +.gradle +.build/ +/core/build/ +/build/publish/ +/app/build +/java/build/ +/build/reports +/java/bin +/java/libraries/svg/bin +/java/preprocessor/build +/java/lsp/build ### Gradle ### .gradle **/build/ @@ -123,4 +136,16 @@ generated/ !java/libraries/serial/library/jssc.jar /app/windows/obj /java/gradle/build +/core/examples/build /java/gradle/example/.processing +/app/windows/obj +/java/android/example/build +/java/android/example/.processing +/java/gradle/example/build +/java/gradle/example/gradle/wrapper/gradle-wrapper.jar +/java/gradle/example/gradle/wrapper/gradle-wrapper.properties +/java/gradle/example/gradlew +/java/gradle/example/gradlew.bat +/java/gradle/example/.kotlin/errors +/java/gradle/hotreload/build +*.iml diff --git a/app/utils/build.gradle.kts b/app/utils/build.gradle.kts index 193188f95..1618e1706 100644 --- a/app/utils/build.gradle.kts +++ b/app/utils/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("java") + alias(libs.plugins.mavenPublish) } repositories { @@ -11,6 +12,15 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") } +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + tasks.test { useJUnitPlatform() } \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8f7211b13..f646bc9b8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -15,6 +15,7 @@ sourceSets{ main{ java{ srcDirs("src") + exclude("**/*.jnilib") } resources{ srcDirs("src") @@ -34,10 +35,21 @@ dependencies { testImplementation(libs.junit) } +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} mavenPublishing{ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - signAllPublications() + + // Only sign if signing is set up + if(project.hasProperty("signing.keyId") || project.hasProperty("signing.signingInMemoryKey")) + signAllPublications() pom{ name.set("Processing Core") @@ -77,3 +89,6 @@ tasks.withType { tasks.compileJava{ options.encoding = "UTF-8" } +tasks.javadoc{ + options.encoding = "UTF-8" +} diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 4fccd1a53..d9df211eb 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -705,7 +705,7 @@ public class PApplet implements PConstants { protected boolean exitCalled; // ok to be static because it's not possible to mix enabled/disabled - static protected boolean disableAWT; + static protected boolean disableAWT = System.getProperty("processing.awt.disable", "false").equals("true");; // messages to send if attached as an external vm @@ -9940,19 +9940,21 @@ static public void runSketch(final String[] args, System.exit(1); } - boolean external = false; - int[] location = null; - int[] editorLocation = null; + boolean external = System.getProperty("processing.external", "false").equals("true");; + int[] location = System.getProperty("processing.location", null) != null ? + parseInt(split(System.getProperty("processing.location"), ',')) : null; + int[] editorLocation = System.getProperty("processing.editor.location", null) != null ? + parseInt(split(System.getProperty("processing.editor.location"), ',')) : null; String name = null; int windowColor = 0; int stopColor = 0xff808080; - boolean hideStop = false; + boolean hideStop = System.getProperty("processing.stop.hide", "false").equals("true"); int displayNum = -1; // use default - boolean present = false; - boolean fullScreen = false; - float uiScale = 0; + boolean present = System.getProperty("processing.present", "false").equals("true"); + boolean fullScreen = System.getProperty("processing.fullscreen", "false").equals("true"); + float uiScale = parseInt(System.getProperty("processing.uiScale", "0"), 0); String param, value; String folder = calcSketchPath(); diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts new file mode 100644 index 000000000..0171384f4 --- /dev/null +++ b/java/gradle/build.gradle.kts @@ -0,0 +1,41 @@ +plugins{ + `java-gradle-plugin` + alias(libs.plugins.gradlePublish) + + kotlin("jvm") version libs.versions.kotlin +} + +repositories { + mavenCentral() + maven("https://jogamp.org/deployment/maven") +} + +dependencies{ + implementation(project(":java:preprocessor")) + + implementation(libs.composeGradlePlugin) + implementation(libs.kotlinGradlePlugin) + implementation(libs.kotlinComposePlugin) + + testImplementation(project(":core")) + testImplementation(libs.junit) +} + +// TODO: CI/CD for publishing the plugin to the Gradle Plugin Portal +gradlePlugin{ + plugins{ + create("processing.java"){ + id = "org.processing.java" + implementationClass = "org.processing.java.gradle.ProcessingPlugin" + } + } +} +publishing{ + repositories{ + mavenLocal() + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} \ No newline at end of file diff --git a/java/gradle/example/brightness.pde b/java/gradle/example/brightness.pde new file mode 100644 index 000000000..dad7885af --- /dev/null +++ b/java/gradle/example/brightness.pde @@ -0,0 +1,28 @@ +/** + * Brightness + * by Rusty Robison. + * + * Brightness is the relative lightness or darkness of a color. + * Move the cursor vertically over each bar to alter its brightness. + */ + +int barWidth = 20; +int lastBar = -1; + + +void setup() { + size(640, 360, P2D); + colorMode(HSB, width, 100, height); + noStroke(); + background(0); +} + +void draw() { + int whichBar = mouseX / barWidth; + if (whichBar != lastBar) { + int barX = whichBar * barWidth; + fill(barX, 100, mouseY); + rect(barX, 0, barWidth, height); + lastBar = whichBar; + } +} diff --git a/java/gradle/example/build.gradle.kts b/java/gradle/example/build.gradle.kts new file mode 100644 index 000000000..b476d51bb --- /dev/null +++ b/java/gradle/example/build.gradle.kts @@ -0,0 +1,3 @@ +plugins{ + id("org.processing.java") +} \ No newline at end of file diff --git a/java/gradle/example/settings.gradle.kts b/java/gradle/example/settings.gradle.kts new file mode 100644 index 000000000..ee9c97e15 --- /dev/null +++ b/java/gradle/example/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "processing-gradle-plugin-demo" + +pluginManagement { + includeBuild("../../../") +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/DependenciesTask.kt b/java/gradle/src/main/kotlin/DependenciesTask.kt new file mode 100644 index 000000000..8e2cb9bca --- /dev/null +++ b/java/gradle/src/main/kotlin/DependenciesTask.kt @@ -0,0 +1,79 @@ +package org.processing.java.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.ObjectInputStream + +/* +* The DependenciesTask resolves the dependencies for the sketch based on the libraries used + */ +abstract class DependenciesTask: DefaultTask() { + @InputFile + val librariesMetaData: RegularFileProperty = project.objects.fileProperty() + + @InputFile + val sketchMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + librariesMetaData.convention(project.layout.buildDirectory.file("processing/libraries")) + sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch")) + } + + @TaskAction + fun execute() { + val sketchMetaFile = sketchMetaData.get().asFile + val librariesMetaFile = librariesMetaData.get().asFile + + val libraries = librariesMetaFile.inputStream().use { input -> + ObjectInputStream(input).readObject() as ArrayList + } + + val sketch = sketchMetaFile.inputStream().use { input -> + ObjectInputStream(input).readObject() as PDETask.SketchMeta + } + + val dependencies = mutableSetOf() + + // Loop over the import statements in the sketch and import the relevant jars from the libraries + sketch.importStatements.forEach import@{ statement -> + libraries.forEach { library -> + library.jars.forEach { jar -> + jar.classes.forEach { className -> + if (className.startsWith(statement)) { + dependencies.addAll(library.jars.map { it.path } ) + return@import + } + } + } + } + } + project.dependencies.add("implementation", project.files(dependencies) ) + + // TODO: Mutating the dependencies of configuration ':implementation' after it has been resolved or consumed. This + + // TODO: Add only if user is compiling for P2D or P3D + // Add JOGL and Gluegen dependencies + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0") + project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0") + + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + + val variant = when { + os.contains("mac") -> "macosx-universal" + os.contains("win") && arch.contains("64") -> "windows-amd64" + os.contains("linux") && arch.contains("aarch64") -> "linux-aarch64" + os.contains("linux") && arch.contains("arm") -> "linux-arm" + os.contains("linux") && arch.contains("amd64") -> "linux-amd64" + else -> throw GradleException("Unsupported OS/architecture: $os / $arch") + } + + project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-$variant") + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-$variant") + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:newt:2.5.0:natives-$variant") + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/LibrariesTask.kt b/java/gradle/src/main/kotlin/LibrariesTask.kt new file mode 100644 index 000000000..2ccca5cde --- /dev/null +++ b/java/gradle/src/main/kotlin/LibrariesTask.kt @@ -0,0 +1,81 @@ +package org.processing.java.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.ObjectOutputStream +import java.util.jar.JarFile + +/* +The libraries task scans the sketchbook libraries folder for all the libraries +This task stores the resulting information in a file that can be used later to resolve dependencies + */ +abstract class LibrariesTask : DefaultTask() { + + @InputFiles + val libraryDirectories: ConfigurableFileCollection = project.files() + + @OutputFile + val librariesMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + librariesMetaData.convention { project.gradle.gradleUserHomeDir.resolve("common/processing/libraries") } + } + + data class Jar( + val path: File, + val classes: List + ) : java.io.Serializable + + data class Library( + val jars: List + ) : java.io.Serializable + + @TaskAction + fun execute() { + val output = libraryDirectories.flatMap { librariesDirectory -> + if (!librariesDirectory.exists()) { + logger.error("Libraries directory (${librariesDirectory.path}) does not exist. Libraries will not be imported.") + return@flatMap emptyList() + } + val libraries = librariesDirectory + .listFiles { file -> file.isDirectory } + ?.map { folder -> + // Find all the jars in the sketchbook + val jars = folder.resolve("library") + .listFiles{ file -> file.extension == "jar" } + ?.map{ file -> + + // Inside each jar, look for the defined classes + val jar = JarFile(file) + val classes = jar.entries().asSequence() + .filter { entry -> entry.name.endsWith(".class") } + .map { entry -> entry.name } + .map { it.substringBeforeLast('/').replace('/', '.') } + .distinct() + .toList() + + // Return a reference to the jar and its classes + return@map Jar( + path = file, + classes = classes + ) + }?: emptyList() + + // Save the parsed jars and which folder + return@map Library( + jars = jars + ) + }?: emptyList() + + return@flatMap libraries + } + val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream()) + meta.writeObject(output) + meta.close() + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/PDETask.kt b/java/gradle/src/main/kotlin/PDETask.kt new file mode 100644 index 000000000..76ac195e5 --- /dev/null +++ b/java/gradle/src/main/kotlin/PDETask.kt @@ -0,0 +1,83 @@ +package org.processing.java.gradle + +import org.gradle.api.file.* +import org.gradle.api.tasks.* +import org.gradle.internal.file.Deleter +import org.gradle.work.InputChanges +import processing.mode.java.preproc.PdePreprocessor +import java.io.File +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.concurrent.Callable +import java.util.jar.JarFile +import javax.inject.Inject + + +// TODO: Generate sourcemaps +/* +* The PDETask is the main task that processes the .pde files and generates the Java source code through the PdePreprocessor. + */ +abstract class PDETask : SourceTask() { + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories + @get:SkipWhenEmpty + open val stableSources: FileCollection = project.files(Callable { this.source }) + + @OutputDirectory + val outputDirectory: DirectoryProperty = project.objects.directoryProperty() + + @get:Input + var sketchName: String = "processing" + + @OutputFile + val sketchMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + outputDirectory.convention(project.layout.buildDirectory.dir("generated/pde")) + sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch")) + } + + data class SketchMeta( + val sketchName: String, + val sketchRenderer: String?, + val importStatements: List + ) : Serializable + + @TaskAction + fun execute() { + // Using stableSources since we can only run the pre-processor on the full set of sources + val combined = stableSources + .files + .groupBy { it.name } + .map { entry -> + entry.value.firstOrNull { it.parentFile?.name == "unsaved" } + ?: entry.value.first() + } + .joinToString("\n"){ + it.readText() + } + val javaFile = File(outputDirectory.get().asFile, "$sketchName.java").bufferedWriter() + + val meta = PdePreprocessor + .builderFor(sketchName) + .setTabSize(4) + .build() + .write(javaFile, combined) + + // TODO: Save the edits to meta files + + javaFile.flush() + javaFile.close() + + val sketchMeta = SketchMeta( + sketchName = sketchName, + sketchRenderer = meta.sketchRenderer, + importStatements = meta.importStatements.map { importStatement -> importStatement.packageName } + ) + + val metaFile = ObjectOutputStream(sketchMetaData.get().asFile.outputStream()) + metaFile.writeObject(sketchMeta) + metaFile.close() + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt new file mode 100644 index 000000000..df558710f --- /dev/null +++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt @@ -0,0 +1,216 @@ +package org.processing.java.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.internal.file.DefaultSourceDirectorySet +import org.gradle.api.internal.tasks.TaskDependencyFactory +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.JavaExec +import org.jetbrains.compose.ComposeExtension +import org.jetbrains.compose.desktop.DesktopExtension +import java.io.File +import java.net.Socket +import javax.inject.Inject + +class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFactory) : Plugin { + override fun apply(project: Project) { + val sketchName = project.layout.projectDirectory.asFile.name.replace(Regex("[^a-zA-Z0-9_]"), "_") + + val isProcessing = project.findProperty("processing.version") != null + val processingVersion = project.findProperty("processing.version") as String? ?: "4.3.4" + val processingGroup = project.findProperty("processing.group") as String? ?: "org.processing" + val workingDir = project.findProperty("processing.workingDir") as String? + val debugPort = project.findProperty("processing.debugPort") as String? + val logPort = project.findProperty("processing.logPort") as String? + val errPort = project.findProperty("processing.errPort") as String? + + // TODO: Setup sketchbook when using as a standalone plugin, use the Java Preferences + val sketchbook = project.findProperty("processing.sketchbook") as String? + val settings = project.findProperty("processing.settings") as String? + val root = project.findProperty("processing.root") as String? + + // Apply the Java plugin to the Project, equivalent of + // plugins { + // java + // } + project.plugins.apply(JavaPlugin::class.java) + + if(isProcessing){ + // Set the build directory to a temp file so it doesn't clutter up the sketch folder + // Only if the build directory doesn't exist, otherwise proceed as normal + if(!project.layout.buildDirectory.asFile.get().exists()) { + project.layout.buildDirectory.set(File(project.findProperty("processing.workingDir") as String)) + } + // Disable the wrapper in the sketch to keep it cleaner + project.tasks.findByName("wrapper")?.enabled = false + } + + // Add kotlin support, equivalent of + // plugins { + // kotlin("jvm") version "1.8.0" + // kotlin("plugin.compose") version "1.8.0" + // } + project.plugins.apply("org.jetbrains.kotlin.jvm") + // Add jetpack compose support + project.plugins.apply("org.jetbrains.kotlin.plugin.compose") + // Add the compose plugin to wrap the sketch in an executable + project.plugins.apply("org.jetbrains.compose") + + // Add the Processing core library (within Processing from the internal maven repo and outside from the internet), equivalent of + // dependencies { + // implementation("org.processing:core:4.3.4") + // } + project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}") + + // Add the jars in the code folder, equivalent of + // dependencies { + // implementation(fileTree("src") { include("**/code/*.jar") }) + // } + project.dependencies.add("implementation", project.fileTree("src").apply { include("**/code/*.jar") }) + + // Add the repositories necessary for building the sketch, equivalent of + // repositories { + // maven("https://jogamp.org/deployment/maven") + // mavenCentral() + // mavenLocal() + // } + project.repositories.add(project.repositories.maven { it.setUrl("https://jogamp.org/deployment/maven") }) + project.repositories.add(project.repositories.mavenCentral()) + project.repositories.add(project.repositories.mavenLocal()) + + // Configure the compose Plugin, equivalent of + // compose { + // application { + // mainClass.set(sketchName) + // nativeDistributions { + // includeAllModules() + // } + // } + // } + project.extensions.configure(ComposeExtension::class.java) { extension -> + extension.extensions.getByType(DesktopExtension::class.java).application { application -> + // Set the class to be executed initially + application.mainClass = sketchName + application.nativeDistributions.includeAllModules = true + if(debugPort != null) { + application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") + } + } + } + + // TODO: Add support for customizing distributables + // TODO: Setup sensible defaults for the distributables + + // Add convenience tasks for running, presenting, and exporting the sketch outside of Processing + if(!isProcessing) { + project.tasks.create("sketch").apply { + group = "processing" + description = "Runs the Processing sketch" + dependsOn("run") + } + project.tasks.create("present").apply { + group = "processing" + description = "Presents the Processing sketch" + doFirst { + project.tasks.withType(JavaExec::class.java).configureEach { task -> + task.systemProperty("processing.fullscreen", "true") + } + } + finalizedBy("run") + } + project.tasks.create("export").apply { + group = "processing" + description = "Creates a distributable version of the Processing sketch" + + dependsOn("createDistributable") + + } + } + + project.afterEvaluate { + // Copy the result of create distributable to the project directory + project.tasks.named("createDistributable") { task -> + task.doLast { + project.copy { + it.from(project.tasks.named("createDistributable").get().outputs.files) + it.into(project.layout.projectDirectory) + } + } + } + } + + // Move the processing variables into javaexec tasks so they can be used in the sketch as well + project.tasks.withType(JavaExec::class.java).configureEach { task -> + project.properties + .filterKeys { it.startsWith("processing") } + .forEach { (key, value) -> task.systemProperty(key, value) } + + // Connect the stdio to the PDE if ports are specified + if(logPort != null) task.standardOutput = Socket("localhost", logPort.toInt()).outputStream + if(errPort != null) task.errorOutput = Socket("localhost", errPort.toInt()).outputStream + + } + + // For every Java Source Set (main, test, etc) add a PDE source set that includes .pde files + // and a task to process them before compilation + project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.first().let{ sourceSet -> + val pdeSourceSet = objectFactory.newInstance( + DefaultPDESourceDirectorySet::class.java, + objectFactory.sourceDirectorySet("${sourceSet.name}.pde", "${sourceSet.name} Processing Source") + ) + + // Configure the PDE source set to include all .pde files in the sketch folder except those in the build directory + pdeSourceSet.apply { + srcDir("./") + srcDir("$workingDir/unsaved") + + filter.include("**/*.pde") + filter.exclude("${project.layout.buildDirectory.asFile.get().name}/**") + } + sourceSet.allSource.source(pdeSourceSet) + + // Add top level java source files + sourceSet.java.srcDir(project.layout.projectDirectory).apply { + include("/*.java") + } + + // Scan the libraries before compiling the sketches + val librariesTaskName = sourceSet.getTaskName("scanLibraries", "PDE") + val librariesScan = project.tasks.register(librariesTaskName, LibrariesTask::class.java) { task -> + task.description = "Scans the libraries in the sketchbook" + task.libraryDirectories.from(sketchbook?.let { File(it, "libraries") }, root?.let { File(it).resolve("modes/java/libraries") }) + } + + // Create a task to process the .pde files before compiling the java sources + val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE") + val pdeTask = project.tasks.register(pdeTaskName, PDETask::class.java) { task -> + task.description = "Processes the ${sourceSet.name} PDE" + task.source = pdeSourceSet + task.sketchName = sketchName + + // Set the output of the pre-processor as the input for the java compiler + sourceSet.java.srcDir(task.outputDirectory) + } + + val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE") + project.tasks.register(depsTaskName, DependenciesTask::class.java){ task -> + // Link the output of the libraries task to the dependencies task + task.librariesMetaData.set(librariesScan.get().librariesMetaData) + task.dependsOn(pdeTask, librariesScan) + } + + // Make sure that the PDE tasks runs before the java compilation task + project.tasks.named(sourceSet.compileJavaTaskName) { task -> + task.dependsOn(pdeTaskName, depsTaskName) + } + } + } + abstract class DefaultPDESourceDirectorySet @Inject constructor( + sourceDirectorySet: SourceDirectorySet, + taskDependencyFactory: TaskDependencyFactory + ) : DefaultSourceDirectorySet(sourceDirectorySet, taskDependencyFactory), SourceDirectorySet +} + diff --git a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt new file mode 100644 index 000000000..7ffeeecb5 --- /dev/null +++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt @@ -0,0 +1,303 @@ +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.lang.management.ManagementFactory +import java.net.URLClassLoader + +class ProcessingPluginTest{ + // TODO: Test on multiple platforms since there are meaningful differences between the platforms + data class TemporaryProcessingSketchResult( + val buildResult: BuildResult, + val sketchFolder: File, + val classLoader: ClassLoader + ) + + fun createTemporaryProcessingSketch(vararg arguments: String, configure: (sketchFolder: File) -> Unit): TemporaryProcessingSketchResult{ + val directory = TemporaryFolder() + directory.create() + val sketchFolder = directory.newFolder("sketch") + directory.newFile("sketch/build.gradle.kts").writeText(""" + plugins { + id("org.processing.java") + } + """.trimIndent()) + directory.newFile("sketch/settings.gradle.kts") + configure(sketchFolder) + + val buildResult = GradleRunner.create() + .withProjectDir(sketchFolder) + .withArguments(*arguments) + .withPluginClasspath() + .withDebug(true) + .build() + + val classDir = sketchFolder.resolve("build/classes/java/main") + val classLoader = URLClassLoader(arrayOf(classDir.toURI().toURL()), this::class.java.classLoader) + + return TemporaryProcessingSketchResult( + buildResult, + sketchFolder, + classLoader + ) + } + + data class TemporaryProcessingLibraryResult( + val buildResult: BuildResult, + val libraryFolder: File + ) + + fun createTemporaryProcessingLibrary(name: String): TemporaryProcessingLibraryResult{ + val directory = TemporaryFolder() + directory.create() + val libraryFolder = directory.newFolder("libraries",name) + directory.newFile("libraries/$name/build.gradle.kts").writeText(""" + plugins { + java + } + tasks.jar{ + destinationDirectory.set(file("library")) + } + """.trimIndent()) + val srcDirectory = directory.newFolder("libraries", name,"src", "main", "java") + directory.newFile("libraries/$name/src/main/java/Example.java").writeText(""" + package testing.example; + + public class Example { + public void exampleMethod() { + System.out.println("Hello from Example library"); + } + } + """.trimIndent()) + directory.newFile("libraries/$name/settings.gradle.kts") + directory.newFile("libraries/$name/library.properties").writeText(""" + name=$name + author=Test Author + version=1.0.0 + sentence=An example library + paragraph=This is a longer description of the example library. + category=Examples + url=http://example.com + """.trimIndent()) + + if(isDebuggerAttached()){ + openFolderInFinder(libraryFolder) + } + + val buildResult = GradleRunner.create() + .withProjectDir(libraryFolder) + .withArguments("jar") + .withPluginClasspath() + .withDebug(true) + .build() + + + return TemporaryProcessingLibraryResult( + buildResult, + libraryFolder + ) + } + + @Test + fun testSinglePDE(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) { + "Method setup not found in class sketch" + } + + assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) { + "Method draw not found in class sketch" + } + } + + @Test + fun testMultiplePDE(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + otherFunction(); + } + """.trimIndent()) + sketchFolder.resolve("sketch2.pde").writeText(""" + void otherFunction(){ + println("Hi"); + } + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "otherFunction" } != null) { + "Method otherFunction not found in class sketch" + } + + } + + @Test + fun testJavaSourceFile(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("extra.java").writeText(""" + class SketchJava { + public void javaMethod() { + System.out.println("Hello from Java"); + } + } + """.trimIndent()) + } + val sketchJavaClass = classLoader.loadClass("SketchJava") + + assert(sketchJavaClass != null) { + "Class SketchJava not found" + } + + assert(sketchJavaClass?.methods?.find { method -> method.name == "javaMethod" } != null) { + "Method javaMethod not found in class SketchJava" + } + } + + @Test + fun testWithUnsavedSource(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("../unsaved").mkdirs() + sketchFolder.resolve("../unsaved/sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + + void newMethod(){ + println("This is an unsaved method"); + } + """.trimIndent()) + sketchFolder.resolve("gradle.properties").writeText(""") + processing.workingDir = ${sketchFolder.parentFile.absolutePath} + """.trimIndent()) + } + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "newMethod" } != null) { + "Method otherFunction not found in class sketch" + } + } + + @Test + fun testImportingLibrary(){ + val libraryResult = createTemporaryProcessingLibrary("ExampleLibrary") + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build") { sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + import testing.example.*; + + Example example; + + void setup(){ + size(100, 100); + example = new Example(); + example.exampleMethod(); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("gradle.properties").writeText(""") + processing.sketchbook = ${libraryResult.libraryFolder.parentFile.parentFile.absolutePath} + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) { + "Method setup not found in class sketch" + } + + assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) { + "Method draw not found in class sketch" + } + } + + @Test + fun testUseInternalLibraries(){ + + } + + @Test + fun testUseCodeJar(){ + // TODO: test if adding jars to the code folder works + } + + fun isDebuggerAttached(): Boolean { + val runtimeMxBean = ManagementFactory.getRuntimeMXBean() + val inputArguments = runtimeMxBean.inputArguments + return inputArguments.any { + it.contains("-agentlib:jdwp") + } + } + fun openFolderInFinder(folder: File) { + if (!folder.exists() || !folder.isDirectory) { + println("Invalid directory: ${folder.absolutePath}") + return + } + + val process = ProcessBuilder("open", folder.absolutePath) + .inheritIO() + .start() + process.waitFor() + } +} + + diff --git a/java/preprocessor/build.gradle.kts b/java/preprocessor/build.gradle.kts index 8e4300d31..e108b58a4 100644 --- a/java/preprocessor/build.gradle.kts +++ b/java/preprocessor/build.gradle.kts @@ -28,7 +28,6 @@ afterEvaluate{ } dependencies{ - implementation(project(":core")) implementation(project(":app:utils")) implementation(libs.antlr) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f8cb74c7..a2be58c69 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ include( "app", "java", "java:preprocessor", + "java:gradle", "java:libraries:dxf", "java:libraries:io", "java:libraries:net",