haveno/desktop/package/package.gradle

501 lines
24 KiB
Groovy
Raw Normal View History

import org.apache.tools.ant.taskdefs.condition.Os
2021-05-04 20:20:01 -04:00
import java.time.LocalDateTime
2024-10-08 13:26:49 -04:00
import java.util.regex.Pattern
2021-05-04 20:20:01 -04:00
task jpackageSanityChecks {
description 'Interactive sanity checks on the version of the code that will be packaged'
doLast {
if (!System.getenv("CI")) {
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')
}
2021-05-04 20:20:01 -04:00
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')
}
2021-05-04 20:20:01 -04:00
// 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 Haveno.\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')
}
} else {
println "CI environment detected, skipping interactive sanity checks"
executeCmd("git --no-pager log -5 --oneline")
executeCmd("git status --short --branch")
executeCmd("java --version")
2021-05-04 20:20:01 -04:00
}
}
}
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().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
// Define the download URLs (and associated binary hashes) for the JDK used to package the installers
// These JDKs are independent of what is installed on the building system
//
// If these specific versions are not hosted by AdoptOpenJDK anymore, or if different versions are desired,
// simply update the links and associated hashes below
//
// See https://adoptopenjdk.net/releases.html?variant=openjdk15&jvmVariant=hotspot for latest download URLs
// On the download page linked above, filter as follows to get the binary URL + associated SHA256:
// - architecture: x64
// - operating system:
// -- linux ( -> use the tar.gz JDK link)
// -- macOS ( -> use the tar.gz JDK link)
// -- windows ( -> use the .zip JDK link)
Map jdk21Binaries = [
'linux' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-linux-amd64-full.tar.gz',
'linux-sha256' : '7eda80851fba1da023e03446c77100f19e7c770491b0d5bc9f893044e1b2b69b',
'linux-aarch64' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-linux-aarch64-full.tar.gz',
'linux-aarch64-sha256' : 'a477fc72085f30b03bf71fbed47923cea3b6f33b5b6a5a74718623b772a3a043',
'mac' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-macos-amd64-full.tar.gz',
'mac-sha256' : '42b528206595e559803b6f9f6bdbbf236ec6d10684058f46bc5261f5498d345c',
'mac-aarch64' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-macos-aarch64-full.tar.gz',
'mac-aarch64-sha256' : 'eba73a9bff7234220dc9a1da7f44b3d7ed2a562663eadc1c53bd74b355839a55',
'windows' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-windows-amd64-full.zip',
'windows-sha256' : 'f823eff0234af5bef095e53e5431191dbee8c2e42ca321eda23148a15cbf8d5b',
'windows-aarch64' : 'https://download.bell-sw.com/java/21.0.2+14/bellsoft-jdk21.0.2+14-windows-aarch64-full.zip',
'windows-aarch64-sha256': 'a2e9edecaf9637f83ef1cddab3a74f39ac55f8e1a479f10f3584ad939dfadd0a'
2021-05-04 20:20:01 -04:00
]
String osKey
String architecture = System.getProperty("os.arch").toLowerCase()
2021-05-04 20:20:01 -04:00
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
if (architecture.contains("aarch64") || architecture.contains("arm")) {
osKey = "windows-aarch64"
} else {
osKey = "windows"
}
2021-05-04 20:20:01 -04:00
} else if (Os.isFamily(Os.FAMILY_MAC)) {
if (architecture.contains("aarch64") || architecture.contains("arm")) {
osKey = "mac-aarch64"
} else {
osKey = "mac"
}
2021-05-04 20:20:01 -04:00
} else {
if (architecture.contains("aarch64") || architecture.contains("arm")) {
osKey = "linux-aarch64"
} else {
osKey = "linux"
}
2021-05-04 20:20:01 -04:00
}
2024-10-02 10:36:29 -04:00
ext.osKey = osKey
ext.jdk21Binary_DownloadURL = jdk21Binaries[osKey]
ext.jdk21Binary_SHA256Hash = jdk21Binaries[osKey + '-sha256']
2021-05-04 20:20:01 -04:00
}
}
task retrieveAndExtractJavaBinaries {
description 'Retrieve necessary Java binaries and extract them'
dependsOn 'getJavaBinariesDownloadURLs'
doLast {
File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") as File
// 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('jdk21Binary_DownloadURL')
String jdkForJpackageArchiveHash = getJavaBinariesDownloadURLs.property('jdk21Binary_SHA256Hash')
2021-05-04 20:20:01 -04:00
String jdkForJpackageArchiveFileName = jdkForJpackageArchiveURL.tokenize('/').last()
File jdkForJpackageFile = new File(jdkForJpackageDir, jdkForJpackageArchiveFileName)
// Download necessary JDK binaries + verify hash
ext.downloadAndVerifyArchive(jdkForJpackageArchiveURL, jdkForJpackageArchiveHash, 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: groovy.io.FileType.FILES, nameFilter: jpackageBinaryFileName) {
2021-05-04 20:20:01 -04:00
println 'Using jpackage binary from ' + it
ext.jpackageFilePath = it.path
}
}
ext.downloadAndVerifyArchive = { String archiveURL, String archiveSHA256, File destinationArchiveFile ->
println "Downloading ${archiveURL}"
ant.get(src: archiveURL, dest: destinationArchiveFile)
println 'Download saved to ' + destinationArchiveFile
println 'Verifying checksum for downloaded binary ...'
ant.jdkHash = archiveSHA256
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${jdkHash}', verifyProperty: 'hashMatches')
if (ant.properties['hashMatches'] != 'true') {
ant.fail('Checksum mismatch: Downloaded JDK binary has a different checksum than expected')
}
println 'Checksum verified'
}
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 artifacts and create a fresh shadowJar for the installers
2021-05-04 20:20:01 -04:00
dependsOn rootProject.clean
dependsOn ':core:havenoDeps'
2021-05-04 20:20:01 -04:00
dependsOn ':desktop:shadowJar'
doLast {
String jPackageFilePath = retrieveAndExtractJavaBinaries.property('jpackageFilePath')
File binariesFolderPath = file(getJavaBinariesDownloadURLs.property('binariesFolderPath'))
File tempRootDir = getJavaBinariesDownloadURLs.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}")
// Store deterministic jar SHA-256
ant.checksum(file: "${fatJarFolderPath}/${mainJarName}", algorithm: 'SHA-256')
copy {
from "${fatJarFolderPath}/${mainJarName}.SHA-256"
into binariesFolderPath
}
// 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 Haveno version ${appVersion}"
2021-05-04 20:20:01 -04:00
// zip jar lib for Raspberry Pi only on macOS as there are path issues on Windows and 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 monero exchange network.'
2024-01-07 21:13:03 -05:00
String appCopyright = '© 2024 Haveno'
String appNameAndVendor = 'Haveno'
2021-05-04 20:20:01 -04:00
String commonOpts = new String(
// Generic options
" --dest \"${binariesFolderPath}\"" +
" --name ${appNameAndVendor}" +
//" --description \"${appDescription}\"" + // TODO: task managers show app description instead of name, so we disable it
2021-05-04 20:20:01 -04:00
" --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 haveno.desktop.app.HavenoAppMain" +
2021-05-04 20:20:01 -04:00
" --java-options -Xss1280k" +
" --java-options -XX:MaxRAM=4g" +
" --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED" +
" --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED" +
" --java-options --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" +
" --java-options --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED" +
" --java-options -Djava.net.preferIPv4Stack=true" +
" --arguments --baseCurrencyNetwork=XMR_STAGENET"
2021-05-04 20:20:01 -04:00
// 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)) {
String windowsOpts = new String(
" --icon \"${project(':desktop').projectDir}/package/windows/Haveno.ico\"" +
2021-05-04 20:20:01 -04:00
" --resource-dir \"${project(':desktop').projectDir}/package/windows\"" +
" --win-dir-chooser" +
" --win-per-user-install" +
" --win-menu" +
" --win-shortcut"
)
2024-03-08 11:55:19 -05:00
executeCmd(jPackageFilePath + commonOpts + windowsOpts + " --verbose > desktop/build/output.txt --type exe")
2021-05-04 20:20:01 -04:00
} 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\""
)
executeCmd(jPackageFilePath + commonOpts + macOpts + " --type dmg")
2021-05-04 20:20:01 -04:00
} 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 haveno" +
2021-05-04 20:20:01 -04:00
// 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@haveno.exchange" +
2021-05-04 20:20:01 -04:00
" --type deb")
2024-10-02 10:36:29 -04:00
// Clean jpackage temp folder, needs to be empty for the next packaging step (AppImage)
jpackageTempDir.deleteDir()
jpackageTempDir.mkdirs()
executeCmd(jPackageFilePath + commonOpts +
" --dest \"${jpackageTempDir}\"" +
" --type app-image")
// Path to the app-image directory: THIS IS NOT THE ACTUAL .AppImage FILE.
// See JPackage documentation on --type app-image for more.
String appImagePath = new String(
"\"${binariesFolderPath}/${appNameAndVendor}\""
)
// Which version of AppImageTool to use
String AppImageToolVersion = "13";
// Download AppImageTool
Map AppImageToolBinaries = [
'linux' : "https://github.com/AppImage/AppImageKit/releases/download/${AppImageToolVersion}/appimagetool-x86_64.AppImage",
'linux-aarch64' : "https://github.com/AppImage/AppImageKit/releases/download/${AppImageToolVersion}/appimagetool-aarch64.AppImage",
]
String osKey = getJavaBinariesDownloadURLs.property('osKey')
File appDir = new File("${jpackageTempDir}/Haveno")
File templateAppDir = new File("${project(':desktop').projectDir}/package/linux/Haveno.AppDir")
File jpackDir = appDir
appDir.mkdirs()
File AppImageToolBinary = new File("${jpackageTempDir}/appimagetool.AppImage")
// Adding a platform to the AppImageToolBinaries essentially adds it to the "supported" list of platforms able to make AppImages
// However, be warned that any platform that doesn't support unix `ln` and `chmod` will not work with the current method.
if (AppImageToolBinaries.containsKey(osKey)) {
println "Downloading ${AppImageToolBinaries[osKey]}"
ant.get(src: AppImageToolBinaries[osKey], dest: AppImageToolBinary)
println 'Download saved to ' + jpackageTempDir
project.exec {
commandLine('chmod', '+x', AppImageToolBinary)
}
copy {
from templateAppDir
into appDir
boolean includeEmptyDirs = true
}
project.exec {
workingDir appDir
commandLine 'ln', '-s', 'bin/Haveno', 'AppRun'
}
project.exec {
commandLine "${AppImageToolBinary}", appDir, "${binariesFolderPath}/haveno_${appVersion}.AppImage"
}
} else {
println "Your platform does not support AppImageTool ${AppImageToolVersion}"
}
2021-05-04 20:20:01 -04:00
// 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")
2024-10-08 13:26:49 -04:00
// Define Flatpak-related properties
String flatpakManifestFile = 'package/linux/exchange.haveno.Haveno.yml'
String linuxDir = 'package/linux'
String flatpakOutputDir = 'package/linux/build'
String flatpakExportDir = "${binariesFolderPath}/fpexport"
String flatpakBundleFile = "${binariesFolderPath}/haveno.flatpak"
// Read the default app name from the HavenoExecutable.java file
def filer = file('../core/src/main/java/haveno/core/app/HavenoExecutable.java')
def content = filer.text
def matcher = Pattern.compile(/public static final String DEFAULT_APP_NAME = "(.*?)";/).matcher(content)
def defaultAppName = "Haveno"
if (matcher.find()) {
defaultAppName = matcher.group(1)
} else {
throw new GradleException("DEFAULT_APP_NAME not found in HavenoExecutable.java")
}
2024-10-08 13:36:28 -04:00
// Copy the manifest to a new tmp one in the same place
2024-10-08 13:26:49 -04:00
// and add a --filesystem=.local/share/${name} to the flatpak manifest
def manifest = file(flatpakManifestFile)
def newManifest = file('exchange.haveno.Haveno.yaml')
newManifest.write(manifest.text.replace("- --share=network", "- --share=network\n - --filesystem=~/.local/share/${defaultAppName}:create"))
flatpakManifestFile = 'exchange.haveno.Haveno.yaml'
// Command to build the Flatpak
exec {
commandLine 'flatpak-builder', '--force-clean', flatpakOutputDir, flatpakManifestFile, '--user', '--install-deps-from=flathub'
}
// Command to export the Flatpak
exec {
commandLine 'flatpak', 'build-export', flatpakExportDir, flatpakOutputDir
}
// Command to create the Flatpak bundle
exec {
commandLine 'flatpak', 'build-bundle', flatpakExportDir, flatpakBundleFile, 'exchange.haveno.Haveno', '--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo'
}
// delete the flatpak build directory
delete(flatpakOutputDir)
delete(flatpakExportDir)
delete(flatpakManifestFile)
println "Flatpak package created at ${flatpakBundleFile}"
2021-05-04 20:20:01 -04:00
}
// Env variable can be set by calling "export HAVENO_SHARED_FOLDER='Some value'"
2021-05-04 20:20:01 -04:00
// This is to copy the final binary/ies to a shared folder for further processing if a VM is used.
String envVariableSharedFolder = "$System.env.HAVENO_SHARED_FOLDER"
println "Environment variable HAVENO_SHARED_FOLDER is: ${envVariableSharedFolder}"
if (envVariableSharedFolder != "null") {
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
}
2024-10-02 10:36:29 -04:00
executeCmd("open " + envVariableSharedFolder)
2021-05-04 20:20:01 -04:00
}
}
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)
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
}