diff --git a/.editorconfig b/.editorconfig index 404977be2c..d5de1ec2ff 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,10 @@ indent_size = 2 [*.{py,md}] indent_style = unset +# Makefiles require tabs +[Makefile] +indent_style = tab + # ignore nf-core files [side-quests/solutions/nf-core/**] charset = unset diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3dfaec2a0b..4972c9cc9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,7 @@ repos: hooks: - id: editorconfig-checker alias: ec + exclude: hello-plugins/.*/nf-greeting/(COPYING|gradlew)$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -19,6 +20,8 @@ repos: - id: trailing-whitespace exclude_types: - svg + exclude: hello-plugins/.*/COPYING$ - id: end-of-file-fixer exclude_types: - svg + exclude: hello-plugins/.*/COPYING$ diff --git a/.prettierignore b/.prettierignore index 01e5fa4bf7..affec13aaa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,6 @@ side-quests/solutions/nf-core/**/*.yaml side-quests/solutions/nf-core/**/*.json side-quests/solutions/nf-core/**/*.html side-quests/solutions/nf-core/**/*.yml + +# Ignore auto-generated license files in plugin solutions +hello-plugins/**/COPYING diff --git a/docs/hello_plugins/00_orientation.md b/docs/hello_plugins/00_orientation.md new file mode 100644 index 0000000000..796f1f0c0c --- /dev/null +++ b/docs/hello_plugins/00_orientation.md @@ -0,0 +1,112 @@ +# Orientation + +This page will help you set up your environment and navigate through the training. + +!!! warning "Development sections are advanced" + + Using existing plugins (Parts 1-2) is straightforward and valuable for all Nextflow users. + + However, **developing your own plugins** (Parts 3-7) is an advanced topic. + It involves Java/Groovy programming, build tools, and software engineering concepts that may be unfamiliar if you come from a pure bioinformatics background. + + Most Nextflow users will never need to develop plugins. + The existing plugin ecosystem covers the vast majority of use cases. + If development sections feel challenging, focus on Parts 1-2 and bookmark the rest for later. + +--- + +## 1. Open the training environment + +If you haven't yet done so, make sure to open the training environment as described in the [Environment Setup](../envsetup/index.md). + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training?quickstart=1&ref=master) + +--- + +## 2. Verify Java installation + +For plugin development (Parts 3-7), you need Java 21 or later. +Check that Java is available: + +```bash +java -version +``` + +You should see Java 21 or later. +The training Codespace comes with Java pre-installed. + +??? info "What are Java, Groovy, and Gradle?" + + If these terms are unfamiliar, here's a quick primer: + + **Java** is a widely-used programming language. + Nextflow itself is built with Java, and plugins must be compatible with the Java runtime. + + **Groovy** is a programming language that runs on Java and is designed to be more concise and flexible. + Nextflow's DSL is based on Groovy, which is why Nextflow syntax looks the way it does. + Plugin code is typically written in Groovy. + + **Gradle** is a build tool that compiles code, runs tests, and packages software. + You don't need to understand Gradle deeply; we'll use simple commands like `./gradlew build`. + + The good news: you don't need to be an expert in any of these. + Many successful Nextflow plugin authors come from bioinformatics backgrounds, not Java development. + We'll explain the relevant concepts as we go, and the plugin template handles most of the complexity for you. + +--- + +## 3. Move into the project directory + +```bash +cd hello-plugins +``` + +--- + +## 4. Review the materials + +```console title="Directory contents" +. +├── greetings.csv +├── main.nf +├── nextflow.config +└── random_id_example.nf +``` + +We have a simple greeting pipeline and materials for both using and developing plugins. + +--- + +## 5. What we'll cover + +This training is organized into two parts: + +**Using plugins (Parts 1-2):** + +- Understand plugin architecture and extension types +- Discover, install, and use existing plugins like `nf-hello` + +**Developing plugins (Parts 3-7):** + +- Create a plugin project with `nf-greeting` +- Implement custom functions +- Build, test, and install locally +- Add trace observers for lifecycle events +- Make plugins configurable +- Distribute your plugin + +--- + +## 6. Readiness checklist + +- [ ] My codespace is running +- [ ] I'm in the `hello-plugins` directory +- [ ] Java is installed (required for plugin development parts) + +--- + +### What's next? + +In the next section, you'll learn about plugin architecture and start using existing plugins. + +[Continue to Part 1 :material-arrow-right:](01_plugin_basics.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/01_plugin_basics.md b/docs/hello_plugins/01_plugin_basics.md new file mode 100644 index 0000000000..d3a40d0632 --- /dev/null +++ b/docs/hello_plugins/01_plugin_basics.md @@ -0,0 +1,267 @@ +# Part 1: Plugin Basics + +Before diving into plugin usage and development, this section explains what plugins are and how they extend Nextflow. +Then you'll learn how to discover and use existing plugins from the community. + +!!! tip "This is the most important part for most users" + + Even if you never develop your own plugin, knowing how to use existing plugins is valuable. + Many powerful features are available through plugins, such as input validation with nf-schema. + If plugin development seems daunting, focus on mastering this part first. + +--- + +## 1. Plugin architecture + +### 1.1. How plugins extend Nextflow + +Nextflow's plugin system is built on [PF4J](https://pf4j.org/), a lightweight plugin framework for Java. +Plugins can extend Nextflow in several ways: + +| Extension Type | Purpose | Example | +| --------------- | ---------------------------------------- | ----------------------- | +| Functions | Custom functions callable from workflows | `samplesheetToList()` | +| Executors | Custom task execution backends | AWS Batch, Kubernetes | +| Filesystems | Custom storage backends | S3, Azure Blob | +| Trace Observers | Monitor workflow execution | Custom logging, metrics | + +Plugins can enhance Nextflow's functionality without modifying its core code, making them ideal for adding supplementary features to pipelines. + +### 1.2. Why use plugins? + +You can define custom functions directly in your Nextflow scripts, so why use plugins? + +| Approach | Best for | Limitations | +| ------------------- | ---------------------- | ------------------------------------------- | +| **Local functions** | Project-specific logic | Copy-paste between pipelines, no versioning | +| **Plugins** | Reusable utilities | Requires Java/Groovy knowledge to create | + +Plugins are ideal when you need to: + +- Share functionality across multiple pipelines +- Extend existing pipelines with extra features (e.g., Slack notifications) +- Version and manage dependencies properly +- Access Nextflow internals (channels, sessions, lifecycle events, etc.) +- Integrate with external infrastructure (cloud platforms, storage systems) + +--- + +## 2. Discovering plugins + +The [Nextflow Plugin Registry](https://registry.nextflow.io/) is the central hub for finding available plugins. +Browse the registry to discover plugins for: + +- Input validation and samplesheet parsing +- Cloud platform integration (AWS, Google Cloud, Azure) +- Provenance tracking and reporting +- Notifications (Slack, Teams) +- And more + +Each plugin page in the registry shows: + +- Description and purpose +- Available versions +- Installation instructions +- Links to documentation and source code + +![The nf-hello plugin page on registry.nextflow.io](img/plugin-registry-nf-hello.png) + +You can also search GitHub for repositories with the `nf-` prefix, as most Nextflow plugins follow this naming convention. + +??? exercise "Explore the registry" + + Take a few minutes to browse the [Nextflow Plugin Registry](https://registry.nextflow.io/). + + 1. Find a plugin that provides Slack notifications + 2. Look at nf-schema. How many downloads does it have? + 3. Find a plugin that was released in the last month + + This familiarity will help you discover useful plugins for your own pipelines. + +--- + +## 3. Installing plugins + +Plugins are declared in your `nextflow.config` file using the `plugins {}` block: + +```groovy title="nextflow.config" +plugins { + id 'nf-schema@2.1.1' +} +``` + +Key points: + +- Use the `id` keyword followed by the plugin name +- Specify a version with `@version` (recommended for reproducibility); if omitted, the latest version is used +- Nextflow automatically downloads plugins from the registry at runtime + +!!! info "Local vs published plugins" + + When you add a plugin to your `nextflow.config`, Nextflow automatically downloads it from the plugin registry the first time you run your pipeline. + The plugin is then cached locally in `$NXF_HOME/plugins/` (typically `~/.nextflow/plugins/`). + + Later in this training (Parts 3-7), we'll develop our own plugin and install it locally for testing. + Part 7 covers how to publish plugins for others to use. + +--- + +## 4. Importing plugin functions + +Once a plugin is installed, you can import its functions using the familiar `include` syntax with a special `plugin/` prefix: + +```groovy title="main.nf" +include { samplesheetToList } from 'plugin/nf-schema' +``` + +This imports the `samplesheetToList` function from the nf-schema plugin, making it available in your workflow. + +--- + +## 5. Example: Using nf-schema for validation + +The nf-schema plugin is widely used in nf-core pipelines for input validation. +Here's how it works in practice: + +```groovy title="main.nf" linenums="1" +#!/usr/bin/env nextflow + +include { samplesheetToList } from 'plugin/nf-schema' + +params.input = 'samplesheet.csv' + +workflow { + // Validate and parse input samplesheet + ch_samples = Channel.fromList( + samplesheetToList(params.input, "assets/schema_input.json") + ) + + ch_samples.view { sample -> "Sample: $sample" } +} +``` + +The `samplesheetToList` function: + +1. Reads the input CSV file +2. Validates it against a JSON schema +3. Returns a list of validated entries +4. Throws helpful errors if validation fails + +This pattern is used extensively in nf-core pipelines to ensure input data is valid before processing begins. + +--- + +## 6. Plugin configuration + +Some plugins accept configuration options in `nextflow.config`: + +```groovy title="nextflow.config" +plugins { + id 'nf-schema@2.1.1' +} + +// Plugin-specific configuration +validation { + monochromeLogs = true + ignoreParams = ['custom_param'] +} +``` + +Each plugin documents its configuration options. +Check the plugin's documentation for available settings. + +--- + +## 7. Try it: Using the nf-hello plugin + +The [nf-hello](https://github.com/nextflow-io/nf-hello) plugin provides a `randomString` function that generates random strings. +The following example demonstrates using it in a workflow. + +### 7.1. See the starting point + +First, look at what we're working with. +The `random_id_example.nf` file contains a workflow with an embedded `randomString` function: + +```bash +cat random_id_example.nf +``` + +Notice the function is defined locally in the file. +Run it to see how it works: + +```bash +nextflow run random_id_example.nf +``` + +This works, but the function is trapped in this file. +Now replace it with the plugin version. + +### 7.2. Configure the plugin + +Add the plugin to your `nextflow.config`: + +```groovy title="nextflow.config" +plugins { + id 'nf-hello@0.5.0' +} +``` + +### 7.3. Use the plugin function + +Update `random_id_example.nf` to use `randomString` from the plugin: + +```groovy title="random_id_example.nf" +#!/usr/bin/env nextflow + +include { randomString } from 'plugin/nf-hello' + +workflow { + // Generate random IDs for each sample + Channel.of('sample_A', 'sample_B', 'sample_C') + .map { sample -> "${sample}_${randomString(8)}" } + .view() +} +``` + +### 7.4. Run it + +```bash +nextflow run random_id_example.nf +``` + +```console title="Output" +Pipeline is starting! 🚀 +sample_A_xcwzhtbm +sample_B_yqurtfsq +sample_C_lpxepimu +Pipeline complete! 👋 +``` + +(Your random strings will be different!) + +The first run downloads the plugin automatically. +Any pipeline using `nf-hello@0.5.0` gets the exact same `randomString` function. + +Note that we're using a function someone else wrote. +The development burden is on the plugin developer, not the pipeline developer. +Nextflow also handles installing and updating plugins on your behalf. + +--- + +## Takeaway + +You learned that: + +- Plugins extend Nextflow through well-defined extension points: functions, observers, executors, and filesystems +- The Nextflow Plugin Registry is the central hub for discovering plugins +- Plugins are declared in `nextflow.config` with `plugins { id 'plugin-name@version' }` +- Import plugin functions with `include { function } from 'plugin/plugin-id'` + +--- + +## What's next? + +Now that you understand how to use plugins, the following sections show you how to build your own. +If you're not interested in plugin development, you can stop here or skip ahead to the [Summary](summary.md). + +[Continue to Part 2 :material-arrow-right:](02_create_project.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/02_create_project.md b/docs/hello_plugins/02_create_project.md new file mode 100644 index 0000000000..2f0d9d76ba --- /dev/null +++ b/docs/hello_plugins/02_create_project.md @@ -0,0 +1,224 @@ +# Part 2: Create a Plugin Project + +In this section, you'll scaffold a new plugin project and understand how the generated components work together. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 1 to use as your starting point: + + ```bash + cp -r solutions/1-plugin-basics/* . + ``` + +!!! info "Official documentation" + + This section and those that follow cover plugin development essentials. + For comprehensive details, see the [official Nextflow plugin development documentation](https://www.nextflow.io/docs/latest/plugins/developing-plugins.html). + +--- + +## 1. Using the plugin create command + +The easiest way to create a plugin is with the built-in command: + +```bash +nextflow plugin create nf-greeting training +``` + +This scaffolds a complete plugin project. +The first argument is the plugin name, and the second is your organization name (used for the package namespace). + +!!! tip "Manual creation" + + You can also create plugin projects manually or use the [nf-hello template](https://github.com/nextflow-io/nf-hello) on GitHub as a starting point. + +--- + +## 2. Understand the plugin architecture + +Before diving into the generated files, here's how the pieces fit together: + +```mermaid +graph TD + A[GreetingPlugin] -->|registers| B[GreetingExtension] + A -->|registers| C[GreetingFactory] + C -->|creates| D[GreetingObserver] + + B -->|provides| E["@Function methods
(callable from workflows)"] + D -->|hooks into| F["Lifecycle events
(onFlowCreate, onProcessComplete, etc.)"] + + style A fill:#e1f5fe + style B fill:#fff3e0 + style C fill:#fff3e0 + style D fill:#fff3e0 +``` + +| Class | Purpose | +| ------------------- | ---------------------------------------------------- | +| `GreetingPlugin` | Entry point that registers all extension points | +| `GreetingExtension` | Contains `@Function` methods callable from workflows | +| `GreetingFactory` | Creates trace observer instances | +| `GreetingObserver` | Hooks into workflow lifecycle events | + +This separation keeps concerns organized: functions go in the Extension, event handling goes in Observers created by the Factory. + +--- + +## 3. Examine the generated project + +Change into the plugin directory: + +```bash +cd nf-greeting +``` + +List the contents: + +```bash +tree +``` + +You should see: + +```console +. +├── build.gradle +├── COPYING +├── gradle +│ └── wrapper +│ ├── gradle-wrapper.jar +│ └── gradle-wrapper.properties +├── gradlew +├── Makefile +├── README.md +├── settings.gradle +└── src + ├── main + │ └── groovy + │ └── training + │ └── plugin + │ ├── GreetingExtension.groovy + │ ├── GreetingFactory.groovy + │ ├── GreetingObserver.groovy + │ └── GreetingPlugin.groovy + └── test + └── groovy + └── training + └── plugin + └── GreetingObserverTest.groovy + +11 directories, 13 files +``` + +--- + +## 4. Explore the key files + +With the project scaffolded, we need to understand how the pieces fit together. +The two most important files for project configuration are `settings.gradle` and `build.gradle`. + +### 4.1. settings.gradle + +This file identifies the project: + +```bash +cat settings.gradle +``` + +```groovy title="settings.gradle" +rootProject.name = 'nf-greeting' +``` + +The name here must match what you'll put in `nextflow.config` when using the plugin. + +### 4.2. build.gradle + +The build file is where most configuration happens: + +```bash +cat build.gradle +``` + +Key sections: + +```groovy title="build.gradle" +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '24.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + +} +``` + +The `nextflowPlugin` block configures: + +- `nextflowVersion`: Minimum Nextflow version required +- `provider`: Your name or organization +- `className`: The main plugin class (uses your package name) +- `extensionPoints`: Classes providing extensions (functions, observers, etc.) + +--- + +## 5. Explore the source files + +The actual plugin code lives in `src/main/groovy/training/plugin/`. +Each file has a specific role, corresponding to the architecture diagram from section 2. + +Open each file to see what the template generated: + +```bash +cat src/main/groovy/training/plugin/GreetingPlugin.groovy +``` + +This is the entry point. It extends `BasePlugin` and is the class referenced in `build.gradle`. +Nextflow loads this class first, which then registers the other components. + +```bash +cat src/main/groovy/training/plugin/GreetingExtension.groovy +``` + +The extension class holds functions marked with `@Function` that become callable from Nextflow workflows. +This is where you'll add most of your plugin's functionality. + +```bash +cat src/main/groovy/training/plugin/GreetingFactory.groovy +``` + +The factory creates trace observer instances when workflows start. +This indirection allows observers to be configured based on session settings. + +```bash +cat src/main/groovy/training/plugin/GreetingObserver.groovy +``` + +The observer hooks into workflow lifecycle events like start, task completion, and end. +The template includes messages that print "Pipeline is starting!" and "Pipeline complete!" + +--- + +## Takeaway + +You learned that: + +- The `nextflow plugin create` command scaffolds a complete project +- Plugins have four main components: Plugin (entry point), Extension (functions), Factory (creates observers), and Observer (lifecycle hooks) +- The `build.gradle` file configures plugin metadata, dependencies, and extension points + +--- + +## What's next? + +Now we'll implement custom functions in the Extension class. + +[Continue to Part 3 :material-arrow-right:](03_custom_functions.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/03_custom_functions.md b/docs/hello_plugins/03_custom_functions.md new file mode 100644 index 0000000000..f5b8ae361a --- /dev/null +++ b/docs/hello_plugins/03_custom_functions.md @@ -0,0 +1,311 @@ +# Part 3: Custom Functions + +In this section, you'll implement custom functions that can be called from Nextflow workflows. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 2 to use as your starting point: + + ```bash + cp -r solutions/2-create-project/* . + ``` + + Then change into the plugin directory: + + ```bash + cd nf-greeting + ``` + +--- + +## 1. The PluginExtensionPoint class + +Functions are defined in classes that extend `PluginExtensionPoint`. +Open the extension file: + +```bash +cat src/main/groovy/training/plugin/GreetingExtension.groovy +``` + +The template includes a sample `sayHello` function. +We'll replace it with our own functions. +The goal is to create a small library of string manipulation functions: one that reverses text, one that decorates text with markers, and one that formats a friendly greeting. + +??? info "Understanding the Groovy syntax" + + If the code looks unfamiliar, here's a breakdown of the key elements: + + **`package training.plugin`**: Declares which package (folder structure) this code belongs to. + This must match the directory structure. + + **`import ...`**: Brings in code from other packages, similar to Python's `import` or R's `library()`. + + **`@CompileStatic`**: An annotation (marked with `@`) that tells Groovy to check types at compile time. + This catches errors earlier. + + **`class GreetingExtension extends PluginExtensionPoint`**: Defines a class that inherits from `PluginExtensionPoint`. + The `extends` keyword means "this class is a type of that class." + + **`@Override`**: Indicates we're replacing a method from the parent class. + + **`@Function`**: The key annotation that makes a method available as a Nextflow function. + + **`String reverseGreeting(String greeting)`**: A method that takes a String parameter and returns a String. + In Groovy, you can often omit `return`; the last expression is returned automatically. + +--- + +## 2. Add the first function: reverseGreeting + +Start by replacing the template's `sayHello` function with something more interesting: a function that reverses a greeting string. +This demonstrates the basic pattern of defining a plugin function. + +Edit `src/main/groovy/training/plugin/GreetingExtension.groovy` to replace the `sayHello` method: + +=== "After" + + ```groovy title="GreetingExtension.groovy" linenums="24" hl_lines="8-14" + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + } + ``` + +=== "Before" + + ```groovy title="GreetingExtension.groovy" linenums="24" hl_lines="12-20" + /** + * Implements a custom function which can be imported by + * Nextflow scripts. + */ + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Say hello to the given target. + * + * @param target + */ + @Function + void sayHello(String target) { + println "Hello, ${target}!" + } + + } + ``` + +The key parts of this function: + +- **`@Function`**: This annotation makes the method callable from Nextflow workflows +- **`String reverseGreeting(String greeting)`**: Takes a String, returns a String +- **`greeting.reverse()`**: Groovy's built-in string reversal method + +--- + +## 3. Add the second function: decorateGreeting + +With the basic pattern established, add a second function. +This one wraps a greeting with decorative markers, demonstrating string interpolation. + +Add this method after `reverseGreeting`, before the closing brace of the class: + +=== "After" + + ```groovy title="GreetingExtension.groovy" linenums="24" hl_lines="16-22" + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + } + ``` + +=== "Before" + + ```groovy title="GreetingExtension.groovy" linenums="24" + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + } + ``` + +This function uses Groovy string interpolation (`"*** ${greeting} ***"`) to embed the greeting variable inside a string. + +--- + +## 4. Add the third function: friendlyGreeting + +The final function demonstrates default parameter values, a feature that makes functions more flexible without requiring callers to provide every argument. + +Add this method after `decorateGreeting`: + +=== "After" + + ```groovy title="GreetingExtension.groovy" linenums="24" hl_lines="24-30" + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + /** + * Convert greeting to a friendly format with a name + */ + @Function + String friendlyGreeting(String greeting, String name = 'World') { + return "${greeting}, ${name}!" + } + + } + ``` + +=== "Before" + + ```groovy title="GreetingExtension.groovy" linenums="24" + @CompileStatic + class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + } + ``` + +The `String name = 'World'` syntax provides a default value, just like in Python. +Users can call `friendlyGreeting('Hello')` or `friendlyGreeting('Hello', 'Alice')`. + +--- + +## 5. Understanding the @Function annotation + +All three functions share a common pattern: the `@Function` annotation. +This annotation is what makes a method callable from Nextflow workflows. + +Key requirements: + +- **Methods must be public**: In Groovy, methods are public by default +- **Return type**: Can be any serializable type (`String`, `List`, `Map`, etc.) +- **Parameters**: Can have any number of parameters, including default values + +Once defined, functions are available via the `include` statement: + +```groovy +include { reverseGreeting; decorateGreeting } from 'plugin/nf-greeting' +``` + +--- + +## 6. The init() method + +You may have noticed the `init()` method in the extension class. +This method is called when the plugin loads: + +```groovy +@Override +void init(Session session) { + // Access session configuration + // Initialize resources + // Set up state +} +``` + +You can access configuration via `session.config`. +We'll use this in Part 6 to make our plugin configurable. + +--- + +## Takeaway + +You learned that: + +- Functions are defined with the `@Function` annotation in `PluginExtensionPoint` subclasses +- Methods can have any return type and accept parameters with default values +- Once defined, functions become available to import in Nextflow workflows + +--- + +## What's next? + +Now we build and test our plugin. + +[Continue to Part 4 :material-arrow-right:](04_build_and_test.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/04_build_and_test.md b/docs/hello_plugins/04_build_and_test.md new file mode 100644 index 0000000000..9315090eb2 --- /dev/null +++ b/docs/hello_plugins/04_build_and_test.md @@ -0,0 +1,530 @@ +# Part 4: Build and Test + +In this section, you'll learn the plugin development cycle: building, testing, installing locally, and using your plugin in a workflow. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 3 to use as your starting point: + + ```bash + cp -r solutions/3-custom-functions/* . + ``` + + Then change into the plugin directory: + + ```bash + cd nf-greeting + ``` + +??? info "Why do we need to build?" + + If you're used to scripting languages like Python, R, or even Nextflow's DSL, you might wonder why we need a "build" step at all. + In those languages, you write code and run it directly. + + Nextflow plugins are written in Groovy, which runs on the Java Virtual Machine (JVM). + JVM languages need to be **compiled** before they can run. + The human-readable source code is converted into bytecode that the JVM can execute. + + The build process: + + 1. **Compiles** your Groovy code into JVM bytecode + 2. **Packages** it into a JAR file (Java ARchive, like a ZIP of compiled code) + 3. **Bundles** metadata so Nextflow knows how to load the plugin + + The build tools handle all this automatically. + Run `make assemble` and let Gradle do the work. + +--- + +## 1. The development cycle + +The plugin development cycle follows a simple pattern: + +```mermaid +graph LR + A[Write/Edit Code] --> B[make assemble] + B --> C[make test] + C --> D{Tests pass?} + D -->|No| A + D -->|Yes| E[make install] + E --> F[Test in pipeline] + F --> G{Works?} + G -->|No| A + G -->|Yes| H[Done!] +``` + +--- + +## 2. Build the plugin + +The Makefile provides convenient commands: + +```bash +make assemble +``` + +Or directly with the Gradle wrapper: + +```bash +./gradlew assemble +``` + +??? info "What is `./gradlew`?" + + The `./gradlew` script is the **Gradle wrapper**, a small script included with the project that automatically downloads and runs the correct version of Gradle. + + This means you don't need Gradle installed on your system. + The first time you run `./gradlew`, it will download Gradle (which may take a moment), then run your command. + + The `make` commands in the Makefile are just shortcuts that call `./gradlew` for you. + +??? example "Build output" + + The first time you run this, Gradle will download itself (this may take a minute): + + ```console + Downloading https://services.gradle.org/distributions/gradle-8.14-bin.zip + ...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% + + Welcome to Gradle 8.14! + ... + + Deprecated Gradle features were used in this build... + + BUILD SUCCESSFUL in 23s + 4 actionable tasks: 4 executed + ``` + + **The warnings are expected.** + + - **"Downloading gradle..."**: This only happens the first time. Subsequent builds are much faster. + - **"Deprecated Gradle features..."**: This warning comes from the plugin template, not your code. It's safe to ignore. + - **"BUILD SUCCESSFUL"**: This is what matters. Your plugin compiled without errors. + +--- + +## 3. Write unit tests + +A successful build means the code compiles, but not that it works correctly. +Tests verify your functions behave as expected and help catch bugs when you make changes later. + +??? info "What are unit tests?" + + **Unit tests** are small pieces of code that automatically check if your functions work correctly. + Each test calls a function with known inputs and checks that the output matches what you expect. + + For example, if you have a function that reverses strings, a test might check that `reverse("Hello")` returns `"olleH"`. + + Tests are valuable because: + + - They catch bugs before users do + - They give you confidence to make changes without breaking things + - They serve as documentation showing how functions should be used + + You don't need to write tests to use a plugin, but they're good practice for any code you plan to share or maintain. + +The generated project includes a test for the Observer class, but we need to create a new test file for our extension functions. + +### 3.1. Understanding Spock tests + +The plugin template uses [Spock](https://spockframework.org/), a testing framework for Groovy that reads almost like plain English. +Here's the basic structure: + +```groovy +def 'should reverse a greeting'() { // (1)! + given: // (2)! + def ext = new GreetingExtension() + + expect: // (3)! + ext.reverseGreeting('Hello') == 'olleH' +} +``` + +1. **Test name in quotes**: Describes what the test checks. Use plain English. +2. **`given:` block**: Set up what you need for the test (create objects, prepare data) +3. **`expect:` block**: The actual checks. Each line should be `true` for the test to pass + +This structure makes tests readable: "Given an extension object, expect that `reverseGreeting('Hello')` equals `'olleH'`." + +### 3.2. Create the test file + +```bash +touch src/test/groovy/training/plugin/GreetingExtensionTest.groovy +``` + +Open it in your editor and add the following content: + +```groovy title="src/test/groovy/training/plugin/GreetingExtensionTest.groovy" linenums="1" +package training.plugin + +import spock.lang.Specification + +/** + * Tests for the greeting extension functions + */ +class GreetingExtensionTest extends Specification { + + def 'should reverse a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.reverseGreeting('Hello') == 'olleH' + ext.reverseGreeting('Bonjour') == 'ruojnoB' + } + + def 'should decorate a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.decorateGreeting('Hello') == '*** Hello ***' + } + + def 'should create friendly greeting with default name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello') == 'Hello, World!' + } + + def 'should create friendly greeting with custom name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello', 'Alice') == 'Hello, Alice!' + } +} +``` + +--- + +## 4. Run the tests + +```bash +make test +``` + +Or: + +```bash +./gradlew test +``` + +??? example "Test output" + + ```console + BUILD SUCCESSFUL in 5s + 6 actionable tasks: 2 executed, 4 up-to-date + ``` + + **Where are the test results?** Gradle hides detailed output when all tests pass. + "BUILD SUCCESSFUL" means everything worked. + If any test fails, you'll see detailed error messages. + +--- + +## 5. View the test report + +To see detailed results for each test, you can view the HTML test report that Gradle generates. + +Start a simple web server in the test report directory: + +```bash +pushd build/reports/tests/test +python -m http.server +``` + +VS Code will prompt you to open the application in your browser. +Click through to your test class to see individual test results: + +![Test report showing all tests passed](./img/test_report.png) + +The report shows each test method, its duration, and whether it passed or failed. +This confirms that all four of our greeting functions are being tested correctly. + +Press ++ctrl+c++ in the terminal to stop the server when you're done, then return to the plugin directory: + +```bash +popd +``` + +!!! tip "If the build fails" + + Build errors can be intimidating, but they usually point to a specific problem. + Common issues include: + + - **Syntax errors**: A missing bracket, quote, or semicolon. The error message usually includes a line number. + - **Import errors**: A class name is misspelled or the import statement is missing. + - **Type errors**: You're passing the wrong type of data to a function. + - **"cannot find symbol"**: You're using a variable that wasn't declared. Check that you've added the instance variable (e.g., `private String prefix`) before using it. + + Read the error message carefully. + It often tells you exactly what's wrong and where. + If you're stuck, compare your code character-by-character with the examples. + +??? warning "Common runtime issues" + + Even if the build succeeds, you might encounter issues when running: + + - **"Plugin not found"**: Did you run `make install`? The plugin must be installed locally before Nextflow can use it. + - **"Unknown function"**: Check that you've imported the function with `include { functionName } from 'plugin/nf-greeting'`. + - **Wrong directory**: Make sure you're in the right directory. Use `pwd` to check, and `cd ..` or `cd nf-greeting` as needed. + - **IDE showing errors**: The VS Code Nextflow extension may show warnings for plugin imports. If the build succeeds and Nextflow runs correctly, you can ignore these. + +--- + +## 6. Install locally + +Tests pass, so the plugin is ready to use. +To make it available to Nextflow, install it to your local plugins directory: + +```bash +make install +``` + +??? example "Expected output" + + ```console + > Task :installPlugin + Plugin nf-greeting installed successfully! + Installation location: /home/codespace/.nextflow/plugins + Installation location determined by - Default location (~/.nextflow/plugins) + + BUILD SUCCESSFUL in 1s + ``` + + The exact path will vary depending on your environment, but you should see "Plugin nf-greeting installed successfully!" and "BUILD SUCCESSFUL". + +This copies the plugin to `$NXF_HOME/plugins/` (typically `~/.nextflow/plugins/`). + +--- + +## 7. Use your plugin in a workflow + +With the plugin installed locally, you can use it in a Nextflow pipeline. + +### 7.1. Configure the plugin + +Go back to the pipeline directory: + +```bash +cd .. +``` + +Edit `nextflow.config` to replace the `nf-hello` plugin with our new `nf-greeting` plugin: + +=== "After" + + ```groovy title="nextflow.config" hl_lines="3" + // Configuration for plugin development exercises + plugins { + id 'nf-greeting@0.1.0' + } + ``` + +=== "Before" + + ```groovy title="nextflow.config" hl_lines="3" + // Configuration for plugin development exercises + plugins { + id 'nf-hello@0.5.0' + } + ``` + +We're replacing `nf-hello` with `nf-greeting` because we want to use our own plugin's functions instead. + +!!! note "What about random_id_example.nf?" + + The `random_id_example.nf` file we modified earlier still imports from `nf-hello`, so it won't work with this config change. + That's fine. We won't use it again. + We'll work with `main.nf` from here on. + +!!! note "Version required for local plugins" + + When using locally installed plugins, you must specify the version (e.g., `nf-greeting@0.1.0`). + Published plugins in the registry can use just the name. + +### 7.2. Import and use functions + +We provided a simple greeting pipeline in `main.nf` that reads greetings from a CSV file and writes them to output files. + +#### See the starting point + +First, run the pipeline as-is to see what we're working with: + +```bash +nextflow run main.nf +``` + +```console title="Output" +Output: Hello +Output: Bonjour +Output: Holà +Output: Ciao +Output: Hallo +``` + +Look at the code: + +```bash +cat main.nf +``` + +```groovy title="main.nf (starting point)" +#!/usr/bin/env nextflow + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + """ + echo '$greeting' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Output: ${result.trim()}" } +} +``` + +#### Update the workflow + +Enhance it to use our plugin functions. +Edit `main.nf` to import and use the custom functions: + +=== "After" + + ```groovy title="main.nf" hl_lines="4-5 15-18 28-30 33" linenums="1" + #!/usr/bin/env nextflow + + // Import custom functions from our plugin + include { reverseGreeting } from 'plugin/nf-greeting' + include { decorateGreeting } from 'plugin/nf-greeting' + + params.input = 'greetings.csv' + + process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ + } + + workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated: ${result.trim()}" } + } + ``` + +=== "Before" + + ```groovy title="main.nf" linenums="1" hl_lines="11 12 21" + #!/usr/bin/env nextflow + + params.input = 'greetings.csv' + + process SAY_HELLO { + input: + val greeting + output: + stdout + script: + """ + echo '$greeting' + """ + } + + workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Output: ${result.trim()}" } + } + ``` + +The key changes: + +- **Lines 4-5**: Import our plugin functions using `include { function } from 'plugin/plugin-name'` +- **Lines 17-18**: Use `decorateGreeting()` **inside the process script** to transform the greeting before output +- **Lines 28-30**: Use `reverseGreeting()` **in a `map` operation** to transform channel items in the workflow + +Plugin functions work in both process scripts and workflow operations. + +### 7.3. Run the pipeline + +```bash +nextflow run main.nf +``` + +??? example "Output" + + ```console + N E X T F L O W ~ version 25.10.2 + + Launching `main.nf` [elated_marconi] DSL2 - revision: cd8d52c97c + + Pipeline is starting! 🚀 + executor > local (5) + [fe/109754] process > SAY_HELLO (5) [100%] 5 of 5 ✔ + Reversed: olleH + Reversed: ruojnoB + Reversed: àloH + Reversed: oaiC + Reversed: ollaH + Decorated: *** Hello *** + Decorated: *** Bonjour *** + Decorated: *** Holà *** + Decorated: *** Ciao *** + Decorated: *** Hallo *** + Pipeline complete! 👋 + ``` + + The "Pipeline is starting!" and "Pipeline complete!" messages come from the `GreetingObserver` trace observer that was included in the generated plugin template. + +The `decorateGreeting()` function wraps each greeting with decorative markers, and `reverseGreeting()` shows the reversed strings. + +--- + +## Takeaway + +You learned that: + +- Use `make assemble` to compile and `make test` to run tests +- Install with `make install` to use the plugin locally +- Import plugin functions with `include { function } from 'plugin/plugin-id'` +- Plugin functions work in both process scripts and workflow operations + +--- + +## What's next? + +The next section explores trace observers for hooking into workflow lifecycle events. + +[Continue to Part 5 :material-arrow-right:](05_observers.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/05_observers.md b/docs/hello_plugins/05_observers.md new file mode 100644 index 0000000000..393074a02e --- /dev/null +++ b/docs/hello_plugins/05_observers.md @@ -0,0 +1,256 @@ +# Part 5: Trace Observers + +In Part 1, we saw that plugins can provide many types of extensions. +So far we've implemented custom functions. +This part explores **trace observers**, which let you hook into workflow lifecycle events. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 4 to use as your starting point: + + ```bash + cp -r solutions/4-build-and-test/* . + ``` + +--- + +## 1. Understanding the existing trace observer + +Remember the "Pipeline is starting!" message when you ran the pipeline? +That came from the `GreetingObserver` class in your plugin. + +Look at the observer code: + +```bash +cat nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy +``` + +This observer hooks into workflow lifecycle events. +Trace observers can respond to many events: + +| Method | When it's called | +| ------------------- | ----------------------- | +| `onFlowCreate` | Workflow starts | +| `onFlowComplete` | Workflow finishes | +| `onProcessStart` | A task begins execution | +| `onProcessComplete` | A task finishes | +| `onProcessCached` | A cached task is reused | +| `onFilePublish` | A file is published | + +This enables powerful use cases like custom reports, Slack notifications, or metrics collection. + +--- + +## 2. Try it: Add a task counter observer + +Rather than modifying the existing observer, create a new one that counts completed tasks. +We'll build it up progressively: first a minimal version, then add features. + +### 2.1. Create a minimal observer + +Create a new file: + +```bash +touch nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy +``` + +Start with the simplest possible observer. +Just print a message when any task completes: + +```groovy title="nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy" linenums="1" +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.processor.TaskHandler +import nextflow.trace.TraceObserver +import nextflow.trace.TraceRecord + +/** + * Observer that responds to task completion + */ +@CompileStatic +class TaskCounterObserver implements TraceObserver { + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + println "✓ Task completed!" + } +} +``` + +This is the minimum needed: + +- Import the required classes (`TraceObserver`, `TaskHandler`, `TraceRecord`) +- Create a class that `implements TraceObserver` +- Override `onProcessComplete` to do something when a task finishes + +### 2.2. Register the observer + +The `GreetingFactory` creates observers. +Take a look at it: + +```bash +cat nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy +``` + +```groovy title="GreetingFactory.groovy (starting point)" +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + return List.of(new GreetingObserver()) + } +} +``` + +Edit `GreetingFactory.groovy` to add our new observer: + +=== "After" + + ```groovy title="GreetingFactory.groovy" linenums="31" hl_lines="3-6" + @Override + Collection create(Session session) { + return [ + new GreetingObserver(), + new TaskCounterObserver() + ] + } + ``` + +=== "Before" + + ```groovy title="GreetingFactory.groovy" linenums="31" hl_lines="3" + @Override + Collection create(Session session) { + return List.of(new GreetingObserver()) + } + ``` + +!!! note "Groovy list syntax" + + We've replaced the Java-style `List.of(...)` with Groovy's simpler list literal `[...]`. + Both return a `Collection`, but the Groovy syntax is more readable when adding multiple items. + +### 2.3. Build, install, and test + +```bash +cd nf-greeting && make assemble && make install && cd .. +nextflow run main.nf -ansi-log false +``` + +You should see "✓ Task completed!" printed five times (once per task): + +```console title="Expected output (partial)" +... +[be/bd8e72] Submitted process > SAY_HELLO (2) +✓ Task completed! +[5b/d24c2b] Submitted process > SAY_HELLO (1) +✓ Task completed! +... +``` + +Our observer is responding to task completion events. +The next step makes it more useful. + +### 2.4. Add counting logic + +Update `TaskCounterObserver.groovy` to track a count and report a summary: + +```groovy title="nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy" linenums="1" hl_lines="14 18-19 22-24" +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.processor.TaskHandler +import nextflow.trace.TraceObserver +import nextflow.trace.TraceRecord + +/** + * Observer that counts completed tasks + */ +@CompileStatic +class TaskCounterObserver implements TraceObserver { + + private int taskCount = 0 + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + taskCount++ + println "📊 Tasks completed so far: ${taskCount}" + } + + @Override + void onFlowComplete() { + println "📈 Final task count: ${taskCount}" + } +} +``` + +The key additions: + +- **Line 14**: A private instance variable `taskCount` persists across method calls +- **Lines 18-19**: Increment the counter and print the running total +- **Lines 22-24**: `onFlowComplete` is called once when the workflow finishes, perfect for a summary + +Rebuild and test: + +```bash +cd nf-greeting && make assemble && make install && cd .. +nextflow run main.nf -ansi-log false +``` + +```console title="Expected output" +N E X T F L O W ~ version 25.10.2 +Launching `main.nf` [pensive_engelbart] DSL2 - revision: 85fefd90d0 +Pipeline is starting! 🚀 +Reversed: olleH +Reversed: ruojnoB +Reversed: àloH +Reversed: oaiC +Reversed: ollaH +[be/bd8e72] Submitted process > SAY_HELLO (2) +[5b/d24c2b] Submitted process > SAY_HELLO (1) +[14/1f9dbe] Submitted process > SAY_HELLO (3) +Decorated: *** Bonjour *** +Decorated: *** Hello *** +[85/a6b3ad] Submitted process > SAY_HELLO (4) +📊 Tasks completed so far: 1 +📊 Tasks completed so far: 2 +Decorated: *** Holà *** +📊 Tasks completed so far: 3 +Decorated: *** Ciao *** +[3c/be6686] Submitted process > SAY_HELLO (5) +📊 Tasks completed so far: 4 +Decorated: *** Hallo *** +📊 Tasks completed so far: 5 +Pipeline complete! 👋 +📈 Final task count: 5 +``` + +!!! tip "Why `-ansi-log false`?" + + By default, Nextflow's ANSI progress display overwrites previous lines to show a clean, updating view of progress. + This means you'd only see the *final* task count, not the intermediate "Tasks completed so far" messages. + They'd be overwritten as new output arrives. + + Using `-ansi-log false` disables this behavior and shows all output sequentially, which is essential when testing observers that print messages during execution. + Without this flag, you might think your observer isn't working when it actually is. + The output is just being overwritten. + +--- + +## Takeaway + +You learned that: + +- Trace observers hook into workflow lifecycle events like `onFlowCreate`, `onProcessComplete`, and `onFlowComplete` +- Create observers by implementing `TraceObserver` and registering them in a Factory +- Observers are useful for custom logging, metrics collection, notifications, and reporting + +--- + +## What's next? + +The next section shows how plugins can read configuration from `nextflow.config`. + +[Continue to Part 6 :material-arrow-right:](06_configuration.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/06_configuration.md b/docs/hello_plugins/06_configuration.md new file mode 100644 index 0000000000..9295059bf5 --- /dev/null +++ b/docs/hello_plugins/06_configuration.md @@ -0,0 +1,558 @@ +# Part 6: Configuration + +In this section, you'll make your plugin configurable by reading settings from `nextflow.config`. +Users will be able to customize plugin behavior without modifying code. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 5 to use as your starting point: + + ```bash + cp -r solutions/5-observers/* . + ``` + +Nextflow provides two approaches for plugin configuration: + +| Approach | Best for | Trade-offs | +| --------------------------- | ---------------------------------- | --------------------------------------- | +| `session.config.navigate()` | Quick prototyping, simple plugins | No IDE support, manual type conversion | +| `@ConfigScope` classes | Production plugins, complex config | More code, but type-safe and documented | + +We'll start with the simple approach, then upgrade to the formal approach. + +--- + +## 1. Simple configuration with navigate() + +The `session.config.navigate()` method reads nested configuration values: + +```groovy +// Read 'greeting.enabled' from nextflow.config, defaulting to true +final enabled = session.config.navigate('greeting.enabled', true) +``` + +This lets users control plugin behavior: + +```groovy title="nextflow.config" +greeting { + enabled = false +} +``` + +This approach works well for quick prototyping and simple plugins. + +--- + +## 2. Try it: Make the task counter configurable + +This exercise adds configuration options to: + +1. Enable/disable the entire greeting plugin +2. Control whether per-task counter messages are shown + +### 2.1. Update TaskCounterObserver + +First, edit `TaskCounterObserver.groovy` to accept a configuration flag: + +=== "After" + + ```groovy title="TaskCounterObserver.groovy" linenums="1" hl_lines="14 17-19 24-26" + package training.plugin + + import groovy.transform.CompileStatic + import nextflow.processor.TaskHandler + import nextflow.trace.TraceObserver + import nextflow.trace.TraceRecord + + /** + * Observer that counts completed tasks + */ + @CompileStatic + class TaskCounterObserver implements TraceObserver { + + private final boolean verbose + private int taskCount = 0 + + TaskCounterObserver(boolean verbose) { + this.verbose = verbose + } + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + taskCount++ + if (verbose) { + println "📊 Tasks completed so far: ${taskCount}" + } + } + + @Override + void onFlowComplete() { + println "📈 Final task count: ${taskCount}" + } + } + ``` + +=== "Before" + + ```groovy title="TaskCounterObserver.groovy" linenums="1" hl_lines="19" + package training.plugin + + import groovy.transform.CompileStatic + import nextflow.processor.TaskHandler + import nextflow.trace.TraceObserver + import nextflow.trace.TraceRecord + + /** + * Observer that counts completed tasks + */ + @CompileStatic + class TaskCounterObserver implements TraceObserver { + + private int taskCount = 0 + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + taskCount++ + println "📊 Tasks completed so far: ${taskCount}" + } + + @Override + void onFlowComplete() { + println "📈 Final task count: ${taskCount}" + } + } + ``` + +The key changes: + +- **Line 14**: Add a `verbose` flag to control whether per-task messages are printed +- **Lines 17-19**: Constructor that accepts the verbose setting +- **Lines 24-26**: Only print per-task messages if `verbose` is true + +### 2.2. Update the Factory + +Now update `GreetingFactory.groovy` to read the configuration and pass it to the observer: + +=== "After" + + ```groovy title="GreetingFactory.groovy" linenums="31" hl_lines="3-6 9" + @Override + Collection create(Session session) { + final enabled = session.config.navigate('greeting.enabled', true) + if (!enabled) return [] + + final verbose = session.config.navigate('greeting.taskCounter.verbose', true) as boolean + return [ + new GreetingObserver(), + new TaskCounterObserver(verbose) + ] + } + ``` + +=== "Before" + + ```groovy title="GreetingFactory.groovy" linenums="31" + @Override + Collection create(Session session) { + return [ + new GreetingObserver(), + new TaskCounterObserver() + ] + } + ``` + +The factory now: + +- **Lines 33-34**: Reads the `greeting.enabled` config and returns early if disabled +- **Line 36**: Reads the `greeting.taskCounter.verbose` config (defaulting to `true`) +- **Line 39**: Passes the verbose setting to the `TaskCounterObserver` constructor + +### 2.3. Build and test + +Rebuild and reinstall the plugin: + +```bash +cd nf-greeting && make assemble && make install && cd .. +``` + +Now update `nextflow.config` to disable the per-task messages: + +=== "After" + + ```groovy title="nextflow.config" linenums="1" hl_lines="5-7" + plugins { + id 'nf-greeting@0.1.0' + } + + greeting { + // enabled = false // Disable plugin entirely + taskCounter.verbose = false // Disable per-task messages + } + ``` + +=== "Before" + + ```groovy title="nextflow.config" linenums="1" + plugins { + id 'nf-greeting@0.1.0' + } + ``` + +Run the pipeline and observe that only the final count appears: + +```bash +nextflow run main.nf -ansi-log false +``` + +```console title="Expected output" +N E X T F L O W ~ version 25.10.2 +Launching `main.nf` [stoic_wegener] DSL2 - revision: 63f3119fbc +Pipeline is starting! 🚀 +Reversed: olleH +Reversed: ruojnoB +Reversed: àloH +Reversed: oaiC +Reversed: ollaH +[5e/9c1f21] Submitted process > SAY_HELLO (2) +[20/8f6f91] Submitted process > SAY_HELLO (1) +[6d/496bae] Submitted process > SAY_HELLO (4) +[5c/a7fe10] Submitted process > SAY_HELLO (3) +[48/18199f] Submitted process > SAY_HELLO (5) +Decorated: *** Hello *** +Decorated: *** Bonjour *** +Decorated: *** Holà *** +Decorated: *** Ciao *** +Decorated: *** Hallo *** +Pipeline complete! 👋 +📈 Final task count: 5 +``` + +--- + +## 3. Try it: Make the decorator configurable + +This exercise makes the `decorateGreeting` function use configurable prefix/suffix. +We'll intentionally make a common mistake to understand how Groovy/Java handles variables. + +### 3.1. Add the configuration reading (this will fail!) + +Edit `GreetingExtension.groovy` to read configuration in `init()` and use it in `decorateGreeting()`: + +```groovy title="GreetingExtension.groovy" linenums="35" hl_lines="7-8 18" +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + // Read configuration with defaults + prefix = session.config.navigate('greeting.prefix', '***') as String + suffix = session.config.navigate('greeting.suffix', '***') as String + } + + // ... other methods unchanged ... + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "${prefix} ${greeting} ${suffix}" + } +``` + +Now try to build: + +```bash +cd nf-greeting && make assemble +``` + +### 3.2. Observe the error + +The build fails with an error like: + +```console +> Task :compileGroovy FAILED +GreetingExtension.groovy: 30: [Static type checking] - The variable [prefix] is undeclared. + @ line 30, column 9. + prefix = session.config.navigate('greeting.prefix', '***') as String + ^ + +GreetingExtension.groovy: 31: [Static type checking] - The variable [suffix] is undeclared. +``` + +**What went wrong?** In Groovy (and Java), you can't just use a variable. +You must _declare_ it first. +We're trying to assign values to `prefix` and `suffix`, but we never told the class that these variables exist. + +### 3.3. Fix by declaring instance variables + +Add the variable declarations at the top of the class, right after the opening brace: + +```groovy title="GreetingExtension.groovy" linenums="35" hl_lines="4-5" +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + private String prefix = '***' + private String suffix = '***' + + @Override + protected void init(Session session) { + // Read configuration with defaults + prefix = session.config.navigate('greeting.prefix', '***') as String + suffix = session.config.navigate('greeting.suffix', '***') as String + } + + // ... rest of class unchanged ... +``` + +The `private String prefix = '***'` line does two things: + +1. **Declares** a variable named `prefix` that can hold a String +2. **Initializes** it with a default value of `'***'` + +Now the `init()` method can assign new values to these variables, and `decorateGreeting()` can read them. + +### 3.4. Build again + +```bash +make assemble +``` + +This time it should succeed with "BUILD SUCCESSFUL". + +```bash +make install && cd .. +``` + +!!! tip "Learning from errors" + + This "declare before use" pattern is fundamental to Java/Groovy but unfamiliar if you come from Python or R where variables spring into existence when you first assign them. + Experiencing this error once helps you recognize and fix it quickly in the future. + +### 3.5. Test the configurable decorator + +Update `nextflow.config` to customize the decoration: + +=== "After" + + ```groovy title="nextflow.config" hl_lines="7-8" + plugins { + id 'nf-greeting@0.1.0' + } + + greeting { + taskCounter.verbose = false + prefix = '>>>' + suffix = '<<<' + } + ``` + +=== "Before" + + ```groovy title="nextflow.config" + plugins { + id 'nf-greeting@0.1.0' + } + + greeting { + taskCounter.verbose = false + } + ``` + +Run the pipeline: + +```bash +nextflow run main.nf -ansi-log false +``` + +```console title="Expected output (partial)" +Decorated: >>> Hello <<< +Decorated: >>> Bonjour <<< +... +``` + +The decorator now uses our custom prefix and suffix. + +--- + +## 4. Formal configuration with ConfigScope + +The `session.config.navigate()` approach works, but has limitations: + +- No IDE autocompletion for users writing `nextflow.config` +- Configuration options aren't self-documenting +- Manual type conversion with `as String`, `as boolean`, etc. + +For production plugins, Nextflow provides a formal configuration system using annotations. +This creates a schema that documents available options. + +### 4.1. Understanding the annotations + +| Annotation | Purpose | +| ----------------------- | ----------------------------------------------------- | +| `@ScopeName('name')` | Declares a configuration block (e.g., `greeting { }`) | +| `@ConfigOption` | Marks a field as a configuration option | +| `ConfigScope` interface | Must be implemented by config classes | + +### 4.2. Create a configuration class + +Create a new file `GreetingConfig.groovy`: + +```bash +touch nf-greeting/src/main/groovy/training/plugin/GreetingConfig.groovy +``` + +Add the configuration class: + +```groovy title="GreetingConfig.groovy" linenums="1" +package training.plugin + +import nextflow.config.spec.ConfigOption +import nextflow.config.spec.ConfigScope +import nextflow.config.spec.ScopeName + +/** + * Configuration options for the nf-greeting plugin. + * + * Users configure these in nextflow.config: + * + * greeting { + * enabled = true + * prefix = '>>>' + * suffix = '<<<' + * taskCounter.verbose = false + * } + */ +@ScopeName('greeting') +class GreetingConfig implements ConfigScope { + + GreetingConfig() {} + + GreetingConfig(Map opts) { + this.enabled = opts.enabled as Boolean ?: true + this.prefix = opts.prefix as String ?: '***' + this.suffix = opts.suffix as String ?: '***' + if (opts.taskCounter instanceof Map) { + this.taskCounter = new TaskCounterConfig(opts.taskCounter as Map) + } + } + + /** + * Enable or disable the plugin entirely. + */ + @ConfigOption + boolean enabled = true + + /** + * Prefix for decorated greetings. + */ + @ConfigOption + String prefix = '***' + + /** + * Suffix for decorated greetings. + */ + @ConfigOption + String suffix = '***' + + /** + * Task counter configuration + */ + TaskCounterConfig taskCounter = new TaskCounterConfig() + + static class TaskCounterConfig implements ConfigScope { + TaskCounterConfig() {} + TaskCounterConfig(Map opts) { + this.verbose = opts.verbose as Boolean ?: true + } + + @ConfigOption + boolean verbose = true + } +} +``` + +Key points: + +- **`@ScopeName('greeting')`**: Maps to the `greeting { }` block in config +- **`implements ConfigScope`**: Required interface for config classes +- **`@ConfigOption`**: Each field becomes a configuration option +- **Nested class**: For nested paths like `taskCounter.verbose`, use a nested class +- **Constructors**: Both no-arg and Map constructors are needed +- **Default values**: Set directly on the fields + +### 4.3. Register the config class + +Update `build.gradle` to register the config class as an extension point: + +=== "After" + + ```groovy title="build.gradle" hl_lines="4" + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory', + 'training.plugin.GreetingConfig' + ] + ``` + +=== "Before" + + ```groovy title="build.gradle" + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + ``` + +### 4.4. Build and test + +The config class provides documentation and schema validation. +Your code continues using `session.config.navigate()` for reading values: + +```bash +cd nf-greeting && make assemble && make install && cd .. +nextflow run main.nf -ansi-log false +``` + +The behavior is identical, but now your configuration is: + +- **Self-documenting**: The config class shows all available options +- **Structured**: Nested configuration is explicit + +!!! note "Config class vs runtime access" + + The config class primarily serves as documentation and schema definition. + Runtime value access still uses `session.config.navigate()`. + This is the pattern used by most Nextflow plugins including nf-validation. + +--- + +## 5. Summary + +| Use case | Recommended approach | +| ----------------------------------- | ----------------------------------------- | +| Quick prototype or simple plugin | `session.config.navigate()` only | +| Production plugin with many options | Add `ConfigScope` class for documentation | +| Plugin you'll share publicly | Add `ConfigScope` class for documentation | + +For this training, the `navigate()` approach is sufficient. +Adding a config class helps users understand available options. + +--- + +## Takeaway + +You learned that: + +- `session.config.navigate()` provides simple, quick configuration reading +- `@ScopeName` and `@ConfigOption` annotations create self-documenting configuration +- Configuration can be applied to both observers and extension functions +- Variables must be declared before use in Groovy/Java + +--- + +## What's next? + +The next section covers how to share your plugin with others. + +[Continue to Part 7 :material-arrow-right:](07_distribution.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/07_distribution.md b/docs/hello_plugins/07_distribution.md new file mode 100644 index 0000000000..7c1c57bcb8 --- /dev/null +++ b/docs/hello_plugins/07_distribution.md @@ -0,0 +1,253 @@ +# Part 7: Distribution + +Once your plugin is working locally, you have several options for sharing it with others. + +!!! tip "Starting from here?" + + If you're joining at this part, copy the solution from Part 6 to use as your starting point: + + ```bash + cp -r solutions/6-configuration/* . + ``` + +--- + +## 1. Distribution options + +| Distribution method | Use case | Approval required | +| -------------------- | -------------------------------------------------- | -------------------------- | +| **Public registry** | Open source plugins for the community | Yes (name must be claimed) | +| **Internal hosting** | Private/proprietary plugins within an organization | No | + +--- + +## 2. Publishing to the public registry + +The [Nextflow plugin registry](https://registry.nextflow.io/) is the official way to share plugins with the community. + +!!! tip "Plugin registry" + + The Nextflow plugin registry is currently in public preview. + See the [Nextflow documentation](https://www.nextflow.io/docs/latest/guides/gradle-plugin.html#publishing-a-plugin) for the latest details. + +### 2.1. Claim your plugin name + +Before publishing, claim your plugin name in the registry: + +1. Go to the [Nextflow plugin registry](https://registry.nextflow.io/) +2. Sign in with your GitHub account +3. Claim your plugin name (e.g., `nf-greeting`) + +You can claim a name before the plugin exists to reserve it. + +### 2.2. Configure API credentials + +Create a Gradle properties file to store your registry credentials: + +```bash +touch ~/.gradle/gradle.properties +``` + +Add your API key (obtain this from the registry after signing in): + +```properties title="~/.gradle/gradle.properties" +npr.apiKey=YOUR_API_KEY_HERE +``` + +!!! warning "Keep your API key secret" + + Don't commit this file to version control. + The `~/.gradle/` directory is outside your project, so it won't be included in your repository. + +### 2.3. Prepare for release + +Before publishing, ensure your plugin is ready: + +1. **Update the version** in `build.gradle` (use [semantic versioning](https://semver.org/)) +2. **Run tests** to ensure everything works: `make test` +3. **Update documentation** in your README + +```groovy title="build.gradle" +version = '1.0.0' // Use semantic versioning: MAJOR.MINOR.PATCH +``` + +### 2.4. Publish to the registry + +Run the release command from your plugin directory: + +```bash +cd nf-greeting +make release +``` + +This builds the plugin and publishes it to the registry in one step. + +??? info "What `make release` does" + + The `make release` command runs `./gradlew publishPlugin`, which: + + 1. Compiles your plugin code + 2. Runs tests + 3. Packages the plugin as a JAR file + 4. Uploads to the Nextflow plugin registry + 5. Makes it available for users to install + +### 2.5. Using published plugins + +Once published, users can install your plugin without any local setup: + +```groovy title="nextflow.config" +plugins { + id 'nf-greeting' // Latest version + id 'nf-greeting@1.0.0' // Specific version (recommended) +} +``` + +Nextflow automatically downloads the plugin from the registry on first use. + +--- + +## 3. Internal distribution + +Organizations often need to distribute plugins internally without using the public registry. +This is useful for proprietary plugins, plugins under development, or plugins that shouldn't be publicly available. + +!!! note "What internal distribution provides" + + Internal distribution uses a simple `plugins.json` file that tells Nextflow where to download plugin ZIP files. + This is **not** a full self-hosted registry (no web UI, search, or automatic updates). + A full self-hosted registry solution may be available in the future. + +### 3.1. Build the plugin ZIP + +The ZIP file will be at `build/distributions/`: + +```bash +./gradlew assemble +ls build/distributions/ +# nf-myplugin-0.1.0.zip +``` + +### 3.2. Host the files + +Host the ZIP file(s) somewhere accessible to your users: + +- Internal web server +- S3 bucket (with appropriate access) +- GitHub releases (for private repos) +- Shared network drive (using `file://` URLs) + +### 3.3. Create the plugins.json index + +Create a `plugins.json` file that describes available plugins: + +```json +[ + { + "id": "nf-myplugin", + "releases": [ + { + "version": "1.0.0", + "url": "https://internal.example.com/plugins/nf-myplugin-1.0.0.zip", + "date": "2025-01-09T10:00:00Z", + "sha512sum": "5abe4cbc643ca0333cba545846494b17488d19d17...", + "requires": ">=24.04.0" + } + ] + } +] +``` + +Host this file alongside your plugin ZIPs (or anywhere accessible). + +??? tip "Generating the checksum" + + ```bash + sha512sum nf-myplugin-1.0.0.zip | awk '{print $1}' + ``` + +??? info "plugins.json field reference" + + | Field | Description | + |-------|-------------| + | `id` | Plugin identifier (e.g., `nf-myplugin`) | + | `version` | Semantic version string | + | `url` | Direct download URL to the plugin ZIP | + | `date` | ISO 8601 timestamp | + | `sha512sum` | SHA-512 checksum of the ZIP file | + | `requires` | Minimum Nextflow version (e.g., `>=24.04.0`) | + +### 3.4. Configure Nextflow to use your index + +Set the environment variable before running Nextflow: + +```bash +export NXF_PLUGINS_TEST_REPOSITORY="https://internal.example.com/plugins/plugins.json" +``` + +Then use the plugin as normal: + +```groovy title="nextflow.config" +plugins { + id 'nf-myplugin@1.0.0' +} +``` + +!!! tip "Setting the variable permanently" + + Add the export to your shell profile (`~/.bashrc`, `~/.zshrc`) or set it in your CI/CD pipeline configuration. + +--- + +## 4. Versioning best practices + +Follow semantic versioning for your releases: + +| Version change | When to use | Example | +| ------------------------- | --------------------------------- | ------------------------------------------ | +| **MAJOR** (1.0.0 → 2.0.0) | Breaking changes | Removing a function, changing return types | +| **MINOR** (1.0.0 → 1.1.0) | New features, backward compatible | Adding a new function | +| **PATCH** (1.0.0 → 1.0.1) | Bug fixes, backward compatible | Fixing a bug in existing function | + +--- + +## 5. Advanced extension types + +Some extension types require significant infrastructure or deep Nextflow knowledge to implement. +This section provides a brief conceptual overview. +For implementation details, see the [Nextflow plugin documentation](https://www.nextflow.io/docs/latest/plugins/developing-plugins.html). + +### 5.1. Executors + +Executors define how tasks are submitted to compute resources: + +- AWS Batch, Google Cloud Batch, Azure Batch +- Kubernetes, SLURM, PBS, LSF +- Creating a custom executor is complex and typically done by platform vendors + +### 5.2. Filesystems + +Filesystems define how files are accessed: + +- S3, Google Cloud Storage, Azure Blob +- Custom storage systems +- Creating a custom filesystem requires implementing Java NIO interfaces + +--- + +## Takeaway + +You learned that: + +- Use the public registry for open source plugins (requires claiming a name) +- For internal distribution, host plugin ZIPs and a `plugins.json` index, then set `NXF_PLUGINS_TEST_REPOSITORY` +- Use semantic versioning to communicate changes to users +- Executors and filesystems are advanced extension types typically created by platform vendors + +--- + +## What's next? + +You've completed the plugin development training! + +[Continue to Summary :material-arrow-right:](summary.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/img/plugin-registry-nf-hello.png b/docs/hello_plugins/img/plugin-registry-nf-hello.png new file mode 100644 index 0000000000..c4cf2af234 Binary files /dev/null and b/docs/hello_plugins/img/plugin-registry-nf-hello.png differ diff --git a/docs/hello_plugins/img/test_report.png b/docs/hello_plugins/img/test_report.png new file mode 100644 index 0000000000..8ca8517f9c Binary files /dev/null and b/docs/hello_plugins/img/test_report.png differ diff --git a/docs/hello_plugins/index.md b/docs/hello_plugins/index.md new file mode 100644 index 0000000000..61ce9631ab --- /dev/null +++ b/docs/hello_plugins/index.md @@ -0,0 +1,89 @@ +--- +title: Hello Plugins +hide: + - toc +--- + +# Hello Plugins + +Nextflow's plugin system allows you to extend the language with custom functions, executors, trace observers, and more. +Plugins enable the community to add features to Nextflow without modifying its core, making them ideal for sharing reusable functionality across pipelines. + +During this training, you will learn how to use existing plugins and optionally create your own. + +## Audience & prerequisites + +This training is designed for two audiences: + +1. **All Nextflow users** (Parts 1-2): Learn to discover, install, and use existing plugins to extend your pipelines. +2. **Developers** (Parts 3-7): Learn to create your own plugins with custom functions, observers, and configuration. + +**Prerequisites** + +- A GitHub account OR a local installation as described [here](../envsetup/02_local). +- Completed the [Hello Nextflow](../hello_nextflow/index.md) course or equivalent. +- For plugin development (Parts 3-7): Java 21+ installed and basic familiarity with object-oriented programming. + +## Learning objectives + +By the end of this training, you will be able to: + +**Using plugins (Parts 1-2):** + +- Understand what plugins are and how they extend Nextflow +- Install and configure existing plugins in your workflows +- Import and use plugin functions + +**Developing plugins (Parts 3-7):** + +- Create a new plugin project using Nextflow's scaffolding command +- Implement custom functions callable from workflows +- Create trace observers to hook into workflow lifecycle events +- Add configuration options to make plugins customizable +- Build, test, and distribute your plugin + +## Detailed lesson plan + +This training course teaches you both how to use plugins and how to create them. + +#### Part 1: Plugin basics + +You'll start by understanding **what plugins are and how they work**. +You'll learn about the different types of extensions plugins can provide (functions, observers, executors, filesystems) and how Nextflow's plugin system is built on PF4J. +Then you'll discover and use existing plugins from the Nextflow Plugin Registry. + +#### Part 2: Create a plugin project + +Next, you'll **scaffold a new plugin project** using Nextflow's built-in command. +You'll examine the generated project structure and understand how the different components (Plugin, Extension, Factory, Observer) work together. + +#### Part 3: Custom functions + +You'll **implement custom functions** that can be called from Nextflow workflows. +Using the `@Function` annotation, you'll create functions that transform data and make them available via the familiar `include` syntax. + +#### Part 4: Build and test + +You'll learn the **plugin development cycle**: building with Gradle, writing unit tests with Spock, and installing locally for testing. +You'll see how to iterate quickly and debug common issues. + +#### Part 5: Trace observers + +You'll explore **trace observers**, which let your plugin hook into workflow lifecycle events. +You'll build an observer that responds to task completion and workflow events, enabling features like custom logging, metrics collection, or notifications. + +#### Part 6: Configuration + +You'll make your plugin **configurable** by reading settings from `nextflow.config`. +Users will be able to customize plugin behavior without modifying code. + +#### Part 7: Distribution + +Finally, you'll learn **how to share your plugin** with others. +You can publish to the public Nextflow Plugin Registry or set up internal distribution for proprietary plugins. + +**By the end of the course, you'll have created a fully functional Nextflow plugin with custom functions, lifecycle observers, and configuration support.** + +Ready to take the course? + +[Start learning :material-arrow-right:](00_orientation.md){ .md-button .md-button--primary } diff --git a/docs/hello_plugins/next_steps.md b/docs/hello_plugins/next_steps.md new file mode 100644 index 0000000000..33f032a7fc --- /dev/null +++ b/docs/hello_plugins/next_steps.md @@ -0,0 +1,69 @@ +# Next Steps + +Congratulations on completing the **Hello Plugins** training course and thank you for completing our survey. + +--- + +## 1. Top 3 ways to continue your plugin journey + +Here are our top three recommendations for what to do next based on the course you just completed. + +### 1.1. Explore the plugin registry + +**Browse the [Nextflow Plugin Registry](https://registry.nextflow.io/)** to see what plugins are available. +You'll find plugins for: + +- Input validation (nf-schema) +- Cloud platform integration (AWS, Google Cloud, Azure) +- Notifications (Slack, Teams) +- Provenance tracking +- And much more + +Understanding what's already available can save you time and inspire ideas for your own plugins. + +### 1.2. Study existing plugin source code + +**Look at the source code of popular plugins** to see how they're structured and implemented. +The [nf-hello](https://github.com/nextflow-io/nf-hello) plugin is intentionally simple and well-documented. +The [official plugins repository](https://github.com/nextflow-io/plugins) contains more complex examples including executors and filesystem implementations. + +### 1.3. Build something useful + +**The best way to learn is by building.** +Think about functionality you've wanted in Nextflow but couldn't find. +Some ideas: + +- Custom logging or reporting for your organization +- Integration with internal tools or APIs +- Domain-specific utility functions +- Notifications to your preferred platform + +Start small, get it working, then iterate. + +--- + +## 2. Get help from the community + +**The Nextflow community is friendly and helpful.** + +- [Nextflow Slack](https://www.nextflow.io/slack-invite.html): Ask questions and share your work +- [Community forum](https://community.seqera.io/): Discuss ideas and get advice +- [GitHub discussions](https://github.com/nextflow-io/nextflow/discussions): Technical questions and feature requests + +If you build a useful plugin, consider sharing it with the community through the plugin registry. + +--- + +## 3. Continue your Nextflow training + +If you haven't already, check out our other training courses: + +- **[Hello Nextflow](../hello_nextflow/index.md)**: Foundational Nextflow concepts +- **[Hello nf-core](../hello_nf-core/index.md)**: nf-core pipelines and best practices +- **[Side Quests](../side_quests/index.md)**: Deep dives into specific topics + +--- + +### That's it for now + +**Good luck building amazing plugins and don't hesitate to share your work with the community.** diff --git a/docs/hello_plugins/summary.md b/docs/hello_plugins/summary.md new file mode 100644 index 0000000000..1beeda7ef0 --- /dev/null +++ b/docs/hello_plugins/summary.md @@ -0,0 +1,98 @@ +# Summary + +Congratulations on completing the Hello Plugins training. + +--- + +## What you learned + +**If you completed Parts 1-2**, you now know how to discover, configure, and use existing plugins to extend your Nextflow pipelines. +This knowledge will help you leverage the growing ecosystem of community plugins. + +**If you completed Parts 3-7**, you've also learned how to create your own plugins, implementing custom functions, trace observers, and more. +Plugin development opens up powerful possibilities for: + +- Sharing reusable functions across your organization +- Integrating with external services and APIs +- Custom monitoring and reporting +- Supporting new execution platforms + +--- + +## Plugin development checklist + +- [ ] Java 21+ installed +- [ ] Create project with `nextflow plugin create ` +- [ ] Implement extension class with `@Function` methods +- [ ] Optionally add `TraceObserver` implementations for workflow events +- [ ] Write unit tests +- [ ] Build with `make assemble` +- [ ] Install with `make install` +- [ ] Enable in `nextflow.config` with `plugins { id 'plugin-id' }` +- [ ] Import functions with `include { fn } from 'plugin/plugin-id'` + +--- + +## Key code patterns + +**Function definition:** + +```groovy +@Function +String myFunction(String input, String optional = 'default') { + return input.transform() +} +``` + +**Plugin configuration:** + +```groovy +nextflowPlugin { + provider = 'my-org' + className = 'my.org.MyPlugin' + extensionPoints = ['my.org.MyExtension'] +} +``` + +**Using in workflows:** + +```groovy +include { myFunction } from 'plugin/my-plugin' + +workflow { + channel.of('a', 'b', 'c') + .map { item -> myFunction(item) } + .view() +} +``` + +--- + +## Extension point summary + +| Type | Annotation | Purpose | +| -------- | ----------- | ----------------------- | +| Function | `@Function` | Callable from workflows | + +--- + +## Additional resources + +**Official documentation:** + +- [Using plugins](https://www.nextflow.io/docs/latest/plugins/plugins.html): comprehensive guide to installing and configuring plugins +- [Developing plugins](https://www.nextflow.io/docs/latest/plugins/developing-plugins.html): detailed plugin development reference + +**Plugin discovery:** + +- [Nextflow Plugin Registry](https://registry.nextflow.io/): browse and discover available plugins +- [Plugin registry docs](https://www.nextflow.io/docs/latest/plugins/plugin-registry.html): registry documentation + +**Examples and references:** + +- [nf-hello](https://github.com/nextflow-io/nf-hello): simple example plugin (great starting point) +- [Nextflow plugins repository](https://github.com/nextflow-io/plugins): collection of official plugins for reference + +--- + +Whether you're using existing plugins or building your own, you now have the tools to extend Nextflow beyond its core capabilities. diff --git a/docs/hello_plugins/survey.md b/docs/hello_plugins/survey.md new file mode 100644 index 0000000000..c292ab382c --- /dev/null +++ b/docs/hello_plugins/survey.md @@ -0,0 +1,7 @@ +# Feedback survey + +Before you move on, please complete this short 5-question survey to rate the training, share any feedback you may have about your experience, and let us know what else we could do to help you in your Nextflow journey. + +This should take you less than a minute to complete. Thank you for helping us improve our training materials for everyone! + +
diff --git a/docs/side_quests/img/plugin-registry-nf-hello.png b/docs/side_quests/img/plugin-registry-nf-hello.png new file mode 100644 index 0000000000..c4cf2af234 Binary files /dev/null and b/docs/side_quests/img/plugin-registry-nf-hello.png differ diff --git a/docs/side_quests/img/test_report.png b/docs/side_quests/img/test_report.png new file mode 100644 index 0000000000..8ca8517f9c Binary files /dev/null and b/docs/side_quests/img/test_report.png differ diff --git a/hello-plugins/greetings.csv b/hello-plugins/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/hello-plugins/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/hello-plugins/main.nf b/hello-plugins/main.nf new file mode 100644 index 0000000000..c4943d5842 --- /dev/null +++ b/hello-plugins/main.nf @@ -0,0 +1,34 @@ +#!/usr/bin/env nextflow + +// Import custom functions from our plugin +include { reverseGreeting } from 'plugin/nf-greeting' +include { decorateGreeting } from 'plugin/nf-greeting' + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated: ${result.trim()}" } +} diff --git a/hello-plugins/nextflow.config b/hello-plugins/nextflow.config new file mode 100644 index 0000000000..ca4e92447f --- /dev/null +++ b/hello-plugins/nextflow.config @@ -0,0 +1 @@ +// Configuration for plugin development exercises diff --git a/hello-plugins/random_id_example.nf b/hello-plugins/random_id_example.nf new file mode 100644 index 0000000000..1a90d9d773 --- /dev/null +++ b/hello-plugins/random_id_example.nf @@ -0,0 +1,17 @@ +#!/usr/bin/env nextflow + +/** + * Generate a random alphanumeric string + */ +def randomString(int length) { + def chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + def random = new Random() + return (1..length).collect { chars[random.nextInt(chars.size())] }.join() +} + +workflow { + // Generate random IDs for each sample + Channel.of('sample_A', 'sample_B', 'sample_C') + .map { sample -> "${sample}_${randomString(8)}" } + .view() +} diff --git a/hello-plugins/solutions/1-plugin-basics/nextflow.config b/hello-plugins/solutions/1-plugin-basics/nextflow.config new file mode 100644 index 0000000000..a4daf95bd4 --- /dev/null +++ b/hello-plugins/solutions/1-plugin-basics/nextflow.config @@ -0,0 +1,4 @@ +// Configuration for plugin development exercises +plugins { + id 'nf-hello@0.5.0' +} diff --git a/hello-plugins/solutions/1-plugin-basics/random_id_example.nf b/hello-plugins/solutions/1-plugin-basics/random_id_example.nf new file mode 100644 index 0000000000..1b5ff4f5e0 --- /dev/null +++ b/hello-plugins/solutions/1-plugin-basics/random_id_example.nf @@ -0,0 +1,10 @@ +#!/usr/bin/env nextflow + +include { randomString } from 'plugin/nf-hello' + +workflow { + // Generate random IDs for each sample + Channel.of('sample_A', 'sample_B', 'sample_C') + .map { sample -> "${sample}_${randomString(8)}" } + .view() +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/.gitignore b/hello-plugins/solutions/2-create-project/nf-greeting/.gitignore new file mode 100644 index 0000000000..dbef60b25d --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle +.idea +.nextflow* + +# Ignore Gradle build output directory +build +work diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/COPYING b/hello-plugins/solutions/2-create-project/nf-greeting/COPYING new file mode 100644 index 0000000000..68c771a099 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/COPYING @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/Makefile b/hello-plugins/solutions/2-create-project/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/README.md b/hello-plugins/solutions/2-create-project/nf-greeting/README.md new file mode 100644 index 0000000000..3d3c906404 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/README.md @@ -0,0 +1,31 @@ +# nf-greeting plugin + +## Building + +To build the plugin: + +```bash +make assemble +``` + +## Testing with Nextflow + +The plugin can be tested without a local Nextflow installation: + +1. Build and install the plugin to your local Nextflow installation: `make install` +2. Run a pipeline with the plugin: `nextflow run hello -plugins nf-greeting@0.1.0` + +## Publishing + +Plugins can be published to a central plugin registry to make them accessible to the Nextflow community. + +Follow these steps to publish the plugin to the Nextflow Plugin Registry: + +1. Create a file named `$HOME/.gradle/gradle.properties`, where $HOME is your home directory. Add the following properties: + + - `pluginRegistry.accessToken`: Your Nextflow Plugin Registry access token. + +2. Use the following command to package and create a release for your plugin on GitHub: `make release`. + +> [!NOTE] +> The Nextflow Pluging registry is currently avaialable as private beta technology. Contact info@nextflow.io to learn how to get access to it. diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/build.gradle b/hello-plugins/solutions/2-create-project/nf-greeting/build.gradle new file mode 100644 index 0000000000..317ba7806d --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '25.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.jar b/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.properties b/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ca025c83a7 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/gradlew b/hello-plugins/solutions/2-create-project/nf-greeting/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/settings.gradle b/hello-plugins/solutions/2-create-project/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy new file mode 100644 index 0000000000..aaff849386 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.plugin.extension.Function +import nextflow.plugin.extension.PluginExtensionPoint + +/** + * Implements a custom function which can be imported by + * Nextflow scripts. + */ +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Say hello to the given target. + * + * @param target + */ + @Function + void sayHello(String target) { + println "Hello, ${target}!" + } + +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy new file mode 100644 index 0000000000..0d7f093ab4 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.trace.TraceObserver +import nextflow.trace.TraceObserverFactory + +/** + * Implements a factory object required to create + * the {@link GreetingObserver} instance. + */ +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + return List.of(new GreetingObserver()) + } + +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy new file mode 100644 index 0000000000..d12189b00b --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.trace.TraceObserver + +/** + * Implements an observer that allows implementing custom + * logic on nextflow execution events. + */ +@Slf4j +@CompileStatic +class GreetingObserver implements TraceObserver { + + @Override + void onFlowCreate(Session session) { + println "Pipeline is starting! 🚀" + } + + @Override + void onFlowComplete() { + println "Pipeline complete! 👋" + } +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy new file mode 100644 index 0000000000..41f8b387ed --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.plugin.BasePlugin +import org.pf4j.PluginWrapper + +/** + * The plugin entry point + */ +@CompileStatic +class GreetingPlugin extends BasePlugin { + + GreetingPlugin(PluginWrapper wrapper) { + super(wrapper) + } +} diff --git a/hello-plugins/solutions/2-create-project/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy b/hello-plugins/solutions/2-create-project/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy new file mode 100644 index 0000000000..a6135e5869 --- /dev/null +++ b/hello-plugins/solutions/2-create-project/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy @@ -0,0 +1,22 @@ +package training.plugin + +import nextflow.Session +import spock.lang.Specification + +/** + * Implements a basic factory test + * + */ +class GreetingObserverTest extends Specification { + + def 'should create the observer instance' () { + given: + def factory = new GreetingFactory() + when: + def result = factory.create(Mock(Session)) + then: + result.size() == 1 + result.first() instanceof GreetingObserver + } + +} diff --git a/hello-plugins/solutions/3-custom-functions/greetings.csv b/hello-plugins/solutions/3-custom-functions/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/hello-plugins/solutions/3-custom-functions/main.nf b/hello-plugins/solutions/3-custom-functions/main.nf new file mode 100644 index 0000000000..7eec641233 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/main.nf @@ -0,0 +1,22 @@ +#!/usr/bin/env nextflow + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + """ + echo '$greeting' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Output: ${result.trim()}" } +} diff --git a/hello-plugins/solutions/3-custom-functions/nextflow.config b/hello-plugins/solutions/3-custom-functions/nextflow.config new file mode 100644 index 0000000000..a4daf95bd4 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nextflow.config @@ -0,0 +1,4 @@ +// Configuration for plugin development exercises +plugins { + id 'nf-hello@0.5.0' +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/.gitignore b/hello-plugins/solutions/3-custom-functions/nf-greeting/.gitignore new file mode 100644 index 0000000000..dbef60b25d --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle +.idea +.nextflow* + +# Ignore Gradle build output directory +build +work diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/COPYING b/hello-plugins/solutions/3-custom-functions/nf-greeting/COPYING new file mode 100644 index 0000000000..68c771a099 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/COPYING @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/Makefile b/hello-plugins/solutions/3-custom-functions/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/README.md b/hello-plugins/solutions/3-custom-functions/nf-greeting/README.md new file mode 100644 index 0000000000..3d3c906404 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/README.md @@ -0,0 +1,31 @@ +# nf-greeting plugin + +## Building + +To build the plugin: + +```bash +make assemble +``` + +## Testing with Nextflow + +The plugin can be tested without a local Nextflow installation: + +1. Build and install the plugin to your local Nextflow installation: `make install` +2. Run a pipeline with the plugin: `nextflow run hello -plugins nf-greeting@0.1.0` + +## Publishing + +Plugins can be published to a central plugin registry to make them accessible to the Nextflow community. + +Follow these steps to publish the plugin to the Nextflow Plugin Registry: + +1. Create a file named `$HOME/.gradle/gradle.properties`, where $HOME is your home directory. Add the following properties: + + - `pluginRegistry.accessToken`: Your Nextflow Plugin Registry access token. + +2. Use the following command to package and create a release for your plugin on GitHub: `make release`. + +> [!NOTE] +> The Nextflow Pluging registry is currently avaialable as private beta technology. Contact info@nextflow.io to learn how to get access to it. diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/build.gradle b/hello-plugins/solutions/3-custom-functions/nf-greeting/build.gradle new file mode 100644 index 0000000000..317ba7806d --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '25.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.jar b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.properties b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ca025c83a7 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/gradlew b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/settings.gradle b/hello-plugins/solutions/3-custom-functions/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy new file mode 100644 index 0000000000..e0a28f8746 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.plugin.extension.Function +import nextflow.plugin.extension.PluginExtensionPoint + +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + /** + * Convert greeting to a friendly format with a name + */ + @Function + String friendlyGreeting(String greeting, String name = 'World') { + return "${greeting}, ${name}!" + } + +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy new file mode 100644 index 0000000000..0d7f093ab4 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.trace.TraceObserver +import nextflow.trace.TraceObserverFactory + +/** + * Implements a factory object required to create + * the {@link GreetingObserver} instance. + */ +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + return List.of(new GreetingObserver()) + } + +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy new file mode 100644 index 0000000000..d12189b00b --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.trace.TraceObserver + +/** + * Implements an observer that allows implementing custom + * logic on nextflow execution events. + */ +@Slf4j +@CompileStatic +class GreetingObserver implements TraceObserver { + + @Override + void onFlowCreate(Session session) { + println "Pipeline is starting! 🚀" + } + + @Override + void onFlowComplete() { + println "Pipeline complete! 👋" + } +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy new file mode 100644 index 0000000000..41f8b387ed --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.plugin.BasePlugin +import org.pf4j.PluginWrapper + +/** + * The plugin entry point + */ +@CompileStatic +class GreetingPlugin extends BasePlugin { + + GreetingPlugin(PluginWrapper wrapper) { + super(wrapper) + } +} diff --git a/hello-plugins/solutions/3-custom-functions/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy new file mode 100644 index 0000000000..a6135e5869 --- /dev/null +++ b/hello-plugins/solutions/3-custom-functions/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy @@ -0,0 +1,22 @@ +package training.plugin + +import nextflow.Session +import spock.lang.Specification + +/** + * Implements a basic factory test + * + */ +class GreetingObserverTest extends Specification { + + def 'should create the observer instance' () { + given: + def factory = new GreetingFactory() + when: + def result = factory.create(Mock(Session)) + then: + result.size() == 1 + result.first() instanceof GreetingObserver + } + +} diff --git a/hello-plugins/solutions/4-build-and-test/greetings.csv b/hello-plugins/solutions/4-build-and-test/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/hello-plugins/solutions/4-build-and-test/main.nf b/hello-plugins/solutions/4-build-and-test/main.nf new file mode 100644 index 0000000000..c4943d5842 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/main.nf @@ -0,0 +1,34 @@ +#!/usr/bin/env nextflow + +// Import custom functions from our plugin +include { reverseGreeting } from 'plugin/nf-greeting' +include { decorateGreeting } from 'plugin/nf-greeting' + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated: ${result.trim()}" } +} diff --git a/hello-plugins/solutions/4-build-and-test/nextflow.config b/hello-plugins/solutions/4-build-and-test/nextflow.config new file mode 100644 index 0000000000..7906a8aba1 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nextflow.config @@ -0,0 +1,4 @@ +// Configuration for plugin development exercises +plugins { + id 'nf-greeting@0.1.0' +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/.gitignore b/hello-plugins/solutions/4-build-and-test/nf-greeting/.gitignore new file mode 100644 index 0000000000..dbef60b25d --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle +.idea +.nextflow* + +# Ignore Gradle build output directory +build +work diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/COPYING b/hello-plugins/solutions/4-build-and-test/nf-greeting/COPYING new file mode 100644 index 0000000000..68c771a099 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/COPYING @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/Makefile b/hello-plugins/solutions/4-build-and-test/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/README.md b/hello-plugins/solutions/4-build-and-test/nf-greeting/README.md new file mode 100644 index 0000000000..3d3c906404 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/README.md @@ -0,0 +1,31 @@ +# nf-greeting plugin + +## Building + +To build the plugin: + +```bash +make assemble +``` + +## Testing with Nextflow + +The plugin can be tested without a local Nextflow installation: + +1. Build and install the plugin to your local Nextflow installation: `make install` +2. Run a pipeline with the plugin: `nextflow run hello -plugins nf-greeting@0.1.0` + +## Publishing + +Plugins can be published to a central plugin registry to make them accessible to the Nextflow community. + +Follow these steps to publish the plugin to the Nextflow Plugin Registry: + +1. Create a file named `$HOME/.gradle/gradle.properties`, where $HOME is your home directory. Add the following properties: + + - `pluginRegistry.accessToken`: Your Nextflow Plugin Registry access token. + +2. Use the following command to package and create a release for your plugin on GitHub: `make release`. + +> [!NOTE] +> The Nextflow Pluging registry is currently avaialable as private beta technology. Contact info@nextflow.io to learn how to get access to it. diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/build.gradle b/hello-plugins/solutions/4-build-and-test/nf-greeting/build.gradle new file mode 100644 index 0000000000..317ba7806d --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '25.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.jar b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.properties b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ca025c83a7 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/gradlew b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/settings.gradle b/hello-plugins/solutions/4-build-and-test/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy new file mode 100644 index 0000000000..e0a28f8746 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.plugin.extension.Function +import nextflow.plugin.extension.PluginExtensionPoint + +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + /** + * Reverse a greeting string + */ + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + /** + * Decorate a greeting with celebratory markers + */ + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + /** + * Convert greeting to a friendly format with a name + */ + @Function + String friendlyGreeting(String greeting, String name = 'World') { + return "${greeting}, ${name}!" + } + +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy new file mode 100644 index 0000000000..0d7f093ab4 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.trace.TraceObserver +import nextflow.trace.TraceObserverFactory + +/** + * Implements a factory object required to create + * the {@link GreetingObserver} instance. + */ +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + return List.of(new GreetingObserver()) + } + +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy new file mode 100644 index 0000000000..d12189b00b --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.trace.TraceObserver + +/** + * Implements an observer that allows implementing custom + * logic on nextflow execution events. + */ +@Slf4j +@CompileStatic +class GreetingObserver implements TraceObserver { + + @Override + void onFlowCreate(Session session) { + println "Pipeline is starting! 🚀" + } + + @Override + void onFlowComplete() { + println "Pipeline complete! 👋" + } +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy new file mode 100644 index 0000000000..41f8b387ed --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.plugin.BasePlugin +import org.pf4j.PluginWrapper + +/** + * The plugin entry point + */ +@CompileStatic +class GreetingPlugin extends BasePlugin { + + GreetingPlugin(PluginWrapper wrapper) { + super(wrapper) + } +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy new file mode 100644 index 0000000000..97d335551a --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy @@ -0,0 +1,42 @@ +package training.plugin + +import spock.lang.Specification + +/** + * Tests for the greeting extension functions + */ +class GreetingExtensionTest extends Specification { + + def 'should reverse a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.reverseGreeting('Hello') == 'olleH' + ext.reverseGreeting('Bonjour') == 'ruojnoB' + } + + def 'should decorate a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.decorateGreeting('Hello') == '*** Hello ***' + } + + def 'should create friendly greeting with default name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello') == 'Hello, World!' + } + + def 'should create friendly greeting with custom name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello', 'Alice') == 'Hello, Alice!' + } +} diff --git a/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy new file mode 100644 index 0000000000..a6135e5869 --- /dev/null +++ b/hello-plugins/solutions/4-build-and-test/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy @@ -0,0 +1,22 @@ +package training.plugin + +import nextflow.Session +import spock.lang.Specification + +/** + * Implements a basic factory test + * + */ +class GreetingObserverTest extends Specification { + + def 'should create the observer instance' () { + given: + def factory = new GreetingFactory() + when: + def result = factory.create(Mock(Session)) + then: + result.size() == 1 + result.first() instanceof GreetingObserver + } + +} diff --git a/hello-plugins/solutions/5-observers/greetings.csv b/hello-plugins/solutions/5-observers/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/hello-plugins/solutions/5-observers/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/hello-plugins/solutions/5-observers/main.nf b/hello-plugins/solutions/5-observers/main.nf new file mode 100644 index 0000000000..c4943d5842 --- /dev/null +++ b/hello-plugins/solutions/5-observers/main.nf @@ -0,0 +1,34 @@ +#!/usr/bin/env nextflow + +// Import custom functions from our plugin +include { reverseGreeting } from 'plugin/nf-greeting' +include { decorateGreeting } from 'plugin/nf-greeting' + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated: ${result.trim()}" } +} diff --git a/hello-plugins/solutions/5-observers/nextflow.config b/hello-plugins/solutions/5-observers/nextflow.config new file mode 100644 index 0000000000..7906a8aba1 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nextflow.config @@ -0,0 +1,4 @@ +// Configuration for plugin development exercises +plugins { + id 'nf-greeting@0.1.0' +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/Makefile b/hello-plugins/solutions/5-observers/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/hello-plugins/solutions/5-observers/nf-greeting/build.gradle b/hello-plugins/solutions/5-observers/nf-greeting/build.gradle new file mode 100644 index 0000000000..317ba7806d --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '25.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory' + ] + +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.jar b/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.properties b/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ca025c83a7 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hello-plugins/solutions/5-observers/nf-greeting/gradlew b/hello-plugins/solutions/5-observers/nf-greeting/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hello-plugins/solutions/5-observers/nf-greeting/settings.gradle b/hello-plugins/solutions/5-observers/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy new file mode 100644 index 0000000000..4434a1fe74 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy @@ -0,0 +1,29 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.plugin.extension.Function +import nextflow.plugin.extension.PluginExtensionPoint + +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + @Override + protected void init(Session session) { + } + + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + @Function + String decorateGreeting(String greeting) { + return "*** ${greeting} ***" + } + + @Function + String friendlyGreeting(String greeting, String name = 'World') { + return "${greeting}, ${name}!" + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy new file mode 100644 index 0000000000..5a1fb36927 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy @@ -0,0 +1,18 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.trace.TraceObserver +import nextflow.trace.TraceObserverFactory + +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + return [ + new GreetingObserver(), + new TaskCounterObserver() + ] + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy new file mode 100644 index 0000000000..d12189b00b --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.trace.TraceObserver + +/** + * Implements an observer that allows implementing custom + * logic on nextflow execution events. + */ +@Slf4j +@CompileStatic +class GreetingObserver implements TraceObserver { + + @Override + void onFlowCreate(Session session) { + println "Pipeline is starting! 🚀" + } + + @Override + void onFlowComplete() { + println "Pipeline complete! 👋" + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy new file mode 100644 index 0000000000..41f8b387ed --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.plugin.BasePlugin +import org.pf4j.PluginWrapper + +/** + * The plugin entry point + */ +@CompileStatic +class GreetingPlugin extends BasePlugin { + + GreetingPlugin(PluginWrapper wrapper) { + super(wrapper) + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy new file mode 100644 index 0000000000..edddaf9beb --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy @@ -0,0 +1,26 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.processor.TaskHandler +import nextflow.trace.TraceObserver +import nextflow.trace.TraceRecord + +/** + * Observer that counts completed tasks + */ +@CompileStatic +class TaskCounterObserver implements TraceObserver { + + private int taskCount = 0 + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + taskCount++ + println "📊 Tasks completed so far: ${taskCount}" + } + + @Override + void onFlowComplete() { + println "📈 Final task count: ${taskCount}" + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy new file mode 100644 index 0000000000..97d335551a --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy @@ -0,0 +1,42 @@ +package training.plugin + +import spock.lang.Specification + +/** + * Tests for the greeting extension functions + */ +class GreetingExtensionTest extends Specification { + + def 'should reverse a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.reverseGreeting('Hello') == 'olleH' + ext.reverseGreeting('Bonjour') == 'ruojnoB' + } + + def 'should decorate a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.decorateGreeting('Hello') == '*** Hello ***' + } + + def 'should create friendly greeting with default name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello') == 'Hello, World!' + } + + def 'should create friendly greeting with custom name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello', 'Alice') == 'Hello, Alice!' + } +} diff --git a/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy b/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy new file mode 100644 index 0000000000..a6135e5869 --- /dev/null +++ b/hello-plugins/solutions/5-observers/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy @@ -0,0 +1,22 @@ +package training.plugin + +import nextflow.Session +import spock.lang.Specification + +/** + * Implements a basic factory test + * + */ +class GreetingObserverTest extends Specification { + + def 'should create the observer instance' () { + given: + def factory = new GreetingFactory() + when: + def result = factory.create(Mock(Session)) + then: + result.size() == 1 + result.first() instanceof GreetingObserver + } + +} diff --git a/hello-plugins/solutions/6-configuration/greetings.csv b/hello-plugins/solutions/6-configuration/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/hello-plugins/solutions/6-configuration/main.nf b/hello-plugins/solutions/6-configuration/main.nf new file mode 100644 index 0000000000..c4943d5842 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/main.nf @@ -0,0 +1,34 @@ +#!/usr/bin/env nextflow + +// Import custom functions from our plugin +include { reverseGreeting } from 'plugin/nf-greeting' +include { decorateGreeting } from 'plugin/nf-greeting' + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated: ${result.trim()}" } +} diff --git a/hello-plugins/solutions/6-configuration/nextflow.config b/hello-plugins/solutions/6-configuration/nextflow.config new file mode 100644 index 0000000000..ae48c3af9a --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nextflow.config @@ -0,0 +1,9 @@ +plugins { + id 'nf-greeting@0.1.0' +} + +greeting { + taskCounter.verbose = false + prefix = '>>>' + suffix = '<<<' +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/Makefile b/hello-plugins/solutions/6-configuration/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/build.gradle b/hello-plugins/solutions/6-configuration/nf-greeting/build.gradle new file mode 100644 index 0000000000..6fbbb0534d --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '1.0.0-beta.10' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '25.10.0' + + provider = 'training' + className = 'training.plugin.GreetingPlugin' + extensionPoints = [ + 'training.plugin.GreetingExtension', + 'training.plugin.GreetingFactory', + 'training.plugin.GreetingConfig' + ] + +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.jar b/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.properties b/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ca025c83a7 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/gradlew b/hello-plugins/solutions/6-configuration/nf-greeting/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/settings.gradle b/hello-plugins/solutions/6-configuration/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingConfig.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingConfig.groovy new file mode 100644 index 0000000000..525817533a --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingConfig.groovy @@ -0,0 +1,65 @@ +package training.plugin + +import nextflow.config.spec.ConfigOption +import nextflow.config.spec.ConfigScope +import nextflow.config.spec.ScopeName + +/** + * Configuration options for the nf-greeting plugin. + * + * Users configure these in nextflow.config: + * + * greeting { + * enabled = true + * prefix = '>>>' + * suffix = '<<<' + * taskCounter.verbose = false + * } + */ +@ScopeName('greeting') +class GreetingConfig implements ConfigScope { + + GreetingConfig() {} + + GreetingConfig(Map opts) { + this.enabled = opts.enabled as Boolean ?: true + this.prefix = opts.prefix as String ?: '***' + this.suffix = opts.suffix as String ?: '***' + if (opts.taskCounter instanceof Map) { + this.taskCounter = new TaskCounterConfig(opts.taskCounter as Map) + } + } + + /** + * Enable or disable the plugin entirely. + */ + @ConfigOption + boolean enabled = true + + /** + * Prefix for decorated greetings. + */ + @ConfigOption + String prefix = '***' + + /** + * Suffix for decorated greetings. + */ + @ConfigOption + String suffix = '***' + + /** + * Task counter configuration + */ + TaskCounterConfig taskCounter = new TaskCounterConfig() + + static class TaskCounterConfig implements ConfigScope { + TaskCounterConfig() {} + TaskCounterConfig(Map opts) { + this.verbose = opts.verbose as Boolean ?: true + } + + @ConfigOption + boolean verbose = true + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy new file mode 100644 index 0000000000..103d11b24e --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingExtension.groovy @@ -0,0 +1,36 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.plugin.extension.Function +import nextflow.plugin.extension.PluginExtensionPoint + +@CompileStatic +class GreetingExtension extends PluginExtensionPoint { + + private String prefix = '***' + private String suffix = '***' + + @Override + protected void init(Session session) { + // Read configuration with defaults + // (GreetingConfig class provides validation/documentation) + prefix = session.config.navigate('greeting.prefix', '***') as String + suffix = session.config.navigate('greeting.suffix', '***') as String + } + + @Function + String reverseGreeting(String greeting) { + return greeting.reverse() + } + + @Function + String decorateGreeting(String greeting) { + return "${prefix} ${greeting} ${suffix}" + } + + @Function + String friendlyGreeting(String greeting, String name = 'World') { + return "${greeting}, ${name}!" + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy new file mode 100644 index 0000000000..7f0e9ef1fd --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingFactory.groovy @@ -0,0 +1,23 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.Session +import nextflow.trace.TraceObserver +import nextflow.trace.TraceObserverFactory + +@CompileStatic +class GreetingFactory implements TraceObserverFactory { + + @Override + Collection create(Session session) { + // Read from GreetingConfig scope (GreetingConfig class provides validation) + final enabled = session.config.navigate('greeting.enabled', true) + if (!enabled) return [] + + final verbose = session.config.navigate('greeting.taskCounter.verbose', true) as boolean + return [ + new GreetingObserver(), + new TaskCounterObserver(verbose) + ] + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy new file mode 100644 index 0000000000..d12189b00b --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingObserver.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.trace.TraceObserver + +/** + * Implements an observer that allows implementing custom + * logic on nextflow execution events. + */ +@Slf4j +@CompileStatic +class GreetingObserver implements TraceObserver { + + @Override + void onFlowCreate(Session session) { + println "Pipeline is starting! 🚀" + } + + @Override + void onFlowComplete() { + println "Pipeline complete! 👋" + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy new file mode 100644 index 0000000000..41f8b387ed --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/GreetingPlugin.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 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 training.plugin + +import groovy.transform.CompileStatic +import nextflow.plugin.BasePlugin +import org.pf4j.PluginWrapper + +/** + * The plugin entry point + */ +@CompileStatic +class GreetingPlugin extends BasePlugin { + + GreetingPlugin(PluginWrapper wrapper) { + super(wrapper) + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy new file mode 100644 index 0000000000..55bc42d294 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/main/groovy/training/plugin/TaskCounterObserver.groovy @@ -0,0 +1,33 @@ +package training.plugin + +import groovy.transform.CompileStatic +import nextflow.processor.TaskHandler +import nextflow.trace.TraceObserver +import nextflow.trace.TraceRecord + +/** + * Observer that counts completed tasks + */ +@CompileStatic +class TaskCounterObserver implements TraceObserver { + + private final boolean verbose + private int taskCount = 0 + + TaskCounterObserver(boolean verbose) { + this.verbose = verbose + } + + @Override + void onProcessComplete(TaskHandler handler, TraceRecord trace) { + taskCount++ + if (verbose) { + println "📊 Tasks completed so far: ${taskCount}" + } + } + + @Override + void onFlowComplete() { + println "📈 Final task count: ${taskCount}" + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy new file mode 100644 index 0000000000..97d335551a --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingExtensionTest.groovy @@ -0,0 +1,42 @@ +package training.plugin + +import spock.lang.Specification + +/** + * Tests for the greeting extension functions + */ +class GreetingExtensionTest extends Specification { + + def 'should reverse a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.reverseGreeting('Hello') == 'olleH' + ext.reverseGreeting('Bonjour') == 'ruojnoB' + } + + def 'should decorate a greeting'() { + given: + def ext = new GreetingExtension() + + expect: + ext.decorateGreeting('Hello') == '*** Hello ***' + } + + def 'should create friendly greeting with default name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello') == 'Hello, World!' + } + + def 'should create friendly greeting with custom name'() { + given: + def ext = new GreetingExtension() + + expect: + ext.friendlyGreeting('Hello', 'Alice') == 'Hello, Alice!' + } +} diff --git a/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy b/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy new file mode 100644 index 0000000000..a6135e5869 --- /dev/null +++ b/hello-plugins/solutions/6-configuration/nf-greeting/src/test/groovy/training/plugin/GreetingObserverTest.groovy @@ -0,0 +1,22 @@ +package training.plugin + +import nextflow.Session +import spock.lang.Specification + +/** + * Implements a basic factory test + * + */ +class GreetingObserverTest extends Specification { + + def 'should create the observer instance' () { + given: + def factory = new GreetingFactory() + when: + def result = factory.create(Mock(Session)) + then: + result.size() == 1 + result.first() instanceof GreetingObserver + } + +} diff --git a/mkdocs.yml b/mkdocs.yml index 57e6ef18e5..f532a94407 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,19 @@ nav: - hello_nf-core/05_input_validation.md - hello_nf-core/next_steps.md - hello_nf-core/survey.md + - Hello Plugins: + - hello_plugins/index.md + - hello_plugins/00_orientation.md + - hello_plugins/01_plugin_basics.md + - hello_plugins/02_create_project.md + - hello_plugins/03_custom_functions.md + - hello_plugins/04_build_and_test.md + - hello_plugins/05_observers.md + - hello_plugins/06_configuration.md + - hello_plugins/07_distribution.md + - hello_plugins/summary.md + - hello_plugins/next_steps.md + - hello_plugins/survey.md - Nextflow for Science: - nf4_science/index.md - Genomics: @@ -211,6 +224,7 @@ plugins: - nextflow_run/00_orientation.md - hello_nextflow/00_orientation.md - hello_nf-core/00_orientation.md + - hello_plugins/00_orientation.md - nf4_science/genomics/00_orientation.md - nf4_science/rnaseq/00_orientation.md - side_quests/orientation.md @@ -223,6 +237,7 @@ plugins: - nextflow_run/*.md - hello_nextflow/*.md - hello_nf-core/*.md + - hello_plugins/*.md - nf4_science/genomics/*.md - nf4_science/rnaseq/*.md - nf4_science/imaging/*.md diff --git a/side-quests/plugin_development/greetings.csv b/side-quests/plugin_development/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/side-quests/plugin_development/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/side-quests/plugin_development/main.nf b/side-quests/plugin_development/main.nf new file mode 100644 index 0000000000..7eec641233 --- /dev/null +++ b/side-quests/plugin_development/main.nf @@ -0,0 +1,22 @@ +#!/usr/bin/env nextflow + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + """ + echo '$greeting' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Output: ${result.trim()}" } +} diff --git a/side-quests/plugin_development/nextflow.config b/side-quests/plugin_development/nextflow.config new file mode 100644 index 0000000000..ca4e92447f --- /dev/null +++ b/side-quests/plugin_development/nextflow.config @@ -0,0 +1 @@ +// Configuration for plugin development exercises diff --git a/side-quests/plugin_development/random_id_example.nf b/side-quests/plugin_development/random_id_example.nf new file mode 100644 index 0000000000..d10db9d402 --- /dev/null +++ b/side-quests/plugin_development/random_id_example.nf @@ -0,0 +1,15 @@ +#!/usr/bin/env nextflow + +// Local function - must be copied to every pipeline that needs it +def randomString(int length) { + def chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + def random = new Random() + return (1..length).collect { chars[random.nextInt(chars.length())] }.join() +} + +workflow { + // Generate random IDs for each sample + Channel.of('sample_A', 'sample_B', 'sample_C') + .map { sample -> "${sample}_${randomString(8)}" } + .view() +} diff --git a/side-quests/solutions/plugin_development/greetings.csv b/side-quests/solutions/plugin_development/greetings.csv new file mode 100644 index 0000000000..6157e1aac3 --- /dev/null +++ b/side-quests/solutions/plugin_development/greetings.csv @@ -0,0 +1,6 @@ +greeting,language +Hello,English +Bonjour,French +Holà,Spanish +Ciao,Italian +Hallo,German diff --git a/side-quests/solutions/plugin_development/main.nf b/side-quests/solutions/plugin_development/main.nf new file mode 100644 index 0000000000..881e4f8c3a --- /dev/null +++ b/side-quests/solutions/plugin_development/main.nf @@ -0,0 +1,34 @@ +#!/usr/bin/env nextflow + +// Import custom functions from our plugin +include { reverseGreeting } from 'plugin/nf-greeting' +include { decorateGreeting } from 'plugin/nf-greeting' + +params.input = 'greetings.csv' + +process SAY_HELLO { + input: + val greeting + output: + stdout + script: + // Use our custom plugin function to decorate the greeting + def decorated = decorateGreeting(greeting) + """ + echo '$decorated' + """ +} + +workflow { + greeting_ch = channel.fromPath(params.input) + .splitCsv(header: true) + .map { row -> row.greeting } + + // Demonstrate using reverseGreeting function + greeting_ch + .map { greeting -> reverseGreeting(greeting) } + .view { reversed -> "Reversed: $reversed" } + + SAY_HELLO(greeting_ch) + SAY_HELLO.out.view { result -> "Decorated with custom prefix: ${result.trim()}" } +} diff --git a/side-quests/solutions/plugin_development/nextflow.config b/side-quests/solutions/plugin_development/nextflow.config new file mode 100644 index 0000000000..ae48c3af9a --- /dev/null +++ b/side-quests/solutions/plugin_development/nextflow.config @@ -0,0 +1,9 @@ +plugins { + id 'nf-greeting@0.1.0' +} + +greeting { + taskCounter.verbose = false + prefix = '>>>' + suffix = '<<<' +} diff --git a/side-quests/solutions/plugin_development/nf-greeting/Makefile b/side-quests/solutions/plugin_development/nf-greeting/Makefile new file mode 100644 index 0000000000..2d01abd846 --- /dev/null +++ b/side-quests/solutions/plugin_development/nf-greeting/Makefile @@ -0,0 +1,21 @@ +# Build the plugin +assemble: + ./gradlew assemble + +clean: + rm -rf .nextflow* + rm -rf work + rm -rf build + ./gradlew clean + +# Run plugin unit tests +test: + ./gradlew test + +# Install the plugin into local nextflow plugins dir +install: + ./gradlew install + +# Publish the plugin +release: + ./gradlew releasePlugin diff --git a/side-quests/solutions/plugin_development/nf-greeting/build.gradle b/side-quests/solutions/plugin_development/nf-greeting/build.gradle new file mode 100644 index 0000000000..75fde1284f --- /dev/null +++ b/side-quests/solutions/plugin_development/nf-greeting/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'io.nextflow.nextflow-plugin' version '0.0.1-alpha4' +} + +version = '0.1.0' + +nextflowPlugin { + nextflowVersion = '24.10.0' + + provider = 'training' + className = 'training.plugin.NfGreetingPlugin' + extensionPoints = [ + 'training.plugin.NfGreetingExtension', + 'training.plugin.NfGreetingFactory' + ] + + publishing { + registry { + url = 'https://nf-plugins-registry.dev-tower.net/api' + authToken = project.findProperty('pluginRegistry.accessToken') + } + } +} diff --git a/side-quests/solutions/plugin_development/nf-greeting/settings.gradle b/side-quests/solutions/plugin_development/nf-greeting/settings.gradle new file mode 100644 index 0000000000..ad082127ff --- /dev/null +++ b/side-quests/solutions/plugin_development/nf-greeting/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'nf-greeting' diff --git a/side-quests/solutions/plugin_development/random_id_example.nf b/side-quests/solutions/plugin_development/random_id_example.nf new file mode 100644 index 0000000000..1b5ff4f5e0 --- /dev/null +++ b/side-quests/solutions/plugin_development/random_id_example.nf @@ -0,0 +1,10 @@ +#!/usr/bin/env nextflow + +include { randomString } from 'plugin/nf-hello' + +workflow { + // Generate random IDs for each sample + Channel.of('sample_A', 'sample_B', 'sample_C') + .map { sample -> "${sample}_${randomString(8)}" } + .view() +}