mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 18:03:12 +01:00
3723dd32ef
Enforce the use of JDK 15 when packaging. This removes the need to download and unpack JDK 15 just for the use of jpackager.
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 = '© 2021 Bisq'
|
|
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
|
|
}
|