From f42b1aee3484bdd3f9da902084d3d29fe829af34 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 09:40:38 +0100 Subject: [PATCH 1/5] JAVA-6057: fix verifyCryptLibs gpg path handling on Windows The gpg on the Evergreen Windows hosts is a Cygwin build that only understands POSIX paths. Handed native Windows paths (C:\dir) it treats them as relative, mangles the keyring location, and fails to find a writable keyring. Translate drive-letter paths to the Cygwin form (C:\dir -> /cygdrive/c/dir) on Windows for every gpg path argument (homedir, public key, signatures, tarballs); other platforms are unchanged. Also surface gpg stdout/stderr and the gnupgHome/publicKey state on failure so future gpg issues are diagnosable instead of showing only Gradle's opaque non-zero exit value. --- mongodb-crypt/build.gradle.kts | 119 ++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 208034beaa..6d6c625a8a 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -168,10 +168,45 @@ abstract class VerifyLibmongocryptTask : DefaultTask() { return } + // The gpg shipped on the Evergreen Windows hosts is a Cygwin build that only understands POSIX paths. + // Handed a native Windows path ("C:\dir") it treats the backslash path as relative, prepends its working + // directory, and fails with "no writable keyring found". Translate drive-letter paths to the Cygwin form + // ("C:\dir" -> "/cygdrive/c/dir") on Windows so every path argument (homedir, key, signatures, tarballs) + // is parsed correctly; other platforms pass paths through unchanged. + val isWindows = System.getProperty("os.name").startsWith("Windows", ignoreCase = true) + fun toGpgPath(path: String): String { + if (!isWindows) { + return path + } + val driveLetter = Regex("^([A-Za-z]):[\\\\/](.*)$").matchEntire(path) ?: return path.replace('\\', '/') + val (drive, rest) = driveLetter.destructured + return "/cygdrive/${drive.lowercase()}/${rest.replace('\\', '/')}" + } + + // Run gpg capturing both streams; on non-zero exit throw with the captured output appended so the + // underlying gpg diagnostic is visible instead of Gradle's opaque "finished with non-zero exit value N". + fun runGpg(vararg args: String, onFailure: (String) -> String) { + val out = ByteArrayOutputStream() + val err = ByteArrayOutputStream() + val result = + execOps.exec { + commandLine(listOf("gpg") + args) + standardOutput = out + errorOutput = err + isIgnoreExitValue = true + } + val combined = (out.toString().trim() + "\n" + err.toString().trim()).trim() + logger.info("gpg ${args.joinToString(" ")} -> exit ${result.exitValue}\n$combined") + if (result.exitValue != 0) { + throw GradleException("${onFailure(combined)}\ngpg command: gpg ${args.joinToString(" ")}") + } + } + + val versionOut = ByteArrayOutputStream() try { execOps.exec { commandLine("gpg", "--version") - standardOutput = ByteArrayOutputStream() + standardOutput = versionOut } } catch (e: Exception) { throw GradleException( @@ -180,6 +215,7 @@ abstract class VerifyLibmongocryptTask : DefaultTask() { "or pass -PskipCryptVerify=true for offline development builds.", e) } + logger.lifecycle("Using gpg:\n${versionOut.toString().trim().lineSequence().firstOrNull() ?: ""}") val home = gnupgHome.get().asFile.apply { @@ -193,41 +229,28 @@ abstract class VerifyLibmongocryptTask : DefaultTask() { setExecutable(false, false) setExecutable(true, true) } + val homedir = toGpgPath(home.path) + logger.lifecycle( + "libmongocrypt verify: gnupgHome=${home.path} -> $homedir (exists=${home.exists()}, " + + "writable=${home.canWrite()}), publicKey=${publicKey.get().asFile.path} " + + "(exists=${publicKey.get().asFile.exists()})") - execOps.exec { - commandLine( - "gpg", - "--homedir", - home.path, - "--batch", - "--quiet", - "--no-autostart", - "--import", - publicKey.get().asFile.path) - standardOutput = ByteArrayOutputStream() - errorOutput = ByteArrayOutputStream() + runGpg("--homedir", homedir, "--batch", "--no-autostart", "--import", toGpgPath(publicKey.get().asFile.path)) { + output -> + "Failed to import libmongocrypt signing key into scratch keyring at ${home.path}.\n$output" } - try { - execOps.exec { - commandLine( - "gpg", - "--homedir", - home.path, - "--batch", - "--no-autostart", - "--with-colons", - "--fingerprint", - expectedFingerprint.get()) - standardOutput = ByteArrayOutputStream() - errorOutput = ByteArrayOutputStream() - } - } catch (e: Exception) { - throw GradleException( + runGpg( + "--homedir", + homedir, + "--batch", + "--no-autostart", + "--with-colons", + "--fingerprint", + expectedFingerprint.get()) { output -> "Imported libmongocrypt signing key fingerprint does not match expected value " + - "${expectedFingerprint.get()}. The downloaded public key may have been rotated.", - e) - } + "${expectedFingerprint.get()}. The downloaded public key may have been rotated.\n$output" + } // Pair tarballs with signatures by basename; ConfigurableFileCollection.files is an // unordered Set, so zipping the two collections could mismatch pairs. @@ -238,28 +261,18 @@ abstract class VerifyLibmongocryptTask : DefaultTask() { signaturesByName[signatureName] ?: throw GradleException( "Missing signature $signatureName for ${tarball.name}; expected it next to the tarball.") - val verifyErr = ByteArrayOutputStream() - try { - execOps.exec { - commandLine( - "gpg", - "--homedir", - home.path, - "--batch", - "--quiet", - "--no-autostart", - "--trust-model", - "always", - "--verify", - signature.path, - tarball.path) - standardOutput = ByteArrayOutputStream() - errorOutput = verifyErr + runGpg( + "--homedir", + homedir, + "--batch", + "--no-autostart", + "--trust-model", + "always", + "--verify", + toGpgPath(signature.path), + toGpgPath(tarball.path)) { output -> + "GPG signature verification failed for ${tarball.name}:\n$output" } - } catch (e: Exception) { - throw GradleException( - "GPG signature verification failed for ${tarball.name}:\n${verifyErr.toString().trim()}", e) - } } verificationStamp From fab0222236f1b975f96b988e072eca7c4c721489 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 09:43:47 +0100 Subject: [PATCH 2/5] Fix server version checking for nested MQL asString. --- .../client/model/mql/TypeMqlValuesFunctionalTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java index 228dc3ede7..16c0c2f856 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java @@ -190,8 +190,8 @@ public void asStringTest() { } @Test - public void asStringTestNestedPre82() { - assumeTrue(serverVersionLessThan(8, 2)); + public void asStringTestNestedPre83() { + assumeTrue(serverVersionLessThan(8, 3)); // Arrays and documents are not (yet) supported: assertThrows(MongoCommandException.class, () -> @@ -202,7 +202,7 @@ public void asStringTestNestedPre82() { @Test public void asStringTestNested() { - assumeTrue(serverVersionAtLeast(8, 2)); + assumeTrue(serverVersionAtLeast(8, 3)); assertExpression("[1,2]", ofIntegerArray(1, 2).asString()); assertExpression("{\"a\":1}", of(Document.parse("{a: 1}")).asString()); From a5b7569b905f5abd37890cca9c6f06d6d58818e3 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 10:01:20 +0100 Subject: [PATCH 3/5] Set a default OS in setup-env.bash Should fix publishing of snapshots. JAVA-6057 --- .evergreen/setup-env.bash | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.evergreen/setup-env.bash b/.evergreen/setup-env.bash index a8a7840292..c8a96b0300 100644 --- a/.evergreen/setup-env.bash +++ b/.evergreen/setup-env.bash @@ -1,5 +1,15 @@ # Java configurations for evergreen +# On Windows Evergreen hosts `OS` is a native environment variable set to +# "Windows_NT". It is not set on other platforms, so default it from `uname` +# to avoid an unbound variable error under `set -u`. +if [ -z "${OS:-}" ]; then + case "$(uname -s)" in + CYGWIN*|MINGW*|MSYS*|Windows_NT) OS="Windows_NT" ;; + *) OS="$(uname -s)" ;; + esac +fi + if [ "Windows_NT" == "$OS" ]; then export JDK8="/cygdrive/c/java/jdk8" export JDK11="/cygdrive/c/java/jdk11" From 199c6a979860527a6e5dda50da36512fd0860619 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 10:21:40 +0100 Subject: [PATCH 4/5] Ignore drop errors in the shutdown hook. Brings it inline with the reactive streams shutdown hook. Ensures tests don't fail on shutdown with a DatabaseDropPending exception --- .../src/test/functional/com/mongodb/client/Fixture.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java index 8114d62e41..8e1227803b 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java +++ b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java @@ -54,7 +54,11 @@ public static synchronized MongoClient getMongoClient() { return; } if (defaultDatabase != null) { - defaultDatabase.drop(); + try { + defaultDatabase.drop(); + } catch (Exception e) { + // ignore + } } mongoClient.close(); mongoClient = null; From b6407b9689ac73f8733bb88356997b0e42a4411d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 15:52:14 +0100 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- mongodb-crypt/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6d6c625a8a..9858e283af 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -180,7 +180,7 @@ abstract class VerifyLibmongocryptTask : DefaultTask() { } val driveLetter = Regex("^([A-Za-z]):[\\\\/](.*)$").matchEntire(path) ?: return path.replace('\\', '/') val (drive, rest) = driveLetter.destructured - return "/cygdrive/${drive.lowercase()}/${rest.replace('\\', '/')}" + return "/cygdrive/${drive.lowercase(java.util.Locale.ROOT)}/${rest.replace('\\', '/')}" } // Run gpg capturing both streams; on non-zero exit throw with the captured output appended so the