Merge branch 'develop' into doc/JAL-4090_Release_2_11_3_0
[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
1834     info.events = [ TestLogEvent.FAILED ]
1835   }
1836
1837   if (OperatingSystem.current().isMacOsX()) {
1838     testTask.systemProperty "apple.awt.UIElement", "true"
1839     testTask.environment "JAVA_TOOL_OPTIONS", "-Dapple.awt.UIElement=true"
1840   }
1841
1842
1843   ignoreFailures = true // Always try to run all tests for all modules
1844
1845   afterSuite { desc, result ->
1846     if (desc.parent)
1847       return // Only summarize results for whole modules
1848
1849     def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
1850
1851     rootProject.ext.testsResults.add(resultsInfo)
1852   }
1853
1854   // from original test task
1855   maxHeapSize = "1024m"
1856
1857   workingDir = jalviewDir
1858   def testLaf = project.findProperty("test_laf")
1859   if (testLaf != null) {
1860     println("Setting Test LaF to '${testLaf}'")
1861     systemProperty "laf", testLaf
1862   }
1863   def testHiDPIScale = project.findProperty("test_HiDPIScale")
1864   if (testHiDPIScale != null) {
1865     println("Setting Test HiDPI Scale to '${testHiDPIScale}'")
1866     systemProperty "sun.java2d.uiScale", testHiDPIScale
1867   }
1868   sourceCompatibility = compile_source_compatibility
1869   targetCompatibility = compile_target_compatibility
1870   jvmArgs += additional_compiler_args
1871
1872   doFirst {
1873     // this is not perfect yet -- we should only add the commandLineIncludePatterns to the
1874     // testTasks that include the tests, and exclude all from the others.
1875     // get --test argument
1876     filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns
1877     // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the
1878     // commandLineIncludePatterns not matching anything.  Instead we are doing setFailOnNoMatchingTests(false) below
1879
1880
1881     if (useClover) {
1882       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
1883     }
1884   }
1885
1886
1887   /* don't fail on no matching tests (so --tests will run across all testTasks) */
1888   testTask.filter.setFailOnNoMatchingTests(false)
1889
1890   /* ensure the "test" task dependsOn all the testTasks */
1891   test.dependsOn testTask
1892 }
1893
1894 gradle.buildFinished {
1895     def allResults = rootProject.ext.testsResults
1896
1897     if (!allResults.isEmpty()) {
1898         printResults allResults
1899         allResults.each {r ->
1900           if (r[2].resultType == TestResult.ResultType.FAILURE)
1901             throw new GradleException("Failed tests!")
1902         }
1903     }
1904 }
1905
1906 private static String colString(styler, col, colour, text) {
1907   return col?"${styler[colour](text)}":text
1908 }
1909
1910 private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) {
1911   def colour = 'black'
1912   def text = rt
1913   def nocol = false
1914   if (rc == 0) {
1915     text = "-----"
1916     nocol = true
1917   } else {
1918     switch(rt) {
1919       case TestResult.ResultType.SUCCESS:
1920         colour = 'green'
1921         break;
1922       case TestResult.ResultType.FAILURE:
1923         colour = 'red'
1924         break;
1925       default:
1926         nocol = true
1927         break;
1928     }
1929   }
1930   StringBuilder sb = new StringBuilder()
1931   sb.append("${pn}")
1932   if (tn != null)
1933     sb.append(":${tn}")
1934   sb.append(" results: ")
1935   sb.append(colString(s, col && !nocol, colour, text))
1936   sb.append(" (")
1937   sb.append("${rc} tests, ")
1938   sb.append(colString(s, col && rs > 0, 'green', rs))
1939   sb.append(" successes, ")
1940   sb.append(colString(s, col && rf > 0, 'red', rf))
1941   sb.append(" failures, ")
1942   sb.append("${rsk} skipped) in ${t}")
1943   return sb.toString()
1944 }
1945
1946 private static void printResults(allResults) {
1947
1948     // styler from https://stackoverflow.com/a/56139852
1949     def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] }
1950
1951     def maxLength = 0
1952     def failedTests = false
1953     def summaryLines = []
1954     def totalcount = 0
1955     def totalsuccess = 0
1956     def totalfail = 0
1957     def totalskip = 0
1958     def totaltime = TimeCategory.getSeconds(0)
1959     // sort on project name then task name
1960     allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each {
1961       def projectName = it[0]
1962       def taskName = it[1]
1963       def result = it[2]
1964       def time = it[3]
1965       def report = it[4]
1966       def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true)
1967       def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false)
1968       def reportLine = "Report file: ${report}"
1969       def ls = summaryPlain.length()
1970       def lr = reportLine.length()
1971       def m = [ls, lr].max()
1972       if (m > maxLength)
1973         maxLength = m
1974       def info = [ls, summaryCol, reportLine]
1975       summaryLines.add(info)
1976       failedTests |= result.resultType == TestResult.ResultType.FAILURE
1977       totalcount += result.testCount
1978       totalsuccess += result.successfulTestCount
1979       totalfail += result.failedTestCount
1980       totalskip += result.skippedTestCount
1981       totaltime += time
1982     }
1983     def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true)
1984     def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false)
1985     def tls = totalSummaryPlain.length()
1986     if (tls > maxLength)
1987       maxLength = tls
1988     def info = [tls, totalSummaryCol, null]
1989     summaryLines.add(info)
1990
1991     def allSummaries = []
1992     for(sInfo : summaryLines) {
1993       def ls = sInfo[0]
1994       def summary = sInfo[1]
1995       def report = sInfo[2]
1996
1997       StringBuilder sb = new StringBuilder()
1998       sb.append("│" + summary + " " * (maxLength - ls) + "│")
1999       if (report != null) {
2000         sb.append("\n│" + report + " " * (maxLength - report.length()) + "│")
2001       }
2002       allSummaries += sb.toString()
2003     }
2004
2005     println "┌${"${"─" * maxLength}"}┐"
2006     println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n")
2007     println "└${"${"─" * maxLength}"}┘"
2008 }
2009 /* END of test tasks results summary */
2010
2011
2012 task compileLinkCheck(type: JavaCompile) {
2013   options.fork = true
2014   classpath = files("${jalviewDir}/${utils_dir}")
2015   destinationDir = file("${jalviewDir}/${utils_dir}")
2016   source = fileTree(dir: "${jalviewDir}/${utils_dir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])
2017
2018   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2019   inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
2020   outputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.class")
2021   outputs.file("${jalviewDir}/${utils_dir}/BufferedLineReader.class")
2022 }
2023
2024
2025 task linkCheck(type: JavaExec) {
2026   dependsOn prepare
2027   dependsOn compileLinkCheck
2028
2029   def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
2030   classpath = files("${jalviewDir}/${utils_dir}")
2031   main = "HelpLinksChecker"
2032   workingDir = "${helpBuildDir}"
2033   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
2034
2035   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
2036   standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
2037     outFOS,
2038     System.out)
2039   errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
2040     outFOS,
2041     System.err)
2042
2043   inputs.dir(helpBuildDir)
2044   outputs.file(helpLinksCheckerOutFile)
2045 }
2046
2047
2048 // import the pubhtmlhelp target
2049 ant.properties.basedir = "${jalviewDir}"
2050 ant.properties.helpBuildDir = "${helpBuildDir}/${help_dir}"
2051 ant.importBuild "${utils_dir}/publishHelp.xml"
2052
2053
2054 task cleanPackageDir(type: Delete) {
2055   doFirst {
2056     delete fileTree(dir: "${jalviewDir}/${package_dir}", include: "*.jar")
2057   }
2058 }
2059
2060
2061 jar {
2062   dependsOn prepare
2063   dependsOn linkCheck
2064
2065   manifest {
2066     attributes "Main-Class": main_class,
2067     "Permissions": "all-permissions",
2068     "Application-Name": applicationName,
2069     "Codebase": application_codebase,
2070     "Implementation-Version": JALVIEW_VERSION
2071   }
2072
2073   def outputDir = "${jalviewDir}/${package_dir}"
2074   destinationDirectory = file(outputDir)
2075   archiveFileName = rootProject.name+".jar"
2076   duplicatesStrategy "EXCLUDE"
2077
2078
2079   exclude "cache*/**"
2080   exclude "*.jar"
2081   exclude "*.jar.*"
2082   exclude "**/*.jar"
2083   exclude "**/*.jar.*"
2084
2085   inputs.dir(sourceSets.main.java.outputDir)
2086   sourceSets.main.resources.srcDirs.each{ dir ->
2087     inputs.dir(dir)
2088   }
2089   outputs.file("${outputDir}/${archiveFileName}")
2090 }
2091
2092
2093 task copyJars(type: Copy) {
2094   from fileTree(dir: classesDir, include: "**/*.jar").files
2095   into "${jalviewDir}/${package_dir}"
2096 }
2097
2098
2099 // doing a Sync instead of Copy as Copy doesn't deal with "outputs" very well
2100 task syncJars(type: Sync) {
2101   dependsOn jar
2102   from fileTree(dir: "${jalviewDir}/${libDistDir}", include: "**/*.jar").files
2103   into "${jalviewDir}/${package_dir}"
2104   preserve {
2105     include jar.archiveFileName.getOrNull()
2106   }
2107 }
2108
2109
2110 task makeDist {
2111   group = "build"
2112   description = "Put all required libraries in dist"
2113   // order of "cleanPackageDir", "copyJars", "jar" important!
2114   jar.mustRunAfter cleanPackageDir
2115   syncJars.mustRunAfter cleanPackageDir
2116   dependsOn cleanPackageDir
2117   dependsOn syncJars
2118   dependsOn jar
2119   outputs.dir("${jalviewDir}/${package_dir}")
2120 }
2121
2122
2123 task cleanDist {
2124   dependsOn cleanPackageDir
2125   dependsOn cleanTest
2126   dependsOn clean
2127 }
2128
2129
2130 shadowJar {
2131   group = "distribution"
2132   description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar"
2133   if (buildDist) {
2134     dependsOn makeDist
2135   }
2136   from ("${jalviewDir}/${libDistDir}") {
2137     include("*.jar")
2138   }
2139   manifest {
2140     attributes "Implementation-Version": JALVIEW_VERSION,
2141     "Application-Name": applicationName
2142   }
2143
2144   duplicatesStrategy "INCLUDE"
2145
2146   mainClassName = shadow_jar_main_class
2147   mergeServiceFiles()
2148   classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
2149   minimize()
2150 }
2151
2152 task getdownImagesCopy() {
2153   inputs.dir getdownImagesDir
2154   outputs.dir getdownImagesBuildDir
2155
2156   doFirst {
2157     copy {
2158       from(getdownImagesDir) {
2159         include("*getdown*.png")
2160       }
2161       into getdownImagesBuildDir
2162     }
2163   }
2164 }
2165
2166 task getdownImagesProcess() {
2167   dependsOn getdownImagesCopy
2168
2169   doFirst {
2170     if (backgroundImageText) {
2171       if (convertBinary == null) {
2172         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2173       }
2174       if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
2175         throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2176       }
2177       fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
2178         exec {
2179           executable convertBinary
2180           args = [
2181             file.getPath(),
2182             '-font', getdown_background_image_text_font,
2183             '-fill', getdown_background_image_text_colour,
2184             '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
2185             '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2186             '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2187             file.getPath()
2188           ]
2189         }
2190       }
2191     }
2192   }
2193 }
2194
2195 task getdownImages() {
2196   dependsOn getdownImagesProcess
2197 }
2198
2199 task getdownWebsite() {
2200   group = "distribution"
2201   description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
2202
2203   dependsOn getdownImages
2204   if (buildDist) {
2205     dependsOn makeDist
2206   }
2207
2208   def getdownWebsiteResourceFilenames = []
2209   def getdownResourceDir = getdownResourceDir
2210   def getdownResourceFilenames = []
2211
2212   doFirst {
2213     // clean the getdown website and files dir before creating getdown folders
2214     delete getdownAppBaseDir
2215     delete getdownFilesDir
2216
2217     copy {
2218       from buildProperties
2219       rename(file(buildProperties).getName(), getdown_build_properties)
2220       into getdownAppDir
2221     }
2222     getdownWebsiteResourceFilenames += "${getdownAppDistDir}/${getdown_build_properties}"
2223
2224     copy {
2225       from channelPropsFile
2226       filter(ReplaceTokens,
2227         beginToken: '__',
2228         endToken: '__',
2229         tokens: [
2230           'SUFFIX': channelSuffix
2231         ]
2232       )
2233       into getdownAppBaseDir
2234     }
2235     getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
2236
2237     // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
2238     def props = project.properties.sort { it.key }
2239     if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
2240       props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
2241     }
2242     if (getdownAltJavaMaxVersion != null && getdownAltJavaMaxVersion.length() > 0) {
2243       props.put("getdown_txt_java_max_version", getdownAltJavaMaxVersion)
2244     }
2245     if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
2246       props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
2247     }
2248     if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
2249       props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
2250       props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}")
2251       props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}")
2252       props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}")
2253       props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
2254       props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
2255     }
2256
2257     props.put("getdown_txt_title", jalview_name)
2258     props.put("getdown_txt_ui.name", applicationName)
2259
2260     // start with appbase
2261     getdownTextLines += "appbase = ${getdownAppBase}"
2262     props.each{ prop, val ->
2263       if (prop.startsWith("getdown_txt_") && val != null) {
2264         if (prop.startsWith("getdown_txt_multi_")) {
2265           def key = prop.substring(18)
2266           val.split(",").each{ v ->
2267             def line = "${key} = ${v}"
2268             getdownTextLines += line
2269           }
2270         } else {
2271           // file values rationalised
2272           if (val.indexOf('/') > -1 || prop.startsWith("getdown_txt_resource")) {
2273             def r = null
2274             if (val.indexOf('/') == 0) {
2275               // absolute path
2276               r = file(val)
2277             } else if (val.indexOf('/') > 0) {
2278               // relative path (relative to jalviewDir)
2279               r = file( "${jalviewDir}/${val}" )
2280             }
2281             if (r.exists()) {
2282               val = "${getdown_resource_dir}/" + r.getName()
2283               getdownWebsiteResourceFilenames += val
2284               getdownResourceFilenames += r.getPath()
2285             }
2286           }
2287           if (! prop.startsWith("getdown_txt_resource")) {
2288             def line = prop.substring(12) + " = ${val}"
2289             getdownTextLines += line
2290           }
2291         }
2292       }
2293     }
2294
2295     getdownWebsiteResourceFilenames.each{ filename ->
2296       getdownTextLines += "resource = ${filename}"
2297     }
2298     getdownResourceFilenames.each{ filename ->
2299       copy {
2300         from filename
2301         into getdownResourceDir
2302       }
2303     }
2304     
2305     def getdownWrapperScripts = [ getdown_bash_wrapper_script, getdown_powershell_wrapper_script, getdown_batch_wrapper_script ]
2306     getdownWrapperScripts.each{ script ->
2307       def s = file( "${jalviewDir}/utils/getdown/${getdown_wrapper_script_dir}/${script}" )
2308       if (s.exists()) {
2309         copy {
2310           from s
2311           into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
2312         }
2313         getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
2314       }
2315     }
2316
2317     def codeFiles = []
2318     fileTree(file(package_dir)).each{ f ->
2319       if (f.isDirectory()) {
2320         def files = fileTree(dir: f, include: ["*"]).getFiles()
2321         codeFiles += files
2322       } else if (f.exists()) {
2323         codeFiles += f
2324       }
2325     }
2326     def jalviewJar = jar.archiveFileName.getOrNull()
2327     // put jalview.jar first for CLASSPATH and .properties files reasons
2328     codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
2329       def name = f.getName()
2330       def line = "code = ${getdownAppDistDir}/${name}"
2331       getdownTextLines += line
2332       copy {
2333         from f.getPath()
2334         into getdownAppDir
2335       }
2336     }
2337
2338     // NOT USING MODULES YET, EVERYTHING SHOULD BE IN dist
2339     /*
2340     if (JAVA_VERSION.equals("11")) {
2341     def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
2342     j11libFiles.sort().each{f ->
2343     def name = f.getName()
2344     def line = "code = ${getdown_j11lib_dir}/${name}"
2345     getdownTextLines += line
2346     copy {
2347     from f.getPath()
2348     into getdownJ11libDir
2349     }
2350     }
2351     }
2352      */
2353
2354     // 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.
2355     //getdownTextLines += "class = " + file(getdownLauncher).getName()
2356     getdownTextLines += "resource = ${getdown_launcher_new}"
2357     getdownTextLines += "class = ${main_class}"
2358     // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
2359     if (getdownSetAppBaseProperty) {
2360       getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
2361       getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
2362     }
2363
2364     def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
2365     getdownTxt.write(getdownTextLines.join("\n"))
2366
2367     getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
2368     def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
2369     launchJvl.write("appbase=${getdownAppBase}")
2370
2371     // files going into the getdown website dir: getdown-launcher.jar
2372     copy {
2373       from getdownLauncher
2374       rename(file(getdownLauncher).getName(), getdown_launcher_new)
2375       into getdownAppBaseDir
2376     }
2377
2378     // files going into the getdown website dir: getdown-launcher(-local).jar
2379     copy {
2380       from getdownLauncher
2381       if (file(getdownLauncher).getName() != getdown_launcher) {
2382         rename(file(getdownLauncher).getName(), getdown_launcher)
2383       }
2384       into getdownAppBaseDir
2385     }
2386
2387     // files going into the getdown website dir: ./install dir and files
2388     if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
2389       copy {
2390         from getdownTxt
2391         from getdownLauncher
2392         from "${getdownAppDir}/${getdown_build_properties}"
2393         if (file(getdownLauncher).getName() != getdown_launcher) {
2394           rename(file(getdownLauncher).getName(), getdown_launcher)
2395         }
2396         into getdownInstallDir
2397       }
2398
2399       // and make a copy in the getdown files dir (these are not downloaded by getdown)
2400       copy {
2401         from getdownInstallDir
2402         into getdownFilesInstallDir
2403       }
2404     }
2405
2406     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2407     copy {
2408       from getdownTxt
2409       from launchJvl
2410       from getdownLauncher
2411       from "${getdownAppBaseDir}/${getdown_build_properties}"
2412       from "${getdownAppBaseDir}/${channel_props}"
2413       if (file(getdownLauncher).getName() != getdown_launcher) {
2414         rename(file(getdownLauncher).getName(), getdown_launcher)
2415       }
2416       into getdownFilesDir
2417     }
2418
2419     // and ./resource (not all downloaded by getdown)
2420     copy {
2421       from getdownResourceDir
2422       into "${getdownFilesDir}/${getdown_resource_dir}"
2423     }
2424   }
2425
2426   if (buildDist) {
2427     inputs.dir("${jalviewDir}/${package_dir}")
2428   }
2429   outputs.dir(getdownAppBaseDir)
2430   outputs.dir(getdownFilesDir)
2431 }
2432
2433
2434 // a helper task to allow getdown digest of any dir: `gradle getdownDigestDir -PDIGESTDIR=/path/to/my/random/getdown/dir
2435 task getdownDigestDir(type: JavaExec) {
2436   group "Help"
2437   description "A task to run a getdown Digest on a dir with getdown.txt. Provide a DIGESTDIR property via -PDIGESTDIR=..."
2438
2439   def digestDirPropertyName = "DIGESTDIR"
2440   doFirst {
2441     classpath = files(getdownLauncher)
2442     def digestDir = findProperty(digestDirPropertyName)
2443     if (digestDir == null) {
2444       throw new GradleException("Must provide a DIGESTDIR value to produce an alternative getdown digest")
2445     }
2446     args digestDir
2447   }
2448   main = "com.threerings.getdown.tools.Digester"
2449 }
2450
2451
2452 task getdownDigest(type: JavaExec) {
2453   group = "distribution"
2454   description = "Digest the getdown website folder"
2455   dependsOn getdownWebsite
2456   doFirst {
2457     classpath = files(getdownLauncher)
2458   }
2459   main = "com.threerings.getdown.tools.Digester"
2460   args getdownAppBaseDir
2461   inputs.dir(getdownAppBaseDir)
2462   outputs.file("${getdownAppBaseDir}/digest2.txt")
2463 }
2464
2465
2466 task getdown() {
2467   group = "distribution"
2468   description = "Create the minimal and full getdown app folder for installers and website and create digest file"
2469   dependsOn getdownDigest
2470   doLast {
2471     if (reportRsyncCommand) {
2472       def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
2473       def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
2474       println "LIKELY RSYNC COMMAND:"
2475       println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
2476       if (RUNRSYNC == "true") {
2477         exec {
2478           commandLine "mkdir", "-p", toDir
2479         }
2480         exec {
2481           commandLine "rsync", "-avh", "--delete", fromDir, toDir
2482         }
2483       }
2484     }
2485   }
2486 }
2487
2488
2489 task getdownArchiveBuild() {
2490   group = "distribution"
2491   description = "Put files in the archive dir to go on the website"
2492
2493   dependsOn getdownWebsite
2494
2495   def v = "v${JALVIEW_VERSION_UNDERSCORES}"
2496   def vDir = "${getdownArchiveDir}/${v}"
2497   getdownFullArchiveDir = "${vDir}/getdown"
2498   getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
2499
2500   def vAltDir = "alt_${v}"
2501   def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
2502
2503   doFirst {
2504     // cleanup old "old" dir
2505     delete getdownArchiveDir
2506
2507     def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
2508     getdownArchiveTxt.getParentFile().mkdirs()
2509     def getdownArchiveTextLines = []
2510     def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
2511
2512     // the libdir
2513     copy {
2514       from "${getdownAppBaseDir}/${getdownAppDistDir}"
2515       into "${getdownFullArchiveDir}/${vAltDir}"
2516     }
2517
2518     getdownTextLines.each { line ->
2519       line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
2520       line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
2521       line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
2522       line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
2523       line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
2524       line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
2525       // remove the existing resource = resource/ or bin/ lines
2526       if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
2527         getdownArchiveTextLines += line
2528       }
2529     }
2530
2531     // the resource dir -- add these files as resource lines in getdown.txt
2532     copy {
2533       from "${archiveImagesDir}"
2534       into "${getdownFullArchiveDir}/${getdown_resource_dir}"
2535       eachFile { file ->
2536         getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
2537       }
2538     }
2539
2540     getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
2541
2542     def vLaunchJvl = file(getdownVersionLaunchJvl)
2543     vLaunchJvl.getParentFile().mkdirs()
2544     vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
2545     def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
2546     def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
2547     // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
2548     //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
2549     java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
2550
2551     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
2552     copy {
2553       from getdownLauncher
2554       from "${getdownAppBaseDir}/${getdownLaunchJvl}"
2555       from "${getdownAppBaseDir}/${getdown_launcher_new}"
2556       from "${getdownAppBaseDir}/${channel_props}"
2557       if (file(getdownLauncher).getName() != getdown_launcher) {
2558         rename(file(getdownLauncher).getName(), getdown_launcher)
2559       }
2560       into getdownFullArchiveDir
2561     }
2562
2563   }
2564 }
2565
2566 task getdownArchiveDigest(type: JavaExec) {
2567   group = "distribution"
2568   description = "Digest the getdown archive folder"
2569
2570   dependsOn getdownArchiveBuild
2571
2572   doFirst {
2573     classpath = files(getdownLauncher)
2574     args getdownFullArchiveDir
2575   }
2576   main = "com.threerings.getdown.tools.Digester"
2577   inputs.dir(getdownFullArchiveDir)
2578   outputs.file("${getdownFullArchiveDir}/digest2.txt")
2579 }
2580
2581 task getdownArchive() {
2582   group = "distribution"
2583   description = "Build the website archive dir with getdown digest"
2584
2585   dependsOn getdownArchiveBuild
2586   dependsOn getdownArchiveDigest
2587 }
2588
2589 tasks.withType(JavaCompile) {
2590         options.encoding = 'UTF-8'
2591 }
2592
2593
2594 clean {
2595   doFirst {
2596     delete getdownAppBaseDir
2597     delete getdownFilesDir
2598     delete getdownArchiveDir
2599   }
2600 }
2601
2602
2603 install4j {
2604   if (file(install4jHomeDir).exists()) {
2605     // good to go!
2606   } else if (file(System.getProperty("user.home")+"/buildtools/install4j").exists()) {
2607     install4jHomeDir = System.getProperty("user.home")+"/buildtools/install4j"
2608   } else if (file("/Applications/install4j.app/Contents/Resources/app").exists()) {
2609     install4jHomeDir = "/Applications/install4j.app/Contents/Resources/app"
2610   }
2611   installDir(file(install4jHomeDir))
2612
2613   mediaTypes = Arrays.asList(install4j_media_types.split(","))
2614 }
2615
2616
2617 task copyInstall4jTemplate {
2618   def install4jTemplateFile = file("${install4jDir}/${install4j_template}")
2619   def install4jFileAssociationsFile = file("${install4jDir}/${install4j_installer_file_associations}")
2620   inputs.file(install4jTemplateFile)
2621   inputs.file(install4jFileAssociationsFile)
2622   inputs.property("CHANNEL", { CHANNEL })
2623   outputs.file(install4jConfFile)
2624
2625   doLast {
2626     def install4jConfigXml = new XmlParser().parse(install4jTemplateFile)
2627
2628     // turn off code signing if no OSX_KEYPASS
2629     if (OSX_KEYPASS == "") {
2630       install4jConfigXml.'**'.codeSigning.each { codeSigning ->
2631         codeSigning.'@macEnabled' = "false"
2632       }
2633       install4jConfigXml.'**'.windows.each { windows ->
2634         windows.'@runPostProcessor' = "false"
2635       }
2636     }
2637
2638     // disable install screen for OSX dmg (for 2.11.2.0)
2639     install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
2640       macosArchive.attributes().remove('executeSetupApp')
2641       macosArchive.attributes().remove('setupAppId')
2642     }
2643
2644     // turn off checksum creation for LOCAL channel
2645     def e = install4jConfigXml.application[0]
2646     e.'@createChecksums' = string(install4jCheckSums)
2647
2648     // put file association actions where placeholder action is
2649     def install4jFileAssociationsText = install4jFileAssociationsFile.text
2650     def fileAssociationActions = new XmlParser().parseText("<actions>${install4jFileAssociationsText}</actions>")
2651     install4jConfigXml.'**'.action.any { a -> // .any{} stops after the first one that returns true
2652       if (a.'@name' == 'EXTENSIONS_REPLACED_BY_GRADLE') {
2653         def parent = a.parent()
2654         parent.remove(a)
2655         fileAssociationActions.each { faa ->
2656             parent.append(faa)
2657         }
2658         // don't need to continue in .any loop once replacements have been made
2659         return true
2660       }
2661     }
2662
2663     // use Windows Program Group with Examples folder for RELEASE, and Program Group without Examples for everything else
2664     // NB we're deleting the /other/ one!
2665     // Also remove the examples subdir from non-release versions
2666     def customizedIdToDelete = "PROGRAM_GROUP_RELEASE"
2667     // 2.11.1.0 NOT releasing with the Examples folder in the Program Group
2668     if (false && CHANNEL=="RELEASE") { // remove 'false && ' to include Examples folder in RELEASE channel
2669       customizedIdToDelete = "PROGRAM_GROUP_NON_RELEASE"
2670     } else {
2671       // remove the examples subdir from Full File Set
2672       def files = install4jConfigXml.files[0]
2673       def fileset = files.filesets.fileset.find { fs -> fs.'@customizedId' == "FULL_FILE_SET" }
2674       def root = files.roots.root.find { r -> r.'@fileset' == fileset.'@id' }
2675       def mountPoint = files.mountPoints.mountPoint.find { mp -> mp.'@root' == root.'@id' }
2676       def dirEntry = files.entries.dirEntry.find { de -> de.'@mountPoint' == mountPoint.'@id' && de.'@subDirectory' == "examples" }
2677       dirEntry.parent().remove(dirEntry)
2678     }
2679     install4jConfigXml.'**'.action.any { a ->
2680       if (a.'@customizedId' == customizedIdToDelete) {
2681         def parent = a.parent()
2682         parent.remove(a)
2683         return true
2684       }
2685     }
2686
2687     // write install4j file
2688     install4jConfFile.text = XmlUtil.serialize(install4jConfigXml)
2689   }
2690 }
2691
2692
2693 clean {
2694   doFirst {
2695     delete install4jConfFile
2696   }
2697 }
2698
2699 task cleanInstallersDataFiles {
2700   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
2701   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
2702   def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
2703   doFirst {
2704     delete installersOutputTxt
2705     delete installersSha256
2706     delete hugoDataJsonFile
2707   }
2708 }
2709
2710 task install4jDMGBackgroundImageCopy {
2711   inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
2712   outputs.dir "${install4jDMGBackgroundImageBuildDir}"
2713   doFirst {
2714     copy {
2715       from(install4jDMGBackgroundImageDir) {
2716         include(install4jDMGBackgroundImageFile)
2717       }
2718       into install4jDMGBackgroundImageBuildDir
2719     }
2720   }
2721 }
2722
2723 task install4jDMGBackgroundImageProcess {
2724   dependsOn install4jDMGBackgroundImageCopy
2725
2726   doFirst {
2727     if (backgroundImageText) {
2728       if (convertBinary == null) {
2729         throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
2730       }
2731       if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
2732         throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
2733       }
2734       fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
2735         exec {
2736           executable convertBinary
2737           args = [
2738             file.getPath(),
2739             '-font', install4j_background_image_text_font,
2740             '-fill', install4j_background_image_text_colour,
2741             '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
2742             '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
2743             '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
2744             file.getPath()
2745           ]
2746         }
2747       }
2748     }
2749   }
2750 }
2751
2752 task install4jDMGBackgroundImage {
2753   dependsOn install4jDMGBackgroundImageProcess
2754 }
2755
2756 task installerFiles(type: com.install4j.gradle.Install4jTask) {
2757   group = "distribution"
2758   description = "Create the install4j installers"
2759   dependsOn getdown
2760   dependsOn copyInstall4jTemplate
2761   dependsOn cleanInstallersDataFiles
2762   dependsOn install4jDMGBackgroundImage
2763
2764   projectFile = install4jConfFile
2765
2766   // create an md5 for the input files to use as version for install4j conf file
2767   def digest = MessageDigest.getInstance("MD5")
2768   digest.update(
2769     (file("${install4jDir}/${install4j_template}").text + 
2770     file("${install4jDir}/${install4j_info_plist_file_associations}").text +
2771     file("${install4jDir}/${install4j_installer_file_associations}").text).bytes)
2772   def filesMd5 = new BigInteger(1, digest.digest()).toString(16)
2773   if (filesMd5.length() >= 8) {
2774     filesMd5 = filesMd5.substring(0,8)
2775   }
2776   def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"
2777
2778   variables = [
2779     'JALVIEW_NAME': jalview_name,
2780     'JALVIEW_APPLICATION_NAME': applicationName,
2781     'JALVIEW_DIR': "../..",
2782     'OSX_KEYSTORE': OSX_KEYSTORE,
2783     'OSX_APPLEID': OSX_APPLEID,
2784     'OSX_ALTOOLPASS': OSX_ALTOOLPASS,
2785     'JSIGN_SH': JSIGN_SH,
2786     'JRE_DIR': getdown_app_dir_java,
2787     'INSTALLER_TEMPLATE_VERSION': install4jTemplateVersion,
2788     'JALVIEW_VERSION': JALVIEW_VERSION,
2789     'JAVA_MIN_VERSION': JAVA_MIN_VERSION,
2790     'JAVA_MAX_VERSION': JAVA_MAX_VERSION,
2791     'JAVA_VERSION': JAVA_VERSION,
2792     'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
2793     'VERSION': JALVIEW_VERSION,
2794     'COPYRIGHT_MESSAGE': install4j_copyright_message,
2795     'BUNDLE_ID': install4jBundleId,
2796     'INTERNAL_ID': install4jInternalId,
2797     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
2798     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
2799     'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
2800     'WRAPPER_LINK': getdownWrapperLink,
2801     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
2802     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
2803     'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script,
2804     'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
2805     'INSTALLER_NAME': install4jInstallerName,
2806     'INSTALL4J_UTILS_DIR': install4j_utils_dir,
2807     'GETDOWN_CHANNEL_DIR': getdownChannelDir,
2808     'GETDOWN_FILES_DIR': getdown_files_dir,
2809     'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
2810     'GETDOWN_DIST_DIR': getdownAppDistDir,
2811     'GETDOWN_ALT_DIR': getdown_app_dir_alt,
2812     'GETDOWN_INSTALL_DIR': getdown_install_dir,
2813     'INFO_PLIST_FILE_ASSOCIATIONS_FILE': install4j_info_plist_file_associations,
2814     'BUILD_DIR': install4jBuildDir,
2815     'APPLICATION_CATEGORIES': install4j_application_categories,
2816     'APPLICATION_FOLDER': install4jApplicationFolder,
2817     'UNIX_APPLICATION_FOLDER': install4jUnixApplicationFolder,
2818     'EXECUTABLE_NAME': install4jExecutableName,
2819     'EXTRA_SCHEME': install4jExtraScheme,
2820     'MAC_ICONS_FILE': install4jMacIconsFile,
2821     'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
2822     'PNG_ICON_FILE': install4jPngIconFile,
2823     'BACKGROUND': install4jBackground,
2824   ]
2825
2826   def varNameMap = [
2827     'mac': 'MACOS',
2828     'windows': 'WINDOWS',
2829     'linux': 'LINUX'
2830   ]
2831   
2832   // these are the bundled OS/architecture VMs needed by install4j
2833   def osArch = [
2834     [ "mac", "x64" ],
2835     [ "mac", "aarch64" ],
2836     [ "windows", "x64" ],
2837     [ "linux", "x64" ],
2838     [ "linux", "aarch64" ]
2839   ]
2840   osArch.forEach { os, arch ->
2841     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)
2842     // N.B. For some reason install4j requires the below filename to have underscores and not hyphens
2843     // otherwise running `gradle installers` generates a non-useful error:
2844     // `install4j: compilation failed. Reason: java.lang.NumberFormatException: For input string: "windows"`
2845     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)
2846   }
2847
2848   //println("INSTALL4J VARIABLES:")
2849   //variables.each{k,v->println("${k}=${v}")}
2850
2851   destination = "${jalviewDir}/${install4jBuildDir}"
2852   buildSelected = true
2853
2854   if (install4j_faster.equals("true") || CHANNEL.startsWith("LOCAL")) {
2855     faster = true
2856     disableSigning = true
2857     disableNotarization = true
2858   }
2859
2860   if (OSX_KEYPASS) {
2861     macKeystorePassword = OSX_KEYPASS
2862   } 
2863   
2864   if (OSX_ALTOOLPASS) {
2865     appleIdPassword = OSX_ALTOOLPASS
2866     disableNotarization = false
2867   } else {
2868     disableNotarization = true
2869   }
2870
2871   doFirst {
2872     println("Using projectFile "+projectFile)
2873     if (!disableNotarization) { println("Will notarize OSX App DMG") }
2874   }
2875   //verbose=true
2876
2877   inputs.dir(getdownAppBaseDir)
2878   inputs.file(install4jConfFile)
2879   inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
2880   outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
2881 }
2882
2883 def getDataHash(File myFile) {
2884   HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
2885   return myFile.exists()
2886   ? [
2887       "file" : myFile.getName(),
2888       "filesize" : myFile.length(),
2889       "sha256" : hash.toString()
2890     ]
2891   : null
2892 }
2893
2894 def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
2895   def hash = [
2896     "channel" : getdownChannelName,
2897     "date" : getDate("yyyy-MM-dd HH:mm:ss"),
2898     "git-commit" : "${gitHash} [${gitBranch}]",
2899     "version" : JALVIEW_VERSION
2900   ]
2901   // install4j installer files
2902   if (installersOutputTxt.exists()) {
2903     def idHash = [:]
2904     installersOutputTxt.readLines().each { def line ->
2905       if (line.startsWith("#")) {
2906         return;
2907       }
2908       line.replaceAll("\n","")
2909       def vals = line.split("\t")
2910       def filename = vals[3]
2911       def filesize = file(filename).length()
2912       filename = filename.replaceAll(/^.*\//, "")
2913       hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
2914       idHash."${filename}" = vals[0]
2915     }
2916     if (install4jCheckSums && installersSha256.exists()) {
2917       installersSha256.readLines().each { def line ->
2918         if (line.startsWith("#")) {
2919           return;
2920         }
2921         line.replaceAll("\n","")
2922         def vals = line.split(/\s+\*?/)
2923         def filename = vals[1]
2924         def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
2925       }
2926     }
2927   }
2928
2929   [
2930     "JAR": shadowJar.archiveFile, // executable JAR
2931     "JVL": getdownVersionLaunchJvl, // version JVL
2932     "SOURCE": sourceDist.archiveFile // source TGZ
2933   ].each { key, value ->
2934     def file = file(value)
2935     if (file.exists()) {
2936       def fileHash = getDataHash(file)
2937       if (fileHash != null) {
2938         hash."${key}" = fileHash;
2939       }
2940     }
2941   }
2942   return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
2943 }
2944
2945 task staticMakeInstallersJsonFile {
2946   doFirst {
2947     def output = findProperty("i4j_output")
2948     def sha256 = findProperty("i4j_sha256")
2949     def json = findProperty("i4j_json")
2950     if (output == null || sha256 == null || json == null) {
2951       throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
2952     }
2953     writeDataJsonFile(file(output), file(sha256), file(json))
2954   }
2955 }
2956
2957 task installers {
2958   dependsOn installerFiles
2959 }
2960
2961
2962 spotless {
2963   java {
2964     eclipse().configFile(eclipse_codestyle_file)
2965   }
2966 }
2967
2968 task createSourceReleaseProperties(type: WriteProperties) {
2969   group = "distribution"
2970   description = "Create the source RELEASE properties file"
2971   
2972   def sourceTarBuildDir = "${buildDir}/sourceTar"
2973   def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
2974   outputFile (sourceReleasePropertiesFile)
2975
2976   doFirst {
2977     releaseProps.each{ key, val -> property key, val }
2978     property "git.branch", gitBranch
2979     property "git.hash", gitHash
2980   }
2981
2982   outputs.file(outputFile)
2983 }
2984
2985 task sourceDist(type: Tar) {
2986   group "distribution"
2987   description "Create a source .tar.gz file for distribution"
2988
2989   dependsOn createBuildProperties
2990   dependsOn convertMdFiles
2991   dependsOn eclipseAllPreferences
2992   dependsOn createSourceReleaseProperties
2993
2994
2995   def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
2996   archiveFileName = outputFileName
2997   
2998   compression Compression.GZIP
2999   
3000   into project.name
3001
3002   def EXCLUDE_FILES=[
3003     "build/*",
3004     "bin/*",
3005     "test-output/",
3006     "test-reports",
3007     "tests",
3008     "clover*/*",
3009     ".*",
3010     "benchmarking/*",
3011     "**/.*",
3012     "*.class",
3013     "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
3014     "*locales/**",
3015     "utils/InstallAnywhere",
3016     "**/*.log",
3017     "RELEASE",
3018   ] 
3019   def PROCESS_FILES=[
3020     "AUTHORS",
3021     "CITATION",
3022     "FEATURETODO",
3023     "JAVA-11-README",
3024     "FEATURETODO",
3025     "LICENSE",
3026     "**/README",
3027     "THIRDPARTYLIBS",
3028     "TESTNG",
3029     "build.gradle",
3030     "gradle.properties",
3031     "**/*.java",
3032     "**/*.html",
3033     "**/*.xml",
3034     "**/*.gradle",
3035     "**/*.groovy",
3036     "**/*.properties",
3037     "**/*.perl",
3038     "**/*.sh",
3039   ]
3040   def INCLUDE_FILES=[
3041     ".classpath",
3042     ".settings/org.eclipse.buildship.core.prefs",
3043     ".settings/org.eclipse.jdt.core.prefs"
3044   ]
3045
3046   from(jalviewDir) {
3047     exclude (EXCLUDE_FILES)
3048     include (PROCESS_FILES)
3049     filter(ReplaceTokens,
3050       beginToken: '$$',
3051       endToken: '$$',
3052       tokens: [
3053         'Version-Rel': JALVIEW_VERSION,
3054         'Year-Rel': getDate("yyyy")
3055       ]
3056     )
3057   }
3058   from(jalviewDir) {
3059     exclude (EXCLUDE_FILES)
3060     exclude (PROCESS_FILES)
3061     exclude ("appletlib")
3062     exclude ("**/*locales")
3063     exclude ("*locales/**")
3064     exclude ("utils/InstallAnywhere")
3065
3066     exclude (getdown_files_dir)
3067     // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
3068     //exclude (getdown_website_dir)
3069     //exclude (getdown_archive_dir)
3070
3071     // exluding these as not using jars as modules yet
3072     exclude ("${j11modDir}/**/*.jar")
3073   }
3074   from(jalviewDir) {
3075     include(INCLUDE_FILES)
3076   }
3077 //  from (jalviewDir) {
3078 //    // explicit includes for stuff that seemed to not get included
3079 //    include(fileTree("test/**/*."))
3080 //    exclude(EXCLUDE_FILES)
3081 //    exclude(PROCESS_FILES)
3082 //  }
3083
3084   from(file(buildProperties).getParent()) {
3085     include(file(buildProperties).getName())
3086     rename(file(buildProperties).getName(), "build_properties")
3087     filter({ line ->
3088       line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
3089     })
3090   }
3091
3092   def sourceTarBuildDir = "${buildDir}/sourceTar"
3093   from(sourceTarBuildDir) {
3094     // this includes the appended RELEASE properties file
3095   }
3096 }
3097
3098 task dataInstallersJson {
3099   group "website"
3100   description "Create the installers-VERSION.json data file for installer files created"
3101
3102   mustRunAfter installers
3103   mustRunAfter shadowJar
3104   mustRunAfter sourceDist
3105   mustRunAfter getdownArchive
3106
3107   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
3108   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
3109
3110   if (installersOutputTxt.exists()) {
3111     inputs.file(installersOutputTxt)
3112   }
3113   if (install4jCheckSums && installersSha256.exists()) {
3114     inputs.file(installersSha256)
3115   }
3116   [
3117     shadowJar.archiveFile, // executable JAR
3118     getdownVersionLaunchJvl, // version JVL
3119     sourceDist.archiveFile // source TGZ
3120   ].each { fileName ->
3121     if (file(fileName).exists()) {
3122       inputs.file(fileName)
3123     }
3124   }
3125
3126   outputs.file(hugoDataJsonFile)
3127
3128   doFirst {
3129     writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
3130   }
3131 }
3132
3133 task helppages {
3134   group "help"
3135   description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
3136
3137   dependsOn copyHelp
3138   dependsOn pubhtmlhelp
3139   
3140   inputs.dir("${helpBuildDir}/${help_dir}")
3141   outputs.dir("${buildDir}/distributions/${help_dir}")
3142 }
3143
3144
3145 task j2sSetHeadlessBuild {
3146   doFirst {
3147     IN_ECLIPSE = false
3148   }
3149 }
3150
3151
3152 task jalviewjsEnableAltFileProperty(type: WriteProperties) {
3153   group "jalviewjs"
3154   description "Enable the alternative J2S Config file for headless build"
3155
3156   outputFile = jalviewjsJ2sSettingsFileName
3157   def j2sPropsFile = file(jalviewjsJ2sSettingsFileName)
3158   def j2sProps = new Properties()
3159   if (j2sPropsFile.exists()) {
3160     try {
3161       def j2sPropsFileFIS = new FileInputStream(j2sPropsFile)
3162       j2sProps.load(j2sPropsFileFIS)
3163       j2sPropsFileFIS.close()
3164
3165       j2sProps.each { prop, val ->
3166         property(prop, val)
3167       }
3168     } catch (Exception e) {
3169       println("Exception reading ${jalviewjsJ2sSettingsFileName}")
3170       e.printStackTrace()
3171     }
3172   }
3173   if (! j2sProps.stringPropertyNames().contains(jalviewjs_j2s_alt_file_property_config)) {
3174     property(jalviewjs_j2s_alt_file_property_config, jalviewjs_j2s_alt_file_property)
3175   }
3176 }
3177
3178
3179 task jalviewjsSetEclipseWorkspace {
3180   def propKey = "jalviewjs_eclipse_workspace"
3181   def propVal = null
3182   if (project.hasProperty(propKey)) {
3183     propVal = project.getProperty(propKey)
3184     if (propVal.startsWith("~/")) {
3185       propVal = System.getProperty("user.home") + propVal.substring(1)
3186     }
3187   }
3188   def propsFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_workspace_location_file}"
3189   def propsFile = file(propsFileName)
3190   def eclipseWsDir = propVal
3191   def props = new Properties()
3192
3193   def writeProps = true
3194   if (( eclipseWsDir == null || !file(eclipseWsDir).exists() ) && propsFile.exists()) {
3195     def ins = new FileInputStream(propsFileName)
3196     props.load(ins)
3197     ins.close()
3198     if (props.getProperty(propKey, null) != null) {
3199       eclipseWsDir = props.getProperty(propKey)
3200       writeProps = false
3201     }
3202   }
3203
3204   if (eclipseWsDir == null || !file(eclipseWsDir).exists()) {
3205     def tempDir = File.createTempDir()
3206     eclipseWsDir = tempDir.getAbsolutePath()
3207     writeProps = true
3208   }
3209   eclipseWorkspace = file(eclipseWsDir)
3210
3211   doFirst {
3212     // do not run a headless transpile when we claim to be in Eclipse
3213     if (IN_ECLIPSE) {
3214       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3215       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3216     } else {
3217       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3218     }
3219
3220     if (writeProps) {
3221       props.setProperty(propKey, eclipseWsDir)
3222       propsFile.parentFile.mkdirs()
3223       def bytes = new ByteArrayOutputStream()
3224       props.store(bytes, null)
3225       def propertiesString = bytes.toString()
3226       propsFile.text = propertiesString
3227       print("NEW ")
3228     } else {
3229       print("EXISTING ")
3230     }
3231
3232     println("ECLIPSE WORKSPACE: "+eclipseWorkspace.getPath())
3233   }
3234
3235   //inputs.property(propKey, eclipseWsDir) // eclipseWsDir only gets set once this task runs, so will be out-of-date
3236   outputs.file(propsFileName)
3237   outputs.upToDateWhen { eclipseWorkspace.exists() && propsFile.exists() }
3238 }
3239
3240
3241 task jalviewjsEclipsePaths {
3242   def eclipseProduct
3243
3244   def eclipseRoot = jalviewjs_eclipse_root
3245   if (eclipseRoot.startsWith("~/")) {
3246     eclipseRoot = System.getProperty("user.home") + eclipseRoot.substring(1)
3247   }
3248   if (OperatingSystem.current().isMacOsX()) {
3249     eclipseRoot += "/Eclipse.app"
3250     eclipseBinary = "${eclipseRoot}/Contents/MacOS/eclipse"
3251     eclipseProduct = "${eclipseRoot}/Contents/Eclipse/.eclipseproduct"
3252   } else if (OperatingSystem.current().isWindows()) { // check these paths!!
3253     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3254       eclipseRoot += "/eclipse"
3255     }
3256     eclipseBinary = "${eclipseRoot}/eclipse.exe"
3257     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3258   } else { // linux or unix
3259     if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
3260       eclipseRoot += "/eclipse"
3261 println("eclipseDir exists")
3262     }
3263     eclipseBinary = "${eclipseRoot}/eclipse"
3264     eclipseProduct = "${eclipseRoot}/.eclipseproduct"
3265   }
3266
3267   eclipseVersion = "4.13" // default
3268   def assumedVersion = true
3269   if (file(eclipseProduct).exists()) {
3270     def fis = new FileInputStream(eclipseProduct)
3271     def props = new Properties()
3272     props.load(fis)
3273     eclipseVersion = props.getProperty("version")
3274     fis.close()
3275     assumedVersion = false
3276   }
3277   
3278   def propKey = "eclipse_debug"
3279   eclipseDebug = (project.hasProperty(propKey) && project.getProperty(propKey).equals("true"))
3280
3281   doFirst {
3282     // do not run a headless transpile when we claim to be in Eclipse
3283     if (IN_ECLIPSE) {
3284       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3285       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3286     } else {
3287       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3288     }
3289
3290     if (!assumedVersion) {
3291       println("ECLIPSE VERSION=${eclipseVersion}")
3292     }
3293   }
3294 }
3295
3296
3297 task printProperties {
3298   group "Debug"
3299   description "Output to console all System.properties"
3300   doFirst {
3301     System.properties.each { key, val -> System.out.println("Property: ${key}=${val}") }
3302   }
3303 }
3304
3305
3306 task eclipseSetup {
3307   dependsOn eclipseProject
3308   dependsOn eclipseClasspath
3309   dependsOn eclipseJdt
3310 }
3311
3312
3313 // this version (type: Copy) will delete anything in the eclipse dropins folder that isn't in fromDropinsDir
3314 task jalviewjsEclipseCopyDropins(type: Copy) {
3315   dependsOn jalviewjsEclipsePaths
3316
3317   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_eclipse_dropins_dir}", include: "*.jar")
3318   inputFiles += file("${jalviewDir}/${jalviewjsJ2sPlugin}")
3319   def outputDir = "${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}"
3320
3321   from inputFiles
3322   into outputDir
3323 }
3324
3325
3326 // this eclipse -clean doesn't actually work
3327 task jalviewjsCleanEclipse(type: Exec) {
3328   dependsOn eclipseSetup
3329   dependsOn jalviewjsEclipsePaths
3330   dependsOn jalviewjsEclipseCopyDropins
3331
3332   executable(eclipseBinary)
3333   args(["-nosplash", "--launcher.suppressErrors", "-data", eclipseWorkspace.getPath(), "-clean", "-console", "-consoleLog"])
3334   if (eclipseDebug) {
3335     args += "-debug"
3336   }
3337   args += "-l"
3338
3339   def inputString = """exit
3340 y
3341 """
3342   def inputByteStream = new ByteArrayInputStream(inputString.getBytes())
3343   standardInput = inputByteStream
3344 }
3345
3346 /* not really working yet
3347 jalviewjsEclipseCopyDropins.finalizedBy jalviewjsCleanEclipse
3348 */
3349
3350
3351 task jalviewjsTransferUnzipSwingJs {
3352   def file_zip = "${jalviewDir}/${jalviewjs_swingjs_zip}"
3353
3354   doLast {
3355     copy {
3356       from zipTree(file_zip)
3357       into "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3358     }
3359   }
3360
3361   inputs.file file_zip
3362   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3363 }
3364
3365
3366 task jalviewjsTransferUnzipLib {
3367   def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip")
3368
3369   doLast {
3370     zipFiles.each { file_zip -> 
3371       copy {
3372         from zipTree(file_zip)
3373         into "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3374       }
3375     }
3376   }
3377
3378   inputs.files zipFiles
3379   outputs.dir "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3380 }
3381
3382
3383 task jalviewjsTransferUnzipAllLibs {
3384   dependsOn jalviewjsTransferUnzipSwingJs
3385   dependsOn jalviewjsTransferUnzipLib
3386 }
3387
3388
3389 task jalviewjsCreateJ2sSettings(type: WriteProperties) {
3390   group "JalviewJS"
3391   description "Create the alternative j2s file from the j2s.* properties"
3392
3393   jalviewjsJ2sProps = project.properties.findAll { it.key.startsWith("j2s.") }.sort { it.key }
3394   def siteDirProperty = "j2s.site.directory"
3395   def setSiteDir = false
3396   jalviewjsJ2sProps.each { prop, val ->
3397     if (val != null) {
3398       if (prop == siteDirProperty) {
3399         if (!(val.startsWith('/') || val.startsWith("file://") )) {
3400           val = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${val}"
3401         }
3402         setSiteDir = true
3403       }
3404       property(prop,val)
3405     }
3406     if (!setSiteDir) { // default site location, don't override specifically set property
3407       property(siteDirProperty,"${jalviewDirRelativePath}/${jalviewjsTransferSiteJsDir}")
3408     }
3409   }
3410   outputFile = jalviewjsJ2sAltSettingsFileName
3411
3412   if (! IN_ECLIPSE) {
3413     inputs.properties(jalviewjsJ2sProps)
3414     outputs.file(jalviewjsJ2sAltSettingsFileName)
3415   }
3416 }
3417
3418
3419 task jalviewjsEclipseSetup {
3420   dependsOn jalviewjsEclipseCopyDropins
3421   dependsOn jalviewjsSetEclipseWorkspace
3422   dependsOn jalviewjsCreateJ2sSettings
3423 }
3424
3425
3426 task jalviewjsSyncAllLibs (type: Sync) {
3427   dependsOn jalviewjsTransferUnzipAllLibs
3428   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
3429   inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
3430   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3431
3432   from inputFiles
3433   into outputDir
3434   def outputFiles = []
3435   rename { filename ->
3436     outputFiles += "${outputDir}/${filename}"
3437     null
3438   }
3439   preserve {
3440     include "**"
3441   }
3442
3443   // should this be exclude really ?
3444   duplicatesStrategy "INCLUDE"
3445
3446   outputs.files outputFiles
3447   inputs.files inputFiles
3448 }
3449
3450
3451 task jalviewjsSyncResources (type: Sync) {
3452   dependsOn buildResources
3453
3454   def inputFiles = fileTree(dir: resourcesBuildDir)
3455   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3456
3457   from inputFiles
3458   into outputDir
3459   def outputFiles = []
3460   rename { filename ->
3461     outputFiles += "${outputDir}/${filename}"
3462     null
3463   }
3464   preserve {
3465     include "**"
3466   }
3467   outputs.files outputFiles
3468   inputs.files inputFiles
3469 }
3470
3471
3472 task jalviewjsSyncSiteResources (type: Sync) {
3473   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_site_resource_dir}")
3474   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3475
3476   from inputFiles
3477   into outputDir
3478   def outputFiles = []
3479   rename { filename ->
3480     outputFiles += "${outputDir}/${filename}"
3481     null
3482   }
3483   preserve {
3484     include "**"
3485   }
3486   outputs.files outputFiles
3487   inputs.files inputFiles
3488 }
3489
3490
3491 task jalviewjsSyncBuildProperties (type: Sync) {
3492   dependsOn createBuildProperties
3493   def inputFiles = [file(buildProperties)]
3494   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"
3495
3496   from inputFiles
3497   into outputDir
3498   def outputFiles = []
3499   rename { filename ->
3500     outputFiles += "${outputDir}/${filename}"
3501     null
3502   }
3503   preserve {
3504     include "**"
3505   }
3506   outputs.files outputFiles
3507   inputs.files inputFiles
3508 }
3509
3510
3511 task jalviewjsProjectImport(type: Exec) {
3512   dependsOn eclipseSetup
3513   dependsOn jalviewjsEclipsePaths
3514   dependsOn jalviewjsEclipseSetup
3515
3516   doFirst {
3517     // do not run a headless import when we claim to be in Eclipse
3518     if (IN_ECLIPSE) {
3519       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3520       throw new StopExecutionException("Not running headless import whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3521     } else {
3522       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3523     }
3524   }
3525
3526   //def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview/org.eclipse.jdt.core"
3527   def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview"
3528   executable(eclipseBinary)
3529   args(["-nosplash", "--launcher.suppressErrors", "-application", "com.seeq.eclipse.importprojects.headlessimport", "-data", eclipseWorkspace.getPath(), "-import", jalviewDirAbsolutePath])
3530   if (eclipseDebug) {
3531     args += "-debug"
3532   }
3533   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3534   if (!IN_ECLIPSE) {
3535     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3536     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3537   }
3538
3539   inputs.file("${jalviewDir}/.project")
3540   outputs.upToDateWhen { 
3541     file(projdir).exists()
3542   }
3543 }
3544
3545
3546 task jalviewjsTranspile(type: Exec) {
3547   dependsOn jalviewjsEclipseSetup 
3548   dependsOn jalviewjsProjectImport
3549   dependsOn jalviewjsEclipsePaths
3550   if (!IN_ECLIPSE) {
3551     dependsOn jalviewjsEnableAltFileProperty
3552   }
3553
3554   doFirst {
3555     // do not run a headless transpile when we claim to be in Eclipse
3556     if (IN_ECLIPSE) {
3557       println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3558       throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
3559     } else {
3560       println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
3561     }
3562   }
3563
3564   executable(eclipseBinary)
3565   args(["-nosplash", "--launcher.suppressErrors", "-application", "org.eclipse.jdt.apt.core.aptBuild", "-data", eclipseWorkspace, "-${jalviewjs_eclipse_build_arg}", eclipse_project_name ])
3566   if (eclipseDebug) {
3567     args += "-debug"
3568   }
3569   args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
3570   if (!IN_ECLIPSE) {
3571     args += [ "-D${j2sHeadlessBuildProperty}=true" ]
3572     args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
3573   }
3574
3575   def stdout
3576   def stderr
3577   doFirst {
3578     stdout = new ByteArrayOutputStream()
3579     stderr = new ByteArrayOutputStream()
3580
3581     def logOutFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}"
3582     def logOutFile = file(logOutFileName)
3583     logOutFile.createNewFile()
3584     logOutFile.text = """ROOT: ${jalviewjs_eclipse_root}
3585 BINARY: ${eclipseBinary}
3586 VERSION: ${eclipseVersion}
3587 WORKSPACE: ${eclipseWorkspace}
3588 DEBUG: ${eclipseDebug}
3589 ----
3590 """
3591     def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3592     // combine stdout and stderr
3593     def logErrFOS = logOutFOS
3594
3595     if (jalviewjs_j2s_to_console.equals("true")) {
3596       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3597         new org.apache.tools.ant.util.TeeOutputStream(
3598           logOutFOS,
3599           stdout),
3600         System.out)
3601       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3602         new org.apache.tools.ant.util.TeeOutputStream(
3603           logErrFOS,
3604           stderr),
3605         System.err)
3606     } else {
3607       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3608         logOutFOS,
3609         stdout)
3610       errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3611         logErrFOS,
3612         stderr)
3613     }
3614   }
3615
3616   doLast {
3617     if (stdout.toString().contains("Error processing ")) {
3618       // j2s did not complete transpile
3619       //throw new TaskExecutionException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3620       if (jalviewjs_ignore_transpile_errors.equals("true")) {
3621         println("IGNORING TRANSPILE ERRORS")
3622         println("See eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3623       } else {
3624         throw new GradleException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
3625       }
3626     }
3627   }
3628
3629   inputs.dir("${jalviewDir}/${sourceDir}")
3630   outputs.dir("${jalviewDir}/${jalviewjsTransferSiteJsDir}")
3631   outputs.upToDateWhen( { file("${jalviewDir}/${jalviewjsTransferSiteJsDir}${jalviewjs_server_resource}").exists() } )
3632 }
3633
3634
3635 def jalviewjsCallCore(String name, FileCollection list, String prefixFile, String suffixFile, String jsfile, String zjsfile, File logOutFile, Boolean logOutConsole) {
3636
3637   def stdout = new ByteArrayOutputStream()
3638   def stderr = new ByteArrayOutputStream()
3639
3640   def coreFile = file(jsfile)
3641   def msg = ""
3642   msg = "Creating core for ${name}...\nGenerating ${jsfile}"
3643   println(msg)
3644   logOutFile.createNewFile()
3645   logOutFile.append(msg+"\n")
3646
3647   def coreTop = file(prefixFile)
3648   def coreBottom = file(suffixFile)
3649   coreFile.getParentFile().mkdirs()
3650   coreFile.createNewFile()
3651   coreFile.write( coreTop.getText("UTF-8") )
3652   list.each {
3653     f ->
3654     if (f.exists()) {
3655       def t = f.getText("UTF-8")
3656       t.replaceAll("Clazz\\.([^_])","Clazz_${1}")
3657       coreFile.append( t )
3658     } else {
3659       msg = "...file '"+f.getPath()+"' does not exist, skipping"
3660       println(msg)
3661       logOutFile.append(msg+"\n")
3662     }
3663   }
3664   coreFile.append( coreBottom.getText("UTF-8") )
3665
3666   msg = "Generating ${zjsfile}"
3667   println(msg)
3668   logOutFile.append(msg+"\n")
3669   def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
3670   def logErrFOS = logOutFOS
3671
3672   javaexec {
3673     classpath = files(["${jalviewDir}/${jalviewjs_closure_compiler}"])
3674     main = "com.google.javascript.jscomp.CommandLineRunner"
3675     jvmArgs = [ "-Dfile.encoding=UTF-8" ]
3676     args = [ "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
3677     maxHeapSize = "2g"
3678
3679     msg = "\nRunning '"+commandLine.join(' ')+"'\n"
3680     println(msg)
3681     logOutFile.append(msg+"\n")
3682
3683     if (logOutConsole) {
3684       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3685         new org.apache.tools.ant.util.TeeOutputStream(
3686           logOutFOS,
3687           stdout),
3688         standardOutput)
3689         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3690           new org.apache.tools.ant.util.TeeOutputStream(
3691             logErrFOS,
3692             stderr),
3693           System.err)
3694     } else {
3695       standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
3696         logOutFOS,
3697         stdout)
3698         errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
3699           logErrFOS,
3700           stderr)
3701     }
3702   }
3703   msg = "--"
3704   println(msg)
3705   logOutFile.append(msg+"\n")
3706 }
3707
3708
3709 task jalviewjsBuildAllCores {
3710   group "JalviewJS"
3711   description "Build the core js lib closures listed in the classlists dir"
3712   dependsOn jalviewjsTranspile
3713   dependsOn jalviewjsTransferUnzipSwingJs
3714
3715   def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${jalviewjs_j2s_subdir}"
3716   def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_j2s_subdir}"
3717   def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteLibDir}/${jalviewjs_j2s_subdir}"
3718   def jsDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_js_subdir}"
3719   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}/${jalviewjs_j2s_subdir}/core"
3720   def prefixFile = "${jsDir}/core/coretop2.js"
3721   def suffixFile = "${jsDir}/core/corebottom2.js"
3722
3723   inputs.file prefixFile
3724   inputs.file suffixFile
3725
3726   def classlistFiles = []
3727   // add the classlists found int the jalviewjs_classlists_dir
3728   fileTree(dir: "${jalviewDir}/${jalviewjs_classlists_dir}", include: "*.txt").each {
3729     file ->
3730     def name = file.getName() - ".txt"
3731     classlistFiles += [
3732       'file': file,
3733       'name': name
3734     ]
3735   }
3736
3737   // _jmol and _jalview cores. Add any other peculiar classlist.txt files here
3738   //classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jmol}"), 'name': "_jvjmol" ]
3739   classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jalview}"), 'name': jalviewjsJalviewCoreName ]
3740
3741   jalviewjsCoreClasslists = []
3742
3743   classlistFiles.each {
3744     hash ->
3745
3746     def file = hash['file']
3747     if (! file.exists()) {
3748       //println("...classlist file '"+file.getPath()+"' does not exist, skipping")
3749       return false // this is a "continue" in groovy .each closure
3750     }
3751     def name = hash['name']
3752     if (name == null) {
3753       name = file.getName() - ".txt"
3754     }
3755
3756     def filelist = []
3757     file.eachLine {
3758       line ->
3759         filelist += line
3760     }
3761     def list = fileTree(dir: j2sDir, includes: filelist)
3762
3763     def jsfile = "${outputDir}/core${name}.js"
3764     def zjsfile = "${outputDir}/core${name}.z.js"
3765
3766     jalviewjsCoreClasslists += [
3767       'jsfile': jsfile,
3768       'zjsfile': zjsfile,
3769       'list': list,
3770       'name': name
3771     ]
3772
3773     inputs.file(file)
3774     inputs.files(list)
3775     outputs.file(jsfile)
3776     outputs.file(zjsfile)
3777   }
3778   
3779   // _stevesoft core. add any cores without a classlist here (and the inputs and outputs)
3780   def stevesoftClasslistName = "_stevesoft"
3781   def stevesoftClasslist = [
3782     'jsfile': "${outputDir}/core${stevesoftClasslistName}.js",
3783     'zjsfile': "${outputDir}/core${stevesoftClasslistName}.z.js",
3784     'list': fileTree(dir: j2sDir, include: "com/stevesoft/pat/**/*.js"),
3785     'name': stevesoftClasslistName
3786   ]
3787   jalviewjsCoreClasslists += stevesoftClasslist
3788   inputs.files(stevesoftClasslist['list'])
3789   outputs.file(stevesoftClasslist['jsfile'])
3790   outputs.file(stevesoftClasslist['zjsfile'])
3791
3792   // _all core
3793   def allClasslistName = "_all"
3794   def allJsFiles = fileTree(dir: j2sDir, include: "**/*.js")
3795   allJsFiles += fileTree(
3796     dir: libJ2sDir,
3797     include: "**/*.js",
3798     excludes: [
3799       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3800       "**/org/jmol/jvxl/readers/IsoIntersectFileReader.js",
3801       "**/org/jmol/export/JSExporter.js"
3802     ]
3803   )
3804   allJsFiles += fileTree(
3805     dir: swingJ2sDir,
3806     include: "**/*.js",
3807     excludes: [
3808       // these exlusions are files that the closure-compiler produces errors for. Should fix them
3809       "**/sun/misc/Unsafe.js",
3810       "**/swingjs/jquery/jquery-editable-select.js",
3811       "**/swingjs/jquery/j2sComboBox.js",
3812       "**/sun/misc/FloatingDecimal.js"
3813     ]
3814   )
3815   def allClasslist = [
3816     'jsfile': "${outputDir}/core${allClasslistName}.js",
3817     'zjsfile': "${outputDir}/core${allClasslistName}.z.js",
3818     'list': allJsFiles,
3819     'name': allClasslistName
3820   ]
3821   // not including this version of "all" core at the moment
3822   //jalviewjsCoreClasslists += allClasslist
3823   inputs.files(allClasslist['list'])
3824   outputs.file(allClasslist['jsfile'])
3825   outputs.file(allClasslist['zjsfile'])
3826
3827   doFirst {
3828     def logOutFile = file("${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_closure_stdout}")
3829     logOutFile.getParentFile().mkdirs()
3830     logOutFile.createNewFile()
3831     logOutFile.write(getDate("yyyy-MM-dd HH:mm:ss")+" jalviewjsBuildAllCores\n----\n")
3832
3833     jalviewjsCoreClasslists.each {
3834       jalviewjsCallCore(it.name, it.list, prefixFile, suffixFile, it.jsfile, it.zjsfile, logOutFile, jalviewjs_j2s_to_console.equals("true"))
3835     }
3836   }
3837
3838 }
3839
3840
3841 def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inputFile, String outputFile) {
3842   copy {
3843     from inputFile
3844     into file(outputFile).getParentFile()
3845     rename { filename ->
3846       if (filename.equals(inputFile.getName())) {
3847         return file(outputFile).getName()
3848       }
3849       return null
3850     }
3851     filter(ReplaceTokens,
3852       beginToken: '_',
3853       endToken: '_',
3854       tokens: [
3855         'MAIN': '"'+main_class+'"',
3856         'CODE': "null",
3857         'NAME': jalviewjsJalviewTemplateName+" [core ${coreName}]",
3858         'COREKEY': jalviewjs_core_key,
3859         'CORENAME': coreName
3860       ]
3861     )
3862   }
3863 }
3864
3865
3866 task jalviewjsPublishCoreTemplates {
3867   dependsOn jalviewjsBuildAllCores
3868   def inputFileName = "${jalviewDir}/${j2s_coretemplate_html}"
3869   def inputFile = file(inputFileName)
3870   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3871
3872   def outputFiles = []
3873   jalviewjsCoreClasslists.each { cl ->
3874     def outputFile = "${outputDir}/${jalviewjsJalviewTemplateName}_${cl.name}.html"
3875     cl['outputfile'] = outputFile
3876     outputFiles += outputFile
3877   }
3878
3879   doFirst {
3880     jalviewjsCoreClasslists.each { cl ->
3881       jalviewjsPublishCoreTemplate(cl.name, jalviewjsJalviewTemplateName, inputFile, cl.outputfile)
3882     }
3883   }
3884   inputs.file(inputFile)
3885   outputs.files(outputFiles)
3886 }
3887
3888
3889 task jalviewjsSyncCore (type: Sync) {
3890   dependsOn jalviewjsBuildAllCores
3891   dependsOn jalviewjsPublishCoreTemplates
3892   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteCoreDir}")
3893   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
3894
3895   from inputFiles
3896   into outputDir
3897   def outputFiles = []
3898   rename { filename ->
3899     outputFiles += "${outputDir}/${filename}"
3900     null
3901   }
3902   preserve {
3903     include "**"
3904   }
3905   outputs.files outputFiles
3906   inputs.files inputFiles
3907 }
3908
3909
3910 // this Copy version of TransferSiteJs will delete anything else in the target dir
3911 task jalviewjsCopyTransferSiteJs(type: Copy) {
3912   dependsOn jalviewjsTranspile
3913   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3914   into "${jalviewDir}/${jalviewjsSiteDir}"
3915 }
3916
3917
3918 // this Sync version of TransferSite is used by buildship to keep the website automatically up to date when a file changes
3919 task jalviewjsSyncTransferSiteJs(type: Sync) {
3920   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3921   include "**/*.*"
3922   into "${jalviewDir}/${jalviewjsSiteDir}"
3923   preserve {
3924     include "**"
3925   }
3926 }
3927
3928
3929 jalviewjsSyncAllLibs.mustRunAfter jalviewjsCopyTransferSiteJs
3930 jalviewjsSyncResources.mustRunAfter jalviewjsCopyTransferSiteJs
3931 jalviewjsSyncSiteResources.mustRunAfter jalviewjsCopyTransferSiteJs
3932 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsCopyTransferSiteJs
3933
3934 jalviewjsSyncAllLibs.mustRunAfter jalviewjsSyncTransferSiteJs
3935 jalviewjsSyncResources.mustRunAfter jalviewjsSyncTransferSiteJs
3936 jalviewjsSyncSiteResources.mustRunAfter jalviewjsSyncTransferSiteJs
3937 jalviewjsSyncBuildProperties.mustRunAfter jalviewjsSyncTransferSiteJs
3938
3939
3940 task jalviewjsPrepareSite {
3941   group "JalviewJS"
3942   description "Prepares the website folder including unzipping files and copying resources"
3943   dependsOn jalviewjsSyncAllLibs
3944   dependsOn jalviewjsSyncResources
3945   dependsOn jalviewjsSyncSiteResources
3946   dependsOn jalviewjsSyncBuildProperties
3947   dependsOn jalviewjsSyncCore
3948 }
3949
3950
3951 task jalviewjsBuildSite {
3952   group "JalviewJS"
3953   description "Builds the whole website including transpiled code"
3954   dependsOn jalviewjsCopyTransferSiteJs
3955   dependsOn jalviewjsPrepareSite
3956 }
3957
3958
3959 task cleanJalviewjsTransferSite {
3960   doFirst {
3961     delete "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
3962     delete "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
3963     delete "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
3964     delete "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
3965   }
3966 }
3967
3968
3969 task cleanJalviewjsSite {
3970   dependsOn cleanJalviewjsTransferSite
3971   doFirst {
3972     delete "${jalviewDir}/${jalviewjsSiteDir}"
3973   }
3974 }
3975
3976
3977 task jalviewjsSiteTar(type: Tar) {
3978   group "JalviewJS"
3979   description "Creates a tar.gz file for the website"
3980   dependsOn jalviewjsBuildSite
3981   def outputFilename = "jalviewjs-site-${JALVIEW_VERSION}.tar.gz"
3982   archiveFileName = outputFilename
3983
3984   compression Compression.GZIP
3985
3986   from "${jalviewDir}/${jalviewjsSiteDir}"
3987   into jalviewjs_site_dir // this is inside the tar file
3988
3989   inputs.dir("${jalviewDir}/${jalviewjsSiteDir}")
3990 }
3991
3992
3993 task jalviewjsServer {
3994   group "JalviewJS"
3995   def filename = "jalviewjsTest.html"
3996   description "Starts a webserver on localhost to test the website. See ${filename} to access local site on most recently used port."
3997   def htmlFile = "${jalviewDirAbsolutePath}/${filename}"
3998   doLast {
3999
4000     def factory
4001     try {
4002       def f = Class.forName("org.gradle.plugins.javascript.envjs.http.simple.SimpleHttpFileServerFactory")
4003       factory = f.newInstance()
4004     } catch (ClassNotFoundException e) {
4005       throw new GradleException("Unable to create SimpleHttpFileServerFactory")
4006     }
4007     def port = Integer.valueOf(jalviewjs_server_port)
4008     def start = port
4009     def running = false
4010     def url
4011     def jalviewjsServer
4012     while(port < start+1000 && !running) {
4013       try {
4014         def doc_root = new File("${jalviewDirAbsolutePath}/${jalviewjsSiteDir}")
4015         jalviewjsServer = factory.start(doc_root, port)
4016         running = true
4017         url = jalviewjsServer.getResourceUrl(jalviewjs_server_resource)
4018         println("SERVER STARTED with document root ${doc_root}.")
4019         println("Go to "+url+" . Run  gradle --stop  to stop (kills all gradle daemons).")
4020         println("For debug: "+url+"?j2sdebug")
4021         println("For verbose: "+url+"?j2sverbose")
4022       } catch (Exception e) {
4023         port++;
4024       }
4025     }
4026     def htmlText = """
4027       <p><a href="${url}">JalviewJS Test. &lt;${url}&gt;</a></p>
4028       <p><a href="${url}?j2sdebug">JalviewJS Test with debug. &lt;${url}?j2sdebug&gt;</a></p>
4029       <p><a href="${url}?j2sverbose">JalviewJS Test with verbose. &lt;${url}?j2sdebug&gt;</a></p>
4030       """
4031     jalviewjsCoreClasslists.each { cl ->
4032       def urlcore = jalviewjsServer.getResourceUrl(file(cl.outputfile).getName())
4033       htmlText += """
4034       <p><a href="${urlcore}">${jalviewjsJalviewTemplateName} [core ${cl.name}]. &lt;${urlcore}&gt;</a></p>
4035       """
4036       println("For core ${cl.name}: "+urlcore)
4037     }
4038
4039     file(htmlFile).text = htmlText
4040   }
4041
4042   outputs.file(htmlFile)
4043   outputs.upToDateWhen({false})
4044 }
4045
4046
4047 task cleanJalviewjsAll {
4048   group "JalviewJS"
4049   description "Delete all configuration and build artifacts to do with JalviewJS build"
4050   dependsOn cleanJalviewjsSite
4051   dependsOn jalviewjsEclipsePaths
4052   
4053   doFirst {
4054     delete "${jalviewDir}/${jalviewjsBuildDir}"
4055     delete "${jalviewDir}/${eclipse_bin_dir}"
4056     if (eclipseWorkspace != null && file(eclipseWorkspace.getAbsolutePath()+"/.metadata").exists()) {
4057       delete file(eclipseWorkspace.getAbsolutePath()+"/.metadata")
4058     }
4059     delete jalviewjsJ2sAltSettingsFileName
4060   }
4061
4062   outputs.upToDateWhen( { false } )
4063 }
4064
4065
4066 task jalviewjsIDE_checkJ2sPlugin {
4067   group "00 JalviewJS in Eclipse"
4068   description "Compare the swingjs/net.sf.j2s.core(-j11)?.jar file with the Eclipse IDE's plugin version (found in the 'dropins' dir)"
4069
4070   doFirst {
4071     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4072     def j2sPluginFile = file(j2sPlugin)
4073     def eclipseHome = System.properties["eclipse.home.location"]
4074     if (eclipseHome == null || ! IN_ECLIPSE) {
4075       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. Skipping J2S Plugin Check.")
4076     }
4077     def eclipseJ2sPluginDirs = [ "${eclipseHome}/dropins" ]
4078     def altPluginsDir = System.properties["org.eclipse.equinox.p2.reconciler.dropins.directory"]
4079     if (altPluginsDir != null && file(altPluginsDir).exists()) {
4080       eclipseJ2sPluginDirs += altPluginsDir
4081     }
4082     def foundPlugin = false
4083     def j2sPluginFileName = j2sPluginFile.getName()
4084     def eclipseJ2sPlugin
4085     def eclipseJ2sPluginFile
4086     eclipseJ2sPluginDirs.any { dir ->
4087       eclipseJ2sPlugin = "${dir}/${j2sPluginFileName}"
4088       eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4089       if (eclipseJ2sPluginFile.exists()) {
4090         foundPlugin = true
4091         return true
4092       }
4093     }
4094     if (!foundPlugin) {
4095       def msg = "Eclipse J2S Plugin is not installed (could not find '${j2sPluginFileName}' in\n"+eclipseJ2sPluginDirs.join("\n")+"\n)\nTry running task jalviewjsIDE_copyJ2sPlugin"
4096       System.err.println(msg)
4097       throw new StopExecutionException(msg)
4098     }
4099
4100     def digest = MessageDigest.getInstance("MD5")
4101
4102     digest.update(j2sPluginFile.text.bytes)
4103     def j2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4104
4105     digest.update(eclipseJ2sPluginFile.text.bytes)
4106     def eclipseJ2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
4107      
4108     if (j2sPluginMd5 != eclipseJ2sPluginMd5) {
4109       def msg = "WARNING! Eclipse J2S Plugin '${eclipseJ2sPlugin}' is different to this commit's version '${j2sPlugin}'"
4110       System.err.println(msg)
4111       throw new StopExecutionException(msg)
4112     } else {
4113       def msg = "Eclipse J2S Plugin '${eclipseJ2sPlugin}' is the same as '${j2sPlugin}' (this is good)"
4114       println(msg)
4115     }
4116   }
4117 }
4118
4119 task jalviewjsIDE_copyJ2sPlugin {
4120   group "00 JalviewJS in Eclipse"
4121   description "Copy the swingjs/net.sf.j2s.core(-j11)?.jar file into the Eclipse IDE's 'dropins' dir"
4122
4123   doFirst {
4124     def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
4125     def j2sPluginFile = file(j2sPlugin)
4126     def eclipseHome = System.properties["eclipse.home.location"]
4127     if (eclipseHome == null || ! IN_ECLIPSE) {
4128       throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. NOT copying J2S Plugin.")
4129     }
4130     def eclipseJ2sPlugin = "${eclipseHome}/dropins/${j2sPluginFile.getName()}"
4131     def eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
4132     def msg = "WARNING! Copying this commit's j2s plugin '${j2sPlugin}' to Eclipse J2S Plugin '${eclipseJ2sPlugin}'\n* May require an Eclipse restart"
4133     System.err.println(msg)
4134     copy {
4135       from j2sPlugin
4136       eclipseJ2sPluginFile.getParentFile().mkdirs()
4137       into eclipseJ2sPluginFile.getParent()
4138     }
4139   }
4140 }
4141
4142
4143 task jalviewjsIDE_j2sFile {
4144   group "00 JalviewJS in Eclipse"
4145   description "Creates the .j2s file"
4146   dependsOn jalviewjsCreateJ2sSettings
4147 }
4148
4149
4150 task jalviewjsIDE_SyncCore {
4151   group "00 JalviewJS in Eclipse"
4152   description "Build the core js lib closures listed in the classlists dir and publish core html from template"
4153   dependsOn jalviewjsSyncCore
4154 }
4155
4156
4157 task jalviewjsIDE_SyncSiteAll {
4158   dependsOn jalviewjsSyncAllLibs
4159   dependsOn jalviewjsSyncResources
4160   dependsOn jalviewjsSyncSiteResources
4161   dependsOn jalviewjsSyncBuildProperties
4162 }
4163
4164
4165 cleanJalviewjsTransferSite.mustRunAfter jalviewjsIDE_SyncSiteAll
4166
4167
4168 task jalviewjsIDE_PrepareSite {
4169   group "00 JalviewJS in Eclipse"
4170   description "Sync libs and resources to site dir, but not closure cores"
4171
4172   dependsOn jalviewjsIDE_SyncSiteAll
4173   //dependsOn cleanJalviewjsTransferSite // not sure why this clean is here -- will slow down a re-run of this task
4174 }
4175
4176
4177 task jalviewjsIDE_AssembleSite {
4178   group "00 JalviewJS in Eclipse"
4179   description "Assembles unzipped supporting zipfiles, resources, site resources and closure cores into the Eclipse transpiled site"
4180   dependsOn jalviewjsPrepareSite
4181 }
4182
4183
4184 task jalviewjsIDE_SiteClean {
4185   group "00 JalviewJS in Eclipse"
4186   description "Deletes the Eclipse transpiled site"
4187   dependsOn cleanJalviewjsSite
4188 }
4189
4190
4191 task jalviewjsIDE_Server {
4192   group "00 JalviewJS in Eclipse"
4193   description "Starts a webserver on localhost to test the website"
4194   dependsOn jalviewjsServer
4195 }
4196
4197
4198 // buildship runs this at import or gradle refresh
4199 task eclipseSynchronizationTask {
4200   //dependsOn eclipseSetup
4201   dependsOn createBuildProperties
4202   if (J2S_ENABLED) {
4203     dependsOn jalviewjsIDE_j2sFile
4204     dependsOn jalviewjsIDE_checkJ2sPlugin
4205     dependsOn jalviewjsIDE_PrepareSite
4206   }
4207 }
4208
4209
4210 // buildship runs this at build time or project refresh
4211 task eclipseAutoBuildTask {
4212   //dependsOn jalviewjsIDE_checkJ2sPlugin
4213   //dependsOn jalviewjsIDE_PrepareSite
4214 }
4215
4216
4217 task jalviewjsCopyStderrLaunchFile(type: Copy) {
4218   from file(jalviewjs_stderr_launch)
4219   into jalviewjsSiteDir
4220
4221   inputs.file jalviewjs_stderr_launch
4222   outputs.file jalviewjsStderrLaunchFilename
4223 }
4224
4225 task cleanJalviewjsChromiumUserDir {
4226   doFirst {
4227     delete jalviewjsChromiumUserDir
4228   }
4229   outputs.dir jalviewjsChromiumUserDir
4230   // always run when depended on
4231   outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
4232 }
4233
4234 task jalviewjsChromiumProfile {
4235   dependsOn cleanJalviewjsChromiumUserDir
4236   mustRunAfter cleanJalviewjsChromiumUserDir
4237
4238   def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
4239
4240   doFirst {
4241     mkdir jalviewjsChromiumProfileDir
4242     firstRun.text = ""
4243   }
4244   outputs.file firstRun
4245 }
4246
4247 task jalviewjsLaunchTest {
4248   group "Test"
4249   description "Check JalviewJS opens in a browser"
4250   dependsOn jalviewjsBuildSite
4251   dependsOn jalviewjsCopyStderrLaunchFile
4252   dependsOn jalviewjsChromiumProfile
4253
4254   def macOS = OperatingSystem.current().isMacOsX()
4255   def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary
4256   if (chromiumBinary.startsWith("~/")) {
4257     chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1)
4258   }
4259   
4260   def stdout
4261   def stderr
4262   doFirst {
4263     def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000
4264     
4265     def binary = file(chromiumBinary)
4266     if (!binary.exists()) {
4267       throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
4268     }
4269     stdout = new ByteArrayOutputStream()
4270     stderr = new ByteArrayOutputStream()
4271     def execStdout
4272     def execStderr
4273     if (jalviewjs_j2s_to_console.equals("true")) {
4274       execStdout = new org.apache.tools.ant.util.TeeOutputStream(
4275         stdout,
4276         System.out)
4277       execStderr = new org.apache.tools.ant.util.TeeOutputStream(
4278         stderr,
4279         System.err)
4280     } else {
4281       execStdout = stdout
4282       execStderr = stderr
4283     }
4284     def execArgs = [
4285       "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER
4286       "--headless=new",
4287       "--disable-gpu",
4288       "--timeout=${timeoutms}",
4289       "--virtual-time-budget=${timeoutms}",
4290       "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
4291       "--profile-directory=${jalviewjs_chromium_profile_name}",
4292       "--allow-file-access-from-files",
4293       "--enable-logging=stderr",
4294       "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}"
4295     ]
4296     
4297     if (true || macOS) {
4298       ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
4299       Future f1 = executor.submit(
4300         () -> {
4301           exec {
4302             standardOutput = execStdout
4303             errorOutput = execStderr
4304             executable(chromiumBinary)
4305             args(execArgs)
4306             println "COMMAND: '"+commandLine.join(" ")+"'"
4307           }
4308           executor.shutdownNow()
4309         }
4310       )
4311
4312       def noChangeBytes = 0
4313       def noChangeIterations = 0
4314       executor.scheduleAtFixedRate(
4315         () -> {
4316           String stderrString = stderr.toString()
4317           // shutdown the task if we have a success string
4318           if (stderrString.contains(jalviewjs_desktop_init_string)) {
4319             f1.cancel()
4320             Thread.sleep(1000)
4321             executor.shutdownNow()
4322           }
4323           // if no change in stderr for 10s then also end
4324           if (noChangeIterations >= jalviewjs_chromium_idle_timeout) {
4325             executor.shutdownNow()
4326           }
4327           if (stderrString.length() == noChangeBytes) {
4328             noChangeIterations++
4329           } else {
4330             noChangeBytes = stderrString.length()
4331             noChangeIterations = 0
4332           }
4333         },
4334         1, 1, TimeUnit.SECONDS)
4335
4336       executor.schedule(new Runnable(){
4337         public void run(){
4338           f1.cancel()
4339           executor.shutdownNow()
4340         }
4341       }, timeoutms, TimeUnit.MILLISECONDS)
4342
4343       executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
4344       executor.shutdownNow()
4345     }
4346
4347   }
4348   
4349   doLast {
4350     def found = false
4351     stderr.toString().eachLine { line ->
4352       if (line.contains(jalviewjs_desktop_init_string)) {
4353         println("Found line '"+line+"'")
4354         found = true
4355         return
4356       }
4357     }
4358     if (!found) {
4359       throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
4360     }
4361   }
4362 }
4363   
4364
4365 task jalviewjs {
4366   group "JalviewJS"
4367   description "Build the JalviewJS site and run the launch test"
4368   dependsOn jalviewjsBuildSite
4369   dependsOn jalviewjsLaunchTest
4370 }