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