Merge branch 'develop' into patch/JAL-4110_stdout_for_tests
[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     showStandardStreams true
1855
1856     info.events = [ TestLogEvent.FAILED ]
1857   }
1858
1859   if (OperatingSystem.current().isMacOsX()) {
1860     testTask.systemProperty "apple.awt.UIElement", "true"
1861     testTask.environment "JAVA_TOOL_OPTIONS", "-Dapple.awt.UIElement=true"
1862   }
1863
1864
1865   ignoreFailures = true // Always try to run all tests for all modules
1866
1867   afterSuite { desc, result ->
1868     if (desc.parent)
1869       return // Only summarize results for whole modules
1870
1871     def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
1872
1873     rootProject.ext.testsResults.add(resultsInfo)
1874   }
1875
1876   // from original test task
1877   maxHeapSize = "1024m"
1878
1879   workingDir = jalviewDir
1880   def testLaf = project.findProperty("test_laf")
1881   if (testLaf != null) {
1882     println("Setting Test LaF to '${testLaf}'")
1883     systemProperty "laf", testLaf
1884   }
1885   def testHiDPIScale = project.findProperty("test_HiDPIScale")
1886   if (testHiDPIScale != null) {
1887     println("Setting Test HiDPI Scale to '${testHiDPIScale}'")
1888     systemProperty "sun.java2d.uiScale", testHiDPIScale
1889   }
1890   sourceCompatibility = compile_source_compatibility
1891   targetCompatibility = compile_target_compatibility
1892   jvmArgs += additional_compiler_args
1893
1894   doFirst {
1895     // this is not perfect yet -- we should only add the commandLineIncludePatterns to the
1896     // testTasks that include the tests, and exclude all from the others.
1897     // get --test argument
1898     filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns
1899     // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the
1900     // commandLineIncludePatterns not matching anything.  Instead we are doing setFailOnNoMatchingTests(false) below
1901
1902
1903     if (useClover) {
1904       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
1905     }
1906   }
1907
1908
1909   /* don't fail on no matching tests (so --tests will run across all testTasks) */
1910   testTask.filter.setFailOnNoMatchingTests(false)
1911
1912   /* ensure the "test" task dependsOn all the testTasks */
1913   test.dependsOn testTask
1914 }
1915
1916 gradle.buildFinished {
1917     def allResults = rootProject.ext.testsResults
1918
1919     if (!allResults.isEmpty()) {
1920         printResults allResults
1921         allResults.each {r ->
1922           if (r[2].resultType == TestResult.ResultType.FAILURE)
1923             throw new GradleException("Failed tests!")
1924         }
1925     }
1926 }
1927
1928 private static String colString(styler, col, colour, text) {
1929   return col?"${styler[colour](text)}":text
1930 }
1931
1932 private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) {
1933   def colour = 'black'
1934   def text = rt
1935   def nocol = false
1936   if (rc == 0) {
1937     text = "-----"
1938     nocol = true
1939   } else {
1940     switch(rt) {
1941       case TestResult.ResultType.SUCCESS:
1942         colour = 'green'
1943         break;
1944       case TestResult.ResultType.FAILURE:
1945         colour = 'red'
1946         break;
1947       default:
1948         nocol = true
1949         break;
1950     }
1951   }
1952   StringBuilder sb = new StringBuilder()
1953   sb.append("${pn}")
1954   if (tn != null)
1955     sb.append(":${tn}")
1956   sb.append(" results: ")
1957   sb.append(colString(s, col && !nocol, colour, text))
1958   sb.append(" (")
1959   sb.append("${rc} tests, ")
1960   sb.append(colString(s, col && rs > 0, 'green', rs))
1961   sb.append(" successes, ")
1962   sb.append(colString(s, col && rf > 0, 'red', rf))
1963   sb.append(" failures, ")
1964   sb.append("${rsk} skipped) in ${t}")
1965   return sb.toString()
1966 }
1967
1968 private static void printResults(allResults) {
1969
1970     // styler from https://stackoverflow.com/a/56139852
1971     def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] }
1972
1973     def maxLength = 0
1974     def failedTests = false
1975     def summaryLines = []
1976     def totalcount = 0
1977     def totalsuccess = 0
1978     def totalfail = 0
1979     def totalskip = 0
1980     def totaltime = TimeCategory.getSeconds(0)
1981     // sort on project name then task name
1982     allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each {
1983       def projectName = it[0]
1984       def taskName = it[1]
1985       def result = it[2]
1986       def time = it[3]
1987       def report = it[4]
1988       def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true)
1989       def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false)
1990       def reportLine = "Report file: ${report}"
1991       def ls = summaryPlain.length()
1992       def lr = reportLine.length()
1993       def m = [ls, lr].max()
1994       if (m > maxLength)
1995         maxLength = m
1996       def info = [ls, summaryCol, reportLine]
1997       summaryLines.add(info)
1998       failedTests |= result.resultType == TestResult.ResultType.FAILURE
1999       totalcount += result.testCount
2000       totalsuccess += result.successfulTestCount
2001       totalfail += result.failedTestCount
2002       totalskip += result.skippedTestCount
2003       totaltime += time
2004     }
2005     def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true)
2006     def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false)
2007     def tls = totalSummaryPlain.length()
2008     if (tls > maxLength)
2009       maxLength = tls
2010     def info = [tls, totalSummaryCol, null]
2011     summaryLines.add(info)
2012
2013     def allSummaries = []
2014     for(sInfo : summaryLines) {
2015       def ls = sInfo[0]
2016       def summary = sInfo[1]
2017       def report = sInfo[2]
2018
2019       StringBuilder sb = new StringBuilder()
2020       sb.append("│" + summary + " " * (maxLength - ls) + "│")
2021       if (report != null) {
2022         sb.append("\n│" + report + " " * (maxLength - report.length()) + "│")
2023       }
2024       allSummaries += sb.toString()
2025     }
2026
2027     println "┌${"${"─" * maxLength}"}┐"
2028     println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n")
2029     println "└${"${"─" * maxLength}"}┘"
2030 }
2031 /* END of test tasks results summary */
2032
2033
2034 task compileLinkCheck(type: JavaCompile) {
2035   options.fork = true
2036   classpath = files("${jalviewDir}/${utils_dir}")
2037   destinationDir = file("${jalviewDir}/${utils_dir}")
2038   source = fileTree(dir: "${jalviewDir}/${utils_dir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])
2039
2040   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2041   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2042   outputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.class")
2043   outputs.file("${jalviewDir}/${utils_dir}/BufferedLineReader.class")
2044 }
2045
2046
2047 task linkCheck(type: JavaExec) {
2048   dependsOn prepare
2049   dependsOn compileLinkCheck
2050
2051   def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
2052   classpath = files("${jalviewDir}/${utils_dir}")
2053   main = "HelpLinksChecker"
2054   workingDir = "${helpBuildDir}"
2055   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
2056
2057   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
2058   standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
2059     outFOS,
2060     System.out)
2061   errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
2062     outFOS,
2063     System.err)
2064
2065   inputs.dir(helpBuildDir)
2066   outputs.file(helpLinksCheckerOutFile)
2067 }
2068
2069
2070 // import the pubhtmlhelp target
2071 ant.properties.basedir = "${jalviewDir}"
2072 ant.properties.helpBuildDir = "${helpBuildDir}/${help_dir}"
2073 ant.importBuild "${utils_dir}/publishHelp.xml"
2074
2075
2076 task cleanPackageDir(type: Delete) {
2077   doFirst {
2078     delete fileTree(dir: "${jalviewDir}/${package_dir}", include: "*.jar")
2079   }
2080 }
2081
2082
2083 jar {
2084   dependsOn prepare
2085   dependsOn linkCheck
2086
2087   manifest {
2088     attributes "Main-Class": main_class,
2089     "Permissions": "all-permissions",
2090     "Application-Name": applicationName,
2091     "Codebase": application_codebase,
2092     "Implementation-Version": JALVIEW_VERSION
2093   }
2094
2095   def outputDir = "${jalviewDir}/${package_dir}"
2096   destinationDirectory = file(outputDir)
2097   archiveFileName = rootProject.name+".jar"
2098   duplicatesStrategy "EXCLUDE"
2099
2100
2101   exclude "cache*/**"
2102   exclude "*.jar"
2103   exclude "*.jar.*"
2104   exclude "**/*.jar"
2105   exclude "**/*.jar.*"
2106
2107   inputs.dir(sourceSets.main.java.outputDir)
2108   sourceSets.main.resources.srcDirs.each{ dir ->
2109     inputs.dir(dir)
2110   }
2111   outputs.file("${outputDir}/${archiveFileName}")
2112 }
2113
2114
2115 task copyJars(type: Copy) {
2116   from fileTree(dir: classesDir, include: "**/*.jar").files
2117   into "${jalviewDir}/${package_dir}"
2118 }
2119
2120
2121 // doing a Sync instead of Copy as Copy doesn't deal with "outputs" very well
2122 task syncJars(type: Sync) {
2123   dependsOn jar
2124   from fileTree(dir: "${jalviewDir}/${libDistDir}", include: "**/*.jar").files
2125   into "${jalviewDir}/${package_dir}"
2126   preserve {
2127     include jar.archiveFileName.getOrNull()
2128   }
2129 }
2130
2131
2132 task makeDist {
2133   group = "build"
2134   description = "Put all required libraries in dist"
2135   // order of "cleanPackageDir", "copyJars", "jar" important!
2136   jar.mustRunAfter cleanPackageDir
2137   syncJars.mustRunAfter cleanPackageDir
2138   dependsOn cleanPackageDir
2139   dependsOn syncJars
2140   dependsOn jar
2141   outputs.dir("${jalviewDir}/${package_dir}")
2142 }
2143
2144
2145 task cleanDist {
2146   dependsOn cleanPackageDir
2147   dependsOn cleanTest
2148   dependsOn clean
2149 }
2150
2151
2152 shadowJar {
2153   group = "distribution"
2154   description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar"
2155   if (buildDist) {
2156     dependsOn makeDist
2157   }
2158   from ("${jalviewDir}/${libDistDir}") {
2159     include("*.jar")
2160   }
2161   manifest {
2162     attributes "Implementation-Version": JALVIEW_VERSION,
2163     "Application-Name": applicationName
2164   }
2165
2166   duplicatesStrategy "INCLUDE"
2167
2168   mainClassName = shadow_jar_main_class
2169   mergeServiceFiles()
2170   classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
2171   minimize()
2172 }
2173
2174 task getdownImagesCopy() {
2175   inputs.dir getdownImagesDir
2176   outputs.dir getdownImagesBuildDir
2177
2178   doFirst {
2179     copy {
2180       from(getdownImagesDir) {
2181         include("*getdown*.png")
2182       }
2183       into getdownImagesBuildDir
2184     }
2185   }
2186 }
2187
2188 task getdownImagesProcess() {
2189   dependsOn getdownImagesCopy
2190
2191   doFirst {
2192     if (backgroundImageText) {
2193       if (convertBinary == null) {
2194         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2195       }
2196       if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
2197         throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2198       }
2199       fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
2200         exec {
2201           executable convertBinary
2202           args = [
2203             file.getPath(),
2204             '-font', getdown_background_image_text_font,
2205             '-fill', getdown_background_image_text_colour,
2206             '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
2207             '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2208             '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2209             file.getPath()
2210           ]
2211         }
2212       }
2213     }
2214   }
2215 }
2216
2217 task getdownImages() {
2218   dependsOn getdownImagesProcess
2219 }
2220
2221 task getdownWebsite() {
2222   group = "distribution"
2223   description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
2224
2225   dependsOn getdownImages
2226   if (buildDist) {
2227     dependsOn makeDist
2228   }
2229
2230   def getdownWebsiteResourceFilenames = []
2231   def getdownResourceDir = getdownResourceDir
2232   def getdownResourceFilenames = []
2233
2234   doFirst {
2235     // clean the getdown website and files dir before creating getdown folders
2236     delete getdownAppBaseDir
2237     delete getdownFilesDir
2238
2239     copy {
2240       from buildProperties
2241       rename(file(buildProperties).getName(), getdown_build_properties)
2242       into getdownAppDir
2243     }
2244     getdownWebsiteResourceFilenames += "${getdownAppDistDir}/${getdown_build_properties}"
2245
2246     copy {
2247       from channelPropsFile
2248       filter(ReplaceTokens,
2249         beginToken: '__',
2250         endToken: '__',
2251         tokens: [
2252           'SUFFIX': channelSuffix
2253         ]
2254       )
2255       into getdownAppBaseDir
2256     }
2257     getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
2258
2259     // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
2260     def props = project.properties.sort { it.key }
2261     if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
2262       props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
2263     }
2264     if (getdownAltJavaMaxVersion != null && getdownAltJavaMaxVersion.length() > 0) {
2265       props.put("getdown_txt_java_max_version", getdownAltJavaMaxVersion)
2266     }
2267     if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
2268       props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
2269     }
2270     if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
2271       props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
2272       props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}")
2273       props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}")
2274       props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}")
2275       props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
2276       props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
2277     }
2278
2279     props.put("getdown_txt_title", jalview_name)
2280     props.put("getdown_txt_ui.name", applicationName)
2281
2282     // start with appbase
2283     getdownTextLines += "appbase = ${getdownAppBase}"
2284     props.each{ prop, val ->
2285       if (prop.startsWith("getdown_txt_") && val != null) {
2286         if (prop.startsWith("getdown_txt_multi_")) {
2287           def key = prop.substring(18)
2288           val.split(",").each{ v ->
2289             def line = "${key} = ${v}"
2290             getdownTextLines += line
2291           }
2292         } else {
2293           // file values rationalised
2294           if (val.indexOf('/') > -1 || prop.startsWith("getdown_txt_resource")) {
2295             def r = null
2296             if (val.indexOf('/') == 0) {
2297               // absolute path
2298               r = file(val)
2299             } else if (val.indexOf('/') > 0) {
2300               // relative path (relative to jalviewDir)
2301               r = file( "${jalviewDir}/${val}" )
2302             }
2303             if (r.exists()) {
2304               val = "${getdown_resource_dir}/" + r.getName()
2305               getdownWebsiteResourceFilenames += val
2306               getdownResourceFilenames += r.getPath()
2307             }
2308           }
2309           if (! prop.startsWith("getdown_txt_resource")) {
2310             def line = prop.substring(12) + " = ${val}"
2311             getdownTextLines += line
2312           }
2313         }
2314       }
2315     }
2316
2317     getdownWebsiteResourceFilenames.each{ filename ->
2318       getdownTextLines += "resource = ${filename}"
2319     }
2320     getdownResourceFilenames.each{ filename ->
2321       copy {
2322         from filename
2323         into getdownResourceDir
2324       }
2325     }
2326     
2327     def getdownWrapperScripts = [ getdown_bash_wrapper_script, getdown_powershell_wrapper_script, getdown_batch_wrapper_script ]
2328     getdownWrapperScripts.each{ script ->
2329       def s = file( "${jalviewDir}/utils/getdown/${getdown_wrapper_script_dir}/${script}" )
2330       if (s.exists()) {
2331         copy {
2332           from s
2333           into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
2334         }
2335         getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
2336       }
2337     }
2338
2339     def codeFiles = []
2340     fileTree(file(package_dir)).each{ f ->
2341       if (f.isDirectory()) {
2342         def files = fileTree(dir: f, include: ["*"]).getFiles()
2343         codeFiles += files
2344       } else if (f.exists()) {
2345         codeFiles += f
2346       }
2347     }
2348     def jalviewJar = jar.archiveFileName.getOrNull()
2349     // put jalview.jar first for CLASSPATH and .properties files reasons
2350     codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
2351       def name = f.getName()
2352       def line = "code = ${getdownAppDistDir}/${name}"
2353       getdownTextLines += line
2354       copy {
2355         from f.getPath()
2356         into getdownAppDir
2357       }
2358     }
2359
2360     // NOT USING MODULES YET, EVERYTHING SHOULD BE IN dist
2361     /*
2362     if (JAVA_VERSION.equals("11")) {
2363     def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
2364     j11libFiles.sort().each{f ->
2365     def name = f.getName()
2366     def line = "code = ${getdown_j11lib_dir}/${name}"
2367     getdownTextLines += line
2368     copy {
2369     from f.getPath()
2370     into getdownJ11libDir
2371     }
2372     }
2373     }
2374      */
2375
2376     // 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.
2377     //getdownTextLines += "class = " + file(getdownLauncher).getName()
2378     getdownTextLines += "resource = ${getdown_launcher_new}"
2379     getdownTextLines += "class = ${main_class}"
2380     // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
2381     if (getdownSetAppBaseProperty) {
2382       getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
2383       getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
2384     }
2385
2386     def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
2387     getdownTxt.write(getdownTextLines.join("\n"))
2388
2389     getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
2390     def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
2391     launchJvl.write("appbase=${getdownAppBase}")
2392
2393     // files going into the getdown website dir: getdown-launcher.jar
2394     copy {
2395       from getdownLauncher
2396       rename(file(getdownLauncher).getName(), getdown_launcher_new)
2397       into getdownAppBaseDir
2398     }
2399
2400     // files going into the getdown website dir: getdown-launcher(-local).jar
2401     copy {
2402       from getdownLauncher
2403       if (file(getdownLauncher).getName() != getdown_launcher) {
2404         rename(file(getdownLauncher).getName(), getdown_launcher)
2405       }
2406       into getdownAppBaseDir
2407     }
2408
2409     // files going into the getdown website dir: ./install dir and files
2410     if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
2411       copy {
2412         from getdownTxt
2413         from getdownLauncher
2414         from "${getdownAppDir}/${getdown_build_properties}"
2415         if (file(getdownLauncher).getName() != getdown_launcher) {
2416           rename(file(getdownLauncher).getName(), getdown_launcher)
2417         }
2418         into getdownInstallDir
2419       }
2420
2421       // and make a copy in the getdown files dir (these are not downloaded by getdown)
2422       copy {
2423         from getdownInstallDir
2424         into getdownFilesInstallDir
2425       }
2426     }
2427
2428     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2429     copy {
2430       from getdownTxt
2431       from launchJvl
2432       from getdownLauncher
2433       from "${getdownAppBaseDir}/${getdown_build_properties}"
2434       from "${getdownAppBaseDir}/${channel_props}"
2435       if (file(getdownLauncher).getName() != getdown_launcher) {
2436         rename(file(getdownLauncher).getName(), getdown_launcher)
2437       }
2438       into getdownFilesDir
2439     }
2440
2441     // and ./resource (not all downloaded by getdown)
2442     copy {
2443       from getdownResourceDir
2444       into "${getdownFilesDir}/${getdown_resource_dir}"
2445     }
2446   }
2447
2448   if (buildDist) {
2449     inputs.dir("${jalviewDir}/${package_dir}")
2450   }
2451   outputs.dir(getdownAppBaseDir)
2452   outputs.dir(getdownFilesDir)
2453 }
2454
2455
2456 // a helper task to allow getdown digest of any dir: `gradle getdownDigestDir -PDIGESTDIR=/path/to/my/random/getdown/dir
2457 task getdownDigestDir(type: JavaExec) {
2458   group "Help"
2459   description "A task to run a getdown Digest on a dir with getdown.txt. Provide a DIGESTDIR property via -PDIGESTDIR=..."
2460
2461   def digestDirPropertyName = "DIGESTDIR"
2462   doFirst {
2463     classpath = files(getdownLauncher)
2464     def digestDir = findProperty(digestDirPropertyName)
2465     if (digestDir == null) {
2466       throw new GradleException("Must provide a DIGESTDIR value to produce an alternative getdown digest")
2467     }
2468     args digestDir
2469   }
2470   main = "com.threerings.getdown.tools.Digester"
2471 }
2472
2473
2474 task getdownDigest(type: JavaExec) {
2475   group = "distribution"
2476   description = "Digest the getdown website folder"
2477   dependsOn getdownWebsite
2478   doFirst {
2479     classpath = files(getdownLauncher)
2480   }
2481   main = "com.threerings.getdown.tools.Digester"
2482   args getdownAppBaseDir
2483   inputs.dir(getdownAppBaseDir)
2484   outputs.file("${getdownAppBaseDir}/digest2.txt")
2485 }
2486
2487
2488 task getdown() {
2489   group = "distribution"
2490   description = "Create the minimal and full getdown app folder for installers and website and create digest file"
2491   dependsOn getdownDigest
2492   doLast {
2493     if (reportRsyncCommand) {
2494       def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
2495       def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
2496       println "LIKELY RSYNC COMMAND:"
2497       println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
2498       if (RUNRSYNC == "true") {
2499         exec {
2500           commandLine "mkdir", "-p", toDir
2501         }
2502         exec {
2503           commandLine "rsync", "-avh", "--delete", fromDir, toDir
2504         }
2505       }
2506     }
2507   }
2508 }
2509
2510
2511 task getdownArchiveBuild() {
2512   group = "distribution"
2513   description = "Put files in the archive dir to go on the website"
2514
2515   dependsOn getdownWebsite
2516
2517   def v = "v${JALVIEW_VERSION_UNDERSCORES}"
2518   def vDir = "${getdownArchiveDir}/${v}"
2519   getdownFullArchiveDir = "${vDir}/getdown"
2520   getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
2521
2522   def vAltDir = "alt_${v}"
2523   def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
2524
2525   doFirst {
2526     // cleanup old "old" dir
2527     delete getdownArchiveDir
2528
2529     def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
2530     getdownArchiveTxt.getParentFile().mkdirs()
2531     def getdownArchiveTextLines = []
2532     def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
2533
2534     // the libdir
2535     copy {
2536       from "${getdownAppBaseDir}/${getdownAppDistDir}"
2537       into "${getdownFullArchiveDir}/${vAltDir}"
2538     }
2539
2540     getdownTextLines.each { line ->
2541       line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
2542       line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
2543       line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
2544       line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
2545       line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
2546       line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
2547       // remove the existing resource = resource/ or bin/ lines
2548       if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
2549         getdownArchiveTextLines += line
2550       }
2551     }
2552
2553     // the resource dir -- add these files as resource lines in getdown.txt
2554     copy {
2555       from "${archiveImagesDir}"
2556       into "${getdownFullArchiveDir}/${getdown_resource_dir}"
2557       eachFile { file ->
2558         getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
2559       }
2560     }
2561
2562     getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
2563
2564     def vLaunchJvl = file(getdownVersionLaunchJvl)
2565     vLaunchJvl.getParentFile().mkdirs()
2566     vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
2567     def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
2568     def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
2569     // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
2570     //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
2571     java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
2572
2573     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2574     copy {
2575       from getdownLauncher
2576       from "${getdownAppBaseDir}/${getdownLaunchJvl}"
2577       from "${getdownAppBaseDir}/${getdown_launcher_new}"
2578       from "${getdownAppBaseDir}/${channel_props}"
2579       if (file(getdownLauncher).getName() != getdown_launcher) {
2580         rename(file(getdownLauncher).getName(), getdown_launcher)
2581       }
2582       into getdownFullArchiveDir
2583     }
2584
2585   }
2586 }
2587
2588 task getdownArchiveDigest(type: JavaExec) {
2589   group = "distribution"
2590   description = "Digest the getdown archive folder"
2591
2592   dependsOn getdownArchiveBuild
2593
2594   doFirst {
2595     classpath = files(getdownLauncher)
2596     args getdownFullArchiveDir
2597   }
2598   main = "com.threerings.getdown.tools.Digester"
2599   inputs.dir(getdownFullArchiveDir)
2600   outputs.file("${getdownFullArchiveDir}/digest2.txt")
2601 }
2602
2603 task getdownArchive() {
2604   group = "distribution"
2605   description = "Build the website archive dir with getdown digest"
2606
2607   dependsOn getdownArchiveBuild
2608   dependsOn getdownArchiveDigest
2609 }
2610
2611 tasks.withType(JavaCompile) {
2612         options.encoding = 'UTF-8'
2613 }
2614
2615
2616 clean {
2617   doFirst {
2618     delete getdownAppBaseDir
2619     delete getdownFilesDir
2620     delete getdownArchiveDir
2621   }
2622 }
2623
2624
2625 install4j {
2626   if (file(install4jHomeDir).exists()) {
2627     // good to go!
2628   } else if (file(System.getProperty("user.home")+"/buildtools/install4j").exists()) {
2629     install4jHomeDir = System.getProperty("user.home")+"/buildtools/install4j"
2630   } else if (file("/Applications/install4j.app/Contents/Resources/app").exists()) {
2631     install4jHomeDir = "/Applications/install4j.app/Contents/Resources/app"
2632   }
2633   installDir(file(install4jHomeDir))
2634
2635   mediaTypes = Arrays.asList(install4j_media_types.split(","))
2636 }
2637
2638
2639 task copyInstall4jTemplate {
2640   def install4jTemplateFile = file("${install4jDir}/${install4j_template}")
2641   def install4jFileAssociationsFile = file("${install4jDir}/${install4j_installer_file_associations}")
2642   inputs.file(install4jTemplateFile)
2643   inputs.file(install4jFileAssociationsFile)
2644   inputs.property("CHANNEL", { CHANNEL })
2645   outputs.file(install4jConfFile)
2646
2647   doLast {
2648     def install4jConfigXml = new XmlParser().parse(install4jTemplateFile)
2649
2650     // turn off code signing if no OSX_KEYPASS
2651     if (OSX_KEYPASS == "") {
2652       install4jConfigXml.'**'.codeSigning.each { codeSigning ->
2653         codeSigning.'@macEnabled' = "false"
2654       }
2655       install4jConfigXml.'**'.windows.each { windows ->
2656         windows.'@runPostProcessor' = "false"
2657       }
2658     }
2659
2660     // disable install screen for OSX dmg (for 2.11.2.0)
2661     install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
2662       macosArchive.attributes().remove('executeSetupApp')
2663       macosArchive.attributes().remove('setupAppId')
2664     }
2665
2666     // turn off checksum creation for LOCAL channel
2667     def e = install4jConfigXml.application[0]
2668     e.'@createChecksums' = string(install4jCheckSums)
2669
2670     // put file association actions where placeholder action is
2671     def install4jFileAssociationsText = install4jFileAssociationsFile.text
2672     def fileAssociationActions = new XmlParser().parseText("<actions>${install4jFileAssociationsText}</actions>")
2673     install4jConfigXml.'**'.action.any { a -> // .any{} stops after the first one that returns true
2674       if (a.'@name' == 'EXTENSIONS_REPLACED_BY_GRADLE') {
2675         def parent = a.parent()
2676         parent.remove(a)
2677         fileAssociationActions.each { faa ->
2678             parent.append(faa)
2679         }
2680         // don't need to continue in .any loop once replacements have been made
2681         return true
2682       }
2683     }
2684
2685     // use Windows Program Group with Examples folder for RELEASE, and Program Group without Examples for everything else
2686     // NB we're deleting the /other/ one!
2687     // Also remove the examples subdir from non-release versions
2688     def customizedIdToDelete = "PROGRAM_GROUP_RELEASE"
2689     // 2.11.1.0 NOT releasing with the Examples folder in the Program Group
2690     if (false && CHANNEL=="RELEASE") { // remove 'false && ' to include Examples folder in RELEASE channel
2691       customizedIdToDelete = "PROGRAM_GROUP_NON_RELEASE"
2692     } else {
2693       // remove the examples subdir from Full File Set
2694       def files = install4jConfigXml.files[0]
2695       def fileset = files.filesets.fileset.find { fs -> fs.'@customizedId' == "FULL_FILE_SET" }
2696       def root = files.roots.root.find { r -> r.'@fileset' == fileset.'@id' }
2697       def mountPoint = files.mountPoints.mountPoint.find { mp -> mp.'@root' == root.'@id' }
2698       def dirEntry = files.entries.dirEntry.find { de -> de.'@mountPoint' == mountPoint.'@id' && de.'@subDirectory' == "examples" }
2699       dirEntry.parent().remove(dirEntry)
2700     }
2701     install4jConfigXml.'**'.action.any { a ->
2702       if (a.'@customizedId' == customizedIdToDelete) {
2703         def parent = a.parent()
2704         parent.remove(a)
2705         return true
2706       }
2707     }
2708
2709     // write install4j file
2710     install4jConfFile.text = XmlUtil.serialize(install4jConfigXml)
2711   }
2712 }
2713
2714
2715 clean {
2716   doFirst {
2717     delete install4jConfFile
2718   }
2719 }
2720
2721 task cleanInstallersDataFiles {
2722   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
2723   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
2724   def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
2725   doFirst {
2726     delete installersOutputTxt
2727     delete installersSha256
2728     delete hugoDataJsonFile
2729   }
2730 }
2731
2732 task install4jDMGBackgroundImageCopy {
2733   inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
2734   outputs.dir "${install4jDMGBackgroundImageBuildDir}"
2735   doFirst {
2736     copy {
2737       from(install4jDMGBackgroundImageDir) {
2738         include(install4jDMGBackgroundImageFile)
2739       }
2740       into install4jDMGBackgroundImageBuildDir
2741     }
2742   }
2743 }
2744
2745 task install4jDMGBackgroundImageProcess {
2746   dependsOn install4jDMGBackgroundImageCopy
2747
2748   doFirst {
2749     if (backgroundImageText) {
2750       if (convertBinary == null) {
2751         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2752       }
2753       if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
2754         throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2755       }
2756       fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
2757         exec {
2758           executable convertBinary
2759           args = [
2760             file.getPath(),
2761             '-font', install4j_background_image_text_font,
2762             '-fill', install4j_background_image_text_colour,
2763             '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
2764             '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2765             '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2766             file.getPath()
2767           ]
2768         }
2769       }
2770     }
2771   }
2772 }
2773
2774 task install4jDMGBackgroundImage {
2775   dependsOn install4jDMGBackgroundImageProcess
2776 }
2777
2778 task installerFiles(type: com.install4j.gradle.Install4jTask) {
2779   group = "distribution"
2780   description = "Create the install4j installers"
2781   dependsOn getdown
2782   dependsOn copyInstall4jTemplate
2783   dependsOn cleanInstallersDataFiles
2784   dependsOn install4jDMGBackgroundImage
2785
2786   projectFile = install4jConfFile
2787
2788   // create an md5 for the input files to use as version for install4j conf file
2789   def digest = MessageDigest.getInstance("MD5")
2790   digest.update(
2791     (file("${install4jDir}/${install4j_template}").text + 
2792     file("${install4jDir}/${install4j_info_plist_file_associations}").text +
2793     file("${install4jDir}/${install4j_installer_file_associations}").text).bytes)
2794   def filesMd5 = new BigInteger(1, digest.digest()).toString(16)
2795   if (filesMd5.length() >= 8) {
2796     filesMd5 = filesMd5.substring(0,8)
2797   }
2798   def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"
2799
2800   variables = [
2801     'JALVIEW_NAME': jalview_name,
2802     'JALVIEW_APPLICATION_NAME': applicationName,
2803     'JALVIEW_DIR': "../..",
2804     'OSX_KEYSTORE': OSX_KEYSTORE,
2805     'OSX_APPLEID': OSX_APPLEID,
2806     'OSX_ALTOOLPASS': OSX_ALTOOLPASS,
2807     'JSIGN_SH': JSIGN_SH,
2808     'JRE_DIR': getdown_app_dir_java,
2809     'INSTALLER_TEMPLATE_VERSION': install4jTemplateVersion,
2810     'JALVIEW_VERSION': JALVIEW_VERSION,
2811     'JAVA_MIN_VERSION': JAVA_MIN_VERSION,
2812     'JAVA_MAX_VERSION': JAVA_MAX_VERSION,
2813     'JAVA_VERSION': JAVA_VERSION,
2814     'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
2815     'VERSION': JALVIEW_VERSION,
2816     'COPYRIGHT_MESSAGE': install4j_copyright_message,
2817     'BUNDLE_ID': install4jBundleId,
2818     'INTERNAL_ID': install4jInternalId,
2819     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
2820     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
2821     'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
2822     'WRAPPER_LINK': getdownWrapperLink,
2823     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
2824     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
2825     'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script,
2826     'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
2827     'INSTALLER_NAME': install4jInstallerName,
2828     'INSTALL4J_UTILS_DIR': install4j_utils_dir,
2829     'GETDOWN_CHANNEL_DIR': getdownChannelDir,
2830     'GETDOWN_FILES_DIR': getdown_files_dir,
2831     'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
2832     'GETDOWN_DIST_DIR': getdownAppDistDir,
2833     'GETDOWN_ALT_DIR': getdown_app_dir_alt,
2834     'GETDOWN_INSTALL_DIR': getdown_install_dir,
2835     'INFO_PLIST_FILE_ASSOCIATIONS_FILE': install4j_info_plist_file_associations,
2836     'BUILD_DIR': install4jBuildDir,
2837     'APPLICATION_CATEGORIES': install4j_application_categories,
2838     'APPLICATION_FOLDER': install4jApplicationFolder,
2839     'UNIX_APPLICATION_FOLDER': install4jUnixApplicationFolder,
2840     'EXECUTABLE_NAME': install4jExecutableName,
2841     'EXTRA_SCHEME': install4jExtraScheme,
2842     'MAC_ICONS_FILE': install4jMacIconsFile,
2843     'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
2844     'PNG_ICON_FILE': install4jPngIconFile,
2845     'BACKGROUND': install4jBackground,
2846   ]
2847
2848   def varNameMap = [
2849     'mac': 'MACOS',
2850     'windows': 'WINDOWS',
2851     'linux': 'LINUX'
2852   ]
2853   
2854   // these are the bundled OS/architecture VMs needed by install4j
2855   def osArch = [
2856     [ "mac", "x64" ],
2857     [ "mac", "aarch64" ],
2858     [ "windows", "x64" ],
2859     [ "linux", "x64" ],
2860     [ "linux", "aarch64" ]
2861   ]
2862   osArch.forEach { os, arch ->
2863     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)
2864     // N.B. For some reason install4j requires the below filename to have underscores and not hyphens
2865     // otherwise running `gradle installers` generates a non-useful error:
2866     // `install4j: compilation failed. Reason: java.lang.NumberFormatException: For input string: "windows"`
2867     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)
2868   }
2869
2870   //println("INSTALL4J VARIABLES:")
2871   //variables.each{k,v->println("${k}=${v}")}
2872
2873   destination = "${jalviewDir}/${install4jBuildDir}"
2874   buildSelected = true
2875
2876   if (install4j_faster.equals("true") || CHANNEL.startsWith("LOCAL")) {
2877     faster = true
2878     disableSigning = true
2879     disableNotarization = true
2880   }
2881
2882   if (OSX_KEYPASS) {
2883     macKeystorePassword = OSX_KEYPASS
2884   } 
2885   
2886   if (OSX_ALTOOLPASS) {
2887     appleIdPassword = OSX_ALTOOLPASS
2888     disableNotarization = false
2889   } else {
2890     disableNotarization = true
2891   }
2892
2893   doFirst {
2894     println("Using projectFile "+projectFile)
2895     if (!disableNotarization) { println("Will notarize OSX App DMG") }
2896   }
2897   //verbose=true
2898
2899   inputs.dir(getdownAppBaseDir)
2900   inputs.file(install4jConfFile)
2901   inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
2902   outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
2903 }
2904
2905 def getDataHash(File myFile) {
2906   HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
2907   return myFile.exists()
2908   ? [
2909       "file" : myFile.getName(),
2910       "filesize" : myFile.length(),
2911       "sha256" : hash.toString()
2912     ]
2913   : null
2914 }
2915
2916 def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
2917   def hash = [
2918     "channel" : getdownChannelName,
2919     "date" : getDate("yyyy-MM-dd HH:mm:ss"),
2920     "git-commit" : "${gitHash} [${gitBranch}]",
2921     "version" : JALVIEW_VERSION
2922   ]
2923   // install4j installer files
2924   if (installersOutputTxt.exists()) {
2925     def idHash = [:]
2926     installersOutputTxt.readLines().each { def line ->
2927       if (line.startsWith("#")) {
2928         return;
2929       }
2930       line.replaceAll("\n","")
2931       def vals = line.split("\t")
2932       def filename = vals[3]
2933       def filesize = file(filename).length()
2934       filename = filename.replaceAll(/^.*\//, "")
2935       hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
2936       idHash."${filename}" = vals[0]
2937     }
2938     if (install4jCheckSums && installersSha256.exists()) {
2939       installersSha256.readLines().each { def line ->
2940         if (line.startsWith("#")) {
2941           return;
2942         }
2943         line.replaceAll("\n","")
2944         def vals = line.split(/\s+\*?/)
2945         def filename = vals[1]
2946         def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
2947       }
2948     }
2949   }
2950
2951   [
2952     "JAR": shadowJar.archiveFile, // executable JAR
2953     "JVL": getdownVersionLaunchJvl, // version JVL
2954     "SOURCE": sourceDist.archiveFile // source TGZ
2955   ].each { key, value ->
2956     def file = file(value)
2957     if (file.exists()) {
2958       def fileHash = getDataHash(file)
2959       if (fileHash != null) {
2960         hash."${key}" = fileHash;
2961       }
2962     }
2963   }
2964   return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
2965 }
2966
2967 task staticMakeInstallersJsonFile {
2968   doFirst {
2969     def output = findProperty("i4j_output")
2970     def sha256 = findProperty("i4j_sha256")
2971     def json = findProperty("i4j_json")
2972     if (output == null || sha256 == null || json == null) {
2973       throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
2974     }
2975     writeDataJsonFile(file(output), file(sha256), file(json))
2976   }
2977 }
2978
2979 task installers {
2980   dependsOn installerFiles
2981 }
2982
2983
2984 spotless {
2985   java {
2986     eclipse().configFile(eclipse_codestyle_file)
2987   }
2988 }
2989
2990 task createSourceReleaseProperties(type: WriteProperties) {
2991   group = "distribution"
2992   description = "Create the source RELEASE properties file"
2993   
2994   def sourceTarBuildDir = "${buildDir}/sourceTar"
2995   def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
2996   outputFile (sourceReleasePropertiesFile)
2997
2998   doFirst {
2999     releaseProps.each{ key, val -> property key, val }
3000     property "git.branch", gitBranch
3001     property "git.hash", gitHash
3002   }
3003
3004   outputs.file(outputFile)
3005 }
3006
3007 task sourceDist(type: Tar) {
3008   group "distribution"
3009   description "Create a source .tar.gz file for distribution"
3010
3011   dependsOn createBuildProperties
3012   dependsOn convertMdFiles
3013   dependsOn eclipseAllPreferences
3014   dependsOn createSourceReleaseProperties
3015
3016
3017   def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
3018   archiveFileName = outputFileName
3019   
3020   compression Compression.GZIP
3021   
3022   into project.name
3023
3024   def EXCLUDE_FILES=[
3025     "build/*",
3026     "bin/*",
3027     "test-output/",
3028     "test-reports",
3029     "tests",
3030     "clover*/*",
3031     ".*",
3032     "benchmarking/*",
3033     "**/.*",
3034     "*.class",
3035     "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
3036     "*locales/**",
3037     "utils/InstallAnywhere",
3038     "**/*.log",
3039     "RELEASE",
3040   ] 
3041   def PROCESS_FILES=[
3042     "AUTHORS",
3043     "CITATION",
3044     "FEATURETODO",
3045     "JAVA-11-README",
3046     "FEATURETODO",
3047     "LICENSE",
3048     "**/README",
3049     "THIRDPARTYLIBS",
3050     "TESTNG",
3051     "build.gradle",
3052     "gradle.properties",
3053     "**/*.java",
3054     "**/*.html",
3055     "**/*.xml",
3056     "**/*.gradle",
3057     "**/*.groovy",
3058     "**/*.properties",
3059     "**/*.perl",
3060     "**/*.sh",
3061   ]
3062   def INCLUDE_FILES=[
3063     ".classpath",
3064     ".settings/org.eclipse.buildship.core.prefs",
3065     ".settings/org.eclipse.jdt.core.prefs"
3066   ]
3067
3068   from(jalviewDir) {
3069     exclude (EXCLUDE_FILES)
3070     include (PROCESS_FILES)
3071     filter(ReplaceTokens,
3072       beginToken: '$$',
3073       endToken: '$$',
3074       tokens: [
3075         'Version-Rel': JALVIEW_VERSION,
3076         'Year-Rel': getDate("yyyy")
3077       ]
3078     )
3079   }
3080   from(jalviewDir) {
3081     exclude (EXCLUDE_FILES)
3082     exclude (PROCESS_FILES)
3083     exclude ("appletlib")
3084     exclude ("**/*locales")
3085     exclude ("*locales/**")
3086     exclude ("utils/InstallAnywhere")
3087
3088     exclude (getdown_files_dir)
3089     // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
3090     //exclude (getdown_website_dir)
3091     //exclude (getdown_archive_dir)
3092
3093     // exluding these as not using jars as modules yet
3094     exclude ("${j11modDir}/**/*.jar")
3095   }
3096   from(jalviewDir) {
3097     include(INCLUDE_FILES)
3098   }
3099 //  from (jalviewDir) {
3100 //    // explicit includes for stuff that seemed to not get included
3101 //    include(fileTree("test/**/*."))
3102 //    exclude(EXCLUDE_FILES)
3103 //    exclude(PROCESS_FILES)
3104 //  }
3105
3106   from(file(buildProperties).getParent()) {
3107     include(file(buildProperties).getName())
3108     rename(file(buildProperties).getName(), "build_properties")
3109     filter({ line ->
3110       line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
3111     })
3112   }
3113
3114   def sourceTarBuildDir = "${buildDir}/sourceTar"
3115   from(sourceTarBuildDir) {
3116     // this includes the appended RELEASE properties file
3117   }
3118 }
3119
3120 task dataInstallersJson {
3121   group "website"
3122   description "Create the installers-VERSION.json data file for installer files created"
3123
3124   mustRunAfter installers
3125   mustRunAfter shadowJar
3126   mustRunAfter sourceDist
3127   mustRunAfter getdownArchive
3128
3129   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
3130   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
3131
3132   if (installersOutputTxt.exists()) {
3133     inputs.file(installersOutputTxt)
3134   }
3135   if (install4jCheckSums && installersSha256.exists()) {
3136     inputs.file(installersSha256)
3137   }
3138   [
3139     shadowJar.archiveFile, // executable JAR
3140     getdownVersionLaunchJvl, // version JVL
3141     sourceDist.archiveFile // source TGZ
3142   ].each { fileName ->
3143     if (file(fileName).exists()) {
3144       inputs.file(fileName)
3145     }
3146   }
3147
3148   outputs.file(hugoDataJsonFile)
3149
3150   doFirst {
3151     writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
3152   }
3153 }
3154
3155 task helppages {
3156   group "help"
3157   description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
3158
3159   dependsOn copyHelp
3160   dependsOn pubhtmlhelp
3161   
3162   inputs.dir("${helpBuildDir}/${help_dir}")
3163   outputs.dir("${buildDir}/distributions/${help_dir}")
3164 }
3165
3166
3167 task j2sSetHeadlessBuild {
3168   doFirst {
3169     IN_ECLIPSE = false
3170   }
3171 }
3172
3173
3174 task jalviewjsEnableAltFileProperty(type: WriteProperties) {
3175   group "jalviewjs"
3176   description "Enable the alternative J2S Config file for headless build"
3177
3178   outputFile = jalviewjsJ2sSettingsFileName
3179   def j2sPropsFile = file(jalviewjsJ2sSettingsFileName)
3180   def j2sProps = new Properties()
3181   if (j2sPropsFile.exists()) {
3182     try {
3183       def j2sPropsFileFIS = new FileInputStream(j2sPropsFile)
3184       j2sProps.load(j2sPropsFileFIS)
3185       j2sPropsFileFIS.close()
3186
3187       j2sProps.each { prop, val ->
3188         property(prop, val)
3189       }
3190     } catch (Exception e) {
3191       println("Exception reading ${jalviewjsJ2sSettingsFileName}")
3192       e.printStackTrace()
3193     }
3194   }
3195   if (! j2sProps.stringPropertyNames().contains(jalviewjs_j2s_alt_file_property_config)) {
3196     property(jalviewjs_j2s_alt_file_property_config, jalviewjs_j2s_alt_file_property)
3197   }
3198 }
3199
3200
3201 task jalviewjsSetEclipseWorkspace {
3202   def propKey = "jalviewjs_eclipse_workspace"
3203   def propVal = null
3204   if (project.hasProperty(propKey)) {
3205     propVal = project.getProperty(propKey)
3206     if (propVal.startsWith("~/")) {
3207       propVal = System.getProperty("user.home") + propVal.substring(1)
3208     }
3209   }
3210   def propsFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_workspace_location_file}"
3211   def propsFile = file(propsFileName)
3212   def eclipseWsDir = propVal
3213   def props = new Properties()
3214
3215   def writeProps = true
3216   if (( eclipseWsDir == null || !file(eclipseWsDir).exists() ) && propsFile.exists()) {
3217     def ins = new FileInputStream(propsFileName)
3218     props.load(ins)
3219     ins.close()
3220     if (props.getProperty(propKey, null) != null) {
3221       eclipseWsDir = props.getProperty(propKey)
3222       writeProps = false
3223     }
3224   }
3225
3226   if (eclipseWsDir == null || !file(eclipseWsDir).exists()) {
3227     def tempDir = File.createTempDir()
3228     eclipseWsDir = tempDir.getAbsolutePath()
3229     writeProps = true
3230   }
3231   eclipseWorkspace = file(eclipseWsDir)
3232
3233   doFirst {
3234     // do not run a headless transpile when we claim to be in Eclipse
3235     if (IN_ECLIPSE) {
3236       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3237       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3238     } else {
3239       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3240     }
3241
3242     if (writeProps) {
3243       props.setProperty(propKey, eclipseWsDir)
3244       propsFile.parentFile.mkdirs()
3245       def bytes = new ByteArrayOutputStream()
3246       props.store(bytes, null)
3247       def propertiesString = bytes.toString()
3248       propsFile.text = propertiesString
3249       print("NEW ")
3250     } else {
3251       print("EXISTING ")
3252     }
3253
3254     println("ECLIPSE WORKSPACE: "+eclipseWorkspace.getPath())
3255   }
3256
3257   //inputs.property(propKey, eclipseWsDir) // eclipseWsDir only gets set once this task runs, so will be out-of-date
3258   outputs.file(propsFileName)
3259   outputs.upToDateWhen { eclipseWorkspace.exists() && propsFile.exists() }
3260 }
3261
3262
3263 task jalviewjsEclipsePaths {
3264   def eclipseProduct
3265
3266   def eclipseRoot = jalviewjs_eclipse_root
3267   if (eclipseRoot.startsWith("~/")) {
3268     eclipseRoot = System.getProperty("user.home") + eclipseRoot.substring(1)
3269   }
3270   if (OperatingSystem.current().isMacOsX()) {
3271     eclipseRoot += "/Eclipse.app"
3272     eclipseBinary = "${eclipseRoot}/Contents/MacOS/eclipse"
3273     eclipseProduct = "${eclipseRoot}/Contents/Eclipse/.eclipseproduct"
3274   } else if (OperatingSystem.current().isWindows()) { // check these paths!!
3275     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3276       eclipseRoot += "/eclipse"
3277     }
3278     eclipseBinary = "${eclipseRoot}/eclipse.exe"
3279     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3280   } else { // linux or unix
3281     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3282       eclipseRoot += "/eclipse"
3283 println("eclipseDir exists")
3284     }
3285     eclipseBinary = "${eclipseRoot}/eclipse"
3286     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3287   }
3288
3289   eclipseVersion = "4.13" // default
3290   def assumedVersion = true
3291   if (file(eclipseProduct).exists()) {
3292     def fis = new FileInputStream(eclipseProduct)
3293     def props = new Properties()
3294     props.load(fis)
3295     eclipseVersion = props.getProperty("version")
3296     fis.close()
3297     assumedVersion = false
3298   }
3299   
3300   def propKey = "eclipse_debug"
3301   eclipseDebug = (project.hasProperty(propKey) && project.getProperty(propKey).equals("true"))
3302
3303   doFirst {
3304     // do not run a headless transpile when we claim to be in Eclipse
3305     if (IN_ECLIPSE) {
3306       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3307       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3308     } else {
3309       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3310     }
3311
3312     if (!assumedVersion) {
3313       println("ECLIPSE VERSION=${eclipseVersion}")
3314     }
3315   }
3316 }
3317
3318
3319 task printProperties {
3320   group "Debug"
3321   description "Output to console all System.properties"
3322   doFirst {
3323     System.properties.each { key, val -> System.out.println("Property: ${key}=${val}") }
3324   }
3325 }
3326
3327
3328 task eclipseSetup {
3329   dependsOn eclipseProject
3330   dependsOn eclipseClasspath
3331   dependsOn eclipseJdt
3332 }
3333
3334
3335 // this version (type: Copy) will delete anything in the eclipse dropins folder that isn't in fromDropinsDir
3336 task jalviewjsEclipseCopyDropins(type: Copy) {
3337   dependsOn jalviewjsEclipsePaths
3338
3339   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_eclipse_dropins_dir}", include: "*.jar")
3340   inputFiles += file("${jalviewDir}/${jalviewjsJ2sPlugin}")
3341   def outputDir = "${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}"
3342
3343   from inputFiles
3344   into outputDir
3345 }
3346
3347
3348 // this eclipse -clean doesn't actually work
3349 task jalviewjsCleanEclipse(type: Exec) {
3350   dependsOn eclipseSetup
3351   dependsOn jalviewjsEclipsePaths
3352   dependsOn jalviewjsEclipseCopyDropins
3353
3354   executable(eclipseBinary)
3355   args(["-nosplash", "--launcher.suppressErrors", "-data", eclipseWorkspace.getPath(), "-clean", "-console", "-consoleLog"])
3356   if (eclipseDebug) {
3357     args += "-debug"
3358   }
3359   args += "-l"
3360
3361   def inputString = """exit
3362 y
3363 """
3364   def inputByteStream = new ByteArrayInputStream(inputString.getBytes())
3365   standardInput = inputByteStream
3366 }
3367
3368 /* not really working yet
3369 jalviewjsEclipseCopyDropins.finalizedBy jalviewjsCleanEclipse
3370 */
3371
3372
3373 task jalviewjsTransferUnzipSwingJs {
3374   def file_zip = "${jalviewDir}/${jalviewjs_swingjs_zip}"
3375
3376   doLast {
3377     copy {
3378       from zipTree(file_zip)
3379       into "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3380     }
3381   }
3382
3383   inputs.file file_zip
3384   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3385 }
3386
3387
3388 task jalviewjsTransferUnzipLib {
3389   def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip")
3390
3391   doLast {
3392     zipFiles.each { file_zip -> 
3393       copy {
3394         from zipTree(file_zip)
3395         into "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3396       }
3397     }
3398   }
3399
3400   inputs.files zipFiles
3401   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3402 }
3403
3404
3405 task jalviewjsTransferUnzipAllLibs {
3406   dependsOn jalviewjsTransferUnzipSwingJs
3407   dependsOn jalviewjsTransferUnzipLib
3408 }
3409
3410
3411 task jalviewjsCreateJ2sSettings(type: WriteProperties) {
3412   group "JalviewJS"
3413   description "Create the alternative j2s file from the j2s.* properties"
3414
3415   jalviewjsJ2sProps = project.properties.findAll { it.key.startsWith("j2s.") }.sort { it.key }
3416   def siteDirProperty = "j2s.site.directory"
3417   def setSiteDir = false
3418   jalviewjsJ2sProps.each { prop, val ->
3419     if (val != null) {
3420       if (prop == siteDirProperty) {
3421         if (!(val.startsWith('/') || val.startsWith("file://") )) {
3422           val = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${val}"
3423         }
3424         setSiteDir = true
3425       }
3426       property(prop,val)
3427     }
3428     if (!setSiteDir) { // default site location, don't override specifically set property
3429       property(siteDirProperty,"${jalviewDirRelativePath}/${jalviewjsTransferSiteJsDir}")
3430     }
3431   }
3432   outputFile = jalviewjsJ2sAltSettingsFileName
3433
3434   if (! IN_ECLIPSE) {
3435     inputs.properties(jalviewjsJ2sProps)
3436     outputs.file(jalviewjsJ2sAltSettingsFileName)
3437   }
3438 }
3439
3440
3441 task jalviewjsEclipseSetup {
3442   dependsOn jalviewjsEclipseCopyDropins
3443   dependsOn jalviewjsSetEclipseWorkspace
3444   dependsOn jalviewjsCreateJ2sSettings
3445 }
3446
3447
3448 task jalviewjsSyncAllLibs (type: Sync) {
3449   dependsOn jalviewjsTransferUnzipAllLibs
3450   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
3451   inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
3452   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3453
3454   from inputFiles
3455   into outputDir
3456   def outputFiles = []
3457   rename { filename ->
3458     outputFiles += "${outputDir}/${filename}"
3459     null
3460   }
3461   preserve {
3462     include "**"
3463   }
3464
3465   // should this be exclude really ?
3466   duplicatesStrategy "INCLUDE"
3467
3468   outputs.files outputFiles
3469   inputs.files inputFiles
3470 }
3471
3472
3473 task jalviewjsSyncResources (type: Sync) {
3474   dependsOn buildResources
3475
3476   def inputFiles = fileTree(dir: resourcesBuildDir)
3477   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3478
3479   from inputFiles
3480   into outputDir
3481   def outputFiles = []
3482   rename { filename ->
3483     outputFiles += "${outputDir}/${filename}"
3484     null
3485   }
3486   preserve {
3487     include "**"
3488   }
3489   outputs.files outputFiles
3490   inputs.files inputFiles
3491 }
3492
3493
3494 task jalviewjsSyncSiteResources (type: Sync) {
3495   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_site_resource_dir}")
3496   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3497
3498   from inputFiles
3499   into outputDir
3500   def outputFiles = []
3501   rename { filename ->
3502     outputFiles += "${outputDir}/${filename}"
3503     null
3504   }
3505   preserve {
3506     include "**"
3507   }
3508   outputs.files outputFiles
3509   inputs.files inputFiles
3510 }
3511
3512
3513 task jalviewjsSyncBuildProperties (type: Sync) {
3514   dependsOn createBuildProperties
3515   def inputFiles = [file(buildProperties)]
3516   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3517
3518   from inputFiles
3519   into outputDir
3520   def outputFiles = []
3521   rename { filename ->
3522     outputFiles += "${outputDir}/${filename}"
3523     null
3524   }
3525   preserve {
3526     include "**"
3527   }
3528   outputs.files outputFiles
3529   inputs.files inputFiles
3530 }
3531
3532
3533 task jalviewjsProjectImport(type: Exec) {
3534   dependsOn eclipseSetup
3535   dependsOn jalviewjsEclipsePaths
3536   dependsOn jalviewjsEclipseSetup
3537
3538   doFirst {
3539     // do not run a headless import when we claim to be in Eclipse
3540     if (IN_ECLIPSE) {
3541       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3542       throw new StopExecutionException("Not running headless import whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3543     } else {
3544       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3545     }
3546   }
3547
3548   //def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview/org.eclipse.jdt.core"
3549   def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview"
3550   executable(eclipseBinary)
3551   args(["-nosplash", "--launcher.suppressErrors", "-application", "com.seeq.eclipse.importprojects.headlessimport", "-data", eclipseWorkspace.getPath(), "-import", jalviewDirAbsolutePath])
3552   if (eclipseDebug) {
3553     args += "-debug"
3554   }
3555   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3556   if (!IN_ECLIPSE) {
3557     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3558     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3559   }
3560
3561   inputs.file("${jalviewDir}/.project")
3562   outputs.upToDateWhen { 
3563     file(projdir).exists()
3564   }
3565 }
3566
3567
3568 task jalviewjsTranspile(type: Exec) {
3569   dependsOn jalviewjsEclipseSetup 
3570   dependsOn jalviewjsProjectImport
3571   dependsOn jalviewjsEclipsePaths
3572   if (!IN_ECLIPSE) {
3573     dependsOn jalviewjsEnableAltFileProperty
3574   }
3575
3576   doFirst {
3577     // do not run a headless transpile when we claim to be in Eclipse
3578     if (IN_ECLIPSE) {
3579       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3580       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3581     } else {
3582       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3583     }
3584   }
3585
3586   executable(eclipseBinary)
3587   args(["-nosplash", "--launcher.suppressErrors", "-application", "org.eclipse.jdt.apt.core.aptBuild", "-data", eclipseWorkspace, "-${jalviewjs_eclipse_build_arg}", eclipse_project_name ])
3588   if (eclipseDebug) {
3589     args += "-debug"
3590   }
3591   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3592   if (!IN_ECLIPSE) {
3593     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3594     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3595   }
3596
3597   def stdout
3598   def stderr
3599   doFirst {
3600     stdout = new ByteArrayOutputStream()
3601     stderr = new ByteArrayOutputStream()
3602
3603     def logOutFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}"
3604     def logOutFile = file(logOutFileName)
3605     logOutFile.createNewFile()
3606     logOutFile.text = """ROOT: ${jalviewjs_eclipse_root}
3607 BINARY: ${eclipseBinary}
3608 VERSION: ${eclipseVersion}
3609 WORKSPACE: ${eclipseWorkspace}
3610 DEBUG: ${eclipseDebug}
3611 ----
3612 """
3613     def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3614     // combine stdout and stderr
3615     def logErrFOS = logOutFOS
3616
3617     if (jalviewjs_j2s_to_console.equals("true")) {
3618       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3619         new org.apache.tools.ant.util.TeeOutputStream(
3620           logOutFOS,
3621           stdout),
3622         System.out)
3623       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3624         new org.apache.tools.ant.util.TeeOutputStream(
3625           logErrFOS,
3626           stderr),
3627         System.err)
3628     } else {
3629       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3630         logOutFOS,
3631         stdout)
3632       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3633         logErrFOS,
3634         stderr)
3635     }
3636   }
3637
3638   doLast {
3639     if (stdout.toString().contains("Error processing ")) {
3640       // j2s did not complete transpile
3641       //throw new TaskExecutionException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3642       if (jalviewjs_ignore_transpile_errors.equals("true")) {
3643         println("IGNORING TRANSPILE ERRORS")
3644         println("See eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3645       } else {
3646         throw new GradleException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3647       }
3648     }
3649   }
3650
3651   inputs.dir("${jalviewDir}/${sourceDir}")
3652   outputs.dir("${jalviewDir}/${jalviewjsTransferSiteJsDir}")
3653   outputs.upToDateWhen( { file("${jalviewDir}/${jalviewjsTransferSiteJsDir}${jalviewjs_server_resource}").exists() } )
3654 }
3655
3656
3657 def jalviewjsCallCore(String name, FileCollection list, String prefixFile, String suffixFile, String jsfile, String zjsfile, File logOutFile, Boolean logOutConsole) {
3658
3659   def stdout = new ByteArrayOutputStream()
3660   def stderr = new ByteArrayOutputStream()
3661
3662   def coreFile = file(jsfile)
3663   def msg = ""
3664   msg = "Creating core for ${name}...\nGenerating ${jsfile}"
3665   println(msg)
3666   logOutFile.createNewFile()
3667   logOutFile.append(msg+"\n")
3668
3669   def coreTop = file(prefixFile)
3670   def coreBottom = file(suffixFile)
3671   coreFile.getParentFile().mkdirs()
3672   coreFile.createNewFile()
3673   coreFile.write( coreTop.getText("UTF-8") )
3674   list.each {
3675     f ->
3676     if (f.exists()) {
3677       def t = f.getText("UTF-8")
3678       t.replaceAll("Clazz\\.([^_])","Clazz_${1}")
3679       coreFile.append( t )
3680     } else {
3681       msg = "...file '"+f.getPath()+"' does not exist, skipping"
3682       println(msg)
3683       logOutFile.append(msg+"\n")
3684     }
3685   }
3686   coreFile.append( coreBottom.getText("UTF-8") )
3687
3688   msg = "Generating ${zjsfile}"
3689   println(msg)
3690   logOutFile.append(msg+"\n")
3691   def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3692   def logErrFOS = logOutFOS
3693
3694   javaexec {
3695     classpath = files(["${jalviewDir}/${jalviewjs_closure_compiler}"])
3696     main = "com.google.javascript.jscomp.CommandLineRunner"
3697     jvmArgs = [ "-Dfile.encoding=UTF-8" ]
3698     args = [ "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
3699     maxHeapSize = "2g"
3700
3701     msg = "\nRunning '"+commandLine.join(' ')+"'\n"
3702     println(msg)
3703     logOutFile.append(msg+"\n")
3704
3705     if (logOutConsole) {
3706       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3707         new org.apache.tools.ant.util.TeeOutputStream(
3708           logOutFOS,
3709           stdout),
3710         standardOutput)
3711         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3712           new org.apache.tools.ant.util.TeeOutputStream(
3713             logErrFOS,
3714             stderr),
3715           System.err)
3716     } else {
3717       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3718         logOutFOS,
3719         stdout)
3720         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3721           logErrFOS,
3722           stderr)
3723     }
3724   }
3725   msg = "--"
3726   println(msg)
3727   logOutFile.append(msg+"\n")
3728 }
3729
3730
3731 task jalviewjsBuildAllCores {
3732   group "JalviewJS"
3733   description "Build the core js lib closures listed in the classlists dir"
3734   dependsOn jalviewjsTranspile
3735   dependsOn jalviewjsTransferUnzipSwingJs
3736
3737   def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${jalviewjs_j2s_subdir}"
3738   def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_j2s_subdir}"
3739   def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteLibDir}/${jalviewjs_j2s_subdir}"
3740   def jsDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_js_subdir}"
3741   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}/${jalviewjs_j2s_subdir}/core"
3742   def prefixFile = "${jsDir}/core/coretop2.js"
3743   def suffixFile = "${jsDir}/core/corebottom2.js"
3744
3745   inputs.file prefixFile
3746   inputs.file suffixFile
3747
3748   def classlistFiles = []
3749   // add the classlists found int the jalviewjs_classlists_dir
3750   fileTree(dir: "${jalviewDir}/${jalviewjs_classlists_dir}", include: "*.txt").each {
3751     file ->
3752     def name = file.getName() - ".txt"
3753     classlistFiles += [
3754       'file': file,
3755       'name': name
3756     ]
3757   }
3758
3759   // _jmol and _jalview cores. Add any other peculiar classlist.txt files here
3760   //classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jmol}"), 'name': "_jvjmol" ]
3761   classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jalview}"), 'name': jalviewjsJalviewCoreName ]
3762
3763   jalviewjsCoreClasslists = []
3764
3765   classlistFiles.each {
3766     hash ->
3767
3768     def file = hash['file']
3769     if (! file.exists()) {
3770       //println("...classlist file '"+file.getPath()+"' does not exist, skipping")
3771       return false // this is a "continue" in groovy .each closure
3772     }
3773     def name = hash['name']
3774     if (name == null) {
3775       name = file.getName() - ".txt"
3776     }
3777
3778     def filelist = []
3779     file.eachLine {
3780       line ->
3781         filelist += line
3782     }
3783     def list = fileTree(dir: j2sDir, includes: filelist)
3784
3785     def jsfile = "${outputDir}/core${name}.js"
3786     def zjsfile = "${outputDir}/core${name}.z.js"
3787
3788     jalviewjsCoreClasslists += [
3789       'jsfile': jsfile,
3790       'zjsfile': zjsfile,
3791       'list': list,
3792       'name': name
3793     ]
3794
3795     inputs.file(file)
3796     inputs.files(list)
3797     outputs.file(jsfile)
3798     outputs.file(zjsfile)
3799   }
3800   
3801   // _stevesoft core. add any cores without a classlist here (and the inputs and outputs)
3802   def stevesoftClasslistName = "_stevesoft"
3803   def stevesoftClasslist = [
3804     'jsfile': "${outputDir}/core${stevesoftClasslistName}.js",
3805     'zjsfile': "${outputDir}/core${stevesoftClasslistName}.z.js",
3806     'list': fileTree(dir: j2sDir, include: "com/stevesoft/pat/**/*.js"),
3807     'name': stevesoftClasslistName
3808   ]
3809   jalviewjsCoreClasslists += stevesoftClasslist
3810   inputs.files(stevesoftClasslist['list'])
3811   outputs.file(stevesoftClasslist['jsfile'])
3812   outputs.file(stevesoftClasslist['zjsfile'])
3813
3814   // _all core
3815   def allClasslistName = "_all"
3816   def allJsFiles = fileTree(dir: j2sDir, include: "**/*.js")
3817   allJsFiles += fileTree(
3818     dir: libJ2sDir,
3819     include: "**/*.js",
3820     excludes: [
3821       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3822       "**/org/jmol/jvxl/readers/IsoIntersectFileReader.js",
3823       "**/org/jmol/export/JSExporter.js"
3824     ]
3825   )
3826   allJsFiles += fileTree(
3827     dir: swingJ2sDir,
3828     include: "**/*.js",
3829     excludes: [
3830       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3831       "**/sun/misc/Unsafe.js",
3832       "**/swingjs/jquery/jquery-editable-select.js",
3833       "**/swingjs/jquery/j2sComboBox.js",
3834       "**/sun/misc/FloatingDecimal.js"
3835     ]
3836   )
3837   def allClasslist = [
3838     'jsfile': "${outputDir}/core${allClasslistName}.js",
3839     'zjsfile': "${outputDir}/core${allClasslistName}.z.js",
3840     'list': allJsFiles,
3841     'name': allClasslistName
3842   ]
3843   // not including this version of "all" core at the moment
3844   //jalviewjsCoreClasslists += allClasslist
3845   inputs.files(allClasslist['list'])
3846   outputs.file(allClasslist['jsfile'])
3847   outputs.file(allClasslist['zjsfile'])
3848
3849   doFirst {
3850     def logOutFile = file("${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_closure_stdout}")
3851     logOutFile.getParentFile().mkdirs()
3852     logOutFile.createNewFile()
3853     logOutFile.write(getDate("yyyy-MM-dd HH:mm:ss")+" jalviewjsBuildAllCores\n----\n")
3854
3855     jalviewjsCoreClasslists.each {
3856       jalviewjsCallCore(it.name, it.list, prefixFile, suffixFile, it.jsfile, it.zjsfile, logOutFile, jalviewjs_j2s_to_console.equals("true"))
3857     }
3858   }
3859
3860 }
3861
3862
3863 def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inputFile, String outputFile) {
3864   copy {
3865     from inputFile
3866     into file(outputFile).getParentFile()
3867     rename { filename ->
3868       if (filename.equals(inputFile.getName())) {
3869         return file(outputFile).getName()
3870       }
3871       return null
3872     }
3873     filter(ReplaceTokens,
3874       beginToken: '_',
3875       endToken: '_',
3876       tokens: [
3877         'MAIN': '"'+main_class+'"',
3878         'CODE': "null",
3879         'NAME': jalviewjsJalviewTemplateName+" [core ${coreName}]",
3880         'COREKEY': jalviewjs_core_key,
3881         'CORENAME': coreName
3882       ]
3883     )
3884   }
3885 }
3886
3887
3888 task jalviewjsPublishCoreTemplates {
3889   dependsOn jalviewjsBuildAllCores
3890   def inputFileName = "${jalviewDir}/${j2s_coretemplate_html}"
3891   def inputFile = file(inputFileName)
3892   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3893
3894   def outputFiles = []
3895   jalviewjsCoreClasslists.each { cl ->
3896     def outputFile = "${outputDir}/${jalviewjsJalviewTemplateName}_${cl.name}.html"
3897     cl['outputfile'] = outputFile
3898     outputFiles += outputFile
3899   }
3900
3901   doFirst {
3902     jalviewjsCoreClasslists.each { cl ->
3903       jalviewjsPublishCoreTemplate(cl.name, jalviewjsJalviewTemplateName, inputFile, cl.outputfile)
3904     }
3905   }
3906   inputs.file(inputFile)
3907   outputs.files(outputFiles)
3908 }
3909
3910
3911 task jalviewjsSyncCore (type: Sync) {
3912   dependsOn jalviewjsBuildAllCores
3913   dependsOn jalviewjsPublishCoreTemplates
3914   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteCoreDir}")
3915   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3916
3917   from inputFiles
3918   into outputDir
3919   def outputFiles = []
3920   rename { filename ->
3921     outputFiles += "${outputDir}/${filename}"
3922     null
3923   }
3924   preserve {
3925     include "**"
3926   }
3927   outputs.files outputFiles
3928   inputs.files inputFiles
3929 }
3930
3931
3932 // this Copy version of TransferSiteJs will delete anything else in the target dir
3933 task jalviewjsCopyTransferSiteJs(type: Copy) {
3934   dependsOn jalviewjsTranspile
3935   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3936   into "${jalviewDir}/${jalviewjsSiteDir}"
3937 }
3938
3939
3940 // this Sync version of TransferSite is used by buildship to keep the website automatically up to date when a file changes
3941 task jalviewjsSyncTransferSiteJs(type: Sync) {
3942   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3943   include "**/*.*"
3944   into "${jalviewDir}/${jalviewjsSiteDir}"
3945   preserve {
3946     include "**"
3947   }
3948 }
3949
3950
3951 jalviewjsSyncAllLibs.mustRunAfter jalviewjsCopyTransferSiteJs
3952 jalviewjsSyncResources.mustRunAfter jalviewjsCopyTransferSiteJs
3953 jalviewjsSyncSiteResources.mustRunAfter jalviewjsCopyTransferSiteJs
3954 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsCopyTransferSiteJs
3955
3956 jalviewjsSyncAllLibs.mustRunAfter jalviewjsSyncTransferSiteJs
3957 jalviewjsSyncResources.mustRunAfter jalviewjsSyncTransferSiteJs
3958 jalviewjsSyncSiteResources.mustRunAfter jalviewjsSyncTransferSiteJs
3959 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsSyncTransferSiteJs
3960
3961
3962 task jalviewjsPrepareSite {
3963   group "JalviewJS"
3964   description "Prepares the website folder including unzipping files and copying resources"
3965   dependsOn jalviewjsSyncAllLibs
3966   dependsOn jalviewjsSyncResources
3967   dependsOn jalviewjsSyncSiteResources
3968   dependsOn jalviewjsSyncBuildProperties
3969   dependsOn jalviewjsSyncCore
3970 }
3971
3972
3973 task jalviewjsBuildSite {
3974   group "JalviewJS"
3975   description "Builds the whole website including transpiled code"
3976   dependsOn jalviewjsCopyTransferSiteJs
3977   dependsOn jalviewjsPrepareSite
3978 }
3979
3980
3981 task cleanJalviewjsTransferSite {
3982   doFirst {
3983     delete "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3984     delete "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3985     delete "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3986     delete "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3987   }
3988 }
3989
3990
3991 task cleanJalviewjsSite {
3992   dependsOn cleanJalviewjsTransferSite
3993   doFirst {
3994     delete "${jalviewDir}/${jalviewjsSiteDir}"
3995   }
3996 }
3997
3998
3999 task jalviewjsSiteTar(type: Tar) {
4000   group "JalviewJS"
4001   description "Creates a tar.gz file for the website"
4002   dependsOn jalviewjsBuildSite
4003   def outputFilename = "jalviewjs-site-${JALVIEW_VERSION}.tar.gz"
4004   archiveFileName = outputFilename
4005
4006   compression Compression.GZIP
4007
4008   from "${jalviewDir}/${jalviewjsSiteDir}"
4009   into jalviewjs_site_dir // this is inside the tar file
4010
4011   inputs.dir("${jalviewDir}/${jalviewjsSiteDir}")
4012 }
4013
4014
4015 task jalviewjsServer {
4016   group "JalviewJS"
4017   def filename = "jalviewjsTest.html"
4018   description "Starts a webserver on localhost to test the website. See ${filename} to access local site on most recently used port."
4019   def htmlFile = "${jalviewDirAbsolutePath}/${filename}"
4020   doLast {
4021
4022     def factory
4023     try {
4024       def f = Class.forName("org.gradle.plugins.javascript.envjs.http.simple.SimpleHttpFileServerFactory")
4025       factory = f.newInstance()
4026     } catch (ClassNotFoundException e) {
4027       throw new GradleException("Unable to create SimpleHttpFileServerFactory")
4028     }
4029     def port = Integer.valueOf(jalviewjs_server_port)
4030     def start = port
4031     def running = false
4032     def url
4033     def jalviewjsServer
4034     while(port < start+1000 && !running) {
4035       try {
4036         def doc_root = new File("${jalviewDirAbsolutePath}/${jalviewjsSiteDir}")
4037         jalviewjsServer = factory.start(doc_root, port)
4038         running = true
4039         url = jalviewjsServer.getResourceUrl(jalviewjs_server_resource)
4040         println("SERVER STARTED with document root ${doc_root}.")
4041         println("Go to "+url+" . Run  gradle --stop  to stop (kills all gradle daemons).")
4042         println("For debug: "+url+"?j2sdebug")
4043         println("For verbose: "+url+"?j2sverbose")
4044       } catch (Exception e) {
4045         port++;
4046       }
4047     }
4048     def htmlText = """
4049       <p><a href="${url}">JalviewJS Test. &lt;${url}&gt;</a></p>
4050       <p><a href="${url}?j2sdebug">JalviewJS Test with debug. &lt;${url}?j2sdebug&gt;</a></p>
4051       <p><a href="${url}?j2sverbose">JalviewJS Test with verbose. &lt;${url}?j2sdebug&gt;</a></p>
4052       """
4053     jalviewjsCoreClasslists.each { cl ->
4054       def urlcore = jalviewjsServer.getResourceUrl(file(cl.outputfile).getName())
4055       htmlText += """
4056       <p><a href="${urlcore}">${jalviewjsJalviewTemplateName} [core ${cl.name}]. &lt;${urlcore}&gt;</a></p>
4057       """
4058       println("For core ${cl.name}: "+urlcore)
4059     }
4060
4061     file(htmlFile).text = htmlText
4062   }
4063
4064   outputs.file(htmlFile)
4065   outputs.upToDateWhen({false})
4066 }
4067
4068
4069 task cleanJalviewjsAll {
4070   group "JalviewJS"
4071   description "Delete all configuration and build artifacts to do with JalviewJS build"
4072   dependsOn cleanJalviewjsSite
4073   dependsOn jalviewjsEclipsePaths
4074   
4075   doFirst {
4076     delete "${jalviewDir}/${jalviewjsBuildDir}"
4077     delete "${jalviewDir}/${eclipse_bin_dir}"
4078     if (eclipseWorkspace != null && file(eclipseWorkspace.getAbsolutePath()+"/.metadata").exists()) {
4079       delete file(eclipseWorkspace.getAbsolutePath()+"/.metadata")
4080     }
4081     delete jalviewjsJ2sAltSettingsFileName
4082   }
4083
4084   outputs.upToDateWhen( { false } )
4085 }
4086
4087
4088 task jalviewjsIDE_checkJ2sPlugin {
4089   group "00 JalviewJS in Eclipse"
4090   description "Compare the swingjs/net.sf.j2s.core(-j11)?.jar file with the Eclipse IDE's plugin version (found in the 'dropins' dir)"
4091
4092   doFirst {
4093     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4094     def j2sPluginFile = file(j2sPlugin)
4095     def eclipseHome = System.properties["eclipse.home.location"]
4096     if (eclipseHome == null || ! IN_ECLIPSE) {
4097       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. Skipping J2S Plugin Check.")
4098     }
4099     def eclipseJ2sPluginDirs = [ "${eclipseHome}/dropins" ]
4100     def altPluginsDir = System.properties["org.eclipse.equinox.p2.reconciler.dropins.directory"]
4101     if (altPluginsDir != null && file(altPluginsDir).exists()) {
4102       eclipseJ2sPluginDirs += altPluginsDir
4103     }
4104     def foundPlugin = false
4105     def j2sPluginFileName = j2sPluginFile.getName()
4106     def eclipseJ2sPlugin
4107     def eclipseJ2sPluginFile
4108     eclipseJ2sPluginDirs.any { dir ->
4109       eclipseJ2sPlugin = "${dir}/${j2sPluginFileName}"
4110       eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4111       if (eclipseJ2sPluginFile.exists()) {
4112         foundPlugin = true
4113         return true
4114       }
4115     }
4116     if (!foundPlugin) {
4117       def msg = "Eclipse J2S Plugin is not installed (could not find '${j2sPluginFileName}' in\n"+eclipseJ2sPluginDirs.join("\n")+"\n)\nTry running task jalviewjsIDE_copyJ2sPlugin"
4118       System.err.println(msg)
4119       throw new StopExecutionException(msg)
4120     }
4121
4122     def digest = MessageDigest.getInstance("MD5")
4123
4124     digest.update(j2sPluginFile.text.bytes)
4125     def j2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4126
4127     digest.update(eclipseJ2sPluginFile.text.bytes)
4128     def eclipseJ2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4129      
4130     if (j2sPluginMd5 != eclipseJ2sPluginMd5) {
4131       def msg = "WARNING! Eclipse J2S Plugin '${eclipseJ2sPlugin}' is different to this commit's version '${j2sPlugin}'"
4132       System.err.println(msg)
4133       throw new StopExecutionException(msg)
4134     } else {
4135       def msg = "Eclipse J2S Plugin '${eclipseJ2sPlugin}' is the same as '${j2sPlugin}' (this is good)"
4136       println(msg)
4137     }
4138   }
4139 }
4140
4141 task jalviewjsIDE_copyJ2sPlugin {
4142   group "00 JalviewJS in Eclipse"
4143   description "Copy the swingjs/net.sf.j2s.core(-j11)?.jar file into the Eclipse IDE's 'dropins' dir"
4144
4145   doFirst {
4146     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4147     def j2sPluginFile = file(j2sPlugin)
4148     def eclipseHome = System.properties["eclipse.home.location"]
4149     if (eclipseHome == null || ! IN_ECLIPSE) {
4150       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. NOT copying J2S Plugin.")
4151     }
4152     def eclipseJ2sPlugin = "${eclipseHome}/dropins/${j2sPluginFile.getName()}"
4153     def eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4154     def msg = "WARNING! Copying this commit's j2s plugin '${j2sPlugin}' to Eclipse J2S Plugin '${eclipseJ2sPlugin}'\n* May require an Eclipse restart"
4155     System.err.println(msg)
4156     copy {
4157       from j2sPlugin
4158       eclipseJ2sPluginFile.getParentFile().mkdirs()
4159       into eclipseJ2sPluginFile.getParent()
4160     }
4161   }
4162 }
4163
4164
4165 task jalviewjsIDE_j2sFile {
4166   group "00 JalviewJS in Eclipse"
4167   description "Creates the .j2s file"
4168   dependsOn jalviewjsCreateJ2sSettings
4169 }
4170
4171
4172 task jalviewjsIDE_SyncCore {
4173   group "00 JalviewJS in Eclipse"
4174   description "Build the core js lib closures listed in the classlists dir and publish core html from template"
4175   dependsOn jalviewjsSyncCore
4176 }
4177
4178
4179 task jalviewjsIDE_SyncSiteAll {
4180   dependsOn jalviewjsSyncAllLibs
4181   dependsOn jalviewjsSyncResources
4182   dependsOn jalviewjsSyncSiteResources
4183   dependsOn jalviewjsSyncBuildProperties
4184 }
4185
4186
4187 cleanJalviewjsTransferSite.mustRunAfter jalviewjsIDE_SyncSiteAll
4188
4189
4190 task jalviewjsIDE_PrepareSite {
4191   group "00 JalviewJS in Eclipse"
4192   description "Sync libs and resources to site dir, but not closure cores"
4193
4194   dependsOn jalviewjsIDE_SyncSiteAll
4195   //dependsOn cleanJalviewjsTransferSite // not sure why this clean is here -- will slow down a re-run of this task
4196 }
4197
4198
4199 task jalviewjsIDE_AssembleSite {
4200   group "00 JalviewJS in Eclipse"
4201   description "Assembles unzipped supporting zipfiles, resources, site resources and closure cores into the Eclipse transpiled site"
4202   dependsOn jalviewjsPrepareSite
4203 }
4204
4205
4206 task jalviewjsIDE_SiteClean {
4207   group "00 JalviewJS in Eclipse"
4208   description "Deletes the Eclipse transpiled site"
4209   dependsOn cleanJalviewjsSite
4210 }
4211
4212
4213 task jalviewjsIDE_Server {
4214   group "00 JalviewJS in Eclipse"
4215   description "Starts a webserver on localhost to test the website"
4216   dependsOn jalviewjsServer
4217 }
4218
4219
4220 // buildship runs this at import or gradle refresh
4221 task eclipseSynchronizationTask {
4222   //dependsOn eclipseSetup
4223   dependsOn createBuildProperties
4224   if (J2S_ENABLED) {
4225     dependsOn jalviewjsIDE_j2sFile
4226     dependsOn jalviewjsIDE_checkJ2sPlugin
4227     dependsOn jalviewjsIDE_PrepareSite
4228   }
4229 }
4230
4231
4232 // buildship runs this at build time or project refresh
4233 task eclipseAutoBuildTask {
4234   //dependsOn jalviewjsIDE_checkJ2sPlugin
4235   //dependsOn jalviewjsIDE_PrepareSite
4236 }
4237
4238
4239 task jalviewjsCopyStderrLaunchFile(type: Copy) {
4240   from file(jalviewjs_stderr_launch)
4241   into jalviewjsSiteDir
4242
4243   inputs.file jalviewjs_stderr_launch
4244   outputs.file jalviewjsStderrLaunchFilename
4245 }
4246
4247 task cleanJalviewjsChromiumUserDir {
4248   doFirst {
4249     delete jalviewjsChromiumUserDir
4250   }
4251   outputs.dir jalviewjsChromiumUserDir
4252   // always run when depended on
4253   outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
4254 }
4255
4256 task jalviewjsChromiumProfile {
4257   dependsOn cleanJalviewjsChromiumUserDir
4258   mustRunAfter cleanJalviewjsChromiumUserDir
4259
4260   def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
4261
4262   doFirst {
4263     mkdir jalviewjsChromiumProfileDir
4264     firstRun.text = ""
4265   }
4266   outputs.file firstRun
4267 }
4268
4269 task jalviewjsLaunchTest {
4270   group "Test"
4271   description "Check JalviewJS opens in a browser"
4272   dependsOn jalviewjsBuildSite
4273   dependsOn jalviewjsCopyStderrLaunchFile
4274   dependsOn jalviewjsChromiumProfile
4275
4276   def macOS = OperatingSystem.current().isMacOsX()
4277   def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary
4278   if (chromiumBinary.startsWith("~/")) {
4279     chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1)
4280   }
4281   
4282   def stdout
4283   def stderr
4284   doFirst {
4285     def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000
4286     
4287     def binary = file(chromiumBinary)
4288     if (!binary.exists()) {
4289       throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
4290     }
4291     stdout = new ByteArrayOutputStream()
4292     stderr = new ByteArrayOutputStream()
4293     def execStdout
4294     def execStderr
4295     if (jalviewjs_j2s_to_console.equals("true")) {
4296       execStdout = new org.apache.tools.ant.util.TeeOutputStream(
4297         stdout,
4298         System.out)
4299       execStderr = new org.apache.tools.ant.util.TeeOutputStream(
4300         stderr,
4301         System.err)
4302     } else {
4303       execStdout = stdout
4304       execStderr = stderr
4305     }
4306     def execArgs = [
4307       "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER
4308       "--headless=new",
4309       "--disable-gpu",
4310       "--timeout=${timeoutms}",
4311       "--virtual-time-budget=${timeoutms}",
4312       "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
4313       "--profile-directory=${jalviewjs_chromium_profile_name}",
4314       "--allow-file-access-from-files",
4315       "--enable-logging=stderr",
4316       "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}"
4317     ]
4318     
4319     if (true || macOS) {
4320       ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
4321       Future f1 = executor.submit(
4322         () -> {
4323           exec {
4324             standardOutput = execStdout
4325             errorOutput = execStderr
4326             executable(chromiumBinary)
4327             args(execArgs)
4328             println "COMMAND: '"+commandLine.join(" ")+"'"
4329           }
4330           executor.shutdownNow()
4331         }
4332       )
4333
4334       def noChangeBytes = 0
4335       def noChangeIterations = 0
4336       executor.scheduleAtFixedRate(
4337         () -> {
4338           String stderrString = stderr.toString()
4339           // shutdown the task if we have a success string
4340           if (stderrString.contains(jalviewjs_desktop_init_string)) {
4341             f1.cancel()
4342             Thread.sleep(1000)
4343             executor.shutdownNow()
4344           }
4345           // if no change in stderr for 10s then also end
4346           if (noChangeIterations >= jalviewjs_chromium_idle_timeout) {
4347             executor.shutdownNow()
4348           }
4349           if (stderrString.length() == noChangeBytes) {
4350             noChangeIterations++
4351           } else {
4352             noChangeBytes = stderrString.length()
4353             noChangeIterations = 0
4354           }
4355         },
4356         1, 1, TimeUnit.SECONDS)
4357
4358       executor.schedule(new Runnable(){
4359         public void run(){
4360           f1.cancel()
4361           executor.shutdownNow()
4362         }
4363       }, timeoutms, TimeUnit.MILLISECONDS)
4364
4365       executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
4366       executor.shutdownNow()
4367     }
4368
4369   }
4370   
4371   doLast {
4372     def found = false
4373     stderr.toString().eachLine { line ->
4374       if (line.contains(jalviewjs_desktop_init_string)) {
4375         println("Found line '"+line+"'")
4376         found = true
4377         return
4378       }
4379     }
4380     if (!found) {
4381       throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
4382     }
4383   }
4384 }
4385   
4386
4387 task jalviewjs {
4388   group "JalviewJS"
4389   description "Build the JalviewJS site and run the launch test"
4390   dependsOn jalviewjsBuildSite
4391   dependsOn jalviewjsLaunchTest
4392 }