Create scaffolding of editor's CLI interface

This commit is contained in:
2025-07-11 00:19:18 +02:00
parent 3faf677925
commit c048d4eb0c
14 changed files with 249 additions and 123 deletions

View File

@@ -37,6 +37,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "org.jetbrains.kotlin:kotlin-stdlib"
// CLI arg parser
implementation "com.xenomachina:kotlin-argparser:${kotlinArgParserVersion}"
// JSON Proto // JSON Proto
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"

View File

@@ -1,8 +1,14 @@
package com.bartlomiejpluta.base.editor package com.bartlomiejpluta.base.editor
import com.bartlomiejpluta.base.editor.cli.model.CLIArgs
import com.bartlomiejpluta.base.editor.code.build.exception.BuildException
import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildPipelineService
import com.bartlomiejpluta.base.editor.main.controller.MainController import com.bartlomiejpluta.base.editor.main.controller.MainController
import com.bartlomiejpluta.base.editor.main.view.MainView import com.bartlomiejpluta.base.editor.main.view.MainView
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.xenomachina.argparser.ArgParser
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.ConfigurableApplicationContext import org.springframework.context.ConfigurableApplicationContext
@@ -10,32 +16,69 @@ import tornadofx.App
import tornadofx.DIContainer import tornadofx.DIContainer
import tornadofx.FX import tornadofx.FX
import tornadofx.launch import tornadofx.launch
import java.io.File
import kotlin.reflect.KClass import kotlin.reflect.KClass
@SpringBootApplication @SpringBootApplication
open class EditorApp : App(MainView::class) { open class EditorApp : App(MainView::class) {
private lateinit var context: ConfigurableApplicationContext private lateinit var context: ConfigurableApplicationContext
@Autowired @Autowired
private lateinit var mainController: MainController private lateinit var mainController: MainController
override fun init() { override fun init() {
this.context = SpringApplication.run(this.javaClass) this.context = SpringApplication.run(this.javaClass)
context.autowireCapableBeanFactory.autowireBean(this) context.autowireCapableBeanFactory.autowireBean(this)
FX.dicontainer = object : DIContainer { FX.dicontainer = object : DIContainer {
override fun <T : Any> getInstance(type: KClass<T>): T = context.getBean(type.java) override fun <T : Any> getInstance(type: KClass<T>): T = context.getBean(type.java)
override fun <T : Any> getInstance(type: KClass<T>, name: String): T = context.getBean(name, type.java) override fun <T : Any> getInstance(type: KClass<T>, name: String): T = context.getBean(name, type.java)
} }
} }
override fun stop() { override fun stop() {
super.stop() super.stop()
context.close() context.close()
mainController.clearResources() mainController.clearResources()
} }
}
@SpringBootApplication
open class CLIApp(
@Autowired private val context: ProjectContext,
@Autowired private val pipelineService: BuildPipelineService
) : CommandLineRunner {
override fun run(vararg args: String) {
ArgParser(args).parseInto(::CLIArgs).run {
project?.let { context.open(it) }
build.takeIf { it }.let { dir ->
pipelineService.initStreams(System.out, System.err)
val startTime = System.currentTimeMillis()
try {
context.project?.let { project ->
output?.let { project.changeBuildDirectory(File(it)) }
pipelineService.runPipeline(project)
}
println("Build completed")
} catch (e: BuildException) {
System.err.println("Build failed")
} finally {
val buildingTime = (System.currentTimeMillis() - startTime) / 1000.0
println("Finished in [${buildingTime}s]")
}
}
}
}
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
launch<EditorApp>(args) ArgParser(args).parseInto(::CLIArgs).run {
if (headless) {
SpringApplication.run(CLIApp::class.java, *args)
} else {
launch<EditorApp>(args)
}
}
} }

View File

@@ -0,0 +1,12 @@
package com.bartlomiejpluta.base.editor.cli.model
import com.xenomachina.argparser.ArgParser
import com.xenomachina.argparser.default
import java.io.File
class CLIArgs(parser: ArgParser) {
val headless by parser.flagging("-H", "--headless", help = "run editor in headless mode (without GUI)").default(false)
val project by parser.storing("-p", "--project", help = "project file") { File(this) }.default(null)
val build by parser.flagging("-b", "--build", help = "build project").default(false)
val output by parser.storing("-o", "--output", help = "define build output folder").default(null)
}

View File

@@ -1,3 +1,6 @@
package com.bartlomiejpluta.base.editor.code.build.exception package com.bartlomiejpluta.base.editor.code.build.exception
class BuildException : Exception() class BuildException : Exception {
constructor() : super()
constructor(cause: Throwable) : super(cause)
}

View File

@@ -1,12 +1,110 @@
package com.bartlomiejpluta.base.editor.code.build.pipeline package com.bartlomiejpluta.base.editor.code.build.pipeline
import javafx.beans.property.BooleanProperty import com.bartlomiejpluta.base.editor.code.build.asset.AssetSerializer
import javafx.concurrent.Task import com.bartlomiejpluta.base.editor.code.build.compiler.Compiler
import com.bartlomiejpluta.base.editor.code.build.database.DatabaseAssembler
import com.bartlomiejpluta.base.editor.code.build.game.GameEngineProvider
import com.bartlomiejpluta.base.editor.code.build.generator.CodeGenerator
import com.bartlomiejpluta.base.editor.code.build.packager.JarPackager
import com.bartlomiejpluta.base.editor.code.build.project.ProjectAssembler
import com.bartlomiejpluta.base.editor.code.dependency.DependenciesProvider
import com.bartlomiejpluta.base.editor.file.model.FileSystemNode
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 java.io.OutputStream import java.io.OutputStream
import java.io.PrintStream
interface BuildPipelineService { @Component
fun initStreams(stdout: OutputStream, stderr: OutputStream) open class BuildPipelineService {
val isRunningProperty: BooleanProperty
fun build(): Task<Boolean> @Autowired
fun clean() private lateinit var dependenciesProvider: DependenciesProvider
@Autowired
private lateinit var generators: List<CodeGenerator>
@Autowired
private lateinit var compiler: Compiler
@Autowired
private lateinit var packager: JarPackager
@Autowired
private lateinit var engineProvider: GameEngineProvider
@Autowired
private lateinit var assetSerializer: AssetSerializer
@Autowired
private lateinit var projectAssembler: ProjectAssembler
@Autowired
private lateinit var databaseAssembler: DatabaseAssembler
@Autowired
private lateinit var projectContext: ProjectContext
private lateinit var stdout: OutputStream
private lateinit var stderr: OutputStream
private var out: PrintStream = System.out
private var err: PrintStream = System.err
fun initStreams(stdout: OutputStream, stderr: OutputStream) {
this.stdout = stdout
this.stderr = stderr
this.out = PrintStream(stdout)
this.err = PrintStream(stderr)
}
fun runPipeline(project: Project) {
prepareBuildDirectory(project)
val outputFile = project.buildOutputJarFile
out.println("Providing compile-time dependencies...")
val dependencies = dependenciesProvider.provideDependenciesTo(project.buildDependenciesDirectory)
out.println("Generating sources...")
generators.forEach(CodeGenerator::generate)
out.println("Compiling sources...")
compiler.compile(
arrayOf(project.codeFSNode, FileSystemNode(project.buildGeneratedCodeDirectory)),
project.buildClassesDirectory,
dependencies.toTypedArray(),
out,
err
)
out.println("Assembling game engine...")
engineProvider.provideBaseGameEngine(outputFile, out, err)
out.println("Linking compilation units...")
packager.pack(project.buildClassesDirectory, outputFile, "BOOT-INF/classes")
out.println("Serializing project assets...")
assetSerializer.serializeAssets(project)
out.println("Assembling project assets...")
projectAssembler.assembly(project, outputFile, out, err)
out.println("Assembling database...")
databaseAssembler.assembly(project, outputFile, out, err)
}
private fun prepareBuildDirectory(project: Project) {
project.buildDirectory.deleteRecursively()
project.buildClassesDirectory.mkdirs()
project.buildOutDirectory.mkdirs()
project.buildDependenciesDirectory.mkdirs()
project.buildGeneratedCodeDirectory.mkdirs()
}
fun clean() {
projectContext.project?.apply { buildDirectory.deleteRecursively() }
out.println("Cleaning done")
}
} }

View File

@@ -0,0 +1,12 @@
package com.bartlomiejpluta.base.editor.code.build.pipeline
import javafx.beans.property.BooleanProperty
import javafx.concurrent.Task
import java.io.OutputStream
interface BuildService {
fun initStreams(stdout: OutputStream, stderr: OutputStream)
val isRunningProperty: BooleanProperty
fun build(): Task<Boolean>
fun clean()
}

View File

@@ -22,32 +22,10 @@ import java.io.OutputStream
import java.io.PrintStream import java.io.PrintStream
@Component @Component
class DefaultBuildPipelineService : BuildPipelineService { class DefaultBuildService : BuildService {
@Autowired @Autowired
private lateinit var dependenciesProvider: DependenciesProvider private lateinit var pipelineService: BuildPipelineService
@Autowired
private lateinit var generators: List<CodeGenerator>
@Autowired
private lateinit var compiler: Compiler
@Autowired
private lateinit var packager: JarPackager
@Autowired
private lateinit var engineProvider: GameEngineProvider
@Autowired
private lateinit var assetSerializer: AssetSerializer
@Autowired
private lateinit var projectAssembler: ProjectAssembler
@Autowired
private lateinit var databaseAssembler: DatabaseAssembler
@Autowired @Autowired
private lateinit var projectContext: ProjectContext private lateinit var projectContext: ProjectContext
@@ -56,14 +34,11 @@ class DefaultBuildPipelineService : BuildPipelineService {
private lateinit var stdout: OutputStream private lateinit var stdout: OutputStream
private lateinit var stderr: OutputStream private lateinit var stderr: OutputStream
private lateinit var out: PrintStream private var out: PrintStream = System.out
private lateinit var err: PrintStream private var err: PrintStream = System.err
override fun initStreams(stdout: OutputStream, stderr: OutputStream) { override fun initStreams(stdout: OutputStream, stderr: OutputStream) {
this.stdout = stdout pipelineService.initStreams(stdout, stderr)
this.stderr = stderr
this.out = PrintStream(stdout)
this.err = PrintStream(stderr)
} }
override val isRunningProperty = false.toProperty() override val isRunningProperty = false.toProperty()
@@ -89,7 +64,8 @@ class DefaultBuildPipelineService : BuildPipelineService {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
try { try {
projectContext.project?.let(this@DefaultBuildPipelineService::runPipeline) eventbus.fire(ClearBuildLogsEvent)
projectContext.project?.let(pipelineService::runPipeline)
out.println("Build completed") out.println("Build completed")
return@runAsync true return@runAsync true
} catch (e: BuildException) { } catch (e: BuildException) {
@@ -103,52 +79,6 @@ class DefaultBuildPipelineService : BuildPipelineService {
false false
} }
private fun runPipeline(project: Project) {
eventbus.fire(ClearBuildLogsEvent)
prepareBuildDirectory(project)
val outputFile = project.buildOutputJarFile
out.println("Providing compile-time dependencies...")
val dependencies = dependenciesProvider.provideDependenciesTo(project.buildDependenciesDirectory)
out.println("Generating sources...")
generators.forEach(CodeGenerator::generate)
out.println("Compiling sources...")
compiler.compile(
arrayOf(project.codeFSNode, FileSystemNode(project.buildGeneratedCodeDirectory)),
project.buildClassesDirectory,
dependencies.toTypedArray(),
out,
err
)
out.println("Assembling game engine...")
engineProvider.provideBaseGameEngine(outputFile, out, err)
out.println("Linking compilation units...")
packager.pack(project.buildClassesDirectory, outputFile, "BOOT-INF/classes")
out.println("Serializing project assets...")
assetSerializer.serializeAssets(project)
out.println("Assembling project assets...")
projectAssembler.assembly(project, outputFile, out, err)
out.println("Assembling database...")
databaseAssembler.assembly(project, outputFile, out, err)
}
private fun prepareBuildDirectory(project: Project) {
project.buildDirectory.deleteRecursively()
project.buildClassesDirectory.mkdirs()
project.buildOutDirectory.mkdirs()
project.buildDependenciesDirectory.mkdirs()
project.buildGeneratedCodeDirectory.mkdirs()
}
override fun clean() { override fun clean() {
projectContext.project?.apply { buildDirectory.deleteRecursively() } projectContext.project?.apply { buildDirectory.deleteRecursively() }
out.println("Cleaning done") out.println("Cleaning done")

View File

@@ -19,7 +19,7 @@ class DefaultProjectAssembler : ProjectAssembler {
tryToAssembly(project, targetJar) tryToAssembly(project, targetJar)
} catch (e: Exception) { } catch (e: Exception) {
stderr.println("[$TAG] ${e.message}") stderr.println("[$TAG] ${e.message}")
throw BuildException() throw BuildException(e)
} }
} }

View File

@@ -1,13 +1,13 @@
package com.bartlomiejpluta.base.editor.code.view.build package com.bartlomiejpluta.base.editor.code.view.build
import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildPipelineService import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildService
import com.bartlomiejpluta.base.editor.common.logs.component.LogsPane import com.bartlomiejpluta.base.editor.common.logs.component.LogsPane
import com.bartlomiejpluta.base.editor.event.ClearBuildLogsEvent import com.bartlomiejpluta.base.editor.event.ClearBuildLogsEvent
import org.kordamp.ikonli.javafx.FontIcon import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.* import tornadofx.*
class BuildLogsView : View() { class BuildLogsView : View() {
private val pipelineService: BuildPipelineService by di() private val pipelineService: BuildService by di()
private val logsPane = LogsPane() private val logsPane = LogsPane()

View File

@@ -1,6 +1,6 @@
package com.bartlomiejpluta.base.editor.main.view package com.bartlomiejpluta.base.editor.main.view
import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildPipelineService import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildService
import com.bartlomiejpluta.base.editor.main.controller.MainController import com.bartlomiejpluta.base.editor.main.controller.MainController
import com.bartlomiejpluta.base.editor.process.runner.app.ApplicationRunner import com.bartlomiejpluta.base.editor.process.runner.app.ApplicationRunner
import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.context.ProjectContext
@@ -10,7 +10,7 @@ import tornadofx.*
class MainMenuView : View() { class MainMenuView : View() {
private val mainController: MainController by di() private val mainController: MainController by di()
private val projectContext: ProjectContext by di() private val projectContext: ProjectContext by di()
private val buildPipelineService: BuildPipelineService by di() private val buildPipelineService: BuildService by di()
private val applicationRunner: ApplicationRunner by di() private val applicationRunner: ApplicationRunner by di()
override val root = menubar { override val root = menubar {

View File

@@ -1,6 +1,6 @@
package com.bartlomiejpluta.base.editor.process.runner.app package com.bartlomiejpluta.base.editor.process.runner.app
import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildPipelineService import com.bartlomiejpluta.base.editor.code.build.pipeline.BuildService
import com.bartlomiejpluta.base.editor.event.ClearProcessLogsEvent import com.bartlomiejpluta.base.editor.event.ClearProcessLogsEvent
import com.bartlomiejpluta.base.editor.process.runner.jar.JarRunner import com.bartlomiejpluta.base.editor.process.runner.jar.JarRunner
import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.context.ProjectContext
@@ -24,7 +24,7 @@ class DefaultApplicationRunner : ApplicationRunner {
private lateinit var projectContext: ProjectContext private lateinit var projectContext: ProjectContext
@Autowired @Autowired
private lateinit var pipelineService: BuildPipelineService private lateinit var pipelineService: BuildService
@Autowired @Autowired
private lateinit var jarRunner: JarRunner private lateinit var jarRunner: JarRunner

View File

@@ -98,7 +98,6 @@ class Project {
// Build directories // Build directories
val buildDirectoryProperty = SimpleObjectProperty<File>() val buildDirectoryProperty = SimpleObjectProperty<File>()
var buildDirectory by buildDirectoryProperty var buildDirectory by buildDirectoryProperty
private set
val buildClassesDirectoryProperty = SimpleObjectProperty<File>() val buildClassesDirectoryProperty = SimpleObjectProperty<File>()
var buildClassesDirectory by buildClassesDirectoryProperty var buildClassesDirectory by buildClassesDirectoryProperty
@@ -154,17 +153,22 @@ class Project {
widgetsDirectory = File(it, WIDGETS_DIR) widgetsDirectory = File(it, WIDGETS_DIR)
audioDirectory = File(it, AUDIO_DIR) audioDirectory = File(it, AUDIO_DIR)
codeDirectory = File(it, CODE_DIR) codeDirectory = File(it, CODE_DIR)
buildDirectory = File(it, BUILD_DIR)
buildClassesDirectory = File(it, BUILD_CLASSES_DIR) changeBuildDirectory(File(it, BUILD_DIR))
buildDependenciesDirectory = File(it, BUILD_DEPENDENCIES_DIR)
buildGeneratedCodeDirectory = File(it, BUILD_GENERATED_DIR)
buildAssetsDir = File(it, BUILD_ASSETS_DIR)
buildDatabaseDumpDirectory = File(it, BUILD_DATABASE_DUMP_DIR)
buildOutDirectory = File(it, BUILD_OUT_DIR)
} }
} }
} }
fun changeBuildDirectory(dir: File) {
buildDirectory = File(dir, BUILD_DIR)
buildClassesDirectory = File(dir, BUILD_CLASSES_DIR)
buildDependenciesDirectory = File(dir, BUILD_DEPENDENCIES_DIR)
buildGeneratedCodeDirectory = File(dir, BUILD_GENERATED_DIR)
buildAssetsDir = File(dir, BUILD_ASSETS_DIR)
buildDatabaseDumpDirectory = File(dir, BUILD_DATABASE_DUMP_DIR)
buildOutDirectory = File(dir, BUILD_OUT_DIR)
}
fun init() { fun init() {
database = H2DBDataSource(databaseFile) database = H2DBDataSource(databaseFile)
mkdirs() mkdirs()
@@ -208,11 +212,11 @@ class Project {
const val AUDIO_DIR = "audio" const val AUDIO_DIR = "audio"
const val CODE_DIR = "src/main/java" const val CODE_DIR = "src/main/java"
const val BUILD_DIR = "build" const val BUILD_DIR = "build"
const val BUILD_CLASSES_DIR = "$BUILD_DIR/classes" const val BUILD_CLASSES_DIR = "classes"
const val BUILD_OUT_DIR = "$BUILD_DIR/out" const val BUILD_OUT_DIR = "out"
const val BUILD_DEPENDENCIES_DIR = "$BUILD_DIR/dependencies" const val BUILD_DEPENDENCIES_DIR = "dependencies"
const val BUILD_GENERATED_DIR = "$BUILD_DIR/generated" const val BUILD_GENERATED_DIR = "generated"
const val BUILD_ASSETS_DIR = "$BUILD_DIR/assets" const val BUILD_ASSETS_DIR = "assets"
const val BUILD_DATABASE_DUMP_DIR = "$BUILD_DIR/db" const val BUILD_DATABASE_DUMP_DIR = "db"
} }
} }

View File

@@ -457,6 +457,26 @@
"hash": "sha256-VKNPqFAqRryQ79tJJiYAWR+oC/mjT1pMeYMRrsFsqXc=" "hash": "sha256-VKNPqFAqRryQ79tJJiYAWR+oC/mjT1pMeYMRrsFsqXc="
} }
}, },
"com.xenomachina:kotlin-argparser:2.0.7": {
"kotlin-argparser-2.0.7.jar": {
"url": "https://repo.maven.apache.org/maven2/com/xenomachina/kotlin-argparser/2.0.7/kotlin-argparser-2.0.7.jar",
"hash": "sha256-/wMS6nwU/l7OjE5hf6hVpAj5NvtFPDxb49fVfnoUN1M="
},
"kotlin-argparser-2.0.7.pom": {
"url": "https://repo.maven.apache.org/maven2/com/xenomachina/kotlin-argparser/2.0.7/kotlin-argparser-2.0.7.pom",
"hash": "sha256-Oi3hHJvOk2WolEAvp3fmbPgolrHpB86tWShPAjrfhvI="
}
},
"com.xenomachina:xenocom:0.0.7": {
"xenocom-0.0.7.jar": {
"url": "https://repo.maven.apache.org/maven2/com/xenomachina/xenocom/0.0.7/xenocom-0.0.7.jar",
"hash": "sha256-xiQmUrnoLXzk6BHDz3YCWC+46NyRk7PLfzR14CCqaNE="
},
"xenocom-0.0.7.pom": {
"url": "https://repo.maven.apache.org/maven2/com/xenomachina/xenocom/0.0.7/xenocom-0.0.7.pom",
"hash": "sha256-tN/7gDFK2W32lbNKB6orVLVoGNLziKKoE/h1ExDqF2s="
}
},
"com.zaxxer:HikariCP:5.0.0": { "com.zaxxer:HikariCP:5.0.0": {
"HikariCP-5.0.0.jar": { "HikariCP-5.0.0.jar": {
"url": "https://repo.maven.apache.org/maven2/com/zaxxer/HikariCP/5.0.0/HikariCP-5.0.0.jar", "url": "https://repo.maven.apache.org/maven2/com/zaxxer/HikariCP/5.0.0/HikariCP-5.0.0.jar",

View File

@@ -18,3 +18,4 @@ apacheCommonsVersion=3.6.1
h2Version=1.4.200 h2Version=1.4.200
hikariVersion=5.0.0 hikariVersion=5.0.0
javaPoetVersion=1.13.0 javaPoetVersion=1.13.0
kotlinArgParserVersion=2.0.7