diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml deleted file mode 100644 index d9aa204a..00000000 --- a/.github/workflows/build-ffmpeg.yml +++ /dev/null @@ -1,448 +0,0 @@ -# FFmpeg Static Binary Build Workflow -# Based on GitHub Actions best practices for multi-platform native binary builds -# -# Key patterns applied: -# - Matrix strategy for parallel platform builds -# - Native runners (not QEMU emulation) for macOS arm64/x64 -# - Docker buildx with GHA cache for Linux builds -# - Tar archives to preserve file permissions in artifacts -# - OIDC for npm publishing (no long-lived tokens) -# - Minimal GITHUB_TOKEN permissions per job -# - Pinned action versions for security -# -# Runner/Action versions updated: January 2026 -# - macos-13 deprecated Dec 2025, using macos-15-intel for x64 -# - macos-15 for ARM64 (macos-latest) -# - ubuntu-latest = Ubuntu 24.04 -# - All actions on Node.js 24 (v5/v6 releases) - -name: Build FFmpeg Static Binaries - -on: - push: - tags: - - 'v*' - - 'deps-*' # Also trigger on deps releases (e.g., deps-v1, deps-v2) - workflow_dispatch: - inputs: - ffmpeg_version: - description: 'FFmpeg version/branch to build' - required: false - default: 'master' - skip_publish: - description: 'Skip npm publish step' - required: false - type: boolean - default: false - deps_version: - description: 'Create deps release with this version (e.g., v1, v2). Leave empty to skip deps release.' - required: false - type: string - default: '' - -# Default minimal permissions - override per job -permissions: - contents: read - -env: - # Codec versions - update these for reproducible builds (Jan 2026) - X264_VERSION: 'stable' - X265_VERSION: '3.6' - LIBVPX_VERSION: 'v1.15.2' - LIBAOM_VERSION: 'v3.12.1' - OPUS_VERSION: '1.5.2' - LAME_VERSION: '3.100' - FFMPEG_VERSION: 'n8.0' - # SHA256 checksums for tarball downloads (hermetic build verification) - # These MUST be updated when changing versions above - # Sources: https://opus-codec.org/downloads/, https://sourceforge.net/projects/lame/ - # NASM uses GitHub source archive (more reliable than nasm.us) - NASM_VERSION: '2.16.03' - NASM_SHA256: 'e7f77b8247de72f3c2a2c57a9c72b2a0c847ec5e99ce6e68c1e225fa2e37c04c' # GitHub source archive - OPUS_SHA256: '65c1d2f78b9f2fb20082c38cbe47c951ad5839345876e46941612ee87f9a7ce1' - LAME_SHA256: 'ddfe36cab873794038ae2c1210557ad34857a4b6bdc515785d1da9e175b1da1e' - # npm scope for platform packages - NPM_SCOPE: '@pproenca/ffmpeg' - # Bump this to invalidate all caches - CACHE_VERSION: '11' - # macOS deployment target - must match binding.gyp MACOSX_DEPLOYMENT_TARGET - # This ensures ABI compatibility between FFmpeg libs and the native addon. - # Using 11.0 (Big Sur) as minimum for widest compatibility. - MACOS_DEPLOYMENT_TARGET: '11.0' - -jobs: - # ============================================================================ - # Linux x64 Build - Docker-based for musl static linking - # ============================================================================ - build-linux-x64: - name: Build Linux x64 (musl) - runs-on: ubuntu-24.04 # Explicit version for reproducibility - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: '22' - - run: npm install --ignore-scripts - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build FFmpeg in Alpine container - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile.linux-x64 - push: false - load: true - tags: ffmpeg-builder:linux-x64 - cache-from: type=gha,scope=linux-x64-v${{ env.CACHE_VERSION }} - cache-to: type=gha,mode=max,scope=linux-x64-v${{ env.CACHE_VERSION }} - - - name: Extract binaries and dev files from container - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts extract-docker --image ffmpeg-builder:linux-x64 --container extract --platform linux-x64 --artifacts artifacts --ldd musl - - # Tar to preserve permissions (chmod +x is lost in artifact upload) - - name: Package artifacts - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts package-artifacts --platform linux-x64 --artifacts artifacts - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: ffmpeg-linux-x64 - path: artifacts/linux-x64.tar - retention-days: 7 - compression-level: 0 # Already compressed, faster upload - - # ============================================================================ - # Linux x64 Build (glibc) - For native addon linking on Ubuntu/Debian - # ============================================================================ - # The musl build above produces fully static binaries but its static libraries - # cannot be linked into shared objects on glibc systems. This build produces - # static libraries compatible with glibc for use by build-prebuilds.yml. - # ============================================================================ - build-linux-x64-glibc: - name: Build Linux x64 (glibc) - runs-on: ubuntu-24.04 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: '22' - - run: npm install --ignore-scripts - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build FFmpeg in Ubuntu container - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile.linux-x64-glibc - push: false - load: true - tags: ffmpeg-builder:linux-x64-glibc - cache-from: type=gha,scope=linux-x64-glibc-v${{ env.CACHE_VERSION }} - cache-to: type=gha,mode=max,scope=linux-x64-glibc-v${{ env.CACHE_VERSION }} - - - name: Extract binaries and dev files from container - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts extract-docker --image ffmpeg-builder:linux-x64-glibc --container extract-glibc --platform linux-x64-glibc --artifacts artifacts --ldd glibc - - - name: Package artifacts - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts package-artifacts --platform linux-x64-glibc --artifacts artifacts - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: ffmpeg-linux-x64-glibc - path: artifacts/linux-x64-glibc.tar - retention-days: 7 - compression-level: 0 - - # ============================================================================ - # macOS Builds - Native runners for both architectures - # ============================================================================ - build-macos: - name: Build macOS ${{ matrix.arch }} - runs-on: ${{ matrix.runner }} - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - include: - # macos-13 deprecated Dec 2025 - use macos-15-intel for x64 - # macos-15-intel available until Aug 2027 (last Intel runner) - - runner: macos-15-intel - arch: x64 - target: x86_64 - # macos-15 is ARM64 (same as macos-latest since Aug 2025) - - runner: macos-15 - arch: arm64 - target: arm64 - - env: - TARGET: ${{ github.workspace }}/ffmpeg_build - ARCH: ${{ matrix.target }} - MACOSX_DEPLOYMENT_TARGET: '11.0' - - steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: '22' - - run: npm install --ignore-scripts - - - name: Install build dependencies - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts install-macos-deps - - - name: Cache compiled libraries - uses: actions/cache@v5 - id: cache-libs - with: - path: | - ${{ github.workspace }}/ffmpeg_build - ${{ github.workspace }}/ffmpeg_sources - # Include ALL library versions to ensure cache invalidates when any dependency changes - key: macos-${{ matrix.arch }}-libs-${{ env.X264_VERSION }}-${{ env.X265_VERSION }}-${{ env.LIBVPX_VERSION }}-${{ env.LIBAOM_VERSION }}-${{ env.OPUS_VERSION }}-${{ env.LAME_VERSION }}-${{ env.MACOS_DEPLOYMENT_TARGET }}-v${{ env.CACHE_VERSION }} - restore-keys: | - macos-${{ matrix.arch }}-libs- - - - name: Build codec libraries - if: steps.cache-libs.outputs.cache-hit != 'true' - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts build-macos-codecs - - - name: Build FFmpeg - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts build-macos-ffmpeg - - - name: Verify macOS deployment targets - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts verify-macos-abi - - - name: Verify and strip binaries - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts verify-strip - - - name: Package artifacts - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts package-macos --target "$TARGET" --arch "${{ matrix.arch }}" --artifacts artifacts - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: ffmpeg-darwin-${{ matrix.arch }} - path: artifacts/darwin-${{ matrix.arch }}.tar - retention-days: 7 - compression-level: 0 - - # ============================================================================ - # Package npm modules - # ============================================================================ - package-npm: - name: Package npm modules - runs-on: ubuntu-24.04 - needs: [build-linux-x64, build-macos] - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '22' - registry-url: 'https://registry.npmjs.org' - - run: npm install --ignore-scripts - - - name: Download all artifacts - uses: actions/download-artifact@v6 - with: - pattern: ffmpeg-* - path: artifacts - - - name: Extract and organize binaries - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts extract-package-npm --artifacts artifacts --packages packages - - - name: Create main package - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts create-main-package --packages packages - - - name: Upload packaged artifacts - uses: actions/upload-artifact@v6 - with: - name: npm-packages - path: packages/ - retention-days: 7 - - # ============================================================================ - # Publish to npm (only on tag push) - # ============================================================================ - publish-npm: - name: Publish to npm - runs-on: ubuntu-24.04 - needs: [package-npm] - if: startsWith(github.ref, 'refs/tags/v') && !inputs.skip_publish - permissions: - contents: read - id-token: write # Required for npm provenance - - environment: - name: npm - url: https://www.npmjs.com/package/${{ env.NPM_SCOPE }} - - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '22' - registry-url: 'https://registry.npmjs.org' - - run: npm install --ignore-scripts - - - name: Download npm packages - uses: actions/download-artifact@v6 - with: - name: npm-packages - path: packages - - - name: Publish platform packages - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts publish-npm --packages packages - - # ============================================================================ - # Create GitHub Release - # ============================================================================ - release: - name: Create GitHub Release - runs-on: ubuntu-24.04 - needs: [build-linux-x64, build-macos] - if: startsWith(github.ref, 'refs/tags/v') - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: '22' - - run: npm install --ignore-scripts - - name: Download all artifacts - uses: actions/download-artifact@v6 - with: - pattern: ffmpeg-* - path: artifacts - - - name: Prepare release assets - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts prepare-release-assets --artifacts artifacts --release release - - - name: Create Release - uses: softprops/action-gh-release@v2.2.1 - with: - files: release/* - generate_release_notes: true - body: | - ## FFmpeg Static Binaries - - This release includes statically linked FFmpeg binaries for: - - Linux x64 (musl libc, fully static) - - macOS x64 (Intel) - - macOS arm64 (Apple Silicon) - - ### Included Codecs - - H.264 (libx264) - GPL - - H.265/HEVC (libx265) - GPL - - VP8/VP9 (libvpx) - BSD - - AV1 (libaom) - BSD - - Opus (libopus) - BSD - - MP3 (libmp3lame) - LGPL - - ### npm Installation - ```bash - npm install ${{ env.NPM_SCOPE }} - ``` - - ### License - These binaries are licensed under GPL v2+ due to the inclusion of libx264 and libx265. - - # ============================================================================ - # Create Dependencies Release (for CI/build-prebuilds consumption) - # ============================================================================ - # This job creates the deps-vN release that ci.yml and build-prebuilds.yml - # download FFmpeg binaries from. Files are named without version prefix: - # ffmpeg-linux-x64.tar.gz (not ffmpeg-v1.0.0-linux-x64.tar.gz) - # ============================================================================ - release-deps: - name: Create Dependencies Release - runs-on: ubuntu-24.04 - needs: [build-linux-x64, build-linux-x64-glibc, build-macos] - # Run if: manually triggered with deps_version input OR tag push matches deps-* - if: inputs.deps_version != '' || startsWith(github.ref, 'refs/tags/deps-') - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: '22' - - run: npm install --ignore-scripts - - name: Download all artifacts - uses: actions/download-artifact@v6 - with: - pattern: ffmpeg-* - path: artifacts - - - name: Determine deps version - id: version - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts resolve-deps-version --input "${{ inputs.deps_version }}" --ref "${{ github.ref_name }}" - - - name: Prepare deps release assets - run: npx tsx scripts/ci/build-ffmpeg-workflow.ts prepare-deps-assets --artifacts artifacts --release release - - - name: Create Dependencies Release - uses: softprops/action-gh-release@v2.2.1 - with: - tag_name: deps-${{ steps.version.outputs.version }} - name: FFmpeg Dependencies ${{ steps.version.outputs.version }} - files: release/* - body: | - ## FFmpeg Static Libraries for CI - - This release contains FFmpeg static binaries used by the CI and build pipelines. - **Do not use directly** - these are consumed by `ci.yml` and `build-prebuilds.yml`. - - ### Files - - `ffmpeg-linux-x64.tar.gz` - Linux x64 (musl, fully static binaries) - - `ffmpeg-linux-x64-glibc.tar.gz` - Linux x64 (glibc, for native addon linking) - - `ffmpeg-darwin-x64.tar.gz` - macOS Intel - - `ffmpeg-darwin-arm64.tar.gz` - macOS Apple Silicon - - **Note:** `build-prebuilds.yml` uses `linux-x64-glibc` for native addon builds because - musl-compiled static libraries cannot be linked into shared objects on glibc systems. - - ### Codec Versions - - FFmpeg: ${{ env.FFMPEG_VERSION }} - - x264: ${{ env.X264_VERSION }} - - x265: ${{ env.X265_VERSION }} - - libvpx: ${{ env.LIBVPX_VERSION }} - - libaom: ${{ env.LIBAOM_VERSION }} - - opus: ${{ env.OPUS_VERSION }} - - lame: ${{ env.LAME_VERSION }} - - ### Usage - Update `DEPS_VERSION` in `ci.yml` and `build-prebuilds.yml` to use this release: - ```yaml - env: - DEPS_VERSION: ${{ steps.version.outputs.version }} - ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9528f3e8..7180ab02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,7 +198,47 @@ jobs: if: runner.os == 'Linux' run: npx tsx scripts/ci/ci-workflow.ts install-build-tools --os linux - - name: Download FFmpeg from Release + # Try npm packages first (faster, no GitHub API rate limits) + # Falls back to GitHub releases if package doesn't exist yet + - name: Resolve FFmpeg source + id: ffmpeg-source + run: | + # Determine npm package name based on platform + if [[ "${{ matrix.platform }}" == "linux-x64-musl" ]]; then + NPM_PKG="@pproenca/ffmpeg-dev-linux-x64-musl" + else + NPM_PKG="@pproenca/ffmpeg-dev-${{ matrix.platform }}" + fi + + echo "npm_package=$NPM_PKG" >> "$GITHUB_OUTPUT" + + # Check if npm package exists + if npm view "$NPM_PKG" version > /dev/null 2>&1; then + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "✓ FFmpeg available from npm: $NPM_PKG" + else + echo "source=github-release" >> "$GITHUB_OUTPUT" + echo "⚠ FFmpeg npm package not found, using GitHub releases" + fi + + - name: Install FFmpeg from npm + if: steps.ffmpeg-source.outputs.source == 'npm' + run: | + echo "Installing ${{ steps.ffmpeg-source.outputs.npm_package }}..." + npm install --no-save ${{ steps.ffmpeg-source.outputs.npm_package }} + + # Set FFMPEG_ROOT for gyp/ffmpeg-paths.js + FFMPEG_ROOT="$(npm root)/${{ steps.ffmpeg-source.outputs.npm_package }}" + echo "FFMPEG_ROOT=$FFMPEG_ROOT" >> "$GITHUB_ENV" + + echo "✓ FFmpeg installed from npm" + echo "FFMPEG_ROOT=$FFMPEG_ROOT" + + # Verify installation + ls -lh "$FFMPEG_ROOT/lib/" | head -5 + + - name: Download FFmpeg from GitHub Release (fallback) + if: steps.ffmpeg-source.outputs.source == 'github-release' uses: dsaltares/fetch-gh-release-asset@aa2ab1243d6e0d5b405b973c89fa4d06a2d0fff7 # v1.1.2 with: repo: ${{ github.repository }} @@ -211,7 +251,8 @@ jobs: target: ffmpeg-${{ matrix.platform }}.tar.gz token: ${{ secrets.GITHUB_TOKEN }} - - name: Extract FFmpeg and Set Environment + - name: Extract FFmpeg from GitHub Release (fallback) + if: steps.ffmpeg-source.outputs.source == 'github-release' run: npx tsx scripts/ci/ci-workflow.ts extract-ffmpeg --archive "ffmpeg-${{ matrix.platform }}.tar.gz" --out ffmpeg-install - name: Build with prebuildify diff --git a/CLAUDE.md b/CLAUDE.md index a4f3ebd4..689e57ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,5 +190,5 @@ Test GitHub Actions locally: ```bash act -l # List jobs -act push -j build-linux-x64 --container-architecture linux/amd64 -W .github/workflows/build-ffmpeg.yml +act push -j build-native --container-architecture linux/amd64 -W .github/workflows/ci.yml ``` diff --git a/docker/Dockerfile.linux-x64 b/docker/Dockerfile.linux-x64 deleted file mode 100644 index 372d0d76..00000000 --- a/docker/Dockerfile.linux-x64 +++ /dev/null @@ -1,272 +0,0 @@ -# FFmpeg Static Build Dockerfile for Linux x64 -# Uses Alpine Linux with musl libc for truly static binaries -# -# Build: docker build -f Dockerfile.linux-x64 -t ffmpeg-builder:linux-x64 . -# Extract: docker create --name tmp ffmpeg-builder:linux-x64 && docker cp tmp:/build/bin . -# -# Library versions updated: January 2026 - -FROM alpine:3.21 AS builder - -# Build arguments for version pinning (updated Jan 2026) -ARG X264_VERSION=stable -ARG X265_VERSION=3.6 -ARG LIBVPX_VERSION=v1.15.2 -ARG LIBAOM_VERSION=v3.12.1 -ARG OPUS_VERSION=1.5.2 -ARG LAME_VERSION=3.100 -ARG FFMPEG_VERSION=n8.0 - -# Install build dependencies -RUN apk add --no-cache \ - build-base \ - git \ - cmake \ - nasm \ - yasm \ - pkgconfig \ - autoconf \ - automake \ - libtool \ - linux-headers \ - perl \ - diffutils \ - coreutils \ - bash \ - wget \ - curl \ - zlib-dev \ - zlib-static - -# Set up build environment -ENV PREFIX=/build -ENV PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig -ENV PATH=$PREFIX/bin:$PATH -ENV MAKEFLAGS="-j$(nproc)" - -WORKDIR /src - -# ============================================================================ -# Build x264 (GPL - required for H.264 encoding) -# ============================================================================ -RUN git clone --depth 1 --branch ${X264_VERSION} https://code.videolan.org/videolan/x264.git && \ - cd x264 && \ - ./configure \ - --prefix=$PREFIX \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --disable-cli \ - --disable-opencl && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf x264 - -# ============================================================================ -# Build x265 (GPL - required for H.265/HEVC encoding) -# Note: x265 cmake doesn't always install .pc file, so we create it manually -# ============================================================================ -RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git.git && \ - mkdir -p x265_git/build/linux && \ - cd x265_git/build/linux && \ - cmake -G "Unix Makefiles" \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DLIB_INSTALL_DIR=$PREFIX/lib \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DENABLE_SHARED=OFF \ - -DENABLE_CLI=OFF \ - -DSTATIC_LINK_CRT=ON \ - -DHIGH_BIT_DEPTH=ON \ - ../../source && \ - make -j$(nproc) && \ - make install && \ - mkdir -p $PREFIX/lib/pkgconfig && \ - printf '%s\n' \ - "prefix=/build" \ - "exec_prefix=\${prefix}" \ - "libdir=\${prefix}/lib" \ - "includedir=\${prefix}/include" \ - "" \ - "Name: x265" \ - "Description: H.265/HEVC video encoder" \ - "Version: 3.6" \ - "Libs: -L\${libdir} -lx265" \ - "Libs.private: -lstdc++ -lm -lpthread" \ - "Cflags: -I\${includedir}" \ - > $PREFIX/lib/pkgconfig/x265.pc && \ - cd /src && rm -rf x265_git - -# ============================================================================ -# Build libvpx (BSD - VP8/VP9 codec) -# ============================================================================ -RUN git clone --depth 1 --branch ${LIBVPX_VERSION} https://chromium.googlesource.com/webm/libvpx.git && \ - cd libvpx && \ - ./configure \ - --prefix=$PREFIX \ - --disable-examples \ - --disable-unit-tests \ - --disable-docs \ - --enable-vp9-highbitdepth \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --as=yasm && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libvpx - -# ============================================================================ -# Build libaom (BSD - AV1 codec) -# ============================================================================ -RUN git clone --depth 1 --branch ${LIBAOM_VERSION} https://aomedia.googlesource.com/aom && \ - mkdir aom_build && \ - cd aom_build && \ - cmake -G "Unix Makefiles" \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DENABLE_NASM=ON \ - -DENABLE_DOCS=OFF \ - -DENABLE_EXAMPLES=OFF \ - -DENABLE_TESTS=OFF \ - -DCONFIG_AV1_ENCODER=1 \ - -DCONFIG_AV1_DECODER=1 \ - ../aom && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf aom aom_build - -# ============================================================================ -# Build libopus (BSD - Opus audio codec) -# ============================================================================ -RUN wget -q https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz && \ - tar xzf opus-${OPUS_VERSION}.tar.gz && \ - cd opus-${OPUS_VERSION} && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --disable-doc \ - --disable-extra-programs && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf opus-${OPUS_VERSION}* - -# ============================================================================ -# Build libmp3lame (LGPL - MP3 encoder) -# ============================================================================ -RUN wget -q "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ - tar xzf lame-${LAME_VERSION}.tar.gz && \ - cd lame-${LAME_VERSION} && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --enable-nasm \ - --disable-frontend && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf lame-${LAME_VERSION}* - -# ============================================================================ -# Build libogg + libvorbis (BSD - Vorbis audio codec) -# ============================================================================ -RUN wget -q https://ftp.osuosl.org/pub/xiph/releases/ogg/libogg-1.3.5.tar.gz && \ - tar xzf libogg-1.3.5.tar.gz && \ - cd libogg-1.3.5 && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libogg-1.3.5* - -RUN wget -q https://ftp.osuosl.org/pub/xiph/releases/vorbis/libvorbis-1.3.7.tar.gz && \ - tar xzf libvorbis-1.3.7.tar.gz && \ - cd libvorbis-1.3.7 && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --with-ogg=$PREFIX && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libvorbis-1.3.7* - -# ============================================================================ -# Build FFmpeg with all codecs (GPL due to x264/x265) -# ============================================================================ -RUN git clone --depth 1 --branch ${FFMPEG_VERSION} https://git.ffmpeg.org/ffmpeg.git && \ - cd ffmpeg && \ - ./configure \ - --prefix=$PREFIX \ - --pkg-config-flags="--static" \ - --extra-cflags="-I$PREFIX/include -static" \ - --extra-ldflags="-L$PREFIX/lib -static" \ - --extra-libs="-lpthread -lm -lstdc++" \ - --enable-gpl \ - --enable-version3 \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --disable-ffplay \ - --disable-doc \ - --disable-debug \ - --disable-network \ - --enable-libx264 \ - --enable-libx265 \ - --enable-libvpx \ - --enable-libaom \ - --enable-libopus \ - --enable-libmp3lame \ - --enable-libvorbis && \ - make -j$(nproc) && \ - make install - -# ============================================================================ -# Strip debug symbols and verify static linking -# ============================================================================ -RUN strip $PREFIX/bin/ffmpeg $PREFIX/bin/ffprobe && \ - echo "=== Verifying static binary ===" && \ - file $PREFIX/bin/ffmpeg && \ - # musl ldd says "Not a valid dynamic program" for static binaries - # glibc ldd says "not a dynamic executable" - if ldd $PREFIX/bin/ffmpeg 2>&1 | grep -qE "(not a dynamic executable|Not a valid dynamic program)"; then \ - echo "✓ Binary is fully static"; \ - else \ - echo "✗ Binary has dynamic dependencies:" && ldd $PREFIX/bin/ffmpeg && exit 1; \ - fi && \ - echo "=== Binary sizes ===" && \ - ls -lh $PREFIX/bin/ffmpeg $PREFIX/bin/ffprobe && \ - echo "=== FFmpeg version ===" && \ - $PREFIX/bin/ffmpeg -version - -# ============================================================================ -# Final stage - includes binaries AND dev files for native addon compilation -# ============================================================================ -FROM scratch AS export - -COPY --from=builder /build/bin/ffmpeg /build/bin/ffmpeg -COPY --from=builder /build/bin/ffprobe /build/bin/ffprobe - -# For extraction via docker cp, we need a runnable container -FROM alpine:3.21 AS runtime - -# Copy binaries -COPY --from=builder /build/bin/ffmpeg /build/bin/ffmpeg -COPY --from=builder /build/bin/ffprobe /build/bin/ffprobe - -# Copy development files needed for native addon compilation -# These are required by build-prebuilds.yml to link against FFmpeg -COPY --from=builder /build/lib /build/lib -COPY --from=builder /build/include /build/include - -# Verify binaries work in clean Alpine environment -RUN /build/bin/ffmpeg -version - -CMD ["/build/bin/ffmpeg", "-version"] diff --git a/docker/Dockerfile.linux-x64-glibc b/docker/Dockerfile.linux-x64-glibc deleted file mode 100644 index 01d3c664..00000000 --- a/docker/Dockerfile.linux-x64-glibc +++ /dev/null @@ -1,294 +0,0 @@ -# FFmpeg Static Build Dockerfile for Linux x64 (glibc) -# Uses Ubuntu 24.04 with glibc for native addon compatibility -# -# This builds static libraries that can be linked into shared objects (.node) -# on glibc-based systems like Ubuntu. The musl version (Dockerfile.linux-x64) -# produces fully static binaries but cannot be linked on glibc systems. -# -# Build: docker build -f Dockerfile.linux-x64-glibc -t ffmpeg-builder:linux-x64-glibc . -# Extract: docker create --name tmp ffmpeg-builder:linux-x64-glibc && docker cp tmp:/build . -# -# Library versions updated: January 2026 - -FROM ubuntu:24.04 AS builder - -# Build arguments for version pinning (updated Jan 2026) -ARG X264_VERSION=stable -ARG X265_VERSION=3.6 -ARG LIBVPX_VERSION=v1.15.2 -ARG LIBAOM_VERSION=v3.12.1 -ARG OPUS_VERSION=1.5.2 -ARG LAME_VERSION=3.100 -ARG FFMPEG_VERSION=n8.0 - -# Prevent interactive prompts during apt-get -ENV DEBIAN_FRONTEND=noninteractive - -# Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - git \ - cmake \ - nasm \ - yasm \ - pkg-config \ - autoconf \ - automake \ - libtool \ - linux-libc-dev \ - perl \ - diffutils \ - coreutils \ - bash \ - wget \ - curl \ - ca-certificates \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - -# Set up build environment -# CRITICAL: -fPIC is required for all code that will be linked into shared objects -ENV PREFIX=/build -ENV PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig -ENV PATH=$PREFIX/bin:$PATH -ENV MAKEFLAGS="-j$(nproc)" -ENV CFLAGS="-fPIC" -ENV CXXFLAGS="-fPIC" - -WORKDIR /src - -# ============================================================================ -# Build x264 (GPL - required for H.264 encoding) -# ============================================================================ -RUN git clone --depth 1 --branch ${X264_VERSION} https://code.videolan.org/videolan/x264.git && \ - cd x264 && \ - ./configure \ - --prefix=$PREFIX \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --disable-cli \ - --disable-opencl && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf x264 - -# ============================================================================ -# Build x265 (GPL - required for H.265/HEVC encoding) -# Note: x265 cmake doesn't always install .pc file, so we create it manually -# ============================================================================ -RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git.git && \ - mkdir -p x265_git/build/linux && \ - cd x265_git/build/linux && \ - cmake -G "Unix Makefiles" \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DLIB_INSTALL_DIR=$PREFIX/lib \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DCMAKE_C_FLAGS="-fPIC" \ - -DCMAKE_CXX_FLAGS="-fPIC" \ - -DENABLE_SHARED=OFF \ - -DENABLE_CLI=OFF \ - -DHIGH_BIT_DEPTH=ON \ - ../../source && \ - make -j$(nproc) && \ - make install && \ - mkdir -p $PREFIX/lib/pkgconfig && \ - printf '%s\n' \ - "prefix=/build" \ - "exec_prefix=\${prefix}" \ - "libdir=\${prefix}/lib" \ - "includedir=\${prefix}/include" \ - "" \ - "Name: x265" \ - "Description: H.265/HEVC video encoder" \ - "Version: 3.6" \ - "Libs: -L\${libdir} -lx265" \ - "Libs.private: -lstdc++ -lm -lpthread" \ - "Cflags: -I\${includedir}" \ - > $PREFIX/lib/pkgconfig/x265.pc && \ - cd /src && rm -rf x265_git - -# ============================================================================ -# Build libvpx (BSD - VP8/VP9 codec) -# ============================================================================ -RUN git clone --depth 1 --branch ${LIBVPX_VERSION} https://chromium.googlesource.com/webm/libvpx.git && \ - cd libvpx && \ - ./configure \ - --prefix=$PREFIX \ - --disable-examples \ - --disable-unit-tests \ - --disable-docs \ - --enable-vp9-highbitdepth \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --as=yasm && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libvpx - -# ============================================================================ -# Build libaom (BSD - AV1 codec) -# ============================================================================ -RUN git clone --depth 1 --branch ${LIBAOM_VERSION} https://aomedia.googlesource.com/aom && \ - mkdir aom_build && \ - cd aom_build && \ - cmake -G "Unix Makefiles" \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DCMAKE_C_FLAGS="-fPIC" \ - -DCMAKE_CXX_FLAGS="-fPIC" \ - -DBUILD_SHARED_LIBS=OFF \ - -DENABLE_NASM=ON \ - -DENABLE_DOCS=OFF \ - -DENABLE_EXAMPLES=OFF \ - -DENABLE_TESTS=OFF \ - -DCONFIG_AV1_ENCODER=1 \ - -DCONFIG_AV1_DECODER=1 \ - ../aom && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf aom aom_build - -# ============================================================================ -# Build libopus (BSD - Opus audio codec) -# ============================================================================ -RUN wget -q https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz && \ - tar xzf opus-${OPUS_VERSION}.tar.gz && \ - cd opus-${OPUS_VERSION} && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --disable-doc \ - --disable-extra-programs \ - CFLAGS="-fPIC" && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf opus-${OPUS_VERSION}* - -# ============================================================================ -# Build libmp3lame (LGPL - MP3 encoder) -# ============================================================================ -RUN wget -q "https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" && \ - tar xzf lame-${LAME_VERSION}.tar.gz && \ - cd lame-${LAME_VERSION} && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --enable-nasm \ - --disable-frontend \ - CFLAGS="-fPIC" && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf lame-${LAME_VERSION}* - -# ============================================================================ -# Build libogg + libvorbis (BSD - Vorbis audio codec) -# ============================================================================ -RUN wget -q https://ftp.osuosl.org/pub/xiph/releases/ogg/libogg-1.3.5.tar.gz && \ - tar xzf libogg-1.3.5.tar.gz && \ - cd libogg-1.3.5 && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - CFLAGS="-fPIC" && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libogg-1.3.5* - -RUN wget -q https://ftp.osuosl.org/pub/xiph/releases/vorbis/libvorbis-1.3.7.tar.gz && \ - tar xzf libvorbis-1.3.7.tar.gz && \ - cd libvorbis-1.3.7 && \ - ./configure \ - --prefix=$PREFIX \ - --disable-shared \ - --enable-static \ - --with-pic \ - --with-ogg=$PREFIX \ - CFLAGS="-fPIC" && \ - make -j$(nproc) && \ - make install && \ - cd .. && rm -rf libvorbis-1.3.7* - -# ============================================================================ -# Build FFmpeg with all codecs (GPL due to x264/x265) -# Note: We don't use -static here because we want libs that can link into .so -# ============================================================================ -RUN git clone --depth 1 --branch ${FFMPEG_VERSION} https://git.ffmpeg.org/ffmpeg.git && \ - cd ffmpeg && \ - ./configure \ - --prefix=$PREFIX \ - --pkg-config-flags="--static" \ - --extra-cflags="-I$PREFIX/include -fPIC" \ - --extra-ldflags="-L$PREFIX/lib" \ - --extra-libs="-lpthread -lm -lstdc++" \ - --enable-gpl \ - --enable-version3 \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --disable-ffplay \ - --disable-doc \ - --disable-debug \ - --disable-network \ - --enable-libx264 \ - --enable-libx265 \ - --enable-libvpx \ - --enable-libaom \ - --enable-libopus \ - --enable-libmp3lame \ - --enable-libvorbis && \ - make -j$(nproc) && \ - make install - -# ============================================================================ -# Strip debug symbols and verify PIC in static libraries -# ============================================================================ -RUN strip $PREFIX/bin/ffmpeg $PREFIX/bin/ffprobe && \ - echo "=== Verifying PIC in static libraries ===" && \ - # Check that object files in .a archives have PIC relocations - for lib in $PREFIX/lib/*.a; do \ - echo "Checking $lib for PIC..."; \ - # Extract and check for non-PIC relocations (R_X86_64_32 or R_X86_64_32S) - # These would fail when linking into a shared object - if ar -t "$lib" 2>/dev/null | head -1 | xargs -I{} sh -c "ar -p '$lib' {} 2>/dev/null | readelf -r - 2>/dev/null | grep -q 'R_X86_64_32'"; then \ - echo "WARNING: $lib may have non-PIC code"; \ - else \ - echo "OK: $lib"; \ - fi; \ - done && \ - echo "=== Binary sizes ===" && \ - ls -lh $PREFIX/bin/ffmpeg $PREFIX/bin/ffprobe && \ - echo "=== FFmpeg version ===" && \ - $PREFIX/bin/ffmpeg -version - -# ============================================================================ -# Final stage - includes binaries AND dev files for native addon compilation -# ============================================================================ -FROM scratch AS export - -COPY --from=builder /build/bin/ffmpeg /build/bin/ffmpeg -COPY --from=builder /build/bin/ffprobe /build/bin/ffprobe - -# For extraction via docker cp, we need a runnable container -FROM ubuntu:24.04 AS runtime - -# Copy binaries -COPY --from=builder /build/bin/ffmpeg /build/bin/ffmpeg -COPY --from=builder /build/bin/ffprobe /build/bin/ffprobe - -# Copy development files needed for native addon compilation -# These are required by build-prebuilds.yml to link against FFmpeg -COPY --from=builder /build/lib /build/lib -COPY --from=builder /build/include /build/include - -# Verify binaries work (will link against system glibc) -RUN /build/bin/ffmpeg -version - -CMD ["/build/bin/ffmpeg", "-version"] diff --git a/docs/plans/2025-01-04-migrate-binaries-out.md b/docs/archived/2025-01-04-migrate-binaries-out.md similarity index 100% rename from docs/plans/2025-01-04-migrate-binaries-out.md rename to docs/archived/2025-01-04-migrate-binaries-out.md diff --git a/docs/plans/2025-01-04-prebuilt-binaries-refactor.md b/docs/archived/2025-01-04-prebuilt-binaries-refactor.md similarity index 100% rename from docs/plans/2025-01-04-prebuilt-binaries-refactor.md rename to docs/archived/2025-01-04-prebuilt-binaries-refactor.md diff --git a/docs/plans/2025-01-04-support-linux-alpine.md b/docs/archived/2025-01-04-support-linux-alpine.md similarity index 100% rename from docs/plans/2025-01-04-support-linux-alpine.md rename to docs/archived/2025-01-04-support-linux-alpine.md diff --git a/docs/build-system.md b/docs/archived/build-system-legacy.md similarity index 100% rename from docs/build-system.md rename to docs/archived/build-system-legacy.md diff --git a/docs/libc-tagging-implementation.md b/docs/archived/libc-tagging-implementation.md similarity index 100% rename from docs/libc-tagging-implementation.md rename to docs/archived/libc-tagging-implementation.md diff --git a/docs/ffmpeg-packages.md b/docs/ffmpeg-packages.md new file mode 100644 index 00000000..8b45f09b --- /dev/null +++ b/docs/ffmpeg-packages.md @@ -0,0 +1,93 @@ +# FFmpeg Package Management + +## Overview + +node-webcodecs uses FFmpeg static libraries for native addon compilation. FFmpeg dependencies are distributed via **npm packages** from the [ffmpeg-prebuilds](https://github.com/pproenca/ffmpeg-prebuilds) repository. + +## Packages + +**Development packages:** `@pproenca/ffmpeg-dev-*` (static libraries + headers) + +**Platforms:** +- `@pproenca/ffmpeg-dev-darwin-arm64` (macOS Apple Silicon) +- `@pproenca/ffmpeg-dev-darwin-x64` (macOS Intel) +- `@pproenca/ffmpeg-dev-linux-x64-glibc` (Linux glibc) +- `@pproenca/ffmpeg-dev-linux-x64-musl` (Linux musl/Alpine) + +**Included codecs:** H.264, H.265, VP9, AV1, Opus, MP3 (via libmp3lame), Vorbis + +**Repository:** [pproenca/ffmpeg-prebuilds](https://github.com/pproenca/ffmpeg-prebuilds) + +## CI Workflow + +The CI workflow automatically installs the appropriate FFmpeg package for each platform: + +```yaml +- name: Install FFmpeg from npm + run: | + npm install --no-save @pproenca/ffmpeg-dev-${{ matrix.platform }} + FFMPEG_ROOT="$(npm root)/@pproenca/ffmpeg-dev-${{ matrix.platform }}" + echo "FFMPEG_ROOT=$FFMPEG_ROOT" >> "$GITHUB_ENV" +``` + +The `gyp/ffmpeg-paths-lib.ts` resolver uses `FFMPEG_ROOT` to locate libraries and headers. + +## Local Development + +```bash +# Install FFmpeg dev package for your platform +npm install --save-dev @pproenca/ffmpeg-dev-darwin-arm64 + +# Set FFMPEG_ROOT +export FFMPEG_ROOT="$(npm root)/@pproenca/ffmpeg-dev-darwin-arm64" + +# Build (gyp will find FFmpeg automatically) +npm run build +``` + +## Package Versioning + +FFmpeg npm package versions follow the FFmpeg release version: + +| FFmpeg Version | npm Package Version | +|----------------|---------------------| +| n8.0 | 8.0.0 | +| n8.1 (future) | 8.1.0 | + +## Troubleshooting + +### npm package not found + +```bash +# Check if package exists for your platform +npm view @pproenca/ffmpeg-dev-linux-x64-glibc +``` + +### Build can't find FFmpeg + +```bash +# Verify FFMPEG_ROOT is set +echo $FFMPEG_ROOT + +# Check gyp resolution +node gyp/ffmpeg-paths.js include +node gyp/ffmpeg-paths.js lib +``` + +### Specific FFmpeg version needed + +```bash +# Install exact version +npm install --save-dev @pproenca/ffmpeg-dev-darwin-arm64@8.0.0 +``` + +## Related Files + +- **CI Workflow:** `.github/workflows/ci.yml` (FFmpeg installation steps) +- **FFmpeg Resolver:** `gyp/ffmpeg-paths-lib.ts` (FFMPEG_ROOT resolution) +- **Binding Config:** `binding.gyp` (uses gyp/ffmpeg-paths.js) + +## References + +- [ffmpeg-prebuilds Repository](https://github.com/pproenca/ffmpeg-prebuilds) - Build and distribution +- [sharp-libvips](https://github.com/lovell/sharp-libvips) - Pattern inspiration diff --git a/gyp/ffmpeg-paths-lib.ts b/gyp/ffmpeg-paths-lib.ts index ac79b048..8103fd9b 100644 --- a/gyp/ffmpeg-paths-lib.ts +++ b/gyp/ffmpeg-paths-lib.ts @@ -4,13 +4,12 @@ // Resolve FFmpeg paths for node-gyp binding. // // Resolution order: -// 1. FFMPEG_ROOT env var (set by CI from deps-v* release artifacts) +// 1. FFMPEG_ROOT env var (set by CI from @pproenca/ffmpeg-dev-* npm packages) // 2. ./ffmpeg-install directory (local development) // 3. System pkg-config (fallback) // -// The FFmpeg static libraries are built from: -// - Linux: docker/Dockerfile.linux-x64 (Alpine musl, fully static) -// - macOS: .github/workflows/build-ffmpeg.yml (native build) +// FFmpeg static libraries are distributed via npm packages from the +// ffmpeg-prebuilds repository (https://github.com/pproenca/ffmpeg-prebuilds) // // All codec dependencies (x264, x265, vpx, opus, etc.) are resolved automatically // via the .pc files in the FFmpeg build. diff --git a/scripts/ci/build-ffmpeg-workflow.ts b/scripts/ci/build-ffmpeg-workflow.ts deleted file mode 100644 index ba1c3300..00000000 --- a/scripts/ci/build-ffmpeg-workflow.ts +++ /dev/null @@ -1,916 +0,0 @@ -#!/usr/bin/env tsx -import { - chmodSync, - copyFileSync, - existsSync, - mkdirSync, - readFileSync, - readdirSync, - renameSync, - rmSync, - writeFileSync, -} from 'node:fs'; -import {basename, join, resolve} from 'node:path'; -import {isMainModule} from '../shared/runtime'; -import {parseArgs, requireFlag} from '../shared/args'; -import {findFirstFile, listDirectories} from './fs-utils'; -import {writeGithubOutput} from './github'; -import {DEFAULT_RUNNER, runShellScript, type CommandRunner} from './runner'; - -interface DockerExtractOptions { - readonly image: string; - readonly container: string; - readonly platform: string; - readonly artifactsDir: string; - readonly lddMode: 'musl' | 'glibc'; -} - -interface PackageArtifactsOptions { - readonly artifactsDir: string; - readonly platform: string; -} - -interface MacosPackageOptions { - readonly targetDir: string; - readonly artifactsDir: string; - readonly arch: string; -} - -const SEMVER_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$/; -const PLATFORM_ORDER = ['linux-x64', 'linux-x64-glibc', 'darwin-x64', 'darwin-arm64']; - -function ensureDir(pathname: string): void { - mkdirSync(pathname, {recursive: true}); -} - -function resolveVersionFromRef(refName: string): string { - const raw = refName.startsWith('v') ? refName.slice(1) : refName; - if (!SEMVER_PATTERN.test(raw)) { - return '0.0.0-dev'; - } - return raw; -} - -function parsePlatform(platform: string): {os: string; cpu: string} { - const parts = platform.split('-'); - if (parts.length !== 2) { - throw new Error(`Invalid platform string: ${platform}`); - } - return {os: parts[0], cpu: parts[1]}; -} - -function buildPlatformPackageJson( - scope: string, - platform: string, - version: string, -): Record { - const {os, cpu} = parsePlatform(platform); - return { - name: `${scope}-${platform}`, - version, - description: `FFmpeg static binary for ${platform}`, - os: [os], - cpu: [cpu], - files: ['bin/'], - license: 'GPL-2.0-or-later', - repository: { - type: 'git', - url: 'https://github.com/pproenca/node-webcodecs', - }, - }; -} - -function buildMainPackageJson(scope: string, version: string): Record { - return { - name: scope, - version, - description: 'FFmpeg static binaries for Node.js', - main: 'index.js', - types: 'index.d.ts', - scripts: { - postinstall: 'node install.js', - }, - optionalDependencies: { - [`${scope}-linux-x64`]: version, - [`${scope}-darwin-x64`]: version, - [`${scope}-darwin-arm64`]: version, - }, - license: 'GPL-2.0-or-later', - repository: { - type: 'git', - url: 'https://github.com/pproenca/node-webcodecs', - }, - keywords: ['ffmpeg', 'video', 'audio', 'encoding', 'webcodecs'], - engines: { - node: '>=16', - }, - }; -} - -function buildIndexJs(scope: string): string { - return ` -const path = require('path'); -const { execSync, spawn } = require('child_process'); - -const PLATFORMS = { - 'darwin-arm64': '${scope}-darwin-arm64', - 'darwin-x64': '${scope}-darwin-x64', - 'linux-x64': '${scope}-linux-x64', -}; - -function getBinaryPath(binary = 'ffmpeg') { - const platform = \`\${process.platform}-\${process.arch}\`; - const pkg = PLATFORMS[platform]; - - if (!pkg) { - throw new Error( - \`Unsupported platform: \${platform}. \` + - \`Supported: \${Object.keys(PLATFORMS).join(', ')}\` - ); - } - - try { - const pkgPath = require.resolve(\`\${pkg}/package.json\`); - return path.join(path.dirname(pkgPath), 'bin', binary); - } catch (e) { - throw new Error( - \`Binary package \${pkg} not found. \` + - \`Run: npm install --include=optional\` - ); - } -} - -function ffmpeg(args, options = {}) { - const binary = getBinaryPath('ffmpeg'); - if (typeof args === 'string') { - return execSync(\`"\${binary}" \${args}\`, { encoding: 'utf8', ...options }); - } - return spawn(binary, args, options); -} - -function ffprobe(args, options = {}) { - const binary = getBinaryPath('ffprobe'); - if (typeof args === 'string') { - return execSync(\`"\${binary}" \${args}\`, { encoding: 'utf8', ...options }); - } - return spawn(binary, args, options); -} - -module.exports = { - getBinaryPath, - ffmpegPath: getBinaryPath('ffmpeg'), - ffprobePath: getBinaryPath('ffprobe'), - ffmpeg, - ffprobe, -}; -`.trimStart(); -} - -function buildIndexDts(): string { - return ` -import { SpawnOptions, ChildProcess, ExecSyncOptions } from 'child_process'; - -export function getBinaryPath(binary?: 'ffmpeg' | 'ffprobe'): string; -export const ffmpegPath: string; -export const ffprobePath: string; - -export function ffmpeg(args: string, options?: ExecSyncOptions): string; -export function ffmpeg(args: string[], options?: SpawnOptions): ChildProcess; - -export function ffprobe(args: string, options?: ExecSyncOptions): string; -export function ffprobe(args: string[], options?: SpawnOptions): ChildProcess; -`.trimStart(); -} - -function buildInstallJs(): string { - return ` -const { getBinaryPath } = require('./index'); -const { execSync } = require('child_process'); - -try { - const ffmpegPath = getBinaryPath('ffmpeg'); - const version = execSync(\`"\${ffmpegPath}" -version\`, { encoding: 'utf8' }); - console.log('FFmpeg binary verified:', ffmpegPath); - console.log(version.split('\\n')[0]); -} catch (e) { - console.warn('Warning: FFmpeg binary not found for this platform'); - console.warn(e.message); -} -`.trimStart(); -} - -function rewritePkgConfigContent(content: string, originalPrefix: string): string { - // biome-ignore lint/suspicious/noTemplateCurlyInString: This is a literal pkg-config variable token - const prefixToken = '${prefix}'; - return content - .split('\n') - .map(line => { - if (line.startsWith('prefix=')) { - return line; - } - if (line.startsWith(`libdir=${originalPrefix}/lib`)) { - return `libdir=${prefixToken}/lib`; - } - if (line.startsWith(`includedir=${originalPrefix}/include`)) { - return `includedir=${prefixToken}/include`; - } - if (line.startsWith(`libdir=${originalPrefix}`)) { - return `libdir=${prefixToken}`; - } - if (line.startsWith(`includedir=${originalPrefix}`)) { - return `includedir=${prefixToken}`; - } - return line.replaceAll(originalPrefix, prefixToken); - }) - .join('\n'); -} - -function rewritePkgConfigFiles(platformDir: string): void { - const pkgConfigDir = join(platformDir, 'lib', 'pkgconfig'); - if (!existsSync(pkgConfigDir)) { - return; - } - const entries = readdirSync(pkgConfigDir); - for (const entry of entries) { - if (!entry.endsWith('.pc')) { - continue; - } - const pcPath = join(pkgConfigDir, entry); - const content = readFileSync(pcPath, 'utf8'); - const prefixLine = content.split('\n').find(line => line.startsWith('prefix=')); - if (!prefixLine) { - continue; - } - const originalPrefix = prefixLine.slice('prefix='.length); - if (!originalPrefix) { - continue; - } - const updated = rewritePkgConfigContent(content, originalPrefix); - writeFileSync(pcPath, updated); - } -} - -export function extractDockerArtifacts( - runner: CommandRunner, - options: DockerExtractOptions, -): void { - const targetDir = resolve(options.artifactsDir, options.platform); - ensureDir(join(targetDir, 'bin')); - ensureDir(join(targetDir, 'lib')); - ensureDir(join(targetDir, 'include')); - - runner.runOrThrow('docker', ['create', '--name', options.container, options.image], { - stdio: 'inherit', - }); - - runner.runOrThrow('docker', [ - 'cp', - `${options.container}:/build/bin/ffmpeg`, - join(targetDir, 'bin', 'ffmpeg'), - ]); - runner.runOrThrow('docker', [ - 'cp', - `${options.container}:/build/bin/ffprobe`, - join(targetDir, 'bin', 'ffprobe'), - ]); - runner.runOrThrow('docker', [ - 'cp', - `${options.container}:/build/lib/.`, - join(targetDir, 'lib'), - ]); - runner.runOrThrow('docker', [ - 'cp', - `${options.container}:/build/include/.`, - join(targetDir, 'include'), - ]); - - runner.run('docker', ['rm', '-f', options.container]); - - const ffmpegPath = join(targetDir, 'bin', 'ffmpeg'); - if (!existsSync(ffmpegPath)) { - throw new Error('ffmpeg binary not extracted from container'); - } - - runner.runOrThrow('file', [ffmpegPath], {stdio: 'inherit'}); - - const lddResult = runner.run('ldd', [ffmpegPath]); - if (options.lddMode === 'musl') { - if (lddResult.exitCode === 0) { - console.log('Warning: binary may have dynamic deps'); - } - } else if (lddResult.exitCode !== 0) { - console.log('Note: ldd may fail if glibc versions differ'); - } - - chmodSync(ffmpegPath, 0o755); - const versionResult = runner.run(ffmpegPath, ['-version']); - writeFileSync(join(targetDir, 'version.txt'), `${versionResult.stdout}${versionResult.stderr}`); - - runner.runOrThrow('ls', ['-la', join(targetDir, 'lib')], {stdio: 'inherit'}); - const pkgconfigDir = join(targetDir, 'lib', 'pkgconfig'); - if (existsSync(pkgconfigDir)) { - runner.runOrThrow('ls', ['-la', pkgconfigDir], {stdio: 'inherit'}); - } -} - -export function packageArtifacts( - runner: CommandRunner, - options: PackageArtifactsOptions, -): void { - const artifactsRoot = resolve(options.artifactsDir); - runner.runOrThrow('tar', ['-cvf', `${options.platform}.tar`, `${options.platform}/`], { - cwd: artifactsRoot, - stdio: 'inherit', - }); -} - -export function installMacosDependencies(runner: CommandRunner): void { - runner.runOrThrow('brew', ['install', 'autoconf', 'automake', 'libtool', 'nasm', 'cmake', 'pkg-config'], { - stdio: 'inherit', - }); -} - -export function buildMacosCodecs(runner: CommandRunner, env: NodeJS.ProcessEnv): void { - if (!env.TARGET || !env.ARCH || !env.MACOS_DEPLOYMENT_TARGET) { - throw new Error('TARGET, ARCH, and MACOS_DEPLOYMENT_TARGET must be set'); - } - const workspace = env.GITHUB_WORKSPACE ?? process.cwd(); - const script = ` -set -e -export PATH="$TARGET/bin:$PATH" -export PKG_CONFIG_PATH="$TARGET/lib/pkgconfig" -mkdir -p "$TARGET"/{include,lib,bin} -mkdir -p "${workspace}/ffmpeg_sources" && cd "${workspace}/ffmpeg_sources" - -rm -rf x264 x265_git libvpx aom aom_build opus-* lame-* nasm-* - -echo "=== Building nasm ===" -NASM_URL="https://github.com/netwide-assembler/nasm/archive/refs/tags/nasm-${env.NASM_VERSION}.tar.gz" - -echo "Downloading NASM from GitHub..." -curl -fSL --retry 3 --retry-delay 5 "$NASM_URL" -o nasm.tar.gz || { - echo "ERROR: Failed to download NASM from $NASM_URL" - exit 1 -} - -echo "${env.NASM_SHA256} nasm.tar.gz" | shasum -a 256 -c - || { - echo "ERROR: NASM checksum verification failed!" - echo "Expected: ${env.NASM_SHA256}" - echo "Got: $(shasum -a 256 nasm.tar.gz | cut -d' ' -f1)" - exit 1 -} -echo "NASM checksum verified" - -tar xzf nasm.tar.gz -cd nasm-nasm-${env.NASM_VERSION} -./autogen.sh -./configure --prefix="$TARGET" -make -j$(sysctl -n hw.ncpu) -mkdir -p "$TARGET/bin" -install -c nasm ndisasm "$TARGET/bin/" -cd .. - -echo "=== Building x264 (GPL) ===" -git clone --depth 1 --branch ${env.X264_VERSION} https://code.videolan.org/videolan/x264.git -cd x264 -./configure \ - --prefix="$TARGET" \ - --enable-static \ - --disable-shared \ - --enable-pic \ - --disable-cli \ - --extra-cflags="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ - --extra-ldflags="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" -make -j$(sysctl -n hw.ncpu) -make install -cd .. - -echo "=== Building x265 (GPL) ===" -git clone --depth 1 https://bitbucket.org/multicoreware/x265_git.git -mkdir -p x265_git/build/xcode && cd x265_git/build/xcode -cmake \ - -DCMAKE_INSTALL_PREFIX="$TARGET" \ - -DLIB_INSTALL_DIR="$TARGET/lib" \ - -DENABLE_SHARED=OFF \ - -DENABLE_CLI=OFF \ - -DCMAKE_OSX_ARCHITECTURES=$ARCH \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=${env.MACOS_DEPLOYMENT_TARGET} \ - ../../source -make -j$(sysctl -n hw.ncpu) -make install -mkdir -p "$TARGET/lib/pkgconfig" -cat > "$TARGET/lib/pkgconfig/x265.pc" << PCEOF -prefix=$TARGET -exec_prefix=\${prefix} -libdir=\${prefix}/lib -includedir=\${prefix}/include - -Name: x265 -Description: H.265/HEVC video encoder -Version: 3.6 -Libs: -L\${libdir} -lx265 -Libs.private: -lc++ -lm -lpthread -Cflags: -I\${includedir} -PCEOF -cd ../../.. - -echo "=== Building libvpx (BSD) ===" -git clone --depth 1 --branch ${env.LIBVPX_VERSION} https://chromium.googlesource.com/webm/libvpx.git -cd libvpx -DARWIN_VERSION=$(uname -r | cut -d. -f1) -if [ "$ARCH" = "arm64" ]; then - VPX_TARGET="arm64-darwin${DARWIN_VERSION}-gcc" -else - VPX_TARGET="x86_64-darwin${DARWIN_VERSION}-gcc" -fi -echo "Using libvpx target: $VPX_TARGET" -LDFLAGS="-mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ -./configure \ - --prefix="$TARGET" \ - --target=$VPX_TARGET \ - --enable-vp8 \ - --enable-vp9 \ - --disable-examples \ - --disable-unit-tests \ - --enable-vp9-highbitdepth \ - --enable-static \ - --disable-shared \ - --extra-cflags="-mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" -make -j$(sysctl -n hw.ncpu) -make install -cd .. - -echo "=== Building libaom (AV1, BSD) ===" -git clone --depth 1 --branch ${env.LIBAOM_VERSION} https://aomedia.googlesource.com/aom -mkdir aom_build && cd aom_build -cmake \ - -DCMAKE_INSTALL_PREFIX="$TARGET" \ - -DBUILD_SHARED_LIBS=OFF \ - -DENABLE_DOCS=OFF \ - -DENABLE_EXAMPLES=OFF \ - -DENABLE_TESTS=OFF \ - -DCMAKE_OSX_ARCHITECTURES=$ARCH \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=${env.MACOS_DEPLOYMENT_TARGET} \ - ../aom -make -j$(sysctl -n hw.ncpu) -make install -cd .. - -echo "=== Building libopus (BSD) ===" -curl -fSL --retry 3 https://downloads.xiph.org/releases/opus/opus-${env.OPUS_VERSION}.tar.gz -o opus.tar.gz || { - echo "ERROR: Failed to download Opus from xiph.org" - exit 1 -} - -echo "${env.OPUS_SHA256} opus.tar.gz" | shasum -a 256 -c - || { - echo "ERROR: Opus checksum verification failed!" - echo "Expected: ${env.OPUS_SHA256}" - echo "Got: $(shasum -a 256 opus.tar.gz | cut -d' ' -f1)" - exit 1 -} -echo "Opus checksum verified" - -tar xzf opus.tar.gz -cd opus-${env.OPUS_VERSION} -./configure \ - --prefix="$TARGET" \ - --disable-shared \ - --enable-static \ - CFLAGS="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ - LDFLAGS="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" -make -j$(sysctl -n hw.ncpu) -make install -cd .. - -echo "=== Building libmp3lame (LGPL) ===" -curl -fSL --retry 3 "https://downloads.sourceforge.net/project/lame/lame/${env.LAME_VERSION}/lame-${env.LAME_VERSION}.tar.gz" -o lame.tar.gz || { - echo "ERROR: Failed to download LAME from SourceForge" - exit 1 -} - -echo "${env.LAME_SHA256} lame.tar.gz" | shasum -a 256 -c - || { - echo "ERROR: LAME checksum verification failed!" - echo "Expected: ${env.LAME_SHA256}" - echo "Got: $(shasum -a 256 lame.tar.gz | cut -d' ' -f1)" - exit 1 -} -echo "LAME checksum verified" - -tar xzf lame.tar.gz -cd lame-${env.LAME_VERSION} -./configure \ - --prefix="$TARGET" \ - --disable-shared \ - --enable-static \ - --enable-nasm \ - CFLAGS="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ - LDFLAGS="-arch $ARCH -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" -make -j$(sysctl -n hw.ncpu) -make install -cd .. -`; - runShellScript(runner, script, {env, stdio: 'inherit'}); -} - -export function buildMacosFfmpeg(runner: CommandRunner, env: NodeJS.ProcessEnv): void { - if (!env.TARGET || !env.ARCH || !env.MACOS_DEPLOYMENT_TARGET) { - throw new Error('TARGET, ARCH, and MACOS_DEPLOYMENT_TARGET must be set'); - } - const workspace = env.GITHUB_WORKSPACE ?? process.cwd(); - const script = ` -set -e -export PATH="$TARGET/bin:$PATH" -export PKG_CONFIG_PATH="$TARGET/lib/pkgconfig" - -mkdir -p "${workspace}/ffmpeg_sources" -cd "${workspace}/ffmpeg_sources" - -if [ ! -d ffmpeg ]; then - for i in 1 2 3; do - git clone --depth 1 https://github.com/FFmpeg/FFmpeg.git ffmpeg && break - echo "Clone attempt $i failed, retrying in 10s..." - sleep 10 - done - if [ ! -d ffmpeg ]; then - echo "ERROR: Failed to clone FFmpeg after 3 attempts" - exit 1 - fi -fi - -cd ffmpeg -make distclean 2>/dev/null || true - -./configure \ - --cc="clang -arch $ARCH" \ - --prefix="$TARGET" \ - --extra-cflags="-I$TARGET/include -fno-stack-check -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ - --extra-ldflags="-L$TARGET/lib -mmacosx-version-min=${env.MACOS_DEPLOYMENT_TARGET}" \ - --pkg-config-flags="--static" \ - --enable-static \ - --disable-shared \ - --enable-gpl \ - --enable-version3 \ - --enable-pthreads \ - --enable-runtime-cpudetect \ - --disable-ffplay \ - --disable-doc \ - --disable-debug \ - --enable-libx264 \ - --enable-libx265 \ - --enable-libvpx \ - --enable-libaom \ - --enable-libopus \ - --enable-libmp3lame - -make -j$(sysctl -n hw.ncpu) -make install -`; - runShellScript(runner, script, {env, stdio: 'inherit'}); -} - -export function verifyMacosTargets(runner: CommandRunner, env: NodeJS.ProcessEnv): void { - const root = env.TARGET; - if (!root) { - throw new Error('TARGET must be set to verify macOS ABI'); - } - runner.runOrThrow( - 'npx', - ['tsx', 'scripts/check-macos-abi.ts'], - {env: {...env, FFMPEG_ROOT: root}, stdio: 'inherit'}, - ); -} - -export function verifyAndStripMacosBinaries( - runner: CommandRunner, - env: NodeJS.ProcessEnv, -): void { - if (!env.TARGET) { - throw new Error('TARGET must be set'); - } - const targetDir = env.TARGET; - const ffmpegPath = join(targetDir, 'bin', 'ffmpeg'); - const ffprobePath = join(targetDir, 'bin', 'ffprobe'); - - runner.runOrThrow('otool', ['-L', ffmpegPath], {stdio: 'inherit'}); - const depsOutput = runner.run('otool', ['-L', ffmpegPath]); - const deps = depsOutput.stdout - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.includes('libSystem') && !line.endsWith(':')); - if (deps.length > 0) { - console.log('Warning: Found unexpected dynamic dependencies'); - for (const line of deps) { - console.log(line); - } - } - - runner.runOrThrow('strip', [ffmpegPath], {stdio: 'inherit'}); - runner.runOrThrow('strip', [ffprobePath], {stdio: 'inherit'}); - runner.runOrThrow('ls', ['-lh', ffmpegPath, ffprobePath], {stdio: 'inherit'}); - runner.runOrThrow(ffmpegPath, ['-version'], {stdio: 'inherit'}); -} - -export function packageMacosArtifacts( - runner: CommandRunner, - options: MacosPackageOptions, -): void { - const platform = `darwin-${options.arch}`; - const outputDir = resolve(options.artifactsDir, platform); - ensureDir(join(outputDir, 'bin')); - ensureDir(join(outputDir, 'lib')); - ensureDir(join(outputDir, 'include')); - - const ffmpegPath = join(options.targetDir, 'bin', 'ffmpeg'); - const ffprobePath = join(options.targetDir, 'bin', 'ffprobe'); - copyFileSync(ffmpegPath, join(outputDir, 'bin', 'ffmpeg')); - copyFileSync(ffprobePath, join(outputDir, 'bin', 'ffprobe')); - - const versionResult = runner.run(ffmpegPath, ['-version']); - writeFileSync(join(outputDir, 'version.txt'), `${versionResult.stdout}${versionResult.stderr}`); - - const libDir = join(options.targetDir, 'lib'); - const includeDir = join(options.targetDir, 'include'); - runner.runOrThrow('cp', ['-r', `${libDir}/`, `${outputDir}/lib/`], {stdio: 'inherit'}); - runner.runOrThrow('cp', ['-r', `${includeDir}/`, `${outputDir}/include/`], {stdio: 'inherit'}); - runner.runOrThrow('ls', ['-la', join(outputDir, 'lib')], {stdio: 'inherit'}); - runner.runOrThrow('ls', ['-la', join(outputDir, 'lib', 'pkgconfig')], {stdio: 'inherit'}); - - runner.runOrThrow('tar', ['-cvf', `${platform}.tar`, `${platform}/`], { - cwd: resolve(options.artifactsDir), - stdio: 'inherit', - }); -} - -export function extractAndPackageNpm( - runner: CommandRunner, - env: NodeJS.ProcessEnv, - artifactsDir: string, - packagesDir: string, -): void { - const scope = env.NPM_SCOPE ?? '@pproenca/ffmpeg'; - const refName = env.GITHUB_REF_NAME ?? ''; - const version = resolveVersionFromRef(refName); - - ensureDir(packagesDir); - - const artifactDirs = listDirectories(artifactsDir).filter(dir => basename(dir).startsWith('ffmpeg-')); - for (const artifactDir of artifactDirs) { - const tarball = findFirstFile(artifactDir, pathname => pathname.endsWith('.tar')); - if (tarball) { - runner.runOrThrow('tar', ['-xvf', tarball, '-C', artifactDir], {stdio: 'inherit'}); - } - - const platform = basename(artifactDir).replace('ffmpeg-', ''); - const srcDir = join(artifactDir, platform); - const ffmpegPath = join(srcDir, 'bin', 'ffmpeg'); - if (!existsSync(ffmpegPath)) { - console.log(`Warning: Skipping ${platform} - binary not found at ${ffmpegPath}`); - continue; - } - - const pkgDir = resolve(packagesDir, `${scope}-${platform}`); - ensureDir(join(pkgDir, 'bin')); - copyFileSync(join(srcDir, 'bin', 'ffmpeg'), join(pkgDir, 'bin', 'ffmpeg')); - copyFileSync(join(srcDir, 'bin', 'ffprobe'), join(pkgDir, 'bin', 'ffprobe')); - chmodSync(join(pkgDir, 'bin', 'ffmpeg'), 0o755); - chmodSync(join(pkgDir, 'bin', 'ffprobe'), 0o755); - - const pkgJson = buildPlatformPackageJson(scope, platform, version); - writeFileSync(join(pkgDir, 'package.json'), `${JSON.stringify(pkgJson, null, 2)}\n`); - console.log(`Created package: ${pkgDir}`); - } -} - -export function createMainNpmPackage( - env: NodeJS.ProcessEnv, - packagesDir: string, -): void { - const scope = env.NPM_SCOPE ?? '@pproenca/ffmpeg'; - const version = resolveVersionFromRef(env.GITHUB_REF_NAME ?? ''); - const mainDir = resolve(packagesDir, scope); - ensureDir(mainDir); - - const pkgJson = buildMainPackageJson(scope, version); - writeFileSync(join(mainDir, 'package.json'), `${JSON.stringify(pkgJson, null, 2)}\n`); - writeFileSync(join(mainDir, 'index.js'), buildIndexJs(scope)); - writeFileSync(join(mainDir, 'index.d.ts'), buildIndexDts()); - writeFileSync(join(mainDir, 'install.js'), buildInstallJs()); -} - -export function publishFfmpegPackages( - runner: CommandRunner, - env: NodeJS.ProcessEnv, - packagesDir: string, -): void { - const scope = env.NPM_SCOPE ?? '@pproenca/ffmpeg'; - const packageDirs = listDirectories(packagesDir).filter(dir => basename(dir).startsWith(`${scope}-`)); - for (const dir of packageDirs) { - runner.runOrThrow('npm', ['publish', '--access', 'public', '--provenance'], { - cwd: dir, - stdio: 'inherit', - }); - } - runner.runOrThrow('sleep', ['10']); - - const mainDir = resolve(packagesDir, scope); - runner.runOrThrow('npm', ['publish', '--access', 'public', '--provenance'], { - cwd: mainDir, - stdio: 'inherit', - }); -} - -export function prepareReleaseAssets( - runner: CommandRunner, - env: NodeJS.ProcessEnv, - artifactsDir: string, - releaseDir: string, -): void { - const refName = env.GITHUB_REF_NAME ?? ''; - ensureDir(releaseDir); - - const artifactDirs = listDirectories(artifactsDir).filter(dir => basename(dir).startsWith('ffmpeg-')); - for (const artifactDir of artifactDirs) { - const tarball = findFirstFile(artifactDir, pathname => pathname.endsWith('.tar')); - if (!tarball) { - continue; - } - const platform = basename(artifactDir).replace('ffmpeg-', ''); - runner.runOrThrow('tar', ['-xf', tarball, '-C', artifactsDir], {stdio: 'inherit'}); - - const platformDir = resolve(artifactsDir, platform); - const binDir = join(platformDir, 'bin'); - if (existsSync(binDir)) { - renameSync(join(binDir, 'ffmpeg'), join(platformDir, 'ffmpeg')); - renameSync(join(binDir, 'ffprobe'), join(platformDir, 'ffprobe')); - rmSync(binDir, {recursive: true, force: true}); - rmSync(join(platformDir, 'lib'), {recursive: true, force: true}); - rmSync(join(platformDir, 'include'), {recursive: true, force: true}); - } - - runner.runOrThrow( - 'tar', - ['-czvf', join(releaseDir, `ffmpeg-${refName}-${platform}.tar.gz`), `${platform}/`], - {cwd: artifactsDir, stdio: 'inherit'}, - ); - } - runner.runOrThrow('ls', ['-la', releaseDir], {stdio: 'inherit'}); -} - -export function resolveDepsVersion(inputVersion: string | undefined, refName: string): string { - if (inputVersion) { - return inputVersion; - } - return refName.startsWith('deps-') ? refName.slice('deps-'.length) : refName; -} - -export function prepareDepsReleaseAssets( - runner: CommandRunner, - artifactsDir: string, - releaseDir: string, -): void { - ensureDir(releaseDir); - const artifactDirs = listDirectories(artifactsDir).filter(dir => basename(dir).startsWith('ffmpeg-')); - for (const artifactDir of artifactDirs) { - const tarball = findFirstFile(artifactDir, pathname => pathname.endsWith('.tar')); - if (!tarball) { - continue; - } - const platform = basename(artifactDir).replace('ffmpeg-', ''); - runner.runOrThrow('tar', ['-xf', tarball, '-C', artifactsDir], {stdio: 'inherit'}); - const platformDir = resolve(artifactsDir, platform); - const libDir = join(platformDir, 'lib'); - if (!existsSync(libDir)) { - throw new Error(`Missing lib/ directory for ${platform}`); - } - rewritePkgConfigFiles(platformDir); - - runner.runOrThrow( - 'tar', - ['-czvf', join(releaseDir, `ffmpeg-${platform}.tar.gz`), 'lib/', 'include/', 'bin/', 'version.txt'], - {cwd: platformDir, stdio: 'inherit'}, - ); - } - - runner.runOrThrow('ls', ['-la', releaseDir], {stdio: 'inherit'}); - for (const platform of PLATFORM_ORDER) { - const path = join(releaseDir, `ffmpeg-${platform}.tar.gz`); - if (!existsSync(path)) { - throw new Error(`Missing ffmpeg-${platform}.tar.gz`); - } - runner.runOrThrow('tar', ['-tzf', path], {stdio: 'inherit'}); - } - console.log('All platform artifacts verified.'); -} - -export function main( - args: string[], - runner: CommandRunner = DEFAULT_RUNNER, - env: NodeJS.ProcessEnv = process.env, -): number { - const {positional, flags} = parseArgs(args); - const command = positional[0]; - - try { - if (command === 'extract-docker') { - const image = requireFlag(flags, 'image'); - const container = requireFlag(flags, 'container'); - const platform = requireFlag(flags, 'platform'); - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - const lddMode = (flags.ldd as 'musl' | 'glibc') ?? 'musl'; - extractDockerArtifacts(runner, {image, container, platform, artifactsDir, lddMode}); - return 0; - } - - if (command === 'package-artifacts') { - const platform = requireFlag(flags, 'platform'); - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - packageArtifacts(runner, {platform, artifactsDir}); - return 0; - } - - if (command === 'install-macos-deps') { - installMacosDependencies(runner); - return 0; - } - - if (command === 'build-macos-codecs') { - buildMacosCodecs(runner, env); - return 0; - } - - if (command === 'build-macos-ffmpeg') { - buildMacosFfmpeg(runner, env); - return 0; - } - - if (command === 'verify-macos-abi') { - verifyMacosTargets(runner, env); - return 0; - } - - if (command === 'verify-strip') { - verifyAndStripMacosBinaries(runner, env); - return 0; - } - - if (command === 'package-macos') { - const targetDir = requireFlag(flags, 'target'); - const arch = requireFlag(flags, 'arch'); - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - packageMacosArtifacts(runner, {targetDir, artifactsDir, arch}); - return 0; - } - - if (command === 'extract-package-npm') { - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - const packagesDir = resolve(flags.packages ?? 'packages'); - extractAndPackageNpm(runner, env, artifactsDir, packagesDir); - return 0; - } - - if (command === 'create-main-package') { - const packagesDir = resolve(flags.packages ?? 'packages'); - createMainNpmPackage(env, packagesDir); - return 0; - } - - if (command === 'publish-npm') { - const packagesDir = resolve(flags.packages ?? 'packages'); - publishFfmpegPackages(runner, env, packagesDir); - return 0; - } - - if (command === 'prepare-release-assets') { - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - const releaseDir = resolve(flags.release ?? 'release'); - prepareReleaseAssets(runner, env, artifactsDir, releaseDir); - return 0; - } - - if (command === 'resolve-deps-version') { - const inputVersion = flags.input; - const refName = requireFlag(flags, 'ref'); - const version = resolveDepsVersion(inputVersion, refName); - writeGithubOutput(env, 'version', version); - return 0; - } - - if (command === 'prepare-deps-assets') { - const artifactsDir = resolve(flags.artifacts ?? 'artifacts'); - const releaseDir = resolve(flags.release ?? 'release'); - prepareDepsReleaseAssets(runner, artifactsDir, releaseDir); - return 0; - } - - console.error(`Unknown command: ${command ?? '(none)'}`); - return 1; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(message); - return 1; - } -} - -if (isMainModule(import.meta.url)) { - process.exit(main(process.argv.slice(2))); -} diff --git a/test/unit/ci-workflows.test.ts b/test/unit/ci-workflows.test.ts index d34b1fbd..4b19e882 100644 --- a/test/unit/ci-workflows.test.ts +++ b/test/unit/ci-workflows.test.ts @@ -19,12 +19,6 @@ import { resolveReleaseVersion, verifyCiCompleted, } from '../../scripts/ci/release-workflow'; -import { - createMainNpmPackage, - extractAndPackageNpm, - resolveDepsVersion, - verifyAndStripMacosBinaries, -} from '../../scripts/ci/build-ffmpeg-workflow'; interface RecordedCall { readonly command: string; @@ -191,43 +185,3 @@ test('resolveReleaseVersion extracts tag versions', () => { test('resolveReleaseVersion rejects non-tag refs', () => { assert.throws(() => resolveReleaseVersion({GITHUB_REF: 'refs/heads/main'})); }); - -test('resolveDepsVersion prefers explicit input', () => { - assert.strictEqual(resolveDepsVersion('v5', 'deps-v3'), 'v5'); - assert.strictEqual(resolveDepsVersion(undefined, 'deps-v3'), 'v3'); -}); - -test('verifyAndStripMacosBinaries fails without TARGET', () => { - const {runner} = createRunner([]); - assert.throws(() => verifyAndStripMacosBinaries(runner, {})); -}); - -test('extractAndPackageNpm writes platform packages', () => { - const root = createTempRoot(); - const artifactsDir = join(root, 'artifacts'); - const packagesDir = join(root, 'packages'); - const artifactDir = join(artifactsDir, 'ffmpeg-linux-x64'); - const srcDir = join(artifactDir, 'linux-x64', 'bin'); - mkdirSync(srcDir, {recursive: true}); - writeFileSync(join(artifactDir, 'bundle.tar'), 'tar'); - writeFileSync(join(srcDir, 'ffmpeg'), 'bin'); - writeFileSync(join(srcDir, 'ffprobe'), 'bin'); - - const {runner} = createRunner([okResult()]); - extractAndPackageNpm(runner, {NPM_SCOPE: '@pproenca/ffmpeg', GITHUB_REF_NAME: 'v1.2.3'}, artifactsDir, packagesDir); - - const pkgJson = readFileSync( - join(packagesDir, '@pproenca/ffmpeg-linux-x64', 'package.json'), - 'utf8', - ); - assert.ok(pkgJson.includes('"name": "@pproenca/ffmpeg-linux-x64"')); -}); - -test('createMainNpmPackage writes main package files', () => { - const root = createTempRoot(); - const packagesDir = join(root, 'packages'); - createMainNpmPackage({NPM_SCOPE: '@pproenca/ffmpeg', GITHUB_REF_NAME: 'v1.2.3'}, packagesDir); - - const pkgPath = join(packagesDir, '@pproenca/ffmpeg', 'package.json'); - assert.ok(readFileSync(pkgPath, 'utf8').includes('"name": "@pproenca/ffmpeg"')); -});