diff --git a/app/src/processing/app/gradle/GradleJob.kt b/app/src/processing/app/gradle/GradleJob.kt index 2904d4164..135f28de6 100644 --- a/app/src/processing/app/gradle/GradleJob.kt +++ b/app/src/processing/app/gradle/GradleJob.kt @@ -33,7 +33,9 @@ import java.nio.file.Path import kotlin.io.path.deleteIfExists import kotlin.io.path.writeText -// Starts a gradle job to run the sketch +/* +* The gradle job runs the gradle tasks and manages the gradle connection + */ class GradleJob( vararg val tasks: String, val workingDir: Path, @@ -231,16 +233,18 @@ class GradleJob( } } - fun launchJob(block: suspend CoroutineScope.() -> Unit){ - val job = scope.launch { block() } - jobs.add(job) - } + fun cancel(){ cancel.cancel() jobs.forEach(Job::cancel) } + private fun launchJob(block: suspend CoroutineScope.() -> Unit){ + val job = scope.launch { block() } + jobs.add(job) + } + // Handle exception thrown by Gradle private fun handleExceptions(action: () -> Unit){ try{ @@ -278,6 +282,7 @@ class GradleJob( } } + // TODO: Move to separate file private fun BuildLauncher.addStateListener(){ addProgressListener(ProgressListener { event -> if(event is TaskStartEvent) { diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts index f48dea8ef..6c446b13c 100644 --- a/java/gradle/build.gradle.kts +++ b/java/gradle/build.gradle.kts @@ -7,6 +7,7 @@ plugins{ repositories { mavenCentral() + maven("https://jogamp.org/deployment/maven") } dependencies{ @@ -16,9 +17,11 @@ dependencies{ 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"){ diff --git a/java/gradle/src/main/kotlin/LibrariesTask.kt b/java/gradle/src/main/kotlin/LibrariesTask.kt index 57f09bfdd..472d9e3b7 100644 --- a/java/gradle/src/main/kotlin/LibrariesTask.kt +++ b/java/gradle/src/main/kotlin/LibrariesTask.kt @@ -17,6 +17,7 @@ This task stores the resulting information in a file that can be used later to r */ abstract class LibrariesTask : DefaultTask() { + // TODO: Allow multiple directories @InputDirectory @Optional val librariesDirectory: DirectoryProperty = project.objects.directoryProperty() @@ -41,6 +42,9 @@ abstract class LibrariesTask : DefaultTask() { fun execute() { if (!librariesDirectory.isPresent) { logger.error("Libraries directory is not set. Libraries will not be imported.") + val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream()) + meta.writeObject(arrayListOf()) + meta.close() return } val libraries = librariesDirectory.get().asFile diff --git a/java/gradle/src/main/kotlin/PDETask.kt b/java/gradle/src/main/kotlin/PDETask.kt index 57fa31f3e..ccde2bec8 100644 --- a/java/gradle/src/main/kotlin/PDETask.kt +++ b/java/gradle/src/main/kotlin/PDETask.kt @@ -7,6 +7,7 @@ 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 @@ -14,7 +15,7 @@ import javax.inject.Inject // TODO: Generate sourcemaps /* -* The PDETask is the main task that processes the .pde files and generates the Java source code +* 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 @@ -24,21 +25,13 @@ abstract class PDETask : SourceTask() { open val stableSources: FileCollection = project.files(Callable { this.source }) @OutputDirectory - val outputDirectory = project.objects.directoryProperty() - - @get:Input - @get:Optional - var workingDir: String? = null + val outputDirectory: DirectoryProperty = project.objects.directoryProperty() @get:Input var sketchName: String = "processing" - @get:Input - @get:Optional - var sketchBook: String? = null - @OutputFile - val sketchMetaData = project.objects.fileProperty() + val sketchMetaData: RegularFileProperty = project.objects.fileProperty() init{ outputDirectory.convention(project.layout.buildDirectory.dir("generated/pde")) @@ -49,18 +42,16 @@ abstract class PDETask : SourceTask() { val sketchName: String, val sketchRenderer: String?, val importStatements: List - ) : java.io.Serializable + ) : Serializable @TaskAction fun execute() { - // TODO: Allow pre-processor to run on individual files (future) - // TODO: Only compare file names from both defined roots (e.g. sketch.pde and folder/sketch.pde should both be included) - // 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 -> + // TODO: Select by which one is in the unsaved folder entry.value.maxByOrNull { it.lastModified() }!! } .joinToString("\n"){ @@ -74,6 +65,8 @@ abstract class PDETask : SourceTask() { .build() .write(javaFile, combined) + // TODO: Save the edits to meta files + javaFile.flush() javaFile.close() @@ -87,10 +80,4 @@ abstract class PDETask : SourceTask() { metaFile.writeObject(sketchMeta) metaFile.close() } - - @get:Inject - open val deleter: Deleter - get() { - throw UnsupportedOperationException("Decorator takes care of injection") - } } \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt index 2864a88e0..0af297e95 100644 --- a/java/gradle/src/main/kotlin/ProcessingPlugin.kt +++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt @@ -17,7 +17,6 @@ import java.net.Socket import java.util.* import javax.inject.Inject -// TODO: CI/CD for publishing the plugin 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_]"), "_") @@ -32,6 +31,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact // 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? // Apply the Java plugin to the Project project.plugins.apply(JavaPlugin::class.java) @@ -46,13 +46,12 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact project.tasks.findByName("wrapper")?.enabled = false } - // Add the compose plugin to wrap the sketch in an executable - project.plugins.apply("org.jetbrains.compose") - // Add kotlin support 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) project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}") @@ -129,8 +128,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact } - project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.all { sourceSet -> - // For each java source set (mostly main) add a new source set for the PDE files + 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") @@ -142,11 +140,16 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact srcDir("$workingDir/unsaved") } sourceSet.allSource.source(pdeSourceSet) + sourceSet.java.srcDir(project.layout.projectDirectory).apply { + include("**/*.java") + exclude("${project.layout.buildDirectory.asFile.get()}/**/*") + } 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.librariesDirectory.set(sketchbook?.let { File(it, "libraries") }) + // TODO: Save the libraries metadata to settings folder to share between sketches } val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE") @@ -154,31 +157,20 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact task.description = "Processes the ${sourceSet.name} PDE" task.source = pdeSourceSet task.sketchName = sketchName - task.workingDir = workingDir - task.sketchBook = sketchbook // Set the output of the pre-processor as the input for the java compiler sourceSet.java.srcDir(task.outputDirectory) - - task.doLast { - // Copy java files from the root to the generated directory - project.copy { copyTask -> - copyTask.from(project.layout.projectDirectory){ from -> - from.include("*.java") - } - copyTask.into(task.outputDirectory) - } - } } val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE") project.tasks.register(depsTaskName, DependenciesTask::class.java){ task -> + task.librariesMetaData task.dependsOn(pdeTask, librariesScan) + // TODO: Save the libraries metadata to settings folder to share between sketches } - project.tasks.named( - sourceSet.compileJavaTaskName - ) { task -> + // Make sure that the PDE task runs before the java compilation task + project.tasks.named(sourceSet.compileJavaTaskName) { task -> task.dependsOn(pdeTaskName, depsTaskName) } } diff --git a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt index 7e43a2c39..a58e16b60 100644 --- a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt +++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt @@ -1,18 +1,198 @@ -import org.gradle.api.Task -import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner -import org.junit.Rule 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: Write tests - // TODO: Test on multiple platforms since there is meaningful differences between the platforms - @Test - fun testPluginAddsSketchTask(){ - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("org.processing.java") + // 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 + ) - assert(project.tasks.getByName("sketch") is Task) + 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 + ) + } + + @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" + } + } + +} + + +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() +} \ No newline at end of file