diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index 0fe47e782c..8f19093aec 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -1569,7 +1569,11 @@ class TaskProcessor { ResourcesBundle getModuleBundle() { final script = this.getOwnerScript() final meta = ScriptMeta.get(script) - return meta?.isModule() ? meta.getModuleBundle() : null + // Resolve the resources bundle whenever the owner script has a known path, + // not only when it was loaded as an included module. This allows module + // binaries to be picked up also when a module is launched directly as the + // entry script via `nextflow module run` -- see #7087 + return meta?.getScriptPath() ? 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..17830d4495 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,41 @@ class TaskProcessorTest extends Specification { } + def 'should resolve module bundle when script path is set regardless of isModule' () { + 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 + // Simulate the failing case: script is loaded as the entry, not as an included module + isModule() >> false + getModuleBundle() >> ResourcesBundle.scan(mod.resolve('resources')) + } + and: + def session = Mock(Session) { getConfig() >> [:] } + 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 + bundle.getBinDirs() == [bin] + + cleanup: + folder?.deleteDir() + } + @Unroll def 'should add module bin paths to task env' () { given: