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 { 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') } // TODO Evtl check programmatically in gradle (i.e. fail if below v11) executeCmd("java --version") ant.input(message: "Above you see the installed java version, which will be used to compile and build Bisq.\n" + "Is this java version ok for that? (y=continue, n=abort)", addproperty: "sanity-check-3", validargs: "y,n") if (ant.properties['sanity-check-3'] == 'n') { ant.fail('Aborting') } } } task getJavaBinariesDownloadURLs { description 'Find out which JDK will be used for jpackage and prepare to download it' 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().getNano() 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 // TODO Extend script logic to alternatively allow a local (separate, v14+) JDK for jpackage // TODO Another option is to use the local JDK for everything: build jars and use jpackage (but then it has to be v14+) // It seems only the latest version of each JDK is available for download // Therefore, if at any future AdoptOpenJDK release these download links won't work, simply update them // See https://adoptopenjdk.net/releases.html?variant=openjdk15&jvmVariant=hotspot for latest download URLs // The links must be for: x64 + JDK + zip (for windows) or tar.gz (for mac/linux) Map jdk15DownloadURLs = [ 'linux' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_linux_hotspot_15_36.tar.gz', 'mac' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_mac_hotspot_15_36.tar.gz', 'windows' : 'https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_windows_hotspot_15_36.zip' // TODO For some reason, using "--runtime-image jdk-11" does NOT work with a v15 jpackage, but works with v14 // v14 // 'linux' : 'https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_linux_hotspot_14.0.2_12.tar.gz', // 'mac' : 'https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_mac_hotspot_14.0.2_12.tar.gz', // 'windows' : 'https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_windows_hotspot_14.0.2_12.zip' ] String osKey if (Os.isFamily(Os.FAMILY_WINDOWS)) { osKey = 'windows' } else if (Os.isFamily(Os.FAMILY_MAC)) { osKey = 'mac' } else { osKey = 'linux' } ext.jdk15DownloadURL = jdk15DownloadURLs[osKey] // The download URLs can be manually overwritten here, if necessary // ext.jdk15DownloadURL = ... } } task retrieveAndExtractJavaBinaries { description 'Retrieve necessary Java binaries and extract them' dependsOn 'getJavaBinariesDownloadURLs' doLast { File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") // Folder where the jpackage JDK archive will be downloaded and extracted String jdkForJpackageDirName = "jdk-jpackage" File jdkForJpackageDir = new File(tempRootDir, jdkForJpackageDirName) jdkForJpackageDir.mkdirs(); String jdkForJpackageArchiveURL = getJavaBinariesDownloadURLs.property('jdk15DownloadURL') String jdkForJpackageArchiveFileName = jdkForJpackageArchiveURL.tokenize('/').last() File jdkForJpackageFile = new File(jdkForJpackageDir, jdkForJpackageArchiveFileName) // Download necessary JDK binaries ext.downloadArchive(jdkForJpackageArchiveURL, jdkForJpackageFile) // Extract them String jpackageBinaryFileName if (Os.isFamily(Os.FAMILY_WINDOWS)) { ext.extractArchiveZip(jdkForJpackageFile, jdkForJpackageDir) jpackageBinaryFileName = 'jpackage.exe' } else { ext.extractArchiveTarGz(jdkForJpackageFile, jdkForJpackageDir) jpackageBinaryFileName = 'jpackage' } // Find jpackage in the newly extracted JDK // Don't rely on hardcoded paths to reach it, because the path depends on the version and platform jdkForJpackageDir.traverse(type: FILES, nameFilter: jpackageBinaryFileName) { println 'Using jpackage binary from ' + it ext.jpackageFilePath = it.path } } ext.downloadArchive = {String archiveURL, File destinationArchiveFile -> println "Downloading ${archiveURL}" ant.get(src: archiveURL, dest: destinationArchiveFile) println 'Download saved to ' + destinationArchiveFile } ext.extractArchiveTarGz = {File tarGzFile, File destinationDir -> println "Extracting tar.gz ${tarGzFile}" // Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly) copy { from tarTree(resources.gzip(tarGzFile)) into destinationDir } println "Extracted to ${destinationDir}" } ext.extractArchiveZip = {File zipFile, File destinationDir -> println "Extracting zip ${zipFile}..." ant.unzip(src: zipFile, dest: destinationDir) println "Extracted to ${destinationDir}" } } task packageInstallers { description 'Call jpackage to prepare platform-specific binaries for this platform' dependsOn 'retrieveAndExtractJavaBinaries' // Clean all previous artefacts and create a fresh shadowJar for the installers dependsOn rootProject.clean dependsOn ':desktop:shadowJar' doLast { String jPackageFilePath = retrieveAndExtractJavaBinaries.property('jpackageFilePath') String licenseFilePath = "${rootProject.projectDir}/LICENSE" File binariesFolderPath = file(getJavaBinariesDownloadURLs.property('binariesFolderPath')) File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") // The jpackateTempDir stores temp files used by jpackage for building the installers // It can be inspected in order to troubleshoot the packaging process File jpackateTempDir = new File(tempRootDir, "jpackage-temp") jpackateTempDir.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" String mainJarName = file(fatJarFolderPath).list()[0] // 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 // For mac, valid versions are one to three integers separated by dot // TODO Special handling for mac? Or always remove -SNAPSHOT suffix? String appVersion = '1.1.1' println "Packaging Bisq version ${appVersion}" String commonOpts = ( // Generic options " --dest \"${binariesFolderPath}\"" + " --name Bisq" + " --description \"A decentralized bitcoin exchange network.\"" + " --app-version ${appVersion}" + " --copyright \"© 2020 Bisq\"" + " --vendor Bisq" + " --temp \"${jpackateTempDir}\"" + // 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=4g" + " --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" + // Options for creating the application package " --license-file \"${licenseFilePath}\"" ) if (Os.isFamily(Os.FAMILY_WINDOWS)) { // TODO Found no benefit in using --resource-dir "..package/windows", it has the same outcome as opts below // Actually using the --resource-dir option caused issues with uninstalling the newly installed Bisq // Therefore, not using --resource-dir for windows, for now String windowsOpts = ( " --icon ${project(':desktop').projectDir}/package/windows/Bisq.ico" + " --win-per-user-install" + " --win-menu" + " --win-shortcut" ) // TODO How does the current package/windows/Bisq.iss play into this? executeCmd(jPackageFilePath + commonOpts + windowsOpts + " --type exe") } else if (Os.isFamily(Os.FAMILY_MAC)) { // TODO Include signing args, like --mac-sign / -mac-signing-keychain / --mac-signing-key-user-name // See https://docs.oracle.com/en/java/javase/14/jpackage/override-jpackage-resources.html // for details of "--resource-dir" String macOpts = ( " --resource-dir \"${project(':desktop').projectDir}/package/macosx\"" ) executeCmd(jPackageFilePath + commonOpts + macOpts + " --type dmg") } else { String linuxOpts = ( " --icon ${project(':desktop').projectDir}/package/linux/icon.png" + " --linux-package-name bisq-build-test" + " --linux-app-release ${appVersion}" + " --linux-menu-group Network" + " --linux-shortcut" ) // Package deb executeCmd(jPackageFilePath + commonOpts + linuxOpts + " --linux-deb-maintainer contact@bisq.network" + " --type deb") // Clean jpackage temp folder, needs to be empty for the next packaging step (rpm) jpackateTempDir.deleteDir(); jpackateTempDir.mkdirs(); // Package rpm executeCmd(jPackageFilePath + commonOpts + linuxOpts + " --linux-rpm-license-type AGPLv3" + // https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses " --type rpm") } println "The binaries are ready:" binariesFolderPath.traverse { println it.path } } } 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) if (process.waitFor() == 0) { println "Command output (stdout):\n${process.text}" } else { println "Command output (stderr):\n${process.err.text}" } }