@@ -700,6 +700,32 @@ def canonicalPath = { File file ->
700700 rootProject. relativePath(file). replace(File . separatorChar, ' /' as char )
701701}
702702
703+ def normalizeReleaseVersion = { String rawReleaseVersion ->
704+ if (rawReleaseVersion == null || rawReleaseVersion. trim(). empty) {
705+ throw new GradleException (' Missing required -PreleaseVersion=<version>, for example -PreleaseVersion=1.10.0' )
706+ }
707+
708+ def releaseVersion = rawReleaseVersion. trim()
709+ if (releaseVersion. startsWith(' v' )) {
710+ releaseVersion = releaseVersion. substring(1 )
711+ }
712+ if (! (releaseVersion ==~ / \d +\.\d +\.\d +([.-][A-Za-z0-9_.-]+)?/ )) {
713+ throw new GradleException (" Invalid release version '${ rawReleaseVersion} '. Expected a version such as 1.10.0 or v1.10.0." )
714+ }
715+
716+ releaseVersion
717+ }
718+
719+ def resolveGpgExecutable = {
720+ def configuredGpgExecutable = providers. gradleProperty(' gpgExecutable' ). orNull ?:
721+ providers. environmentVariable(' GPG_EXECUTABLE' ). orNull
722+ configuredGpgExecutable ?: [
723+ ' /opt/homebrew/bin/gpg' ,
724+ ' /usr/local/bin/gpg' ,
725+ ' /usr/bin/gpg'
726+ ]. find { new File (it). canExecute() } ?: ' gpg'
727+ }
728+
703729def enabledJarTasks = {
704730 allprojects. collectMany { project ->
705731 project. tasks. withType(Jar ). findAll { it. enabled }
@@ -2709,6 +2735,184 @@ tasks.register('verifyReleaseBuild') {
27092735 }
27102736}
27112737
2738+ tasks. register(' createReleaseJarTxt' ) {
2739+ group = ' distribution'
2740+ description = ' Creates Bisq-<version>.jar.txt from the four platform jar SHA-256 files.'
2741+
2742+ def releaseVersionProperty = providers. gradleProperty(' releaseVersion' )
2743+ def releaseDirProperty = providers. gradleProperty(' releaseDir' )
2744+ def jarSha256Inputs = [
2745+ [property : ' macosX86_64JarSha256' , label : ' macOS Intel' ],
2746+ [property : ' macosAarch64JarSha256' , label : ' macOS Apple Silicon' ],
2747+ [property : ' linuxJarSha256' , label : ' Linux' ],
2748+ [property : ' windowsJarSha256' , label : ' Windows' ]
2749+ ]
2750+
2751+ inputs. property(' releaseVersion' , releaseVersionProperty. orElse(' ' ))
2752+ inputs. property(' releaseDir' , releaseDirProperty. orElse(' ' ))
2753+ jarSha256Inputs. each { inputSpec ->
2754+ inputs. property(inputSpec. property, providers. gradleProperty(inputSpec. property). orElse(' ' ))
2755+ }
2756+ outputs. upToDateWhen { false }
2757+
2758+ doLast {
2759+ def releaseVersion = normalizeReleaseVersion(releaseVersionProperty. orNull)
2760+ def rawReleaseDir = releaseDirProperty. orNull
2761+ if (rawReleaseDir == null || rawReleaseDir. trim(). empty) {
2762+ throw new GradleException (' Missing required -PreleaseDir=<directory>, for example -PreleaseDir=/Users/ben/Documents/v1.10.0' )
2763+ }
2764+
2765+ def releaseDir = file(rawReleaseDir. trim())
2766+ if (! releaseDir. exists() && ! releaseDir. mkdirs()) {
2767+ throw new GradleException (" Could not create release directory: ${ releaseDir} " )
2768+ }
2769+ if (! releaseDir. directory) {
2770+ throw new GradleException (" Release directory is not a directory: ${ releaseDir} " )
2771+ }
2772+
2773+ def sourceFiles = jarSha256Inputs. collect { inputSpec ->
2774+ def rawPath = providers. gradleProperty(inputSpec. property). orNull
2775+ if (rawPath == null || rawPath. trim(). empty) {
2776+ throw new GradleException (" Missing required -P${ inputSpec.property} =<path> for the ${ inputSpec.label} jar SHA-256 file." )
2777+ }
2778+
2779+ def sourceFile = file(rawPath. trim())
2780+ if (! sourceFile. file) {
2781+ throw new GradleException (" Missing ${ inputSpec.label} jar SHA-256 file: ${ sourceFile} " )
2782+ }
2783+ sourceFile
2784+ }
2785+
2786+ def outputFile = new File (releaseDir, " Bisq-${ releaseVersion} .jar.txt" )
2787+ outputFile. withOutputStream { output ->
2788+ sourceFiles. each { sourceFile ->
2789+ def bytes = sourceFile. bytes
2790+ output. write(bytes)
2791+ if (bytes. length == 0 || bytes[bytes. length - 1 ] != ((byte ) 10 )) {
2792+ output. write(' \n ' . getBytes(StandardCharsets . UTF_8 . name()))
2793+ }
2794+ }
2795+ }
2796+
2797+ logger. lifecycle(" Wrote ${ outputFile.absolutePath} " )
2798+ logger. lifecycle(" Aggregated jar checksum sources:" )
2799+ sourceFiles. each { sourceFile ->
2800+ logger. lifecycle(" - ${ sourceFile.absolutePath} " )
2801+ }
2802+ }
2803+ }
2804+
2805+ tasks. register(' signReleaseArtifacts' ) {
2806+ group = ' distribution'
2807+ description = ' Signs release binary artifacts and Bisq-<version>.jar.txt in -PreleaseDir with detached armored GPG signatures.'
2808+
2809+ def releaseVersionProperty = providers. gradleProperty(' releaseVersion' )
2810+ def releaseDirProperty = providers. gradleProperty(' releaseDir' )
2811+ def gpgUserProperty = providers. gradleProperty(' gpgUser' )
2812+ def bisqGpgUserProperty = providers. gradleProperty(' bisqGpgUser' )
2813+ def bisqGpgUserEnvironment = providers. environmentVariable(' BISQ_GPG_USER' )
2814+ def gpgExecutableProperty = providers. gradleProperty(' gpgExecutable' )
2815+ def gpgExecutableEnvironment = providers. environmentVariable(' GPG_EXECUTABLE' )
2816+ def binaryExtensions = [' dmg' , ' deb' , ' rpm' , ' exe' , ' zip' , ' pkg' , ' msi' , ' tar.gz' , ' tgz' ]
2817+
2818+ inputs. property(' releaseVersion' , releaseVersionProperty. orElse(' ' ))
2819+ inputs. property(' releaseDir' , releaseDirProperty. orElse(' ' ))
2820+ inputs. property(' gpgUser' , gpgUserProperty. orElse(bisqGpgUserProperty). orElse(bisqGpgUserEnvironment). orElse(' ' ))
2821+ inputs. property(' gpgExecutable' , gpgExecutableProperty. orElse(gpgExecutableEnvironment). orElse(' ' ))
2822+ outputs. upToDateWhen { false }
2823+
2824+ doLast {
2825+ def releaseVersion = normalizeReleaseVersion(releaseVersionProperty. orNull)
2826+ def rawReleaseDir = releaseDirProperty. orNull
2827+ if (rawReleaseDir == null || rawReleaseDir. trim(). empty) {
2828+ throw new GradleException (' Missing required -PreleaseDir=<directory>, for example -PreleaseDir=/Users/ben/Documents/v1.10.0' )
2829+ }
2830+
2831+ def gpgUser = gpgUserProperty. orNull ?:
2832+ bisqGpgUserProperty. orNull ?:
2833+ bisqGpgUserEnvironment. orNull
2834+ if (gpgUser == null || gpgUser. trim(). empty) {
2835+ throw new GradleException (' Missing required -PgpgUser=<key-id-or-email>. You can also use -PbisqGpgUser=<key-id-or-email> or BISQ_GPG_USER.' )
2836+ }
2837+ gpgUser = gpgUser. trim()
2838+
2839+ def releaseDir = file(rawReleaseDir. trim())
2840+ if (! releaseDir. directory) {
2841+ throw new GradleException (" Release directory is not a directory: ${ releaseDir} " )
2842+ }
2843+
2844+ def jarTxtFileName = " Bisq-${ releaseVersion} .jar.txt"
2845+ def jarTxtFile = new File (releaseDir, jarTxtFileName)
2846+ if (! jarTxtFile. file) {
2847+ throw new GradleException (
2848+ " Missing ${ jarTxtFileName} in ${ releaseDir} . Create it with the createReleaseJarTxt task or desktop/package/macosx/finalize.sh before signing."
2849+ )
2850+ }
2851+
2852+ def releaseDirFiles = releaseDir. listFiles()
2853+ if (releaseDirFiles == null ) {
2854+ throw new GradleException (" Cannot list release directory: ${ releaseDir} . Check that it is readable." )
2855+ }
2856+
2857+ def artifacts = releaseDirFiles
2858+ .findAll { candidate ->
2859+ def signableFile = candidate. file &&
2860+ ! candidate. name. startsWith(' .' ) &&
2861+ ! candidate. name. endsWith(' .asc' )
2862+ def lowerName = candidate. name. toLowerCase(Locale . ROOT )
2863+ signableFile &&
2864+ (candidate. name == jarTxtFileName ||
2865+ binaryExtensions. any { extension -> lowerName. endsWith(" .${ extension} " ) })
2866+ }
2867+ .sort { left , right -> left. name <=> right. name }
2868+
2869+ if (artifacts. empty) {
2870+ throw new GradleException (" No release artifacts found to sign in ${ releaseDir} " )
2871+ }
2872+
2873+ def gpgExecutable = resolveGpgExecutable()
2874+ def failureDetails = { result ->
2875+ [result. stderr, result. stdout]
2876+ .findAll { it != null && ! it. trim(). empty }
2877+ .join(' \n ' )
2878+ .trim()
2879+ }
2880+
2881+ logger. lifecycle(" Signing ${ artifacts.size()} release artifact(s) in ${ releaseDir.absolutePath} " )
2882+ artifacts. each { artifact ->
2883+ def signatureFile = new File (artifact. parentFile, " ${ artifact.name} .asc" )
2884+ def signResult = execResult([
2885+ gpgExecutable,
2886+ ' --yes' ,
2887+ ' --digest-algo' , ' SHA256' ,
2888+ ' --local-user' , gpgUser,
2889+ ' --output' , signatureFile. absolutePath,
2890+ ' --detach-sig' ,
2891+ ' --armor' ,
2892+ artifact. absolutePath
2893+ ])
2894+ if (signResult. exitValue != 0 ) {
2895+ throw new GradleException (" Failed to sign ${ artifact.name} : ${ failureDetails(signResult)} " )
2896+ }
2897+ if (! signatureFile. file || signatureFile. length() == 0 ) {
2898+ throw new GradleException (" GPG did not create a non-empty signature file for ${ artifact.name} : ${ signatureFile} " )
2899+ }
2900+
2901+ def verifyResult = execResult([
2902+ gpgExecutable,
2903+ ' --verify' ,
2904+ signatureFile. absolutePath,
2905+ artifact. absolutePath
2906+ ])
2907+ if (verifyResult. exitValue != 0 ) {
2908+ throw new GradleException (" Failed to verify ${ signatureFile.name} : ${ failureDetails(verifyResult)} " )
2909+ }
2910+
2911+ logger. lifecycle(" Signed and verified ${ artifact.name} -> ${ signatureFile.name} " )
2912+ }
2913+ }
2914+ }
2915+
27122916tasks. register(' verifyGithubReleaseReadiness' ) {
27132917 group = ' verification'
27142918 description = ' Manually verifies GitHub release assets, Bisq download URLs, versions, and signer keys for -PreleaseVersion=<version>.'
@@ -2898,6 +3102,7 @@ tasks.register('verifyGithubReleaseReadiness') {
28983102 " Bisq-aarch64-${ releaseVersion} .dmg" . toString(),
28993103 " Bisq-aarch64-${ releaseVersion} .dmg.asc" . toString(),
29003104 " Bisq-${ releaseVersion} .jar.txt" . toString(),
3105+ " Bisq-${ releaseVersion} .jar.txt.asc" . toString(),
29013106 " Bisq-64bit-${ releaseVersion} .deb" . toString(),
29023107 " Bisq-64bit-${ releaseVersion} .deb.asc" . toString(),
29033108 " Bisq-64bit-${ releaseVersion} .rpm" . toString(),
@@ -2923,6 +3128,7 @@ tasks.register('verifyGithubReleaseReadiness') {
29233128 " Bisq-64bit-${ releaseVersion} .exe" . toString(),
29243129 " Bisq-64bit-${ releaseVersion} .exe.asc" . toString(),
29253130 " Bisq-${ releaseVersion} .jar.txt" . toString(),
3131+ " Bisq-${ releaseVersion} .jar.txt.asc" . toString(),
29263132 ' signingkey.asc'
29273133 ]. sort()
29283134
0 commit comments