looked = new ArrayList<>();
@@ -56,7 +61,7 @@ static Found find() {
Diagnostic.misuse(
Status.MISUSE, "-D" + PROPERTY + "=" + named + " names no file"));
}
- return new Found(p, "-D" + PROPERTY);
+ return new Found(p, "-D" + PROPERTY, looked);
}
looked.add("-D" + PROPERTY);
@@ -67,20 +72,31 @@ static Found find() {
throw new ZuProgrammingException(
Diagnostic.misuse(Status.MISUSE, ENVIRONMENT + "=" + env + " names no file"));
}
- return new Found(p, ENVIRONMENT);
+ return new Found(p, ENVIRONMENT, looked);
}
looked.add(ENVIRONMENT);
- String resource = "dev/zudb/native/" + platform() + "/" + System.mapLibraryName("zu");
+ String flavour = flavour();
+ String resource = "dev/zudb/native/" + flavour + "/" + System.mapLibraryName("zu");
Path unpacked = unpack(resource);
if (unpacked != null) {
- return new Found(unpacked, "the zudb-native-" + platform() + " artifact");
+ return new Found(unpacked, "the zudb-native artifact, " + flavour, looked);
}
- looked.add("a zudb-native-" + platform() + " artifact on the classpath");
+ looked.add(
+ "a zudb-native artifact for "
+ + flavour
+ + ", which was not on the classpath"
+ + (Library.class.getModule().isNamed() && ModuleLayer.boot().findModule(
+ NATIVE_MODULE).isEmpty()
+ ? ". This is a module path, and nothing requires that artifact, so a jar sitting"
+ + " on the path is not resolved and its library is not visible: add"
+ + " --add-modules " + NATIVE_MODULE
+ : ""));
// A bare name, which is the platform being asked to search:
// java.library.path, and then whatever the loader does after that.
- return new Found(Paths.get(System.mapLibraryName("zu")), "the platform library path");
+ return new Found(
+ Paths.get(System.mapLibraryName("zu")), "the platform library path", looked);
}
/**
@@ -117,6 +133,39 @@ static String platform() {
return goos + "-" + goarch;
}
+ /**
+ * The same, and which C library on the platform where there are two.
+ *
+ * Alpine is not a smaller Linux, it is a different one: a shared
+ * object built against glibc does not load on musl and says so in a message
+ * about an interpreter rather than about a database. The two builds are two
+ * artifacts everywhere else this engine ships, so they are two here as well,
+ * and the choice is made by looking for musl's own loader, which is the one
+ * file whose path is fixed by the ABI rather than by a distribution.
+ *
+ *
Nowhere but Linux has a second answer, so nowhere but Linux is asked.
+ *
+ * @return for example {@code darwin-arm64} or {@code linux-amd64-musl}
+ */
+ static String flavour() {
+ String platform = platform();
+ if (!platform.startsWith("linux-")) {
+ return platform;
+ }
+ return musl() ? platform + "-musl" : platform;
+ }
+
+ /** Whether this is a musl system, by its loader rather than by its name. */
+ private static boolean musl() {
+ for (String loader :
+ new String[] {"/lib/ld-musl-x86_64.so.1", "/lib/ld-musl-aarch64.so.1"}) {
+ if (Files.exists(Paths.get(loader))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Copies a library out of the classpath, because a library inside a jar is
* not a file and every loader on every platform wants a file.
@@ -124,7 +173,7 @@ static String platform() {
* @param resource where it is
* @return the copy, or null if there is no such resource
*/
- private static Path unpack(String resource) {
+ static Path unpack(String resource) {
ClassLoader loader = Library.class.getClassLoader();
try (InputStream in =
loader == null
diff --git a/zudb/src/main/java/dev/zudb/Zu.java b/zudb/src/main/java/dev/zudb/Zu.java
index 82f0948..eced10c 100644
--- a/zudb/src/main/java/dev/zudb/Zu.java
+++ b/zudb/src/main/java/dev/zudb/Zu.java
@@ -86,6 +86,22 @@ public static Path library() {
return Holder.BOUND.library();
}
+ /**
+ * Which of the four places it was found in, in the words the failure
+ * message would have used.
+ *
+ *
This is the second question a bug report has to answer, and the one a
+ * user cannot work out for themselves: a path is a path, and whether it came
+ * from a property somebody set three shells ago or from a jar is the part
+ * that explains why the wrong engine is loaded.
+ *
+ * @return a phrase, for example {@code "the zudb-native artifact,
+ * darwin-arm64"} or {@code "-Dzu.library"}
+ */
+ public static String source() {
+ return Holder.BOUND.source();
+ }
+
private static Bound bind() {
Library.Found found = Library.find();
String wanted = System.getProperty(PROVIDER_PROPERTY);
@@ -147,6 +163,13 @@ private static String unavailable(Library.Found found, List refused) {
} else {
sb.append("Every provider refused. ").append(String.join("; ", refused));
}
+ // What was ruled out on the way here, in order, because the last place
+ // searched is the least informative one to be told about: a user whose
+ // artifact is on the module path but unresolved needs to hear that and
+ // not that java.library.path has no libzu in it.
+ if (!found.looked().isEmpty()) {
+ sb.append(" Before that: ").append(String.join("; then ", found.looked())).append(".");
+ }
sb.append(" Set -D").append(Library.PROPERTY).append(" to point at a libzu of your own.");
return sb.toString();
}
diff --git a/zudb/src/test/java/dev/zudb/LibraryTest.java b/zudb/src/test/java/dev/zudb/LibraryTest.java
index 86157dd..9057e1b 100644
--- a/zudb/src/test/java/dev/zudb/LibraryTest.java
+++ b/zudb/src/test/java/dev/zudb/LibraryTest.java
@@ -1,11 +1,16 @@
package dev.zudb;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -50,6 +55,70 @@ void thePlatformIsSpelledTheWayTheArtifactsAre() {
assertEquals(2, platform.split("-").length);
}
+ @Test
+ void theCLibraryIsPartOfTheAnswerOnLinuxAndNowhereElse() {
+ // A shared object built against glibc does not load on musl, so the two
+ // are two artifacts. Everywhere else there is one C library and nothing
+ // to say about it.
+ String platform = Library.platform();
+ String flavour = Library.flavour();
+ if (platform.startsWith("linux-")) {
+ assertTrue(
+ flavour.equals(platform) || flavour.equals(platform + "-musl"),
+ flavour + " is not " + platform + " with or without musl after it");
+ } else {
+ assertEquals(platform, flavour);
+ }
+ }
+
+ @Test
+ void aLibraryInAJarBecomesAFileWithTheSameBytes() throws Exception {
+ // What the zudb-native artifact holds is a resource, and no loader on any
+ // platform can map one of those. The stand-in under test resources is not
+ // a library, deliberately: what is being checked is the copy, and a real
+ // one would only make the test slower and platform-specific.
+ String resource = "dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in";
+ Path unpacked = Library.unpack(resource);
+ assertTrue(unpacked != null, "the stand-in is not on the test classpath");
+ assertTrue(Files.isRegularFile(unpacked));
+ byte[] want;
+ try (var in = LibraryTest.class.getClassLoader().getResourceAsStream(resource)) {
+ want = in.readAllBytes();
+ }
+ assertArrayEquals(want, Files.readAllBytes(unpacked));
+ // A directory of its own each time, so two callers cannot land on one
+ // file and so a copy cannot be made over a library already mapped.
+ Path again = Library.unpack(resource);
+ assertNotEquals(unpacked, again);
+ }
+
+ @Test
+ void theSearchSaysWhatItRuledOutOnTheWay() {
+ // The four places in order, and what is reported is however many of them
+ // were ruled out before one answered. How many that is depends on the
+ // machine this runs on, which is the point: the list is what tells a user
+ // what was tried, and the order is what makes it readable.
+ List places =
+ List.of("-Dzu.library", "ZU_LIBRARY", "a zudb-native artifact", "the platform library");
+ Library.Found found = Library.find();
+ List looked = found.looked();
+ assertTrue(looked.size() < places.size(), "everything was ruled out and something was found");
+ for (int i = 0; i < looked.size(); i++) {
+ assertTrue(
+ looked.get(i).startsWith(places.get(i)),
+ looked.get(i) + " is not the place that comes " + (i + 1) + "th");
+ }
+ assertFalse(
+ looked.contains(found.source()), "the place it was found is listed as one it was not");
+ }
+
+ @Test
+ void aPlatformWithNoArtifactIsNotAFailure() {
+ // The classpath is the third of four places, so nothing there means the
+ // search carries on to the platform's own rather than stopping.
+ assertNull(Library.unpack("dev/zudb/native/vax-11-780/libzu.so"));
+ }
+
private static void restore(String before) {
if (before == null) {
System.clearProperty(Library.PROPERTY);
diff --git a/zudb/src/test/resources/dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in b/zudb/src/test/resources/dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in
new file mode 100644
index 0000000..3f5a455
--- /dev/null
+++ b/zudb/src/test/resources/dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in
@@ -0,0 +1,2 @@
+not a library, and that is the point: what is checked here is that
+the bytes in the jar are the bytes on the disk afterwards.