1616import ProjectExtensions.configureJarManifest
1717import ProjectExtensions.configureMavenPublication
1818import de.undercouch.gradle.tasks.download.Download
19+ import java.io.ByteArrayOutputStream
20+ import javax.inject.Inject
21+ import org.gradle.api.GradleException
22+ import org.gradle.process.ExecOperations
1923
2024plugins {
2125 id(" project.java" )
@@ -48,72 +52,256 @@ configureJarManifest {
4852 */
4953val jnaDownloadsDir = rootProject.file(" build/jnaLibs/downloads/" ).path
5054val jnaResourcesDir = rootProject.file(" build/jnaLibs/resources/" ).path
55+
56+ tasks.clean { delete(rootProject.file(" build/jnaLibs" )) }
57+
5158val jnaLibPlatform: String =
5259 if (com.sun.jna.Platform .RESOURCE_PREFIX .startsWith(" darwin" )) " darwin" else com.sun.jna.Platform .RESOURCE_PREFIX
53- val jnaLibsPath: String = System .getProperty(" jnaLibsPath" , " ${jnaResourcesDir}${jnaLibPlatform} " )
60+ // When -DjnaLibsPath is set, the user wants to use a pre-existing local copy of the libmongocrypt
61+ // binaries instead of fetching them from the libmongocrypt GitHub release, so we skip the whole
62+ // download / verify / extract chain.
63+ val userSuppliedJnaLibsPath: String? = System .getProperty(" jnaLibsPath" )
64+ val jnaLibsPath: String = userSuppliedJnaLibsPath ? : " ${jnaResourcesDir} /${jnaLibPlatform} "
5465val jnaResources: String = System .getProperty(" jna.library.path" , jnaLibsPath)
5566
56- // Download jnaLibs that match the git tag or revision to jnaResourcesBuildDir
57- val downloadRevision = " 1.17.3"
58- val binariesArchiveName = " libmongocrypt-java.tar.gz"
67+ // Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir.
68+ // To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build.
69+ val downloadRevision = " 1.18.1"
70+ val downloadUrlBase = " https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision "
5971
6072/* *
61- * The name of the archive includes downloadRevision to ensure that:
62- * - the archive is downloaded if the revision changes.
63- * - the archive is not downloaded if the revision is the same and archive had already been saved in build output.
73+ * Maps a JNA platform key (the directory consumed by `jna.library.path`) to the libmongocrypt GitHub release tarball
74+ * that ships its native library, plus the path of that library inside the tarball. The tarball name and its internal
75+ * layout differ per platform, so both must be tracked explicitly.
76+ *
77+ * libmongocrypt's signature assets replace the `.tar.gz` suffix with `.asc` (e.g.
78+ * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-1.18.1.asc`).
6479 */
65- val localBinariesArchiveName = " libmongocrypt-java-$downloadRevision .tar.gz"
66-
67- val downloadUrl: String =
68- " https://mciuploads.s3.amazonaws.com/libmongocrypt/java/$downloadRevision /$binariesArchiveName "
80+ data class CryptBinary (val jnaPlatform : String , val tarball : String , val libPathInTarball : String ) {
81+ val signature: String = tarball.removeSuffix(" .tar.gz" ) + " .asc"
82+ }
6983
70- val jnaMapping: Map <String , String > =
71- mapOf (
72- " rhel-62-64-bit" to " linux-x86-64" ,
73- " rhel72-zseries-test" to " linux-s390x" ,
74- " rhel-71-ppc64el" to " linux-ppc64le" ,
75- " ubuntu1604-arm64" to " linux-aarch64" ,
76- " windows-test" to " win32-x86-64" ,
77- " macos" to " darwin" )
84+ val cryptBinaries: List <CryptBinary > =
85+ listOf (
86+ CryptBinary (
87+ " linux-x86-64" ,
88+ " libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-$downloadRevision .tar.gz" ,
89+ " lib64/libmongocrypt.so" ),
90+ CryptBinary (
91+ " linux-s390x" ,
92+ " libmongocrypt-linux-s390x-glibc_2_7-nocrypto-$downloadRevision .tar.gz" ,
93+ " lib64/libmongocrypt.so" ),
94+ CryptBinary (
95+ " linux-ppc64le" ,
96+ " libmongocrypt-linux-ppc64le-glibc_2_17-nocrypto-$downloadRevision .tar.gz" ,
97+ " lib64/libmongocrypt.so" ),
98+ CryptBinary (
99+ " linux-aarch64" ,
100+ " libmongocrypt-linux-arm64-glibc_2_17-nocrypto-$downloadRevision .tar.gz" ,
101+ " lib64/libmongocrypt.so" ),
102+ CryptBinary (" win32-x86-64" , " libmongocrypt-windows-x86_64-$downloadRevision .tar.gz" , " bin/mongocrypt.dll" ),
103+ CryptBinary (" darwin" , " libmongocrypt-macos-universal-$downloadRevision .tar.gz" , " lib/libmongocrypt.dylib" ))
78104
79105sourceSets { main { java { resources { srcDirs(jnaResourcesDir) } } } }
80106
81- tasks.register<Download >(" downloadJava" ) {
82- src(downloadUrl)
83- dest(" ${jnaDownloadsDir} /$localBinariesArchiveName " )
84- overwrite(true )
85- /* To make sure we don't download archive with binaries if it hasn't been changed in S3 bucket since last download.*/
86- onlyIfModified(true )
87- }
107+ /* *
108+ * Public key used to sign libmongocrypt release tarballs. See:
109+ * https://www.mongodb.com/docs/manual/tutorial/verify-mongodb-packages/#std-label-verify-pkgs
110+ */
111+ val libmongocryptPublicKeyUrl = " https://pgp.mongodb.com/libmongocrypt.pub"
112+ val libmongocryptPublicKeyFile = " libmongocrypt.pub"
113+
114+ tasks.register<Download >(" downloadCryptLibs" ) {
115+ src(
116+ cryptBinaries.flatMap { listOf (" $downloadUrlBase /${it.tarball} " , " $downloadUrlBase /${it.signature} " ) } +
117+ libmongocryptPublicKeyUrl)
118+ dest(jnaDownloadsDir)
119+ /* Reuse already-downloaded files. Useful for offline builds and reduces network churn. */
120+ overwrite(false )
121+ quiet(true )
122+
123+ /* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
124+ onlyIf { userSuppliedJnaLibsPath == null }
88125
89- tasks.register<Copy >(" unzipJava" ) {
90- /*
91- Clean up the directory first if the task is not UP-TO-DATE.
92- This can happen if the download revision has been changed and the archive is downloaded again.
93- */
94126 doFirst {
95- println (" Cleaning up $jnaResourcesDir " )
96- delete(jnaResourcesDir)
127+ val missing = cryptBinaries.filter { ! file(" $jnaDownloadsDir /${it.tarball} " ).exists() }
128+ if (missing.isNotEmpty()) {
129+ logger.lifecycle(" Downloading libmongocrypt $downloadRevision binaries:" )
130+ missing.forEach { logger.lifecycle(" ${it.tarball} " ) }
131+ }
132+ }
133+ }
134+
135+ /*
136+ * Verify the signature of every downloaded libmongocrypt tarball before extracting it.
137+ * Per DRIVERS-3441, drivers that bundle libmongocrypt must verify GPG signatures of
138+ * release tarballs against the official MongoDB libmongocrypt signing key.
139+ *
140+ * The keyring is kept under `build/` so this task does not touch the developer's
141+ * system GPG keyring and so `./gradlew clean` resets the trust state.
142+ */
143+ val skipCryptVerify = providers.gradleProperty(" skipCryptVerify" ).map { it.toBoolean() }.orElse(false )
144+
145+ abstract class VerifyLibmongocryptTask : DefaultTask () {
146+ @get:Inject abstract val execOps: ExecOperations
147+
148+ @get:InputFiles abstract val tarballs: ConfigurableFileCollection
149+ @get:InputFiles abstract val signatures: ConfigurableFileCollection
150+ @get:InputFile abstract val publicKey: RegularFileProperty
151+ @get:Input abstract val skipVerify: Property <Boolean >
152+ @get:Input abstract val expectedFingerprint: Property <String >
153+ @get:OutputFile abstract val verificationStamp: RegularFileProperty
154+
155+ /* Scratch keyring directory. Marked @Internal (not @OutputDirectory) because the directory is
156+ * genuinely ephemeral - nothing downstream consumes it. */
157+ @get:Internal abstract val gnupgHome: DirectoryProperty
158+
159+ @TaskAction
160+ fun verify () {
161+ if (skipVerify.get()) {
162+ logger.warn(
163+ " SKIPPING libmongocrypt signature verification because -PskipCryptVerify=true was set. " +
164+ " Do not use this for release builds." )
165+ verificationStamp.get().asFile.writeText(" Skipped verification at ${System .currentTimeMillis()} " )
166+ return
167+ }
168+
169+ try {
170+ execOps.exec {
171+ commandLine(" gpg" , " --version" )
172+ standardOutput = ByteArrayOutputStream ()
173+ }
174+ } catch (e: Exception ) {
175+ throw GradleException (
176+ " gpg is required to verify libmongocrypt tarballs since 1.18.0 but was not found on PATH. " +
177+ " Install gpg (e.g. `apt-get install gnupg`, `brew install gnupg`, Gpg4win on Windows), " +
178+ " or pass -PskipCryptVerify=true for offline development builds." ,
179+ e)
180+ }
181+
182+ val home =
183+ gnupgHome.get().asFile.apply {
184+ deleteRecursively()
185+ mkdirs()
186+ // GPG refuses to use a homedir with permissions broader than the owner.
187+ setReadable(false , false )
188+ setReadable(true , true )
189+ setWritable(false , false )
190+ setWritable(true , true )
191+ setExecutable(false , false )
192+ setExecutable(true , true )
193+ }
194+
195+ execOps.exec {
196+ commandLine(
197+ " gpg" ,
198+ " --homedir" ,
199+ home.path,
200+ " --batch" ,
201+ " --quiet" ,
202+ " --no-autostart" ,
203+ " --import" ,
204+ publicKey.get().asFile.path)
205+ standardOutput = ByteArrayOutputStream ()
206+ errorOutput = ByteArrayOutputStream ()
207+ }
208+
209+ try {
210+ execOps.exec {
211+ commandLine(
212+ " gpg" ,
213+ " --homedir" ,
214+ home.path,
215+ " --batch" ,
216+ " --no-autostart" ,
217+ " --with-colons" ,
218+ " --fingerprint" ,
219+ expectedFingerprint.get())
220+ standardOutput = ByteArrayOutputStream ()
221+ errorOutput = ByteArrayOutputStream ()
222+ }
223+ } catch (e: Exception ) {
224+ throw GradleException (
225+ " Imported libmongocrypt signing key fingerprint does not match expected value " +
226+ " ${expectedFingerprint.get()} . The downloaded public key may have been rotated." ,
227+ e)
228+ }
229+
230+ // Pair tarballs with signatures by basename; ConfigurableFileCollection.files is an
231+ // unordered Set, so zipping the two collections could mismatch pairs.
232+ val signaturesByName = signatures.files.associateBy { it.name }
233+ tarballs.files.forEach { tarball ->
234+ val signatureName = tarball.name.removeSuffix(" .tar.gz" ) + " .asc"
235+ val signature =
236+ signaturesByName[signatureName]
237+ ? : throw GradleException (
238+ " Missing signature $signatureName for ${tarball.name} ; expected it next to the tarball." )
239+ val verifyErr = ByteArrayOutputStream ()
240+ try {
241+ execOps.exec {
242+ commandLine(
243+ " gpg" ,
244+ " --homedir" ,
245+ home.path,
246+ " --batch" ,
247+ " --quiet" ,
248+ " --no-autostart" ,
249+ " --trust-model" ,
250+ " always" ,
251+ " --verify" ,
252+ signature.path,
253+ tarball.path)
254+ standardOutput = ByteArrayOutputStream ()
255+ errorOutput = verifyErr
256+ }
257+ } catch (e: Exception ) {
258+ throw GradleException (
259+ " GPG signature verification failed for ${tarball.name} :\n ${verifyErr.toString().trim()} " , e)
260+ }
261+ }
262+
263+ verificationStamp
264+ .get()
265+ .asFile
266+ .writeText(
267+ " verified=${System .currentTimeMillis()} \n " + " tarballs=${tarballs.files.joinToString { it.name }} \n " )
268+ }
269+ }
270+
271+ tasks.register<VerifyLibmongocryptTask >(" verifyCryptLibs" ) {
272+ dependsOn(" downloadCryptLibs" )
273+ tarballs.from(cryptBinaries.map { " $jnaDownloadsDir /${it.tarball} " })
274+ signatures.from(cryptBinaries.map { " $jnaDownloadsDir /${it.signature} " })
275+ publicKey.set(file(" $jnaDownloadsDir /$libmongocryptPublicKeyFile " ))
276+ skipVerify.set(skipCryptVerify)
277+ expectedFingerprint.set(" F2F5BF4ABF517E039AFCADAA81F1404DEBACA586" )
278+ gnupgHome.set(layout.buildDirectory.dir(" jnaLibs/gnupg" ))
279+ verificationStamp.set(layout.buildDirectory.file(" jnaLibs/verified.stamp" ))
280+
281+ /* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
282+ onlyIf { userSuppliedJnaLibsPath == null }
283+ }
284+
285+ tasks.register<Sync >(" extractCryptLibs" ) {
286+ cryptBinaries.forEach { spec ->
287+ from(tarTree(resources.gzip(" $jnaDownloadsDir /${spec.tarball} " ))) {
288+ include(spec.libPathInTarball)
289+ eachFile { path = " ${spec.jnaPlatform} /${name} " }
290+ includeEmptyDirs = false
291+ }
97292 }
98- from(tarTree(resources.gzip(" ${jnaDownloadsDir} /$localBinariesArchiveName " )))
99- include(
100- jnaMapping.keys.flatMap {
101- listOf (
102- " ${it} /nocrypto/**/libmongocrypt.so" , " ${it} /lib/**/libmongocrypt.dylib" , " ${it} /bin/**/mongocrypt.dll" )
103- })
104- eachFile { path = " ${jnaMapping[path.substringBefore(" /" )]} /${name} " }
105293 into(jnaResourcesDir)
106- dependsOn(" downloadJava " )
294+ dependsOn(" downloadCryptLibs " , " verifyCryptLibs " )
107295
108- doLast { println (" jna.library.path contents: \n ${fileTree(jnaResourcesDir).files.joinToString(" ,\n " )} " ) }
296+ /* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
297+ onlyIf { userSuppliedJnaLibsPath == null }
109298}
110299
111300// The `processResources` task (defined by the `java-library` plug-in) consumes files in the main
112- // source set.
113- // Add a dependency on `unzipJava`. `unzipJava` adds libmongocrypt libraries to the main source set.
114- tasks.processResources { mustRunAfter(tasks.named(" unzipJava" )) }
301+ // source set. Extraction must complete first so the native libraries are present.
302+ tasks.processResources { dependsOn(" extractCryptLibs" ) }
115303
116- tasks.register(" downloadJnaLibs" ) { dependsOn(" downloadJava " , " unzipJava " ) }
304+ tasks.register(" downloadJnaLibs" ) { dependsOn(" downloadCryptLibs " , " verifyCryptLibs " , " extractCryptLibs " ) }
117305
118306tasks.test {
119307 systemProperty(" jna.debug_load" , " true" )
@@ -122,10 +310,11 @@ tasks.test {
122310 testLogging { events(" passed" , " skipped" , " failed" ) }
123311
124312 doFirst {
125- println (" jna.library.path contents:" )
126- println (fileTree(jnaResources) { this .setIncludes(listOf (" *.*" )) }.files.joinToString(" ,\n " , " " ))
313+ logger.lifecycle(" jna.library.path contents:" )
314+ logger.lifecycle(
315+ fileTree(jnaResources) { this .setIncludes(listOf (" **/*.*" )) }.files.joinToString(" ,\n " , " " ))
127316 }
128- dependsOn(" downloadJnaLibs" , " downloadJava " , " unzipJava " )
317+ dependsOn(" downloadJnaLibs" )
129318}
130319
131320tasks.withType<AbstractPublishToMaven > {
@@ -134,8 +323,13 @@ tasks.withType<AbstractPublishToMaven> {
134323 | System properties:
135324 | =================
136325 |
137- | jnaLibsPath : Custom local JNA library path for inclusion into the build (rather than downloading from s3)
138- | gitRevision : Optional Git Revision to download the built resources for from s3.
326+ | jnaLibsPath : Custom local JNA library path to use at runtime (bypasses downloading/verifying/extracting libmongocrypt release artifacts).
327+ |
328+ | Project properties:
329+ | ===================
330+ |
331+ | skipCryptVerify : Pass -PskipCryptVerify=true to skip GPG verification of downloaded libmongocrypt tarballs.
332+ | Intended for offline development; do not use for release builds.
139333 """ .trimMargin()
140334}
141335
0 commit comments