JAL-4167 Added colours to bash output
[jalview.git] / build.gradle
index 958343b..3b06476 100644 (file)
@@ -9,6 +9,7 @@ import org.gradle.util.ConfigureUtil
 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 groovy.transform.ExternalizeMethods
 import groovy.util.XmlParser
 import groovy.xml.XmlUtil
@@ -37,6 +38,7 @@ buildscript {
   dependencies {
     classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
     classpath "org.jsoup:jsoup:1.14.3"
+    classpath "com.eowise:gradle-imagemagick:0.5.1"
   }
 }
 
@@ -47,8 +49,8 @@ plugins {
   id 'eclipse'
   id "com.diffplug.gradle.spotless" version "3.28.0"
   id 'com.github.johnrengelman.shadow' version '4.0.3'
-  id 'com.install4j.gradle' version '9.0.6'
-  id 'com.dorongold.task-tree' version '1.5' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
+  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
 }
 
@@ -108,8 +110,12 @@ ext {
   getdownChannelName = CHANNEL.toLowerCase()
   // default to "default". Currently only has different cosmetics for "develop", "release", "default"
   propertiesChannelName = ["develop", "release", "test-release", "jalviewjs", "jalviewjs-release" ].contains(getdownChannelName) ? getdownChannelName : "default"
+  channelDirName = propertiesChannelName
   // Import channel_properties
-  channelDir = string("${jalviewDir}/${channel_properties_dir}/${propertiesChannelName}")
+  if (getdownChannelName.startsWith("develop-")) {
+    channelDirName = "develop-SUFFIX"
+  }
+  channelDir = string("${jalviewDir}/${channel_properties_dir}/${channelDirName}")
   channelGradleProperties = string("${channelDir}/channel_gradle.properties")
   channelPropsFile = string("${channelDir}/${resource_dir}/${channel_props}")
   overrideProperties(channelGradleProperties, false)
@@ -138,6 +144,7 @@ ext {
   if (findProperty("JALVIEW_VERSION")==null || "".equals(JALVIEW_VERSION)) {
     JALVIEW_VERSION = releaseProps.get("jalview.version")
   }
+  println("JALVIEW_VERSION is set to '${JALVIEW_VERSION}'")
   
   // this property set when running Eclipse headlessly
   j2sHeadlessBuildProperty = string("net.sf.j2s.core.headlessbuild")
@@ -197,6 +204,8 @@ ext {
   testSourceDir = useClover ? cloverTestInstrDir : testDir
   testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
 
+  channelSuffix = ""
+  backgroundImageText = BACKGROUNDIMAGETEXT
   getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
   getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
   getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
@@ -214,12 +223,15 @@ ext {
   getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
   getdownAppDistDir = getdown_app_dir_alt
   getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
+  getdownImagesBuildDir = string("${buildDir}/imagemagick/getdown")
   getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
   reportRsyncCommand = false
   jvlChannelName = CHANNEL.toLowerCase()
   install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build
   install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
-  install4jDMGBackgroundImage = "${install4j_images_dir}/${install4j_dmg_background}"
+  install4jDMGBackgroundImageDir = "${install4j_images_dir}"
+  install4jDMGBackgroundImageBuildDir = "build/imagemagick/install4j"
+  install4jDMGBackgroundImageFile = "${install4j_dmg_background}"
   install4jInstallerName = "${jalview_name} Non-Release Installer"
   install4jExecutableName = install4j_executable_name
   install4jExtraScheme = "jalviewx"
@@ -244,6 +256,7 @@ ext {
       testng_excluded_groups = "Not-bamboo"
     }
     install4jExtraScheme = "jalviewb"
+    backgroundImageText = true
     break
 
     case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
@@ -274,7 +287,7 @@ ext {
     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
     getdownAppBase = file(getdownAppBaseDir).toURI().toString()
     if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
-      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
+      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution [did not find '${ARCHIVEDIR}/${package_dir}']")
     } else {
       package_dir = string("${ARCHIVEDIR}/${package_dir}")
       buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
@@ -286,6 +299,23 @@ ext {
     install4jExtraScheme = "jalviewa"
     break
 
+    case ~/^DEVELOP-([\.\-\w]*)$/:
+    def suffix = Matcher.lastMatcher[0][1]
+    reportRsyncCommand = true
+    getdownSetAppBaseProperty = true
+    JALVIEW_VERSION=JALVIEW_VERSION+"-d${suffix}-${buildDate}"
+    install4jSuffix = "Develop ${suffix}"
+    install4jExtraScheme = "jalviewd"
+    install4jInstallerName = "${jalview_name} Develop ${suffix} Installer"
+    getdownChannelName = string("develop-${suffix}")
+    getdownChannelDir = string("${getdown_website_dir}/${getdownChannelName}")
+    getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
+    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
+    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
+    channelSuffix = string(suffix)
+    backgroundImageText = true
+    break
+
     case "DEVELOP":
     reportRsyncCommand = true
     getdownSetAppBaseProperty = true
@@ -295,6 +325,7 @@ ext {
     install4jSuffix = "Develop"
     install4jExtraScheme = "jalviewd"
     install4jInstallerName = "${jalview_name} Develop Installer"
+    backgroundImageText = true
     break
 
     case "TEST-RELEASE":
@@ -309,6 +340,7 @@ ext {
     install4jSuffix = "Test"
     install4jExtraScheme = "jalviewt"
     install4jInstallerName = "${jalview_name} Test Installer"
+    backgroundImageText = true
     break
 
     case ~/^SCRATCH(|-[-\w]*)$/:
@@ -332,6 +364,7 @@ ext {
     install4jSuffix = "Test-Local"
     install4jExtraScheme = "jalviewt"
     install4jInstallerName = "${jalview_name} Test Installer"
+    backgroundImageText = true
     break
 
     case [ "LOCAL", "JALVIEWJS" ]:
@@ -482,16 +515,10 @@ ext {
   // for install4j
   JAVA_MIN_VERSION = JAVA_VERSION
   JAVA_MAX_VERSION = JAVA_VERSION
-  def jreInstallsDir = string(jre_installs_dir)
+  jreInstallsDir = string(jre_installs_dir)
   if (jreInstallsDir.startsWith("~/")) {
     jreInstallsDir = System.getProperty("user.home") + jreInstallsDir.substring(1)
   }
-  macosJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-mac-x64/jre")
-  windowsJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-windows-x64/jre")
-  linuxJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-linux-x64/jre")
-  macosJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_mac_x64.tar.gz")
-  windowsJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_windows_x64.tar.gz")
-  linuxJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_linux_x64.tar.gz")
   install4jDir = string("${jalviewDir}/${install4j_utils_dir}")
   install4jConfFileName = string("jalview-install4j-conf.install4j")
   install4jConfFile = file("${install4jDir}/${install4jConfFileName}")
@@ -513,6 +540,14 @@ ext {
   helpSourceDir = string("${helpParentDir}/${help_dir}")
   helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")
 
+  convertBinary = null
+  convertBinaryExpectedLocation = imagemagick_convert
+  if (convertBinaryExpectedLocation.startsWith("~/")) {
+    convertBinaryExpectedLocation = System.getProperty("user.home") + convertBinaryExpectedLocation.substring(1)
+  }
+  if (file(convertBinaryExpectedLocation).exists()) {
+    convertBinary = convertBinaryExpectedLocation
+  }
 
   relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
   jalviewjsBuildDir = string("${relativeBuildDir}/jalviewjs")
@@ -1029,7 +1064,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")
@@ -1041,7 +1076,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")
   }
@@ -1199,6 +1234,214 @@ task convertMdFiles {
 }
 
 
+def hugoTemplateSubstitutions(String input, Map extras=null) {
+  def replacements = [
+    DATE: getDate("yyyy-MM-dd"),
+    CHANNEL: propertiesChannelName,
+    APPLICATION_NAME: applicationName,
+    GIT_HASH: gitHash,
+    GIT_BRANCH: gitBranch,
+    VERSION: JALVIEW_VERSION,
+    JAVA_VERSION: JAVA_VERSION,
+    VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
+    DRAFT: "false",
+    JVL_HEADER: ""
+  ]
+  def output = input
+  if (extras != null) {
+    extras.each{ k, v ->
+      output = output.replaceAll("__${k}__", ((v == null)?"":v))
+    }
+  }
+  replacements.each{ k, v ->
+    output = output.replaceAll("__${k}__", ((v == null)?"":v))
+  }
+  return output
+}
+
+def mdFileComponents(File mdFile, def dateOnly=false) {
+  def map = [:]
+  def content = ""
+  if (mdFile.exists()) {
+    def inFrontMatter = false
+    def firstLine = true
+    mdFile.eachLine { line ->
+      if (line.matches("---")) {
+        def prev = inFrontMatter
+        inFrontMatter = firstLine
+        if (inFrontMatter != prev)
+          return false
+      }
+      if (inFrontMatter) {
+        def m = null
+        if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
+          map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
+        } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
+          map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
+        } else if (m = line =~ /^channel:\s*(\S+)/) {
+          map["channel"] = m[0][1]
+        } else if (m = line =~ /^version:\s*(\S+)/) {
+          map["version"] = m[0][1]
+        } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
+          map[ m[0][1] ] = m[0][2]
+        }
+        if (dateOnly && map["date"] != null) {
+          return false
+        }
+      } else {
+        if (dateOnly)
+          return false
+        content += line+"\n"
+      }
+      firstLine = false
+    }
+  }
+  return dateOnly ? map["date"] : [map, content]
+}
+
+task hugoTemplates {
+  group "website"
+  description "Create partially populated md pages for hugo website build"
+
+  def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
+  def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
+  def templateFiles = fileTree(dir: hugoTemplatesDir)
+  def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+  def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+  def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
+  def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")
+
+  doFirst {
+    // specific release template for version archive
+    def changes = ""
+    def whatsnew = null
+    def givenDate = null
+    def givenChannel = null
+    def givenVersion = null
+    if (CHANNEL == "RELEASE") {
+      def (map, content) = mdFileComponents(releaseMdFile)
+      givenDate = map.date
+      givenChannel = map.channel
+      givenVersion = map.version
+      changes = content
+      if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
+        throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
+      }
+
+      if (whatsnewMdFile.exists())
+        whatsnew = whatsnewMdFile.text
+    }
+
+    def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
+    def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []
+
+    def changesHugo = null
+    if (changes != null) {
+      changesHugo = '<div class="release_notes">\n\n'
+      def inSection = false
+      changes.eachLine { line ->
+        def m = null
+        if (m = line =~ /^##([^#].*)$/) {
+          if (inSection) {
+            changesHugo += "</div>\n\n"
+          }
+          def section = m[0][1].trim()
+          section = section.toLowerCase()
+          section = section.replaceAll(/ +/, "_")
+          section = section.replaceAll(/[^a-z0-9_\-]/, "")
+          changesHugo += "<div class=\"${section}\">\n\n"
+          inSection = true
+        } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
+          def comment = m[0][2].trim()
+          if (comment != "") {
+            comment = comment.replaceAll('"', "&quot;")
+            def issuekeys = []
+            comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
+            def newline = m[0][1]
+            if (comment.trim() != "")
+              newline += "{{<comment>}}${comment}{{</comment>}}  "
+            newline += m[0][3].trim()
+            if (issuekeys.size() > 0)
+              newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
+            if (m[0][4] != null)
+              newline += m[0][4]
+            line = newline
+          }
+        }
+        changesHugo += line+"\n"
+      }
+      if (inSection) {
+        changesHugo += "\n</div>\n\n"
+      }
+      changesHugo += '</div>'
+    }
+
+    templateFiles.each{ templateFile ->
+      def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
+      def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
+      def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )
+
+      def outPathName = string("${hugoBuildDir}/$newRelPathName")
+
+      copy {
+        from templateFile
+        rename(templateFile.getName(), newFileName)
+        into outPathName
+      }
+
+      def newFile = file("${outPathName}/${newFileName}".toString())
+      def content = newFile.text
+      newFile.text = hugoTemplateSubstitutions(content,
+        [
+          WHATSNEW: whatsnew,
+          CHANGES: changesHugo,
+          DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
+          DRAFT: givenDate == null ? "true" : "false",
+          JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
+          JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
+        ]
+      )
+    }
+
+  }
+
+  inputs.file(oldJvlFile)
+  inputs.dir(hugoTemplatesDir)
+  inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
+  inputs.property("CHANNEL", { CHANNEL })
+}
+
+def getMdDate(File mdFile) {
+  return mdFileComponents(mdFile, true)
+}
+
+def getMdSections(String content) {
+  def sections = [:]
+  def sectionContent = ""
+  def sectionName = null
+  content.eachLine { line ->
+    def m = null
+    if (m = line =~ /^##([^#].*)$/) {
+      if (sectionName != null) {
+        sections[sectionName] = sectionContent
+        sectionName = null
+        sectionContent = ""
+      }
+      sectionName = m[0][1].trim()
+      sectionName = sectionName.toLowerCase()
+      sectionName = sectionName.replaceAll(/ +/, "_")
+      sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
+    } else if (sectionName != null) {
+      sectionContent += line+"\n"
+    }
+  }
+  if (sectionContent != null) {
+    sections[sectionName] = sectionContent
+  }
+  return sections
+}
+
+
 task copyHelp(type: Copy) {
   def inputDir = helpSourceDir
   def outputDir = "${helpBuildDir}/${help_dir}"
@@ -1234,46 +1477,195 @@ task copyHelp(type: Copy) {
 }
 
 
-task copyResources(type: Copy) {
-  group = "build"
-  description = "Copy (and make text substitutions in) the resources dir to the build area"
+task releasesTemplates {
+  group "help"
+  description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
 
-  def inputDir = resourceDir
-  def outputDir = resourcesBuildDir
-  from(inputDir) {
-    include('**/*.txt')
-    include('**/*.md')
-    include('**/*.html')
-    include('**/*.xml')
-    filter(ReplaceTokens,
-      beginToken: '$$',
-      endToken: '$$',
-      tokens: [
-        'Version-Rel': JALVIEW_VERSION,
-        'Year-Rel': getDate("yyyy")
-      ]
-    )
-  }
-  from(inputDir) {
-    exclude('**/*.txt')
-    exclude('**/*.md')
-    exclude('**/*.html')
-    exclude('**/*.xml')
-  }
-  into outputDir
+  dependsOn copyHelp
 
-  inputs.dir(inputDir)
-  outputs.dir(outputDir)
-}
+  def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
+  def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
+  def releasesHtmlFile = file("${helpBuildDir}/${help_dir}/${releases_html}")
+  def whatsnewHtmlFile = file("${helpBuildDir}/${help_dir}/${whatsnew_html}")
+  def releasesMdDir = "${jalviewDir}/${releases_dir}"
+  def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
 
-task copyChannelResources(type: Copy) {
-  dependsOn copyResources
+  doFirst {
+    def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+    def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+
+    if (CHANNEL == "RELEASE") {
+      if (!releaseMdFile.exists()) {
+        throw new GradleException("File ${releaseMdFile} must be created for RELEASE")
+      }
+      if (!whatsnewMdFile.exists()) {
+        throw new GradleException("File ${whatsnewMdFile} must be created for RELEASE")
+      }
+    }
+
+    def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
+    def releaseFilesDates = releaseFiles.collectEntries {
+      [(it): getMdDate(it)]
+    }
+    releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
+
+    def releasesTemplate = releasesTemplateFile.text
+    def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
+    def versionTemplate = m[0][1]
+
+    MutableDataSet options = new MutableDataSet()
+
+    def extensions = new ArrayList<>()
+    options.set(Parser.EXTENSIONS, extensions)
+    options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)
+
+    Parser parser = Parser.builder(options).build()
+    HtmlRenderer renderer = HtmlRenderer.builder(options).build()
+
+    def actualVersions = releaseFiles.collect { rf ->
+      def (rfMap, rfContent) = mdFileComponents(rf)
+      return rfMap.version
+    }
+    def versionsHtml = ""
+    def linkedVersions = []
+    releaseFiles.reverse().each { rFile ->
+      def (rMap, rContent) = mdFileComponents(rFile)
+
+      def versionLink = ""
+      def partialVersion = ""
+      def firstPart = true
+      rMap.version.split("\\.").each { part ->
+        def displayPart = ( firstPart ? "" : "." ) + part
+        partialVersion += displayPart
+        if (
+            linkedVersions.contains(partialVersion)
+            || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
+            ) {
+          versionLink += displayPart
+        } else {
+          versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
+          linkedVersions += partialVersion
+        }
+        firstPart = false
+      }
+      def displayDate = releaseFilesDates[rFile].format("dd/MM/yyyy")
+
+      def lm = null
+      def rContentProcessed = ""
+      rContent.eachLine { line ->
+        if (lm = line =~ /^(\s*-)(\s*<!--[^>]*?-->)(.*)$/) {
+          line = "${lm[0][1]}${lm[0][3]}${lm[0][2]}"
+      } else if (lm = line =~ /^###([^#]+.*)$/) {
+          line = "_${lm[0][1].trim()}_"
+        }
+        rContentProcessed += line + "\n"
+      }
+
+      def rContentSections = getMdSections(rContentProcessed)
+      def rVersion = versionTemplate
+      if (rVersion != "") {
+        def rNewFeatures = rContentSections["new_features"]
+        def rIssuesResolved = rContentSections["issues_resolved"]
+        Node newFeaturesNode = parser.parse(rNewFeatures)
+        String newFeaturesHtml = renderer.render(newFeaturesNode)
+        Node issuesResolvedNode = parser.parse(rIssuesResolved)
+        String issuesResolvedHtml = renderer.render(issuesResolvedNode)
+        rVersion = hugoTemplateSubstitutions(rVersion,
+          [
+            VERSION: rMap.version,
+            VERSION_LINK: versionLink,
+            DISPLAY_DATE: displayDate,
+            NEW_FEATURES: newFeaturesHtml,
+            ISSUES_RESOLVED: issuesResolvedHtml
+          ]
+        )
+        versionsHtml += rVersion
+      }
+    }
+
+    releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
+    releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
+    releasesHtmlFile.text = releasesTemplate
+
+    if (whatsnewMdFile.exists()) {
+      def wnDisplayDate = releaseFilesDates[releaseMdFile] != null ? releaseFilesDates[releaseMdFile].format("dd MMMM yyyy") : ""
+      def whatsnewMd = hugoTemplateSubstitutions(whatsnewMdFile.text)
+      Node whatsnewNode = parser.parse(whatsnewMd)
+      String whatsnewHtml = renderer.render(whatsnewNode)
+      whatsnewHtml = whatsnewTemplateFile.text.replaceAll("__WHATS_NEW__", whatsnewHtml)
+      whatsnewHtmlFile.text = hugoTemplateSubstitutions(whatsnewHtml,
+        [
+            VERSION: JALVIEW_VERSION,
+          DISPLAY_DATE: wnDisplayDate
+        ]
+      )
+    } else if (gradle.taskGraph.hasTask(":linkCheck")) {
+      whatsnewHtmlFile.text = "Development build " + getDate("yyyy-MM-dd HH:mm:ss")
+    }
+
+  }
+
+  inputs.file(releasesTemplateFile)
+  inputs.file(whatsnewTemplateFile)
+  inputs.dir(releasesMdDir)
+  inputs.dir(whatsnewMdDir)
+  outputs.file(releasesHtmlFile)
+  outputs.file(whatsnewHtmlFile)
+}
+
+
+task copyResources(type: Copy) {
+  group = "build"
+  description = "Copy (and make text substitutions in) the resources dir to the build area"
+
+  def inputDir = resourceDir
+  def outputDir = resourcesBuildDir
+  from(inputDir) {
+    include('**/*.txt')
+    include('**/*.md')
+    include('**/*.html')
+    include('**/*.xml')
+    filter(ReplaceTokens,
+      beginToken: '$$',
+      endToken: '$$',
+      tokens: [
+        'Version-Rel': JALVIEW_VERSION,
+        'Year-Rel': getDate("yyyy")
+      ]
+    )
+  }
+  from(inputDir) {
+    exclude('**/*.txt')
+    exclude('**/*.md')
+    exclude('**/*.html')
+    exclude('**/*.xml')
+  }
+  into outputDir
+
+  inputs.dir(inputDir)
+  outputs.dir(outputDir)
+}
+
+task copyChannelResources(type: Copy) {
+  dependsOn copyResources
   group = "build"
   description = "Copy the channel resources dir to the build resources area"
 
   def inputDir = "${channelDir}/${resource_dir}"
   def outputDir = resourcesBuildDir
-  from inputDir
+  from(inputDir) {
+    include(channel_props)
+    filter(ReplaceTokens,
+      beginToken: '__',
+      endToken: '__',
+      tokens: [
+        'SUFFIX': channelSuffix
+      ]
+    )
+  }
+  from(inputDir) {
+    exclude(channel_props)
+  }
   into outputDir
 
   inputs.dir(inputDir)
@@ -1294,6 +1686,7 @@ task createBuildProperties(type: WriteProperties) {
   property "BUILD_DATE", getDate("HH:mm:ss dd MMMM yyyy")
   property "VERSION", JALVIEW_VERSION
   property "INSTALLATION", INSTALLATION+" git-commit:"+gitHash+" ["+gitBranch+"]"
+  property "JAVA_COMPILE_VERSION", JAVA_INTEGER_VERSION
   if (getdownSetAppBaseProperty) {
     property "GETDOWNAPPBASE", getdownAppBase
     property "GETDOWNAPPDISTDIR", getdownAppDistDir
@@ -1331,6 +1724,7 @@ task prepare {
   dependsOn buildResources
   dependsOn copyDocs
   dependsOn copyHelp
+  dependsOn releasesTemplates
   dependsOn convertMdFiles
   dependsOn buildIndices
 }
@@ -1338,26 +1732,91 @@ 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
-
-  if (useClover) {
-    dependsOn cloverClasses
-   } else { //?
-    dependsOn compileJava //?
+ext.testsFailed = false
+/* 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.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
   }
+}
 
+/* separated tests */
+task testTask1(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
   useTestNG() {
-    includeGroups testng_groups
-    excludeGroups testng_excluded_groups
+    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
+  }
+
+  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 summary = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
+
+    // Add reports in `testsResults`, keep failed suites at the end
+    if (result.resultType == TestResult.ResultType.SUCCESS) {
+      rootProject.ext.testsResults.add(0, summary)
+    } else {
+      rootProject.ext.testsResults.add(summary)
+    }
+
+    if (result.resultType == TestResult.ResultType.FAILURE) {
+      testsFailed = true
+    }
+  }
 
+  // from original test task
   maxHeapSize = "1024m"
 
   workingDir = jalviewDir
@@ -1380,6 +1839,108 @@ test {
       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
     }
   }
+
+}
+
+gradle.buildFinished {
+    def allResults = rootProject.ext.testsResults
+
+    if (!allResults.isEmpty()) {
+        printResults allResults
+        allResults.each {r ->
+          if (r[2].resultType == TestResult.ResultType.FAILURE)
+            throw GradleException("Failed tests (buildFinished)!")
+        }
+    }
+}
+
+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 = []
+    allResults.each {
+      def projectName = it[0]
+      def taskName = it[1]
+      def result = it[2]
+      def time = it[3]
+      def report = it[4]
+      def colour = 'black'
+      switch(result.resultType) {
+        case TestResult.ResultType.SUCCESS:
+          colour = 'green'
+          break;
+        case TestResult.ResultType.FAILURE:
+          colour = 'red'
+          failedTests = true
+          break;
+        default:
+          colour = 'yellow'
+          break;
+      }
+      def summaryCol = "${projectName}:${taskName} results: ${styler[colour](result.resultType)} (" +
+      "${result.testCount} tests, " +
+      (result.successfulTestCount > 0 ? "${styler['green'](result.successfulTestCount)} successes" : "${result.successfulTestCount} successes") + ", " +
+      (result.failedTestCount > 0 ? "${styler['red'](result.failedTestCount)} failures" : "${result.failedTestCount} failures") + ", " +
+      "${result.skippedTestCount} skipped" +
+      ") " + "in ${time}"
+      def summaryPlain = "${projectName}:${taskName} results: ${result.resultType} (" +
+      "${result.testCount} tests, " +
+      "${result.successfulTestCount} successes, " +
+      "${result.failedTestCount} failures, " +
+      "${result.skippedTestCount} skipped" +
+      ") " + "in ${time}"
+      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)
+    }
+
+    println "┌${"${"─" * maxLength}"}┐"
+
+    println summaryLines.collect {info ->
+      def ls = info[0]
+      def summary = info[1]
+      def report = info[2]
+
+      return "│" + summary + " " * (maxLength - ls) + "│" + "\n" +
+      "│" + report + " " * (maxLength - report.length()) + "│"
+
+      }.join("\n├${"${"─" * maxLength}"}┤\n")
+
+    println "└${"${"─" * maxLength}"}┘"
+}
+/* END of test tasks results summary */
+
+task verifyTestStatus {
+  group = "Verification"
+  description = "Task that FAILs the build if any tests failed"
+  doLast {
+    if (testsFailed) {
+      throw new GradleException("There were failing tests!")
+    }
+  }
+}
+
+test {
+  // from original test task
+  if (useClover) {
+    dependsOn cloverClasses
+  } else { //?
+    dependsOn testClasses
+  }
+  dependsOn tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}
+  finalizedBy verifyTestStatus
+
+
+  // not running tests in this task
+  exclude "**/*"
 }
 
 
@@ -1403,7 +1964,7 @@ task linkCheck(type: JavaExec) {
   def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
   classpath = files("${jalviewDir}/${utils_dir}")
   main = "HelpLinksChecker"
-  workingDir = jalviewDir
+  workingDir = "${helpBuildDir}"
   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
 
   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
@@ -1523,10 +2084,58 @@ shadowJar {
   minimize()
 }
 
+task getdownImagesCopy() {
+  inputs.dir getdownImagesDir
+  outputs.dir getdownImagesBuildDir
+
+  doFirst {
+    copy {
+      from(getdownImagesDir) {
+        include("*getdown*.png")
+      }
+      into getdownImagesBuildDir
+    }
+  }
+}
+
+task getdownImagesProcess() {
+  dependsOn getdownImagesCopy
+
+  doFirst {
+    if (backgroundImageText) {
+      if (convertBinary == null) {
+        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+      }
+      if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
+        throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+      }
+      fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
+        exec {
+          executable convertBinary
+          args = [
+            file.getPath(),
+            '-font', getdown_background_image_text_font,
+            '-fill', getdown_background_image_text_colour,
+            '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
+            '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+            '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+            file.getPath()
+          ]
+        }
+      }
+    }
+  }
+}
+
+task getdownImages() {
+  dependsOn getdownImagesProcess
+}
 
 task getdownWebsite() {
   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"
+
+  dependsOn getdownImages
   if (buildDist) {
     dependsOn makeDist
   }
@@ -1549,6 +2158,13 @@ task getdownWebsite() {
 
     copy {
       from channelPropsFile
+      filter(ReplaceTokens,
+        beginToken: '__',
+        endToken: '__',
+        tokens: [
+          'SUFFIX': channelSuffix
+        ]
+      )
       into getdownAppBaseDir
     }
     getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
@@ -1564,13 +2180,13 @@ task getdownWebsite() {
     if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
       props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
     }
-    if (getdownImagesDir != null && file(getdownImagesDir).exists()) {
-      props.put("getdown_txt_ui.background_image", "${getdownImagesDir}/${getdown_background_image}")
-      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesDir}/${getdown_instant_background_image}")
-      props.put("getdown_txt_ui.error_background", "${getdownImagesDir}/${getdown_error_background}")
-      props.put("getdown_txt_ui.progress_image", "${getdownImagesDir}/${getdown_progress_image}")
-      props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
-      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
+    if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
+      props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
+      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_title", jalview_name)
@@ -2026,12 +2642,59 @@ task cleanInstallersDataFiles {
   }
 }
 
+task install4jDMGBackgroundImageCopy {
+  inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
+  outputs.dir "${install4jDMGBackgroundImageBuildDir}"
+  doFirst {
+    copy {
+      from(install4jDMGBackgroundImageDir) {
+        include(install4jDMGBackgroundImageFile)
+      }
+      into install4jDMGBackgroundImageBuildDir
+    }
+  }
+}
+
+task install4jDMGBackgroundImageProcess {
+  dependsOn install4jDMGBackgroundImageCopy
+
+  doFirst {
+    if (backgroundImageText) {
+      if (convertBinary == null) {
+        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+      }
+      if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
+        throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+      }
+      fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
+        exec {
+          executable convertBinary
+          args = [
+            file.getPath(),
+            '-font', install4j_background_image_text_font,
+            '-fill', install4j_background_image_text_colour,
+            '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
+            '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+            '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+            file.getPath()
+          ]
+        }
+      }
+    }
+  }
+}
+
+task install4jDMGBackgroundImage {
+  dependsOn install4jDMGBackgroundImageProcess
+}
+
 task installerFiles(type: com.install4j.gradle.Install4jTask) {
   group = "distribution"
   description = "Create the install4j installers"
   dependsOn getdown
   dependsOn copyInstall4jTemplate
   dependsOn cleanInstallersDataFiles
+  dependsOn install4jDMGBackgroundImage
 
   projectFile = install4jConfFile
 
@@ -2063,18 +2726,12 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) {
     'JAVA_VERSION': JAVA_VERSION,
     'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
     'VERSION': JALVIEW_VERSION,
-    'MACOS_JAVA_VM_DIR': macosJavaVMDir,
-    'WINDOWS_JAVA_VM_DIR': windowsJavaVMDir,
-    'LINUX_JAVA_VM_DIR': linuxJavaVMDir,
-    'MACOS_JAVA_VM_TGZ': macosJavaVMTgz,
-    'WINDOWS_JAVA_VM_TGZ': windowsJavaVMTgz,
-    'LINUX_JAVA_VM_TGZ': linuxJavaVMTgz,
     'COPYRIGHT_MESSAGE': install4j_copyright_message,
     'BUNDLE_ID': install4jBundleId,
     'INTERNAL_ID': install4jInternalId,
     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
-    'MACOS_DMG_BG_IMAGE': install4jDMGBackgroundImage,
+    'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
     'WRAPPER_LINK': getdownWrapperLink,
     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
@@ -2098,8 +2755,29 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) {
     'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
     'PNG_ICON_FILE': install4jPngIconFile,
     'BACKGROUND': install4jBackground,
+  ]
 
+  def varNameMap = [
+    'mac': 'MACOS',
+    'windows': 'WINDOWS',
+    'linux': 'LINUX'
+  ]
+  
+  // these are the bundled OS/architecture VMs needed by install4j
+  def osArch = [
+    [ "mac", "x64" ],
+    [ "mac", "aarch64" ],
+    [ "windows", "x64" ],
+    [ "linux", "x64" ],
+    [ "linux", "aarch64" ]
   ]
+  osArch.forEach { os, arch ->
+    variables[ sprintf("%s_%s_JAVA_VM_DIR", varNameMap[os], arch.toUpperCase(Locale.ROOT)) ] = sprintf("%s/jre-%s-%s-%s/jre", jreInstallsDir, JAVA_INTEGER_VERSION, os, arch)
+    // N.B. For some reason install4j requires the below filename to have underscores and not hyphens
+    // otherwise running `gradle installers` generates a non-useful error:
+    // `install4j: compilation failed. Reason: java.lang.NumberFormatException: For input string: "windows"`
+    variables[ sprintf("%s_%s_JAVA_VM_TGZ", varNameMap[os], arch.toUpperCase(Locale.ROOT)) ] = sprintf("%s/tgz/jre_%s_%s_%s.tar.gz", jreInstallsDir, JAVA_INTEGER_VERSION, os, arch)
+  }
 
   //println("INSTALL4J VARIABLES:")
   //variables.each{k,v->println("${k}=${v}")}
@@ -2133,8 +2811,6 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) {
   inputs.dir(getdownAppBaseDir)
   inputs.file(install4jConfFile)
   inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
-  inputs.dir(macosJavaVMDir)
-  inputs.dir(windowsJavaVMDir)
   outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
 }
 
@@ -2206,486 +2882,192 @@ task staticMakeInstallersJsonFile {
     def sha256 = findProperty("i4j_sha256")
     def json = findProperty("i4j_json")
     if (output == null || sha256 == null || json == null) {
-      throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
-    }
-    writeDataJsonFile(file(output), file(sha256), file(json))
-  }
-}
-
-task installers {
-  dependsOn installerFiles
-}
-
-
-spotless {
-  java {
-    eclipse().configFile(eclipse_codestyle_file)
-  }
-}
-
-task createSourceReleaseProperties(type: WriteProperties) {
-  group = "distribution"
-  description = "Create the source RELEASE properties file"
-  
-  def sourceTarBuildDir = "${buildDir}/sourceTar"
-  def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
-  outputFile (sourceReleasePropertiesFile)
-
-  doFirst {
-    releaseProps.each{ key, val -> property key, val }
-    property "git.branch", gitBranch
-    property "git.hash", gitHash
-  }
-
-  outputs.file(outputFile)
-}
-
-task sourceDist(type: Tar) {
-  group "distribution"
-  description "Create a source .tar.gz file for distribution"
-
-  dependsOn createBuildProperties
-  dependsOn convertMdFiles
-  dependsOn eclipseAllPreferences
-  dependsOn createSourceReleaseProperties
-
-
-  def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
-  archiveFileName = outputFileName
-  
-  compression Compression.GZIP
-  
-  into project.name
-
-  def EXCLUDE_FILES=[
-    "build/*",
-    "bin/*",
-    "test-output/",
-    "test-reports",
-    "tests",
-    "clover*/*",
-    ".*",
-    "benchmarking/*",
-    "**/.*",
-    "*.class",
-    "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
-    "*locales/**",
-    "utils/InstallAnywhere",
-    "**/*.log",
-    "RELEASE",
-  ] 
-  def PROCESS_FILES=[
-    "AUTHORS",
-    "CITATION",
-    "FEATURETODO",
-    "JAVA-11-README",
-    "FEATURETODO",
-    "LICENSE",
-    "**/README",
-    "THIRDPARTYLIBS",
-    "TESTNG",
-    "build.gradle",
-    "gradle.properties",
-    "**/*.java",
-    "**/*.html",
-    "**/*.xml",
-    "**/*.gradle",
-    "**/*.groovy",
-    "**/*.properties",
-    "**/*.perl",
-    "**/*.sh",
-  ]
-  def INCLUDE_FILES=[
-    ".classpath",
-    ".settings/org.eclipse.buildship.core.prefs",
-    ".settings/org.eclipse.jdt.core.prefs"
-  ]
-
-  from(jalviewDir) {
-    exclude (EXCLUDE_FILES)
-    include (PROCESS_FILES)
-    filter(ReplaceTokens,
-      beginToken: '$$',
-      endToken: '$$',
-      tokens: [
-        'Version-Rel': JALVIEW_VERSION,
-        'Year-Rel': getDate("yyyy")
-      ]
-    )
-  }
-  from(jalviewDir) {
-    exclude (EXCLUDE_FILES)
-    exclude (PROCESS_FILES)
-    exclude ("appletlib")
-    exclude ("**/*locales")
-    exclude ("*locales/**")
-    exclude ("utils/InstallAnywhere")
-
-    exclude (getdown_files_dir)
-    // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
-    //exclude (getdown_website_dir)
-    //exclude (getdown_archive_dir)
-
-    // exluding these as not using jars as modules yet
-    exclude ("${j11modDir}/**/*.jar")
-  }
-  from(jalviewDir) {
-    include(INCLUDE_FILES)
-  }
-//  from (jalviewDir) {
-//    // explicit includes for stuff that seemed to not get included
-//    include(fileTree("test/**/*."))
-//    exclude(EXCLUDE_FILES)
-//    exclude(PROCESS_FILES)
-//  }
-
-  from(file(buildProperties).getParent()) {
-    include(file(buildProperties).getName())
-    rename(file(buildProperties).getName(), "build_properties")
-    filter({ line ->
-      line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
-    })
-  }
-
-  def sourceTarBuildDir = "${buildDir}/sourceTar"
-  from(sourceTarBuildDir) {
-    // this includes the appended RELEASE properties file
-  }
-}
-
-task dataInstallersJson {
-  group "website"
-  description "Create the installers-VERSION.json data file for installer files created"
-
-  mustRunAfter installers
-  mustRunAfter shadowJar
-  mustRunAfter sourceDist
-  mustRunAfter getdownArchive
-
-  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
-  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
-
-  if (installersOutputTxt.exists()) {
-    inputs.file(installersOutputTxt)
-  }
-  if (install4jCheckSums && installersSha256.exists()) {
-    inputs.file(installersSha256)
-  }
-  [
-    shadowJar.archiveFile, // executable JAR
-    getdownVersionLaunchJvl, // version JVL
-    sourceDist.archiveFile // source TGZ
-  ].each { fileName ->
-    if (file(fileName).exists()) {
-      inputs.file(fileName)
-    }
-  }
-
-  outputs.file(hugoDataJsonFile)
-
-  doFirst {
-    writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
-  }
-}
-
-def hugoTemplateSubstitutions(String input, Map extras=null) {
-  def replacements = [
-    DATE: getDate("yyyy-MM-dd"),
-    CHANNEL: propertiesChannelName,
-    APPLICATION_NAME: applicationName,
-    GIT_HASH: gitHash,
-    GIT_BRANCH: gitBranch,
-    VERSION: JALVIEW_VERSION,
-    JAVA_VERSION: JAVA_VERSION,
-    VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
-    DRAFT: "false",
-    JVL_HEADER: ""
-  ]
-  def output = input
-  if (extras != null) {
-    extras.each{ k, v ->
-      output = output.replaceAll("__${k}__", ((v == null)?"":v))
-    }
-  }
-  replacements.each{ k, v ->
-    output = output.replaceAll("__${k}__", ((v == null)?"":v))
-  }
-  return output
-}
-
-def mdFileComponents(File mdFile, def dateOnly=false) {
-  def map = [:]
-  def content = ""
-  if (mdFile.exists()) {
-    def inFrontMatter = false
-    def firstLine = true
-    mdFile.eachLine { line ->
-      if (line.matches("---")) {
-        def prev = inFrontMatter
-        inFrontMatter = firstLine
-        if (inFrontMatter != prev)
-          return false
-      }
-      if (inFrontMatter) {
-        def m = null
-        if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
-          map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
-        } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
-          map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
-        } else if (m = line =~ /^channel:\s*(\S+)/) {
-          map["channel"] = m[0][1]
-        } else if (m = line =~ /^version:\s*(\S+)/) {
-          map["version"] = m[0][1]
-        } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
-          map[ m[0][1] ] = m[0][2]
-        }
-        if (dateOnly && map["date"] != null) {
-          return false
-        }
-      } else {
-        if (dateOnly)
-          return false
-        content += line+"\n"
-      }
-      firstLine = false
-    }
-  }
-  return dateOnly ? map["date"] : [map, content]
-}
-
-task hugoTemplates {
-  group "website"
-  description "Create partially populated md pages for hugo website build"
-
-  def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
-  def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
-  def templateFiles = fileTree(dir: hugoTemplatesDir)
-  def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
-  def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
-  def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
-  def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")
-
-  doFirst {
-    // specific release template for version archive
-    def changes = ""
-    def whatsnew = null
-    def givenDate = null
-    def givenChannel = null
-    def givenVersion = null
-    if (CHANNEL == "RELEASE") {
-      def (map, content) = mdFileComponents(releaseMdFile)
-      givenDate = map.date
-      givenChannel = map.channel
-      givenVersion = map.version
-      changes = content
-      if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
-        throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
-      }
-
-      if (whatsnewMdFile.exists())
-        whatsnew = whatsnewMdFile.text
-    }
-
-    def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
-    def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []
-
-    def changesHugo = null
-    if (changes != null) {
-      changesHugo = '<div class="release_notes">\n\n'
-      def inSection = false
-      changes.eachLine { line ->
-        def m = null
-        if (m = line =~ /^##([^#].*)$/) {
-          if (inSection) {
-            changesHugo += "</div>\n\n"
-          }
-          def section = m[0][1].trim()
-          section = section.toLowerCase()
-          section = section.replaceAll(/ +/, "_")
-          section = section.replaceAll(/[^a-z0-9_\-]/, "")
-          changesHugo += "<div class=\"${section}\">\n\n"
-          inSection = true
-        } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
-          def comment = m[0][2].trim()
-          if (comment != "") {
-            comment = comment.replaceAll('"', "&quot;")
-            def issuekeys = []
-            comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
-            def newline = m[0][1]
-            if (comment.trim() != "")
-              newline += "{{<comment>}}${comment}{{</comment>}}  "
-            newline += m[0][3].trim()
-            if (issuekeys.size() > 0)
-              newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
-            if (m[0][4] != null)
-              newline += m[0][4]
-            line = newline
-          }
-        }
-        changesHugo += line+"\n"
-      }
-      if (inSection) {
-        changesHugo += "\n</div>\n\n"
-      }
-      changesHugo += '</div>'
-    }
-
-    templateFiles.each{ templateFile ->
-      def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
-      def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
-      def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )
-
-      def outPathName = string("${hugoBuildDir}/$newRelPathName")
-
-      copy {
-        from templateFile
-        rename(templateFile.getName(), newFileName)
-        into outPathName
-      }
-
-      def newFile = file("${outPathName}/${newFileName}".toString())
-      def content = newFile.text
-      newFile.text = hugoTemplateSubstitutions(content,
-        [
-          WHATSNEW: whatsnew,
-          CHANGES: changesHugo,
-          DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
-          DRAFT: givenDate == null ? "true" : "false",
-          JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
-          JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
-        ]
-      )
-    }
-
-  }
-
-  inputs.file(oldJvlFile)
-  inputs.dir(hugoTemplatesDir)
-  inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
-  inputs.property("CHANNEL", { CHANNEL })
+      throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
+    }
+    writeDataJsonFile(file(output), file(sha256), file(json))
+  }
 }
 
-def getMdDate(File mdFile) {
-  return mdFileComponents(mdFile, true)
+task installers {
+  dependsOn installerFiles
 }
 
-def getMdSections(String content) {
-  def sections = [:]
-  def sectionContent = ""
-  def sectionName = null
-  content.eachLine { line ->
-    def m = null
-    if (m = line =~ /^##([^#].*)$/) {
-      if (sectionName != null) {
-        sections[sectionName] = sectionContent
-        sectionName = null
-        sectionContent = ""
-      }
-      sectionName = m[0][1].trim()
-      sectionName = sectionName.toLowerCase()
-      sectionName = sectionName.replaceAll(/ +/, "_")
-      sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
-    } else if (sectionName != null) {
-      sectionContent += line+"\n"
-    }
-  }
-  if (sectionContent != null) {
-    sections[sectionName] = sectionContent
+
+spotless {
+  java {
+    eclipse().configFile(eclipse_codestyle_file)
   }
-  return sections
 }
 
-task releasesTemplates {
-  def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
-  def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
-  def releasesHtmlFile = file("${helpSourceDir}/${releases_html}")
-  def whatsnewHtmlFile = file("${helpSourceDir}/${whatsnew_html}")
-  def releasesMdDir = "${jalviewDir}/${releases_dir}"
-  def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
+task createSourceReleaseProperties(type: WriteProperties) {
+  group = "distribution"
+  description = "Create the source RELEASE properties file"
+  
+  def sourceTarBuildDir = "${buildDir}/sourceTar"
+  def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
+  outputFile (sourceReleasePropertiesFile)
 
   doFirst {
-    def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
-    def releaseFilesDates = releaseFiles.collectEntries {
-      [(it): getMdDate(it)]
-    }
-    releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
+    releaseProps.each{ key, val -> property key, val }
+    property "git.branch", gitBranch
+    property "git.hash", gitHash
+  }
 
-    def releasesTemplate = releasesTemplateFile.text
-    def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
-    def versionTemplate = m[0][1]
+  outputs.file(outputFile)
+}
 
-    MutableDataSet options = new MutableDataSet()
+task sourceDist(type: Tar) {
+  group "distribution"
+  description "Create a source .tar.gz file for distribution"
 
-    def extensions = new ArrayList<>()
-    options.set(Parser.EXTENSIONS, extensions)
-    options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)
+  dependsOn createBuildProperties
+  dependsOn convertMdFiles
+  dependsOn eclipseAllPreferences
+  dependsOn createSourceReleaseProperties
 
-    Parser parser = Parser.builder(options).build()
-    HtmlRenderer renderer = HtmlRenderer.builder(options).build()
 
-    def actualVersions = releaseFiles.collect { rf ->
-      def (rfMap, rfContent) = mdFileComponents(rf)
-      return rfMap.version
-    }
-    def versionsHtml = ""
-    def linkedVersions = []
-    releaseFiles.reverse().each { rFile ->
-      def (rMap, rContent) = mdFileComponents(rFile)
+  def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
+  archiveFileName = outputFileName
+  
+  compression Compression.GZIP
+  
+  into project.name
 
-      def versionLink = ""
-      def partialVersion = ""
-      def firstPart = true
-      rMap.version.split("\\.").each { part ->
-        def displayPart = ( firstPart ? "" : "." ) + part
-        partialVersion += displayPart
-        if (
-            linkedVersions.contains(partialVersion)
-            || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
-            ) {
-          versionLink += displayPart
-        } else {
-          versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
-          linkedVersions += partialVersion
-        }
-        firstPart = false
-      }
-      def displayDate = releaseFilesDates[rFile].format("d MMMMM yyyy")
+  def EXCLUDE_FILES=[
+    "build/*",
+    "bin/*",
+    "test-output/",
+    "test-reports",
+    "tests",
+    "clover*/*",
+    ".*",
+    "benchmarking/*",
+    "**/.*",
+    "*.class",
+    "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
+    "*locales/**",
+    "utils/InstallAnywhere",
+    "**/*.log",
+    "RELEASE",
+  ] 
+  def PROCESS_FILES=[
+    "AUTHORS",
+    "CITATION",
+    "FEATURETODO",
+    "JAVA-11-README",
+    "FEATURETODO",
+    "LICENSE",
+    "**/README",
+    "THIRDPARTYLIBS",
+    "TESTNG",
+    "build.gradle",
+    "gradle.properties",
+    "**/*.java",
+    "**/*.html",
+    "**/*.xml",
+    "**/*.gradle",
+    "**/*.groovy",
+    "**/*.properties",
+    "**/*.perl",
+    "**/*.sh",
+  ]
+  def INCLUDE_FILES=[
+    ".classpath",
+    ".settings/org.eclipse.buildship.core.prefs",
+    ".settings/org.eclipse.jdt.core.prefs"
+  ]
 
-      def rContentSections = getMdSections(rContent)
-      def rVersion = versionTemplate
-      if (rVersion != "") {
-        Node newFeaturesNode = parser.parse(rContentSections["new_features"])
-        String newFeaturesHtml = renderer.render(newFeaturesNode)
-        Node issuesResolvedNode = parser.parse(rContentSections["issues_resolved"])
-        String issuesResolvedHtml = renderer.render(issuesResolvedNode)
-        rVersion = hugoTemplateSubstitutions(rVersion,
-          [
-            VERSION: rMap.version,
-            VERSION_LINK: versionLink,
-            DISPLAY_DATE: displayDate,
-            NEW_FEATURES: newFeaturesHtml,
-            ISSUES_RESOLVED: issuesResolvedHtml
-          ]
-        )
-        versionsHtml += rVersion
-      }
-    }
+  from(jalviewDir) {
+    exclude (EXCLUDE_FILES)
+    include (PROCESS_FILES)
+    filter(ReplaceTokens,
+      beginToken: '$$',
+      endToken: '$$',
+      tokens: [
+        'Version-Rel': JALVIEW_VERSION,
+        'Year-Rel': getDate("yyyy")
+      ]
+    )
+  }
+  from(jalviewDir) {
+    exclude (EXCLUDE_FILES)
+    exclude (PROCESS_FILES)
+    exclude ("appletlib")
+    exclude ("**/*locales")
+    exclude ("*locales/**")
+    exclude ("utils/InstallAnywhere")
 
-    releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
-    releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
-    releasesHtmlFile.text = releasesTemplate
+    exclude (getdown_files_dir)
+    // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
+    //exclude (getdown_website_dir)
+    //exclude (getdown_archive_dir)
+
+    // exluding these as not using jars as modules yet
+    exclude ("${j11modDir}/**/*.jar")
+  }
+  from(jalviewDir) {
+    include(INCLUDE_FILES)
   }
+//  from (jalviewDir) {
+//    // explicit includes for stuff that seemed to not get included
+//    include(fileTree("test/**/*."))
+//    exclude(EXCLUDE_FILES)
+//    exclude(PROCESS_FILES)
+//  }
 
-  inputs.file(releasesTemplateFile)
-  inputs.file(whatsnewTemplateFile)
-  inputs.dir(releasesMdDir)
-  inputs.dir(whatsnewMdDir)
-  outputs.file(releasesHtmlFile)
-  outputs.file(whatsnewHtmlFile)
+  from(file(buildProperties).getParent()) {
+    include(file(buildProperties).getName())
+    rename(file(buildProperties).getName(), "build_properties")
+    filter({ line ->
+      line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
+    })
+  }
+
+  def sourceTarBuildDir = "${buildDir}/sourceTar"
+  from(sourceTarBuildDir) {
+    // this includes the appended RELEASE properties file
+  }
 }
 
+task dataInstallersJson {
+  group "website"
+  description "Create the installers-VERSION.json data file for installer files created"
+
+  mustRunAfter installers
+  mustRunAfter shadowJar
+  mustRunAfter sourceDist
+  mustRunAfter getdownArchive
+
+  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
+  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
+
+  if (installersOutputTxt.exists()) {
+    inputs.file(installersOutputTxt)
+  }
+  if (install4jCheckSums && installersSha256.exists()) {
+    inputs.file(installersSha256)
+  }
+  [
+    shadowJar.archiveFile, // executable JAR
+    getdownVersionLaunchJvl, // version JVL
+    sourceDist.archiveFile // source TGZ
+  ].each { fileName ->
+    if (file(fileName).exists()) {
+      inputs.file(fileName)
+    }
+  }
+
+  outputs.file(hugoDataJsonFile)
+
+  doFirst {
+    writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
+  }
+}
 
 task helppages {
+  group "help"
+  description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
+
   dependsOn copyHelp
   dependsOn pubhtmlhelp
   
@@ -3771,3 +4153,4 @@ task jalviewjs {
   description "Build the site"
   dependsOn jalviewjsBuildSite
 }
+