mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 18:03:12 +01:00
1b07c8fd69
Substitute 2021 with 2022, and add date range for consistency with MacOS notice.
425 lines
21 KiB
Groovy
425 lines
21 KiB
Groovy
import java.time.LocalDateTime
|
|
import org.apache.tools.ant.taskdefs.condition.Os
|
|
|
|
import static groovy.io.FileType.*
|
|
|
|
task jpackageSanityChecks {
|
|
description 'Interactive sanity checks on the version of the code that will be packaged'
|
|
|
|
doLast {
|
|
// Enforce JDK 15 for packaging. This will ensure:
|
|
// - Java 15 is used to compile the jars
|
|
// - jpackager from JDK 15 is used to package the binaries
|
|
// - JRE 15 is bundled in the released binaries
|
|
// TODO Use jpackage flag "--runtime-image jdk-11" to include another version of JRE in generated binaries
|
|
// (But buggy in jpackager v15? Flag didn't work in last v15 test)
|
|
assert JavaVersion.current() == JavaVersion.VERSION_15: "JDK 15 is required when packaging"
|
|
|
|
executeCmd("git --no-pager log -5 --oneline")
|
|
ant.input(message: "Above you see the current HEAD and its recent history.\n" +
|
|
"Is this the right commit for packaging? (y=continue, n=abort)",
|
|
addproperty: "sanity-check-1",
|
|
validargs: "y,n")
|
|
if (ant.properties['sanity-check-1'] == 'n') {
|
|
ant.fail('Aborting')
|
|
}
|
|
|
|
executeCmd("git status --short --branch")
|
|
ant.input(message: "Above you see any local changes that are not in the remote branch.\n" +
|
|
"If you have any local changes, please abort, get them merged, get the latest branch and try again.\n" +
|
|
"Continue with packaging? (y=continue, n=abort)",
|
|
addproperty: "sanity-check-2",
|
|
validargs: "y,n")
|
|
if (ant.properties['sanity-check-2'] == 'n') {
|
|
ant.fail('Aborting')
|
|
}
|
|
}
|
|
}
|
|
|
|
task createNewTempFolder {
|
|
description 'Create new temp folder where the packaging files will be placed'
|
|
dependsOn 'jpackageSanityChecks'
|
|
|
|
doLast {
|
|
// The build directory will be deleted next time the clean task runs
|
|
// Therefore, we can use it to store any temp files (separate JDK for jpackage, etc) and resulting build artefacts
|
|
// We create a temp folder in the build directory which holds all jpackage-related artefacts (not just the final installers)
|
|
String tempRootDirName = 'temp-' + LocalDateTime.now().format('yyyy.MM.dd-HHmmssSSS')
|
|
File tempRootDir = new File(project.buildDir, tempRootDirName)
|
|
tempRootDir.mkdirs()
|
|
ext.tempRootDir = tempRootDir
|
|
println "Created temp root folder " + tempRootDir
|
|
|
|
File binariesFolderPath = new File(tempRootDir, "binaries")
|
|
binariesFolderPath.mkdirs()
|
|
ext.binariesFolderPath = binariesFolderPath
|
|
}
|
|
}
|
|
|
|
task packageInstallers {
|
|
description 'Call jpackage to prepare platform-specific binaries for this platform'
|
|
dependsOn 'createNewTempFolder'
|
|
dependsOn rootProject.clean
|
|
dependsOn ':desktop:build' // Full build needed for "desktop/build/app/lib", used for raspi package
|
|
|
|
doLast {
|
|
String jPackageFilePath = "jpackage" // Binary is in the PATH because we're running JDK v15
|
|
File binariesFolderPath = file(createNewTempFolder.property('binariesFolderPath'))
|
|
|
|
File tempRootDir = createNewTempFolder.property("tempRootDir") as File
|
|
// The jpackageTempDir stores temp files used by jpackage for building the installers
|
|
// It can be inspected in order to troubleshoot the packaging process
|
|
File jpackageTempDir = new File(tempRootDir, "jpackage-temp")
|
|
jpackageTempDir.mkdirs()
|
|
|
|
// ALL contents of this folder will be included in the resulting installers
|
|
// However, the fat jar is the only one we need
|
|
// Therefore, this location should point to a folder that ONLY contains the fat jar
|
|
// If later we will need to include other non-jar resources, we can do that by adding --resource-dir to the jpackage opts
|
|
String fatJarFolderPath = "${project(':desktop').buildDir}/libs/fatJar"
|
|
String mainJarName = shadowJar.getArchiveFileName().get()
|
|
|
|
delete(fatJarFolderPath)
|
|
mkdir(fatJarFolderPath)
|
|
copy {
|
|
from "${project(':desktop').buildDir}/libs/${mainJarName}"
|
|
into fatJarFolderPath
|
|
}
|
|
|
|
// We convert the fat jar into a deterministic one by stripping out comments with date, etc.
|
|
// jar file created from https://github.com/ManfredKarrer/tools
|
|
executeCmd("java -jar \"${project(':desktop').projectDir}/package/tools-1.0.jar\" ${fatJarFolderPath}/${mainJarName}")
|
|
|
|
// TODO For non-modular applications: use jlink to create a custom runtime containing only the modules required
|
|
|
|
// See jpackager argument documentation:
|
|
// https://docs.oracle.com/en/java/javase/15/docs/specs/man/jpackage.html
|
|
|
|
// Remove the -SNAPSHOT suffix from the version string (originally defined in build.gradle)
|
|
// Having it in would have resulted in an invalid version property for several platforms (mac, linux/rpm)
|
|
String appVersion = version.replaceAll("-SNAPSHOT", "")
|
|
println "Packaging Bisq version ${appVersion}"
|
|
|
|
// zip jar lib for Raspberry Pi only on macOS as it is only needed once for the release
|
|
if (Os.isFamily(Os.FAMILY_MAC)) {
|
|
println "Zipping jar lib for raspberry pi"
|
|
ant.zip(basedir: "${project(':desktop').buildDir}/app/lib",
|
|
destfile: "${binariesFolderPath}/jar-lib-for-raspberry-pi-${appVersion}.zip")
|
|
}
|
|
|
|
String appDescription = 'A decentralized bitcoin exchange network.'
|
|
String appCopyright = 'Copyright © 2013-2022 - The Bisq developers'
|
|
String appNameAndVendor = 'Bisq'
|
|
|
|
String commonOpts = new String(
|
|
// Generic options
|
|
" --dest \"${binariesFolderPath}\"" +
|
|
" --name ${appNameAndVendor}" +
|
|
" --description \"${appDescription}\"" +
|
|
" --app-version ${appVersion}" +
|
|
" --copyright \"${appCopyright}\"" +
|
|
" --vendor ${appNameAndVendor}" +
|
|
" --temp \"${jpackageTempDir}\"" +
|
|
|
|
// Options for creating the application image
|
|
" --input ${fatJarFolderPath}" +
|
|
|
|
// Options for creating the application launcher
|
|
" --main-jar ${mainJarName}" +
|
|
" --main-class bisq.desktop.app.BisqAppMain" +
|
|
" --java-options -Xss1280k" +
|
|
" --java-options -XX:MaxRAM=8g" +
|
|
" --java-options -XX:+UseG1GC" +
|
|
" --java-options -XX:MaxHeapFreeRatio=10" +
|
|
" --java-options -XX:MinHeapFreeRatio=5" +
|
|
" --java-options -XX:+UseStringDeduplication" +
|
|
" --java-options -Djava.net.preferIPv4Stack=true"
|
|
// Warning: this will cause guice reflection exceptions and lead to issues with the guice internal cache
|
|
// resulting in the UI not loading
|
|
// " --java-options -Djdk.module.illegalAccess=deny" +
|
|
)
|
|
|
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
|
// TODO Found no benefit in using --resource-dir "..package/windows", it has the same outcome as opts below
|
|
String windowsOpts = new String(
|
|
" --icon \"${project(':desktop').projectDir}/package/windows/Bisq.ico\"" +
|
|
" --resource-dir \"${project(':desktop').projectDir}/package/windows\"" +
|
|
" --win-dir-chooser" +
|
|
" --win-per-user-install" +
|
|
" --win-menu" +
|
|
" --win-shortcut"
|
|
)
|
|
|
|
executeCmd(jPackageFilePath + commonOpts + windowsOpts + " --type exe")
|
|
|
|
// Set the necessary permissions before calling signtool
|
|
executeCmd("\"attrib -R \"${binariesFolderPath}/Bisq-${appVersion}.exe\"\"")
|
|
|
|
// In addition to the groovy quotes around the string, the entire Windows command must also be surrounded
|
|
// by quotes, plus each path inside the command has to be quoted as well
|
|
// Reason for this is that the path to the called executable contains spaces
|
|
// See https://stackoverflow.com/questions/6376113/how-do-i-use-spaces-in-the-command-prompt/6378038#6378038
|
|
executeCmd("\"\"C:\\Program Files (x86)\\Windows Kits\\10\\App Certification Kit\\signtool.exe\" sign /v /fd SHA256 /a \"${binariesFolderPath}/Bisq-${appVersion}.exe\"\"")
|
|
} else if (Os.isFamily(Os.FAMILY_MAC)) {
|
|
// See https://docs.oracle.com/en/java/javase/14/jpackage/override-jpackage-resources.html
|
|
// for details of "--resource-dir"
|
|
|
|
String macOpts = new String(
|
|
" --resource-dir \"${project(':desktop').projectDir}/package/macosx\""
|
|
)
|
|
|
|
// Env variable can be set by calling "export BISQ_PACKAGE_SIGNING_IDENTITY='Some value'"
|
|
// See "man codesign" for details about the expected signing identity
|
|
String envVariableSigningID = "$System.env.BISQ_PACKAGE_SIGNING_IDENTITY"
|
|
println "Environment variable BISQ_PACKAGE_SIGNING_IDENTITY is: ${envVariableSigningID}"
|
|
ant.input(message: "Sign the app using the above signing identity? (y=yes, n=no)",
|
|
addproperty: "macos-sign-check",
|
|
validargs: "y,n")
|
|
if (ant.properties['macos-sign-check'] == 'y') {
|
|
// Create a temp folder to extract the macos-specific dylibs that need to be signed
|
|
File tempDylibFolderPath = new File(tempRootDir, "dylibs-to-sign")
|
|
tempDylibFolderPath.mkdirs()
|
|
|
|
// Dylibs relevant for signing (paths relative to the tempDylibFolderPath)
|
|
String dylibsToSign = new String(
|
|
" libjavafx_iio.dylib" +
|
|
" libglass.dylib" +
|
|
" libjavafx_font.dylib" +
|
|
" libprism_common.dylib" +
|
|
" libprism_es2.dylib" +
|
|
" libdecora_sse.dylib" +
|
|
" libprism_sw.dylib" +
|
|
" META-INF/native/libio_grpc_netty_shaded_netty_tcnative_osx_x86_64.jnilib"
|
|
)
|
|
|
|
// macOS step 1: Sign dylibs and replace them in the shadow jar
|
|
// Extract dylibss for signing
|
|
executeCmd("cd ${tempDylibFolderPath} &&" +
|
|
" jar xf ${fatJarFolderPath}/${mainJarName}" +
|
|
dylibsToSign)
|
|
// Sign them
|
|
executeCmd("cd ${tempDylibFolderPath} &&" +
|
|
" codesign -vvv --options runtime --deep --force --sign \"${envVariableSigningID}\"" +
|
|
dylibsToSign)
|
|
|
|
// Verify signature
|
|
executeCmd("cd ${tempDylibFolderPath} &&" +
|
|
" codesign -vvv --deep --strict " + dylibsToSign)
|
|
|
|
// Replace unsigned files in jar file
|
|
executeCmd("cd ${tempDylibFolderPath} &&" +
|
|
" jar uf ${fatJarFolderPath}/${mainJarName}" +
|
|
dylibsToSign)
|
|
|
|
// macOS step 2: Build app-image using the shadow jar above (containing signed dylibs)
|
|
// NOTE: licensing file cannot be added at this point only when creating the dmg later
|
|
executeCmd(jPackageFilePath +
|
|
commonOpts +
|
|
macOpts +
|
|
" --type app-image")
|
|
|
|
// macOS step 3: Sign app (hardened runtime)
|
|
File bisqAppImageFullPath = new File(binariesFolderPath, "Bisq.app")
|
|
executeCmd("codesign" +
|
|
" --sign \"${envVariableSigningID}\"" +
|
|
" --options runtime" +
|
|
" --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" +
|
|
" --force" +
|
|
" --verbose" +
|
|
" ${bisqAppImageFullPath}/Contents/runtime/Contents/MacOS/libjli.dylib")
|
|
executeCmd("codesign" +
|
|
" --sign \"${envVariableSigningID}\"" +
|
|
" --options runtime" +
|
|
" --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" +
|
|
" --force" +
|
|
" --verbose" +
|
|
" ${bisqAppImageFullPath}/Contents/MacOS/Bisq")
|
|
executeCmd("codesign" +
|
|
" --sign \"${envVariableSigningID}\"" +
|
|
" --options runtime" +
|
|
" --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" +
|
|
" --force" +
|
|
" --verbose" +
|
|
" ${bisqAppImageFullPath}")
|
|
|
|
// macOS step 4: Package the app-image into a dmg bundle
|
|
executeCmd(jPackageFilePath +
|
|
" --dest \"${binariesFolderPath}\"" +
|
|
" --name ${appNameAndVendor}" +
|
|
" --description \"${appDescription}\"" +
|
|
" --app-version ${appVersion}" +
|
|
" --copyright \"${appCopyright}\"" +
|
|
" --vendor ${appNameAndVendor}" +
|
|
" --temp \"${jpackageTempDir}\"" +
|
|
" --app-image ${bisqAppImageFullPath}" +
|
|
" --mac-sign" +
|
|
macOpts +
|
|
" --type dmg")
|
|
|
|
// macOS step 5: Delete unused app image
|
|
delete(bisqAppImageFullPath)
|
|
|
|
// macOS step 6: Sign dmg bundle
|
|
executeCmd("codesign" +
|
|
" --sign \"${envVariableSigningID}\"" +
|
|
" --options runtime" +
|
|
" --entitlements '${project(':desktop').projectDir}/package/macosx/macos.entitlements'" +
|
|
" -vvvv" +
|
|
" --deep" +
|
|
" '${binariesFolderPath}/Bisq-${appVersion}.dmg'")
|
|
|
|
// macOS step 7: Upload for notarization
|
|
// See https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3087734
|
|
String envVariableAcUsername = "$System.env.BISQ_PACKAGE_NOTARIZATION_AC_USERNAME"
|
|
String envVariableAscProvider = "$System.env.BISQ_PACKAGE_NOTARIZATION_ASC_PROVIDER"
|
|
// e.g. network.bisq.CAT is used when binaries are built by @ripcurlx
|
|
String envVariablePrimaryBundleId = "$System.env.BISQ_PRIMARY_BUNDLE_ID"
|
|
def uploadForNotarizationOutput = executeCmd("xcrun altool --notarize-app" +
|
|
" --primary-bundle-id '${envVariablePrimaryBundleId}'" +
|
|
" --username '${envVariableAcUsername}'" +
|
|
" --password '@keychain:AC_PASSWORD'" +
|
|
" --asc-provider '${envVariableAscProvider}'" +
|
|
" --file '${binariesFolderPath}/Bisq-${appVersion}.dmg'")
|
|
// Response:
|
|
// No errors uploading '[PATH_TO_BISQ_REPO]/bisq/desktop/build/temp-620637000/binaries/Bisq-1.1.1.dmg'.
|
|
// RequestUUID = ea8bba77-97b7-4c15-a53f-8bbccf627190
|
|
def requestUUID = uploadForNotarizationOutput.split('RequestUUID = ')[1].trim()
|
|
println "Extracted RequestUUID: " + requestUUID
|
|
|
|
// Every 1 minute, check the status
|
|
def notarizationEndedInSuccess = false
|
|
def notarizationEndedInFailure = false
|
|
while (!(notarizationEndedInSuccess || notarizationEndedInFailure)) {
|
|
println "Current time is:"
|
|
executeCmd('date')
|
|
println "Waiting for 1 minute..."
|
|
sleep(1 * 60 * 1000)
|
|
|
|
println "Checking notarization status"
|
|
|
|
def checkNotarizationStatusOutput = executeCmd("xcrun altool --notarization-info" +
|
|
" '${requestUUID}'" +
|
|
" --username '${envVariableAcUsername}'" +
|
|
" --password '@keychain:AC_PASSWORD'")
|
|
|
|
notarizationEndedInSuccess = checkNotarizationStatusOutput.contains('success')
|
|
notarizationEndedInFailure = checkNotarizationStatusOutput.contains('invalid')
|
|
}
|
|
|
|
if (notarizationEndedInFailure) {
|
|
ant.fail('Notarization failed, aborting')
|
|
}
|
|
|
|
if (notarizationEndedInSuccess) {
|
|
println "Notarization was successful"
|
|
|
|
// macOS step 8: Staple ticket on dmg
|
|
executeCmd("xcrun stapler staple" +
|
|
" '${binariesFolderPath}/Bisq-${appVersion}.dmg'")
|
|
}
|
|
|
|
} else {
|
|
// If user didn't confirm the optional signing step, then generate a plain non-signed dmg
|
|
executeCmd(jPackageFilePath + commonOpts + macOpts + " --type dmg")
|
|
}
|
|
} else {
|
|
String linuxOpts = new String(
|
|
" --icon ${project(':desktop').projectDir}/package/linux/icon.png" +
|
|
|
|
// This defines the first part of the resulting packages (the application name)
|
|
// deb requires lowercase letters, therefore the application name is written in lowercase
|
|
" --linux-package-name bisq" +
|
|
|
|
// This represents the linux package version (revision)
|
|
// By convention, this is part of the deb/rpm package names, in addition to the software version
|
|
" --linux-app-release 1" +
|
|
|
|
" --linux-menu-group Network" +
|
|
" --linux-shortcut"
|
|
)
|
|
|
|
// Package deb
|
|
executeCmd(jPackageFilePath + commonOpts + linuxOpts +
|
|
" --linux-deb-maintainer noreply@bisq.network" +
|
|
" --type deb")
|
|
|
|
// Clean jpackage temp folder, needs to be empty for the next packaging step (rpm)
|
|
jpackageTempDir.deleteDir()
|
|
jpackageTempDir.mkdirs()
|
|
|
|
// Package rpm
|
|
executeCmd(jPackageFilePath + commonOpts + linuxOpts +
|
|
" --linux-rpm-license-type AGPLv3" + // https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses
|
|
" --type rpm")
|
|
}
|
|
|
|
// After binaries have been generated, copy the (deterministic, signed) fat jar to the binaries folder
|
|
copy {
|
|
from "${fatJarFolderPath}/${mainJarName}"
|
|
into binariesFolderPath
|
|
// desktop-1.6.4-SNAPSHOT-all.jar => desktop-1.6.4-SNAPSHOT-all-mac.jar (or -win.jar, or -linux.jar)
|
|
rename { String fileName -> fileName.replace('-all.jar', "-all-" + os + ".jar") }
|
|
}
|
|
|
|
// Checksum each file present in the binaries folder
|
|
ant.checksum(algorithm: 'SHA-256') {
|
|
ant.fileset(dir: "${binariesFolderPath}")
|
|
}
|
|
|
|
println "The binaries and checksums are ready:"
|
|
FileCollection collection = layout.files { binariesFolderPath.listFiles() }
|
|
collection.collect { it.path }.sort().each { println it }
|
|
|
|
// After binaries are ready, copy them to shared folder
|
|
// Env variable can be set by calling "export BISQ_SHARED_FOLDER='Some value'"
|
|
// This is to copy the final binary/ies to a shared folder for further processing if a VM is used.
|
|
String envVariableSharedFolder = "$System.env.BISQ_SHARED_FOLDER"
|
|
println "Environment variable BISQ_SHARED_FOLDER is: ${envVariableSharedFolder}"
|
|
ant.input(message: "Copy the created binary to a shared folder? (y=yes, n=no)",
|
|
addproperty: "copy-to-shared-folder",
|
|
validargs: "y,n")
|
|
if (ant.properties['copy-to-shared-folder'] == 'y') {
|
|
copy {
|
|
from binariesFolderPath
|
|
into envVariableSharedFolder
|
|
}
|
|
|
|
// Try to open a native file explorer window at the shared folder location
|
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
|
executeCmd("start '${envVariableSharedFolder}'")
|
|
} else if (Os.isFamily(Os.FAMILY_MAC)) {
|
|
executeCmd("open '${envVariableSharedFolder}'")
|
|
} else {
|
|
executeCmd("nautilus '${envVariableSharedFolder}'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def executeCmd(String cmd) {
|
|
String shell
|
|
String shellArg
|
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
|
shell = 'cmd'
|
|
shellArg = '/c'
|
|
} else {
|
|
shell = 'bash'
|
|
shellArg = '-c'
|
|
}
|
|
|
|
println "Executing command:\n${cmd}\n"
|
|
// See "Executing External Processes" section of
|
|
// http://docs.groovy-lang.org/next/html/documentation/
|
|
def commands = [shell, shellArg, cmd]
|
|
def process = commands.execute(null, project.rootDir)
|
|
def result
|
|
if (process.waitFor() == 0) {
|
|
result = process.text
|
|
println "Command output (stdout):\n${result}"
|
|
} else {
|
|
result = process.err.text
|
|
println "Command output (stderr):\n${result}"
|
|
}
|
|
return result
|
|
}
|