JAL-4217 move CommandsTest to testTask3 involving structure image export and random...
[jalview.git] / build.gradle
1 /* Convention for properties.  Read from gradle.properties, use lower_case_underlines for property names.
2  * For properties set within build.gradle, use camelCaseNoSpace.
3  */
4 import org.apache.tools.ant.filters.ReplaceTokens
5 import org.gradle.internal.os.OperatingSystem
6 import org.gradle.plugins.ide.internal.generator.PropertiesPersistableConfigurationObject
7 import org.gradle.api.internal.PropertiesTransformer
8 import org.gradle.util.ConfigureUtil
9 import org.gradle.plugins.ide.eclipse.model.Output
10 import org.gradle.plugins.ide.eclipse.model.Library
11 import java.security.MessageDigest
12 import java.util.regex.Matcher
13 import java.util.concurrent.Executors
14 import java.util.concurrent.Future
15 import java.util.concurrent.ScheduledExecutorService
16 import java.util.concurrent.TimeUnit
17 import groovy.transform.ExternalizeMethods
18 import groovy.util.XmlParser
19 import groovy.xml.XmlUtil
20 import groovy.json.JsonBuilder
21 import com.vladsch.flexmark.util.ast.Node
22 import com.vladsch.flexmark.html.HtmlRenderer
23 import com.vladsch.flexmark.parser.Parser
24 import com.vladsch.flexmark.util.data.MutableDataSet
25 import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
26 import com.vladsch.flexmark.ext.tables.TablesExtension
27 import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
28 import com.vladsch.flexmark.ext.autolink.AutolinkExtension
29 import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
30 import com.vladsch.flexmark.ext.toc.TocExtension
31 import com.google.common.hash.HashCode
32 import com.google.common.hash.Hashing
33 import com.google.common.io.Files
34 import org.jsoup.Jsoup
35 import org.jsoup.nodes.Element
36
37 buildscript {
38   repositories {
39     mavenCentral()
40     mavenLocal()
41   }
42   dependencies {
43     classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
44     classpath "org.jsoup:jsoup:1.14.3"
45     classpath "com.eowise:gradle-imagemagick:0.5.1"
46   }
47 }
48
49
50 plugins {
51   id 'java'
52   id 'application'
53   id 'eclipse'
54   id "com.diffplug.gradle.spotless" version "3.28.0"
55   id 'com.github.johnrengelman.shadow' version '4.0.3'
56   id 'com.install4j.gradle' version '10.0.3'
57   id 'com.dorongold.task-tree' version '2.1.1' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
58   id 'com.palantir.git-version' version '0.13.0' apply false
59 }
60
61 repositories {
62   jcenter()
63   mavenCentral()
64   mavenLocal()
65 }
66
67
68
69 // in ext the values are cast to Object. Ensure string values are cast as String (and not GStringImpl) for later use
70 def string(Object o) {
71   return o == null ? "" : o.toString()
72 }
73
74 def overrideProperties(String propsFileName, boolean output = false) {
75   if (propsFileName == null) {
76     return
77   }
78   def propsFile = file(propsFileName)
79   if (propsFile != null && propsFile.exists()) {
80     println("Using properties from file '${propsFileName}'")
81     try {
82       def p = new Properties()
83       def localPropsFIS = new FileInputStream(propsFile)
84       p.load(localPropsFIS)
85       localPropsFIS.close()
86       p.each {
87         key, val -> 
88           def oldval
89           if (project.hasProperty(key)) {
90             oldval = project.findProperty(key)
91             project.setProperty(key, val)
92             if (output) {
93               println("Overriding property '${key}' ('${oldval}') with ${file(propsFile).getName()} value '${val}'")
94             }
95           } else {
96             ext.setProperty(key, val)
97             if (output) {
98               println("Setting ext property '${key}' with ${file(propsFile).getName()}s value '${val}'")
99             }
100           }
101       }
102     } catch (Exception e) {
103       println("Exception reading local.properties")
104       e.printStackTrace()
105     }
106   }
107 }
108
109 ext {
110   jalviewDirAbsolutePath = file(jalviewDir).getAbsolutePath()
111   jalviewDirRelativePath = jalviewDir
112   date = new Date()
113
114   getdownChannelName = CHANNEL.toLowerCase()
115   // default to "default". Currently only has different cosmetics for "develop", "release", "default"
116   propertiesChannelName = ["develop", "release", "test-release", "jalviewjs", "jalviewjs-release" ].contains(getdownChannelName) ? getdownChannelName : "default"
117   channelDirName = propertiesChannelName
118   // Import channel_properties
119   if (getdownChannelName.startsWith("develop-")) {
120     channelDirName = "develop-SUFFIX"
121   }
122   channelDir = string("${jalviewDir}/${channel_properties_dir}/${channelDirName}")
123   channelGradleProperties = string("${channelDir}/channel_gradle.properties")
124   channelPropsFile = string("${channelDir}/${resource_dir}/${channel_props}")
125   overrideProperties(channelGradleProperties, false)
126   // local build environment properties
127   // can be "projectDir/local.properties"
128   overrideProperties("${projectDir}/local.properties", true)
129   // or "../projectDir_local.properties"
130   overrideProperties(projectDir.getParent() + "/" + projectDir.getName() + "_local.properties", true)
131
132   ////  
133   // Import releaseProps from the RELEASE file
134   // or a file specified via JALVIEW_RELEASE_FILE if defined
135   // Expect jalview.version and target release branch in jalview.release        
136   releaseProps = new Properties();
137   def releasePropFile = findProperty("JALVIEW_RELEASE_FILE");
138   def defaultReleasePropFile = "${jalviewDirAbsolutePath}/RELEASE";
139   try {
140     (new File(releasePropFile!=null ? releasePropFile : defaultReleasePropFile)).withInputStream { 
141      releaseProps.load(it)
142     }
143   } catch (Exception fileLoadError) {
144     throw new Error("Couldn't load release properties file "+(releasePropFile==null ? defaultReleasePropFile : "from custom location: releasePropFile"),fileLoadError);
145   }
146   ////
147   // Set JALVIEW_VERSION if it is not already set
148   if (findProperty("JALVIEW_VERSION")==null || "".equals(JALVIEW_VERSION)) {
149     JALVIEW_VERSION = releaseProps.get("jalview.version")
150   }
151   println("JALVIEW_VERSION is set to '${JALVIEW_VERSION}'")
152   
153   // this property set when running Eclipse headlessly
154   j2sHeadlessBuildProperty = string("net.sf.j2s.core.headlessbuild")
155   // this property set by Eclipse
156   eclipseApplicationProperty = string("eclipse.application")
157   // CHECK IF RUNNING FROM WITHIN ECLIPSE
158   def eclipseApplicationPropertyVal = System.properties[eclipseApplicationProperty]
159   IN_ECLIPSE = eclipseApplicationPropertyVal != null && eclipseApplicationPropertyVal.startsWith("org.eclipse.ui.")
160   // BUT WITHOUT THE HEADLESS BUILD PROPERTY SET
161   if (System.properties[j2sHeadlessBuildProperty].equals("true")) {
162     println("Setting IN_ECLIPSE to ${IN_ECLIPSE} as System.properties['${j2sHeadlessBuildProperty}'] == '${System.properties[j2sHeadlessBuildProperty]}'")
163     IN_ECLIPSE = false
164   }
165   if (IN_ECLIPSE) {
166     println("WITHIN ECLIPSE IDE")
167   } else {
168     println("HEADLESS BUILD")
169   }
170   
171   J2S_ENABLED = (project.hasProperty('j2s.compiler.status') && project['j2s.compiler.status'] != null && project['j2s.compiler.status'] == "enable")
172   if (J2S_ENABLED) {
173     println("J2S ENABLED")
174   } 
175   /* *-/
176   System.properties.sort { it.key }.each {
177     key, val -> println("SYSTEM PROPERTY ${key}='${val}'")
178   }
179   /-* *-/
180   if (false && IN_ECLIPSE) {
181     jalviewDir = jalviewDirAbsolutePath
182   }
183   */
184
185   // datestamp
186   buildDate = new Date().format("yyyyMMdd")
187
188   // essentials
189   bareSourceDir = string(source_dir)
190   sourceDir = string("${jalviewDir}/${bareSourceDir}")
191   resourceDir = string("${jalviewDir}/${resource_dir}")
192   bareTestSourceDir = string(test_source_dir)
193   testDir = string("${jalviewDir}/${bareTestSourceDir}")
194
195   classesDir = string("${jalviewDir}/${classes_dir}")
196
197   // clover
198   useClover = clover.equals("true")
199   cloverBuildDir = "${buildDir}/clover"
200   cloverInstrDir = file("${cloverBuildDir}/clover-instr")
201   cloverClassesDir = file("${cloverBuildDir}/clover-classes")
202   cloverReportDir = file("${buildDir}/reports/clover")
203   cloverTestInstrDir = file("${cloverBuildDir}/clover-test-instr")
204   cloverTestClassesDir = file("${cloverBuildDir}/clover-test-classes")
205   //cloverTestClassesDir = cloverClassesDir
206   cloverDb = string("${cloverBuildDir}/clover.db")
207
208   testSourceDir = useClover ? cloverTestInstrDir : testDir
209   testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
210
211   channelSuffix = ""
212   backgroundImageText = BACKGROUNDIMAGETEXT
213   getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
214   getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
215   getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
216   getdownFullArchiveDir = null
217   getdownTextLines = []
218   getdownLaunchJvl = null
219   getdownVersionLaunchJvl = null
220   buildDist = true
221   buildProperties = null
222
223   // the following values might be overridden by the CHANNEL switch
224   getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
225   getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
226   getdownArchiveAppBase = getdown_archive_base
227   getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
228   getdownAppDistDir = getdown_app_dir_alt
229   getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
230   getdownImagesBuildDir = string("${buildDir}/imagemagick/getdown")
231   getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
232   reportRsyncCommand = false
233   jvlChannelName = CHANNEL.toLowerCase()
234   install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build
235   install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
236   install4jDMGBackgroundImageDir = "${install4j_images_dir}"
237   install4jDMGBackgroundImageBuildDir = "build/imagemagick/install4j"
238   install4jDMGBackgroundImageFile = "${install4j_dmg_background}"
239   install4jInstallerName = "${jalview_name} Non-Release Installer"
240   install4jExecutableName = install4j_executable_name
241   install4jExtraScheme = "jalviewx"
242   install4jMacIconsFile = string("${install4j_images_dir}/${install4j_mac_icons_file}")
243   install4jWindowsIconsFile = string("${install4j_images_dir}/${install4j_windows_icons_file}")
244   install4jPngIconFile = string("${install4j_images_dir}/${install4j_png_icon_file}")
245   install4jBackground = string("${install4j_images_dir}/${install4j_background}")
246   install4jBuildDir = "${install4j_build_dir}/${JAVA_VERSION}"
247   install4jCheckSums = true
248
249   applicationName = "${jalview_name}"
250   switch (CHANNEL) {
251
252     case "BUILD":
253     // TODO: get bamboo build artifact URL for getdown artifacts
254     getdown_channel_base = bamboo_channelbase
255     getdownChannelName = string("${bamboo_planKey}/${JAVA_VERSION}")
256     getdownAppBase = string("${bamboo_channelbase}/${bamboo_planKey}${bamboo_getdown_channel_suffix}/${JAVA_VERSION}")
257     jvlChannelName += "_${getdownChannelName}"
258     // automatically add the test group Not-bamboo for exclusion 
259     if ("".equals(testng_excluded_groups)) { 
260       testng_excluded_groups = "Not-bamboo"
261     }
262     install4jExtraScheme = "jalviewb"
263     backgroundImageText = true
264     break
265
266     case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
267     getdownAppDistDir = getdown_app_dir_release
268     getdownSetAppBaseProperty = true
269     reportRsyncCommand = true
270     install4jSuffix = ""
271     install4jInstallerName = "${jalview_name} Installer"
272     break
273
274     case "ARCHIVE":
275     getdownChannelName = CHANNEL.toLowerCase()+"/${JALVIEW_VERSION}"
276     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
277     getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
278     if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
279       throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
280     } else {
281       package_dir = string("${ARCHIVEDIR}/${package_dir}")
282       buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
283       buildDist = false
284     }
285     reportRsyncCommand = true
286     install4jExtraScheme = "jalviewa"
287     break
288
289     case "ARCHIVELOCAL":
290     getdownChannelName = string("archive/${JALVIEW_VERSION}")
291     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
292     getdownAppBase = file(getdownAppBaseDir).toURI().toString()
293     if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
294       throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution [did not find '${ARCHIVEDIR}/${package_dir}']")
295     } else {
296       package_dir = string("${ARCHIVEDIR}/${package_dir}")
297       buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
298       buildDist = false
299     }
300     reportRsyncCommand = true
301     getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
302     install4jSuffix = "Archive"
303     install4jExtraScheme = "jalviewa"
304     break
305
306     case ~/^DEVELOP-([\.\-\w]*)$/:
307     def suffix = Matcher.lastMatcher[0][1]
308     reportRsyncCommand = true
309     getdownSetAppBaseProperty = true
310     JALVIEW_VERSION=JALVIEW_VERSION+"-d${suffix}-${buildDate}"
311     install4jSuffix = "Develop ${suffix}"
312     install4jExtraScheme = "jalviewd"
313     install4jInstallerName = "${jalview_name} Develop ${suffix} Installer"
314     getdownChannelName = string("develop-${suffix}")
315     getdownChannelDir = string("${getdown_website_dir}/${getdownChannelName}")
316     getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
317     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
318     getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
319     channelSuffix = string(suffix)
320     backgroundImageText = true
321     break
322
323     case "DEVELOP":
324     reportRsyncCommand = true
325     getdownSetAppBaseProperty = true
326     // DEVELOP-RELEASE is usually associated with a Jalview release series so set the version
327     JALVIEW_VERSION=JALVIEW_VERSION+"-d${buildDate}"
328     
329     install4jSuffix = "Develop"
330     install4jExtraScheme = "jalviewd"
331     install4jInstallerName = "${jalview_name} Develop Installer"
332     backgroundImageText = true
333     break
334
335     case "TEST-RELEASE":
336     reportRsyncCommand = true
337     getdownSetAppBaseProperty = true
338     // Don't ignore transpile errors for release build
339     if (jalviewjs_ignore_transpile_errors.equals("true")) {
340       jalviewjs_ignore_transpile_errors = "false"
341       println("Setting jalviewjs_ignore_transpile_errors to 'false'")
342     }
343     JALVIEW_VERSION = JALVIEW_VERSION+"-test"
344     install4jSuffix = "Test"
345     install4jExtraScheme = "jalviewt"
346     install4jInstallerName = "${jalview_name} Test Installer"
347     backgroundImageText = true
348     break
349
350     case ~/^SCRATCH(|-[-\w]*)$/:
351     getdownChannelName = CHANNEL
352     JALVIEW_VERSION = JALVIEW_VERSION+"-"+CHANNEL
353     
354     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
355     getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
356     reportRsyncCommand = true
357     install4jSuffix = "Scratch"
358     break
359
360     case "TEST-LOCAL":
361     if (!file("${LOCALDIR}").exists()) {
362       throw new GradleException("Must provide a LOCALDIR value to produce a local distribution")
363     } else {
364       getdownAppBase = file(file("${LOCALDIR}").getAbsolutePath()).toURI().toString()
365       getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
366     }
367     JALVIEW_VERSION = "TEST"
368     install4jSuffix = "Test-Local"
369     install4jExtraScheme = "jalviewt"
370     install4jInstallerName = "${jalview_name} Test Installer"
371     backgroundImageText = true
372     break
373
374     case [ "LOCAL", "JALVIEWJS" ]:
375     JALVIEW_VERSION = "TEST"
376     getdownAppBase = file(getdownAppBaseDir).toURI().toString()
377     getdownArchiveAppBase = file("${jalviewDir}/${getdown_archive_dir}").toURI().toString()
378     getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
379     install4jExtraScheme = "jalviewl"
380     install4jCheckSums = false
381     break
382
383     default: // something wrong specified
384     throw new GradleException("CHANNEL must be one of BUILD, RELEASE, ARCHIVE, DEVELOP, TEST-RELEASE, SCRATCH-..., LOCAL [default]")
385     break
386
387   }
388   JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
389   hugoDataJsonFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_data_installers_dir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
390   hugoArchiveMdFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_version_archive_dir}/Version-${JALVIEW_VERSION_UNDERSCORES}/_index.md")
391   // override getdownAppBase if requested
392   if (findProperty("getdown_appbase_override") != null) {
393     // revert to LOCAL if empty string
394     if (string(getdown_appbase_override) == "") {
395       getdownAppBase = file(getdownAppBaseDir).toURI().toString()
396       getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
397     } else if (string(getdown_appbase_override).startsWith("file://")) {
398       getdownAppBase = string(getdown_appbase_override)
399       getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
400     } else {
401       getdownAppBase = string(getdown_appbase_override)
402     }
403     println("Overriding getdown appbase with '${getdownAppBase}'")
404   }
405   // sanitise file name for jalview launcher file for this channel
406   jvlChannelName = jvlChannelName.replaceAll("[^\\w\\-]+", "_")
407   // install4j application and folder names
408   if (install4jSuffix == "") {
409     install4jBundleId = "${install4j_bundle_id}"
410     install4jWinApplicationId = install4j_release_win_application_id
411   } else {
412     applicationName = "${jalview_name} ${install4jSuffix}"
413     install4jBundleId = "${install4j_bundle_id}-" + install4jSuffix.toLowerCase()
414     // add int hash of install4jSuffix to the last part of the application_id
415     def id = install4j_release_win_application_id
416     def idsplitreverse = id.split("-").reverse()
417     idsplitreverse[0] = idsplitreverse[0].toInteger() + install4jSuffix.hashCode()
418     install4jWinApplicationId = idsplitreverse.reverse().join("-")
419   }
420   // sanitise folder and id names
421   // install4jApplicationFolder = e.g. "Jalview Build"
422   install4jApplicationFolder = applicationName
423                                     .replaceAll("[\"'~:/\\\\\\s]", "_") // replace all awkward filename chars " ' ~ : / \
424                                     .replaceAll("_+", "_") // collapse __
425   install4jInternalId = applicationName
426                                     .replaceAll(" ","_")
427                                     .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
428                                     .replaceAll("_+", "") // collapse __
429                                     //.replaceAll("_*-_*", "-") // collapse _-_
430   install4jUnixApplicationFolder = applicationName
431                                     .replaceAll(" ","_")
432                                     .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
433                                     .replaceAll("_+", "_") // collapse __
434                                     .replaceAll("_*-_*", "-") // collapse _-_
435                                     .toLowerCase()
436
437   getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local"
438   getdownAppDir = string("${getdownAppBaseDir}/${getdownAppDistDir}")
439   //getdownJ11libDir = "${getdownAppBaseDir}/${getdown_j11lib_dir}"
440   getdownResourceDir = string("${getdownAppBaseDir}/${getdown_resource_dir}")
441   getdownInstallDir = string("${getdownAppBaseDir}/${getdown_install_dir}")
442   getdownFilesDir = string("${jalviewDir}/${getdown_files_dir}/${JAVA_VERSION}/")
443   getdownFilesInstallDir = string("${getdownFilesDir}/${getdown_install_dir}")
444   /* compile without modules -- using classpath libraries
445   modules_compileClasspath = fileTree(dir: "${jalviewDir}/${j11modDir}", include: ["*.jar"])
446   modules_runtimeClasspath = modules_compileClasspath
447   */
448
449   gitHash = "SOURCE"
450   gitBranch = "Source"
451   try {
452     apply plugin: "com.palantir.git-version"
453     def details = versionDetails()
454     gitHash = details.gitHash
455     gitBranch = details.branchName
456   } catch(org.gradle.api.internal.plugins.PluginApplicationException e) {
457     println("Not in a git repository. Using git values from RELEASE properties file.")
458     gitHash = releaseProps.getProperty("git.hash")
459     gitBranch = releaseProps.getProperty("git.branch")
460   } catch(java.lang.RuntimeException e1) {
461     throw new GradleException("Error with git-version plugin.  Directory '.git' exists but versionDetails() cannot be found.")
462   }
463
464   println("Using a ${CHANNEL} profile.")
465
466   additional_compiler_args = []
467   // configure classpath/args for j8/j11 compilation
468   if (JAVA_VERSION.equals("1.8")) {
469     JAVA_INTEGER_VERSION = string("8")
470     //libDir = j8libDir
471     libDir = j11libDir
472     libDistDir = j8libDir
473     compile_source_compatibility = 1.8
474     compile_target_compatibility = 1.8
475     // these are getdown.txt properties defined dependent on the JAVA_VERSION
476     getdownAltJavaMinVersion = string(findProperty("getdown_alt_java8_min_version"))
477     getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java8_max_version"))
478     // this property is assigned below and expanded to multiple lines in the getdown task
479     getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java8_txt_multi_java_location"))
480     // this property is for the Java library used in eclipse
481     eclipseJavaRuntimeName = string("JavaSE-1.8")
482   } else if (JAVA_VERSION.equals("11")) {
483     JAVA_INTEGER_VERSION = string("11")
484     libDir = j11libDir
485     libDistDir = j11libDir
486     compile_source_compatibility = 11
487     compile_target_compatibility = 11
488     getdownAltJavaMinVersion = string(findProperty("getdown_alt_java11_min_version"))
489     getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java11_max_version"))
490     getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java11_txt_multi_java_location"))
491     eclipseJavaRuntimeName = string("JavaSE-11")
492     /* compile without modules -- using classpath libraries
493     additional_compiler_args += [
494     '--module-path', modules_compileClasspath.asPath,
495     '--add-modules', j11modules
496     ]
497      */
498   } else if (JAVA_VERSION.equals("17")) {
499     JAVA_INTEGER_VERSION = string("17")
500     libDir = j17libDir
501     libDistDir = j17libDir
502     compile_source_compatibility = 17
503     compile_target_compatibility = 17
504     getdownAltJavaMinVersion = string(findProperty("getdown_alt_java11_min_version"))
505     getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java11_max_version"))
506     getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java11_txt_multi_java_location"))
507     eclipseJavaRuntimeName = string("JavaSE-17")
508     /* compile without modules -- using classpath libraries
509     additional_compiler_args += [
510     '--module-path', modules_compileClasspath.asPath,
511     '--add-modules', j11modules
512     ]
513      */
514   } else {
515     throw new GradleException("JAVA_VERSION=${JAVA_VERSION} not currently supported by Jalview")
516   }
517
518
519   // for install4j
520   JAVA_MIN_VERSION = JAVA_VERSION
521   JAVA_MAX_VERSION = JAVA_VERSION
522   jreInstallsDir = string(jre_installs_dir)
523   if (jreInstallsDir.startsWith("~/")) {
524     jreInstallsDir = System.getProperty("user.home") + jreInstallsDir.substring(1)
525   }
526   install4jDir = string("${jalviewDir}/${install4j_utils_dir}")
527   install4jConfFileName = string("jalview-install4j-conf.install4j")
528   install4jConfFile = file("${install4jDir}/${install4jConfFileName}")
529   install4jHomeDir = install4j_home_dir
530   if (install4jHomeDir.startsWith("~/")) {
531     install4jHomeDir = System.getProperty("user.home") + install4jHomeDir.substring(1)
532   }
533
534   resourceBuildDir = string("${buildDir}/resources")
535   resourcesBuildDir = string("${resourceBuildDir}/resources_build")
536   helpBuildDir = string("${resourceBuildDir}/help_build")
537   docBuildDir = string("${resourceBuildDir}/doc_build")
538
539   if (buildProperties == null) {
540     buildProperties = string("${resourcesBuildDir}/${build_properties_file}")
541   }
542   buildingHTML = string("${jalviewDir}/${doc_dir}/building.html")
543   helpParentDir = string("${jalviewDir}/${help_parent_dir}")
544   helpSourceDir = string("${helpParentDir}/${help_dir}")
545   helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")
546
547   convertBinary = null
548   convertBinaryExpectedLocation = imagemagick_convert
549   if (convertBinaryExpectedLocation.startsWith("~/")) {
550     convertBinaryExpectedLocation = System.getProperty("user.home") + convertBinaryExpectedLocation.substring(1)
551   }
552   if (file(convertBinaryExpectedLocation).exists()) {
553     convertBinary = convertBinaryExpectedLocation
554   }
555
556   relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
557   jalviewjsBuildDir = string("${relativeBuildDir}/jalviewjs")
558   jalviewjsSiteDir = string("${jalviewjsBuildDir}/${jalviewjs_site_dir}")
559   if (IN_ECLIPSE) {
560     jalviewjsTransferSiteJsDir = string(jalviewjsSiteDir)
561   } else {
562     jalviewjsTransferSiteJsDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_js")
563   }
564   jalviewjsTransferSiteLibDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_lib")
565   jalviewjsTransferSiteSwingJsDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_swingjs")
566   jalviewjsTransferSiteCoreDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_core")
567   jalviewjsJalviewCoreHtmlFile = string("")
568   jalviewjsJalviewCoreName = string(jalviewjs_core_name)
569   jalviewjsCoreClasslists = []
570   jalviewjsJalviewTemplateName = string(jalviewjs_name)
571   jalviewjsJ2sSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_settings}")
572   jalviewjsJ2sAltSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_alt_settings}")
573   jalviewjsJ2sProps = null
574   jalviewjsJ2sPlugin = jalviewjs_j2s_plugin
575   jalviewjsStderrLaunchFilename = "${jalviewjsSiteDir}/"+(file(jalviewjs_stderr_launch).getName())
576
577   eclipseWorkspace = null
578   eclipseBinary = string("")
579   eclipseVersion = string("")
580   eclipseDebug = false
581
582   jalviewjsChromiumUserDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}"
583   jalviewjsChromiumProfileDir = "${ext.jalviewjsChromiumUserDir}/${jalviewjs_chromium_profile_name}"
584
585   // ENDEXT
586 }
587
588
589 sourceSets {
590   main {
591     java {
592       srcDirs sourceDir
593       outputDir = file(classesDir)
594     }
595
596     resources {
597       srcDirs = [ resourcesBuildDir, docBuildDir, helpBuildDir ]
598     }
599
600     compileClasspath = files(sourceSets.main.java.outputDir)
601     compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
602
603     runtimeClasspath = compileClasspath
604     runtimeClasspath += files(sourceSets.main.resources.srcDirs)
605   }
606
607   clover {
608     java {
609       srcDirs cloverInstrDir
610       outputDir = cloverClassesDir
611     }
612
613     resources {
614       srcDirs = sourceSets.main.resources.srcDirs
615     }
616
617     compileClasspath = files( sourceSets.clover.java.outputDir )
618     //compileClasspath += files( testClassesDir )
619     compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
620     compileClasspath += fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
621     compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])
622
623     runtimeClasspath = compileClasspath
624   }
625
626   test {
627     java {
628       srcDirs testSourceDir
629       outputDir = file(testClassesDir)
630     }
631
632     resources {
633       srcDirs = useClover ? sourceSets.clover.resources.srcDirs : sourceSets.main.resources.srcDirs
634     }
635
636     compileClasspath = files( sourceSets.test.java.outputDir )
637     compileClasspath += useClover ? sourceSets.clover.compileClasspath : sourceSets.main.compileClasspath
638     compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])
639
640     runtimeClasspath = compileClasspath
641     runtimeClasspath += files(sourceSets.test.resources.srcDirs)
642   }
643
644 }
645
646
647 // eclipse project and settings files creation, also used by buildship
648 eclipse {
649   project {
650     name = eclipse_project_name
651
652     natures 'org.eclipse.jdt.core.javanature',
653     'org.eclipse.jdt.groovy.core.groovyNature',
654     'org.eclipse.buildship.core.gradleprojectnature'
655
656     buildCommand 'org.eclipse.jdt.core.javabuilder'
657     buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
658   }
659
660   classpath {
661     //defaultOutputDir = sourceSets.main.java.outputDir
662     configurations.each{ c->
663       if (c.isCanBeResolved()) {
664         minusConfigurations += [c]
665       }
666     }
667
668     plusConfigurations = [ ]
669     file {
670
671       whenMerged { cp ->
672         def removeTheseToo = []
673         HashMap<String, Boolean> alreadyAddedSrcPath = new HashMap<>();
674         cp.entries.each { entry ->
675           // This conditional removes all src classpathentries that a) have already been added or b) aren't "src" or "test".
676           // e.g. this removes the resources dir being copied into bin/main, bin/test AND bin/clover
677           // we add the resources and help/help dirs in as libs afterwards (see below)
678           if (entry.kind == 'src') {
679             if (alreadyAddedSrcPath.getAt(entry.path) || !(entry.path == bareSourceDir || entry.path == bareTestSourceDir)) {
680               removeTheseToo += entry
681             } else {
682               alreadyAddedSrcPath.putAt(entry.path, true)
683             }
684           }
685
686         }
687         cp.entries.removeAll(removeTheseToo)
688
689         //cp.entries += new Output("${eclipse_bin_dir}/main")
690         if (file(helpParentDir).isDirectory()) {
691           cp.entries += new Library(fileReference(helpParentDir))
692         }
693         if (file(resourceDir).isDirectory()) {
694           cp.entries += new Library(fileReference(resourceDir))
695         }
696
697         HashMap<String, Boolean> alreadyAddedLibPath = new HashMap<>();
698
699         sourceSets.main.compileClasspath.findAll { it.name.endsWith(".jar") }.any {
700           //don't want to add outputDir as eclipse is using its own output dir in bin/main
701           if (it.isDirectory() || ! it.exists()) {
702             // don't add dirs to classpath, especially if they don't exist
703             return false // groovy "continue" in .any closure
704           }
705           def itPath = it.toString()
706           if (itPath.startsWith("${jalviewDirAbsolutePath}/")) {
707             // make relative path
708             itPath = itPath.substring(jalviewDirAbsolutePath.length()+1)
709           }
710           if (alreadyAddedLibPath.get(itPath)) {
711             //println("Not adding duplicate entry "+itPath)
712           } else {
713             //println("Adding entry "+itPath)
714             cp.entries += new Library(fileReference(itPath))
715             alreadyAddedLibPath.put(itPath, true)
716           }
717         }
718
719         sourceSets.test.compileClasspath.findAll { it.name.endsWith(".jar") }.any {
720           //no longer want to add outputDir as eclipse is using its own output dir in bin/main
721           if (it.isDirectory() || ! it.exists()) {
722             // don't add dirs to classpath
723             return false // groovy "continue" in .any closure
724           }
725
726           def itPath = it.toString()
727           if (itPath.startsWith("${jalviewDirAbsolutePath}/")) {
728             itPath = itPath.substring(jalviewDirAbsolutePath.length()+1)
729           }
730           if (alreadyAddedLibPath.get(itPath)) {
731             // don't duplicate
732           } else {
733             def lib = new Library(fileReference(itPath))
734             lib.entryAttributes["test"] = "true"
735             cp.entries += lib
736             alreadyAddedLibPath.put(itPath, true)
737           }
738         }
739
740       } // whenMerged
741
742     } // file
743
744     containers 'org.eclipse.buildship.core.gradleclasspathcontainer'
745
746   } // classpath
747
748   jdt {
749     // for the IDE, use java 11 compatibility
750     sourceCompatibility = compile_source_compatibility
751     targetCompatibility = compile_target_compatibility
752     javaRuntimeName = eclipseJavaRuntimeName
753
754     // add in jalview project specific properties/preferences into eclipse core preferences
755     file {
756       withProperties { props ->
757         def jalview_prefs = new Properties()
758         def ins = new FileInputStream("${jalviewDirAbsolutePath}/${eclipse_extra_jdt_prefs_file}")
759         jalview_prefs.load(ins)
760         ins.close()
761         jalview_prefs.forEach { t, v ->
762           if (props.getAt(t) == null) {
763             props.putAt(t, v)
764           }
765         }
766         // codestyle file -- overrides previous formatter prefs
767         def csFile = file("${jalviewDirAbsolutePath}/${eclipse_codestyle_file}")
768         if (csFile.exists()) {
769           XmlParser parser = new XmlParser()
770           def profiles = parser.parse(csFile)
771           def profile = profiles.'profile'.find { p -> (p.'@kind' == "CodeFormatterProfile" && p.'@name' == "Jalview") }
772           if (profile != null) {
773             profile.'setting'.each { s ->
774               def id = s.'@id'
775               def value = s.'@value'
776               if (id != null && value != null) {
777                 props.putAt(id, value)
778               }
779             }
780           }
781         }
782       }
783     }
784
785   } // jdt
786
787   if (IN_ECLIPSE) {
788     // Don't want these to be activated if in headless build
789     synchronizationTasks "eclipseSynchronizationTask"
790     //autoBuildTasks "eclipseAutoBuildTask"
791
792   }
793 }
794
795
796 /* hack to change eclipse prefs in .settings files other than org.eclipse.jdt.core.prefs */
797 // Class to allow updating arbitrary properties files
798 class PropertiesFile extends PropertiesPersistableConfigurationObject {
799   public PropertiesFile(PropertiesTransformer t) { super(t); }
800   @Override protected void load(Properties properties) { }
801   @Override protected void store(Properties properties) { }
802   @Override protected String getDefaultResourceName() { return ""; }
803   // This is necessary, because PropertiesPersistableConfigurationObject fails
804   // if no default properties file exists.
805   @Override public void loadDefaults() { load(new StringBufferInputStream("")); }
806 }
807
808 // Task to update arbitrary properties files (set outputFile)
809 class PropertiesFileTask extends PropertiesGeneratorTask<PropertiesFile> {
810   private final PropertiesFileContentMerger file;
811   public PropertiesFileTask() { file = new PropertiesFileContentMerger(getTransformer()); }
812   protected PropertiesFile create() { return new PropertiesFile(getTransformer()); }
813   protected void configure(PropertiesFile props) {
814     file.getBeforeMerged().execute(props); file.getWhenMerged().execute(props);
815   }
816   public void file(Closure closure) { ConfigureUtil.configure(closure, file); }
817 }
818
819 task eclipseUIPreferences(type: PropertiesFileTask) {
820   description = "Generate Eclipse additional settings"
821   def filename = "org.eclipse.jdt.ui.prefs"
822   outputFile = "$projectDir/.settings/${filename}" as File
823   file {
824     withProperties {
825       it.load new FileInputStream("$projectDir/utils/eclipse/${filename}" as String)
826     }
827   }
828 }
829
830 task eclipseGroovyCorePreferences(type: PropertiesFileTask) {
831   description = "Generate Eclipse additional settings"
832   def filename = "org.eclipse.jdt.groovy.core.prefs"
833   outputFile = "$projectDir/.settings/${filename}" as File
834   file {
835     withProperties {
836       it.load new FileInputStream("$projectDir/utils/eclipse/${filename}" as String)
837     }
838   }
839 }
840
841 task eclipseAllPreferences {
842   dependsOn eclipseJdt
843   dependsOn eclipseUIPreferences
844   dependsOn eclipseGroovyCorePreferences
845 }
846
847 eclipseUIPreferences.mustRunAfter eclipseJdt
848 eclipseGroovyCorePreferences.mustRunAfter eclipseJdt
849
850 /* end of eclipse preferences hack */
851
852
853 // clover bits
854
855
856 task cleanClover {
857   doFirst {
858     delete cloverBuildDir
859     delete cloverReportDir
860   }
861 }
862
863
864 task cloverInstrJava(type: JavaExec) {
865   group = "Verification"
866   description = "Create clover instrumented source java files"
867
868   dependsOn cleanClover
869
870   inputs.files(sourceSets.main.allJava)
871   outputs.dir(cloverInstrDir)
872
873   //classpath = fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
874   classpath = sourceSets.clover.compileClasspath
875   main = "com.atlassian.clover.CloverInstr"
876
877   def argsList = [
878     "--encoding",
879     "UTF-8",
880     "--initstring",
881     cloverDb,
882     "--destdir",
883     cloverInstrDir.getPath(),
884   ]
885   def srcFiles = sourceSets.main.allJava.files
886   argsList.addAll(
887     srcFiles.collect(
888       { file -> file.absolutePath }
889     )
890   )
891   args argsList.toArray()
892
893   doFirst {
894     delete cloverInstrDir
895     println("Clover: About to instrument "+srcFiles.size() +" files")
896   }
897 }
898
899
900 task cloverInstrTests(type: JavaExec) {
901   group = "Verification"
902   description = "Create clover instrumented source test files"
903
904   dependsOn cleanClover
905
906   inputs.files(testDir)
907   outputs.dir(cloverTestInstrDir)
908
909   classpath = sourceSets.clover.compileClasspath
910   main = "com.atlassian.clover.CloverInstr"
911
912   def argsList = [
913     "--encoding",
914     "UTF-8",
915     "--initstring",
916     cloverDb,
917     "--srcdir",
918     testDir,
919     "--destdir",
920     cloverTestInstrDir.getPath(),
921   ]
922   args argsList.toArray()
923
924   doFirst {
925     delete cloverTestInstrDir
926     println("Clover: About to instrument test files")
927   }
928 }
929
930
931 task cloverInstr {
932   group = "Verification"
933   description = "Create clover instrumented all source files"
934
935   dependsOn cloverInstrJava
936   dependsOn cloverInstrTests
937 }
938
939
940 cloverClasses.dependsOn cloverInstr
941
942
943 task cloverConsoleReport(type: JavaExec) {
944   group = "Verification"
945   description = "Creates clover console report"
946
947   onlyIf {
948     file(cloverDb).exists()
949   }
950
951   inputs.dir cloverClassesDir
952
953   classpath = sourceSets.clover.runtimeClasspath
954   main = "com.atlassian.clover.reporters.console.ConsoleReporter"
955
956   if (cloverreport_mem.length() > 0) {
957     maxHeapSize = cloverreport_mem
958   }
959   if (cloverreport_jvmargs.length() > 0) {
960     jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
961   }
962
963   def argsList = [
964     "--alwaysreport",
965     "--initstring",
966     cloverDb,
967     "--unittests"
968   ]
969
970   args argsList.toArray()
971 }
972
973
974 task cloverHtmlReport(type: JavaExec) {
975   group = "Verification"
976   description = "Creates clover HTML report"
977
978   onlyIf {
979     file(cloverDb).exists()
980   }
981
982   def cloverHtmlDir = cloverReportDir
983   inputs.dir cloverClassesDir
984   outputs.dir cloverHtmlDir
985
986   classpath = sourceSets.clover.runtimeClasspath
987   main = "com.atlassian.clover.reporters.html.HtmlReporter"
988
989   if (cloverreport_mem.length() > 0) {
990     maxHeapSize = cloverreport_mem
991   }
992   if (cloverreport_jvmargs.length() > 0) {
993     jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
994   }
995
996   def argsList = [
997     "--alwaysreport",
998     "--initstring",
999     cloverDb,
1000     "--outputdir",
1001     cloverHtmlDir
1002   ]
1003
1004   if (cloverreport_html_options.length() > 0) {
1005     argsList += cloverreport_html_options.split(" ")
1006   }
1007
1008   args argsList.toArray()
1009 }
1010
1011
1012 task cloverXmlReport(type: JavaExec) {
1013   group = "Verification"
1014   description = "Creates clover XML report"
1015
1016   onlyIf {
1017     file(cloverDb).exists()
1018   }
1019
1020   def cloverXmlFile = "${cloverReportDir}/clover.xml"
1021   inputs.dir cloverClassesDir
1022   outputs.file cloverXmlFile
1023
1024   classpath = sourceSets.clover.runtimeClasspath
1025   main = "com.atlassian.clover.reporters.xml.XMLReporter"
1026
1027   if (cloverreport_mem.length() > 0) {
1028     maxHeapSize = cloverreport_mem
1029   }
1030   if (cloverreport_jvmargs.length() > 0) {
1031     jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
1032   }
1033
1034   def argsList = [
1035     "--alwaysreport",
1036     "--initstring",
1037     cloverDb,
1038     "--outfile",
1039     cloverXmlFile
1040   ]
1041
1042   if (cloverreport_xml_options.length() > 0) {
1043     argsList += cloverreport_xml_options.split(" ")
1044   }
1045
1046   args argsList.toArray()
1047 }
1048
1049
1050 task cloverReport {
1051   group = "Verification"
1052   description = "Creates clover reports"
1053
1054   dependsOn cloverXmlReport
1055   dependsOn cloverHtmlReport
1056 }
1057
1058
1059 compileCloverJava {
1060
1061   doFirst {
1062     sourceCompatibility = compile_source_compatibility
1063     targetCompatibility = compile_target_compatibility
1064     options.compilerArgs += additional_compiler_args
1065     print ("Setting target compatibility to "+targetCompatibility+"\n")
1066   }
1067   //classpath += configurations.cloverRuntime
1068 }
1069 // end clover bits
1070
1071
1072 compileJava {
1073   // JBP->BS should the print statement in doFirst refer to compile_target_compatibility ?
1074   sourceCompatibility = compile_source_compatibility
1075   targetCompatibility = compile_target_compatibility
1076   options.compilerArgs += additional_compiler_args
1077   options.encoding = "UTF-8"
1078   doFirst {
1079     print ("Setting target compatibility to "+compile_target_compatibility+"\n")
1080   }
1081
1082 }
1083
1084
1085 compileTestJava {
1086   sourceCompatibility = compile_source_compatibility
1087   targetCompatibility = compile_target_compatibility
1088   options.compilerArgs += additional_compiler_args
1089   doFirst {
1090     print ("Setting target compatibility to "+targetCompatibility+"\n")
1091   }
1092 }
1093
1094
1095 clean {
1096   doFirst {
1097     delete sourceSets.main.java.outputDir
1098   }
1099 }
1100
1101
1102 cleanTest {
1103   dependsOn cleanClover
1104   doFirst {
1105     delete sourceSets.test.java.outputDir
1106   }
1107 }
1108
1109
1110 // format is a string like date.format("dd MMMM yyyy")
1111 def getDate(format) {
1112   return date.format(format)
1113 }
1114
1115
1116 def convertMdToHtml (FileTree mdFiles, File cssFile) {
1117   MutableDataSet options = new MutableDataSet()
1118
1119   def extensions = new ArrayList<>()
1120   extensions.add(AnchorLinkExtension.create()) 
1121   extensions.add(AutolinkExtension.create())
1122   extensions.add(StrikethroughExtension.create())
1123   extensions.add(TaskListExtension.create())
1124   extensions.add(TablesExtension.create())
1125   extensions.add(TocExtension.create())
1126   
1127   options.set(Parser.EXTENSIONS, extensions)
1128
1129   // set GFM table parsing options
1130   options.set(TablesExtension.WITH_CAPTION, false)
1131   options.set(TablesExtension.COLUMN_SPANS, false)
1132   options.set(TablesExtension.MIN_HEADER_ROWS, 1)
1133   options.set(TablesExtension.MAX_HEADER_ROWS, 1)
1134   options.set(TablesExtension.APPEND_MISSING_COLUMNS, true)
1135   options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)
1136   options.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
1137   // GFM anchor links
1138   options.set(AnchorLinkExtension.ANCHORLINKS_SET_ID, false)
1139   options.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor")
1140   options.set(AnchorLinkExtension.ANCHORLINKS_SET_NAME, true)
1141   options.set(AnchorLinkExtension.ANCHORLINKS_TEXT_PREFIX, "<span class=\"octicon octicon-link\"></span>")
1142
1143   Parser parser = Parser.builder(options).build()
1144   HtmlRenderer renderer = HtmlRenderer.builder(options).build()
1145
1146   mdFiles.each { mdFile ->
1147     // add table of contents
1148     def mdText = "[TOC]\n"+mdFile.text
1149
1150     // grab the first top-level title
1151     def title = null
1152     def titleRegex = /(?m)^#(\s+|([^#]))(.*)/
1153     def matcher = mdText =~ titleRegex
1154     if (matcher.size() > 0) {
1155       // matcher[0][2] is the first character of the title if there wasn't any whitespace after the #
1156       title = (matcher[0][2] != null ? matcher[0][2] : "")+matcher[0][3]
1157     }
1158     // or use the filename if none found
1159     if (title == null) {
1160       title = mdFile.getName()
1161     }
1162
1163     Node document = parser.parse(mdText)
1164     String htmlBody = renderer.render(document)
1165     def htmlText = '''<html>
1166 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1167 <html xmlns="http://www.w3.org/1999/xhtml">
1168   <head>
1169     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1170     <meta http-equiv="Content-Style-Type" content="text/css" />
1171     <meta name="generator" content="flexmark" />
1172 '''
1173     htmlText += ((title != null) ? "  <title>${title}</title>" : '' )
1174     htmlText += '''
1175     <style type="text/css">code{white-space: pre;}</style>
1176 '''
1177     htmlText += ((cssFile != null) ? cssFile.text : '')
1178     htmlText += '''</head>
1179   <body>
1180 '''
1181     htmlText += htmlBody
1182     htmlText += '''
1183   </body>
1184 </html>
1185 '''
1186
1187     def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
1188     def htmlFile = file(htmlFilePath)
1189     println("Creating ${htmlFilePath}")
1190     htmlFile.text = htmlText
1191   }
1192 }
1193
1194
1195 task copyDocs(type: Copy) {
1196   def inputDir = "${jalviewDir}/${doc_dir}"
1197   def outputDir = "${docBuildDir}/${doc_dir}"
1198   from(inputDir) {
1199     include('**/*.txt')
1200     include('**/*.md')
1201     include('**/*.html')
1202     include('**/*.xml')
1203     filter(ReplaceTokens,
1204       beginToken: '$$',
1205       endToken: '$$',
1206       tokens: [
1207         'Version-Rel': JALVIEW_VERSION,
1208         'Year-Rel': getDate("yyyy")
1209       ]
1210     )
1211   }
1212   from(inputDir) {
1213     exclude('**/*.txt')
1214     exclude('**/*.md')
1215     exclude('**/*.html')
1216     exclude('**/*.xml')
1217   }
1218   into outputDir
1219
1220   inputs.dir(inputDir)
1221   outputs.dir(outputDir)
1222 }
1223
1224
1225 task convertMdFiles {
1226   dependsOn copyDocs
1227   def mdFiles = fileTree(dir: docBuildDir, include: "**/*.md")
1228   def cssFile = file("${jalviewDir}/${flexmark_css}")
1229
1230   doLast {
1231     convertMdToHtml(mdFiles, cssFile)
1232   }
1233
1234   inputs.files(mdFiles)
1235   inputs.file(cssFile)
1236
1237   def htmlFiles = []
1238   mdFiles.each { mdFile ->
1239     def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
1240     htmlFiles.add(file(htmlFilePath))
1241   }
1242   outputs.files(htmlFiles)
1243 }
1244
1245
1246 def hugoTemplateSubstitutions(String input, Map extras=null) {
1247   def replacements = [
1248     DATE: getDate("yyyy-MM-dd"),
1249     CHANNEL: propertiesChannelName,
1250     APPLICATION_NAME: applicationName,
1251     GIT_HASH: gitHash,
1252     GIT_BRANCH: gitBranch,
1253     VERSION: JALVIEW_VERSION,
1254     JAVA_VERSION: JAVA_VERSION,
1255     VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
1256     DRAFT: "false",
1257     JVL_HEADER: ""
1258   ]
1259   def output = input
1260   if (extras != null) {
1261     extras.each{ k, v ->
1262       output = output.replaceAll("__${k}__", ((v == null)?"":v))
1263     }
1264   }
1265   replacements.each{ k, v ->
1266     output = output.replaceAll("__${k}__", ((v == null)?"":v))
1267   }
1268   return output
1269 }
1270
1271 def mdFileComponents(File mdFile, def dateOnly=false) {
1272   def map = [:]
1273   def content = ""
1274   if (mdFile.exists()) {
1275     def inFrontMatter = false
1276     def firstLine = true
1277     mdFile.eachLine { line ->
1278       if (line.matches("---")) {
1279         def prev = inFrontMatter
1280         inFrontMatter = firstLine
1281         if (inFrontMatter != prev)
1282           return false
1283       }
1284       if (inFrontMatter) {
1285         def m = null
1286         if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
1287           map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
1288         } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
1289           map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
1290         } else if (m = line =~ /^channel:\s*(\S+)/) {
1291           map["channel"] = m[0][1]
1292         } else if (m = line =~ /^version:\s*(\S+)/) {
1293           map["version"] = m[0][1]
1294         } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
1295           map[ m[0][1] ] = m[0][2]
1296         }
1297         if (dateOnly && map["date"] != null) {
1298           return false
1299         }
1300       } else {
1301         if (dateOnly)
1302           return false
1303         content += line+"\n"
1304       }
1305       firstLine = false
1306     }
1307   }
1308   return dateOnly ? map["date"] : [map, content]
1309 }
1310
1311 task hugoTemplates {
1312   group "website"
1313   description "Create partially populated md pages for hugo website build"
1314
1315   def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
1316   def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
1317   def templateFiles = fileTree(dir: hugoTemplatesDir)
1318   def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
1319   def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
1320   def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
1321   def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")
1322
1323   doFirst {
1324     // specific release template for version archive
1325     def changes = ""
1326     def whatsnew = null
1327     def givenDate = null
1328     def givenChannel = null
1329     def givenVersion = null
1330     if (CHANNEL == "RELEASE") {
1331       def (map, content) = mdFileComponents(releaseMdFile)
1332       givenDate = map.date
1333       givenChannel = map.channel
1334       givenVersion = map.version
1335       changes = content
1336       if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
1337         throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
1338       }
1339
1340       if (whatsnewMdFile.exists())
1341         whatsnew = whatsnewMdFile.text
1342     }
1343
1344     def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
1345     def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []
1346
1347     def changesHugo = null
1348     if (changes != null) {
1349       changesHugo = '<div class="release_notes">\n\n'
1350       def inSection = false
1351       changes.eachLine { line ->
1352         def m = null
1353         if (m = line =~ /^##([^#].*)$/) {
1354           if (inSection) {
1355             changesHugo += "</div>\n\n"
1356           }
1357           def section = m[0][1].trim()
1358           section = section.toLowerCase()
1359           section = section.replaceAll(/ +/, "_")
1360           section = section.replaceAll(/[^a-z0-9_\-]/, "")
1361           changesHugo += "<div class=\"${section}\">\n\n"
1362           inSection = true
1363         } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
1364           def comment = m[0][2].trim()
1365           if (comment != "") {
1366             comment = comment.replaceAll('"', "&quot;")
1367             def issuekeys = []
1368             comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
1369             def newline = m[0][1]
1370             if (comment.trim() != "")
1371               newline += "{{<comment>}}${comment}{{</comment>}}  "
1372             newline += m[0][3].trim()
1373             if (issuekeys.size() > 0)
1374               newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
1375             if (m[0][4] != null)
1376               newline += m[0][4]
1377             line = newline
1378           }
1379         }
1380         changesHugo += line+"\n"
1381       }
1382       if (inSection) {
1383         changesHugo += "\n</div>\n\n"
1384       }
1385       changesHugo += '</div>'
1386     }
1387
1388     templateFiles.each{ templateFile ->
1389       def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
1390       def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
1391       def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )
1392
1393       def outPathName = string("${hugoBuildDir}/$newRelPathName")
1394
1395       copy {
1396         from templateFile
1397         rename(templateFile.getName(), newFileName)
1398         into outPathName
1399       }
1400
1401       def newFile = file("${outPathName}/${newFileName}".toString())
1402       def content = newFile.text
1403       newFile.text = hugoTemplateSubstitutions(content,
1404         [
1405           WHATSNEW: whatsnew,
1406           CHANGES: changesHugo,
1407           DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
1408           DRAFT: givenDate == null ? "true" : "false",
1409           JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
1410           JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
1411         ]
1412       )
1413     }
1414
1415   }
1416
1417   inputs.file(oldJvlFile)
1418   inputs.dir(hugoTemplatesDir)
1419   inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
1420   inputs.property("CHANNEL", { CHANNEL })
1421 }
1422
1423 def getMdDate(File mdFile) {
1424   return mdFileComponents(mdFile, true)
1425 }
1426
1427 def getMdSections(String content) {
1428   def sections = [:]
1429   def sectionContent = ""
1430   def sectionName = null
1431   content.eachLine { line ->
1432     def m = null
1433     if (m = line =~ /^##([^#].*)$/) {
1434       if (sectionName != null) {
1435         sections[sectionName] = sectionContent
1436         sectionName = null
1437         sectionContent = ""
1438       }
1439       sectionName = m[0][1].trim()
1440       sectionName = sectionName.toLowerCase()
1441       sectionName = sectionName.replaceAll(/ +/, "_")
1442       sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
1443     } else if (sectionName != null) {
1444       sectionContent += line+"\n"
1445     }
1446   }
1447   if (sectionContent != null) {
1448     sections[sectionName] = sectionContent
1449   }
1450   return sections
1451 }
1452
1453
1454 task copyHelp(type: Copy) {
1455   def inputDir = helpSourceDir
1456   def outputDir = "${helpBuildDir}/${help_dir}"
1457   from(inputDir) {
1458     include('**/*.txt')
1459     include('**/*.md')
1460     include('**/*.html')
1461     include('**/*.hs')
1462     include('**/*.xml')
1463     include('**/*.jhm')
1464     filter(ReplaceTokens,
1465       beginToken: '$$',
1466       endToken: '$$',
1467       tokens: [
1468         'Version-Rel': JALVIEW_VERSION,
1469         'Year-Rel': getDate("yyyy")
1470       ]
1471     )
1472   }
1473   from(inputDir) {
1474     exclude('**/*.txt')
1475     exclude('**/*.md')
1476     exclude('**/*.html')
1477     exclude('**/*.hs')
1478     exclude('**/*.xml')
1479     exclude('**/*.jhm')
1480   }
1481   into outputDir
1482
1483   inputs.dir(inputDir)
1484   outputs.files(helpFile)
1485   outputs.dir(outputDir)
1486 }
1487
1488
1489 task releasesTemplates {
1490   group "help"
1491   description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
1492
1493   dependsOn copyHelp
1494
1495   def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
1496   def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
1497   def releasesHtmlFile = file("${helpBuildDir}/${help_dir}/${releases_html}")
1498   def whatsnewHtmlFile = file("${helpBuildDir}/${help_dir}/${whatsnew_html}")
1499   def releasesMdDir = "${jalviewDir}/${releases_dir}"
1500   def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
1501
1502   doFirst {
1503     def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
1504     def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
1505
1506     if (CHANNEL == "RELEASE") {
1507       if (!releaseMdFile.exists()) {
1508         throw new GradleException("File ${releaseMdFile} must be created for RELEASE")
1509       }
1510       if (!whatsnewMdFile.exists()) {
1511         throw new GradleException("File ${whatsnewMdFile} must be created for RELEASE")
1512       }
1513     }
1514
1515     def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
1516     def releaseFilesDates = releaseFiles.collectEntries {
1517       [(it): getMdDate(it)]
1518     }
1519     releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
1520
1521     def releasesTemplate = releasesTemplateFile.text
1522     def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
1523     def versionTemplate = m[0][1]
1524
1525     MutableDataSet options = new MutableDataSet()
1526
1527     def extensions = new ArrayList<>()
1528     options.set(Parser.EXTENSIONS, extensions)
1529     options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)
1530
1531     Parser parser = Parser.builder(options).build()
1532     HtmlRenderer renderer = HtmlRenderer.builder(options).build()
1533
1534     def actualVersions = releaseFiles.collect { rf ->
1535       def (rfMap, rfContent) = mdFileComponents(rf)
1536       return rfMap.version
1537     }
1538     def versionsHtml = ""
1539     def linkedVersions = []
1540     releaseFiles.reverse().each { rFile ->
1541       def (rMap, rContent) = mdFileComponents(rFile)
1542
1543       def versionLink = ""
1544       def partialVersion = ""
1545       def firstPart = true
1546       rMap.version.split("\\.").each { part ->
1547         def displayPart = ( firstPart ? "" : "." ) + part
1548         partialVersion += displayPart
1549         if (
1550             linkedVersions.contains(partialVersion)
1551             || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
1552             ) {
1553           versionLink += displayPart
1554         } else {
1555           versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
1556           linkedVersions += partialVersion
1557         }
1558         firstPart = false
1559       }
1560       def displayDate = releaseFilesDates[rFile].format("dd/MM/yyyy")
1561
1562       def lm = null
1563       def rContentProcessed = ""
1564       rContent.eachLine { line ->
1565         if (lm = line =~ /^(\s*-)(\s*<!--[^>]*?-->)(.*)$/) {
1566           line = "${lm[0][1]}${lm[0][3]}${lm[0][2]}"
1567       } else if (lm = line =~ /^###([^#]+.*)$/) {
1568           line = "_${lm[0][1].trim()}_"
1569         }
1570         rContentProcessed += line + "\n"
1571       }
1572
1573       def rContentSections = getMdSections(rContentProcessed)
1574       def rVersion = versionTemplate
1575       if (rVersion != "") {
1576         def rNewFeatures = rContentSections["new_features"]
1577         def rIssuesResolved = rContentSections["issues_resolved"]
1578         Node newFeaturesNode = parser.parse(rNewFeatures)
1579         String newFeaturesHtml = renderer.render(newFeaturesNode)
1580         Node issuesResolvedNode = parser.parse(rIssuesResolved)
1581         String issuesResolvedHtml = renderer.render(issuesResolvedNode)
1582         rVersion = hugoTemplateSubstitutions(rVersion,
1583           [
1584             VERSION: rMap.version,
1585             VERSION_LINK: versionLink,
1586             DISPLAY_DATE: displayDate,
1587             NEW_FEATURES: newFeaturesHtml,
1588             ISSUES_RESOLVED: issuesResolvedHtml
1589           ]
1590         )
1591         versionsHtml += rVersion
1592       }
1593     }
1594
1595     releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
1596     releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
1597     releasesHtmlFile.text = releasesTemplate
1598
1599     if (whatsnewMdFile.exists()) {
1600       def wnDisplayDate = releaseFilesDates[releaseMdFile] != null ? releaseFilesDates[releaseMdFile].format("dd MMMM yyyy") : ""
1601       def whatsnewMd = hugoTemplateSubstitutions(whatsnewMdFile.text)
1602       Node whatsnewNode = parser.parse(whatsnewMd)
1603       String whatsnewHtml = renderer.render(whatsnewNode)
1604       whatsnewHtml = whatsnewTemplateFile.text.replaceAll("__WHATS_NEW__", whatsnewHtml)
1605       whatsnewHtmlFile.text = hugoTemplateSubstitutions(whatsnewHtml,
1606         [
1607             VERSION: JALVIEW_VERSION,
1608           DISPLAY_DATE: wnDisplayDate
1609         ]
1610       )
1611     } else if (gradle.taskGraph.hasTask(":linkCheck")) {
1612       whatsnewHtmlFile.text = "Development build " + getDate("yyyy-MM-dd HH:mm:ss")
1613     }
1614
1615   }
1616
1617   inputs.file(releasesTemplateFile)
1618   inputs.file(whatsnewTemplateFile)
1619   inputs.dir(releasesMdDir)
1620   inputs.dir(whatsnewMdDir)
1621   outputs.file(releasesHtmlFile)
1622   outputs.file(whatsnewHtmlFile)
1623 }
1624
1625
1626 task copyResources(type: Copy) {
1627   group = "build"
1628   description = "Copy (and make text substitutions in) the resources dir to the build area"
1629
1630   def inputDir = resourceDir
1631   def outputDir = resourcesBuildDir
1632   from(inputDir) {
1633     include('**/*.txt')
1634     include('**/*.md')
1635     include('**/*.html')
1636     include('**/*.xml')
1637     filter(ReplaceTokens,
1638       beginToken: '$$',
1639       endToken: '$$',
1640       tokens: [
1641         'Version-Rel': JALVIEW_VERSION,
1642         'Year-Rel': getDate("yyyy")
1643       ]
1644     )
1645   }
1646   from(inputDir) {
1647     exclude('**/*.txt')
1648     exclude('**/*.md')
1649     exclude('**/*.html')
1650     exclude('**/*.xml')
1651   }
1652   into outputDir
1653
1654   inputs.dir(inputDir)
1655   outputs.dir(outputDir)
1656 }
1657
1658 task copyChannelResources(type: Copy) {
1659   dependsOn copyResources
1660   group = "build"
1661   description = "Copy the channel resources dir to the build resources area"
1662
1663   def inputDir = "${channelDir}/${resource_dir}"
1664   def outputDir = resourcesBuildDir
1665   from(inputDir) {
1666     include(channel_props)
1667     filter(ReplaceTokens,
1668       beginToken: '__',
1669       endToken: '__',
1670       tokens: [
1671         'SUFFIX': channelSuffix
1672       ]
1673     )
1674   }
1675   from(inputDir) {
1676     exclude(channel_props)
1677   }
1678   into outputDir
1679
1680   inputs.dir(inputDir)
1681   outputs.dir(outputDir)
1682 }
1683
1684 task createBuildProperties(type: WriteProperties) {
1685   dependsOn copyResources
1686   group = "build"
1687   description = "Create the ${buildProperties} file"
1688   
1689   inputs.dir(sourceDir)
1690   inputs.dir(resourcesBuildDir)
1691   outputFile (buildProperties)
1692   // taking time specific comment out to allow better incremental builds
1693   comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd HH:mm:ss")
1694   //comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd")
1695   property "BUILD_DATE", getDate("HH:mm:ss dd MMMM yyyy")
1696   property "VERSION", JALVIEW_VERSION
1697   property "INSTALLATION", INSTALLATION+" git-commit:"+gitHash+" ["+gitBranch+"]"
1698   property "JAVA_COMPILE_VERSION", JAVA_INTEGER_VERSION
1699   if (getdownSetAppBaseProperty) {
1700     property "GETDOWNAPPBASE", getdownAppBase
1701     property "GETDOWNAPPDISTDIR", getdownAppDistDir
1702   }
1703   outputs.file(outputFile)
1704 }
1705
1706
1707 task buildIndices(type: JavaExec) {
1708   dependsOn copyHelp
1709   classpath = sourceSets.main.compileClasspath
1710   main = "com.sun.java.help.search.Indexer"
1711   workingDir = "${helpBuildDir}/${help_dir}"
1712   def argDir = "html"
1713   args = [ argDir ]
1714   inputs.dir("${workingDir}/${argDir}")
1715
1716   outputs.dir("${classesDir}/doc")
1717   outputs.dir("${classesDir}/help")
1718   outputs.file("${workingDir}/JavaHelpSearch/DOCS")
1719   outputs.file("${workingDir}/JavaHelpSearch/DOCS.TAB")
1720   outputs.file("${workingDir}/JavaHelpSearch/OFFSETS")
1721   outputs.file("${workingDir}/JavaHelpSearch/POSITIONS")
1722   outputs.file("${workingDir}/JavaHelpSearch/SCHEMA")
1723   outputs.file("${workingDir}/JavaHelpSearch/TMAP")
1724 }
1725
1726 task buildResources {
1727   dependsOn copyResources
1728   dependsOn copyChannelResources
1729   dependsOn createBuildProperties
1730 }
1731
1732 task prepare {
1733   dependsOn buildResources
1734   dependsOn copyDocs
1735   dependsOn copyHelp
1736   dependsOn releasesTemplates
1737   dependsOn convertMdFiles
1738   dependsOn buildIndices
1739 }
1740
1741
1742 compileJava.dependsOn prepare
1743 run.dependsOn compileJava
1744 compileTestJava.dependsOn compileJava
1745
1746
1747
1748 test {
1749   group = "Verification"
1750   description = "Runs all testTaskN tasks)"
1751
1752   if (useClover) {
1753     dependsOn cloverClasses
1754   } else { //?
1755     dependsOn testClasses
1756   }
1757
1758   // not running tests in this task
1759   exclude "**/*"
1760 }
1761 /* testTask0 is the main test task */
1762 task testTask0(type: Test) {
1763   group = "Verification"
1764   description = "The main test task. Runs all non-testTaskN-labelled tests (unless excluded)"
1765   useTestNG() {
1766     includeGroups testng_groups.split(",")
1767     excludeGroups testng_excluded_groups.split(",")
1768     tasks.withType(Test).matching {it.name.startsWith("testTask") && it.name != name}.all {t -> excludeGroups t.name}
1769     preserveOrder true
1770     useDefaultListeners=true
1771   }
1772 }
1773
1774 /* separated tests */
1775 task testTask1(type: Test) {
1776   group = "Verification"
1777   description = "Tests that need to be isolated from the main test run"
1778   useTestNG() {
1779     includeGroups name
1780     excludeGroups testng_excluded_groups.split(",")
1781     preserveOrder true
1782     useDefaultListeners=true
1783   }
1784 }
1785
1786 task testTask2(type: Test) {
1787   group = "Verification"
1788   description = "Tests that need to be isolated from the main test run"
1789   useTestNG() {
1790     includeGroups name
1791     excludeGroups testng_excluded_groups.split(",")
1792     preserveOrder true
1793     useDefaultListeners=true
1794   }
1795 }
1796 task testTask3(type: Test) {
1797   group = "Verification"
1798   description = "Tests that need to be isolated from the main test run"
1799   useTestNG() {
1800     includeGroups name
1801     excludeGroups testng_excluded_groups.split(",")
1802     preserveOrder true
1803     useDefaultListeners=true
1804   }
1805 }
1806
1807 /* insert more testTaskNs here -- change N to next digit or other string */
1808 /*
1809 task testTaskN(type: Test) {
1810   group = "Verification"
1811   description = "Tests that need to be isolated from the main test run"
1812   useTestNG() {
1813     includeGroups name
1814     excludeGroups testng_excluded_groups.split(",")
1815     preserveOrder true
1816     useDefaultListeners=true
1817   }
1818 }
1819 */
1820
1821 /*
1822  * adapted from https://medium.com/@wasyl/pretty-tests-summary-in-gradle-744804dd676c
1823  * to summarise test results from all Test tasks
1824  */
1825 /* START of test tasks results summary */
1826 import groovy.time.TimeCategory
1827 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
1828 import org.gradle.api.tasks.testing.logging.TestLogEvent
1829 rootProject.ext.testsResults = [] // Container for tests summaries
1830
1831 tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { testTask ->
1832
1833   // from original test task
1834   if (useClover) {
1835     dependsOn cloverClasses
1836   } else { //?
1837     dependsOn testClasses //?
1838   }
1839
1840   // run main tests first
1841   if (!testTask.name.equals("testTask0"))
1842     testTask.mustRunAfter "testTask0"
1843
1844   testTask.testLogging { logging ->
1845     events TestLogEvent.FAILED
1846 //      TestLogEvent.SKIPPED,
1847 //      TestLogEvent.STANDARD_OUT,
1848 //      TestLogEvent.STANDARD_ERROR
1849
1850     exceptionFormat TestExceptionFormat.FULL
1851     showExceptions true
1852     showCauses true
1853     showStackTraces true
1854
1855     info.events = [ TestLogEvent.FAILED ]
1856   }
1857
1858   if (OperatingSystem.current().isMacOsX()) {
1859     testTask.systemProperty "apple.awt.UIElement", "true"
1860     testTask.environment "JAVA_TOOL_OPTIONS", "-Dapple.awt.UIElement=true"
1861   }
1862
1863
1864   ignoreFailures = true // Always try to run all tests for all modules
1865
1866   afterSuite { desc, result ->
1867     if (desc.parent)
1868       return // Only summarize results for whole modules
1869
1870     def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
1871
1872     rootProject.ext.testsResults.add(resultsInfo)
1873   }
1874
1875   // from original test task
1876   maxHeapSize = "1024m"
1877
1878   workingDir = jalviewDir
1879   def testLaf = project.findProperty("test_laf")
1880   if (testLaf != null) {
1881     println("Setting Test LaF to '${testLaf}'")
1882     systemProperty "laf", testLaf
1883   }
1884   def testHiDPIScale = project.findProperty("test_HiDPIScale")
1885   if (testHiDPIScale != null) {
1886     println("Setting Test HiDPI Scale to '${testHiDPIScale}'")
1887     systemProperty "sun.java2d.uiScale", testHiDPIScale
1888   }
1889   sourceCompatibility = compile_source_compatibility
1890   targetCompatibility = compile_target_compatibility
1891   jvmArgs += additional_compiler_args
1892
1893   doFirst {
1894     // this is not perfect yet -- we should only add the commandLineIncludePatterns to the
1895     // testTasks that include the tests, and exclude all from the others.
1896     // get --test argument
1897     filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns
1898     // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the
1899     // commandLineIncludePatterns not matching anything.  Instead we are doing setFailOnNoMatchingTests(false) below
1900
1901
1902     if (useClover) {
1903       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
1904     }
1905   }
1906
1907
1908   /* don't fail on no matching tests (so --tests will run across all testTasks) */
1909   testTask.filter.setFailOnNoMatchingTests(false)
1910
1911   /* ensure the "test" task dependsOn all the testTasks */
1912   test.dependsOn testTask
1913 }
1914
1915 gradle.buildFinished {
1916     def allResults = rootProject.ext.testsResults
1917
1918     if (!allResults.isEmpty()) {
1919         printResults allResults
1920         allResults.each {r ->
1921           if (r[2].resultType == TestResult.ResultType.FAILURE)
1922             throw new GradleException("Failed tests!")
1923         }
1924     }
1925 }
1926
1927 private static String colString(styler, col, colour, text) {
1928   return col?"${styler[colour](text)}":text
1929 }
1930
1931 private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) {
1932   def colour = 'black'
1933   def text = rt
1934   def nocol = false
1935   if (rc == 0) {
1936     text = "-----"
1937     nocol = true
1938   } else {
1939     switch(rt) {
1940       case TestResult.ResultType.SUCCESS:
1941         colour = 'green'
1942         break;
1943       case TestResult.ResultType.FAILURE:
1944         colour = 'red'
1945         break;
1946       default:
1947         nocol = true
1948         break;
1949     }
1950   }
1951   StringBuilder sb = new StringBuilder()
1952   sb.append("${pn}")
1953   if (tn != null)
1954     sb.append(":${tn}")
1955   sb.append(" results: ")
1956   sb.append(colString(s, col && !nocol, colour, text))
1957   sb.append(" (")
1958   sb.append("${rc} tests, ")
1959   sb.append(colString(s, col && rs > 0, 'green', rs))
1960   sb.append(" successes, ")
1961   sb.append(colString(s, col && rf > 0, 'red', rf))
1962   sb.append(" failures, ")
1963   sb.append("${rsk} skipped) in ${t}")
1964   return sb.toString()
1965 }
1966
1967 private static void printResults(allResults) {
1968
1969     // styler from https://stackoverflow.com/a/56139852
1970     def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] }
1971
1972     def maxLength = 0
1973     def failedTests = false
1974     def summaryLines = []
1975     def totalcount = 0
1976     def totalsuccess = 0
1977     def totalfail = 0
1978     def totalskip = 0
1979     def totaltime = TimeCategory.getSeconds(0)
1980     // sort on project name then task name
1981     allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each {
1982       def projectName = it[0]
1983       def taskName = it[1]
1984       def result = it[2]
1985       def time = it[3]
1986       def report = it[4]
1987       def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true)
1988       def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false)
1989       def reportLine = "Report file: ${report}"
1990       def ls = summaryPlain.length()
1991       def lr = reportLine.length()
1992       def m = [ls, lr].max()
1993       if (m > maxLength)
1994         maxLength = m
1995       def info = [ls, summaryCol, reportLine]
1996       summaryLines.add(info)
1997       failedTests |= result.resultType == TestResult.ResultType.FAILURE
1998       totalcount += result.testCount
1999       totalsuccess += result.successfulTestCount
2000       totalfail += result.failedTestCount
2001       totalskip += result.skippedTestCount
2002       totaltime += time
2003     }
2004     def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true)
2005     def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false)
2006     def tls = totalSummaryPlain.length()
2007     if (tls > maxLength)
2008       maxLength = tls
2009     def info = [tls, totalSummaryCol, null]
2010     summaryLines.add(info)
2011
2012     def allSummaries = []
2013     for(sInfo : summaryLines) {
2014       def ls = sInfo[0]
2015       def summary = sInfo[1]
2016       def report = sInfo[2]
2017
2018       StringBuilder sb = new StringBuilder()
2019       sb.append("│" + summary + " " * (maxLength - ls) + "│")
2020       if (report != null) {
2021         sb.append("\n│" + report + " " * (maxLength - report.length()) + "│")
2022       }
2023       allSummaries += sb.toString()
2024     }
2025
2026     println "┌${"${"─" * maxLength}"}┐"
2027     println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n")
2028     println "└${"${"─" * maxLength}"}┘"
2029 }
2030 /* END of test tasks results summary */
2031
2032
2033 task compileLinkCheck(type: JavaCompile) {
2034   options.fork = true
2035   classpath = files("${jalviewDir}/${utils_dir}")
2036   destinationDir = file("${jalviewDir}/${utils_dir}")
2037   source = fileTree(dir: "${jalviewDir}/${utils_dir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])
2038
2039   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2040   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2041   outputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.class")
2042   outputs.file("${jalviewDir}/${utils_dir}/BufferedLineReader.class")
2043 }
2044
2045
2046 task linkCheck(type: JavaExec) {
2047   dependsOn prepare
2048   dependsOn compileLinkCheck
2049
2050   def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
2051   classpath = files("${jalviewDir}/${utils_dir}")
2052   main = "HelpLinksChecker"
2053   workingDir = "${helpBuildDir}"
2054   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
2055
2056   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
2057   standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
2058     outFOS,
2059     System.out)
2060   errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
2061     outFOS,
2062     System.err)
2063
2064   inputs.dir(helpBuildDir)
2065   outputs.file(helpLinksCheckerOutFile)
2066 }
2067
2068
2069 // import the pubhtmlhelp target
2070 ant.properties.basedir = "${jalviewDir}"
2071 ant.properties.helpBuildDir = "${helpBuildDir}/${help_dir}"
2072 ant.importBuild "${utils_dir}/publishHelp.xml"
2073
2074
2075 task cleanPackageDir(type: Delete) {
2076   doFirst {
2077     delete fileTree(dir: "${jalviewDir}/${package_dir}", include: "*.jar")
2078   }
2079 }
2080
2081
2082 jar {
2083   dependsOn prepare
2084   dependsOn linkCheck
2085
2086   manifest {
2087     attributes "Main-Class": main_class,
2088     "Permissions": "all-permissions",
2089     "Application-Name": applicationName,
2090     "Codebase": application_codebase,
2091     "Implementation-Version": JALVIEW_VERSION
2092   }
2093
2094   def outputDir = "${jalviewDir}/${package_dir}"
2095   destinationDirectory = file(outputDir)
2096   archiveFileName = rootProject.name+".jar"
2097   duplicatesStrategy "EXCLUDE"
2098
2099
2100   exclude "cache*/**"
2101   exclude "*.jar"
2102   exclude "*.jar.*"
2103   exclude "**/*.jar"
2104   exclude "**/*.jar.*"
2105
2106   inputs.dir(sourceSets.main.java.outputDir)
2107   sourceSets.main.resources.srcDirs.each{ dir ->
2108     inputs.dir(dir)
2109   }
2110   outputs.file("${outputDir}/${archiveFileName}")
2111 }
2112
2113
2114 task copyJars(type: Copy) {
2115   from fileTree(dir: classesDir, include: "**/*.jar").files
2116   into "${jalviewDir}/${package_dir}"
2117 }
2118
2119
2120 // doing a Sync instead of Copy as Copy doesn't deal with "outputs" very well
2121 task syncJars(type: Sync) {
2122   dependsOn jar
2123   from fileTree(dir: "${jalviewDir}/${libDistDir}", include: "**/*.jar").files
2124   into "${jalviewDir}/${package_dir}"
2125   preserve {
2126     include jar.archiveFileName.getOrNull()
2127   }
2128 }
2129
2130
2131 task makeDist {
2132   group = "build"
2133   description = "Put all required libraries in dist"
2134   // order of "cleanPackageDir", "copyJars", "jar" important!
2135   jar.mustRunAfter cleanPackageDir
2136   syncJars.mustRunAfter cleanPackageDir
2137   dependsOn cleanPackageDir
2138   dependsOn syncJars
2139   dependsOn jar
2140   outputs.dir("${jalviewDir}/${package_dir}")
2141 }
2142
2143
2144 task cleanDist {
2145   dependsOn cleanPackageDir
2146   dependsOn cleanTest
2147   dependsOn clean
2148 }
2149
2150
2151 shadowJar {
2152   group = "distribution"
2153   description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar"
2154   if (buildDist) {
2155     dependsOn makeDist
2156   }
2157   from ("${jalviewDir}/${libDistDir}") {
2158     include("*.jar")
2159   }
2160   manifest {
2161     attributes "Implementation-Version": JALVIEW_VERSION,
2162     "Application-Name": applicationName
2163   }
2164
2165   duplicatesStrategy "INCLUDE"
2166
2167   mainClassName = shadow_jar_main_class
2168   mergeServiceFiles()
2169   classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
2170   minimize()
2171 }
2172
2173 task getdownImagesCopy() {
2174   inputs.dir getdownImagesDir
2175   outputs.dir getdownImagesBuildDir
2176
2177   doFirst {
2178     copy {
2179       from(getdownImagesDir) {
2180         include("*getdown*.png")
2181       }
2182       into getdownImagesBuildDir
2183     }
2184   }
2185 }
2186
2187 task getdownImagesProcess() {
2188   dependsOn getdownImagesCopy
2189
2190   doFirst {
2191     if (backgroundImageText) {
2192       if (convertBinary == null) {
2193         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2194       }
2195       if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
2196         throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2197       }
2198       fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
2199         exec {
2200           executable convertBinary
2201           args = [
2202             file.getPath(),
2203             '-font', getdown_background_image_text_font,
2204             '-fill', getdown_background_image_text_colour,
2205             '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
2206             '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2207             '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2208             file.getPath()
2209           ]
2210         }
2211       }
2212     }
2213   }
2214 }
2215
2216 task getdownImages() {
2217   dependsOn getdownImagesProcess
2218 }
2219
2220 task getdownWebsite() {
2221   group = "distribution"
2222   description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
2223
2224   dependsOn getdownImages
2225   if (buildDist) {
2226     dependsOn makeDist
2227   }
2228
2229   def getdownWebsiteResourceFilenames = []
2230   def getdownResourceDir = getdownResourceDir
2231   def getdownResourceFilenames = []
2232
2233   doFirst {
2234     // clean the getdown website and files dir before creating getdown folders
2235     delete getdownAppBaseDir
2236     delete getdownFilesDir
2237
2238     copy {
2239       from buildProperties
2240       rename(file(buildProperties).getName(), getdown_build_properties)
2241       into getdownAppDir
2242     }
2243     getdownWebsiteResourceFilenames += "${getdownAppDistDir}/${getdown_build_properties}"
2244
2245     copy {
2246       from channelPropsFile
2247       filter(ReplaceTokens,
2248         beginToken: '__',
2249         endToken: '__',
2250         tokens: [
2251           'SUFFIX': channelSuffix
2252         ]
2253       )
2254       into getdownAppBaseDir
2255     }
2256     getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
2257
2258     // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
2259     def props = project.properties.sort { it.key }
2260     if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
2261       props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
2262     }
2263     if (getdownAltJavaMaxVersion != null && getdownAltJavaMaxVersion.length() > 0) {
2264       props.put("getdown_txt_java_max_version", getdownAltJavaMaxVersion)
2265     }
2266     if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
2267       props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
2268     }
2269     if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
2270       props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
2271       props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}")
2272       props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}")
2273       props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}")
2274       props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
2275       props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
2276     }
2277
2278     props.put("getdown_txt_title", jalview_name)
2279     props.put("getdown_txt_ui.name", applicationName)
2280
2281     // start with appbase
2282     getdownTextLines += "appbase = ${getdownAppBase}"
2283     props.each{ prop, val ->
2284       if (prop.startsWith("getdown_txt_") && val != null) {
2285         if (prop.startsWith("getdown_txt_multi_")) {
2286           def key = prop.substring(18)
2287           val.split(",").each{ v ->
2288             def line = "${key} = ${v}"
2289             getdownTextLines += line
2290           }
2291         } else {
2292           // file values rationalised
2293           if (val.indexOf('/') > -1 || prop.startsWith("getdown_txt_resource")) {
2294             def r = null
2295             if (val.indexOf('/') == 0) {
2296               // absolute path
2297               r = file(val)
2298             } else if (val.indexOf('/') > 0) {
2299               // relative path (relative to jalviewDir)
2300               r = file( "${jalviewDir}/${val}" )
2301             }
2302             if (r.exists()) {
2303               val = "${getdown_resource_dir}/" + r.getName()
2304               getdownWebsiteResourceFilenames += val
2305               getdownResourceFilenames += r.getPath()
2306             }
2307           }
2308           if (! prop.startsWith("getdown_txt_resource")) {
2309             def line = prop.substring(12) + " = ${val}"
2310             getdownTextLines += line
2311           }
2312         }
2313       }
2314     }
2315
2316     getdownWebsiteResourceFilenames.each{ filename ->
2317       getdownTextLines += "resource = ${filename}"
2318     }
2319     getdownResourceFilenames.each{ filename ->
2320       copy {
2321         from filename
2322         into getdownResourceDir
2323       }
2324     }
2325     
2326     def getdownWrapperScripts = [ getdown_bash_wrapper_script, getdown_powershell_wrapper_script, getdown_batch_wrapper_script ]
2327     getdownWrapperScripts.each{ script ->
2328       def s = file( "${jalviewDir}/utils/getdown/${getdown_wrapper_script_dir}/${script}" )
2329       if (s.exists()) {
2330         copy {
2331           from s
2332           into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
2333         }
2334         getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
2335       }
2336     }
2337
2338     def codeFiles = []
2339     fileTree(file(package_dir)).each{ f ->
2340       if (f.isDirectory()) {
2341         def files = fileTree(dir: f, include: ["*"]).getFiles()
2342         codeFiles += files
2343       } else if (f.exists()) {
2344         codeFiles += f
2345       }
2346     }
2347     def jalviewJar = jar.archiveFileName.getOrNull()
2348     // put jalview.jar first for CLASSPATH and .properties files reasons
2349     codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
2350       def name = f.getName()
2351       def line = "code = ${getdownAppDistDir}/${name}"
2352       getdownTextLines += line
2353       copy {
2354         from f.getPath()
2355         into getdownAppDir
2356       }
2357     }
2358
2359     // NOT USING MODULES YET, EVERYTHING SHOULD BE IN dist
2360     /*
2361     if (JAVA_VERSION.equals("11")) {
2362     def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
2363     j11libFiles.sort().each{f ->
2364     def name = f.getName()
2365     def line = "code = ${getdown_j11lib_dir}/${name}"
2366     getdownTextLines += line
2367     copy {
2368     from f.getPath()
2369     into getdownJ11libDir
2370     }
2371     }
2372     }
2373      */
2374
2375     // 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.
2376     //getdownTextLines += "class = " + file(getdownLauncher).getName()
2377     getdownTextLines += "resource = ${getdown_launcher_new}"
2378     getdownTextLines += "class = ${main_class}"
2379     // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
2380     if (getdownSetAppBaseProperty) {
2381       getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
2382       getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
2383     }
2384
2385     def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
2386     getdownTxt.write(getdownTextLines.join("\n"))
2387
2388     getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
2389     def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
2390     launchJvl.write("appbase=${getdownAppBase}")
2391
2392     // files going into the getdown website dir: getdown-launcher.jar
2393     copy {
2394       from getdownLauncher
2395       rename(file(getdownLauncher).getName(), getdown_launcher_new)
2396       into getdownAppBaseDir
2397     }
2398
2399     // files going into the getdown website dir: getdown-launcher(-local).jar
2400     copy {
2401       from getdownLauncher
2402       if (file(getdownLauncher).getName() != getdown_launcher) {
2403         rename(file(getdownLauncher).getName(), getdown_launcher)
2404       }
2405       into getdownAppBaseDir
2406     }
2407
2408     // files going into the getdown website dir: ./install dir and files
2409     if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
2410       copy {
2411         from getdownTxt
2412         from getdownLauncher
2413         from "${getdownAppDir}/${getdown_build_properties}"
2414         if (file(getdownLauncher).getName() != getdown_launcher) {
2415           rename(file(getdownLauncher).getName(), getdown_launcher)
2416         }
2417         into getdownInstallDir
2418       }
2419
2420       // and make a copy in the getdown files dir (these are not downloaded by getdown)
2421       copy {
2422         from getdownInstallDir
2423         into getdownFilesInstallDir
2424       }
2425     }
2426
2427     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2428     copy {
2429       from getdownTxt
2430       from launchJvl
2431       from getdownLauncher
2432       from "${getdownAppBaseDir}/${getdown_build_properties}"
2433       from "${getdownAppBaseDir}/${channel_props}"
2434       if (file(getdownLauncher).getName() != getdown_launcher) {
2435         rename(file(getdownLauncher).getName(), getdown_launcher)
2436       }
2437       into getdownFilesDir
2438     }
2439
2440     // and ./resource (not all downloaded by getdown)
2441     copy {
2442       from getdownResourceDir
2443       into "${getdownFilesDir}/${getdown_resource_dir}"
2444     }
2445   }
2446
2447   if (buildDist) {
2448     inputs.dir("${jalviewDir}/${package_dir}")
2449   }
2450   outputs.dir(getdownAppBaseDir)
2451   outputs.dir(getdownFilesDir)
2452 }
2453
2454
2455 // a helper task to allow getdown digest of any dir: `gradle getdownDigestDir -PDIGESTDIR=/path/to/my/random/getdown/dir
2456 task getdownDigestDir(type: JavaExec) {
2457   group "Help"
2458   description "A task to run a getdown Digest on a dir with getdown.txt. Provide a DIGESTDIR property via -PDIGESTDIR=..."
2459
2460   def digestDirPropertyName = "DIGESTDIR"
2461   doFirst {
2462     classpath = files(getdownLauncher)
2463     def digestDir = findProperty(digestDirPropertyName)
2464     if (digestDir == null) {
2465       throw new GradleException("Must provide a DIGESTDIR value to produce an alternative getdown digest")
2466     }
2467     args digestDir
2468   }
2469   main = "com.threerings.getdown.tools.Digester"
2470 }
2471
2472
2473 task getdownDigest(type: JavaExec) {
2474   group = "distribution"
2475   description = "Digest the getdown website folder"
2476   dependsOn getdownWebsite
2477   doFirst {
2478     classpath = files(getdownLauncher)
2479   }
2480   main = "com.threerings.getdown.tools.Digester"
2481   args getdownAppBaseDir
2482   inputs.dir(getdownAppBaseDir)
2483   outputs.file("${getdownAppBaseDir}/digest2.txt")
2484 }
2485
2486
2487 task getdown() {
2488   group = "distribution"
2489   description = "Create the minimal and full getdown app folder for installers and website and create digest file"
2490   dependsOn getdownDigest
2491   doLast {
2492     if (reportRsyncCommand) {
2493       def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
2494       def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
2495       println "LIKELY RSYNC COMMAND:"
2496       println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
2497       if (RUNRSYNC == "true") {
2498         exec {
2499           commandLine "mkdir", "-p", toDir
2500         }
2501         exec {
2502           commandLine "rsync", "-avh", "--delete", fromDir, toDir
2503         }
2504       }
2505     }
2506   }
2507 }
2508
2509
2510 task getdownArchiveBuild() {
2511   group = "distribution"
2512   description = "Put files in the archive dir to go on the website"
2513
2514   dependsOn getdownWebsite
2515
2516   def v = "v${JALVIEW_VERSION_UNDERSCORES}"
2517   def vDir = "${getdownArchiveDir}/${v}"
2518   getdownFullArchiveDir = "${vDir}/getdown"
2519   getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
2520
2521   def vAltDir = "alt_${v}"
2522   def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
2523
2524   doFirst {
2525     // cleanup old "old" dir
2526     delete getdownArchiveDir
2527
2528     def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
2529     getdownArchiveTxt.getParentFile().mkdirs()
2530     def getdownArchiveTextLines = []
2531     def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
2532
2533     // the libdir
2534     copy {
2535       from "${getdownAppBaseDir}/${getdownAppDistDir}"
2536       into "${getdownFullArchiveDir}/${vAltDir}"
2537     }
2538
2539     getdownTextLines.each { line ->
2540       line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
2541       line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
2542       line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
2543       line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
2544       line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
2545       line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
2546       // remove the existing resource = resource/ or bin/ lines
2547       if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
2548         getdownArchiveTextLines += line
2549       }
2550     }
2551
2552     // the resource dir -- add these files as resource lines in getdown.txt
2553     copy {
2554       from "${archiveImagesDir}"
2555       into "${getdownFullArchiveDir}/${getdown_resource_dir}"
2556       eachFile { file ->
2557         getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
2558       }
2559     }
2560
2561     getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
2562
2563     def vLaunchJvl = file(getdownVersionLaunchJvl)
2564     vLaunchJvl.getParentFile().mkdirs()
2565     vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
2566     def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
2567     def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
2568     // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
2569     //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
2570     java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
2571
2572     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2573     copy {
2574       from getdownLauncher
2575       from "${getdownAppBaseDir}/${getdownLaunchJvl}"
2576       from "${getdownAppBaseDir}/${getdown_launcher_new}"
2577       from "${getdownAppBaseDir}/${channel_props}"
2578       if (file(getdownLauncher).getName() != getdown_launcher) {
2579         rename(file(getdownLauncher).getName(), getdown_launcher)
2580       }
2581       into getdownFullArchiveDir
2582     }
2583
2584   }
2585 }
2586
2587 task getdownArchiveDigest(type: JavaExec) {
2588   group = "distribution"
2589   description = "Digest the getdown archive folder"
2590
2591   dependsOn getdownArchiveBuild
2592
2593   doFirst {
2594     classpath = files(getdownLauncher)
2595     args getdownFullArchiveDir
2596   }
2597   main = "com.threerings.getdown.tools.Digester"
2598   inputs.dir(getdownFullArchiveDir)
2599   outputs.file("${getdownFullArchiveDir}/digest2.txt")
2600 }
2601
2602 task getdownArchive() {
2603   group = "distribution"
2604   description = "Build the website archive dir with getdown digest"
2605
2606   dependsOn getdownArchiveBuild
2607   dependsOn getdownArchiveDigest
2608 }
2609
2610 tasks.withType(JavaCompile) {
2611         options.encoding = 'UTF-8'
2612 }
2613
2614
2615 clean {
2616   doFirst {
2617     delete getdownAppBaseDir
2618     delete getdownFilesDir
2619     delete getdownArchiveDir
2620   }
2621 }
2622
2623
2624 install4j {
2625   if (file(install4jHomeDir).exists()) {
2626     // good to go!
2627   } else if (file(System.getProperty("user.home")+"/buildtools/install4j").exists()) {
2628     install4jHomeDir = System.getProperty("user.home")+"/buildtools/install4j"
2629   } else if (file("/Applications/install4j.app/Contents/Resources/app").exists()) {
2630     install4jHomeDir = "/Applications/install4j.app/Contents/Resources/app"
2631   }
2632   installDir(file(install4jHomeDir))
2633
2634   mediaTypes = Arrays.asList(install4j_media_types.split(","))
2635 }
2636
2637
2638 task copyInstall4jTemplate {
2639   def install4jTemplateFile = file("${install4jDir}/${install4j_template}")
2640   def install4jFileAssociationsFile = file("${install4jDir}/${install4j_installer_file_associations}")
2641   inputs.file(install4jTemplateFile)
2642   inputs.file(install4jFileAssociationsFile)
2643   inputs.property("CHANNEL", { CHANNEL })
2644   outputs.file(install4jConfFile)
2645
2646   doLast {
2647     def install4jConfigXml = new XmlParser().parse(install4jTemplateFile)
2648
2649     // turn off code signing if no OSX_KEYPASS
2650     if (OSX_KEYPASS == "") {
2651       install4jConfigXml.'**'.codeSigning.each { codeSigning ->
2652         codeSigning.'@macEnabled' = "false"
2653       }
2654       install4jConfigXml.'**'.windows.each { windows ->
2655         windows.'@runPostProcessor' = "false"
2656       }
2657     }
2658
2659     // disable install screen for OSX dmg (for 2.11.2.0)
2660     install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
2661       macosArchive.attributes().remove('executeSetupApp')
2662       macosArchive.attributes().remove('setupAppId')
2663     }
2664
2665     // turn off checksum creation for LOCAL channel
2666     def e = install4jConfigXml.application[0]
2667     e.'@createChecksums' = string(install4jCheckSums)
2668
2669     // put file association actions where placeholder action is
2670     def install4jFileAssociationsText = install4jFileAssociationsFile.text
2671     def fileAssociationActions = new XmlParser().parseText("<actions>${install4jFileAssociationsText}</actions>")
2672     install4jConfigXml.'**'.action.any { a -> // .any{} stops after the first one that returns true
2673       if (a.'@name' == 'EXTENSIONS_REPLACED_BY_GRADLE') {
2674         def parent = a.parent()
2675         parent.remove(a)
2676         fileAssociationActions.each { faa ->
2677             parent.append(faa)
2678         }
2679         // don't need to continue in .any loop once replacements have been made
2680         return true
2681       }
2682     }
2683
2684     // use Windows Program Group with Examples folder for RELEASE, and Program Group without Examples for everything else
2685     // NB we're deleting the /other/ one!
2686     // Also remove the examples subdir from non-release versions
2687     def customizedIdToDelete = "PROGRAM_GROUP_RELEASE"
2688     // 2.11.1.0 NOT releasing with the Examples folder in the Program Group
2689     if (false && CHANNEL=="RELEASE") { // remove 'false && ' to include Examples folder in RELEASE channel
2690       customizedIdToDelete = "PROGRAM_GROUP_NON_RELEASE"
2691     } else {
2692       // remove the examples subdir from Full File Set
2693       def files = install4jConfigXml.files[0]
2694       def fileset = files.filesets.fileset.find { fs -> fs.'@customizedId' == "FULL_FILE_SET" }
2695       def root = files.roots.root.find { r -> r.'@fileset' == fileset.'@id' }
2696       def mountPoint = files.mountPoints.mountPoint.find { mp -> mp.'@root' == root.'@id' }
2697       def dirEntry = files.entries.dirEntry.find { de -> de.'@mountPoint' == mountPoint.'@id' && de.'@subDirectory' == "examples" }
2698       dirEntry.parent().remove(dirEntry)
2699     }
2700     install4jConfigXml.'**'.action.any { a ->
2701       if (a.'@customizedId' == customizedIdToDelete) {
2702         def parent = a.parent()
2703         parent.remove(a)
2704         return true
2705       }
2706     }
2707
2708     // write install4j file
2709     install4jConfFile.text = XmlUtil.serialize(install4jConfigXml)
2710   }
2711 }
2712
2713
2714 clean {
2715   doFirst {
2716     delete install4jConfFile
2717   }
2718 }
2719
2720 task cleanInstallersDataFiles {
2721   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
2722   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
2723   def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
2724   doFirst {
2725     delete installersOutputTxt
2726     delete installersSha256
2727     delete hugoDataJsonFile
2728   }
2729 }
2730
2731 task install4jDMGBackgroundImageCopy {
2732   inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
2733   outputs.dir "${install4jDMGBackgroundImageBuildDir}"
2734   doFirst {
2735     copy {
2736       from(install4jDMGBackgroundImageDir) {
2737         include(install4jDMGBackgroundImageFile)
2738       }
2739       into install4jDMGBackgroundImageBuildDir
2740     }
2741   }
2742 }
2743
2744 task install4jDMGBackgroundImageProcess {
2745   dependsOn install4jDMGBackgroundImageCopy
2746
2747   doFirst {
2748     if (backgroundImageText) {
2749       if (convertBinary == null) {
2750         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2751       }
2752       if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
2753         throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2754       }
2755       fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
2756         exec {
2757           executable convertBinary
2758           args = [
2759             file.getPath(),
2760             '-font', install4j_background_image_text_font,
2761             '-fill', install4j_background_image_text_colour,
2762             '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
2763             '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2764             '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2765             file.getPath()
2766           ]
2767         }
2768       }
2769     }
2770   }
2771 }
2772
2773 task install4jDMGBackgroundImage {
2774   dependsOn install4jDMGBackgroundImageProcess
2775 }
2776
2777 task installerFiles(type: com.install4j.gradle.Install4jTask) {
2778   group = "distribution"
2779   description = "Create the install4j installers"
2780   dependsOn getdown
2781   dependsOn copyInstall4jTemplate
2782   dependsOn cleanInstallersDataFiles
2783   dependsOn install4jDMGBackgroundImage
2784
2785   projectFile = install4jConfFile
2786
2787   // create an md5 for the input files to use as version for install4j conf file
2788   def digest = MessageDigest.getInstance("MD5")
2789   digest.update(
2790     (file("${install4jDir}/${install4j_template}").text + 
2791     file("${install4jDir}/${install4j_info_plist_file_associations}").text +
2792     file("${install4jDir}/${install4j_installer_file_associations}").text).bytes)
2793   def filesMd5 = new BigInteger(1, digest.digest()).toString(16)
2794   if (filesMd5.length() >= 8) {
2795     filesMd5 = filesMd5.substring(0,8)
2796   }
2797   def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"
2798
2799   variables = [
2800     'JALVIEW_NAME': jalview_name,
2801     'JALVIEW_APPLICATION_NAME': applicationName,
2802     'JALVIEW_DIR': "../..",
2803     'OSX_KEYSTORE': OSX_KEYSTORE,
2804     'OSX_APPLEID': OSX_APPLEID,
2805     'OSX_ALTOOLPASS': OSX_ALTOOLPASS,
2806     'JSIGN_SH': JSIGN_SH,
2807     'JRE_DIR': getdown_app_dir_java,
2808     'INSTALLER_TEMPLATE_VERSION': install4jTemplateVersion,
2809     'JALVIEW_VERSION': JALVIEW_VERSION,
2810     'JAVA_MIN_VERSION': JAVA_MIN_VERSION,
2811     'JAVA_MAX_VERSION': JAVA_MAX_VERSION,
2812     'JAVA_VERSION': JAVA_VERSION,
2813     'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
2814     'VERSION': JALVIEW_VERSION,
2815     'COPYRIGHT_MESSAGE': install4j_copyright_message,
2816     'BUNDLE_ID': install4jBundleId,
2817     'INTERNAL_ID': install4jInternalId,
2818     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
2819     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
2820     'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
2821     'WRAPPER_LINK': getdownWrapperLink,
2822     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
2823     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
2824     'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script,
2825     'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
2826     'INSTALLER_NAME': install4jInstallerName,
2827     'INSTALL4J_UTILS_DIR': install4j_utils_dir,
2828     'GETDOWN_CHANNEL_DIR': getdownChannelDir,
2829     'GETDOWN_FILES_DIR': getdown_files_dir,
2830     'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
2831     'GETDOWN_DIST_DIR': getdownAppDistDir,
2832     'GETDOWN_ALT_DIR': getdown_app_dir_alt,
2833     'GETDOWN_INSTALL_DIR': getdown_install_dir,
2834     'INFO_PLIST_FILE_ASSOCIATIONS_FILE': install4j_info_plist_file_associations,
2835     'BUILD_DIR': install4jBuildDir,
2836     'APPLICATION_CATEGORIES': install4j_application_categories,
2837     'APPLICATION_FOLDER': install4jApplicationFolder,
2838     'UNIX_APPLICATION_FOLDER': install4jUnixApplicationFolder,
2839     'EXECUTABLE_NAME': install4jExecutableName,
2840     'EXTRA_SCHEME': install4jExtraScheme,
2841     'MAC_ICONS_FILE': install4jMacIconsFile,
2842     'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
2843     'PNG_ICON_FILE': install4jPngIconFile,
2844     'BACKGROUND': install4jBackground,
2845   ]
2846
2847   def varNameMap = [
2848     'mac': 'MACOS',
2849     'windows': 'WINDOWS',
2850     'linux': 'LINUX'
2851   ]
2852   
2853   // these are the bundled OS/architecture VMs needed by install4j
2854   def osArch = [
2855     [ "mac", "x64" ],
2856     [ "mac", "aarch64" ],
2857     [ "windows", "x64" ],
2858     [ "linux", "x64" ],
2859     [ "linux", "aarch64" ]
2860   ]
2861   osArch.forEach { os, arch ->
2862     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)
2863     // N.B. For some reason install4j requires the below filename to have underscores and not hyphens
2864     // otherwise running `gradle installers` generates a non-useful error:
2865     // `install4j: compilation failed. Reason: java.lang.NumberFormatException: For input string: "windows"`
2866     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)
2867   }
2868
2869   //println("INSTALL4J VARIABLES:")
2870   //variables.each{k,v->println("${k}=${v}")}
2871
2872   destination = "${jalviewDir}/${install4jBuildDir}"
2873   buildSelected = true
2874
2875   if (install4j_faster.equals("true") || CHANNEL.startsWith("LOCAL")) {
2876     faster = true
2877     disableSigning = true
2878     disableNotarization = true
2879   }
2880
2881   if (OSX_KEYPASS) {
2882     macKeystorePassword = OSX_KEYPASS
2883   } 
2884   
2885   if (OSX_ALTOOLPASS) {
2886     appleIdPassword = OSX_ALTOOLPASS
2887     disableNotarization = false
2888   } else {
2889     disableNotarization = true
2890   }
2891
2892   doFirst {
2893     println("Using projectFile "+projectFile)
2894     if (!disableNotarization) { println("Will notarize OSX App DMG") }
2895   }
2896   //verbose=true
2897
2898   inputs.dir(getdownAppBaseDir)
2899   inputs.file(install4jConfFile)
2900   inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
2901   outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
2902 }
2903
2904 def getDataHash(File myFile) {
2905   HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
2906   return myFile.exists()
2907   ? [
2908       "file" : myFile.getName(),
2909       "filesize" : myFile.length(),
2910       "sha256" : hash.toString()
2911     ]
2912   : null
2913 }
2914
2915 def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
2916   def hash = [
2917     "channel" : getdownChannelName,
2918     "date" : getDate("yyyy-MM-dd HH:mm:ss"),
2919     "git-commit" : "${gitHash} [${gitBranch}]",
2920     "version" : JALVIEW_VERSION
2921   ]
2922   // install4j installer files
2923   if (installersOutputTxt.exists()) {
2924     def idHash = [:]
2925     installersOutputTxt.readLines().each { def line ->
2926       if (line.startsWith("#")) {
2927         return;
2928       }
2929       line.replaceAll("\n","")
2930       def vals = line.split("\t")
2931       def filename = vals[3]
2932       def filesize = file(filename).length()
2933       filename = filename.replaceAll(/^.*\//, "")
2934       hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
2935       idHash."${filename}" = vals[0]
2936     }
2937     if (install4jCheckSums && installersSha256.exists()) {
2938       installersSha256.readLines().each { def line ->
2939         if (line.startsWith("#")) {
2940           return;
2941         }
2942         line.replaceAll("\n","")
2943         def vals = line.split(/\s+\*?/)
2944         def filename = vals[1]
2945         def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
2946       }
2947     }
2948   }
2949
2950   [
2951     "JAR": shadowJar.archiveFile, // executable JAR
2952     "JVL": getdownVersionLaunchJvl, // version JVL
2953     "SOURCE": sourceDist.archiveFile // source TGZ
2954   ].each { key, value ->
2955     def file = file(value)
2956     if (file.exists()) {
2957       def fileHash = getDataHash(file)
2958       if (fileHash != null) {
2959         hash."${key}" = fileHash;
2960       }
2961     }
2962   }
2963   return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
2964 }
2965
2966 task staticMakeInstallersJsonFile {
2967   doFirst {
2968     def output = findProperty("i4j_output")
2969     def sha256 = findProperty("i4j_sha256")
2970     def json = findProperty("i4j_json")
2971     if (output == null || sha256 == null || json == null) {
2972       throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
2973     }
2974     writeDataJsonFile(file(output), file(sha256), file(json))
2975   }
2976 }
2977
2978 task installers {
2979   dependsOn installerFiles
2980 }
2981
2982
2983 spotless {
2984   java {
2985     eclipse().configFile(eclipse_codestyle_file)
2986   }
2987 }
2988
2989 task createSourceReleaseProperties(type: WriteProperties) {
2990   group = "distribution"
2991   description = "Create the source RELEASE properties file"
2992   
2993   def sourceTarBuildDir = "${buildDir}/sourceTar"
2994   def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
2995   outputFile (sourceReleasePropertiesFile)
2996
2997   doFirst {
2998     releaseProps.each{ key, val -> property key, val }
2999     property "git.branch", gitBranch
3000     property "git.hash", gitHash
3001   }
3002
3003   outputs.file(outputFile)
3004 }
3005
3006 task sourceDist(type: Tar) {
3007   group "distribution"
3008   description "Create a source .tar.gz file for distribution"
3009
3010   dependsOn createBuildProperties
3011   dependsOn convertMdFiles
3012   dependsOn eclipseAllPreferences
3013   dependsOn createSourceReleaseProperties
3014
3015
3016   def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
3017   archiveFileName = outputFileName
3018   
3019   compression Compression.GZIP
3020   
3021   into project.name
3022
3023   def EXCLUDE_FILES=[
3024     "build/*",
3025     "bin/*",
3026     "test-output/",
3027     "test-reports",
3028     "tests",
3029     "clover*/*",
3030     ".*",
3031     "benchmarking/*",
3032     "**/.*",
3033     "*.class",
3034     "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
3035     "*locales/**",
3036     "utils/InstallAnywhere",
3037     "**/*.log",
3038     "RELEASE",
3039   ] 
3040   def PROCESS_FILES=[
3041     "AUTHORS",
3042     "CITATION",
3043     "FEATURETODO",
3044     "JAVA-11-README",
3045     "FEATURETODO",
3046     "LICENSE",
3047     "**/README",
3048     "THIRDPARTYLIBS",
3049     "TESTNG",
3050     "build.gradle",
3051     "gradle.properties",
3052     "**/*.java",
3053     "**/*.html",
3054     "**/*.xml",
3055     "**/*.gradle",
3056     "**/*.groovy",
3057     "**/*.properties",
3058     "**/*.perl",
3059     "**/*.sh",
3060   ]
3061   def INCLUDE_FILES=[
3062     ".classpath",
3063     ".settings/org.eclipse.buildship.core.prefs",
3064     ".settings/org.eclipse.jdt.core.prefs"
3065   ]
3066
3067   from(jalviewDir) {
3068     exclude (EXCLUDE_FILES)
3069     include (PROCESS_FILES)
3070     filter(ReplaceTokens,
3071       beginToken: '$$',
3072       endToken: '$$',
3073       tokens: [
3074         'Version-Rel': JALVIEW_VERSION,
3075         'Year-Rel': getDate("yyyy")
3076       ]
3077     )
3078   }
3079   from(jalviewDir) {
3080     exclude (EXCLUDE_FILES)
3081     exclude (PROCESS_FILES)
3082     exclude ("appletlib")
3083     exclude ("**/*locales")
3084     exclude ("*locales/**")
3085     exclude ("utils/InstallAnywhere")
3086
3087     exclude (getdown_files_dir)
3088     // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
3089     //exclude (getdown_website_dir)
3090     //exclude (getdown_archive_dir)
3091
3092     // exluding these as not using jars as modules yet
3093     exclude ("${j11modDir}/**/*.jar")
3094   }
3095   from(jalviewDir) {
3096     include(INCLUDE_FILES)
3097   }
3098 //  from (jalviewDir) {
3099 //    // explicit includes for stuff that seemed to not get included
3100 //    include(fileTree("test/**/*."))
3101 //    exclude(EXCLUDE_FILES)
3102 //    exclude(PROCESS_FILES)
3103 //  }
3104
3105   from(file(buildProperties).getParent()) {
3106     include(file(buildProperties).getName())
3107     rename(file(buildProperties).getName(), "build_properties")
3108     filter({ line ->
3109       line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
3110     })
3111   }
3112
3113   def sourceTarBuildDir = "${buildDir}/sourceTar"
3114   from(sourceTarBuildDir) {
3115     // this includes the appended RELEASE properties file
3116   }
3117 }
3118
3119 task dataInstallersJson {
3120   group "website"
3121   description "Create the installers-VERSION.json data file for installer files created"
3122
3123   mustRunAfter installers
3124   mustRunAfter shadowJar
3125   mustRunAfter sourceDist
3126   mustRunAfter getdownArchive
3127
3128   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
3129   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
3130
3131   if (installersOutputTxt.exists()) {
3132     inputs.file(installersOutputTxt)
3133   }
3134   if (install4jCheckSums && installersSha256.exists()) {
3135     inputs.file(installersSha256)
3136   }
3137   [
3138     shadowJar.archiveFile, // executable JAR
3139     getdownVersionLaunchJvl, // version JVL
3140     sourceDist.archiveFile // source TGZ
3141   ].each { fileName ->
3142     if (file(fileName).exists()) {
3143       inputs.file(fileName)
3144     }
3145   }
3146
3147   outputs.file(hugoDataJsonFile)
3148
3149   doFirst {
3150     writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
3151   }
3152 }
3153
3154 task helppages {
3155   group "help"
3156   description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
3157
3158   dependsOn copyHelp
3159   dependsOn pubhtmlhelp
3160   
3161   inputs.dir("${helpBuildDir}/${help_dir}")
3162   outputs.dir("${buildDir}/distributions/${help_dir}")
3163 }
3164
3165
3166 task j2sSetHeadlessBuild {
3167   doFirst {
3168     IN_ECLIPSE = false
3169   }
3170 }
3171
3172
3173 task jalviewjsEnableAltFileProperty(type: WriteProperties) {
3174   group "jalviewjs"
3175   description "Enable the alternative J2S Config file for headless build"
3176
3177   outputFile = jalviewjsJ2sSettingsFileName
3178   def j2sPropsFile = file(jalviewjsJ2sSettingsFileName)
3179   def j2sProps = new Properties()
3180   if (j2sPropsFile.exists()) {
3181     try {
3182       def j2sPropsFileFIS = new FileInputStream(j2sPropsFile)
3183       j2sProps.load(j2sPropsFileFIS)
3184       j2sPropsFileFIS.close()
3185
3186       j2sProps.each { prop, val ->
3187         property(prop, val)
3188       }
3189     } catch (Exception e) {
3190       println("Exception reading ${jalviewjsJ2sSettingsFileName}")
3191       e.printStackTrace()
3192     }
3193   }
3194   if (! j2sProps.stringPropertyNames().contains(jalviewjs_j2s_alt_file_property_config)) {
3195     property(jalviewjs_j2s_alt_file_property_config, jalviewjs_j2s_alt_file_property)
3196   }
3197 }
3198
3199
3200 task jalviewjsSetEclipseWorkspace {
3201   def propKey = "jalviewjs_eclipse_workspace"
3202   def propVal = null
3203   if (project.hasProperty(propKey)) {
3204     propVal = project.getProperty(propKey)
3205     if (propVal.startsWith("~/")) {
3206       propVal = System.getProperty("user.home") + propVal.substring(1)
3207     }
3208   }
3209   def propsFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_workspace_location_file}"
3210   def propsFile = file(propsFileName)
3211   def eclipseWsDir = propVal
3212   def props = new Properties()
3213
3214   def writeProps = true
3215   if (( eclipseWsDir == null || !file(eclipseWsDir).exists() ) && propsFile.exists()) {
3216     def ins = new FileInputStream(propsFileName)
3217     props.load(ins)
3218     ins.close()
3219     if (props.getProperty(propKey, null) != null) {
3220       eclipseWsDir = props.getProperty(propKey)
3221       writeProps = false
3222     }
3223   }
3224
3225   if (eclipseWsDir == null || !file(eclipseWsDir).exists()) {
3226     def tempDir = File.createTempDir()
3227     eclipseWsDir = tempDir.getAbsolutePath()
3228     writeProps = true
3229   }
3230   eclipseWorkspace = file(eclipseWsDir)
3231
3232   doFirst {
3233     // do not run a headless transpile when we claim to be in Eclipse
3234     if (IN_ECLIPSE) {
3235       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3236       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3237     } else {
3238       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3239     }
3240
3241     if (writeProps) {
3242       props.setProperty(propKey, eclipseWsDir)
3243       propsFile.parentFile.mkdirs()
3244       def bytes = new ByteArrayOutputStream()
3245       props.store(bytes, null)
3246       def propertiesString = bytes.toString()
3247       propsFile.text = propertiesString
3248       print("NEW ")
3249     } else {
3250       print("EXISTING ")
3251     }
3252
3253     println("ECLIPSE WORKSPACE: "+eclipseWorkspace.getPath())
3254   }
3255
3256   //inputs.property(propKey, eclipseWsDir) // eclipseWsDir only gets set once this task runs, so will be out-of-date
3257   outputs.file(propsFileName)
3258   outputs.upToDateWhen { eclipseWorkspace.exists() && propsFile.exists() }
3259 }
3260
3261
3262 task jalviewjsEclipsePaths {
3263   def eclipseProduct
3264
3265   def eclipseRoot = jalviewjs_eclipse_root
3266   if (eclipseRoot.startsWith("~/")) {
3267     eclipseRoot = System.getProperty("user.home") + eclipseRoot.substring(1)
3268   }
3269   if (OperatingSystem.current().isMacOsX()) {
3270     eclipseRoot += "/Eclipse.app"
3271     eclipseBinary = "${eclipseRoot}/Contents/MacOS/eclipse"
3272     eclipseProduct = "${eclipseRoot}/Contents/Eclipse/.eclipseproduct"
3273   } else if (OperatingSystem.current().isWindows()) { // check these paths!!
3274     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3275       eclipseRoot += "/eclipse"
3276     }
3277     eclipseBinary = "${eclipseRoot}/eclipse.exe"
3278     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3279   } else { // linux or unix
3280     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3281       eclipseRoot += "/eclipse"
3282 println("eclipseDir exists")
3283     }
3284     eclipseBinary = "${eclipseRoot}/eclipse"
3285     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3286   }
3287
3288   eclipseVersion = "4.13" // default
3289   def assumedVersion = true
3290   if (file(eclipseProduct).exists()) {
3291     def fis = new FileInputStream(eclipseProduct)
3292     def props = new Properties()
3293     props.load(fis)
3294     eclipseVersion = props.getProperty("version")
3295     fis.close()
3296     assumedVersion = false
3297   }
3298   
3299   def propKey = "eclipse_debug"
3300   eclipseDebug = (project.hasProperty(propKey) && project.getProperty(propKey).equals("true"))
3301
3302   doFirst {
3303     // do not run a headless transpile when we claim to be in Eclipse
3304     if (IN_ECLIPSE) {
3305       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3306       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3307     } else {
3308       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3309     }
3310
3311     if (!assumedVersion) {
3312       println("ECLIPSE VERSION=${eclipseVersion}")
3313     }
3314   }
3315 }
3316
3317
3318 task printProperties {
3319   group "Debug"
3320   description "Output to console all System.properties"
3321   doFirst {
3322     System.properties.each { key, val -> System.out.println("Property: ${key}=${val}") }
3323   }
3324 }
3325
3326
3327 task eclipseSetup {
3328   dependsOn eclipseProject
3329   dependsOn eclipseClasspath
3330   dependsOn eclipseJdt
3331 }
3332
3333
3334 // this version (type: Copy) will delete anything in the eclipse dropins folder that isn't in fromDropinsDir
3335 task jalviewjsEclipseCopyDropins(type: Copy) {
3336   dependsOn jalviewjsEclipsePaths
3337
3338   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_eclipse_dropins_dir}", include: "*.jar")
3339   inputFiles += file("${jalviewDir}/${jalviewjsJ2sPlugin}")
3340   def outputDir = "${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}"
3341
3342   from inputFiles
3343   into outputDir
3344 }
3345
3346
3347 // this eclipse -clean doesn't actually work
3348 task jalviewjsCleanEclipse(type: Exec) {
3349   dependsOn eclipseSetup
3350   dependsOn jalviewjsEclipsePaths
3351   dependsOn jalviewjsEclipseCopyDropins
3352
3353   executable(eclipseBinary)
3354   args(["-nosplash", "--launcher.suppressErrors", "-data", eclipseWorkspace.getPath(), "-clean", "-console", "-consoleLog"])
3355   if (eclipseDebug) {
3356     args += "-debug"
3357   }
3358   args += "-l"
3359
3360   def inputString = """exit
3361 y
3362 """
3363   def inputByteStream = new ByteArrayInputStream(inputString.getBytes())
3364   standardInput = inputByteStream
3365 }
3366
3367 /* not really working yet
3368 jalviewjsEclipseCopyDropins.finalizedBy jalviewjsCleanEclipse
3369 */
3370
3371
3372 task jalviewjsTransferUnzipSwingJs {
3373   def file_zip = "${jalviewDir}/${jalviewjs_swingjs_zip}"
3374
3375   doLast {
3376     copy {
3377       from zipTree(file_zip)
3378       into "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3379     }
3380   }
3381
3382   inputs.file file_zip
3383   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3384 }
3385
3386
3387 task jalviewjsTransferUnzipLib {
3388   def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip")
3389
3390   doLast {
3391     zipFiles.each { file_zip -> 
3392       copy {
3393         from zipTree(file_zip)
3394         into "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3395       }
3396     }
3397   }
3398
3399   inputs.files zipFiles
3400   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3401 }
3402
3403
3404 task jalviewjsTransferUnzipAllLibs {
3405   dependsOn jalviewjsTransferUnzipSwingJs
3406   dependsOn jalviewjsTransferUnzipLib
3407 }
3408
3409
3410 task jalviewjsCreateJ2sSettings(type: WriteProperties) {
3411   group "JalviewJS"
3412   description "Create the alternative j2s file from the j2s.* properties"
3413
3414   jalviewjsJ2sProps = project.properties.findAll { it.key.startsWith("j2s.") }.sort { it.key }
3415   def siteDirProperty = "j2s.site.directory"
3416   def setSiteDir = false
3417   jalviewjsJ2sProps.each { prop, val ->
3418     if (val != null) {
3419       if (prop == siteDirProperty) {
3420         if (!(val.startsWith('/') || val.startsWith("file://") )) {
3421           val = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${val}"
3422         }
3423         setSiteDir = true
3424       }
3425       property(prop,val)
3426     }
3427     if (!setSiteDir) { // default site location, don't override specifically set property
3428       property(siteDirProperty,"${jalviewDirRelativePath}/${jalviewjsTransferSiteJsDir}")
3429     }
3430   }
3431   outputFile = jalviewjsJ2sAltSettingsFileName
3432
3433   if (! IN_ECLIPSE) {
3434     inputs.properties(jalviewjsJ2sProps)
3435     outputs.file(jalviewjsJ2sAltSettingsFileName)
3436   }
3437 }
3438
3439
3440 task jalviewjsEclipseSetup {
3441   dependsOn jalviewjsEclipseCopyDropins
3442   dependsOn jalviewjsSetEclipseWorkspace
3443   dependsOn jalviewjsCreateJ2sSettings
3444 }
3445
3446
3447 task jalviewjsSyncAllLibs (type: Sync) {
3448   dependsOn jalviewjsTransferUnzipAllLibs
3449   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
3450   inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
3451   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3452
3453   from inputFiles
3454   into outputDir
3455   def outputFiles = []
3456   rename { filename ->
3457     outputFiles += "${outputDir}/${filename}"
3458     null
3459   }
3460   preserve {
3461     include "**"
3462   }
3463
3464   // should this be exclude really ?
3465   duplicatesStrategy "INCLUDE"
3466
3467   outputs.files outputFiles
3468   inputs.files inputFiles
3469 }
3470
3471
3472 task jalviewjsSyncResources (type: Sync) {
3473   dependsOn buildResources
3474
3475   def inputFiles = fileTree(dir: resourcesBuildDir)
3476   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3477
3478   from inputFiles
3479   into outputDir
3480   def outputFiles = []
3481   rename { filename ->
3482     outputFiles += "${outputDir}/${filename}"
3483     null
3484   }
3485   preserve {
3486     include "**"
3487   }
3488   outputs.files outputFiles
3489   inputs.files inputFiles
3490 }
3491
3492
3493 task jalviewjsSyncSiteResources (type: Sync) {
3494   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_site_resource_dir}")
3495   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3496
3497   from inputFiles
3498   into outputDir
3499   def outputFiles = []
3500   rename { filename ->
3501     outputFiles += "${outputDir}/${filename}"
3502     null
3503   }
3504   preserve {
3505     include "**"
3506   }
3507   outputs.files outputFiles
3508   inputs.files inputFiles
3509 }
3510
3511
3512 task jalviewjsSyncBuildProperties (type: Sync) {
3513   dependsOn createBuildProperties
3514   def inputFiles = [file(buildProperties)]
3515   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3516
3517   from inputFiles
3518   into outputDir
3519   def outputFiles = []
3520   rename { filename ->
3521     outputFiles += "${outputDir}/${filename}"
3522     null
3523   }
3524   preserve {
3525     include "**"
3526   }
3527   outputs.files outputFiles
3528   inputs.files inputFiles
3529 }
3530
3531
3532 task jalviewjsProjectImport(type: Exec) {
3533   dependsOn eclipseSetup
3534   dependsOn jalviewjsEclipsePaths
3535   dependsOn jalviewjsEclipseSetup
3536
3537   doFirst {
3538     // do not run a headless import when we claim to be in Eclipse
3539     if (IN_ECLIPSE) {
3540       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3541       throw new StopExecutionException("Not running headless import whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3542     } else {
3543       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3544     }
3545   }
3546
3547   //def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview/org.eclipse.jdt.core"
3548   def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview"
3549   executable(eclipseBinary)
3550   args(["-nosplash", "--launcher.suppressErrors", "-application", "com.seeq.eclipse.importprojects.headlessimport", "-data", eclipseWorkspace.getPath(), "-import", jalviewDirAbsolutePath])
3551   if (eclipseDebug) {
3552     args += "-debug"
3553   }
3554   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3555   if (!IN_ECLIPSE) {
3556     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3557     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3558   }
3559
3560   inputs.file("${jalviewDir}/.project")
3561   outputs.upToDateWhen { 
3562     file(projdir).exists()
3563   }
3564 }
3565
3566
3567 task jalviewjsTranspile(type: Exec) {
3568   dependsOn jalviewjsEclipseSetup 
3569   dependsOn jalviewjsProjectImport
3570   dependsOn jalviewjsEclipsePaths
3571   if (!IN_ECLIPSE) {
3572     dependsOn jalviewjsEnableAltFileProperty
3573   }
3574
3575   doFirst {
3576     // do not run a headless transpile when we claim to be in Eclipse
3577     if (IN_ECLIPSE) {
3578       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3579       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3580     } else {
3581       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3582     }
3583   }
3584
3585   executable(eclipseBinary)
3586   args(["-nosplash", "--launcher.suppressErrors", "-application", "org.eclipse.jdt.apt.core.aptBuild", "-data", eclipseWorkspace, "-${jalviewjs_eclipse_build_arg}", eclipse_project_name ])
3587   if (eclipseDebug) {
3588     args += "-debug"
3589   }
3590   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3591   if (!IN_ECLIPSE) {
3592     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3593     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3594   }
3595
3596   def stdout
3597   def stderr
3598   doFirst {
3599     stdout = new ByteArrayOutputStream()
3600     stderr = new ByteArrayOutputStream()
3601
3602     def logOutFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}"
3603     def logOutFile = file(logOutFileName)
3604     logOutFile.createNewFile()
3605     logOutFile.text = """ROOT: ${jalviewjs_eclipse_root}
3606 BINARY: ${eclipseBinary}
3607 VERSION: ${eclipseVersion}
3608 WORKSPACE: ${eclipseWorkspace}
3609 DEBUG: ${eclipseDebug}
3610 ----
3611 """
3612     def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3613     // combine stdout and stderr
3614     def logErrFOS = logOutFOS
3615
3616     if (jalviewjs_j2s_to_console.equals("true")) {
3617       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3618         new org.apache.tools.ant.util.TeeOutputStream(
3619           logOutFOS,
3620           stdout),
3621         System.out)
3622       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3623         new org.apache.tools.ant.util.TeeOutputStream(
3624           logErrFOS,
3625           stderr),
3626         System.err)
3627     } else {
3628       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3629         logOutFOS,
3630         stdout)
3631       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3632         logErrFOS,
3633         stderr)
3634     }
3635   }
3636
3637   doLast {
3638     if (stdout.toString().contains("Error processing ")) {
3639       // j2s did not complete transpile
3640       //throw new TaskExecutionException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3641       if (jalviewjs_ignore_transpile_errors.equals("true")) {
3642         println("IGNORING TRANSPILE ERRORS")
3643         println("See eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3644       } else {
3645         throw new GradleException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3646       }
3647     }
3648   }
3649
3650   inputs.dir("${jalviewDir}/${sourceDir}")
3651   outputs.dir("${jalviewDir}/${jalviewjsTransferSiteJsDir}")
3652   outputs.upToDateWhen( { file("${jalviewDir}/${jalviewjsTransferSiteJsDir}${jalviewjs_server_resource}").exists() } )
3653 }
3654
3655
3656 def jalviewjsCallCore(String name, FileCollection list, String prefixFile, String suffixFile, String jsfile, String zjsfile, File logOutFile, Boolean logOutConsole) {
3657
3658   def stdout = new ByteArrayOutputStream()
3659   def stderr = new ByteArrayOutputStream()
3660
3661   def coreFile = file(jsfile)
3662   def msg = ""
3663   msg = "Creating core for ${name}...\nGenerating ${jsfile}"
3664   println(msg)
3665   logOutFile.createNewFile()
3666   logOutFile.append(msg+"\n")
3667
3668   def coreTop = file(prefixFile)
3669   def coreBottom = file(suffixFile)
3670   coreFile.getParentFile().mkdirs()
3671   coreFile.createNewFile()
3672   coreFile.write( coreTop.getText("UTF-8") )
3673   list.each {
3674     f ->
3675     if (f.exists()) {
3676       def t = f.getText("UTF-8")
3677       t.replaceAll("Clazz\\.([^_])","Clazz_${1}")
3678       coreFile.append( t )
3679     } else {
3680       msg = "...file '"+f.getPath()+"' does not exist, skipping"
3681       println(msg)
3682       logOutFile.append(msg+"\n")
3683     }
3684   }
3685   coreFile.append( coreBottom.getText("UTF-8") )
3686
3687   msg = "Generating ${zjsfile}"
3688   println(msg)
3689   logOutFile.append(msg+"\n")
3690   def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3691   def logErrFOS = logOutFOS
3692
3693   javaexec {
3694     classpath = files(["${jalviewDir}/${jalviewjs_closure_compiler}"])
3695     main = "com.google.javascript.jscomp.CommandLineRunner"
3696     jvmArgs = [ "-Dfile.encoding=UTF-8" ]
3697     args = [ "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
3698     maxHeapSize = "2g"
3699
3700     msg = "\nRunning '"+commandLine.join(' ')+"'\n"
3701     println(msg)
3702     logOutFile.append(msg+"\n")
3703
3704     if (logOutConsole) {
3705       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3706         new org.apache.tools.ant.util.TeeOutputStream(
3707           logOutFOS,
3708           stdout),
3709         standardOutput)
3710         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3711           new org.apache.tools.ant.util.TeeOutputStream(
3712             logErrFOS,
3713             stderr),
3714           System.err)
3715     } else {
3716       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3717         logOutFOS,
3718         stdout)
3719         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3720           logErrFOS,
3721           stderr)
3722     }
3723   }
3724   msg = "--"
3725   println(msg)
3726   logOutFile.append(msg+"\n")
3727 }
3728
3729
3730 task jalviewjsBuildAllCores {
3731   group "JalviewJS"
3732   description "Build the core js lib closures listed in the classlists dir"
3733   dependsOn jalviewjsTranspile
3734   dependsOn jalviewjsTransferUnzipSwingJs
3735
3736   def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${jalviewjs_j2s_subdir}"
3737   def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_j2s_subdir}"
3738   def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteLibDir}/${jalviewjs_j2s_subdir}"
3739   def jsDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_js_subdir}"
3740   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}/${jalviewjs_j2s_subdir}/core"
3741   def prefixFile = "${jsDir}/core/coretop2.js"
3742   def suffixFile = "${jsDir}/core/corebottom2.js"
3743
3744   inputs.file prefixFile
3745   inputs.file suffixFile
3746
3747   def classlistFiles = []
3748   // add the classlists found int the jalviewjs_classlists_dir
3749   fileTree(dir: "${jalviewDir}/${jalviewjs_classlists_dir}", include: "*.txt").each {
3750     file ->
3751     def name = file.getName() - ".txt"
3752     classlistFiles += [
3753       'file': file,
3754       'name': name
3755     ]
3756   }
3757
3758   // _jmol and _jalview cores. Add any other peculiar classlist.txt files here
3759   //classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jmol}"), 'name': "_jvjmol" ]
3760   classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jalview}"), 'name': jalviewjsJalviewCoreName ]
3761
3762   jalviewjsCoreClasslists = []
3763
3764   classlistFiles.each {
3765     hash ->
3766
3767     def file = hash['file']
3768     if (! file.exists()) {
3769       //println("...classlist file '"+file.getPath()+"' does not exist, skipping")
3770       return false // this is a "continue" in groovy .each closure
3771     }
3772     def name = hash['name']
3773     if (name == null) {
3774       name = file.getName() - ".txt"
3775     }
3776
3777     def filelist = []
3778     file.eachLine {
3779       line ->
3780         filelist += line
3781     }
3782     def list = fileTree(dir: j2sDir, includes: filelist)
3783
3784     def jsfile = "${outputDir}/core${name}.js"
3785     def zjsfile = "${outputDir}/core${name}.z.js"
3786
3787     jalviewjsCoreClasslists += [
3788       'jsfile': jsfile,
3789       'zjsfile': zjsfile,
3790       'list': list,
3791       'name': name
3792     ]
3793
3794     inputs.file(file)
3795     inputs.files(list)
3796     outputs.file(jsfile)
3797     outputs.file(zjsfile)
3798   }
3799   
3800   // _stevesoft core. add any cores without a classlist here (and the inputs and outputs)
3801   def stevesoftClasslistName = "_stevesoft"
3802   def stevesoftClasslist = [
3803     'jsfile': "${outputDir}/core${stevesoftClasslistName}.js",
3804     'zjsfile': "${outputDir}/core${stevesoftClasslistName}.z.js",
3805     'list': fileTree(dir: j2sDir, include: "com/stevesoft/pat/**/*.js"),
3806     'name': stevesoftClasslistName
3807   ]
3808   jalviewjsCoreClasslists += stevesoftClasslist
3809   inputs.files(stevesoftClasslist['list'])
3810   outputs.file(stevesoftClasslist['jsfile'])
3811   outputs.file(stevesoftClasslist['zjsfile'])
3812
3813   // _all core
3814   def allClasslistName = "_all"
3815   def allJsFiles = fileTree(dir: j2sDir, include: "**/*.js")
3816   allJsFiles += fileTree(
3817     dir: libJ2sDir,
3818     include: "**/*.js",
3819     excludes: [
3820       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3821       "**/org/jmol/jvxl/readers/IsoIntersectFileReader.js",
3822       "**/org/jmol/export/JSExporter.js"
3823     ]
3824   )
3825   allJsFiles += fileTree(
3826     dir: swingJ2sDir,
3827     include: "**/*.js",
3828     excludes: [
3829       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3830       "**/sun/misc/Unsafe.js",
3831       "**/swingjs/jquery/jquery-editable-select.js",
3832       "**/swingjs/jquery/j2sComboBox.js",
3833       "**/sun/misc/FloatingDecimal.js"
3834     ]
3835   )
3836   def allClasslist = [
3837     'jsfile': "${outputDir}/core${allClasslistName}.js",
3838     'zjsfile': "${outputDir}/core${allClasslistName}.z.js",
3839     'list': allJsFiles,
3840     'name': allClasslistName
3841   ]
3842   // not including this version of "all" core at the moment
3843   //jalviewjsCoreClasslists += allClasslist
3844   inputs.files(allClasslist['list'])
3845   outputs.file(allClasslist['jsfile'])
3846   outputs.file(allClasslist['zjsfile'])
3847
3848   doFirst {
3849     def logOutFile = file("${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_closure_stdout}")
3850     logOutFile.getParentFile().mkdirs()
3851     logOutFile.createNewFile()
3852     logOutFile.write(getDate("yyyy-MM-dd HH:mm:ss")+" jalviewjsBuildAllCores\n----\n")
3853
3854     jalviewjsCoreClasslists.each {
3855       jalviewjsCallCore(it.name, it.list, prefixFile, suffixFile, it.jsfile, it.zjsfile, logOutFile, jalviewjs_j2s_to_console.equals("true"))
3856     }
3857   }
3858
3859 }
3860
3861
3862 def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inputFile, String outputFile) {
3863   copy {
3864     from inputFile
3865     into file(outputFile).getParentFile()
3866     rename { filename ->
3867       if (filename.equals(inputFile.getName())) {
3868         return file(outputFile).getName()
3869       }
3870       return null
3871     }
3872     filter(ReplaceTokens,
3873       beginToken: '_',
3874       endToken: '_',
3875       tokens: [
3876         'MAIN': '"'+main_class+'"',
3877         'CODE': "null",
3878         'NAME': jalviewjsJalviewTemplateName+" [core ${coreName}]",
3879         'COREKEY': jalviewjs_core_key,
3880         'CORENAME': coreName
3881       ]
3882     )
3883   }
3884 }
3885
3886
3887 task jalviewjsPublishCoreTemplates {
3888   dependsOn jalviewjsBuildAllCores
3889   def inputFileName = "${jalviewDir}/${j2s_coretemplate_html}"
3890   def inputFile = file(inputFileName)
3891   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3892
3893   def outputFiles = []
3894   jalviewjsCoreClasslists.each { cl ->
3895     def outputFile = "${outputDir}/${jalviewjsJalviewTemplateName}_${cl.name}.html"
3896     cl['outputfile'] = outputFile
3897     outputFiles += outputFile
3898   }
3899
3900   doFirst {
3901     jalviewjsCoreClasslists.each { cl ->
3902       jalviewjsPublishCoreTemplate(cl.name, jalviewjsJalviewTemplateName, inputFile, cl.outputfile)
3903     }
3904   }
3905   inputs.file(inputFile)
3906   outputs.files(outputFiles)
3907 }
3908
3909
3910 task jalviewjsSyncCore (type: Sync) {
3911   dependsOn jalviewjsBuildAllCores
3912   dependsOn jalviewjsPublishCoreTemplates
3913   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteCoreDir}")
3914   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3915
3916   from inputFiles
3917   into outputDir
3918   def outputFiles = []
3919   rename { filename ->
3920     outputFiles += "${outputDir}/${filename}"
3921     null
3922   }
3923   preserve {
3924     include "**"
3925   }
3926   outputs.files outputFiles
3927   inputs.files inputFiles
3928 }
3929
3930
3931 // this Copy version of TransferSiteJs will delete anything else in the target dir
3932 task jalviewjsCopyTransferSiteJs(type: Copy) {
3933   dependsOn jalviewjsTranspile
3934   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3935   into "${jalviewDir}/${jalviewjsSiteDir}"
3936 }
3937
3938
3939 // this Sync version of TransferSite is used by buildship to keep the website automatically up to date when a file changes
3940 task jalviewjsSyncTransferSiteJs(type: Sync) {
3941   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3942   include "**/*.*"
3943   into "${jalviewDir}/${jalviewjsSiteDir}"
3944   preserve {
3945     include "**"
3946   }
3947 }
3948
3949
3950 jalviewjsSyncAllLibs.mustRunAfter jalviewjsCopyTransferSiteJs
3951 jalviewjsSyncResources.mustRunAfter jalviewjsCopyTransferSiteJs
3952 jalviewjsSyncSiteResources.mustRunAfter jalviewjsCopyTransferSiteJs
3953 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsCopyTransferSiteJs
3954
3955 jalviewjsSyncAllLibs.mustRunAfter jalviewjsSyncTransferSiteJs
3956 jalviewjsSyncResources.mustRunAfter jalviewjsSyncTransferSiteJs
3957 jalviewjsSyncSiteResources.mustRunAfter jalviewjsSyncTransferSiteJs
3958 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsSyncTransferSiteJs
3959
3960
3961 task jalviewjsPrepareSite {
3962   group "JalviewJS"
3963   description "Prepares the website folder including unzipping files and copying resources"
3964   dependsOn jalviewjsSyncAllLibs
3965   dependsOn jalviewjsSyncResources
3966   dependsOn jalviewjsSyncSiteResources
3967   dependsOn jalviewjsSyncBuildProperties
3968   dependsOn jalviewjsSyncCore
3969 }
3970
3971
3972 task jalviewjsBuildSite {
3973   group "JalviewJS"
3974   description "Builds the whole website including transpiled code"
3975   dependsOn jalviewjsCopyTransferSiteJs
3976   dependsOn jalviewjsPrepareSite
3977 }
3978
3979
3980 task cleanJalviewjsTransferSite {
3981   doFirst {
3982     delete "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3983     delete "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3984     delete "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3985     delete "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3986   }
3987 }
3988
3989
3990 task cleanJalviewjsSite {
3991   dependsOn cleanJalviewjsTransferSite
3992   doFirst {
3993     delete "${jalviewDir}/${jalviewjsSiteDir}"
3994   }
3995 }
3996
3997
3998 task jalviewjsSiteTar(type: Tar) {
3999   group "JalviewJS"
4000   description "Creates a tar.gz file for the website"
4001   dependsOn jalviewjsBuildSite
4002   def outputFilename = "jalviewjs-site-${JALVIEW_VERSION}.tar.gz"
4003   archiveFileName = outputFilename
4004
4005   compression Compression.GZIP
4006
4007   from "${jalviewDir}/${jalviewjsSiteDir}"
4008   into jalviewjs_site_dir // this is inside the tar file
4009
4010   inputs.dir("${jalviewDir}/${jalviewjsSiteDir}")
4011 }
4012
4013
4014 task jalviewjsServer {
4015   group "JalviewJS"
4016   def filename = "jalviewjsTest.html"
4017   description "Starts a webserver on localhost to test the website. See ${filename} to access local site on most recently used port."
4018   def htmlFile = "${jalviewDirAbsolutePath}/${filename}"
4019   doLast {
4020
4021     def factory
4022     try {
4023       def f = Class.forName("org.gradle.plugins.javascript.envjs.http.simple.SimpleHttpFileServerFactory")
4024       factory = f.newInstance()
4025     } catch (ClassNotFoundException e) {
4026       throw new GradleException("Unable to create SimpleHttpFileServerFactory")
4027     }
4028     def port = Integer.valueOf(jalviewjs_server_port)
4029     def start = port
4030     def running = false
4031     def url
4032     def jalviewjsServer
4033     while(port < start+1000 && !running) {
4034       try {
4035         def doc_root = new File("${jalviewDirAbsolutePath}/${jalviewjsSiteDir}")
4036         jalviewjsServer = factory.start(doc_root, port)
4037         running = true
4038         url = jalviewjsServer.getResourceUrl(jalviewjs_server_resource)
4039         println("SERVER STARTED with document root ${doc_root}.")
4040         println("Go to "+url+" . Run  gradle --stop  to stop (kills all gradle daemons).")
4041         println("For debug: "+url+"?j2sdebug")
4042         println("For verbose: "+url+"?j2sverbose")
4043       } catch (Exception e) {
4044         port++;
4045       }
4046     }
4047     def htmlText = """
4048       <p><a href="${url}">JalviewJS Test. &lt;${url}&gt;</a></p>
4049       <p><a href="${url}?j2sdebug">JalviewJS Test with debug. &lt;${url}?j2sdebug&gt;</a></p>
4050       <p><a href="${url}?j2sverbose">JalviewJS Test with verbose. &lt;${url}?j2sdebug&gt;</a></p>
4051       """
4052     jalviewjsCoreClasslists.each { cl ->
4053       def urlcore = jalviewjsServer.getResourceUrl(file(cl.outputfile).getName())
4054       htmlText += """
4055       <p><a href="${urlcore}">${jalviewjsJalviewTemplateName} [core ${cl.name}]. &lt;${urlcore}&gt;</a></p>
4056       """
4057       println("For core ${cl.name}: "+urlcore)
4058     }
4059
4060     file(htmlFile).text = htmlText
4061   }
4062
4063   outputs.file(htmlFile)
4064   outputs.upToDateWhen({false})
4065 }
4066
4067
4068 task cleanJalviewjsAll {
4069   group "JalviewJS"
4070   description "Delete all configuration and build artifacts to do with JalviewJS build"
4071   dependsOn cleanJalviewjsSite
4072   dependsOn jalviewjsEclipsePaths
4073   
4074   doFirst {
4075     delete "${jalviewDir}/${jalviewjsBuildDir}"
4076     delete "${jalviewDir}/${eclipse_bin_dir}"
4077     if (eclipseWorkspace != null && file(eclipseWorkspace.getAbsolutePath()+"/.metadata").exists()) {
4078       delete file(eclipseWorkspace.getAbsolutePath()+"/.metadata")
4079     }
4080     delete jalviewjsJ2sAltSettingsFileName
4081   }
4082
4083   outputs.upToDateWhen( { false } )
4084 }
4085
4086
4087 task jalviewjsIDE_checkJ2sPlugin {
4088   group "00 JalviewJS in Eclipse"
4089   description "Compare the swingjs/net.sf.j2s.core(-j11)?.jar file with the Eclipse IDE's plugin version (found in the 'dropins' dir)"
4090
4091   doFirst {
4092     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4093     def j2sPluginFile = file(j2sPlugin)
4094     def eclipseHome = System.properties["eclipse.home.location"]
4095     if (eclipseHome == null || ! IN_ECLIPSE) {
4096       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. Skipping J2S Plugin Check.")
4097     }
4098     def eclipseJ2sPluginDirs = [ "${eclipseHome}/dropins" ]
4099     def altPluginsDir = System.properties["org.eclipse.equinox.p2.reconciler.dropins.directory"]
4100     if (altPluginsDir != null && file(altPluginsDir).exists()) {
4101       eclipseJ2sPluginDirs += altPluginsDir
4102     }
4103     def foundPlugin = false
4104     def j2sPluginFileName = j2sPluginFile.getName()
4105     def eclipseJ2sPlugin
4106     def eclipseJ2sPluginFile
4107     eclipseJ2sPluginDirs.any { dir ->
4108       eclipseJ2sPlugin = "${dir}/${j2sPluginFileName}"
4109       eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4110       if (eclipseJ2sPluginFile.exists()) {
4111         foundPlugin = true
4112         return true
4113       }
4114     }
4115     if (!foundPlugin) {
4116       def msg = "Eclipse J2S Plugin is not installed (could not find '${j2sPluginFileName}' in\n"+eclipseJ2sPluginDirs.join("\n")+"\n)\nTry running task jalviewjsIDE_copyJ2sPlugin"
4117       System.err.println(msg)
4118       throw new StopExecutionException(msg)
4119     }
4120
4121     def digest = MessageDigest.getInstance("MD5")
4122
4123     digest.update(j2sPluginFile.text.bytes)
4124     def j2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4125
4126     digest.update(eclipseJ2sPluginFile.text.bytes)
4127     def eclipseJ2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4128      
4129     if (j2sPluginMd5 != eclipseJ2sPluginMd5) {
4130       def msg = "WARNING! Eclipse J2S Plugin '${eclipseJ2sPlugin}' is different to this commit's version '${j2sPlugin}'"
4131       System.err.println(msg)
4132       throw new StopExecutionException(msg)
4133     } else {
4134       def msg = "Eclipse J2S Plugin '${eclipseJ2sPlugin}' is the same as '${j2sPlugin}' (this is good)"
4135       println(msg)
4136     }
4137   }
4138 }
4139
4140 task jalviewjsIDE_copyJ2sPlugin {
4141   group "00 JalviewJS in Eclipse"
4142   description "Copy the swingjs/net.sf.j2s.core(-j11)?.jar file into the Eclipse IDE's 'dropins' dir"
4143
4144   doFirst {
4145     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4146     def j2sPluginFile = file(j2sPlugin)
4147     def eclipseHome = System.properties["eclipse.home.location"]
4148     if (eclipseHome == null || ! IN_ECLIPSE) {
4149       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. NOT copying J2S Plugin.")
4150     }
4151     def eclipseJ2sPlugin = "${eclipseHome}/dropins/${j2sPluginFile.getName()}"
4152     def eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4153     def msg = "WARNING! Copying this commit's j2s plugin '${j2sPlugin}' to Eclipse J2S Plugin '${eclipseJ2sPlugin}'\n* May require an Eclipse restart"
4154     System.err.println(msg)
4155     copy {
4156       from j2sPlugin
4157       eclipseJ2sPluginFile.getParentFile().mkdirs()
4158       into eclipseJ2sPluginFile.getParent()
4159     }
4160   }
4161 }
4162
4163
4164 task jalviewjsIDE_j2sFile {
4165   group "00 JalviewJS in Eclipse"
4166   description "Creates the .j2s file"
4167   dependsOn jalviewjsCreateJ2sSettings
4168 }
4169
4170
4171 task jalviewjsIDE_SyncCore {
4172   group "00 JalviewJS in Eclipse"
4173   description "Build the core js lib closures listed in the classlists dir and publish core html from template"
4174   dependsOn jalviewjsSyncCore
4175 }
4176
4177
4178 task jalviewjsIDE_SyncSiteAll {
4179   dependsOn jalviewjsSyncAllLibs
4180   dependsOn jalviewjsSyncResources
4181   dependsOn jalviewjsSyncSiteResources
4182   dependsOn jalviewjsSyncBuildProperties
4183 }
4184
4185
4186 cleanJalviewjsTransferSite.mustRunAfter jalviewjsIDE_SyncSiteAll
4187
4188
4189 task jalviewjsIDE_PrepareSite {
4190   group "00 JalviewJS in Eclipse"
4191   description "Sync libs and resources to site dir, but not closure cores"
4192
4193   dependsOn jalviewjsIDE_SyncSiteAll
4194   //dependsOn cleanJalviewjsTransferSite // not sure why this clean is here -- will slow down a re-run of this task
4195 }
4196
4197
4198 task jalviewjsIDE_AssembleSite {
4199   group "00 JalviewJS in Eclipse"
4200   description "Assembles unzipped supporting zipfiles, resources, site resources and closure cores into the Eclipse transpiled site"
4201   dependsOn jalviewjsPrepareSite
4202 }
4203
4204
4205 task jalviewjsIDE_SiteClean {
4206   group "00 JalviewJS in Eclipse"
4207   description "Deletes the Eclipse transpiled site"
4208   dependsOn cleanJalviewjsSite
4209 }
4210
4211
4212 task jalviewjsIDE_Server {
4213   group "00 JalviewJS in Eclipse"
4214   description "Starts a webserver on localhost to test the website"
4215   dependsOn jalviewjsServer
4216 }
4217
4218
4219 // buildship runs this at import or gradle refresh
4220 task eclipseSynchronizationTask {
4221   //dependsOn eclipseSetup
4222   dependsOn createBuildProperties
4223   if (J2S_ENABLED) {
4224     dependsOn jalviewjsIDE_j2sFile
4225     dependsOn jalviewjsIDE_checkJ2sPlugin
4226     dependsOn jalviewjsIDE_PrepareSite
4227   }
4228 }
4229
4230
4231 // buildship runs this at build time or project refresh
4232 task eclipseAutoBuildTask {
4233   //dependsOn jalviewjsIDE_checkJ2sPlugin
4234   //dependsOn jalviewjsIDE_PrepareSite
4235 }
4236
4237
4238 task jalviewjsCopyStderrLaunchFile(type: Copy) {
4239   from file(jalviewjs_stderr_launch)
4240   into jalviewjsSiteDir
4241
4242   inputs.file jalviewjs_stderr_launch
4243   outputs.file jalviewjsStderrLaunchFilename
4244 }
4245
4246 task cleanJalviewjsChromiumUserDir {
4247   doFirst {
4248     delete jalviewjsChromiumUserDir
4249   }
4250   outputs.dir jalviewjsChromiumUserDir
4251   // always run when depended on
4252   outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
4253 }
4254
4255 task jalviewjsChromiumProfile {
4256   dependsOn cleanJalviewjsChromiumUserDir
4257   mustRunAfter cleanJalviewjsChromiumUserDir
4258
4259   def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
4260
4261   doFirst {
4262     mkdir jalviewjsChromiumProfileDir
4263     firstRun.text = ""
4264   }
4265   outputs.file firstRun
4266 }
4267
4268 task jalviewjsLaunchTest {
4269   group "Test"
4270   description "Check JalviewJS opens in a browser"
4271   dependsOn jalviewjsBuildSite
4272   dependsOn jalviewjsCopyStderrLaunchFile
4273   dependsOn jalviewjsChromiumProfile
4274
4275   def macOS = OperatingSystem.current().isMacOsX()
4276   def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary
4277   if (chromiumBinary.startsWith("~/")) {
4278     chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1)
4279   }
4280   
4281   def stdout
4282   def stderr
4283   doFirst {
4284     def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000
4285     
4286     def binary = file(chromiumBinary)
4287     if (!binary.exists()) {
4288       throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
4289     }
4290     stdout = new ByteArrayOutputStream()
4291     stderr = new ByteArrayOutputStream()
4292     def execStdout
4293     def execStderr
4294     if (jalviewjs_j2s_to_console.equals("true")) {
4295       execStdout = new org.apache.tools.ant.util.TeeOutputStream(
4296         stdout,
4297         System.out)
4298       execStderr = new org.apache.tools.ant.util.TeeOutputStream(
4299         stderr,
4300         System.err)
4301     } else {
4302       execStdout = stdout
4303       execStderr = stderr
4304     }
4305     def execArgs = [
4306       "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER
4307       "--headless=new",
4308       "--disable-gpu",
4309       "--timeout=${timeoutms}",
4310       "--virtual-time-budget=${timeoutms}",
4311       "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
4312       "--profile-directory=${jalviewjs_chromium_profile_name}",
4313       "--allow-file-access-from-files",
4314       "--enable-logging=stderr",
4315       "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}"
4316     ]
4317     
4318     if (true || macOS) {
4319       ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
4320       Future f1 = executor.submit(
4321         () -> {
4322           exec {
4323             standardOutput = execStdout
4324             errorOutput = execStderr
4325             executable(chromiumBinary)
4326             args(execArgs)
4327             println "COMMAND: '"+commandLine.join(" ")+"'"
4328           }
4329           executor.shutdownNow()
4330         }
4331       )
4332
4333       def noChangeBytes = 0
4334       def noChangeIterations = 0
4335       executor.scheduleAtFixedRate(
4336         () -> {
4337           String stderrString = stderr.toString()
4338           // shutdown the task if we have a success string
4339           if (stderrString.contains(jalviewjs_desktop_init_string)) {
4340             f1.cancel()
4341             Thread.sleep(1000)
4342             executor.shutdownNow()
4343           }
4344           // if no change in stderr for 10s then also end
4345           if (noChangeIterations >= jalviewjs_chromium_idle_timeout) {
4346             executor.shutdownNow()
4347           }
4348           if (stderrString.length() == noChangeBytes) {
4349             noChangeIterations++
4350           } else {
4351             noChangeBytes = stderrString.length()
4352             noChangeIterations = 0
4353           }
4354         },
4355         1, 1, TimeUnit.SECONDS)
4356
4357       executor.schedule(new Runnable(){
4358         public void run(){
4359           f1.cancel()
4360           executor.shutdownNow()
4361         }
4362       }, timeoutms, TimeUnit.MILLISECONDS)
4363
4364       executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
4365       executor.shutdownNow()
4366     }
4367
4368   }
4369   
4370   doLast {
4371     def found = false
4372     stderr.toString().eachLine { line ->
4373       if (line.contains(jalviewjs_desktop_init_string)) {
4374         println("Found line '"+line+"'")
4375         found = true
4376         return
4377       }
4378     }
4379     if (!found) {
4380       throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
4381     }
4382   }
4383 }
4384   
4385
4386 task jalviewjs {
4387   group "JalviewJS"
4388   description "Build the JalviewJS site and run the launch test"
4389   dependsOn jalviewjsBuildSite
4390   dependsOn jalviewjsLaunchTest
4391 }