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