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