Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -915,11 +915,17 @@ The `lint` command parses and analyzes the given Nextflow scripts and config fil
**Options**

`-exclude`
: File pattern to exclude from linting. Can be specified multiple times (default: `.git, .nf-test, work`).
: File pattern to exclude from linting (default: `.git, .lineage, .nextflow, .nf-test, nf-test.config, work`).
: Can be specified multiple times.

`-format`
: Format scripts and config files that have no errors.

`-lib`
: :::{versionadded} 26.04.0
:::
: Path to lib directory (default: `'./lib'`).

`-o, -output`
: Output mode for reporting errors: `full`, `extended`, `concise`, `json`, `markdown` (default: `full`).

Expand Down
19 changes: 3 additions & 16 deletions modules/nextflow/src/main/groovy/nextflow/Session.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import nextflow.trace.event.TaskEvent
import nextflow.trace.event.WorkflowOutputEvent
import nextflow.trace.event.WorkflowPublishEvent
import nextflow.util.Barrier
import nextflow.util.ConfigHelper
import nextflow.util.ClassLoaderFactory
import nextflow.util.Duration
import nextflow.util.HistoryFile
import nextflow.util.LoggerHelper
Expand Down Expand Up @@ -601,21 +601,8 @@ class Session implements ISession {
ScriptBinding getBinding() { binding }

@Memoized
ClassLoader getClassLoader() { getClassLoader0() }

@PackageScope
ClassLoader getClassLoader0() {
// extend the class-loader if required
final gcl = new GroovyClassLoader()
final libraries = ConfigHelper.resolveClassPaths(getLibDir())

for( Path lib : libraries ) {
def path = lib.complete()
log.debug "Adding to the classpath library: ${path}"
gcl.addClasspath(path.toString())
}

return gcl
ClassLoader getClassLoader() {
ClassLoaderFactory.create(getLibDir())
}

Barrier getBarrier() { monitorsBarrier }
Expand Down
14 changes: 12 additions & 2 deletions modules/nextflow/src/main/groovy/nextflow/cli/CmdLint.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import nextflow.script.formatter.ScriptFormattingVisitor
import nextflow.script.parser.v2.ErrorListener
import nextflow.script.parser.v2.ErrorSummary
import nextflow.script.parser.v2.StandardErrorListener
import nextflow.util.ClassLoaderFactory
import nextflow.util.PathUtils
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
Expand All @@ -62,7 +63,13 @@ class CmdLint extends CmdBase {
names = ['-exclude'],
description = 'File pattern to exclude from error checking (can be specified multiple times)'
)
List<String> excludePatterns = ['.git', '.lineage', '.nf-test', '.nextflow', 'work', 'nf-test.config']
List<String> excludePatterns = ['.git', '.lineage', '.nextflow', '.nf-test', 'nf-test.config', 'work']

@Parameter(
names = ['-lib'],
description = 'Path to lib directory (default: ./lib)'
)
String libDir

@Parameter(
names = ['-o', '-output'],
Expand Down Expand Up @@ -121,7 +128,10 @@ class CmdLint extends CmdBase {
if( !spaces && !tabs )
spaces = 4

scriptParser = new ScriptParser()
final libPath = Path.of(libDir ?: 'lib')
final classLoader = ClassLoaderFactory.create([ libPath ])

scriptParser = new ScriptParser(classLoader)
configParser = new ConfigParser()
errorListener = outputMode == 'json'
? new JsonErrorListener()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2013-2025, 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.
*/

package nextflow.util

import java.nio.file.Path

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
/**
* Helper methods to create class loaders with
* additional classpaths.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
@CompileStatic
class ClassLoaderFactory {

/**
* Create a class loader with the given directories
* added to the classpath.
*
* @param dirs
*/
static GroovyClassLoader create(List<Path> dirs) {
final gcl = new GroovyClassLoader()
final libraries = resolveClassPaths(dirs)
for( final lib : libraries ) {
gcl.addClasspath(lib.complete().toString())
}
return gcl
}

/**
* Given a list of directories, look for the files ending with
* the '.jar' extension and return a list containing the original
* directories and the JAR paths.
*
* @param dirs
*/
static List<Path> resolveClassPaths(List<Path> dirs) {

List<Path> result = []

if( !dirs )
return result

for( final path : dirs ) {
if( path.isFile() && path.name.endsWith('.jar') ) {
result << path
}
else if( path.isDirectory() ) {
result << path
path.eachFileMatch( ~/.+\.jar$/ ) {
if( it.isFile() )
result << it
}
}
}

return result
}

}
28 changes: 0 additions & 28 deletions modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package nextflow.util

import java.nio.file.Path

import groovy.json.JsonOutput
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
Expand Down Expand Up @@ -69,32 +67,6 @@ class ConfigHelper {
return obj
}

/**
* Given a list of paths looks for the files ending with the extension '.jar' and return
* a list containing the original directories, plus the JARs paths
*
* @param dirs
* @return
*/
static List<Path> resolveClassPaths( List<Path> dirs ) {

List<Path> result = []
if( !dirs )
return result

for( Path path : dirs ) {
if( path.isFile() && path.name.endsWith('.jar') ) {
result << path
}
else if( path.isDirectory() ) {
result << path
path.eachFileMatch( ~/.+\.jar$/ ) { if(it.isFile()) result << it }
}
}

return result
}

static private final String TAB = ' '

static private void canonicalFormat(StringBuilder writer, ConfigObject object, int level, boolean sort, List stack) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class RemoteSession implements Serializable, Closeable {
private transient List<Path> localPaths = deserialiseClasspath()

@Lazy
private transient List<Path> resolvedClasspath = ConfigHelper.resolveClassPaths(localPaths)
private transient List<Path> resolvedClasspath = ClassLoaderFactory.resolveClassPaths(localPaths)

protected RemoteSession() { }

Expand Down
35 changes: 35 additions & 0 deletions modules/nextflow/src/test/groovy/nextflow/cli/CmdLintTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,39 @@ class CmdLintTest extends Specification {
dir?.deleteDir()
}

def 'should add lib directory to class loader' () {

given:
def dir = Files.createTempDirectory('test')

dir.resolve('main.nf').text = '''\
println Utils.hello()
'''

dir.resolve('lib').mkdir()
dir.resolve('lib/Utils.groovy').text = '''\
class Utils {

String hello() {
return 'Hello!'
}
}
'''

when:
def cmd = new CmdLint()
cmd.args = [dir.toString()]
cmd.libDir = dir.resolve('lib').toString()
cmd.launcher = Mock(Launcher) {
getOptions() >> Mock(CliOptions)
}
cmd.run()

then:
noExceptionThrown()

cleanup:
dir?.deleteDir()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2013-2024, 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.
*/

package nextflow.util

import java.nio.file.Files
import java.nio.file.Path

import spock.lang.Specification
/**
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
class ClassLoaderFactoryTest extends Specification {

def testResolveClasspaths() {

given:
def path1 = Files.createTempDirectory('path1')
path1.resolve('file1').text = 'File 1'
path1.resolve('file2.jar').text = 'File 2'
path1.resolve('dir').mkdir()
path1.resolve('dir/file3').text = 'File 3'
path1.resolve('dir/file4').text = 'File 4'

def path2 = Files.createTempDirectory('path2')
path2.resolve('file5').text = 'File 5'
path2.resolve('file6.jar').text = 'File 6'

def path3 = Path.of('/some/file')

when:
def list = ClassLoaderFactory.resolveClassPaths([path1, path2, path3])
then:
list.size() == 4
list.contains( path1 )
list.contains( path1.resolve('file2.jar') )
list.contains( path2 )
list.contains( path2.resolve('file6.jar') )

cleanup:
path1?.deleteDir()
path2?.deleteDir()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package nextflow.util

import java.nio.file.Files
import java.nio.file.Paths

import nextflow.config.ConfigClosurePlaceholder
import spock.lang.Specification
import spock.lang.Unroll
Expand Down Expand Up @@ -47,37 +44,6 @@ class ConfigHelperTest extends Specification {

}

def testResolveClasspaths() {

given:
def path1 = Files.createTempDirectory('path1')
path1.resolve('file1').text = 'File 1'
path1.resolve('file2.jar').text = 'File 2'
path1.resolve('dir').mkdir()
path1.resolve('dir/file3').text = 'File 3'
path1.resolve('dir/file4').text = 'File 4'

def path2 = Files.createTempDirectory('path2')
path2.resolve('file5').text = 'File 5'
path2.resolve('file6.jar').text = 'File 6'

def path3 = Paths.get('/some/file')

when:
def list = ConfigHelper.resolveClassPaths([path1, path2, path3])
then:
list.size() == 4
list.contains( path1 )
list.contains( path1.resolve('file2.jar') )
list.contains( path2 )
list.contains( path2.resolve('file6.jar') )

cleanup:
path1?.deleteDir()
path2?.deleteDir()

}

def 'should render config using properties notation' () {

given:
Expand Down
Loading
Loading