diff --git a/.circleci/config.pkl b/.circleci/config.pkl index 671f044..4a79471 100644 --- a/.circleci/config.pkl +++ b/.circleci/config.pkl @@ -15,7 +15,7 @@ jobs { "checkout" new RunStep { command = """ - LD_LIBRARY_PATH=build/native-lib/ ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check + ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check """ } new StoreTestResults { path = "~/test-results" } diff --git a/.circleci/config.yml b/.circleci/config.yml index 87e3dd5..3057e3b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: steps: - checkout - run: - command: LD_LIBRARY_PATH=build/native-lib/ ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check + command: ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check - store_test_results: path: ~/test-results docker: diff --git a/build.gradle.kts b/build.gradle.kts index 9ef576d..5821351 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,12 +15,14 @@ */ import com.github.gradle.node.npm.task.NpmInstallTask import com.github.gradle.node.task.NodeTask +import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.internal.extensions.stdlib.capitalized import org.gradle.internal.os.OperatingSystem import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated plugins { application + idea alias(libs.plugins.kotlin) alias(libs.plugins.kotlinSerialization) alias(libs.plugins.nodeGradle) @@ -37,6 +39,15 @@ java { val pklCli: Configuration by configurations.creating +val jtreeSitterSources: Configuration by configurations.creating + +val buildInfo = extensions.create("buildInfo", project) + +val jsitterMonkeyPatchSourceDir = layout.buildDirectory.dir("generated/libs/jtreesitter") +val nativeLibDir = layout.buildDirectory.dir("generated/libs/native/") +val treeSitterPklRepoDir = layout.buildDirectory.dir("repos/tree-sitter-pkl") +val treeSitterRepoDir = layout.buildDirectory.dir("repos/tree-sitter") + val osName get(): String { val os = OperatingSystem.current() @@ -70,9 +81,41 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(libs.assertJ) testImplementation(libs.junit.jupiter) + jtreeSitterSources(variantOf(libs.jtreesitter) { classifier("sources") }) pklCli("org.pkl-lang:pkl-cli-$osName-$arch:${libs.versions.pkl.get()}") } +idea { module { generatedSourceDirs.add(jsitterMonkeyPatchSourceDir.get().asFile) } } + +/** + * jtreesitter expects the tree-sitter library to exist in system dirs, or to be provided through + * `java.library.path`. + * + * This patches its source code so that we can control exactly where the tree-sitter library + * resides. + */ +val monkeyPatchTreeSitter by + tasks.registering(Copy::class) { + from(zipTree(jtreeSitterSources.singleFile)) { + include("**/TreeSitter.java") + filter { line -> + when { + line.contains("static final SymbolLookup") -> + "static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup(NativeLibraries.getTreeSitter().getLibraryPath(), LIBRARY_ARENA)" + line.contains("package io.github.treesitter.jtreesitter.internal;") -> + """ + $line + + import org.pkl.lsp.treesitter.NativeLibraries; + """ + .trimIndent() + else -> line + } + } + } + into(jsitterMonkeyPatchSourceDir) + } + val configurePklCliExecutable by tasks.registering { doLast { pklCli.singleFile.setExecutable(true) } } @@ -106,17 +149,12 @@ val javaExecutable by // jvmArgs.addAll("-ea", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") } -val treeSitterPklRepo = layout.buildDirectory.dir("repos/tree-sitter-pkl") -val treeSitterRepo = layout.buildDirectory.dir("repos/tree-sitter") - node { version = libs.versions.node - nodeProjectDir = treeSitterPklRepo + nodeProjectDir = treeSitterPklRepoDir download = true } -private val nativeLibDir = layout.buildDirectory.dir("native-lib") - fun configureRepo( repo: String, simpleRepoName: String, @@ -136,9 +174,6 @@ fun configureRepo( val updateTask = tasks.register("update$taskSuffix") { outputs.dir(repoDir) - outputs.upToDateWhen { - versionFile.get().asFile.let { it.exists() && it.readText() == gitTagOrCommit.get() } - } doLast { exec { workingDir = repoDir.get().asFile @@ -155,6 +190,9 @@ fun configureRepo( dependsOn(cloneTask) dependsOn(updateTask) outputs.dir(repoDir) + outputs.upToDateWhen { + versionFile.get().asFile.let { it.exists() && it.readText() == gitTagOrCommit.get() } + } doLast { versionFile.get().asFile.let { file -> file.ensureParentDirsCreated() @@ -169,7 +207,7 @@ val setupTreeSitterRepo = "git@github.com:tree-sitter/tree-sitter", "treeSitter", libs.versions.treeSitterRepo, - treeSitterRepo, + treeSitterRepoDir, ) val setupTreeSitterPklRepo = @@ -177,19 +215,23 @@ val setupTreeSitterPklRepo = "git@github.com:apple/tree-sitter-pkl", "treeSitterPkl", libs.versions.treeSitterPklRepo, - treeSitterPklRepo, + treeSitterPklRepoDir, ) +// Keep in sync with `org.pkl.lsp.treesitter.NativeLibrary.getResourcePath` +private fun resourceLibraryPath(libraryName: String) = + "NATIVE/org/pkl/lsp/treesitter/$osName-$arch/$libraryName" + val makeTreeSitterLib by tasks.registering(Exec::class) { dependsOn(setupTreeSitterRepo) - workingDir = treeSitterRepo.get().asFile + workingDir = treeSitterRepoDir.get().asFile inputs.dir(workingDir) val libraryName = System.mapLibraryName("tree-sitter") commandLine("make", libraryName) - val outputFile = nativeLibDir.map { it.file(libraryName) } + val outputFile = nativeLibDir.map { it.file(resourceLibraryPath(libraryName)) } outputs.file(outputFile) doLast { workingDir.resolve(libraryName).renameTo(outputFile.get().asFile) } @@ -198,20 +240,20 @@ val makeTreeSitterLib by val npmInstallTreeSitter by tasks.registering(NpmInstallTask::class) { dependsOn(setupTreeSitterPklRepo) - doFirst { workingDir = treeSitterPklRepo.get().asFile } + doFirst { workingDir = treeSitterPklRepoDir.get().asFile } } val makeTreeSitterPklLib by tasks.registering(NodeTask::class) { dependsOn(npmInstallTreeSitter) - inputs.dir(treeSitterPklRepo) - doFirst { workingDir = treeSitterPklRepo.get().asFile } + inputs.dir(treeSitterPklRepoDir) + doFirst { workingDir = treeSitterPklRepoDir.get().asFile } val libraryName = System.mapLibraryName("tree-sitter-pkl") - val outputFile = nativeLibDir.map { it.file(libraryName) } + val outputFile = nativeLibDir.map { it.file(resourceLibraryPath(libraryName)) } - script.set(treeSitterPklRepo.get().asFile.resolve("node_modules/.bin/tree-sitter")) + script.set(treeSitterPklRepoDir.get().asFile.resolve("node_modules/.bin/tree-sitter")) args = listOf("build", "--output", outputFile.get().asFile.absolutePath) outputs.file(outputFile) @@ -220,9 +262,30 @@ val makeTreeSitterPklLib by tasks.processResources { dependsOn(makeTreeSitterLib) dependsOn(makeTreeSitterPklLib) + // tree-sitter's CLI always generates debug symbols when on version 0.22. + // we can remove this when tree-sitter-pkl upgrades the tree-sitter-cli dependency to 0.23 or + // newer. + exclude("**/*.dSYM/**") + filesMatching("org/pkl/lsp/Release.properties") { + filter( + "tokens" to + mapOf( + "version" to buildInfo.pklLspVersion, + "treeSitterVersion" to libs.versions.treeSitterRepo.get(), + "treeSitterPklVersion" to libs.versions.treeSitterPklRepo.get(), + ) + ) + } } -sourceSets { main { resources { srcDirs(nativeLibDir) } } } +tasks.compileKotlin { dependsOn(monkeyPatchTreeSitter) } + +sourceSets { + main { + java { srcDirs(jsitterMonkeyPatchSourceDir) } + resources { srcDirs(nativeLibDir) } + } +} private val licenseHeader = """ @@ -256,39 +319,3 @@ spotless { licenseHeader(licenseHeader) } } - -/** - * Builds a self-contained Pkl LSP CLI Jar that is directly executable on *nix and executable with - * `java -jar` on Windows. - * - * For direct execution, the `java` command must be on the PATH. - * - * https://skife.org/java/unix/2011/06/20/really_executable_jars.html - */ -abstract class ExecutableJar : DefaultTask() { - @get:InputFile abstract val inJar: RegularFileProperty - - @get:OutputFile abstract val outJar: RegularFileProperty - - @get:Input abstract val jvmArgs: ListProperty - - @TaskAction - fun buildJar() { - val inFile = inJar.get().asFile - val outFile = outJar.get().asFile - val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" } - val startScript = - """ - #!/bin/sh - exec java $escapedJvmArgs -jar $0 "$@" - """ - .trimIndent() + "\n\n\n" - outFile.outputStream().use { outStream -> - startScript.byteInputStream().use { it.copyTo(outStream) } - inFile.inputStream().use { it.copyTo(outStream) } - } - - // chmod a+x - outFile.setExecutable(true, false) - } -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..57ee309 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,16 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { `kotlin-dsl` } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..11ebbed --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,37 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("UnstableApiUsage") + +rootProject.name = "buildSrc" + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +// makes ~/.gradle/init.gradle unnecessary and ~/.gradle/gradle.properties optional +dependencyResolutionManagement { + // use same version catalog as main build + versionCatalogs { register("libs") { from(files("../gradle/libs.versions.toml")) } } + + repositories { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + mavenCentral() + gradlePluginPortal() + } +} diff --git a/buildSrc/src/main/kotlin/BuildInfo.kt b/buildSrc/src/main/kotlin/BuildInfo.kt new file mode 100644 index 0000000..091f262 --- /dev/null +++ b/buildSrc/src/main/kotlin/BuildInfo.kt @@ -0,0 +1,81 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("MemberVisibilityCanBePrivate") + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType +import org.gradle.internal.os.OperatingSystem + +// `buildInfo` in main build scripts +// `project.extensions.getByType()` in precompiled script plugins +open class BuildInfo(project: Project) { + val isCiBuild: Boolean by lazy { System.getenv("CI") != null } + + val isReleaseBuild: Boolean by lazy { java.lang.Boolean.getBoolean("releaseBuild") } + + val os: OperatingSystem by lazy { + OperatingSystem.current() + } + + // could be `commitId: Provider = project.provider { ... }` + val commitId: String by lazy { + // only run command once per build invocation + if (project === project.rootProject) { + val process = + ProcessBuilder() + .command("git", "rev-parse", "--short", "HEAD") + .directory(project.rootDir) + .start() + process.waitFor().also { exitCode -> + if (exitCode == -1) throw RuntimeException(process.errorStream.reader().readText()) + } + process.inputStream.reader().readText().trim() + } else { + project.rootProject.extensions.getByType(BuildInfo::class.java).commitId + } + } + + val commitish: String by lazy { if (isReleaseBuild) project.version.toString() else commitId } + + val pklLspVersion: String by lazy { + if (isReleaseBuild) { + project.version.toString() + } else { + project.version.toString().replace("-SNAPSHOT", "-dev+$commitId") + } + } + + val pklLspVersionNonUnique: String by lazy { + if (isReleaseBuild) { + project.version.toString() + } else { + project.version.toString().replace("-SNAPSHOT", "-dev") + } + } + + // https://melix.github.io/blog/2021/03/version-catalogs-faq.html#_but_how_can_i_use_the_catalog_in_em_plugins_em_defined_in_code_buildsrc_code + val libs: VersionCatalog by lazy { + project.extensions.getByType().named("libs") + } + + init { + if (!isReleaseBuild) { + project.version = "${project.version}-SNAPSHOT" + } + } +} diff --git a/buildSrc/src/main/kotlin/ExecutableJar.kt b/buildSrc/src/main/kotlin/ExecutableJar.kt new file mode 100644 index 0000000..c763ef8 --- /dev/null +++ b/buildSrc/src/main/kotlin/ExecutableJar.kt @@ -0,0 +1,58 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +/** + * Builds a self-contained Pkl CLI Jar that is directly executable on *nix and executable with `java + * -jar` on Windows. + * + * For direct execution, the `java` command must be on the PATH. + * + * https://skife.org/java/unix/2011/06/20/really_executable_jars.html + */ +abstract class ExecutableJar : DefaultTask() { + @get:InputFile abstract val inJar: RegularFileProperty + + @get:OutputFile abstract val outJar: RegularFileProperty + + @get:Input abstract val jvmArgs: ListProperty + + @TaskAction + fun buildJar() { + val inFile = inJar.get().asFile + val outFile = outJar.get().asFile + val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" } + val startScript = + """ + #!/bin/sh + exec java $escapedJvmArgs -jar $0 "$@" + """ + .trimIndent() + "\n\n\n" + outFile.outputStream().use { outStream -> + startScript.byteInputStream().use { it.copyTo(outStream) } + inFile.inputStream().use { it.copyTo(outStream) } + } + + // chmod a+x + outFile.setExecutable(true, false) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..54f542d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +group=org.pkl-lang +version=0.1.0 diff --git a/src/main/kotlin/org/pkl/lsp/LSPUtil.kt b/src/main/kotlin/org/pkl/lsp/LSPUtil.kt index 25f62cb..e6dcdad 100644 --- a/src/main/kotlin/org/pkl/lsp/LSPUtil.kt +++ b/src/main/kotlin/org/pkl/lsp/LSPUtil.kt @@ -197,7 +197,10 @@ data class Package1CacheDir(override val file: Path) : CacheDir { } } -val pklCacheDir: Path = Path.of(System.getProperty("user.home")).resolve(".pkl/cache") +val homeDir: Path + get() = Path.of(System.getProperty("user.home") ?: throw AssertionError("Cannot find home dir")) + +val pklCacheDir: Path = homeDir.resolve(".pkl/cache") val packages2CacheDir: CacheDir get() = Package2CacheDir(pklCacheDir.resolve("package-2")) diff --git a/src/main/kotlin/org/pkl/lsp/Project.kt b/src/main/kotlin/org/pkl/lsp/Project.kt index ff5c4a1..6dbb68f 100644 --- a/src/main/kotlin/org/pkl/lsp/Project.kt +++ b/src/main/kotlin/org/pkl/lsp/Project.kt @@ -16,13 +16,12 @@ package org.pkl.lsp import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.starProjectedType import org.pkl.lsp.services.* +import org.pkl.lsp.treesitter.PklParser import org.pkl.lsp.util.CachedValuesManager class Project(private val server: PklLSPServer) { @@ -54,8 +53,7 @@ class Project(private val server: PklLSPServer) { val clientCapabilities: PklClientCapabilities by lazy { server.clientCapabilities } - // The thread where all tree-sitter allocations happen - val astExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() } + val pklParser: PklParser by lazy { PklParser(this) } fun initialize(): CompletableFuture<*> { return CompletableFuture.allOf(*myComponents.map { it.initialize() }.toTypedArray()) diff --git a/src/main/kotlin/org/pkl/lsp/Release.kt b/src/main/kotlin/org/pkl/lsp/Release.kt index eafd34a..6752d1f 100644 --- a/src/main/kotlin/org/pkl/lsp/Release.kt +++ b/src/main/kotlin/org/pkl/lsp/Release.kt @@ -31,4 +31,8 @@ object Release { } val version: String by lazy { properties.getProperty("version") ?: "unknown" } + + val treeSitterVersion: String by lazy { properties.getProperty("treeSitterVersion") } + + val treeSitterPklVersion: String by lazy { properties.getProperty("treeSitterPklVersion") } } diff --git a/src/main/kotlin/org/pkl/lsp/VirtualFile.kt b/src/main/kotlin/org/pkl/lsp/VirtualFile.kt index 5c91148..df98b7d 100644 --- a/src/main/kotlin/org/pkl/lsp/VirtualFile.kt +++ b/src/main/kotlin/org/pkl/lsp/VirtualFile.kt @@ -30,7 +30,6 @@ import org.pkl.lsp.packages.PackageDependency import org.pkl.lsp.packages.dto.PackageMetadata import org.pkl.lsp.packages.dto.PklProject import org.pkl.lsp.services.PklProjectManager.Companion.PKL_PROJECT_FILENAME -import org.pkl.lsp.treesitter.PklParser import org.pkl.lsp.util.CachedValue import org.pkl.lsp.util.ModificationTracker @@ -140,12 +139,10 @@ sealed class BaseFile : VirtualFile { private var myContents: String? = null - private val parser = PklParser() - private fun doBuildModule(): PklModule? { return try { logger.log("building $uri") - val moduleCtx = parser.parse(contents, project.astExecutor) + val moduleCtx = project.pklParser.parse(contents) if (readError != null) { readError = null } diff --git a/src/main/kotlin/org/pkl/lsp/cli/Main.kt b/src/main/kotlin/org/pkl/lsp/cli/Main.kt index bc19d8e..3f2b572 100644 --- a/src/main/kotlin/org/pkl/lsp/cli/Main.kt +++ b/src/main/kotlin/org/pkl/lsp/cli/Main.kt @@ -17,23 +17,6 @@ package org.pkl.lsp.cli -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - internal fun main(args: Array) { - extractSharedLibs() LspCommand().main(args) } - -// Java can't find shared libs inside a jar, so we have to extract it to CWD. -private fun extractSharedLibs() { - // TODO: set this to `.pkl/yada` - val libs = listOf("tree-sitter", "tree-sitter-pkl").map(System::mapLibraryName) - libs.forEach { lib -> - val stream = - LspCommand::class.java.getResourceAsStream("/$lib") - ?: throw RuntimeException("Could not find `$lib` library") - stream.use { Files.copy(it, Path.of("./$lib"), StandardCopyOption.REPLACE_EXISTING) } - } -} diff --git a/src/main/kotlin/org/pkl/lsp/treesitter/NativeLibrary.kt b/src/main/kotlin/org/pkl/lsp/treesitter/NativeLibrary.kt new file mode 100644 index 0000000..0e85794 --- /dev/null +++ b/src/main/kotlin/org/pkl/lsp/treesitter/NativeLibrary.kt @@ -0,0 +1,64 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.lsp.treesitter + +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.toPath +import org.pkl.lsp.Release +import org.pkl.lsp.homeDir +import org.pkl.lsp.util.OS + +data class NativeLibrary(val name: String, val version: String) { + companion object { + private val nativeLibsDir by lazy { homeDir.resolve(".pkl/editor-support/native-libs") } + } + + private val systemLibraryName = System.mapLibraryName(name) + + private val resourcePath: Path by lazy { + // keep in sync with `resourceLibraryPath` in build.gradle.kts + val path = "/NATIVE/org/pkl/lsp/treesitter/${OS.name}-${OS.arch}/$systemLibraryName" + NativeLibrary::class.java.getResource(path)?.toURI()?.toPath() + ?: throw AssertionError("Cannot find resource in classpath: $path") + } + + private val storedLibraryPath: Path by lazy { + nativeLibsDir.resolve("$name/$version/$systemLibraryName") + } + + val libraryPath: Path by lazy { + when { + // optimization: if the resource file is a normal file, we can use it directly. + resourcePath.fileSystem == FileSystems.getDefault() -> resourcePath + storedLibraryPath.exists() -> storedLibraryPath + else -> { + storedLibraryPath.createParentDirectories() + Files.copy(resourcePath, storedLibraryPath) + storedLibraryPath + } + } + } +} + +object NativeLibraries { + @JvmStatic val treeSitter = NativeLibrary("tree-sitter", Release.treeSitterVersion) + + @JvmStatic val treeSitterPkl = NativeLibrary("tree-sitter-pkl", Release.treeSitterPklVersion) +} diff --git a/src/main/kotlin/org/pkl/lsp/treesitter/PklParser.kt b/src/main/kotlin/org/pkl/lsp/treesitter/PklParser.kt index 562a788..f62158b 100644 --- a/src/main/kotlin/org/pkl/lsp/treesitter/PklParser.kt +++ b/src/main/kotlin/org/pkl/lsp/treesitter/PklParser.kt @@ -20,12 +20,16 @@ import io.github.treesitter.jtreesitter.Language import io.github.treesitter.jtreesitter.Parser import io.github.treesitter.jtreesitter.Tree import java.util.concurrent.Callable -import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import org.pkl.lsp.Component +import org.pkl.lsp.Project import org.pkl.lsp.ast.TreeSitterNode /** A Pkl parser using tree-sitter-pkl */ -class PklParser { - fun parse(text: String, executor: ExecutorService, oldAst: Tree? = null): TreeSitterNode { +class PklParser(project: Project) : Component(project) { + private val executor by lazy { Executors.newSingleThreadExecutor() } + + fun parse(text: String, oldAst: Tree? = null): TreeSitterNode { return executor .submit( Callable { diff --git a/src/main/kotlin/org/pkl/lsp/treesitter/TreeSitterPkl.kt b/src/main/kotlin/org/pkl/lsp/treesitter/TreeSitterPkl.kt index 9db2999..71a9d54 100644 --- a/src/main/kotlin/org/pkl/lsp/treesitter/TreeSitterPkl.kt +++ b/src/main/kotlin/org/pkl/lsp/treesitter/TreeSitterPkl.kt @@ -15,16 +15,10 @@ */ package org.pkl.lsp.treesitter -import java.lang.foreign.Arena -import java.lang.foreign.FunctionDescriptor -import java.lang.foreign.Linker -import java.lang.foreign.MemoryLayout -import java.lang.foreign.MemorySegment -import java.lang.foreign.SymbolLookup -import java.lang.foreign.ValueLayout +import java.lang.foreign.* +import org.pkl.lsp.treesitter.NativeLibraries.treeSitterPkl class TreeSitterPkl { - companion object { @JvmStatic private val VOID_PTR: ValueLayout = @@ -46,9 +40,10 @@ class TreeSitterPkl { } private val arena: Arena = Arena.ofAuto() - private val library: String = System.mapLibraryName("tree-sitter-pkl") - private val symbols: SymbolLookup = - SymbolLookup.libraryLookup(library, arena).or(SymbolLookup.loaderLookup()) + + private val symbols: SymbolLookup + get() = + SymbolLookup.libraryLookup(treeSitterPkl.libraryPath, arena).or(SymbolLookup.loaderLookup()) @Suppress("SameParameterValue") private fun call(name: String): MemorySegment { diff --git a/src/main/kotlin/org/pkl/lsp/util/OS.kt b/src/main/kotlin/org/pkl/lsp/util/OS.kt new file mode 100644 index 0000000..2acb5a5 --- /dev/null +++ b/src/main/kotlin/org/pkl/lsp/util/OS.kt @@ -0,0 +1,50 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.lsp.util + +import java.util.* + +object OS { + private val osNameProperty + get() = System.getProperty("os.name")?.lowercase(Locale.ROOT) ?: "UNKNOWN" + + private val osArchProperty + get() = System.getProperty("os.arch")?.lowercase(Locale.ROOT) ?: "UNKNOWN" + + /** Same logic as `org.gradle.internal.os.OperatingSystem.forName` */ + val name + get(): String { + return when { + osNameProperty.contains("mac os x") || + osNameProperty.contains("darwin") || + osNameProperty.contains("osx") -> "macos" + osNameProperty.contains("linux") -> "linux" + osNameProperty.contains("windows") -> "windows" + else -> throw RuntimeException("OS $osNameProperty is not supported") + } + } + + /** Same logic as `org.gradle.internal.os.OperatingSystem#arch` */ + val arch: String + get() { + return when (osArchProperty) { + "x86" -> "i386" + "x86_64" -> "amd64" + "powerpc" -> "ppc" + else -> osArchProperty + } + } +} diff --git a/src/main/resources/org/pkl/lsp/Release.properties b/src/main/resources/org/pkl/lsp/Release.properties index ce0c6a9..e95f5fc 100644 --- a/src/main/resources/org/pkl/lsp/Release.properties +++ b/src/main/resources/org/pkl/lsp/Release.properties @@ -1 +1,3 @@ -version=0.1.0 \ No newline at end of file +version=@version@ +treeSitterPklVersion=@treeSitterPklVersion@ +treeSitterVersion=@treeSitterVersion@ \ No newline at end of file diff --git a/src/test/kotlin/org/pkl/lsp/LSPTestBase.kt b/src/test/kotlin/org/pkl/lsp/LSPTestBase.kt index 1d464a2..9c9756c 100644 --- a/src/test/kotlin/org/pkl/lsp/LSPTestBase.kt +++ b/src/test/kotlin/org/pkl/lsp/LSPTestBase.kt @@ -29,19 +29,16 @@ import org.junit.jupiter.api.io.TempDir import org.pkl.lsp.ast.PklModule import org.pkl.lsp.ast.PklNode import org.pkl.lsp.ast.findBySpan -import org.pkl.lsp.treesitter.PklParser abstract class LSPTestBase { companion object { private lateinit var server: PklLSPServer - private lateinit var parser: PklParser internal lateinit var fakeProject: Project @JvmStatic @BeforeAll fun beforeAll() { server = PklLSPServer(true).also { it.connect(TestLanguageClient) } - parser = PklParser() fakeProject = server.project System.getProperty("pklExecutable")?.let { executablePath -> TestLanguageClient.settings["Pkl" to "pkl.cli.path"] = executablePath diff --git a/src/test/kotlin/org/pkl/lsp/ParserTest.kt b/src/test/kotlin/org/pkl/lsp/ParserTest.kt index 829cbe0..079210d 100644 --- a/src/test/kotlin/org/pkl/lsp/ParserTest.kt +++ b/src/test/kotlin/org/pkl/lsp/ParserTest.kt @@ -20,12 +20,11 @@ import java.util.concurrent.Executors import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.pkl.lsp.ast.* -import org.pkl.lsp.treesitter.PklParser class ParserTest { - private val parser = PklParser() private val project = Project(PklLSPServer(true)) + private val parser = project.pklParser @Test fun `parse types`() { @@ -198,7 +197,7 @@ class ParserTest { } private fun parse(text: String): PklModule { - val node = parser.parse(text, project.astExecutor) + val node = parser.parse(text) return PklModuleImpl(node, FsFile(Path.of("."), project)) } }