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