Merge branch 'task/JAL-3991_check_actual_JAVA_VERSION_against_build_properties_in_app...
authorJim Procter <j.procter@dundee.ac.uk>
Mon, 8 Aug 2022 17:01:19 +0000 (18:01 +0100)
committerJim Procter <j.procter@dundee.ac.uk>
Mon, 8 Aug 2022 17:01:19 +0000 (18:01 +0100)
1  2 
build.gradle
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/gui/Desktop.java

diff --combined build.gradle
@@@ -12,7 -12,6 +12,7 @@@ import java.security.MessageDiges
  import groovy.transform.ExternalizeMethods
  import groovy.util.XmlParser
  import groovy.xml.XmlUtil
 +import groovy.json.JsonBuilder
  import com.vladsch.flexmark.util.ast.Node
  import com.vladsch.flexmark.html.HtmlRenderer
  import com.vladsch.flexmark.parser.Parser
@@@ -23,11 -22,6 +23,11 @@@ import com.vladsch.flexmark.ext.gfm.str
  import com.vladsch.flexmark.ext.autolink.AutolinkExtension
  import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
  import com.vladsch.flexmark.ext.toc.TocExtension
 +import com.google.common.hash.HashCode
 +import com.google.common.hash.Hashing
 +import com.google.common.io.Files
 +import org.jsoup.Jsoup
 +import org.jsoup.nodes.Element
  
  buildscript {
    repositories {
@@@ -36,7 -30,6 +36,7 @@@
    }
    dependencies {
      classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
 +    classpath "org.jsoup:jsoup:1.14.3"
    }
  }
  
@@@ -48,7 -41,7 +48,7 @@@ plugins 
    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.dorongold.task-tree' version '2.1.0' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
    id 'com.palantir.git-version' version '0.13.0' apply false
  }
  
@@@ -103,7 -96,6 +103,7 @@@ def overrideProperties(String propsFile
  ext {
    jalviewDirAbsolutePath = file(jalviewDir).getAbsolutePath()
    jalviewDirRelativePath = jalviewDir
 +  date = new Date()
  
    getdownChannelName = CHANNEL.toLowerCase()
    // default to "default". Currently only has different cosmetics for "develop", "release", "default"
    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")
    testSourceDir = useClover ? cloverTestInstrDir : testDir
    testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
  
 -  getdownWebsiteDir = string("${jalviewDir}/${getdown_website_dir}/${JAVA_VERSION}")
 +  getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
 +  getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
 +  getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
 +  getdownFullArchiveDir = null
 +  getdownTextLines = []
 +  getdownLaunchJvl = null
 +  getdownVersionLaunchJvl = null
    buildDist = true
    buildProperties = null
  
    // the following values might be overridden by the CHANNEL switch
    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
 +  getdownArchiveAppBase = getdown_archive_base
    getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
    getdownAppDistDir = getdown_app_dir_alt
    getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
    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}"
 +  install4jCheckSums = true
 +
 +  applicationName = "${jalview_name}"
    switch (CHANNEL) {
  
      case "BUILD":
  
      case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
      getdownAppDistDir = getdown_app_dir_release
 +    getdownSetAppBaseProperty = true
      reportRsyncCommand = true
      install4jSuffix = ""
      install4jInstallerName = "${jalview_name} Installer"
      case "ARCHIVELOCAL":
      getdownChannelName = string("archive/${JALVIEW_VERSION}")
      getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
 -    getdownAppBase = file(getdownWebsiteDir).toURI().toString()
 +    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
      if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
        throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
      } else {
  
      case "TEST-RELEASE":
      reportRsyncCommand = true
 +    getdownSetAppBaseProperty = true
      // Don't ignore transpile errors for release build
      if (jalviewjs_ignore_transpile_errors.equals("true")) {
        jalviewjs_ignore_transpile_errors = "false"
  
      case [ "LOCAL", "JALVIEWJS" ]:
      JALVIEW_VERSION = "TEST"
 -    getdownAppBase = file(getdownWebsiteDir).toURI().toString()
 +    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
 +    getdownArchiveAppBase = file("${jalviewDir}/${getdown_archive_dir}").toURI().toString()
      getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
      install4jExtraScheme = "jalviewl"
 +    install4jCheckSums = false
      break
  
      default: // something wrong specified
      break
  
    }
 +  JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
 +  hugoDataJsonFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_data_installers_dir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
 +  hugoArchiveMdFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_version_archive_dir}/Version-${JALVIEW_VERSION_UNDERSCORES}/_index.md")
    // override getdownAppBase if requested
    if (findProperty("getdown_appbase_override") != null) {
      // revert to LOCAL if empty string
      if (string(getdown_appbase_override) == "") {
 -      getdownAppBase = file(getdownWebsiteDir).toURI().toString()
 +      getdownAppBase = file(getdownAppBaseDir).toURI().toString()
        getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
      } else if (string(getdown_appbase_override).startsWith("file://")) {
        getdownAppBase = string(getdown_appbase_override)
    jvlChannelName = jvlChannelName.replaceAll("[^\\w\\-]+", "_")
    // install4j application and folder names
    if (install4jSuffix == "") {
      install4jBundleId = "${install4j_bundle_id}"
      install4jWinApplicationId = install4j_release_win_application_id
    } else {
 -    install4jApplicationName = "${jalview_name} ${install4jSuffix}"
 +    applicationName = "${jalview_name} ${install4jSuffix}"
      install4jBundleId = "${install4j_bundle_id}-" + install4jSuffix.toLowerCase()
      // add int hash of install4jSuffix to the last part of the application_id
      def id = install4j_release_win_application_id
    }
    // sanitise folder and id names
    // install4jApplicationFolder = e.g. "Jalview Build"
 -  install4jApplicationFolder = install4jApplicationName
 +  install4jApplicationFolder = applicationName
                                      .replaceAll("[\"'~:/\\\\\\s]", "_") // replace all awkward filename chars " ' ~ : / \
                                      .replaceAll("_+", "_") // collapse __
 -  install4jInternalId = install4jApplicationName
 +  install4jInternalId = applicationName
                                      .replaceAll(" ","_")
                                      .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
                                      .replaceAll("_+", "") // collapse __
                                      //.replaceAll("_*-_*", "-") // collapse _-_
 -  install4jUnixApplicationFolder = install4jApplicationName
 +  install4jUnixApplicationFolder = applicationName
                                      .replaceAll(" ","_")
                                      .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
                                      .replaceAll("_+", "_") // collapse __
                                      .toLowerCase()
  
    getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local"
 -  getdownAppDir = string("${getdownWebsiteDir}/${getdownAppDistDir}")
 -  //getdownJ11libDir = "${getdownWebsiteDir}/${getdown_j11lib_dir}"
 -  getdownResourceDir = string("${getdownWebsiteDir}/${getdown_resource_dir}")
 -  getdownInstallDir = string("${getdownWebsiteDir}/${getdown_install_dir}")
 +  getdownAppDir = string("${getdownAppBaseDir}/${getdownAppDistDir}")
 +  //getdownJ11libDir = "${getdownAppBaseDir}/${getdown_j11lib_dir}"
 +  getdownResourceDir = string("${getdownAppBaseDir}/${getdown_resource_dir}")
 +  getdownInstallDir = string("${getdownAppBaseDir}/${getdown_install_dir}")
    getdownFilesDir = string("${jalviewDir}/${getdown_files_dir}/${JAVA_VERSION}/")
    getdownFilesInstallDir = string("${getdownFilesDir}/${getdown_install_dir}")
    /* compile without modules -- using classpath libraries
@@@ -1066,6 -1040,7 +1066,6 @@@ cleanTest 
  
  // format is a string like date.format("dd MMMM yyyy")
  def getDate(format) {
 -  def date = new Date()
    return date.format(format)
  }
  
@@@ -1200,214 -1175,6 +1200,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}"
  }
  
  
 +task releasesTemplates {
 +  group "help"
 +  description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
 +
 +  dependsOn copyHelp
 +
 +  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}"
 +
 +  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"
@@@ -1640,6 -1270,7 +1640,7 @@@ task createBuildProperties(type: WriteP
    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
@@@ -1677,7 -1308,6 +1678,7 @@@ task prepare 
    dependsOn buildResources
    dependsOn copyDocs
    dependsOn copyHelp
 +  dependsOn releasesTemplates
    dependsOn convertMdFiles
    dependsOn buildIndices
  }
@@@ -1750,7 -1380,7 +1751,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
@@@ -1786,7 -1416,7 +1787,7 @@@ jar 
    manifest {
      attributes "Main-Class": main_class,
      "Permissions": "all-permissions",
 -    "Application-Name": install4jApplicationName,
 +    "Application-Name": applicationName,
      "Codebase": application_codebase,
      "Implementation-Version": JALVIEW_VERSION
    }
@@@ -1859,7 -1489,7 +1860,7 @@@ shadowJar 
    }
    manifest {
      attributes "Implementation-Version": JALVIEW_VERSION,
 -    "Application-Name": install4jApplicationName
 +    "Application-Name": applicationName
    }
  
    duplicatesStrategy "INCLUDE"
@@@ -1879,12 -1509,13 +1880,12 @@@ task getdownWebsite() 
    }
  
    def getdownWebsiteResourceFilenames = []
 -  def getdownTextString = ""
    def getdownResourceDir = getdownResourceDir
    def getdownResourceFilenames = []
  
    doFirst {
      // clean the getdown website and files dir before creating getdown folders
 -    delete getdownWebsiteDir
 +    delete getdownAppBaseDir
      delete getdownFilesDir
  
      copy {
  
      copy {
        from channelPropsFile
 -      into getdownWebsiteDir
 +      into getdownAppBaseDir
      }
      getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
  
 -    // set some getdown_txt_ properties then go through all properties looking for getdown_txt_...
 +    // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
      def props = project.properties.sort { it.key }
      if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
        props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
      }
  
      props.put("getdown_txt_title", jalview_name)
 -    props.put("getdown_txt_ui.name", install4jApplicationName)
 +    props.put("getdown_txt_ui.name", applicationName)
  
      // start with appbase
 -    getdownTextString += "appbase = ${getdownAppBase}\n"
 +    getdownTextLines += "appbase = ${getdownAppBase}"
      props.each{ prop, val ->
        if (prop.startsWith("getdown_txt_") && val != null) {
          if (prop.startsWith("getdown_txt_multi_")) {
            def key = prop.substring(18)
            val.split(",").each{ v ->
 -            def line = "${key} = ${v}\n"
 -            getdownTextString += line
 +            def line = "${key} = ${v}"
 +            getdownTextLines += line
            }
          } else {
            // file values rationalised
              }
            }
            if (! prop.startsWith("getdown_txt_resource")) {
 -            def line = prop.substring(12) + " = ${val}\n"
 -            getdownTextString += line
 +            def line = prop.substring(12) + " = ${val}"
 +            getdownTextLines += line
            }
          }
        }
      }
  
      getdownWebsiteResourceFilenames.each{ filename ->
 -      getdownTextString += "resource = ${filename}\n"
 +      getdownTextLines += "resource = ${filename}"
      }
      getdownResourceFilenames.each{ filename ->
        copy {
        if (s.exists()) {
          copy {
            from s
 -          into "${getdownWebsiteDir}/${getdown_wrapper_script_dir}"
 +          into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
          }
 -        getdownTextString += "resource = ${getdown_wrapper_script_dir}/${script}\n"
 +        getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
        }
      }
  
      // put jalview.jar first for CLASSPATH and .properties files reasons
      codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
        def name = f.getName()
 -      def line = "code = ${getdownAppDistDir}/${name}\n"
 -      getdownTextString += line
 +      def line = "code = ${getdownAppDistDir}/${name}"
 +      getdownTextLines += line
        copy {
          from f.getPath()
          into getdownAppDir
      def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
      j11libFiles.sort().each{f ->
      def name = f.getName()
 -    def line = "code = ${getdown_j11lib_dir}/${name}\n"
 -    getdownTextString += line
 +    def line = "code = ${getdown_j11lib_dir}/${name}"
 +    getdownTextLines += line
      copy {
      from f.getPath()
      into getdownJ11libDir
       */
  
      // getdown-launcher.jar should not be in main application class path so the main application can move it when updated.  Listed as a resource so it gets updated.
 -    //getdownTextString += "class = " + file(getdownLauncher).getName() + "\n"
 -    getdownTextString += "resource = ${getdown_launcher_new}\n"
 -    getdownTextString += "class = ${main_class}\n"
 +    //getdownTextLines += "class = " + file(getdownLauncher).getName()
 +    getdownTextLines += "resource = ${getdown_launcher_new}"
 +    getdownTextLines += "class = ${main_class}"
      // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
      if (getdownSetAppBaseProperty) {
 -      getdownTextString += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}\n"
 -      getdownTextString += "jvmarg = -Dgetdownappbase=${getdownAppBase}\n"
 +      getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
 +      getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
      }
  
 -    def getdown_txt = file("${getdownWebsiteDir}/getdown.txt")
 -    getdown_txt.write(getdownTextString)
 +    def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
 +    getdownTxt.write(getdownTextLines.join("\n"))
  
 -    def getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
 -    def launchJvl = file("${getdownWebsiteDir}/${getdownLaunchJvl}")
 +    getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
 +    def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
      launchJvl.write("appbase=${getdownAppBase}")
  
      // files going into the getdown website dir: getdown-launcher.jar
      copy {
        from getdownLauncher
        rename(file(getdownLauncher).getName(), getdown_launcher_new)
 -      into getdownWebsiteDir
 +      into getdownAppBaseDir
      }
  
      // files going into the getdown website dir: getdown-launcher(-local).jar
        if (file(getdownLauncher).getName() != getdown_launcher) {
          rename(file(getdownLauncher).getName(), getdown_launcher)
        }
 -      into getdownWebsiteDir
 +      into getdownAppBaseDir
      }
  
      // files going into the getdown website dir: ./install dir and files
      if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
        copy {
 -        from getdown_txt
 +        from getdownTxt
          from getdownLauncher
          from "${getdownAppDir}/${getdown_build_properties}"
          if (file(getdownLauncher).getName() != getdown_launcher) {
  
      // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
      copy {
 -      from getdown_txt
 +      from getdownTxt
        from launchJvl
        from getdownLauncher
 -      from "${getdownWebsiteDir}/${getdown_build_properties}"
 -      from "${getdownWebsiteDir}/${channel_props}"
 +      from "${getdownAppBaseDir}/${getdown_build_properties}"
 +      from "${getdownAppBaseDir}/${channel_props}"
        if (file(getdownLauncher).getName() != getdown_launcher) {
          rename(file(getdownLauncher).getName(), getdown_launcher)
        }
        into getdownFilesDir
      }
  
 -    // and ./resources (not all downloaded by getdown)
 +    // and ./resource (not all downloaded by getdown)
      copy {
        from getdownResourceDir
        into "${getdownFilesDir}/${getdown_resource_dir}"
    if (buildDist) {
      inputs.dir("${jalviewDir}/${package_dir}")
    }
 -  outputs.dir(getdownWebsiteDir)
 +  outputs.dir(getdownAppBaseDir)
    outputs.dir(getdownFilesDir)
  }
  
@@@ -2123,9 -1754,9 +2124,9 @@@ task getdownDigest(type: JavaExec) 
      classpath = files(getdownLauncher)
    }
    main = "com.threerings.getdown.tools.Digester"
 -  args getdownWebsiteDir
 -  inputs.dir(getdownWebsiteDir)
 -  outputs.file("${getdownWebsiteDir}/digest2.txt")
 +  args getdownAppBaseDir
 +  inputs.dir(getdownAppBaseDir)
 +  outputs.file("${getdownAppBaseDir}/digest2.txt")
  }
  
  
@@@ -2135,7 -1766,7 +2136,7 @@@ task getdown() 
    dependsOn getdownDigest
    doLast {
      if (reportRsyncCommand) {
 -      def fromDir = getdownWebsiteDir + (getdownWebsiteDir.endsWith('/')?'':'/')
 +      def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
        def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
        println "LIKELY RSYNC COMMAND:"
        println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
  }
  
  
 +task getdownArchiveBuild() {
 +  group = "distribution"
 +  description = "Put files in the archive dir to go on the website"
 +
 +  dependsOn getdownWebsite
 +
 +  def v = "v${JALVIEW_VERSION_UNDERSCORES}"
 +  def vDir = "${getdownArchiveDir}/${v}"
 +  getdownFullArchiveDir = "${vDir}/getdown"
 +  getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
 +
 +  def vAltDir = "alt_${v}"
 +  def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
 +
 +  doFirst {
 +    // cleanup old "old" dir
 +    delete getdownArchiveDir
 +
 +    def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
 +    getdownArchiveTxt.getParentFile().mkdirs()
 +    def getdownArchiveTextLines = []
 +    def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
 +
 +    // the libdir
 +    copy {
 +      from "${getdownAppBaseDir}/${getdownAppDistDir}"
 +      into "${getdownFullArchiveDir}/${vAltDir}"
 +    }
 +
 +    getdownTextLines.each { line ->
 +      line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
 +      line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
 +      line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
 +      line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
 +      line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
 +      line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
 +      // remove the existing resource = resource/ or bin/ lines
 +      if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
 +        getdownArchiveTextLines += line
 +      }
 +    }
 +
 +    // the resource dir -- add these files as resource lines in getdown.txt
 +    copy {
 +      from "${archiveImagesDir}"
 +      into "${getdownFullArchiveDir}/${getdown_resource_dir}"
 +      eachFile { file ->
 +        getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
 +      }
 +    }
 +
 +    getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
 +
 +    def vLaunchJvl = file(getdownVersionLaunchJvl)
 +    vLaunchJvl.getParentFile().mkdirs()
 +    vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
 +    def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
 +    def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
 +    // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
 +    //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
 +    java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
 +
 +    // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
 +    copy {
 +      from getdownLauncher
 +      from "${getdownAppBaseDir}/${getdownLaunchJvl}"
 +      from "${getdownAppBaseDir}/${getdown_launcher_new}"
 +      from "${getdownAppBaseDir}/${channel_props}"
 +      if (file(getdownLauncher).getName() != getdown_launcher) {
 +        rename(file(getdownLauncher).getName(), getdown_launcher)
 +      }
 +      into getdownFullArchiveDir
 +    }
 +
 +  }
 +}
 +
 +task getdownArchiveDigest(type: JavaExec) {
 +  group = "distribution"
 +  description = "Digest the getdown archive folder"
 +
 +  dependsOn getdownArchiveBuild
 +
 +  doFirst {
 +    classpath = files(getdownLauncher)
 +    args getdownFullArchiveDir
 +  }
 +  main = "com.threerings.getdown.tools.Digester"
 +  inputs.dir(getdownFullArchiveDir)
 +  outputs.file("${getdownFullArchiveDir}/digest2.txt")
 +}
 +
 +task getdownArchive() {
 +  group = "distribution"
 +  description = "Build the website archive dir with getdown digest"
 +
 +  dependsOn getdownArchiveBuild
 +  dependsOn getdownArchiveDigest
 +}
 +
  tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
  }
  
  clean {
    doFirst {
 -    delete getdownWebsiteDir
 +    delete getdownAppBaseDir
      delete getdownFilesDir
 +    delete getdownArchiveDir
    }
  }
  
@@@ -2309,7 -1839,11 +2310,7 @@@ task copyInstall4jTemplate 
  
      // turn off checksum creation for LOCAL channel
      def e = install4jConfigXml.application[0]
 -    if (CHANNEL == "LOCAL") {
 -      e.'@createChecksums' = "false"
 -    } else {
 -      e.'@createChecksums' = "true"
 -    }
 +    e.'@createChecksums' = string(install4jCheckSums)
  
      // put file association actions where placeholder action is
      def install4jFileAssociationsText = install4jFileAssociationsFile.text
@@@ -2362,23 -1896,12 +2363,23 @@@ clean 
    }
  }
  
 +task cleanInstallersDataFiles {
 +  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
 +  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
 +  def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
 +  doFirst {
 +    delete installersOutputTxt
 +    delete installersSha256
 +    delete hugoDataJsonFile
 +  }
 +}
  
 -task installers(type: com.install4j.gradle.Install4jTask) {
 +task installerFiles(type: com.install4j.gradle.Install4jTask) {
    group = "distribution"
    description = "Create the install4j installers"
    dependsOn getdown
    dependsOn copyInstall4jTemplate
 +  dependsOn cleanInstallersDataFiles
  
    projectFile = install4jConfFile
  
      filesMd5 = filesMd5.substring(0,8)
    }
    def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"
  
    variables = [
      'JALVIEW_NAME': jalview_name,
 -    'JALVIEW_APPLICATION_NAME': install4jApplicationName,
 +    'JALVIEW_APPLICATION_NAME': applicationName,
      'JALVIEW_DIR': "../..",
      'OSX_KEYSTORE': OSX_KEYSTORE,
      'OSX_APPLEID': OSX_APPLEID,
      'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
      'INSTALLER_NAME': install4jInstallerName,
      'INSTALL4J_UTILS_DIR': install4j_utils_dir,
 -    'GETDOWN_WEBSITE_DIR': getdown_website_dir,
 +    'GETDOWN_CHANNEL_DIR': getdownChannelDir,
      'GETDOWN_FILES_DIR': getdown_files_dir,
      'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
      'GETDOWN_DIST_DIR': getdownAppDistDir,
    }
    //verbose=true
  
 -  inputs.dir(getdownWebsiteDir)
 +  inputs.dir(getdownAppBaseDir)
    inputs.file(install4jConfFile)
    inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
    inputs.dir(macosJavaVMDir)
    outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
  }
  
 +def getDataHash(File myFile) {
 +  HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
 +  return myFile.exists()
 +  ? [
 +      "file" : myFile.getName(),
 +      "filesize" : myFile.length(),
 +      "sha256" : hash.toString()
 +    ]
 +  : null
 +}
 +
 +def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
 +  def hash = [
 +    "channel" : getdownChannelName,
 +    "date" : getDate("yyyy-MM-dd HH:mm:ss"),
 +    "git-commit" : "${gitHash} [${gitBranch}]",
 +    "version" : JALVIEW_VERSION
 +  ]
 +  // install4j installer files
 +  if (installersOutputTxt.exists()) {
 +    def idHash = [:]
 +    installersOutputTxt.readLines().each { def line ->
 +      if (line.startsWith("#")) {
 +        return;
 +      }
 +      line.replaceAll("\n","")
 +      def vals = line.split("\t")
 +      def filename = vals[3]
 +      def filesize = file(filename).length()
 +      filename = filename.replaceAll(/^.*\//, "")
 +      hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
 +      idHash."${filename}" = vals[0]
 +    }
 +    if (install4jCheckSums && installersSha256.exists()) {
 +      installersSha256.readLines().each { def line ->
 +        if (line.startsWith("#")) {
 +          return;
 +        }
 +        line.replaceAll("\n","")
 +        def vals = line.split(/\s+\*?/)
 +        def filename = vals[1]
 +        def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
 +      }
 +    }
 +  }
 +
 +  [
 +    "JAR": shadowJar.archiveFile, // executable JAR
 +    "JVL": getdownVersionLaunchJvl, // version JVL
 +    "SOURCE": sourceDist.archiveFile // source TGZ
 +  ].each { key, value ->
 +    def file = file(value)
 +    if (file.exists()) {
 +      def fileHash = getDataHash(file)
 +      if (fileHash != null) {
 +        hash."${key}" = fileHash;
 +      }
 +    }
 +  }
 +  return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
 +}
 +
 +task staticMakeInstallersJsonFile {
 +  doFirst {
 +    def output = findProperty("i4j_output")
 +    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 {
@@@ -2597,7 -2044,8 +2598,7 @@@ task sourceDist(type: Tar) 
    dependsOn createSourceReleaseProperties
  
  
 -  def VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
 -  def outputFileName = "${project.name}_${VERSION_UNDERSCORES}.tar.gz"
 +  def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
    archiveFileName = outputFileName
    
    compression Compression.GZIP
      exclude ("utils/InstallAnywhere")
  
      exclude (getdown_files_dir)
 -    exclude (getdown_website_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")
    }
  }
  
 +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
    
@@@ -514,11 -514,11 +514,11 @@@ label.retrieve_parse_sequence_database_
  label.standard_databases = Standard Databases
  label.fetch_embl_uniprot = Fetch from EMBL/EMBLCDS or Uniprot/PDB and any selected DAS sources
  label.fetch_uniprot_references = Fetch Uniprot references
 -label.search_3dbeacons = 3D-Beacons Search
 +label.search_3dbeacons = Search 3D-Beacons
  label.find_models_from_3dbeacons = Search 3D-Beacons for 3D structures and models
  label.3dbeacons = 3D-Beacons
  label.fetch_references_for = Fetch database references for {0} sequences ?
 -label.fetch_references_for_3dbeacons = 3D Beacons needs Uniprot References. Fetch database references for {0} sequences ?
 +label.fetch_references_for_3dbeacons = 3D Beacons needs to fetch Uniprot References for {0} sequences.  Do you want to continue ?
  label.reset_min_max_colours_to_defaults = Reset min and max colours to defaults from user preferences.
  label.align_structures_using_linked_alignment_views = Superpose structures using {0} selected alignment view(s)
  label.threshold_feature_display_by_score = Threshold the feature display by score.
@@@ -778,10 -778,8 +778,10 @@@ label.transformed_points_for_params = T
  label.variable_color_for = Variable Feature Colour for {0}
  label.select_background_colour = Select Background Colour
  label.invalid_font = Invalid Font
 +label.search_db_all = Search all of {0}
 +label.search_db_index = Search {0} index {1}
  label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
 -label.separate_multiple_query_values = Enter one or more {0}s separated by a semi-colon ";"
 +label.separate_multiple_query_values = Enter one or more {0} separated by a semi-colon ";"
  label.search_all = Enter one or more search values separated by a semi-colon ";" (Note: This searches the entire database)
  label.replace_commas_semicolons = Replace commas with semi-colons
  label.parsing_failed_syntax_errors_shown_below_param = Parsing failed. Syntax errors shown below {0}
@@@ -1334,6 -1332,7 +1334,7 @@@ label.backupfiles_confirm_save_file_bac
  label.backupfiles_confirm_save_new_saved_file_ok = The new saved file seems okay.
  label.backupfiles_confirm_save_new_saved_file_not_ok = The new saved file might not be okay.
  label.continue_operation = Continue operation?
+ label.continue = Continue
  label.backups = Backups
  label.backup = Backup
  label.backup_files = Backup Files
@@@ -1414,3 -1413,5 +1415,5 @@@ label.maximum_memory_tooltip = Enter me
  label.adjustments_for_this_computer = Adjustments for this computer
  label.memory_example_text = Maximum memory that would be used with these settings on this computer
  label.memory_example_tooltip = The memory allocated to Jalview is the smaller of the percentage of physical memory (default 90%) and the maximum absolute memory (default 32GB). If your computer's memory cannot be ascertained then the maximum absolute memory defaults to 8GB (if not customised).<br>Jalview will always try and reserve 512MB for the OS and at least 512MB for itself.
+ warning.wrong_jvm_version_title = Wrong Java Version
+ warning.wrong_jvm_version_message = The Java version being used (Java {0}) may lead to problems.\nThis installation of Jalview should be used with Java {1}.
@@@ -700,8 -700,6 +700,8 @@@ label.transformed_points_for_params = P
  label.variable_color_for = Color variable para la característica de {0}
  label.select_background_colour = Seleccionar color de fondo
  label.invalid_font = Fuente no válida
 +label.search_db_all = Buscar en todo {0}
 +label.search_db_index = Buscar índice {0} {1}
  label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";"
  label.replace_commas_semicolons = Cambiar comas por puntos y comas
  label.parsing_failed_syntax_errors_shown_below_param = Parseo erróneo. A continuación, se muestras los errores de sintaxis {0}
@@@ -1141,7 -1139,7 +1141,7 @@@ label.threshold_filter=Filtro de Umbra
  label.add_reference_annotations=Añadir anotaciones de referencia
  label.hide_insertions=Ocultar Inserciones
  info.change_threshold_mode_to_enable=Cambiar Modo de Umbral para Habilitar
 -label.separate_multiple_query_values=Introducir uno o mas {0}s separados por punto y coma ";"
 +label.separate_multiple_query_values=Introducir uno o mas {0} separados por punto y coma ";"
  label.fetch_chimera_attributes = Buscar atributos desde Chimera
  label.fetch_chimera_attributes_tip = Copiar atributo de Chimera a característica de Jalview
  label.view_rna_structure=Estructura 2D VARNA
@@@ -1324,6 -1322,7 +1324,7 @@@ label.backupfiles_confirm_save_file_bac
  label.backupfiles_confirm_save_new_saved_file_ok = El nuevo archivo guardado parece estar bien.
  label.backupfiles_confirm_save_new_saved_file_not_ok = El nuevo archivo guardado podría no estar bien.
  label.continue_operation = ¿Continuar operación?
+ label.continue = Continua
  label.backups = Respaldos
  label.backup = Respaldo
  label.backup_files = Archivos de respaldos
@@@ -1404,3 -1403,6 +1405,6 @@@ label.maximum_memory_tooltip = Ingrese 
  label.adjustments_for_this_computer = Ajustes para esta computadora
  label.memory_example_text = Memoria máxima que se usaría con esta configuración en esta computadora
  label.memory_example_tooltip = La memoria asignada a Jalview es el menor entre el porcentaje de memoria física (predeterminado 90%) y la memoria absoluta máxima (predeterminado 32 GB). Si no se puede determinar la memoria de su computadora, la memoria absoluta máxima predeterminada es de 8 GB (si no está personalizada).<br>Jalview siempre intentará reservar 512 MB para el sistema operativo y al menos 512 MB para sí mismo.
+ warning.wrong_jvm_version_title = Versión incorrecta de Java
+ warning.wrong_jvm_version_message = La versión de Java que se está utilizando (Java {0}) puede generar problemas.\nEsta instalación de Jalview debe usarse con Java {1}.
@@@ -42,6 -42,7 +42,6 @@@ import java.util.Locale
  import java.util.Properties;
  import java.util.StringTokenizer;
  import java.util.TreeSet;
 -import java.util.regex.Pattern;
  
  import javax.swing.LookAndFeel;
  import javax.swing.UIManager;
@@@ -518,29 -519,25 +518,29 @@@ public class Cach
                      + orgtimeout + " seconds.");
            }
            String remoteVersion = null;
 -          try
 +          if (remoteBuildPropertiesUrl.startsWith("http"))
            {
 -            System.setProperty("sun.net.client.defaultConnectTimeout",
 -                    "5000");
 -            java.net.URL url = new java.net.URL(remoteBuildPropertiesUrl);
 -
 -            BufferedReader in = new BufferedReader(
 -                    new InputStreamReader(url.openStream()));
 -
 -            Properties remoteBuildProperties = new Properties();
 -            remoteBuildProperties.load(in);
 -            remoteVersion = remoteBuildProperties.getProperty("VERSION");
 -          } catch (Exception ex)
 -          {
 -            System.out
 -                    .println("Non-fatal exception when checking version at "
 -                            + remoteBuildPropertiesUrl + ":");
 -            System.out.println(ex);
 -            remoteVersion = getProperty("VERSION");
 +            try
 +            {
 +              System.setProperty("sun.net.client.defaultConnectTimeout",
 +                      "5000");
 +
 +              URL url = new URL(remoteBuildPropertiesUrl);
 +
 +              BufferedReader in = new BufferedReader(
 +                      new InputStreamReader(url.openStream()));
 +
 +              Properties remoteBuildProperties = new Properties();
 +              remoteBuildProperties.load(in);
 +              remoteVersion = remoteBuildProperties.getProperty("VERSION");
 +            } catch (Exception ex)
 +            {
 +              System.out.println(
 +                      "Non-fatal exception when checking version at "
 +                              + remoteBuildPropertiesUrl + ":");
 +              System.out.println(ex);
 +              remoteVersion = getProperty("VERSION");
 +            }
            }
            System.setProperty("sun.net.client.defaultConnectTimeout",
                    orgtimeout);
          applicationProperties.put("VERSION",
                  buildProperties.getProperty("VERSION"));
        }
+       if (buildProperties.getProperty("JAVA_COMPILE_VERSION", null) != null)
+       {
+         applicationProperties.put("JAVA_COMPILE_VERSION",
+                 buildProperties.getProperty("JAVA_COMPILE_VERSION"));
+       }
      } catch (Exception ex)
      {
        System.out.println("Error reading build details: " + ex);
      {
        return;
      }
 -    String line = prefix + (value != null ? value : defaultValue) + suffix;
 -    sb.append(line);
 +    if (prefix != null)
 +      sb.append(prefix);
 +    sb.append(value == null ? defaultValue : value);
 +    if (suffix != null)
 +      sb.append(suffix);
    }
  
    /**
      sb.append(" (");
      sb.append(lafClass);
      sb.append(")\n");
 -    // Not displayed in release version ( determined by possible version number
 -    // regex 9[9.]*9[.-_a9]* )
 -    if (Pattern.matches("^\\d[\\d\\.]*\\d[\\.\\-\\w]*$",
 -            Cache.getDefault("VERSION", "TEST")))
 +    if (Console.isDebugEnabled()
 +            || !"release".equals(ChannelProperties.getProperty("channel")))
      {
 +      appendIfNotNull(sb, "Channel: ",
 +              ChannelProperties.getProperty("channel"), "\n", null);
        appendIfNotNull(sb, "Getdown appdir: ",
 -              System.getProperty("getdownappdir"), "\n", null);
 +              System.getProperty("getdowninstanceappdir"), "\n", null);
        appendIfNotNull(sb, "Getdown appbase: ",
 -              System.getProperty("getdownappbase"), "\n", null);
 +              System.getProperty("getdowninstanceappbase"), "\n", null);
        appendIfNotNull(sb, "Java home: ", System.getProperty("java.home"),
                "\n", "unknown");
      }
@@@ -20,7 -20,6 +20,7 @@@
   */
  package jalview.bin;
  
 +import java.awt.Color;
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
@@@ -45,12 -44,10 +45,13 @@@ import java.util.logging.ConsoleHandler
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
+ import javax.swing.JOptionPane;
 +import javax.swing.SwingUtilities;
  import javax.swing.UIManager;
  import javax.swing.UIManager.LookAndFeelInfo;
  
 +import com.formdev.flatlaf.FlatLightLaf;
 +import com.formdev.flatlaf.util.SystemInfo;
  import com.threerings.getdown.util.LaunchUtil;
  
  //import edu.stanford.ejalbert.launching.IBrowserLaunching;
@@@ -76,6 -73,7 +77,7 @@@ import jalview.schemes.ColourSchemeI
  import jalview.schemes.ColourSchemeProperty;
  import jalview.util.ChannelProperties;
  import jalview.util.HttpUtils;
+ import jalview.util.LaunchUtils;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.ws.jws2.Jws2Discoverer;
@@@ -433,6 -431,35 +435,35 @@@ public class Jalvie
         * @j2sIgnore
         */
        {
+         /**
+          * Check to see that the JVM version being run is suitable for the Java
+          * version this Jalview was compiled for. Popup a warning if not.
+          */
+         if (!LaunchUtils.checkJavaVersion())
+         {
+           Console.warn("The Java version being used (Java "
+                   + LaunchUtils.getJavaVersion()
+                   + ") may lead to problems. This installation of Jalview should be used with Java "
+                   + LaunchUtils.getJavaCompileVersion() + ".");
+           if (!LaunchUtils
+                   .getBooleanUserPreference("IGNORE_JVM_WARNING_POPUP"))
+           {
+             Object[] options = {
+                 MessageManager.getString("label.continue") };
+             JOptionPane.showOptionDialog(null,
+                     MessageManager.formatMessage(
+                             "warning.wrong_jvm_version_message",
+                             LaunchUtils.getJavaVersion(),
+                             LaunchUtils.getJavaCompileVersion()),
+                     MessageManager
+                             .getString("warning.wrong_jvm_version_title"),
+                     JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
+                     null, options, options[0]);
+           }
+         }
          if (!aparser.contains("nowebservicediscovery"))
          {
            desktop.startServiceDiscovery();
        }
      }
  
+     // Check if JVM and compile version might cause problems and log if it
+     // might.
+     if (headless && !Platform.isJS() && !LaunchUtils.checkJavaVersion())
+     {
+       Console.warn("The Java version being used (Java "
+               + LaunchUtils.getJavaVersion()
+               + ") may lead to problems. This installation of Jalview should be used with Java "
+               + LaunchUtils.getJavaCompileVersion() + ".");
+     }
      // Move any new getdown-launcher-new.jar into place over old
      // getdown-launcher.jar
      String appdirString = System.getProperty("getdownappdir");
  
    private static void setLookAndFeel()
    {
 -    // property laf = "crossplatform", "system", "gtk", "metal", "nimbus" or
 -    // "mac"
 +    // property laf = "crossplatform", "system", "gtk", "metal", "nimbus",
 +    // "mac" or "flat"
      // If not set (or chosen laf fails), use the normal SystemLaF and if on Mac,
      // try Quaqua/Vaqua.
      String lafProp = System.getProperty("laf");
          Console.error("Could not set requested laf=" + laf);
        }
        break;
 +    case "flat":
 +      lafSet = setFlatLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
      case "quaqua":
        lafSet = setQuaquaLookAndFeel();
        if (!lafSet)
              "javax.swing.plaf.nimbus.NimbusLookAndFeel", false);
    }
  
 +  private static boolean setFlatLookAndFeel()
 +  {
 +    boolean set = setSpecificLookAndFeel("flatlaf light",
 +            "com.formdev.flatlaf.FlatLightLaf", false);
 +    if (set)
 +    {
 +      if (Platform.isMac())
 +      {
 +        System.setProperty("apple.laf.useScreenMenuBar", "true");
 +        System.setProperty("apple.awt.application.name",
 +                ChannelProperties.getProperty("app_name"));
 +        System.setProperty("apple.awt.application.appearance", "system");
 +        if (SystemInfo.isMacFullWindowContentSupported
 +                && Desktop.desktop != null)
 +        {
 +          Desktop.desktop.getRootPane()
 +                  .putClientProperty("apple.awt.fullWindowContent", true);
 +          Desktop.desktop.getRootPane()
 +                  .putClientProperty("apple.awt.transparentTitleBar", true);
 +        }
 +
 +        SwingUtilities.invokeLater(() -> {
 +          FlatLightLaf.setup();
 +        });
 +      }
 +
 +      UIManager.put("TabbedPane.showTabSeparators", true);
 +      UIManager.put("TabbedPane.tabSeparatorsFullHeight", true);
 +      UIManager.put("TabbedPane.tabsOverlapBorder", true);
 +      // UIManager.put("TabbedPane.hasFullBorder", true);
 +      UIManager.put("TabbedPane.tabLayoutPolicy", "scroll");
 +      UIManager.put("TabbedPane.scrollButtonsPolicy", "asNeeded");
 +      UIManager.put("TabbedPane.smoothScrolling", true);
 +      UIManager.put("TabbedPane.tabWidthMode", "compact");
 +      UIManager.put("TabbedPane.selectedBackground", Color.white);
 +    }
 +    return set;
 +  }
 +
    private static boolean setQuaquaLookAndFeel()
    {
      return setSpecificLookAndFeel("quaqua",
@@@ -121,6 -121,7 +121,7 @@@ import jalview.urls.IdOrgSettings
  import jalview.util.BrowserLauncher;
  import jalview.util.ChannelProperties;
  import jalview.util.ImageMaker.TYPE;
+ import jalview.util.LaunchUtils;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.util.ShortcutKeyMaskExWrapper;
@@@ -182,7 -183,7 +183,7 @@@ public class Desktop extends jalview.jb
  
    private static final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
  
 -  protected static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
 +  public static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
  
    public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
  
       */
      if (Platform.isLinux())
      {
+       if (LaunchUtils.getJavaVersion() >= 11)
+       {
+         jalview.bin.Console.info(
+                 "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that.");
+       }
        try
        {
          Toolkit xToolkit = Toolkit.getDefaultToolkit();
        }
      }
  
 -    /**
 -     * APQHandlers sets handlers for About, Preferences and Quit actions
 -     * peculiar to macOS's application menu. APQHandlers will check to see if a
 -     * handler is supported before setting it.
 -     */
 -    try
 -    {
 -      APQHandlers.setAPQHandlers(this);
 -    } catch (Exception e)
 -    {
 -      System.out.println("Cannot set APQHandlers");
 -      // e.printStackTrace();
 -    } catch (Throwable t)
 -    {
 -      jalview.bin.Console
 -              .warn("Error setting APQHandlers: " + t.toString());
 -      jalview.bin.Console.trace(Cache.getStackTraceString(t));
 -    }
      setIconImages(ChannelProperties.getIconList());
  
      addWindowListener(new WindowAdapter()