From c7e08e7474551e7892317f681da8e5d8db336295 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:34:47 +0700 Subject: [PATCH] The engine comes with the client A Java client is Java and the engine is a shared library, so until now using this meant installing a libzu by hand and telling the JVM where it went. One more dependency now does it: zudb-native carries a build for every platform this client supports, Library.find looks for one on the class path, and a resource is copied to a temp file because no loader on any platform can map a resource. Eight artifacts come out of one module and one staged directory, so they cannot disagree about what is in them. The one with no classifier holds all seven platforms and is what a program developed on a laptop and deployed to a cluster wants, and the only one that survives being shaded. The seven classified ones hold one platform each, which is what a container image that knows what it runs on wants. Alpine is a platform rather than a smaller Linux: 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. Library.flavour picks between the two by looking for musl's own loader, which is the one path the ABI fixes rather than a distribution. Two things a user meets go with it. On the module path nothing requires this artifact, because there is no code in it to require, so a jar that is on the path is never resolved and its library is invisible; the search notices it is on a module path and names --add-modules dev.zudb.natives rather than leaving a user to work out why a class path run worked. And the derived automatic module name would be zudb.native, which is not a legal module name because native is a keyword, so every jar carries Automatic-Module-Name in its manifest. The search now also carries the places it ruled out, and Zu.source says which of the four answered, because a path on its own does not explain why the wrong engine is loaded and a property somebody set three shells ago is exactly the thing that explains it. Nothing in the test suite could check the claim this makes, since the suite is told where the library is so that it tests the binding rather than the search. So CI checks it the way a user meets it, on Linux and macOS: a class path, no property, no environment variable, and a statement that has to answer. --- .github/workflows/ci.yml | 78 +++++++++ .gitignore | 3 + README.md | 44 ++++- pom.xml | 12 ++ scripts/stage-natives.sh | 82 +++++++++ zudb-native/pom.xml | 163 ++++++++++++++++++ zudb/src/main/java/dev/zudb/Library.java | 65 ++++++- zudb/src/main/java/dev/zudb/Zu.java | 23 +++ zudb/src/test/java/dev/zudb/LibraryTest.java | 69 ++++++++ .../a-platform-that-is-not-one/libzu.stand-in | 2 + 10 files changed, 531 insertions(+), 10 deletions(-) create mode 100755 scripts/stage-natives.sh create mode 100644 zudb-native/pom.xml create mode 100644 zudb/src/test/resources/dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18beaac..439b921 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,84 @@ jobs: - run: java -jar zudb-bench/target/benchmarks.jar -f 1 -wi 1 -i 1 -r 1s -w 1s + # The claim the zudb-native artifact makes is that a user who added a + # dependency and installed nothing has an engine. Nothing in the test + # suite can check that, because the suite is told where the library is + # so that it tests the binding rather than the search. So it is + # checked here, once, the way a user meets it: a classpath, no + # property, no environment variable, and a statement. + natives: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + + - uses: actions/checkout@v5 + with: + repository: tamnd/zu + path: engine + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "25" + cache: maven + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: engine + + - name: Build libzu + working-directory: engine + run: cargo build --release -p zu-capi + + # One platform rather than seven, because this runner can only + # build the one it is, so the rule that every platform is staged + # is stood down here and holds where it matters, in the release. + - name: Stage the one platform this runner is + run: | + set -eu + case "$RUNNER_OS" in + Linux) flavour=linux-amd64; library=libzu.so ;; + macOS) flavour=darwin-arm64; library=libzu.dylib ;; + *) echo "no row for $RUNNER_OS"; exit 1 ;; + esac + mkdir -p "zudb-native/lib/$flavour" + cp "engine/target/release/$library" "zudb-native/lib/$flavour/$library" + + - run: mvn $MAVEN_ARGS -Pnatives -DskipTests -Denforcer.skip=true package + + - name: A classpath, and nothing else + run: | + set -eu + mkdir -p "$RUNNER_TEMP/user" + cat > "$RUNNER_TEMP/user/Main.java" <<'EOF' + import dev.zudb.Connection; + import dev.zudb.Result; + import dev.zudb.Zu; + + public class Main { + public static void main(String[] args) { + System.out.println("found " + Zu.library() + " through " + Zu.source()); + try (Connection conn = Connection.memory(); + Result r = conn.query("RETURN 1 AS one")) { + if (r.row(0).getLong(0) != 1L) { + throw new AssertionError("the engine answered something else"); + } + } + System.out.println("the engine came out of the jar and answered"); + } + } + EOF + cp=$(ls zudb/target/zudb-*.jar zudb-ffm/target/zudb-ffm-*.jar \ + zudb-native/target/zudb-native-*.jar | grep -v sources | tr '\n' ':') + javac -cp "$cp" -d "$RUNNER_TEMP/user" "$RUNNER_TEMP/user/Main.java" + env -u ZU_LIBRARY java --enable-native-access=ALL-UNNAMED \ + -cp "$cp$RUNNER_TEMP/user" Main + # What Maven Central will run over the artifacts, run here instead so # that a release is not the first time anyone sees it. javadoc: diff --git a/.gitignore b/.gitignore index 240c72a..478b4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .DS_Store target/ +# What scripts/stage-natives.sh downloads. A build of the engine is a +# thing a release fetches, not a thing a git history carries. +zudb-native/lib/ diff --git a/README.md b/README.md index 13c6a70..24d8478 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,37 @@ Config config = Config.of(Map.of("threads", "1", "memory_limit", "1073741824")); The keys and the parsing belong to the engine rather than to this client, so a key added to the engine since this client was built works anyway, and a key that never existed is refused with the typo named. A suffix such as `MB` is deliberately not parsed anywhere: its two readings differ by 4.9%, and the place to decide which one a user meant is where the user typed it. +## The engine, without installing one + +The client is Java and the engine is a shared library, so something has to put a `libzu` on the machine. Adding one more dependency is that something: + +```xml + + dev.zudb + zudb-native + ${zu.version} + runtime + +``` + +That artifact carries a build for every platform this client supports and is about twenty megabytes. It is the right one for a program developed on a laptop and deployed to a cluster, and the only one that survives being shaded into an uber-jar. A container image knows exactly what it runs on, so it can name a platform and take about three megabytes instead: + +| Classifier | What it holds | +|---|---| +| `linux-amd64` | glibc, x86-64 | +| `linux-arm64` | glibc, aarch64 | +| `linux-amd64-musl` | musl, x86-64 | +| `linux-arm64-musl` | musl, aarch64 | +| `darwin-amd64` | macOS, Intel | +| `darwin-arm64` | macOS, Apple silicon | +| `windows-amd64` | Windows, x86-64 | + +Alpine is a separate row rather than a smaller Linux, because a shared object built against glibc does not load on musl and the message it fails with talks about an interpreter rather than about a database. Which of the two a JVM gets is decided by looking for musl's own loader on disk, which is the one path the ABI fixes rather than a distribution. + +The library inside the jar is a resource, and no loader on any platform can map one of those, so it is copied to a temp file the first time anything needs it and the copy is what gets loaded. That happens once per JVM. + +On the module path the artifact needs `--add-modules dev.zudb.natives`. Nothing `requires` it, since there is no code in it to require, and a jar nothing requires is a jar that is never resolved and whose resources are therefore invisible. The search says so itself when it comes up empty on a module path, so the failure names the flag rather than leaving a user to work out why the same classpath run worked. + ## How it binds The Foreign Function and Memory API is the primary path. The downcall handles are written by hand against `zu.h` rather than generated with `jextract`, because the C ABI here is around seventy functions with a stable shape, and a hand-written layer is where the interesting decisions live: which calls are `Linker.Option.critical` because they are short pure accessors, where the out-parameter scratch space comes from so that a query does not allocate, and how a `zu_error` becomes a typed Java exception exactly once. There is no native code in this repository beyond `libzu` itself. @@ -196,7 +227,7 @@ An SDK that requires a recent JDK in 2026 excludes a large part of the enterpris | `dev.zudb:zudb` | Java 17 | the API, no native code, no FFM types in the public surface | | `dev.zudb:zudb-ffm` | Java 25 | the FFM provider, selected automatically | | `dev.zudb:zudb-jni` | Java 17 | the fallback provider | -| `dev.zudb:zudb-native-{platform}` | | the `libzu` binaries | +| `dev.zudb:zudb-native` | | the `libzu` binaries, all platforms or one by classifier | A `ServiceLoader` picks the provider at run time and application code never names one. The FFM artifact targets Java 25 rather than the Java 22 that finalised the API, because 22 has been out of support since September 2024 and shipping against an unsupported release only moves the problem. CI runs 17, 21, 25, and 26. @@ -226,7 +257,16 @@ The engine has no DDL yet, so there is no `CREATE NODE TABLE` and no statement i mvn test -Dzu.library=/path/to/libzu.dylib ``` -The provider looks at `-Dzu.library`, then `ZU_LIBRARY`, then the platform library path. The tests skip rather than fail when no `libzu` is reachable, so a checkout with no engine build beside it is still green. +The library is looked for in four places, in order: `-Dzu.library`, then `ZU_LIBRARY`, then a `zudb-native` artifact on the class path, then the platform's own search. A named path is first because a bisect and a bug report both start by pointing this at a build, and the platform's search is last because it is the one that can pick up a library nobody in the process chose. `Zu.library()` and `Zu.source()` say which file was loaded and which of the four it came from, and a failure to bind lists what was ruled out on the way. The tests skip rather than fail when no `libzu` is reachable, so a checkout with no engine build beside it is still green. + +The `zudb-native` module is not built unless it is asked for, because what it packages is downloaded rather than compiled: + +```sh +scripts/stage-natives.sh v0.11.0 +mvn -Pnatives package -DskipTests +``` + +The argument is a release tag of the engine, which is fetched with `gh`, or a directory that already holds the archives. The benchmarks are JMH and are not published: diff --git a/pom.xml b/pom.xml index 7e566a5..0e4b882 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,7 @@ 3.4.0 3.12.0 3.6.2 + 3.6.2 3.2.8 0.11.0 @@ -155,6 +156,17 @@ + + + natives + + zudb-native + + + diff --git a/scripts/stage-natives.sh b/scripts/stage-natives.sh new file mode 100755 index 0000000..ee03a60 --- /dev/null +++ b/scripts/stage-natives.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Lay the seven libzu builds out where zudb-native packages them from. +# +# Usage: scripts/stage-natives.sh +# +# scripts/stage-natives.sh v0.11.0 a tag of tamnd/zu +# scripts/stage-natives.sh ../zu/dist a directory of built prefixes +# +# The engine names its targets the way Rust does and this client names +# its platforms the way Go does, because that is what every other client +# of this engine names its artifacts after. The table below is the only +# place the two spellings meet, so a target added to the engine is one +# row here and nothing else. +# +# What is copied is the shared library and only the shared library. The +# archive also carries the static library, the header, the CLI, the +# pkg-config file and the CMake package, and none of those is anything a +# JVM can use: a jar that held them would be a jar that is four times +# the size for a file nobody opens. +set -euo pipefail + +source="${1:?usage: stage-natives.sh }" +here="$(cd "$(dirname "$0")/.." && pwd)" +out="$here/zudb-native/lib" + +# rust target, go platform, library file name +rows=" +x86_64-unknown-linux-gnu linux-amd64 libzu.so +aarch64-unknown-linux-gnu linux-arm64 libzu.so +x86_64-unknown-linux-musl linux-amd64-musl libzu.so +aarch64-unknown-linux-musl linux-arm64-musl libzu.so +x86_64-apple-darwin darwin-amd64 libzu.dylib +aarch64-apple-darwin darwin-arm64 libzu.dylib +x86_64-pc-windows-msvc windows-amd64 zu.dll +" + +work="" +if [ -d "$source" ]; then + prefixes="$source" +else + # A tag, which means the release archives. Downloaded once into a + # directory of this run's own, so that a second run of the script + # cannot half-unpack over the first. + work="$(mktemp -d)" + trap 'rm -rf "$work"' EXIT + prefixes="$work" + echo "downloading libzu $source from tamnd/zu" + for target in $(echo "$rows" | awk 'NF {print $1}'); do + archive="libzu-$target.tar.zst" + gh release download "$source" --repo tamnd/zu --pattern "$archive" --dir "$work" + # The documented fallback as well as the first choice, because + # tar learned --zstd in 1.31 and RHEL 8 ships 1.30. + if tar --zstd -tf "$work/$archive" >/dev/null 2>&1; then + tar --zstd -xf "$work/$archive" -C "$work" + else + zstd -dc "$work/$archive" | tar -xf - -C "$work" + fi + done +fi + +rm -rf "$out" +echo "$rows" | while read -r target platform library; do + [ -n "$target" ] || continue + from="$prefixes/libzu-$target" + # The library lives in bin/ on Windows, where a DLL is a thing that + # runs, and in lib/ everywhere else. + if [ -f "$from/bin/$library" ]; then + from="$from/bin/$library" + else + from="$from/lib/$library" + fi + if [ ! -f "$from" ]; then + echo "no $library for $target under $prefixes" >&2 + exit 1 + fi + mkdir -p "$out/$platform" + cp "$from" "$out/$platform/$library" + # A library a loader has to be able to map, whatever the transport + # did to the mode on the way here. + chmod 0755 "$out/$platform/$library" + echo "$platform $(du -h "$out/$platform/$library" | cut -f1)" +done diff --git a/zudb-native/pom.xml b/zudb-native/pom.xml new file mode 100644 index 0000000..ede9232 --- /dev/null +++ b/zudb-native/pom.xml @@ -0,0 +1,163 @@ + + + + 4.0.0 + + + dev.zudb + zudb-parent + 0.11.0-SNAPSHOT + + + zudb-native + zu for the JVM: the library + libzu for every platform the JVM client supports, as a jar. + + + + ${project.basedir}/lib + + + + + + ${zu.natives.dir} + dev/zudb/native + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven.enforcer.plugin.version} + + + the libraries are staged + enforce + + + + + ${zu.natives.dir}/linux-amd64/libzu.so + ${zu.natives.dir}/linux-arm64/libzu.so + ${zu.natives.dir}/linux-amd64-musl/libzu.so + ${zu.natives.dir}/linux-arm64-musl/libzu.so + ${zu.natives.dir}/darwin-amd64/libzu.dylib + ${zu.natives.dir}/darwin-arm64/libzu.dylib + ${zu.natives.dir}/windows-amd64/zu.dll + + + lib/ has not been staged: run scripts/stage-natives.sh first + + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + dev.zudb.natives + + + + + + linux-amd64 + jar + + linux-amd64 + dev/zudb/native/linux-amd64/** + + + + linux-arm64 + jar + + linux-arm64 + dev/zudb/native/linux-arm64/** + + + + linux-amd64-musl + jar + + linux-amd64-musl + dev/zudb/native/linux-amd64-musl/** + + + + linux-arm64-musl + jar + + linux-arm64-musl + dev/zudb/native/linux-arm64-musl/** + + + + darwin-amd64 + jar + + darwin-amd64 + dev/zudb/native/darwin-amd64/** + + + + darwin-arm64 + jar + + darwin-arm64 + dev/zudb/native/darwin-arm64/** + + + + windows-amd64 + jar + + windows-amd64 + dev/zudb/native/windows-amd64/** + + + + + + + diff --git a/zudb/src/main/java/dev/zudb/Library.java b/zudb/src/main/java/dev/zudb/Library.java index 8b4781b..bd8308c 100644 --- a/zudb/src/main/java/dev/zudb/Library.java +++ b/zudb/src/main/java/dev/zudb/Library.java @@ -35,6 +35,9 @@ final class Library { private Library() {} + /** The module the library artifacts name themselves, on the module path. */ + static final String NATIVE_MODULE = "dev.zudb.natives"; + /** * The library, and how it was found. * @@ -42,8 +45,10 @@ private Library() {} * search for it * @param source a phrase naming where the path came from, for the log line * and for a failure + * @param looked the places tried before this one, in order, so that a + * failure can say what was ruled out rather than only what was left */ - record Found(Path path, String source) {} + record Found(Path path, String source, List looked) {} static Found find() { List 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.