#!/bin/sh
# Helm Node public installer. Generated with immutable image references.
# Read or download this script before running it. Never run it with shell tracing.
set -eu

fail() { printf 'Helm Node: %s\n' "$*" >&2; exit 1; }
add_device_group() {
  case " $model_group $device_groups " in
    *" $1 "*) ;;
    *) device_groups="$device_groups $1";;
  esac
}
usage() {
  cat <<'HELP'
Usage: sh install.sh --backend cuda|vulkan|sycl [--directory PATH] [--models-directory PATH] [--port PORT] [--prepare-only]
  cuda     NVIDIA GPU with its driver and NVIDIA Container Toolkit
  vulkan   AMD GPU with host DRM drivers (/dev/dri)
  sycl     Intel GPU with host DRM drivers (/dev/dri)
Requires Linux x86-64, local Docker Engine, and Docker Compose 2.30 or newer.
Application: /srv/apps/helm-node (must not already exist). Shared models: /srv/models.
Run with permission to create these directories, or choose writable paths explicitly.
The service binds only to 127.0.0.1. No drivers, Docker, models or firewall rules are installed.
HELP
}

write_compose() {
  case "$backend" in
    cuda)
      cat <<'HELM_COMPOSE'
name: ${COMPOSE_PROJECT_NAME:?Missing project name}
services:
  runtime-installer:
    image: ghcr.io/ggml-org/llama.cpp@sha256:6ac921528d613deb0fd142c654735e594a446a1c37a069eeab08d8fd974d4bec
    restart: "no"
    entrypoint:
      - /bin/sh
      - -ec
      - |
        runtime_source="$${RUNTIME_SOURCE_DIR:-/app}"
        rm -rf /runtime/*
        mkdir -p /runtime/app /runtime/libs
        cp -a "$$runtime_source"/. /runtime/app/
        mkdir -p /runtime/rootfs
        for support_pattern in $${RUNTIME_EXTRA_PATHS:-}; do
          for support_path in $$support_pattern; do
            [ -e "$$support_path" ] || continue
            mkdir -p "/runtime/rootfs$$(dirname "$$support_path")"
            cp -a "$$support_path" "/runtime/rootfs$$support_path"
          done
        done
        {
          ldd /runtime/app/llama-server 2>/dev/null || true
          find /runtime/app -maxdepth 1 -type f -name '*.so*' -exec ldd {} \; 2>/dev/null || true
        } | awk '/=> \/|^[[:space:]]*\// { for (i = 1; i <= NF; i++) if ($$i ~ /^\//) print $$i }' | grep -Ev '/(ld-linux|libc|libpthread|libdl|librt|libm)\.so' | sort -u | while read -r lib; do
          cp -L "$$lib" /runtime/libs/ 2>/dev/null || true
        done
        test -x /runtime/app/llama-server
    environment:
      RUNTIME_SOURCE_DIR: ${LOCAL_SERVICE_RUNTIME_SOURCE:-/app}
      RUNTIME_EXTRA_PATHS: ${LOCAL_SERVICE_RUNTIME_EXTRA_PATHS:-}
    volumes:
      - runtime_app:/runtime
  local-service:
    image: ghcr.io/accoladesolutions/grid-node@sha256:87de4c18b47cac4b6154ca3c76214e52e849bce34d3291e287839bc7a1720d34
    init: true
    restart: unless-stopped
    stop_grace_period: 30s
    depends_on:
      runtime-installer:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:${PORT:-13010}:3000"
    environment:
      LOCAL_SERVICE_SETUP_TOKEN: ${LOCAL_SERVICE_SETUP_TOKEN:?Missing setup token}
      LOCAL_SERVICE_ORIGINS: ${LOCAL_SERVICE_ORIGINS:-}
      LOCAL_SERVICE_TRANSFER_TOKEN: ${LOCAL_SERVICE_TRANSFER_TOKEN:-}
      LOCAL_SERVICE_NODE_NAME: ${LOCAL_SERVICE_NODE_NAME:-}
      LOCAL_SERVICE_RUNTIME: ${LOCAL_SERVICE_RUNTIME:-/runtime/app/llama-server}
      LOCAL_SERVICE_RUNTIME_LIBRARY_PATH: ${LOCAL_SERVICE_RUNTIME_LIBRARY_PATH:-/runtime/app:/runtime/libs}
      LOCAL_SERVICE_RUNTIME_LIBRARY_ROOTS: ${LOCAL_SERVICE_RUNTIME_LIBRARY_ROOTS:-}
      NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}
      NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-compute,utility}
    gpus: all
    group_add:
      - "${MODEL_GROUP_ID:-1000}"
HELM_COMPOSE
      for group in $device_groups; do printf '      - "%s"\n' "$group"; done
      cat <<'HELM_COMPOSE'
    volumes:
      - ./config:/config
      - runtime_app:/runtime:ro
      - ${MODEL_DIRECTORY:-/srv/models}:/models:${MODEL_MOUNT_MODE:-ro}
      - ${IMPORT_DIRECTORY:-/srv/models}:/import:ro
    read_only: true
    tmpfs:
      - /tmp:size=256m
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
volumes:
  runtime_app:
HELM_COMPOSE
      ;;
    vulkan)
      cat <<'HELM_COMPOSE'
name: ${COMPOSE_PROJECT_NAME:?Missing project name}
services:
  runtime-installer:
    # This recipe installs the AMD RADV driver and x86-64 loader.
    platform: linux/amd64
    image: ghcr.io/ggml-org/llama.cpp@sha256:819d7a7e313f50e5313f87968710adb270b6714db0de27d9a729597819410ad4
    restart: "no"
    entrypoint:
      - /bin/sh
      - -ec
      - |
        # AMD64 runtime libraries and their matching loader stay isolated from Node.
        test -x /app/llama-server
        test -f /usr/share/vulkan/icd.d/radeon_icd.json
        rm -rf /runtime/app /runtime/bin /runtime/libs /runtime/share
        mkdir -p /runtime/app /runtime/bin /runtime/libs /runtime/share
        cp -a /app/. /runtime/bin/
        cp -a /usr/lib/x86_64-linux-gnu/. /runtime/libs/
        cp -L /lib64/ld-linux-x86-64.so.2 /runtime/ld-linux-x86-64.so.2
        cp /usr/share/vulkan/icd.d/radeon_icd.json /runtime/share/radeon_icd.json
        sed -i 's|"libvulkan_radeon.so"|"/runtime/libs/libvulkan_radeon.so"|' /runtime/share/radeon_icd.json
        cat > /runtime/app/llama-server <<'LAUNCHER'
        #!/bin/sh
        export VK_DRIVER_FILES=/runtime/share/radeon_icd.json
        export LD_LIBRARY_PATH=/runtime/bin:/runtime/libs
        export XDG_CACHE_HOME=/tmp/llama-cache
        # Backend plugins search the working directory when using an explicit loader.
        cd /runtime/bin
        exec /runtime/ld-linux-x86-64.so.2 --library-path /runtime/bin:/runtime/libs /runtime/bin/llama-server "$$@"
        LAUNCHER
        chmod 0755 /runtime/app/llama-server
    volumes:
      - runtime_app:/runtime
  local-service:
    image: ghcr.io/accoladesolutions/grid-node@sha256:87de4c18b47cac4b6154ca3c76214e52e849bce34d3291e287839bc7a1720d34
    init: true
    restart: unless-stopped
    stop_grace_period: 30s
    depends_on:
      runtime-installer:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:${PORT:-13010}:3000"
    environment:
      LOCAL_SERVICE_SETUP_TOKEN: ${LOCAL_SERVICE_SETUP_TOKEN:?Missing setup token}
      LOCAL_SERVICE_ORIGINS: ${LOCAL_SERVICE_ORIGINS:-}
      LOCAL_SERVICE_TRANSFER_TOKEN: ${LOCAL_SERVICE_TRANSFER_TOKEN:-}
      LOCAL_SERVICE_NODE_NAME: ${LOCAL_SERVICE_NODE_NAME:-}
      LOCAL_SERVICE_RUNTIME: /runtime/app/llama-server
      # The wrapper selects the runtime's libc; do not apply it to /bin/sh or Node.
      LOCAL_SERVICE_RUNTIME_LIBRARY_PATH: /runtime/app
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - "${MODEL_GROUP_ID:-1000}"
HELM_COMPOSE
      for group in $device_groups; do printf '      - "%s"\n' "$group"; done
      cat <<'HELM_COMPOSE'
    volumes:
      - ./config:/config
      - runtime_app:/runtime:ro
      - ${MODEL_DIRECTORY:-/srv/models}:/models:${MODEL_MOUNT_MODE:-ro}
      - ${IMPORT_DIRECTORY:-/srv/models}:/import:ro
    read_only: true
    tmpfs:
      - /tmp:size=256m
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
volumes:
  runtime_app:
HELM_COMPOSE
      ;;
    sycl)
      cat <<'HELM_COMPOSE'
name: ${COMPOSE_PROJECT_NAME:?Missing project name}
services:
  runtime-installer:
    image: ghcr.io/accoladesolutions/ai-services-control/local-service@sha256:46009e379ab0c4e8d0efe29de6c19078bd886f8e42c97f6813ff273b80bae0be
    restart: "no"
    entrypoint:
      - /bin/sh
      - -ec
      - |
        runtime_source="$${RUNTIME_SOURCE_DIR:-/app}"
        rm -rf /runtime/* /runtime-opt-intel/* /runtime-opencl/* /runtime-usr-lib/* /runtime-intel-opencl/* /runtime-local-lib/*
        mkdir -p /runtime/app /runtime/libs /runtime-opt-intel /runtime-opencl /runtime-usr-lib /runtime-intel-opencl /runtime-local-lib
        cp -a "$$runtime_source"/. /runtime/app/
        cp -a /opt/intel/. /runtime-opt-intel/
        cp -a /etc/OpenCL/. /runtime-opencl/
        cp -a /usr/lib/x86_64-linux-gnu/intel-opencl/. /runtime-intel-opencl/ 2>/dev/null || true
        cp -a /usr/lib/x86_64-linux-gnu/libze* /usr/lib/x86_64-linux-gnu/libigdgmm* /usr/lib/x86_64-linux-gnu/libdrm_intel* /runtime-usr-lib/ 2>/dev/null || true
        cp -a /usr/local/lib/. /runtime-local-lib/ 2>/dev/null || true
        for support_pattern in $${RUNTIME_EXTRA_PATHS:-}; do
          for support_path in $$support_pattern; do
            [ -e "$$support_path" ] || continue
            mkdir -p "/runtime/rootfs$$(dirname "$$support_path")"
            cp -a "$$support_path" "/runtime/rootfs$$support_path"
          done
        done
        {
          ldd /runtime/app/llama-server 2>/dev/null || true
          find /runtime/app -maxdepth 1 -type f -name '*.so*' -exec ldd {} \; 2>/dev/null || true
        } | awk '/=> \/|^[[:space:]]*\// { for (i = 1; i <= NF; i++) if ($$i ~ /^\//) print $$i }' | grep -Ev '/(ld-linux|libc|libpthread|libdl|librt|libm)\.so' | sort -u | while read -r lib; do
          cp -L "$$lib" /runtime/libs/ 2>/dev/null || true
        done
        test -x /runtime/app/llama-server
    environment:
      RUNTIME_SOURCE_DIR: ${LOCAL_SERVICE_RUNTIME_SOURCE:-/app}
      RUNTIME_EXTRA_PATHS: ${LOCAL_SERVICE_RUNTIME_EXTRA_PATHS:-/usr/share/vulkan/icd.d/intel*.json}
    volumes:
      - runtime_app:/runtime
      - runtime_opt_intel:/runtime-opt-intel
      - runtime_opencl:/runtime-opencl
      - runtime_usr_lib:/runtime-usr-lib
      - runtime_intel_opencl:/runtime-intel-opencl
      - runtime_local_lib:/runtime-local-lib
  local-service:
    image: ghcr.io/accoladesolutions/grid-node@sha256:87de4c18b47cac4b6154ca3c76214e52e849bce34d3291e287839bc7a1720d34
    init: true
    restart: unless-stopped
    stop_grace_period: 30s
    depends_on:
      runtime-installer:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:${PORT:-13010}:3000"
    environment:
      LOCAL_SERVICE_SETUP_TOKEN: ${LOCAL_SERVICE_SETUP_TOKEN:?Missing setup token}
      LOCAL_SERVICE_ORIGINS: ${LOCAL_SERVICE_ORIGINS:-}
      LOCAL_SERVICE_TRANSFER_TOKEN: ${LOCAL_SERVICE_TRANSFER_TOKEN:-}
      LOCAL_SERVICE_NODE_NAME: ${LOCAL_SERVICE_NODE_NAME:-}
      LOCAL_SERVICE_RUNTIME: ${LOCAL_SERVICE_RUNTIME:-/runtime/app/llama-server}
      LOCAL_SERVICE_RUNTIME_LIBRARY_PATH: ${LOCAL_SERVICE_RUNTIME_LIBRARY_PATH:-/runtime/app:/runtime/libs:/runtime-usr-lib:/usr/local/lib:/opt/intel/oneapi/compiler/2025.3/lib:/opt/intel/oneapi/2025.3/lib}
      LOCAL_SERVICE_RUNTIME_LIBRARY_ROOTS: ${LOCAL_SERVICE_RUNTIME_LIBRARY_ROOTS:-/opt/intel,/runtime-usr-lib,/usr/local/lib,/usr/lib/x86_64-linux-gnu/intel-opencl}
      LOCAL_SERVICE_RUNTIME_ENV_ONEAPI_ROOT: ${LOCAL_SERVICE_RUNTIME_ENV_ONEAPI_ROOT:-/opt/intel/oneapi}
      LOCAL_SERVICE_RUNTIME_ENV_OCL_ICD_FILENAMES: ${LOCAL_SERVICE_RUNTIME_ENV_OCL_ICD_FILENAMES:-/opt/intel/oneapi/compiler/2025.3/lib/libintelocl.so}
      LOCAL_SERVICE_RUNTIME_ENV_INTEL_PYTHONHOME: ${LOCAL_SERVICE_RUNTIME_ENV_INTEL_PYTHONHOME:-/opt/intel/oneapi/debugger/2025.3/opt/debugger}
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - "${MODEL_GROUP_ID:-1000}"
HELM_COMPOSE
      for group in $device_groups; do printf '      - "%s"\n' "$group"; done
      cat <<'HELM_COMPOSE'
    volumes:
      - ./config:/config
      - runtime_app:/runtime:ro
      - runtime_opt_intel:/opt/intel:ro
      - runtime_opencl:/etc/OpenCL:ro
      - runtime_usr_lib:/runtime-usr-lib:ro
      - runtime_intel_opencl:/usr/lib/x86_64-linux-gnu/intel-opencl:ro
      - runtime_local_lib:/usr/local/lib:ro
      - ${MODEL_DIRECTORY:-/srv/models}:/models:${MODEL_MOUNT_MODE:-ro}
      - ${IMPORT_DIRECTORY:-/srv/models}:/import:ro
    read_only: true
    tmpfs:
      - /tmp:size=256m
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
volumes:
  runtime_app:
  runtime_opt_intel:
  runtime_opencl:
  runtime_usr_lib:
  runtime_intel_opencl:
  runtime_local_lib:
HELM_COMPOSE
      ;;

  esac
}

main() {
  backend= directory=/srv/apps/helm-node models_directory=/srv/models port=13010 prepare_only=false
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --backend|--directory|--models-directory|--port)
        [ "$#" -ge 2 ] || fail "Missing value for $1"
        case "$1" in
          --backend) backend=$2;;
          --directory) directory=$2;;
          --models-directory) models_directory=$2;;
          --port) port=$2;;
        esac
        shift 2;;
      --prepare-only) prepare_only=true; shift;;
      --help|-h) usage; return;;
      *) usage >&2; fail "Unknown option: $1";;
    esac
  done
  case "$backend" in cuda|vulkan|sycl) ;; *) usage >&2; fail 'Choose --backend cuda, vulkan or sycl.';; esac
  case "$port" in ''|*[!0-9]*|0*) fail 'Port must be an integer from 1024 to 65535.';; esac
  [ "${#port}" -le 5 ] && [ "$port" -ge 1024 ] && [ "$port" -le 65535 ] || fail 'Port must be between 1024 and 65535.'
  case "$directory" in /*) ;; *) directory="$PWD/$directory";; esac
  case "$models_directory" in /*) ;; *) models_directory="$PWD/$models_directory";; esac
  case "$models_directory" in *[!a-zA-Z0-9_./\ -]*) fail 'Model directory supports letters, numbers, spaces, /, ., _, and -.';; esac
  [ ! -e "$models_directory" ] || [ -d "$models_directory" ] || fail 'Model storage must be a directory.'
  [ ! -L "$models_directory" ] || [ -d "$models_directory" ] || fail 'Model storage symlink is broken.'
  [ ! -e "$directory" ] && [ ! -L "$directory" ] || fail "Directory already exists; it will not be overwritten: $directory"
  [ "$(uname -s)" = Linux ] && [ "$(uname -m)" = x86_64 ] || fail 'This GPU installer currently requires Linux x86-64.'
  for utility in docker od tr id stat dirname; do command -v "$utility" >/dev/null 2>&1 || fail "Required command is missing: $utility"; done
  docker_endpoint=${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}')}
  case "$docker_endpoint" in unix://*) ;; *) fail 'Use a local Docker Engine (Unix socket), not a remote Docker context.';; esac
  server_platform=$(docker info --format '{{.OSType}}/{{.Architecture}}') || fail 'Docker Engine is unavailable. Check the daemon and your Docker permissions.'
  case "$server_platform" in linux/x86_64|linux/amd64) ;; *) fail "Unsupported Docker host: $server_platform";; esac
  compose_version=$(docker compose version --short) || fail 'Install the Docker Compose plugin (2.30 or newer).'
  compose_version=${compose_version#v}
  major=${compose_version%%.*}; rest=${compose_version#*.}; minor=${rest%%.*}
  case "$major:$minor" in *[!0-9:]*|:|*:) fail 'Cannot determine Docker Compose version.';; esac
  { [ "$major" -eq 2 ] && [ "$minor" -ge 30 ]; } || [ "$major" -gt 2 ] || fail 'Docker Compose 2.30 or newer is required.'
  model_group=$(id -g)
  device_groups=
  case "$backend" in
    cuda)
      command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1 || fail 'Install a working NVIDIA driver and NVIDIA Container Toolkit first.';;
    vulkan|sycl)
      [ -d /dev/dri ] || fail 'No /dev/dri devices found. Install the GPU host driver first.'
      found_render=false
      for device in /dev/dri/renderD* /dev/dri/card*; do
        [ -c "$device" ] || continue
        case "$device" in */renderD*) found_render=true;; esac
        add_device_group "$(stat -c '%g' "$device")"
      done
      [ "$found_render" = true ] || fail 'No GPU render device found under /dev/dri.';;
  esac
  umask 077
  mkdir -p -- "$(dirname -- "$directory")" || fail 'Cannot create the application parent directory. Use an account with write access or --directory.'
  mkdir -- "$directory" || fail 'Cannot create the install directory.'
  directory=$(CDPATH= cd -- "$directory" && pwd -P)
  mkdir -- "$directory/config"
  chmod 2770 "$directory/config"
  if [ ! -d "$models_directory" ]; then
    mkdir -p -- "$models_directory" || fail 'Cannot create model storage. Use an account with write access or --models-directory.'
    chmod 2770 "$models_directory"
  fi
  # Shared model storage keeps its existing owner/mode; add its real numeric group.
  all_groups="$device_groups $(id -g)"
  model_group=$(stat -L -c '%g' "$models_directory")
  device_groups=
  for group in $all_groups; do add_device_group "$group"; done
  setup_token=$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')
  [ "${#setup_token}" -eq 48 ] || fail 'Could not generate the setup token.'
  project_suffix=$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')
  [ "${#project_suffix}" -eq 12 ] || fail 'Could not generate the project name.'
  cat > "$directory/.env" <<EOF
COMPOSE_PROJECT_NAME=helm-node-$project_suffix
PORT=$port
MODEL_GROUP_ID=$model_group
MODEL_DIRECTORY='$models_directory'
IMPORT_DIRECTORY='$models_directory'
MODEL_MOUNT_MODE=rw
LOCAL_SERVICE_SETUP_TOKEN=$setup_token
EOF
  write_compose > "$directory/compose.yaml"
  # Supplementary numeric groups grant access to configuration and host DRM devices.
  for group in $device_groups; do
    printf '      - "%s"\n' "$group"
  done > "$directory/device-groups.txt"
  # write_compose includes device groups directly; this file is only an operator record.
  printf '%s\n' '178fdb3bf141286bd3f25af60d58d27d9678780d' > "$directory/release-revision.txt"
  cd -- "$directory"
  # Ignore inherited Compose/application variables (including model paths and tokens).
  compose() { env -i PATH="$PATH" HOME="$HOME" DOCKER_HOST="$docker_endpoint" DOCKER_CONFIG="${DOCKER_CONFIG:-$HOME/.docker}" docker compose --env-file "$directory/.env" -f "$directory/compose.yaml" "$@"; }
  compose config --quiet || fail "Invalid Compose configuration. Files preserved in $directory"
  printf 'Prepared Helm Node (%s) in %s\n' "$backend" "$directory"
  if [ "$prepare_only" = true ]; then
    printf '%s\n' 'Review compose.yaml and the private .env, then run docker compose up -d --wait from that directory.'
    return
  fi
  compose pull || fail "Image download failed. Files preserved in $directory; retry docker compose pull there."
  compose up -d --wait --wait-timeout 180 || fail "Startup failed. Files preserved in $directory; inspect docker compose logs and retry docker compose up -d --wait."
  printf '\nHelm Node is responding at http://127.0.0.1:%s\n' "$port"
  printf '%s\n' 'Read LOCAL_SERVICE_SETUP_TOKEN in the private .env to create your administrator account.'
  printf '%s\n' 'For a remote host, use an SSH tunnel. The service is not exposed to the network.'
  printf '%s\n' 'Next: verify detected GPUs, add a model, start a profile and test inference. Service health is not an inference test.'
}

# Keep execution at the end so an incomplete download cannot start installation.
main "$@"
