Skip to content

Commit c4e90c4

Browse files
committed
Add intermediate step between SymbolLookup.libraryLookup and System.loadLibrary, that attempts to find library bundle inside jtreesitter jar file
1 parent 25cf626 commit c4e90c4

File tree

1 file changed

+119
-4
lines changed

1 file changed

+119
-4
lines changed

src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package io.github.treesitter.jtreesitter.internal;
22

33
import io.github.treesitter.jtreesitter.NativeLibraryLookup;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
47
import java.lang.foreign.Arena;
58
import java.lang.foreign.Linker;
69
import java.lang.foreign.SymbolLookup;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.StandardCopyOption;
13+
import java.util.Locale;
714
import java.util.Optional;
815
import java.util.ServiceLoader;
916

@@ -31,12 +38,120 @@ private static SymbolLookup findLibrary(Arena arena) {
3138
return SymbolLookup.libraryLookup(library, arena);
3239
} catch (IllegalArgumentException ex1) {
3340
try {
34-
System.loadLibrary("tree-sitter");
35-
return SymbolLookup.loaderLookup();
41+
findLibraryBundledInJar("tree-sitter");
3642
} catch (UnsatisfiedLinkError ex2) {
37-
ex1.addSuppressed(ex2);
38-
throw ex1;
43+
try {
44+
System.loadLibrary("tree-sitter");
45+
} catch (UnsatisfiedLinkError ex3) {
46+
ex2.addSuppressed(ex3);
47+
ex1.addSuppressed(ex2);
48+
throw ex1;
49+
}
50+
}
51+
return SymbolLookup.loaderLookup();
52+
}
53+
}
54+
55+
private static void findLibraryBundledInJar(String libBaseName) throws UnsatisfiedLinkError {
56+
/*
57+
* Strategy:
58+
* 1) Resolve os & arch and compute candidate resource names
59+
* 2) Try to locate resource inside JAR with several common layouts
60+
* 3) Extract to temp file and System.load it
61+
*/
62+
63+
final String mappedName = System.mapLibraryName(libBaseName); // platform-native file name (libtree-sitter.so, tree-sitter.dll, ...)
64+
final String os = detectOs();
65+
final String arch = detectArch();
66+
final String ext = extractExtension(mappedName); // ".so" or ".dll" or ".dylib"
67+
68+
// Candidate resource paths inside the JAR. Adapt these to however you pack native libs.
69+
String[] candidates = new String[] {
70+
// platform-specific directories (most specific)
71+
"/natives/" + os + "-" + arch + "/" + mappedName,
72+
"/natives/" + arch + "/" + mappedName,
73+
"/native/" + os + "-" + arch + "/" + mappedName,
74+
"/native/" + arch + "/" + mappedName,
75+
// less specific
76+
"/natives/" + mappedName,
77+
"/native/" + mappedName,
78+
// fallback: just the file at root of jar (not recommended but sometimes used)
79+
"/" + mappedName
80+
};
81+
82+
InputStream foundStream = null;
83+
String foundResource = null;
84+
for (String candidate : candidates) {
85+
InputStream is = ChainedLibraryLookup.class.getResourceAsStream(candidate);
86+
if (is != null) {
87+
foundStream = is;
88+
foundResource = candidate;
89+
break;
90+
}
91+
}
92+
93+
if (foundStream == null) {
94+
// helpful message mentioning what we tried
95+
String tried = String.join(", ", candidates);
96+
throw new UnsatisfiedLinkError("Could not find bundled native library resource for '"
97+
+ libBaseName + "'. Tried: " + tried);
98+
}
99+
100+
// Create temp file and copy resource contents
101+
Path temp = null;
102+
try (InputStream in = foundStream) {
103+
String suffix = ext != null ? ext : null; // Files.createTempFile needs suffix with dot
104+
// create a predictable prefix but allow uniqueness
105+
String prefix = "jtreesitter-" + libBaseName + "-";
106+
if (suffix == null) {
107+
// fallback if we couldn't detect extension
108+
temp = Files.createTempFile(prefix, null);
109+
} else {
110+
temp = Files.createTempFile(prefix, suffix);
111+
}
112+
// Ensure cleanup on exit as best-effort
113+
temp.toFile().deleteOnExit();
114+
115+
// Copy bytes
116+
Files.copy(in, temp, StandardCopyOption.REPLACE_EXISTING);
117+
118+
// On unix-like systems make executable (not strictly necessary for shared objects, but safe)
119+
try {
120+
temp.toFile().setExecutable(true, true);
121+
} catch (Exception ignored) {
39122
}
123+
124+
// Load the native library from the extracted temp file
125+
System.load(temp.toAbsolutePath().toString());
126+
} catch (IOException e) {
127+
// wrap as UnsatisfiedLinkError to match calling code expectations
128+
UnsatisfiedLinkError ule = new UnsatisfiedLinkError("Failed to extract and load native library from JAR (resource: " + foundResource + "): " + e);
129+
ule.initCause(e);
130+
throw ule;
40131
}
41132
}
133+
134+
private static String detectOs() {
135+
String osProp = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
136+
if (osProp.contains("win")) return "windows";
137+
if (osProp.contains("mac") || osProp.contains("darwin") || osProp.contains("os x")) return "macos";
138+
if (osProp.contains("nux") || osProp.contains("nix") || osProp.contains("linux")) return "linux";
139+
// fallback
140+
return osProp.replaceAll("\\s+", "");
141+
}
142+
143+
private static String detectArch() {
144+
String archProp = System.getProperty("os.arch", "").toLowerCase(Locale.ENGLISH);
145+
if (archProp.equals("x86_64") || archProp.equals("amd64")) return "x86_64";
146+
if (archProp.equals("aarch64") || archProp.equals("arm64")) return "aarch64";
147+
// other architectures we return raw (but normalized)
148+
return archProp.replaceAll("\\s+", "");
149+
}
150+
151+
private static String extractExtension(String mappedName) {
152+
if (mappedName == null) return null;
153+
int idx = mappedName.lastIndexOf('.');
154+
if (idx == -1) return null;
155+
return mappedName.substring(idx); // includes dot, e.g. ".so"
156+
}
42157
}

0 commit comments

Comments
 (0)