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