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