diff --git a/src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java b/src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java index ac03f19..f504a96 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java +++ b/src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java @@ -1,9 +1,16 @@ package io.github.treesitter.jtreesitter.internal; import io.github.treesitter.jtreesitter.NativeLibraryLookup; + +import java.io.IOException; +import java.io.InputStream; import java.lang.foreign.Arena; import java.lang.foreign.Linker; import java.lang.foreign.SymbolLookup; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Locale; import java.util.Optional; import java.util.ServiceLoader; @@ -21,15 +28,130 @@ public SymbolLookup get(Arena arena) { for (var libraryLookup : serviceLoader) { lookup = lookup.or(libraryLookup.get(arena)); } - return lookup.or(findLibrary(arena)).or(Linker.nativeLinker().defaultLookup()); + + return lookup.or((name) -> findLibrary(arena).find(name)).or(Linker.nativeLinker().defaultLookup()); } private static SymbolLookup findLibrary(Arena arena) { try { var library = System.mapLibraryName("tree-sitter"); return SymbolLookup.libraryLookup(library, arena); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException ex1) { + try { + findLibraryBundledInJar("tree-sitter"); + } catch (UnsatisfiedLinkError ex2) { + try { + System.loadLibrary("tree-sitter"); + } catch (UnsatisfiedLinkError ex3) { + ex2.addSuppressed(ex3); + ex1.addSuppressed(ex2); + throw ex1; + } + } return SymbolLookup.loaderLookup(); } } + + private static void findLibraryBundledInJar(String libBaseName) throws UnsatisfiedLinkError { + /* + * Strategy: + * 1) Resolve os & arch and compute candidate resource names + * 2) Try to locate resource inside JAR with several common layouts + * 3) Extract to temp file and System.load it + */ + + final String mappedName = System.mapLibraryName(libBaseName); // platform-native file name (libtree-sitter.so, tree-sitter.dll, ...) + final String os = detectOs(); + final String arch = detectArch(); + final String ext = extractExtension(mappedName); // ".so" or ".dll" or ".dylib" + + // Candidate resource paths inside the JAR. Adapt these to however you pack native libs. + String[] candidates = new String[] { + // platform-specific directories (most specific) + "/natives/" + os + "-" + arch + "/" + mappedName, + "/natives/" + arch + "/" + mappedName, + "/native/" + os + "-" + arch + "/" + mappedName, + "/native/" + arch + "/" + mappedName, + // less specific + "/natives/" + mappedName, + "/native/" + mappedName, + // fallback: just the file at root of jar (not recommended but sometimes used) + "/" + mappedName + }; + + InputStream foundStream = null; + String foundResource = null; + for (String candidate : candidates) { + InputStream is = ChainedLibraryLookup.class.getResourceAsStream(candidate); + if (is != null) { + foundStream = is; + foundResource = candidate; + break; + } + } + + if (foundStream == null) { + // helpful message mentioning what we tried + String tried = String.join(", ", candidates); + throw new UnsatisfiedLinkError("Could not find bundled native library resource for '" + + libBaseName + "'. Tried: " + tried); + } + + // Create temp file and copy resource contents + Path temp = null; + try (InputStream in = foundStream) { + String suffix = ext != null ? ext : null; // Files.createTempFile needs suffix with dot + // create a predictable prefix but allow uniqueness + String prefix = "jtreesitter-" + libBaseName + "-"; + if (suffix == null) { + // fallback if we couldn't detect extension + temp = Files.createTempFile(prefix, null); + } else { + temp = Files.createTempFile(prefix, suffix); + } + // Ensure cleanup on exit as best-effort + temp.toFile().deleteOnExit(); + + // Copy bytes + Files.copy(in, temp, StandardCopyOption.REPLACE_EXISTING); + + // On unix-like systems make executable (not strictly necessary for shared objects, but safe) + try { + temp.toFile().setExecutable(true, true); + } catch (Exception ignored) { + } + + // Load the native library from the extracted temp file + System.load(temp.toAbsolutePath().toString()); + } catch (IOException e) { + // wrap as UnsatisfiedLinkError to match calling code expectations + UnsatisfiedLinkError ule = new UnsatisfiedLinkError("Failed to extract and load native library from JAR (resource: " + foundResource + "): " + e); + ule.initCause(e); + throw ule; + } + } + + private static String detectOs() { + String osProp = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); + if (osProp.contains("win")) return "windows"; + if (osProp.contains("mac") || osProp.contains("darwin") || osProp.contains("os x")) return "macos"; + if (osProp.contains("nux") || osProp.contains("nix") || osProp.contains("linux")) return "linux"; + // fallback + return osProp.replaceAll("\\s+", ""); + } + + private static String detectArch() { + String archProp = System.getProperty("os.arch", "").toLowerCase(Locale.ENGLISH); + if (archProp.equals("x86_64") || archProp.equals("amd64")) return "x86_64"; + if (archProp.equals("aarch64") || archProp.equals("arm64")) return "aarch64"; + // other architectures we return raw (but normalized) + return archProp.replaceAll("\\s+", ""); + } + + private static String extractExtension(String mappedName) { + if (mappedName == null) return null; + int idx = mappedName.lastIndexOf('.'); + if (idx == -1) return null; + return mappedName.substring(idx); // includes dot, e.g. ".so" + } } diff --git a/src/main/java/io/github/treesitter/jtreesitter/package-info.java b/src/main/java/io/github/treesitter/jtreesitter/package-info.java index c2f1282..a375a9e 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/package-info.java +++ b/src/main/java/io/github/treesitter/jtreesitter/package-info.java @@ -43,10 +43,9 @@ * SymbolLookup.libraryLookup(String, Arena)}. * *
  • - * The libraries can be loaded manually by calling - * {@link java.lang.System#loadLibrary(String) System.loadLibrary(String)}, - * if the library is installed in {@systemProperty java.library.path}, - * or {@link java.lang.System#load(String) System.load(String)}. + * If the libraries are installed in {@systemProperty java.library.path} instead, + * they will be loaded automatically by {@link java.lang.foreign.SymbolLookup#loaderLookup() + * SymbolLookup.loaderLookup()}. *
  • *
  • * The libraries can be loaded manually by registering a custom implementation of diff --git a/src/main/resources/natives/linux-aarch64/libtree-sitter.so b/src/main/resources/natives/linux-aarch64/libtree-sitter.so new file mode 100755 index 0000000..d6f78a4 Binary files /dev/null and b/src/main/resources/natives/linux-aarch64/libtree-sitter.so differ diff --git a/src/main/resources/natives/linux-x86_64/libtree-sitter.so b/src/main/resources/natives/linux-x86_64/libtree-sitter.so new file mode 100755 index 0000000..0f4f48b Binary files /dev/null and b/src/main/resources/natives/linux-x86_64/libtree-sitter.so differ diff --git a/src/main/resources/natives/macos-aarch64/libtree-sitter.dylib b/src/main/resources/natives/macos-aarch64/libtree-sitter.dylib new file mode 100755 index 0000000..04826c3 Binary files /dev/null and b/src/main/resources/natives/macos-aarch64/libtree-sitter.dylib differ diff --git a/src/main/resources/natives/macos-x86_64/libtree-sitter.dylib b/src/main/resources/natives/macos-x86_64/libtree-sitter.dylib new file mode 100644 index 0000000..425bc59 Binary files /dev/null and b/src/main/resources/natives/macos-x86_64/libtree-sitter.dylib differ diff --git a/src/main/resources/natives/windows-x86_64/libtree-sitter.dll b/src/main/resources/natives/windows-x86_64/libtree-sitter.dll new file mode 100755 index 0000000..ba5f67e Binary files /dev/null and b/src/main/resources/natives/windows-x86_64/libtree-sitter.dll differ