the native library bundled in the jar for this OS/arch, unpacked to a per-user cache
+ * ({@code $XDG_CACHE_HOME}/{@code ~/.cache} on Linux and macOS, {@code %LOCALAPPDATA%} on
+ * Windows).
*
*
* The bundled libraries are placed on the classpath at build time (see {@code
@@ -72,10 +75,11 @@ interface TempDirSupplier {
*/
static LibraryLoader fromEnvironment() {
Map env = System.getenv();
+ String os = mapOs(System.getProperty("os.name"));
return new LibraryLoader(
env,
- defaultCacheDir(env),
- mapOs(System.getProperty("os.name")),
+ defaultCacheDir(env, os),
+ os,
mapArch(System.getProperty("os.arch")),
LibraryLoader::classpathResource);
}
@@ -85,9 +89,26 @@ static InputStream classpathResource(String name) {
return LibraryLoader.class.getClassLoader().getResourceAsStream(name);
}
- static Path defaultCacheDir(Map env) {
+ /**
+ * The per-user cache directory for unpacked natives: {@code $XDG_CACHE_HOME} if set (an
+ * explicit override on every OS), else the idiomatic per-OS cache root — {@code
+ * %LOCALAPPDATA%} on Windows, {@code ~/.cache} elsewhere.
+ */
+ static Path defaultCacheDir(Map env, String os) {
String xdg = env.get("XDG_CACHE_HOME");
- Path base = (xdg != null && !xdg.isEmpty()) ? Path.of(xdg) : Path.of(home(env), ".cache");
+ if (xdg != null && !xdg.isEmpty()) {
+ return cacheSubdir(Path.of(xdg));
+ }
+ if (os.equals("windows")) {
+ String localAppData = env.get("LOCALAPPDATA");
+ if (localAppData != null && !localAppData.isEmpty()) {
+ return cacheSubdir(Path.of(localAppData));
+ }
+ }
+ return cacheSubdir(Path.of(home(env), ".cache"));
+ }
+
+ private static Path cacheSubdir(Path base) {
return base.resolve("hegel-java").resolve("libhegel");
}
@@ -104,8 +125,11 @@ static String mapOs(String osName) {
if (os.contains("linux")) {
return "linux";
}
+ if (os.contains("windows")) {
+ return "windows";
+ }
throw new HegelException(
- "libhegel does not support this operating system: '" + osName + "' (linux/macOS only).");
+ "libhegel does not support this operating system: '" + osName + "' (Linux, macOS, and Windows only).");
}
static String mapArch(String osArch) {
@@ -120,16 +144,22 @@ static String mapArch(String osArch) {
}
private String libExt() {
+ if (os.equals("windows")) {
+ return "dll";
+ }
return os.equals("darwin") ? "dylib" : "so";
}
- /** The shared-library file name for this OS (e.g. {@code libhegel.so}). */
+ /** The shared-library file name for this OS (e.g. {@code libhegel.so}, {@code libhegel.dll}). */
private String libFileName() {
return "libhegel." + libExt();
}
/** The OS's conventional shared-library search-path environment variable. */
private String libraryPathVar() {
+ if (os.equals("windows")) {
+ return "PATH";
+ }
return os.equals("darwin") ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH";
}
diff --git a/src/test/java/dev/hegel/LibraryLoaderTest.java b/src/test/java/dev/hegel/LibraryLoaderTest.java
index 797c5d4..25b22db 100644
--- a/src/test/java/dev/hegel/LibraryLoaderTest.java
+++ b/src/test/java/dev/hegel/LibraryLoaderTest.java
@@ -42,7 +42,8 @@ void mapOsAndArch() {
assertEquals("linux", LibraryLoader.mapOs("Linux"));
assertEquals("darwin", LibraryLoader.mapOs("Mac OS X"));
assertEquals("darwin", LibraryLoader.mapOs("Darwin"));
- assertThrows(HegelException.class, () -> LibraryLoader.mapOs("Windows 11"));
+ assertEquals("windows", LibraryLoader.mapOs("Windows 11"));
+ assertThrows(HegelException.class, () -> LibraryLoader.mapOs("FreeBSD"));
assertEquals("amd64", LibraryLoader.mapArch("amd64"));
assertEquals("amd64", LibraryLoader.mapArch("x86_64"));
@@ -54,23 +55,47 @@ void mapOsAndArch() {
@Test
void defaultCacheDirHonoursXdgThenHome() {
assertEquals(
- Path.of("/xdg/hegel-java/libhegel"), LibraryLoader.defaultCacheDir(Map.of("XDG_CACHE_HOME", "/xdg")));
- assertEquals(Path.of("/h/.cache/hegel-java/libhegel"), LibraryLoader.defaultCacheDir(Map.of("HOME", "/h")));
+ Path.of("/xdg/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("XDG_CACHE_HOME", "/xdg"), "linux"));
+ assertEquals(
+ Path.of("/h/.cache/hegel-java/libhegel"), LibraryLoader.defaultCacheDir(Map.of("HOME", "/h"), "linux"));
}
@Test
void cacheDirAndHomeEdgeCases() {
assertEquals(
Path.of("/h/.cache/hegel-java/libhegel"),
- LibraryLoader.defaultCacheDir(Map.of("XDG_CACHE_HOME", "", "HOME", "/h")));
+ LibraryLoader.defaultCacheDir(Map.of("XDG_CACHE_HOME", "", "HOME", "/h"), "linux"));
// No HOME and no XDG falls back to the user.home system property.
- Path d = LibraryLoader.defaultCacheDir(Map.of());
+ Path d = LibraryLoader.defaultCacheDir(Map.of(), "linux");
assertTrue(d.endsWith(Path.of("hegel-java/libhegel")));
// Empty HOME also falls back to user.home.
- Path d2 = LibraryLoader.defaultCacheDir(Map.of("HOME", ""));
+ Path d2 = LibraryLoader.defaultCacheDir(Map.of("HOME", ""), "linux");
assertTrue(d2.endsWith(Path.of("hegel-java/libhegel")));
}
+ @Test
+ void windowsCacheDirPrefersLocalAppData() {
+ assertEquals(
+ Path.of("/lad/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("LOCALAPPDATA", "/lad", "HOME", "/h"), "windows"));
+ // XDG_CACHE_HOME is an explicit override on every OS, Windows included.
+ assertEquals(
+ Path.of("/xdg/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("XDG_CACHE_HOME", "/xdg", "LOCALAPPDATA", "/lad"), "windows"));
+ // Unset or empty LOCALAPPDATA falls back to the POSIX-style default.
+ assertEquals(
+ Path.of("/h/.cache/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("HOME", "/h"), "windows"));
+ assertEquals(
+ Path.of("/h/.cache/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("LOCALAPPDATA", "", "HOME", "/h"), "windows"));
+ // LOCALAPPDATA is ignored off-Windows.
+ assertEquals(
+ Path.of("/h/.cache/hegel-java/libhegel"),
+ LibraryLoader.defaultCacheDir(Map.of("LOCALAPPDATA", "/lad", "HOME", "/h"), "linux"));
+ }
+
@Test
void sha256OfEmptyInput() {
assertEquals(
@@ -84,6 +109,8 @@ void resourcePathPerPlatform(@TempDir Path dir) {
assertEquals("native/linux-amd64/libhegel.so", linux.resourcePath());
LibraryLoader darwin = new LibraryLoader(Map.of(), dir, "darwin", "arm64", NO_RESOURCES);
assertEquals("native/darwin-arm64/libhegel.dylib", darwin.resourcePath());
+ LibraryLoader windows = new LibraryLoader(Map.of(), dir, "windows", "amd64", NO_RESOURCES);
+ assertEquals("native/windows-amd64/libhegel.dll", windows.resourcePath());
}
@Test
@@ -167,6 +194,16 @@ void darwinSearchesDyldLibraryPath(@TempDir Path dir) throws IOException {
assertEquals(lib, l.resolve());
}
+ @Test
+ void windowsSearchesPath(@TempDir Path dir) throws IOException {
+ Path libDir = Files.createDirectories(dir.resolve("libs"));
+ Path lib = libDir.resolve("libhegel.dll");
+ Files.writeString(lib, "win-lib");
+ Map env = Map.of("PATH", libDir.toString());
+ LibraryLoader l = new LibraryLoader(new HashMap<>(env), dir.resolve("cache"), "windows", "amd64", NO_RESOURCES);
+ assertEquals(lib, l.resolve());
+ }
+
@Test
void bundledNativeUnpackedAndCached(@TempDir Path dir) throws IOException {
byte[] payload = "ELF-ish-bytes".getBytes(StandardCharsets.UTF_8);