JAL-4421 Remove the disk_image_alias from the backgroundImageAlias altogether as...
[jalview.git] / build.gradle
index 23f3a7b..a8d55dd 100644 (file)
@@ -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.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")
@@ -574,7 +590,10 @@ ext {
   eclipseBinary = string("")
   eclipseVersion = string("")
   eclipseDebug = false
-  
+
+  jalviewjsChromiumUserDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}"
+  jalviewjsChromiumProfileDir = "${ext.jalviewjsChromiumUserDir}/${jalviewjs_chromium_profile_name}"
+
   // ENDEXT
 }
 
@@ -1762,6 +1781,7 @@ task testTask0(type: Test) {
     preserveOrder true
     useDefaultListeners=true
   }
+  timeout = Duration.ofMinutes(15)
 }
 
 /* separated tests */
@@ -1774,6 +1794,30 @@ task testTask1(type: Test) {
     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)
 }
 
 /* insert more testTaskNs here -- change N to next digit or other string */
@@ -1790,6 +1834,7 @@ task testTaskN(type: Test) {
 }
 */
 
+
 /*
  * adapted from https://medium.com/@wasyl/pretty-tests-summary-in-gradle-744804dd676c
  * to summarise test results from all Test tasks
@@ -1823,10 +1868,16 @@ tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { te
     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
@@ -2116,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
@@ -2185,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) {
@@ -2299,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}"
       }
     }
 
@@ -2441,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)
   }
@@ -2474,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}"
@@ -2526,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)
@@ -2624,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')
@@ -2738,8 +2855,52 @@ task install4jDMGBackgroundImageProcess {
   }
 }
 
-task install4jDMGBackgroundImage {
+
+python {
+  pip 'ds_store:1.3.1'
+}
+
+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" ]
+  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" ]
+  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) {
@@ -2748,10 +2909,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(
@@ -2784,14 +2948,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,
@@ -2810,6 +2977,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 = [
@@ -2989,6 +3159,7 @@ task sourceDist(type: Tar) {
   into project.name
 
   def EXCLUDE_FILES=[
+    "dist/*",
     "build/*",
     "bin/*",
     "test-output/",
@@ -4203,13 +4374,6 @@ task eclipseAutoBuildTask {
 }
 
 
-task jalviewjs {
-  group "JalviewJS"
-  description "Build the site"
-  dependsOn jalviewjsBuildSite
-}
-
-
 task jalviewjsCopyStderrLaunchFile(type: Copy) {
   from file(jalviewjs_stderr_launch)
   into jalviewjsSiteDir
@@ -4218,62 +4382,133 @@ task jalviewjsCopyStderrLaunchFile(type: Copy) {
   outputs.file jalviewjsStderrLaunchFilename
 }
 
+task cleanJalviewjsChromiumUserDir {
+  doFirst {
+    delete jalviewjsChromiumUserDir
+  }
+  outputs.dir jalviewjsChromiumUserDir
+  // always run when depended on
+  outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
+}
+
 task jalviewjsChromiumProfile {
-  def profileDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}/${jalviewjs_chromium_profile_name}"
-  def firstRun = file("${profileDir}/First Run")
+  dependsOn cleanJalviewjsChromiumUserDir
+  mustRunAfter cleanJalviewjsChromiumUserDir
+
+  def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
 
   doFirst {
-    mkdir profileDir
+    mkdir jalviewjsChromiumProfileDir
     firstRun.text = ""
   }
-  
   outputs.file firstRun
 }
 
-task jalviewjsLaunchTest(type: Exec) {
+task jalviewjsLaunchTest {
   group "Test"
   description "Check JalviewJS opens in a browser"
   dependsOn jalviewjsBuildSite
   dependsOn jalviewjsCopyStderrLaunchFile
   dependsOn jalviewjsChromiumProfile
 
-  def chromiumBinary = jalviewjs_chromium_binary
+  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 exec = file(chromiumBinary)
-    if (!exec.exists()) {
-      throw new GradleException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
+    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)
 
-  executable(chromiumBinary)
-  args([
-    "--headless=new",
-    "--timeout=60000",
-    "--virtual-time-budget=60000",
-    "--user-data-dir=${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
-    "--profile-directory=${jalviewjs_chromium_profile_name}",
-    "--allow-file-access-from-files",
-    "--enable-logging=stderr",
-    jalviewjsStderrLaunchFilename
-  ])
-
-  /*
-  standardOutput = new ByteArrayOutputStream()
-  errorOutput = new ByteArrayOutputStream()
+      executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
+      executor.shutdownNow()
+    }
+
+  }
   
   doLast {
-    println("Chrome STDOUT: ")
-    println(standardOutput.toString())
-    println("Chrome STDERR: ")
-    println(errorOutput.toString())
-
     def found = false
-    def stderr = errorOutput.toString()
-    stderr.eachLine { line ->
+    stderr.toString().eachLine { line ->
       if (line.contains(jalviewjs_desktop_init_string)) {
         println("Found line '"+line+"'")
         found = true
@@ -4284,5 +4519,12 @@ task jalviewjsLaunchTest(type: Exec) {
       throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
     }
   }
-  */
+}
+  
+
+task jalviewjs {
+  group "JalviewJS"
+  description "Build the JalviewJS site and run the launch test"
+  dependsOn jalviewjsBuildSite
+  dependsOn jalviewjsLaunchTest
 }