Files
ma/nextflow/build.gradle
2026-04-29 23:01:54 +02:00

516 lines
18 KiB
Groovy

/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id 'java'
id 'idea'
}
// Add ability to test with upcoming versions of Groovy
def groovyVer = System.getenv('CI_GROOVY_VERSION')
if (groovyVer) {
def repo = groovyVer.startsWith('com.github.apache:') ? 'https://jitpack.io' : 'https://oss.jfrog.org/oss-snapshot-local/'
logger.lifecycle "Overridden Groovy dependency to use $groovyVer - repository: $repo"
allprojects {
repositories {
maven { url repo }
}
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'org.apache.groovy') {
if( groovyVer.contains(':') )
details.useTarget(groovyVer)
else
details.useVersion(groovyVer)
println ">> Overriding $details.requested with version: $groovyVer"
}
}
}
}
}
def projects(String...args) {
args.collect {project(it)}
}
String gitVersion() {
def p = new ProcessBuilder() .command('sh','-c','git rev-parse --short HEAD') .start()
def r = p.waitFor()
return r==0 ? p.text.trim() : '(unknown)'
}
group = 'io.nextflow'
version = rootProject.file('VERSION').text.trim()
ext.commitId = gitVersion()
allprojects {
apply plugin: 'java'
apply plugin: 'java-test-fixtures'
apply plugin: 'idea'
apply plugin: 'groovy'
apply plugin: 'java-library'
apply plugin: 'jacoco'
java {
// these settings apply to all jvm tooling, including groovy
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
sourceCompatibility = 17
targetCompatibility = 17
}
idea {
module.inheritOutputDirs = true
}
repositories {
mavenCentral()
maven { url 'https://repo.eclipse.org/content/groups/releases' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url = "https://s3-eu-west-1.amazonaws.com/maven.seqera.io/releases" }
maven { url = "https://s3-eu-west-1.amazonaws.com/maven.seqera.io/snapshots" }
}
configurations {
// see https://docs.gradle.org/4.1/userguide/dependency_management.html#sub:exclude_transitive_dependencies
all*.exclude group: 'org.apache.groovy', module: 'groovy-all'
all*.exclude group: 'org.apache.groovy', module: 'groovy-cli-picocli'
// groovydoc libs
groovyDoc.extendsFrom runtime
}
dependencies {
// see https://docs.gradle.org/4.1/userguide/dependency_management.html#sec:module_replacement
modules {
module("commons-logging:commons-logging") { replacedBy("org.slf4j:jcl-over-slf4j") }
}
// JUnit Platform launcher required for Gradle 9.1+ when using useJUnitPlatform()
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.5'
// Documentation required libraries
groovyDoc 'org.fusesource.jansi:jansi:2.4.0'
groovyDoc "org.apache.groovy:groovy-groovydoc:4.0.31"
groovyDoc "org.apache.groovy:groovy-ant:4.0.31"
}
test {
useJUnitPlatform()
}
// this is required due to this IDEA bug
// https://youtrack.jetbrains.com/issue/IDEA-129282
sourceSets {
main {
output.resourcesDir = 'build/classes/main'
}
}
// Disable strict javadoc checks
// See http://blog.joda.org/2014/02/turning-off-doclint-in-jdk-8-javadoc.html
if (JavaVersion.current().isJava8Compatible()) {
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
}
}
tasks.withType(Jar) {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
// patched as described here
// http://forums.gradle.org/gradle/topics/gradle_task_groovydoc_failing_with_noclassdeffounderror
tasks.withType(Groovydoc) {
groovyClasspath = project.configurations.groovyDoc
includes = ["nextflow/**"]
}
// Required to run tests on Java 9 and higher in compatibility mode
tasks.withType(Test) {
jvmArgs ([
'--enable-preview',
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-opens=java.base/java.nio.file.spi=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED',
'--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.ch=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.fs=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.http=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.https=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.ftp=ALL-UNNAMED',
'--add-opens=java.base/sun.net.www.protocol.file=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.vm=ALL-UNNAMED',
])
}
/**
* Code coverage with JaCoCo.
* See: https://www.jacoco.org/; https://docs.gradle.org/current/userguide/jacoco_plugin.html
*/
// Code coverage report is always generated after tests run
test { finalizedBy jacocoTestReport }
jacocoTestReport {
// Tests are required to run before generating the code coverage report
dependsOn test
// Remove closure classes from the report, as they are already covered by the enclosing class coverage stats adding only noise.
// See: https://stackoverflow.com/questions/39453696
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect { dir ->
fileTree(dir: dir, excludes: ['**/*$*_closure*'])
}))
}
}
}
// disable jar for root project
jar.enabled = false
/*
* Update the build timestamp in the source source file
*/
task buildInfo {
// Always run this task - never consider it up-to-date
outputs.upToDateWhen { false }
doLast {
def file0 = file('modules/nextflow/src/main/resources/META-INF/build-info.properties')
def buildNum = 0
// Use GitHub Actions run number if available, otherwise increment local counter
if (System.getenv('GITHUB_RUN_NUMBER')) {
buildNum = System.getenv('GITHUB_RUN_NUMBER').toInteger()
println "Using GitHub Actions run number: $buildNum"
}
// -- update build-info file
file0.text = """\
build=${buildNum}
version=${version}
timestamp=${System.currentTimeMillis()}
commitId=${project.property('commitId')}
""".stripIndent()
}}
/*
* Update release information in nextflow wrapper, dockerfile, and plugin metadata.
*
* This task:
* 1. Updates the NXF_VER version string in the nextflow launch script
* 2. Updates the release version in docker/Dockerfile
* 3. Generates plugins-info.txt with current plugin versions from VERSION files
*
* This task always runs to ensure all release artifacts are updated with current versions.
*/
task releaseInfo {
dependsOn buildInfo
// Always run this task - never consider it up-to-date
outputs.upToDateWhen { false }
doLast {
// -- update 'nextflow' wrapper
def file0 = file('nextflow')
def src = file0.text
src = src.replaceAll(/NXF_VER\=\$\{NXF_VER:-'.*'\}/, 'NXF_VER=\\${NXF_VER:-\'' + version + '\'}')
file0.text = src
// -- update dockerfile
file0 = file('docker/Dockerfile')
src = file0.text
src = src.replaceAll(/releases\/v[0-9a-zA-Z_\-\.]+\//, "releases/v$version/" as String)
file0.text = src
// -- create plugins-info file
def plugins = []
new File(rootProject.rootDir, 'plugins')
.eachDir { if(it.name.startsWith('nf-')) plugins << project(":plugins:${it.name}") }
def meta = plugins.collect { "$it.name@$it.version" }
file('modules/nextflow/src/main/resources/META-INF/plugins-info.txt').text = meta.toSorted().join('\n')
}}
/*
* Validate that plugins-info.txt matches plugin VERSION files and that build-info.properties
* contains the correct build number and commit ID when running in GitHub Actions.
*
* This task ensures:
* 1. All plugin versions in plugins-info.txt match their corresponding VERSION files
* 2. The build number in build-info.properties matches GITHUB_RUN_NUMBER (in CI)
* 3. The commit ID in build-info.properties matches GITHUB_SHA (in CI)
*
* This validation prevents releases with stale or mismatched metadata.
*/
task validatePluginVersions {
dependsOn buildInfo
inputs.file('modules/nextflow/src/main/resources/META-INF/plugins-info.txt')
inputs.file('modules/nextflow/src/main/resources/META-INF/build-info.properties')
inputs.files(fileTree('plugins') { include '*/VERSION' })
doLast {
// Get expected versions from plugin projects
def expected = []
new File(rootProject.rootDir, 'plugins')
.eachDir { if(it.name.startsWith('nf-')) expected << project(":plugins:${it.name}") }
def expectedVersions = expected.collect { "$it.name@$it.version" }.toSorted()
// Get actual versions from plugins-info.txt
def actualVersions = file('modules/nextflow/src/main/resources/META-INF/plugins-info.txt').readLines()
// Compare plugin versions
if (expectedVersions != actualVersions) {
def diffs = []
expectedVersions.eachWithIndex { exp, i ->
def act = actualVersions.size() > i ? actualVersions[i] : 'missing'
if (exp != act) diffs << " expected: $exp, actual: $act"
}
throw new GradleException("Plugin version mismatch:\n${diffs.join('\n')}\nRun 'make assemble' to fix.")
}
// Validate build-info.properties - require GitHub Actions environment variables
if (!System.getenv('GITHUB_RUN_NUMBER')) {
throw new GradleException("GITHUB_RUN_NUMBER environment variable is required")
}
if (!System.getenv('GITHUB_SHA')) {
throw new GradleException("GITHUB_SHA environment variable is required")
}
def buildInfoFile = file('modules/nextflow/src/main/resources/META-INF/build-info.properties')
def props = new Properties()
buildInfoFile.withInputStream { props.load(it) }
def actualBuild = props.getProperty('build')
def expectedBuild = System.getenv('GITHUB_RUN_NUMBER')
if (actualBuild != expectedBuild) {
throw new GradleException("Build number mismatch: build-info.properties has '${actualBuild}' but GITHUB_RUN_NUMBER is '${expectedBuild}'. Run 'make assemble' to fix.")
}
def actualCommit = props.getProperty('commitId')
def expectedCommit = System.getenv('GITHUB_SHA').take(9) // GitHub SHA is full hash, we use short form
if (actualCommit != expectedCommit) {
throw new GradleException("Commit ID mismatch: build-info.properties has '${actualCommit}' but GITHUB_SHA is '${expectedCommit}'. Run 'make assemble' to fix.")
}
println "✅ Build info validation passed: build=${actualBuild}, commitId=${actualCommit}"
println "✅ Plugin version validation passed: all ${expected.size()} plugin versions match"
}
}
/*
* Compile sources and copies all libs to target directory
*/
task compile {
dependsOn allprojects.classes
}
def getRuntimeConfigs() {
def names = subprojects
.findAll { prj -> prj.name in ['nextflow','nf-commons','nf-httpfs','nf-lang','nf-lineage'] }
.collect { it.name }
FileCollection result = null
for( def it : names ) {
def cfg = project(it).configurations.getByName('runtimeClasspath')
if( result==null )
result = cfg
else
result += cfg
// this include the module actual jar file
// note: migrating to gradle 7 does not work any more
//result = result + cfg.getOutgoing().getArtifacts().getFiles()
}
return result?.files ?: []
}
/*
* Save the runtime classpath
* NOTE: This task uses a provider to delay execution, but still triggers configuration
* resolution when the provider is evaluated. While not ideal for Gradle 9.1's strict
* configuration resolution requirements, this approach works in practice for our use case.
*/
task exportClasspath {
dependsOn allprojects.jar
// Use provider to delay configuration resolution until task execution
def configurationFiles = provider {
def libs = []
// Resolve configurations during provider evaluation (not ideal but functional)
['nextflow','nf-commons','nf-httpfs','nf-lang','nf-lineage'].each { moduleName ->
def moduleProject = project(":$moduleName")
def cfg = moduleProject.configurations.getByName('runtimeClasspath')
libs.addAll(cfg.files.collect { it.canonicalPath })
}
// Add module jars
['nextflow','nf-commons','nf-httpfs','nf-lang','nf-lineage'].each {
libs << file("modules/$it/build/libs/${it}-${version}.jar").canonicalPath
}
return libs.unique()
}
inputs.files(configurationFiles)
outputs.file('.launch.classpath')
doLast {
def libs = configurationFiles.get()
file('.launch.classpath').text = libs.join(':')
}
}
ext.nexusUsername = project.findProperty('nexusUsername') ?: System.getenv('AWS_ACCESS_KEY_ID')
ext.nexusPassword = project.findProperty('nexusPassword') ?: System.getenv('AWS_SECRET_ACCESS_KEY')
ext.nexusFullName = project.findProperty('nexusFullName')
ext.nexusEmail = project.findProperty('nexusEmail')
// `signing.keyId` property needs to be defined in the `gradle.properties` file
ext.enableSignArchives = project.findProperty('signing.keyId')
ext.coreProjects = projects( ':nextflow', ':nf-commons', ':nf-httpfs', ':nf-lang', ':nf-lineage' )
configure(coreProjects) {
group = 'io.nextflow'
version = rootProject.file('VERSION').text.trim()
}
/*
* Maven central deployment
* http://central.sonatype.org/pages/gradle.html
*/
configure(coreProjects) {
apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocJar(type: Jar) {
archiveClassifier = 'javadoc'
from configurations.groovyDoc
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
publishing {
publications {
mavenJava(MavenPublication) {
suppressPomMetadataWarningsFor('testFixturesApiElements')
suppressPomMetadataWarningsFor('testFixturesRuntimeElements')
from components.java
versionMapping {
usage('java-api') {
fromResolutionOf('runtimeClasspath')
}
usage('java-runtime') {
fromResolutionResult()
}
}
pom {
name = 'Nextflow'
description = 'A DSL modelled around the UNIX pipe concept, that simplifies writing parallel and scalable pipelines in a portable manner'
url = 'http://www.nextflow.io'
licenses {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = nexusUsername
name = nexusFullName
email = nexusEmail
}
}
scm {
connection = 'scm:git:https://github.com/nextflow-io/nextflow'
developerConnection = 'scm:git:git@github.com:nextflow-io/nextflow.git'
url = 'https://github.com/nextflow-io/nextflow'
}
}
artifact sourcesJar
artifact javadocJar
}
}
repositories {
maven {
name = 'Seqera'
// change URLs to point to your repos, e.g. http://my.org/repo
def releasesRepoUrl = "s3://maven.seqera.io/releases/"
def snapshotsRepoUrl = "s3://maven.seqera.io/snapshots/"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
credentials(AwsCredentials) {
accessKey nexusUsername
secretKey nexusPassword
}
}
}
}
signing {
required { enableSignArchives }
sign publishing.publications.mavenJava
}
}
String bytesToHex(byte[] bytes) {
StringBuffer result = new StringBuffer();
for (byte byt : bytes) result.append(Integer.toString((byt & 0xff) + 0x100, 16).substring(1));
return result.toString();
}
task makeDigest { doLast {
byte[] digest
String str = file('nextflow').text
// create sha1
digest = java.security.MessageDigest.getInstance("SHA1").digest(str.getBytes())
file('nextflow.sha1').text = new BigInteger(1, digest).toString(16) + '\n'
// create sha-256
digest = java.security.MessageDigest.getInstance("SHA-256").digest(str.getBytes())
file('nextflow.sha256').text = bytesToHex(digest) + '\n'
// create md5
digest = java.security.MessageDigest.getInstance("MD5").digest(str.getBytes())
file('nextflow.md5').text = bytesToHex(digest) + '\n'
}}
// Make releaseInfo task automatically run makeDigest after updating versions
releaseInfo.finalizedBy makeDigest
task upload {
dependsOn compile
dependsOn coreProjects.publish
dependsOn validatePluginVersions
}
if( System.env.BUILD_PACK ) {
apply from: 'packing.gradle'
}