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