diff --git a/.devops/cpu.Dockerfile b/.devops/cpu.Dockerfile index a1a458d0..e22959e1 100644 --- a/.devops/cpu.Dockerfile +++ b/.devops/cpu.Dockerfile @@ -3,9 +3,7 @@ # Usage: # docker build -f .devops/cpu.Dockerfile -t local/audiocpp:full-cpu . -# ============================================================ -# [BUILD] Compile all release binaries -# ============================================================ +# ── BUILD: Compile all release binaries ─────────────────────────────────────── ARG UBUNTU_VERSION=24.04 ARG BUILD_DATE=N/A ARG APP_VERSION=N/A @@ -28,12 +26,13 @@ ENV CC=gcc-${GCC_VERSION} CXX=g++-${GCC_VERSION} WORKDIR /app COPY . . -# Configure +# Configure and build RUN cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DENGINE_ENABLE_CUDA=OFF \ -DENGINE_ENABLE_VULKAN=OFF \ -DENGINE_ENABLE_OPENMP=ON \ + -DENGINE_ENABLE_NATIVE_CPU=OFF \ -DENGINE_BUILD_EXAMPLES=OFF \ -DENGINE_BUILD_TESTS=OFF \ -DENGINE_BUILD_WARMBENCH=OFF && \ @@ -50,16 +49,14 @@ RUN mkdir -p /app/full && \ cp .devops/entrypoint.sh /app/full/entrypoint.sh && \ chmod +x /app/full/entrypoint.sh -# ============================================================ -# [BASE] Shared runtime (OS + common libs) -# ============================================================ +# ── BASE: Shared runtime (OS + common libs) ─────────────────────────────────── FROM docker.io/ubuntu:$UBUNTU_VERSION AS base ARG BUILD_DATE=N/A ARG APP_VERSION=N/A ARG APP_REVISION=N/A -ARG IMAGE_URL=https://github.com/0xShug0/audio.cpp -ARG IMAGE_SOURCE=https://github.com/0xShug0/audio.cpp +ARG IMAGE_URL=N/A +ARG IMAGE_SOURCE=N/A LABEL org.opencontainers.image.created=$BUILD_DATE \ org.opencontainers.image.version=$APP_VERSION \ @@ -81,16 +78,11 @@ RUN apt-get update && \ WORKDIR /app -# ============================================================ -# [FULL] All binaries + entrypoint.sh multiplexer -# ============================================================ +# ── FULL: All binaries + entrypoint.sh multiplexer ──────────────────────────── FROM base AS full COPY --from=build /app/full /app USER ubuntu -HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=15s \ - CMD curl -f http://localhost:8080/health || exit 1 - ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/.devops/cuda.Dockerfile b/.devops/cuda.Dockerfile index e4aa52b8..eb3112f4 100644 --- a/.devops/cuda.Dockerfile +++ b/.devops/cuda.Dockerfile @@ -3,11 +3,9 @@ # Usage: # docker build -f .devops/cuda.Dockerfile -t local/audiocpp:full-cuda . -# ============================================================ -# [BUILD] Compile all release binaries with CUDA -# ============================================================ +# ── BUILD: Compile all release binaries with CUDA ───────────────────────────── ARG UBUNTU_VERSION=24.04 -ARG CUDA_VERSION=12.9.0 +ARG CUDA_VERSION=12.9.2 ARG BUILD_DATE=N/A ARG APP_VERSION=N/A ARG APP_REVISION=N/A @@ -32,13 +30,14 @@ ENV CC=gcc-${GCC_VERSION} CXX=g++-${GCC_VERSION} CUDAHOSTCXX=g++-${GCC_VERSION} WORKDIR /app COPY . . -# Configure +# Configure and build RUN cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DENGINE_ENABLE_CUDA=ON \ -DENGINE_ENABLE_CUDA_GRAPHS=ON \ -DENGINE_ENABLE_VULKAN=OFF \ -DENGINE_ENABLE_OPENMP=ON \ + -DENGINE_ENABLE_NATIVE_CPU=OFF \ -DENGINE_BUILD_EXAMPLES=OFF \ -DENGINE_BUILD_TESTS=OFF \ -DENGINE_BUILD_WARMBENCH=OFF && \ @@ -55,16 +54,14 @@ RUN mkdir -p /app/full && \ cp .devops/entrypoint.sh /app/full/entrypoint.sh && \ chmod +x /app/full/entrypoint.sh -# ============================================================ -# [BASE] Shared runtime (NVIDIA CUDA + common libs) -# ============================================================ +# ── BASE: Shared runtime (NVIDIA CUDA + common libs) ────────────────────────── FROM ${BASE_CUDA_RUN_CONTAINER} AS base ARG BUILD_DATE=N/A ARG APP_VERSION=N/A ARG APP_REVISION=N/A -ARG IMAGE_URL=https://github.com/0xShug0/audio.cpp -ARG IMAGE_SOURCE=https://github.com/0xShug0/audio.cpp +ARG IMAGE_URL=N/A +ARG IMAGE_SOURCE=N/A LABEL org.opencontainers.image.created=$BUILD_DATE \ org.opencontainers.image.version=$APP_VERSION \ @@ -86,17 +83,11 @@ RUN apt-get update && \ WORKDIR /app -# ============================================================ -# [FULL] All binaries + entrypoint.sh multiplexer -# ============================================================ +# ── FULL: All binaries + entrypoint.sh multiplexer ──────────────────────────── FROM base AS full COPY --from=build /app/full /app USER ubuntu -HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=15s \ - CMD curl -f http://localhost:8080/health || exit 1 - ENTRYPOINT ["/app/entrypoint.sh"] - diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..c6c66e71 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,320 @@ +# audio.cpp — Build and publish Docker images +# +# Produces multi-arch (amd64, arm64) images on ghcr.io: +# full-cpu (latest, mutable) +# full-cpu-YYYYMMDD-HHHHHHH (pinned to date + short SHA, immutable) +# full-cuda12 (latest, mutable) +# full-cuda12-YYYYMMDD-HHHHHHH (pinned to date + short SHA, immutable) +# full-cuda13 (latest, mutable) +# full-cuda13-YYYYMMDD-HHHHHHH (pinned to date + short SHA, immutable) + +name: Build and publish Docker images + +on: + # Runs daily at 03:21 UTC + schedule: + - cron: '21 3 * * *' + # or when triggered manually + workflow_dispatch: + inputs: + force_build: + description: 'Force build even if no new commits' + type: boolean + default: true + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} + cancel-in-progress: true + +permissions: + packages: write + +env: + IMAGE_REPO: ${{ github.repository }} + +jobs: + # ── Check for new commits ─────────────────────────────────────────────────── + check-commits: + name: Check for new commits + runs-on: ubuntu-24.04 + outputs: + skip_build: ${{ steps.check.outputs.skip }} + short_sha: ${{ steps.check.outputs.short_sha }} + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Check for new commits + id: check + run: | + SHORT_SHA=$(git rev-parse --short=7 HEAD) + echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.force_build }}" == 'true' ]]; then + echo "Manual trigger (force_build=true) — building" + echo "skip=false" >> "$GITHUB_OUTPUT" + else + LAST_TAG=$(git tag -l 'last-docker-build' | head -1) + if [[ -n "$LAST_TAG" ]]; then + LAST_SHA=$(git rev-list -1 "$LAST_TAG" 2>/dev/null || echo "") + HEAD_SHA=$(git rev-parse HEAD) + if [[ "$LAST_SHA" == "$HEAD_SHA" ]]; then + echo "No new commits since last daily build — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "New commits found — building" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + else + echo "No previous build tag found — building" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + fi + + # ── Shared metadata (date computed once for all jobs) ─────────────────────── + metadata: + name: Generate metadata + needs: check-commits + if: ${{ needs.check-commits.outputs.skip_build != 'true' }} + runs-on: ubuntu-24.04 + outputs: + build_date: ${{ steps.date.outputs.date }} + date_tag: ${{ steps.date.outputs.tag }} + steps: + - name: Get build date + id: date + run: | + echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" + echo "tag=$(date -u +'%Y%m%d')" >> "$GITHUB_OUTPUT" + + # ── Build each arch, push by digest ───────────────────────────────────────── + build: + name: Build (${{ matrix.tag }}, ${{ matrix.platforms }}) + needs: [check-commits, metadata] + if: ${{ needs.check-commits.outputs.skip_build != 'true' }} + runs-on: ${{ matrix.runs_on }} + strategy: + fail-fast: false + matrix: + include: + # ── CPU ── + - tag: cpu + dockerfile: .devops/cpu.Dockerfile + platforms: linux/amd64 + arch: amd64 + runs_on: ubuntu-24.04 + enabled: true + - tag: cpu + dockerfile: .devops/cpu.Dockerfile + platforms: linux/arm64 + arch: arm64 + runs_on: ubuntu-24.04-arm + enabled: true + # ── CUDA 12 ── + - tag: cuda12 + dockerfile: .devops/cuda.Dockerfile + platforms: linux/amd64 + arch: amd64 + runs_on: ubuntu-24.04 + free_disk: true + cuda_version: "12.9.2" + enabled: true + - tag: cuda12 + dockerfile: .devops/cuda.Dockerfile + platforms: linux/arm64 + arch: arm64 + runs_on: ubuntu-24.04-arm + free_disk: true + cuda_version: "12.9.2" + enabled: true + # ── CUDA 13 ── + - tag: cuda13 + dockerfile: .devops/cuda.Dockerfile + platforms: linux/amd64 + arch: amd64 + runs_on: ubuntu-24.04 + free_disk: true + cuda_version: "13.3.0" + enabled: true + - tag: cuda13 + dockerfile: .devops/cuda.Dockerfile + platforms: linux/arm64 + arch: arm64 + runs_on: ubuntu-24.04-arm + free_disk: true + cuda_version: "13.3.0" + enabled: true + steps: + - name: Skip when not enabled + if: ${{ matrix.enabled != true }} + run: echo "Skipping ${{ matrix.tag }} build" + + - name: Checkout + if: ${{ matrix.enabled == true }} + uses: actions/checkout@v5 + + - name: Set up Docker Buildx + if: ${{ matrix.enabled == true }} + uses: docker/setup-buildx-action@v4 + + - name: Log in to GHCR + if: ${{ matrix.enabled == true }} + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Free disk space + if: ${{ matrix.enabled == true && matrix.free_disk }} + uses: ggml-org/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Build and push + id: push + if: ${{ matrix.enabled == true }} + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ matrix.dockerfile }} + target: full + platforms: ${{ matrix.platforms }} + provenance: false + outputs: type=image,name=ghcr.io/${{ env.IMAGE_REPO }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true + build-args: | + BUILD_DATE=${{ needs.metadata.outputs.build_date }} + APP_VERSION=${{ github.sha }} + APP_REVISION=${{ github.sha }} + IMAGE_URL=${{ github.server_url }}/${{ github.repository }} + IMAGE_SOURCE=${{ github.server_url }}/${{ github.repository }} + ${{ matrix.cuda_version && format('CUDA_VERSION={0}', matrix.cuda_version) || '' }} + cache-from: type=registry,ref=ghcr.io/${{ env.IMAGE_REPO }}:cache-${{ matrix.arch }}-${{ matrix.tag }} + cache-to: type=registry,ref=ghcr.io/${{ env.IMAGE_REPO }}:cache-${{ matrix.arch }}-${{ matrix.tag }},mode=max + + - name: Upload digest + if: ${{ matrix.enabled == true }} + shell: bash + run: | + set -euo pipefail + mkdir -p /tmp/digests + digest="${{ steps.push.outputs.digest }}" + + if [[ "$digest" != sha256:* ]]; then + echo "::error::Invalid digest format: $digest" + exit 1 + fi + + printf '%s\n' "$digest" > "/tmp/digests/${{ matrix.tag }}-${{ matrix.arch }}.txt" + + - name: Save digest artifact + if: ${{ matrix.enabled == true }} + uses: actions/upload-artifact@v7 + with: + name: digest-${{ matrix.arch }}-${{ matrix.tag }} + path: /tmp/digests/ + if-no-files-found: error + + # ── Merge per-arch digests into multi-arch manifests ──────────────────────── + merge: + name: Merge (${{ matrix.tag }}) + needs: [check-commits, metadata, build] + # Merge will only run if there was actually a new commit to act on and + # if all build jobs succeeded. As soon as one build fails, we won't + # produce any merged images. + if: ${{ needs.check-commits.outputs.skip_build != 'true' && needs.build.result == 'success' }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + tag: [cpu, cuda12, cuda13] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Download digests + uses: actions/download-artifact@v8 + with: + pattern: digest-* + path: /tmp/digests + merge-multiple: true + + - name: Create manifest + shell: bash + run: | + set -euo pipefail + + repo="ghcr.io/${{ env.IMAGE_REPO }}" + date_tag="${{ needs.metadata.outputs.date_tag }}" + short_sha="${{ needs.check-commits.outputs.short_sha }}" + tag="${{ matrix.tag }}" + build_date="${{ needs.metadata.outputs.build_date }}" + image_url="${{ github.server_url }}/${{ github.repository }}" + + mutable="full-${tag}" + immutable="full-${tag}-${date_tag}-${short_sha}" + + # Collect per-arch digests. + # Because this job is gated on needs.build.result == 'success', a missing + # digest can only mean the variant was intentionally disabled (enabled: false). + expected_arches=(amd64 arm64) + refs=() + for arch in "${expected_arches[@]}"; do + file="/tmp/digests/${tag}-${arch}.txt" + [ -f "$file" ] || continue + refs+=("${repo}@$(cat "$file")") + done + + echo "${tag}: ${#refs[@]}/${#expected_arches[@]} digests merged" + + if [ ${#refs[@]} -eq 0 ]; then + echo " → skipping (all variants disabled)" + exit 0 + fi + + # OCI annotations for the merged manifest + annotations=( + --annotation "index:org.opencontainers.image.created=${build_date}" + --annotation "index:org.opencontainers.image.revision=${{ github.sha }}" + --annotation "index:org.opencontainers.image.url=${image_url}" + --annotation "index:org.opencontainers.image.source=${image_url}" + ) + + echo " → ${mutable}" + docker buildx imagetools create "${annotations[@]}" --tag "${repo}:${mutable}" "${refs[@]}" + + echo " → ${immutable}" + docker buildx imagetools create "${annotations[@]}" --tag "${repo}:${immutable}" "${refs[@]}" + + # ── Update last-docker-build tag ──────────────────────────────────────────── + update-tag: + name: Update build tag + needs: [check-commits, merge] + if: ${{ needs.check-commits.outputs.skip_build != 'true' }} + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Update last-docker-build tag + run: | + set -euo pipefail + git tag -f last-docker-build HEAD + git push origin last-docker-build -f diff --git a/.gitignore b/.gitignore index 06fc54e9..0d202bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /build*/ /cmake-build*/ ._* +.idea/ .vs/ .vscode/ compile_commands.json