diff --git a/modules/nextflow/src/main/groovy/nextflow/Session.groovy b/modules/nextflow/src/main/groovy/nextflow/Session.groovy index 0d54c60a3d..b0128cadad 100644 --- a/modules/nextflow/src/main/groovy/nextflow/Session.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/Session.groovy @@ -902,6 +902,23 @@ class Session implements ISession { NF.isModuleBinariesEnabled() } + /** + * Whether the entry script was launched directly as a module via + * `nextflow module run`. Used to decide whether the entry script's + * `resources/` bundle (and module bin paths) should be picked up + * even though the script is not being loaded via `include`. + */ + private volatile boolean moduleRun + + boolean isModuleRun() { + return moduleRun + } + + Session setModuleRun(boolean value) { + this.moduleRun = value + return this + } + boolean failOnIgnore() { config.navigate('workflow.failOnIgnore', false) as boolean } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy index 1f1cbbb712..c7703bfa86 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy @@ -414,6 +414,7 @@ class CmdRun extends CmdBase implements HubOptions { runner.session.agentLog = SysEnv.isAgentMode() runner.session.debug = launcher.options.remoteDebug runner.session.disableJobsCancellation = getDisableJobsCancellation() + runner.session.setModuleRun(isModuleRun()) final isTowerEnabled = config.navigate('tower.enabled') as Boolean final isDataEnabled = config.navigate("lineage.enabled") as Boolean @@ -491,6 +492,12 @@ class CmdRun extends CmdBase implements HubOptions { printLaunchInfo(repo, head, revision) } + /** + * @return {@code true} when the entry script is being launched directly as a + * module via `nextflow module run`. Overridden by {@link nextflow.cli.module.CmdModuleRun}. + */ + protected boolean isModuleRun() { false } + static void detectModuleBinaryFeature(ConfigMap config) { final moduleBinaries = config.navigate('nextflow.enable.moduleBinaries', false) if( moduleBinaries ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/module/CmdModuleRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/module/CmdModuleRun.groovy index 7753545875..779d6606a1 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/module/CmdModuleRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/module/CmdModuleRun.groovy @@ -54,6 +54,9 @@ class CmdModuleRun extends CmdRun { return 'run' } + @Override + protected boolean isModuleRun() { true } + @Override void run() { if( !args ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index 0fe47e782c..628367c8e8 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -1569,7 +1569,12 @@ class TaskProcessor { ResourcesBundle getModuleBundle() { final script = this.getOwnerScript() final meta = ScriptMeta.get(script) - return meta?.isModule() ? meta.getModuleBundle() : null + if( meta?.scriptPath == null ) + return null + // The bundle is resolved when the owner script is either an included + // module, or it is the entry script of a `nextflow module run` + // invocation -- see #7087 + return (meta.isModule() || session.isModuleRun()) ? meta.getModuleBundle() : null } @Memoized diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy index a4f6c7ef2d..6bebca3c71 100644 --- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy @@ -35,6 +35,7 @@ import nextflow.script.BaseScript import nextflow.script.BodyDef import nextflow.script.ProcessConfig import nextflow.script.ProcessConfigV1 +import nextflow.script.ScriptMeta import nextflow.script.ScriptType import nextflow.script.bundle.ResourcesBundle import nextflow.script.params.FileInParam @@ -100,6 +101,49 @@ class TaskProcessorTest extends Specification { } + @Unroll + def 'should resolve module bundle for entry script when running as module #desc' () { + given: + def folder = Files.createTempDirectory('test') + def mod = folder.resolve('mod1'); mod.mkdir() + def bin = mod.resolve('resources/usr/bin'); bin.mkdirs() + def scriptPath = mod.resolve('main.nf'); Files.createFile(scriptPath) + Files.createFile(bin.resolve('echo.sh')) + and: + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) { + getScriptPath() >> scriptPath + isModule() >> IS_MODULE + getModuleBundle() >> ResourcesBundle.scan(mod.resolve('resources')) + } + and: + def session = Mock(Session) { + getConfig() >> [:] + isModuleRun() >> IS_MODULE_RUN + } + def executor = Mock(Executor) {} + def processor = Spy(TaskProcessor, constructorArgs: [[session:session, executor:executor]]) + processor.getOwnerScript() >> script + + when: + ResourcesBundle bundle + GroovyMock(ScriptMeta, global: true) + ScriptMeta.get(script) >> meta + bundle = processor.getModuleBundle() + + then: + (bundle != null) == EXPECTED + + cleanup: + folder?.deleteDir() + + where: + desc | IS_MODULE | IS_MODULE_RUN | EXPECTED + '(included module)' | true | false | true + '(nextflow module run entry)' | false | true | true + '(plain entry, no module run flag)' | false | false | false + } + @Unroll def 'should add module bin paths to task env' () { given: