diff --git a/pom.xml b/pom.xml index 903365a..8a03c3e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.mcaccess prism - 0.17.3 + 0.18.2 PRISM Java bindings for PRISM. @@ -113,7 +113,7 @@ https://github.com/ethindp/prism/releases/download/v${project.version}/prism-sdk-v${project.version}.zip true - 6273c126fc35a300b2d76685383d8556fc9c822a0b26682558c32ce28f914364 + e4e5634aeaeddd3ffca527d8b99c27cbc0b51c806f7876ab27dbee335bded9ab @@ -123,6 +123,22 @@ maven-resources-plugin 3.3.1 + + generate-build-info + generate-sources + + copy-resources + + + ${project.build.directory}/generated-sources/version + + + src/main/java-templates + true + + + + generate-resources @@ -191,6 +207,8 @@ ${project.build.directory}/generated-sources/jextract --target-package org.mcaccess.prism.natives + -I + ${prism}/include ${prism}/include/prism.h @@ -210,6 +228,7 @@ ${project.build.directory}/generated-sources/jextract + ${project.build.directory}/generated-sources/version diff --git a/src/main/java-templates/org/mcaccess/prism/natives/PrismBuildInfo.java b/src/main/java-templates/org/mcaccess/prism/natives/PrismBuildInfo.java new file mode 100644 index 0000000..9c445a3 --- /dev/null +++ b/src/main/java-templates/org/mcaccess/prism/natives/PrismBuildInfo.java @@ -0,0 +1,10 @@ +package org.mcaccess.prism.natives; + +/** Generated from pom.xml. Do not edit. */ +public final class PrismBuildInfo { + /** The prism release this binding was built against. */ + public static final String PRISM_VERSION = "${project.version}"; + + private PrismBuildInfo() { + } +} diff --git a/src/main/java/org/mcaccess/prism/Prism.java b/src/main/java/org/mcaccess/prism/Prism.java index a7bf341..4acf4e1 100644 --- a/src/main/java/org/mcaccess/prism/Prism.java +++ b/src/main/java/org/mcaccess/prism/Prism.java @@ -65,4 +65,11 @@ public static boolean isAvailable() { return false; } } + + /** + * @return the load failure or {@code null} + */ + public static Throwable getLoadFailure() { + return NativeLoader.getLoadFailure(); + } } diff --git a/src/main/java/org/mcaccess/prism/natives/NativeLoader.java b/src/main/java/org/mcaccess/prism/natives/NativeLoader.java index a148582..9f2a636 100644 --- a/src/main/java/org/mcaccess/prism/natives/NativeLoader.java +++ b/src/main/java/org/mcaccess/prism/natives/NativeLoader.java @@ -1,9 +1,11 @@ package org.mcaccess.prism.natives; -import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; +import java.net.URL; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Locale; @@ -12,7 +14,11 @@ * Utility class responsible for extracting and loading the PRISM native libraries. */ public final class NativeLoader { + /** System property naming the directory to extract native libraries into, overriding the platform cache. */ + public static final String NATIVE_DIR_PROPERTY = "org.mcaccess.prism.nativeDir"; + private static volatile boolean loaded = false; + private static volatile Throwable loadFailure; private static final Object LOCK = new Object(); private NativeLoader() { @@ -29,100 +35,121 @@ public static void load() { if (loaded) { return; } - loadInternal(); + try { + loadInternal(); + } catch (Throwable t) { + loadFailure = t; + throw t; + } loaded = true; + loadFailure = null; } } - private static void loadInternal() { - String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - String osArch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT); + /** + * The failure that prevented the native library from loading, or null if it loaded or was never attempted. + */ + public static Throwable getLoadFailure() { + return loadFailure; + } - String os; - if (osName.contains("win")) { - os = "windows"; - } else if (osName.contains("mac") || osName.contains("darwin")) { - os = "mac"; - } else if (osName.contains("linux") || osName.contains("unix") || osName.contains("sunos")) { - os = "linux"; - } else { - os = osName; + /** + * Where a library is extracted to when it cannot be loaded in place. + *

+ * {@value #NATIVE_DIR_PROPERTY} wins if set, so an application whose home directory is redirected or read-only, + * as under Flatpak, Snap or the macOS App Sandbox, can place the library itself. Otherwise this is the platform's + * cache location: extracted libraries are regenerable, and a version-scoped path there is shared by every + * application using this binding rather than duplicated per process. Windows uses LOCALAPPDATA rather than + * APPDATA because a native binary for one architecture must not roam to another machine. + */ + static Path extractionDir(String os, String arch) { + String override = System.getProperty(NATIVE_DIR_PROPERTY); + if (override != null && !override.isBlank()) { + return Path.of(override); } - String arch; - if (osArch.contains("aarch64") || osArch.contains("arm64")) { - arch = "aarch64"; - } else if (osArch.contains("amd64") || osArch.contains("x86_64") || osArch.contains("x64")) { - arch = "amd64"; + String home = System.getProperty("user.home", "."); + Path cacheRoot; + if ("windows".equals(os)) { + String localAppData = System.getenv("LOCALAPPDATA"); + cacheRoot = (localAppData != null && !localAppData.isBlank()) + ? Path.of(localAppData) + : Path.of(home, "AppData", "Local"); + } else if ("mac".equals(os)) { + cacheRoot = Path.of(home, "Library", "Caches"); } else { - arch = osArch; + String xdgCache = System.getenv("XDG_CACHE_HOME"); + cacheRoot = (xdgCache != null && !xdgCache.isBlank()) + ? Path.of(xdgCache) + : Path.of(home, ".cache"); } + return cacheRoot.resolve("prism-java").resolve(PrismBuildInfo.PRISM_VERSION).resolve(os + "-" + arch); + } + + private static void loadInternal() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + String osArch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT); + + String os = osName.contains("win") ? "windows" + : (osName.contains("mac") || osName.contains("darwin")) ? "mac" + : (osName.contains("linux") || osName.contains("unix") || osName.contains("sunos")) ? "linux" + : osName; + + String arch = (osArch.contains("aarch64") || osArch.contains("arm64")) ? "aarch64" + : (osArch.contains("amd64") || osArch.contains("x86_64") || osArch.contains("x64")) ? "amd64" + : osArch; + + String fileName = System.mapLibraryName("prism"); + String resourcePath = "mac".equals(os) + ? "/natives/mac/" + fileName + : "/natives/" + os + "/" + arch + "/" + fileName; try { - if ("windows".equals(os)) { - loadWindows(arch); - } else if ("linux".equals(os)) { - loadLinux(arch); - } else if ("mac".equals(os)) { - loadMac(); - } else { - System.loadLibrary("prism"); + URL resource = NativeLoader.class.getResource(resourcePath); + if (resource == null) { + throw new NoSuchFileException("Resource not found in classpath: " + resourcePath); } - } catch (Throwable t) { - try { - System.loadLibrary("prism"); - } catch (Throwable fallbackError) { - t.addSuppressed(fallbackError); - throw new UnsatisfiedLinkError("Failed to load PRISM native library: " + t.getMessage()); + + if ("file".equals(resource.getProtocol())) { + System.load(Path.of(resource.toURI()).toAbsolutePath().toString()); + return; } - } - } - private static void loadWindows(String arch) throws IOException { - Path tempDir = getTempDir(); - String resourceDir = "/natives/windows/" + arch + "/"; - Path prismPath = extractResource(resourceDir + "prism.dll", tempDir.resolve("prism.dll")); - if (prismPath != null) { - System.load(prismPath.toAbsolutePath().toString()); - } else { - System.loadLibrary("prism"); - } - } + Path dir = extractionDir(os, arch); + Path target = dir.resolve(fileName); - private static void loadLinux(String arch) throws IOException { - Path tempDir = getTempDir(); - String resourceDir = "/natives/linux/" + arch + "/"; - Path prismPath = extractResource(resourceDir + "libprism.so", tempDir.resolve("libprism.so")); - if (prismPath != null) { - System.load(prismPath.toAbsolutePath().toString()); - } else { - System.loadLibrary("prism"); - } - } + if (!Files.exists(target)) { + Files.createDirectories(dir); + Path tmp = Files.createTempFile(dir, fileName, ".tmp"); - private static void loadMac() throws IOException { - Path tempDir = getTempDir(); - Path prismPath = extractResource("/natives/mac/libprism.dylib", tempDir.resolve("libprism.dylib")); - if (prismPath != null) { - System.load(prismPath.toAbsolutePath().toString()); - } else { - System.loadLibrary("prism"); - } - } + // Use resource.openStream() so we don't have to query the classpath a second time + try (InputStream in = resource.openStream()) { + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); - private static Path extractResource(String resourcePath, Path target) throws IOException { - try (InputStream in = NativeLoader.class.getResourceAsStream(resourcePath)) { - if (in == null) { - return null; + try { + Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + // Fallback to non-atomic move + Files.move(tmp, target); + } + } catch (FileAlreadyExistsException e) { + // Another JVM process won the race and created the file concurrently. Safe to ignore. + } finally { + Files.deleteIfExists(tmp); + } + } + System.load(target.toAbsolutePath().toString()); + } catch (Throwable extractError) { + // Fallback to system library path if resource extraction failed or wasn't found + try { + System.loadLibrary("prism"); + } catch (Throwable fallbackError) { + UnsatisfiedLinkError error = new UnsatisfiedLinkError( + "Failed to load PRISM native library. Extraction error: " + extractError.getMessage()); + error.initCause(extractError); // Preserves original extraction stack trace + error.addSuppressed(fallbackError); + throw error; } - Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); - return target; } } - - private static Path getTempDir() throws IOException { - Path tempDir = Files.createTempDirectory("prism_native_"); - tempDir.toFile().deleteOnExit(); - return tempDir; - } } diff --git a/src/test/java/org/mcaccess/prism/natives/NativeLoaderTest.java b/src/test/java/org/mcaccess/prism/natives/NativeLoaderTest.java new file mode 100644 index 0000000..9602ff5 --- /dev/null +++ b/src/test/java/org/mcaccess/prism/natives/NativeLoaderTest.java @@ -0,0 +1,74 @@ +package org.mcaccess.prism.natives; + +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class NativeLoaderTest { + + @AfterEach + void clearOverride() { + System.clearProperty(NativeLoader.NATIVE_DIR_PROPERTY); + } + + @Test + void theOverridePropertyWins() { + System.setProperty(NativeLoader.NATIVE_DIR_PROPERTY, Path.of("build", "custom").toString()); + assertThat(NativeLoader.extractionDir("windows", "amd64")).isEqualTo(Path.of("build", "custom")); + assertThat(NativeLoader.extractionDir("linux", "aarch64")).isEqualTo(Path.of("build", "custom")); + assertThat(NativeLoader.extractionDir("mac", "aarch64")).isEqualTo(Path.of("build", "custom")); + } + + @Test + void aBlankOverrideIsIgnored() { + System.setProperty(NativeLoader.NATIVE_DIR_PROPERTY, " "); + assertThat(NativeLoader.extractionDir("windows", "amd64")) + .endsWithRaw(Path.of("prism-java", PrismBuildInfo.PRISM_VERSION, "windows-amd64")); + } + + @Test + void theDefaultIsVersionAndPlatformScoped() { + assertThat(NativeLoader.extractionDir("windows", "amd64")) + .endsWithRaw(Path.of("prism-java", PrismBuildInfo.PRISM_VERSION, "windows-amd64")); + assertThat(NativeLoader.extractionDir("linux", "aarch64")) + .endsWithRaw(Path.of("prism-java", PrismBuildInfo.PRISM_VERSION, "linux-aarch64")); + assertThat(NativeLoader.extractionDir("mac", "aarch64")) + .endsWithRaw(Path.of("prism-java", PrismBuildInfo.PRISM_VERSION, "mac-aarch64")); + } + + @Test + void eachPlatformResolvesUnderItsOwnCacheRoot() { + Path home = Path.of(System.getProperty("user.home")); + assertThat(NativeLoader.extractionDir("mac", "aarch64")) + .startsWithRaw(home.resolve("Library").resolve("Caches")); + + Path linux = NativeLoader.extractionDir("linux", "amd64"); + String xdg = System.getenv("XDG_CACHE_HOME"); + assertThat(linux).startsWithRaw(xdg != null && !xdg.isBlank() ? Path.of(xdg) : home.resolve(".cache")); + + Path windows = NativeLoader.extractionDir("windows", "amd64"); + String localAppData = System.getenv("LOCALAPPDATA"); + assertThat(windows).startsWithRaw(localAppData != null && !localAppData.isBlank() + ? Path.of(localAppData) + : home.resolve("AppData").resolve("Local")); + } + + @Test + void platformsDoNotShareADirectory() { + assertThat(NativeLoader.extractionDir("windows", "amd64")) + .isNotEqualTo(NativeLoader.extractionDir("windows", "aarch64")) + .isNotEqualTo(NativeLoader.extractionDir("linux", "amd64")) + .isNotEqualTo(NativeLoader.extractionDir("mac", "amd64")); + assertThat(NativeLoader.extractionDir("mac", "amd64")) + .isNotEqualTo(NativeLoader.extractionDir("mac", "aarch64")); + } + + @Test + void theLibraryLoadedAndReportsNoFailure() { + NativeLoader.load(); + assertThat(NativeLoader.getLoadFailure()).isNull(); + } +}