From 106c85325568449055ae42e3bcb04ac75ac28a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Fri, 6 Mar 2026 10:48:03 -0800 Subject: [PATCH 1/2] feat: add tarball build pipeline for process injection Add self-contained runtime tarball that can be injected into any base image via dockerArgs, replacing the need for pre-built Docker images. - Add build-tarball.sh using uv for Python version management - Add bootstrap.sh entry point for injected runtime - Add Makefile tarball/tarball-test/tarball-test-local targets - Add release-tarball.yml CI workflow triggered on release - Unify all base images on Python 3.11 (matching PyTorch runtime) - Fix VERSION regex to handle spaces around = in version.py - Add --platform linux/amd64 to test targets for Apple Silicon - Update dependency_installer for tarball-aware install paths - Add tarball constants (TARBALL_URL_TEMPLATE, TARBALL_INSTALL_DIR) --- .github/workflows/ci.yml | 55 +++++++++ .github/workflows/release-tarball.yml | 60 ++++++++++ Makefile | 24 ++++ scripts/bootstrap.sh | 41 +++++++ scripts/build-tarball.sh | 150 ++++++++++++++++++++++++ src/constants.py | 4 + src/dependency_installer.py | 10 +- tests/unit/test_dependency_installer.py | 37 ++++++ 8 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-tarball.yml create mode 100755 scripts/bootstrap.sh create mode 100755 scripts/build-tarball.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30975f8..db59ef4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,54 @@ jobs: echo "Testing LB handler in Docker environment..." docker run --rm flash-lb-cpu:test ./test-lb-handler.sh + tarball: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Build tarball + env: + PYTHON_VERSION: "3.11" + run: bash scripts/build-tarball.sh + + - name: Test tarball in bare ubuntu container + run: | + TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz) + docker run --rm -v "$(pwd)/dist:/dist" ubuntu:22.04 \ + bash -c "tar xzf /dist/$(basename $TARBALL) -C /opt && /opt/flash-worker/bootstrap.sh --test" + + - name: Upload tarball artifact + uses: actions/upload-artifact@v4 + with: + name: flash-worker-tarball + path: dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz + retention-days: 30 + overwrite: true + + - name: Post artifact link on PR + env: + GH_TOKEN: ${{ github.token }} + run: | + TARBALL=$(basename dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz) + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + BODY="**Tarball artifact:** [\`${TARBALL}\`](${RUN_URL}#artifacts) + + To test: \`FLASH_WORKER_TARBALL_URL= flash deploy\`" + + # Delete previous tarball comments to keep PR clean + gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ + --jq '.[] | select(.body | contains("Tarball artifact:")) | .id' | \ + xargs -I{} gh api -X DELETE "repos/${{ github.repository }}/issues/comments/{}" 2>/dev/null || true + + gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY" + docker-validation: runs-on: ubuntu-latest needs: [test, lint, docker-test, docker-test-lb-cpu] @@ -229,6 +277,13 @@ jobs: cache-from: type=gha,scope=gpu cache-to: type=gha,mode=max,scope=gpu + tarball-release: + needs: [release] + if: needs.release.outputs.release_created + uses: ./.github/workflows/release-tarball.yml + with: + tag_name: ${{ needs.release.outputs.tag_name }} + docker-prod-cpu: runs-on: ubuntu-latest needs: [release] diff --git a/.github/workflows/release-tarball.yml b/.github/workflows/release-tarball.yml new file mode 100644 index 0000000..e1833f2 --- /dev/null +++ b/.github/workflows/release-tarball.yml @@ -0,0 +1,60 @@ +name: Release Tarball + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (build but don't upload)" + required: false + default: "false" + type: boolean + +# Triggered by release job in ci.yml via workflow_call +# or manually via workflow_dispatch + workflow_call: + inputs: + tag_name: + required: true + type: string + +permissions: + contents: write + +jobs: + build-tarball: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Build tarball + env: + PYTHON_VERSION: "3.11" + run: bash scripts/build-tarball.sh + + - name: Test tarball in bare ubuntu container + run: | + TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz) + docker run --rm -v "$(pwd)/dist:/dist" ubuntu:22.04 \ + bash -c "tar xzf /dist/$(basename $TARBALL) -C /opt && /opt/flash-worker/bootstrap.sh --test" + + - name: Upload tarball to GitHub Release + if: inputs.dry_run != 'true' && inputs.tag_name != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + TARBALL=$(ls dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz) + gh release upload "${{ inputs.tag_name }}" "$TARBALL" --clobber + + - name: Upload tarball as artifact (for dry runs) + if: inputs.dry_run == 'true' || inputs.tag_name == '' + uses: actions/upload-artifact@v4 + with: + name: flash-worker-tarball + path: dist/flash-worker-v*-py3.11-linux-x86_64.tar.gz + retention-days: 7 diff --git a/Makefile b/Makefile index 250536c..18bf975 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,9 @@ IMAGE = runpod/flash TAG = $(or $(FLASH_IMAGE_TAG),local) FULL_IMAGE = $(IMAGE):$(TAG) FULL_IMAGE_CPU = $(IMAGE)-cpu:$(TAG) +VERSION = $(shell python3 -c "import re; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', open('src/version.py').read()).group(1))") +# Must match base image Python: pytorch:2.9.1-cuda12.8-cudnn9-runtime and python:3.11-slim +TARBALL_PYTHON_VERSION ?= 3.11 # Detect host platform for local builds ARCH := $(shell uname -m) @@ -56,6 +59,27 @@ clean: # Remove build artifacts and cache files find . -type f -name "*.pyc" -delete find . -type f -name "*.pkl" -delete +# Tarball targets (process-injectable runtime) +tarball: # Build self-contained runtime tarball (runs in Docker, linux/amd64) + docker run --rm --platform linux/amd64 \ + -e PYTHON_VERSION=$(TARBALL_PYTHON_VERSION) \ + -e UV_CACHE_DIR=/workspace/dist/.uv-cache \ + -v $(PWD):/workspace -w /workspace \ + python:3.11-slim \ + bash -c 'apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1 && pip install uv -q && bash scripts/build-tarball.sh' + +tarball-test: tarball # Test tarball in bare ubuntu container + docker run --rm --platform linux/amd64 -v $(PWD)/dist:/dist ubuntu:22.04 \ + bash -c 'tar xzf /dist/flash-worker-v$(VERSION)-py$(TARBALL_PYTHON_VERSION)-linux-x86_64.tar.gz -C /opt && /opt/flash-worker/bootstrap.sh --test' + +tarball-test-local: # Test tarball injection with mounted file (no rebuild) + docker run --rm --platform linux/amd64 \ + -v $(PWD)/dist/flash-worker-v$(VERSION)-py$(TARBALL_PYTHON_VERSION)-linux-x86_64.tar.gz:/tmp/flash-worker.tar.gz \ + ubuntu:22.04 \ + bash -c 'set -e; FW_DIR=/opt/flash-worker; mkdir -p $$FW_DIR; \ + tar xzf /tmp/flash-worker.tar.gz -C $$FW_DIR --strip-components=1; \ + $$FW_DIR/bootstrap.sh --test' + setup: dev # Initialize project and sync dependencies @echo "Setup complete. Development environment ready." diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..6ac6157 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Flash Worker bootstrap -- entry point for process-injected runtime. +# Launched by dockerArgs after tarball extraction. +set -e + +FW_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Self-test mode (used by tarball-test targets) +if [ "$1" = "--test" ]; then + echo "Flash Worker bootstrap self-test" + echo "FW_DIR: $FW_DIR" + echo "Python: $("$FW_DIR/python/bin/python3" --version)" + echo "uv: $("$FW_DIR/uv" --version)" + "$FW_DIR/venv/bin/python" -c "import pydantic; print(f'pydantic {pydantic.__version__}')" + "$FW_DIR/venv/bin/python" -c "import fastapi; print(f'fastapi {fastapi.__version__}')" + echo "Version: $(cat "$FW_DIR/.version")" + echo "Self-test passed" + exit 0 +fi + +# Isolated flash-worker environment +export PATH="$FW_DIR/venv/bin:$FW_DIR/python/bin:$FW_DIR:$PATH" +export VIRTUAL_ENV="$FW_DIR/venv" +PYTHON_MINOR=$("$FW_DIR/python/bin/python3" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") +export PYTHONPATH="$FW_DIR/src:$VIRTUAL_ENV/lib/python${PYTHON_MINOR}/site-packages${PYTHONPATH:+:$PYTHONPATH}" + +# Signal tarball mode for dependency installer +export FLASH_WORKER_INSTALL_DIR="$FW_DIR" + +# Mode detection (same contract as Docker images) +ENDPOINT_TYPE="${FLASH_ENDPOINT_TYPE:-qb}" + +if [ "$ENDPOINT_TYPE" = "lb" ]; then + exec uvicorn lb_handler:app \ + --host 0.0.0.0 \ + --port 80 \ + --timeout-keep-alive 600 \ + --app-dir "$FW_DIR/src" +else + exec python3 "$FW_DIR/src/handler.py" +fi diff --git a/scripts/build-tarball.sh b/scripts/build-tarball.sh new file mode 100755 index 0000000..6e2b1a0 --- /dev/null +++ b/scripts/build-tarball.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# Build a self-contained flash-worker tarball for process injection. +# Output: dist/flash-worker-v{VERSION}-py{PYTHON_VERSION}-linux-x86_64.tar.gz +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Read version from source +VERSION=$(python3 -c " +import re +text = open('$REPO_ROOT/src/version.py').read() +print(re.search(r'__version__\\s*=\\s*\"([^\"]+)\"', text).group(1)) +") +echo "Building flash-worker tarball v${VERSION}" + +# Configuration +PYTHON_VERSION="${PYTHON_VERSION:-3.11}" + +# Validate Python version against project requirement (>=3.10, <3.15) +PY_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) +if [ "$PY_MINOR" -lt 10 ] || [ "$PY_MINOR" -ge 15 ]; then + echo "ERROR: Python ${PYTHON_VERSION} is outside project requirement (>=3.10, <3.15)" + exit 1 +fi + +UV_VERSION="0.7.19" +UV_URL="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz" + +# Build in container-local tmpdir to avoid macOS case-insensitive filesystem issues +BUILD_DIR="/tmp/flash-worker-build" +TARBALL_ROOT="$BUILD_DIR/flash-worker" +OUTPUT_DIR="$REPO_ROOT/dist" +TARBALL_NAME="flash-worker-v${VERSION}-py${PYTHON_VERSION}-linux-x86_64.tar.gz" + +# Clean previous build +rm -rf "$BUILD_DIR" +mkdir -p "$TARBALL_ROOT" "$OUTPUT_DIR" "$OUTPUT_DIR/.cache" + +# 1. Install Python via uv (handles version resolution and caching) +echo "Installing Python ${PYTHON_VERSION} via uv..." +uv python install "$PYTHON_VERSION" +PYTHON_BIN=$(uv python find --python-preference only-managed "$PYTHON_VERSION") +PYTHON_INSTALL_DIR=$(cd "$(dirname "$PYTHON_BIN")/.." && pwd -P) +cp -r "$PYTHON_INSTALL_DIR" "$TARBALL_ROOT/python" + +# Verify installation +if [ ! -f "$TARBALL_ROOT/python/bin/python3" ]; then + echo "ERROR: Python installation failed - python3 binary not found" + exit 1 +fi +PYTHON_FULL_VERSION=$("$TARBALL_ROOT/python/bin/python3" -c "import sys; v=sys.version_info; print(f'{v.major}.{v.minor}.{v.micro}')") +echo " Python ${PYTHON_FULL_VERSION} installed" + +# 2. Download uv static binary +echo "Downloading uv ${UV_VERSION}..." +if [ -f "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" ]; then + echo " Using cached uv download" + tar xzf "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" -C "$TARBALL_ROOT" --no-same-owner --strip-components=1 "uv-x86_64-unknown-linux-gnu/uv" 2>/dev/null || true +else + curl -fsSL "$UV_URL" -o "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" + tar xzf "$OUTPUT_DIR/.cache/uv-${UV_VERSION}.tar.gz" -C "$TARBALL_ROOT" --no-same-owner --strip-components=1 "uv-x86_64-unknown-linux-gnu/uv" 2>/dev/null || true +fi +chmod +x "$TARBALL_ROOT/uv" + +# 3. Create venv using portable Python +echo "Creating virtual environment..." +"$TARBALL_ROOT/python/bin/python3" -m venv "$TARBALL_ROOT/venv" + +# Fix venv symlinks to be relative (python -m venv creates absolute symlinks) +cd "$TARBALL_ROOT/venv/bin" +for link in python python3 python3.*; do + [ -L "$link" ] || continue + target=$(readlink "$link") + case "$target" in + /*) # Absolute path — make relative to ../../python/bin/ + basename=$(basename "$target") + ln -sf "../../python/bin/$basename" "$link" + ;; + esac +done +cd "$REPO_ROOT" + +# 4. Export and install production dependencies +echo "Installing production dependencies..." +cd "$REPO_ROOT" +# Use the host uv to export requirements (it reads pyproject.toml/uv.lock) +uv export --format requirements-txt --no-dev --no-hashes > "$BUILD_DIR/requirements.txt" + +# Install into the tarball's venv using the tarball's uv +"$TARBALL_ROOT/uv" pip install \ + --python "$TARBALL_ROOT/venv/bin/python" \ + -r "$BUILD_DIR/requirements.txt" + +# 5. Copy source files +echo "Copying source files..." +cp -r "$REPO_ROOT/src/"*.py "$TARBALL_ROOT/src/" 2>/dev/null || true +mkdir -p "$TARBALL_ROOT/src" +for f in "$REPO_ROOT/src/"*.py; do + [ -f "$f" ] && cp "$f" "$TARBALL_ROOT/src/" +done +# Copy test scripts (used by --test flag) +for f in "$REPO_ROOT/src/"*.sh; do + [ -f "$f" ] && cp "$f" "$TARBALL_ROOT/src/" && chmod +x "$TARBALL_ROOT/src/$(basename "$f")" +done +# Copy test JSON files +if [ -d "$REPO_ROOT/src/tests" ]; then + cp -r "$REPO_ROOT/src/tests" "$TARBALL_ROOT/src/tests" +fi + +# 6. Copy bootstrap script +cp "$REPO_ROOT/scripts/bootstrap.sh" "$TARBALL_ROOT/bootstrap.sh" +chmod +x "$TARBALL_ROOT/bootstrap.sh" + +# 7. Write version file for cache invalidation +echo "$VERSION" > "$TARBALL_ROOT/.version" + +# 8. Write MANIFEST.json +# Use sha256sum on Linux, shasum on macOS +if command -v sha256sum >/dev/null 2>&1; then + SHA_CMD="sha256sum" +else + SHA_CMD="shasum -a 256" +fi +CONTENTS_SHA=$(find "$TARBALL_ROOT" -type f -exec $SHA_CMD {} \; | sort | $SHA_CMD | cut -d' ' -f1) +cat > "$TARBALL_ROOT/MANIFEST.json" < Date: Fri, 6 Mar 2026 18:06:47 -0800 Subject: [PATCH 2/2] fix: rewrite venv shebangs to portable paths in tarball build uv pip install stamps absolute build-time paths in console_scripts shebangs (e.g. /tmp/flash-worker-build/.../python), making binaries like uvicorn unfindable at runtime in the extracted tarball. Rewrite any shebang referencing the build dir to #!/usr/bin/env python3 after dependency installation. --- scripts/build-tarball.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/build-tarball.sh b/scripts/build-tarball.sh index 6e2b1a0..c35ac22 100755 --- a/scripts/build-tarball.sh +++ b/scripts/build-tarball.sh @@ -92,6 +92,14 @@ uv export --format requirements-txt --no-dev --no-hashes > "$BUILD_DIR/requireme --python "$TARBALL_ROOT/venv/bin/python" \ -r "$BUILD_DIR/requirements.txt" +# 4b. Fix shebangs in venv/bin scripts to use portable /usr/bin/env path +# (uv pip install stamps absolute build-time paths in console_scripts) +for script in "$TARBALL_ROOT/venv/bin/"*; do + [ -f "$script" ] || continue + head -1 "$script" | grep -q "^#!.*$BUILD_DIR" || continue + sed -i "1s|^#!.*|#!/usr/bin/env python3|" "$script" +done + # 5. Copy source files echo "Copying source files..." cp -r "$REPO_ROOT/src/"*.py "$TARBALL_ROOT/src/" 2>/dev/null || true