Skip to content

Commit fc73ec0

Browse files
committed
Improve handling of native libraries
This implements changes to put native libs in `~/.pkl/editor-support/`. When loading the parser, it first looks for libraries there, and if they don't exist, they get copied from bundled resources. When building libraries, the os name and architecture get included in the resource path. This enables starting the jar plainly without needing any extra argument (e.g. no need for -Djava.library.path), and eliminates the need to copy native libraries to the current working dir.
1 parent a1967a9 commit fc73ec0

File tree

20 files changed

+420
-104
lines changed

20 files changed

+420
-104
lines changed

.circleci/config.pkl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs {
1515
"checkout"
1616
new RunStep {
1717
command = """
18-
LD_LIBRARY_PATH=build/native-lib/ ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check
18+
./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check
1919
"""
2020
}
2121
new StoreTestResults { path = "~/test-results" }

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
steps:
88
- checkout
99
- run:
10-
command: LD_LIBRARY_PATH=build/native-lib/ ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check
10+
command: ./gradlew --info --stacktrace -DtestReportsDir="${HOME}/test-results" check
1111
- store_test_results:
1212
path: ~/test-results
1313
docker:

build.gradle.kts

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
*/
1616
import com.github.gradle.node.npm.task.NpmInstallTask
1717
import com.github.gradle.node.task.NodeTask
18+
import org.apache.tools.ant.filters.ReplaceTokens
1819
import org.gradle.internal.extensions.stdlib.capitalized
1920
import org.gradle.internal.os.OperatingSystem
2021
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
2122

2223
plugins {
2324
application
25+
idea
2426
alias(libs.plugins.kotlin)
2527
alias(libs.plugins.kotlinSerialization)
2628
alias(libs.plugins.nodeGradle)
@@ -37,6 +39,15 @@ java {
3739

3840
val pklCli: Configuration by configurations.creating
3941

42+
val jtreeSitterSources: Configuration by configurations.creating
43+
44+
val buildInfo = extensions.create<BuildInfo>("buildInfo", project)
45+
46+
val jsitterMonkeyPatchSourceDir = layout.buildDirectory.dir("generated/libs/jtreesitter")
47+
val nativeLibDir = layout.buildDirectory.dir("generated/libs/native/")
48+
val treeSitterPklRepoDir = layout.buildDirectory.dir("repos/tree-sitter-pkl")
49+
val treeSitterRepoDir = layout.buildDirectory.dir("repos/tree-sitter")
50+
4051
val osName
4152
get(): String {
4253
val os = OperatingSystem.current()
@@ -70,9 +81,41 @@ dependencies {
7081
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
7182
testImplementation(libs.assertJ)
7283
testImplementation(libs.junit.jupiter)
84+
jtreeSitterSources(variantOf(libs.jtreesitter) { classifier("sources") })
7385
pklCli("org.pkl-lang:pkl-cli-$osName-$arch:${libs.versions.pkl.get()}")
7486
}
7587

88+
idea { module { generatedSourceDirs.add(jsitterMonkeyPatchSourceDir.get().asFile) } }
89+
90+
/**
91+
* jtreesitter expects the tree-sitter library to exist in system dirs, or to be provided through
92+
* `java.library.path`.
93+
*
94+
* This patches its source code so that we can control exactly where the tree-sitter library
95+
* resides.
96+
*/
97+
val monkeyPatchTreeSitter by
98+
tasks.registering(Copy::class) {
99+
from(zipTree(jtreeSitterSources.singleFile)) {
100+
include("**/TreeSitter.java")
101+
filter { line ->
102+
when {
103+
line.contains("static final SymbolLookup") ->
104+
"static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup(NativeLibraries.getTreeSitter().getLibraryPath(), LIBRARY_ARENA)"
105+
line.contains("package io.github.treesitter.jtreesitter.internal;") ->
106+
"""
107+
$line
108+
109+
import org.pkl.lsp.treesitter.NativeLibraries;
110+
"""
111+
.trimIndent()
112+
else -> line
113+
}
114+
}
115+
}
116+
into(jsitterMonkeyPatchSourceDir)
117+
}
118+
76119
val configurePklCliExecutable by
77120
tasks.registering { doLast { pklCli.singleFile.setExecutable(true) } }
78121

@@ -106,17 +149,12 @@ val javaExecutable by
106149
// jvmArgs.addAll("-ea", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")
107150
}
108151

109-
val treeSitterPklRepo = layout.buildDirectory.dir("repos/tree-sitter-pkl")
110-
val treeSitterRepo = layout.buildDirectory.dir("repos/tree-sitter")
111-
112152
node {
113153
version = libs.versions.node
114-
nodeProjectDir = treeSitterPklRepo
154+
nodeProjectDir = treeSitterPklRepoDir
115155
download = true
116156
}
117157

118-
private val nativeLibDir = layout.buildDirectory.dir("native-lib")
119-
120158
fun configureRepo(
121159
repo: String,
122160
simpleRepoName: String,
@@ -136,9 +174,6 @@ fun configureRepo(
136174
val updateTask =
137175
tasks.register("update$taskSuffix") {
138176
outputs.dir(repoDir)
139-
outputs.upToDateWhen {
140-
versionFile.get().asFile.let { it.exists() && it.readText() == gitTagOrCommit.get() }
141-
}
142177
doLast {
143178
exec {
144179
workingDir = repoDir.get().asFile
@@ -155,6 +190,9 @@ fun configureRepo(
155190
dependsOn(cloneTask)
156191
dependsOn(updateTask)
157192
outputs.dir(repoDir)
193+
outputs.upToDateWhen {
194+
versionFile.get().asFile.let { it.exists() && it.readText() == gitTagOrCommit.get() }
195+
}
158196
doLast {
159197
versionFile.get().asFile.let { file ->
160198
file.ensureParentDirsCreated()
@@ -169,27 +207,31 @@ val setupTreeSitterRepo =
169207
"[email protected]:tree-sitter/tree-sitter",
170208
"treeSitter",
171209
libs.versions.treeSitterRepo,
172-
treeSitterRepo,
210+
treeSitterRepoDir,
173211
)
174212

175213
val setupTreeSitterPklRepo =
176214
configureRepo(
177215
"[email protected]:apple/tree-sitter-pkl",
178216
"treeSitterPkl",
179217
libs.versions.treeSitterPklRepo,
180-
treeSitterPklRepo,
218+
treeSitterPklRepoDir,
181219
)
182220

221+
// Keep in sync with `org.pkl.lsp.treesitter.NativeLibrary.getResourcePath`
222+
private fun resourceLibraryPath(libraryName: String) =
223+
"NATIVE/org/pkl/lsp/treesitter/$osName-$arch/$libraryName"
224+
183225
val makeTreeSitterLib by
184226
tasks.registering(Exec::class) {
185227
dependsOn(setupTreeSitterRepo)
186-
workingDir = treeSitterRepo.get().asFile
228+
workingDir = treeSitterRepoDir.get().asFile
187229
inputs.dir(workingDir)
188230

189231
val libraryName = System.mapLibraryName("tree-sitter")
190232
commandLine("make", libraryName)
191233

192-
val outputFile = nativeLibDir.map { it.file(libraryName) }
234+
val outputFile = nativeLibDir.map { it.file(resourceLibraryPath(libraryName)) }
193235
outputs.file(outputFile)
194236

195237
doLast { workingDir.resolve(libraryName).renameTo(outputFile.get().asFile) }
@@ -198,20 +240,20 @@ val makeTreeSitterLib by
198240
val npmInstallTreeSitter by
199241
tasks.registering(NpmInstallTask::class) {
200242
dependsOn(setupTreeSitterPklRepo)
201-
doFirst { workingDir = treeSitterPklRepo.get().asFile }
243+
doFirst { workingDir = treeSitterPklRepoDir.get().asFile }
202244
}
203245

204246
val makeTreeSitterPklLib by
205247
tasks.registering(NodeTask::class) {
206248
dependsOn(npmInstallTreeSitter)
207-
inputs.dir(treeSitterPklRepo)
208-
doFirst { workingDir = treeSitterPklRepo.get().asFile }
249+
inputs.dir(treeSitterPklRepoDir)
250+
doFirst { workingDir = treeSitterPklRepoDir.get().asFile }
209251

210252
val libraryName = System.mapLibraryName("tree-sitter-pkl")
211253

212-
val outputFile = nativeLibDir.map { it.file(libraryName) }
254+
val outputFile = nativeLibDir.map { it.file(resourceLibraryPath(libraryName)) }
213255

214-
script.set(treeSitterPklRepo.get().asFile.resolve("node_modules/.bin/tree-sitter"))
256+
script.set(treeSitterPklRepoDir.get().asFile.resolve("node_modules/.bin/tree-sitter"))
215257
args = listOf("build", "--output", outputFile.get().asFile.absolutePath)
216258

217259
outputs.file(outputFile)
@@ -220,9 +262,30 @@ val makeTreeSitterPklLib by
220262
tasks.processResources {
221263
dependsOn(makeTreeSitterLib)
222264
dependsOn(makeTreeSitterPklLib)
265+
// tree-sitter's CLI always generates debug symbols when on version 0.22.
266+
// we can remove this when tree-sitter-pkl upgrades the tree-sitter-cli dependency to 0.23 or
267+
// newer.
268+
exclude("**/*.dSYM/**")
269+
filesMatching("org/pkl/lsp/Release.properties") {
270+
filter<ReplaceTokens>(
271+
"tokens" to
272+
mapOf(
273+
"version" to buildInfo.pklLspVersion,
274+
"treeSitterVersion" to libs.versions.treeSitterRepo.get(),
275+
"treeSitterPklVersion" to libs.versions.treeSitterPklRepo.get(),
276+
)
277+
)
278+
}
223279
}
224280

225-
sourceSets { main { resources { srcDirs(nativeLibDir) } } }
281+
tasks.compileKotlin { dependsOn(monkeyPatchTreeSitter) }
282+
283+
sourceSets {
284+
main {
285+
java { srcDirs(jsitterMonkeyPatchSourceDir) }
286+
resources { srcDirs(nativeLibDir) }
287+
}
288+
}
226289

227290
private val licenseHeader =
228291
"""
@@ -256,39 +319,3 @@ spotless {
256319
licenseHeader(licenseHeader)
257320
}
258321
}
259-
260-
/**
261-
* Builds a self-contained Pkl LSP CLI Jar that is directly executable on *nix and executable with
262-
* `java -jar` on Windows.
263-
*
264-
* For direct execution, the `java` command must be on the PATH.
265-
*
266-
* https://skife.org/java/unix/2011/06/20/really_executable_jars.html
267-
*/
268-
abstract class ExecutableJar : DefaultTask() {
269-
@get:InputFile abstract val inJar: RegularFileProperty
270-
271-
@get:OutputFile abstract val outJar: RegularFileProperty
272-
273-
@get:Input abstract val jvmArgs: ListProperty<String>
274-
275-
@TaskAction
276-
fun buildJar() {
277-
val inFile = inJar.get().asFile
278-
val outFile = outJar.get().asFile
279-
val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" }
280-
val startScript =
281-
"""
282-
#!/bin/sh
283-
exec java $escapedJvmArgs -jar $0 "$@"
284-
"""
285-
.trimIndent() + "\n\n\n"
286-
outFile.outputStream().use { outStream ->
287-
startScript.byteInputStream().use { it.copyTo(outStream) }
288-
inFile.inputStream().use { it.copyTo(outStream) }
289-
}
290-
291-
// chmod a+x
292-
outFile.setExecutable(true, false)
293-
}
294-
}

buildSrc/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
plugins { `kotlin-dsl` }

buildSrc/settings.gradle.kts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:Suppress("UnstableApiUsage")
17+
18+
rootProject.name = "buildSrc"
19+
20+
pluginManagement {
21+
repositories {
22+
mavenCentral()
23+
gradlePluginPortal()
24+
}
25+
}
26+
27+
// makes ~/.gradle/init.gradle unnecessary and ~/.gradle/gradle.properties optional
28+
dependencyResolutionManagement {
29+
// use same version catalog as main build
30+
versionCatalogs { register("libs") { from(files("../gradle/libs.versions.toml")) } }
31+
32+
repositories {
33+
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
34+
mavenCentral()
35+
gradlePluginPortal()
36+
}
37+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:Suppress("MemberVisibilityCanBePrivate")
17+
18+
import org.gradle.api.Project
19+
import org.gradle.api.artifacts.VersionCatalog
20+
import org.gradle.api.artifacts.VersionCatalogsExtension
21+
import org.gradle.kotlin.dsl.getByType
22+
23+
// `buildInfo` in main build scripts
24+
// `project.extensions.getByType<BuildInfo>()` in precompiled script plugins
25+
open class BuildInfo(project: Project) {
26+
val isCiBuild: Boolean by lazy { System.getenv("CI") != null }
27+
28+
val isReleaseBuild: Boolean by lazy { java.lang.Boolean.getBoolean("releaseBuild") }
29+
30+
val os: org.gradle.internal.os.OperatingSystem by lazy {
31+
org.gradle.internal.os.OperatingSystem.current()
32+
}
33+
34+
// could be `commitId: Provider<String> = project.provider { ... }`
35+
val commitId: String by lazy {
36+
// only run command once per build invocation
37+
if (project === project.rootProject) {
38+
val process =
39+
ProcessBuilder()
40+
.command("git", "rev-parse", "--short", "HEAD")
41+
.directory(project.rootDir)
42+
.start()
43+
process.waitFor().also { exitCode ->
44+
if (exitCode == -1) throw RuntimeException(process.errorStream.reader().readText())
45+
}
46+
process.inputStream.reader().readText().trim()
47+
} else {
48+
project.rootProject.extensions.getByType(BuildInfo::class.java).commitId
49+
}
50+
}
51+
52+
val commitish: String by lazy { if (isReleaseBuild) project.version.toString() else commitId }
53+
54+
val pklLspVersion: String by lazy {
55+
if (isReleaseBuild) {
56+
project.version.toString()
57+
} else {
58+
project.version.toString().replace("-SNAPSHOT", "-dev+$commitId")
59+
}
60+
}
61+
62+
val pklLspVersionNonUnique: String by lazy {
63+
if (isReleaseBuild) {
64+
project.version.toString()
65+
} else {
66+
project.version.toString().replace("-SNAPSHOT", "-dev")
67+
}
68+
}
69+
70+
// 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
71+
val libs: VersionCatalog by lazy {
72+
project.extensions.getByType<VersionCatalogsExtension>().named("libs")
73+
}
74+
75+
init {
76+
if (!isReleaseBuild) {
77+
project.version = "${project.version}-SNAPSHOT"
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)