tirbofish/dropbear · diff
added magna-carta plugin support for template.
Signature present but could not be verified.
Unverified
@@ -2,34 +2,14 @@ name: Release Magna Carta on: push: - branches: - - main + tags: + - 'magna-carta-v*' permissions: contents: write jobs: - check-and-release: - runs-on: ubuntu-latest - outputs: - should_release: ${{ steps.check-commit.outputs.should_release }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Check commit message - id: check-commit - run: | - COMMIT_MSG=$(git log -1 --pretty=%B) - if [[ "$COMMIT_MSG" == *"jarvis, create a new release for magna-carta"* ]]; then - echo "should_release=true" >> $GITHUB_OUTPUT - else - echo "should_release=false" >> $GITHUB_OUTPUT - fi - build-and-release: - needs: check-and-release - if: needs.check-and-release.outputs.should_release == 'true' strategy: fail-fast: false matrix: @@ -74,17 +54,9 @@ jobs: - name: Rename and prepare artifact shell: bash run: | - # Source binary path SRC="target/${{ matrix.target }}/release/${{ matrix.binary_name }}" DST="${{ matrix.artifact_name }}" - - # Copy to root for easy upload cp "$SRC" "./$DST" - - # Set executable permissions (Unix) - if [[ "${{ matrix.os }}" != "windows-latest" ]]; then - chmod +x "./$DST" - fi - name: Upload artifact uses: actions/upload-artifact@v4 @@ -93,8 +65,7 @@ jobs: path: ${{ matrix.artifact_name }} create-release: - needs: [check-and-release, build-and-release] - if: needs.check-and-release.outputs.should_release == 'true' + needs: build-and-release runs-on: ubuntu-latest steps: - name: Checkout code @@ -105,12 +76,6 @@ jobs: with: path: artifacts - - name: Restore executable permissions (just in case) - run: | - chmod +x artifacts/magna-carta-linux-x64/magna-carta-linux-x64 - chmod +x artifacts/magna-carta-macos-x64/magna-carta-macos-x64 - chmod +x artifacts/magna-carta-macos-arm64/magna-carta-macos-arm64 - - name: Check if changelog exists id: changelog-check run: | @@ -126,8 +91,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: magna-carta-v${{ github.run_number }} - name: magna-carta release v${{ github.run_number }} + tag_name: ${{ github.ref_name }} # Uses the actual tag name (e.g., magna-carta-v1.2.3) + name: Magna Carta Release ${{ github.ref_name }} body_path: ${{ steps.changelog-check.outputs.changelog_exists == 'true' && 'RELEASE_BODY.md' || '' }} files: | artifacts/magna-carta-linux-x64/magna-carta-linux-x64 @@ -0,0 +1,28 @@ +import org.gradle.kotlin.dsl.compileOnly + +plugins { + `kotlin-dsl` + `maven-publish` +} + +group = "com.dropbear" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation("de.undercouch:gradle-download-task:5.6.0") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${KotlinVersion.CURRENT}") +} + +gradlePlugin { + plugins { + create("dropbearPlugin") { + id = "magna-carta" + implementationClass = "com.dropbear.magna_carta.MagnaCartaPlugin" + version = "1.0-SNAPSHOT" + } + } +} @@ -0,0 +1,106 @@ +package com.dropbear.magna_carta + +import de.undercouch.gradle.tasks.download.Download +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.internal.os.OperatingSystem +import java.io.File +import java.security.MessageDigest + +abstract class DownloadMagnaCartaToolTask: Download() { + @get:Input + abstract val toolVersion: Property<String> + + @get:Internal + abstract val outputDir: DirectoryProperty + + @get:OutputFile + val outputFile: Provider<RegularFile> = outputDir.file(getToolFileName()) + + @TaskAction + override fun download() { + val os = OperatingSystem.current() + val arch = System.getProperty("os.arch") + + val (fileName, url, expectedSha256) = when { + os.isLinux && arch == "amd64" -> Triple( + "magna-carta-linux-x64", + "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-linux-x64", + "4cbfeca26ff0cfca5342dbd748066978041c213e6e3eb05214eec0f18a5b37b7" + ) + os.isMacOsX && arch == "aarch64" -> Triple( + "magna-carta-macos-arm64", + "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-arm64", + "446a89a9077fc78674342e3a16308957e9b39c347d3a313de00056630a446bb4" + ) + os.isMacOsX && (arch == "x86_64" || arch == "amd64") -> Triple( + "magna-carta-macos-x64", + "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-macos-x64", + "5705f0ef43aab59e9f6b1b35fec391cceb373213f275b12d85e3614272ec0660" + ) + os.isWindows && arch == "aarch64" -> Triple( + "magna-carta-windows-arm64.exe", + "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-arm64.exe", + "39072fab25a69ff8c321638897b7df00d5782c6a46b578eafbaedcb9d4efc53b" + ) + os.isWindows && (arch == "x86_64" || arch == "amd64") -> Triple( + "magna-carta-windows-x64.exe", + "https://github.com/4tkbytes/dropbear/releases/download/${toolVersion.get()}/magna-carta-windows-x64.exe", + "cce4c645da4e8a929a0f77f6685dcb104f955c19a7caba55e11d9b4c1c0ad077" + ) + else -> throw GradleException("Unsupported OS/arch: $os / $arch") + } + + val outputFile = outputDir.get().file(fileName).asFile + if (outputFile.exists() && verifyChecksum(outputFile, expectedSha256)) { + println("Using cached magna-carta tool: $outputFile") + return + } + + src(url) + dest(outputFile) + overwrite(false) + onlyIfModified(true) + + super.download() + + if (!verifyChecksum(outputFile, expectedSha256)) { + outputFile.delete() + throw GradleException("Checksum verification failed for $fileName") + } + + if (!os.isWindows) { + outputFile.setExecutable(true) + } + } + + private fun verifyChecksum(file: File, expectedSha256: String): Boolean { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().use { input -> + val buffer = ByteArray(8192) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } > 0) { + digest.update(buffer, 0, bytesRead) + } + } + val actualSha256 = digest.digest().joinToString("") { "%02x".format(it) } + return actualSha256 == expectedSha256 + } + + private fun getToolFileName(): String { + val os = OperatingSystem.current() + val arch = System.getProperty("os.arch") + return when { + os.isLinux && arch == "amd64" -> "magna-carta-linux-x64" + os.isMacOsX && arch == "aarch64" -> "magna-carta-macos-arm64" + os.isMacOsX && (arch == "x86_64" || arch == "amd64") -> "magna-carta-macos-x64" + os.isWindows && arch == "aarch64" -> "magna-carta-windows-arm64.exe" + os.isWindows && (arch == "x86_64" || arch == "amd64") -> "magna-carta-windows-x64.exe" + else -> throw GradleException("Unsupported OS/arch: $os / $arch") + } + } +} @@ -0,0 +1,48 @@ +package com.dropbear.magna_carta + + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.internal.os.OperatingSystem + + +abstract class GenerateMagnaCartaTask : DefaultTask() { + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val toolExecutable: RegularFileProperty + + @get:Input + abstract val target: Property<String> + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputDir: DirectoryProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val tool = toolExecutable.get().asFile + val input = inputDir.get().asFile + val output = outputDir.get().asFile + + output.mkdirs() + + val os = OperatingSystem.current() + val command = if (os.isWindows) { + listOf(tool.absolutePath, "--input", input.absolutePath, "--output", output.absolutePath, "--target", target.get()) + } else { + listOf("bash", "-c", "${tool.absolutePath} --input ${input.absolutePath} --output ${output.absolutePath} --target ${target.get()}") + } + + project.exec { + commandLine = command + workingDir = project.projectDir + } + } +} @@ -0,0 +1,63 @@ +package com.dropbear.magna_carta + +import de.undercouch.gradle.tasks.download.Download +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import java.io.File +import java.security.MessageDigest + +class MagnaCartaPlugin : Plugin<Project> { + override fun apply(project: Project) { + val extension = project.extensions.create("magna-carta", MagnaCartaExtension::class) + + val downloadToolTask = project.tasks.register("downloadMagnaCartaTool", DownloadMagnaCartaToolTask::class) { + toolVersion.set("magna-carta-v2") + outputDir.set(project.gradle.gradleUserHomeDir.resolve("magna-carta")) + } + + val generateJvmTask = project.tasks.register("generateMagnaCartaJvm", GenerateMagnaCartaTask::class) { + dependsOn(downloadToolTask) + toolExecutable.set(downloadToolTask.flatMap { it.outputFile }) + target.set("jvm") + inputDir.set(project.projectDir.resolve("src")) + outputDir.set(project.layout.buildDirectory.dir("generated/magna-carta/jvm")) + } + + val generateNativeTask = project.tasks.register("generateMagnaCartaNative", GenerateMagnaCartaTask::class) { + dependsOn(downloadToolTask) + toolExecutable.set(downloadToolTask.flatMap { it.outputFile }) + target.set("native") + inputDir.set(project.projectDir.resolve("src")) + outputDir.set(project.layout.buildDirectory.dir("generated/magna-carta/native")) + } + + project.pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { + val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class) + kotlin.sourceSets.apply { + val jvmMain = getByName("jvmMain") + jvmMain.kotlin.srcDir(generateJvmTask.map { it.outputDir }) + + val nativeMain = maybeCreate("nativeMain") + nativeMain.kotlin.srcDir(generateNativeTask.map { it.outputDir }) + + kotlin.targets.withType(KotlinNativeTarget::class.java) { + compilations.getByName("main").defaultSourceSet.dependsOn(nativeMain) + } + } + } + } +} + +abstract class MagnaCartaExtension { } @@ -13,3 +13,4 @@ tree-sitter-kotlin.workspace = true clap = { version = "4.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +chrono = "0.4" @@ -1,6 +1,8 @@ use crate::ScriptManifest; use crate::generator::Generator; use std::fmt::Write; +use std::collections::HashMap; +use chrono::Utc; pub struct KotlinJVMGenerator; @@ -8,11 +10,16 @@ impl Generator for KotlinJVMGenerator { fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String> { let mut output = String::new(); + let local_time = Utc::now(); + writeln!( output, - "// Auto-generated by dropbear-engine with the magna-carta parser" + "// Auto-generated by dropbear-engine with the magna-carta parser on {}", + local_time.format("%Y-%m-%d %H:%M:%S") )?; writeln!(output)?; + writeln!(output, "package com.dropbear")?; + writeln!(output)?; let mut imported_packages = std::collections::HashSet::new(); for item in manifest.items() { @@ -22,27 +29,63 @@ impl Generator for KotlinJVMGenerator { } } - for package in imported_packages.iter() { + for package in &imported_packages { writeln!(output, "import {}.*", package)?; } writeln!(output)?; - writeln!(output, "object RunnableRegistry {{")?; - writeln!(output, " val SCRIPTS = listOf(")?; - - for (i, item) in manifest.items().iter().enumerate() { - if i > 0 { - writeln!(output, ",")?; + let mut tag_map: HashMap<String, Vec<String>> = HashMap::new(); + for item in manifest.items() { + let simple_name = item.simple_name(); + if item.tags().is_empty() { + continue; + } + for tag in item.tags() { + tag_map.entry(tag.clone()).or_default().push(simple_name.to_string()); } - writeln!(output, " {}::class", item.simple_name())?; } - if !manifest.items().is_empty() { - writeln!(output)?; + writeln!(output, "object RunnableRegistry {{")?; + writeln!(output, " private val TAG_REGISTRY = mapOf(")?; + + let tag_entries: Vec<String> = tag_map + .iter() + .map(|(tag, classes)| { + let factories: Vec<String> = classes + .iter() + .map(|cls| format!("::{}", cls)) + .collect(); + format!(" \"{}\" to listOf({})", tag, factories.join(", ")) + }) + .collect(); + + if !tag_entries.is_empty() { + writeln!(output, "{}", tag_entries.join(",\n"))?; } + writeln!(output, " )")?; + writeln!(output)?; + + writeln!( + output, + " fun getScriptFactories(tag: String): List<() -> com.dropbear.System> {{" + )?; + writeln!(output, " return TAG_REGISTRY[tag] ?: emptyList()")?; + writeln!(output, " }}")?; + writeln!(output)?; + + writeln!( + output, + " fun instantiateScripts(tag: String): List<com.dropbear.System> {{" + )?; + writeln!( + output, + " return getScriptFactories(tag).map {{ it() }}" + )?; + writeln!(output, " }}")?; + writeln!(output, "}}")?; Ok(output) } -} +} @@ -47,19 +47,15 @@ mod tests { let generator = KotlinNativeGenerator; let output = generator.generate(&manifest).unwrap(); - // Verify imports assert!(output.contains("import com.game.Player")); assert!(output.contains("import com.game.GlobalLogger")); - // Verify tags assert!(output.contains("tags = listOf(\"player\", \"movement\")")); assert!(output.contains("tags = listOf()")); - // Verify class instantiations assert!(output.contains("script = Player()")); assert!(output.contains("script = GlobalLogger()")); - // Verify CName exports assert!(output.contains("@CName(\"dropbear_load\")")); assert!(output.contains("@CName(\"dropbear_update\")")); assert!(output.contains("@CName(\"dropbear_destroy\")")); @@ -1,171 +1,137 @@ use crate::ScriptManifest; use crate::generator::Generator; use std::fmt::Write; +use std::collections::HashMap; +use chrono::Utc; pub struct KotlinNativeGenerator; impl Generator for KotlinNativeGenerator { fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String> { let mut output = String::new(); - + let local_time = Utc::now(); writeln!( output, - "// Auto-generated by dropbear-engine with the magna-carta parser" + "// Auto-generated by dropbear-engine with the magna-carta parser on {}", + local_time.format("%Y-%m-%d %H:%M:%S") )?; writeln!( output, - "@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)" + "@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)" )?; writeln!(output)?; + writeln!(output, "package com.dropbear")?; + writeln!(output)?; writeln!(output, "import com.dropbear.DropbearEngine")?; writeln!(output, "import com.dropbear.EntityId")?; writeln!(output, "import com.dropbear.EntityRef")?; - writeln!(output, "import com.dropbear.ProjectScriptingMetadata")?; - writeln!(output, "import com.dropbear.ScriptRegistration")?; writeln!(output, "import com.dropbear.ffi.NativeEngine")?; writeln!(output, "import kotlinx.cinterop.COpaquePointer")?; writeln!(output, "import kotlinx.cinterop.ExperimentalForeignApi")?; - writeln!(output, "import kotlin.experimental.ExperimentalNativeApi")?; writeln!(output, "import kotlin.native.CName")?; writeln!(output)?; - let mut imported_classes = std::collections::HashSet::new(); + let mut imported_classes = Vec::new(); for item in manifest.items() { if let Some(last_dot) = item.fqcn().rfind('.') { let package = &item.fqcn()[..last_dot]; let simple_name = &item.fqcn()[last_dot + 1..]; writeln!(output, "import {}.{}", package, simple_name)?; - imported_classes.insert(simple_name.to_string()); + imported_classes.push(simple_name.to_string()); } else { - imported_classes.insert(item.simple_name().to_string()); + imported_classes.push(item.simple_name().to_string()); } } writeln!(output)?; - writeln!( - output, - "private val scriptInstances: List<ScriptRegistration> by lazy {{" - )?; - writeln!(output, " Metadata().getScripts()")?; - writeln!(output, "}}")?; - writeln!(output)?; - - writeln!(output, "class Metadata: ProjectScriptingMetadata {{")?; - writeln!( - output, - " override fun getScripts(): List<ScriptRegistration> {{" - )?; - writeln!(output, " return listOf(")?; - - for (i, item) in manifest.items().iter().enumerate() { - if i > 0 { - writeln!(output, ",")?; + let mut tag_map: HashMap<String, Vec<String>> = HashMap::new(); + for item in manifest.items() { + let simple_name = item.simple_name(); + if item.tags().is_empty() { + continue; + } + for tag in item.tags() { + tag_map.entry(tag.clone()).or_default().push(simple_name.to_string()); } + } - let tags_str = if item.tags().is_empty() { - "listOf()".to_string() - } else { - let tag_list: Vec<String> = item - .tags() - .iter() - .map(|tag| format!("\"{}\"", tag)) - .collect(); - format!("listOf({})", tag_list.join(", ")) - }; + writeln!(output, "private fun getScriptFactories(tag: String): List<() -> System> {{")?; + writeln!(output, " return when (tag) {{")?; + for (tag, classes) in &tag_map { + let factories: Vec<String> = classes + .iter() + .map(|cls| format!("{{ {}() }}", cls)) + .collect(); writeln!( output, - " ScriptRegistration(\n tags = {},\n script = {}()\n )", - tags_str, - item.simple_name() + " \"{}\" -> listOf({})", + tag, + factories.join(", ") )?; } - if !manifest.items().is_empty() { - writeln!(output)?; - } - writeln!(output, " )")?; + writeln!(output, " else -> emptyList()")?; writeln!(output, " }}")?; writeln!(output, "}}")?; writeln!(output)?; writeln!( output, - "fun getDropbearEngine(worldPointer: COpaquePointer?, currentEntity: Long?): DropbearEngine {{" + "private fun getDropbearEngine(worldPointer: COpaquePointer?, currentEntity: Long?): DropbearEngine {{" )?; writeln!(output, " val nativeEngine = NativeEngine()")?; writeln!(output, " nativeEngine.init(worldPointer)")?; writeln!( output, - " val dropbearEngine = DropbearEngine(nativeEngine, if (currentEntity == null) null else EntityRef(EntityId(currentEntity)))" - )?; - writeln!(output, " return dropbearEngine")?; - writeln!(output, "}}")?; - writeln!(output)?; - - writeln!(output, "@CName(\"dropbear_load\")")?; - writeln!( - output, - "fun loadScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) {{" - )?; - writeln!(output, " if (tag == null) return")?; - writeln!( - output, - " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}" + " val entityRef = currentEntity?.let {{ EntityRef(EntityId(it)) }}" )?; - writeln!( - output, - " val engine = getDropbearEngine(worldPointer, currentEntity)" - )?; - writeln!(output, " for (script in scripts) {{")?; - writeln!(output, " script.script.load(engine)")?; - writeln!(output, " }}")?; + writeln!(output, " return DropbearEngine(nativeEngine, entityRef)")?; writeln!(output, "}}")?; writeln!(output)?; - writeln!(output, "@CName(\"dropbear_update\")")?; - writeln!( - output, - "fun updateScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?, deltaTime: Double) {{" - )?; - writeln!(output, " if (tag == null) return")?; - writeln!( - output, - " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}" - )?; - writeln!( - output, - " val engine = getDropbearEngine(worldPointer, currentEntity)" - )?; - writeln!(output, " for (script in scripts) {{")?; - writeln!( - output, - " script.script.update(engine, deltaTime.toFloat())" - )?; - writeln!(output, " }}")?; - writeln!(output, "}}")?; - writeln!(output)?; + for (func_name, method_name) in [ + ("dropbear_load", "load"), + ("dropbear_update", "update"), + ("dropbear_destroy", "destroy"), + ] { + let param_extra = if method_name == "update" { + ", deltaTime: Double" + } else { + "" + }; + let call_extra = if method_name == "update" { + ", deltaTime.toFloat()" + } else { + "" + }; - writeln!(output, "@CName(\"dropbear_destroy\")")?; - writeln!( - output, - "fun destroyScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) {{" - )?; - writeln!(output, " if (tag == null) return")?; - writeln!( - output, - " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}" - )?; - writeln!( - output, - " val engine = getDropbearEngine(worldPointer, currentEntity)" - )?; - writeln!(output, " for (script in scripts) {{")?; - writeln!(output, " script.script.destroy(engine)")?; - writeln!(output, " }}")?; - writeln!(output, "}}")?; + writeln!(output, "@CName(\"{}\")", func_name)?; + writeln!( + output, + "fun {}ScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?{}) {{", + method_name, + param_extra + )?; + writeln!(output, " if (tag == null) return")?; + writeln!(output, " val factories = getScriptFactories(tag)")?; + writeln!( + output, + " val engine = getDropbearEngine(worldPointer, currentEntity)" + )?; + writeln!(output, " for (factory in factories) {{")?; + writeln!(output, " val script = factory()")?; + if method_name == "update" { + writeln!(output, " script.{}(engine{})", method_name, call_extra)?; + } else { + writeln!(output, " script.{}(engine)", method_name)?; + } + writeln!(output, " }}")?; + writeln!(output, "}}")?; + writeln!(output)?; + } Ok(output) } -} +} @@ -5,4 +5,5 @@ pluginManagement { mavenCentral() gradlePluginPortal() } -} +} +include("magna-carta-plugin")