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