diff --git a/.github/detekt.yml b/.github/detekt.yml index ef14acf..1d15fa7 100644 --- a/.github/detekt.yml +++ b/.github/detekt.yml @@ -26,5 +26,6 @@ style: - "3" - "10" - "16" + - "0x7F" ReturnCount: max: 3 diff --git a/.github/scripts/build-jni.ps1 b/.github/scripts/build-jni.ps1 index 748a717..cc33042 100755 --- a/.github/scripts/build-jni.ps1 +++ b/.github/scripts/build-jni.ps1 @@ -2,13 +2,15 @@ & cmake -S ktreesitter -B ktreesitter/.cmake/build ` -DCMAKE_VERBOSE_MAKEFILE=ON ` + -DCMAKE_INSTALL_PREFIX=ktreesitter/src/jvmMain/resources ` -DCMAKE_INSTALL_BINDIR="$env:CMAKE_INSTALL_LIBDIR" & cmake --build ktreesitter/.cmake/build --config Debug -& cmake --install ktreesitter/.cmake/build --config Debug --prefix ktreesitter/src/jvmMain/resources +& cmake --install ktreesitter/.cmake/build --config Debug foreach ($dir in Get-ChildItem -Directory -Path languages) { - & cmake -S "$dir" -B "$dir/.cmake/build" ` + & cmake -S "$dir/build/generated" -B "$dir/.cmake/build" ` + -DCMAKE_INSTALL_PREFIX="$dir/build/generated/src/jvmMain/resources" ` -DCMAKE_INSTALL_BINDIR="$env:CMAKE_INSTALL_LIBDIR" & cmake --build "$dir/.cmake/build" --config Debug - & cmake --install "$dir/.cmake/build" --config Debug --prefix "$dir/src/jvmMain/resources" + & cmake --install "$dir/.cmake/build" --config Debug } diff --git a/.github/scripts/build-jni.sh b/.github/scripts/build-jni.sh index e9622ef..89474b8 100755 --- a/.github/scripts/build-jni.sh +++ b/.github/scripts/build-jni.sh @@ -4,16 +4,17 @@ cmake -S ktreesitter -B ktreesitter/.cmake/build \ -DCMAKE_BUILD_TYPE=RelWithDebugInfo \ -DCMAKE_VERBOSE_MAKEFILE=ON \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_INSTALL_PREFIX=ktreesitter/src/jvmMain/resources \ -DCMAKE_INSTALL_LIBDIR="$CMAKE_INSTALL_LIBDIR" - cmake --build ktreesitter/.cmake/build -cmake --install ktreesitter/.cmake/build --prefix ktreesitter/src/jvmMain/resources +cmake --install ktreesitter/.cmake/build for dir in languages/*/; do - cmake -S "$dir" -B "${dir}.cmake/build" \ + cmake -S "${dir}build/generated" -B "${dir}.cmake/build" \ -DCMAKE_BUILD_TYPE=RelWithDebugInfo \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_INSTALL_PREFIX="${dir}build/generated/src/jvmMain/resources" \ -DCMAKE_INSTALL_LIBDIR="$CMAKE_INSTALL_LIBDIR" cmake --build "${dir}.cmake/build" - cmake --install "${dir}.cmake/build" --prefix "${dir}src/jvmMain/resources" + cmake --install "${dir}.cmake/build" done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3adf43f..553eebb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,42 +23,78 @@ permissions: contents: write jobs: + generate: + runs-on: ubuntu-latest + name: Generate grammar files + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: gradle + cache-dependency-path: | + gradle/libs.versions.toml + gradle/wrapper/gradle-wrapper.properties + - name: Cache Kotlin/Native prebuilt + uses: actions/cache@v4 + with: + path: ${{runner.tool_cache}}/konan/kotlin-native-prebuilt-* + key: konan-${{runner.os}}-prebuilt-1.9 + - name: Generate files + run: ./gradlew --no-daemon generateGrammarFiles + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: generated-files + path: languages/*/build/generated/** + retention-days: 1 test: runs-on: ${{matrix.os}} name: >- Test ${{matrix.platform}} platform ${{matrix.lib_platform && format('({0}-{1})', matrix.lib_platform, matrix.lib_arch)}} + needs: [generate] strategy: fail-fast: false matrix: include: - os: ubuntu-latest platform: JVM - targets: jvmTest + targets: :ktreesitter:jvmTest lib_platform: linux lib_arch: x64 - os: windows-latest platform: JVM - targets: jvmTest + targets: :ktreesitter:jvmTest lib_platform: windows lib_arch: x64 - os: macos-latest platform: JVM - targets: jvmTest + targets: :ktreesitter:jvmTest lib_platform: macos lib_arch: aarch64 # - os: ubuntu-latest # platform: Android - # targets: connectedDebugAndroidTest + # targets: :ktreesitter:connectedDebugAndroidTest - os: ubuntu-latest platform: Linux - targets: compileKotlinLinuxArm64 linuxX64Test + targets: >- + :ktreesitter:compileKotlinLinuxArm64 + :ktreesitter:linuxX64Test - os: windows-latest platform: Windows - targets: mingwX64Test + targets: :ktreesitter:mingwX64Test - os: macos-latest platform: macOS/iOS - targets: macosX64Test macosArm64Test iosSimulatorArm64Test + targets: >- + :ktreesitter:macosX64Test + :ktreesitter:macosArm64Test + :ktreesitter:iosSimulatorArm64Test steps: - name: Checkout repository uses: actions/checkout@v4 @@ -81,6 +117,11 @@ jobs: with: path: ${{runner.tool_cache}}/konan/kotlin-native-prebuilt-* key: konan-${{runner.os}}-prebuilt-1.9 + - name: Download generated files + uses: actions/download-artifact@v4 + with: + path: languages + name: generated-files - name: Build JNI libraries if: matrix.platform == 'JVM' run: .github/scripts/build-jni.${{matrix.os == 'windows-latest' && 'ps1' || 'sh'}} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a616af8..fd13cf6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,7 +49,7 @@ jobs: path: ${{runner.tool_cache}}/konan/dependencies key: konan-${{runner.os}}-dependencies - name: Build documentation - run: ./gradlew --no-daemon :ktreesitter:dokkaHtml + run: ./gradlew --no-daemon generateFiles :ktreesitter:dokkaHtml env: KONAN_DATA_DIR: ${{runner.tool_cache}}/konan - name: Upload pages artifact diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6d69dfb..80aec1f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -146,7 +146,6 @@ jobs: pattern: ktreesitter-lib-* merge-multiple: true - name: Build packages - shell: bash run: ./gradlew --no-daemon ${{matrix.targets}} env: SIGNING_KEY: ${{secrets.SIGNING_KEY}} @@ -160,7 +159,7 @@ jobs: ktreesitter/build/repo/** !ktreesitter/build/repo/**/maven-metadata.* retention-days: 2 - publish: + publish-library: runs-on: ubuntu-latest name: Publish packages on Maven Central needs: [build-jars] @@ -201,3 +200,28 @@ jobs: run: gh release create $GITHUB_REF_NAME --generate-notes env: GH_TOKEN: ${{github.token}} + publish-plugin: + runs-on: ubuntu-latest + name: Publish Gradle plugin + needs: [build-jars] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: gradle + cache-dependency-path: | + gradle/libs.versions.toml + gradle/wrapper/gradle-wrapper.properties + - name: Publish plugin + run: >- + ./gradlew --no-daemon :ktreesitter-plugin:publishPlugins + -Pgradle.publish.key=$PUBLISH_KEY -Pgradle.publish.secret=$PUBLISH_SECRET + env: + PUBLISH_KEY: ${{secrets.GRADLE_PUBLISH_KEY}} + PUBLISH_SECRET: ${{secrets.GRADLE_PUBLISH_SECRET}} diff --git a/.idea/conventionalCommit.json b/.idea/conventionalCommit.json index 5c9fcae..f507ea1 100644 --- a/.idea/conventionalCommit.json +++ b/.idea/conventionalCommit.json @@ -34,6 +34,7 @@ "jvm": {}, "native": {}, "jni": {}, + "plugin": {}, "languages": {} }, "footerTypes": [ diff --git a/README.md b/README.md index 1f804ad..e0bb222 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,6 @@ Kotlin bindings to the Tree-sitter parsing library - [ ] Swift - [ ] Markdown - [ ] CMake -- [ ] Gradle plugin +- [x] Gradle plugin - [ ] Package - [ ] DSL? diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6301a1..b6b9ff1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,10 @@ version.ref = "kotest" id = "org.jetbrains.dokka" version.ref = "dokka" +[plugins.gradle-publish] +id = "com.gradle.plugin-publish" +version = "1.2.1" + [bundles] kotest-core = [ "kotest-engine", diff --git a/ktreesitter-plugin/build.gradle.kts b/ktreesitter-plugin/build.gradle.kts new file mode 100644 index 0000000..46fb1a0 --- /dev/null +++ b/ktreesitter-plugin/build.gradle.kts @@ -0,0 +1,40 @@ +import java.util.Properties +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +group = "io.github.tree-sitter" +version = with(Properties()) { + file("../gradle.properties").reader().use(::load) + getProperty("project.version") +} + +plugins { + `kotlin-dsl` + id("com.gradle.plugin-publish") version "1.2.1" +} + +repositories { + mavenCentral() +} + +kotlin { + compilerOptions { + languageVersion.set(KotlinVersion.KOTLIN_1_9) + } +} + +@Suppress("UnstableApiUsage") +gradlePlugin { + vcsUrl = "https://github.com/tree-sitter/kotlin-tree-sitter" + website = "https://github.com/tree-sitter/kotlin-tree-sitter/tree/master/plugin" + plugins.create("ktreesitter") { + id = "$group.${project.name}" + displayName = "KTreeSitter grammar plugin" + description = "A plugin that generates code for KTreeSitter grammar packages" + implementationClass = "io.github.treesitter.ktreesitter.plugin.GrammarPlugin" + tags = listOf("tree-sitter", "code", "generator") + } +} + +tasks.validatePlugins { + enableStricterValidation.set(true) +} diff --git a/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarExtension.kt b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarExtension.kt new file mode 100644 index 0000000..5cbe3f3 --- /dev/null +++ b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarExtension.kt @@ -0,0 +1,16 @@ +package io.github.treesitter.ktreesitter.plugin + +import java.io.File +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property + +abstract class GrammarExtension { + abstract val baseDir: Property + abstract val grammarName: Property + abstract val files: Property> + abstract val interopName: Property + abstract val libraryName: Property + abstract val packageName: Property + abstract val className: Property + abstract val languageMethods: MapProperty +} diff --git a/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarFilesTask.kt b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarFilesTask.kt new file mode 100644 index 0000000..ea0a3f8 --- /dev/null +++ b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarFilesTask.kt @@ -0,0 +1,177 @@ +package io.github.treesitter.ktreesitter.plugin + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class GrammarFilesTask : DefaultTask() { + @InputDirectory + lateinit var grammarDir: File + + @Input + lateinit var grammarName: String + + @InputFiles + lateinit var grammarFiles: Array + + @Input + lateinit var interopName: String + + @Input + lateinit var libraryName: String + + @Input + lateinit var packageName: String + + @Input + lateinit var className: String + + @Input + lateinit var languageMethods: Map + + @get:OutputDirectory + abstract val generatedSrc: DirectoryProperty + + @get:OutputFile + abstract val cmakeListsFile: RegularFileProperty + + @get:OutputFile + abstract val interopFile: RegularFileProperty + + @TaskAction + @Suppress("ThrowsCount", "LongMethod") + fun generate() { + val srcDir = generatedSrc.get().asFile.apply(File::mkdir) + + if (!packageNameRegex.matches(packageName)) + throw GradleException("Package name is not valid: $packageName") + if (!className.isJavaIdentifier()) + throw GradleException("Class name is not valid: $className") + for ((key, value) in languageMethods) { + if (!key.isJavaIdentifier()) + throw GradleException("Method name is not valid: $key") + if (!value.isJavaIdentifier()) + throw GradleException("Method name is not valid: $value") + } + + val jniClassName = className.jniTransform() + val jniPackageName = packageName.jniTransform().replace('.', '_') + val jniPrefix = "Java_${jniPackageName}_${jniClassName}_" + val jniBinding = srcDir.resolve("jni").apply(File::mkdir).resolve("binding.c") + jniBinding.writeText( + JNI_TEMPLATE.trimStart().format( + grammarName, + languageMethods.values.joinToString("\n\n") { name -> + """ + NATIVE_FUNCTION($jniPrefix${name.jniTransform()}) { + return (jlong)$name(); + } + """.trimIndent() + } + ) + ) + + val classFile = "kotlin/${packageName.replace('.', '/')}/$className.kt" + srcDir.resolve("commonMain/$classFile").apply { parentFile.mkdirs() }.writeText( + COMMON_TEMPLATE.trimStart().format( + packageName, + className, + languageMethods.keys.joinToString("\n\n") { + " fun $it(): Any" + } + ) + ) + srcDir.resolve("nativeMain/$classFile").apply { parentFile.mkdirs() }.writeText( + NATIVE_TEMPLATE.trimStart().format( + packageName, + packageName, + className, + languageMethods.map { (kotlinName, nativeName) -> + " actual fun $kotlinName(): Any = $nativeName()!!" + }.joinToString("\n\n") + ) + ) + srcDir.resolve("androidMain/$classFile").apply { parentFile.mkdirs() }.writeText( + ANDROID_TEMPLATE.trimStart().format( + packageName, + className, + libraryName, + languageMethods.map { (kotlinName, jniName) -> + """ + | actual fun $kotlinName(): Any = $jniName() + | + | @JvmStatic + | @CriticalNative + | private external fun $jniName(): Long + """.trimMargin() + }.joinToString("\n\n") + ) + ) + srcDir.resolve("jvmMain/$classFile").apply { parentFile.mkdirs() }.writeText( + JVM_TEMPLATE.trimStart().format( + packageName, + className, + libraryName, + languageMethods.map { (kotlinName, jniName) -> + """ + | actual fun $kotlinName(): Any = $jniName() + | + | @JvmStatic + | private external fun $jniName(): Long + """.trimMargin() + }.joinToString("\n\n") + ) + ) + + interopFile.get().asFile.apply { parentFile.mkdirs() }.writeText( + INTEROP_TEMPLATE.trimStart().format( + packageName, + grammarName, + grammarName + ) + ) + + cmakeListsFile.get().asFile.writeText( + CMAKE_TEMPLATE.trimStart().format( + grammarDir.resolve("bindings/c").relative, + libraryName, + grammarFiles.joinToString(" ", "${jniBinding.relative} ") { it.relative }, + libraryName, + libraryName + ) + ) + } + + private inline val File.relative: String + get() = toRelativeString(generatedSrc.get().asFile.parentFile) + + private companion object { + @JvmStatic + private val packageNameRegex = Regex("""^[A-Za-z_]\w*(?:[.][A-Za-z_]\w*)*$""") + + @JvmStatic + private fun String.jniTransform() = map { + when { + it == '_' -> "_1" + it == ';' -> "_2" + it == '[' -> "_3" + it.code > 0x7F -> "_0%04x".format(it.code) + else -> it.toString() + } + }.joinToString("") + + @JvmStatic + private fun String.isJavaIdentifier(): Boolean { + if (!get(0).isJavaIdentifierStart()) return false + return drop(1).all { it.isJavaIdentifierPart() } + } + } +} diff --git a/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarPlugin.kt b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarPlugin.kt new file mode 100644 index 0000000..794cfb6 --- /dev/null +++ b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/GrammarPlugin.kt @@ -0,0 +1,39 @@ +package io.github.treesitter.ktreesitter.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register + +class GrammarPlugin : Plugin { + override fun apply(project: Project) { + val extension = project.extensions.create("grammar") + extension.baseDir.convention(project.projectDir.parentFile.parentFile) + extension.interopName.convention("grammar") + extension.libraryName.convention( + extension.grammarName.map { "ktreesitter-$it" } + ) + extension.languageMethods.convention( + extension.grammarName.map { mapOf("language" to "tree_sitter_$it") } + ) + + project.tasks.register("generateGrammarFiles") { + grammarDir = extension.baseDir.get() + grammarName = extension.grammarName.get() + grammarFiles = extension.files.get() + interopName = extension.interopName.get() + libraryName = extension.libraryName.get() + packageName = extension.packageName.get() + className = extension.className.get() + languageMethods = extension.languageMethods.get() + + val generatedDir = project.layout.buildDirectory.get().dir("generated") + generatedSrc.set(generatedDir.dir("src")) + cmakeListsFile.set(generatedDir.file("CMakeLists.txt")) + interopFile.set(generatedDir.dir("src").dir("nativeInterop").file("$interopName.def")) + + outputs.dir(generatedSrc) + outputs.files(cmakeListsFile, interopFile) + } + } +} diff --git a/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/templates.kt b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/templates.kt new file mode 100644 index 0000000..d203a14 --- /dev/null +++ b/ktreesitter-plugin/src/main/kotlin/io/github/treesitter/ktreesitter/plugin/templates.kt @@ -0,0 +1,167 @@ +package io.github.treesitter.ktreesitter.plugin + +internal const val JNI_TEMPLATE = """ +// Automatically generated file. DO NOT MODIFY + +#include +#include + +#ifndef __ANDROID__ +#define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name(JNIEnv * _env, jclass _class) +#else +#define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name() +#endif + +%s +""" + +internal const val INTEROP_TEMPLATE = """ +# Automatically generated file. DO NOT MODIFY + +package = %s.internal +headers = tree-sitter-%s.h +compilerOpts = -DTREE_SITTER_HIDE_SYMBOLS +staticLibraries = libtree-sitter-%s.a +""" + +internal const val COMMON_TEMPLATE = """ +// Automatically generated file. DO NOT MODIFY + +package %s + +expect object %s { +%s +} +""" + +internal const val NATIVE_TEMPLATE = """ +// Automatically generated file. DO NOT MODIFY + +package %s + +import %s.internal.* +import kotlinx.cinterop.ExperimentalForeignApi + +@OptIn(ExperimentalForeignApi::class) +actual object %s { +%s +} +""" + +internal const val ANDROID_TEMPLATE = """ +// Automatically generated file. DO NOT MODIFY + +package %s + +import dalvik.annotation.optimization.CriticalNative +import javax.annotation.processing.Generated + +@Suppress("FunctionName") +@Generated("io.github.treesitter.ktreesitter-plugin") +actual object %s { + init { + System.loadLibrary("%s") + } + +%s +} +""" + +internal const val JVM_TEMPLATE = """ +// Automatically generated file. DO NOT MODIFY + +package %s + +import java.io.File.createTempFile +import javax.annotation.processing.Generated + +@Suppress("FunctionName") +@Generated("io.github.treesitter.ktreesitter-plugin") +actual object %s { + private const val LIB_NAME = "%s" + + init { + try { + System.loadLibrary(LIB_NAME) + } catch (ex: UnsatisfiedLinkError) { + @Suppress("UnsafeDynamicallyLoadedCode") + System.load(libPath() ?: throw ex) + } + } + +%s + + @JvmStatic + @Suppress("ConvertToStringTemplate") + @Throws(UnsupportedOperationException::class) + internal fun libPath(): String? { + val osName = System.getProperty("os.name")!!.lowercase() + val archName = System.getProperty("os.arch")!!.lowercase() + val ext: String + val os: String + val prefix: String + when { + "windows" in osName -> { + ext = "dll" + os = "windows" + prefix = "" + } + "linux" in osName -> { + ext = "so" + os = "linux" + prefix = "lib" + } + "mac" in osName -> { + ext = "dylib" + os = "macos" + prefix = "lib" + } + else -> { + throw UnsupportedOperationException("Unsupported operating system: " + osName) + } + } + val arch = when { + "amd64" in archName || "x86_64" in archName -> "x64" + "aarch64" in archName || "arm64" in archName -> "aarch64" + else -> throw UnsupportedOperationException("Unsupported architecture: " + archName) + } + val libPath = "/lib/" + os + "/" + arch + "/" + prefix + LIB_NAME + "." + ext + val libUrl = javaClass.getResource(libPath) ?: return null + return createTempFile(prefix + LIB_NAME, "." + ext).apply { + writeBytes(libUrl.openStream().use { it.readAllBytes() }) + deleteOnExit() + }.path + } +} +""" + +internal const val CMAKE_TEMPLATE = """ +# Automatically generated file. DO NOT MODIFY + +cmake_minimum_required(VERSION 3.12.0) + +project(ktreesitter-java LANGUAGES C) + +find_package(JNI REQUIRED) + +set(CMAKE_C_STANDARD 11) + +if(MSVC) + add_compile_options(/W3 /wd4244) +else(MSVC) + set(CMAKE_C_VISIBILITY_PRESET hidden) + add_compile_options(-Wall -Wextra + -Wno-unused-parameter + -Werror=implicit-function-declaration) +endif(MSVC) + +include_directories(${'$'}{JNI_INCLUDE_DIRS} %s) + +add_compile_definitions(TREE_SITTER_HIDE_SYMBOLS) + +add_library(%s SHARED %s) + +set_target_properties(%s PROPERTIES DEFINE_SYMBOL "") + +install(TARGETS %s ARCHIVE EXCLUDE_FROM_ALL) +""" diff --git a/ktreesitter/build.gradle.kts b/ktreesitter/build.gradle.kts index 8d5117a..4957f6f 100644 --- a/ktreesitter/build.gradle.kts +++ b/ktreesitter/build.gradle.kts @@ -98,13 +98,6 @@ kotlin { } } - /* - getByName("androidUnitTest") { - dependencies { - implementation(libs.kotest.junit.runner) - } - } - */ getByName("androidInstrumentedTest") { dependencies { implementation(libs.bundles.kotest.core) @@ -153,9 +146,6 @@ android { "win32-x86/attach_hotspot_windows.dll" ) } - buildFeatures { - resValues = false - } } tasks.create("javadocJar") { @@ -268,7 +258,7 @@ tasks.withType().configureEach { tasks.withType().configureEach { if (name.startsWith("cinteropTest")) return@configureEach - val runKonan = file(konanHome.get()).resolve("bin") + val runKonan = File(konanHome.get()).resolve("bin") .resolve(if (os.isWindows) "run_konan.bat" else "run_konan").path val libFile = libsDir.dir(konanTarget.name).file( "${konanTarget.family.staticPrefix}tree-sitter.${konanTarget.family.staticSuffix}" diff --git a/languages/java/CMakeLists.txt b/languages/java/CMakeLists.txt deleted file mode 100644 index 687fc85..0000000 --- a/languages/java/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -cmake_minimum_required(VERSION 3.12.0) - -file(READ tree-sitter-java/Makefile MAKEFILE) - -string(REGEX MATCH "VERSION := ([0-9.]+)" _ ${MAKEFILE}) - -project(ktreesitter-java VERSION ${CMAKE_MATCH_1}) - -find_package(JNI REQUIRED) - -set(CMAKE_C_STANDARD 11) - -if(MSVC) - add_compile_options(/W3 /wd4244) -else(MSVC) - set(CMAKE_C_VISIBILITY_PRESET hidden) - add_compile_options(-Wall -Wextra - -Wno-unused-parameter - -Werror=implicit-function-declaration) -endif(MSVC) - -include_directories(${JNI_INCLUDE_DIRS} - tree-sitter-java/bindings/c) - -add_compile_definitions(TREE_SITTER_HIDE_SYMBOLS) - -add_library(ktreesitter-java SHARED - tree-sitter-java/src/parser.c - # tree-sitter-java/src/scanner.c - src/jni/binding.c) - -set_target_properties(ktreesitter-java PROPERTIES DEFINE_SYMBOL "") - -install(TARGETS ktreesitter-java ARCHIVE EXCLUDE_FROM_ALL) diff --git a/languages/java/build.gradle.kts b/languages/java/build.gradle.kts index 40d5cdb..dbe4f13 100644 --- a/languages/java/build.gradle.kts +++ b/languages/java/build.gradle.kts @@ -11,11 +11,6 @@ inline val File.unixPath: String val os: OperatingSystem = OperatingSystem.current() val libsDir = layout.buildDirectory.get().dir("libs") val grammarDir = projectDir.resolve("tree-sitter-java") -val grammarName = project.name -val grammarFiles = arrayOf( - // grammarDir.resolve("src/scanner.c"), - grammarDir.resolve("src/parser.c") -) version = grammarDir.resolve("Makefile").readLines() .first { it.startsWith("VERSION := ") }.removePrefix("VERSION := ") @@ -25,8 +20,22 @@ plugins { signing alias(libs.plugins.kotlin.mpp) alias(libs.plugins.android.library) + id("io.github.tree-sitter.ktreesitter-plugin") } +grammar { + baseDir = grammarDir + grammarName = project.name + className = "TreeSitterJava" + packageName = "io.github.treesitter.ktreesitter.java" + files = arrayOf( + // grammarDir.resolve("src/scanner.c"), + grammarDir.resolve("src/parser.c") + ) +} + +val generateTask = tasks.generateGrammarFiles.get() + kotlin { jvm {} @@ -50,7 +59,8 @@ kotlin { } }.forEach { target -> target.compilations.configureEach { - cinterops.create("parser") { + cinterops.create(grammar.interopName.get()) { + defFileProperty.set(generateTask.interopFile.asFile) includeDirs.allHeaders(grammarDir.resolve("bindings/c")) extraOpts("-libraryPath", libsDir.dir(konanTarget.name)) } @@ -60,6 +70,11 @@ kotlin { jvmToolchain(17) sourceSets { + val generatedSrc = generateTask.generatedSrc.get() + configureEach { + kotlin.srcDir(generatedSrc.dir(name).dir("kotlin")) + } + commonMain { @OptIn(ExperimentalKotlinGradlePluginApi::class) languageSettings { @@ -72,24 +87,29 @@ kotlin { implementation(libs.kotlin.stdlib) } } + + jvmMain { + resources.srcDir(generatedSrc.dir(name).dir("resources")) + } } } android { - namespace = "io.github.treesitter.ktreesitter.$grammarName" + namespace = "io.github.treesitter.ktreesitter.${grammar.grammarName.get()}" compileSdk = (property("sdk.version.compile") as String).toInt() ndkVersion = property("ndk.version") as String defaultConfig { minSdk = (property("sdk.version.min") as String).toInt() ndk { - moduleName = "ktreesitter-java" + moduleName = grammar.libraryName.get() //noinspection ChromeOsAbiSupport abiFilters += setOf("x86_64", "arm64-v8a", "armeabi-v7a") } + resValue("string", "version", version as String) } externalNativeBuild { cmake { - path = file("CMakeLists.txt") + path = generateTask.cmakeListsFile.get().asFile buildStagingDirectory = file(".cmake") version = property("cmake.version") as String } @@ -98,9 +118,54 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - buildFeatures { - resValues = false +} + +tasks.withType().configureEach { + if (name.startsWith("cinteropTest")) return@configureEach + + val grammarFiles = grammar.files.get() + val grammarName = grammar.grammarName.get() + val runKonan = File(konanHome.get()).resolve("bin") + .resolve(if (os.isWindows) "run_konan.bat" else "run_konan").path + val libFile = libsDir.dir(konanTarget.name).file("libtree-sitter-$grammarName.a").asFile + val objectFiles = grammarFiles.map { + grammarDir.resolve(it.nameWithoutExtension + ".o").path + }.toTypedArray() + val loader = PlatformManager(konanHome.get(), false, konanDataDir.orNull).loader(konanTarget) + + doFirst { + if (!File(loader.absoluteTargetToolchain).isDirectory) loader.downloadDependencies() + + val argsFile = File.createTempFile("args", null) + argsFile.deleteOnExit() + argsFile.writer().useToRun { + write("-I" + grammarDir.resolve("src").unixPath + "\n") + write("-DTREE_SITTER_HIDE_SYMBOLS\n") + write("-fvisibility=hidden\n") + write("-std=c11\n") + write("-O2\n") + write("-g\n") + write("-c\n") + grammarFiles.forEach { write(it.unixPath + "\n") } + } + + exec { + executable = runKonan + workingDir = grammarDir + standardOutput = nullOutputStream() + args("clang", "clang", konanTarget.name, "@" + argsFile.path) + } + + exec { + executable = runKonan + workingDir = grammarDir + standardOutput = nullOutputStream() + args("llvm", "llvm-ar", "rcs", libFile.path, *objectFiles) + } } + + inputs.files(*grammarFiles) + outputs.file(libFile) } tasks.create("javadocJar") { @@ -110,7 +175,8 @@ tasks.create("javadocJar") { publishing { publications.withType(MavenPublication::class) { - artifactId = "ktreesitter-$grammarName" + val grammarName = grammar.grammarName.get() + artifactId = grammar.libraryName.get() artifact(tasks["javadocJar"]) pom { name.set("KTreeSitter $grammarName") @@ -156,52 +222,6 @@ signing { sign(publishing.publications) } -tasks.withType().configureEach { - if (name.startsWith("cinteropTest")) return@configureEach - - val runKonan = file(konanHome.get()).resolve("bin") - .resolve(if (os.isWindows) "run_konan.bat" else "run_konan").path - val libFile = libsDir.dir(konanTarget.name).file("libtree-sitter-$grammarName.a").asFile - val objectFiles = grammarFiles.map { - grammarDir.resolve(it.nameWithoutExtension + ".o").path - }.toTypedArray() - val loader = PlatformManager(konanHome.get(), false, konanDataDir.orNull).loader(konanTarget) - - doFirst { - if (!File(loader.absoluteTargetToolchain).isDirectory) loader.downloadDependencies() - - val argsFile = File.createTempFile("args", null) - argsFile.deleteOnExit() - argsFile.writer().useToRun { - write("-I" + grammarDir.resolve("src").unixPath + "\n") - write("-DTREE_SITTER_HIDE_SYMBOLS\n") - write("-fvisibility=hidden\n") - write("-std=c11\n") - write("-O2\n") - write("-g\n") - write("-c\n") - grammarFiles.forEach { write(it.unixPath + "\n") } - } - - exec { - executable = runKonan - workingDir = grammarDir - standardOutput = nullOutputStream() - args("clang", "clang", konanTarget.name, "@" + argsFile.path) - } - - exec { - executable = runKonan - workingDir = grammarDir - standardOutput = nullOutputStream() - args("llvm", "llvm-ar", "rcs", libFile.path, *objectFiles) - } - } - - inputs.files(*grammarFiles) - outputs.file(libFile) -} - tasks.withType().configureEach { mustRunAfter(tasks.withType()) } diff --git a/languages/java/src/androidMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt b/languages/java/src/androidMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt deleted file mode 100644 index fc4846f..0000000 --- a/languages/java/src/androidMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.treesitter.ktreesitter.java - -import dalvik.annotation.optimization.CriticalNative - -actual object TreeSitterJava { - init { - System.loadLibrary("ktreesitter-java") - } - - actual fun language(): Any = nativeLanguage() - - @JvmStatic - @CriticalNative - private external fun nativeLanguage(): Long -} diff --git a/languages/java/src/commonMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt b/languages/java/src/commonMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt deleted file mode 100644 index cf8aac1..0000000 --- a/languages/java/src/commonMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.treesitter.ktreesitter.java - -expect object TreeSitterJava { - fun language(): Any -} diff --git a/languages/java/src/jni/binding.c b/languages/java/src/jni/binding.c deleted file mode 100644 index d185162..0000000 --- a/languages/java/src/jni/binding.c +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include - -#ifndef __ANDROID__ -#define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name(JNIEnv * _env, jclass _class) -#else -#define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name() -#endif - -NATIVE_FUNCTION(Java_io_github_treesitter_ktreesitter_java_TreeSitterJava_nativeLanguage) { - return (jlong)tree_sitter_java(); -} diff --git a/languages/java/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt b/languages/java/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt deleted file mode 100644 index 5b2aa8f..0000000 --- a/languages/java/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.treesitter.ktreesitter.java - -import java.io.File.createTempFile - -actual object TreeSitterJava { - private const val LIB_NAME = "ktreesitter-java" - - init { - try { - System.loadLibrary(LIB_NAME) - } catch (ex: UnsatisfiedLinkError) { - @Suppress("UnsafeDynamicallyLoadedCode") - System.load(libPath() ?: throw ex) - } - } - - actual fun language(): Any = nativeLanguage() - - @JvmStatic - @Throws(UnsupportedOperationException::class) - private fun libPath(): String? { - val osName = System.getProperty("os.name")!!.lowercase() - val archName = System.getProperty("os.arch")!!.lowercase() - val ext: String - val os: String - val prefix: String - when { - "windows" in osName -> { - ext = "dll" - os = "windows" - prefix = "" - } - "linux" in osName -> { - ext = "so" - os = "linux" - prefix = "lib" - } - "mac" in osName -> { - ext = "dylib" - os = "macos" - prefix = "lib" - } - else -> { - throw UnsupportedOperationException("Unsupported operating system: $osName") - } - } - val arch = when { - "amd64" in archName || "x86_64" in archName -> "x64" - "aarch64" in archName || "arm64" in archName -> "aarch64" - else -> throw UnsupportedOperationException("Unsupported architecture: $archName") - } - val libUrl = javaClass.getResource("/lib/$os/$arch/$prefix$LIB_NAME.$ext") ?: return null - return createTempFile("$prefix$LIB_NAME", ".$ext").apply { - writeBytes(libUrl.openStream().use { it.readAllBytes() }) - deleteOnExit() - }.path - } - - @JvmStatic - private external fun nativeLanguage(): Long -} diff --git a/languages/java/src/nativeInterop/cinterop/parser.def b/languages/java/src/nativeInterop/cinterop/parser.def deleted file mode 100644 index 84dc205..0000000 --- a/languages/java/src/nativeInterop/cinterop/parser.def +++ /dev/null @@ -1,4 +0,0 @@ -package = io.github.treesitter.ktreesitter.java.internal -headers = tree-sitter-java.h -compilerOpts = -DTREE_SITTER_HIDE_SYMBOLS -staticLibraries = libtree-sitter-java.a diff --git a/languages/java/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt b/languages/java/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt deleted file mode 100644 index ee3059b..0000000 --- a/languages/java/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/java/TreeSitterJava.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.treesitter.ktreesitter.java - -import io.github.treesitter.ktreesitter.java.internal.tree_sitter_java -import kotlinx.cinterop.ExperimentalForeignApi - -@OptIn(ExperimentalForeignApi::class) -actual object TreeSitterJava { - actual fun language(): Any = tree_sitter_java()!! -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 98cf253..56071fc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "ktreesitter" pluginManagement { + includeBuild("ktreesitter-plugin") repositories { google() mavenCentral()