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