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