R/singularity_generation.R

Defines functions generate_singularity

Documented in generate_singularity

#' Generate Singularity Definition File
#'
#' @description
#' Generate a Singularity/Apptainer definition file for HPC environments.
#' Singularity is commonly used in HPC clusters where Docker is not available.
#'
#' @param output_dir Character. Directory to save Singularity files (required).
#' @param r_version Character. R version to use. Default is current R version.
#' @param base_image Character. Base Docker image. Default "rocker/r-ver"
#' @param conda_env Character. Path to conda environment file. Optional.
#' @param system_deps Character vector. System dependencies to install
#' @param project_name Character. Name for the project
#'
#' @return List of generated file paths
#'
#' @export
#'
#' @examples
#' \dontrun{
#' generate_singularity(
#'   output_dir = tempdir(),
#'   project_name = "my_analysis",
#'   system_deps = c("samtools", "bwa")
#' )
#' }
generate_singularity <- function(
    output_dir,
    r_version = NULL,
    base_image = "rocker/r-ver",
    conda_env = NULL,
    system_deps = NULL,
    project_name = "reproflow-project") {
  if (is.null(r_version)) {
    r_version <- paste(R.version$major, R.version$minor, sep = ".")
  }

  # Create output directory
  dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)

  cli::cli_alert_info("Generating Singularity definition file...")

  # Build definition file
  def_lines <- c(
    "Bootstrap: docker",
    paste0("From: ", base_image, ":", r_version),
    "",
    "%labels",
    paste0("    PROJECT ", project_name),
    paste0("    R_VERSION ", r_version),
    "    GENERATED_BY Capsule",
    "",
    "%help",
    paste0("    Reproducible R environment for: ", project_name),
    paste0("    R version: ", r_version),
    "    Generated by Capsule package",
    "",
    "%post",
    "    # Update package lists",
    "    apt-get update -y",
    "",
    "    # Install essential build tools",
    "    apt-get install -y \\",
    "        wget \\",
    "        ca-certificates \\",
    "        git \\",
    "        libcurl4-openssl-dev \\",
    "        libssl-dev \\",
    "        libxml2-dev"
  )

  # Add system dependencies
  if (!is.null(system_deps) && length(system_deps) > 0) {
    def_lines <- c(def_lines, "")
    def_lines <- c(def_lines, "    # Install additional system dependencies")
    for (dep in system_deps) {
      def_lines <- c(def_lines, paste0("    apt-get install -y ", dep))
    }
  }

  def_lines <- c(
    def_lines,
    "",
    "    # Clean up",
    "    apt-get clean",
    "    rm -rf /var/lib/apt/lists/*",
    ""
  )

  # Conda installation if needed
  if (!is.null(conda_env) && file.exists(conda_env)) {
    def_lines <- c(
      def_lines,
      "    # Install Miniconda",
      "    cd /opt",
      "    wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh",
      "    bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/conda",
      "    rm Miniconda3-latest-Linux-x86_64.sh",
      "",
      "    # Add conda to PATH",
      "    export PATH=/opt/conda/bin:$PATH",
      "    conda init bash",
      "",
      paste0("    # Restore conda environment from ", basename(conda_env)),
      paste0("    conda env create -f /project/", basename(conda_env)),
      ""
    )
  }

  # R packages via renv
  def_lines <- c(
    def_lines,
    "    # Install renv for R package management",
    "    R -e \"install.packages('renv', repos='https://cloud.r-project.org/')\"",
    ""
  )

  # Files section
  def_lines <- c(
    def_lines,
    "%files",
    "    # Copy project files (adjust as needed)",
    "    renv.lock /project/renv.lock"
  )

  if (!is.null(conda_env) && file.exists(conda_env)) {
    def_lines <- c(
      def_lines,
      paste0("    ", conda_env, " /project/", basename(conda_env))
    )
  }

  def_lines <- c(def_lines, "")

  # Environment section
  def_lines <- c(
    def_lines,
    "%environment",
    "    export LC_ALL=C",
    "    export PATH=/opt/R/bin:$PATH"
  )

  if (!is.null(conda_env)) {
    def_lines <- c(
      def_lines,
      "    export PATH=/opt/conda/bin:$PATH"
    )
  }

  def_lines <- c(
    def_lines,
    "",
    "%runscript",
    "    # Default: launch R",
    "    cd /project",
    "    exec R \"$@\""
  )

  # Write definition file
  def_file <- file.path(output_dir, paste0(project_name, ".def"))
  writeLines(def_lines, def_file)
  cli::cli_alert_success("Definition file created: {.file {def_file}}")

  # Create build script
  build_script <- c(
    "#!/bin/bash",
    "# Build Singularity/Apptainer container",
    "",
    paste0("singularity build ", project_name, ".sif ", project_name, ".def"),
    "",
    "# If singularity is not available, try apptainer",
    "# apptainer build ", project_name, ".sif ", project_name, ".def"
  )

  build_file <- file.path(output_dir, "build_singularity.sh")
  writeLines(build_script, build_file)
  Sys.chmod(build_file, mode = "0755")
  cli::cli_alert_success("Build script created: {.file {build_file}}")

  # Create usage README
  readme_lines <- c(
    paste("#", "Singularity Container for", project_name),
    "",
    "## Building the Container",
    "",
    "```bash",
    paste0("sudo singularity build ", project_name, ".sif ", project_name, ".def"),
    "```",
    "",
    "Or use the provided script:",
    "```bash",
    "sudo bash build_singularity.sh",
    "```",
    "",
    "**Note:** Building requires sudo/root privileges.",
    "",
    "## Running the Container",
    "",
    "### Interactive R session",
    "```bash",
    paste0("singularity shell ", project_name, ".sif"),
    "```",
    "",
    "### Execute R script",
    "```bash",
    paste0("singularity exec ", project_name, ".sif Rscript your_script.R"),
    "```",
    "",
    "### Bind mount your data directory",
    "```bash",
    paste0("singularity exec --bind /path/to/data:/data ", project_name, ".sif Rscript analysis.R"),
    "```",
    "",
    "## HPC Usage",
    "",
    "Most HPC systems support Singularity. Example SLURM job:",
    "",
    "```bash",
    "#!/bin/bash",
    "#SBATCH --job-name=reproflow",
    "#SBATCH --ntasks=1",
    "#SBATCH --cpus-per-task=4",
    "#SBATCH --mem=16G",
    "",
    paste0("singularity exec ", project_name, ".sif Rscript analysis.R"),
    "```",
    "",
    "## Apptainer",
    "",
    "Singularity has been renamed to Apptainer. The commands are identical:",
    "```bash",
    paste0("apptainer build ", project_name, ".sif ", project_name, ".def"),
    paste0("apptainer exec ", project_name, ".sif Rscript analysis.R"),
    "```"
  )

  readme_file <- file.path(output_dir, "SINGULARITY_README.md")
  writeLines(readme_lines, readme_file)
  cli::cli_alert_success("README created: {.file {readme_file}}")

  cli::cli_alert_success("Singularity configuration complete!")
  cli::cli_h2("Next steps:")
  cli::cli_ul(c(
    paste("Build container: sudo bash", build_file),
    paste("Run container: singularity exec", project_name, ".sif R")
  ))

  invisible(list(
    definition = def_file,
    build_script = build_file,
    readme = readme_file
  ))
}

Try the Capsule package in your browser

Any scripts or data that you put into this service are public.

Capsule documentation built on Nov. 11, 2025, 5:14 p.m.