X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=build.gradle;h=3ed6c564622fd5c61b047cc33c29db7fe7187cf7;hb=refs%2Fheads%2Fdevelop;hp=9e2b4a31a53455d9480c68f3023038c1de8566a6;hpb=8e62def6cf40cdd19e1c15ac2ed7e9f3d8b3aa54;p=jalview.git diff --git a/build.gradle b/build.gradle index 9e2b4a3..d057994 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,10 @@ import org.gradle.plugins.ide.eclipse.model.Output import org.gradle.plugins.ide.eclipse.model.Library import java.security.MessageDigest import java.util.regex.Matcher +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit import groovy.transform.ExternalizeMethods import groovy.util.XmlParser import groovy.xml.XmlUtil @@ -39,6 +43,7 @@ buildscript { classpath "com.vladsch.flexmark:flexmark-all:0.62.0" classpath "org.jsoup:jsoup:1.14.3" classpath "com.eowise:gradle-imagemagick:0.5.1" + classpath 'ru.vyarus:gradle-use-python-plugin:4.0.0' } } @@ -48,10 +53,11 @@ plugins { id 'application' id 'eclipse' id "com.diffplug.gradle.spotless" version "3.28.0" - id 'com.github.johnrengelman.shadow' version '4.0.3' + id 'com.github.johnrengelman.shadow' version '6.0.0' id 'com.install4j.gradle' version '10.0.3' - id 'com.dorongold.task-tree' version '2.1.0' // only needed to display task dependency tree with gradle task1 [task2 ...] taskTree + id 'com.dorongold.task-tree' version '2.1.1' // only needed to display task dependency tree with gradle task1 [task2 ...] taskTree id 'com.palantir.git-version' version '0.13.0' apply false + id 'ru.vyarus.use-python' version '4.0.0' } repositories { @@ -229,17 +235,21 @@ ext { jvlChannelName = CHANNEL.toLowerCase() install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}" + install4jDMGDSStoreJSON = "${install4j_images_dir}/${install4j_dmg_ds_store_json}" install4jDMGBackgroundImageDir = "${install4j_images_dir}" install4jDMGBackgroundImageBuildDir = "build/imagemagick/install4j" install4jDMGBackgroundImageFile = "${install4j_dmg_background}" - install4jInstallerName = "${jalview_name} Non-Release Installer" + install4jmacOSArchiveName = "${jalview_name} Non-Release ${JALVIEW_VERSION} Installer" install4jExecutableName = install4j_executable_name - install4jExtraScheme = "jalviewx" + install4jExtraScheme = "jalviewextra" install4jMacIconsFile = string("${install4j_images_dir}/${install4j_mac_icons_file}") install4jWindowsIconsFile = string("${install4j_images_dir}/${install4j_windows_icons_file}") install4jPngIconFile = string("${install4j_images_dir}/${install4j_png_icon_file}") install4jBackground = string("${install4j_images_dir}/${install4j_background}") install4jBuildDir = "${install4j_build_dir}/${JAVA_VERSION}" + install4jDMGFixedDSStoreX64 = "build/macos_dmg/${install4j_dmg_ds_store}-x64" + install4jDMGFixedDSStoreAarch64 = "build/macos_dmg/${install4j_dmg_ds_store}-aarch64" + install4jDMGVolumeIcon = string("${install4j_images_dir}/${install4j_dmg_volume_icon}") install4jCheckSums = true applicationName = "${jalview_name}" @@ -264,7 +274,8 @@ ext { getdownSetAppBaseProperty = true reportRsyncCommand = true install4jSuffix = "" - install4jInstallerName = "${jalview_name} Installer" + install4jmacOSArchiveName = "Install ${jalview_name} ${JALVIEW_VERSION}" + install4jExtraScheme = (CHANNEL=="RELEASE")?"jalviewx":"jalviewjs" break case "ARCHIVE": @@ -306,7 +317,7 @@ ext { JALVIEW_VERSION=JALVIEW_VERSION+"-d${suffix}-${buildDate}" install4jSuffix = "Develop ${suffix}" install4jExtraScheme = "jalviewd" - install4jInstallerName = "${jalview_name} Develop ${suffix} Installer" + install4jmacOSArchiveName = "Install ${jalview_name} Develop ${suffix} ${JALVIEW_VERSION}" getdownChannelName = string("develop-${suffix}") getdownChannelDir = string("${getdown_website_dir}/${getdownChannelName}") getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}") @@ -324,7 +335,7 @@ ext { install4jSuffix = "Develop" install4jExtraScheme = "jalviewd" - install4jInstallerName = "${jalview_name} Develop Installer" + install4jmacOSArchiveName = "Install ${jalview_name} Develop ${JALVIEW_VERSION}" backgroundImageText = true break @@ -339,7 +350,7 @@ ext { JALVIEW_VERSION = JALVIEW_VERSION+"-test" install4jSuffix = "Test" install4jExtraScheme = "jalviewt" - install4jInstallerName = "${jalview_name} Test Installer" + install4jmacOSArchiveName = "Install ${jalview_name} Test ${JALVIEW_VERSION}" backgroundImageText = true break @@ -363,7 +374,7 @@ ext { JALVIEW_VERSION = "TEST" install4jSuffix = "Test-Local" install4jExtraScheme = "jalviewt" - install4jInstallerName = "${jalview_name} Test Installer" + install4jmacOSArchiveName = "Install ${jalview_name} Test ${JALVIEW_VERSION}" backgroundImageText = true break @@ -429,6 +440,8 @@ ext { .replaceAll("_+", "_") // collapse __ .replaceAll("_*-_*", "-") // collapse _-_ .toLowerCase() + install4jmacOSArchiveX64Name = "${install4jmacOSArchiveName} (Intel)" + install4jmacOSArchiveAarch64Name = "${install4jmacOSArchiveName} (Apple Silicon)" getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local" getdownAppDir = string("${getdownAppBaseDir}/${getdownAppDistDir}") @@ -526,6 +539,9 @@ ext { if (install4jHomeDir.startsWith("~/")) { install4jHomeDir = System.getProperty("user.home") + install4jHomeDir.substring(1) } + install4jmacOSArchiveX64DMGFilename = "${install4jApplicationFolder}-${JALVIEW_VERSION}-macos-x64-java_${JAVA_INTEGER_VERSION}" + install4jmacOSArchiveAarch64DMGFilename = "${install4jApplicationFolder}-${JALVIEW_VERSION}-macos-aarch64-java_${JAVA_INTEGER_VERSION}" + resourceBuildDir = string("${buildDir}/resources") resourcesBuildDir = string("${resourceBuildDir}/resources_build") @@ -568,11 +584,16 @@ ext { jalviewjsJ2sAltSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_alt_settings}") jalviewjsJ2sProps = null jalviewjsJ2sPlugin = jalviewjs_j2s_plugin + jalviewjsStderrLaunchFilename = "${jalviewjsSiteDir}/"+(file(jalviewjs_stderr_launch).getName()) eclipseWorkspace = null eclipseBinary = string("") eclipseVersion = string("") eclipseDebug = false + + jalviewjsChromiumUserDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}" + jalviewjsChromiumProfileDir = "${ext.jalviewjsChromiumUserDir}/${jalviewjs_chromium_profile_name}" + // ENDEXT } @@ -1064,7 +1085,7 @@ compileJava { // JBP->BS should the print statement in doFirst refer to compile_target_compatibility ? sourceCompatibility = compile_source_compatibility targetCompatibility = compile_target_compatibility - options.compilerArgs = additional_compiler_args + options.compilerArgs += additional_compiler_args options.encoding = "UTF-8" doFirst { print ("Setting target compatibility to "+compile_target_compatibility+"\n") @@ -1076,7 +1097,7 @@ compileJava { compileTestJava { sourceCompatibility = compile_source_compatibility targetCompatibility = compile_target_compatibility - options.compilerArgs = additional_compiler_args + options.compilerArgs += additional_compiler_args doFirst { print ("Setting target compatibility to "+targetCompatibility+"\n") } @@ -1732,36 +1753,147 @@ task prepare { compileJava.dependsOn prepare run.dependsOn compileJava -//run.dependsOn prepare +compileTestJava.dependsOn compileJava + -//testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir test { - dependsOn prepare + group = "Verification" + description = "Runs all testTaskN tasks)" if (useClover) { dependsOn cloverClasses - } else { //? - dependsOn compileJava //? + } else { //? + dependsOn testClasses } + // not running tests in this task + exclude "**/*" +} +/* testTask0 is the main test task */ +task testTask0(type: Test) { + group = "Verification" + description = "The main test task. Runs all non-testTaskN-labelled tests (unless excluded)" useTestNG() { - includeGroups testng_groups - excludeGroups testng_excluded_groups + includeGroups testng_groups.split(",") + excludeGroups testng_excluded_groups.split(",") + tasks.withType(Test).matching {it.name.startsWith("testTask") && it.name != name}.all {t -> excludeGroups t.name} preserveOrder true useDefaultListeners=true } + timeout = Duration.ofMinutes(15) +} - maxHeapSize = "2048m" - jvmArgs '-Xmx2048m', '-Xms2048m' +/* separated tests */ +task testTask1(type: Test) { + group = "Verification" + description = "Tests that need to be isolated from the main test run" + useTestNG() { + includeGroups name + excludeGroups testng_excluded_groups.split(",") + preserveOrder true + useDefaultListeners=true + } + timeout = Duration.ofMinutes(5) +} + +task testTask2(type: Test) { + group = "Verification" + description = "Tests that need to be isolated from the main test run" + useTestNG() { + includeGroups name + excludeGroups testng_excluded_groups.split(",") + preserveOrder true + useDefaultListeners=true + } + timeout = Duration.ofMinutes(5) +} +task testTask3(type: Test) { + group = "Verification" + description = "Tests that need to be isolated from the main test run" + useTestNG() { + includeGroups name + excludeGroups testng_excluded_groups.split(",") + preserveOrder true + useDefaultListeners=true + } + timeout = Duration.ofMinutes(5) +} -/* delete these! - maxParallelForks = 1 - forkEvery = 1 - failFast = true - jvmArgs '-Xmx2048m', '-Xms2048m' +/* insert more testTaskNs here -- change N to next digit or other string */ +/* +task testTaskN(type: Test) { + group = "Verification" + description = "Tests that need to be isolated from the main test run" + useTestNG() { + includeGroups name + excludeGroups testng_excluded_groups.split(",") + preserveOrder true + useDefaultListeners=true + } +} */ + +/* + * adapted from https://medium.com/@wasyl/pretty-tests-summary-in-gradle-744804dd676c + * to summarise test results from all Test tasks + */ +/* START of test tasks results summary */ +import groovy.time.TimeCategory +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +rootProject.ext.testsResults = [] // Container for tests summaries + +tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { testTask -> + + // from original test task + if (useClover) { + dependsOn cloverClasses + } else { //? + dependsOn testClasses //? + } + + // run main tests first + if (!testTask.name.equals("testTask0")) + testTask.mustRunAfter "testTask0" + + testTask.testLogging { logging -> + events TestLogEvent.FAILED +// TestLogEvent.SKIPPED, +// TestLogEvent.STANDARD_OUT, +// TestLogEvent.STANDARD_ERROR + + exceptionFormat TestExceptionFormat.FULL + showExceptions true + showCauses true + showStackTraces true + if (test_output) { + showStandardStreams true + } + info.events = [ TestLogEvent.FAILED ] + } + + if (OperatingSystem.current().isMacOsX()) { + testTask.systemProperty "apple.awt.UIElement", "true" + testTask.environment "JAVA_TOOL_OPTIONS", "-Dapple.awt.UIElement=true" + } + + + ignoreFailures = true // Always try to run all tests for all modules + + afterSuite { desc, result -> + if (desc.parent) + return // Only summarize results for whole modules + + def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint] + + rootProject.ext.testsResults.add(resultsInfo) + } + + // from original test task + maxHeapSize = "1024m" + workingDir = jalviewDir def testLaf = project.findProperty("test_laf") if (testLaf != null) { @@ -1778,12 +1910,144 @@ test { jvmArgs += additional_compiler_args doFirst { + // this is not perfect yet -- we should only add the commandLineIncludePatterns to the + // testTasks that include the tests, and exclude all from the others. + // get --test argument + filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns + // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the + // commandLineIncludePatterns not matching anything. Instead we are doing setFailOnNoMatchingTests(false) below + + if (useClover) { println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover") } } + + + /* don't fail on no matching tests (so --tests will run across all testTasks) */ + testTask.filter.setFailOnNoMatchingTests(false) + + /* ensure the "test" task dependsOn all the testTasks */ + test.dependsOn testTask } +gradle.buildFinished { + def allResults = rootProject.ext.testsResults + + if (!allResults.isEmpty()) { + printResults allResults + allResults.each {r -> + if (r[2].resultType == TestResult.ResultType.FAILURE) + throw new GradleException("Failed tests!") + } + } +} + +private static String colString(styler, col, colour, text) { + return col?"${styler[colour](text)}":text +} + +private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) { + def colour = 'black' + def text = rt + def nocol = false + if (rc == 0) { + text = "-----" + nocol = true + } else { + switch(rt) { + case TestResult.ResultType.SUCCESS: + colour = 'green' + break; + case TestResult.ResultType.FAILURE: + colour = 'red' + break; + default: + nocol = true + break; + } + } + StringBuilder sb = new StringBuilder() + sb.append("${pn}") + if (tn != null) + sb.append(":${tn}") + sb.append(" results: ") + sb.append(colString(s, col && !nocol, colour, text)) + sb.append(" (") + sb.append("${rc} tests, ") + sb.append(colString(s, col && rs > 0, 'green', rs)) + sb.append(" successes, ") + sb.append(colString(s, col && rf > 0, 'red', rf)) + sb.append(" failures, ") + sb.append("${rsk} skipped) in ${t}") + return sb.toString() +} + +private static void printResults(allResults) { + + // styler from https://stackoverflow.com/a/56139852 + def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] } + + def maxLength = 0 + def failedTests = false + def summaryLines = [] + def totalcount = 0 + def totalsuccess = 0 + def totalfail = 0 + def totalskip = 0 + def totaltime = TimeCategory.getSeconds(0) + // sort on project name then task name + allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each { + def projectName = it[0] + def taskName = it[1] + def result = it[2] + def time = it[3] + def report = it[4] + def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true) + def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false) + def reportLine = "Report file: ${report}" + def ls = summaryPlain.length() + def lr = reportLine.length() + def m = [ls, lr].max() + if (m > maxLength) + maxLength = m + def info = [ls, summaryCol, reportLine] + summaryLines.add(info) + failedTests |= result.resultType == TestResult.ResultType.FAILURE + totalcount += result.testCount + totalsuccess += result.successfulTestCount + totalfail += result.failedTestCount + totalskip += result.skippedTestCount + totaltime += time + } + def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true) + def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false) + def tls = totalSummaryPlain.length() + if (tls > maxLength) + maxLength = tls + def info = [tls, totalSummaryCol, null] + summaryLines.add(info) + + def allSummaries = [] + for(sInfo : summaryLines) { + def ls = sInfo[0] + def summary = sInfo[1] + def report = sInfo[2] + + StringBuilder sb = new StringBuilder() + sb.append("│" + summary + " " * (maxLength - ls) + "│") + if (report != null) { + sb.append("\n│" + report + " " * (maxLength - report.length()) + "│") + } + allSummaries += sb.toString() + } + + println "┌${"${"─" * maxLength}"}┐" + println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n") + println "└${"${"─" * maxLength}"}┘" +} +/* END of test tasks results summary */ + task compileLinkCheck(type: JavaCompile) { options.fork = true @@ -1903,22 +2167,56 @@ task cleanDist { } +task launcherJar(type: Jar) { + manifest { + attributes ( + "Main-Class": shadow_jar_main_class, + "Implementation-Version": JALVIEW_VERSION, + "Application-Name": applicationName + ) + } +} + shadowJar { group = "distribution" description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar" if (buildDist) { dependsOn makeDist } - from ("${jalviewDir}/${libDistDir}") { - include("*.jar") - } + + def jarFiles = fileTree(dir: "${jalviewDir}/${libDistDir}", include: "*.jar", exclude: "regex.jar").getFiles() + def groovyJars = jarFiles.findAll {it1 -> file(it1).getName().startsWith("groovy-swing")} + def otherJars = jarFiles.findAll {it2 -> !file(it2).getName().startsWith("groovy-swing")} + from groovyJars + from otherJars + manifest { - attributes "Implementation-Version": JALVIEW_VERSION, - "Application-Name": applicationName + // shadowJar manifest must inheritFrom another Jar task. Can't set attributes here. + inheritFrom(project.tasks.launcherJar.manifest) + } + // we need to include the groovy-swing Include-Package for it to run in the shadowJar + doFirst { + def jarFileManifests = [] + groovyJars.each { jarFile -> + def mf = zipTree(jarFile).getFiles().find { it.getName().equals("MANIFEST.MF") } + if (mf != null) { + jarFileManifests += mf + } + } + manifest { + from (jarFileManifests) { + eachEntry { details -> + if (!details.key.equals("Import-Package")) { + details.exclude() + } + } + } + } } duplicatesStrategy "INCLUDE" + // this mainClassName is mandatory but gets ignored due to manifest created in doFirst{}. Set the Main-Class as an attribute in launcherJar instead mainClassName = shadow_jar_main_class mergeServiceFiles() classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION @@ -1972,9 +2270,9 @@ task getdownImages() { dependsOn getdownImagesProcess } -task getdownWebsite() { +task getdownWebsiteBuild() { group = "distribution" - description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer" + description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer. No digest is created." dependsOn getdownImages if (buildDist) { @@ -2026,8 +2324,8 @@ task getdownWebsite() { props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}") props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}") props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}") - props.put("getdown_txt_ui.icon", "${getdownImagesBuildDir}/${getdown_icon}") - props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesBuildDir}/${getdown_mac_dock_icon}") + props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}") + props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}") } props.put("getdown_txt_title", jalview_name) @@ -2086,7 +2384,7 @@ task getdownWebsite() { from s into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}" } - getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}" + getdownTextLines += "xresource = ${getdown_wrapper_script_dir}/${script}" } } @@ -2228,7 +2526,9 @@ task getdownDigestDir(type: JavaExec) { task getdownDigest(type: JavaExec) { group = "distribution" description = "Digest the getdown website folder" - dependsOn getdownWebsite + + dependsOn getdownWebsiteBuild + doFirst { classpath = files(getdownLauncher) } @@ -2261,12 +2561,19 @@ task getdown() { } } +task getdownWebsite { + group = "distribution" + description = "A task to create the whole getdown channel website dir including digest file" + + dependsOn getdownWebsiteBuild + dependsOn getdownDigest +} task getdownArchiveBuild() { group = "distribution" description = "Put files in the archive dir to go on the website" - dependsOn getdownWebsite + dependsOn getdownWebsiteBuild def v = "v${JALVIEW_VERSION_UNDERSCORES}" def vDir = "${getdownArchiveDir}/${v}" @@ -2313,6 +2620,14 @@ task getdownArchiveBuild() { } } + // the wrapper scripts dir + if ( file("${getdownAppBaseDir}/${getdown_wrapper_script_dir}").exists() ) { + copy { + from "${getdownAppBaseDir}/${getdown_wrapper_script_dir}" + into "${getdownFullArchiveDir}/${getdown_wrapper_script_dir}" + } + } + getdownArchiveTxt.write(getdownArchiveTextLines.join("\n")) def vLaunchJvl = file(getdownVersionLaunchJvl) @@ -2411,6 +2726,21 @@ task copyInstall4jTemplate { } } + // delete .VolumeIcon.icns in macos DMG if there isn't one + if (!file(install4jDMGVolumeIcon).exists()) { + println("No '.VolumeIcon.icns' file found. Removing from install4j file.") + install4jConfigXml.'**'.macosArchive.topLevelFiles.each { topLevelFiles -> + topLevelFiles.file.each() { file -> + if (file.attribute("name") && file.attribute("name").equals(".VolumeIcon.icns")) { + println("Removing "+file.toString()) + topLevelFiles.remove(file) + } + } + } + } else { + println("Using '.VolumeIcon.icns' file '${install4jDMGVolumeIcon}'") + } + // disable install screen for OSX dmg (for 2.11.2.0) install4jConfigXml.'**'.macosArchive.each { macosArchive -> macosArchive.attributes().remove('executeSetupApp') @@ -2525,8 +2855,66 @@ task install4jDMGBackgroundImageProcess { } } -task install4jDMGBackgroundImage { + +python { + pip 'ds_store:1.3.0' +} + +task install4jCustomiseDS_StoreX64(type: PythonTask) { + inputs.file(install4jDMGDSStore) + outputs.file(install4jDMGFixedDSStoreX64) + def command_args = [ jalview_customise_ds_store, + '--input', install4jDMGDSStore, + '--output', install4jDMGFixedDSStoreX64, + '--volumename', install4jmacOSArchiveX64Name, + '--backgroundfile', install4j_dmg_background_filename, + '--dmg', install4jmacOSArchiveX64DMGFilename + ".dmg", + '--appname', "${applicationName}.app", + ] + if (file(install4jDMGDSStoreJSON).exists()) { + command_args += [ '--config', install4jDMGDSStoreJSON ] + inputs.file(install4jDMGDSStoreJSON) + } + command = command_args + doFirst { + println("Running command '${command_args.join(' ')}'") + } +} + +task install4jCustomiseDS_StoreAarch64(type: PythonTask) { + inputs.file(install4jDMGDSStore) + outputs.file(install4jDMGFixedDSStoreAarch64) + def command_args = [ jalview_customise_ds_store, + '--input', install4jDMGDSStore, + '--output', install4jDMGFixedDSStoreAarch64, + '--volumename', install4jmacOSArchiveAarch64Name, + '--backgroundfile', install4j_dmg_background_filename, + '--dmg', install4jmacOSArchiveAarch64DMGFilename + ".dmg", + '--appname', "${applicationName}.app", + ] + if (file(install4jDMGDSStoreJSON).exists()) { + command_args += [ '--config', install4jDMGDSStoreJSON ] + inputs.file(install4jDMGDSStoreJSON) + } + command = command_args + doFirst { + def print_args = [] + for (int i = 0; i < command_args.size(); i++) { + def arg = command_args[i] + print_args += (i > 0 && !arg.startsWith("-")) ? "\"${arg}\"" : arg + } + println("Running command '${print_args.join(' ')}'") + } +} + +task install4jCustomiseDS_Store { + dependsOn install4jCustomiseDS_StoreX64 + dependsOn install4jCustomiseDS_StoreAarch64 +} + +task install4jDMGProcesses { dependsOn install4jDMGBackgroundImageProcess + dependsOn install4jCustomiseDS_Store } task installerFiles(type: com.install4j.gradle.Install4jTask) { @@ -2535,10 +2923,13 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) { dependsOn getdown dependsOn copyInstall4jTemplate dependsOn cleanInstallersDataFiles - dependsOn install4jDMGBackgroundImage + dependsOn install4jDMGProcesses projectFile = install4jConfFile + // run install4j with 4g + vmParameters = ["-Xmx4294967296"] + // create an md5 for the input files to use as version for install4j conf file def digest = MessageDigest.getInstance("MD5") digest.update( @@ -2571,13 +2962,17 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) { 'BUNDLE_ID': install4jBundleId, 'INTERNAL_ID': install4jInternalId, 'WINDOWS_APPLICATION_ID': install4jWinApplicationId, - 'MACOS_DMG_DS_STORE': install4jDMGDSStore, + 'MACOS_X64_DMG_DS_STORE': install4jDMGFixedDSStoreX64, + 'MACOS_AARCH64_DMG_DS_STORE': install4jDMGFixedDSStoreAarch64, 'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}", + 'MACOS_DMG_BG_FILENAME': install4j_dmg_background_filename, 'WRAPPER_LINK': getdownWrapperLink, 'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script, 'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script, + 'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script, 'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir, - 'INSTALLER_NAME': install4jInstallerName, + 'MACOSARCHIVE_X64_NAME': install4jmacOSArchiveX64Name, + 'MACOSARCHIVE_AARCH64_NAME': install4jmacOSArchiveAarch64Name, 'INSTALL4J_UTILS_DIR': install4j_utils_dir, 'GETDOWN_CHANNEL_DIR': getdownChannelDir, 'GETDOWN_FILES_DIR': getdown_files_dir, @@ -2596,6 +2991,9 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) { 'WINDOWS_ICONS_FILE': install4jWindowsIconsFile, 'PNG_ICON_FILE': install4jPngIconFile, 'BACKGROUND': install4jBackground, + 'MACOSARCHIVE_X64_DMG_FILENAME': install4jmacOSArchiveX64DMGFilename, + 'MACOSARCHIVE_AARCH64_DMG_FILENAME': install4jmacOSArchiveAarch64DMGFilename, + 'MACOSARCHIVE_VOLUMEICON': install4jDMGVolumeIcon, ] def varNameMap = [ @@ -2775,6 +3173,7 @@ task sourceDist(type: Tar) { into project.name def EXCLUDE_FILES=[ + "dist/*", "build/*", "bin/*", "test-output/", @@ -3989,9 +4388,157 @@ task eclipseAutoBuildTask { } +task jalviewjsCopyStderrLaunchFile(type: Copy) { + from file(jalviewjs_stderr_launch) + into jalviewjsSiteDir + + inputs.file jalviewjs_stderr_launch + outputs.file jalviewjsStderrLaunchFilename +} + +task cleanJalviewjsChromiumUserDir { + doFirst { + delete jalviewjsChromiumUserDir + } + outputs.dir jalviewjsChromiumUserDir + // always run when depended on + outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() } +} + +task jalviewjsChromiumProfile { + dependsOn cleanJalviewjsChromiumUserDir + mustRunAfter cleanJalviewjsChromiumUserDir + + def firstRun = file("${jalviewjsChromiumUserDir}/First Run") + + doFirst { + mkdir jalviewjsChromiumProfileDir + firstRun.text = "" + } + outputs.file firstRun +} + +task jalviewjsLaunchTest { + group "Test" + description "Check JalviewJS opens in a browser" + dependsOn jalviewjsBuildSite + dependsOn jalviewjsCopyStderrLaunchFile + dependsOn jalviewjsChromiumProfile + + def macOS = OperatingSystem.current().isMacOsX() + def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary + if (chromiumBinary.startsWith("~/")) { + chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1) + } + + def stdout + def stderr + doFirst { + def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000 + + def binary = file(chromiumBinary) + if (!binary.exists()) { + throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.") + } + stdout = new ByteArrayOutputStream() + stderr = new ByteArrayOutputStream() + def execStdout + def execStderr + if (jalviewjs_j2s_to_console.equals("true")) { + execStdout = new org.apache.tools.ant.util.TeeOutputStream( + stdout, + System.out) + execStderr = new org.apache.tools.ant.util.TeeOutputStream( + stderr, + System.err) + } else { + execStdout = stdout + execStderr = stderr + } + def execArgs = [ + "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER + "--headless=new", + "--disable-gpu", + "--timeout=${timeoutms}", + "--virtual-time-budget=${timeoutms}", + "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}", + "--profile-directory=${jalviewjs_chromium_profile_name}", + "--allow-file-access-from-files", + "--enable-logging=stderr", + "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}" + ] + + if (true || macOS) { + ScheduledExecutorService executor = Executors.newScheduledThreadPool(3); + Future f1 = executor.submit( + () -> { + exec { + standardOutput = execStdout + errorOutput = execStderr + executable(chromiumBinary) + args(execArgs) + println "COMMAND: '"+commandLine.join(" ")+"'" + } + executor.shutdownNow() + } + ) + + def noChangeBytes = 0 + def noChangeIterations = 0 + executor.scheduleAtFixedRate( + () -> { + String stderrString = stderr.toString() + // shutdown the task if we have a success string + if (stderrString.contains(jalviewjs_desktop_init_string)) { + f1.cancel() + Thread.sleep(1000) + executor.shutdownNow() + } + // if no change in stderr for 10s then also end + if (noChangeIterations >= jalviewjs_chromium_idle_timeout) { + executor.shutdownNow() + } + if (stderrString.length() == noChangeBytes) { + noChangeIterations++ + } else { + noChangeBytes = stderrString.length() + noChangeIterations = 0 + } + }, + 1, 1, TimeUnit.SECONDS) + + executor.schedule(new Runnable(){ + public void run(){ + f1.cancel() + executor.shutdownNow() + } + }, timeoutms, TimeUnit.MILLISECONDS) + + executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS) + executor.shutdownNow() + } + + } + + doLast { + def found = false + stderr.toString().eachLine { line -> + if (line.contains(jalviewjs_desktop_init_string)) { + println("Found line '"+line+"'") + found = true + return + } + } + if (!found) { + throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.") + } + } +} + + task jalviewjs { group "JalviewJS" - description "Build the site" + description "Build the JalviewJS site and run the launch test" dependsOn jalviewjsBuildSite + dependsOn jalviewjsLaunchTest } -