From 4b385cd92f448fdb131edd67f85c8a44c4277771 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 1 Oct 2026 00:36:24 +0200 Subject: [PATCH] build: add macOS universal libmpv.dylib CI build Vendor ci/build-libmpv-macos.sh, which builds every dependency statically for the native runner architecture and links them into a single self-contained libmpv dylib (coreaudio output, videotoolbox hardware decode, OpenGL render API; no vulkan/cocoa player). build-macos.yml runs it on macos-14 (x86_64) and macos-15 (arm64) in parallel and a merge job combines the two dylibs into one universal libmpv.dylib with lipo, matching the file name jlibmpv expects on macOS. --- .github/workflows/build-macos-arch.yml | 94 ++++++++++ .github/workflows/build-macos.yml | 78 ++++++++ README.md | 8 +- ci/build-libmpv-macos.sh | 248 +++++++++++++++++++++++++ 4 files changed, 424 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-macos-arch.yml create mode 100644 .github/workflows/build-macos.yml create mode 100644 ci/build-libmpv-macos.sh diff --git a/.github/workflows/build-macos-arch.yml b/.github/workflows/build-macos-arch.yml new file mode 100644 index 0000000..407e004 --- /dev/null +++ b/.github/workflows/build-macos-arch.yml @@ -0,0 +1,94 @@ +name: build-macos-arch + +# Builds the self-contained libmpv dylib for one macOS architecture +# (x86_64 on macos-14, arm64 on macos-15) using the vendored build script. +# The merge job in build-macos.yml combines the per-architecture dylibs into +# a universal binary. + +on: + workflow_call: + inputs: + arch: + description: Target architecture (x86_64 or arm64) + required: true + type: string + runner: + description: GitHub runner image providing that architecture + required: true + type: string + +env: + MPV_REF: v0.41.0 + ARCH: ${{ inputs.arch }} + +jobs: + build: + runs-on: ${{ inputs.runner }} + env: + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + steps: + - name: Check out mpv-natives (build scripts) + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Check out mpv (pinned tag) + uses: actions/checkout@v4 + with: + repository: mpv-player/mpv + ref: ${{ env.MPV_REF }} + path: mpv + fetch-depth: 1 + + - name: Get time + id: get_time + run: echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT + + - name: Restore ccache + uses: actions/cache/restore@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: macos-${{ env.ARCH }}-${{ steps.get_time.outputs.timestamp }} + restore-keys: macos-${{ env.ARCH }}- + + - name: Install dependencies + run: | + brew install ccache nasm ninja + python3 -m pip install --user meson + export PATH="$HOME/Library/Python/3.12/bin:$PATH" + # export so meson finds pip-installed tools + echo "$HOME/Library/Python/3.12/bin" >> "$GITHUB_PATH" + + # Run the vendored build script from inside the mpv source tree, so the + # native build writes macos_prefix/ and the meson build next to mpv's + # meson.build and produces the self-contained dylib in mpv/artifact/. + - name: Build self-contained libmpv + working-directory: mpv + run: bash "${{ github.workspace }}/ci/build-libmpv-macos.sh" + + - name: Print meson log on failure + if: failure() + run: cat mpv/macos_build/meson-logs/meson-log.txt + + - name: Collect artifact + run: | + mkdir -p "artifact-macos-${{ env.ARCH }}" + cp -pv mpv/artifact/libmpv-2.dylib "artifact-macos-${{ env.ARCH }}/" + cp -pv mpv/artifact/*.h "artifact-macos-${{ env.ARCH }}/" 2>/dev/null || true + echo "=== contents ===" + ls -l "artifact-macos-${{ env.ARCH }}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mpv-natives-macos-${{ env.ARCH }}-mpv-${{ env.MPV_REF }} + path: artifact-macos-${{ env.ARCH }} + + - name: Save ccache + uses: actions/cache/save@v4 + if: always() + with: + path: ${{ env.CCACHE_DIR }} + key: macos-${{ env.ARCH }}-${{ steps.get_time.outputs.timestamp }} \ No newline at end of file diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 0000000..680dfbd --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,78 @@ +name: build-macos + +# Builds a universal (arm64 + x86_64) self-contained libmpv.dylib for macOS +# from mpv source pinned to a release tag. Two native builds run in parallel +# (x86_64 on macos-14, arm64 on macos-15); a merge job combines the two dylibs +# with lipo into one universal library and uploads it. +# +# Every dependency is built static and linked into the one dylib (same +# approach as the Windows build). jlibmpv expects the file name +# `libmpv.dylib` (no -2) and one universal binary for both architectures. +# +# Pinned to mpv v0.41.0 — the version whose client API (131077) the jlibmpv +# JNA bindings are validated against. Bump the ref here for a libmpv update. +# The build recipe lives in ci/build-libmpv-macos.sh in this repository. + +on: + push: + branches: [main] + workflow_dispatch: + +env: + MPV_REF: v0.41.0 + +jobs: + build-x86_64: + uses: ./.github/workflows/build-macos-arch.yml + with: + arch: x86_64 + runner: macos-14 + + build-arm64: + uses: ./.github/workflows/build-macos-arch.yml + with: + arch: arm64 + runner: macos-15 + + merge: + needs: [build-x86_64, build-arm64] + runs-on: macos-15 + steps: + - name: Check out mpv-natives (build scripts) + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Check out mpv (pinned tag) + uses: actions/checkout@v4 + with: + repository: mpv-player/mpv + ref: ${{ env.MPV_REF }} + path: mpv + fetch-depth: 1 + + - name: Download architecture artifacts + uses: actions/download-artifact@v4 + with: + pattern: mpv-natives-macos-${{ env.MPV_REF }}-* + path: arch + merge-multiple: true + + - name: Create universal dylib + run: | + mkdir -p artifact-libmpv + lipo -create \ + "arch/x86_64/libmpv-2.dylib" \ + "arch/arm64/libmpv-2.dylib" \ + -output artifact-libmpv/libmpv.dylib + lipo -info artifact-libmpv/libmpv.dylib + cp -pv mpv/mpvrender.h mpv/mpv/client.h mpv/ebml_types.h artifact-libmpv/ 2>/dev/null || true + sha256sum artifact-libmpv/libmpv.dylib > artifact-libmpv/checksums.txt + cat artifact-libmpv/checksums.txt + ls -l artifact-libmpv + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mpv-natives-macos-universal-mpv-${{ env.MPV_REF }} + path: artifact-libmpv \ No newline at end of file diff --git a/README.md b/README.md index ac0e8b0..2624eec 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ here, as versioned GitHub release assets. jlibmpv JNA bindings are validated against. A libmpv update means bumping `MPV_REF` in the workflows. - **Build source:** the mpv source tree is checked out at that tag by - GitHub Actions and built with mpv's own CI scripts - (`ci/build-mingw64.sh`, ...). No mpv source or third-party binaries are - stored in this repository. + GitHub Actions and built with the vendored scripts in `ci/` + (`build-libmpv-mingw64.sh`, `build-libmpv-macos.sh`). No mpv source or + third-party binaries are stored in this repository. ## Releases @@ -26,7 +26,7 @@ single file) and a `checksums.txt` (SHA-256) for verification. | Asset | Contents | |---|---| | `mpv-natives-windows-x86_64-mpv-.zip` | `libmpv-2.dll` (all dependencies statically linked, mingw64 build) | -| `mpv-natives-macos--mpv-.zip` | `libmpv.dylib` (in preparation) | +| `mpv-natives-macos-universal-mpv-.zip` | `libmpv.dylib` (universal arm64 + x86_64, all dependencies statically linked) | The Windows DLL is fully self-contained: every dependency (ffmpeg, libass, libplacebo, harfbuzz, freetype, fribidi, …) is statically linked into diff --git a/ci/build-libmpv-macos.sh b/ci/build-libmpv-macos.sh new file mode 100644 index 0000000..4c53338 --- /dev/null +++ b/ci/build-libmpv-macos.sh @@ -0,0 +1,248 @@ +#!/bin/bash -e +set -e +# (The shebang -e is not honored when the workflow invokes us via `bash file`, +# so set -e is repeated here to guarantee fail-fast regardless of caller.) +# +# Build a single self-contained libmpv dylib for macOS on the runner's own +# architecture (x86_64 on macos-14, arm64 on macos-15). The workflow runs +# this script on both runner kinds and merges the two dylibs into a +# universal binary with lipo. +# +# Recipe: every dependency is built STATIC and linked into the one dylib +# (same approach as ci/build-libmpv-mingw64.sh for Windows). macOS needs no +# cross-compilation: the host Apple Clang builds natively. No iconv is built +# (macOS ships one), no system frameworks are vendored (CoreAudio, +# VideoToolbox, ... are always present on the host). +# +# Vulkan is intentionally NOT built: jlibmpv renders through an externally +# supplied OpenGL context (JOGL + render API) and never uses the Vulkan video +# backend. CoreAudio is the audio output (ao=coreaudio) and VideoToolbox +# provides hardware decode. +# +# All third-party sources are pinned to the latest stable tag that existed on +# the mpv v0.41.0 release date (2025-12-21), so a rebuild reproduces the same +# binary instead of drifting to git HEAD. + +# The mpv source tree is the current working directory (checked out by CI at +# MPV_REF). Everything is built next to it under macos_prefix/. +prefix_dir=$PWD/macos_prefix +mkdir -p "$prefix_dir" + +wget="wget -nc --progress=bar:force" + +export CC=cc +export CXX=c++ +export AR=ar +export NM=nm +export RANLIB=ranlib + +export CFLAGS="-O2 -pipe -Wall" +export LDFLAGS="" + +# anything that uses pkg-config +export PKG_CONFIG_SYSROOT_DIR="$prefix_dir" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig" + +# --- Pinned dependency versions ------------------------------------------- +# Latest stable tag on or before the mpv v0.41.0 release (2025-12-21). +FFMPEG_VER=n8.0.1 +DAV1D_VER=1.5.2 +LIBPLACEBO_VER=v7.351.0 +LIBASS_VER=0.17.4 +LCMS2_VER=lcms2.17 +ZLIB_VER=1.3.1 +FREETYPE_VER=2.14.1 +FRIBIDI_VER=1.0.16 +HARFBUZZ_VER=12.2.0 + +# Static everywhere: every dependency becomes a .a archive that gets linked +# into the single libmpv dylib. (mpv's own script ships shared libs instead.) +commonflags="--disable-shared --enable-static" + +function builddir { + [ -d "$1/builddir" ] && rm -rf "$1/builddir" + mkdir -p "$1/builddir" + pushd "$1/builddir" +} + +function makeplusinstall { + if [ -f build.ninja ]; then + ninja + DESTDIR="$prefix_dir" ninja install + else + make -j$(sysctl -n hw.ncpu) + make DESTDIR="$prefix_dir" install + fi +} + +function gettar { + local name="${1##*/}" + [ -d "${name%%.*}" ] && return 0 + $wget "$1" + tar -xaf "$name" +} + +# Clone a git dependency at a pinned tag (shallow, tag-checked-out). +# Submodules are fetched shallow too: libplacebo needs its bundled `glad`. +function gitpin { + local url=$1 dest=$2 tag=$3 + [ -d "$dest" ] || git clone --depth=1 --branch "$tag" \ + --recurse-submodules --shallow-submodules "$url" "$dest" + pushd "$dest" + git checkout --quiet "$tag" + popd +} + +function build_if_missing { + local name=${1//-/_} + local mark_var=_${name}_mark + local mark_file=$prefix_dir/${!mark_var} + [ -e "$mark_file" ] && return 0 + echo "::group::Building $1" + _$name + echo "::endgroup::" + if [ ! -e "$mark_file" ]; then + echo "Error: Build of $1 completed but $mark_file was not created." + return 2 + fi +} + +## mpv's dependencies (all static, native build) + +_zlib () { + gettar "https://zlib.net/fossils/zlib-${ZLIB_VER}.tar.gz" + builddir zlib-${ZLIB_VER} + CC=cc AR=ar RANLIB=ranlib make -fMakefile.lib \ + LIBSUFFIX= PREFIX=/ LIBPATH=$prefix_dir/lib install + popd +} +_zlib_mark=lib/libz.a + +_dav1d () { + gitpin https://code.videolan.org/videolan/dav1d.git dav1d "$DAV1D_VER" + builddir dav1d + meson setup .. -Denable_{tools,tests}=false -Ddefault_library=static + makeplusinstall + popd +} +_dav1d_mark=lib/libdav1d.a + +_lcms2 () { + gitpin https://github.com/mm2/Little-CMS.git lcms2 "$LCMS2_VER" + builddir lcms2 + meson setup .. -Dtests=disabled -Dutils=false -Ddefault_library=static + makeplusinstall + popd +} +_lcms2_mark=lib/liblcms2.a + +_ffmpeg () { + gitpin https://github.com/FFmpeg/FFmpeg.git ffmpeg "$FFMPEG_VER" + builddir ffmpeg + local args=( + --pkg-config=pkg-config --target-os=macosx --enable-gpl + --enable-cross-compile \ + --extra-cflags="-I$prefix_dir/include" \ + --extra-ldflags="-L$prefix_dir/lib" \ + $commonflags + --disable-{doc,programs} + --enable-muxer=spdif --enable-encoder=mjpeg,png --enable-libdav1d + ) + ../configure "${args[@]}" + makeplusinstall + popd +} +_ffmpeg_mark=lib/libavcodec.a + +_freetype () { + gettar "https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VER}.tar.xz" + builddir freetype-${FREETYPE_VER} + meson setup .. -Ddefault_library=static + makeplusinstall + popd +} +_freetype_mark=lib/libfreetype.a + +_fribidi () { + gettar "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VER}/fribidi-${FRIBIDI_VER}.tar.xz" + builddir fribidi-${FRIBIDI_VER} + meson setup .. -D{tests,docs}=false -Ddefault_library=static + makeplusinstall + popd +} +_fribidi_mark=lib/libfribidi.a + +_harfbuzz () { + gettar "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VER}/harfbuzz-${HARFBUZZ_VER}.tar.xz" + builddir harfbuzz-${HARFBUZZ_VER} + meson setup .. -Dtests=disabled -Ddefault_library=static + makeplusinstall + popd +} +_harfbuzz_mark=lib/libharfbuzz.a + +_libass () { + gitpin https://github.com/libass/libass.git libass "$LIBASS_VER" + builddir libass + meson setup .. -Ddefault_library=static + makeplusinstall + popd +} +_libass_mark=lib/libass.a + +for x in zlib dav1d lcms2; do + build_if_missing $x +done +for x in ffmpeg freetype fribidi harfbuzz libass; do + build_if_missing $x +done +_libplacebo () { + gitpin https://code.videolan.org/videolan/libplacebo.git libplacebo "$LIBPLACEBO_VER" + builddir libplacebo + # opengl enabled (the only renderer jlibmpv uses); vulkan/d3d11 off + meson setup .. -Ddemos=false -Dopengl=enabled -Dd3d11=disabled \ + -Dvulkan=disabled -Dlcms=disabled -Ddefault_library=static + makeplusinstall + popd +} +_libplacebo_mark=lib/libplacebo.a +build_if_missing libplacebo + +## mpv + +build=macos_build +rm -rf $build + +# libmpv itself is the ONE shared object (default_library=shared), linked +# against the static dependency archives built above. No CLI player, no +# tests, no lua/javascript, no vulkan. OpenGL stays (render API via JOGL); +# coreaudio is the audio output; videotoolbox gives hardware decode. +meson setup $build \ + --buildtype release -Dstrip=true \ + -Ddefault_library=shared \ + -Dlibmpv=true \ + -Dcplayer=false \ + -Dtests=false \ + -Dlua=disabled \ + -Djavascript=disabled \ + -Dshaderc=disabled \ + -Dspirv-cross=disabled \ + -Dvulkan=disabled \ + -Dcocoa=disabled \ + -Dgl-cocoa=disabled \ + -Dcoreaudio=enabled \ + -Dvideotoolbox-pl=enabled +meson compile -C $build + +## Collect the self-contained dylib + +mkdir -p artifact +LIBMPV_DYLIB=$(find $build -maxdepth 1 -name 'libmpv-2.dylib' | head -n 1) +[ -n "$LIBMPV_DYLIB" ] || { echo "ERROR: libmpv dylib not found in $build"; ls -l $build; exit 1; } +cp -pv "$LIBMPV_DYLIB" artifact/ +cp -pv "$build"/*.h artifact/ 2>/dev/null || true + +echo "=== artifact contents ===" +ls -l artifact +echo "=== libmpv-2.dylib load commands (should be system frameworks only) ===" +otool -L "artifact/$(basename "$LIBMPV_DYLIB")" \ No newline at end of file