Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>org.mcaccess</groupId>
<artifactId>prism</artifactId>
<version>0.17.3</version>
<version>0.18.2</version>

<name>PRISM</name>
<description>Java bindings for PRISM.</description>
Expand Down Expand Up @@ -113,7 +113,7 @@
<configuration>
<url>https://github.com/ethindp/prism/releases/download/v${project.version}/prism-sdk-v${project.version}.zip</url>
<unpack>true</unpack>
<sha256>6273c126fc35a300b2d76685383d8556fc9c822a0b26682558c32ce28f914364</sha256>
<sha256>e4e5634aeaeddd3ffca527d8b99c27cbc0b51c806f7876ab27dbee335bded9ab</sha256>
</configuration>
</execution>
</executions>
Expand All @@ -123,6 +123,22 @@
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>generate-build-info</id>
<phase>generate-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/generated-sources/version</outputDirectory>
<resources>
<resource>
<directory>src/main/java-templates</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<phase>generate-resources</phase>
<goals>
Expand Down Expand Up @@ -191,6 +207,8 @@
<argument>${project.build.directory}/generated-sources/jextract</argument>
<argument>--target-package</argument>
<argument>org.mcaccess.prism.natives</argument>
<argument>-I</argument>
<argument>${prism}/include</argument>
<argument>${prism}/include/prism.h</argument>
</arguments>
</configuration>
Expand All @@ -210,6 +228,7 @@
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/jextract</source>
<source>${project.build.directory}/generated-sources/version</source>
</sources>
</configuration>
</execution>
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/mcaccess/prism/Prism.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,11 @@ public static boolean isAvailable() {
return false;
}
}

/**
* @return the load failure or {@code null}
*/
public static Throwable getLoadFailure() {
return NativeLoader.getLoadFailure();
}
}
183 changes: 105 additions & 78 deletions src/main/java/org/mcaccess/prism/natives/NativeLoader.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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.
* <p>
* {@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;
}
}
74 changes: 74 additions & 0 deletions src/test/java/org/mcaccess/prism/natives/NativeLoaderTest.java
Original file line number Diff line number Diff line change
@@ -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();
}
}