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