Skip to content

Commit e2c77c6

Browse files
jorgeebentsherman
andauthored
Allow module run to run modules with local path (#7057)
Co-authored-by: Ben Sherman <bentshermann@gmail.com>
1 parent 433b10a commit e2c77c6

3 files changed

Lines changed: 118 additions & 26 deletions

File tree

docs/reference/cli.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,11 +1277,10 @@ The `module` command provides a comprehensive system for managing registry-based
12771277

12781278
(cli-module-run)=
12791279

1280-
`run [options] [namespace/name] [--<input_name> <input-value>]`
1280+
`run [options] [namespace/name | path] [--<input_name> <input-value>]`
12811281

1282-
: Execute a module directly from the registry without creating a wrapper workflow.
1283-
: Automatically downloads the module if not already installed. Accepts all standard Nextflow run options.
1284-
: The `module run` command extends the `run` command and accepts all its options, including `-profile`, `-resume`, `-c`, etc. Command-line params (i.e., `--<input_name>`) are inferred from the module's declared inputs.
1282+
: Execute a module directly. Can be a remote module (`namespace/name`) or a local module path (beginning with `./`, `../`, or `/`). Automatically downloads the module if not already installed.
1283+
: Accepts all standard Nextflow run options, including `-profile`, `-resume`, `-c`, etc. Command-line params (i.e., `--<input_name>`) are inferred from the module's declared inputs.
12851284
: The following additional options are available:
12861285

12871286
`-version`
@@ -1290,15 +1289,20 @@ The `module` command provides a comprehensive system for managing registry-based
12901289
: **Examples:**
12911290

12921291
```console
1293-
# Run module with inputs
1294-
$ nextflow module run nf-core/fastqc --input 'data/*.fastq.gz'
1292+
# Run remote module
1293+
$ nextflow module run nf-core/fastqc \
1294+
--input 'data/*.fastq.gz'
12951295

1296-
# Run specific version with Nextflow options
1296+
# Run remote module with specific version and run options
12971297
$ nextflow module run nf-core/fastqc \
1298-
--input 'data/*.fastq.gz' \
12991298
-version 1.0.0 \
1300-
-profile docker \
1299+
--input 'data/*.fastq.gz' \
1300+
-with-conda \
13011301
-resume
1302+
1303+
# Run local module
1304+
$ nextflow module run ./modules/nf-core/fastqc/main.nf \
1305+
--input 'data/*.fastq.gz'
13021306
```
13031307

13041308
(cli-module-search)=

modules/nextflow/src/main/groovy/nextflow/cli/module/CmdModuleRun.groovy

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,22 @@
1616

1717
package nextflow.cli.module
1818

19+
import java.nio.file.Path
20+
1921
import com.beust.jcommander.Parameter
2022
import com.beust.jcommander.Parameters
2123
import groovy.transform.CompileStatic
2224
import io.seqera.npr.client.RegistryClient
25+
import nextflow.Const
2326
import nextflow.cli.CmdRun
2427
import nextflow.config.ConfigBuilder
2528
import nextflow.config.RegistryConfig
2629
import nextflow.exception.AbortOperationException
2730
import nextflow.module.ModuleReference
28-
import nextflow.module.RegistryClientFactory
2931
import nextflow.module.ModuleResolver
30-
32+
import nextflow.module.RegistryClientFactory
3133
import nextflow.util.TestOnly
3234

33-
import java.nio.file.Path
34-
import java.nio.file.Paths
35-
3635
/**
3736
* Module run subcommand
3837
*
@@ -58,31 +57,53 @@ class CmdModuleRun extends CmdRun {
5857
@Override
5958
void run() {
6059
if( !args ) {
61-
throw new AbortOperationException("Arguments not provided")
60+
throw new AbortOperationException("Module name/path not provided")
61+
}
62+
63+
final moduleFile = isLocalModule(args[0])
64+
? resolveLocalModule(args[0])
65+
: resolveRemoteModule(args[0], version)
66+
67+
if( moduleFile ) {
68+
args[0] = moduleFile.toAbsolutePath().toString()
69+
super.run()
6270
}
71+
}
72+
73+
private boolean isLocalModule(String str) {
74+
return str.startsWith('/') || str.startsWith('./') || str.startsWith('../')
75+
}
76+
77+
private Path resolveLocalModule(String str) {
78+
final module = Path.of(str).toAbsolutePath().normalize()
79+
final path = module.isDirectory() ? module.resolve(Const.DEFAULT_MAIN_FILE_NAME) : module
80+
if( !path.exists() )
81+
throw new AbortOperationException("Invalid module path: ${str}")
82+
return path
83+
}
6384

85+
private Path resolveRemoteModule(String name, String version) {
6486
// Parse and validate module reference
6587
ModuleReference reference
6688
try {
67-
reference = ModuleReference.parse(args[0])
89+
reference = ModuleReference.parse(name)
6890
} catch( Exception e ) {
69-
throw new AbortOperationException("Invalid module reference: ${args[0]}", e)
91+
throw new AbortOperationException("Invalid module reference: ${name}", e)
7092
}
7193

7294
// Get config
73-
def baseDir = root ?: Paths.get('.').toAbsolutePath().normalize()
74-
def config = new ConfigBuilder()
95+
final baseDir = root ?: Path.of('.').toAbsolutePath().normalize()
96+
final config = new ConfigBuilder()
7597
.setOptions(launcher.options)
7698
.setBaseDir(baseDir)
7799
.build()
78100

79-
def registryConfig = config.navigate('registry') as RegistryConfig ?: new RegistryConfig()
80-
81-
def resolver = new ModuleResolver(baseDir, client ?: RegistryClientFactory.forConfig(registryConfig))
82-
Path moduleFile = resolver.installModule(reference, version)
83-
if( moduleFile ) {
84-
args[0] = moduleFile.toAbsolutePath().toString()
85-
super.run()
101+
final registryConfig = new RegistryConfig(config.registry as Map ?: Collections.emptyMap())
102+
try {
103+
final resolver = new ModuleResolver(baseDir, client ?: RegistryClientFactory.forConfig(registryConfig))
104+
return resolver.installModule(reference, version)
105+
} catch( Exception e ) {
106+
throw new AbortOperationException("Unable to install module: ${name}", e)
86107
}
87108
}
88109
}

modules/nextflow/src/test/groovy/nextflow/cli/module/CmdModuleRunTest.groovy

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,73 @@ class CmdModuleRunTest extends Specification {
195195

196196
}
197197

198+
def 'should run module from local path'() {
199+
given:
200+
def moduleScript = '''
201+
process CREATE_LOCAL_FILE {
202+
output:
203+
path "local_output.txt"
204+
205+
script:
206+
"""
207+
echo "Local module executed" > local_output.txt
208+
"""
209+
}
210+
'''.stripIndent()
211+
212+
and:
213+
// Create a local module directory with main.nf
214+
def moduleDir = tempDir.resolve('local-module')
215+
Files.createDirectories(moduleDir)
216+
moduleDir.resolve('main.nf').text = moduleScript
217+
218+
def escapedPath = Pattern.quote(tempDir.toString())
219+
def pattern = ~/"${escapedPath}\/.+\/local_output\.txt"/
220+
221+
and:
222+
def cmd = new CmdModuleRun()
223+
def opts = new CliOptions()
224+
opts.setQuiet(true)
225+
cmd.launcher = Mock(Launcher) {
226+
getOptions() >> opts
227+
getCliString() >> "nextflow module run ${moduleDir}"
228+
}
229+
cmd.args = [moduleDir.toString()]
230+
cmd.root = tempDir
231+
cmd.workDir = tempDir.toString()
232+
cmd.outputDir = tempDir.resolve('results').toString()
233+
cmd.outputFormat = 'json'
234+
235+
when:
236+
cmd.run()
237+
def stdout = capture
238+
.toString()
239+
.readLines()// remove the log part
240+
.findResults { line -> !line.contains('DEBUG') ? line : null }
241+
.findResults { line -> !line.contains('INFO') ? line : null }.join(" ")
242+
243+
then:
244+
assert (stdout =~ pattern).find()
245+
}
246+
247+
def 'should fail when path and module do not exist'() {
248+
given:
249+
def nonExistentPath = tempDir.resolve('does-not-exist').toString()
250+
def cmd = new CmdModuleRun()
251+
cmd.launcher = Mock(Launcher) {
252+
getOptions() >> null
253+
}
254+
cmd.args = [nonExistentPath]
255+
cmd.root = tempDir
256+
257+
when:
258+
cmd.run()
259+
260+
then:
261+
def e = thrown(AbortOperationException)
262+
e.message.contains('Invalid module path')
263+
}
264+
198265
def 'should fail with no arguments'() {
199266
given:
200267
def cmd = new CmdModuleRun()

0 commit comments

Comments
 (0)