Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ jobs:
-Dchdb.cache.dir="$PWD/target/ci-cache" \
org.chdb.examples.QuickStart

- name: Consume the driver as a repository artifact
# Issue #10: nothing had ever depended on this as a published artifact, and a reactor
# build cannot find the things a consumer hits -- the POMs come from target/, the BOM is
# never imported, and the native libraries are read from a directory rather than
# unpacked from a JAR in a local repository.
#
# The script deploys to a throwaway file:// repository and resolves from it in a project
# outside this checkout with its own empty local repository. Run here, on every platform,
# so that the answer does not go stale between releases; the one thing it cannot cover is
# a real network transfer, which only Central can.
run: scripts/verify-consumer.sh ${{ matrix.platform }}

- name: Archive debug symbols
# Kept out of the runtime JAR (they are several times its size) but needed to read a
# crash report, so they are archived per build.
Expand Down
527 changes: 527 additions & 0 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ chdb-examples/ runnable examples
scripts/engine.properties the pinned engine version and its checksums
scripts/fetch-libchdb.sh downloads and verifies the pinned engine
scripts/build-native.sh builds the shim and stages a platform package
scripts/verify-consumer.sh resolves the driver from a repository, outside this checkout
scripts/check-release-tag.sh refuses a release whose tag does not name the commit
```

## Reporting a problem
Expand Down
37 changes: 37 additions & 0 deletions chdb-jdbc/src/main/java/org/chdb/internal/NativeLibraryLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
import java.nio.file.attribute.PosixFilePermission;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
Expand Down Expand Up @@ -278,6 +280,13 @@ private static LoadedRuntime loadFromJavaLibraryPath(Platform platform) {
System.loadLibrary("chdb");
System.loadLibrary("chdb_java_jni");
} catch (UnsatisfiedLinkError e) {
// Naming the packages that *are* on the classpath, when they are for some other
// machine. Without it this message reads the same whether a consumer declared no
// platform package at all or declared the wrong one, and the second is the more
// likely mistake and the more confusing to read: they can see the dependency in
// their POM and are being told to add it. Measured by scripts/verify-consumer.sh,
// which reproduces both cases from a Maven repository.
String foreign = describeForeignPackages(platform);
throw new ChdbNativeException(
"No chDB native runtime for "
+ platform
Expand All @@ -286,6 +295,7 @@ private static LoadedRuntime loadFromJavaLibraryPath(Platform platform) {
+ " 1. -D" + PROP_LIBRARY_PATH + " (not set)\n"
+ " 2. a chdb-native-" + platform.id() + " JAR on the classpath (not found)\n"
+ " 3. java.library.path (" + System.getProperty("java.library.path") + ")\n"
+ foreign
+ "\nAdd the platform package for this machine:\n"
+ " <dependency>\n"
+ " <groupId>org.chdb</groupId>\n"
Expand All @@ -299,6 +309,33 @@ private static LoadedRuntime loadFromJavaLibraryPath(Platform platform) {
return verify(platform, null, null, "java.library.path", new LinkedHashMap<String, String>());
}

/**
* A sentence naming the platform packages on the classpath that are for another machine, or
* the empty string if there are none.
*
* <p>Never throws: this runs while building an error message, and a diagnostic that can
* fail turns a clear failure into an obscure one.
*/
private static String describeForeignPackages(Platform platform) {
try {
List<String> foreign = new ArrayList<>();
for (String id : NativePackage.foreignPackages(platform)) {
foreign.add("chdb-native-" + id);
}
if (foreign.isEmpty()) {
return "";
}
return "\nThe classpath does carry "
+ String.join(" and ", foreign)
+ ", which "
+ (foreign.size() == 1 ? "is" : "are")
+ " for a different machine. A platform package is not interchangeable: it"
+ " holds a shim linked for one architecture.\n";
} catch (RuntimeException | LinkageError ignored) {
return "";
}
}

private static LoadedRuntime loadPair(
Platform platform, Path engine, Path jni, String source, Map<String, String> manifest) {
// Engine first: the shim's dependency on @rpath/libchdb.so is satisfied from the
Expand Down
24 changes: 24 additions & 0 deletions chdb-jdbc/src/main/java/org/chdb/internal/NativePackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

Expand Down Expand Up @@ -67,6 +69,28 @@ static NativePackage find(Platform platform) {
return new NativePackage(platform, manifest, checksums);
}

/**
* Platform package ids on the classpath that are for a machine other than this one.
*
* <p>{@link #find(Platform)} looks only for the current platform's resources, so a JAR for
* another architecture is indistinguishable from no JAR at all -- which makes the "no
* native runtime" message read identically whether a consumer declared nothing or declared
* the wrong package. This is how the two are told apart. Diagnostics only: nothing here
* makes a foreign package loadable.
*/
static List<String> foreignPackages(Platform platform) {
List<String> found = new ArrayList<>();
for (String id : Platform.ALL_IDS) {
if (id.equals(platform.id())) {
continue;
}
if (loader().getResource(Platform.resourcePrefixFor(id) + "/manifest.properties") != null) {
found.add(id);
}
}
return found;
}

/**
* Parses {@code sha256sums.txt}, which is in {@code sha256sum(1)} format: a hex digest,
* whitespace, then the file name.
Expand Down
32 changes: 32 additions & 0 deletions chdb-jdbc/src/main/java/org/chdb/internal/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ public final class Platform {
public static final String LINUX_X86_64_GNU = "linux-x86_64-gnu";
public static final String LINUX_AARCH64_GNU = "linux-aarch64-gnu";

/**
* Every platform id V1 ships. Only used for diagnostics -- {@link #current()} answers the
* question that matters -- but a message that can say "the package you declared is for
* another machine" needs to know what the other machines are called.
*/
static final String[] ALL_IDS = {MACOS_AARCH64, MACOS_X86_64, LINUX_X86_64_GNU, LINUX_AARCH64_GNU};

private final String id;
private final String os;
private final String arch;
Expand Down Expand Up @@ -85,6 +92,31 @@ public String resourcePrefix() {
return "META-INF/chdb/native/" + os + "/" + arch;
}

/**
* The same prefix, for a platform this JVM is not running on.
*
* <p>There is no {@code Platform} instance for another machine, on purpose: one exists only
* for the detected triple, so nothing can accidentally load a package for the wrong
* architecture. Diagnostics still need to look for those packages on the classpath, which
* is what this is for.
*
* @throws IllegalArgumentException if the id is not one of {@link #ALL_IDS}
*/
static String resourcePrefixFor(String platformId) {
switch (platformId) {
case MACOS_AARCH64:
return "META-INF/chdb/native/macos/aarch64";
case MACOS_X86_64:
return "META-INF/chdb/native/macos/x86_64";
case LINUX_X86_64_GNU:
return "META-INF/chdb/native/linux/x86_64";
case LINUX_AARCH64_GNU:
return "META-INF/chdb/native/linux/aarch64";
default:
throw new IllegalArgumentException("not a V1 platform id: " + platformId);
}
}

@Override
public String toString() {
return id + " (os=" + os + " arch=" + arch + " libc=" + libc + ")";
Expand Down
23 changes: 23 additions & 0 deletions chdb-jdbc/src/test/java/org/chdb/internal/PlatformTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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 org.junit.jupiter.api.DisplayName;
Expand Down Expand Up @@ -74,4 +75,26 @@ void currentPlatform() {
// Detection is cached, so repeated calls must agree.
assertEquals(platform.id(), Platform.current().id());
}

@Test
@DisplayName("resourcePrefixFor covers every V1 id and agrees with the detected platform's own")
void resourcePrefixForEveryId() {
// Two ways of computing the same path -- one from the detected triple, one from an id
// for a machine this JVM is not -- and the diagnostic that tells a consumer they
// declared the wrong platform package depends on them matching. A new platform added
// to ALL_IDS without a case in the switch fails here rather than in a message.
assertEquals(4, Platform.ALL_IDS.length);
for (String id : Platform.ALL_IDS) {
String prefix = Platform.resourcePrefixFor(id);
assertTrue(prefix.startsWith("META-INF/chdb/native/"), prefix);
}
Platform platform = Platform.current();
assertEquals(platform.resourcePrefix(), Platform.resourcePrefixFor(platform.id()));
}

@Test
@DisplayName("resourcePrefixFor rejects an id that is not a V1 platform")
void resourcePrefixForRejectsUnknown() {
assertThrows(IllegalArgumentException.class, () -> Platform.resourcePrefixFor("windows-x86_64"));
}
}
12 changes: 12 additions & 0 deletions docs/native-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ Every load failure names what was tried and what to do. The ones you are most li
**No platform package.** Lists all three locations that were searched and gives the Maven
coordinate for your platform.

**The wrong platform package.** The same message, plus a line naming the `chdb-native-*` JARs
that *are* on the classpath. Without that line the two cases read identically, which is worst
for the more common one: someone looking at a `chdb-native-linux-x86_64-gnu` dependency in
their POM being told to add a platform package.

Both were found by `scripts/verify-consumer.sh`, and both are still reproduced by it on every
push: it resolves the driver from a repository rather than from the reactor and then runs it
twice, once with `chdb-jdbc` alone and once with a platform package for another architecture.
It asserts on the text of each message — the coordinate the machine needs in both cases, and
the "classpath does carry" line in the second — so neither message can quietly stop saying
what this section says it says. `build` runs it on all four platforms.

**Unsupported platform.** Names the detected `os.name`/`os.arch`/libc and the supported set.
musl Linux is detected specifically — via `/proc/self/maps`, falling back to `ldd --version` —
and reported as unsupported rather than left to fail as a link error.
Expand Down
66 changes: 57 additions & 9 deletions docs/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ needs one DNS record, and one licence question needs somebody with authority to
| Publishing plugin | **done** — `central-publishing-maven-plugin`, `autoPublish=false` |
| Third-party licence inventory | **done** — generated from the engine, shipped in the package, drift-tested |
| `org.chdb` namespace | **not started** — needs a DNS TXT record, and there is no fallback |
| A way to stage all four platforms for one release | **not started** — issue #15 |
| A way to stage all four platforms for one release | **done** — `.github/workflows/release.yml`, issue #15 |
| Proof that the artifacts work when consumed from a repository | **done, locally** — `scripts/verify-consumer.sh`, issue #10 |
| GPG key for the project | **not started** — needs a decision about whose key |
| Position on the LGPL components | **not started** — needs chdb-io |

Expand All @@ -42,6 +43,12 @@ mvn versions:set -DnewVersion=26.7.0.1 # a release, not the -SNAPSHOT in the
mvn -Prelease deploy
```

**In practice, do not do this by hand.** `.github/workflows/release.yml` does all of it, one
platform per runner, and is the only way to get macOS x86_64 without an Intel Mac. The manual
sequence above is kept because it is what the workflow runs, and because reading it is how you
understand what the workflow is for. Section 6 is the running order; `docs/release-readiness.md`
is the checklist.

**Staging is not optional and Maven does not do it.** The two shared libraries are put into
`target/native/` by `scripts/build-native.sh`, and the native modules pick them up as a resource
directory. Run `mvn -Prelease deploy` on a clean checkout and each native module packages an
Expand All @@ -63,10 +70,38 @@ release profile rejects.
purpose: a shim linked for another architecture fails at `System.load()` in a user's JVM rather
than at build time. Linux is covered from either host by the container helper — given a Linux
JDK to point `JAVA_HOME` at, which a macOS machine does not have lying around — but macOS
x86_64 needs an Intel Mac. CI already builds all four on their own runners and does not currently
upload the packaged jars, so assembling a release means either four machines or a release
workflow. That gap is issue #15 and should be closed before the first release rather than
worked around by hand.
x86_64 needs an Intel Mac. So assembling a release means either four machines or a release
workflow, and it is a workflow: `.github/workflows/release.yml` stages each platform on its own
runner, uploads `target/native/` as an artifact, and downloads all four into one deploy job
that runs `mvn -Prelease deploy`. That was issue #15.

Three things about it that are decisions rather than mechanics, recorded at length in the
workflow's own header and summarised here:

- **The version lives in git, not in the workflow.** A release is a commit that carries the
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
release version in its POMs plus a tag on that commit. `versions:set` in CI would publish
bytes that correspond to no commit, so `git show <tag>` could not tell you what went out.
The workflow enforces this on **both** trigger paths: before anything is staged it looks up
`v<POM version>` and refuses unless that tag exists and points at the commit being built. So
`workflow_dispatch` with `channel=release` needs the tag as much as a tag push does — an
earlier version of this workflow checked only the tag name in `github.ref_name`, which is a
branch on a manual run, and a manual release could therefore deploy an untagged commit. A
guarantee with a manual bypass is not one.
- **The same workflow publishes snapshots**, and that is the default for a manual run. It is
the only way to move issue #10, and running the release path for every snapshot is what makes
it already-tested when a release depends on it. The publishing plugin routes a `-SNAPSHOT`
deploy to Central's snapshot repository rather than building a portal bundle, so
`autoPublish` does not apply there.
- **The builds are not bit-identical, and the gap was measured rather than left open.** Two
clean builds of the macos-aarch64 shim from the same source on the same machine differ in
116 bytes of the same-length file: 16 bytes of `LC_UUID`, four bytes holding the per-object
`N_OSO` debug-map timestamps, and ~96 bytes of ad-hoc code signature covering those. `__text`,
`__cstring`, `__const` and `__data` are byte-identical, so the shim's content reproduces and
only build metadata does not. Since the shim is ~130 KB of a ~326 MB package and the engine
half *is* reproducible — pinned by SHA-256 in `scripts/engine.properties` and verified by
`fetch-libchdb.sh` — chasing the remaining bytes would buy nothing a consumer can act on.
What the workflow does instead is record what it shipped: every staged library's SHA-256 goes
into the job summary and into `manifest.properties` inside the JAR.

The version matters too: `deploy` on the `26.7.0.1-SNAPSHOT` currently in the POM publishes a
snapshot, which goes to a different place and is never validated or promoted. A Central release
Expand Down Expand Up @@ -315,12 +350,25 @@ Each step can invalidate the next, so:
</repository>
```

With both in place this is the cheap step that finds everything the reactor build cannot:
whether the POMs resolve from a repository, whether the BOM behaves, whether declaring one
platform package yields a working driver, whether a 166 MB artifact uploads. Tracked as
This used to be described as the step that finds everything the reactor build cannot. Most
of it can now be found locally, without publishing anything:

```bash
scripts/verify-consumer.sh
```

That deploys to a throwaway `file://` repository and resolves from it in a project outside
this checkout, with its own empty local repository — so the POMs, the BOM import, the
transitive `chdb-jdbc` dependency, the unpack-from-JAR path and both "wrong platform
package" error messages are all exercised against a repository rather than a reactor. What
it cannot exercise is a network transfer: a `file://` deploy is a copy. **So the only thing
left for a real snapshot to prove is that a 98–166 MB artifact uploads and downloads
intact** — which is worth knowing, and is the cheapest possible way to know it. Tracked as
issue #10.
4. Then the first real release, with `autoPublish=false` so the bundle is reviewed in the portal
before it becomes permanent.
before it becomes permanent. `.github/workflows/release.yml` does the assembly; the tag has
to be on a commit whose POMs already carry the release version, and whose `build` run was
green.

## 7. Who can do what

Expand Down
Loading
Loading