diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/ScriptCompiler.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/Compiler.kt similarity index 89% rename from editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/ScriptCompiler.kt rename to editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/Compiler.kt index 06716dcc..dee7c5ae 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/ScriptCompiler.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/Compiler.kt @@ -3,6 +3,6 @@ package com.bartlomiejpluta.base.editor.code.build.compiler import com.bartlomiejpluta.base.editor.code.model.FileSystemNode import java.io.File -interface ScriptCompiler { +interface Compiler { fun compile(sourceDirectory: FileSystemNode, targetDirectory: File) } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/JaninoCompiler.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/JaninoCompiler.kt index dd0ed333..60a4ed1f 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/JaninoCompiler.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/compiler/JaninoCompiler.kt @@ -1,28 +1,62 @@ package com.bartlomiejpluta.base.editor.code.build.compiler +import com.bartlomiejpluta.base.editor.code.build.exception.BuildException import com.bartlomiejpluta.base.editor.code.build.model.ClasspathResource import com.bartlomiejpluta.base.editor.code.model.FileSystemNode -import com.bartlomiejpluta.base.editor.event.UpdateCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent.Severity.ERROR +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent.Severity.WARNING import org.codehaus.commons.compiler.CompileException import org.codehaus.commons.compiler.util.resource.FileResource import org.codehaus.commons.compiler.util.resource.Resource import org.codehaus.janino.CompilerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import tornadofx.FX +import tornadofx.FX.Companion.eventbus import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import java.util.stream.Collectors.toList @Component -class JaninoCompiler : ScriptCompiler { +class JaninoCompiler : Compiler { private val compilerFactory = CompilerFactory() @Value("classpath:api/**/*.java") private lateinit var apiFiles: Array - override fun compile(sourceDirectory: FileSystemNode, targetDirectory: File) { + override fun compile(sourceDirectory: FileSystemNode, targetDirectory: File) = try { + tryToCompile(sourceDirectory, targetDirectory) + + /* Because Janino parser does not provide error handler for parsers: + * + * > Notice that there is no Parser.setErrorHandler() method, but parse errors always throw a + * > CompileException. The reason being is that there is no reasonable way to recover from parse errors and + * > continue parsing, so there is no need to install a custom parse error handler. + * > (org.codehaus.janino.Parser.java) + * + * the try-catch statement is required here to catch all parsing exceptions and re-throw them + * as BuildException + */ + } catch (e: CompileException) { + val locationIndex = e.location?.toString()?.length?.plus(2) ?: 0 + val message = e.message?.substring(locationIndex) + + throw BuildException(ERROR, "Compiler", e.location, message, e) + } + + private fun tryToCompile(sourceDirectory: FileSystemNode, targetDirectory: File) { + val compilationUnits = prepareCompilationUnits(sourceDirectory) + + compilerFactory.newCompiler().apply { + setDestinationDirectory(targetDirectory, false) + + setWarningHandler { handle, message, location -> + eventbus.fire(AppendCompilationLogEvent(WARNING, "$message ($handle)", location, "Compiler")) + } + + compile(compilationUnits) + } + } + + private fun prepareCompilationUnits(sourceDirectory: FileSystemNode): Array { val sources = sourceDirectory .allChildren .map(FileSystemNode::file) @@ -34,40 +68,6 @@ class JaninoCompiler : ScriptCompiler { .map(::ClasspathResource) .toTypedArray() - val resources = sources + api - - val compiler = compilerFactory.newCompiler() - - - // FIXME: - // For some reason the compiler's error handler does not want to catch - // syntax errors. The only way to catch it is just catching CompileExceptions - try { - compiler.compile(resources) - FX.eventbus.fire(UpdateCompilationLogEvent(UpdateCompilationLogEvent.Severity.INFO, "Compilation success")) - moveClassFilesToTargetDirectory(sourceDirectory.file, targetDirectory) - } catch (e: CompileException) { - - // Because the Janino compiler assemblies the message with the location - // in the LocatedException.getMessage() method, we just need to remove it - // to have a plain message along with the plain location as separated objects - val locationIndex = e.location?.toString()?.length ?: 0 - val message = e.message?.substring(locationIndex) ?: "" - FX.eventbus.fire(UpdateCompilationLogEvent(UpdateCompilationLogEvent.Severity.ERROR, message, e.location)) - } - } - - private fun moveClassFilesToTargetDirectory(sourceDirectory: File, targetDirectory: File) { - val files = Files.walk(sourceDirectory.toPath()) - .filter(Files::isRegularFile) - .map(Path::toFile) - .collect(toList()) - - files.filter { it.extension == "class" }.forEach { - val relative = it.toRelativeString(sourceDirectory) - val targetFile = File(targetDirectory, relative) - it.copyTo(targetFile, overwrite = true) - it.delete() - } + return sources + api } } diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/exception/BuildException.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/exception/BuildException.kt new file mode 100644 index 00000000..8933017d --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/exception/BuildException.kt @@ -0,0 +1,17 @@ +package com.bartlomiejpluta.base.editor.code.build.exception + +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent +import org.codehaus.commons.compiler.Location + +class BuildException( + val severity: AppendCompilationLogEvent.Severity, + val tag: String, + val location: Location?, + message: String?, + override val cause: Throwable +) : Exception() { + constructor(severity: AppendCompilationLogEvent.Severity, tag: String, message: String?, cause: Throwable) + : this(severity, tag, null, message, cause) + + override val message = message ?: "" +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/DefaultGameEngineProvider.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/DefaultGameEngineProvider.kt index 85675c82..5bb551c6 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/DefaultGameEngineProvider.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/DefaultGameEngineProvider.kt @@ -1,16 +1,39 @@ package com.bartlomiejpluta.base.editor.code.build.game +import com.bartlomiejpluta.base.editor.code.build.exception.BuildException +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent.Severity.ERROR import org.springframework.stereotype.Component import java.io.File +/* TODO + * There is an idea to have a different GameEngine providers for different OS (Windows, Mac OS X, Linux etc.) + * Essentially as far as whole system is built on Java, which is platform-independent, the only thing that + * actually is platform dependent is the game engine which is built on top of the LWJGL, which is in turn built + * on the top of the OpenGL driver. So potentially this is the only place that would be duplicated in order + * to provide a true multi-platform support. Of course the :editor application would have to have a proper + * engines provided to the resources then, however this is a job for the Gradle build system. + */ + @Component class DefaultGameEngineProvider : GameEngineProvider { override fun provideBaseGameEngine(targetJar: File) { - javaClass.getResourceAsStream("/engine/game.jar").use { ris -> + try { + tryToProvide(targetJar) + } catch (e: Exception) { + throw BuildException(ERROR, "Engine Provider", e.message, e) + } + } + + private fun tryToProvide(targetJar: File) { + javaClass.getResourceAsStream(GAME_ENGINE_JAR).use { ris -> targetJar.outputStream().use { fos -> ris.copyTo(fos) } } } + + companion object { + private const val GAME_ENGINE_JAR = "/engine/game.jar" + } } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/GameEngineProvider.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/GameEngineProvider.kt index 4bfdc848..07a74e43 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/GameEngineProvider.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/game/GameEngineProvider.kt @@ -3,5 +3,5 @@ package com.bartlomiejpluta.base.editor.code.build.game import java.io.File interface GameEngineProvider { - fun provideBaseGameEngine(target: File) + fun provideBaseGameEngine(targetJar: File) } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/BuildPipelineService.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/BuildPipelineService.kt index 8d9fe214..e3fcd97d 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/BuildPipelineService.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/BuildPipelineService.kt @@ -1,5 +1,7 @@ package com.bartlomiejpluta.base.editor.code.build.pipeline +import javafx.concurrent.Task + interface BuildPipelineService { - fun build() + fun build(): Task } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/DefaultBuildPipelineService.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/DefaultBuildPipelineService.kt index 5d2b4def..6d5fa7d0 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/DefaultBuildPipelineService.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/pipeline/DefaultBuildPipelineService.kt @@ -1,20 +1,26 @@ package com.bartlomiejpluta.base.editor.code.build.pipeline -import com.bartlomiejpluta.base.editor.code.build.compiler.ScriptCompiler +import com.bartlomiejpluta.base.editor.code.build.compiler.Compiler +import com.bartlomiejpluta.base.editor.code.build.exception.BuildException import com.bartlomiejpluta.base.editor.code.build.game.GameEngineProvider import com.bartlomiejpluta.base.editor.code.build.packager.JarPackager import com.bartlomiejpluta.base.editor.code.build.project.ProjectAssembler +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent.Severity.INFO +import com.bartlomiejpluta.base.editor.event.ClearCompilationLogEvent import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.model.Project import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component +import tornadofx.FX.Companion.eventbus +import tornadofx.runAsync import java.io.File @Component class DefaultBuildPipelineService : BuildPipelineService { @Autowired - private lateinit var compiler: ScriptCompiler + private lateinit var compiler: Compiler @Autowired private lateinit var packager: JarPackager @@ -28,17 +34,40 @@ class DefaultBuildPipelineService : BuildPipelineService { @Autowired private lateinit var projectContext: ProjectContext - override fun build() { - projectContext.project?.let { - prepareBuildDirectory(it) - - val outputFile = File(it.buildOutDirectory, OUTPUT_JAR_NAME) - - compiler.compile(it.codeFSNode, it.buildClassesDirectory) - engineProvider.provideBaseGameEngine(outputFile) - packager.pack(it.buildClassesDirectory, outputFile, "BOOT-INF/classes") - projectAssembler.assembly(it, outputFile) + // FIXME + // There is runAsync used right here, however it does not prevent from + // multiple pipeline running. There should be some kind of assertion + // that enforces the pipeline to run only once in the same time. + override fun build() = runAsync { + try { + projectContext.project?.let(this@DefaultBuildPipelineService::runPipeline) + } catch (e: BuildException) { + val event = AppendCompilationLogEvent(e.severity, e.message, e.location, e.tag) + eventbus.fire(event) } + + Unit + } + + private fun runPipeline(project: Project) { + eventbus.fire(ClearCompilationLogEvent) + prepareBuildDirectory(project) + + val outputFile = File(project.buildOutDirectory, OUTPUT_JAR_NAME) + val startTime = System.currentTimeMillis() + + eventbus.fire(AppendCompilationLogEvent(INFO, "Compiling sources...", tag = TAG)) + compiler.compile(project.codeFSNode, project.buildClassesDirectory) + + eventbus.fire(AppendCompilationLogEvent(INFO, "Assembling game engine...", tag = TAG)) + engineProvider.provideBaseGameEngine(outputFile) + + eventbus.fire(AppendCompilationLogEvent(INFO, "Assembling project assets...", tag = TAG)) + packager.pack(project.buildClassesDirectory, outputFile, "BOOT-INF/classes") + projectAssembler.assembly(project, outputFile) + + val buildingTime = (System.currentTimeMillis() - startTime) / 1000.0 + eventbus.fire(AppendCompilationLogEvent(INFO, "Done [${buildingTime}s]", tag = TAG)) } private fun prepareBuildDirectory(project: Project) { @@ -50,5 +79,6 @@ class DefaultBuildPipelineService : BuildPipelineService { companion object { private const val OUTPUT_JAR_NAME = "game.jar" + private const val TAG = "Build" } } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/project/DefaultProjectAssembler.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/project/DefaultProjectAssembler.kt index 1f722c07..d6bfbbdb 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/project/DefaultProjectAssembler.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/project/DefaultProjectAssembler.kt @@ -1,6 +1,8 @@ package com.bartlomiejpluta.base.editor.code.build.project +import com.bartlomiejpluta.base.editor.code.build.exception.BuildException import com.bartlomiejpluta.base.editor.code.build.packager.JarPackager +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent import com.bartlomiejpluta.base.editor.project.model.Project import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @@ -13,6 +15,14 @@ class DefaultProjectAssembler : ProjectAssembler { private lateinit var packager: JarPackager override fun assembly(project: Project, targetJar: File) { + try { + tryToAssembly(project, targetJar) + } catch (e: Exception) { + throw BuildException(AppendCompilationLogEvent.Severity.ERROR, "Project Assembler", e.message, e) + } + } + + private fun tryToAssembly(project: Project, targetJar: File) { packager.pack(project.mapsDirectory, targetJar, "BOOT-INF/classes/project/maps") packager.pack(project.tileSetsDirectory, targetJar, "BOOT-INF/classes/project/tilesets") packager.pack(project.imagesDirectory, targetJar, "BOOT-INF/classes/project/images") diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CompilationLogs.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CompilationLogs.kt index c976d0bc..d1e7398a 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CompilationLogs.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/component/CompilationLogs.kt @@ -1,7 +1,7 @@ package com.bartlomiejpluta.base.editor.code.component import com.bartlomiejpluta.base.editor.code.stylesheet.CompilerLogsStylesheet -import com.bartlomiejpluta.base.editor.event.UpdateCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent import javafx.scene.Cursor import javafx.scene.layout.StackPane import javafx.scene.paint.Color @@ -21,13 +21,14 @@ class CompilationLogs(private val locationClick: (location: Location) -> Unit) : children += editor } - fun setEntry(message: String, severity: UpdateCompilationLogEvent.Severity, location: Location?) { - editor.clear() - + fun appendEntry(message: String, severity: AppendCompilationLogEvent.Severity, location: Location?, tag: String?) { val locationRef = CompilationLogStyle(location = location, onClick = locationClick) + val severityStyle = CompilationLogStyle(severity = severity) + tag?.let { editor.insert(editor.length, "[$it] ", severityStyle) } editor.insert(editor.length, location?.toString() ?: "", locationRef) - editor.insert(editor.length, message, CompilationLogStyle(severity = severity)) + editor.insert(editor.length, (location?.let { ": " } ?: "") + message, CompilationLogStyle(severity = severity)) + editor.insert(editor.length, "\n", CompilationLogStyle.NO_STYLE) } fun clear() = editor.clear() @@ -36,7 +37,7 @@ class CompilationLogs(private val locationClick: (location: Location) -> Unit) : class CompilationLogStyle( private val location: Location? = null, - private val severity: UpdateCompilationLogEvent.Severity? = null, + private val severity: AppendCompilationLogEvent.Severity? = null, private val onClick: (Location) -> Unit = {} ) { @@ -47,11 +48,11 @@ class CompilationLogs(private val locationClick: (location: Location) -> Unit) : } } - private fun message(text: Text, severity: UpdateCompilationLogEvent.Severity) { + private fun message(text: Text, severity: AppendCompilationLogEvent.Severity) { text.fill = when (severity) { - UpdateCompilationLogEvent.Severity.WARNING -> Color.ORANGE - UpdateCompilationLogEvent.Severity.ERROR -> Color.RED - UpdateCompilationLogEvent.Severity.INFO -> text.fill + AppendCompilationLogEvent.Severity.WARNING -> Color.ORANGE + AppendCompilationLogEvent.Severity.ERROR -> Color.RED + AppendCompilationLogEvent.Severity.INFO -> text.fill } } diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CompilerLogsView.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CompilerLogsView.kt index 877ce645..6b0ee9ee 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CompilerLogsView.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/view/CompilerLogsView.kt @@ -1,7 +1,8 @@ package com.bartlomiejpluta.base.editor.code.view import com.bartlomiejpluta.base.editor.code.component.CompilationLogs -import com.bartlomiejpluta.base.editor.event.UpdateCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent +import com.bartlomiejpluta.base.editor.event.ClearCompilationLogEvent import com.bartlomiejpluta.base.editor.main.controller.MainController import com.bartlomiejpluta.base.editor.project.context.ProjectContext import org.codehaus.commons.compiler.Location @@ -17,8 +18,12 @@ class CompilerLogsView : View() { private val compilationLogs = CompilationLogs(this::locationClick) init { - subscribe { event -> - compilationLogs.setEntry(event.message, event.severity, event.location) + subscribe { event -> + compilationLogs.appendEntry(event.message, event.severity, event.location, event.tag) + } + + subscribe { + compilationLogs.clear() } } diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/UpdateCompilationLogEvent.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/AppendCompilationLogEvent.kt similarity index 62% rename from editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/UpdateCompilationLogEvent.kt rename to editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/AppendCompilationLogEvent.kt index 30ff026a..58a977c9 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/UpdateCompilationLogEvent.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/AppendCompilationLogEvent.kt @@ -4,7 +4,12 @@ import org.codehaus.commons.compiler.Location import tornadofx.EventBus import tornadofx.FXEvent -data class UpdateCompilationLogEvent(val severity: Severity, val message: String, val location: Location? = null) : +data class AppendCompilationLogEvent( + val severity: Severity, + val message: String, + val location: Location? = null, + val tag: String? = null +) : FXEvent(EventBus.RunOn.ApplicationThread) { enum class Severity { diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/ClearCompilationLogEvent.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/ClearCompilationLogEvent.kt new file mode 100644 index 00000000..9d711e2c --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/event/ClearCompilationLogEvent.kt @@ -0,0 +1,6 @@ +package com.bartlomiejpluta.base.editor.event + +import tornadofx.EventBus +import tornadofx.FXEvent + +object ClearCompilationLogEvent : FXEvent(EventBus.RunOn.ApplicationThread) \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/main/view/MainView.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/main/view/MainView.kt index b8f653a1..09ed87b5 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/main/view/MainView.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/main/view/MainView.kt @@ -6,8 +6,8 @@ import com.bartlomiejpluta.base.editor.code.view.CodeEditorFragment import com.bartlomiejpluta.base.editor.code.view.CompilerLogsView import com.bartlomiejpluta.base.editor.code.view.ScriptFilesView import com.bartlomiejpluta.base.editor.code.viewmodel.CodeVM +import com.bartlomiejpluta.base.editor.event.AppendCompilationLogEvent import com.bartlomiejpluta.base.editor.event.SelectMainViewTabEvent -import com.bartlomiejpluta.base.editor.event.UpdateCompilationLogEvent import com.bartlomiejpluta.base.editor.main.controller.MainController import com.bartlomiejpluta.base.editor.map.model.map.GameMap import com.bartlomiejpluta.base.editor.map.view.editor.MapFragment @@ -78,7 +78,7 @@ class MainView : View("BASE Game Editor") { title = "BASE Game Editor$projectName" } - subscribe { + subscribe { compilationLogItem.expanded = true }