Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,14 +915,20 @@ 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.

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

`-project-dir`
: :::{versionadded} 26.04.0
:::
: Path to project directory (default: `'.'`). Used to locate project-level assets such as the lib directory and modules directory.

`-sort-declarations`
: Sort script declarations in Nextflow scripts (default: `false`).

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 @@ -80,7 +80,7 @@ import nextflow.trace.event.FilePublishEvent
import nextflow.trace.event.TaskEvent
import nextflow.trace.event.WorkflowOutputEvent
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 @@ -602,21 +602,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
15 changes: 13 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,7 @@ 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 = ['-o', '-output'],
Expand All @@ -82,6 +83,12 @@ class CmdLint extends CmdBase {
}
}

@Parameter(
names = ['-project-dir'],
description = 'Path to project directory (default: .)'
)
String projectDir = '.'

@Parameter(names = ['-format'], description = 'Format scripts and config files that have no errors')
boolean formatting

Expand Down Expand Up @@ -121,7 +128,11 @@ class CmdLint extends CmdBase {
if( !spaces && !tabs )
spaces = 4

scriptParser = new ScriptParser()
final baseDir = Path.of(projectDir)
final libDir = baseDir.resolve('lib')
final classLoader = ClassLoaderFactory.create([ libDir ])

scriptParser = new ScriptParser(baseDir, 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.projectDir = dir.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