From 8a00096b1973a596758029c14e83ab8a64709bb7 Mon Sep 17 00:00:00 2001 From: CI Date: Thu, 12 Jun 2025 09:21:15 +0000 Subject: [PATCH] Build branch gunzip with version gunzip (add30ba) Build pipeline: viash-hub.toolbox.gunzip-zjfds Source commit: https://github.com/viash-hub/toolbox/commit/add30ba0f36bd8a8b07f0ba640707016d04e11b2 Source message: add gitignore --- .gitignore | 18 + CHANGELOG.md | 23 + CONTRIBUTING.md | 383 ++ LICENSE | 21 + README.md | 139 + README.qmd | 119 + _viash.yaml | 25 + docs/viash-hub.png | Bin 0 -> 150976 bytes main.nf | 3 + nextflow.config | 6 + src/bgzip/config.vsh.yaml | 90 + src/bgzip/help.txt | 22 + src/bgzip/script.sh | 21 + src/bgzip/test.sh | 19 + src/bgzip/test_data/script.sh | 10 + src/bgzip/test_data/test.vcf | 23 + src/yq/config.vsh.yaml | 87 + src/yq/help.txt | 72 + target/.build.yaml | 0 target/executable/bgzip/.config.vsh.yaml | 267 ++ target/executable/bgzip/bgzip | 1397 +++++++ target/executable/yq/.config.vsh.yaml | 297 ++ target/executable/yq/yq | 1260 +++++++ target/nextflow/bgzip/.config.vsh.yaml | 267 ++ target/nextflow/bgzip/main.nf | 3917 ++++++++++++++++++++ target/nextflow/bgzip/nextflow.config | 125 + target/nextflow/bgzip/nextflow_schema.json | 127 + target/nextflow/yq/.config.vsh.yaml | 297 ++ target/nextflow/yq/main.nf | 3914 +++++++++++++++++++ target/nextflow/yq/nextflow.config | 125 + target/nextflow/yq/nextflow_schema.json | 141 + 31 files changed, 13215 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.qmd create mode 100644 _viash.yaml create mode 100644 docs/viash-hub.png create mode 100644 main.nf create mode 100644 nextflow.config create mode 100644 src/bgzip/config.vsh.yaml create mode 100644 src/bgzip/help.txt create mode 100755 src/bgzip/script.sh create mode 100755 src/bgzip/test.sh create mode 100644 src/bgzip/test_data/script.sh create mode 100644 src/bgzip/test_data/test.vcf create mode 100644 src/yq/config.vsh.yaml create mode 100644 src/yq/help.txt create mode 100644 target/.build.yaml create mode 100644 target/executable/bgzip/.config.vsh.yaml create mode 100755 target/executable/bgzip/bgzip create mode 100644 target/executable/yq/.config.vsh.yaml create mode 100755 target/executable/yq/yq create mode 100644 target/nextflow/bgzip/.config.vsh.yaml create mode 100644 target/nextflow/bgzip/main.nf create mode 100644 target/nextflow/bgzip/nextflow.config create mode 100644 target/nextflow/bgzip/nextflow_schema.json create mode 100644 target/nextflow/yq/.config.vsh.yaml create mode 100644 target/nextflow/yq/main.nf create mode 100644 target/nextflow/yq/nextflow.config create mode 100644 target/nextflow/yq/nextflow_schema.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c4c611 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.DS_Store +*__pycache__ + +# IDE ignores +.idea/ +.vscode/ + +# R specific ignores +.Rhistory +.Rproj.user +*.Rproj + +# viash specific ignores +target/ + +# nextflow specific ignores +.nextflow* +work \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ebe5b02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# toolbox v0.1.1 + +## MINOR CHANGES + +* Updated the test CI (PR #6). + +* Bump viash to 0.9.0 (PR #10). + +* Bump viash to 0.9.4 (PR #13). + +* Update README (PR #13). + +# toolbox v0.1.0 + +## NEW FEATURES + +* `bgzip`: Add bgzip functionality to compress and decompress files (initial commit). + +* `yq`: A portable YAML, JSON, XML, CSV, TOML and properties processor (PR #1). + +## MINOR CHANGES + +* Use newer viash-actions updated for Viash 0.9 (PR #2). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7393bc7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,383 @@ + +# Contributing guidelines + +We encourage contributions from the community. To contribute: + +1. **Fork the Repository**: Start by forking this repository to your account. +2. **Develop Your Component**: Create your Viash component, ensuring it aligns with our best practices (detailed below). +3. **Submit a Pull Request**: After testing your component, submit a pull request for review. + +## Procedure of adding a component + +### Step 1: Find a component to contribute + +* Find a tool to contribute to this repo. + +* Check whether it is already in the [Project board](https://github.com/orgs/viash-hub/projects/1). + +* Check whether there is a corresponding [Snakemake wrapper](https://github.com/snakemake/snakemake-wrappers/blob/master/bio) or [nf-core module](https://github.com/nf-core/modules/tree/master/modules/nf-core) which we can use as inspiration. + +* Create an issue to show that you are working on this component. + + +### Step 2: Add config template + +Change all occurrences of `xxx` to the name of the component. + +Create a file at `src/xxx/config.vsh.yaml` with contents: + +```yaml +name: xxx +description: xxx +keywords: [tag1, tag2] +links: + homepage: yyy + documentation: yyy + issue_tracker: yyy + repository: yyy +references: + doi: 12345/12345678.yz +license: MIT/Apache-2.0/GPL-3.0/... +argument_groups: + - name: Inputs + arguments: <...> + - name: Outputs + arguments: <...> + - name: Arguments + arguments: <...> +resources: + - type: bash_script + path: script.sh +test_resources: + - type: bash_script + path: test.sh + - type: file + path: test_data +engines: + - <...> +runners: + - type: executable + - type: nextflow +``` + +### Step 3: Fill in the metadata + +Fill in the relevant metadata fields in the config. Here is an example of the metadata of an existing component. + +```yaml +functionality: + name: arriba + description: Detect gene fusions from RNA-Seq data + keywords: [Gene fusion, RNA-Seq] + links: + homepage: https://arriba.readthedocs.io/en/latest/ + documentation: https://arriba.readthedocs.io/en/latest/ + repository: https://github.com/suhrig/arriba + issue_tracker: https://github.com/suhrig/arriba/issues + references: + doi: 10.1101/gr.257246.119 + bibtex: | + @article{ + ... a bibtex entry in case the doi is not available ... + } + license: MIT +``` + +### Step 4: Find a suitable container + +Google `biocontainer ` and find the container that is most suitable. Typically the link will be `https://quay.io/repository/biocontainers/xxx?tab=tags`. + +If no such container is found, you can create a custom container in the next step. + + +### Step 5: Create help file + +To help develop the component, we store the `--help` output of the tool in a file at `src/xxx/help.txt`. + +````bash +cat < src/xxx/help.txt +```sh +xxx --help +``` +EOF + +docker run quay.io/biocontainers/xxx:tag xxx --help >> src/xxx/help.txt +```` + +Notes: + +* This help file has no functional purpose, but it is useful for the developer to see the help output of the tool. + +* Some tools might not have a `--help` argument but instead have a `-h` argument. For example, for `arriba`, the help message is obtained by running `arriba -h`: + + ```bash + docker run quay.io/biocontainers/arriba:2.4.0--h0033a41_2 arriba -h + ``` + + +### Step 6: Create or fetch test data + +To help develop the component, it's interesting to have some test data available. In most cases, we can use the test data from the Snakemake wrappers. + +To make sure we can reproduce the test data in the future, we store the command to fetch the test data in a file at `src/xxx/test_data/script.sh`. + +```bash +cat < src/xxx/test_data/script.sh + +# clone repo +if [ ! -d /tmp/snakemake-wrappers ]; then + git clone --depth 1 --single-branch --branch master https://github.com/snakemake/snakemake-wrappers /tmp/snakemake-wrappers +fi + +# copy test data +cp -r /tmp/snakemake-wrappers/bio/xxx/test/* src/xxx/test_data +EOF +``` + +The test data should be suitable for testing this component. Ensure that the test data is small enough: ideally <1KB, preferably <10KB, if need be <100KB. + +### Step 7: Add arguments for the input files + +By looking at the help file, we add the input arguments to the config file. Here is an example of the input arguments of an existing component. + +For instance, in the [arriba help file](src/arriba/help.txt), we see the following: + + Usage: arriba [-c Chimeric.out.sam] -x Aligned.out.bam \ + -g annotation.gtf -a assembly.fa [-b blacklists.tsv] [-k known_fusions.tsv] \ + [-t tags.tsv] [-p protein_domains.gff3] [-d structural_variants_from_WGS.tsv] \ + -o fusions.tsv [-O fusions.discarded.tsv] \ + [OPTIONS] + + -x FILE File in SAM/BAM/CRAM format with main alignments as generated by STAR + (Aligned.out.sam). Arriba extracts candidate reads from this file. + +Based on this information, we can add the following input arguments to the config file. + +```yaml +argument_groups: + - name: Inputs + arguments: + - name: --bam + alternatives: -x + type: file + description: | + File in SAM/BAM/CRAM format with main alignments as generated by STAR + (Aligned.out.sam). Arriba extracts candidate reads from this file. + required: true + example: Aligned.out.bam +``` + +Check the [documentation](https://viash.io/reference/config/functionality/arguments) for more information on the format of input arguments. + +Several notes: + +* Argument names should be formatted in `--snake_case`. This means arguments like `--foo-bar` should be formatted as `--foo_bar`, and short arguments like `-f` should receive a longer name like `--foo`. + +* Input arguments can have `multiple: true` to allow the user to specify multiple files. + + + +### Step 8: Add arguments for the output files + +By looking at the help file, we now also add output arguments to the config file. + +For example, in the [arriba help file](src/arriba/help.txt), we see the following: + + + Usage: arriba [-c Chimeric.out.sam] -x Aligned.out.bam \ + -g annotation.gtf -a assembly.fa [-b blacklists.tsv] [-k known_fusions.tsv] \ + [-t tags.tsv] [-p protein_domains.gff3] [-d structural_variants_from_WGS.tsv] \ + -o fusions.tsv [-O fusions.discarded.tsv] \ + [OPTIONS] + + -o FILE Output file with fusions that have passed all filters. + + -O FILE Output file with fusions that were discarded due to filtering. + +Based on this information, we can add the following output arguments to the config file. + +```yaml +argument_groups: + - name: Outputs + arguments: + - name: --fusions + alternatives: -o + type: file + direction: output + description: | + Output file with fusions that have passed all filters. + required: true + example: fusions.tsv + - name: --fusions_discarded + alternatives: -O + type: file + direction: output + description: | + Output file with fusions that were discarded due to filtering. + required: false + example: fusions.discarded.tsv +``` + +Note: + +* Preferably, these outputs should not be directores but files. For example, if a tool outputs a directory `foo/` containing files `foo/bar.txt` and `foo/baz.txt`, there should be two output arguments `--bar` and `--baz` (as opposed to one output argument which outputs the whole `foo/` directory). + +### Step 9: Add arguments for the other arguments + +Finally, add all other arguments to the config file. There are a few exceptions: + +* Arguments related to specifying CPU and memory requirements are handled separately and should not be added to the config file. + +* Arguments related to printing the information such as printing the version (`-v`, `--version`) or printing the help (`-h`, `--help`) should not be added to the config file. + + +### Step 10: Add a Docker engine + +To ensure reproducibility of components, we require that all components are run in a Docker container. + +```yaml +engines: + - type: docker + image: quay.io/biocontainers/xxx:0.1.0--py_0 +``` + +The container should have your tool installed, as well as `ps`. + +If you didn't find a suitable container in the previous step, you can create a custom container. For example: + +```yaml +engines: + - type: docker + image: python:3.10 + setup: + - type: python + packages: numpy +``` + +For more information on how to do this, see the [documentation](https://viash.io/guide/component/add-dependencies.html#steps-for-creating-a-custom-docker-platform). + +Here is a list of base containers we can recommend: + +* Bash: [`bash`](https://hub.docker.com/_/bash), [`ubuntu`](https://hub.docker.com/_/ubuntu) +* C#: [`ghcr.io/data-intuitive/dotnet-script`](https://github.com/data-intuitive/ghcr-dotnet-script/pkgs/container/dotnet-script) +* JavaScript: [`node`](https://hub.docker.com/_/node) +* Python: [`python`](https://hub.docker.com/_/python), [`nvcr.io/nvidia/pytorch`](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch) +* R: [`eddelbuettel/r2u`](https://hub.docker.com/r/eddelbuettel/r2u), [`rocker/tidyverse`](https://hub.docker.com/r/rocker/tidyverse) +* Scala: [`sbtscala/scala-sbt`](https://hub.docker.com/r/sbtscala/scala-sbt) + +### Step 11: Write a runner script + +Next, we need to write a runner script that runs the tool with the input arguments. Create a Bash script named `src/xxx/script.sh` which runs the tool with the input arguments. + +```bash +#!/bin/bash + +## VIASH START +## VIASH END + +xxx \ + --input "$par_input" \ + --output "$par_output" \ + $([ "$par_option" = "true" ] && echo "--option") +``` + +When building a Viash component, Viash will automatically replace the `## VIASH START` and `## VIASH END` lines (and anything in between) with environment variables based on the arguments specified in the config. + +As an example, this is what the Bash script for the `arriba` component looks like: + +```bash +#!/bin/bash + +## VIASH START +## VIASH END + +arriba \ + -x "$par_bam" \ + -a "$par_genome" \ + -g "$par_gene_annotation" \ + -o "$par_fusions" \ + ${par_known_fusions:+-k "${par_known_fusions}"} \ + ${par_blacklist:+-b "${par_blacklist}"} \ + ${par_structural_variants:+-d "${par_structural_variants}"} \ + $([ "$par_skip_duplicate_marking" = "true" ] && echo "-u") \ + $([ "$par_extra_information" = "true" ] && echo "-X") \ + $([ "$par_fill_gaps" = "true" ] && echo "-I") +``` + + +### Step 12: Create test script + + +If the unit test requires test resources, these should be provided in the `test_resources` section of the component. + +```yaml +functionality: + # ... + test_resources: + - type: bash_script + path: test.sh + - type: file + path: test_data +``` + +Create a test script at `src/xxx/test.sh` that runs the component with the test data. This script should run the component (available with `$meta_executable`) with the test data and check if the output is as expected. The script should exit with a non-zero exit code if the output is not as expected. For example: + +```bash +#!/bin/bash + +## VIASH START +## VIASH END + +echo "> Run xxx with test data" +"$meta_executable" \ + --input "$meta_resources_dir/test_data/input.txt" \ + --output "output.txt" \ + --option + +echo ">> Checking output" +[ ! -f "output.txt" ] && echo "Output file output.txt does not exist" && exit 1 +``` + + +For example, this is what the test script for the `arriba` component looks like: + +```bash +#!/bin/bash + +## VIASH START +## VIASH END + +echo "> Run arriba with blacklist" +"$meta_executable" \ + --bam "$meta_resources_dir/test_data/A.bam" \ + --genome "$meta_resources_dir/test_data/genome.fasta" \ + --gene_annotation "$meta_resources_dir/test_data/annotation.gtf" \ + --blacklist "$meta_resources_dir/test_data/blacklist.tsv" \ + --fusions "fusions.tsv" \ + --fusions_discarded "fusions_discarded.tsv" \ + --interesting_contigs "1,2" + +echo ">> Checking output" +[ ! -f "fusions.tsv" ] && echo "Output file fusions.tsv does not exist" && exit 1 +[ ! -f "fusions_discarded.tsv" ] && echo "Output file fusions_discarded.tsv does not exist" && exit 1 + +echo ">> Check if output is empty" +[ ! -s "fusions.tsv" ] && echo "Output file fusions.tsv is empty" && exit 1 +[ ! -s "fusions_discarded.tsv" ] && echo "Output file fusions_discarded.tsv is empty" && exit 1 +``` + +### Step 12: Create a `/var/software_versions.txt` file + +For the sake of transparency and reproducibility, we require that the versions of the software used in the component are documented. + +For now, this is managed by creating a file `/var/software_versions.txt` in the `setup` section of the Docker engine. + +```yaml +engines: + - type: docker + image: quay.io/biocontainers/xxx:0.1.0--py_0 + setup: + - type: docker + run: | + echo "xxx: \"0.1.0\"" > /var/software_versions.txt +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..968d811 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Data Intuitive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ffb898 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ + + +# 🛠📦 toolbox + +[![ViashHub](https://img.shields.io/badge/ViashHub-toolbox-7a4baa.svg)](https://www.viash-hub.com/packages/toolbox) +[![GitHub](https://img.shields.io/badge/GitHub-viash--hub%2Ftoolbox-blue.svg)](https://github.com/viash-hub/toolbox) +[![GitHub +License](https://img.shields.io/github/license/viash-hub/toolbox.svg)](https://github.com/viash-hub/toolbox/blob/main/LICENSE) +[![GitHub +Issues](https://img.shields.io/github/issues/viash-hub/toolbox.svg)](https://github.com/viash-hub/toolbox/issues) +[![Viash +version](https://img.shields.io/badge/Viash-v0.9.4-blue.svg)](https://viash.io) + +A collection of curated command-line tools for general IT tasks, built +with Viash. + +## Introduction + +`toolbox` provides a versatile suite of IT components, following the +robust Viash (https://viash.io) framework. This package focuses on +delivering reliable, standalone tools that can be easily integrated into +larger computational workflows. + +The core philosophy emphasizes **reusability**, **reproducibility**, and +adherence to **best practices** in component creation. Key features of +`toolbox` components include: + +- **Standalone & Nextflow Ready:** Execute components directly from the + command line or seamlessly incorporate them into Nextflow workflows. +- **High Quality Standards:** + - Comprehensive documentation for each component and its parameters. + - Full exposure of the underlying tool’s arguments for maximum + flexibility. + - Containerized (Docker) to ensure consistent environments and manage + dependencies, leading to enhanced reproducibility. + - Unit tested to verify functionality and ensure reliability. + +## Example Usage + +Viash components in toolbox can be run in various ways: + +``` mermaid lang="mermaid" +flowchart TD + A[toolbox v0.1.0] --> B(Viash Hub Launch) + A --> C(Viash CLI) + A --> D(Nextflow CLI) + A --> E(Seqera Cloud) + A --> F(As a dependency) +``` + +### 1. Via the Viash Hub Launch interface + +You can run this component directly from the Viash Hub [Launch +interface](https://www.viash-hub.com/launch?package=toolbox&version=v0.1.0&component=yq&runner=Executable). + +![](docs/viash-hub.png) + +### 2. Via the Viash CLI + +You can run this component directly from the command line using the +Viash CLI. + +``` bash +viash run vsh://toolbox@v0.1.0/yq -- --help + +viash run vsh://toolbox@v0.1.0/yq -- \ + --input path/to/input.yaml \ + --output output.yaml +``` + +This will run the component with the specified input files and output +the results to the specified output file. + +### 3. Via the Nextflow CLI or Seqera Cloud + +You can run this component as a Nextflow pipeline. + +``` bash +nextflow run https://packages.viash-hub.com/vsh/toolbox \ + -revision v0.1.0 \ + -main-script target/nextflow/yq/main.nf \ + -latest -resume \ + -profile docker \ + --input path/to/input.yaml \ + --publish_dir path/to/output +``` + +**Note:** Make sure that the [Nextflow +SCM](https://www.nextflow.io/docs/latest/git.html#git-configuration) is +set up properly. You can do this by adding the following lines to your +`~/.nextflow/scm` file: + +``` groovy +providers.vsh.platform = 'gitlab' +providers.vsh.server = 'https://packages.viash-hub.com' +``` + +**Tip:** This will also work with Seqera Cloud or other +Nextflow-compatible platforms. + +### 4. As a dependency + +In your Viash config file (`config.vsh.yaml`), you can add this +component as a dependency: + +``` yaml +dependencies: + - name: yq + repository: vsh://toolbox@v0.1.0 +``` + +**Tip:** See the [Viash +documentation](https://viash.io/guide/nextflow_vdsl3/create-a-pipeline.html#pipeline-as-a-component) +for more details on how to use Viash components as a dependency in your +own Nextflow workflows. + +## Contributing + +Contributions are welcome! We aim to build a comprehensive collection of +high-quality bioinformatics components. If you’d like to contribute, +please follow these general steps: + +1. Find a component to contribute +2. Add config template +3. Fill in the metadata +4. Find a suitable container +5. Create help file +6. Create or fetch test data +7. Add arguments for the input files +8. Add arguments for the output files +9. Add arguments for the other arguments +10. Add a Docker engine +11. Write a runner script +12. Create test script +13. Create a `/var/software_versions.txt` file + +See the +[CONTRIBUTING](https://github.com/viash-hub/toolbox/blob/main/CONTRIBUTING.md) +file for more details. diff --git a/README.qmd b/README.qmd new file mode 100644 index 0000000..23e0f1a --- /dev/null +++ b/README.qmd @@ -0,0 +1,119 @@ +--- +format: gfm +--- +```{r setup, include=FALSE} +package <- yaml::read_yaml("_viash.yaml") +license <- paste0(package$links$repository, "/blob/main/LICENSE") +contributing <- paste0(package$links$repository, "/blob/main/CONTRIBUTING.md") + +pkg <- package$name +ver <- if (!is.null(package$version)) package$version else "v0.1.0" +comp <- "yq" +``` +# 🛠📦 `r pkg` + +[![ViashHub](https://img.shields.io/badge/ViashHub-`r pkg`-7a4baa.svg)](https://www.viash-hub.com/packages/`r pkg`) +[![GitHub](https://img.shields.io/badge/GitHub-viash--hub%2F`r pkg`-blue.svg)](`r package$links$repository`) +[![GitHub License](https://img.shields.io/github/license/viash-hub/`r pkg`.svg)](`r license`) +[![GitHub Issues](https://img.shields.io/github/issues/viash-hub/`r pkg`.svg)](`r package$links$issue_tracker`) +[![Viash version](https://img.shields.io/badge/Viash-v`r gsub("-", "--", package$viash_version)`-blue.svg)](https://viash.io) + +`r package$summary` + +## Introduction + +`r package$description` + +## Example Usage + +Viash components in `r pkg` can be run in various ways: + +```{r mmd, echo=FALSE, results='asis'} +cat( + "```mermaid\n", + "flowchart TD\n", + " A[", pkg, " ", ver, "] --> B(Viash Hub Launch)\n", + " A --> C(Viash CLI)\n", + " A --> D(Nextflow CLI)\n", + " A --> E(Seqera Cloud)\n", + " A --> F(As a dependency)\n", + "```\n", + sep = "" +) +``` + +### 1. Via the Viash Hub Launch interface + +You can run this component directly from the Viash Hub [Launch interface](https://www.viash-hub.com/launch?package=`r pkg`&version=`r ver`&component=`r comp`&runner=Executable). + +![](docs/viash-hub.png) + +### 2. Via the Viash CLI + +You can run this component directly from the command line using the Viash CLI. + +```bash +viash run vsh://`r pkg`@`r ver`/`r comp` -- --help + +viash run vsh://`r pkg`@`r ver`/`r comp` -- \ + --input path/to/input.yaml \ + --output output.yaml +``` + +This will run the component with the specified input files and output the results to the specified output file. + +### 3. Via the Nextflow CLI or Seqera Cloud + +You can run this component as a Nextflow pipeline. + +```bash +nextflow run https://packages.viash-hub.com/vsh/`r pkg` \ + -revision `r ver` \ + -main-script target/nextflow/`r comp`/main.nf \ + -latest -resume \ + -profile docker \ + --input path/to/input.yaml \ + --publish_dir path/to/output +``` + +**Note:** Make sure that the [Nextflow SCM](https://www.nextflow.io/docs/latest/git.html#git-configuration) is set up properly. You can do this by adding the following lines to your `~/.nextflow/scm` file: + +```groovy +providers.vsh.platform = 'gitlab' +providers.vsh.server = 'https://packages.viash-hub.com' +``` + +**Tip:** This will also work with Seqera Cloud or other Nextflow-compatible platforms. + +### 4. As a dependency + +In your Viash config file (`config.vsh.yaml`), you can add this component as a dependency: + +```yaml +dependencies: + - name: `r comp` + repository: vsh://`r pkg`@`r ver` +``` + +**Tip:** See the [Viash documentation](https://viash.io/guide/nextflow_vdsl3/create-a-pipeline.html#pipeline-as-a-component) for more details on how to use Viash components as a dependency in your own Nextflow workflows. + +## Contributing + +Contributions are welcome! We aim to build a comprehensive collection of high-quality bioinformatics components. If you'd like to contribute, please follow these general steps: + + +```{r echo=FALSE} +lines <- readr::read_lines("CONTRIBUTING.md") + +index_start <- grep("^### Step [0-9]*:", lines) + +index_end <- c(index_start[-1] - 1, length(lines)) + +name <- gsub("^### Step [0-9]*: *", "", lines[index_start]) + +knitr::asis_output( + paste(paste0(" 1. ", name, "\n"), collapse = "") +) +``` + +See the [CONTRIBUTING](`r contributing`) file for more details. diff --git a/_viash.yaml b/_viash.yaml new file mode 100644 index 0000000..b1c2c4a --- /dev/null +++ b/_viash.yaml @@ -0,0 +1,25 @@ +name: toolbox +summary: | + A collection of curated command-line tools for general IT tasks, built with Viash. +description: | + `toolbox` provides a versatile suite of IT components, following the robust Viash (https://viash.io) framework. + This package focuses on delivering reliable, standalone tools that can be easily integrated into larger computational workflows. + + The core philosophy emphasizes **reusability**, **reproducibility**, and adherence to **best practices** in component creation. Key features of `toolbox` components include: + + * **Standalone & Nextflow Ready:** Execute components directly from the command line or seamlessly incorporate them into Nextflow workflows. + * **High Quality Standards:** + * Comprehensive documentation for each component and its parameters. + * Full exposure of the underlying tool's arguments for maximum flexibility. + * Containerized (Docker) to ensure consistent environments and manage dependencies, leading to enhanced reproducibility. + * Unit tested to verify functionality and ensure reliability. +license: MIT +keywords: [toolbox, command-line, tools] +links: + issue_tracker: https://github.com/viash-hub/toolbox/issues + repository: https://github.com/viash-hub/toolbox + +viash_version: 0.9.4 + +config_mods: | + .requirements.commands := ['ps'] diff --git a/docs/viash-hub.png b/docs/viash-hub.png new file mode 100644 index 0000000000000000000000000000000000000000..01515224938270b8efbf9f3ddf9bc7c44e3b924d GIT binary patch literal 150976 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Õ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
  • Bts;J8G#jB-%96~DSV7E2 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 | sed 's/Version:\s\(.*\)/bgzip: "\1"/' > /var/software_versions.txt +runners: + - type: executable + - type: nextflow \ No newline at end of file diff --git a/src/bgzip/help.txt b/src/bgzip/help.txt new file mode 100644 index 0000000..d4012ef --- /dev/null +++ b/src/bgzip/help.txt @@ -0,0 +1,22 @@ +```bash +bgzip -h +``` + +Version: 1.19 +Usage: bgzip [OPTIONS] [FILE] ... +Options: + -b, --offset INT decompress at virtual file pointer (0-based uncompressed offset) + -c, --stdout write on standard output, keep original files unchanged + -d, --decompress decompress + -f, --force overwrite files without asking + -g, --rebgzip use an index file to bgzip a file + -h, --help give this help + -i, --index compress and create BGZF index + -I, --index-name FILE name of BGZF index file [file.gz.gzi] + -k, --keep don't delete input files during operation + -l, --compress-level INT Compression level to use when compressing; 0 to 9, or -1 for default [-1] + -r, --reindex (re)index compressed file + -s, --size INT decompress INT bytes (uncompressed size) + -t, --test test integrity of compressed file + --binary Don't align blocks with text lines + -@, --threads INT number of compression threads to use [1] diff --git a/src/bgzip/script.sh b/src/bgzip/script.sh new file mode 100755 index 0000000..5021362 --- /dev/null +++ b/src/bgzip/script.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +[[ "$par_decompress" == "false" ]] && unset par_decompress +[[ "$par_rebgzip" == "false" ]] && unset par_rebgzip +[[ "$par_index" == "false" ]] && unset par_index +[[ "$par_reindex" == "false" ]] && unset par_reindex +[[ "$par_test" == "false" ]] && unset par_test +[[ "$par_binary" == "false" ]] && unset par_binary +bgzip -c \ + ${meta_cpus:+--threads "${meta_cpus}"} \ + ${par_offset:+-b "${par_offset}"} \ + ${par_decompress:+-d} \ + ${par_rebgzip:+-g} \ + ${par_index:+-i} \ + ${par_index_name:+-I "${par_index_name}"} \ + ${par_compress_level:+-l "${par_compress_level}"} \ + ${par_reindex:+-r} \ + ${par_size:+-s "${par_size}"} \ + ${par_test:+-t} \ + ${par_binary:+--binary} \ + "$par_input" > "$par_output" diff --git a/src/bgzip/test.sh b/src/bgzip/test.sh new file mode 100755 index 0000000..3e2a9b9 --- /dev/null +++ b/src/bgzip/test.sh @@ -0,0 +1,19 @@ +set -e + +"$meta_executable" --input "$meta_resources_dir/test_data/test.vcf" --output "test.vcf.gz" + +echo ">> Checking output of compressing" +[ ! -f "test.vcf.gz" ] && echo "Output file test.vcf.gz does not exist" && exit 1 + +"$meta_executable" --input "test.vcf.gz" --output "test.vcf" --decompress + +echo ">> Checking output of decompressing" +[ ! -f "test.vcf" ] && echo "Output file test.vcf does not exist" && exit 1 + +echo ">> Checking original and decompressed files are the same" +set +e +cmp --silent -- "$meta_resources_dir/test_data/test.vcf" "test.vcf" +[ $? -ne 0 ] && echo "files are different" && exit 1 +set -e + +echo "> Test successful" diff --git a/src/bgzip/test_data/script.sh b/src/bgzip/test_data/script.sh new file mode 100644 index 0000000..c911447 --- /dev/null +++ b/src/bgzip/test_data/script.sh @@ -0,0 +1,10 @@ +# bgzip test data + +# Test data was obtained from https://github.com/snakemake/snakemake-wrappers/tree/master/bio/bgzip/test. + +if [ ! -d /tmp/snakemake-wrappers ]; then + git clone --depth 1 --single-branch --branch master https://github.com/snakemake/snakemake-wrappers /tmp/snakemake-wrappers +fi + +cp -r /tmp/snakemake-wrappers/bio/bgzip/test/* src/bgzip/test_data + diff --git a/src/bgzip/test_data/test.vcf b/src/bgzip/test_data/test.vcf new file mode 100644 index 0000000..11b5400 --- /dev/null +++ b/src/bgzip/test_data/test.vcf @@ -0,0 +1,23 @@ +##fileformat=VCFv4.0 +##fileDate=20090805 +##source=https://www.internationalgenome.org/wiki/Analysis/vcf4.0/ +##reference=1000GenomesPilot-NCBI36 +##phasing=partial +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##FILTER= +##FILTER= +##FORMAT= +##FORMAT= +##FORMAT= +##FORMAT= +#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NA00001 NA00002 NA00003 +20 14370 rs6054257 G A 29 PASS NS=3;DP=14;AF=0.5;DB;H2 GT:GQ:DP:HQ 0|0:48:1:51,51 1|0:48:8:51,51 1/1:43:5:.,. +20 17330 . T A 3 q10 NS=3;DP=11;AF=0.017 GT:GQ:DP:HQ 0|0:49:3:58,50 0|1:3:5:65,3 0/0:41:3 +20 1110696 rs6040355 A G,T 67 PASS NS=2;DP=10;AF=0.333,0.667;AA=T;DB GT:GQ:DP:HQ 1|2:21:6:23,27 2|1:2:0:18,2 2/2:35:4 +20 1230237 . T . 47 PASS NS=3;DP=13;AA=T GT:GQ:DP:HQ 0|0:54:7:56,60 0|0:48:4:51,51 0/0:61:2 +20 1234567 microsat1 GTCT G,GTACT 50 PASS NS=3;DP=9;AA=G GT:GQ:DP 0/1:35:4 0/2:17:2 1/1:40:3 diff --git a/src/yq/config.vsh.yaml b/src/yq/config.vsh.yaml new file mode 100644 index 0000000..932fae2 --- /dev/null +++ b/src/yq/config.vsh.yaml @@ -0,0 +1,87 @@ +name: yq +description: A portable YAML, JSON, XML, CSV, TOML and properties processor +keywords: [ yaml, json, xml, csv, toml, properties ] +links: + homepage: https://mikefarah.gitbook.io/yq + documentation: https://mikefarah.gitbook.io/yq/ + repository: https://github.com/mikefarah/yq +license: MIT +requirements: + commands: [ yq ] +argument_groups: + - name: Inputs + arguments: + - name: --input + type: file + direction: input + description: files to be processed + required: true + example: input.yaml + - name: Outputs + arguments: + - name: --output + type: file + direction: output + description: output file + required: true + example: output.yaml + - name: Arguments + arguments: + - name: --eval + type: string + description: expression to evaluate + required: true + example: '.name = "foo"' + - name: --indent + type: integer + description: sets indent level for output (default 2) + alternatives: -I + - name: --input_format + type: string + description: 'parse format for input. (default "auto")' + alternatives: -p + choices: [ auto, a, yaml, "y", json, j, props, p, csv, c, tsv, t, xml, x, base64, uri, toml, shell, s, lua, l ] + - name: --output_format + type: string + description: 'output format type. (default "auto")' + alternatives: -o + choices: [ auto, a, yaml, "y", json, j, props, p, csv, c, tsv, t, xml, x, base64, uri, toml, shell, s, lua, l ] + - name: --pretty_print + type: boolean_true + description: pretty print, shorthand for '... style = ""' + alternatives: -P + +resources: + - type: bash_script + text: | + #!/bin/sh + [[ "$par_pretty_print" == "false" ]] && unset par_pretty_print + yq eval \ + ${par_indent:+-I "${par_indent}"} \ + ${par_input_format:+-p "${par_input_format}"} \ + ${par_output_format:+-o "${par_output_format}"} \ + ${par_pretty_print:+-P} \ + --expression "$par_eval" \ + --no-colors \ + "$par_input" > "$par_output" +test_resources: + - type: bash_script + text: | + set -e + echo "name: 'bar'" > test.yaml + "$meta_executable" --input test.yaml --output output.yaml --eval '.name = "foo"' + "$meta_executable" --input output.yaml --output output2.yaml --eval '.name' + grep "^foo$" output2.yaml + +engines: + - type: docker + image: alpine:latest + setup: + - type: apk + packages: [bash, yq-go] + - type: docker + run: | + /usr/bin/yq --version | sed 's/.*version\sv\(.*\)/yq: "\1"/' > /var/software_versions.txt +runners: + - type: executable + - type: nextflow diff --git a/src/yq/help.txt b/src/yq/help.txt new file mode 100644 index 0000000..a10530d --- /dev/null +++ b/src/yq/help.txt @@ -0,0 +1,72 @@ +yq is a portable command-line data file processor (https://github.com/mikefarah/yq/) +See https://mikefarah.gitbook.io/yq/ for detailed documentation and examples. + +Usage: + yq [flags] + yq [command] + +Examples: + +# yq tries to auto-detect the file format based off the extension, and defaults to YAML if it's unknown (or piping through STDIN) +# Use the '-p/--input-format' flag to specify a format type. +cat file.xml | yq -p xml + +# read the "stuff" node from "myfile.yml" +yq '.stuff' < myfile.yml + +# update myfile.yml in place +yq -i '.stuff = "foo"' myfile.yml + +# print contents of sample.json as idiomatic YAML +yq -P -oy sample.json + + +Available Commands: + completion Generate the autocompletion script for the specified shell + eval (default) Apply the expression to each document in each yaml file in sequence + eval-all Loads _all_ yaml documents of _all_ yaml files and runs expression once + help Help about any command + +Flags: + -C, --colors force print with colors + --csv-auto-parse parse CSV YAML/JSON values (default true) + --csv-separator char CSV Separator character (default ,) + -e, --exit-status set exit status if there are no matches or null or false is returned + --expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file. + --from-file string Load expression from specified file. + -f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact + --header-preprocess Slurp any header comments and separators before processing expression. (default true) + -h, --help help for yq + -I, --indent int sets indent level for output (default 2) + -i, --inplace update the file in place of first file given. + -p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l] parse format for input. (default "auto") + --lua-globals output keys as top-level global variables + --lua-prefix string prefix (default "return ") + --lua-suffix string suffix (default ";\n") + --lua-unquoted output unquoted string keys (e.g. {foo="bar"}) + -M, --no-colors force print with no colors + -N, --no-doc Don't print document separators (---) + -0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char. + -n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch. + -o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l] output format type. (default "auto") + -P, --prettyPrint pretty print, shorthand for '... style = ""' + --properties-array-brackets use [x] in array paths (e.g. for SpringBoot) + --properties-separator string separator to use between keys and values (default " = ") + -s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. + --split-exp-file string Use a file to specify the split-exp expression. + --string-interpolation Toggles strings interpolation of \(exp) (default true) + --tsv-auto-parse parse TSV YAML/JSON values (default true) + -r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true) + -v, --verbose verbose mode + -V, --version Print version information and quit + --xml-attribute-prefix string prefix for xml attributes (default "+@") + --xml-content-name string name for xml content (if no attribute name is present). (default "+content") + --xml-directive-name string name for xml directives (e.g. ) (default "+directive") + --xml-keep-namespace enables keeping namespace after parsing attributes (default true) + --xml-proc-inst-prefix string prefix for xml processing instructions (e.g. ) (default "+p_") + --xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true) + --xml-skip-directives skip over directives (e.g. ) + --xml-skip-proc-inst skip over process instructions (e.g. ) + --xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details. + +Use "yq [command] --help" for more information about a command. diff --git a/target/.build.yaml b/target/.build.yaml new file mode 100644 index 0000000..e69de29 diff --git a/target/executable/bgzip/.config.vsh.yaml b/target/executable/bgzip/.config.vsh.yaml new file mode 100644 index 0000000..a0bcc52 --- /dev/null +++ b/target/executable/bgzip/.config.vsh.yaml @@ -0,0 +1,267 @@ +name: "bgzip" +version: "gunzip" +argument_groups: +- name: "Inputs" + arguments: + - type: "file" + name: "--input" + description: "file to be compressed or decompressed" + info: null + must_exist: true + create_parent: true + required: true + direction: "input" + multiple: false + multiple_sep: ";" +- name: "Outputs" + arguments: + - type: "file" + name: "--output" + description: "compressed or decompressed output" + info: null + must_exist: true + create_parent: true + required: true + direction: "output" + multiple: false + multiple_sep: ";" + - type: "file" + name: "--index_name" + alternatives: + - "-I" + description: "name of BGZF index file [file.gz.gzi]" + info: null + must_exist: true + create_parent: true + required: false + direction: "output" + multiple: false + multiple_sep: ";" +- name: "Arguments" + arguments: + - type: "integer" + name: "--offset" + alternatives: + - "-b" + description: "decompress at virtual file pointer (0-based uncompressed offset)" + info: null + required: false + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--decompress" + alternatives: + - "-d" + description: "decompress the input file" + info: null + direction: "input" + - type: "boolean_true" + name: "--rebgzip" + alternatives: + - "-g" + description: "use an index file to bgzip a file" + info: null + direction: "input" + - type: "boolean_true" + name: "--index" + alternatives: + - "-i" + description: "compress and create BGZF index" + info: null + direction: "input" + - type: "integer" + name: "--compress_level" + alternatives: + - "-l" + description: "compression level to use when compressing; 0 to 9, or -1 for default\ + \ [-1]" + info: null + required: false + min: -1 + max: 9 + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--reindex" + alternatives: + - "-r" + description: "(re)index the output file" + info: null + direction: "input" + - type: "integer" + name: "--size" + alternatives: + - "-s" + description: "decompress INT bytes (uncompressed size)" + info: null + required: false + min: 0 + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--test" + alternatives: + - "-t" + description: "test integrity of compressed file" + info: null + direction: "input" + - type: "boolean_true" + name: "--binary" + description: "Don't align blocks with text lines" + info: null + direction: "input" +resources: +- type: "bash_script" + path: "script.sh" + is_executable: true +description: "Block compression/decompression utility" +test_resources: +- type: "bash_script" + path: "test.sh" + is_executable: true +- type: "file" + path: "test_data" +info: null +status: "enabled" +scope: + image: "public" + target: "public" +requirements: + commands: + - "ps" +license: "MIT" +references: + doi: + - "10.1093/gigascience/giab007" +links: + repository: "https://github.com/samtools/htslib" + homepage: "https://www.htslib.org/" + documentation: "https://www.htslib.org/doc/bgzip.html" +runners: +- type: "executable" + id: "executable" + docker_setup_strategy: "ifneedbepullelsecachedbuild" +- type: "nextflow" + id: "nextflow" + directives: + tag: "$id" + auto: + simplifyInput: true + simplifyOutput: false + transcript: false + publish: false + config: + labels: + mem1gb: "memory = 1000000000.B" + mem2gb: "memory = 2000000000.B" + mem5gb: "memory = 5000000000.B" + mem10gb: "memory = 10000000000.B" + mem20gb: "memory = 20000000000.B" + mem50gb: "memory = 50000000000.B" + mem100gb: "memory = 100000000000.B" + mem200gb: "memory = 200000000000.B" + mem500gb: "memory = 500000000000.B" + mem1tb: "memory = 1000000000000.B" + mem2tb: "memory = 2000000000000.B" + mem5tb: "memory = 5000000000000.B" + mem10tb: "memory = 10000000000000.B" + mem20tb: "memory = 20000000000000.B" + mem50tb: "memory = 50000000000000.B" + mem100tb: "memory = 100000000000000.B" + mem200tb: "memory = 200000000000000.B" + mem500tb: "memory = 500000000000000.B" + mem1gib: "memory = 1073741824.B" + mem2gib: "memory = 2147483648.B" + mem4gib: "memory = 4294967296.B" + mem8gib: "memory = 8589934592.B" + mem16gib: "memory = 17179869184.B" + mem32gib: "memory = 34359738368.B" + mem64gib: "memory = 68719476736.B" + mem128gib: "memory = 137438953472.B" + mem256gib: "memory = 274877906944.B" + mem512gib: "memory = 549755813888.B" + mem1tib: "memory = 1099511627776.B" + mem2tib: "memory = 2199023255552.B" + mem4tib: "memory = 4398046511104.B" + mem8tib: "memory = 8796093022208.B" + mem16tib: "memory = 17592186044416.B" + mem32tib: "memory = 35184372088832.B" + mem64tib: "memory = 70368744177664.B" + mem128tib: "memory = 140737488355328.B" + mem256tib: "memory = 281474976710656.B" + mem512tib: "memory = 562949953421312.B" + cpu1: "cpus = 1" + cpu2: "cpus = 2" + cpu5: "cpus = 5" + cpu10: "cpus = 10" + cpu20: "cpus = 20" + cpu50: "cpus = 50" + cpu100: "cpus = 100" + cpu200: "cpus = 200" + cpu500: "cpus = 500" + cpu1000: "cpus = 1000" + debug: false + container: "docker" +engines: +- type: "docker" + id: "docker" + image: "quay.io/biocontainers/htslib:1.19--h81da01d_0" + target_registry: "images.viash-hub.com" + target_tag: "gunzip" + namespace_separator: "/" + setup: + - type: "docker" + run: + - "bgzip -h | grep 'Version:' 2>&1 | sed 's/Version:\\s\\(.*\\)/bgzip: \"\\1\"\ + /' > /var/software_versions.txt\n" + entrypoint: [] + cmd: null +- type: "native" + id: "native" +build_info: + config: "src/bgzip/config.vsh.yaml" + runner: "executable" + engine: "docker|native" + output: "target/executable/bgzip" + executable: "target/executable/bgzip/bgzip" + viash_version: "0.9.4" + git_commit: "add30ba0f36bd8a8b07f0ba640707016d04e11b2" + git_remote: "https://github.com/viash-hub/toolbox" +package_config: + name: "toolbox" + version: "gunzip" + summary: "A collection of curated command-line tools for general IT tasks, built\ + \ with Viash.\n" + description: "`toolbox` provides a versatile suite of IT components, following the\ + \ robust Viash (https://viash.io) framework.\nThis package focuses on delivering\ + \ reliable, standalone tools that can be easily integrated into larger computational\ + \ workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**,\ + \ and adherence to **best practices** in component creation. Key features of `toolbox`\ + \ components include:\n\n* **Standalone & Nextflow Ready:** Execute components\ + \ directly from the command line or seamlessly incorporate them into Nextflow\ + \ workflows.\n* **High Quality Standards:**\n * Comprehensive documentation\ + \ for each component and its parameters.\n * Full exposure of the underlying\ + \ tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure\ + \ consistent environments and manage dependencies, leading to enhanced reproducibility.\n\ + \ * Unit tested to verify functionality and ensure reliability.\n" + info: null + viash_version: "0.9.4" + source: "src" + target: "target" + config_mods: + - ".requirements.commands := ['ps']\n" + - ".engines += { type: \"native\" }" + - ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'" + - ".engines[.type == 'docker'].target_tag := 'gunzip'" + keywords: + - "toolbox" + - "command-line" + - "tools" + license: "MIT" + organization: "vsh" + links: + repository: "https://github.com/viash-hub/toolbox" + issue_tracker: "https://github.com/viash-hub/toolbox/issues" diff --git a/target/executable/bgzip/bgzip b/target/executable/bgzip/bgzip new file mode 100755 index 0000000..e1357b0 --- /dev/null +++ b/target/executable/bgzip/bgzip @@ -0,0 +1,1397 @@ +#!/usr/bin/env bash + +# bgzip gunzip +# +# This wrapper script is auto-generated by viash 0.9.4 and is thus a derivative +# work thereof. This software comes with ABSOLUTELY NO WARRANTY from Data +# Intuitive. +# +# The component may contain files which fall under a different license. The +# authors of this component should specify the license in the header of such +# files, or include a separate license file detailing the licenses of all included +# files. + +set -e + +if [ -z "$VIASH_TEMP" ]; then + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TEMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TMP} + VIASH_TEMP=${VIASH_TEMP:-$TMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$TMP} + VIASH_TEMP=${VIASH_TEMP:-$TEMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$TEMP} + VIASH_TEMP=${VIASH_TEMP:-/tmp} +fi + +# define helper functions +# ViashQuote: put quotes around non flag values +# $1 : unquoted string +# return : possibly quoted string +# examples: +# ViashQuote --foo # returns --foo +# ViashQuote bar # returns 'bar' +# Viashquote --foo=bar # returns --foo='bar' +function ViashQuote { + if [[ "$1" =~ ^-+[a-zA-Z0-9_\-]+=.+$ ]]; then + echo "$1" | sed "s#=\(.*\)#='\1'#" + elif [[ "$1" =~ ^-+[a-zA-Z0-9_\-]+$ ]]; then + echo "$1" + else + echo "'$1'" + fi +} +# ViashRemoveFlags: Remove leading flag +# $1 : string with a possible leading flag +# return : string without possible leading flag +# examples: +# ViashRemoveFlags --foo=bar # returns bar +function ViashRemoveFlags { + echo "$1" | sed 's/^--*[a-zA-Z0-9_\-]*=//' +} +# ViashSourceDir: return the path of a bash file, following symlinks +# usage : ViashSourceDir ${BASH_SOURCE[0]} +# $1 : Should always be set to ${BASH_SOURCE[0]} +# returns : The absolute path of the bash file +function ViashSourceDir { + local source="$1" + while [ -h "$source" ]; do + local dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$dir/$source" + done + cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd +} +# ViashFindTargetDir: return the path of the '.build.yaml' file, following symlinks +# usage : ViashFindTargetDir 'ScriptPath' +# $1 : The location from where to start the upward search +# returns : The absolute path of the '.build.yaml' file +function ViashFindTargetDir { + local source="$1" + while [[ "$source" != "" && ! -e "$source/.build.yaml" ]]; do + source=${source%/*} + done + echo $source +} +# see https://en.wikipedia.org/wiki/Syslog#Severity_level +VIASH_LOGCODE_EMERGENCY=0 +VIASH_LOGCODE_ALERT=1 +VIASH_LOGCODE_CRITICAL=2 +VIASH_LOGCODE_ERROR=3 +VIASH_LOGCODE_WARNING=4 +VIASH_LOGCODE_NOTICE=5 +VIASH_LOGCODE_INFO=6 +VIASH_LOGCODE_DEBUG=7 +VIASH_VERBOSITY=$VIASH_LOGCODE_NOTICE + +# ViashLog: Log events depending on the verbosity level +# usage: ViashLog 1 alert Oh no something went wrong! +# $1: required verbosity level +# $2: display tag +# $3+: messages to display +# stdout: Your input, prepended by '[$2] '. +function ViashLog { + local required_level="$1" + local display_tag="$2" + shift 2 + if [ $VIASH_VERBOSITY -ge $required_level ]; then + >&2 echo "[$display_tag]" "$@" + fi +} + +# ViashEmergency: log events when the system is unstable +# usage: ViashEmergency Oh no something went wrong. +# stdout: Your input, prepended by '[emergency] '. +function ViashEmergency { + ViashLog $VIASH_LOGCODE_EMERGENCY emergency "$@" +} + +# ViashAlert: log events when actions must be taken immediately (e.g. corrupted system database) +# usage: ViashAlert Oh no something went wrong. +# stdout: Your input, prepended by '[alert] '. +function ViashAlert { + ViashLog $VIASH_LOGCODE_ALERT alert "$@" +} + +# ViashCritical: log events when a critical condition occurs +# usage: ViashCritical Oh no something went wrong. +# stdout: Your input, prepended by '[critical] '. +function ViashCritical { + ViashLog $VIASH_LOGCODE_CRITICAL critical "$@" +} + +# ViashError: log events when an error condition occurs +# usage: ViashError Oh no something went wrong. +# stdout: Your input, prepended by '[error] '. +function ViashError { + ViashLog $VIASH_LOGCODE_ERROR error "$@" +} + +# ViashWarning: log potentially abnormal events +# usage: ViashWarning Something may have gone wrong. +# stdout: Your input, prepended by '[warning] '. +function ViashWarning { + ViashLog $VIASH_LOGCODE_WARNING warning "$@" +} + +# ViashNotice: log significant but normal events +# usage: ViashNotice This just happened. +# stdout: Your input, prepended by '[notice] '. +function ViashNotice { + ViashLog $VIASH_LOGCODE_NOTICE notice "$@" +} + +# ViashInfo: log normal events +# usage: ViashInfo This just happened. +# stdout: Your input, prepended by '[info] '. +function ViashInfo { + ViashLog $VIASH_LOGCODE_INFO info "$@" +} + +# ViashDebug: log all events, for debugging purposes +# usage: ViashDebug This just happened. +# stdout: Your input, prepended by '[debug] '. +function ViashDebug { + ViashLog $VIASH_LOGCODE_DEBUG debug "$@" +} + +# find source folder of this component +VIASH_META_RESOURCES_DIR=`ViashSourceDir ${BASH_SOURCE[0]}` + +# find the root of the built components & dependencies +VIASH_TARGET_DIR=`ViashFindTargetDir $VIASH_META_RESOURCES_DIR` + +# define meta fields +VIASH_META_NAME="bgzip" +VIASH_META_FUNCTIONALITY_NAME="bgzip" +VIASH_META_EXECUTABLE="$VIASH_META_RESOURCES_DIR/$VIASH_META_NAME" +VIASH_META_CONFIG="$VIASH_META_RESOURCES_DIR/.config.vsh.yaml" +VIASH_META_TEMP_DIR="$VIASH_TEMP" + + + +# initialise variables +VIASH_MODE='run' +VIASH_ENGINE_ID='docker' + +######## Helper functions for setting up Docker images for viash ######## +# expects: ViashDockerBuild + +# ViashDockerInstallationCheck: check whether Docker is installed correctly +# +# examples: +# ViashDockerInstallationCheck +function ViashDockerInstallationCheck { + ViashDebug "Checking whether Docker is installed" + if [ ! command -v docker &> /dev/null ]; then + ViashCritical "Docker doesn't seem to be installed. See 'https://docs.docker.com/get-docker/' for instructions." + exit 1 + fi + + ViashDebug "Checking whether the Docker daemon is running" + local save=$-; set +e + local docker_version=$(docker version --format '{{.Client.APIVersion}}' 2> /dev/null) + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashCritical "Docker daemon does not seem to be running. Try one of the following:" + ViashCritical "- Try running 'dockerd' in the command line" + ViashCritical "- See https://docs.docker.com/config/daemon/" + exit 1 + fi +} + +# ViashDockerRemoteTagCheck: check whether a Docker image is available +# on a remote. Assumes `docker login` has been performed, if relevant. +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerRemoteTagCheck python:latest +# echo $? # returns '0' +# ViashDockerRemoteTagCheck sdaizudceahifu +# echo $? # returns '1' +function ViashDockerRemoteTagCheck { + docker manifest inspect $1 > /dev/null 2> /dev/null +} + +# ViashDockerLocalTagCheck: check whether a Docker image is available locally +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# docker pull python:latest +# ViashDockerLocalTagCheck python:latest +# echo $? # returns '0' +# ViashDockerLocalTagCheck sdaizudceahifu +# echo $? # returns '1' +function ViashDockerLocalTagCheck { + [ -n "$(docker images -q $1)" ] +} + +# ViashDockerPull: pull a Docker image +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerPull python:latest +# echo $? # returns '0' +# ViashDockerPull sdaizudceahifu +# echo $? # returns '1' +function ViashDockerPull { + ViashNotice "Checking if Docker image is available at '$1'" + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + docker pull $1 && return 0 || return 1 + else + local save=$-; set +e + docker pull $1 2> /dev/null > /dev/null + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashWarning "Could not pull from '$1'. Docker image doesn't exist or is not accessible." + fi + return $out + fi +} + +# ViashDockerPush: push a Docker image +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerPush python:latest +# echo $? # returns '0' +# ViashDockerPush sdaizudceahifu +# echo $? # returns '1' +function ViashDockerPush { + ViashNotice "Pushing image to '$1'" + local save=$-; set +e + local out + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + docker push $1 + out=$? + else + docker push $1 2> /dev/null > /dev/null + out=$? + fi + [[ $save =~ e ]] && set -e + if [ $out -eq 0 ]; then + ViashNotice "Container '$1' push succeeded." + else + ViashError "Container '$1' push errored. You might not be logged in or have the necessary permissions." + fi + return $out +} + +# ViashDockerPullElseBuild: pull a Docker image, else build it +# +# $1 : image identifier with format `[registry/]image[:tag]` +# ViashDockerBuild : a Bash function which builds a docker image, takes image identifier as argument. +# examples: +# ViashDockerPullElseBuild mynewcomponent +function ViashDockerPullElseBuild { + local save=$-; set +e + ViashDockerPull $1 + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashDockerBuild $@ + fi +} + +# ViashDockerSetup: create a Docker image, according to specified docker setup strategy +# +# $1 : image identifier with format `[registry/]image[:tag]` +# $2 : docker setup strategy, see DockerSetupStrategy.scala +# examples: +# ViashDockerSetup mynewcomponent alwaysbuild +function ViashDockerSetup { + local image_id="$1" + local setup_strategy="$2" + if [ "$setup_strategy" == "alwaysbuild" -o "$setup_strategy" == "build" -o "$setup_strategy" == "b" ]; then + ViashDockerBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayspull" -o "$setup_strategy" == "pull" -o "$setup_strategy" == "p" ]; then + ViashDockerPull $image_id + elif [ "$setup_strategy" == "alwayspullelsebuild" -o "$setup_strategy" == "pullelsebuild" ]; then + ViashDockerPullElseBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayspullelsecachedbuild" -o "$setup_strategy" == "pullelsecachedbuild" ]; then + ViashDockerPullElseBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayscachedbuild" -o "$setup_strategy" == "cachedbuild" -o "$setup_strategy" == "cb" ]; then + ViashDockerBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [[ "$setup_strategy" =~ ^ifneedbe ]]; then + local save=$-; set +e + ViashDockerLocalTagCheck $image_id + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -eq 0 ]; then + ViashInfo "Image $image_id already exists" + elif [ "$setup_strategy" == "ifneedbebuild" ]; then + ViashDockerBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbecachedbuild" ]; then + ViashDockerBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbepull" ]; then + ViashDockerPull $image_id + elif [ "$setup_strategy" == "ifneedbepullelsebuild" ]; then + ViashDockerPullElseBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbepullelsecachedbuild" ]; then + ViashDockerPullElseBuild $image_id $(ViashDockerBuildArgs "$engine_id") + else + ViashError "Unrecognised Docker strategy: $setup_strategy" + exit 1 + fi + elif [ "$setup_strategy" == "push" -o "$setup_strategy" == "forcepush" -o "$setup_strategy" == "alwayspush" ]; then + ViashDockerPush "$image_id" + elif [ "$setup_strategy" == "pushifnotpresent" -o "$setup_strategy" == "gentlepush" -o "$setup_strategy" == "maybepush" ]; then + local save=$-; set +e + ViashDockerRemoteTagCheck $image_id + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -eq 0 ]; then + ViashNotice "Container '$image_id' exists, doing nothing." + else + ViashNotice "Container '$image_id' does not yet exist." + ViashDockerPush "$image_id" + fi + elif [ "$setup_strategy" == "donothing" -o "$setup_strategy" == "meh" ]; then + ViashNotice "Skipping setup." + else + ViashError "Unrecognised Docker strategy: $setup_strategy" + exit 1 + fi +} + +# ViashDockerCheckCommands: Check whether a docker container has the required commands +# +# $1 : image identifier with format `[registry/]image[:tag]` +# $@ : commands to verify being present +# examples: +# ViashDockerCheckCommands bash:4.0 bash ps foo +function ViashDockerCheckCommands { + local image_id="$1" + shift 1 + local commands="$@" + local save=$-; set +e + local missing # mark 'missing' as local in advance, otherwise the exit code of the command will be missing and always be '0' + missing=$(docker run --rm --entrypoint=sh "$image_id" -c "for command in $commands; do command -v \$command >/dev/null 2>&1; if [ \$? -ne 0 ]; then echo \$command; exit 1; fi; done") + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -ne 0 ]; then + ViashError "Docker container '$image_id' does not contain command '$missing'." + exit 1 + fi +} + +# ViashDockerBuild: build a docker image +# $1 : image identifier with format `[registry/]image[:tag]` +# $... : additional arguments to pass to docker build +# $VIASH_META_TEMP_DIR : temporary directory to store dockerfile & optional resources in +# $VIASH_META_NAME : name of the component +# $VIASH_META_RESOURCES_DIR : directory containing the resources +# $VIASH_VERBOSITY : verbosity level +# exit code $? : whether or not the image was built successfully +function ViashDockerBuild { + local image_id="$1" + shift 1 + + # create temporary directory to store dockerfile & optional resources in + local tmpdir=$(mktemp -d "$VIASH_META_TEMP_DIR/dockerbuild-$VIASH_META_NAME-XXXXXX") + local dockerfile="$tmpdir/Dockerfile" + function clean_up { + rm -rf "$tmpdir" + } + trap clean_up EXIT + + # store dockerfile and resources + ViashDockerfile "$VIASH_ENGINE_ID" > "$dockerfile" + + # generate the build command + local docker_build_cmd="docker build -t '$image_id' $@ '$VIASH_META_RESOURCES_DIR' -f '$dockerfile'" + + # build the container + ViashNotice "Building container '$image_id' with Dockerfile" + ViashInfo "$docker_build_cmd" + local save=$-; set +e + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + eval $docker_build_cmd + else + eval $docker_build_cmd &> "$tmpdir/docker_build.log" + fi + + # check exit code + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashError "Error occurred while building container '$image_id'" + if [ $VIASH_VERBOSITY -lt $VIASH_LOGCODE_INFO ]; then + ViashError "Transcript: --------------------------------" + cat "$tmpdir/docker_build.log" + ViashError "End of transcript --------------------------" + fi + exit 1 + fi +} + +######## End of helper functions for setting up Docker images for viash ######## + +# ViashDockerFile: print the dockerfile to stdout +# $1 : engine identifier +# return : dockerfile required to run this component +# examples: +# ViashDockerFile +function ViashDockerfile { + local engine_id="$1" + + if [[ "$engine_id" == "docker" ]]; then + cat << 'VIASHDOCKER' +FROM quay.io/biocontainers/htslib:1.19--h81da01d_0 +ENTRYPOINT [] +RUN bgzip -h | grep 'Version:' 2>&1 | sed 's/Version:\s\(.*\)/bgzip: "\1"/' > /var/software_versions.txt + +LABEL org.opencontainers.image.description="Companion container for running component bgzip" +LABEL org.opencontainers.image.created="2025-06-12T09:15:19Z" +LABEL org.opencontainers.image.source="https://github.com/samtools/htslib" +LABEL org.opencontainers.image.revision="add30ba0f36bd8a8b07f0ba640707016d04e11b2" +LABEL org.opencontainers.image.version="gunzip" + +VIASHDOCKER + fi +} + +# ViashDockerBuildArgs: return the arguments to pass to docker build +# $1 : engine identifier +# return : arguments to pass to docker build +function ViashDockerBuildArgs { + local engine_id="$1" + + if [[ "$engine_id" == "docker" ]]; then + echo "" + fi +} + +# ViashAbsolutePath: generate absolute path from relative path +# borrowed from https://stackoverflow.com/a/21951256 +# $1 : relative filename +# return : absolute path +# examples: +# ViashAbsolutePath some_file.txt # returns /path/to/some_file.txt +# ViashAbsolutePath /foo/bar/.. # returns /foo +function ViashAbsolutePath { + local thePath + local parr + local outp + local len + if [[ ! "$1" =~ ^/ ]]; then + thePath="$PWD/$1" + else + thePath="$1" + fi + echo "$thePath" | ( + IFS=/ + read -a parr + declare -a outp + for i in "${parr[@]}"; do + case "$i" in + ''|.) continue ;; + ..) + len=${#outp[@]} + if ((len==0)); then + continue + else + unset outp[$((len-1))] + fi + ;; + *) + len=${#outp[@]} + outp[$len]="$i" + ;; + esac + done + echo /"${outp[*]}" + ) +} +# ViashDockerAutodetectMount: auto configuring docker mounts from parameters +# $1 : The parameter value +# returns : New parameter +# $VIASH_DIRECTORY_MOUNTS : Added another parameter to be passed to docker +# $VIASH_DOCKER_AUTOMOUNT_PREFIX : The prefix to be used for the automounts +# examples: +# ViashDockerAutodetectMount /path/to/bar # returns '/viash_automount/path/to/bar' +# ViashDockerAutodetectMountArg /path/to/bar # returns '--volume="/path/to:/viash_automount/path/to"' +function ViashDockerAutodetectMount { + local abs_path=$(ViashAbsolutePath "$1") + local mount_source + local base_name + if [ -d "$abs_path" ]; then + mount_source="$abs_path" + base_name="" + else + mount_source=`dirname "$abs_path"` + base_name=`basename "$abs_path"` + fi + local mount_target="$VIASH_DOCKER_AUTOMOUNT_PREFIX$mount_source" + if [ -z "$base_name" ]; then + echo "$mount_target" + else + echo "$mount_target/$base_name" + fi +} +function ViashDockerAutodetectMountArg { + local abs_path=$(ViashAbsolutePath "$1") + local mount_source + local base_name + if [ -d "$abs_path" ]; then + mount_source="$abs_path" + base_name="" + else + mount_source=`dirname "$abs_path"` + base_name=`basename "$abs_path"` + fi + local mount_target="$VIASH_DOCKER_AUTOMOUNT_PREFIX$mount_source" + ViashDebug "ViashDockerAutodetectMountArg $1 -> $mount_source -> $mount_target" + echo "--volume=\"$mount_source:$mount_target\"" +} +function ViashDockerStripAutomount { + local abs_path=$(ViashAbsolutePath "$1") + echo "${abs_path#$VIASH_DOCKER_AUTOMOUNT_PREFIX}" +} +# initialise variables +VIASH_DIRECTORY_MOUNTS=() + +# configure default docker automount prefix if it is unset +if [ -z "${VIASH_DOCKER_AUTOMOUNT_PREFIX+x}" ]; then + VIASH_DOCKER_AUTOMOUNT_PREFIX="/viash_automount" +fi + +# initialise docker variables +VIASH_DOCKER_RUN_ARGS=(-i --rm) + + +# ViashHelp: Display helpful explanation about this executable +function ViashHelp { + echo "bgzip gunzip" + echo "" + echo "Block compression/decompression utility" + echo "" + echo "Inputs:" + echo " --input" + echo " type: file, required parameter, file must exist" + echo " file to be compressed or decompressed" + echo "" + echo "Outputs:" + echo " --output" + echo " type: file, required parameter, output, file must exist" + echo " compressed or decompressed output" + echo "" + echo " -I, --index_name" + echo " type: file, output, file must exist" + echo " name of BGZF index file [file.gz.gzi]" + echo "" + echo "Arguments:" + echo " -b, --offset" + echo " type: integer" + echo " decompress at virtual file pointer (0-based uncompressed offset)" + echo "" + echo " -d, --decompress" + echo " type: boolean_true" + echo " decompress the input file" + echo "" + echo " -g, --rebgzip" + echo " type: boolean_true" + echo " use an index file to bgzip a file" + echo "" + echo " -i, --index" + echo " type: boolean_true" + echo " compress and create BGZF index" + echo "" + echo " -l, --compress_level" + echo " type: integer" + echo " min: -1" + echo " max: 9" + echo " compression level to use when compressing; 0 to 9, or -1 for default" + echo " [-1]" + echo "" + echo " -r, --reindex" + echo " type: boolean_true" + echo " (re)index the output file" + echo "" + echo " -s, --size" + echo " type: integer" + echo " min: 0" + echo " decompress INT bytes (uncompressed size)" + echo "" + echo " -t, --test" + echo " type: boolean_true" + echo " test integrity of compressed file" + echo "" + echo " --binary" + echo " type: boolean_true" + echo " Don't align blocks with text lines" + echo "" + echo "Viash built in Computational Requirements:" + echo " ---cpus=INT" + echo " Number of CPUs to use" + echo " ---memory=STRING" + echo " Amount of memory to use. Examples: 4GB, 3MiB." + echo "" + echo "Viash built in Docker:" + echo " ---setup=STRATEGY" + echo " Setup the docker container. Options are: alwaysbuild, alwayscachedbuild, ifneedbebuild, ifneedbecachedbuild, alwayspull, alwayspullelsebuild, alwayspullelsecachedbuild, ifneedbepull, ifneedbepullelsebuild, ifneedbepullelsecachedbuild, push, pushifnotpresent, donothing." + echo " Default: ifneedbepullelsecachedbuild" + echo " ---dockerfile" + echo " Print the dockerfile to stdout." + echo " ---docker_run_args=ARG" + echo " Provide runtime arguments to Docker. See the documentation on \`docker run\` for more information." + echo " ---docker_image_id" + echo " Print the docker image id to stdout." + echo " ---debug" + echo " Enter the docker container for debugging purposes." + echo "" + echo "Viash built in Engines:" + echo " ---engine=ENGINE_ID" + echo " Specify the engine to use. Options are: docker, native." + echo " Default: docker" +} + +# initialise array +VIASH_POSITIONAL_ARGS='' + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + ViashHelp + exit + ;; + ---v|---verbose) + let "VIASH_VERBOSITY=VIASH_VERBOSITY+1" + shift 1 + ;; + ---verbosity) + VIASH_VERBOSITY="$2" + shift 2 + ;; + ---verbosity=*) + VIASH_VERBOSITY="$(ViashRemoveFlags "$1")" + shift 1 + ;; + --version) + echo "bgzip gunzip" + exit + ;; + --input) + [ -n "$VIASH_PAR_INPUT" ] && ViashError Bad arguments for option \'--input\': \'$VIASH_PAR_INPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --input. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --input=*) + [ -n "$VIASH_PAR_INPUT" ] && ViashError Bad arguments for option \'--input=*\': \'$VIASH_PAR_INPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT=$(ViashRemoveFlags "$1") + shift 1 + ;; + --output) + [ -n "$VIASH_PAR_OUTPUT" ] && ViashError Bad arguments for option \'--output\': \'$VIASH_PAR_OUTPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --output. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --output=*) + [ -n "$VIASH_PAR_OUTPUT" ] && ViashError Bad arguments for option \'--output=*\': \'$VIASH_PAR_OUTPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT=$(ViashRemoveFlags "$1") + shift 1 + ;; + --index_name) + [ -n "$VIASH_PAR_INDEX_NAME" ] && ViashError Bad arguments for option \'--index_name\': \'$VIASH_PAR_INDEX_NAME\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDEX_NAME="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --index_name. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --index_name=*) + [ -n "$VIASH_PAR_INDEX_NAME" ] && ViashError Bad arguments for option \'--index_name=*\': \'$VIASH_PAR_INDEX_NAME\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDEX_NAME=$(ViashRemoveFlags "$1") + shift 1 + ;; + -I) + [ -n "$VIASH_PAR_INDEX_NAME" ] && ViashError Bad arguments for option \'-I\': \'$VIASH_PAR_INDEX_NAME\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDEX_NAME="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -I. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --offset) + [ -n "$VIASH_PAR_OFFSET" ] && ViashError Bad arguments for option \'--offset\': \'$VIASH_PAR_OFFSET\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OFFSET="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --offset. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --offset=*) + [ -n "$VIASH_PAR_OFFSET" ] && ViashError Bad arguments for option \'--offset=*\': \'$VIASH_PAR_OFFSET\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OFFSET=$(ViashRemoveFlags "$1") + shift 1 + ;; + -b) + [ -n "$VIASH_PAR_OFFSET" ] && ViashError Bad arguments for option \'-b\': \'$VIASH_PAR_OFFSET\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OFFSET="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -b. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --decompress) + [ -n "$VIASH_PAR_DECOMPRESS" ] && ViashError Bad arguments for option \'--decompress\': \'$VIASH_PAR_DECOMPRESS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_DECOMPRESS=true + shift 1 + ;; + -d) + [ -n "$VIASH_PAR_DECOMPRESS" ] && ViashError Bad arguments for option \'-d\': \'$VIASH_PAR_DECOMPRESS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_DECOMPRESS=true + shift 1 + ;; + --rebgzip) + [ -n "$VIASH_PAR_REBGZIP" ] && ViashError Bad arguments for option \'--rebgzip\': \'$VIASH_PAR_REBGZIP\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_REBGZIP=true + shift 1 + ;; + -g) + [ -n "$VIASH_PAR_REBGZIP" ] && ViashError Bad arguments for option \'-g\': \'$VIASH_PAR_REBGZIP\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_REBGZIP=true + shift 1 + ;; + --index) + [ -n "$VIASH_PAR_INDEX" ] && ViashError Bad arguments for option \'--index\': \'$VIASH_PAR_INDEX\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDEX=true + shift 1 + ;; + -i) + [ -n "$VIASH_PAR_INDEX" ] && ViashError Bad arguments for option \'-i\': \'$VIASH_PAR_INDEX\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDEX=true + shift 1 + ;; + --compress_level) + [ -n "$VIASH_PAR_COMPRESS_LEVEL" ] && ViashError Bad arguments for option \'--compress_level\': \'$VIASH_PAR_COMPRESS_LEVEL\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_COMPRESS_LEVEL="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --compress_level. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --compress_level=*) + [ -n "$VIASH_PAR_COMPRESS_LEVEL" ] && ViashError Bad arguments for option \'--compress_level=*\': \'$VIASH_PAR_COMPRESS_LEVEL\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_COMPRESS_LEVEL=$(ViashRemoveFlags "$1") + shift 1 + ;; + -l) + [ -n "$VIASH_PAR_COMPRESS_LEVEL" ] && ViashError Bad arguments for option \'-l\': \'$VIASH_PAR_COMPRESS_LEVEL\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_COMPRESS_LEVEL="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -l. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --reindex) + [ -n "$VIASH_PAR_REINDEX" ] && ViashError Bad arguments for option \'--reindex\': \'$VIASH_PAR_REINDEX\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_REINDEX=true + shift 1 + ;; + -r) + [ -n "$VIASH_PAR_REINDEX" ] && ViashError Bad arguments for option \'-r\': \'$VIASH_PAR_REINDEX\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_REINDEX=true + shift 1 + ;; + --size) + [ -n "$VIASH_PAR_SIZE" ] && ViashError Bad arguments for option \'--size\': \'$VIASH_PAR_SIZE\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_SIZE="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --size. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --size=*) + [ -n "$VIASH_PAR_SIZE" ] && ViashError Bad arguments for option \'--size=*\': \'$VIASH_PAR_SIZE\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_SIZE=$(ViashRemoveFlags "$1") + shift 1 + ;; + -s) + [ -n "$VIASH_PAR_SIZE" ] && ViashError Bad arguments for option \'-s\': \'$VIASH_PAR_SIZE\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_SIZE="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -s. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --test) + [ -n "$VIASH_PAR_TEST" ] && ViashError Bad arguments for option \'--test\': \'$VIASH_PAR_TEST\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_TEST=true + shift 1 + ;; + -t) + [ -n "$VIASH_PAR_TEST" ] && ViashError Bad arguments for option \'-t\': \'$VIASH_PAR_TEST\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_TEST=true + shift 1 + ;; + --binary) + [ -n "$VIASH_PAR_BINARY" ] && ViashError Bad arguments for option \'--binary\': \'$VIASH_PAR_BINARY\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_BINARY=true + shift 1 + ;; + ---engine) + VIASH_ENGINE_ID="$2" + shift 2 + ;; + ---engine=*) + VIASH_ENGINE_ID="$(ViashRemoveFlags "$1")" + shift 1 + ;; + ---setup) + VIASH_MODE='setup' + VIASH_SETUP_STRATEGY="$2" + shift 2 + ;; + ---setup=*) + VIASH_MODE='setup' + VIASH_SETUP_STRATEGY="$(ViashRemoveFlags "$1")" + shift 1 + ;; + ---dockerfile) + VIASH_MODE='dockerfile' + shift 1 + ;; + ---docker_run_args) + VIASH_DOCKER_RUN_ARGS+=("$2") + shift 2 + ;; + ---docker_run_args=*) + VIASH_DOCKER_RUN_ARGS+=("$(ViashRemoveFlags "$1")") + shift 1 + ;; + ---docker_image_id) + VIASH_MODE='docker_image_id' + shift 1 + ;; + ---debug) + VIASH_MODE='debug' + shift 1 + ;; + ---cpus) + [ -n "$VIASH_META_CPUS" ] && ViashError Bad arguments for option \'---cpus\': \'$VIASH_META_CPUS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_CPUS="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to ---cpus. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + ---cpus=*) + [ -n "$VIASH_META_CPUS" ] && ViashError Bad arguments for option \'---cpus=*\': \'$VIASH_META_CPUS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_CPUS=$(ViashRemoveFlags "$1") + shift 1 + ;; + ---memory) + [ -n "$VIASH_META_MEMORY" ] && ViashError Bad arguments for option \'---memory\': \'$VIASH_META_MEMORY\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_MEMORY="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to ---memory. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + ---memory=*) + [ -n "$VIASH_META_MEMORY" ] && ViashError Bad arguments for option \'---memory=*\': \'$VIASH_META_MEMORY\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_MEMORY=$(ViashRemoveFlags "$1") + shift 1 + ;; + *) # positional arg or unknown option + # since the positional args will be eval'd, can we always quote, instead of using ViashQuote + VIASH_POSITIONAL_ARGS="$VIASH_POSITIONAL_ARGS '$1'" + [[ $1 == -* ]] && ViashWarning $1 looks like a parameter but is not a defined parameter and will instead be treated as a positional argument. Use "--help" to get more information on the parameters. + shift # past argument + ;; + esac +done + +# parse positional parameters +eval set -- $VIASH_POSITIONAL_ARGS + + +if [ "$VIASH_ENGINE_ID" == "native" ] ; then + VIASH_ENGINE_TYPE='native' +elif [ "$VIASH_ENGINE_ID" == "docker" ] ; then + VIASH_ENGINE_TYPE='docker' +else + ViashError "Engine '$VIASH_ENGINE_ID' is not recognized. Options are: docker, native." + exit 1 +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # check if docker is installed properly + ViashDockerInstallationCheck + + # determine docker image id + if [[ "$VIASH_ENGINE_ID" == 'docker' ]]; then + VIASH_DOCKER_IMAGE_ID='images.viash-hub.com/vsh/toolbox/bgzip:gunzip' + fi + + # print dockerfile + if [ "$VIASH_MODE" == "dockerfile" ]; then + ViashDockerfile "$VIASH_ENGINE_ID" + exit 0 + + elif [ "$VIASH_MODE" == "docker_image_id" ]; then + echo "$VIASH_DOCKER_IMAGE_ID" + exit 0 + + # enter docker container + elif [[ "$VIASH_MODE" == "debug" ]]; then + VIASH_CMD="docker run --entrypoint=bash ${VIASH_DOCKER_RUN_ARGS[@]} -v '$(pwd)':/pwd --workdir /pwd -t $VIASH_DOCKER_IMAGE_ID" + ViashNotice "+ $VIASH_CMD" + eval $VIASH_CMD + exit + + # build docker image + elif [ "$VIASH_MODE" == "setup" ]; then + ViashDockerSetup "$VIASH_DOCKER_IMAGE_ID" "$VIASH_SETUP_STRATEGY" + ViashDockerCheckCommands "$VIASH_DOCKER_IMAGE_ID" 'ps' 'bash' + exit 0 + fi + + # check if docker image exists + ViashDockerSetup "$VIASH_DOCKER_IMAGE_ID" ifneedbepullelsecachedbuild + ViashDockerCheckCommands "$VIASH_DOCKER_IMAGE_ID" 'ps' 'bash' +fi + +# setting computational defaults + +# helper function for parsing memory strings +function ViashMemoryAsBytes { + local memory=`echo "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'` + local memory_regex='^([0-9]+)([kmgtp]i?b?|b)$' + if [[ $memory =~ $memory_regex ]]; then + local number=${memory/[^0-9]*/} + local symbol=${memory/*[0-9]/} + + case $symbol in + b) memory_b=$number ;; + kb|k) memory_b=$(( $number * 1000 )) ;; + mb|m) memory_b=$(( $number * 1000 * 1000 )) ;; + gb|g) memory_b=$(( $number * 1000 * 1000 * 1000 )) ;; + tb|t) memory_b=$(( $number * 1000 * 1000 * 1000 * 1000 )) ;; + pb|p) memory_b=$(( $number * 1000 * 1000 * 1000 * 1000 * 1000 )) ;; + kib|ki) memory_b=$(( $number * 1024 )) ;; + mib|mi) memory_b=$(( $number * 1024 * 1024 )) ;; + gib|gi) memory_b=$(( $number * 1024 * 1024 * 1024 )) ;; + tib|ti) memory_b=$(( $number * 1024 * 1024 * 1024 * 1024 )) ;; + pib|pi) memory_b=$(( $number * 1024 * 1024 * 1024 * 1024 * 1024 )) ;; + esac + echo "$memory_b" + fi +} +# compute memory in different units +if [ ! -z ${VIASH_META_MEMORY+x} ]; then + VIASH_META_MEMORY_B=`ViashMemoryAsBytes $VIASH_META_MEMORY` + # do not define other variables if memory_b is an empty string + if [ ! -z "$VIASH_META_MEMORY_B" ]; then + VIASH_META_MEMORY_KB=$(( ($VIASH_META_MEMORY_B+999) / 1000 )) + VIASH_META_MEMORY_MB=$(( ($VIASH_META_MEMORY_KB+999) / 1000 )) + VIASH_META_MEMORY_GB=$(( ($VIASH_META_MEMORY_MB+999) / 1000 )) + VIASH_META_MEMORY_TB=$(( ($VIASH_META_MEMORY_GB+999) / 1000 )) + VIASH_META_MEMORY_PB=$(( ($VIASH_META_MEMORY_TB+999) / 1000 )) + VIASH_META_MEMORY_KIB=$(( ($VIASH_META_MEMORY_B+1023) / 1024 )) + VIASH_META_MEMORY_MIB=$(( ($VIASH_META_MEMORY_KIB+1023) / 1024 )) + VIASH_META_MEMORY_GIB=$(( ($VIASH_META_MEMORY_MIB+1023) / 1024 )) + VIASH_META_MEMORY_TIB=$(( ($VIASH_META_MEMORY_GIB+1023) / 1024 )) + VIASH_META_MEMORY_PIB=$(( ($VIASH_META_MEMORY_TIB+1023) / 1024 )) + else + # unset memory if string is empty + unset $VIASH_META_MEMORY_B + fi +fi +# unset nproc if string is empty +if [ -z "$VIASH_META_CPUS" ]; then + unset $VIASH_META_CPUS +fi + + +# check whether required parameters exist +if [ -z ${VIASH_PAR_INPUT+x} ]; then + ViashError '--input' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_PAR_OUTPUT+x} ]; then + ViashError '--output' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_NAME+x} ]; then + ViashError 'name' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then + ViashError 'functionality_name' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_RESOURCES_DIR+x} ]; then + ViashError 'resources_dir' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_EXECUTABLE+x} ]; then + ViashError 'executable' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_CONFIG+x} ]; then + ViashError 'config' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_TEMP_DIR+x} ]; then + ViashError 'temp_dir' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi + +# filling in defaults +if [ -z ${VIASH_PAR_DECOMPRESS+x} ]; then + VIASH_PAR_DECOMPRESS="false" +fi +if [ -z ${VIASH_PAR_REBGZIP+x} ]; then + VIASH_PAR_REBGZIP="false" +fi +if [ -z ${VIASH_PAR_INDEX+x} ]; then + VIASH_PAR_INDEX="false" +fi +if [ -z ${VIASH_PAR_REINDEX+x} ]; then + VIASH_PAR_REINDEX="false" +fi +if [ -z ${VIASH_PAR_TEST+x} ]; then + VIASH_PAR_TEST="false" +fi +if [ -z ${VIASH_PAR_BINARY+x} ]; then + VIASH_PAR_BINARY="false" +fi + +# check whether required files exist +if [ ! -z "$VIASH_PAR_INPUT" ] && [ ! -e "$VIASH_PAR_INPUT" ]; then + ViashError "Input file '$VIASH_PAR_INPUT' does not exist." + exit 1 +fi + +# check whether parameters values are of the right type +if [[ -n "$VIASH_PAR_OFFSET" ]]; then + if ! [[ "$VIASH_PAR_OFFSET" =~ ^[-+]?[0-9]+$ ]]; then + ViashError '--offset' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_DECOMPRESS" ]]; then + if ! [[ "$VIASH_PAR_DECOMPRESS" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--decompress' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_REBGZIP" ]]; then + if ! [[ "$VIASH_PAR_REBGZIP" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--rebgzip' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_INDEX" ]]; then + if ! [[ "$VIASH_PAR_INDEX" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--index' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_COMPRESS_LEVEL" ]]; then + if ! [[ "$VIASH_PAR_COMPRESS_LEVEL" =~ ^[-+]?[0-9]+$ ]]; then + ViashError '--compress_level' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi + if [[ $VIASH_PAR_COMPRESS_LEVEL -lt -1 ]]; then + ViashError '--compress_level' has be more than or equal to -1. Use "--help" to get more information on the parameters. + exit 1 + fi + if [[ $VIASH_PAR_COMPRESS_LEVEL -gt 9 ]]; then + ViashError '--compress_level' has be less than or equal to 9. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_REINDEX" ]]; then + if ! [[ "$VIASH_PAR_REINDEX" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--reindex' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_SIZE" ]]; then + if ! [[ "$VIASH_PAR_SIZE" =~ ^[-+]?[0-9]+$ ]]; then + ViashError '--size' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi + if [[ $VIASH_PAR_SIZE -lt 0 ]]; then + ViashError '--size' has be more than or equal to 0. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_TEST" ]]; then + if ! [[ "$VIASH_PAR_TEST" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--test' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_BINARY" ]]; then + if ! [[ "$VIASH_PAR_BINARY" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--binary' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_CPUS" ]]; then + if ! [[ "$VIASH_META_CPUS" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'cpus' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_B" ]]; then + if ! [[ "$VIASH_META_MEMORY_B" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_b' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_KB" ]]; then + if ! [[ "$VIASH_META_MEMORY_KB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_kb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_MB" ]]; then + if ! [[ "$VIASH_META_MEMORY_MB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_mb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_GB" ]]; then + if ! [[ "$VIASH_META_MEMORY_GB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_gb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_TB" ]]; then + if ! [[ "$VIASH_META_MEMORY_TB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_tb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_PB" ]]; then + if ! [[ "$VIASH_META_MEMORY_PB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_pb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_KIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_KIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_kib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_MIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_MIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_mib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_GIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_GIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_gib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_TIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_TIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_tib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_PIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_PIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_pib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi + +# create parent directories of output files, if so desired +if [ ! -z "$VIASH_PAR_OUTPUT" ] && [ ! -d "$(dirname "$VIASH_PAR_OUTPUT")" ]; then + mkdir -p "$(dirname "$VIASH_PAR_OUTPUT")" +fi +if [ ! -z "$VIASH_PAR_INDEX_NAME" ] && [ ! -d "$(dirname "$VIASH_PAR_INDEX_NAME")" ]; then + mkdir -p "$(dirname "$VIASH_PAR_INDEX_NAME")" +fi + +if [ "$VIASH_ENGINE_ID" == "native" ] ; then + if [ "$VIASH_MODE" == "run" ]; then + VIASH_CMD="bash" + else + ViashError "Engine '$VIASH_ENGINE_ID' does not support mode '$VIASH_MODE'." + exit 1 + fi +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # detect volumes from file arguments + VIASH_CHOWN_VARS=() +if [ ! -z "$VIASH_PAR_INPUT" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_PAR_INPUT")" ) + VIASH_PAR_INPUT=$(ViashDockerAutodetectMount "$VIASH_PAR_INPUT") +fi +if [ ! -z "$VIASH_PAR_OUTPUT" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_PAR_OUTPUT")" ) + VIASH_PAR_OUTPUT=$(ViashDockerAutodetectMount "$VIASH_PAR_OUTPUT") + VIASH_CHOWN_VARS+=( "$VIASH_PAR_OUTPUT" ) +fi +if [ ! -z "$VIASH_PAR_INDEX_NAME" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_PAR_INDEX_NAME")" ) + VIASH_PAR_INDEX_NAME=$(ViashDockerAutodetectMount "$VIASH_PAR_INDEX_NAME") + VIASH_CHOWN_VARS+=( "$VIASH_PAR_INDEX_NAME" ) +fi +if [ ! -z "$VIASH_META_RESOURCES_DIR" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_RESOURCES_DIR")" ) + VIASH_META_RESOURCES_DIR=$(ViashDockerAutodetectMount "$VIASH_META_RESOURCES_DIR") +fi +if [ ! -z "$VIASH_META_EXECUTABLE" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_EXECUTABLE")" ) + VIASH_META_EXECUTABLE=$(ViashDockerAutodetectMount "$VIASH_META_EXECUTABLE") +fi +if [ ! -z "$VIASH_META_CONFIG" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_CONFIG")" ) + VIASH_META_CONFIG=$(ViashDockerAutodetectMount "$VIASH_META_CONFIG") +fi +if [ ! -z "$VIASH_META_TEMP_DIR" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_TEMP_DIR")" ) + VIASH_META_TEMP_DIR=$(ViashDockerAutodetectMount "$VIASH_META_TEMP_DIR") +fi + + # get unique mounts + VIASH_UNIQUE_MOUNTS=($(for val in "${VIASH_DIRECTORY_MOUNTS[@]}"; do echo "$val"; done | sort -u)) +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # change file ownership + function ViashPerformChown { + if (( ${#VIASH_CHOWN_VARS[@]} )); then + set +e + VIASH_CMD="docker run --entrypoint=bash --rm ${VIASH_UNIQUE_MOUNTS[@]} $VIASH_DOCKER_IMAGE_ID -c 'chown $(id -u):$(id -g) --silent --recursive ${VIASH_CHOWN_VARS[@]}'" + ViashDebug "+ $VIASH_CMD" + eval $VIASH_CMD + set -e + fi + } + trap ViashPerformChown EXIT +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # helper function for filling in extra docker args + if [ ! -z "$VIASH_META_MEMORY_B" ]; then + VIASH_DOCKER_RUN_ARGS+=("--memory=${VIASH_META_MEMORY_B}") + fi + if [ ! -z "$VIASH_META_CPUS" ]; then + VIASH_DOCKER_RUN_ARGS+=("--cpus=${VIASH_META_CPUS}") + fi +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + VIASH_CMD="docker run --entrypoint=bash ${VIASH_DOCKER_RUN_ARGS[@]} ${VIASH_UNIQUE_MOUNTS[@]} $VIASH_DOCKER_IMAGE_ID" +fi + + +# set dependency paths + + +ViashDebug "Running command: $(echo $VIASH_CMD)" +cat << VIASHEOF | eval $VIASH_CMD +set -e +tempscript=\$(mktemp "$VIASH_META_TEMP_DIR/viash-run-bgzip-XXXXXX").sh +function clean_up { + rm "\$tempscript" +} +function interrupt { + echo -e "\nCTRL-C Pressed..." + exit 1 +} +trap clean_up EXIT +trap interrupt INT SIGINT +cat > "\$tempscript" << 'VIASHMAIN' +## VIASH START +# The following code has been auto-generated by Viash. +$( if [ ! -z ${VIASH_PAR_INPUT+x} ]; then echo "${VIASH_PAR_INPUT}" | sed "s#'#'\"'\"'#g;s#.*#par_input='&'#" ; else echo "# par_input="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT+x} ]; then echo "${VIASH_PAR_OUTPUT}" | sed "s#'#'\"'\"'#g;s#.*#par_output='&'#" ; else echo "# par_output="; fi ) +$( if [ ! -z ${VIASH_PAR_INDEX_NAME+x} ]; then echo "${VIASH_PAR_INDEX_NAME}" | sed "s#'#'\"'\"'#g;s#.*#par_index_name='&'#" ; else echo "# par_index_name="; fi ) +$( if [ ! -z ${VIASH_PAR_OFFSET+x} ]; then echo "${VIASH_PAR_OFFSET}" | sed "s#'#'\"'\"'#g;s#.*#par_offset='&'#" ; else echo "# par_offset="; fi ) +$( if [ ! -z ${VIASH_PAR_DECOMPRESS+x} ]; then echo "${VIASH_PAR_DECOMPRESS}" | sed "s#'#'\"'\"'#g;s#.*#par_decompress='&'#" ; else echo "# par_decompress="; fi ) +$( if [ ! -z ${VIASH_PAR_REBGZIP+x} ]; then echo "${VIASH_PAR_REBGZIP}" | sed "s#'#'\"'\"'#g;s#.*#par_rebgzip='&'#" ; else echo "# par_rebgzip="; fi ) +$( if [ ! -z ${VIASH_PAR_INDEX+x} ]; then echo "${VIASH_PAR_INDEX}" | sed "s#'#'\"'\"'#g;s#.*#par_index='&'#" ; else echo "# par_index="; fi ) +$( if [ ! -z ${VIASH_PAR_COMPRESS_LEVEL+x} ]; then echo "${VIASH_PAR_COMPRESS_LEVEL}" | sed "s#'#'\"'\"'#g;s#.*#par_compress_level='&'#" ; else echo "# par_compress_level="; fi ) +$( if [ ! -z ${VIASH_PAR_REINDEX+x} ]; then echo "${VIASH_PAR_REINDEX}" | sed "s#'#'\"'\"'#g;s#.*#par_reindex='&'#" ; else echo "# par_reindex="; fi ) +$( if [ ! -z ${VIASH_PAR_SIZE+x} ]; then echo "${VIASH_PAR_SIZE}" | sed "s#'#'\"'\"'#g;s#.*#par_size='&'#" ; else echo "# par_size="; fi ) +$( if [ ! -z ${VIASH_PAR_TEST+x} ]; then echo "${VIASH_PAR_TEST}" | sed "s#'#'\"'\"'#g;s#.*#par_test='&'#" ; else echo "# par_test="; fi ) +$( if [ ! -z ${VIASH_PAR_BINARY+x} ]; then echo "${VIASH_PAR_BINARY}" | sed "s#'#'\"'\"'#g;s#.*#par_binary='&'#" ; else echo "# par_binary="; fi ) +$( if [ ! -z ${VIASH_META_NAME+x} ]; then echo "${VIASH_META_NAME}" | sed "s#'#'\"'\"'#g;s#.*#meta_name='&'#" ; else echo "# meta_name="; fi ) +$( if [ ! -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then echo "${VIASH_META_FUNCTIONALITY_NAME}" | sed "s#'#'\"'\"'#g;s#.*#meta_functionality_name='&'#" ; else echo "# meta_functionality_name="; fi ) +$( if [ ! -z ${VIASH_META_RESOURCES_DIR+x} ]; then echo "${VIASH_META_RESOURCES_DIR}" | sed "s#'#'\"'\"'#g;s#.*#meta_resources_dir='&'#" ; else echo "# meta_resources_dir="; fi ) +$( if [ ! -z ${VIASH_META_EXECUTABLE+x} ]; then echo "${VIASH_META_EXECUTABLE}" | sed "s#'#'\"'\"'#g;s#.*#meta_executable='&'#" ; else echo "# meta_executable="; fi ) +$( if [ ! -z ${VIASH_META_CONFIG+x} ]; then echo "${VIASH_META_CONFIG}" | sed "s#'#'\"'\"'#g;s#.*#meta_config='&'#" ; else echo "# meta_config="; fi ) +$( if [ ! -z ${VIASH_META_TEMP_DIR+x} ]; then echo "${VIASH_META_TEMP_DIR}" | sed "s#'#'\"'\"'#g;s#.*#meta_temp_dir='&'#" ; else echo "# meta_temp_dir="; fi ) +$( if [ ! -z ${VIASH_META_CPUS+x} ]; then echo "${VIASH_META_CPUS}" | sed "s#'#'\"'\"'#g;s#.*#meta_cpus='&'#" ; else echo "# meta_cpus="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_B+x} ]; then echo "${VIASH_META_MEMORY_B}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_b='&'#" ; else echo "# meta_memory_b="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KB+x} ]; then echo "${VIASH_META_MEMORY_KB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_kb='&'#" ; else echo "# meta_memory_kb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MB+x} ]; then echo "${VIASH_META_MEMORY_MB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_mb='&'#" ; else echo "# meta_memory_mb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GB+x} ]; then echo "${VIASH_META_MEMORY_GB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_gb='&'#" ; else echo "# meta_memory_gb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TB+x} ]; then echo "${VIASH_META_MEMORY_TB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_tb='&'#" ; else echo "# meta_memory_tb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PB+x} ]; then echo "${VIASH_META_MEMORY_PB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_pb='&'#" ; else echo "# meta_memory_pb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KIB+x} ]; then echo "${VIASH_META_MEMORY_KIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_kib='&'#" ; else echo "# meta_memory_kib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MIB+x} ]; then echo "${VIASH_META_MEMORY_MIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_mib='&'#" ; else echo "# meta_memory_mib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GIB+x} ]; then echo "${VIASH_META_MEMORY_GIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_gib='&'#" ; else echo "# meta_memory_gib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TIB+x} ]; then echo "${VIASH_META_MEMORY_TIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_tib='&'#" ; else echo "# meta_memory_tib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PIB+x} ]; then echo "${VIASH_META_MEMORY_PIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_pib='&'#" ; else echo "# meta_memory_pib="; fi ) + +## VIASH END +#!/bin/bash + +[[ "\$par_decompress" == "false" ]] && unset par_decompress +[[ "\$par_rebgzip" == "false" ]] && unset par_rebgzip +[[ "\$par_index" == "false" ]] && unset par_index +[[ "\$par_reindex" == "false" ]] && unset par_reindex +[[ "\$par_test" == "false" ]] && unset par_test +[[ "\$par_binary" == "false" ]] && unset par_binary +bgzip -c \\ + \${meta_cpus:+--threads "\${meta_cpus}"} \\ + \${par_offset:+-b "\${par_offset}"} \\ + \${par_decompress:+-d} \\ + \${par_rebgzip:+-g} \\ + \${par_index:+-i} \\ + \${par_index_name:+-I "\${par_index_name}"} \\ + \${par_compress_level:+-l "\${par_compress_level}"} \\ + \${par_reindex:+-r} \\ + \${par_size:+-s "\${par_size}"} \\ + \${par_test:+-t} \\ + \${par_binary:+--binary} \\ + "\$par_input" > "\$par_output" +VIASHMAIN +bash "\$tempscript" & +wait "\$!" + +VIASHEOF + + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # strip viash automount from file paths + + if [ ! -z "$VIASH_PAR_INPUT" ]; then + VIASH_PAR_INPUT=$(ViashDockerStripAutomount "$VIASH_PAR_INPUT") + fi + if [ ! -z "$VIASH_PAR_OUTPUT" ]; then + VIASH_PAR_OUTPUT=$(ViashDockerStripAutomount "$VIASH_PAR_OUTPUT") + fi + if [ ! -z "$VIASH_PAR_INDEX_NAME" ]; then + VIASH_PAR_INDEX_NAME=$(ViashDockerStripAutomount "$VIASH_PAR_INDEX_NAME") + fi + if [ ! -z "$VIASH_META_RESOURCES_DIR" ]; then + VIASH_META_RESOURCES_DIR=$(ViashDockerStripAutomount "$VIASH_META_RESOURCES_DIR") + fi + if [ ! -z "$VIASH_META_EXECUTABLE" ]; then + VIASH_META_EXECUTABLE=$(ViashDockerStripAutomount "$VIASH_META_EXECUTABLE") + fi + if [ ! -z "$VIASH_META_CONFIG" ]; then + VIASH_META_CONFIG=$(ViashDockerStripAutomount "$VIASH_META_CONFIG") + fi + if [ ! -z "$VIASH_META_TEMP_DIR" ]; then + VIASH_META_TEMP_DIR=$(ViashDockerStripAutomount "$VIASH_META_TEMP_DIR") + fi +fi + + +# check whether required files exist +if [ ! -z "$VIASH_PAR_OUTPUT" ] && [ ! -e "$VIASH_PAR_OUTPUT" ]; then + ViashError "Output file '$VIASH_PAR_OUTPUT' does not exist." + exit 1 +fi +if [ ! -z "$VIASH_PAR_INDEX_NAME" ] && [ ! -e "$VIASH_PAR_INDEX_NAME" ]; then + ViashError "Output file '$VIASH_PAR_INDEX_NAME' does not exist." + exit 1 +fi + + +exit 0 diff --git a/target/executable/yq/.config.vsh.yaml b/target/executable/yq/.config.vsh.yaml new file mode 100644 index 0000000..64f06bf --- /dev/null +++ b/target/executable/yq/.config.vsh.yaml @@ -0,0 +1,297 @@ +name: "yq" +version: "gunzip" +argument_groups: +- name: "Inputs" + arguments: + - type: "file" + name: "--input" + description: "files to be processed" + info: null + example: + - "input.yaml" + must_exist: true + create_parent: true + required: true + direction: "input" + multiple: false + multiple_sep: ";" +- name: "Outputs" + arguments: + - type: "file" + name: "--output" + description: "output file" + info: null + example: + - "output.yaml" + must_exist: true + create_parent: true + required: true + direction: "output" + multiple: false + multiple_sep: ";" +- name: "Arguments" + arguments: + - type: "string" + name: "--eval" + description: "expression to evaluate" + info: null + example: + - ".name = \"foo\"" + required: true + direction: "input" + multiple: false + multiple_sep: ";" + - type: "integer" + name: "--indent" + alternatives: + - "-I" + description: "sets indent level for output (default 2)" + info: null + required: false + direction: "input" + multiple: false + multiple_sep: ";" + - type: "string" + name: "--input_format" + alternatives: + - "-p" + description: "parse format for input. (default \"auto\")" + info: null + required: false + choices: + - "auto" + - "a" + - "yaml" + - "y" + - "json" + - "j" + - "props" + - "p" + - "csv" + - "c" + - "tsv" + - "t" + - "xml" + - "x" + - "base64" + - "uri" + - "toml" + - "shell" + - "s" + - "lua" + - "l" + direction: "input" + multiple: false + multiple_sep: ";" + - type: "string" + name: "--output_format" + alternatives: + - "-o" + description: "output format type. (default \"auto\")" + info: null + required: false + choices: + - "auto" + - "a" + - "yaml" + - "y" + - "json" + - "j" + - "props" + - "p" + - "csv" + - "c" + - "tsv" + - "t" + - "xml" + - "x" + - "base64" + - "uri" + - "toml" + - "shell" + - "s" + - "lua" + - "l" + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--pretty_print" + alternatives: + - "-P" + description: "pretty print, shorthand for '... style = \"\"'" + info: null + direction: "input" +resources: +- type: "bash_script" + text: | + #!/bin/sh + [[ "$par_pretty_print" == "false" ]] && unset par_pretty_print + yq eval \ + ${par_indent:+-I "${par_indent}"} \ + ${par_input_format:+-p "${par_input_format}"} \ + ${par_output_format:+-o "${par_output_format}"} \ + ${par_pretty_print:+-P} \ + --expression "$par_eval" \ + --no-colors \ + "$par_input" > "$par_output" + + + dest: "./script.sh" + is_executable: true +description: "A portable YAML, JSON, XML, CSV, TOML and properties processor" +test_resources: +- type: "bash_script" + text: "set -e\necho \"name: 'bar'\" > test.yaml\n\"$meta_executable\" --input test.yaml\ + \ --output output.yaml --eval '.name = \"foo\"'\n\"$meta_executable\" --input\ + \ output.yaml --output output2.yaml --eval '.name'\ngrep \"^foo$\" output2.yaml\n" + dest: "./script.sh" + is_executable: true +info: null +status: "enabled" +scope: + image: "public" + target: "public" +requirements: + commands: + - "ps" +keywords: +- "yaml" +- "json" +- "xml" +- "csv" +- "toml" +- "properties" +license: "MIT" +links: + repository: "https://github.com/mikefarah/yq" + homepage: "https://mikefarah.gitbook.io/yq" + documentation: "https://mikefarah.gitbook.io/yq/" +runners: +- type: "executable" + id: "executable" + docker_setup_strategy: "ifneedbepullelsecachedbuild" +- type: "nextflow" + id: "nextflow" + directives: + tag: "$id" + auto: + simplifyInput: true + simplifyOutput: false + transcript: false + publish: false + config: + labels: + mem1gb: "memory = 1000000000.B" + mem2gb: "memory = 2000000000.B" + mem5gb: "memory = 5000000000.B" + mem10gb: "memory = 10000000000.B" + mem20gb: "memory = 20000000000.B" + mem50gb: "memory = 50000000000.B" + mem100gb: "memory = 100000000000.B" + mem200gb: "memory = 200000000000.B" + mem500gb: "memory = 500000000000.B" + mem1tb: "memory = 1000000000000.B" + mem2tb: "memory = 2000000000000.B" + mem5tb: "memory = 5000000000000.B" + mem10tb: "memory = 10000000000000.B" + mem20tb: "memory = 20000000000000.B" + mem50tb: "memory = 50000000000000.B" + mem100tb: "memory = 100000000000000.B" + mem200tb: "memory = 200000000000000.B" + mem500tb: "memory = 500000000000000.B" + mem1gib: "memory = 1073741824.B" + mem2gib: "memory = 2147483648.B" + mem4gib: "memory = 4294967296.B" + mem8gib: "memory = 8589934592.B" + mem16gib: "memory = 17179869184.B" + mem32gib: "memory = 34359738368.B" + mem64gib: "memory = 68719476736.B" + mem128gib: "memory = 137438953472.B" + mem256gib: "memory = 274877906944.B" + mem512gib: "memory = 549755813888.B" + mem1tib: "memory = 1099511627776.B" + mem2tib: "memory = 2199023255552.B" + mem4tib: "memory = 4398046511104.B" + mem8tib: "memory = 8796093022208.B" + mem16tib: "memory = 17592186044416.B" + mem32tib: "memory = 35184372088832.B" + mem64tib: "memory = 70368744177664.B" + mem128tib: "memory = 140737488355328.B" + mem256tib: "memory = 281474976710656.B" + mem512tib: "memory = 562949953421312.B" + cpu1: "cpus = 1" + cpu2: "cpus = 2" + cpu5: "cpus = 5" + cpu10: "cpus = 10" + cpu20: "cpus = 20" + cpu50: "cpus = 50" + cpu100: "cpus = 100" + cpu200: "cpus = 200" + cpu500: "cpus = 500" + cpu1000: "cpus = 1000" + debug: false + container: "docker" +engines: +- type: "docker" + id: "docker" + image: "alpine:latest" + target_registry: "images.viash-hub.com" + target_tag: "gunzip" + namespace_separator: "/" + setup: + - type: "apk" + packages: + - "bash" + - "yq-go" + - type: "docker" + run: + - "/usr/bin/yq --version | sed 's/.*version\\sv\\(.*\\)/yq: \"\\1\"/' > /var/software_versions.txt\n" + entrypoint: [] + cmd: null +- type: "native" + id: "native" +build_info: + config: "src/yq/config.vsh.yaml" + runner: "executable" + engine: "docker|native" + output: "target/executable/yq" + executable: "target/executable/yq/yq" + viash_version: "0.9.4" + git_commit: "add30ba0f36bd8a8b07f0ba640707016d04e11b2" + git_remote: "https://github.com/viash-hub/toolbox" +package_config: + name: "toolbox" + version: "gunzip" + summary: "A collection of curated command-line tools for general IT tasks, built\ + \ with Viash.\n" + description: "`toolbox` provides a versatile suite of IT components, following the\ + \ robust Viash (https://viash.io) framework.\nThis package focuses on delivering\ + \ reliable, standalone tools that can be easily integrated into larger computational\ + \ workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**,\ + \ and adherence to **best practices** in component creation. Key features of `toolbox`\ + \ components include:\n\n* **Standalone & Nextflow Ready:** Execute components\ + \ directly from the command line or seamlessly incorporate them into Nextflow\ + \ workflows.\n* **High Quality Standards:**\n * Comprehensive documentation\ + \ for each component and its parameters.\n * Full exposure of the underlying\ + \ tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure\ + \ consistent environments and manage dependencies, leading to enhanced reproducibility.\n\ + \ * Unit tested to verify functionality and ensure reliability.\n" + info: null + viash_version: "0.9.4" + source: "src" + target: "target" + config_mods: + - ".requirements.commands := ['ps']\n" + - ".engines += { type: \"native\" }" + - ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'" + - ".engines[.type == 'docker'].target_tag := 'gunzip'" + keywords: + - "toolbox" + - "command-line" + - "tools" + license: "MIT" + organization: "vsh" + links: + repository: "https://github.com/viash-hub/toolbox" + issue_tracker: "https://github.com/viash-hub/toolbox/issues" diff --git a/target/executable/yq/yq b/target/executable/yq/yq new file mode 100755 index 0000000..628c081 --- /dev/null +++ b/target/executable/yq/yq @@ -0,0 +1,1260 @@ +#!/usr/bin/env bash + +# yq gunzip +# +# This wrapper script is auto-generated by viash 0.9.4 and is thus a derivative +# work thereof. This software comes with ABSOLUTELY NO WARRANTY from Data +# Intuitive. +# +# The component may contain files which fall under a different license. The +# authors of this component should specify the license in the header of such +# files, or include a separate license file detailing the licenses of all included +# files. + +set -e + +if [ -z "$VIASH_TEMP" ]; then + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TEMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$VIASH_TMP} + VIASH_TEMP=${VIASH_TEMP:-$TMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$TMP} + VIASH_TEMP=${VIASH_TEMP:-$TEMPDIR} + VIASH_TEMP=${VIASH_TEMP:-$TEMP} + VIASH_TEMP=${VIASH_TEMP:-/tmp} +fi + +# define helper functions +# ViashQuote: put quotes around non flag values +# $1 : unquoted string +# return : possibly quoted string +# examples: +# ViashQuote --foo # returns --foo +# ViashQuote bar # returns 'bar' +# Viashquote --foo=bar # returns --foo='bar' +function ViashQuote { + if [[ "$1" =~ ^-+[a-zA-Z0-9_\-]+=.+$ ]]; then + echo "$1" | sed "s#=\(.*\)#='\1'#" + elif [[ "$1" =~ ^-+[a-zA-Z0-9_\-]+$ ]]; then + echo "$1" + else + echo "'$1'" + fi +} +# ViashRemoveFlags: Remove leading flag +# $1 : string with a possible leading flag +# return : string without possible leading flag +# examples: +# ViashRemoveFlags --foo=bar # returns bar +function ViashRemoveFlags { + echo "$1" | sed 's/^--*[a-zA-Z0-9_\-]*=//' +} +# ViashSourceDir: return the path of a bash file, following symlinks +# usage : ViashSourceDir ${BASH_SOURCE[0]} +# $1 : Should always be set to ${BASH_SOURCE[0]} +# returns : The absolute path of the bash file +function ViashSourceDir { + local source="$1" + while [ -h "$source" ]; do + local dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$dir/$source" + done + cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd +} +# ViashFindTargetDir: return the path of the '.build.yaml' file, following symlinks +# usage : ViashFindTargetDir 'ScriptPath' +# $1 : The location from where to start the upward search +# returns : The absolute path of the '.build.yaml' file +function ViashFindTargetDir { + local source="$1" + while [[ "$source" != "" && ! -e "$source/.build.yaml" ]]; do + source=${source%/*} + done + echo $source +} +# see https://en.wikipedia.org/wiki/Syslog#Severity_level +VIASH_LOGCODE_EMERGENCY=0 +VIASH_LOGCODE_ALERT=1 +VIASH_LOGCODE_CRITICAL=2 +VIASH_LOGCODE_ERROR=3 +VIASH_LOGCODE_WARNING=4 +VIASH_LOGCODE_NOTICE=5 +VIASH_LOGCODE_INFO=6 +VIASH_LOGCODE_DEBUG=7 +VIASH_VERBOSITY=$VIASH_LOGCODE_NOTICE + +# ViashLog: Log events depending on the verbosity level +# usage: ViashLog 1 alert Oh no something went wrong! +# $1: required verbosity level +# $2: display tag +# $3+: messages to display +# stdout: Your input, prepended by '[$2] '. +function ViashLog { + local required_level="$1" + local display_tag="$2" + shift 2 + if [ $VIASH_VERBOSITY -ge $required_level ]; then + >&2 echo "[$display_tag]" "$@" + fi +} + +# ViashEmergency: log events when the system is unstable +# usage: ViashEmergency Oh no something went wrong. +# stdout: Your input, prepended by '[emergency] '. +function ViashEmergency { + ViashLog $VIASH_LOGCODE_EMERGENCY emergency "$@" +} + +# ViashAlert: log events when actions must be taken immediately (e.g. corrupted system database) +# usage: ViashAlert Oh no something went wrong. +# stdout: Your input, prepended by '[alert] '. +function ViashAlert { + ViashLog $VIASH_LOGCODE_ALERT alert "$@" +} + +# ViashCritical: log events when a critical condition occurs +# usage: ViashCritical Oh no something went wrong. +# stdout: Your input, prepended by '[critical] '. +function ViashCritical { + ViashLog $VIASH_LOGCODE_CRITICAL critical "$@" +} + +# ViashError: log events when an error condition occurs +# usage: ViashError Oh no something went wrong. +# stdout: Your input, prepended by '[error] '. +function ViashError { + ViashLog $VIASH_LOGCODE_ERROR error "$@" +} + +# ViashWarning: log potentially abnormal events +# usage: ViashWarning Something may have gone wrong. +# stdout: Your input, prepended by '[warning] '. +function ViashWarning { + ViashLog $VIASH_LOGCODE_WARNING warning "$@" +} + +# ViashNotice: log significant but normal events +# usage: ViashNotice This just happened. +# stdout: Your input, prepended by '[notice] '. +function ViashNotice { + ViashLog $VIASH_LOGCODE_NOTICE notice "$@" +} + +# ViashInfo: log normal events +# usage: ViashInfo This just happened. +# stdout: Your input, prepended by '[info] '. +function ViashInfo { + ViashLog $VIASH_LOGCODE_INFO info "$@" +} + +# ViashDebug: log all events, for debugging purposes +# usage: ViashDebug This just happened. +# stdout: Your input, prepended by '[debug] '. +function ViashDebug { + ViashLog $VIASH_LOGCODE_DEBUG debug "$@" +} + +# find source folder of this component +VIASH_META_RESOURCES_DIR=`ViashSourceDir ${BASH_SOURCE[0]}` + +# find the root of the built components & dependencies +VIASH_TARGET_DIR=`ViashFindTargetDir $VIASH_META_RESOURCES_DIR` + +# define meta fields +VIASH_META_NAME="yq" +VIASH_META_FUNCTIONALITY_NAME="yq" +VIASH_META_EXECUTABLE="$VIASH_META_RESOURCES_DIR/$VIASH_META_NAME" +VIASH_META_CONFIG="$VIASH_META_RESOURCES_DIR/.config.vsh.yaml" +VIASH_META_TEMP_DIR="$VIASH_TEMP" + + + +# initialise variables +VIASH_MODE='run' +VIASH_ENGINE_ID='docker' + +######## Helper functions for setting up Docker images for viash ######## +# expects: ViashDockerBuild + +# ViashDockerInstallationCheck: check whether Docker is installed correctly +# +# examples: +# ViashDockerInstallationCheck +function ViashDockerInstallationCheck { + ViashDebug "Checking whether Docker is installed" + if [ ! command -v docker &> /dev/null ]; then + ViashCritical "Docker doesn't seem to be installed. See 'https://docs.docker.com/get-docker/' for instructions." + exit 1 + fi + + ViashDebug "Checking whether the Docker daemon is running" + local save=$-; set +e + local docker_version=$(docker version --format '{{.Client.APIVersion}}' 2> /dev/null) + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashCritical "Docker daemon does not seem to be running. Try one of the following:" + ViashCritical "- Try running 'dockerd' in the command line" + ViashCritical "- See https://docs.docker.com/config/daemon/" + exit 1 + fi +} + +# ViashDockerRemoteTagCheck: check whether a Docker image is available +# on a remote. Assumes `docker login` has been performed, if relevant. +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerRemoteTagCheck python:latest +# echo $? # returns '0' +# ViashDockerRemoteTagCheck sdaizudceahifu +# echo $? # returns '1' +function ViashDockerRemoteTagCheck { + docker manifest inspect $1 > /dev/null 2> /dev/null +} + +# ViashDockerLocalTagCheck: check whether a Docker image is available locally +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# docker pull python:latest +# ViashDockerLocalTagCheck python:latest +# echo $? # returns '0' +# ViashDockerLocalTagCheck sdaizudceahifu +# echo $? # returns '1' +function ViashDockerLocalTagCheck { + [ -n "$(docker images -q $1)" ] +} + +# ViashDockerPull: pull a Docker image +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerPull python:latest +# echo $? # returns '0' +# ViashDockerPull sdaizudceahifu +# echo $? # returns '1' +function ViashDockerPull { + ViashNotice "Checking if Docker image is available at '$1'" + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + docker pull $1 && return 0 || return 1 + else + local save=$-; set +e + docker pull $1 2> /dev/null > /dev/null + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashWarning "Could not pull from '$1'. Docker image doesn't exist or is not accessible." + fi + return $out + fi +} + +# ViashDockerPush: push a Docker image +# +# $1 : image identifier with format `[registry/]image[:tag]` +# exit code $? : whether or not the image was found +# examples: +# ViashDockerPush python:latest +# echo $? # returns '0' +# ViashDockerPush sdaizudceahifu +# echo $? # returns '1' +function ViashDockerPush { + ViashNotice "Pushing image to '$1'" + local save=$-; set +e + local out + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + docker push $1 + out=$? + else + docker push $1 2> /dev/null > /dev/null + out=$? + fi + [[ $save =~ e ]] && set -e + if [ $out -eq 0 ]; then + ViashNotice "Container '$1' push succeeded." + else + ViashError "Container '$1' push errored. You might not be logged in or have the necessary permissions." + fi + return $out +} + +# ViashDockerPullElseBuild: pull a Docker image, else build it +# +# $1 : image identifier with format `[registry/]image[:tag]` +# ViashDockerBuild : a Bash function which builds a docker image, takes image identifier as argument. +# examples: +# ViashDockerPullElseBuild mynewcomponent +function ViashDockerPullElseBuild { + local save=$-; set +e + ViashDockerPull $1 + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashDockerBuild $@ + fi +} + +# ViashDockerSetup: create a Docker image, according to specified docker setup strategy +# +# $1 : image identifier with format `[registry/]image[:tag]` +# $2 : docker setup strategy, see DockerSetupStrategy.scala +# examples: +# ViashDockerSetup mynewcomponent alwaysbuild +function ViashDockerSetup { + local image_id="$1" + local setup_strategy="$2" + if [ "$setup_strategy" == "alwaysbuild" -o "$setup_strategy" == "build" -o "$setup_strategy" == "b" ]; then + ViashDockerBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayspull" -o "$setup_strategy" == "pull" -o "$setup_strategy" == "p" ]; then + ViashDockerPull $image_id + elif [ "$setup_strategy" == "alwayspullelsebuild" -o "$setup_strategy" == "pullelsebuild" ]; then + ViashDockerPullElseBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayspullelsecachedbuild" -o "$setup_strategy" == "pullelsecachedbuild" ]; then + ViashDockerPullElseBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "alwayscachedbuild" -o "$setup_strategy" == "cachedbuild" -o "$setup_strategy" == "cb" ]; then + ViashDockerBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [[ "$setup_strategy" =~ ^ifneedbe ]]; then + local save=$-; set +e + ViashDockerLocalTagCheck $image_id + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -eq 0 ]; then + ViashInfo "Image $image_id already exists" + elif [ "$setup_strategy" == "ifneedbebuild" ]; then + ViashDockerBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbecachedbuild" ]; then + ViashDockerBuild $image_id $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbepull" ]; then + ViashDockerPull $image_id + elif [ "$setup_strategy" == "ifneedbepullelsebuild" ]; then + ViashDockerPullElseBuild $image_id --no-cache $(ViashDockerBuildArgs "$engine_id") + elif [ "$setup_strategy" == "ifneedbepullelsecachedbuild" ]; then + ViashDockerPullElseBuild $image_id $(ViashDockerBuildArgs "$engine_id") + else + ViashError "Unrecognised Docker strategy: $setup_strategy" + exit 1 + fi + elif [ "$setup_strategy" == "push" -o "$setup_strategy" == "forcepush" -o "$setup_strategy" == "alwayspush" ]; then + ViashDockerPush "$image_id" + elif [ "$setup_strategy" == "pushifnotpresent" -o "$setup_strategy" == "gentlepush" -o "$setup_strategy" == "maybepush" ]; then + local save=$-; set +e + ViashDockerRemoteTagCheck $image_id + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -eq 0 ]; then + ViashNotice "Container '$image_id' exists, doing nothing." + else + ViashNotice "Container '$image_id' does not yet exist." + ViashDockerPush "$image_id" + fi + elif [ "$setup_strategy" == "donothing" -o "$setup_strategy" == "meh" ]; then + ViashNotice "Skipping setup." + else + ViashError "Unrecognised Docker strategy: $setup_strategy" + exit 1 + fi +} + +# ViashDockerCheckCommands: Check whether a docker container has the required commands +# +# $1 : image identifier with format `[registry/]image[:tag]` +# $@ : commands to verify being present +# examples: +# ViashDockerCheckCommands bash:4.0 bash ps foo +function ViashDockerCheckCommands { + local image_id="$1" + shift 1 + local commands="$@" + local save=$-; set +e + local missing # mark 'missing' as local in advance, otherwise the exit code of the command will be missing and always be '0' + missing=$(docker run --rm --entrypoint=sh "$image_id" -c "for command in $commands; do command -v \$command >/dev/null 2>&1; if [ \$? -ne 0 ]; then echo \$command; exit 1; fi; done") + local outCheck=$? + [[ $save =~ e ]] && set -e + if [ $outCheck -ne 0 ]; then + ViashError "Docker container '$image_id' does not contain command '$missing'." + exit 1 + fi +} + +# ViashDockerBuild: build a docker image +# $1 : image identifier with format `[registry/]image[:tag]` +# $... : additional arguments to pass to docker build +# $VIASH_META_TEMP_DIR : temporary directory to store dockerfile & optional resources in +# $VIASH_META_NAME : name of the component +# $VIASH_META_RESOURCES_DIR : directory containing the resources +# $VIASH_VERBOSITY : verbosity level +# exit code $? : whether or not the image was built successfully +function ViashDockerBuild { + local image_id="$1" + shift 1 + + # create temporary directory to store dockerfile & optional resources in + local tmpdir=$(mktemp -d "$VIASH_META_TEMP_DIR/dockerbuild-$VIASH_META_NAME-XXXXXX") + local dockerfile="$tmpdir/Dockerfile" + function clean_up { + rm -rf "$tmpdir" + } + trap clean_up EXIT + + # store dockerfile and resources + ViashDockerfile "$VIASH_ENGINE_ID" > "$dockerfile" + + # generate the build command + local docker_build_cmd="docker build -t '$image_id' $@ '$VIASH_META_RESOURCES_DIR' -f '$dockerfile'" + + # build the container + ViashNotice "Building container '$image_id' with Dockerfile" + ViashInfo "$docker_build_cmd" + local save=$-; set +e + if [ $VIASH_VERBOSITY -ge $VIASH_LOGCODE_INFO ]; then + eval $docker_build_cmd + else + eval $docker_build_cmd &> "$tmpdir/docker_build.log" + fi + + # check exit code + local out=$? + [[ $save =~ e ]] && set -e + if [ $out -ne 0 ]; then + ViashError "Error occurred while building container '$image_id'" + if [ $VIASH_VERBOSITY -lt $VIASH_LOGCODE_INFO ]; then + ViashError "Transcript: --------------------------------" + cat "$tmpdir/docker_build.log" + ViashError "End of transcript --------------------------" + fi + exit 1 + fi +} + +######## End of helper functions for setting up Docker images for viash ######## + +# ViashDockerFile: print the dockerfile to stdout +# $1 : engine identifier +# return : dockerfile required to run this component +# examples: +# ViashDockerFile +function ViashDockerfile { + local engine_id="$1" + + if [[ "$engine_id" == "docker" ]]; then + cat << 'VIASHDOCKER' +FROM alpine:latest +ENTRYPOINT [] +RUN apk add --no-cache bash yq-go + +RUN /usr/bin/yq --version | sed 's/.*version\sv\(.*\)/yq: "\1"/' > /var/software_versions.txt + +LABEL org.opencontainers.image.description="Companion container for running component yq" +LABEL org.opencontainers.image.created="2025-06-12T09:15:19Z" +LABEL org.opencontainers.image.source="https://github.com/mikefarah/yq" +LABEL org.opencontainers.image.revision="add30ba0f36bd8a8b07f0ba640707016d04e11b2" +LABEL org.opencontainers.image.version="gunzip" + +VIASHDOCKER + fi +} + +# ViashDockerBuildArgs: return the arguments to pass to docker build +# $1 : engine identifier +# return : arguments to pass to docker build +function ViashDockerBuildArgs { + local engine_id="$1" + + if [[ "$engine_id" == "docker" ]]; then + echo "" + fi +} + +# ViashAbsolutePath: generate absolute path from relative path +# borrowed from https://stackoverflow.com/a/21951256 +# $1 : relative filename +# return : absolute path +# examples: +# ViashAbsolutePath some_file.txt # returns /path/to/some_file.txt +# ViashAbsolutePath /foo/bar/.. # returns /foo +function ViashAbsolutePath { + local thePath + local parr + local outp + local len + if [[ ! "$1" =~ ^/ ]]; then + thePath="$PWD/$1" + else + thePath="$1" + fi + echo "$thePath" | ( + IFS=/ + read -a parr + declare -a outp + for i in "${parr[@]}"; do + case "$i" in + ''|.) continue ;; + ..) + len=${#outp[@]} + if ((len==0)); then + continue + else + unset outp[$((len-1))] + fi + ;; + *) + len=${#outp[@]} + outp[$len]="$i" + ;; + esac + done + echo /"${outp[*]}" + ) +} +# ViashDockerAutodetectMount: auto configuring docker mounts from parameters +# $1 : The parameter value +# returns : New parameter +# $VIASH_DIRECTORY_MOUNTS : Added another parameter to be passed to docker +# $VIASH_DOCKER_AUTOMOUNT_PREFIX : The prefix to be used for the automounts +# examples: +# ViashDockerAutodetectMount /path/to/bar # returns '/viash_automount/path/to/bar' +# ViashDockerAutodetectMountArg /path/to/bar # returns '--volume="/path/to:/viash_automount/path/to"' +function ViashDockerAutodetectMount { + local abs_path=$(ViashAbsolutePath "$1") + local mount_source + local base_name + if [ -d "$abs_path" ]; then + mount_source="$abs_path" + base_name="" + else + mount_source=`dirname "$abs_path"` + base_name=`basename "$abs_path"` + fi + local mount_target="$VIASH_DOCKER_AUTOMOUNT_PREFIX$mount_source" + if [ -z "$base_name" ]; then + echo "$mount_target" + else + echo "$mount_target/$base_name" + fi +} +function ViashDockerAutodetectMountArg { + local abs_path=$(ViashAbsolutePath "$1") + local mount_source + local base_name + if [ -d "$abs_path" ]; then + mount_source="$abs_path" + base_name="" + else + mount_source=`dirname "$abs_path"` + base_name=`basename "$abs_path"` + fi + local mount_target="$VIASH_DOCKER_AUTOMOUNT_PREFIX$mount_source" + ViashDebug "ViashDockerAutodetectMountArg $1 -> $mount_source -> $mount_target" + echo "--volume=\"$mount_source:$mount_target\"" +} +function ViashDockerStripAutomount { + local abs_path=$(ViashAbsolutePath "$1") + echo "${abs_path#$VIASH_DOCKER_AUTOMOUNT_PREFIX}" +} +# initialise variables +VIASH_DIRECTORY_MOUNTS=() + +# configure default docker automount prefix if it is unset +if [ -z "${VIASH_DOCKER_AUTOMOUNT_PREFIX+x}" ]; then + VIASH_DOCKER_AUTOMOUNT_PREFIX="/viash_automount" +fi + +# initialise docker variables +VIASH_DOCKER_RUN_ARGS=(-i --rm) + + +# ViashHelp: Display helpful explanation about this executable +function ViashHelp { + echo "yq gunzip" + echo "" + echo "A portable YAML, JSON, XML, CSV, TOML and properties processor" + echo "" + echo "Inputs:" + echo " --input" + echo " type: file, required parameter, file must exist" + echo " example: input.yaml" + echo " files to be processed" + echo "" + echo "Outputs:" + echo " --output" + echo " type: file, required parameter, output, file must exist" + echo " example: output.yaml" + echo " output file" + echo "" + echo "Arguments:" + echo " --eval" + echo " type: string, required parameter" + echo " example: .name = \"foo\"" + echo " expression to evaluate" + echo "" + echo " -I, --indent" + echo " type: integer" + echo " sets indent level for output (default 2)" + echo "" + echo " -p, --input_format" + echo " type: string" + echo " choices: [ auto, a, yaml, y, json, j, props, p, csv, c, tsv, t, xml, x," + echo "base64, uri, toml, shell, s, lua, l ]" + echo " parse format for input. (default \"auto\")" + echo "" + echo " -o, --output_format" + echo " type: string" + echo " choices: [ auto, a, yaml, y, json, j, props, p, csv, c, tsv, t, xml, x," + echo "base64, uri, toml, shell, s, lua, l ]" + echo " output format type. (default \"auto\")" + echo "" + echo " -P, --pretty_print" + echo " type: boolean_true" + echo " pretty print, shorthand for '... style = \"\"'" + echo "" + echo "Viash built in Computational Requirements:" + echo " ---cpus=INT" + echo " Number of CPUs to use" + echo " ---memory=STRING" + echo " Amount of memory to use. Examples: 4GB, 3MiB." + echo "" + echo "Viash built in Docker:" + echo " ---setup=STRATEGY" + echo " Setup the docker container. Options are: alwaysbuild, alwayscachedbuild, ifneedbebuild, ifneedbecachedbuild, alwayspull, alwayspullelsebuild, alwayspullelsecachedbuild, ifneedbepull, ifneedbepullelsebuild, ifneedbepullelsecachedbuild, push, pushifnotpresent, donothing." + echo " Default: ifneedbepullelsecachedbuild" + echo " ---dockerfile" + echo " Print the dockerfile to stdout." + echo " ---docker_run_args=ARG" + echo " Provide runtime arguments to Docker. See the documentation on \`docker run\` for more information." + echo " ---docker_image_id" + echo " Print the docker image id to stdout." + echo " ---debug" + echo " Enter the docker container for debugging purposes." + echo "" + echo "Viash built in Engines:" + echo " ---engine=ENGINE_ID" + echo " Specify the engine to use. Options are: docker, native." + echo " Default: docker" +} + +# initialise array +VIASH_POSITIONAL_ARGS='' + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + ViashHelp + exit + ;; + ---v|---verbose) + let "VIASH_VERBOSITY=VIASH_VERBOSITY+1" + shift 1 + ;; + ---verbosity) + VIASH_VERBOSITY="$2" + shift 2 + ;; + ---verbosity=*) + VIASH_VERBOSITY="$(ViashRemoveFlags "$1")" + shift 1 + ;; + --version) + echo "yq gunzip" + exit + ;; + --input) + [ -n "$VIASH_PAR_INPUT" ] && ViashError Bad arguments for option \'--input\': \'$VIASH_PAR_INPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --input. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --input=*) + [ -n "$VIASH_PAR_INPUT" ] && ViashError Bad arguments for option \'--input=*\': \'$VIASH_PAR_INPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT=$(ViashRemoveFlags "$1") + shift 1 + ;; + --output) + [ -n "$VIASH_PAR_OUTPUT" ] && ViashError Bad arguments for option \'--output\': \'$VIASH_PAR_OUTPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --output. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --output=*) + [ -n "$VIASH_PAR_OUTPUT" ] && ViashError Bad arguments for option \'--output=*\': \'$VIASH_PAR_OUTPUT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT=$(ViashRemoveFlags "$1") + shift 1 + ;; + --eval) + [ -n "$VIASH_PAR_EVAL" ] && ViashError Bad arguments for option \'--eval\': \'$VIASH_PAR_EVAL\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_EVAL="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --eval. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --eval=*) + [ -n "$VIASH_PAR_EVAL" ] && ViashError Bad arguments for option \'--eval=*\': \'$VIASH_PAR_EVAL\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_EVAL=$(ViashRemoveFlags "$1") + shift 1 + ;; + --indent) + [ -n "$VIASH_PAR_INDENT" ] && ViashError Bad arguments for option \'--indent\': \'$VIASH_PAR_INDENT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDENT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --indent. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --indent=*) + [ -n "$VIASH_PAR_INDENT" ] && ViashError Bad arguments for option \'--indent=*\': \'$VIASH_PAR_INDENT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDENT=$(ViashRemoveFlags "$1") + shift 1 + ;; + -I) + [ -n "$VIASH_PAR_INDENT" ] && ViashError Bad arguments for option \'-I\': \'$VIASH_PAR_INDENT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INDENT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -I. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --input_format) + [ -n "$VIASH_PAR_INPUT_FORMAT" ] && ViashError Bad arguments for option \'--input_format\': \'$VIASH_PAR_INPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT_FORMAT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --input_format. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --input_format=*) + [ -n "$VIASH_PAR_INPUT_FORMAT" ] && ViashError Bad arguments for option \'--input_format=*\': \'$VIASH_PAR_INPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT_FORMAT=$(ViashRemoveFlags "$1") + shift 1 + ;; + -p) + [ -n "$VIASH_PAR_INPUT_FORMAT" ] && ViashError Bad arguments for option \'-p\': \'$VIASH_PAR_INPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_INPUT_FORMAT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -p. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --output_format) + [ -n "$VIASH_PAR_OUTPUT_FORMAT" ] && ViashError Bad arguments for option \'--output_format\': \'$VIASH_PAR_OUTPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT_FORMAT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to --output_format. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --output_format=*) + [ -n "$VIASH_PAR_OUTPUT_FORMAT" ] && ViashError Bad arguments for option \'--output_format=*\': \'$VIASH_PAR_OUTPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT_FORMAT=$(ViashRemoveFlags "$1") + shift 1 + ;; + -o) + [ -n "$VIASH_PAR_OUTPUT_FORMAT" ] && ViashError Bad arguments for option \'-o\': \'$VIASH_PAR_OUTPUT_FORMAT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_OUTPUT_FORMAT="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to -o. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + --pretty_print) + [ -n "$VIASH_PAR_PRETTY_PRINT" ] && ViashError Bad arguments for option \'--pretty_print\': \'$VIASH_PAR_PRETTY_PRINT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_PRETTY_PRINT=true + shift 1 + ;; + -P) + [ -n "$VIASH_PAR_PRETTY_PRINT" ] && ViashError Bad arguments for option \'-P\': \'$VIASH_PAR_PRETTY_PRINT\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_PAR_PRETTY_PRINT=true + shift 1 + ;; + ---engine) + VIASH_ENGINE_ID="$2" + shift 2 + ;; + ---engine=*) + VIASH_ENGINE_ID="$(ViashRemoveFlags "$1")" + shift 1 + ;; + ---setup) + VIASH_MODE='setup' + VIASH_SETUP_STRATEGY="$2" + shift 2 + ;; + ---setup=*) + VIASH_MODE='setup' + VIASH_SETUP_STRATEGY="$(ViashRemoveFlags "$1")" + shift 1 + ;; + ---dockerfile) + VIASH_MODE='dockerfile' + shift 1 + ;; + ---docker_run_args) + VIASH_DOCKER_RUN_ARGS+=("$2") + shift 2 + ;; + ---docker_run_args=*) + VIASH_DOCKER_RUN_ARGS+=("$(ViashRemoveFlags "$1")") + shift 1 + ;; + ---docker_image_id) + VIASH_MODE='docker_image_id' + shift 1 + ;; + ---debug) + VIASH_MODE='debug' + shift 1 + ;; + ---cpus) + [ -n "$VIASH_META_CPUS" ] && ViashError Bad arguments for option \'---cpus\': \'$VIASH_META_CPUS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_CPUS="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to ---cpus. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + ---cpus=*) + [ -n "$VIASH_META_CPUS" ] && ViashError Bad arguments for option \'---cpus=*\': \'$VIASH_META_CPUS\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_CPUS=$(ViashRemoveFlags "$1") + shift 1 + ;; + ---memory) + [ -n "$VIASH_META_MEMORY" ] && ViashError Bad arguments for option \'---memory\': \'$VIASH_META_MEMORY\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_MEMORY="$2" + [ $# -lt 2 ] && ViashError Not enough arguments passed to ---memory. Use "--help" to get more information on the parameters. && exit 1 + shift 2 + ;; + ---memory=*) + [ -n "$VIASH_META_MEMORY" ] && ViashError Bad arguments for option \'---memory=*\': \'$VIASH_META_MEMORY\' \& \'$2\' - you should provide exactly one argument for this option. && exit 1 + VIASH_META_MEMORY=$(ViashRemoveFlags "$1") + shift 1 + ;; + *) # positional arg or unknown option + # since the positional args will be eval'd, can we always quote, instead of using ViashQuote + VIASH_POSITIONAL_ARGS="$VIASH_POSITIONAL_ARGS '$1'" + [[ $1 == -* ]] && ViashWarning $1 looks like a parameter but is not a defined parameter and will instead be treated as a positional argument. Use "--help" to get more information on the parameters. + shift # past argument + ;; + esac +done + +# parse positional parameters +eval set -- $VIASH_POSITIONAL_ARGS + + +if [ "$VIASH_ENGINE_ID" == "native" ] ; then + VIASH_ENGINE_TYPE='native' +elif [ "$VIASH_ENGINE_ID" == "docker" ] ; then + VIASH_ENGINE_TYPE='docker' +else + ViashError "Engine '$VIASH_ENGINE_ID' is not recognized. Options are: docker, native." + exit 1 +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # check if docker is installed properly + ViashDockerInstallationCheck + + # determine docker image id + if [[ "$VIASH_ENGINE_ID" == 'docker' ]]; then + VIASH_DOCKER_IMAGE_ID='images.viash-hub.com/vsh/toolbox/yq:gunzip' + fi + + # print dockerfile + if [ "$VIASH_MODE" == "dockerfile" ]; then + ViashDockerfile "$VIASH_ENGINE_ID" + exit 0 + + elif [ "$VIASH_MODE" == "docker_image_id" ]; then + echo "$VIASH_DOCKER_IMAGE_ID" + exit 0 + + # enter docker container + elif [[ "$VIASH_MODE" == "debug" ]]; then + VIASH_CMD="docker run --entrypoint=bash ${VIASH_DOCKER_RUN_ARGS[@]} -v '$(pwd)':/pwd --workdir /pwd -t $VIASH_DOCKER_IMAGE_ID" + ViashNotice "+ $VIASH_CMD" + eval $VIASH_CMD + exit + + # build docker image + elif [ "$VIASH_MODE" == "setup" ]; then + ViashDockerSetup "$VIASH_DOCKER_IMAGE_ID" "$VIASH_SETUP_STRATEGY" + ViashDockerCheckCommands "$VIASH_DOCKER_IMAGE_ID" 'ps' 'bash' + exit 0 + fi + + # check if docker image exists + ViashDockerSetup "$VIASH_DOCKER_IMAGE_ID" ifneedbepullelsecachedbuild + ViashDockerCheckCommands "$VIASH_DOCKER_IMAGE_ID" 'ps' 'bash' +fi + +# setting computational defaults + +# helper function for parsing memory strings +function ViashMemoryAsBytes { + local memory=`echo "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'` + local memory_regex='^([0-9]+)([kmgtp]i?b?|b)$' + if [[ $memory =~ $memory_regex ]]; then + local number=${memory/[^0-9]*/} + local symbol=${memory/*[0-9]/} + + case $symbol in + b) memory_b=$number ;; + kb|k) memory_b=$(( $number * 1000 )) ;; + mb|m) memory_b=$(( $number * 1000 * 1000 )) ;; + gb|g) memory_b=$(( $number * 1000 * 1000 * 1000 )) ;; + tb|t) memory_b=$(( $number * 1000 * 1000 * 1000 * 1000 )) ;; + pb|p) memory_b=$(( $number * 1000 * 1000 * 1000 * 1000 * 1000 )) ;; + kib|ki) memory_b=$(( $number * 1024 )) ;; + mib|mi) memory_b=$(( $number * 1024 * 1024 )) ;; + gib|gi) memory_b=$(( $number * 1024 * 1024 * 1024 )) ;; + tib|ti) memory_b=$(( $number * 1024 * 1024 * 1024 * 1024 )) ;; + pib|pi) memory_b=$(( $number * 1024 * 1024 * 1024 * 1024 * 1024 )) ;; + esac + echo "$memory_b" + fi +} +# compute memory in different units +if [ ! -z ${VIASH_META_MEMORY+x} ]; then + VIASH_META_MEMORY_B=`ViashMemoryAsBytes $VIASH_META_MEMORY` + # do not define other variables if memory_b is an empty string + if [ ! -z "$VIASH_META_MEMORY_B" ]; then + VIASH_META_MEMORY_KB=$(( ($VIASH_META_MEMORY_B+999) / 1000 )) + VIASH_META_MEMORY_MB=$(( ($VIASH_META_MEMORY_KB+999) / 1000 )) + VIASH_META_MEMORY_GB=$(( ($VIASH_META_MEMORY_MB+999) / 1000 )) + VIASH_META_MEMORY_TB=$(( ($VIASH_META_MEMORY_GB+999) / 1000 )) + VIASH_META_MEMORY_PB=$(( ($VIASH_META_MEMORY_TB+999) / 1000 )) + VIASH_META_MEMORY_KIB=$(( ($VIASH_META_MEMORY_B+1023) / 1024 )) + VIASH_META_MEMORY_MIB=$(( ($VIASH_META_MEMORY_KIB+1023) / 1024 )) + VIASH_META_MEMORY_GIB=$(( ($VIASH_META_MEMORY_MIB+1023) / 1024 )) + VIASH_META_MEMORY_TIB=$(( ($VIASH_META_MEMORY_GIB+1023) / 1024 )) + VIASH_META_MEMORY_PIB=$(( ($VIASH_META_MEMORY_TIB+1023) / 1024 )) + else + # unset memory if string is empty + unset $VIASH_META_MEMORY_B + fi +fi +# unset nproc if string is empty +if [ -z "$VIASH_META_CPUS" ]; then + unset $VIASH_META_CPUS +fi + + +# check whether required parameters exist +if [ -z ${VIASH_PAR_INPUT+x} ]; then + ViashError '--input' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_PAR_OUTPUT+x} ]; then + ViashError '--output' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_PAR_EVAL+x} ]; then + ViashError '--eval' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_NAME+x} ]; then + ViashError 'name' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then + ViashError 'functionality_name' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_RESOURCES_DIR+x} ]; then + ViashError 'resources_dir' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_EXECUTABLE+x} ]; then + ViashError 'executable' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_CONFIG+x} ]; then + ViashError 'config' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi +if [ -z ${VIASH_META_TEMP_DIR+x} ]; then + ViashError 'temp_dir' is a required argument. Use "--help" to get more information on the parameters. + exit 1 +fi + +# filling in defaults +if [ -z ${VIASH_PAR_PRETTY_PRINT+x} ]; then + VIASH_PAR_PRETTY_PRINT="false" +fi + +# check whether required files exist +if [ ! -z "$VIASH_PAR_INPUT" ] && [ ! -e "$VIASH_PAR_INPUT" ]; then + ViashError "Input file '$VIASH_PAR_INPUT' does not exist." + exit 1 +fi + +# check whether parameters values are of the right type +if [[ -n "$VIASH_PAR_INDENT" ]]; then + if ! [[ "$VIASH_PAR_INDENT" =~ ^[-+]?[0-9]+$ ]]; then + ViashError '--indent' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_PAR_PRETTY_PRINT" ]]; then + if ! [[ "$VIASH_PAR_PRETTY_PRINT" =~ ^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO)$ ]]; then + ViashError '--pretty_print' has to be a boolean_true. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_CPUS" ]]; then + if ! [[ "$VIASH_META_CPUS" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'cpus' has to be an integer. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_B" ]]; then + if ! [[ "$VIASH_META_MEMORY_B" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_b' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_KB" ]]; then + if ! [[ "$VIASH_META_MEMORY_KB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_kb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_MB" ]]; then + if ! [[ "$VIASH_META_MEMORY_MB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_mb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_GB" ]]; then + if ! [[ "$VIASH_META_MEMORY_GB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_gb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_TB" ]]; then + if ! [[ "$VIASH_META_MEMORY_TB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_tb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_PB" ]]; then + if ! [[ "$VIASH_META_MEMORY_PB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_pb' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_KIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_KIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_kib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_MIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_MIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_mib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_GIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_GIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_gib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_TIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_TIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_tib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi +if [[ -n "$VIASH_META_MEMORY_PIB" ]]; then + if ! [[ "$VIASH_META_MEMORY_PIB" =~ ^[-+]?[0-9]+$ ]]; then + ViashError 'memory_pib' has to be a long. Use "--help" to get more information on the parameters. + exit 1 + fi +fi + +# check whether value is belongs to a set of choices +if [ ! -z "$VIASH_PAR_INPUT_FORMAT" ]; then + VIASH_PAR_INPUT_FORMAT_CHOICES=("auto;a;yaml;y;json;j;props;p;csv;c;tsv;t;xml;x;base64;uri;toml;shell;s;lua;l") + IFS=';' + set -f + if ! [[ ";${VIASH_PAR_INPUT_FORMAT_CHOICES[*]};" =~ ";$VIASH_PAR_INPUT_FORMAT;" ]]; then + ViashError '--input_format' specified value of \'$VIASH_PAR_INPUT_FORMAT\' is not in the list of allowed values. Use "--help" to get more information on the parameters. + exit 1 + fi + set +f + unset IFS +fi + +if [ ! -z "$VIASH_PAR_OUTPUT_FORMAT" ]; then + VIASH_PAR_OUTPUT_FORMAT_CHOICES=("auto;a;yaml;y;json;j;props;p;csv;c;tsv;t;xml;x;base64;uri;toml;shell;s;lua;l") + IFS=';' + set -f + if ! [[ ";${VIASH_PAR_OUTPUT_FORMAT_CHOICES[*]};" =~ ";$VIASH_PAR_OUTPUT_FORMAT;" ]]; then + ViashError '--output_format' specified value of \'$VIASH_PAR_OUTPUT_FORMAT\' is not in the list of allowed values. Use "--help" to get more information on the parameters. + exit 1 + fi + set +f + unset IFS +fi + +# create parent directories of output files, if so desired +if [ ! -z "$VIASH_PAR_OUTPUT" ] && [ ! -d "$(dirname "$VIASH_PAR_OUTPUT")" ]; then + mkdir -p "$(dirname "$VIASH_PAR_OUTPUT")" +fi + +if [ "$VIASH_ENGINE_ID" == "native" ] ; then + if [ "$VIASH_MODE" == "run" ]; then + VIASH_CMD="bash" + else + ViashError "Engine '$VIASH_ENGINE_ID' does not support mode '$VIASH_MODE'." + exit 1 + fi +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # detect volumes from file arguments + VIASH_CHOWN_VARS=() +if [ ! -z "$VIASH_PAR_INPUT" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_PAR_INPUT")" ) + VIASH_PAR_INPUT=$(ViashDockerAutodetectMount "$VIASH_PAR_INPUT") +fi +if [ ! -z "$VIASH_PAR_OUTPUT" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_PAR_OUTPUT")" ) + VIASH_PAR_OUTPUT=$(ViashDockerAutodetectMount "$VIASH_PAR_OUTPUT") + VIASH_CHOWN_VARS+=( "$VIASH_PAR_OUTPUT" ) +fi +if [ ! -z "$VIASH_META_RESOURCES_DIR" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_RESOURCES_DIR")" ) + VIASH_META_RESOURCES_DIR=$(ViashDockerAutodetectMount "$VIASH_META_RESOURCES_DIR") +fi +if [ ! -z "$VIASH_META_EXECUTABLE" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_EXECUTABLE")" ) + VIASH_META_EXECUTABLE=$(ViashDockerAutodetectMount "$VIASH_META_EXECUTABLE") +fi +if [ ! -z "$VIASH_META_CONFIG" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_CONFIG")" ) + VIASH_META_CONFIG=$(ViashDockerAutodetectMount "$VIASH_META_CONFIG") +fi +if [ ! -z "$VIASH_META_TEMP_DIR" ]; then + VIASH_DIRECTORY_MOUNTS+=( "$(ViashDockerAutodetectMountArg "$VIASH_META_TEMP_DIR")" ) + VIASH_META_TEMP_DIR=$(ViashDockerAutodetectMount "$VIASH_META_TEMP_DIR") +fi + + # get unique mounts + VIASH_UNIQUE_MOUNTS=($(for val in "${VIASH_DIRECTORY_MOUNTS[@]}"; do echo "$val"; done | sort -u)) +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # change file ownership + function ViashPerformChown { + if (( ${#VIASH_CHOWN_VARS[@]} )); then + set +e + VIASH_CMD="docker run --entrypoint=bash --rm ${VIASH_UNIQUE_MOUNTS[@]} $VIASH_DOCKER_IMAGE_ID -c 'chown $(id -u):$(id -g) --silent --recursive ${VIASH_CHOWN_VARS[@]}'" + ViashDebug "+ $VIASH_CMD" + eval $VIASH_CMD + set -e + fi + } + trap ViashPerformChown EXIT +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # helper function for filling in extra docker args + if [ ! -z "$VIASH_META_MEMORY_B" ]; then + VIASH_DOCKER_RUN_ARGS+=("--memory=${VIASH_META_MEMORY_B}") + fi + if [ ! -z "$VIASH_META_CPUS" ]; then + VIASH_DOCKER_RUN_ARGS+=("--cpus=${VIASH_META_CPUS}") + fi +fi + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + VIASH_CMD="docker run --entrypoint=bash ${VIASH_DOCKER_RUN_ARGS[@]} ${VIASH_UNIQUE_MOUNTS[@]} $VIASH_DOCKER_IMAGE_ID" +fi + + +# set dependency paths + + +ViashDebug "Running command: $(echo $VIASH_CMD)" +cat << VIASHEOF | eval $VIASH_CMD +set -e +tempscript=\$(mktemp "$VIASH_META_TEMP_DIR/viash-run-yq-XXXXXX").sh +function clean_up { + rm "\$tempscript" +} +function interrupt { + echo -e "\nCTRL-C Pressed..." + exit 1 +} +trap clean_up EXIT +trap interrupt INT SIGINT +cat > "\$tempscript" << 'VIASHMAIN' +## VIASH START +# The following code has been auto-generated by Viash. +$( if [ ! -z ${VIASH_PAR_INPUT+x} ]; then echo "${VIASH_PAR_INPUT}" | sed "s#'#'\"'\"'#g;s#.*#par_input='&'#" ; else echo "# par_input="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT+x} ]; then echo "${VIASH_PAR_OUTPUT}" | sed "s#'#'\"'\"'#g;s#.*#par_output='&'#" ; else echo "# par_output="; fi ) +$( if [ ! -z ${VIASH_PAR_EVAL+x} ]; then echo "${VIASH_PAR_EVAL}" | sed "s#'#'\"'\"'#g;s#.*#par_eval='&'#" ; else echo "# par_eval="; fi ) +$( if [ ! -z ${VIASH_PAR_INDENT+x} ]; then echo "${VIASH_PAR_INDENT}" | sed "s#'#'\"'\"'#g;s#.*#par_indent='&'#" ; else echo "# par_indent="; fi ) +$( if [ ! -z ${VIASH_PAR_INPUT_FORMAT+x} ]; then echo "${VIASH_PAR_INPUT_FORMAT}" | sed "s#'#'\"'\"'#g;s#.*#par_input_format='&'#" ; else echo "# par_input_format="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT_FORMAT+x} ]; then echo "${VIASH_PAR_OUTPUT_FORMAT}" | sed "s#'#'\"'\"'#g;s#.*#par_output_format='&'#" ; else echo "# par_output_format="; fi ) +$( if [ ! -z ${VIASH_PAR_PRETTY_PRINT+x} ]; then echo "${VIASH_PAR_PRETTY_PRINT}" | sed "s#'#'\"'\"'#g;s#.*#par_pretty_print='&'#" ; else echo "# par_pretty_print="; fi ) +$( if [ ! -z ${VIASH_META_NAME+x} ]; then echo "${VIASH_META_NAME}" | sed "s#'#'\"'\"'#g;s#.*#meta_name='&'#" ; else echo "# meta_name="; fi ) +$( if [ ! -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then echo "${VIASH_META_FUNCTIONALITY_NAME}" | sed "s#'#'\"'\"'#g;s#.*#meta_functionality_name='&'#" ; else echo "# meta_functionality_name="; fi ) +$( if [ ! -z ${VIASH_META_RESOURCES_DIR+x} ]; then echo "${VIASH_META_RESOURCES_DIR}" | sed "s#'#'\"'\"'#g;s#.*#meta_resources_dir='&'#" ; else echo "# meta_resources_dir="; fi ) +$( if [ ! -z ${VIASH_META_EXECUTABLE+x} ]; then echo "${VIASH_META_EXECUTABLE}" | sed "s#'#'\"'\"'#g;s#.*#meta_executable='&'#" ; else echo "# meta_executable="; fi ) +$( if [ ! -z ${VIASH_META_CONFIG+x} ]; then echo "${VIASH_META_CONFIG}" | sed "s#'#'\"'\"'#g;s#.*#meta_config='&'#" ; else echo "# meta_config="; fi ) +$( if [ ! -z ${VIASH_META_TEMP_DIR+x} ]; then echo "${VIASH_META_TEMP_DIR}" | sed "s#'#'\"'\"'#g;s#.*#meta_temp_dir='&'#" ; else echo "# meta_temp_dir="; fi ) +$( if [ ! -z ${VIASH_META_CPUS+x} ]; then echo "${VIASH_META_CPUS}" | sed "s#'#'\"'\"'#g;s#.*#meta_cpus='&'#" ; else echo "# meta_cpus="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_B+x} ]; then echo "${VIASH_META_MEMORY_B}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_b='&'#" ; else echo "# meta_memory_b="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KB+x} ]; then echo "${VIASH_META_MEMORY_KB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_kb='&'#" ; else echo "# meta_memory_kb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MB+x} ]; then echo "${VIASH_META_MEMORY_MB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_mb='&'#" ; else echo "# meta_memory_mb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GB+x} ]; then echo "${VIASH_META_MEMORY_GB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_gb='&'#" ; else echo "# meta_memory_gb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TB+x} ]; then echo "${VIASH_META_MEMORY_TB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_tb='&'#" ; else echo "# meta_memory_tb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PB+x} ]; then echo "${VIASH_META_MEMORY_PB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_pb='&'#" ; else echo "# meta_memory_pb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KIB+x} ]; then echo "${VIASH_META_MEMORY_KIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_kib='&'#" ; else echo "# meta_memory_kib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MIB+x} ]; then echo "${VIASH_META_MEMORY_MIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_mib='&'#" ; else echo "# meta_memory_mib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GIB+x} ]; then echo "${VIASH_META_MEMORY_GIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_gib='&'#" ; else echo "# meta_memory_gib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TIB+x} ]; then echo "${VIASH_META_MEMORY_TIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_tib='&'#" ; else echo "# meta_memory_tib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PIB+x} ]; then echo "${VIASH_META_MEMORY_PIB}" | sed "s#'#'\"'\"'#g;s#.*#meta_memory_pib='&'#" ; else echo "# meta_memory_pib="; fi ) + +## VIASH END +#!/bin/sh +[[ "\$par_pretty_print" == "false" ]] && unset par_pretty_print +yq eval \\ + \${par_indent:+-I "\${par_indent}"} \\ + \${par_input_format:+-p "\${par_input_format}"} \\ + \${par_output_format:+-o "\${par_output_format}"} \\ + \${par_pretty_print:+-P} \\ + --expression "\$par_eval" \\ + --no-colors \\ + "\$par_input" > "\$par_output" +VIASHMAIN +bash "\$tempscript" & +wait "\$!" + +VIASHEOF + + +if [[ "$VIASH_ENGINE_TYPE" == "docker" ]]; then + # strip viash automount from file paths + + if [ ! -z "$VIASH_PAR_INPUT" ]; then + VIASH_PAR_INPUT=$(ViashDockerStripAutomount "$VIASH_PAR_INPUT") + fi + if [ ! -z "$VIASH_PAR_OUTPUT" ]; then + VIASH_PAR_OUTPUT=$(ViashDockerStripAutomount "$VIASH_PAR_OUTPUT") + fi + if [ ! -z "$VIASH_META_RESOURCES_DIR" ]; then + VIASH_META_RESOURCES_DIR=$(ViashDockerStripAutomount "$VIASH_META_RESOURCES_DIR") + fi + if [ ! -z "$VIASH_META_EXECUTABLE" ]; then + VIASH_META_EXECUTABLE=$(ViashDockerStripAutomount "$VIASH_META_EXECUTABLE") + fi + if [ ! -z "$VIASH_META_CONFIG" ]; then + VIASH_META_CONFIG=$(ViashDockerStripAutomount "$VIASH_META_CONFIG") + fi + if [ ! -z "$VIASH_META_TEMP_DIR" ]; then + VIASH_META_TEMP_DIR=$(ViashDockerStripAutomount "$VIASH_META_TEMP_DIR") + fi +fi + + +# check whether required files exist +if [ ! -z "$VIASH_PAR_OUTPUT" ] && [ ! -e "$VIASH_PAR_OUTPUT" ]; then + ViashError "Output file '$VIASH_PAR_OUTPUT' does not exist." + exit 1 +fi + + +exit 0 diff --git a/target/nextflow/bgzip/.config.vsh.yaml b/target/nextflow/bgzip/.config.vsh.yaml new file mode 100644 index 0000000..9f90671 --- /dev/null +++ b/target/nextflow/bgzip/.config.vsh.yaml @@ -0,0 +1,267 @@ +name: "bgzip" +version: "gunzip" +argument_groups: +- name: "Inputs" + arguments: + - type: "file" + name: "--input" + description: "file to be compressed or decompressed" + info: null + must_exist: true + create_parent: true + required: true + direction: "input" + multiple: false + multiple_sep: ";" +- name: "Outputs" + arguments: + - type: "file" + name: "--output" + description: "compressed or decompressed output" + info: null + must_exist: true + create_parent: true + required: true + direction: "output" + multiple: false + multiple_sep: ";" + - type: "file" + name: "--index_name" + alternatives: + - "-I" + description: "name of BGZF index file [file.gz.gzi]" + info: null + must_exist: true + create_parent: true + required: false + direction: "output" + multiple: false + multiple_sep: ";" +- name: "Arguments" + arguments: + - type: "integer" + name: "--offset" + alternatives: + - "-b" + description: "decompress at virtual file pointer (0-based uncompressed offset)" + info: null + required: false + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--decompress" + alternatives: + - "-d" + description: "decompress the input file" + info: null + direction: "input" + - type: "boolean_true" + name: "--rebgzip" + alternatives: + - "-g" + description: "use an index file to bgzip a file" + info: null + direction: "input" + - type: "boolean_true" + name: "--index" + alternatives: + - "-i" + description: "compress and create BGZF index" + info: null + direction: "input" + - type: "integer" + name: "--compress_level" + alternatives: + - "-l" + description: "compression level to use when compressing; 0 to 9, or -1 for default\ + \ [-1]" + info: null + required: false + min: -1 + max: 9 + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--reindex" + alternatives: + - "-r" + description: "(re)index the output file" + info: null + direction: "input" + - type: "integer" + name: "--size" + alternatives: + - "-s" + description: "decompress INT bytes (uncompressed size)" + info: null + required: false + min: 0 + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--test" + alternatives: + - "-t" + description: "test integrity of compressed file" + info: null + direction: "input" + - type: "boolean_true" + name: "--binary" + description: "Don't align blocks with text lines" + info: null + direction: "input" +resources: +- type: "bash_script" + path: "script.sh" + is_executable: true +description: "Block compression/decompression utility" +test_resources: +- type: "bash_script" + path: "test.sh" + is_executable: true +- type: "file" + path: "test_data" +info: null +status: "enabled" +scope: + image: "public" + target: "public" +requirements: + commands: + - "ps" +license: "MIT" +references: + doi: + - "10.1093/gigascience/giab007" +links: + repository: "https://github.com/samtools/htslib" + homepage: "https://www.htslib.org/" + documentation: "https://www.htslib.org/doc/bgzip.html" +runners: +- type: "executable" + id: "executable" + docker_setup_strategy: "ifneedbepullelsecachedbuild" +- type: "nextflow" + id: "nextflow" + directives: + tag: "$id" + auto: + simplifyInput: true + simplifyOutput: false + transcript: false + publish: false + config: + labels: + mem1gb: "memory = 1000000000.B" + mem2gb: "memory = 2000000000.B" + mem5gb: "memory = 5000000000.B" + mem10gb: "memory = 10000000000.B" + mem20gb: "memory = 20000000000.B" + mem50gb: "memory = 50000000000.B" + mem100gb: "memory = 100000000000.B" + mem200gb: "memory = 200000000000.B" + mem500gb: "memory = 500000000000.B" + mem1tb: "memory = 1000000000000.B" + mem2tb: "memory = 2000000000000.B" + mem5tb: "memory = 5000000000000.B" + mem10tb: "memory = 10000000000000.B" + mem20tb: "memory = 20000000000000.B" + mem50tb: "memory = 50000000000000.B" + mem100tb: "memory = 100000000000000.B" + mem200tb: "memory = 200000000000000.B" + mem500tb: "memory = 500000000000000.B" + mem1gib: "memory = 1073741824.B" + mem2gib: "memory = 2147483648.B" + mem4gib: "memory = 4294967296.B" + mem8gib: "memory = 8589934592.B" + mem16gib: "memory = 17179869184.B" + mem32gib: "memory = 34359738368.B" + mem64gib: "memory = 68719476736.B" + mem128gib: "memory = 137438953472.B" + mem256gib: "memory = 274877906944.B" + mem512gib: "memory = 549755813888.B" + mem1tib: "memory = 1099511627776.B" + mem2tib: "memory = 2199023255552.B" + mem4tib: "memory = 4398046511104.B" + mem8tib: "memory = 8796093022208.B" + mem16tib: "memory = 17592186044416.B" + mem32tib: "memory = 35184372088832.B" + mem64tib: "memory = 70368744177664.B" + mem128tib: "memory = 140737488355328.B" + mem256tib: "memory = 281474976710656.B" + mem512tib: "memory = 562949953421312.B" + cpu1: "cpus = 1" + cpu2: "cpus = 2" + cpu5: "cpus = 5" + cpu10: "cpus = 10" + cpu20: "cpus = 20" + cpu50: "cpus = 50" + cpu100: "cpus = 100" + cpu200: "cpus = 200" + cpu500: "cpus = 500" + cpu1000: "cpus = 1000" + debug: false + container: "docker" +engines: +- type: "docker" + id: "docker" + image: "quay.io/biocontainers/htslib:1.19--h81da01d_0" + target_registry: "images.viash-hub.com" + target_tag: "gunzip" + namespace_separator: "/" + setup: + - type: "docker" + run: + - "bgzip -h | grep 'Version:' 2>&1 | sed 's/Version:\\s\\(.*\\)/bgzip: \"\\1\"\ + /' > /var/software_versions.txt\n" + entrypoint: [] + cmd: null +- type: "native" + id: "native" +build_info: + config: "src/bgzip/config.vsh.yaml" + runner: "nextflow" + engine: "docker|native" + output: "target/nextflow/bgzip" + executable: "target/nextflow/bgzip/main.nf" + viash_version: "0.9.4" + git_commit: "add30ba0f36bd8a8b07f0ba640707016d04e11b2" + git_remote: "https://github.com/viash-hub/toolbox" +package_config: + name: "toolbox" + version: "gunzip" + summary: "A collection of curated command-line tools for general IT tasks, built\ + \ with Viash.\n" + description: "`toolbox` provides a versatile suite of IT components, following the\ + \ robust Viash (https://viash.io) framework.\nThis package focuses on delivering\ + \ reliable, standalone tools that can be easily integrated into larger computational\ + \ workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**,\ + \ and adherence to **best practices** in component creation. Key features of `toolbox`\ + \ components include:\n\n* **Standalone & Nextflow Ready:** Execute components\ + \ directly from the command line or seamlessly incorporate them into Nextflow\ + \ workflows.\n* **High Quality Standards:**\n * Comprehensive documentation\ + \ for each component and its parameters.\n * Full exposure of the underlying\ + \ tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure\ + \ consistent environments and manage dependencies, leading to enhanced reproducibility.\n\ + \ * Unit tested to verify functionality and ensure reliability.\n" + info: null + viash_version: "0.9.4" + source: "src" + target: "target" + config_mods: + - ".requirements.commands := ['ps']\n" + - ".engines += { type: \"native\" }" + - ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'" + - ".engines[.type == 'docker'].target_tag := 'gunzip'" + keywords: + - "toolbox" + - "command-line" + - "tools" + license: "MIT" + organization: "vsh" + links: + repository: "https://github.com/viash-hub/toolbox" + issue_tracker: "https://github.com/viash-hub/toolbox/issues" diff --git a/target/nextflow/bgzip/main.nf b/target/nextflow/bgzip/main.nf new file mode 100644 index 0000000..0dbc807 --- /dev/null +++ b/target/nextflow/bgzip/main.nf @@ -0,0 +1,3917 @@ +// bgzip gunzip +// +// This wrapper script is auto-generated by viash 0.9.4 and is thus a derivative +// work thereof. This software comes with ABSOLUTELY NO WARRANTY from Data +// Intuitive. +// +// The component may contain files which fall under a different license. The +// authors of this component should specify the license in the header of such +// files, or include a separate license file detailing the licenses of all included +// files. + +//////////////////////////// +// VDSL3 helper functions // +//////////////////////////// + +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_checkArgumentType.nf' +class UnexpectedArgumentTypeException extends Exception { + String errorIdentifier + String stage + String plainName + String expectedClass + String foundClass + + // ${key ? " in module '$key'" : ""}${id ? " id '$id'" : ""} + UnexpectedArgumentTypeException(String errorIdentifier, String stage, String plainName, String expectedClass, String foundClass) { + super("Error${errorIdentifier ? " $errorIdentifier" : ""}:${stage ? " $stage" : "" } argument '${plainName}' has the wrong type. " + + "Expected type: ${expectedClass}. Found type: ${foundClass}") + this.errorIdentifier = errorIdentifier + this.stage = stage + this.plainName = plainName + this.expectedClass = expectedClass + this.foundClass = foundClass + } +} + +/** + * Checks if the given value is of the expected type. If not, an exception is thrown. + * + * @param stage The stage of the argument (input or output) + * @param par The parameter definition + * @param value The value to check + * @param errorIdentifier The identifier to use in the error message + * @return The value, if it is of the expected type + * @throws UnexpectedArgumentTypeException If the value is not of the expected type +*/ +def _checkArgumentType(String stage, Map par, Object value, String errorIdentifier) { + // expectedClass will only be != null if value is not of the expected type + def expectedClass = null + def foundClass = null + + // todo: split if need be + + if (!par.required && value == null) { + expectedClass = null + } else if (par.multiple) { + if (value !instanceof Collection) { + value = [value] + } + + // split strings + value = value.collectMany{ val -> + if (val instanceof String) { + // collect() to ensure that the result is a List and not simply an array + val.split(par.multiple_sep).collect() + } else { + [val] + } + } + + // process globs + if (par.type == "file" && par.direction == "input") { + value = value.collect{ it instanceof String ? file(it, hidden: true) : it }.flatten() + } + + // check types of elements in list + try { + value = value.collect { listVal -> + _checkArgumentType(stage, par + [multiple: false], listVal, errorIdentifier) + } + } catch (UnexpectedArgumentTypeException e) { + expectedClass = "List[${e.expectedClass}]" + foundClass = "List[${e.foundClass}]" + } + } else if (par.type == "string") { + // cast to string if need be. only cast if the value is a GString + if (value instanceof GString) { + value = value as String + } + expectedClass = value instanceof String ? null : "String" + } else if (par.type == "integer") { + // cast to integer if need be + if (value !instanceof Integer) { + try { + value = value as Integer + } catch (NumberFormatException e) { + expectedClass = "Integer" + } + } + } else if (par.type == "long") { + // cast to long if need be + if (value !instanceof Long) { + try { + value = value as Long + } catch (NumberFormatException e) { + expectedClass = "Long" + } + } + } else if (par.type == "double") { + // cast to double if need be + if (value !instanceof Double) { + try { + value = value as Double + } catch (NumberFormatException e) { + expectedClass = "Double" + } + } + } else if (par.type == "float") { + // cast to float if need be + if (value !instanceof Float) { + try { + value = value as Float + } catch (NumberFormatException e) { + expectedClass = "Float" + } + } + } else if (par.type == "boolean" | par.type == "boolean_true" | par.type == "boolean_false") { + // cast to boolean if need be + if (value !instanceof Boolean) { + try { + value = value as Boolean + } catch (Exception e) { + expectedClass = "Boolean" + } + } + } else if (par.type == "file" && (par.direction == "input" || stage == "output")) { + // cast to path if need be + if (value instanceof String) { + value = file(value, hidden: true) + } + if (value instanceof File) { + value = value.toPath() + } + expectedClass = value instanceof Path ? null : "Path" + } else if (par.type == "file" && stage == "input" && par.direction == "output") { + // cast to string if need be + if (value !instanceof String) { + try { + value = value as String + } catch (Exception e) { + expectedClass = "String" + } + } + } else { + // didn't find a match for par.type + expectedClass = par.type + } + + if (expectedClass != null) { + if (foundClass == null) { + foundClass = value.getClass().getName() + } + throw new UnexpectedArgumentTypeException(errorIdentifier, stage, par.plainName, expectedClass, foundClass) + } + + return value +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processInputValues.nf' +Map _processInputValues(Map inputs, Map config, String id, String key) { + if (!workflow.stubRun) { + config.allArguments.each { arg -> + if (arg.required && arg.direction == "input") { + assert inputs.containsKey(arg.plainName) && inputs.get(arg.plainName) != null : + "Error in module '${key}' id '${id}': required input argument '${arg.plainName}' is missing" + } + } + + inputs = inputs.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid input argument" + + value = _checkArgumentType("input", par, value, "in module '$key' id '$id'") + + [ name, value ] + } + } + return inputs +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processOutputValues.nf' +Map _checkValidOutputArgument(Map outputs, Map config, String id, String key) { + if (!workflow.stubRun) { + outputs = outputs.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && it.direction == "output" } + assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid output argument" + + value = _checkArgumentType("output", par, value, "in module '$key' id '$id'") + + [ name, value ] + } + } + return outputs +} + +void _checkAllRequiredOuputsPresent(Map outputs, Map config, String id, String key) { + if (!workflow.stubRun) { + config.allArguments.each { arg -> + if (arg.direction == "output" && arg.required) { + assert outputs.containsKey(arg.plainName) && outputs.get(arg.plainName) != null : + "Error in module '${key}' id '${id}': required output argument '${arg.plainName}' is missing" + } + } + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/IDChecker.nf' +class IDChecker { + final def items = [] as Set + + @groovy.transform.WithWriteLock + boolean observe(String item) { + if (items.contains(item)) { + return false + } else { + items << item + return true + } + } + + @groovy.transform.WithReadLock + boolean contains(String item) { + return items.contains(item) + } + + @groovy.transform.WithReadLock + Set getItems() { + return items.clone() + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_checkUniqueIds.nf' + +/** + * Check if the ids are unique across parameter sets + * + * @param parameterSets a list of parameter sets. + */ +private void _checkUniqueIds(List>> parameterSets) { + def ppIds = parameterSets.collect{it[0]} + assert ppIds.size() == ppIds.unique().size() : "All argument sets should have unique ids. Detected ids: $ppIds" +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_getChild.nf' + +// helper functions for reading params from file // +def _getChild(parent, child) { + if (child.contains("://") || java.nio.file.Paths.get(child).isAbsolute()) { + child + } else { + def parentAbsolute = java.nio.file.Paths.get(parent).toAbsolutePath().toString() + parentAbsolute.replaceAll('/[^/]*$', "/") + child + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_parseParamList.nf' +/** + * Figure out the param list format based on the file extension + * + * @param param_list A String containing the path to the parameter list file. + * + * @return A String containing the format of the parameter list file. + */ +def _paramListGuessFormat(param_list) { + if (param_list !instanceof String) { + "asis" + } else if (param_list.endsWith(".csv")) { + "csv" + } else if (param_list.endsWith(".json") || param_list.endsWith(".jsn")) { + "json" + } else if (param_list.endsWith(".yaml") || param_list.endsWith(".yml")) { + "yaml" + } else { + "yaml_blob" + } +} + + +/** + * Read the param list + * + * @param param_list One of the following: + * - A String containing the path to the parameter list file (csv, json or yaml), + * - A yaml blob of a list of maps (yaml_blob), + * - Or a groovy list of maps (asis). + * @param config A Map of the Viash configuration. + * + * @return A List of Maps containing the parameters. + */ +def _parseParamList(param_list, Map config) { + // first determine format by extension + def paramListFormat = _paramListGuessFormat(param_list) + + def paramListPath = (paramListFormat != "asis" && paramListFormat != "yaml_blob") ? + file(param_list, hidden: true) : + null + + // get the correct parser function for the detected params_list format + def paramSets = [] + if (paramListFormat == "asis") { + paramSets = param_list + } else if (paramListFormat == "yaml_blob") { + paramSets = readYamlBlob(param_list) + } else if (paramListFormat == "yaml") { + paramSets = readYaml(paramListPath) + } else if (paramListFormat == "json") { + paramSets = readJson(paramListPath) + } else if (paramListFormat == "csv") { + paramSets = readCsv(paramListPath) + } else { + error "Format of provided --param_list not recognised.\n" + + "Found: '$paramListFormat'.\n" + + "Expected: a csv file, a json file, a yaml file,\n" + + "a yaml blob or a groovy list of maps." + } + + // data checks + assert paramSets instanceof List: "--param_list should contain a list of maps" + for (value in paramSets) { + assert value instanceof Map: "--param_list should contain a list of maps" + } + + // id is argument + def idIsArgument = config.allArguments.any{it.plainName == "id"} + + // Reformat from List to List> by adding the ID as first element of a Tuple2 + paramSets = paramSets.collect({ data -> + def id = data.id + if (!idIsArgument) { + data = data.findAll{k, v -> k != "id"} + } + [id, data] + }) + + // Split parameters with 'multiple: true' + paramSets = paramSets.collect({ id, data -> + data = _splitParams(data, config) + [id, data] + }) + + // The paths of input files inside a param_list file may have been specified relatively to the + // location of the param_list file. These paths must be made absolute. + if (paramListPath) { + paramSets = paramSets.collect({ id, data -> + def new_data = data.collectEntries{ parName, parValue -> + def par = config.allArguments.find{it.plainName == parName} + if (par && par.type == "file" && par.direction == "input") { + if (parValue instanceof Collection) { + parValue = parValue.collectMany{path -> + def x = _resolveSiblingIfNotAbsolute(path, paramListPath) + x instanceof Collection ? x : [x] + } + } else { + parValue = _resolveSiblingIfNotAbsolute(parValue, paramListPath) + } + } + [parName, parValue] + } + [id, new_data] + }) + } + + return paramSets +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_splitParams.nf' +/** + * Split parameters for arguments that accept multiple values using their separator + * + * @param paramList A Map containing parameters to split. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A Map of parameters where the parameter values have been split into a list using + * their seperator. + */ +Map _splitParams(Map parValues, Map config){ + def parsedParamValues = parValues.collectEntries { parName, parValue -> + def parameterSettings = config.allArguments.find({it.plainName == parName}) + + if (!parameterSettings) { + // if argument is not found, do not alter + return [parName, parValue] + } + if (parameterSettings.multiple) { // Check if parameter can accept multiple values + if (parValue instanceof Collection) { + parValue = parValue.collect{it instanceof String ? it.split(parameterSettings.multiple_sep) : it } + } else if (parValue instanceof String) { + parValue = parValue.split(parameterSettings.multiple_sep) + } else if (parValue == null) { + parValue = [] + } else { + parValue = [ parValue ] + } + parValue = parValue.flatten() + } + // For all parameters check if multiple values are only passed for + // arguments that allow it. Quietly simplify lists of length 1. + if (!parameterSettings.multiple && parValue instanceof Collection) { + assert parValue.size() == 1 : + "Error: argument ${parName} has too many values.\n" + + " Expected amount: 1. Found: ${parValue.size()}" + parValue = parValue[0] + } + [parName, parValue] + } + return parsedParamValues +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/channelFromParams.nf' +/** + * Parse nextflow parameters based on settings defined in a viash config. + * Return a list of parameter sets, each parameter set corresponding to + * an event in a nextflow channel. The output from this function can be used + * with Channel.fromList to create a nextflow channel with Vdsl3 formatted + * events. + * + * This function performs: + * - A filtering of the params which can be found in the config file. + * - Process the params_list argument which allows a user to to initialise + * a Vsdl3 channel with multiple parameter sets. Possible formats are + * csv, json, yaml, or simply a yaml_blob. A csv should have column names + * which correspond to the different arguments of this pipeline. A json or a yaml + * file should be a list of maps, each of which has keys corresponding to the + * arguments of the pipeline. A yaml blob can also be passed directly as a parameter. + * When passing a csv, json or yaml, relative path names are relativized to the + * location of the parameter file. + * - Combine the parameter sets into a vdsl3 Channel. + * + * @param params Input parameters. Can optionaly contain a 'param_list' key that + * provides a list of arguments that can be split up into multiple events + * in the output channel possible formats of param_lists are: a csv file, + * json file, a yaml file or a yaml blob. Each parameters set (event) must + * have a unique ID. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A list of parameters with the first element of the event being + * the event ID and the second element containing a map of the parsed parameters. + */ + +private List>> _paramsToParamSets(Map params, Map config){ + // todo: fetch key from run args + def key_ = config.name + + /* parse regular parameters (not in param_list) */ + /*************************************************/ + def globalParams = config.allArguments + .findAll { params.containsKey(it.plainName) } + .collectEntries { [ it.plainName, params[it.plainName] ] } + def globalID = params.get("id", null) + + /* process params_list arguments */ + /*********************************/ + def paramList = params.containsKey("param_list") && params.param_list != null ? + params.param_list : [] + // if (paramList instanceof String) { + // paramList = [paramList] + // } + // def paramSets = paramList.collectMany{ _parseParamList(it, config) } + // TODO: be able to process param_list when it is a list of strings + def paramSets = _parseParamList(paramList, config) + if (paramSets.isEmpty()) { + paramSets = [[null, [:]]] + } + + /* combine arguments into channel */ + /**********************************/ + def processedParams = paramSets.indexed().collect{ index, tup -> + // Process ID + def id = tup[0] ?: globalID + + if (workflow.stubRun && !id) { + // if stub run, explicitly add an id if missing + id = "stub${index}" + } + assert id != null: "Each parameter set should have at least an 'id'" + + // Process params + def parValues = globalParams + tup[1] + // // Remove parameters which are null, if the default is also null + // parValues = parValues.collectEntries{paramName, paramValue -> + // parameterSettings = config.functionality.allArguments.find({it.plainName == paramName}) + // if ( paramValue != null || parameterSettings.get("default", null) != null ) { + // [paramName, paramValue] + // } + // } + parValues = parValues.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + assert par != null : "Error in module '${key_}' id '${id}': '${name}' is not a valid input argument" + + if (par == null) { + return [:] + } + value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'") + + [ name, value ] + } + + [id, parValues] + } + + // Check if ids (first element of each list) is unique + _checkUniqueIds(processedParams) + return processedParams +} + +/** + * Parse nextflow parameters based on settings defined in a viash config + * and return a nextflow channel. + * + * @param params Input parameters. Can optionaly contain a 'param_list' key that + * provides a list of arguments that can be split up into multiple events + * in the output channel possible formats of param_lists are: a csv file, + * json file, a yaml file or a yaml blob. Each parameters set (event) must + * have a unique ID. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A nextflow Channel with events. Events are formatted as a tuple that contains + * first contains the ID of the event and as second element holds a parameter map. + * + * + */ +def channelFromParams(Map params, Map config) { + def processedParams = _paramsToParamSets(params, config) + return Channel.fromList(processedParams) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/checkUniqueIds.nf' +def checkUniqueIds(Map args) { + def stopOnError = args.stopOnError == null ? args.stopOnError : true + + def idChecker = new IDChecker() + + return filter { tup -> + if (!idChecker.observe(tup[0])) { + if (stopOnError) { + error "Duplicate id: ${tup[0]}" + } else { + log.warn "Duplicate id: ${tup[0]}, removing duplicate entry" + return false + } + } + return true + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/preprocessInputs.nf' +// This helper file will be deprecated soon +preprocessInputsDeprecationWarningPrinted = false + +def preprocessInputsDeprecationWarning() { + if (!preprocessInputsDeprecationWarningPrinted) { + preprocessInputsDeprecationWarningPrinted = true + System.err.println("Warning: preprocessInputs() is deprecated and will be removed in Viash 0.9.0.") + } +} + +/** + * Generate a nextflow Workflow that allows processing a channel of + * Vdsl3 formatted events and apply a Viash config to them: + * - Gather default parameters from the Viash config and make + * sure that they are correctly formatted (see applyConfig method). + * - Format the input parameters (also using the applyConfig method). + * - Apply the default parameter to the input parameters. + * - Do some assertions: + * ~ Check if the event IDs in the channel are unique. + * + * The events in the channel are formatted as tuples, with the + * first element of the tuples being a unique id of the parameter set, + * and the second element containg the the parameters themselves. + * Optional extra elements of the tuples will be passed to the output as is. + * + * @param args A map that must contain a 'config' key that points + * to a parsed config (see readConfig()). Optionally, a + * 'key' key can be provided which can be used to create a unique + * name for the workflow process. + * + * @return A workflow that allows processing a channel of Vdsl3 formatted events + * and apply a Viash config to them. + */ +def preprocessInputs(Map args) { + preprocessInputsDeprecationWarning() + + def config = args.config + assert config instanceof Map : + "Error in preprocessInputs: config must be a map. " + + "Expected class: Map. Found: config.getClass() is ${config.getClass()}" + def key_ = args.key ?: config.name + + // Get different parameter types (used throughout this function) + def defaultArgs = config.allArguments + .findAll { it.containsKey("default") } + .collectEntries { [ it.plainName, it.default ] } + + map { tup -> + def id = tup[0] + def data = tup[1] + def passthrough = tup.drop(2) + + def new_data = (defaultArgs + data).collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + + if (par != null) { + value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'") + } + + [ name, value ] + } + + [ id, new_data ] + passthrough + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runComponents.nf' +/** + * Run a list of components on a stream of data. + * + * @param components: list of Viash VDSL3 modules to run + * @param fromState: a closure, a map or a list of keys to extract from the input data. + * If a closure, it will be called with the id, the data and the component config. + * @param toState: a closure, a map or a list of keys to extract from the output data + * If a closure, it will be called with the id, the output data, the old state and the component config. + * @param filter: filter function to apply to the input. + * It will be called with the id, the data and the component config. + * @param id: id to use for the output data + * If a closure, it will be called with the id, the data and the component config. + * @param auto: auto options to pass to the components + * + * @return: a workflow that runs the components + **/ +def runComponents(Map args) { + log.warn("runComponents is deprecated, use runEach instead") + assert args.components: "runComponents should be passed a list of components to run" + + def components_ = args.components + if (components_ !instanceof List) { + components_ = [ components_ ] + } + assert components_.size() > 0: "pass at least one component to runComponents" + + def fromState_ = args.fromState + def toState_ = args.toState + def filter_ = args.filter + def id_ = args.id + + workflow runComponentsWf { + take: input_ch + main: + + // generate one channel per method + out_chs = components_.collect{ comp_ -> + def comp_config = comp_.config + + def filter_ch = filter_ + ? input_ch | filter{tup -> + filter_(tup[0], tup[1], comp_config) + } + : input_ch + def id_ch = id_ + ? filter_ch | map{tup -> + // def new_id = id_(tup[0], tup[1], comp_config) + def new_id = tup[0] + if (id_ instanceof String) { + new_id = id_ + } else if (id_ instanceof Closure) { + new_id = id_(new_id, tup[1], comp_config) + } + [new_id] + tup.drop(1) + } + : filter_ch + def data_ch = id_ch | map{tup -> + def new_data = tup[1] + if (fromState_ instanceof Map) { + new_data = fromState_.collectEntries{ key0, key1 -> + [key0, new_data[key1]] + } + } else if (fromState_ instanceof List) { + new_data = fromState_.collectEntries{ key -> + [key, new_data[key]] + } + } else if (fromState_ instanceof Closure) { + new_data = fromState_(tup[0], new_data, comp_config) + } + tup.take(1) + [new_data] + tup.drop(1) + } + def out_ch = data_ch + | comp_.run( + auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false] + ) + def post_ch = toState_ + ? out_ch | map{tup -> + def output = tup[1] + def old_state = tup[2] + def new_state = null + if (toState_ instanceof Map) { + new_state = old_state + toState_.collectEntries{ key0, key1 -> + [key0, output[key1]] + } + } else if (toState_ instanceof List) { + new_state = old_state + toState_.collectEntries{ key -> + [key, output[key]] + } + } else if (toState_ instanceof Closure) { + new_state = toState_(tup[0], output, old_state, comp_config) + } + [tup[0], new_state] + tup.drop(3) + } + : out_ch + + post_ch + } + + // mix all results + output_ch = + (out_chs.size == 1) + ? out_chs[0] + : out_chs[0].mix(*out_chs.drop(1)) + + emit: output_ch + } + + return runComponentsWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runEach.nf' +/** + * Run a list of components on a stream of data. + * + * @param components: list of Viash VDSL3 modules to run + * @param fromState: a closure, a map or a list of keys to extract from the input data. + * If a closure, it will be called with the id, the data and the component itself. + * @param toState: a closure, a map or a list of keys to extract from the output data + * If a closure, it will be called with the id, the output data, the old state and the component itself. + * @param filter: filter function to apply to the input. + * It will be called with the id, the data and the component itself. + * @param id: id to use for the output data + * If a closure, it will be called with the id, the data and the component itself. + * @param auto: auto options to pass to the components + * + * @return: a workflow that runs the components + **/ +def runEach(Map args) { + assert args.components: "runEach should be passed a list of components to run" + + def components_ = args.components + if (components_ !instanceof List) { + components_ = [ components_ ] + } + assert components_.size() > 0: "pass at least one component to runEach" + + def fromState_ = args.fromState + def toState_ = args.toState + def filter_ = args.filter + def runIf_ = args.runIf + def id_ = args.id + + assert !runIf_ || runIf_ instanceof Closure: "runEach: must pass a Closure to runIf." + + workflow runEachWf { + take: input_ch + main: + + // generate one channel per method + out_chs = components_.collect{ comp_ -> + def filter_ch = filter_ + ? input_ch | filter{tup -> + filter_(tup[0], tup[1], comp_) + } + : input_ch + def id_ch = id_ + ? filter_ch | map{tup -> + def new_id = id_ + if (new_id instanceof Closure) { + new_id = new_id(tup[0], tup[1], comp_) + } + assert new_id instanceof String : "Error in runEach: id should be a String or a Closure that returns a String. Expected: id instanceof String. Found: ${new_id.getClass()}" + [new_id] + tup.drop(1) + } + : filter_ch + def chPassthrough = null + def chRun = null + if (runIf_) { + def idRunIfBranch = id_ch.branch{ tup -> + run: runIf_(tup[0], tup[1], comp_) + passthrough: true + } + chPassthrough = idRunIfBranch.passthrough + chRun = idRunIfBranch.run + } else { + chRun = id_ch + chPassthrough = Channel.empty() + } + def data_ch = chRun | map{tup -> + def new_data = tup[1] + if (fromState_ instanceof Map) { + new_data = fromState_.collectEntries{ key0, key1 -> + [key0, new_data[key1]] + } + } else if (fromState_ instanceof List) { + new_data = fromState_.collectEntries{ key -> + [key, new_data[key]] + } + } else if (fromState_ instanceof Closure) { + new_data = fromState_(tup[0], new_data, comp_) + } + tup.take(1) + [new_data] + tup.drop(1) + } + def out_ch = data_ch + | comp_.run( + auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false] + ) + def post_ch = toState_ + ? out_ch | map{tup -> + def output = tup[1] + def old_state = tup[2] + def new_state = null + if (toState_ instanceof Map) { + new_state = old_state + toState_.collectEntries{ key0, key1 -> + [key0, output[key1]] + } + } else if (toState_ instanceof List) { + new_state = old_state + toState_.collectEntries{ key -> + [key, output[key]] + } + } else if (toState_ instanceof Closure) { + new_state = toState_(tup[0], output, old_state, comp_) + } + [tup[0], new_state] + tup.drop(3) + } + : out_ch + + def return_ch = post_ch + | concat(chPassthrough) + + return_ch + } + + // mix all results + output_ch = + (out_chs.size == 1) + ? out_chs[0] + : out_chs[0].mix(*out_chs.drop(1)) + + emit: output_ch + } + + return runEachWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/safeJoin.nf' +/** + * Join sourceChannel to targetChannel + * + * This function joins the sourceChannel to the targetChannel. + * However, each id in the targetChannel must be present in the + * sourceChannel. If _meta.join_id exists in the targetChannel, that is + * used as an id instead. If the id doesn't match any id in the sourceChannel, + * an error is thrown. + */ + +def safeJoin(targetChannel, sourceChannel, key) { + def sourceIDs = new IDChecker() + + def sourceCheck = sourceChannel + | map { tup -> + sourceIDs.observe(tup[0]) + tup + } + def targetCheck = targetChannel + | map { tup -> + def id = tup[0] + + if (!sourceIDs.contains(id)) { + error ( + "Error in module '${key}' when merging output with original state.\n" + + " Reason: output with id '${id}' could not be joined with source channel.\n" + + " If the IDs in the output channel differ from the input channel,\n" + + " please set `tup[1]._meta.join_id to the original ID.\n" + + " Original IDs in input channel: ['${sourceIDs.getItems().join("', '")}'].\n" + + " Unexpected ID in the output channel: '${id}'.\n" + + " Example input event: [\"id\", [input: file(...)]],\n" + + " Example output event: [\"newid\", [output: file(...), _meta: [join_id: \"id\"]]]" + ) + } + // TODO: add link to our documentation on how to fix this + + tup + } + + sourceCheck.cross(targetChannel) + | map{ left, right -> + right + left.drop(1) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/_processArgument.nf' +def _processArgument(arg) { + arg.multiple = arg.multiple != null ? arg.multiple : false + arg.required = arg.required != null ? arg.required : false + arg.direction = arg.direction != null ? arg.direction : "input" + arg.multiple_sep = arg.multiple_sep != null ? arg.multiple_sep : ";" + arg.plainName = arg.name.replaceAll("^-*", "") + + if (arg.type == "file") { + arg.must_exist = arg.must_exist != null ? arg.must_exist : true + arg.create_parent = arg.create_parent != null ? arg.create_parent : true + } + + // add default values to output files which haven't already got a default + if (arg.type == "file" && arg.direction == "output" && arg.default == null) { + def mult = arg.multiple ? "_*" : "" + def extSearch = "" + if (arg.default != null) { + extSearch = arg.default + } else if (arg.example != null) { + extSearch = arg.example + } + if (extSearch instanceof List) { + extSearch = extSearch[0] + } + def extSearchResult = extSearch.find("\\.[^\\.]+\$") + def ext = extSearchResult != null ? extSearchResult : "" + arg.default = "\$id.\$key.${arg.plainName}${mult}${ext}" + if (arg.multiple) { + arg.default = [arg.default] + } + } + + if (!arg.multiple) { + if (arg.default != null && arg.default instanceof List) { + arg.default = arg.default[0] + } + if (arg.example != null && arg.example instanceof List) { + arg.example = arg.example[0] + } + } + + if (arg.type == "boolean_true") { + arg.default = false + } + if (arg.type == "boolean_false") { + arg.default = true + } + + arg +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/addGlobalParams.nf' +def addGlobalArguments(config) { + def localConfig = [ + "argument_groups": [ + [ + "name": "Nextflow input-output arguments", + "description": "Input/output parameters for Nextflow itself. Please note that both publishDir and publish_dir are supported but at least one has to be configured.", + "arguments" : [ + [ + 'name': '--publish_dir', + 'required': true, + 'type': 'string', + 'description': 'Path to an output directory.', + 'example': 'output/', + 'multiple': false + ], + [ + 'name': '--param_list', + 'required': false, + 'type': 'string', + 'description': '''Allows inputting multiple parameter sets to initialise a Nextflow channel. A `param_list` can either be a list of maps, a csv file, a json file, a yaml file, or simply a yaml blob. + | + |* A list of maps (as-is) where the keys of each map corresponds to the arguments of the pipeline. Example: in a `nextflow.config` file: `param_list: [ ['id': 'foo', 'input': 'foo.txt'], ['id': 'bar', 'input': 'bar.txt'] ]`. + |* A csv file should have column names which correspond to the different arguments of this pipeline. Example: `--param_list data.csv` with columns `id,input`. + |* A json or a yaml file should be a list of maps, each of which has keys corresponding to the arguments of the pipeline. Example: `--param_list data.json` with contents `[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]`. + |* A yaml blob can also be passed directly as a string. Example: `--param_list "[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]"`. + | + |When passing a csv, json or yaml file, relative path names are relativized to the location of the parameter file. No relativation is performed when `param_list` is a list of maps (as-is) or a yaml blob.'''.stripMargin(), + 'example': 'my_params.yaml', + 'multiple': false, + 'hidden': true + ] + // TODO: allow multiple: true in param_list? + // TODO: allow to specify a --param_list_regex to filter the param_list? + // TODO: allow to specify a --param_list_from_state to remap entries in the param_list? + ] + ] + ] + ] + + return processConfig(_mergeMap(config, localConfig)) +} + +def _mergeMap(Map lhs, Map rhs) { + return rhs.inject(lhs.clone()) { map, entry -> + if (map[entry.key] instanceof Map && entry.value instanceof Map) { + map[entry.key] = _mergeMap(map[entry.key], entry.value) + } else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) { + map[entry.key] += entry.value + } else { + map[entry.key] = entry.value + } + return map + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/generateHelp.nf' +def _generateArgumentHelp(param) { + // alternatives are not supported + // def names = param.alternatives ::: List(param.name) + + def unnamedProps = [ + ["required parameter", param.required], + ["multiple values allowed", param.multiple], + ["output", param.direction.toLowerCase() == "output"], + ["file must exist", param.type == "file" && param.must_exist] + ].findAll{it[1]}.collect{it[0]} + + def dflt = null + if (param.default != null) { + if (param.default instanceof List) { + dflt = param.default.join(param.multiple_sep != null ? param.multiple_sep : ", ") + } else { + dflt = param.default.toString() + } + } + def example = null + if (param.example != null) { + if (param.example instanceof List) { + example = param.example.join(param.multiple_sep != null ? param.multiple_sep : ", ") + } else { + example = param.example.toString() + } + } + def min = param.min?.toString() + def max = param.max?.toString() + + def escapeChoice = { choice -> + def s1 = choice.replaceAll("\\n", "\\\\n") + def s2 = s1.replaceAll("\"", """\\\"""") + s2.contains(",") || s2 != choice ? "\"" + s2 + "\"" : s2 + } + def choices = param.choices == null ? + null : + "[ " + param.choices.collect{escapeChoice(it.toString())}.join(", ") + " ]" + + def namedPropsStr = [ + ["type", ([param.type] + unnamedProps).join(", ")], + ["default", dflt], + ["example", example], + ["choices", choices], + ["min", min], + ["max", max] + ] + .findAll{it[1]} + .collect{"\n " + it[0] + ": " + it[1].replaceAll("\n", "\\n")} + .join("") + + def descStr = param.description == null ? + "" : + _paragraphWrap("\n" + param.description.trim(), 80 - 8).join("\n ") + + "\n --" + param.plainName + + namedPropsStr + + descStr +} + +// Based on Helper.generateHelp() in Helper.scala +def _generateHelp(config) { + def fun = config + + // PART 1: NAME AND VERSION + def nameStr = fun.name + + (fun.version == null ? "" : " " + fun.version) + + // PART 2: DESCRIPTION + def descrStr = fun.description == null ? + "" : + "\n\n" + _paragraphWrap(fun.description.trim(), 80).join("\n") + + // PART 3: Usage + def usageStr = fun.usage == null ? + "" : + "\n\nUsage:\n" + fun.usage.trim() + + // PART 4: Options + def argGroupStrs = fun.allArgumentGroups.collect{argGroup -> + def name = argGroup.name + def descriptionStr = argGroup.description == null ? + "" : + "\n " + _paragraphWrap(argGroup.description.trim(), 80-4).join("\n ") + "\n" + def arguments = argGroup.arguments.collect{arg -> + arg instanceof String ? fun.allArguments.find{it.plainName == arg} : arg + }.findAll{it != null} + def argumentStrs = arguments.collect{param -> _generateArgumentHelp(param)} + + "\n\n$name:" + + descriptionStr + + argumentStrs.join("\n") + } + + // FINAL: combine + def out = nameStr + + descrStr + + usageStr + + argGroupStrs.join("") + + return out +} + +// based on Format._paragraphWrap +def _paragraphWrap(str, maxLength) { + def outLines = [] + str.split("\n").each{par -> + def words = par.split("\\s").toList() + + def word = null + def line = words.pop() + while(!words.isEmpty()) { + word = words.pop() + if (line.length() + word.length() + 1 <= maxLength) { + line = line + " " + word + } else { + outLines.add(line) + line = word + } + } + if (words.isEmpty()) { + outLines.add(line) + } + } + return outLines +} + +def helpMessage(config) { + if (params.containsKey("help") && params.help) { + def mergedConfig = addGlobalArguments(config) + def helpStr = _generateHelp(mergedConfig) + println(helpStr) + exit 0 + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/processConfig.nf' +def processConfig(config) { + // set defaults for arguments + config.arguments = + (config.arguments ?: []).collect{_processArgument(it)} + + // set defaults for argument_group arguments + config.argument_groups = + (config.argument_groups ?: []).collect{grp -> + grp.arguments = (grp.arguments ?: []).collect{_processArgument(it)} + grp + } + + // create combined arguments list + config.allArguments = + config.arguments + + config.argument_groups.collectMany{it.arguments} + + // add missing argument groups (based on Functionality::allArgumentGroups()) + def argGroups = config.argument_groups + if (argGroups.any{it.name.toLowerCase() == "arguments"}) { + argGroups = argGroups.collect{ grp -> + if (grp.name.toLowerCase() == "arguments") { + grp = grp + [ + arguments: grp.arguments + config.arguments + ] + } + grp + } + } else { + argGroups = argGroups + [ + name: "Arguments", + arguments: config.arguments + ] + } + config.allArgumentGroups = argGroups + + config +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/readConfig.nf' + +def readConfig(file) { + def config = readYaml(file ?: moduleDir.resolve("config.vsh.yaml")) + processConfig(config) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_resolveSiblingIfNotAbsolute.nf' +/** + * Resolve a path relative to the current file. + * + * @param str The path to resolve, as a String. + * @param parentPath The path to resolve relative to, as a Path. + * + * @return The path that may have been resovled, as a Path. + */ +def _resolveSiblingIfNotAbsolute(str, parentPath) { + if (str !instanceof String) { + return str + } + if (!_stringIsAbsolutePath(str)) { + return parentPath.resolveSibling(str) + } else { + return file(str, hidden: true) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_stringIsAbsolutePath.nf' +/** + * Check whether a path as a string is absolute. + * + * In the past, we tried using `file(., relative: true).isAbsolute()`, + * but the 'relative' option was added in 22.10.0. + * + * @param path The path to check, as a String. + * + * @return Whether the path is absolute, as a boolean. + */ +def _stringIsAbsolutePath(path) { + def _resolve_URL_PROTOCOL = ~/^([a-zA-Z][a-zA-Z0-9]*:)?\\/.+/ + + assert path instanceof String + return _resolve_URL_PROTOCOL.matcher(path).matches() +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/collectTraces.nf' +class CustomTraceObserver implements nextflow.trace.TraceObserver { + List traces + + CustomTraceObserver(List traces) { + this.traces = traces + } + + @Override + void onProcessComplete(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) { + def trace2 = trace.store.clone() + trace2.script = null + traces.add(trace2) + } + + @Override + void onProcessCached(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) { + def trace2 = trace.store.clone() + trace2.script = null + traces.add(trace2) + } +} + +def collectTraces() { + def traces = Collections.synchronizedList([]) + + // add custom trace observer which stores traces in the traces object + session.observers.add(new CustomTraceObserver(traces)) + + traces +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/deepClone.nf' +/** + * Performs a deep clone of the given object. + * @param x an object + */ +def deepClone(x) { + iterateMap(x, {it instanceof Cloneable ? it.clone() : it}) +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getPublishDir.nf' +def getPublishDir() { + return params.containsKey("publish_dir") ? params.publish_dir : + params.containsKey("publishDir") ? params.publishDir : + null +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getRootDir.nf' + +// Recurse upwards until we find a '.build.yaml' file +def _findBuildYamlFile(pathPossiblySymlink) { + def path = pathPossiblySymlink.toRealPath() + def child = path.resolve(".build.yaml") + if (java.nio.file.Files.isDirectory(path) && java.nio.file.Files.exists(child)) { + return child + } else { + def parent = path.getParent() + if (parent == null) { + return null + } else { + return _findBuildYamlFile(parent) + } + } +} + +// get the root of the target folder +def getRootDir() { + def dir = _findBuildYamlFile(meta.resources_dir) + assert dir != null: "Could not find .build.yaml in the folder structure" + dir.getParent() +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/iterateMap.nf' +/** + * Recursively apply a function over the leaves of an object. + * @param obj The object to iterate over. + * @param fun The function to apply to each value. + * @return The object with the function applied to each value. + */ +def iterateMap(obj, fun) { + if (obj instanceof List && obj !instanceof String) { + return obj.collect{item -> + iterateMap(item, fun) + } + } else if (obj instanceof Map) { + return obj.collectEntries{key, item -> + [key.toString(), iterateMap(item, fun)] + } + } else { + return fun(obj) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/niceView.nf' +/** + * A view for printing the event of each channel as a YAML blob. + * This is useful for debugging. + */ +def niceView() { + workflow niceViewWf { + take: input + main: + output = input + | view{toYamlBlob(it)} + emit: output + } + return niceViewWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readCsv.nf' + +def readCsv(file_path) { + def output = [] + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + + // todo: allow escaped quotes in string + // todo: allow single quotes? + def splitRegex = java.util.regex.Pattern.compile(''',(?=(?:[^"]*"[^"]*")*[^"]*$)''') + def removeQuote = java.util.regex.Pattern.compile('''"(.*)"''') + + def br = java.nio.file.Files.newBufferedReader(inputFile) + + def row = -1 + def header = null + while (br.ready() && header == null) { + def line = br.readLine() + row++ + if (!line.startsWith("#")) { + header = splitRegex.split(line, -1).collect{field -> + m = removeQuote.matcher(field) + m.find() ? m.replaceFirst('$1') : field + } + } + } + assert header != null: "CSV file should contain a header" + + while (br.ready()) { + def line = br.readLine() + row++ + if (line == null) { + br.close() + break + } + + if (!line.startsWith("#")) { + def predata = splitRegex.split(line, -1) + def data = predata.collect{field -> + if (field == "") { + return null + } + def m = removeQuote.matcher(field) + if (m.find()) { + return m.replaceFirst('$1') + } else { + return field + } + } + assert header.size() == data.size(): "Row $row should contain the same number as fields as the header" + + def dataMap = [header, data].transpose().collectEntries().findAll{it.value != null} + output.add(dataMap) + } + } + + output +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJson.nf' +def readJson(file_path) { + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + def jsonSlurper = new groovy.json.JsonSlurper() + jsonSlurper.parse(inputFile) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJsonBlob.nf' +def readJsonBlob(str) { + def jsonSlurper = new groovy.json.JsonSlurper() + jsonSlurper.parseText(str) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readTaggedYaml.nf' +// Custom constructor to modify how certain objects are parsed from YAML +class CustomConstructor extends org.yaml.snakeyaml.constructor.Constructor { + Path root + + class ConstructPath extends org.yaml.snakeyaml.constructor.AbstractConstruct { + public Object construct(org.yaml.snakeyaml.nodes.Node node) { + String filename = (String) constructScalar(node); + if (root != null) { + return root.resolve(filename); + } + return java.nio.file.Paths.get(filename); + } + } + + CustomConstructor(org.yaml.snakeyaml.LoaderOptions options, Path root) { + super(options) + this.root = root + // Handling !file tag and parse it back to a File type + this.yamlConstructors.put(new org.yaml.snakeyaml.nodes.Tag("!file"), new ConstructPath()) + } +} + +def readTaggedYaml(Path path) { + def options = new org.yaml.snakeyaml.LoaderOptions() + def constructor = new CustomConstructor(options, path.getParent()) + def yaml = new org.yaml.snakeyaml.Yaml(constructor) + return yaml.load(path.text) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYaml.nf' +def readYaml(file_path) { + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + def yamlSlurper = new org.yaml.snakeyaml.Yaml() + yamlSlurper.load(inputFile) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYamlBlob.nf' +def readYamlBlob(str) { + def yamlSlurper = new org.yaml.snakeyaml.Yaml() + yamlSlurper.load(str) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toJsonBlob.nf' +String toJsonBlob(data) { + return groovy.json.JsonOutput.toJson(data) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toTaggedYamlBlob.nf' +// Custom representer to modify how certain objects are represented in YAML +class CustomRepresenter extends org.yaml.snakeyaml.representer.Representer { + Path relativizer + + class RepresentPath implements org.yaml.snakeyaml.representer.Represent { + public String getFileName(Object obj) { + if (obj instanceof File) { + obj = ((File) obj).toPath(); + } + if (obj !instanceof Path) { + throw new IllegalArgumentException("Object: " + obj + " is not a Path or File"); + } + def path = (Path) obj; + + if (relativizer != null) { + return relativizer.relativize(path).toString() + } else { + return path.toString() + } + } + + public org.yaml.snakeyaml.nodes.Node representData(Object data) { + String filename = getFileName(data); + def tag = new org.yaml.snakeyaml.nodes.Tag("!file"); + return representScalar(tag, filename); + } + } + CustomRepresenter(org.yaml.snakeyaml.DumperOptions options, Path relativizer) { + super(options) + this.relativizer = relativizer + this.representers.put(sun.nio.fs.UnixPath, new RepresentPath()) + this.representers.put(Path, new RepresentPath()) + this.representers.put(File, new RepresentPath()) + } +} + +String toTaggedYamlBlob(data) { + return toRelativeTaggedYamlBlob(data, null) +} +String toRelativeTaggedYamlBlob(data, Path relativizer) { + def options = new org.yaml.snakeyaml.DumperOptions() + options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK) + def representer = new CustomRepresenter(options, relativizer) + def yaml = new org.yaml.snakeyaml.Yaml(representer, options) + return yaml.dump(data) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toYamlBlob.nf' +String toYamlBlob(data) { + def options = new org.yaml.snakeyaml.DumperOptions() + options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK) + options.setPrettyFlow(true) + def yaml = new org.yaml.snakeyaml.Yaml(options) + def cleanData = iterateMap(data, { it instanceof Path ? it.toString() : it }) + return yaml.dump(cleanData) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeJson.nf' +void writeJson(data, file) { + assert data: "writeJson: data should not be null" + assert file: "writeJson: file should not be null" + file.write(toJsonBlob(data)) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeYaml.nf' +void writeYaml(data, file) { + assert data: "writeYaml: data should not be null" + assert file: "writeYaml: file should not be null" + file.write(toYamlBlob(data)) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/findStates.nf' +def findStates(Map params, Map config) { + def auto_config = deepClone(config) + def auto_params = deepClone(params) + + auto_config = auto_config.clone() + // override arguments + auto_config.argument_groups = [] + auto_config.arguments = [ + [ + type: "string", + name: "--id", + description: "A dummy identifier", + required: false + ], + [ + type: "file", + name: "--input_states", + example: "/path/to/input/directory/**/state.yaml", + description: "Path to input directory containing the datasets to be integrated.", + required: true, + multiple: true, + multiple_sep: ";" + ], + [ + type: "string", + name: "--filter", + example: "foo/.*/state.yaml", + description: "Regex to filter state files by path.", + required: false + ], + // to do: make this a yaml blob? + [ + type: "string", + name: "--rename_keys", + example: ["newKey1:oldKey1", "newKey2:oldKey2"], + description: "Rename keys in the detected input files. This is useful if the input files do not match the set of input arguments of the workflow.", + required: false, + multiple: true, + multiple_sep: ";" + ], + [ + type: "string", + name: "--settings", + example: '{"output_dataset": "dataset.h5ad", "k": 10}', + description: "Global arguments as a JSON glob to be passed to all components.", + required: false + ] + ] + if (!(auto_params.containsKey("id"))) { + auto_params["id"] = "auto" + } + + // run auto config through processConfig once more + auto_config = processConfig(auto_config) + + workflow findStatesWf { + helpMessage(auto_config) + + output_ch = + channelFromParams(auto_params, auto_config) + | flatMap { autoId, args -> + + def globalSettings = args.settings ? readYamlBlob(args.settings) : [:] + + // look for state files in input dir + def stateFiles = args.input_states + + // filter state files by regex + if (args.filter) { + stateFiles = stateFiles.findAll{ stateFile -> + def stateFileStr = stateFile.toString() + def matcher = stateFileStr =~ args.filter + matcher.matches()} + } + + // read in states + def states = stateFiles.collect { stateFile -> + def state_ = readTaggedYaml(stateFile) + [state_.id, state_] + } + + // construct renameMap + if (args.rename_keys) { + def renameMap = args.rename_keys.collectEntries{renameString -> + def split = renameString.split(":") + assert split.size() == 2: "Argument 'rename_keys' should be of the form 'newKey:oldKey', or 'newKey:oldKey;newKey:oldKey' in case of multiple values" + split + } + + // rename keys in state, only let states through which have all keys + // also add global settings + states = states.collectMany{id, state -> + def newState = [:] + + for (key in renameMap.keySet()) { + def origKey = renameMap[key] + if (!(state.containsKey(origKey))) { + return [] + } + newState[key] = state[origKey] + } + + [[id, globalSettings + newState]] + } + } + + states + } + emit: + output_ch + } + + return findStatesWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/joinStates.nf' +def joinStates(Closure apply_) { + workflow joinStatesWf { + take: input_ch + main: + output_ch = input_ch + | toSortedList + | filter{ it.size() > 0 } + | map{ tups -> + def ids = tups.collect{it[0]} + def states = tups.collect{it[1]} + apply_(ids, states) + } + + emit: output_ch + } + return joinStatesWf +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/publishFiles.nf' +def publishFiles(Map args) { + def key_ = args.get("key") + + assert key_ != null : "publishFiles: key must be specified" + + workflow publishFilesWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] + + // the input files and the target output filenames + def inputoutputFilenames_ = collectInputOutputPaths(state_, id_ + "." + key_).transpose() + def inputFiles_ = inputoutputFilenames_[0] + def outputFilenames_ = inputoutputFilenames_[1] + + [id_, inputFiles_, outputFilenames_] + } + | publishFilesProc + emit: input_ch + } + return publishFilesWf +} + +process publishFilesProc { + // todo: check publishpath? + publishDir path: "${getPublishDir()}/", mode: "copy" + tag "$id" + input: + tuple val(id), path(inputFiles, stageAs: "_inputfile?/*"), val(outputFiles) + output: + tuple val(id), path{outputFiles} + script: + def copyCommands = [ + inputFiles instanceof List ? inputFiles : [inputFiles], + outputFiles instanceof List ? outputFiles : [outputFiles] + ] + .transpose() + .collectMany{infile, outfile -> + if (infile.toString() != outfile.toString()) { + [ + "[ -d \"\$(dirname '${outfile.toString()}')\" ] || mkdir -p \"\$(dirname '${outfile.toString()}')\"", + "cp -r '${infile.toString()}' '${outfile.toString()}'" + ] + } else { + // no need to copy if infile is the same as outfile + [] + } + } + """ + echo "Copying output files to destination folder" + ${copyCommands.join("\n ")} + """ +} + + +// this assumes that the state contains no other values other than those specified in the config +def publishFilesByConfig(Map args) { + def config = args.get("config") + assert config != null : "publishFilesByConfig: config must be specified" + + def key_ = args.get("key", config.name) + assert key_ != null : "publishFilesByConfig: key must be specified" + + workflow publishFilesSimpleWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] // e.g. [output: new File("myoutput.h5ad"), k: 10] + def origState_ = tup[2] // e.g. [output: '$id.$key.foo.h5ad'] + + + // the processed state is a list of [key, value, inputPath, outputFilename] tuples, where + // - key is a String + // - value is any object that can be serialized to a Yaml (so a String/Integer/Long/Double/Boolean, a List, a Map, or a Path) + // - inputPath is a List[Path] + // - outputFilename is a List[String] + // - (inputPath, outputFilename) are the files that will be copied from src to dest (relative to the state.yaml) + def processedState = + config.allArguments + .findAll { it.direction == "output" } + .collectMany { par -> + def plainName_ = par.plainName + // if the state does not contain the key, it's an + // optional argument for which the component did + // not generate any output OR multiple channels were emitted + // and the output was just not added to using the channel + // that is now being parsed + if (!state_.containsKey(plainName_)) { + return [] + } + def value = state_[plainName_] + // if the parameter is not a file, it should be stored + // in the state as-is, but is not something that needs + // to be copied from the source path to the dest path + if (par.type != "file") { + return [[inputPath: [], outputFilename: []]] + } + // if the orig state does not contain this filename, + // it's an optional argument for which the user specified + // that it should not be returned as a state + if (!origState_.containsKey(plainName_)) { + return [] + } + def filenameTemplate = origState_[plainName_] + // if the pararameter is multiple: true, fetch the template + if (par.multiple && filenameTemplate instanceof List) { + filenameTemplate = filenameTemplate[0] + } + // instantiate the template + def filename = filenameTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + if (par.multiple) { + // if the parameter is multiple: true, the filename + // should contain a wildcard '*' that is replaced with + // the index of the file + assert filename.contains("*") : "Module '${key_}' id '${id_}': Multiple output files specified, but no wildcard '*' in the filename: ${filename}" + def outputPerFile = value.withIndex().collect{ val, ix -> + def filename_ix = filename.replace("*", ix.toString()) + def inputPath = val instanceof File ? val.toPath() : val + [inputPath: inputPath, outputFilename: filename_ix] + } + def transposedOutputs = ["inputPath", "outputFilename"].collectEntries{ key -> + [key, outputPerFile.collect{dic -> dic[key]}] + } + return [[key: plainName_] + transposedOutputs] + } else { + def value_ = java.nio.file.Paths.get(filename) + def inputPath = value instanceof File ? value.toPath() : value + return [[inputPath: [inputPath], outputFilename: [filename]]] + } + } + + def inputPaths = processedState.collectMany{it.inputPath} + def outputFilenames = processedState.collectMany{it.outputFilename} + + + [id_, inputPaths, outputFilenames] + } + | publishFilesProc + emit: input_ch + } + return publishFilesSimpleWf +} + + + + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/publishStates.nf' +def collectFiles(obj) { + if (obj instanceof java.io.File || obj instanceof Path) { + return [obj] + } else if (obj instanceof List && obj !instanceof String) { + return obj.collectMany{item -> + collectFiles(item) + } + } else if (obj instanceof Map) { + return obj.collectMany{key, item -> + collectFiles(item) + } + } else { + return [] + } +} + +/** + * Recurse through a state and collect all input files and their target output filenames. + * @param obj The state to recurse through. + * @param prefix The prefix to prepend to the output filenames. + */ +def collectInputOutputPaths(obj, prefix) { + if (obj instanceof File || obj instanceof Path) { + def path = obj instanceof Path ? obj : obj.toPath() + def ext = path.getFileName().toString().find("\\.[^\\.]+\$") ?: "" + def newFilename = prefix + ext + return [[obj, newFilename]] + } else if (obj instanceof List && obj !instanceof String) { + return obj.withIndex().collectMany{item, ix -> + collectInputOutputPaths(item, prefix + "_" + ix) + } + } else if (obj instanceof Map) { + return obj.collectMany{key, item -> + collectInputOutputPaths(item, prefix + "." + key) + } + } else { + return [] + } +} + +def publishStates(Map args) { + def key_ = args.get("key") + def yamlTemplate_ = args.get("output_state", args.get("outputState", '$id.$key.state.yaml')) + + assert key_ != null : "publishStates: key must be specified" + + workflow publishStatesWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] + + // the input files and the target output filenames + def inputoutputFilenames_ = collectInputOutputPaths(state_, id_ + "." + key_).transpose() + + def yamlFilename = yamlTemplate_ + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + + // TODO: do the pathnames in state_ match up with the outputFilenames_? + + // convert state to yaml blob + def yamlBlob_ = toRelativeTaggedYamlBlob([id: id_] + state_, java.nio.file.Paths.get(yamlFilename)) + + [id_, yamlBlob_, yamlFilename] + } + | publishStatesProc + emit: input_ch + } + return publishStatesWf +} +process publishStatesProc { + // todo: check publishpath? + publishDir path: "${getPublishDir()}/", mode: "copy" + tag "$id" + input: + tuple val(id), val(yamlBlob), val(yamlFile) + output: + tuple val(id), path{[yamlFile]} + script: + """ + mkdir -p "\$(dirname '${yamlFile}')" + echo "Storing state as yaml" + cat > '${yamlFile}' << HERE +${yamlBlob} +HERE + """ +} + + +// this assumes that the state contains no other values other than those specified in the config +def publishStatesByConfig(Map args) { + def config = args.get("config") + assert config != null : "publishStatesByConfig: config must be specified" + + def key_ = args.get("key", config.name) + assert key_ != null : "publishStatesByConfig: key must be specified" + + workflow publishStatesSimpleWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] // e.g. [output: new File("myoutput.h5ad"), k: 10] + def origState_ = tup[2] // e.g. [output: '$id.$key.foo.h5ad'] + + // TODO: allow overriding the state.yaml template + // TODO TODO: if auto.publish == "state", add output_state as an argument + def yamlTemplate = params.containsKey("output_state") ? params.output_state : '$id.$key.state.yaml' + def yamlFilename = yamlTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + def yamlDir = java.nio.file.Paths.get(yamlFilename).getParent() + + // the processed state is a list of [key, value] tuples, where + // - key is a String + // - value is any object that can be serialized to a Yaml (so a String/Integer/Long/Double/Boolean, a List, a Map, or a Path) + // - (key, value) are the tuples that will be saved to the state.yaml file + def processedState = + config.allArguments + .findAll { it.direction == "output" } + .collectMany { par -> + def plainName_ = par.plainName + // if the state does not contain the key, it's an + // optional argument for which the component did + // not generate any output + if (!state_.containsKey(plainName_)) { + return [] + } + def value = state_[plainName_] + // if the parameter is not a file, it should be stored + // in the state as-is, but is not something that needs + // to be copied from the source path to the dest path + if (par.type != "file") { + return [[key: plainName_, value: value]] + } + // if the orig state does not contain this filename, + // it's an optional argument for which the user specified + // that it should not be returned as a state + if (!origState_.containsKey(plainName_)) { + return [] + } + def filenameTemplate = origState_[plainName_] + // if the pararameter is multiple: true, fetch the template + if (par.multiple && filenameTemplate instanceof List) { + filenameTemplate = filenameTemplate[0] + } + // instantiate the template + def filename = filenameTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + if (par.multiple) { + // if the parameter is multiple: true, the filename + // should contain a wildcard '*' that is replaced with + // the index of the file + assert filename.contains("*") : "Module '${key_}' id '${id_}': Multiple output files specified, but no wildcard '*' in the filename: ${filename}" + def outputPerFile = value.withIndex().collect{ val, ix -> + def filename_ix = filename.replace("*", ix.toString()) + def value_ = java.nio.file.Paths.get(filename_ix) + // if id contains a slash + if (yamlDir != null) { + value_ = yamlDir.relativize(value_) + } + return value_ + } + return [["key": plainName_, "value": outputPerFile]] + } else { + def value_ = java.nio.file.Paths.get(filename) + // if id contains a slash + if (yamlDir != null) { + value_ = yamlDir.relativize(value_) + } + def inputPath = value instanceof File ? value.toPath() : value + return [["key": plainName_, value: value_]] + } + } + + + def updatedState_ = processedState.collectEntries{[it.key, it.value]} + + // convert state to yaml blob + def yamlBlob_ = toTaggedYamlBlob([id: id_] + updatedState_) + + [id_, yamlBlob_, yamlFilename] + } + | publishStatesProc + emit: input_ch + } + return publishStatesSimpleWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/setState.nf' +def setState(fun) { + assert fun instanceof Closure || fun instanceof Map || fun instanceof List : + "Error in setState: Expected process argument to be a Closure, a Map, or a List. Found: class ${fun.getClass()}" + + // if fun is a List, convert to map + if (fun instanceof List) { + // check whether fun is a list[string] + assert fun.every{it instanceof CharSequence} : "Error in setState: argument is a List, but not all elements are Strings" + fun = fun.collectEntries{[it, it]} + } + + // if fun is a map, convert to closure + if (fun instanceof Map) { + // check whether fun is a map[string, string] + assert fun.values().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all values are Strings" + assert fun.keySet().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all keys are Strings" + def funMap = fun.clone() + // turn the map into a closure to be used later on + fun = { id_, state_ -> + assert state_ instanceof Map : "Error in setState: the state is not a Map" + funMap.collectMany{newkey, origkey -> + if (state_.containsKey(origkey)) { + [[newkey, state_[origkey]]] + } else { + [] + } + }.collectEntries() + } + } + + map { tup -> + def id = tup[0] + def state = tup[1] + def unfilteredState = fun(id, state) + def newState = unfilteredState.findAll{key, val -> val != null} + [id, newState] + tup.drop(2) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processAuto.nf' +// TODO: unit test processAuto +def processAuto(Map auto) { + // remove null values + auto = auto.findAll{k, v -> v != null} + + // check for unexpected keys + def expectedKeys = ["simplifyInput", "simplifyOutput", "transcript", "publish"] + def unexpectedKeys = auto.keySet() - expectedKeys + assert unexpectedKeys.isEmpty(), "unexpected keys in auto: '${unexpectedKeys.join("', '")}'" + + // check auto.simplifyInput + assert auto.simplifyInput instanceof Boolean, "auto.simplifyInput must be a boolean" + + // check auto.simplifyOutput + assert auto.simplifyOutput instanceof Boolean, "auto.simplifyOutput must be a boolean" + + // check auto.transcript + assert auto.transcript instanceof Boolean, "auto.transcript must be a boolean" + + // check auto.publish + assert auto.publish instanceof Boolean || auto.publish == "state", "auto.publish must be a boolean or 'state'" + + return auto.subMap(expectedKeys) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processDirectives.nf' +def assertMapKeys(map, expectedKeys, requiredKeys, mapName) { + assert map instanceof Map : "Expected argument '$mapName' to be a Map. Found: class ${map.getClass()}" + map.forEach { key, val -> + assert key in expectedKeys : "Unexpected key '$key' in ${mapName ? mapName + " " : ""}map" + } + requiredKeys.forEach { requiredKey -> + assert map.containsKey(requiredKey) : "Missing required key '$key' in ${mapName ? mapName + " " : ""}map" + } +} + +// TODO: unit test processDirectives +def processDirectives(Map drctv) { + // remove null values + drctv = drctv.findAll{k, v -> v != null} + + // check for unexpected keys + def expectedKeys = [ + "accelerator", "afterScript", "beforeScript", "cache", "conda", "container", "containerOptions", "cpus", "disk", "echo", "errorStrategy", "executor", "machineType", "maxErrors", "maxForks", "maxRetries", "memory", "module", "penv", "pod", "publishDir", "queue", "label", "scratch", "storeDir", "stageInMode", "stageOutMode", "tag", "time" + ] + def unexpectedKeys = drctv.keySet() - expectedKeys + assert unexpectedKeys.isEmpty() : "Unexpected keys in process directive: '${unexpectedKeys.join("', '")}'" + + /* DIRECTIVE accelerator + accepted examples: + - [ limit: 4, type: "nvidia-tesla-k80" ] + */ + if (drctv.containsKey("accelerator")) { + assertMapKeys(drctv["accelerator"], ["type", "limit", "request", "runtime"], [], "accelerator") + } + + /* DIRECTIVE afterScript + accepted examples: + - "source /cluster/bin/cleanup" + */ + if (drctv.containsKey("afterScript")) { + assert drctv["afterScript"] instanceof CharSequence + } + + /* DIRECTIVE beforeScript + accepted examples: + - "source /cluster/bin/setup" + */ + if (drctv.containsKey("beforeScript")) { + assert drctv["beforeScript"] instanceof CharSequence + } + + /* DIRECTIVE cache + accepted examples: + - true + - false + - "deep" + - "lenient" + */ + if (drctv.containsKey("cache")) { + assert drctv["cache"] instanceof CharSequence || drctv["cache"] instanceof Boolean + if (drctv["cache"] instanceof CharSequence) { + assert drctv["cache"] in ["deep", "lenient"] : "Unexpected value for cache" + } + } + + /* DIRECTIVE conda + accepted examples: + - "bwa=0.7.15" + - "bwa=0.7.15 fastqc=0.11.5" + - ["bwa=0.7.15", "fastqc=0.11.5"] + */ + if (drctv.containsKey("conda")) { + if (drctv["conda"] instanceof List) { + drctv["conda"] = drctv["conda"].join(" ") + } + assert drctv["conda"] instanceof CharSequence + } + + /* DIRECTIVE container + accepted examples: + - "foo/bar:tag" + - [ registry: "reg", image: "im", tag: "ta" ] + is transformed to "reg/im:ta" + - [ image: "im" ] + is transformed to "im:latest" + */ + if (drctv.containsKey("container")) { + assert drctv["container"] instanceof Map || drctv["container"] instanceof CharSequence + if (drctv["container"] instanceof Map) { + def m = drctv["container"] + assertMapKeys(m, [ "registry", "image", "tag" ], ["image"], "container") + def part1 = + System.getenv('OVERRIDE_CONTAINER_REGISTRY') ? System.getenv('OVERRIDE_CONTAINER_REGISTRY') + "/" : + params.containsKey("override_container_registry") ? params["override_container_registry"] + "/" : // todo: remove? + m.registry ? m.registry + "/" : + "" + def part2 = m.image + def part3 = m.tag ? ":" + m.tag : ":latest" + drctv["container"] = part1 + part2 + part3 + } + } + + /* DIRECTIVE containerOptions + accepted examples: + - "--foo bar" + - ["--foo bar", "-f b"] + */ + if (drctv.containsKey("containerOptions")) { + if (drctv["containerOptions"] instanceof List) { + drctv["containerOptions"] = drctv["containerOptions"].join(" ") + } + assert drctv["containerOptions"] instanceof CharSequence + } + + /* DIRECTIVE cpus + accepted examples: + - 1 + - 10 + */ + if (drctv.containsKey("cpus")) { + assert drctv["cpus"] instanceof Integer + } + + /* DIRECTIVE disk + accepted examples: + - "1 GB" + - "2TB" + - "3.2KB" + - "10.B" + */ + if (drctv.containsKey("disk")) { + assert drctv["disk"] instanceof CharSequence + // assert drctv["disk"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B") + // ^ does not allow closures + } + + /* DIRECTIVE echo + accepted examples: + - true + - false + */ + if (drctv.containsKey("echo")) { + assert drctv["echo"] instanceof Boolean + } + + /* DIRECTIVE errorStrategy + accepted examples: + - "terminate" + - "finish" + */ + if (drctv.containsKey("errorStrategy")) { + assert drctv["errorStrategy"] instanceof CharSequence + assert drctv["errorStrategy"] in ["terminate", "finish", "ignore", "retry"] : "Unexpected value for errorStrategy" + } + + /* DIRECTIVE executor + accepted examples: + - "local" + - "sge" + */ + if (drctv.containsKey("executor")) { + assert drctv["executor"] instanceof CharSequence + assert drctv["executor"] in ["local", "sge", "uge", "lsf", "slurm", "pbs", "pbspro", "moab", "condor", "nqsii", "ignite", "k8s", "awsbatch", "google-pipelines"] : "Unexpected value for executor" + } + + /* DIRECTIVE machineType + accepted examples: + - "n1-highmem-8" + */ + if (drctv.containsKey("machineType")) { + assert drctv["machineType"] instanceof CharSequence + } + + /* DIRECTIVE maxErrors + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxErrors")) { + assert drctv["maxErrors"] instanceof Integer + } + + /* DIRECTIVE maxForks + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxForks")) { + assert drctv["maxForks"] instanceof Integer + } + + /* DIRECTIVE maxRetries + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxRetries")) { + assert drctv["maxRetries"] instanceof Integer + } + + /* DIRECTIVE memory + accepted examples: + - "1 GB" + - "2TB" + - "3.2KB" + - "10.B" + */ + if (drctv.containsKey("memory")) { + assert drctv["memory"] instanceof CharSequence + // assert drctv["memory"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B") + // ^ does not allow closures + } + + /* DIRECTIVE module + accepted examples: + - "ncbi-blast/2.2.27" + - "ncbi-blast/2.2.27:t_coffee/10.0" + - ["ncbi-blast/2.2.27", "t_coffee/10.0"] + */ + if (drctv.containsKey("module")) { + if (drctv["module"] instanceof List) { + drctv["module"] = drctv["module"].join(":") + } + assert drctv["module"] instanceof CharSequence + } + + /* DIRECTIVE penv + accepted examples: + - "smp" + */ + if (drctv.containsKey("penv")) { + assert drctv["penv"] instanceof CharSequence + } + + /* DIRECTIVE pod + accepted examples: + - [ label: "key", value: "val" ] + - [ annotation: "key", value: "val" ] + - [ env: "key", value: "val" ] + - [ [label: "l", value: "v"], [env: "e", value: "v"]] + */ + if (drctv.containsKey("pod")) { + if (drctv["pod"] instanceof Map) { + drctv["pod"] = [ drctv["pod"] ] + } + assert drctv["pod"] instanceof List + drctv["pod"].forEach { pod -> + assert pod instanceof Map + // TODO: should more checks be added? + // See https://www.nextflow.io/docs/latest/process.html?highlight=directives#pod + // e.g. does it contain 'label' and 'value', or 'annotation' and 'value', or ...? + } + } + + /* DIRECTIVE publishDir + accepted examples: + - [] + - [ [ path: "foo", enabled: true ], [ path: "bar", enabled: false ] ] + - "/path/to/dir" + is transformed to [[ path: "/path/to/dir" ]] + - [ path: "/path/to/dir", mode: "cache" ] + is transformed to [[ path: "/path/to/dir", mode: "cache" ]] + */ + // TODO: should we also look at params["publishDir"]? + if (drctv.containsKey("publishDir")) { + def pblsh = drctv["publishDir"] + + // check different options + assert pblsh instanceof List || pblsh instanceof Map || pblsh instanceof CharSequence + + // turn into list if not already so + // for some reason, 'if (!pblsh instanceof List) pblsh = [ pblsh ]' doesn't work. + pblsh = pblsh instanceof List ? pblsh : [ pblsh ] + + // check elements of publishDir + pblsh = pblsh.collect{ elem -> + // turn into map if not already so + elem = elem instanceof CharSequence ? [ path: elem ] : elem + + // check types and keys + assert elem instanceof Map : "Expected publish argument '$elem' to be a String or a Map. Found: class ${elem.getClass()}" + assertMapKeys(elem, [ "path", "mode", "overwrite", "pattern", "saveAs", "enabled" ], ["path"], "publishDir") + + // check elements in map + assert elem.containsKey("path") + assert elem["path"] instanceof CharSequence + if (elem.containsKey("mode")) { + assert elem["mode"] instanceof CharSequence + assert elem["mode"] in [ "symlink", "rellink", "link", "copy", "copyNoFollow", "move" ] + } + if (elem.containsKey("overwrite")) { + assert elem["overwrite"] instanceof Boolean + } + if (elem.containsKey("pattern")) { + assert elem["pattern"] instanceof CharSequence + } + if (elem.containsKey("saveAs")) { + assert elem["saveAs"] instanceof CharSequence //: "saveAs as a Closure is currently not supported. Surround your closure with single quotes to get the desired effect. Example: '\{ foo \}'" + } + if (elem.containsKey("enabled")) { + assert elem["enabled"] instanceof Boolean + } + + // return final result + elem + } + // store final directive + drctv["publishDir"] = pblsh + } + + /* DIRECTIVE queue + accepted examples: + - "long" + - "short,long" + - ["short", "long"] + */ + if (drctv.containsKey("queue")) { + if (drctv["queue"] instanceof List) { + drctv["queue"] = drctv["queue"].join(",") + } + assert drctv["queue"] instanceof CharSequence + } + + /* DIRECTIVE label + accepted examples: + - "big_mem" + - "big_cpu" + - ["big_mem", "big_cpu"] + */ + if (drctv.containsKey("label")) { + if (drctv["label"] instanceof CharSequence) { + drctv["label"] = [ drctv["label"] ] + } + assert drctv["label"] instanceof List + drctv["label"].forEach { label -> + assert label instanceof CharSequence + // assert label.matches("[a-zA-Z0-9]([a-zA-Z0-9_]*[a-zA-Z0-9])?") + // ^ does not allow closures + } + } + + /* DIRECTIVE scratch + accepted examples: + - true + - "/path/to/scratch" + - '$MY_PATH_TO_SCRATCH' + - "ram-disk" + */ + if (drctv.containsKey("scratch")) { + assert drctv["scratch"] == true || drctv["scratch"] instanceof CharSequence + } + + /* DIRECTIVE storeDir + accepted examples: + - "/path/to/storeDir" + */ + if (drctv.containsKey("storeDir")) { + assert drctv["storeDir"] instanceof CharSequence + } + + /* DIRECTIVE stageInMode + accepted examples: + - "copy" + - "link" + */ + if (drctv.containsKey("stageInMode")) { + assert drctv["stageInMode"] instanceof CharSequence + assert drctv["stageInMode"] in ["copy", "link", "symlink", "rellink"] + } + + /* DIRECTIVE stageOutMode + accepted examples: + - "copy" + - "link" + */ + if (drctv.containsKey("stageOutMode")) { + assert drctv["stageOutMode"] instanceof CharSequence + assert drctv["stageOutMode"] in ["copy", "move", "rsync"] + } + + /* DIRECTIVE tag + accepted examples: + - "foo" + - '$id' + */ + if (drctv.containsKey("tag")) { + assert drctv["tag"] instanceof CharSequence + } + + /* DIRECTIVE time + accepted examples: + - "1h" + - "2days" + - "1day 6hours 3minutes 30seconds" + */ + if (drctv.containsKey("time")) { + assert drctv["time"] instanceof CharSequence + // todo: validation regex? + } + + return drctv +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processWorkflowArgs.nf' +def processWorkflowArgs(Map args, Map defaultWfArgs, Map meta) { + // override defaults with args + def workflowArgs = defaultWfArgs + args + + // check whether 'key' exists + assert workflowArgs.containsKey("key") : "Error in module '${meta.config.name}': key is a required argument" + + // if 'key' is a closure, apply it to the original key + if (workflowArgs["key"] instanceof Closure) { + workflowArgs["key"] = workflowArgs["key"](meta.config.name) + } + def key = workflowArgs["key"] + assert key instanceof CharSequence : "Expected process argument 'key' to be a String. Found: class ${key.getClass()}" + assert key ==~ /^[a-zA-Z_]\w*$/ : "Error in module '$key': Expected process argument 'key' to consist of only letters, digits or underscores. Found: ${key}" + + // check for any unexpected keys + def expectedKeys = ["key", "directives", "auto", "map", "mapId", "mapData", "mapPassthrough", "filter", "runIf", "fromState", "toState", "args", "renameKeys", "debug"] + def unexpectedKeys = workflowArgs.keySet() - expectedKeys + assert unexpectedKeys.isEmpty() : "Error in module '$key': unexpected arguments to the '.run()' function: '${unexpectedKeys.join("', '")}'" + + // check whether directives exists and apply defaults + assert workflowArgs.containsKey("directives") : "Error in module '$key': directives is a required argument" + assert workflowArgs["directives"] instanceof Map : "Error in module '$key': Expected process argument 'directives' to be a Map. Found: class ${workflowArgs['directives'].getClass()}" + workflowArgs["directives"] = processDirectives(defaultWfArgs.directives + workflowArgs["directives"]) + + // check whether directives exists and apply defaults + assert workflowArgs.containsKey("auto") : "Error in module '$key': auto is a required argument" + assert workflowArgs["auto"] instanceof Map : "Error in module '$key': Expected process argument 'auto' to be a Map. Found: class ${workflowArgs['auto'].getClass()}" + workflowArgs["auto"] = processAuto(defaultWfArgs.auto + workflowArgs["auto"]) + + // auto define publish, if so desired + if (workflowArgs.auto.publish == true && (workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : [:]).isEmpty()) { + // can't assert at this level thanks to the no_publish profile + // assert params.containsKey("publishDir") || params.containsKey("publish_dir") : + // "Error in module '${workflowArgs['key']}': if auto.publish is true, params.publish_dir needs to be defined.\n" + + // " Example: params.publish_dir = \"./output/\"" + def publishDir = getPublishDir() + + if (publishDir != null) { + workflowArgs.directives.publishDir = [[ + path: publishDir, + saveAs: "{ it.startsWith('.') ? null : it }", // don't publish hidden files, by default + mode: "copy" + ]] + } + } + + // auto define transcript, if so desired + if (workflowArgs.auto.transcript == true) { + // can't assert at this level thanks to the no_publish profile + // assert params.containsKey("transcriptsDir") || params.containsKey("transcripts_dir") || params.containsKey("publishDir") || params.containsKey("publish_dir") : + // "Error in module '${workflowArgs['key']}': if auto.transcript is true, either params.transcripts_dir or params.publish_dir needs to be defined.\n" + + // " Example: params.transcripts_dir = \"./transcripts/\"" + def transcriptsDir = + params.containsKey("transcripts_dir") ? params.transcripts_dir : + params.containsKey("transcriptsDir") ? params.transcriptsDir : + params.containsKey("publish_dir") ? params.publish_dir + "/_transcripts" : + params.containsKey("publishDir") ? params.publishDir + "/_transcripts" : + null + if (transcriptsDir != null) { + def timestamp = nextflow.Nextflow.getSession().getWorkflowMetadata().start.format('yyyy-MM-dd_HH-mm-ss') + def transcriptsPublishDir = [ + path: "$transcriptsDir/$timestamp/\${task.process.replaceAll(':', '-')}/\${id}/", + saveAs: "{ it.startsWith('.') ? it.replaceAll('^.', '') : null }", + mode: "copy" + ] + def publishDirs = workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : null ? workflowArgs.directives.publishDir : [] + workflowArgs.directives.publishDir = publishDirs + transcriptsPublishDir + } + } + + // if this is a stubrun, remove certain directives? + if (workflow.stubRun) { + workflowArgs.directives.keySet().removeAll(["publishDir", "cpus", "memory", "label"]) + } + + for (nam in ["map", "mapId", "mapData", "mapPassthrough", "filter", "runIf"]) { + if (workflowArgs.containsKey(nam) && workflowArgs[nam]) { + assert workflowArgs[nam] instanceof Closure : "Error in module '$key': Expected process argument '$nam' to be null or a Closure. Found: class ${workflowArgs[nam].getClass()}" + } + } + + // TODO: should functions like 'map', 'mapId', 'mapData', 'mapPassthrough' be deprecated as well? + for (nam in ["map", "mapData", "mapPassthrough", "renameKeys"]) { + if (workflowArgs.containsKey(nam) && workflowArgs[nam] != null) { + log.warn "module '$key': workflow argument '$nam' is deprecated and will be removed in Viash 0.9.0. Please use 'fromState' and 'toState' instead." + } + } + + // check fromState + workflowArgs["fromState"] = _processFromState(workflowArgs.get("fromState"), key, meta.config) + + // check toState + workflowArgs["toState"] = _processToState(workflowArgs.get("toState"), key, meta.config) + + // return output + return workflowArgs +} + +def _processFromState(fromState, key_, config_) { + assert fromState == null || fromState instanceof Closure || fromState instanceof Map || fromState instanceof List : + "Error in module '$key_': Expected process argument 'fromState' to be null, a Closure, a Map, or a List. Found: class ${fromState.getClass()}" + if (fromState == null) { + return null + } + + // if fromState is a List, convert to map + if (fromState instanceof List) { + // check whether fromstate is a list[string] + assert fromState.every{it instanceof CharSequence} : "Error in module '$key_': fromState is a List, but not all elements are Strings" + fromState = fromState.collectEntries{[it, it]} + } + + // if fromState is a map, convert to closure + if (fromState instanceof Map) { + // check whether fromstate is a map[string, string] + assert fromState.values().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all values are Strings" + assert fromState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all keys are Strings" + def fromStateMap = fromState.clone() + def requiredInputNames = meta.config.allArguments.findAll{it.required && it.direction == "Input"}.collect{it.plainName} + // turn the map into a closure to be used later on + fromState = { it -> + def state = it[1] + assert state instanceof Map : "Error in module '$key_': the state is not a Map" + def data = fromStateMap.collectMany{newkey, origkey -> + // check whether newkey corresponds to a required argument + if (state.containsKey(origkey)) { + [[newkey, state[origkey]]] + } else if (!requiredInputNames.contains(origkey)) { + [] + } else { + throw new Exception("Error in module '$key_': fromState key '$origkey' not found in current state") + } + }.collectEntries() + data + } + } + + return fromState +} + +def _processToState(toState, key_, config_) { + if (toState == null) { + toState = { tup -> tup[1] } + } + + // toState should be a closure, map[string, string], or list[string] + assert toState instanceof Closure || toState instanceof Map || toState instanceof List : + "Error in module '$key_': Expected process argument 'toState' to be a Closure, a Map, or a List. Found: class ${toState.getClass()}" + + // if toState is a List, convert to map + if (toState instanceof List) { + // check whether toState is a list[string] + assert toState.every{it instanceof CharSequence} : "Error in module '$key_': toState is a List, but not all elements are Strings" + toState = toState.collectEntries{[it, it]} + } + + // if toState is a map, convert to closure + if (toState instanceof Map) { + // check whether toState is a map[string, string] + assert toState.values().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all values are Strings" + assert toState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all keys are Strings" + def toStateMap = toState.clone() + def requiredOutputNames = config_.allArguments.findAll{it.required && it.direction == "Output"}.collect{it.plainName} + // turn the map into a closure to be used later on + toState = { it -> + def output = it[1] + def state = it[2] + assert output instanceof Map : "Error in module '$key_': the output is not a Map" + assert state instanceof Map : "Error in module '$key_': the state is not a Map" + def extraEntries = toStateMap.collectMany{newkey, origkey -> + // check whether newkey corresponds to a required argument + if (output.containsKey(origkey)) { + [[newkey, output[origkey]]] + } else if (!requiredOutputNames.contains(origkey)) { + [] + } else { + throw new Exception("Error in module '$key_': toState key '$origkey' not found in current output") + } + }.collectEntries() + state + extraEntries + } + } + + return toState +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/workflowFactory.nf' +def _debug(workflowArgs, debugKey) { + if (workflowArgs.debug) { + view { "process '${workflowArgs.key}' $debugKey tuple: $it" } + } else { + map { it } + } +} + +// depends on: innerWorkflowFactory +def workflowFactory(Map args, Map defaultWfArgs, Map meta) { + def workflowArgs = processWorkflowArgs(args, defaultWfArgs, meta) + def key_ = workflowArgs["key"] + def multipleArgs = meta.config.allArguments.findAll{ it.multiple }.collect{it.plainName} + + workflow workflowInstance { + take: input_ + + main: + def chModified = input_ + | checkUniqueIds([:]) + | _debug(workflowArgs, "input") + | map { tuple -> + tuple = deepClone(tuple) + + if (workflowArgs.map) { + tuple = workflowArgs.map(tuple) + } + if (workflowArgs.mapId) { + tuple[0] = workflowArgs.mapId(tuple[0]) + } + if (workflowArgs.mapData) { + tuple[1] = workflowArgs.mapData(tuple[1]) + } + if (workflowArgs.mapPassthrough) { + tuple = tuple.take(2) + workflowArgs.mapPassthrough(tuple.drop(2)) + } + + // check tuple + assert tuple instanceof List : + "Error in module '${key_}': element in channel should be a tuple [id, data, ...otherargs...]\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: List. Found: tuple.getClass() is ${tuple.getClass()}" + assert tuple.size() >= 2 : + "Error in module '${key_}': expected length of tuple in input channel to be two or greater.\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Found: tuple.size() == ${tuple.size()}" + + // check id field + if (tuple[0] instanceof GString) { + tuple[0] = tuple[0].toString() + } + assert tuple[0] instanceof CharSequence : + "Error in module '${key_}': first element of tuple in channel should be a String\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Found: ${tuple[0]}" + + // match file to input file + if (workflowArgs.auto.simplifyInput && (tuple[1] instanceof Path || tuple[1] instanceof List)) { + def inputFiles = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + + assert inputFiles.size() == 1 : + "Error in module '${key_}' id '${tuple[0]}'.\n" + + " Anonymous file inputs are only allowed when the process has exactly one file input.\n" + + " Expected: inputFiles.size() == 1. Found: inputFiles.size() is ${inputFiles.size()}" + + tuple[1] = [[ inputFiles[0].plainName, tuple[1] ]].collectEntries() + } + + // check data field + assert tuple[1] instanceof Map : + "Error in module '${key_}' id '${tuple[0]}': second element of tuple in channel should be a Map\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}" + + // rename keys of data field in tuple + if (workflowArgs.renameKeys) { + assert workflowArgs.renameKeys instanceof Map : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class: Map. Found: renameKeys.getClass() is ${workflowArgs.renameKeys.getClass()}" + assert tuple[1] instanceof Map : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}" + + // TODO: allow renameKeys to be a function? + workflowArgs.renameKeys.each { newKey, oldKey -> + assert newKey instanceof CharSequence : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class of newKey: String. Found: newKey.getClass() is ${newKey.getClass()}" + assert oldKey instanceof CharSequence : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class of oldKey: String. Found: oldKey.getClass() is ${oldKey.getClass()}" + assert tuple[1].containsKey(oldKey) : + "Error renaming data keys in module '${key}' id '${tuple[0]}'.\n" + + " Key '$oldKey' is missing in the data map. tuple[1].keySet() is '${tuple[1].keySet()}'" + tuple[1].put(newKey, tuple[1][oldKey]) + } + tuple[1].keySet().removeAll(workflowArgs.renameKeys.collect{ newKey, oldKey -> oldKey }) + } + tuple + } + + + def chRun = null + def chPassthrough = null + if (workflowArgs.runIf) { + def runIfBranch = chModified.branch{ tup -> + run: workflowArgs.runIf(tup[0], tup[1]) + passthrough: true + } + chRun = runIfBranch.run + chPassthrough = runIfBranch.passthrough + } else { + chRun = chModified + chPassthrough = Channel.empty() + } + + def chRunFiltered = workflowArgs.filter ? + chRun | filter{workflowArgs.filter(it)} : + chRun + + def chArgs = workflowArgs.fromState ? + chRunFiltered | map{ + def new_data = workflowArgs.fromState(it.take(2)) + [it[0], new_data] + } : + chRunFiltered | map {tup -> tup.take(2)} + + // fill in defaults + def chArgsWithDefaults = chArgs + | map { tuple -> + def id_ = tuple[0] + def data_ = tuple[1] + + // TODO: could move fromState to here + + // fetch default params from functionality + def defaultArgs = meta.config.allArguments + .findAll { it.containsKey("default") } + .collectEntries { [ it.plainName, it.default ] } + + // fetch overrides in params + def paramArgs = meta.config.allArguments + .findAll { par -> + def argKey = key_ + "__" + par.plainName + params.containsKey(argKey) + } + .collectEntries { [ it.plainName, params[key_ + "__" + it.plainName] ] } + + // fetch overrides in data + def dataArgs = meta.config.allArguments + .findAll { data_.containsKey(it.plainName) } + .collectEntries { [ it.plainName, data_[it.plainName] ] } + + // combine params + def combinedArgs = defaultArgs + paramArgs + workflowArgs.args + dataArgs + + // remove arguments with explicit null values + combinedArgs + .removeAll{_, val -> val == null || val == "viash_no_value" || val == "force_null"} + + combinedArgs = _processInputValues(combinedArgs, meta.config, id_, key_) + + [id_, combinedArgs] + tuple.drop(2) + } + + // TODO: move some of the _meta.join_id wrangling to the safeJoin() function. + def chInitialOutputMulti = chArgsWithDefaults + | _debug(workflowArgs, "processed") + // run workflow + | innerWorkflowFactory(workflowArgs) + def chInitialOutputList = chInitialOutputMulti instanceof List ? chInitialOutputMulti : [chInitialOutputMulti] + assert chInitialOutputList.size() > 0: "should have emitted at least one output channel" + // Add a channel ID to the events, which designates the channel the event was emitted from as a running number + // This number is used to sort the events later when the events are gathered from across the channels. + def chInitialOutputListWithIndexedEvents = chInitialOutputList.withIndex().collect{channel, channelIndex -> + def newChannel = channel + | map {tuple -> + assert tuple instanceof List : + "Error in module '${key_}': element in output channel should be a tuple [id, data, ...otherargs...]\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: List. Found: tuple.getClass() is ${tuple.getClass()}" + + def newEvent = [channelIndex] + tuple + return newEvent + } + return newChannel + } + // Put the events into 1 channel, cover case where there is only one channel is emitted + def chInitialOutput = chInitialOutputList.size() > 1 ? \ + chInitialOutputListWithIndexedEvents[0].mix(*chInitialOutputListWithIndexedEvents.tail()) : \ + chInitialOutputListWithIndexedEvents[0] + def chInitialOutputProcessed = chInitialOutput + | map { tuple -> + def channelId = tuple[0] + def id_ = tuple[1] + def output_ = tuple[2] + + // see if output map contains metadata + def meta_ = + output_ instanceof Map && output_.containsKey("_meta") ? + output_["_meta"] : + [:] + def join_id = meta_.join_id ?: id_ + + // remove metadata + output_ = output_.findAll{k, v -> k != "_meta"} + + // check value types + output_ = _checkValidOutputArgument(output_, meta.config, id_, key_) + + [join_id, channelId, id_, output_] + } + // | view{"chInitialOutput: ${it.take(3)}"} + + // join the output [prev_id, channel_id, new_id, output] with the previous state [prev_id, state, ...] + def chPublishWithPreviousState = safeJoin(chInitialOutputProcessed, chRunFiltered, key_) + // input tuple format: [join_id, channel_id, id, output, prev_state, ...] + // output tuple format: [join_id, channel_id, id, new_state, ...] + | map{ tup -> + def new_state = workflowArgs.toState(tup.drop(2).take(3)) + tup.take(3) + [new_state] + tup.drop(5) + } + if (workflowArgs.auto.publish == "state") { + def chPublishFiles = chPublishWithPreviousState + // input tuple format: [join_id, channel_id, id, new_state, ...] + // output tuple format: [join_id, channel_id, id, new_state] + | map{ tup -> + tup.take(4) + } + + safeJoin(chPublishFiles, chArgsWithDefaults, key_) + // input tuple format: [join_id, channel_id, id, new_state, orig_state, ...] + // output tuple format: [id, new_state, orig_state] + | map { tup -> + tup.drop(2).take(3) + } + | publishFilesByConfig(key: key_, config: meta.config) + } + // Join the state from the events that were emitted from different channels + def chJoined = chInitialOutputProcessed + | map {tuple -> + def join_id = tuple[0] + def channel_id = tuple[1] + def id = tuple[2] + def other = tuple.drop(3) + // Below, groupTuple is used to join the events. To make sure resuming a workflow + // keeps working, the output state must be deterministic. This means the state needs to be + // sorted with groupTuple's has a 'sort' argument. This argument can be set to 'hash', + // but hashing the state when it is large can be problematic in terms of performance. + // Therefore, a custom comparator function is provided. We add the channel ID to the + // states so that we can use the channel ID to sort the items. + def stateWithChannelID = [[channel_id] * other.size(), other].transpose() + // A comparator that is provided to groupTuple's 'sort' argument is applied + // to all elements of the event tuple (that is not the 'id'). The comparator + // closure that is used below expects the input to be List. So the join_id and + // channel_id must also be wrapped in a list. + [[join_id], [channel_id], id] + stateWithChannelID + } + | groupTuple(by: 2, sort: {a, b -> a[0] <=> b[0]}, size: chInitialOutputList.size(), remainder: true) + | map {join_ids, _, id, statesWithChannelID -> + // Remove the channel IDs from the states + def states = statesWithChannelID.collect{it[1]} + def newJoinId = join_ids.flatten().unique{a, b -> a <=> b} + assert newJoinId.size() == 1: "Multiple events were emitted for '$id'." + def newJoinIdUnique = newJoinId[0] + + // Merge the states from the different channels + def newState = states.inject([:]){ old_state, state_to_add -> + return old_state + state_to_add.collectEntries{k, v -> + if (!multipleArgs.contains(k)) { + // if the key is not a multiple argument, we expect only one value + if (old_state.containsKey(k)) { + assert old_state[k] == v : "ID $id: multiple entries for argument $k were emitted." + } + [k, v] + } else { + // if the key is a multiple argument, append the different values into one list + def prevValue = old_state.getOrDefault(k, []) + def prevValueAsList = prevValue instanceof List ? prevValue : [prevValue] + [k, prevValueAsList + v] + } + } + } + + _checkAllRequiredOuputsPresent(newState, meta.config, id, key_) + + // simplify output if need be + if (workflowArgs.auto.simplifyOutput && newState.size() == 1) { + newState = newState.values()[0] + } + + return [newJoinIdUnique, id, newState] + } + + // join the output [prev_id, new_id, output] with the previous state [prev_id, state, ...] + def chNewState = safeJoin(chJoined, chRunFiltered, key_) + // input tuple format: [join_id, id, output, prev_state, ...] + // output tuple format: [join_id, id, new_state, ...] + | map{ tup -> + def new_state = workflowArgs.toState(tup.drop(1).take(3)) + tup.take(2) + [new_state] + tup.drop(4) + } + + if (workflowArgs.auto.publish == "state") { + def chPublishStates = chNewState + // input tuple format: [join_id, id, new_state, ...] + // output tuple format: [join_id, id, new_state] + | map{ tup -> + tup.take(3) + } + + safeJoin(chPublishStates, chArgsWithDefaults, key_) + // input tuple format: [join_id, id, new_state, orig_state, ...] + // output tuple format: [id, new_state, orig_state] + | map { tup -> + tup.drop(1).take(3) + } + | publishStatesByConfig(key: key_, config: meta.config) + } + chReturn = chNewState + | map { tup -> + // input tuple format: [join_id, id, new_state, ...] + // output tuple format: [id, new_state, ...] + tup.drop(1) + } + | _debug(workflowArgs, "output") + | concat(chPassthrough) + + emit: chReturn + } + + def wf = workflowInstance.cloneWithName(key_) + + // add factory function + wf.metaClass.run = { runArgs -> + workflowFactory(runArgs, workflowArgs, meta) + } + // add config to module for later introspection + wf.metaClass.config = meta.config + + return wf +} + +nextflow.enable.dsl=2 + +// START COMPONENT-SPECIFIC CODE + +// create meta object +meta = [ + "resources_dir": moduleDir.toRealPath().normalize(), + "config": processConfig(readJsonBlob('''{ + "name" : "bgzip", + "version" : "gunzip", + "argument_groups" : [ + { + "name" : "Inputs", + "arguments" : [ + { + "type" : "file", + "name" : "--input", + "description" : "file to be compressed or decompressed", + "must_exist" : true, + "create_parent" : true, + "required" : true, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + } + ] + }, + { + "name" : "Outputs", + "arguments" : [ + { + "type" : "file", + "name" : "--output", + "description" : "compressed or decompressed output", + "must_exist" : true, + "create_parent" : true, + "required" : true, + "direction" : "output", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "file", + "name" : "--index_name", + "alternatives" : [ + "-I" + ], + "description" : "name of BGZF index file [file.gz.gzi]", + "must_exist" : true, + "create_parent" : true, + "required" : false, + "direction" : "output", + "multiple" : false, + "multiple_sep" : ";" + } + ] + }, + { + "name" : "Arguments", + "arguments" : [ + { + "type" : "integer", + "name" : "--offset", + "alternatives" : [ + "-b" + ], + "description" : "decompress at virtual file pointer (0-based uncompressed offset)", + "required" : false, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "boolean_true", + "name" : "--decompress", + "alternatives" : [ + "-d" + ], + "description" : "decompress the input file", + "direction" : "input" + }, + { + "type" : "boolean_true", + "name" : "--rebgzip", + "alternatives" : [ + "-g" + ], + "description" : "use an index file to bgzip a file", + "direction" : "input" + }, + { + "type" : "boolean_true", + "name" : "--index", + "alternatives" : [ + "-i" + ], + "description" : "compress and create BGZF index", + "direction" : "input" + }, + { + "type" : "integer", + "name" : "--compress_level", + "alternatives" : [ + "-l" + ], + "description" : "compression level to use when compressing; 0 to 9, or -1 for default [-1]", + "required" : false, + "min" : -1, + "max" : 9, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "boolean_true", + "name" : "--reindex", + "alternatives" : [ + "-r" + ], + "description" : "(re)index the output file", + "direction" : "input" + }, + { + "type" : "integer", + "name" : "--size", + "alternatives" : [ + "-s" + ], + "description" : "decompress INT bytes (uncompressed size)", + "required" : false, + "min" : 0, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "boolean_true", + "name" : "--test", + "alternatives" : [ + "-t" + ], + "description" : "test integrity of compressed file", + "direction" : "input" + }, + { + "type" : "boolean_true", + "name" : "--binary", + "description" : "Don't align blocks with text lines", + "direction" : "input" + } + ] + } + ], + "resources" : [ + { + "type" : "bash_script", + "path" : "script.sh", + "is_executable" : true + } + ], + "description" : "Block compression/decompression utility", + "test_resources" : [ + { + "type" : "bash_script", + "path" : "test.sh", + "is_executable" : true + }, + { + "type" : "file", + "path" : "test_data" + } + ], + "status" : "enabled", + "scope" : { + "image" : "public", + "target" : "public" + }, + "requirements" : { + "commands" : [ + "ps" + ] + }, + "license" : "MIT", + "references" : { + "doi" : [ + "10.1093/gigascience/giab007" + ] + }, + "links" : { + "repository" : "https://github.com/samtools/htslib", + "homepage" : "https://www.htslib.org/", + "documentation" : "https://www.htslib.org/doc/bgzip.html" + }, + "runners" : [ + { + "type" : "executable", + "id" : "executable", + "docker_setup_strategy" : "ifneedbepullelsecachedbuild" + }, + { + "type" : "nextflow", + "id" : "nextflow", + "directives" : { + "tag" : "$id" + }, + "auto" : { + "simplifyInput" : true, + "simplifyOutput" : false, + "transcript" : false, + "publish" : false + }, + "config" : { + "labels" : { + "mem1gb" : "memory = 1000000000.B", + "mem2gb" : "memory = 2000000000.B", + "mem5gb" : "memory = 5000000000.B", + "mem10gb" : "memory = 10000000000.B", + "mem20gb" : "memory = 20000000000.B", + "mem50gb" : "memory = 50000000000.B", + "mem100gb" : "memory = 100000000000.B", + "mem200gb" : "memory = 200000000000.B", + "mem500gb" : "memory = 500000000000.B", + "mem1tb" : "memory = 1000000000000.B", + "mem2tb" : "memory = 2000000000000.B", + "mem5tb" : "memory = 5000000000000.B", + "mem10tb" : "memory = 10000000000000.B", + "mem20tb" : "memory = 20000000000000.B", + "mem50tb" : "memory = 50000000000000.B", + "mem100tb" : "memory = 100000000000000.B", + "mem200tb" : "memory = 200000000000000.B", + "mem500tb" : "memory = 500000000000000.B", + "mem1gib" : "memory = 1073741824.B", + "mem2gib" : "memory = 2147483648.B", + "mem4gib" : "memory = 4294967296.B", + "mem8gib" : "memory = 8589934592.B", + "mem16gib" : "memory = 17179869184.B", + "mem32gib" : "memory = 34359738368.B", + "mem64gib" : "memory = 68719476736.B", + "mem128gib" : "memory = 137438953472.B", + "mem256gib" : "memory = 274877906944.B", + "mem512gib" : "memory = 549755813888.B", + "mem1tib" : "memory = 1099511627776.B", + "mem2tib" : "memory = 2199023255552.B", + "mem4tib" : "memory = 4398046511104.B", + "mem8tib" : "memory = 8796093022208.B", + "mem16tib" : "memory = 17592186044416.B", + "mem32tib" : "memory = 35184372088832.B", + "mem64tib" : "memory = 70368744177664.B", + "mem128tib" : "memory = 140737488355328.B", + "mem256tib" : "memory = 281474976710656.B", + "mem512tib" : "memory = 562949953421312.B", + "cpu1" : "cpus = 1", + "cpu2" : "cpus = 2", + "cpu5" : "cpus = 5", + "cpu10" : "cpus = 10", + "cpu20" : "cpus = 20", + "cpu50" : "cpus = 50", + "cpu100" : "cpus = 100", + "cpu200" : "cpus = 200", + "cpu500" : "cpus = 500", + "cpu1000" : "cpus = 1000" + } + }, + "debug" : false, + "container" : "docker" + } + ], + "engines" : [ + { + "type" : "docker", + "id" : "docker", + "image" : "quay.io/biocontainers/htslib:1.19--h81da01d_0", + "target_registry" : "images.viash-hub.com", + "target_tag" : "gunzip", + "namespace_separator" : "/", + "setup" : [ + { + "type" : "docker", + "run" : [ + "bgzip -h | grep 'Version:' 2>&1 | sed 's/Version:\\\\s\\\\(.*\\\\)/bgzip: \\"\\\\1\\"/' > /var/software_versions.txt\n" + ] + } + ] + }, + { + "type" : "native", + "id" : "native" + } + ], + "build_info" : { + "config" : "/workdir/root/repo/src/bgzip/config.vsh.yaml", + "runner" : "nextflow", + "engine" : "docker|native", + "output" : "target/nextflow/bgzip", + "viash_version" : "0.9.4", + "git_commit" : "add30ba0f36bd8a8b07f0ba640707016d04e11b2", + "git_remote" : "https://github.com/viash-hub/toolbox" + }, + "package_config" : { + "name" : "toolbox", + "version" : "gunzip", + "summary" : "A collection of curated command-line tools for general IT tasks, built with Viash.\n", + "description" : "`toolbox` provides a versatile suite of IT components, following the robust Viash (https://viash.io) framework.\nThis package focuses on delivering reliable, standalone tools that can be easily integrated into larger computational workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**, and adherence to **best practices** in component creation. Key features of `toolbox` components include:\n\n* **Standalone & Nextflow Ready:** Execute components directly from the command line or seamlessly incorporate them into Nextflow workflows.\n* **High Quality Standards:**\n * Comprehensive documentation for each component and its parameters.\n * Full exposure of the underlying tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure consistent environments and manage dependencies, leading to enhanced reproducibility.\n * Unit tested to verify functionality and ensure reliability.\n", + "viash_version" : "0.9.4", + "source" : "src", + "target" : "target", + "config_mods" : [ + ".requirements.commands := ['ps']\n", + ".engines += { type: \\"native\\" }", + ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'", + ".engines[.type == 'docker'].target_tag := 'gunzip'" + ], + "keywords" : [ + "toolbox", + "command-line", + "tools" + ], + "license" : "MIT", + "organization" : "vsh", + "links" : { + "repository" : "https://github.com/viash-hub/toolbox", + "issue_tracker" : "https://github.com/viash-hub/toolbox/issues" + } + } +}''')) +] + +// resolve dependencies dependencies (if any) + + +// inner workflow +// inner workflow hook +def innerWorkflowFactory(args) { + def rawScript = '''set -e +tempscript=".viash_script.sh" +cat > "$tempscript" << VIASHMAIN +## VIASH START +# The following code has been auto-generated by Viash. +$( if [ ! -z ${VIASH_PAR_INPUT+x} ]; then echo "${VIASH_PAR_INPUT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_input='&'#" ; else echo "# par_input="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT+x} ]; then echo "${VIASH_PAR_OUTPUT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_output='&'#" ; else echo "# par_output="; fi ) +$( if [ ! -z ${VIASH_PAR_INDEX_NAME+x} ]; then echo "${VIASH_PAR_INDEX_NAME}" | sed "s#'#'\\"'\\"'#g;s#.*#par_index_name='&'#" ; else echo "# par_index_name="; fi ) +$( if [ ! -z ${VIASH_PAR_OFFSET+x} ]; then echo "${VIASH_PAR_OFFSET}" | sed "s#'#'\\"'\\"'#g;s#.*#par_offset='&'#" ; else echo "# par_offset="; fi ) +$( if [ ! -z ${VIASH_PAR_DECOMPRESS+x} ]; then echo "${VIASH_PAR_DECOMPRESS}" | sed "s#'#'\\"'\\"'#g;s#.*#par_decompress='&'#" ; else echo "# par_decompress="; fi ) +$( if [ ! -z ${VIASH_PAR_REBGZIP+x} ]; then echo "${VIASH_PAR_REBGZIP}" | sed "s#'#'\\"'\\"'#g;s#.*#par_rebgzip='&'#" ; else echo "# par_rebgzip="; fi ) +$( if [ ! -z ${VIASH_PAR_INDEX+x} ]; then echo "${VIASH_PAR_INDEX}" | sed "s#'#'\\"'\\"'#g;s#.*#par_index='&'#" ; else echo "# par_index="; fi ) +$( if [ ! -z ${VIASH_PAR_COMPRESS_LEVEL+x} ]; then echo "${VIASH_PAR_COMPRESS_LEVEL}" | sed "s#'#'\\"'\\"'#g;s#.*#par_compress_level='&'#" ; else echo "# par_compress_level="; fi ) +$( if [ ! -z ${VIASH_PAR_REINDEX+x} ]; then echo "${VIASH_PAR_REINDEX}" | sed "s#'#'\\"'\\"'#g;s#.*#par_reindex='&'#" ; else echo "# par_reindex="; fi ) +$( if [ ! -z ${VIASH_PAR_SIZE+x} ]; then echo "${VIASH_PAR_SIZE}" | sed "s#'#'\\"'\\"'#g;s#.*#par_size='&'#" ; else echo "# par_size="; fi ) +$( if [ ! -z ${VIASH_PAR_TEST+x} ]; then echo "${VIASH_PAR_TEST}" | sed "s#'#'\\"'\\"'#g;s#.*#par_test='&'#" ; else echo "# par_test="; fi ) +$( if [ ! -z ${VIASH_PAR_BINARY+x} ]; then echo "${VIASH_PAR_BINARY}" | sed "s#'#'\\"'\\"'#g;s#.*#par_binary='&'#" ; else echo "# par_binary="; fi ) +$( if [ ! -z ${VIASH_META_NAME+x} ]; then echo "${VIASH_META_NAME}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_name='&'#" ; else echo "# meta_name="; fi ) +$( if [ ! -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then echo "${VIASH_META_FUNCTIONALITY_NAME}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_functionality_name='&'#" ; else echo "# meta_functionality_name="; fi ) +$( if [ ! -z ${VIASH_META_RESOURCES_DIR+x} ]; then echo "${VIASH_META_RESOURCES_DIR}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_resources_dir='&'#" ; else echo "# meta_resources_dir="; fi ) +$( if [ ! -z ${VIASH_META_EXECUTABLE+x} ]; then echo "${VIASH_META_EXECUTABLE}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_executable='&'#" ; else echo "# meta_executable="; fi ) +$( if [ ! -z ${VIASH_META_CONFIG+x} ]; then echo "${VIASH_META_CONFIG}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_config='&'#" ; else echo "# meta_config="; fi ) +$( if [ ! -z ${VIASH_META_TEMP_DIR+x} ]; then echo "${VIASH_META_TEMP_DIR}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_temp_dir='&'#" ; else echo "# meta_temp_dir="; fi ) +$( if [ ! -z ${VIASH_META_CPUS+x} ]; then echo "${VIASH_META_CPUS}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_cpus='&'#" ; else echo "# meta_cpus="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_B+x} ]; then echo "${VIASH_META_MEMORY_B}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_b='&'#" ; else echo "# meta_memory_b="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KB+x} ]; then echo "${VIASH_META_MEMORY_KB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_kb='&'#" ; else echo "# meta_memory_kb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MB+x} ]; then echo "${VIASH_META_MEMORY_MB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_mb='&'#" ; else echo "# meta_memory_mb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GB+x} ]; then echo "${VIASH_META_MEMORY_GB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_gb='&'#" ; else echo "# meta_memory_gb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TB+x} ]; then echo "${VIASH_META_MEMORY_TB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_tb='&'#" ; else echo "# meta_memory_tb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PB+x} ]; then echo "${VIASH_META_MEMORY_PB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_pb='&'#" ; else echo "# meta_memory_pb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KIB+x} ]; then echo "${VIASH_META_MEMORY_KIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_kib='&'#" ; else echo "# meta_memory_kib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MIB+x} ]; then echo "${VIASH_META_MEMORY_MIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_mib='&'#" ; else echo "# meta_memory_mib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GIB+x} ]; then echo "${VIASH_META_MEMORY_GIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_gib='&'#" ; else echo "# meta_memory_gib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TIB+x} ]; then echo "${VIASH_META_MEMORY_TIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_tib='&'#" ; else echo "# meta_memory_tib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PIB+x} ]; then echo "${VIASH_META_MEMORY_PIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_pib='&'#" ; else echo "# meta_memory_pib="; fi ) + +## VIASH END +#!/bin/bash + +[[ "\\$par_decompress" == "false" ]] && unset par_decompress +[[ "\\$par_rebgzip" == "false" ]] && unset par_rebgzip +[[ "\\$par_index" == "false" ]] && unset par_index +[[ "\\$par_reindex" == "false" ]] && unset par_reindex +[[ "\\$par_test" == "false" ]] && unset par_test +[[ "\\$par_binary" == "false" ]] && unset par_binary +bgzip -c \\\\ + \\${meta_cpus:+--threads "\\${meta_cpus}"} \\\\ + \\${par_offset:+-b "\\${par_offset}"} \\\\ + \\${par_decompress:+-d} \\\\ + \\${par_rebgzip:+-g} \\\\ + \\${par_index:+-i} \\\\ + \\${par_index_name:+-I "\\${par_index_name}"} \\\\ + \\${par_compress_level:+-l "\\${par_compress_level}"} \\\\ + \\${par_reindex:+-r} \\\\ + \\${par_size:+-s "\\${par_size}"} \\\\ + \\${par_test:+-t} \\\\ + \\${par_binary:+--binary} \\\\ + "\\$par_input" > "\\$par_output" +VIASHMAIN +bash "$tempscript" +''' + + return vdsl3WorkflowFactory(args, meta, rawScript) +} + + + +/** + * Generate a workflow for VDSL3 modules. + * + * This function is called by the workflowFactory() function. + * + * Input channel: [id, input_map] + * Output channel: [id, output_map] + * + * Internally, this workflow will convert the input channel + * to a format which the Nextflow module will be able to handle. + */ +def vdsl3WorkflowFactory(Map args, Map meta, String rawScript) { + def key = args["key"] + def processObj = null + + workflow processWf { + take: input_ + main: + + if (processObj == null) { + processObj = _vdsl3ProcessFactory(args, meta, rawScript) + } + + output_ = input_ + | map { tuple -> + def id = tuple[0] + def data_ = tuple[1] + + if (workflow.stubRun) { + // add id if missing + data_ = [id: 'stub'] + data_ + } + + // process input files separately + def inputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + .collect { par -> + def val = data_.containsKey(par.plainName) ? data_[par.plainName] : [] + def inputFiles = [] + if (val == null) { + inputFiles = [] + } else if (val instanceof List) { + inputFiles = val + } else if (val instanceof Path) { + inputFiles = [ val ] + } else { + inputFiles = [] + } + if (!workflow.stubRun) { + // throw error when an input file doesn't exist + inputFiles.each{ file -> + assert file.exists() : + "Error in module '${key}' id '${id}' argument '${par.plainName}'.\n" + + " Required input file does not exist.\n" + + " Path: '$file'.\n" + + " Expected input file to exist" + } + } + inputFiles + } + + // remove input files + def argsExclInputFiles = meta.config.allArguments + .findAll { (it.type != "file" || it.direction != "input") && data_.containsKey(it.plainName) } + .collectEntries { par -> + def parName = par.plainName + def val = data_[parName] + if (par.multiple && val instanceof Collection) { + val = val.join(par.multiple_sep) + } + if (par.direction == "output" && par.type == "file") { + val = val + .replaceAll('\\$id', id) + .replaceAll('\\$\\{id\\}', id) + .replaceAll('\\$key', key) + .replaceAll('\\$\\{key\\}', key) + } + [parName, val] + } + + [ id ] + inputPaths + [ argsExclInputFiles, meta.resources_dir ] + } + | processObj + | map { output -> + def outputFiles = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .indexed() + .collectEntries{ index, par -> + def out = output[index + 1] + // strip dummy '.exitcode' file from output (see nextflow-io/nextflow#2678) + if (!out instanceof List || out.size() <= 1) { + if (par.multiple) { + out = [] + } else { + assert !par.required : + "Error in module '${key}' id '${output[0]}' argument '${par.plainName}'.\n" + + " Required output file is missing" + out = null + } + } else if (out.size() == 2 && !par.multiple) { + out = out[1] + } else { + out = out.drop(1) + } + [ par.plainName, out ] + } + + // drop null outputs + outputFiles.removeAll{it.value == null} + + [ output[0], outputFiles ] + } + emit: output_ + } + + return processWf +} + +// depends on: session? +def _vdsl3ProcessFactory(Map workflowArgs, Map meta, String rawScript) { + // autodetect process key + def wfKey = workflowArgs["key"] + def procKeyPrefix = "${wfKey}_process" + def scriptMeta = nextflow.script.ScriptMeta.current() + def existing = scriptMeta.getProcessNames().findAll{it.startsWith(procKeyPrefix)} + def numbers = existing.collect{it.replace(procKeyPrefix, "0").toInteger()} + def newNumber = (numbers + [-1]).max() + 1 + + def procKey = newNumber == 0 ? procKeyPrefix : "$procKeyPrefix$newNumber" + + if (newNumber > 0) { + log.warn "Key for module '${wfKey}' is duplicated.\n", + "If you run a component multiple times in the same workflow,\n" + + "it's recommended you set a unique key for every call,\n" + + "for example: ${wfKey}.run(key: \"foo\")." + } + + // subset directives and convert to list of tuples + def drctv = workflowArgs.directives + + // TODO: unit test the two commands below + // convert publish array into tags + def valueToStr = { val -> + // ignore closures + if (val instanceof CharSequence) { + if (!val.matches('^[{].*[}]$')) { + '"' + val + '"' + } else { + val + } + } else if (val instanceof List) { + "[" + val.collect{valueToStr(it)}.join(", ") + "]" + } else if (val instanceof Map) { + "[" + val.collect{k, v -> k + ": " + valueToStr(v)}.join(", ") + "]" + } else { + val.inspect() + } + } + + // multiple entries allowed: label, publishdir + def drctvStrs = drctv.collect { key, value -> + if (key in ["label", "publishDir"]) { + value.collect{ val -> + if (val instanceof Map) { + "\n$key " + val.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ") + } else if (val == null) { + "" + } else { + "\n$key " + valueToStr(val) + } + }.join() + } else if (value instanceof Map) { + "\n$key " + value.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ") + } else { + "\n$key " + valueToStr(value) + } + }.join() + + def inputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + .collect { ', path(viash_par_' + it.plainName + ', stageAs: "_viash_par/' + it.plainName + '_?/*")' } + .join() + + def outputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .collect { par -> + // insert dummy into every output (see nextflow-io/nextflow#2678) + if (!par.multiple) { + ', path{[".exitcode", args.' + par.plainName + ']}' + } else { + ', path{[".exitcode"] + args.' + par.plainName + '}' + } + } + .join() + + // TODO: move this functionality somewhere else? + if (workflowArgs.auto.transcript) { + outputPaths = outputPaths + ', path{[".exitcode", ".command*"]}' + } else { + outputPaths = outputPaths + ', path{[".exitcode"]}' + } + + // create dirs for output files (based on BashWrapper.createParentFiles) + def createParentStr = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" && it.create_parent } + .collect { par -> + def contents = "args[\"${par.plainName}\"] instanceof List ? args[\"${par.plainName}\"].join('\" \"') : args[\"${par.plainName}\"]" + "\${ args.containsKey(\"${par.plainName}\") ? \"mkdir_parent '\" + escapeText(${contents}) + \"'\" : \"\" }" + } + .join("\n") + + // construct inputFileExports + def inputFileExports = meta.config.allArguments + .findAll { it.type == "file" && it.direction.toLowerCase() == "input" } + .collect { par -> + def contents = "viash_par_${par.plainName} instanceof List ? viash_par_${par.plainName}.join(\"${par.multiple_sep}\") : viash_par_${par.plainName}" + "\n\${viash_par_${par.plainName}.empty ? \"\" : \"export VIASH_PAR_${par.plainName.toUpperCase()}='\" + escapeText(${contents}) + \"'\"}" + } + + // NOTE: if using docker, use /tmp instead of tmpDir! + def tmpDir = java.nio.file.Paths.get( + System.getenv('NXF_TEMP') ?: + System.getenv('VIASH_TEMP') ?: + System.getenv('VIASH_TMPDIR') ?: + System.getenv('VIASH_TEMPDIR') ?: + System.getenv('VIASH_TMP') ?: + System.getenv('TEMP') ?: + System.getenv('TMPDIR') ?: + System.getenv('TEMPDIR') ?: + System.getenv('TMP') ?: + '/tmp' + ).toAbsolutePath() + + // construct stub + def stub = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .collect { par -> + "\${ args.containsKey(\"${par.plainName}\") ? \"touch2 \\\"\" + (args[\"${par.plainName}\"] instanceof String ? args[\"${par.plainName}\"].replace(\"_*\", \"_0\") : args[\"${par.plainName}\"].join('\" \"')) + \"\\\"\" : \"\" }" + } + .join("\n") + + // escape script + def escapedScript = rawScript.replace('\\', '\\\\').replace('$', '\\$').replace('"""', '\\"\\"\\"') + + // publishdir assert + def assertStr = (workflowArgs.auto.publish == true) || workflowArgs.auto.transcript ? + """\nassert task.publishDir.size() > 0: "if auto.publish is true, params.publish_dir needs to be defined.\\n Example: --publish_dir './output/'" """ : + "" + + // generate process string + def procStr = + """nextflow.enable.dsl=2 + | + |def escapeText = { s -> s.toString().replaceAll("'", "'\\\"'\\\"'") } + |process $procKey {$drctvStrs + |input: + | tuple val(id)$inputPaths, val(args), path(resourcesDir, stageAs: ".viash_meta_resources") + |output: + | tuple val("\$id")$outputPaths, optional: true + |stub: + |\"\"\" + |touch2() { mkdir -p "\\\$(dirname "\\\$1")" && touch "\\\$1" ; } + |$stub + |\"\"\" + |script:$assertStr + |def parInject = args + | .findAll{key, value -> value != null} + | .collect{key, value -> "export VIASH_PAR_\${key.toUpperCase()}='\${escapeText(value)}'"} + | .join("\\n") + |\"\"\" + |# meta exports + |export VIASH_META_RESOURCES_DIR="\${resourcesDir}" + |export VIASH_META_TEMP_DIR="${['docker', 'podman', 'charliecloud'].any{ it == workflow.containerEngine } ? '/tmp' : tmpDir}" + |export VIASH_META_NAME="${meta.config.name}" + |# export VIASH_META_EXECUTABLE="\\\$VIASH_META_RESOURCES_DIR/\\\$VIASH_META_NAME" + |export VIASH_META_CONFIG="\\\$VIASH_META_RESOURCES_DIR/.config.vsh.yaml" + |\${task.cpus ? "export VIASH_META_CPUS=\$task.cpus" : "" } + |\${task.memory?.bytes != null ? "export VIASH_META_MEMORY_B=\$task.memory.bytes" : "" } + |if [ ! -z \\\${VIASH_META_MEMORY_B+x} ]; then + | export VIASH_META_MEMORY_KB=\\\$(( (\\\$VIASH_META_MEMORY_B+999) / 1000 )) + | export VIASH_META_MEMORY_MB=\\\$(( (\\\$VIASH_META_MEMORY_KB+999) / 1000 )) + | export VIASH_META_MEMORY_GB=\\\$(( (\\\$VIASH_META_MEMORY_MB+999) / 1000 )) + | export VIASH_META_MEMORY_TB=\\\$(( (\\\$VIASH_META_MEMORY_GB+999) / 1000 )) + | export VIASH_META_MEMORY_PB=\\\$(( (\\\$VIASH_META_MEMORY_TB+999) / 1000 )) + | export VIASH_META_MEMORY_KIB=\\\$(( (\\\$VIASH_META_MEMORY_B+1023) / 1024 )) + | export VIASH_META_MEMORY_MIB=\\\$(( (\\\$VIASH_META_MEMORY_KIB+1023) / 1024 )) + | export VIASH_META_MEMORY_GIB=\\\$(( (\\\$VIASH_META_MEMORY_MIB+1023) / 1024 )) + | export VIASH_META_MEMORY_TIB=\\\$(( (\\\$VIASH_META_MEMORY_GIB+1023) / 1024 )) + | export VIASH_META_MEMORY_PIB=\\\$(( (\\\$VIASH_META_MEMORY_TIB+1023) / 1024 )) + |fi + | + |# meta synonyms + |export VIASH_TEMP="\\\$VIASH_META_TEMP_DIR" + |export TEMP_DIR="\\\$VIASH_META_TEMP_DIR" + | + |# create output dirs if need be + |function mkdir_parent { + | for file in "\\\$@"; do + | mkdir -p "\\\$(dirname "\\\$file")" + | done + |} + |$createParentStr + | + |# argument exports${inputFileExports.join()} + |\$parInject + | + |# process script + |${escapedScript} + |\"\"\" + |} + |""".stripMargin() + + // TODO: print on debug + // if (workflowArgs.debug == true) { + // println("######################\n$procStr\n######################") + // } + + // write process to temp file + def tempFile = java.nio.file.Files.createTempFile("viash-process-${procKey}-", ".nf") + addShutdownHook { java.nio.file.Files.deleteIfExists(tempFile) } + tempFile.text = procStr + + // create process from temp file + def binding = new nextflow.script.ScriptBinding([:]) + def session = nextflow.Nextflow.getSession() + def parser = _getScriptLoader(session) + .setModule(true) + .setBinding(binding) + def moduleScript = parser.runScript(tempFile) + .getScript() + + // register module in meta + def module = new nextflow.script.IncludeDef.Module(name: procKey) + scriptMeta.addModule(moduleScript, module.name, module.alias) + + // retrieve and return process from meta + return scriptMeta.getProcess(procKey) +} + +// use Reflection to get a ScriptParser / ScriptLoader +// <25.02.0-edge: new nextflow.script.ScriptParser(session) +// >=25.02.0-edge: nextflow.script.ScriptLoaderFactory.create(session) +def _getScriptLoader(nextflow.Session session) { + // try using the old method + try { + Class scriptParserClass = Class.forName('nextflow.script.ScriptParser') + return scriptParserClass.getDeclaredConstructor(nextflow.Session).newInstance(session) + } catch (ClassNotFoundException e) { + // else try with the new method + try { + Class scriptLoaderFactoryClass = Class.forName('nextflow.script.ScriptLoaderFactory') + def createMethod = scriptLoaderFactoryClass.getDeclaredMethod('create', nextflow.Session) + return createMethod.invoke(null, session) // null because create is static + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e2) { + // Handle the case where neither class is found + throw new Exception("Neither nextflow.script.ScriptParser nor nextflow.script.ScriptLoaderFactory could be found. Is this a compatible Nextflow version?", e2) + } + } +} + +// defaults +meta["defaults"] = [ + // key to be used to trace the process and determine output names + key: null, + + // fixed arguments to be passed to script + args: [:], + + // default directives + directives: readJsonBlob('''{ + "container" : { + "registry" : "images.viash-hub.com", + "image" : "vsh/toolbox/bgzip", + "tag" : "gunzip" + }, + "tag" : "$id" +}'''), + + // auto settings + auto: readJsonBlob('''{ + "simplifyInput" : true, + "simplifyOutput" : false, + "transcript" : false, + "publish" : false +}'''), + + // Apply a map over the incoming tuple + // Example: `{ tup -> [ tup[0], [input: tup[1].output] ] + tup.drop(2) }` + map: null, + + // Apply a map over the ID element of a tuple (i.e. the first element) + // Example: `{ id -> id + "_foo" }` + mapId: null, + + // Apply a map over the data element of a tuple (i.e. the second element) + // Example: `{ data -> [ input: data.output ] }` + mapData: null, + + // Apply a map over the passthrough elements of a tuple (i.e. the tuple excl. the first two elements) + // Example: `{ pt -> pt.drop(1) }` + mapPassthrough: null, + + // Filter the channel + // Example: `{ tup -> tup[0] == "foo" }` + filter: null, + + // Choose whether or not to run the component on the tuple if the condition is true. + // Otherwise, the tuple will be passed through. + // Example: `{ tup -> tup[0] != "skip_this" }` + runIf: null, + + // Rename keys in the data field of the tuple (i.e. the second element) + // Will likely be deprecated in favour of `fromState`. + // Example: `[ "new_key": "old_key" ]` + renameKeys: null, + + // Fetch data from the state and pass it to the module without altering the current state. + // + // `fromState` should be `null`, `List[String]`, `Map[String, String]` or a function. + // + // - If it is `null`, the state will be passed to the module as is. + // - If it is a `List[String]`, the data will be the values of the state at the given keys. + // - If it is a `Map[String, String]`, the data will be the values of the state at the given keys, with the keys renamed according to the map. + // - If it is a function, the tuple (`[id, state]`) in the channel will be passed to the function, and the result will be used as the data. + // + // Example: `{ id, state -> [input: state.fastq_file] }` + // Default: `null` + fromState: null, + + // Determine how the state should be updated after the module has been run. + // + // `toState` should be `null`, `List[String]`, `Map[String, String]` or a function. + // + // - If it is `null`, the state will be replaced with the output of the module. + // - If it is a `List[String]`, the state will be updated with the values of the data at the given keys. + // - If it is a `Map[String, String]`, the state will be updated with the values of the data at the given keys, with the keys renamed according to the map. + // - If it is a function, a tuple (`[id, output, state]`) will be passed to the function, and the result will be used as the new state. + // + // Example: `{ id, output, state -> state + [counts: state.output] }` + // Default: `{ id, output, state -> output }` + toState: null, + + // Whether or not to print debug messages + // Default: `false` + debug: false +] + +// initialise default workflow +meta["workflow"] = workflowFactory([key: meta.config.name], meta.defaults, meta) + +// add workflow to environment +nextflow.script.ScriptMeta.current().addDefinition(meta.workflow) + +// anonymous workflow for running this module as a standalone +workflow { + // add id argument if it's not already in the config + // TODO: deep copy + def newConfig = deepClone(meta.config) + def newParams = deepClone(params) + + def argsContainsId = newConfig.allArguments.any{it.plainName == "id"} + if (!argsContainsId) { + def idArg = [ + 'name': '--id', + 'required': false, + 'type': 'string', + 'description': 'A unique id for every entry.', + 'multiple': false + ] + newConfig.arguments.add(0, idArg) + newConfig = processConfig(newConfig) + } + if (!newParams.containsKey("id")) { + newParams.id = "run" + } + + helpMessage(newConfig) + + channelFromParams(newParams, newConfig) + // make sure id is not in the state if id is not in the args + | map {id, state -> + if (!argsContainsId) { + [id, state.findAll{k, v -> k != "id"}] + } else { + [id, state] + } + } + | meta.workflow.run( + auto: [ publish: "state" ] + ) +} + +// END COMPONENT-SPECIFIC CODE diff --git a/target/nextflow/bgzip/nextflow.config b/target/nextflow/bgzip/nextflow.config new file mode 100644 index 0000000..253f16f --- /dev/null +++ b/target/nextflow/bgzip/nextflow.config @@ -0,0 +1,125 @@ +manifest { + name = 'bgzip' + mainScript = 'main.nf' + nextflowVersion = '!>=20.12.1-edge' + version = 'gunzip' + description = 'Block compression/decompression utility' +} + +process.container = 'nextflow/bash:latest' + +// detect tempdir +tempDir = java.nio.file.Paths.get( + System.getenv('NXF_TEMP') ?: + System.getenv('VIASH_TEMP') ?: + System.getenv('TEMPDIR') ?: + System.getenv('TMPDIR') ?: + '/tmp' +).toAbsolutePath() + +profiles { + no_publish { + process { + withName: '.*' { + publishDir = [ + enabled: false + ] + } + } + } + mount_temp { + docker.temp = tempDir + podman.temp = tempDir + charliecloud.temp = tempDir + } + docker { + docker.enabled = true + // docker.userEmulation = true + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + singularity { + singularity.enabled = true + singularity.autoMounts = true + docker.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + podman { + podman.enabled = true + docker.enabled = false + singularity.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + shifter { + shifter.enabled = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + charliecloud.enabled = false + } + charliecloud { + charliecloud.enabled = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + } +} + +process{ + withLabel: mem1gb { memory = 1000000000.B } + withLabel: mem2gb { memory = 2000000000.B } + withLabel: mem5gb { memory = 5000000000.B } + withLabel: mem10gb { memory = 10000000000.B } + withLabel: mem20gb { memory = 20000000000.B } + withLabel: mem50gb { memory = 50000000000.B } + withLabel: mem100gb { memory = 100000000000.B } + withLabel: mem200gb { memory = 200000000000.B } + withLabel: mem500gb { memory = 500000000000.B } + withLabel: mem1tb { memory = 1000000000000.B } + withLabel: mem2tb { memory = 2000000000000.B } + withLabel: mem5tb { memory = 5000000000000.B } + withLabel: mem10tb { memory = 10000000000000.B } + withLabel: mem20tb { memory = 20000000000000.B } + withLabel: mem50tb { memory = 50000000000000.B } + withLabel: mem100tb { memory = 100000000000000.B } + withLabel: mem200tb { memory = 200000000000000.B } + withLabel: mem500tb { memory = 500000000000000.B } + withLabel: mem1gib { memory = 1073741824.B } + withLabel: mem2gib { memory = 2147483648.B } + withLabel: mem4gib { memory = 4294967296.B } + withLabel: mem8gib { memory = 8589934592.B } + withLabel: mem16gib { memory = 17179869184.B } + withLabel: mem32gib { memory = 34359738368.B } + withLabel: mem64gib { memory = 68719476736.B } + withLabel: mem128gib { memory = 137438953472.B } + withLabel: mem256gib { memory = 274877906944.B } + withLabel: mem512gib { memory = 549755813888.B } + withLabel: mem1tib { memory = 1099511627776.B } + withLabel: mem2tib { memory = 2199023255552.B } + withLabel: mem4tib { memory = 4398046511104.B } + withLabel: mem8tib { memory = 8796093022208.B } + withLabel: mem16tib { memory = 17592186044416.B } + withLabel: mem32tib { memory = 35184372088832.B } + withLabel: mem64tib { memory = 70368744177664.B } + withLabel: mem128tib { memory = 140737488355328.B } + withLabel: mem256tib { memory = 281474976710656.B } + withLabel: mem512tib { memory = 562949953421312.B } + withLabel: cpu1 { cpus = 1 } + withLabel: cpu2 { cpus = 2 } + withLabel: cpu5 { cpus = 5 } + withLabel: cpu10 { cpus = 10 } + withLabel: cpu20 { cpus = 20 } + withLabel: cpu50 { cpus = 50 } + withLabel: cpu100 { cpus = 100 } + withLabel: cpu200 { cpus = 200 } + withLabel: cpu500 { cpus = 500 } + withLabel: cpu1000 { cpus = 1000 } +} + + diff --git a/target/nextflow/bgzip/nextflow_schema.json b/target/nextflow/bgzip/nextflow_schema.json new file mode 100644 index 0000000..95d4b30 --- /dev/null +++ b/target/nextflow/bgzip/nextflow_schema.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "bgzip", + "description": "Block compression/decompression utility", + "type": "object", + "$defs": { + "inputs": { + "title": "Inputs", + "type": "object", + "description": "No description", + "properties": { + "input": { + "type": "string", + "format": "path", + "exists": true, + "description": "file to be compressed or decompressed", + "help_text": "Type: `file`, multiple: `False`, required, direction: `input`. " + } + } + }, + "outputs": { + "title": "Outputs", + "type": "object", + "description": "No description", + "properties": { + "output": { + "type": "string", + "format": "path", + "description": "compressed or decompressed output", + "help_text": "Type: `file`, multiple: `False`, required, default: `\"$id.$key.output\"`, direction: `output`. ", + "default": "$id.$key.output" + }, + "index_name": { + "type": "string", + "format": "path", + "description": "name of BGZF index file [file.gz.gzi]", + "help_text": "Type: `file`, multiple: `False`, default: `\"$id.$key.index_name\"`, direction: `output`. ", + "default": "$id.$key.index_name" + } + } + }, + "arguments": { + "title": "Arguments", + "type": "object", + "description": "No description", + "properties": { + "offset": { + "type": "integer", + "description": "decompress at virtual file pointer (0-based uncompressed offset)", + "help_text": "Type: `integer`, multiple: `False`. " + }, + "decompress": { + "type": "boolean", + "description": "decompress the input file", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + }, + "rebgzip": { + "type": "boolean", + "description": "use an index file to bgzip a file", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + }, + "index": { + "type": "boolean", + "description": "compress and create BGZF index", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + }, + "compress_level": { + "type": "integer", + "description": "compression level to use when compressing; 0 to 9, or -1 for default [-1]", + "help_text": "Type: `integer`, multiple: `False`. " + }, + "reindex": { + "type": "boolean", + "description": "(re)index the output file", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + }, + "size": { + "type": "integer", + "description": "decompress INT bytes (uncompressed size)", + "help_text": "Type: `integer`, multiple: `False`. " + }, + "test": { + "type": "boolean", + "description": "test integrity of compressed file", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + }, + "binary": { + "type": "boolean", + "description": "Don't align blocks with text lines", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + } + } + }, + "nextflow input-output arguments": { + "title": "Nextflow input-output arguments", + "type": "object", + "description": "Input/output parameters for Nextflow itself. Please note that both publishDir and publish_dir are supported but at least one has to be configured.", + "properties": { + "publish_dir": { + "type": "string", + "description": "Path to an output directory.", + "help_text": "Type: `string`, multiple: `False`, required, example: `\"output/\"`. " + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/inputs" + }, + { + "$ref": "#/$defs/outputs" + }, + { + "$ref": "#/$defs/arguments" + }, + { + "$ref": "#/$defs/nextflow input-output arguments" + } + ] +} diff --git a/target/nextflow/yq/.config.vsh.yaml b/target/nextflow/yq/.config.vsh.yaml new file mode 100644 index 0000000..03bf1a4 --- /dev/null +++ b/target/nextflow/yq/.config.vsh.yaml @@ -0,0 +1,297 @@ +name: "yq" +version: "gunzip" +argument_groups: +- name: "Inputs" + arguments: + - type: "file" + name: "--input" + description: "files to be processed" + info: null + example: + - "input.yaml" + must_exist: true + create_parent: true + required: true + direction: "input" + multiple: false + multiple_sep: ";" +- name: "Outputs" + arguments: + - type: "file" + name: "--output" + description: "output file" + info: null + example: + - "output.yaml" + must_exist: true + create_parent: true + required: true + direction: "output" + multiple: false + multiple_sep: ";" +- name: "Arguments" + arguments: + - type: "string" + name: "--eval" + description: "expression to evaluate" + info: null + example: + - ".name = \"foo\"" + required: true + direction: "input" + multiple: false + multiple_sep: ";" + - type: "integer" + name: "--indent" + alternatives: + - "-I" + description: "sets indent level for output (default 2)" + info: null + required: false + direction: "input" + multiple: false + multiple_sep: ";" + - type: "string" + name: "--input_format" + alternatives: + - "-p" + description: "parse format for input. (default \"auto\")" + info: null + required: false + choices: + - "auto" + - "a" + - "yaml" + - "y" + - "json" + - "j" + - "props" + - "p" + - "csv" + - "c" + - "tsv" + - "t" + - "xml" + - "x" + - "base64" + - "uri" + - "toml" + - "shell" + - "s" + - "lua" + - "l" + direction: "input" + multiple: false + multiple_sep: ";" + - type: "string" + name: "--output_format" + alternatives: + - "-o" + description: "output format type. (default \"auto\")" + info: null + required: false + choices: + - "auto" + - "a" + - "yaml" + - "y" + - "json" + - "j" + - "props" + - "p" + - "csv" + - "c" + - "tsv" + - "t" + - "xml" + - "x" + - "base64" + - "uri" + - "toml" + - "shell" + - "s" + - "lua" + - "l" + direction: "input" + multiple: false + multiple_sep: ";" + - type: "boolean_true" + name: "--pretty_print" + alternatives: + - "-P" + description: "pretty print, shorthand for '... style = \"\"'" + info: null + direction: "input" +resources: +- type: "bash_script" + text: | + #!/bin/sh + [[ "$par_pretty_print" == "false" ]] && unset par_pretty_print + yq eval \ + ${par_indent:+-I "${par_indent}"} \ + ${par_input_format:+-p "${par_input_format}"} \ + ${par_output_format:+-o "${par_output_format}"} \ + ${par_pretty_print:+-P} \ + --expression "$par_eval" \ + --no-colors \ + "$par_input" > "$par_output" + + + dest: "./script.sh" + is_executable: true +description: "A portable YAML, JSON, XML, CSV, TOML and properties processor" +test_resources: +- type: "bash_script" + text: "set -e\necho \"name: 'bar'\" > test.yaml\n\"$meta_executable\" --input test.yaml\ + \ --output output.yaml --eval '.name = \"foo\"'\n\"$meta_executable\" --input\ + \ output.yaml --output output2.yaml --eval '.name'\ngrep \"^foo$\" output2.yaml\n" + dest: "./script.sh" + is_executable: true +info: null +status: "enabled" +scope: + image: "public" + target: "public" +requirements: + commands: + - "ps" +keywords: +- "yaml" +- "json" +- "xml" +- "csv" +- "toml" +- "properties" +license: "MIT" +links: + repository: "https://github.com/mikefarah/yq" + homepage: "https://mikefarah.gitbook.io/yq" + documentation: "https://mikefarah.gitbook.io/yq/" +runners: +- type: "executable" + id: "executable" + docker_setup_strategy: "ifneedbepullelsecachedbuild" +- type: "nextflow" + id: "nextflow" + directives: + tag: "$id" + auto: + simplifyInput: true + simplifyOutput: false + transcript: false + publish: false + config: + labels: + mem1gb: "memory = 1000000000.B" + mem2gb: "memory = 2000000000.B" + mem5gb: "memory = 5000000000.B" + mem10gb: "memory = 10000000000.B" + mem20gb: "memory = 20000000000.B" + mem50gb: "memory = 50000000000.B" + mem100gb: "memory = 100000000000.B" + mem200gb: "memory = 200000000000.B" + mem500gb: "memory = 500000000000.B" + mem1tb: "memory = 1000000000000.B" + mem2tb: "memory = 2000000000000.B" + mem5tb: "memory = 5000000000000.B" + mem10tb: "memory = 10000000000000.B" + mem20tb: "memory = 20000000000000.B" + mem50tb: "memory = 50000000000000.B" + mem100tb: "memory = 100000000000000.B" + mem200tb: "memory = 200000000000000.B" + mem500tb: "memory = 500000000000000.B" + mem1gib: "memory = 1073741824.B" + mem2gib: "memory = 2147483648.B" + mem4gib: "memory = 4294967296.B" + mem8gib: "memory = 8589934592.B" + mem16gib: "memory = 17179869184.B" + mem32gib: "memory = 34359738368.B" + mem64gib: "memory = 68719476736.B" + mem128gib: "memory = 137438953472.B" + mem256gib: "memory = 274877906944.B" + mem512gib: "memory = 549755813888.B" + mem1tib: "memory = 1099511627776.B" + mem2tib: "memory = 2199023255552.B" + mem4tib: "memory = 4398046511104.B" + mem8tib: "memory = 8796093022208.B" + mem16tib: "memory = 17592186044416.B" + mem32tib: "memory = 35184372088832.B" + mem64tib: "memory = 70368744177664.B" + mem128tib: "memory = 140737488355328.B" + mem256tib: "memory = 281474976710656.B" + mem512tib: "memory = 562949953421312.B" + cpu1: "cpus = 1" + cpu2: "cpus = 2" + cpu5: "cpus = 5" + cpu10: "cpus = 10" + cpu20: "cpus = 20" + cpu50: "cpus = 50" + cpu100: "cpus = 100" + cpu200: "cpus = 200" + cpu500: "cpus = 500" + cpu1000: "cpus = 1000" + debug: false + container: "docker" +engines: +- type: "docker" + id: "docker" + image: "alpine:latest" + target_registry: "images.viash-hub.com" + target_tag: "gunzip" + namespace_separator: "/" + setup: + - type: "apk" + packages: + - "bash" + - "yq-go" + - type: "docker" + run: + - "/usr/bin/yq --version | sed 's/.*version\\sv\\(.*\\)/yq: \"\\1\"/' > /var/software_versions.txt\n" + entrypoint: [] + cmd: null +- type: "native" + id: "native" +build_info: + config: "src/yq/config.vsh.yaml" + runner: "nextflow" + engine: "docker|native" + output: "target/nextflow/yq" + executable: "target/nextflow/yq/main.nf" + viash_version: "0.9.4" + git_commit: "add30ba0f36bd8a8b07f0ba640707016d04e11b2" + git_remote: "https://github.com/viash-hub/toolbox" +package_config: + name: "toolbox" + version: "gunzip" + summary: "A collection of curated command-line tools for general IT tasks, built\ + \ with Viash.\n" + description: "`toolbox` provides a versatile suite of IT components, following the\ + \ robust Viash (https://viash.io) framework.\nThis package focuses on delivering\ + \ reliable, standalone tools that can be easily integrated into larger computational\ + \ workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**,\ + \ and adherence to **best practices** in component creation. Key features of `toolbox`\ + \ components include:\n\n* **Standalone & Nextflow Ready:** Execute components\ + \ directly from the command line or seamlessly incorporate them into Nextflow\ + \ workflows.\n* **High Quality Standards:**\n * Comprehensive documentation\ + \ for each component and its parameters.\n * Full exposure of the underlying\ + \ tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure\ + \ consistent environments and manage dependencies, leading to enhanced reproducibility.\n\ + \ * Unit tested to verify functionality and ensure reliability.\n" + info: null + viash_version: "0.9.4" + source: "src" + target: "target" + config_mods: + - ".requirements.commands := ['ps']\n" + - ".engines += { type: \"native\" }" + - ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'" + - ".engines[.type == 'docker'].target_tag := 'gunzip'" + keywords: + - "toolbox" + - "command-line" + - "tools" + license: "MIT" + organization: "vsh" + links: + repository: "https://github.com/viash-hub/toolbox" + issue_tracker: "https://github.com/viash-hub/toolbox/issues" diff --git a/target/nextflow/yq/main.nf b/target/nextflow/yq/main.nf new file mode 100644 index 0000000..015ab55 --- /dev/null +++ b/target/nextflow/yq/main.nf @@ -0,0 +1,3914 @@ +// yq gunzip +// +// This wrapper script is auto-generated by viash 0.9.4 and is thus a derivative +// work thereof. This software comes with ABSOLUTELY NO WARRANTY from Data +// Intuitive. +// +// The component may contain files which fall under a different license. The +// authors of this component should specify the license in the header of such +// files, or include a separate license file detailing the licenses of all included +// files. + +//////////////////////////// +// VDSL3 helper functions // +//////////////////////////// + +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_checkArgumentType.nf' +class UnexpectedArgumentTypeException extends Exception { + String errorIdentifier + String stage + String plainName + String expectedClass + String foundClass + + // ${key ? " in module '$key'" : ""}${id ? " id '$id'" : ""} + UnexpectedArgumentTypeException(String errorIdentifier, String stage, String plainName, String expectedClass, String foundClass) { + super("Error${errorIdentifier ? " $errorIdentifier" : ""}:${stage ? " $stage" : "" } argument '${plainName}' has the wrong type. " + + "Expected type: ${expectedClass}. Found type: ${foundClass}") + this.errorIdentifier = errorIdentifier + this.stage = stage + this.plainName = plainName + this.expectedClass = expectedClass + this.foundClass = foundClass + } +} + +/** + * Checks if the given value is of the expected type. If not, an exception is thrown. + * + * @param stage The stage of the argument (input or output) + * @param par The parameter definition + * @param value The value to check + * @param errorIdentifier The identifier to use in the error message + * @return The value, if it is of the expected type + * @throws UnexpectedArgumentTypeException If the value is not of the expected type +*/ +def _checkArgumentType(String stage, Map par, Object value, String errorIdentifier) { + // expectedClass will only be != null if value is not of the expected type + def expectedClass = null + def foundClass = null + + // todo: split if need be + + if (!par.required && value == null) { + expectedClass = null + } else if (par.multiple) { + if (value !instanceof Collection) { + value = [value] + } + + // split strings + value = value.collectMany{ val -> + if (val instanceof String) { + // collect() to ensure that the result is a List and not simply an array + val.split(par.multiple_sep).collect() + } else { + [val] + } + } + + // process globs + if (par.type == "file" && par.direction == "input") { + value = value.collect{ it instanceof String ? file(it, hidden: true) : it }.flatten() + } + + // check types of elements in list + try { + value = value.collect { listVal -> + _checkArgumentType(stage, par + [multiple: false], listVal, errorIdentifier) + } + } catch (UnexpectedArgumentTypeException e) { + expectedClass = "List[${e.expectedClass}]" + foundClass = "List[${e.foundClass}]" + } + } else if (par.type == "string") { + // cast to string if need be. only cast if the value is a GString + if (value instanceof GString) { + value = value as String + } + expectedClass = value instanceof String ? null : "String" + } else if (par.type == "integer") { + // cast to integer if need be + if (value !instanceof Integer) { + try { + value = value as Integer + } catch (NumberFormatException e) { + expectedClass = "Integer" + } + } + } else if (par.type == "long") { + // cast to long if need be + if (value !instanceof Long) { + try { + value = value as Long + } catch (NumberFormatException e) { + expectedClass = "Long" + } + } + } else if (par.type == "double") { + // cast to double if need be + if (value !instanceof Double) { + try { + value = value as Double + } catch (NumberFormatException e) { + expectedClass = "Double" + } + } + } else if (par.type == "float") { + // cast to float if need be + if (value !instanceof Float) { + try { + value = value as Float + } catch (NumberFormatException e) { + expectedClass = "Float" + } + } + } else if (par.type == "boolean" | par.type == "boolean_true" | par.type == "boolean_false") { + // cast to boolean if need be + if (value !instanceof Boolean) { + try { + value = value as Boolean + } catch (Exception e) { + expectedClass = "Boolean" + } + } + } else if (par.type == "file" && (par.direction == "input" || stage == "output")) { + // cast to path if need be + if (value instanceof String) { + value = file(value, hidden: true) + } + if (value instanceof File) { + value = value.toPath() + } + expectedClass = value instanceof Path ? null : "Path" + } else if (par.type == "file" && stage == "input" && par.direction == "output") { + // cast to string if need be + if (value !instanceof String) { + try { + value = value as String + } catch (Exception e) { + expectedClass = "String" + } + } + } else { + // didn't find a match for par.type + expectedClass = par.type + } + + if (expectedClass != null) { + if (foundClass == null) { + foundClass = value.getClass().getName() + } + throw new UnexpectedArgumentTypeException(errorIdentifier, stage, par.plainName, expectedClass, foundClass) + } + + return value +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processInputValues.nf' +Map _processInputValues(Map inputs, Map config, String id, String key) { + if (!workflow.stubRun) { + config.allArguments.each { arg -> + if (arg.required && arg.direction == "input") { + assert inputs.containsKey(arg.plainName) && inputs.get(arg.plainName) != null : + "Error in module '${key}' id '${id}': required input argument '${arg.plainName}' is missing" + } + } + + inputs = inputs.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid input argument" + + value = _checkArgumentType("input", par, value, "in module '$key' id '$id'") + + [ name, value ] + } + } + return inputs +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processOutputValues.nf' +Map _checkValidOutputArgument(Map outputs, Map config, String id, String key) { + if (!workflow.stubRun) { + outputs = outputs.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && it.direction == "output" } + assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid output argument" + + value = _checkArgumentType("output", par, value, "in module '$key' id '$id'") + + [ name, value ] + } + } + return outputs +} + +void _checkAllRequiredOuputsPresent(Map outputs, Map config, String id, String key) { + if (!workflow.stubRun) { + config.allArguments.each { arg -> + if (arg.direction == "output" && arg.required) { + assert outputs.containsKey(arg.plainName) && outputs.get(arg.plainName) != null : + "Error in module '${key}' id '${id}': required output argument '${arg.plainName}' is missing" + } + } + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/IDChecker.nf' +class IDChecker { + final def items = [] as Set + + @groovy.transform.WithWriteLock + boolean observe(String item) { + if (items.contains(item)) { + return false + } else { + items << item + return true + } + } + + @groovy.transform.WithReadLock + boolean contains(String item) { + return items.contains(item) + } + + @groovy.transform.WithReadLock + Set getItems() { + return items.clone() + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_checkUniqueIds.nf' + +/** + * Check if the ids are unique across parameter sets + * + * @param parameterSets a list of parameter sets. + */ +private void _checkUniqueIds(List>> parameterSets) { + def ppIds = parameterSets.collect{it[0]} + assert ppIds.size() == ppIds.unique().size() : "All argument sets should have unique ids. Detected ids: $ppIds" +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_getChild.nf' + +// helper functions for reading params from file // +def _getChild(parent, child) { + if (child.contains("://") || java.nio.file.Paths.get(child).isAbsolute()) { + child + } else { + def parentAbsolute = java.nio.file.Paths.get(parent).toAbsolutePath().toString() + parentAbsolute.replaceAll('/[^/]*$', "/") + child + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_parseParamList.nf' +/** + * Figure out the param list format based on the file extension + * + * @param param_list A String containing the path to the parameter list file. + * + * @return A String containing the format of the parameter list file. + */ +def _paramListGuessFormat(param_list) { + if (param_list !instanceof String) { + "asis" + } else if (param_list.endsWith(".csv")) { + "csv" + } else if (param_list.endsWith(".json") || param_list.endsWith(".jsn")) { + "json" + } else if (param_list.endsWith(".yaml") || param_list.endsWith(".yml")) { + "yaml" + } else { + "yaml_blob" + } +} + + +/** + * Read the param list + * + * @param param_list One of the following: + * - A String containing the path to the parameter list file (csv, json or yaml), + * - A yaml blob of a list of maps (yaml_blob), + * - Or a groovy list of maps (asis). + * @param config A Map of the Viash configuration. + * + * @return A List of Maps containing the parameters. + */ +def _parseParamList(param_list, Map config) { + // first determine format by extension + def paramListFormat = _paramListGuessFormat(param_list) + + def paramListPath = (paramListFormat != "asis" && paramListFormat != "yaml_blob") ? + file(param_list, hidden: true) : + null + + // get the correct parser function for the detected params_list format + def paramSets = [] + if (paramListFormat == "asis") { + paramSets = param_list + } else if (paramListFormat == "yaml_blob") { + paramSets = readYamlBlob(param_list) + } else if (paramListFormat == "yaml") { + paramSets = readYaml(paramListPath) + } else if (paramListFormat == "json") { + paramSets = readJson(paramListPath) + } else if (paramListFormat == "csv") { + paramSets = readCsv(paramListPath) + } else { + error "Format of provided --param_list not recognised.\n" + + "Found: '$paramListFormat'.\n" + + "Expected: a csv file, a json file, a yaml file,\n" + + "a yaml blob or a groovy list of maps." + } + + // data checks + assert paramSets instanceof List: "--param_list should contain a list of maps" + for (value in paramSets) { + assert value instanceof Map: "--param_list should contain a list of maps" + } + + // id is argument + def idIsArgument = config.allArguments.any{it.plainName == "id"} + + // Reformat from List to List> by adding the ID as first element of a Tuple2 + paramSets = paramSets.collect({ data -> + def id = data.id + if (!idIsArgument) { + data = data.findAll{k, v -> k != "id"} + } + [id, data] + }) + + // Split parameters with 'multiple: true' + paramSets = paramSets.collect({ id, data -> + data = _splitParams(data, config) + [id, data] + }) + + // The paths of input files inside a param_list file may have been specified relatively to the + // location of the param_list file. These paths must be made absolute. + if (paramListPath) { + paramSets = paramSets.collect({ id, data -> + def new_data = data.collectEntries{ parName, parValue -> + def par = config.allArguments.find{it.plainName == parName} + if (par && par.type == "file" && par.direction == "input") { + if (parValue instanceof Collection) { + parValue = parValue.collectMany{path -> + def x = _resolveSiblingIfNotAbsolute(path, paramListPath) + x instanceof Collection ? x : [x] + } + } else { + parValue = _resolveSiblingIfNotAbsolute(parValue, paramListPath) + } + } + [parName, parValue] + } + [id, new_data] + }) + } + + return paramSets +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_splitParams.nf' +/** + * Split parameters for arguments that accept multiple values using their separator + * + * @param paramList A Map containing parameters to split. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A Map of parameters where the parameter values have been split into a list using + * their seperator. + */ +Map _splitParams(Map parValues, Map config){ + def parsedParamValues = parValues.collectEntries { parName, parValue -> + def parameterSettings = config.allArguments.find({it.plainName == parName}) + + if (!parameterSettings) { + // if argument is not found, do not alter + return [parName, parValue] + } + if (parameterSettings.multiple) { // Check if parameter can accept multiple values + if (parValue instanceof Collection) { + parValue = parValue.collect{it instanceof String ? it.split(parameterSettings.multiple_sep) : it } + } else if (parValue instanceof String) { + parValue = parValue.split(parameterSettings.multiple_sep) + } else if (parValue == null) { + parValue = [] + } else { + parValue = [ parValue ] + } + parValue = parValue.flatten() + } + // For all parameters check if multiple values are only passed for + // arguments that allow it. Quietly simplify lists of length 1. + if (!parameterSettings.multiple && parValue instanceof Collection) { + assert parValue.size() == 1 : + "Error: argument ${parName} has too many values.\n" + + " Expected amount: 1. Found: ${parValue.size()}" + parValue = parValue[0] + } + [parName, parValue] + } + return parsedParamValues +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/channelFromParams.nf' +/** + * Parse nextflow parameters based on settings defined in a viash config. + * Return a list of parameter sets, each parameter set corresponding to + * an event in a nextflow channel. The output from this function can be used + * with Channel.fromList to create a nextflow channel with Vdsl3 formatted + * events. + * + * This function performs: + * - A filtering of the params which can be found in the config file. + * - Process the params_list argument which allows a user to to initialise + * a Vsdl3 channel with multiple parameter sets. Possible formats are + * csv, json, yaml, or simply a yaml_blob. A csv should have column names + * which correspond to the different arguments of this pipeline. A json or a yaml + * file should be a list of maps, each of which has keys corresponding to the + * arguments of the pipeline. A yaml blob can also be passed directly as a parameter. + * When passing a csv, json or yaml, relative path names are relativized to the + * location of the parameter file. + * - Combine the parameter sets into a vdsl3 Channel. + * + * @param params Input parameters. Can optionaly contain a 'param_list' key that + * provides a list of arguments that can be split up into multiple events + * in the output channel possible formats of param_lists are: a csv file, + * json file, a yaml file or a yaml blob. Each parameters set (event) must + * have a unique ID. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A list of parameters with the first element of the event being + * the event ID and the second element containing a map of the parsed parameters. + */ + +private List>> _paramsToParamSets(Map params, Map config){ + // todo: fetch key from run args + def key_ = config.name + + /* parse regular parameters (not in param_list) */ + /*************************************************/ + def globalParams = config.allArguments + .findAll { params.containsKey(it.plainName) } + .collectEntries { [ it.plainName, params[it.plainName] ] } + def globalID = params.get("id", null) + + /* process params_list arguments */ + /*********************************/ + def paramList = params.containsKey("param_list") && params.param_list != null ? + params.param_list : [] + // if (paramList instanceof String) { + // paramList = [paramList] + // } + // def paramSets = paramList.collectMany{ _parseParamList(it, config) } + // TODO: be able to process param_list when it is a list of strings + def paramSets = _parseParamList(paramList, config) + if (paramSets.isEmpty()) { + paramSets = [[null, [:]]] + } + + /* combine arguments into channel */ + /**********************************/ + def processedParams = paramSets.indexed().collect{ index, tup -> + // Process ID + def id = tup[0] ?: globalID + + if (workflow.stubRun && !id) { + // if stub run, explicitly add an id if missing + id = "stub${index}" + } + assert id != null: "Each parameter set should have at least an 'id'" + + // Process params + def parValues = globalParams + tup[1] + // // Remove parameters which are null, if the default is also null + // parValues = parValues.collectEntries{paramName, paramValue -> + // parameterSettings = config.functionality.allArguments.find({it.plainName == paramName}) + // if ( paramValue != null || parameterSettings.get("default", null) != null ) { + // [paramName, paramValue] + // } + // } + parValues = parValues.collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + assert par != null : "Error in module '${key_}' id '${id}': '${name}' is not a valid input argument" + + if (par == null) { + return [:] + } + value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'") + + [ name, value ] + } + + [id, parValues] + } + + // Check if ids (first element of each list) is unique + _checkUniqueIds(processedParams) + return processedParams +} + +/** + * Parse nextflow parameters based on settings defined in a viash config + * and return a nextflow channel. + * + * @param params Input parameters. Can optionaly contain a 'param_list' key that + * provides a list of arguments that can be split up into multiple events + * in the output channel possible formats of param_lists are: a csv file, + * json file, a yaml file or a yaml blob. Each parameters set (event) must + * have a unique ID. + * @param config A Map of the Viash configuration. This Map can be generated from the config file + * using the readConfig() function. + * + * @return A nextflow Channel with events. Events are formatted as a tuple that contains + * first contains the ID of the event and as second element holds a parameter map. + * + * + */ +def channelFromParams(Map params, Map config) { + def processedParams = _paramsToParamSets(params, config) + return Channel.fromList(processedParams) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/checkUniqueIds.nf' +def checkUniqueIds(Map args) { + def stopOnError = args.stopOnError == null ? args.stopOnError : true + + def idChecker = new IDChecker() + + return filter { tup -> + if (!idChecker.observe(tup[0])) { + if (stopOnError) { + error "Duplicate id: ${tup[0]}" + } else { + log.warn "Duplicate id: ${tup[0]}, removing duplicate entry" + return false + } + } + return true + } +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/preprocessInputs.nf' +// This helper file will be deprecated soon +preprocessInputsDeprecationWarningPrinted = false + +def preprocessInputsDeprecationWarning() { + if (!preprocessInputsDeprecationWarningPrinted) { + preprocessInputsDeprecationWarningPrinted = true + System.err.println("Warning: preprocessInputs() is deprecated and will be removed in Viash 0.9.0.") + } +} + +/** + * Generate a nextflow Workflow that allows processing a channel of + * Vdsl3 formatted events and apply a Viash config to them: + * - Gather default parameters from the Viash config and make + * sure that they are correctly formatted (see applyConfig method). + * - Format the input parameters (also using the applyConfig method). + * - Apply the default parameter to the input parameters. + * - Do some assertions: + * ~ Check if the event IDs in the channel are unique. + * + * The events in the channel are formatted as tuples, with the + * first element of the tuples being a unique id of the parameter set, + * and the second element containg the the parameters themselves. + * Optional extra elements of the tuples will be passed to the output as is. + * + * @param args A map that must contain a 'config' key that points + * to a parsed config (see readConfig()). Optionally, a + * 'key' key can be provided which can be used to create a unique + * name for the workflow process. + * + * @return A workflow that allows processing a channel of Vdsl3 formatted events + * and apply a Viash config to them. + */ +def preprocessInputs(Map args) { + preprocessInputsDeprecationWarning() + + def config = args.config + assert config instanceof Map : + "Error in preprocessInputs: config must be a map. " + + "Expected class: Map. Found: config.getClass() is ${config.getClass()}" + def key_ = args.key ?: config.name + + // Get different parameter types (used throughout this function) + def defaultArgs = config.allArguments + .findAll { it.containsKey("default") } + .collectEntries { [ it.plainName, it.default ] } + + map { tup -> + def id = tup[0] + def data = tup[1] + def passthrough = tup.drop(2) + + def new_data = (defaultArgs + data).collectEntries { name, value -> + def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") } + + if (par != null) { + value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'") + } + + [ name, value ] + } + + [ id, new_data ] + passthrough + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runComponents.nf' +/** + * Run a list of components on a stream of data. + * + * @param components: list of Viash VDSL3 modules to run + * @param fromState: a closure, a map or a list of keys to extract from the input data. + * If a closure, it will be called with the id, the data and the component config. + * @param toState: a closure, a map or a list of keys to extract from the output data + * If a closure, it will be called with the id, the output data, the old state and the component config. + * @param filter: filter function to apply to the input. + * It will be called with the id, the data and the component config. + * @param id: id to use for the output data + * If a closure, it will be called with the id, the data and the component config. + * @param auto: auto options to pass to the components + * + * @return: a workflow that runs the components + **/ +def runComponents(Map args) { + log.warn("runComponents is deprecated, use runEach instead") + assert args.components: "runComponents should be passed a list of components to run" + + def components_ = args.components + if (components_ !instanceof List) { + components_ = [ components_ ] + } + assert components_.size() > 0: "pass at least one component to runComponents" + + def fromState_ = args.fromState + def toState_ = args.toState + def filter_ = args.filter + def id_ = args.id + + workflow runComponentsWf { + take: input_ch + main: + + // generate one channel per method + out_chs = components_.collect{ comp_ -> + def comp_config = comp_.config + + def filter_ch = filter_ + ? input_ch | filter{tup -> + filter_(tup[0], tup[1], comp_config) + } + : input_ch + def id_ch = id_ + ? filter_ch | map{tup -> + // def new_id = id_(tup[0], tup[1], comp_config) + def new_id = tup[0] + if (id_ instanceof String) { + new_id = id_ + } else if (id_ instanceof Closure) { + new_id = id_(new_id, tup[1], comp_config) + } + [new_id] + tup.drop(1) + } + : filter_ch + def data_ch = id_ch | map{tup -> + def new_data = tup[1] + if (fromState_ instanceof Map) { + new_data = fromState_.collectEntries{ key0, key1 -> + [key0, new_data[key1]] + } + } else if (fromState_ instanceof List) { + new_data = fromState_.collectEntries{ key -> + [key, new_data[key]] + } + } else if (fromState_ instanceof Closure) { + new_data = fromState_(tup[0], new_data, comp_config) + } + tup.take(1) + [new_data] + tup.drop(1) + } + def out_ch = data_ch + | comp_.run( + auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false] + ) + def post_ch = toState_ + ? out_ch | map{tup -> + def output = tup[1] + def old_state = tup[2] + def new_state = null + if (toState_ instanceof Map) { + new_state = old_state + toState_.collectEntries{ key0, key1 -> + [key0, output[key1]] + } + } else if (toState_ instanceof List) { + new_state = old_state + toState_.collectEntries{ key -> + [key, output[key]] + } + } else if (toState_ instanceof Closure) { + new_state = toState_(tup[0], output, old_state, comp_config) + } + [tup[0], new_state] + tup.drop(3) + } + : out_ch + + post_ch + } + + // mix all results + output_ch = + (out_chs.size == 1) + ? out_chs[0] + : out_chs[0].mix(*out_chs.drop(1)) + + emit: output_ch + } + + return runComponentsWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runEach.nf' +/** + * Run a list of components on a stream of data. + * + * @param components: list of Viash VDSL3 modules to run + * @param fromState: a closure, a map or a list of keys to extract from the input data. + * If a closure, it will be called with the id, the data and the component itself. + * @param toState: a closure, a map or a list of keys to extract from the output data + * If a closure, it will be called with the id, the output data, the old state and the component itself. + * @param filter: filter function to apply to the input. + * It will be called with the id, the data and the component itself. + * @param id: id to use for the output data + * If a closure, it will be called with the id, the data and the component itself. + * @param auto: auto options to pass to the components + * + * @return: a workflow that runs the components + **/ +def runEach(Map args) { + assert args.components: "runEach should be passed a list of components to run" + + def components_ = args.components + if (components_ !instanceof List) { + components_ = [ components_ ] + } + assert components_.size() > 0: "pass at least one component to runEach" + + def fromState_ = args.fromState + def toState_ = args.toState + def filter_ = args.filter + def runIf_ = args.runIf + def id_ = args.id + + assert !runIf_ || runIf_ instanceof Closure: "runEach: must pass a Closure to runIf." + + workflow runEachWf { + take: input_ch + main: + + // generate one channel per method + out_chs = components_.collect{ comp_ -> + def filter_ch = filter_ + ? input_ch | filter{tup -> + filter_(tup[0], tup[1], comp_) + } + : input_ch + def id_ch = id_ + ? filter_ch | map{tup -> + def new_id = id_ + if (new_id instanceof Closure) { + new_id = new_id(tup[0], tup[1], comp_) + } + assert new_id instanceof String : "Error in runEach: id should be a String or a Closure that returns a String. Expected: id instanceof String. Found: ${new_id.getClass()}" + [new_id] + tup.drop(1) + } + : filter_ch + def chPassthrough = null + def chRun = null + if (runIf_) { + def idRunIfBranch = id_ch.branch{ tup -> + run: runIf_(tup[0], tup[1], comp_) + passthrough: true + } + chPassthrough = idRunIfBranch.passthrough + chRun = idRunIfBranch.run + } else { + chRun = id_ch + chPassthrough = Channel.empty() + } + def data_ch = chRun | map{tup -> + def new_data = tup[1] + if (fromState_ instanceof Map) { + new_data = fromState_.collectEntries{ key0, key1 -> + [key0, new_data[key1]] + } + } else if (fromState_ instanceof List) { + new_data = fromState_.collectEntries{ key -> + [key, new_data[key]] + } + } else if (fromState_ instanceof Closure) { + new_data = fromState_(tup[0], new_data, comp_) + } + tup.take(1) + [new_data] + tup.drop(1) + } + def out_ch = data_ch + | comp_.run( + auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false] + ) + def post_ch = toState_ + ? out_ch | map{tup -> + def output = tup[1] + def old_state = tup[2] + def new_state = null + if (toState_ instanceof Map) { + new_state = old_state + toState_.collectEntries{ key0, key1 -> + [key0, output[key1]] + } + } else if (toState_ instanceof List) { + new_state = old_state + toState_.collectEntries{ key -> + [key, output[key]] + } + } else if (toState_ instanceof Closure) { + new_state = toState_(tup[0], output, old_state, comp_) + } + [tup[0], new_state] + tup.drop(3) + } + : out_ch + + def return_ch = post_ch + | concat(chPassthrough) + + return_ch + } + + // mix all results + output_ch = + (out_chs.size == 1) + ? out_chs[0] + : out_chs[0].mix(*out_chs.drop(1)) + + emit: output_ch + } + + return runEachWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/safeJoin.nf' +/** + * Join sourceChannel to targetChannel + * + * This function joins the sourceChannel to the targetChannel. + * However, each id in the targetChannel must be present in the + * sourceChannel. If _meta.join_id exists in the targetChannel, that is + * used as an id instead. If the id doesn't match any id in the sourceChannel, + * an error is thrown. + */ + +def safeJoin(targetChannel, sourceChannel, key) { + def sourceIDs = new IDChecker() + + def sourceCheck = sourceChannel + | map { tup -> + sourceIDs.observe(tup[0]) + tup + } + def targetCheck = targetChannel + | map { tup -> + def id = tup[0] + + if (!sourceIDs.contains(id)) { + error ( + "Error in module '${key}' when merging output with original state.\n" + + " Reason: output with id '${id}' could not be joined with source channel.\n" + + " If the IDs in the output channel differ from the input channel,\n" + + " please set `tup[1]._meta.join_id to the original ID.\n" + + " Original IDs in input channel: ['${sourceIDs.getItems().join("', '")}'].\n" + + " Unexpected ID in the output channel: '${id}'.\n" + + " Example input event: [\"id\", [input: file(...)]],\n" + + " Example output event: [\"newid\", [output: file(...), _meta: [join_id: \"id\"]]]" + ) + } + // TODO: add link to our documentation on how to fix this + + tup + } + + sourceCheck.cross(targetChannel) + | map{ left, right -> + right + left.drop(1) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/_processArgument.nf' +def _processArgument(arg) { + arg.multiple = arg.multiple != null ? arg.multiple : false + arg.required = arg.required != null ? arg.required : false + arg.direction = arg.direction != null ? arg.direction : "input" + arg.multiple_sep = arg.multiple_sep != null ? arg.multiple_sep : ";" + arg.plainName = arg.name.replaceAll("^-*", "") + + if (arg.type == "file") { + arg.must_exist = arg.must_exist != null ? arg.must_exist : true + arg.create_parent = arg.create_parent != null ? arg.create_parent : true + } + + // add default values to output files which haven't already got a default + if (arg.type == "file" && arg.direction == "output" && arg.default == null) { + def mult = arg.multiple ? "_*" : "" + def extSearch = "" + if (arg.default != null) { + extSearch = arg.default + } else if (arg.example != null) { + extSearch = arg.example + } + if (extSearch instanceof List) { + extSearch = extSearch[0] + } + def extSearchResult = extSearch.find("\\.[^\\.]+\$") + def ext = extSearchResult != null ? extSearchResult : "" + arg.default = "\$id.\$key.${arg.plainName}${mult}${ext}" + if (arg.multiple) { + arg.default = [arg.default] + } + } + + if (!arg.multiple) { + if (arg.default != null && arg.default instanceof List) { + arg.default = arg.default[0] + } + if (arg.example != null && arg.example instanceof List) { + arg.example = arg.example[0] + } + } + + if (arg.type == "boolean_true") { + arg.default = false + } + if (arg.type == "boolean_false") { + arg.default = true + } + + arg +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/addGlobalParams.nf' +def addGlobalArguments(config) { + def localConfig = [ + "argument_groups": [ + [ + "name": "Nextflow input-output arguments", + "description": "Input/output parameters for Nextflow itself. Please note that both publishDir and publish_dir are supported but at least one has to be configured.", + "arguments" : [ + [ + 'name': '--publish_dir', + 'required': true, + 'type': 'string', + 'description': 'Path to an output directory.', + 'example': 'output/', + 'multiple': false + ], + [ + 'name': '--param_list', + 'required': false, + 'type': 'string', + 'description': '''Allows inputting multiple parameter sets to initialise a Nextflow channel. A `param_list` can either be a list of maps, a csv file, a json file, a yaml file, or simply a yaml blob. + | + |* A list of maps (as-is) where the keys of each map corresponds to the arguments of the pipeline. Example: in a `nextflow.config` file: `param_list: [ ['id': 'foo', 'input': 'foo.txt'], ['id': 'bar', 'input': 'bar.txt'] ]`. + |* A csv file should have column names which correspond to the different arguments of this pipeline. Example: `--param_list data.csv` with columns `id,input`. + |* A json or a yaml file should be a list of maps, each of which has keys corresponding to the arguments of the pipeline. Example: `--param_list data.json` with contents `[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]`. + |* A yaml blob can also be passed directly as a string. Example: `--param_list "[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]"`. + | + |When passing a csv, json or yaml file, relative path names are relativized to the location of the parameter file. No relativation is performed when `param_list` is a list of maps (as-is) or a yaml blob.'''.stripMargin(), + 'example': 'my_params.yaml', + 'multiple': false, + 'hidden': true + ] + // TODO: allow multiple: true in param_list? + // TODO: allow to specify a --param_list_regex to filter the param_list? + // TODO: allow to specify a --param_list_from_state to remap entries in the param_list? + ] + ] + ] + ] + + return processConfig(_mergeMap(config, localConfig)) +} + +def _mergeMap(Map lhs, Map rhs) { + return rhs.inject(lhs.clone()) { map, entry -> + if (map[entry.key] instanceof Map && entry.value instanceof Map) { + map[entry.key] = _mergeMap(map[entry.key], entry.value) + } else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) { + map[entry.key] += entry.value + } else { + map[entry.key] = entry.value + } + return map + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/generateHelp.nf' +def _generateArgumentHelp(param) { + // alternatives are not supported + // def names = param.alternatives ::: List(param.name) + + def unnamedProps = [ + ["required parameter", param.required], + ["multiple values allowed", param.multiple], + ["output", param.direction.toLowerCase() == "output"], + ["file must exist", param.type == "file" && param.must_exist] + ].findAll{it[1]}.collect{it[0]} + + def dflt = null + if (param.default != null) { + if (param.default instanceof List) { + dflt = param.default.join(param.multiple_sep != null ? param.multiple_sep : ", ") + } else { + dflt = param.default.toString() + } + } + def example = null + if (param.example != null) { + if (param.example instanceof List) { + example = param.example.join(param.multiple_sep != null ? param.multiple_sep : ", ") + } else { + example = param.example.toString() + } + } + def min = param.min?.toString() + def max = param.max?.toString() + + def escapeChoice = { choice -> + def s1 = choice.replaceAll("\\n", "\\\\n") + def s2 = s1.replaceAll("\"", """\\\"""") + s2.contains(",") || s2 != choice ? "\"" + s2 + "\"" : s2 + } + def choices = param.choices == null ? + null : + "[ " + param.choices.collect{escapeChoice(it.toString())}.join(", ") + " ]" + + def namedPropsStr = [ + ["type", ([param.type] + unnamedProps).join(", ")], + ["default", dflt], + ["example", example], + ["choices", choices], + ["min", min], + ["max", max] + ] + .findAll{it[1]} + .collect{"\n " + it[0] + ": " + it[1].replaceAll("\n", "\\n")} + .join("") + + def descStr = param.description == null ? + "" : + _paragraphWrap("\n" + param.description.trim(), 80 - 8).join("\n ") + + "\n --" + param.plainName + + namedPropsStr + + descStr +} + +// Based on Helper.generateHelp() in Helper.scala +def _generateHelp(config) { + def fun = config + + // PART 1: NAME AND VERSION + def nameStr = fun.name + + (fun.version == null ? "" : " " + fun.version) + + // PART 2: DESCRIPTION + def descrStr = fun.description == null ? + "" : + "\n\n" + _paragraphWrap(fun.description.trim(), 80).join("\n") + + // PART 3: Usage + def usageStr = fun.usage == null ? + "" : + "\n\nUsage:\n" + fun.usage.trim() + + // PART 4: Options + def argGroupStrs = fun.allArgumentGroups.collect{argGroup -> + def name = argGroup.name + def descriptionStr = argGroup.description == null ? + "" : + "\n " + _paragraphWrap(argGroup.description.trim(), 80-4).join("\n ") + "\n" + def arguments = argGroup.arguments.collect{arg -> + arg instanceof String ? fun.allArguments.find{it.plainName == arg} : arg + }.findAll{it != null} + def argumentStrs = arguments.collect{param -> _generateArgumentHelp(param)} + + "\n\n$name:" + + descriptionStr + + argumentStrs.join("\n") + } + + // FINAL: combine + def out = nameStr + + descrStr + + usageStr + + argGroupStrs.join("") + + return out +} + +// based on Format._paragraphWrap +def _paragraphWrap(str, maxLength) { + def outLines = [] + str.split("\n").each{par -> + def words = par.split("\\s").toList() + + def word = null + def line = words.pop() + while(!words.isEmpty()) { + word = words.pop() + if (line.length() + word.length() + 1 <= maxLength) { + line = line + " " + word + } else { + outLines.add(line) + line = word + } + } + if (words.isEmpty()) { + outLines.add(line) + } + } + return outLines +} + +def helpMessage(config) { + if (params.containsKey("help") && params.help) { + def mergedConfig = addGlobalArguments(config) + def helpStr = _generateHelp(mergedConfig) + println(helpStr) + exit 0 + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/processConfig.nf' +def processConfig(config) { + // set defaults for arguments + config.arguments = + (config.arguments ?: []).collect{_processArgument(it)} + + // set defaults for argument_group arguments + config.argument_groups = + (config.argument_groups ?: []).collect{grp -> + grp.arguments = (grp.arguments ?: []).collect{_processArgument(it)} + grp + } + + // create combined arguments list + config.allArguments = + config.arguments + + config.argument_groups.collectMany{it.arguments} + + // add missing argument groups (based on Functionality::allArgumentGroups()) + def argGroups = config.argument_groups + if (argGroups.any{it.name.toLowerCase() == "arguments"}) { + argGroups = argGroups.collect{ grp -> + if (grp.name.toLowerCase() == "arguments") { + grp = grp + [ + arguments: grp.arguments + config.arguments + ] + } + grp + } + } else { + argGroups = argGroups + [ + name: "Arguments", + arguments: config.arguments + ] + } + config.allArgumentGroups = argGroups + + config +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/config/readConfig.nf' + +def readConfig(file) { + def config = readYaml(file ?: moduleDir.resolve("config.vsh.yaml")) + processConfig(config) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_resolveSiblingIfNotAbsolute.nf' +/** + * Resolve a path relative to the current file. + * + * @param str The path to resolve, as a String. + * @param parentPath The path to resolve relative to, as a Path. + * + * @return The path that may have been resovled, as a Path. + */ +def _resolveSiblingIfNotAbsolute(str, parentPath) { + if (str !instanceof String) { + return str + } + if (!_stringIsAbsolutePath(str)) { + return parentPath.resolveSibling(str) + } else { + return file(str, hidden: true) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_stringIsAbsolutePath.nf' +/** + * Check whether a path as a string is absolute. + * + * In the past, we tried using `file(., relative: true).isAbsolute()`, + * but the 'relative' option was added in 22.10.0. + * + * @param path The path to check, as a String. + * + * @return Whether the path is absolute, as a boolean. + */ +def _stringIsAbsolutePath(path) { + def _resolve_URL_PROTOCOL = ~/^([a-zA-Z][a-zA-Z0-9]*:)?\\/.+/ + + assert path instanceof String + return _resolve_URL_PROTOCOL.matcher(path).matches() +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/collectTraces.nf' +class CustomTraceObserver implements nextflow.trace.TraceObserver { + List traces + + CustomTraceObserver(List traces) { + this.traces = traces + } + + @Override + void onProcessComplete(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) { + def trace2 = trace.store.clone() + trace2.script = null + traces.add(trace2) + } + + @Override + void onProcessCached(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) { + def trace2 = trace.store.clone() + trace2.script = null + traces.add(trace2) + } +} + +def collectTraces() { + def traces = Collections.synchronizedList([]) + + // add custom trace observer which stores traces in the traces object + session.observers.add(new CustomTraceObserver(traces)) + + traces +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/deepClone.nf' +/** + * Performs a deep clone of the given object. + * @param x an object + */ +def deepClone(x) { + iterateMap(x, {it instanceof Cloneable ? it.clone() : it}) +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getPublishDir.nf' +def getPublishDir() { + return params.containsKey("publish_dir") ? params.publish_dir : + params.containsKey("publishDir") ? params.publishDir : + null +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getRootDir.nf' + +// Recurse upwards until we find a '.build.yaml' file +def _findBuildYamlFile(pathPossiblySymlink) { + def path = pathPossiblySymlink.toRealPath() + def child = path.resolve(".build.yaml") + if (java.nio.file.Files.isDirectory(path) && java.nio.file.Files.exists(child)) { + return child + } else { + def parent = path.getParent() + if (parent == null) { + return null + } else { + return _findBuildYamlFile(parent) + } + } +} + +// get the root of the target folder +def getRootDir() { + def dir = _findBuildYamlFile(meta.resources_dir) + assert dir != null: "Could not find .build.yaml in the folder structure" + dir.getParent() +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/iterateMap.nf' +/** + * Recursively apply a function over the leaves of an object. + * @param obj The object to iterate over. + * @param fun The function to apply to each value. + * @return The object with the function applied to each value. + */ +def iterateMap(obj, fun) { + if (obj instanceof List && obj !instanceof String) { + return obj.collect{item -> + iterateMap(item, fun) + } + } else if (obj instanceof Map) { + return obj.collectEntries{key, item -> + [key.toString(), iterateMap(item, fun)] + } + } else { + return fun(obj) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/niceView.nf' +/** + * A view for printing the event of each channel as a YAML blob. + * This is useful for debugging. + */ +def niceView() { + workflow niceViewWf { + take: input + main: + output = input + | view{toYamlBlob(it)} + emit: output + } + return niceViewWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readCsv.nf' + +def readCsv(file_path) { + def output = [] + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + + // todo: allow escaped quotes in string + // todo: allow single quotes? + def splitRegex = java.util.regex.Pattern.compile(''',(?=(?:[^"]*"[^"]*")*[^"]*$)''') + def removeQuote = java.util.regex.Pattern.compile('''"(.*)"''') + + def br = java.nio.file.Files.newBufferedReader(inputFile) + + def row = -1 + def header = null + while (br.ready() && header == null) { + def line = br.readLine() + row++ + if (!line.startsWith("#")) { + header = splitRegex.split(line, -1).collect{field -> + m = removeQuote.matcher(field) + m.find() ? m.replaceFirst('$1') : field + } + } + } + assert header != null: "CSV file should contain a header" + + while (br.ready()) { + def line = br.readLine() + row++ + if (line == null) { + br.close() + break + } + + if (!line.startsWith("#")) { + def predata = splitRegex.split(line, -1) + def data = predata.collect{field -> + if (field == "") { + return null + } + def m = removeQuote.matcher(field) + if (m.find()) { + return m.replaceFirst('$1') + } else { + return field + } + } + assert header.size() == data.size(): "Row $row should contain the same number as fields as the header" + + def dataMap = [header, data].transpose().collectEntries().findAll{it.value != null} + output.add(dataMap) + } + } + + output +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJson.nf' +def readJson(file_path) { + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + def jsonSlurper = new groovy.json.JsonSlurper() + jsonSlurper.parse(inputFile) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJsonBlob.nf' +def readJsonBlob(str) { + def jsonSlurper = new groovy.json.JsonSlurper() + jsonSlurper.parseText(str) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readTaggedYaml.nf' +// Custom constructor to modify how certain objects are parsed from YAML +class CustomConstructor extends org.yaml.snakeyaml.constructor.Constructor { + Path root + + class ConstructPath extends org.yaml.snakeyaml.constructor.AbstractConstruct { + public Object construct(org.yaml.snakeyaml.nodes.Node node) { + String filename = (String) constructScalar(node); + if (root != null) { + return root.resolve(filename); + } + return java.nio.file.Paths.get(filename); + } + } + + CustomConstructor(org.yaml.snakeyaml.LoaderOptions options, Path root) { + super(options) + this.root = root + // Handling !file tag and parse it back to a File type + this.yamlConstructors.put(new org.yaml.snakeyaml.nodes.Tag("!file"), new ConstructPath()) + } +} + +def readTaggedYaml(Path path) { + def options = new org.yaml.snakeyaml.LoaderOptions() + def constructor = new CustomConstructor(options, path.getParent()) + def yaml = new org.yaml.snakeyaml.Yaml(constructor) + return yaml.load(path.text) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYaml.nf' +def readYaml(file_path) { + def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path + def yamlSlurper = new org.yaml.snakeyaml.Yaml() + yamlSlurper.load(inputFile) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYamlBlob.nf' +def readYamlBlob(str) { + def yamlSlurper = new org.yaml.snakeyaml.Yaml() + yamlSlurper.load(str) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toJsonBlob.nf' +String toJsonBlob(data) { + return groovy.json.JsonOutput.toJson(data) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toTaggedYamlBlob.nf' +// Custom representer to modify how certain objects are represented in YAML +class CustomRepresenter extends org.yaml.snakeyaml.representer.Representer { + Path relativizer + + class RepresentPath implements org.yaml.snakeyaml.representer.Represent { + public String getFileName(Object obj) { + if (obj instanceof File) { + obj = ((File) obj).toPath(); + } + if (obj !instanceof Path) { + throw new IllegalArgumentException("Object: " + obj + " is not a Path or File"); + } + def path = (Path) obj; + + if (relativizer != null) { + return relativizer.relativize(path).toString() + } else { + return path.toString() + } + } + + public org.yaml.snakeyaml.nodes.Node representData(Object data) { + String filename = getFileName(data); + def tag = new org.yaml.snakeyaml.nodes.Tag("!file"); + return representScalar(tag, filename); + } + } + CustomRepresenter(org.yaml.snakeyaml.DumperOptions options, Path relativizer) { + super(options) + this.relativizer = relativizer + this.representers.put(sun.nio.fs.UnixPath, new RepresentPath()) + this.representers.put(Path, new RepresentPath()) + this.representers.put(File, new RepresentPath()) + } +} + +String toTaggedYamlBlob(data) { + return toRelativeTaggedYamlBlob(data, null) +} +String toRelativeTaggedYamlBlob(data, Path relativizer) { + def options = new org.yaml.snakeyaml.DumperOptions() + options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK) + def representer = new CustomRepresenter(options, relativizer) + def yaml = new org.yaml.snakeyaml.Yaml(representer, options) + return yaml.dump(data) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toYamlBlob.nf' +String toYamlBlob(data) { + def options = new org.yaml.snakeyaml.DumperOptions() + options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK) + options.setPrettyFlow(true) + def yaml = new org.yaml.snakeyaml.Yaml(options) + def cleanData = iterateMap(data, { it instanceof Path ? it.toString() : it }) + return yaml.dump(cleanData) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeJson.nf' +void writeJson(data, file) { + assert data: "writeJson: data should not be null" + assert file: "writeJson: file should not be null" + file.write(toJsonBlob(data)) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeYaml.nf' +void writeYaml(data, file) { + assert data: "writeYaml: data should not be null" + assert file: "writeYaml: file should not be null" + file.write(toYamlBlob(data)) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/findStates.nf' +def findStates(Map params, Map config) { + def auto_config = deepClone(config) + def auto_params = deepClone(params) + + auto_config = auto_config.clone() + // override arguments + auto_config.argument_groups = [] + auto_config.arguments = [ + [ + type: "string", + name: "--id", + description: "A dummy identifier", + required: false + ], + [ + type: "file", + name: "--input_states", + example: "/path/to/input/directory/**/state.yaml", + description: "Path to input directory containing the datasets to be integrated.", + required: true, + multiple: true, + multiple_sep: ";" + ], + [ + type: "string", + name: "--filter", + example: "foo/.*/state.yaml", + description: "Regex to filter state files by path.", + required: false + ], + // to do: make this a yaml blob? + [ + type: "string", + name: "--rename_keys", + example: ["newKey1:oldKey1", "newKey2:oldKey2"], + description: "Rename keys in the detected input files. This is useful if the input files do not match the set of input arguments of the workflow.", + required: false, + multiple: true, + multiple_sep: ";" + ], + [ + type: "string", + name: "--settings", + example: '{"output_dataset": "dataset.h5ad", "k": 10}', + description: "Global arguments as a JSON glob to be passed to all components.", + required: false + ] + ] + if (!(auto_params.containsKey("id"))) { + auto_params["id"] = "auto" + } + + // run auto config through processConfig once more + auto_config = processConfig(auto_config) + + workflow findStatesWf { + helpMessage(auto_config) + + output_ch = + channelFromParams(auto_params, auto_config) + | flatMap { autoId, args -> + + def globalSettings = args.settings ? readYamlBlob(args.settings) : [:] + + // look for state files in input dir + def stateFiles = args.input_states + + // filter state files by regex + if (args.filter) { + stateFiles = stateFiles.findAll{ stateFile -> + def stateFileStr = stateFile.toString() + def matcher = stateFileStr =~ args.filter + matcher.matches()} + } + + // read in states + def states = stateFiles.collect { stateFile -> + def state_ = readTaggedYaml(stateFile) + [state_.id, state_] + } + + // construct renameMap + if (args.rename_keys) { + def renameMap = args.rename_keys.collectEntries{renameString -> + def split = renameString.split(":") + assert split.size() == 2: "Argument 'rename_keys' should be of the form 'newKey:oldKey', or 'newKey:oldKey;newKey:oldKey' in case of multiple values" + split + } + + // rename keys in state, only let states through which have all keys + // also add global settings + states = states.collectMany{id, state -> + def newState = [:] + + for (key in renameMap.keySet()) { + def origKey = renameMap[key] + if (!(state.containsKey(origKey))) { + return [] + } + newState[key] = state[origKey] + } + + [[id, globalSettings + newState]] + } + } + + states + } + emit: + output_ch + } + + return findStatesWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/joinStates.nf' +def joinStates(Closure apply_) { + workflow joinStatesWf { + take: input_ch + main: + output_ch = input_ch + | toSortedList + | filter{ it.size() > 0 } + | map{ tups -> + def ids = tups.collect{it[0]} + def states = tups.collect{it[1]} + apply_(ids, states) + } + + emit: output_ch + } + return joinStatesWf +} +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/publishFiles.nf' +def publishFiles(Map args) { + def key_ = args.get("key") + + assert key_ != null : "publishFiles: key must be specified" + + workflow publishFilesWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] + + // the input files and the target output filenames + def inputoutputFilenames_ = collectInputOutputPaths(state_, id_ + "." + key_).transpose() + def inputFiles_ = inputoutputFilenames_[0] + def outputFilenames_ = inputoutputFilenames_[1] + + [id_, inputFiles_, outputFilenames_] + } + | publishFilesProc + emit: input_ch + } + return publishFilesWf +} + +process publishFilesProc { + // todo: check publishpath? + publishDir path: "${getPublishDir()}/", mode: "copy" + tag "$id" + input: + tuple val(id), path(inputFiles, stageAs: "_inputfile?/*"), val(outputFiles) + output: + tuple val(id), path{outputFiles} + script: + def copyCommands = [ + inputFiles instanceof List ? inputFiles : [inputFiles], + outputFiles instanceof List ? outputFiles : [outputFiles] + ] + .transpose() + .collectMany{infile, outfile -> + if (infile.toString() != outfile.toString()) { + [ + "[ -d \"\$(dirname '${outfile.toString()}')\" ] || mkdir -p \"\$(dirname '${outfile.toString()}')\"", + "cp -r '${infile.toString()}' '${outfile.toString()}'" + ] + } else { + // no need to copy if infile is the same as outfile + [] + } + } + """ + echo "Copying output files to destination folder" + ${copyCommands.join("\n ")} + """ +} + + +// this assumes that the state contains no other values other than those specified in the config +def publishFilesByConfig(Map args) { + def config = args.get("config") + assert config != null : "publishFilesByConfig: config must be specified" + + def key_ = args.get("key", config.name) + assert key_ != null : "publishFilesByConfig: key must be specified" + + workflow publishFilesSimpleWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] // e.g. [output: new File("myoutput.h5ad"), k: 10] + def origState_ = tup[2] // e.g. [output: '$id.$key.foo.h5ad'] + + + // the processed state is a list of [key, value, inputPath, outputFilename] tuples, where + // - key is a String + // - value is any object that can be serialized to a Yaml (so a String/Integer/Long/Double/Boolean, a List, a Map, or a Path) + // - inputPath is a List[Path] + // - outputFilename is a List[String] + // - (inputPath, outputFilename) are the files that will be copied from src to dest (relative to the state.yaml) + def processedState = + config.allArguments + .findAll { it.direction == "output" } + .collectMany { par -> + def plainName_ = par.plainName + // if the state does not contain the key, it's an + // optional argument for which the component did + // not generate any output OR multiple channels were emitted + // and the output was just not added to using the channel + // that is now being parsed + if (!state_.containsKey(plainName_)) { + return [] + } + def value = state_[plainName_] + // if the parameter is not a file, it should be stored + // in the state as-is, but is not something that needs + // to be copied from the source path to the dest path + if (par.type != "file") { + return [[inputPath: [], outputFilename: []]] + } + // if the orig state does not contain this filename, + // it's an optional argument for which the user specified + // that it should not be returned as a state + if (!origState_.containsKey(plainName_)) { + return [] + } + def filenameTemplate = origState_[plainName_] + // if the pararameter is multiple: true, fetch the template + if (par.multiple && filenameTemplate instanceof List) { + filenameTemplate = filenameTemplate[0] + } + // instantiate the template + def filename = filenameTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + if (par.multiple) { + // if the parameter is multiple: true, the filename + // should contain a wildcard '*' that is replaced with + // the index of the file + assert filename.contains("*") : "Module '${key_}' id '${id_}': Multiple output files specified, but no wildcard '*' in the filename: ${filename}" + def outputPerFile = value.withIndex().collect{ val, ix -> + def filename_ix = filename.replace("*", ix.toString()) + def inputPath = val instanceof File ? val.toPath() : val + [inputPath: inputPath, outputFilename: filename_ix] + } + def transposedOutputs = ["inputPath", "outputFilename"].collectEntries{ key -> + [key, outputPerFile.collect{dic -> dic[key]}] + } + return [[key: plainName_] + transposedOutputs] + } else { + def value_ = java.nio.file.Paths.get(filename) + def inputPath = value instanceof File ? value.toPath() : value + return [[inputPath: [inputPath], outputFilename: [filename]]] + } + } + + def inputPaths = processedState.collectMany{it.inputPath} + def outputFilenames = processedState.collectMany{it.outputFilename} + + + [id_, inputPaths, outputFilenames] + } + | publishFilesProc + emit: input_ch + } + return publishFilesSimpleWf +} + + + + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/publishStates.nf' +def collectFiles(obj) { + if (obj instanceof java.io.File || obj instanceof Path) { + return [obj] + } else if (obj instanceof List && obj !instanceof String) { + return obj.collectMany{item -> + collectFiles(item) + } + } else if (obj instanceof Map) { + return obj.collectMany{key, item -> + collectFiles(item) + } + } else { + return [] + } +} + +/** + * Recurse through a state and collect all input files and their target output filenames. + * @param obj The state to recurse through. + * @param prefix The prefix to prepend to the output filenames. + */ +def collectInputOutputPaths(obj, prefix) { + if (obj instanceof File || obj instanceof Path) { + def path = obj instanceof Path ? obj : obj.toPath() + def ext = path.getFileName().toString().find("\\.[^\\.]+\$") ?: "" + def newFilename = prefix + ext + return [[obj, newFilename]] + } else if (obj instanceof List && obj !instanceof String) { + return obj.withIndex().collectMany{item, ix -> + collectInputOutputPaths(item, prefix + "_" + ix) + } + } else if (obj instanceof Map) { + return obj.collectMany{key, item -> + collectInputOutputPaths(item, prefix + "." + key) + } + } else { + return [] + } +} + +def publishStates(Map args) { + def key_ = args.get("key") + def yamlTemplate_ = args.get("output_state", args.get("outputState", '$id.$key.state.yaml')) + + assert key_ != null : "publishStates: key must be specified" + + workflow publishStatesWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] + + // the input files and the target output filenames + def inputoutputFilenames_ = collectInputOutputPaths(state_, id_ + "." + key_).transpose() + + def yamlFilename = yamlTemplate_ + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + + // TODO: do the pathnames in state_ match up with the outputFilenames_? + + // convert state to yaml blob + def yamlBlob_ = toRelativeTaggedYamlBlob([id: id_] + state_, java.nio.file.Paths.get(yamlFilename)) + + [id_, yamlBlob_, yamlFilename] + } + | publishStatesProc + emit: input_ch + } + return publishStatesWf +} +process publishStatesProc { + // todo: check publishpath? + publishDir path: "${getPublishDir()}/", mode: "copy" + tag "$id" + input: + tuple val(id), val(yamlBlob), val(yamlFile) + output: + tuple val(id), path{[yamlFile]} + script: + """ + mkdir -p "\$(dirname '${yamlFile}')" + echo "Storing state as yaml" + cat > '${yamlFile}' << HERE +${yamlBlob} +HERE + """ +} + + +// this assumes that the state contains no other values other than those specified in the config +def publishStatesByConfig(Map args) { + def config = args.get("config") + assert config != null : "publishStatesByConfig: config must be specified" + + def key_ = args.get("key", config.name) + assert key_ != null : "publishStatesByConfig: key must be specified" + + workflow publishStatesSimpleWf { + take: input_ch + main: + input_ch + | map { tup -> + def id_ = tup[0] + def state_ = tup[1] // e.g. [output: new File("myoutput.h5ad"), k: 10] + def origState_ = tup[2] // e.g. [output: '$id.$key.foo.h5ad'] + + // TODO: allow overriding the state.yaml template + // TODO TODO: if auto.publish == "state", add output_state as an argument + def yamlTemplate = params.containsKey("output_state") ? params.output_state : '$id.$key.state.yaml' + def yamlFilename = yamlTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + def yamlDir = java.nio.file.Paths.get(yamlFilename).getParent() + + // the processed state is a list of [key, value] tuples, where + // - key is a String + // - value is any object that can be serialized to a Yaml (so a String/Integer/Long/Double/Boolean, a List, a Map, or a Path) + // - (key, value) are the tuples that will be saved to the state.yaml file + def processedState = + config.allArguments + .findAll { it.direction == "output" } + .collectMany { par -> + def plainName_ = par.plainName + // if the state does not contain the key, it's an + // optional argument for which the component did + // not generate any output + if (!state_.containsKey(plainName_)) { + return [] + } + def value = state_[plainName_] + // if the parameter is not a file, it should be stored + // in the state as-is, but is not something that needs + // to be copied from the source path to the dest path + if (par.type != "file") { + return [[key: plainName_, value: value]] + } + // if the orig state does not contain this filename, + // it's an optional argument for which the user specified + // that it should not be returned as a state + if (!origState_.containsKey(plainName_)) { + return [] + } + def filenameTemplate = origState_[plainName_] + // if the pararameter is multiple: true, fetch the template + if (par.multiple && filenameTemplate instanceof List) { + filenameTemplate = filenameTemplate[0] + } + // instantiate the template + def filename = filenameTemplate + .replaceAll('\\$id', id_) + .replaceAll('\\$\\{id\\}', id_) + .replaceAll('\\$key', key_) + .replaceAll('\\$\\{key\\}', key_) + if (par.multiple) { + // if the parameter is multiple: true, the filename + // should contain a wildcard '*' that is replaced with + // the index of the file + assert filename.contains("*") : "Module '${key_}' id '${id_}': Multiple output files specified, but no wildcard '*' in the filename: ${filename}" + def outputPerFile = value.withIndex().collect{ val, ix -> + def filename_ix = filename.replace("*", ix.toString()) + def value_ = java.nio.file.Paths.get(filename_ix) + // if id contains a slash + if (yamlDir != null) { + value_ = yamlDir.relativize(value_) + } + return value_ + } + return [["key": plainName_, "value": outputPerFile]] + } else { + def value_ = java.nio.file.Paths.get(filename) + // if id contains a slash + if (yamlDir != null) { + value_ = yamlDir.relativize(value_) + } + def inputPath = value instanceof File ? value.toPath() : value + return [["key": plainName_, value: value_]] + } + } + + + def updatedState_ = processedState.collectEntries{[it.key, it.value]} + + // convert state to yaml blob + def yamlBlob_ = toTaggedYamlBlob([id: id_] + updatedState_) + + [id_, yamlBlob_, yamlFilename] + } + | publishStatesProc + emit: input_ch + } + return publishStatesSimpleWf +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/states/setState.nf' +def setState(fun) { + assert fun instanceof Closure || fun instanceof Map || fun instanceof List : + "Error in setState: Expected process argument to be a Closure, a Map, or a List. Found: class ${fun.getClass()}" + + // if fun is a List, convert to map + if (fun instanceof List) { + // check whether fun is a list[string] + assert fun.every{it instanceof CharSequence} : "Error in setState: argument is a List, but not all elements are Strings" + fun = fun.collectEntries{[it, it]} + } + + // if fun is a map, convert to closure + if (fun instanceof Map) { + // check whether fun is a map[string, string] + assert fun.values().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all values are Strings" + assert fun.keySet().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all keys are Strings" + def funMap = fun.clone() + // turn the map into a closure to be used later on + fun = { id_, state_ -> + assert state_ instanceof Map : "Error in setState: the state is not a Map" + funMap.collectMany{newkey, origkey -> + if (state_.containsKey(origkey)) { + [[newkey, state_[origkey]]] + } else { + [] + } + }.collectEntries() + } + } + + map { tup -> + def id = tup[0] + def state = tup[1] + def unfilteredState = fun(id, state) + def newState = unfilteredState.findAll{key, val -> val != null} + [id, newState] + tup.drop(2) + } +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processAuto.nf' +// TODO: unit test processAuto +def processAuto(Map auto) { + // remove null values + auto = auto.findAll{k, v -> v != null} + + // check for unexpected keys + def expectedKeys = ["simplifyInput", "simplifyOutput", "transcript", "publish"] + def unexpectedKeys = auto.keySet() - expectedKeys + assert unexpectedKeys.isEmpty(), "unexpected keys in auto: '${unexpectedKeys.join("', '")}'" + + // check auto.simplifyInput + assert auto.simplifyInput instanceof Boolean, "auto.simplifyInput must be a boolean" + + // check auto.simplifyOutput + assert auto.simplifyOutput instanceof Boolean, "auto.simplifyOutput must be a boolean" + + // check auto.transcript + assert auto.transcript instanceof Boolean, "auto.transcript must be a boolean" + + // check auto.publish + assert auto.publish instanceof Boolean || auto.publish == "state", "auto.publish must be a boolean or 'state'" + + return auto.subMap(expectedKeys) +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processDirectives.nf' +def assertMapKeys(map, expectedKeys, requiredKeys, mapName) { + assert map instanceof Map : "Expected argument '$mapName' to be a Map. Found: class ${map.getClass()}" + map.forEach { key, val -> + assert key in expectedKeys : "Unexpected key '$key' in ${mapName ? mapName + " " : ""}map" + } + requiredKeys.forEach { requiredKey -> + assert map.containsKey(requiredKey) : "Missing required key '$key' in ${mapName ? mapName + " " : ""}map" + } +} + +// TODO: unit test processDirectives +def processDirectives(Map drctv) { + // remove null values + drctv = drctv.findAll{k, v -> v != null} + + // check for unexpected keys + def expectedKeys = [ + "accelerator", "afterScript", "beforeScript", "cache", "conda", "container", "containerOptions", "cpus", "disk", "echo", "errorStrategy", "executor", "machineType", "maxErrors", "maxForks", "maxRetries", "memory", "module", "penv", "pod", "publishDir", "queue", "label", "scratch", "storeDir", "stageInMode", "stageOutMode", "tag", "time" + ] + def unexpectedKeys = drctv.keySet() - expectedKeys + assert unexpectedKeys.isEmpty() : "Unexpected keys in process directive: '${unexpectedKeys.join("', '")}'" + + /* DIRECTIVE accelerator + accepted examples: + - [ limit: 4, type: "nvidia-tesla-k80" ] + */ + if (drctv.containsKey("accelerator")) { + assertMapKeys(drctv["accelerator"], ["type", "limit", "request", "runtime"], [], "accelerator") + } + + /* DIRECTIVE afterScript + accepted examples: + - "source /cluster/bin/cleanup" + */ + if (drctv.containsKey("afterScript")) { + assert drctv["afterScript"] instanceof CharSequence + } + + /* DIRECTIVE beforeScript + accepted examples: + - "source /cluster/bin/setup" + */ + if (drctv.containsKey("beforeScript")) { + assert drctv["beforeScript"] instanceof CharSequence + } + + /* DIRECTIVE cache + accepted examples: + - true + - false + - "deep" + - "lenient" + */ + if (drctv.containsKey("cache")) { + assert drctv["cache"] instanceof CharSequence || drctv["cache"] instanceof Boolean + if (drctv["cache"] instanceof CharSequence) { + assert drctv["cache"] in ["deep", "lenient"] : "Unexpected value for cache" + } + } + + /* DIRECTIVE conda + accepted examples: + - "bwa=0.7.15" + - "bwa=0.7.15 fastqc=0.11.5" + - ["bwa=0.7.15", "fastqc=0.11.5"] + */ + if (drctv.containsKey("conda")) { + if (drctv["conda"] instanceof List) { + drctv["conda"] = drctv["conda"].join(" ") + } + assert drctv["conda"] instanceof CharSequence + } + + /* DIRECTIVE container + accepted examples: + - "foo/bar:tag" + - [ registry: "reg", image: "im", tag: "ta" ] + is transformed to "reg/im:ta" + - [ image: "im" ] + is transformed to "im:latest" + */ + if (drctv.containsKey("container")) { + assert drctv["container"] instanceof Map || drctv["container"] instanceof CharSequence + if (drctv["container"] instanceof Map) { + def m = drctv["container"] + assertMapKeys(m, [ "registry", "image", "tag" ], ["image"], "container") + def part1 = + System.getenv('OVERRIDE_CONTAINER_REGISTRY') ? System.getenv('OVERRIDE_CONTAINER_REGISTRY') + "/" : + params.containsKey("override_container_registry") ? params["override_container_registry"] + "/" : // todo: remove? + m.registry ? m.registry + "/" : + "" + def part2 = m.image + def part3 = m.tag ? ":" + m.tag : ":latest" + drctv["container"] = part1 + part2 + part3 + } + } + + /* DIRECTIVE containerOptions + accepted examples: + - "--foo bar" + - ["--foo bar", "-f b"] + */ + if (drctv.containsKey("containerOptions")) { + if (drctv["containerOptions"] instanceof List) { + drctv["containerOptions"] = drctv["containerOptions"].join(" ") + } + assert drctv["containerOptions"] instanceof CharSequence + } + + /* DIRECTIVE cpus + accepted examples: + - 1 + - 10 + */ + if (drctv.containsKey("cpus")) { + assert drctv["cpus"] instanceof Integer + } + + /* DIRECTIVE disk + accepted examples: + - "1 GB" + - "2TB" + - "3.2KB" + - "10.B" + */ + if (drctv.containsKey("disk")) { + assert drctv["disk"] instanceof CharSequence + // assert drctv["disk"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B") + // ^ does not allow closures + } + + /* DIRECTIVE echo + accepted examples: + - true + - false + */ + if (drctv.containsKey("echo")) { + assert drctv["echo"] instanceof Boolean + } + + /* DIRECTIVE errorStrategy + accepted examples: + - "terminate" + - "finish" + */ + if (drctv.containsKey("errorStrategy")) { + assert drctv["errorStrategy"] instanceof CharSequence + assert drctv["errorStrategy"] in ["terminate", "finish", "ignore", "retry"] : "Unexpected value for errorStrategy" + } + + /* DIRECTIVE executor + accepted examples: + - "local" + - "sge" + */ + if (drctv.containsKey("executor")) { + assert drctv["executor"] instanceof CharSequence + assert drctv["executor"] in ["local", "sge", "uge", "lsf", "slurm", "pbs", "pbspro", "moab", "condor", "nqsii", "ignite", "k8s", "awsbatch", "google-pipelines"] : "Unexpected value for executor" + } + + /* DIRECTIVE machineType + accepted examples: + - "n1-highmem-8" + */ + if (drctv.containsKey("machineType")) { + assert drctv["machineType"] instanceof CharSequence + } + + /* DIRECTIVE maxErrors + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxErrors")) { + assert drctv["maxErrors"] instanceof Integer + } + + /* DIRECTIVE maxForks + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxForks")) { + assert drctv["maxForks"] instanceof Integer + } + + /* DIRECTIVE maxRetries + accepted examples: + - 1 + - 3 + */ + if (drctv.containsKey("maxRetries")) { + assert drctv["maxRetries"] instanceof Integer + } + + /* DIRECTIVE memory + accepted examples: + - "1 GB" + - "2TB" + - "3.2KB" + - "10.B" + */ + if (drctv.containsKey("memory")) { + assert drctv["memory"] instanceof CharSequence + // assert drctv["memory"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B") + // ^ does not allow closures + } + + /* DIRECTIVE module + accepted examples: + - "ncbi-blast/2.2.27" + - "ncbi-blast/2.2.27:t_coffee/10.0" + - ["ncbi-blast/2.2.27", "t_coffee/10.0"] + */ + if (drctv.containsKey("module")) { + if (drctv["module"] instanceof List) { + drctv["module"] = drctv["module"].join(":") + } + assert drctv["module"] instanceof CharSequence + } + + /* DIRECTIVE penv + accepted examples: + - "smp" + */ + if (drctv.containsKey("penv")) { + assert drctv["penv"] instanceof CharSequence + } + + /* DIRECTIVE pod + accepted examples: + - [ label: "key", value: "val" ] + - [ annotation: "key", value: "val" ] + - [ env: "key", value: "val" ] + - [ [label: "l", value: "v"], [env: "e", value: "v"]] + */ + if (drctv.containsKey("pod")) { + if (drctv["pod"] instanceof Map) { + drctv["pod"] = [ drctv["pod"] ] + } + assert drctv["pod"] instanceof List + drctv["pod"].forEach { pod -> + assert pod instanceof Map + // TODO: should more checks be added? + // See https://www.nextflow.io/docs/latest/process.html?highlight=directives#pod + // e.g. does it contain 'label' and 'value', or 'annotation' and 'value', or ...? + } + } + + /* DIRECTIVE publishDir + accepted examples: + - [] + - [ [ path: "foo", enabled: true ], [ path: "bar", enabled: false ] ] + - "/path/to/dir" + is transformed to [[ path: "/path/to/dir" ]] + - [ path: "/path/to/dir", mode: "cache" ] + is transformed to [[ path: "/path/to/dir", mode: "cache" ]] + */ + // TODO: should we also look at params["publishDir"]? + if (drctv.containsKey("publishDir")) { + def pblsh = drctv["publishDir"] + + // check different options + assert pblsh instanceof List || pblsh instanceof Map || pblsh instanceof CharSequence + + // turn into list if not already so + // for some reason, 'if (!pblsh instanceof List) pblsh = [ pblsh ]' doesn't work. + pblsh = pblsh instanceof List ? pblsh : [ pblsh ] + + // check elements of publishDir + pblsh = pblsh.collect{ elem -> + // turn into map if not already so + elem = elem instanceof CharSequence ? [ path: elem ] : elem + + // check types and keys + assert elem instanceof Map : "Expected publish argument '$elem' to be a String or a Map. Found: class ${elem.getClass()}" + assertMapKeys(elem, [ "path", "mode", "overwrite", "pattern", "saveAs", "enabled" ], ["path"], "publishDir") + + // check elements in map + assert elem.containsKey("path") + assert elem["path"] instanceof CharSequence + if (elem.containsKey("mode")) { + assert elem["mode"] instanceof CharSequence + assert elem["mode"] in [ "symlink", "rellink", "link", "copy", "copyNoFollow", "move" ] + } + if (elem.containsKey("overwrite")) { + assert elem["overwrite"] instanceof Boolean + } + if (elem.containsKey("pattern")) { + assert elem["pattern"] instanceof CharSequence + } + if (elem.containsKey("saveAs")) { + assert elem["saveAs"] instanceof CharSequence //: "saveAs as a Closure is currently not supported. Surround your closure with single quotes to get the desired effect. Example: '\{ foo \}'" + } + if (elem.containsKey("enabled")) { + assert elem["enabled"] instanceof Boolean + } + + // return final result + elem + } + // store final directive + drctv["publishDir"] = pblsh + } + + /* DIRECTIVE queue + accepted examples: + - "long" + - "short,long" + - ["short", "long"] + */ + if (drctv.containsKey("queue")) { + if (drctv["queue"] instanceof List) { + drctv["queue"] = drctv["queue"].join(",") + } + assert drctv["queue"] instanceof CharSequence + } + + /* DIRECTIVE label + accepted examples: + - "big_mem" + - "big_cpu" + - ["big_mem", "big_cpu"] + */ + if (drctv.containsKey("label")) { + if (drctv["label"] instanceof CharSequence) { + drctv["label"] = [ drctv["label"] ] + } + assert drctv["label"] instanceof List + drctv["label"].forEach { label -> + assert label instanceof CharSequence + // assert label.matches("[a-zA-Z0-9]([a-zA-Z0-9_]*[a-zA-Z0-9])?") + // ^ does not allow closures + } + } + + /* DIRECTIVE scratch + accepted examples: + - true + - "/path/to/scratch" + - '$MY_PATH_TO_SCRATCH' + - "ram-disk" + */ + if (drctv.containsKey("scratch")) { + assert drctv["scratch"] == true || drctv["scratch"] instanceof CharSequence + } + + /* DIRECTIVE storeDir + accepted examples: + - "/path/to/storeDir" + */ + if (drctv.containsKey("storeDir")) { + assert drctv["storeDir"] instanceof CharSequence + } + + /* DIRECTIVE stageInMode + accepted examples: + - "copy" + - "link" + */ + if (drctv.containsKey("stageInMode")) { + assert drctv["stageInMode"] instanceof CharSequence + assert drctv["stageInMode"] in ["copy", "link", "symlink", "rellink"] + } + + /* DIRECTIVE stageOutMode + accepted examples: + - "copy" + - "link" + */ + if (drctv.containsKey("stageOutMode")) { + assert drctv["stageOutMode"] instanceof CharSequence + assert drctv["stageOutMode"] in ["copy", "move", "rsync"] + } + + /* DIRECTIVE tag + accepted examples: + - "foo" + - '$id' + */ + if (drctv.containsKey("tag")) { + assert drctv["tag"] instanceof CharSequence + } + + /* DIRECTIVE time + accepted examples: + - "1h" + - "2days" + - "1day 6hours 3minutes 30seconds" + */ + if (drctv.containsKey("time")) { + assert drctv["time"] instanceof CharSequence + // todo: validation regex? + } + + return drctv +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processWorkflowArgs.nf' +def processWorkflowArgs(Map args, Map defaultWfArgs, Map meta) { + // override defaults with args + def workflowArgs = defaultWfArgs + args + + // check whether 'key' exists + assert workflowArgs.containsKey("key") : "Error in module '${meta.config.name}': key is a required argument" + + // if 'key' is a closure, apply it to the original key + if (workflowArgs["key"] instanceof Closure) { + workflowArgs["key"] = workflowArgs["key"](meta.config.name) + } + def key = workflowArgs["key"] + assert key instanceof CharSequence : "Expected process argument 'key' to be a String. Found: class ${key.getClass()}" + assert key ==~ /^[a-zA-Z_]\w*$/ : "Error in module '$key': Expected process argument 'key' to consist of only letters, digits or underscores. Found: ${key}" + + // check for any unexpected keys + def expectedKeys = ["key", "directives", "auto", "map", "mapId", "mapData", "mapPassthrough", "filter", "runIf", "fromState", "toState", "args", "renameKeys", "debug"] + def unexpectedKeys = workflowArgs.keySet() - expectedKeys + assert unexpectedKeys.isEmpty() : "Error in module '$key': unexpected arguments to the '.run()' function: '${unexpectedKeys.join("', '")}'" + + // check whether directives exists and apply defaults + assert workflowArgs.containsKey("directives") : "Error in module '$key': directives is a required argument" + assert workflowArgs["directives"] instanceof Map : "Error in module '$key': Expected process argument 'directives' to be a Map. Found: class ${workflowArgs['directives'].getClass()}" + workflowArgs["directives"] = processDirectives(defaultWfArgs.directives + workflowArgs["directives"]) + + // check whether directives exists and apply defaults + assert workflowArgs.containsKey("auto") : "Error in module '$key': auto is a required argument" + assert workflowArgs["auto"] instanceof Map : "Error in module '$key': Expected process argument 'auto' to be a Map. Found: class ${workflowArgs['auto'].getClass()}" + workflowArgs["auto"] = processAuto(defaultWfArgs.auto + workflowArgs["auto"]) + + // auto define publish, if so desired + if (workflowArgs.auto.publish == true && (workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : [:]).isEmpty()) { + // can't assert at this level thanks to the no_publish profile + // assert params.containsKey("publishDir") || params.containsKey("publish_dir") : + // "Error in module '${workflowArgs['key']}': if auto.publish is true, params.publish_dir needs to be defined.\n" + + // " Example: params.publish_dir = \"./output/\"" + def publishDir = getPublishDir() + + if (publishDir != null) { + workflowArgs.directives.publishDir = [[ + path: publishDir, + saveAs: "{ it.startsWith('.') ? null : it }", // don't publish hidden files, by default + mode: "copy" + ]] + } + } + + // auto define transcript, if so desired + if (workflowArgs.auto.transcript == true) { + // can't assert at this level thanks to the no_publish profile + // assert params.containsKey("transcriptsDir") || params.containsKey("transcripts_dir") || params.containsKey("publishDir") || params.containsKey("publish_dir") : + // "Error in module '${workflowArgs['key']}': if auto.transcript is true, either params.transcripts_dir or params.publish_dir needs to be defined.\n" + + // " Example: params.transcripts_dir = \"./transcripts/\"" + def transcriptsDir = + params.containsKey("transcripts_dir") ? params.transcripts_dir : + params.containsKey("transcriptsDir") ? params.transcriptsDir : + params.containsKey("publish_dir") ? params.publish_dir + "/_transcripts" : + params.containsKey("publishDir") ? params.publishDir + "/_transcripts" : + null + if (transcriptsDir != null) { + def timestamp = nextflow.Nextflow.getSession().getWorkflowMetadata().start.format('yyyy-MM-dd_HH-mm-ss') + def transcriptsPublishDir = [ + path: "$transcriptsDir/$timestamp/\${task.process.replaceAll(':', '-')}/\${id}/", + saveAs: "{ it.startsWith('.') ? it.replaceAll('^.', '') : null }", + mode: "copy" + ] + def publishDirs = workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : null ? workflowArgs.directives.publishDir : [] + workflowArgs.directives.publishDir = publishDirs + transcriptsPublishDir + } + } + + // if this is a stubrun, remove certain directives? + if (workflow.stubRun) { + workflowArgs.directives.keySet().removeAll(["publishDir", "cpus", "memory", "label"]) + } + + for (nam in ["map", "mapId", "mapData", "mapPassthrough", "filter", "runIf"]) { + if (workflowArgs.containsKey(nam) && workflowArgs[nam]) { + assert workflowArgs[nam] instanceof Closure : "Error in module '$key': Expected process argument '$nam' to be null or a Closure. Found: class ${workflowArgs[nam].getClass()}" + } + } + + // TODO: should functions like 'map', 'mapId', 'mapData', 'mapPassthrough' be deprecated as well? + for (nam in ["map", "mapData", "mapPassthrough", "renameKeys"]) { + if (workflowArgs.containsKey(nam) && workflowArgs[nam] != null) { + log.warn "module '$key': workflow argument '$nam' is deprecated and will be removed in Viash 0.9.0. Please use 'fromState' and 'toState' instead." + } + } + + // check fromState + workflowArgs["fromState"] = _processFromState(workflowArgs.get("fromState"), key, meta.config) + + // check toState + workflowArgs["toState"] = _processToState(workflowArgs.get("toState"), key, meta.config) + + // return output + return workflowArgs +} + +def _processFromState(fromState, key_, config_) { + assert fromState == null || fromState instanceof Closure || fromState instanceof Map || fromState instanceof List : + "Error in module '$key_': Expected process argument 'fromState' to be null, a Closure, a Map, or a List. Found: class ${fromState.getClass()}" + if (fromState == null) { + return null + } + + // if fromState is a List, convert to map + if (fromState instanceof List) { + // check whether fromstate is a list[string] + assert fromState.every{it instanceof CharSequence} : "Error in module '$key_': fromState is a List, but not all elements are Strings" + fromState = fromState.collectEntries{[it, it]} + } + + // if fromState is a map, convert to closure + if (fromState instanceof Map) { + // check whether fromstate is a map[string, string] + assert fromState.values().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all values are Strings" + assert fromState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all keys are Strings" + def fromStateMap = fromState.clone() + def requiredInputNames = meta.config.allArguments.findAll{it.required && it.direction == "Input"}.collect{it.plainName} + // turn the map into a closure to be used later on + fromState = { it -> + def state = it[1] + assert state instanceof Map : "Error in module '$key_': the state is not a Map" + def data = fromStateMap.collectMany{newkey, origkey -> + // check whether newkey corresponds to a required argument + if (state.containsKey(origkey)) { + [[newkey, state[origkey]]] + } else if (!requiredInputNames.contains(origkey)) { + [] + } else { + throw new Exception("Error in module '$key_': fromState key '$origkey' not found in current state") + } + }.collectEntries() + data + } + } + + return fromState +} + +def _processToState(toState, key_, config_) { + if (toState == null) { + toState = { tup -> tup[1] } + } + + // toState should be a closure, map[string, string], or list[string] + assert toState instanceof Closure || toState instanceof Map || toState instanceof List : + "Error in module '$key_': Expected process argument 'toState' to be a Closure, a Map, or a List. Found: class ${toState.getClass()}" + + // if toState is a List, convert to map + if (toState instanceof List) { + // check whether toState is a list[string] + assert toState.every{it instanceof CharSequence} : "Error in module '$key_': toState is a List, but not all elements are Strings" + toState = toState.collectEntries{[it, it]} + } + + // if toState is a map, convert to closure + if (toState instanceof Map) { + // check whether toState is a map[string, string] + assert toState.values().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all values are Strings" + assert toState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all keys are Strings" + def toStateMap = toState.clone() + def requiredOutputNames = config_.allArguments.findAll{it.required && it.direction == "Output"}.collect{it.plainName} + // turn the map into a closure to be used later on + toState = { it -> + def output = it[1] + def state = it[2] + assert output instanceof Map : "Error in module '$key_': the output is not a Map" + assert state instanceof Map : "Error in module '$key_': the state is not a Map" + def extraEntries = toStateMap.collectMany{newkey, origkey -> + // check whether newkey corresponds to a required argument + if (output.containsKey(origkey)) { + [[newkey, output[origkey]]] + } else if (!requiredOutputNames.contains(origkey)) { + [] + } else { + throw new Exception("Error in module '$key_': toState key '$origkey' not found in current output") + } + }.collectEntries() + state + extraEntries + } + } + + return toState +} + +// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/workflowFactory.nf' +def _debug(workflowArgs, debugKey) { + if (workflowArgs.debug) { + view { "process '${workflowArgs.key}' $debugKey tuple: $it" } + } else { + map { it } + } +} + +// depends on: innerWorkflowFactory +def workflowFactory(Map args, Map defaultWfArgs, Map meta) { + def workflowArgs = processWorkflowArgs(args, defaultWfArgs, meta) + def key_ = workflowArgs["key"] + def multipleArgs = meta.config.allArguments.findAll{ it.multiple }.collect{it.plainName} + + workflow workflowInstance { + take: input_ + + main: + def chModified = input_ + | checkUniqueIds([:]) + | _debug(workflowArgs, "input") + | map { tuple -> + tuple = deepClone(tuple) + + if (workflowArgs.map) { + tuple = workflowArgs.map(tuple) + } + if (workflowArgs.mapId) { + tuple[0] = workflowArgs.mapId(tuple[0]) + } + if (workflowArgs.mapData) { + tuple[1] = workflowArgs.mapData(tuple[1]) + } + if (workflowArgs.mapPassthrough) { + tuple = tuple.take(2) + workflowArgs.mapPassthrough(tuple.drop(2)) + } + + // check tuple + assert tuple instanceof List : + "Error in module '${key_}': element in channel should be a tuple [id, data, ...otherargs...]\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: List. Found: tuple.getClass() is ${tuple.getClass()}" + assert tuple.size() >= 2 : + "Error in module '${key_}': expected length of tuple in input channel to be two or greater.\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Found: tuple.size() == ${tuple.size()}" + + // check id field + if (tuple[0] instanceof GString) { + tuple[0] = tuple[0].toString() + } + assert tuple[0] instanceof CharSequence : + "Error in module '${key_}': first element of tuple in channel should be a String\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Found: ${tuple[0]}" + + // match file to input file + if (workflowArgs.auto.simplifyInput && (tuple[1] instanceof Path || tuple[1] instanceof List)) { + def inputFiles = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + + assert inputFiles.size() == 1 : + "Error in module '${key_}' id '${tuple[0]}'.\n" + + " Anonymous file inputs are only allowed when the process has exactly one file input.\n" + + " Expected: inputFiles.size() == 1. Found: inputFiles.size() is ${inputFiles.size()}" + + tuple[1] = [[ inputFiles[0].plainName, tuple[1] ]].collectEntries() + } + + // check data field + assert tuple[1] instanceof Map : + "Error in module '${key_}' id '${tuple[0]}': second element of tuple in channel should be a Map\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}" + + // rename keys of data field in tuple + if (workflowArgs.renameKeys) { + assert workflowArgs.renameKeys instanceof Map : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class: Map. Found: renameKeys.getClass() is ${workflowArgs.renameKeys.getClass()}" + assert tuple[1] instanceof Map : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}" + + // TODO: allow renameKeys to be a function? + workflowArgs.renameKeys.each { newKey, oldKey -> + assert newKey instanceof CharSequence : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class of newKey: String. Found: newKey.getClass() is ${newKey.getClass()}" + assert oldKey instanceof CharSequence : + "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" + + " Example: renameKeys: ['new_key': 'old_key'].\n" + + " Expected class of oldKey: String. Found: oldKey.getClass() is ${oldKey.getClass()}" + assert tuple[1].containsKey(oldKey) : + "Error renaming data keys in module '${key}' id '${tuple[0]}'.\n" + + " Key '$oldKey' is missing in the data map. tuple[1].keySet() is '${tuple[1].keySet()}'" + tuple[1].put(newKey, tuple[1][oldKey]) + } + tuple[1].keySet().removeAll(workflowArgs.renameKeys.collect{ newKey, oldKey -> oldKey }) + } + tuple + } + + + def chRun = null + def chPassthrough = null + if (workflowArgs.runIf) { + def runIfBranch = chModified.branch{ tup -> + run: workflowArgs.runIf(tup[0], tup[1]) + passthrough: true + } + chRun = runIfBranch.run + chPassthrough = runIfBranch.passthrough + } else { + chRun = chModified + chPassthrough = Channel.empty() + } + + def chRunFiltered = workflowArgs.filter ? + chRun | filter{workflowArgs.filter(it)} : + chRun + + def chArgs = workflowArgs.fromState ? + chRunFiltered | map{ + def new_data = workflowArgs.fromState(it.take(2)) + [it[0], new_data] + } : + chRunFiltered | map {tup -> tup.take(2)} + + // fill in defaults + def chArgsWithDefaults = chArgs + | map { tuple -> + def id_ = tuple[0] + def data_ = tuple[1] + + // TODO: could move fromState to here + + // fetch default params from functionality + def defaultArgs = meta.config.allArguments + .findAll { it.containsKey("default") } + .collectEntries { [ it.plainName, it.default ] } + + // fetch overrides in params + def paramArgs = meta.config.allArguments + .findAll { par -> + def argKey = key_ + "__" + par.plainName + params.containsKey(argKey) + } + .collectEntries { [ it.plainName, params[key_ + "__" + it.plainName] ] } + + // fetch overrides in data + def dataArgs = meta.config.allArguments + .findAll { data_.containsKey(it.plainName) } + .collectEntries { [ it.plainName, data_[it.plainName] ] } + + // combine params + def combinedArgs = defaultArgs + paramArgs + workflowArgs.args + dataArgs + + // remove arguments with explicit null values + combinedArgs + .removeAll{_, val -> val == null || val == "viash_no_value" || val == "force_null"} + + combinedArgs = _processInputValues(combinedArgs, meta.config, id_, key_) + + [id_, combinedArgs] + tuple.drop(2) + } + + // TODO: move some of the _meta.join_id wrangling to the safeJoin() function. + def chInitialOutputMulti = chArgsWithDefaults + | _debug(workflowArgs, "processed") + // run workflow + | innerWorkflowFactory(workflowArgs) + def chInitialOutputList = chInitialOutputMulti instanceof List ? chInitialOutputMulti : [chInitialOutputMulti] + assert chInitialOutputList.size() > 0: "should have emitted at least one output channel" + // Add a channel ID to the events, which designates the channel the event was emitted from as a running number + // This number is used to sort the events later when the events are gathered from across the channels. + def chInitialOutputListWithIndexedEvents = chInitialOutputList.withIndex().collect{channel, channelIndex -> + def newChannel = channel + | map {tuple -> + assert tuple instanceof List : + "Error in module '${key_}': element in output channel should be a tuple [id, data, ...otherargs...]\n" + + " Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" + + " Expected class: List. Found: tuple.getClass() is ${tuple.getClass()}" + + def newEvent = [channelIndex] + tuple + return newEvent + } + return newChannel + } + // Put the events into 1 channel, cover case where there is only one channel is emitted + def chInitialOutput = chInitialOutputList.size() > 1 ? \ + chInitialOutputListWithIndexedEvents[0].mix(*chInitialOutputListWithIndexedEvents.tail()) : \ + chInitialOutputListWithIndexedEvents[0] + def chInitialOutputProcessed = chInitialOutput + | map { tuple -> + def channelId = tuple[0] + def id_ = tuple[1] + def output_ = tuple[2] + + // see if output map contains metadata + def meta_ = + output_ instanceof Map && output_.containsKey("_meta") ? + output_["_meta"] : + [:] + def join_id = meta_.join_id ?: id_ + + // remove metadata + output_ = output_.findAll{k, v -> k != "_meta"} + + // check value types + output_ = _checkValidOutputArgument(output_, meta.config, id_, key_) + + [join_id, channelId, id_, output_] + } + // | view{"chInitialOutput: ${it.take(3)}"} + + // join the output [prev_id, channel_id, new_id, output] with the previous state [prev_id, state, ...] + def chPublishWithPreviousState = safeJoin(chInitialOutputProcessed, chRunFiltered, key_) + // input tuple format: [join_id, channel_id, id, output, prev_state, ...] + // output tuple format: [join_id, channel_id, id, new_state, ...] + | map{ tup -> + def new_state = workflowArgs.toState(tup.drop(2).take(3)) + tup.take(3) + [new_state] + tup.drop(5) + } + if (workflowArgs.auto.publish == "state") { + def chPublishFiles = chPublishWithPreviousState + // input tuple format: [join_id, channel_id, id, new_state, ...] + // output tuple format: [join_id, channel_id, id, new_state] + | map{ tup -> + tup.take(4) + } + + safeJoin(chPublishFiles, chArgsWithDefaults, key_) + // input tuple format: [join_id, channel_id, id, new_state, orig_state, ...] + // output tuple format: [id, new_state, orig_state] + | map { tup -> + tup.drop(2).take(3) + } + | publishFilesByConfig(key: key_, config: meta.config) + } + // Join the state from the events that were emitted from different channels + def chJoined = chInitialOutputProcessed + | map {tuple -> + def join_id = tuple[0] + def channel_id = tuple[1] + def id = tuple[2] + def other = tuple.drop(3) + // Below, groupTuple is used to join the events. To make sure resuming a workflow + // keeps working, the output state must be deterministic. This means the state needs to be + // sorted with groupTuple's has a 'sort' argument. This argument can be set to 'hash', + // but hashing the state when it is large can be problematic in terms of performance. + // Therefore, a custom comparator function is provided. We add the channel ID to the + // states so that we can use the channel ID to sort the items. + def stateWithChannelID = [[channel_id] * other.size(), other].transpose() + // A comparator that is provided to groupTuple's 'sort' argument is applied + // to all elements of the event tuple (that is not the 'id'). The comparator + // closure that is used below expects the input to be List. So the join_id and + // channel_id must also be wrapped in a list. + [[join_id], [channel_id], id] + stateWithChannelID + } + | groupTuple(by: 2, sort: {a, b -> a[0] <=> b[0]}, size: chInitialOutputList.size(), remainder: true) + | map {join_ids, _, id, statesWithChannelID -> + // Remove the channel IDs from the states + def states = statesWithChannelID.collect{it[1]} + def newJoinId = join_ids.flatten().unique{a, b -> a <=> b} + assert newJoinId.size() == 1: "Multiple events were emitted for '$id'." + def newJoinIdUnique = newJoinId[0] + + // Merge the states from the different channels + def newState = states.inject([:]){ old_state, state_to_add -> + return old_state + state_to_add.collectEntries{k, v -> + if (!multipleArgs.contains(k)) { + // if the key is not a multiple argument, we expect only one value + if (old_state.containsKey(k)) { + assert old_state[k] == v : "ID $id: multiple entries for argument $k were emitted." + } + [k, v] + } else { + // if the key is a multiple argument, append the different values into one list + def prevValue = old_state.getOrDefault(k, []) + def prevValueAsList = prevValue instanceof List ? prevValue : [prevValue] + [k, prevValueAsList + v] + } + } + } + + _checkAllRequiredOuputsPresent(newState, meta.config, id, key_) + + // simplify output if need be + if (workflowArgs.auto.simplifyOutput && newState.size() == 1) { + newState = newState.values()[0] + } + + return [newJoinIdUnique, id, newState] + } + + // join the output [prev_id, new_id, output] with the previous state [prev_id, state, ...] + def chNewState = safeJoin(chJoined, chRunFiltered, key_) + // input tuple format: [join_id, id, output, prev_state, ...] + // output tuple format: [join_id, id, new_state, ...] + | map{ tup -> + def new_state = workflowArgs.toState(tup.drop(1).take(3)) + tup.take(2) + [new_state] + tup.drop(4) + } + + if (workflowArgs.auto.publish == "state") { + def chPublishStates = chNewState + // input tuple format: [join_id, id, new_state, ...] + // output tuple format: [join_id, id, new_state] + | map{ tup -> + tup.take(3) + } + + safeJoin(chPublishStates, chArgsWithDefaults, key_) + // input tuple format: [join_id, id, new_state, orig_state, ...] + // output tuple format: [id, new_state, orig_state] + | map { tup -> + tup.drop(1).take(3) + } + | publishStatesByConfig(key: key_, config: meta.config) + } + chReturn = chNewState + | map { tup -> + // input tuple format: [join_id, id, new_state, ...] + // output tuple format: [id, new_state, ...] + tup.drop(1) + } + | _debug(workflowArgs, "output") + | concat(chPassthrough) + + emit: chReturn + } + + def wf = workflowInstance.cloneWithName(key_) + + // add factory function + wf.metaClass.run = { runArgs -> + workflowFactory(runArgs, workflowArgs, meta) + } + // add config to module for later introspection + wf.metaClass.config = meta.config + + return wf +} + +nextflow.enable.dsl=2 + +// START COMPONENT-SPECIFIC CODE + +// create meta object +meta = [ + "resources_dir": moduleDir.toRealPath().normalize(), + "config": processConfig(readJsonBlob('''{ + "name" : "yq", + "version" : "gunzip", + "argument_groups" : [ + { + "name" : "Inputs", + "arguments" : [ + { + "type" : "file", + "name" : "--input", + "description" : "files to be processed", + "example" : [ + "input.yaml" + ], + "must_exist" : true, + "create_parent" : true, + "required" : true, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + } + ] + }, + { + "name" : "Outputs", + "arguments" : [ + { + "type" : "file", + "name" : "--output", + "description" : "output file", + "example" : [ + "output.yaml" + ], + "must_exist" : true, + "create_parent" : true, + "required" : true, + "direction" : "output", + "multiple" : false, + "multiple_sep" : ";" + } + ] + }, + { + "name" : "Arguments", + "arguments" : [ + { + "type" : "string", + "name" : "--eval", + "description" : "expression to evaluate", + "example" : [ + ".name = \\"foo\\"" + ], + "required" : true, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "integer", + "name" : "--indent", + "alternatives" : [ + "-I" + ], + "description" : "sets indent level for output (default 2)", + "required" : false, + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "string", + "name" : "--input_format", + "alternatives" : [ + "-p" + ], + "description" : "parse format for input. (default \\"auto\\")", + "required" : false, + "choices" : [ + "auto", + "a", + "yaml", + "y", + "json", + "j", + "props", + "p", + "csv", + "c", + "tsv", + "t", + "xml", + "x", + "base64", + "uri", + "toml", + "shell", + "s", + "lua", + "l" + ], + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "string", + "name" : "--output_format", + "alternatives" : [ + "-o" + ], + "description" : "output format type. (default \\"auto\\")", + "required" : false, + "choices" : [ + "auto", + "a", + "yaml", + "y", + "json", + "j", + "props", + "p", + "csv", + "c", + "tsv", + "t", + "xml", + "x", + "base64", + "uri", + "toml", + "shell", + "s", + "lua", + "l" + ], + "direction" : "input", + "multiple" : false, + "multiple_sep" : ";" + }, + { + "type" : "boolean_true", + "name" : "--pretty_print", + "alternatives" : [ + "-P" + ], + "description" : "pretty print, shorthand for '... style = \\"\\"'", + "direction" : "input" + } + ] + } + ], + "resources" : [ + { + "type" : "bash_script", + "text" : "#!/bin/sh\n[[ \\"$par_pretty_print\\" == \\"false\\" ]] && unset par_pretty_print\nyq eval \\\\\n ${par_indent:+-I \\"${par_indent}\\"} \\\\\n ${par_input_format:+-p \\"${par_input_format}\\"} \\\\\n ${par_output_format:+-o \\"${par_output_format}\\"} \\\\\n ${par_pretty_print:+-P} \\\\\n --expression \\"$par_eval\\" \\\\\n --no-colors \\\\\n \\"$par_input\\" > \\"$par_output\\"\n", + "dest" : "./script.sh", + "is_executable" : true + } + ], + "description" : "A portable YAML, JSON, XML, CSV, TOML and properties processor", + "test_resources" : [ + { + "type" : "bash_script", + "text" : "set -e\necho \\"name: 'bar'\\" > test.yaml\n\\"$meta_executable\\" --input test.yaml --output output.yaml --eval '.name = \\"foo\\"'\n\\"$meta_executable\\" --input output.yaml --output output2.yaml --eval '.name'\ngrep \\"^foo$\\" output2.yaml\n", + "dest" : "./script.sh", + "is_executable" : true + } + ], + "status" : "enabled", + "scope" : { + "image" : "public", + "target" : "public" + }, + "requirements" : { + "commands" : [ + "ps" + ] + }, + "keywords" : [ + "yaml", + "json", + "xml", + "csv", + "toml", + "properties" + ], + "license" : "MIT", + "links" : { + "repository" : "https://github.com/mikefarah/yq", + "homepage" : "https://mikefarah.gitbook.io/yq", + "documentation" : "https://mikefarah.gitbook.io/yq/" + }, + "runners" : [ + { + "type" : "executable", + "id" : "executable", + "docker_setup_strategy" : "ifneedbepullelsecachedbuild" + }, + { + "type" : "nextflow", + "id" : "nextflow", + "directives" : { + "tag" : "$id" + }, + "auto" : { + "simplifyInput" : true, + "simplifyOutput" : false, + "transcript" : false, + "publish" : false + }, + "config" : { + "labels" : { + "mem1gb" : "memory = 1000000000.B", + "mem2gb" : "memory = 2000000000.B", + "mem5gb" : "memory = 5000000000.B", + "mem10gb" : "memory = 10000000000.B", + "mem20gb" : "memory = 20000000000.B", + "mem50gb" : "memory = 50000000000.B", + "mem100gb" : "memory = 100000000000.B", + "mem200gb" : "memory = 200000000000.B", + "mem500gb" : "memory = 500000000000.B", + "mem1tb" : "memory = 1000000000000.B", + "mem2tb" : "memory = 2000000000000.B", + "mem5tb" : "memory = 5000000000000.B", + "mem10tb" : "memory = 10000000000000.B", + "mem20tb" : "memory = 20000000000000.B", + "mem50tb" : "memory = 50000000000000.B", + "mem100tb" : "memory = 100000000000000.B", + "mem200tb" : "memory = 200000000000000.B", + "mem500tb" : "memory = 500000000000000.B", + "mem1gib" : "memory = 1073741824.B", + "mem2gib" : "memory = 2147483648.B", + "mem4gib" : "memory = 4294967296.B", + "mem8gib" : "memory = 8589934592.B", + "mem16gib" : "memory = 17179869184.B", + "mem32gib" : "memory = 34359738368.B", + "mem64gib" : "memory = 68719476736.B", + "mem128gib" : "memory = 137438953472.B", + "mem256gib" : "memory = 274877906944.B", + "mem512gib" : "memory = 549755813888.B", + "mem1tib" : "memory = 1099511627776.B", + "mem2tib" : "memory = 2199023255552.B", + "mem4tib" : "memory = 4398046511104.B", + "mem8tib" : "memory = 8796093022208.B", + "mem16tib" : "memory = 17592186044416.B", + "mem32tib" : "memory = 35184372088832.B", + "mem64tib" : "memory = 70368744177664.B", + "mem128tib" : "memory = 140737488355328.B", + "mem256tib" : "memory = 281474976710656.B", + "mem512tib" : "memory = 562949953421312.B", + "cpu1" : "cpus = 1", + "cpu2" : "cpus = 2", + "cpu5" : "cpus = 5", + "cpu10" : "cpus = 10", + "cpu20" : "cpus = 20", + "cpu50" : "cpus = 50", + "cpu100" : "cpus = 100", + "cpu200" : "cpus = 200", + "cpu500" : "cpus = 500", + "cpu1000" : "cpus = 1000" + } + }, + "debug" : false, + "container" : "docker" + } + ], + "engines" : [ + { + "type" : "docker", + "id" : "docker", + "image" : "alpine:latest", + "target_registry" : "images.viash-hub.com", + "target_tag" : "gunzip", + "namespace_separator" : "/", + "setup" : [ + { + "type" : "apk", + "packages" : [ + "bash", + "yq-go" + ] + }, + { + "type" : "docker", + "run" : [ + "/usr/bin/yq --version | sed 's/.*version\\\\sv\\\\(.*\\\\)/yq: \\"\\\\1\\"/' > /var/software_versions.txt\n" + ] + } + ] + }, + { + "type" : "native", + "id" : "native" + } + ], + "build_info" : { + "config" : "/workdir/root/repo/src/yq/config.vsh.yaml", + "runner" : "nextflow", + "engine" : "docker|native", + "output" : "target/nextflow/yq", + "viash_version" : "0.9.4", + "git_commit" : "add30ba0f36bd8a8b07f0ba640707016d04e11b2", + "git_remote" : "https://github.com/viash-hub/toolbox" + }, + "package_config" : { + "name" : "toolbox", + "version" : "gunzip", + "summary" : "A collection of curated command-line tools for general IT tasks, built with Viash.\n", + "description" : "`toolbox` provides a versatile suite of IT components, following the robust Viash (https://viash.io) framework.\nThis package focuses on delivering reliable, standalone tools that can be easily integrated into larger computational workflows.\n\nThe core philosophy emphasizes **reusability**, **reproducibility**, and adherence to **best practices** in component creation. Key features of `toolbox` components include:\n\n* **Standalone & Nextflow Ready:** Execute components directly from the command line or seamlessly incorporate them into Nextflow workflows.\n* **High Quality Standards:**\n * Comprehensive documentation for each component and its parameters.\n * Full exposure of the underlying tool's arguments for maximum flexibility.\n * Containerized (Docker) to ensure consistent environments and manage dependencies, leading to enhanced reproducibility.\n * Unit tested to verify functionality and ensure reliability.\n", + "viash_version" : "0.9.4", + "source" : "src", + "target" : "target", + "config_mods" : [ + ".requirements.commands := ['ps']\n", + ".engines += { type: \\"native\\" }", + ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'", + ".engines[.type == 'docker'].target_tag := 'gunzip'" + ], + "keywords" : [ + "toolbox", + "command-line", + "tools" + ], + "license" : "MIT", + "organization" : "vsh", + "links" : { + "repository" : "https://github.com/viash-hub/toolbox", + "issue_tracker" : "https://github.com/viash-hub/toolbox/issues" + } + } +}''')) +] + +// resolve dependencies dependencies (if any) + + +// inner workflow +// inner workflow hook +def innerWorkflowFactory(args) { + def rawScript = '''set -e +tempscript=".viash_script.sh" +cat > "$tempscript" << VIASHMAIN +## VIASH START +# The following code has been auto-generated by Viash. +$( if [ ! -z ${VIASH_PAR_INPUT+x} ]; then echo "${VIASH_PAR_INPUT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_input='&'#" ; else echo "# par_input="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT+x} ]; then echo "${VIASH_PAR_OUTPUT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_output='&'#" ; else echo "# par_output="; fi ) +$( if [ ! -z ${VIASH_PAR_EVAL+x} ]; then echo "${VIASH_PAR_EVAL}" | sed "s#'#'\\"'\\"'#g;s#.*#par_eval='&'#" ; else echo "# par_eval="; fi ) +$( if [ ! -z ${VIASH_PAR_INDENT+x} ]; then echo "${VIASH_PAR_INDENT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_indent='&'#" ; else echo "# par_indent="; fi ) +$( if [ ! -z ${VIASH_PAR_INPUT_FORMAT+x} ]; then echo "${VIASH_PAR_INPUT_FORMAT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_input_format='&'#" ; else echo "# par_input_format="; fi ) +$( if [ ! -z ${VIASH_PAR_OUTPUT_FORMAT+x} ]; then echo "${VIASH_PAR_OUTPUT_FORMAT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_output_format='&'#" ; else echo "# par_output_format="; fi ) +$( if [ ! -z ${VIASH_PAR_PRETTY_PRINT+x} ]; then echo "${VIASH_PAR_PRETTY_PRINT}" | sed "s#'#'\\"'\\"'#g;s#.*#par_pretty_print='&'#" ; else echo "# par_pretty_print="; fi ) +$( if [ ! -z ${VIASH_META_NAME+x} ]; then echo "${VIASH_META_NAME}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_name='&'#" ; else echo "# meta_name="; fi ) +$( if [ ! -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then echo "${VIASH_META_FUNCTIONALITY_NAME}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_functionality_name='&'#" ; else echo "# meta_functionality_name="; fi ) +$( if [ ! -z ${VIASH_META_RESOURCES_DIR+x} ]; then echo "${VIASH_META_RESOURCES_DIR}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_resources_dir='&'#" ; else echo "# meta_resources_dir="; fi ) +$( if [ ! -z ${VIASH_META_EXECUTABLE+x} ]; then echo "${VIASH_META_EXECUTABLE}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_executable='&'#" ; else echo "# meta_executable="; fi ) +$( if [ ! -z ${VIASH_META_CONFIG+x} ]; then echo "${VIASH_META_CONFIG}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_config='&'#" ; else echo "# meta_config="; fi ) +$( if [ ! -z ${VIASH_META_TEMP_DIR+x} ]; then echo "${VIASH_META_TEMP_DIR}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_temp_dir='&'#" ; else echo "# meta_temp_dir="; fi ) +$( if [ ! -z ${VIASH_META_CPUS+x} ]; then echo "${VIASH_META_CPUS}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_cpus='&'#" ; else echo "# meta_cpus="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_B+x} ]; then echo "${VIASH_META_MEMORY_B}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_b='&'#" ; else echo "# meta_memory_b="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KB+x} ]; then echo "${VIASH_META_MEMORY_KB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_kb='&'#" ; else echo "# meta_memory_kb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MB+x} ]; then echo "${VIASH_META_MEMORY_MB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_mb='&'#" ; else echo "# meta_memory_mb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GB+x} ]; then echo "${VIASH_META_MEMORY_GB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_gb='&'#" ; else echo "# meta_memory_gb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TB+x} ]; then echo "${VIASH_META_MEMORY_TB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_tb='&'#" ; else echo "# meta_memory_tb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PB+x} ]; then echo "${VIASH_META_MEMORY_PB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_pb='&'#" ; else echo "# meta_memory_pb="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_KIB+x} ]; then echo "${VIASH_META_MEMORY_KIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_kib='&'#" ; else echo "# meta_memory_kib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_MIB+x} ]; then echo "${VIASH_META_MEMORY_MIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_mib='&'#" ; else echo "# meta_memory_mib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_GIB+x} ]; then echo "${VIASH_META_MEMORY_GIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_gib='&'#" ; else echo "# meta_memory_gib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_TIB+x} ]; then echo "${VIASH_META_MEMORY_TIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_tib='&'#" ; else echo "# meta_memory_tib="; fi ) +$( if [ ! -z ${VIASH_META_MEMORY_PIB+x} ]; then echo "${VIASH_META_MEMORY_PIB}" | sed "s#'#'\\"'\\"'#g;s#.*#meta_memory_pib='&'#" ; else echo "# meta_memory_pib="; fi ) + +## VIASH END +#!/bin/sh +[[ "\\$par_pretty_print" == "false" ]] && unset par_pretty_print +yq eval \\\\ + \\${par_indent:+-I "\\${par_indent}"} \\\\ + \\${par_input_format:+-p "\\${par_input_format}"} \\\\ + \\${par_output_format:+-o "\\${par_output_format}"} \\\\ + \\${par_pretty_print:+-P} \\\\ + --expression "\\$par_eval" \\\\ + --no-colors \\\\ + "\\$par_input" > "\\$par_output" +VIASHMAIN +bash "$tempscript" +''' + + return vdsl3WorkflowFactory(args, meta, rawScript) +} + + + +/** + * Generate a workflow for VDSL3 modules. + * + * This function is called by the workflowFactory() function. + * + * Input channel: [id, input_map] + * Output channel: [id, output_map] + * + * Internally, this workflow will convert the input channel + * to a format which the Nextflow module will be able to handle. + */ +def vdsl3WorkflowFactory(Map args, Map meta, String rawScript) { + def key = args["key"] + def processObj = null + + workflow processWf { + take: input_ + main: + + if (processObj == null) { + processObj = _vdsl3ProcessFactory(args, meta, rawScript) + } + + output_ = input_ + | map { tuple -> + def id = tuple[0] + def data_ = tuple[1] + + if (workflow.stubRun) { + // add id if missing + data_ = [id: 'stub'] + data_ + } + + // process input files separately + def inputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + .collect { par -> + def val = data_.containsKey(par.plainName) ? data_[par.plainName] : [] + def inputFiles = [] + if (val == null) { + inputFiles = [] + } else if (val instanceof List) { + inputFiles = val + } else if (val instanceof Path) { + inputFiles = [ val ] + } else { + inputFiles = [] + } + if (!workflow.stubRun) { + // throw error when an input file doesn't exist + inputFiles.each{ file -> + assert file.exists() : + "Error in module '${key}' id '${id}' argument '${par.plainName}'.\n" + + " Required input file does not exist.\n" + + " Path: '$file'.\n" + + " Expected input file to exist" + } + } + inputFiles + } + + // remove input files + def argsExclInputFiles = meta.config.allArguments + .findAll { (it.type != "file" || it.direction != "input") && data_.containsKey(it.plainName) } + .collectEntries { par -> + def parName = par.plainName + def val = data_[parName] + if (par.multiple && val instanceof Collection) { + val = val.join(par.multiple_sep) + } + if (par.direction == "output" && par.type == "file") { + val = val + .replaceAll('\\$id', id) + .replaceAll('\\$\\{id\\}', id) + .replaceAll('\\$key', key) + .replaceAll('\\$\\{key\\}', key) + } + [parName, val] + } + + [ id ] + inputPaths + [ argsExclInputFiles, meta.resources_dir ] + } + | processObj + | map { output -> + def outputFiles = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .indexed() + .collectEntries{ index, par -> + def out = output[index + 1] + // strip dummy '.exitcode' file from output (see nextflow-io/nextflow#2678) + if (!out instanceof List || out.size() <= 1) { + if (par.multiple) { + out = [] + } else { + assert !par.required : + "Error in module '${key}' id '${output[0]}' argument '${par.plainName}'.\n" + + " Required output file is missing" + out = null + } + } else if (out.size() == 2 && !par.multiple) { + out = out[1] + } else { + out = out.drop(1) + } + [ par.plainName, out ] + } + + // drop null outputs + outputFiles.removeAll{it.value == null} + + [ output[0], outputFiles ] + } + emit: output_ + } + + return processWf +} + +// depends on: session? +def _vdsl3ProcessFactory(Map workflowArgs, Map meta, String rawScript) { + // autodetect process key + def wfKey = workflowArgs["key"] + def procKeyPrefix = "${wfKey}_process" + def scriptMeta = nextflow.script.ScriptMeta.current() + def existing = scriptMeta.getProcessNames().findAll{it.startsWith(procKeyPrefix)} + def numbers = existing.collect{it.replace(procKeyPrefix, "0").toInteger()} + def newNumber = (numbers + [-1]).max() + 1 + + def procKey = newNumber == 0 ? procKeyPrefix : "$procKeyPrefix$newNumber" + + if (newNumber > 0) { + log.warn "Key for module '${wfKey}' is duplicated.\n", + "If you run a component multiple times in the same workflow,\n" + + "it's recommended you set a unique key for every call,\n" + + "for example: ${wfKey}.run(key: \"foo\")." + } + + // subset directives and convert to list of tuples + def drctv = workflowArgs.directives + + // TODO: unit test the two commands below + // convert publish array into tags + def valueToStr = { val -> + // ignore closures + if (val instanceof CharSequence) { + if (!val.matches('^[{].*[}]$')) { + '"' + val + '"' + } else { + val + } + } else if (val instanceof List) { + "[" + val.collect{valueToStr(it)}.join(", ") + "]" + } else if (val instanceof Map) { + "[" + val.collect{k, v -> k + ": " + valueToStr(v)}.join(", ") + "]" + } else { + val.inspect() + } + } + + // multiple entries allowed: label, publishdir + def drctvStrs = drctv.collect { key, value -> + if (key in ["label", "publishDir"]) { + value.collect{ val -> + if (val instanceof Map) { + "\n$key " + val.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ") + } else if (val == null) { + "" + } else { + "\n$key " + valueToStr(val) + } + }.join() + } else if (value instanceof Map) { + "\n$key " + value.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ") + } else { + "\n$key " + valueToStr(value) + } + }.join() + + def inputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "input" } + .collect { ', path(viash_par_' + it.plainName + ', stageAs: "_viash_par/' + it.plainName + '_?/*")' } + .join() + + def outputPaths = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .collect { par -> + // insert dummy into every output (see nextflow-io/nextflow#2678) + if (!par.multiple) { + ', path{[".exitcode", args.' + par.plainName + ']}' + } else { + ', path{[".exitcode"] + args.' + par.plainName + '}' + } + } + .join() + + // TODO: move this functionality somewhere else? + if (workflowArgs.auto.transcript) { + outputPaths = outputPaths + ', path{[".exitcode", ".command*"]}' + } else { + outputPaths = outputPaths + ', path{[".exitcode"]}' + } + + // create dirs for output files (based on BashWrapper.createParentFiles) + def createParentStr = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" && it.create_parent } + .collect { par -> + def contents = "args[\"${par.plainName}\"] instanceof List ? args[\"${par.plainName}\"].join('\" \"') : args[\"${par.plainName}\"]" + "\${ args.containsKey(\"${par.plainName}\") ? \"mkdir_parent '\" + escapeText(${contents}) + \"'\" : \"\" }" + } + .join("\n") + + // construct inputFileExports + def inputFileExports = meta.config.allArguments + .findAll { it.type == "file" && it.direction.toLowerCase() == "input" } + .collect { par -> + def contents = "viash_par_${par.plainName} instanceof List ? viash_par_${par.plainName}.join(\"${par.multiple_sep}\") : viash_par_${par.plainName}" + "\n\${viash_par_${par.plainName}.empty ? \"\" : \"export VIASH_PAR_${par.plainName.toUpperCase()}='\" + escapeText(${contents}) + \"'\"}" + } + + // NOTE: if using docker, use /tmp instead of tmpDir! + def tmpDir = java.nio.file.Paths.get( + System.getenv('NXF_TEMP') ?: + System.getenv('VIASH_TEMP') ?: + System.getenv('VIASH_TMPDIR') ?: + System.getenv('VIASH_TEMPDIR') ?: + System.getenv('VIASH_TMP') ?: + System.getenv('TEMP') ?: + System.getenv('TMPDIR') ?: + System.getenv('TEMPDIR') ?: + System.getenv('TMP') ?: + '/tmp' + ).toAbsolutePath() + + // construct stub + def stub = meta.config.allArguments + .findAll { it.type == "file" && it.direction == "output" } + .collect { par -> + "\${ args.containsKey(\"${par.plainName}\") ? \"touch2 \\\"\" + (args[\"${par.plainName}\"] instanceof String ? args[\"${par.plainName}\"].replace(\"_*\", \"_0\") : args[\"${par.plainName}\"].join('\" \"')) + \"\\\"\" : \"\" }" + } + .join("\n") + + // escape script + def escapedScript = rawScript.replace('\\', '\\\\').replace('$', '\\$').replace('"""', '\\"\\"\\"') + + // publishdir assert + def assertStr = (workflowArgs.auto.publish == true) || workflowArgs.auto.transcript ? + """\nassert task.publishDir.size() > 0: "if auto.publish is true, params.publish_dir needs to be defined.\\n Example: --publish_dir './output/'" """ : + "" + + // generate process string + def procStr = + """nextflow.enable.dsl=2 + | + |def escapeText = { s -> s.toString().replaceAll("'", "'\\\"'\\\"'") } + |process $procKey {$drctvStrs + |input: + | tuple val(id)$inputPaths, val(args), path(resourcesDir, stageAs: ".viash_meta_resources") + |output: + | tuple val("\$id")$outputPaths, optional: true + |stub: + |\"\"\" + |touch2() { mkdir -p "\\\$(dirname "\\\$1")" && touch "\\\$1" ; } + |$stub + |\"\"\" + |script:$assertStr + |def parInject = args + | .findAll{key, value -> value != null} + | .collect{key, value -> "export VIASH_PAR_\${key.toUpperCase()}='\${escapeText(value)}'"} + | .join("\\n") + |\"\"\" + |# meta exports + |export VIASH_META_RESOURCES_DIR="\${resourcesDir}" + |export VIASH_META_TEMP_DIR="${['docker', 'podman', 'charliecloud'].any{ it == workflow.containerEngine } ? '/tmp' : tmpDir}" + |export VIASH_META_NAME="${meta.config.name}" + |# export VIASH_META_EXECUTABLE="\\\$VIASH_META_RESOURCES_DIR/\\\$VIASH_META_NAME" + |export VIASH_META_CONFIG="\\\$VIASH_META_RESOURCES_DIR/.config.vsh.yaml" + |\${task.cpus ? "export VIASH_META_CPUS=\$task.cpus" : "" } + |\${task.memory?.bytes != null ? "export VIASH_META_MEMORY_B=\$task.memory.bytes" : "" } + |if [ ! -z \\\${VIASH_META_MEMORY_B+x} ]; then + | export VIASH_META_MEMORY_KB=\\\$(( (\\\$VIASH_META_MEMORY_B+999) / 1000 )) + | export VIASH_META_MEMORY_MB=\\\$(( (\\\$VIASH_META_MEMORY_KB+999) / 1000 )) + | export VIASH_META_MEMORY_GB=\\\$(( (\\\$VIASH_META_MEMORY_MB+999) / 1000 )) + | export VIASH_META_MEMORY_TB=\\\$(( (\\\$VIASH_META_MEMORY_GB+999) / 1000 )) + | export VIASH_META_MEMORY_PB=\\\$(( (\\\$VIASH_META_MEMORY_TB+999) / 1000 )) + | export VIASH_META_MEMORY_KIB=\\\$(( (\\\$VIASH_META_MEMORY_B+1023) / 1024 )) + | export VIASH_META_MEMORY_MIB=\\\$(( (\\\$VIASH_META_MEMORY_KIB+1023) / 1024 )) + | export VIASH_META_MEMORY_GIB=\\\$(( (\\\$VIASH_META_MEMORY_MIB+1023) / 1024 )) + | export VIASH_META_MEMORY_TIB=\\\$(( (\\\$VIASH_META_MEMORY_GIB+1023) / 1024 )) + | export VIASH_META_MEMORY_PIB=\\\$(( (\\\$VIASH_META_MEMORY_TIB+1023) / 1024 )) + |fi + | + |# meta synonyms + |export VIASH_TEMP="\\\$VIASH_META_TEMP_DIR" + |export TEMP_DIR="\\\$VIASH_META_TEMP_DIR" + | + |# create output dirs if need be + |function mkdir_parent { + | for file in "\\\$@"; do + | mkdir -p "\\\$(dirname "\\\$file")" + | done + |} + |$createParentStr + | + |# argument exports${inputFileExports.join()} + |\$parInject + | + |# process script + |${escapedScript} + |\"\"\" + |} + |""".stripMargin() + + // TODO: print on debug + // if (workflowArgs.debug == true) { + // println("######################\n$procStr\n######################") + // } + + // write process to temp file + def tempFile = java.nio.file.Files.createTempFile("viash-process-${procKey}-", ".nf") + addShutdownHook { java.nio.file.Files.deleteIfExists(tempFile) } + tempFile.text = procStr + + // create process from temp file + def binding = new nextflow.script.ScriptBinding([:]) + def session = nextflow.Nextflow.getSession() + def parser = _getScriptLoader(session) + .setModule(true) + .setBinding(binding) + def moduleScript = parser.runScript(tempFile) + .getScript() + + // register module in meta + def module = new nextflow.script.IncludeDef.Module(name: procKey) + scriptMeta.addModule(moduleScript, module.name, module.alias) + + // retrieve and return process from meta + return scriptMeta.getProcess(procKey) +} + +// use Reflection to get a ScriptParser / ScriptLoader +// <25.02.0-edge: new nextflow.script.ScriptParser(session) +// >=25.02.0-edge: nextflow.script.ScriptLoaderFactory.create(session) +def _getScriptLoader(nextflow.Session session) { + // try using the old method + try { + Class scriptParserClass = Class.forName('nextflow.script.ScriptParser') + return scriptParserClass.getDeclaredConstructor(nextflow.Session).newInstance(session) + } catch (ClassNotFoundException e) { + // else try with the new method + try { + Class scriptLoaderFactoryClass = Class.forName('nextflow.script.ScriptLoaderFactory') + def createMethod = scriptLoaderFactoryClass.getDeclaredMethod('create', nextflow.Session) + return createMethod.invoke(null, session) // null because create is static + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e2) { + // Handle the case where neither class is found + throw new Exception("Neither nextflow.script.ScriptParser nor nextflow.script.ScriptLoaderFactory could be found. Is this a compatible Nextflow version?", e2) + } + } +} + +// defaults +meta["defaults"] = [ + // key to be used to trace the process and determine output names + key: null, + + // fixed arguments to be passed to script + args: [:], + + // default directives + directives: readJsonBlob('''{ + "container" : { + "registry" : "images.viash-hub.com", + "image" : "vsh/toolbox/yq", + "tag" : "gunzip" + }, + "tag" : "$id" +}'''), + + // auto settings + auto: readJsonBlob('''{ + "simplifyInput" : true, + "simplifyOutput" : false, + "transcript" : false, + "publish" : false +}'''), + + // Apply a map over the incoming tuple + // Example: `{ tup -> [ tup[0], [input: tup[1].output] ] + tup.drop(2) }` + map: null, + + // Apply a map over the ID element of a tuple (i.e. the first element) + // Example: `{ id -> id + "_foo" }` + mapId: null, + + // Apply a map over the data element of a tuple (i.e. the second element) + // Example: `{ data -> [ input: data.output ] }` + mapData: null, + + // Apply a map over the passthrough elements of a tuple (i.e. the tuple excl. the first two elements) + // Example: `{ pt -> pt.drop(1) }` + mapPassthrough: null, + + // Filter the channel + // Example: `{ tup -> tup[0] == "foo" }` + filter: null, + + // Choose whether or not to run the component on the tuple if the condition is true. + // Otherwise, the tuple will be passed through. + // Example: `{ tup -> tup[0] != "skip_this" }` + runIf: null, + + // Rename keys in the data field of the tuple (i.e. the second element) + // Will likely be deprecated in favour of `fromState`. + // Example: `[ "new_key": "old_key" ]` + renameKeys: null, + + // Fetch data from the state and pass it to the module without altering the current state. + // + // `fromState` should be `null`, `List[String]`, `Map[String, String]` or a function. + // + // - If it is `null`, the state will be passed to the module as is. + // - If it is a `List[String]`, the data will be the values of the state at the given keys. + // - If it is a `Map[String, String]`, the data will be the values of the state at the given keys, with the keys renamed according to the map. + // - If it is a function, the tuple (`[id, state]`) in the channel will be passed to the function, and the result will be used as the data. + // + // Example: `{ id, state -> [input: state.fastq_file] }` + // Default: `null` + fromState: null, + + // Determine how the state should be updated after the module has been run. + // + // `toState` should be `null`, `List[String]`, `Map[String, String]` or a function. + // + // - If it is `null`, the state will be replaced with the output of the module. + // - If it is a `List[String]`, the state will be updated with the values of the data at the given keys. + // - If it is a `Map[String, String]`, the state will be updated with the values of the data at the given keys, with the keys renamed according to the map. + // - If it is a function, a tuple (`[id, output, state]`) will be passed to the function, and the result will be used as the new state. + // + // Example: `{ id, output, state -> state + [counts: state.output] }` + // Default: `{ id, output, state -> output }` + toState: null, + + // Whether or not to print debug messages + // Default: `false` + debug: false +] + +// initialise default workflow +meta["workflow"] = workflowFactory([key: meta.config.name], meta.defaults, meta) + +// add workflow to environment +nextflow.script.ScriptMeta.current().addDefinition(meta.workflow) + +// anonymous workflow for running this module as a standalone +workflow { + // add id argument if it's not already in the config + // TODO: deep copy + def newConfig = deepClone(meta.config) + def newParams = deepClone(params) + + def argsContainsId = newConfig.allArguments.any{it.plainName == "id"} + if (!argsContainsId) { + def idArg = [ + 'name': '--id', + 'required': false, + 'type': 'string', + 'description': 'A unique id for every entry.', + 'multiple': false + ] + newConfig.arguments.add(0, idArg) + newConfig = processConfig(newConfig) + } + if (!newParams.containsKey("id")) { + newParams.id = "run" + } + + helpMessage(newConfig) + + channelFromParams(newParams, newConfig) + // make sure id is not in the state if id is not in the args + | map {id, state -> + if (!argsContainsId) { + [id, state.findAll{k, v -> k != "id"}] + } else { + [id, state] + } + } + | meta.workflow.run( + auto: [ publish: "state" ] + ) +} + +// END COMPONENT-SPECIFIC CODE diff --git a/target/nextflow/yq/nextflow.config b/target/nextflow/yq/nextflow.config new file mode 100644 index 0000000..73bb00f --- /dev/null +++ b/target/nextflow/yq/nextflow.config @@ -0,0 +1,125 @@ +manifest { + name = 'yq' + mainScript = 'main.nf' + nextflowVersion = '!>=20.12.1-edge' + version = 'gunzip' + description = 'A portable YAML, JSON, XML, CSV, TOML and properties processor' +} + +process.container = 'nextflow/bash:latest' + +// detect tempdir +tempDir = java.nio.file.Paths.get( + System.getenv('NXF_TEMP') ?: + System.getenv('VIASH_TEMP') ?: + System.getenv('TEMPDIR') ?: + System.getenv('TMPDIR') ?: + '/tmp' +).toAbsolutePath() + +profiles { + no_publish { + process { + withName: '.*' { + publishDir = [ + enabled: false + ] + } + } + } + mount_temp { + docker.temp = tempDir + podman.temp = tempDir + charliecloud.temp = tempDir + } + docker { + docker.enabled = true + // docker.userEmulation = true + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + singularity { + singularity.enabled = true + singularity.autoMounts = true + docker.enabled = false + podman.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + podman { + podman.enabled = true + docker.enabled = false + singularity.enabled = false + shifter.enabled = false + charliecloud.enabled = false + } + shifter { + shifter.enabled = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + charliecloud.enabled = false + } + charliecloud { + charliecloud.enabled = true + docker.enabled = false + singularity.enabled = false + podman.enabled = false + shifter.enabled = false + } +} + +process{ + withLabel: mem1gb { memory = 1000000000.B } + withLabel: mem2gb { memory = 2000000000.B } + withLabel: mem5gb { memory = 5000000000.B } + withLabel: mem10gb { memory = 10000000000.B } + withLabel: mem20gb { memory = 20000000000.B } + withLabel: mem50gb { memory = 50000000000.B } + withLabel: mem100gb { memory = 100000000000.B } + withLabel: mem200gb { memory = 200000000000.B } + withLabel: mem500gb { memory = 500000000000.B } + withLabel: mem1tb { memory = 1000000000000.B } + withLabel: mem2tb { memory = 2000000000000.B } + withLabel: mem5tb { memory = 5000000000000.B } + withLabel: mem10tb { memory = 10000000000000.B } + withLabel: mem20tb { memory = 20000000000000.B } + withLabel: mem50tb { memory = 50000000000000.B } + withLabel: mem100tb { memory = 100000000000000.B } + withLabel: mem200tb { memory = 200000000000000.B } + withLabel: mem500tb { memory = 500000000000000.B } + withLabel: mem1gib { memory = 1073741824.B } + withLabel: mem2gib { memory = 2147483648.B } + withLabel: mem4gib { memory = 4294967296.B } + withLabel: mem8gib { memory = 8589934592.B } + withLabel: mem16gib { memory = 17179869184.B } + withLabel: mem32gib { memory = 34359738368.B } + withLabel: mem64gib { memory = 68719476736.B } + withLabel: mem128gib { memory = 137438953472.B } + withLabel: mem256gib { memory = 274877906944.B } + withLabel: mem512gib { memory = 549755813888.B } + withLabel: mem1tib { memory = 1099511627776.B } + withLabel: mem2tib { memory = 2199023255552.B } + withLabel: mem4tib { memory = 4398046511104.B } + withLabel: mem8tib { memory = 8796093022208.B } + withLabel: mem16tib { memory = 17592186044416.B } + withLabel: mem32tib { memory = 35184372088832.B } + withLabel: mem64tib { memory = 70368744177664.B } + withLabel: mem128tib { memory = 140737488355328.B } + withLabel: mem256tib { memory = 281474976710656.B } + withLabel: mem512tib { memory = 562949953421312.B } + withLabel: cpu1 { cpus = 1 } + withLabel: cpu2 { cpus = 2 } + withLabel: cpu5 { cpus = 5 } + withLabel: cpu10 { cpus = 10 } + withLabel: cpu20 { cpus = 20 } + withLabel: cpu50 { cpus = 50 } + withLabel: cpu100 { cpus = 100 } + withLabel: cpu200 { cpus = 200 } + withLabel: cpu500 { cpus = 500 } + withLabel: cpu1000 { cpus = 1000 } +} + + diff --git a/target/nextflow/yq/nextflow_schema.json b/target/nextflow/yq/nextflow_schema.json new file mode 100644 index 0000000..a596f3b --- /dev/null +++ b/target/nextflow/yq/nextflow_schema.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "yq", + "description": "A portable YAML, JSON, XML, CSV, TOML and properties processor", + "type": "object", + "$defs": { + "inputs": { + "title": "Inputs", + "type": "object", + "description": "No description", + "properties": { + "input": { + "type": "string", + "format": "path", + "exists": true, + "description": "files to be processed", + "help_text": "Type: `file`, multiple: `False`, required, direction: `input`, example: `\"input.yaml\"`. " + } + } + }, + "outputs": { + "title": "Outputs", + "type": "object", + "description": "No description", + "properties": { + "output": { + "type": "string", + "format": "path", + "description": "output file", + "help_text": "Type: `file`, multiple: `False`, required, default: `\"$id.$key.output.yaml\"`, direction: `output`, example: `\"output.yaml\"`. ", + "default": "$id.$key.output.yaml" + } + } + }, + "arguments": { + "title": "Arguments", + "type": "object", + "description": "No description", + "properties": { + "eval": { + "type": "string", + "description": "expression to evaluate", + "help_text": "Type: `string`, multiple: `False`, required, example: `\".name = \"foo\"\"`. " + }, + "indent": { + "type": "integer", + "description": "sets indent level for output (default 2)", + "help_text": "Type: `integer`, multiple: `False`. " + }, + "input_format": { + "type": "string", + "description": "parse format for input", + "help_text": "Type: `string`, multiple: `False`, choices: ``auto`, `a`, `yaml`, `y`, `json`, `j`, `props`, `p`, `csv`, `c`, `tsv`, `t`, `xml`, `x`, `base64`, `uri`, `toml`, `shell`, `s`, `lua`, `l``. ", + "enum": [ + "auto", + "a", + "yaml", + "y", + "json", + "j", + "props", + "p", + "csv", + "c", + "tsv", + "t", + "xml", + "x", + "base64", + "uri", + "toml", + "shell", + "s", + "lua", + "l" + ] + }, + "output_format": { + "type": "string", + "description": "output format type", + "help_text": "Type: `string`, multiple: `False`, choices: ``auto`, `a`, `yaml`, `y`, `json`, `j`, `props`, `p`, `csv`, `c`, `tsv`, `t`, `xml`, `x`, `base64`, `uri`, `toml`, `shell`, `s`, `lua`, `l``. ", + "enum": [ + "auto", + "a", + "yaml", + "y", + "json", + "j", + "props", + "p", + "csv", + "c", + "tsv", + "t", + "xml", + "x", + "base64", + "uri", + "toml", + "shell", + "s", + "lua", + "l" + ] + }, + "pretty_print": { + "type": "boolean", + "description": "pretty print, shorthand for '..", + "help_text": "Type: `boolean_true`, multiple: `False`, default: `false`. ", + "default": false + } + } + }, + "nextflow input-output arguments": { + "title": "Nextflow input-output arguments", + "type": "object", + "description": "Input/output parameters for Nextflow itself. Please note that both publishDir and publish_dir are supported but at least one has to be configured.", + "properties": { + "publish_dir": { + "type": "string", + "description": "Path to an output directory.", + "help_text": "Type: `string`, multiple: `False`, required, example: `\"output/\"`. " + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/inputs" + }, + { + "$ref": "#/$defs/outputs" + }, + { + "$ref": "#/$defs/arguments" + }, + { + "$ref": "#/$defs/nextflow input-output arguments" + } + ] +}