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