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