From e314149f2ff8b4839a74e9c31765ab6b4bd54de5 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 20:21:42 +1000 Subject: [PATCH 01/11] macOS x64 deps: target Monterey (12.0) + minos verification guard (issue #39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deps built on macos-15 default to minos 15.0, so vspipe-bin and the from-source plugins link libc++/libSystem symbols absent on Monterey, causing the "dyld: Symbol not found" crash reported in #39. Pin MACOSX_DEPLOYMENT_TARGET=12.0 for the x64/Intel build only (arm64 is left unchanged — its hardware floor is Big Sur and there is no old arm64 runner). This retargets everything built from source. A new minos verification pass walks every bundled Mach-O and reports anything still newer than the target; it warns by default (STRICT_MIN_OS=1 to fail) so the first CI run surfaces the prebuilt artifacts (Homebrew bottles, ffmpeg, Python) that env-var retargeting can't reach. Co-Authored-By: Claude Opus 4.8 (1M context) --- Scripts/download-deps-macos.sh | 99 +++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index c842095..97123f7 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -49,6 +49,37 @@ fi # Intel Homebrew prefix first in PATH, e.g.: # arch -x86_64 /bin/bash -lc 'PATH=/usr/local/bin:$PATH ./Scripts/download-deps-macos.sh --force' +# ============================================================================ +# Minimum macOS deployment target (issue #39) — x64 / Intel only +# ============================================================================ +# These deps are compiled on the newest available runner (macos-15 / Sequoia). +# Without pinning a deployment target, clang/meson/cmake default to the host SDK +# (minos 15.0) and the binaries link against libc++ / libSystem symbols that do +# not exist on older macOS. That is exactly the "dyld: Symbol not found" crash +# loading vspipe-bin reported on Monterey 12.7.6. +# +# We only commit to back-deploying the **Intel (x64)** bundle, since that is what +# the Monterey-era machines in the field run. Apple Silicon (arm64) is left +# unchanged: its hardware floor is Big Sur 11, those Macs get modern macOS, and +# there is no old arm64 runner to source older Homebrew bottles from anyway. +# +# Exporting MACOSX_DEPLOYMENT_TARGET makes every from-source build (clang's +# -mmacosx-version-min, meson's auto-detection, and CMake's CMAKE_OSX_DEPLOYMENT_TARGET +# fallback) target this floor. Override with $MACOS_MIN_VERSION if needed. +# NOTE: this does NOT retarget prebuilt artifacts the script merely copies +# (Homebrew bottles, evermeet ffmpeg, python-build-standalone, Stefan-Olt plugins) — +# the minos verification guard near the end of this script reports those. +if [ "$ARCH" = "x86_64" ]; then + MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey + export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + # CMake honors the env var as a fallback, but some plugin CMakeLists set their + # own target; pass it explicitly on the cmake lines too (see neo-f3kdb). + export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + echo "Minimum macOS deployment target (x64): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" +else + echo "arm64 build: not pinning a deployment target (Intel-only back-deploy)" +fi + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" DEPS_DIR="$PROJECT_ROOT/deps/$PLATFORM_DIR" @@ -626,7 +657,7 @@ if [ "$ARCH" = "x86_64" ]; then echo ""; echo "=== Building neo-f3kdb (x86_64) ===" rm -rf f3kdb if git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb \ - && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 \ + && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" \ && cmake --build f3kdb/build --config Release; then lib=$(find f3kdb/build -name "*.dylib" -type f | head -1) if [ -n "$lib" ]; then cp "$lib" "$PLUGINS_DIR/libneo-f3kdb.dylib"; echo " Built neo-f3kdb"; else echo " no dylib"; FAILED_PLUGINS+=("neo-f3kdb"); fi @@ -923,7 +954,7 @@ if [ "$ARCH" != "arm64" ]; then rm -rf f3kdb git clone --depth 1 https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb cd f3kdb - cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" + cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" cmake --build build --config Release find build -name "*.dylib" | head -1 | xargs -I {} cp {} "$PLUGINS_DIR/libneo-f3kdb.dylib" cd "$BUILD_DIR" @@ -1431,6 +1462,70 @@ else exit 1 fi +# ---------------------------------------------------------------------------- +# Minimum-OS (minos) verification — x64 only (issue #39) +# ---------------------------------------------------------------------------- +# Only the Intel bundle commits to back-deploying (to Monterey); the arm64 bundle +# is built on/for current macOS, so skip the check there to avoid spurious noise. +if [ "$ARCH" = "x86_64" ]; then +echo "" +echo "Minimum-OS (minos) of every bundled Mach-O (target $MACOS_MIN_VERSION, issue #39):" +# Print the macOS deployment target baked into a Mach-O, reading whichever load +# command it carries: LC_BUILD_VERSION (newer toolchains, "minos X.Y") or +# LC_VERSION_MIN_MACOSX (older, "version X.Y"). Empty for non-Mach-O files. +macho_minos() { + otool -l "$1" 2>/dev/null | awk ' + /LC_BUILD_VERSION/ { inbuild=1; next } + inbuild && /minos/ { print $2; exit } + /LC_VERSION_MIN_MACOSX/ { inmin=1; next } + inmin && /version/ { print $2; exit } + ' +} +# true if version $1 is strictly newer than $2 (e.g. 15.0 > 11.0) +version_gt() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]; } + +TOO_NEW=0 +while IFS= read -r macho; do + [ -f "$macho" ] || continue + file "$macho" 2>/dev/null | grep -q 'Mach-O' || continue + minos=$(macho_minos "$macho") + [ -n "$minos" ] || continue + rel=${macho#"$DEPS_DIR"/} + if version_gt "$minos" "$MACOS_MIN_VERSION"; then + echo " ✗ $rel: minos $minos > target $MACOS_MIN_VERSION" + TOO_NEW=$((TOO_NEW + 1)) + fi +done < <(find "$DEPS_DIR" \( -name '*.dylib' -o -name '*.so' -o -name 'vspipe-bin' -o -name 'ffmpeg' -o -name 'ffprobe' \) -type f) + +if [ "$TOO_NEW" = 0 ]; then + echo " ✓ every bundled binary targets macOS $MACOS_MIN_VERSION or older" +else + echo "" + echo "WARNING: $TOO_NEW binary(ies) above were built for a newer macOS than the" + echo "target ($MACOS_MIN_VERSION) and will fail to load on older systems with a" + echo "'dyld: Symbol not found' error (issue #39)." + echo "" + echo "The MACOSX_DEPLOYMENT_TARGET export fixes everything built from source here." + echo "Anything still flagged is a *prebuilt* artifact this script only copies, so" + echo "it needs its own fix:" + echo " - Homebrew bottles (zimg/fftw/boost/libdvdread/xz): the macos-15 runner" + echo " installs Sequoia bottles. Build these from source with the target set," + echo " or fetch older-OS bottles." + echo " - ffmpeg (evermeet.cx / martin-riedl.de) and Python (python-build-standalone):" + echo " pick a download whose minos is <= $MACOS_MIN_VERSION." + echo " - Stefan-Olt / yuygfgg prebuilt plugins: rebuild from source if too new." + echo "" + # Opt-in hard fail once the prebuilt gaps above are closed, mirroring the + # issue #28 guard. Default to warning so the first CI run still completes and + # reports the full minos picture (including prebuilts we can't retarget by + # env var); flip STRICT_MIN_OS=1 in the workflow once everything is <= target. + if [ "${STRICT_MIN_OS:-0}" = "1" ]; then + echo "STRICT_MIN_OS=1 set — failing the build." + exit 1 + fi +fi +fi # end x86_64-only minos verification + echo "" echo "vspipe architecture:" file "$DEPS_DIR/vapoursynth/vspipe" From 71a222bec6f176e9ffbff2270596797fa25e400a Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 20:30:47 +1000 Subject: [PATCH 02/11] macOS x64 deps: patch vspipe to avoid std::to_chars(double) for Monterey (issue #39) Building vspipe against the new 12.0 target fails to compile: 'to_chars is unavailable: introduced in macOS 13.3' because doubleToString() in src/vspipe/{vsjson,vspipe}.cpp uses the floating-point std::to_chars, whose libc++ implementation only exists on 13.3+. That same symbol is what Monterey's dyld couldn't resolve in #39. Rewrite doubleToString() to emit the shortest round-tripping fixed-notation string via snprintf (verified byte-identical to to_chars fixed across whole numbers, repeating decimals, and the 23.976/29.97/59.94 fps cases). Applied x64-only so the arm64 bundle is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- Scripts/download-deps-macos.sh | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index 97123f7..2c8064f 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -203,6 +203,57 @@ if [ "$FORCE" = true ] || [ ! -f "$DEPS_DIR/vapoursynth/libvapoursynth.dylib" ]; cd "$VS_BUILD_DIR" + # ------------------------------------------------------------------------ + # Back-deploy patch for Monterey (issue #39) — x64 only + # ------------------------------------------------------------------------ + # vspipe's doubleToString() (in src/vspipe/vsjson.cpp and src/vspipe/vspipe.cpp) + # uses std::to_chars(double), whose libc++ implementation was introduced in + # macOS 13.3. Built against a 12.0 deployment target the SDK rejects it + # ('to_chars is unavailable: introduced in macOS 13.3'); shipped from a 15.0 + # build it links a libc++ symbol absent on Monterey -> the "dyld: Symbol not + # found" crash in #39. Rewrite it to emit the shortest round-tripping fixed + # string via snprintf, which back-deploys cleanly. x64 only so the arm64 + # bundle stays byte-for-byte unchanged. + if [ "$ARCH" = "x86_64" ]; then + echo " Patching vspipe doubleToString for Monterey back-deploy (issue #39)..." + for vs_src in src/vspipe/vsjson.cpp src/vspipe/vspipe.cpp; do + VS_SRC="$vs_src" python3 - <<'PYEOF' +import os, sys +path = os.environ["VS_SRC"] +with open(path) as f: + content = f.read() + +old = (" auto res = std::to_chars(buffer, buffer + sizeof(buffer), v, std::chars_format::fixed);\n" + " return std::string(buffer, res.ptr - buffer);") +new = (" // issue #39: std::to_chars(double) needs macOS 13.3+ libc++. Emit the\n" + " // shortest fixed-notation string that round-trips, via snprintf, so\n" + " // vspipe loads on Monterey 12.x.\n" + " for (int prec = 0; prec <= 17; ++prec) {\n" + " std::snprintf(buffer, sizeof(buffer), \"%.*f\", prec, v);\n" + " if (std::strtod(buffer, nullptr) == v)\n" + " return std::string(buffer);\n" + " }\n" + " std::snprintf(buffer, sizeof(buffer), \"%.17f\", v);\n" + " return std::string(buffer);") + +if old not in content: + if "issue #39" in content: + print(f" {path}: already patched") + sys.exit(0) + print(f" ERROR: expected to_chars pattern not found in {path}", file=sys.stderr) + sys.exit(1) + +content = content.replace(old, new) +# Ensure the headers snprintf/strtod need are present (idempotent). +content = content.replace("#include \n", + "#include \n#include \n#include \n", 1) +with open(path, "w") as f: + f.write(content) +print(f" Patched {path}") +PYEOF + done + fi + # Install Cython in our embedded Python for building echo " Installing Cython in embedded Python..." "$PYTHON_BIN" -m pip install --quiet cython From d9af4040576c8eac495ab59cd1712ba75b659d43 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 21:00:51 +1000 Subject: [PATCH 03/11] macOS x64 deps: build on macos-13 to target Ventura (issue #39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abandon the Monterey/12.0 deployment-target approach (it required source- building the Homebrew support libs, incl. an ABI-sensitive boost). Instead set the floor at macOS 13 the simple way: build the x64 deps on the macos-13 runner, so `brew install` pours Ventura bottles and clang defaults to a macOS 13 target. The whole x64 bundle then loads on macOS 13+. Note: this does not help the original #39 reporter on Monterey 12.7.6 — 13 is the new minimum. arm64 is unchanged (still macos-15). Reverts the exploratory deployment-target pin / vspipe patch / minos guard from earlier commits on this branch; download-deps-macos.sh is back to main apart from the runner note. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-deps-macos.yml | 10 +- CLAUDE.md | 7 +- Scripts/download-deps-macos.sh | 156 +------------------------ 3 files changed, 16 insertions(+), 157 deletions(-) diff --git a/.github/workflows/build-deps-macos.yml b/.github/workflows/build-deps-macos.yml index 7f04538..c7efb82 100644 --- a/.github/workflows/build-deps-macos.yml +++ b/.github/workflows/build-deps-macos.yml @@ -3,9 +3,11 @@ name: Build macOS Deps # Builds the VapourSynth/FFmpeg/Python dependency bundles for macOS. # # - arm64 builds natively on macos-15 (Apple Silicon). -# - x64 builds natively on macos-15-intel (the last native Intel image, -# available until ~Aug 2027). FFmpeg comes pre-built from evermeet.cx and -# most plugins from Stefan-Olt; see Scripts/download-deps-macos.sh. +# - x64 builds natively on macos-13 (Ventura, Intel). Building on the oldest +# available Intel image makes brew install pour Ventura bottles and clang +# default to a macOS 13 deployment target, so the whole x64 bundle loads on +# macOS 13+ (issue #39). FFmpeg comes pre-built from evermeet.cx and most +# plugins from Stefan-Olt; see Scripts/download-deps-macos.sh. on: workflow_dispatch: @@ -62,7 +64,7 @@ jobs: build-x64: if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }} - runs-on: macos-15-intel # native Intel image (last one, available until ~Aug 2027) + runs-on: macos-13 # oldest Intel image -> Ventura bottles + macOS 13 minos (issue #39) steps: - uses: actions/checkout@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 0191ced..fa9594a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,7 +143,8 @@ VapourBox/ ``` Both macOS architectures are produced in CI by `build-deps-macos.yml` (arm64 on -`macos-15`, x64 natively on `macos-15-intel`). +`macos-15`, x64 natively on `macos-13` — the oldest Intel image, so brew pours +Ventura bottles and the bundle loads on macOS 13+; see issue #39). ### Build Rust Worker @@ -560,7 +561,7 @@ The `download-deps-windows.ps1` and `download-deps-macos.sh` scripts apply these ### macOS -- Two arches: `deps/macos-arm64` and `deps/macos-x64`, each built natively (arm64 on Apple Silicon, x64 on an Intel Mac / `macos-15-intel`; Rosetta 2 is an optional fallback for building x64 on Apple Silicon — see Download Dependencies). The Flutter `DependencyLocator` and Rust runtime pick the arch at runtime via `uname`. +- Two arches: `deps/macos-arm64` and `deps/macos-x64`, each built natively (arm64 on Apple Silicon, x64 on an Intel Mac / the `macos-13` CI runner — Ventura, so the x64 bundle targets macOS 13+, see issue #39; Rosetta 2 is an optional fallback for building x64 on Apple Silicon — see Download Dependencies). The Flutter `DependencyLocator` and Rust runtime pick the arch at runtime via `uname`. - Fully self-contained deps (no Homebrew at runtime): Python 3.12 (python-build-standalone), VS built from source - Worker sets: `PYTHONHOME`, `PYTHONPATH`, `VAPOURSYNTH_CONF_PATH`, `DYLD_LIBRARY_PATH` - `vspipe` is a wrapper script that generates config dynamically (needed because `VAPOURSYNTH_PLUGIN_PATH` is additive, not a replacement) @@ -688,7 +689,7 @@ Create the app-specific password at appleid.apple.com → Sign-In and Security - Linux deps must be built on Linux (`./Scripts/download-deps-linux.sh`) or via `build-deps-linux.yml` - The macOS CI build (`build-macos.yml`) signs with the Developer ID cert and notarizes — see "macOS Code Signing & Notarization" below. Windows and Linux CI builds remain unsigned. - macOS ships as a **universal** (arm64+x86_64) app by default. `build-macos.yml` takes an `arch` input (`universal` | `both` | `arm64` | `x64`, default `universal`) and fans out via a matrix. Universal = lipo'd worker + Runner built with `ARCHS="arm64 x86_64"`; `both` = two separate single-arch DMGs. The x64 slice cross-compiles on the `macos-15` (arm64) runner. -- Deps are **not** bundled — the app downloads `macos-arm64` or `macos-x64` deps at runtime per `uname`, so a universal app needs **both** deps bundles published. macOS deps are built by `build-deps-macos.yml` (arm64 on `macos-15`, x64 natively on `macos-15-intel`). +- Deps are **not** bundled — the app downloads `macos-arm64` or `macos-x64` deps at runtime per `uname`, so a universal app needs **both** deps bundles published. macOS deps are built by `build-deps-macos.yml` (arm64 on `macos-15`, x64 natively on `macos-13` — Ventura, targeting macOS 13+, see issue #39). - `app/macos/Podfile` reads `VAPOURBOX_ARCHS` (default `arm64`; set to `x86_64` or `arm64 x86_64`); `package-macos.sh --arch universal` sets this and lipo's the worker. - Publishing a new x64 deps zip needs no `deps-version.json` edit — upload the zip and its `.sha256.json` sidecar (the app verifies via the sidecar at download time). diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index 2c8064f..bade47b 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -43,43 +43,14 @@ else exit 1 fi -# NOTE: x64 deps are built natively on an Intel Mac / the macos-15-intel CI -# runner (uname -m reports x86_64 -> macos-x64). To build x64 deps on an Apple +# NOTE: x64 deps are built natively on an Intel Mac / the macos-13 CI runner +# (uname -m reports x86_64 -> macos-x64). macos-13 (Ventura) is used so brew +# pours Ventura bottles and clang targets macOS 13, keeping the bundle loadable +# on 13+ (issue #39). To build x64 deps on an Apple # Silicon Mac instead, run this script translated through Rosetta 2 with an # Intel Homebrew prefix first in PATH, e.g.: # arch -x86_64 /bin/bash -lc 'PATH=/usr/local/bin:$PATH ./Scripts/download-deps-macos.sh --force' -# ============================================================================ -# Minimum macOS deployment target (issue #39) — x64 / Intel only -# ============================================================================ -# These deps are compiled on the newest available runner (macos-15 / Sequoia). -# Without pinning a deployment target, clang/meson/cmake default to the host SDK -# (minos 15.0) and the binaries link against libc++ / libSystem symbols that do -# not exist on older macOS. That is exactly the "dyld: Symbol not found" crash -# loading vspipe-bin reported on Monterey 12.7.6. -# -# We only commit to back-deploying the **Intel (x64)** bundle, since that is what -# the Monterey-era machines in the field run. Apple Silicon (arm64) is left -# unchanged: its hardware floor is Big Sur 11, those Macs get modern macOS, and -# there is no old arm64 runner to source older Homebrew bottles from anyway. -# -# Exporting MACOSX_DEPLOYMENT_TARGET makes every from-source build (clang's -# -mmacosx-version-min, meson's auto-detection, and CMake's CMAKE_OSX_DEPLOYMENT_TARGET -# fallback) target this floor. Override with $MACOS_MIN_VERSION if needed. -# NOTE: this does NOT retarget prebuilt artifacts the script merely copies -# (Homebrew bottles, evermeet ffmpeg, python-build-standalone, Stefan-Olt plugins) — -# the minos verification guard near the end of this script reports those. -if [ "$ARCH" = "x86_64" ]; then - MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey - export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" - # CMake honors the env var as a fallback, but some plugin CMakeLists set their - # own target; pass it explicitly on the cmake lines too (see neo-f3kdb). - export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" - echo "Minimum macOS deployment target (x64): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" -else - echo "arm64 build: not pinning a deployment target (Intel-only back-deploy)" -fi - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" DEPS_DIR="$PROJECT_ROOT/deps/$PLATFORM_DIR" @@ -203,57 +174,6 @@ if [ "$FORCE" = true ] || [ ! -f "$DEPS_DIR/vapoursynth/libvapoursynth.dylib" ]; cd "$VS_BUILD_DIR" - # ------------------------------------------------------------------------ - # Back-deploy patch for Monterey (issue #39) — x64 only - # ------------------------------------------------------------------------ - # vspipe's doubleToString() (in src/vspipe/vsjson.cpp and src/vspipe/vspipe.cpp) - # uses std::to_chars(double), whose libc++ implementation was introduced in - # macOS 13.3. Built against a 12.0 deployment target the SDK rejects it - # ('to_chars is unavailable: introduced in macOS 13.3'); shipped from a 15.0 - # build it links a libc++ symbol absent on Monterey -> the "dyld: Symbol not - # found" crash in #39. Rewrite it to emit the shortest round-tripping fixed - # string via snprintf, which back-deploys cleanly. x64 only so the arm64 - # bundle stays byte-for-byte unchanged. - if [ "$ARCH" = "x86_64" ]; then - echo " Patching vspipe doubleToString for Monterey back-deploy (issue #39)..." - for vs_src in src/vspipe/vsjson.cpp src/vspipe/vspipe.cpp; do - VS_SRC="$vs_src" python3 - <<'PYEOF' -import os, sys -path = os.environ["VS_SRC"] -with open(path) as f: - content = f.read() - -old = (" auto res = std::to_chars(buffer, buffer + sizeof(buffer), v, std::chars_format::fixed);\n" - " return std::string(buffer, res.ptr - buffer);") -new = (" // issue #39: std::to_chars(double) needs macOS 13.3+ libc++. Emit the\n" - " // shortest fixed-notation string that round-trips, via snprintf, so\n" - " // vspipe loads on Monterey 12.x.\n" - " for (int prec = 0; prec <= 17; ++prec) {\n" - " std::snprintf(buffer, sizeof(buffer), \"%.*f\", prec, v);\n" - " if (std::strtod(buffer, nullptr) == v)\n" - " return std::string(buffer);\n" - " }\n" - " std::snprintf(buffer, sizeof(buffer), \"%.17f\", v);\n" - " return std::string(buffer);") - -if old not in content: - if "issue #39" in content: - print(f" {path}: already patched") - sys.exit(0) - print(f" ERROR: expected to_chars pattern not found in {path}", file=sys.stderr) - sys.exit(1) - -content = content.replace(old, new) -# Ensure the headers snprintf/strtod need are present (idempotent). -content = content.replace("#include \n", - "#include \n#include \n#include \n", 1) -with open(path, "w") as f: - f.write(content) -print(f" Patched {path}") -PYEOF - done - fi - # Install Cython in our embedded Python for building echo " Installing Cython in embedded Python..." "$PYTHON_BIN" -m pip install --quiet cython @@ -708,7 +628,7 @@ if [ "$ARCH" = "x86_64" ]; then echo ""; echo "=== Building neo-f3kdb (x86_64) ===" rm -rf f3kdb if git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb \ - && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" \ + && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 \ && cmake --build f3kdb/build --config Release; then lib=$(find f3kdb/build -name "*.dylib" -type f | head -1) if [ -n "$lib" ]; then cp "$lib" "$PLUGINS_DIR/libneo-f3kdb.dylib"; echo " Built neo-f3kdb"; else echo " no dylib"; FAILED_PLUGINS+=("neo-f3kdb"); fi @@ -1005,7 +925,7 @@ if [ "$ARCH" != "arm64" ]; then rm -rf f3kdb git clone --depth 1 https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb cd f3kdb - cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" cmake --build build --config Release find build -name "*.dylib" | head -1 | xargs -I {} cp {} "$PLUGINS_DIR/libneo-f3kdb.dylib" cd "$BUILD_DIR" @@ -1513,70 +1433,6 @@ else exit 1 fi -# ---------------------------------------------------------------------------- -# Minimum-OS (minos) verification — x64 only (issue #39) -# ---------------------------------------------------------------------------- -# Only the Intel bundle commits to back-deploying (to Monterey); the arm64 bundle -# is built on/for current macOS, so skip the check there to avoid spurious noise. -if [ "$ARCH" = "x86_64" ]; then -echo "" -echo "Minimum-OS (minos) of every bundled Mach-O (target $MACOS_MIN_VERSION, issue #39):" -# Print the macOS deployment target baked into a Mach-O, reading whichever load -# command it carries: LC_BUILD_VERSION (newer toolchains, "minos X.Y") or -# LC_VERSION_MIN_MACOSX (older, "version X.Y"). Empty for non-Mach-O files. -macho_minos() { - otool -l "$1" 2>/dev/null | awk ' - /LC_BUILD_VERSION/ { inbuild=1; next } - inbuild && /minos/ { print $2; exit } - /LC_VERSION_MIN_MACOSX/ { inmin=1; next } - inmin && /version/ { print $2; exit } - ' -} -# true if version $1 is strictly newer than $2 (e.g. 15.0 > 11.0) -version_gt() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]; } - -TOO_NEW=0 -while IFS= read -r macho; do - [ -f "$macho" ] || continue - file "$macho" 2>/dev/null | grep -q 'Mach-O' || continue - minos=$(macho_minos "$macho") - [ -n "$minos" ] || continue - rel=${macho#"$DEPS_DIR"/} - if version_gt "$minos" "$MACOS_MIN_VERSION"; then - echo " ✗ $rel: minos $minos > target $MACOS_MIN_VERSION" - TOO_NEW=$((TOO_NEW + 1)) - fi -done < <(find "$DEPS_DIR" \( -name '*.dylib' -o -name '*.so' -o -name 'vspipe-bin' -o -name 'ffmpeg' -o -name 'ffprobe' \) -type f) - -if [ "$TOO_NEW" = 0 ]; then - echo " ✓ every bundled binary targets macOS $MACOS_MIN_VERSION or older" -else - echo "" - echo "WARNING: $TOO_NEW binary(ies) above were built for a newer macOS than the" - echo "target ($MACOS_MIN_VERSION) and will fail to load on older systems with a" - echo "'dyld: Symbol not found' error (issue #39)." - echo "" - echo "The MACOSX_DEPLOYMENT_TARGET export fixes everything built from source here." - echo "Anything still flagged is a *prebuilt* artifact this script only copies, so" - echo "it needs its own fix:" - echo " - Homebrew bottles (zimg/fftw/boost/libdvdread/xz): the macos-15 runner" - echo " installs Sequoia bottles. Build these from source with the target set," - echo " or fetch older-OS bottles." - echo " - ffmpeg (evermeet.cx / martin-riedl.de) and Python (python-build-standalone):" - echo " pick a download whose minos is <= $MACOS_MIN_VERSION." - echo " - Stefan-Olt / yuygfgg prebuilt plugins: rebuild from source if too new." - echo "" - # Opt-in hard fail once the prebuilt gaps above are closed, mirroring the - # issue #28 guard. Default to warning so the first CI run still completes and - # reports the full minos picture (including prebuilts we can't retarget by - # env var); flip STRICT_MIN_OS=1 in the workflow once everything is <= target. - if [ "${STRICT_MIN_OS:-0}" = "1" ]; then - echo "STRICT_MIN_OS=1 set — failing the build." - exit 1 - fi -fi -fi # end x86_64-only minos verification - echo "" echo "vspipe architecture:" file "$DEPS_DIR/vapoursynth/vspipe" From d844668b967aa535c9452221c872bdbfcbde200d Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 21:26:33 +1000 Subject: [PATCH 04/11] Revert macos-13 runner swap: macos-13 image was retired by GitHub (Dec 2025) macos-13 is no longer a valid hosted runner label (jobs hang queued forever). The only Intel runner is macos-15-intel. Reverting to keep the deps workflow working; lowering the x64 floor needs the source-build approach instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-deps-macos.yml | 10 ++++------ CLAUDE.md | 7 +++---- Scripts/download-deps-macos.sh | 6 ++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-deps-macos.yml b/.github/workflows/build-deps-macos.yml index c7efb82..7f04538 100644 --- a/.github/workflows/build-deps-macos.yml +++ b/.github/workflows/build-deps-macos.yml @@ -3,11 +3,9 @@ name: Build macOS Deps # Builds the VapourSynth/FFmpeg/Python dependency bundles for macOS. # # - arm64 builds natively on macos-15 (Apple Silicon). -# - x64 builds natively on macos-13 (Ventura, Intel). Building on the oldest -# available Intel image makes brew install pour Ventura bottles and clang -# default to a macOS 13 deployment target, so the whole x64 bundle loads on -# macOS 13+ (issue #39). FFmpeg comes pre-built from evermeet.cx and most -# plugins from Stefan-Olt; see Scripts/download-deps-macos.sh. +# - x64 builds natively on macos-15-intel (the last native Intel image, +# available until ~Aug 2027). FFmpeg comes pre-built from evermeet.cx and +# most plugins from Stefan-Olt; see Scripts/download-deps-macos.sh. on: workflow_dispatch: @@ -64,7 +62,7 @@ jobs: build-x64: if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }} - runs-on: macos-13 # oldest Intel image -> Ventura bottles + macOS 13 minos (issue #39) + runs-on: macos-15-intel # native Intel image (last one, available until ~Aug 2027) steps: - uses: actions/checkout@v4 diff --git a/CLAUDE.md b/CLAUDE.md index fa9594a..0191ced 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,8 +143,7 @@ VapourBox/ ``` Both macOS architectures are produced in CI by `build-deps-macos.yml` (arm64 on -`macos-15`, x64 natively on `macos-13` — the oldest Intel image, so brew pours -Ventura bottles and the bundle loads on macOS 13+; see issue #39). +`macos-15`, x64 natively on `macos-15-intel`). ### Build Rust Worker @@ -561,7 +560,7 @@ The `download-deps-windows.ps1` and `download-deps-macos.sh` scripts apply these ### macOS -- Two arches: `deps/macos-arm64` and `deps/macos-x64`, each built natively (arm64 on Apple Silicon, x64 on an Intel Mac / the `macos-13` CI runner — Ventura, so the x64 bundle targets macOS 13+, see issue #39; Rosetta 2 is an optional fallback for building x64 on Apple Silicon — see Download Dependencies). The Flutter `DependencyLocator` and Rust runtime pick the arch at runtime via `uname`. +- Two arches: `deps/macos-arm64` and `deps/macos-x64`, each built natively (arm64 on Apple Silicon, x64 on an Intel Mac / `macos-15-intel`; Rosetta 2 is an optional fallback for building x64 on Apple Silicon — see Download Dependencies). The Flutter `DependencyLocator` and Rust runtime pick the arch at runtime via `uname`. - Fully self-contained deps (no Homebrew at runtime): Python 3.12 (python-build-standalone), VS built from source - Worker sets: `PYTHONHOME`, `PYTHONPATH`, `VAPOURSYNTH_CONF_PATH`, `DYLD_LIBRARY_PATH` - `vspipe` is a wrapper script that generates config dynamically (needed because `VAPOURSYNTH_PLUGIN_PATH` is additive, not a replacement) @@ -689,7 +688,7 @@ Create the app-specific password at appleid.apple.com → Sign-In and Security - Linux deps must be built on Linux (`./Scripts/download-deps-linux.sh`) or via `build-deps-linux.yml` - The macOS CI build (`build-macos.yml`) signs with the Developer ID cert and notarizes — see "macOS Code Signing & Notarization" below. Windows and Linux CI builds remain unsigned. - macOS ships as a **universal** (arm64+x86_64) app by default. `build-macos.yml` takes an `arch` input (`universal` | `both` | `arm64` | `x64`, default `universal`) and fans out via a matrix. Universal = lipo'd worker + Runner built with `ARCHS="arm64 x86_64"`; `both` = two separate single-arch DMGs. The x64 slice cross-compiles on the `macos-15` (arm64) runner. -- Deps are **not** bundled — the app downloads `macos-arm64` or `macos-x64` deps at runtime per `uname`, so a universal app needs **both** deps bundles published. macOS deps are built by `build-deps-macos.yml` (arm64 on `macos-15`, x64 natively on `macos-13` — Ventura, targeting macOS 13+, see issue #39). +- Deps are **not** bundled — the app downloads `macos-arm64` or `macos-x64` deps at runtime per `uname`, so a universal app needs **both** deps bundles published. macOS deps are built by `build-deps-macos.yml` (arm64 on `macos-15`, x64 natively on `macos-15-intel`). - `app/macos/Podfile` reads `VAPOURBOX_ARCHS` (default `arm64`; set to `x86_64` or `arm64 x86_64`); `package-macos.sh --arch universal` sets this and lipo's the worker. - Publishing a new x64 deps zip needs no `deps-version.json` edit — upload the zip and its `.sha256.json` sidecar (the app verifies via the sidecar at download time). diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index bade47b..c842095 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -43,10 +43,8 @@ else exit 1 fi -# NOTE: x64 deps are built natively on an Intel Mac / the macos-13 CI runner -# (uname -m reports x86_64 -> macos-x64). macos-13 (Ventura) is used so brew -# pours Ventura bottles and clang targets macOS 13, keeping the bundle loadable -# on 13+ (issue #39). To build x64 deps on an Apple +# NOTE: x64 deps are built natively on an Intel Mac / the macos-15-intel CI +# runner (uname -m reports x86_64 -> macos-x64). To build x64 deps on an Apple # Silicon Mac instead, run this script translated through Rosetta 2 with an # Intel Homebrew prefix first in PATH, e.g.: # arch -x86_64 /bin/bash -lc 'PATH=/usr/local/bin:$PATH ./Scripts/download-deps-macos.sh --force' From 352a320e601f1c83b4d1fbfd6c0adfd6a365fd53 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 21:47:41 +1000 Subject: [PATCH 05/11] macOS x64 deps: source-build support libs to target Monterey 12.0 (issue #39) The only hosted Intel runner is macos-15-intel (macos-13 was retired), where brew pours minos 14/15 bottles that won't load on macOS 12. So build the five bundled support libraries from source with MACOSX_DEPLOYMENT_TARGET=12.0: - xz/liblzma 5.4.6, fftw 3.3.10 (float+threads), zimg 3.0.5, libdvdread 6.1.3 -> stable C ABIs, dropped in over the Homebrew copies before the repoint pass - boost 1.85.0 (filesystem, atomic) -> ABI-sensitive, so nnedi3cl/knlmeanscl are now compiled against it via BOOST_ROOT="$SRCLIB" Combined with the deployment-target pin + the vspipe std::to_chars patch (restored), the whole x64 bundle now targets macOS 12. The minos guard runs with STRICT_MIN_OS=1 in CI so the build fails if anything still exceeds 12.0. arm64 is untouched (the entire block is x86_64-gated). All five build recipes validated locally (correct dylib names + @rpath ids). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-deps-macos.yml | 6 +- Scripts/download-deps-macos.sh | 267 ++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-deps-macos.yml b/.github/workflows/build-deps-macos.yml index 7f04538..1b56487 100644 --- a/.github/workflows/build-deps-macos.yml +++ b/.github/workflows/build-deps-macos.yml @@ -62,11 +62,15 @@ jobs: build-x64: if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }} - runs-on: macos-15-intel # native Intel image (last one, available until ~Aug 2027) + runs-on: macos-15-intel # only hosted Intel image (macos-13 retired; last one until ~Aug 2027) steps: - uses: actions/checkout@v4 - name: Build dependencies (x64, native) + # STRICT_MIN_OS=1: fail the build if any bundled Mach-O targets newer than + # the macOS 12 floor (issue #39) instead of silently shipping it. + env: + STRICT_MIN_OS: "1" run: ./Scripts/download-deps-macos.sh --force - name: Package dependencies diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index c842095..2645212 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -49,6 +49,37 @@ fi # Intel Homebrew prefix first in PATH, e.g.: # arch -x86_64 /bin/bash -lc 'PATH=/usr/local/bin:$PATH ./Scripts/download-deps-macos.sh --force' +# ============================================================================ +# Minimum macOS deployment target (issue #39) — x64 / Intel only +# ============================================================================ +# These deps are compiled on the newest available runner (macos-15 / Sequoia). +# Without pinning a deployment target, clang/meson/cmake default to the host SDK +# (minos 15.0) and the binaries link against libc++ / libSystem symbols that do +# not exist on older macOS. That is exactly the "dyld: Symbol not found" crash +# loading vspipe-bin reported on Monterey 12.7.6. +# +# We only commit to back-deploying the **Intel (x64)** bundle, since that is what +# the Monterey-era machines in the field run. Apple Silicon (arm64) is left +# unchanged: its hardware floor is Big Sur 11, those Macs get modern macOS, and +# there is no old arm64 runner to source older Homebrew bottles from anyway. +# +# Exporting MACOSX_DEPLOYMENT_TARGET makes every from-source build (clang's +# -mmacosx-version-min, meson's auto-detection, and CMake's CMAKE_OSX_DEPLOYMENT_TARGET +# fallback) target this floor. Override with $MACOS_MIN_VERSION if needed. +# NOTE: this does NOT retarget prebuilt artifacts the script merely copies +# (Homebrew bottles, evermeet ffmpeg, python-build-standalone, Stefan-Olt plugins) — +# the minos verification guard near the end of this script reports those. +if [ "$ARCH" = "x86_64" ]; then + MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey + export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + # CMake honors the env var as a fallback, but some plugin CMakeLists set their + # own target; pass it explicitly on the cmake lines too (see neo-f3kdb). + export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + echo "Minimum macOS deployment target (x64): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" +else + echo "arm64 build: not pinning a deployment target (Intel-only back-deploy)" +fi + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" DEPS_DIR="$PROJECT_ROOT/deps/$PLATFORM_DIR" @@ -117,6 +148,84 @@ for dep in "${BREW_DEPS[@]}"; do fi done +# ============================================================================ +# Build redistributable support libraries from source (x64 only, issue #39) +# ============================================================================ +# On the only available Intel runner (macos-15-intel) `brew install` pours +# minos 14/15 bottles that won't load on macOS 12. Build the few libraries we +# *bundle* from source so they inherit MACOSX_DEPLOYMENT_TARGET (12.0): +# zimg/fftw/libdvdread/xz - stable C ABIs; dropped in over the Homebrew +# copies just before the repoint pass. +# boost (filesystem,atomic) - ABI-sensitive, so the OpenCL plugins +# (nnedi3cl/knlmeanscl) are ALSO compiled against +# this build via BOOST_ROOT="$SRCLIB" below. +# arm64 is untouched (no deployment-target pin -> this whole block is skipped). +SRCLIB="$BUILD_DIR/srclib" +if [ "$ARCH" = "x86_64" ]; then + echo "" + echo "=== Building support libraries from source (target $MACOS_MIN_VERSION) ===" + NPROC=$(sysctl -n hw.ncpu) + SRC_TMP="$BUILD_DIR/srclib-src" + mkdir -p "$SRCLIB" "$SRC_TMP" + cd "$SRC_TMP" + + # xz / liblzma 5.4.6 (pre-5.6 to avoid the 5.6.x backdoor) -> liblzma.5.dylib + echo " Building xz (liblzma 5.4.6)..." + curl -fsSL "https://github.com/tukaani-project/xz/releases/download/v5.4.6/xz-5.4.6.tar.gz" -o xz.tar.gz + tar xzf xz.tar.gz && cd xz-5.4.6 + ./configure --prefix="$SRCLIB" --enable-shared --disable-static --disable-doc -q + make -j"$NPROC" >/dev/null && make install >/dev/null + cd "$SRC_TMP" + + # fftw 3.3.10, single precision + threads -> libfftw3f.3 + libfftw3f_threads.3 + echo " Building fftw (3.3.10, float)..." + curl -fsSL "https://www.fftw.org/fftw-3.3.10.tar.gz" -o fftw.tar.gz + tar xzf fftw.tar.gz && cd fftw-3.3.10 + ./configure --prefix="$SRCLIB" --enable-float --enable-threads --enable-shared \ + --disable-static --disable-fortran --disable-doc -q + make -j"$NPROC" >/dev/null && make install >/dev/null + cd "$SRC_TMP" + + # zimg 3.0.5 -> libzimg.2.dylib (VapourSynth core resize; stable C ABI) + echo " Building zimg (3.0.5)..." + curl -fsSL "https://github.com/sekrit-twc/zimg/archive/refs/tags/release-3.0.5.tar.gz" -o zimg.tar.gz + tar xzf zimg.tar.gz && cd zimg-release-3.0.5 + ./autogen.sh + ./configure --prefix="$SRCLIB" --enable-shared --disable-static -q + make -j"$NPROC" >/dev/null && make install >/dev/null + cd "$SRC_TMP" + + # libdvdread 6.1.3 -> libdvdread.dylib (worker dlopens it, resolves by name) + echo " Building libdvdread (6.1.3)..." + curl -fsSL "https://download.videolan.org/pub/videolan/libdvdread/6.1.3/libdvdread-6.1.3.tar.bz2" -o dvdread.tar.bz2 + tar xjf dvdread.tar.bz2 && cd libdvdread-6.1.3 + ./configure --prefix="$SRCLIB" --enable-shared --disable-static -q + make -j"$NPROC" >/dev/null && make install >/dev/null + cd "$SRC_TMP" + + # boost 1.85.0, just filesystem + atomic -> libboost_filesystem/_atomic.dylib + echo " Building boost (1.85.0: filesystem, atomic)..." + curl -fsSL "https://archives.boost.io/release/1.85.0/source/boost_1_85_0.tar.bz2" -o boost.tar.bz2 + tar xjf boost.tar.bz2 && cd boost_1_85_0 + ./bootstrap.sh --with-libraries=filesystem,atomic --prefix="$SRCLIB" >/dev/null + ./b2 -j"$NPROC" --with-filesystem --with-atomic link=shared \ + cxxflags="-mmacosx-version-min=$MACOS_MIN_VERSION" \ + linkflags="-mmacosx-version-min=$MACOS_MIN_VERSION" \ + install >/dev/null + cd "$BUILD_DIR" + + # boost is linked by the OpenCL plugins built later; give it an @rpath id so + # those plugins record a *repointable* reference (the repoint pass skips refs + # that are already @loader_path/...). zimg/fftw/dvdread/lzma get their ids + # normalized when they're dropped into the bundle. + for b in libboost_filesystem libboost_atomic; do + [ -f "$SRCLIB/lib/$b.dylib" ] && install_name_tool -id "@rpath/$b.dylib" "$SRCLIB/lib/$b.dylib" + done + + echo " Built support libs:" + ls -1 "$SRCLIB"/lib/*.dylib 2>/dev/null | sed 's/^/ /' +fi + # ============================================================================ # Download and embed Python framework # ============================================================================ @@ -172,6 +281,57 @@ if [ "$FORCE" = true ] || [ ! -f "$DEPS_DIR/vapoursynth/libvapoursynth.dylib" ]; cd "$VS_BUILD_DIR" + # ------------------------------------------------------------------------ + # Back-deploy patch for Monterey (issue #39) — x64 only + # ------------------------------------------------------------------------ + # vspipe's doubleToString() (in src/vspipe/vsjson.cpp and src/vspipe/vspipe.cpp) + # uses std::to_chars(double), whose libc++ implementation was introduced in + # macOS 13.3. Built against a 12.0 deployment target the SDK rejects it + # ('to_chars is unavailable: introduced in macOS 13.3'); shipped from a 15.0 + # build it links a libc++ symbol absent on Monterey -> the "dyld: Symbol not + # found" crash in #39. Rewrite it to emit the shortest round-tripping fixed + # string via snprintf, which back-deploys cleanly. x64 only so the arm64 + # bundle stays byte-for-byte unchanged. + if [ "$ARCH" = "x86_64" ]; then + echo " Patching vspipe doubleToString for Monterey back-deploy (issue #39)..." + for vs_src in src/vspipe/vsjson.cpp src/vspipe/vspipe.cpp; do + VS_SRC="$vs_src" python3 - <<'PYEOF' +import os, sys +path = os.environ["VS_SRC"] +with open(path) as f: + content = f.read() + +old = (" auto res = std::to_chars(buffer, buffer + sizeof(buffer), v, std::chars_format::fixed);\n" + " return std::string(buffer, res.ptr - buffer);") +new = (" // issue #39: std::to_chars(double) needs macOS 13.3+ libc++. Emit the\n" + " // shortest fixed-notation string that round-trips, via snprintf, so\n" + " // vspipe loads on Monterey 12.x.\n" + " for (int prec = 0; prec <= 17; ++prec) {\n" + " std::snprintf(buffer, sizeof(buffer), \"%.*f\", prec, v);\n" + " if (std::strtod(buffer, nullptr) == v)\n" + " return std::string(buffer);\n" + " }\n" + " std::snprintf(buffer, sizeof(buffer), \"%.17f\", v);\n" + " return std::string(buffer);") + +if old not in content: + if "issue #39" in content: + print(f" {path}: already patched") + sys.exit(0) + print(f" ERROR: expected to_chars pattern not found in {path}", file=sys.stderr) + sys.exit(1) + +content = content.replace(old, new) +# Ensure the headers snprintf/strtod need are present (idempotent). +content = content.replace("#include \n", + "#include \n#include \n#include \n", 1) +with open(path, "w") as f: + f.write(content) +print(f" Patched {path}") +PYEOF + done + fi + # Install Cython in our embedded Python for building echo " Installing Cython in embedded Python..." "$PYTHON_BIN" -m pip install --quiet cython @@ -626,7 +786,7 @@ if [ "$ARCH" = "x86_64" ]; then echo ""; echo "=== Building neo-f3kdb (x86_64) ===" rm -rf f3kdb if git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb \ - && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 \ + && cmake -S f3kdb -B f3kdb/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" \ && cmake --build f3kdb/build --config Release; then lib=$(find f3kdb/build -name "*.dylib" -type f | head -1) if [ -n "$lib" ]; then cp "$lib" "$PLUGINS_DIR/libneo-f3kdb.dylib"; echo " Built neo-f3kdb"; else echo " no dylib"; FAILED_PLUGINS+=("neo-f3kdb"); fi @@ -641,7 +801,7 @@ if [ "$ARCH" = "x86_64" ]; then echo ""; echo "=== Building NNEDI3CL (x86_64) ===" rm -rf nnedi3cl if git clone --depth 1 https://github.com/HomeOfVapourSynthEvolution/VapourSynth-NNEDI3CL.git nnedi3cl \ - && PKG_CONFIG_PATH="$VS_PC_DIR:${PKG_CONFIG_PATH:-}" BOOST_ROOT="$BREW_PREFIX" \ + && PKG_CONFIG_PATH="$VS_PC_DIR:${PKG_CONFIG_PATH:-}" BOOST_ROOT="$SRCLIB" \ meson setup nnedi3cl/build nnedi3cl --buildtype=release \ && ninja -C nnedi3cl/build; then lib=$(find nnedi3cl/build -name "*.dylib" -type f | head -1) @@ -765,7 +925,7 @@ PYEOF echo ""; echo "=== Building KNLMeansCL (x86_64) ===" rm -rf knlmeanscl if git clone --depth 1 https://github.com/Khanattila/KNLMeansCL.git knlmeanscl \ - && PKG_CONFIG_PATH="$VS_PC_DIR:${PKG_CONFIG_PATH:-}" BOOST_ROOT="$BREW_PREFIX" \ + && PKG_CONFIG_PATH="$VS_PC_DIR:${PKG_CONFIG_PATH:-}" BOOST_ROOT="$SRCLIB" \ meson setup knlmeanscl/build knlmeanscl --buildtype=release \ && ninja -C knlmeanscl/build; then lib=$(find knlmeanscl/build -name "*.dylib" -type f | head -1) @@ -923,7 +1083,7 @@ if [ "$ARCH" != "arm64" ]; then rm -rf f3kdb git clone --depth 1 https://github.com/HomeOfAviSynthPlusEvolution/neo_f3kdb.git f3kdb cd f3kdb - cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" + cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$ARCH" -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" cmake --build build --config Release find build -name "*.dylib" | head -1 | xargs -I {} cp {} "$PLUGINS_DIR/libneo-f3kdb.dylib" cd "$BUILD_DIR" @@ -1237,6 +1397,41 @@ else: EOF fi +# ============================================================================ +# Replace the bundled Homebrew support libs with the source builds (x64, #39) +# ============================================================================ +# The copies above pulled Homebrew bottles (minos 14/15). Overwrite them with +# the minos-12 source builds from $SRCLIB so the x64 bundle loads on macOS 12. +# Done here -- after all plugin builds, before the repoint passes -- so the +# existing @loader_path rewiring and signing apply to them uniformly. The +# OpenCL plugins were already compiled against this same boost (BOOST_ROOT). +if [ "$ARCH" = "x86_64" ] && [ -d "$SRCLIB/lib" ]; then + echo "" + echo "=== Replacing Homebrew support libs with source builds (target $MACOS_MIN_VERSION) ===" + # zimg sits next to libvapoursynth and isn't covered by the repoint passes, + # so set its id explicitly here. libvapoursynth already refers to it via + # @loader_path/libzimg.dylib, and zimg's C ABI (libzimg.2) is a safe drop-in. + if [ -f "$SRCLIB/lib/libzimg.2.dylib" ]; then + cp -f "$SRCLIB/lib/libzimg.2.dylib" "$DEPS_DIR/vapoursynth/libzimg.dylib" + install_name_tool -id "@loader_path/libzimg.dylib" "$DEPS_DIR/vapoursynth/libzimg.dylib" 2>/dev/null || true + codesign -s - -f "$DEPS_DIR/vapoursynth/libzimg.dylib" 2>/dev/null || true + echo " zimg -> vapoursynth/libzimg.dylib" + fi + # The remaining libs live in lib/; the repoint passes below normalize their + # ids and inter-references. cp -f follows the versioned symlinks. + for libname in libfftw3f.3.dylib libfftw3f_threads.3.dylib libdvdread.dylib \ + liblzma.5.dylib libboost_filesystem.dylib libboost_atomic.dylib; do + if [ -f "$SRCLIB/lib/$libname" ]; then + cp -f "$SRCLIB/lib/$libname" "$LIB_DIR/$libname" + codesign -s - -f "$LIB_DIR/$libname" 2>/dev/null || true + echo " $libname -> lib/$libname" + else + echo " ERROR: expected source lib $SRCLIB/lib/$libname is missing" >&2 + exit 1 + fi + done +fi + # ============================================================================ # Repoint plugin support-lib references at the bundled copies in lib/ # ============================================================================ @@ -1431,6 +1626,70 @@ else exit 1 fi +# ---------------------------------------------------------------------------- +# Minimum-OS (minos) verification — x64 only (issue #39) +# ---------------------------------------------------------------------------- +# Only the Intel bundle commits to back-deploying (to Monterey); the arm64 bundle +# is built on/for current macOS, so skip the check there to avoid spurious noise. +if [ "$ARCH" = "x86_64" ]; then +echo "" +echo "Minimum-OS (minos) of every bundled Mach-O (target $MACOS_MIN_VERSION, issue #39):" +# Print the macOS deployment target baked into a Mach-O, reading whichever load +# command it carries: LC_BUILD_VERSION (newer toolchains, "minos X.Y") or +# LC_VERSION_MIN_MACOSX (older, "version X.Y"). Empty for non-Mach-O files. +macho_minos() { + otool -l "$1" 2>/dev/null | awk ' + /LC_BUILD_VERSION/ { inbuild=1; next } + inbuild && /minos/ { print $2; exit } + /LC_VERSION_MIN_MACOSX/ { inmin=1; next } + inmin && /version/ { print $2; exit } + ' +} +# true if version $1 is strictly newer than $2 (e.g. 15.0 > 11.0) +version_gt() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]; } + +TOO_NEW=0 +while IFS= read -r macho; do + [ -f "$macho" ] || continue + file "$macho" 2>/dev/null | grep -q 'Mach-O' || continue + minos=$(macho_minos "$macho") + [ -n "$minos" ] || continue + rel=${macho#"$DEPS_DIR"/} + if version_gt "$minos" "$MACOS_MIN_VERSION"; then + echo " ✗ $rel: minos $minos > target $MACOS_MIN_VERSION" + TOO_NEW=$((TOO_NEW + 1)) + fi +done < <(find "$DEPS_DIR" \( -name '*.dylib' -o -name '*.so' -o -name 'vspipe-bin' -o -name 'ffmpeg' -o -name 'ffprobe' \) -type f) + +if [ "$TOO_NEW" = 0 ]; then + echo " ✓ every bundled binary targets macOS $MACOS_MIN_VERSION or older" +else + echo "" + echo "WARNING: $TOO_NEW binary(ies) above were built for a newer macOS than the" + echo "target ($MACOS_MIN_VERSION) and will fail to load on older systems with a" + echo "'dyld: Symbol not found' error (issue #39)." + echo "" + echo "The MACOSX_DEPLOYMENT_TARGET export fixes everything built from source here." + echo "Anything still flagged is a *prebuilt* artifact this script only copies, so" + echo "it needs its own fix:" + echo " - Homebrew bottles (zimg/fftw/boost/libdvdread/xz): the macos-15 runner" + echo " installs Sequoia bottles. Build these from source with the target set," + echo " or fetch older-OS bottles." + echo " - ffmpeg (evermeet.cx / martin-riedl.de) and Python (python-build-standalone):" + echo " pick a download whose minos is <= $MACOS_MIN_VERSION." + echo " - Stefan-Olt / yuygfgg prebuilt plugins: rebuild from source if too new." + echo "" + # Opt-in hard fail once the prebuilt gaps above are closed, mirroring the + # issue #28 guard. Default to warning so the first CI run still completes and + # reports the full minos picture (including prebuilts we can't retarget by + # env var); flip STRICT_MIN_OS=1 in the workflow once everything is <= target. + if [ "${STRICT_MIN_OS:-0}" = "1" ]; then + echo "STRICT_MIN_OS=1 set — failing the build." + exit 1 + fi +fi +fi # end x86_64-only minos verification + echo "" echo "vspipe architecture:" file "$DEPS_DIR/vapoursynth/vspipe" From c286a738a8934e00a2dded8e14742ad3ce6c3d56 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 22:16:05 +1000 Subject: [PATCH 06/11] macOS app: pin deployment target to 12.0 (issue #39) The x64 deps now target macOS 12; pin the app shell + Rust worker to match so the app launches on Monterey. Sets MACOSX_DEPLOYMENT_TARGET=12.0 in the build workflow (rustc reads it for the worker), the Xcode project (Runner, 3 configs), and the Podfile platform. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-macos.yml | 5 +++++ app/macos/Podfile | 2 +- app/macos/Runner.xcodeproj/project.pbxproj | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 4b74609..f5add37 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -42,6 +42,11 @@ jobs: build: needs: setup runs-on: macos-15 # Apple Silicon; x64 cross-compiles via ARCHS=x86_64 + # Pin the macOS deployment target to 12.0 (issue #39): rustc reads this for + # the worker, and it matches the Xcode project / Podfile so the app shell and + # worker load on Monterey — consistent with the 12.0-targeted x64 deps. + env: + MACOSX_DEPLOYMENT_TARGET: "12.0" strategy: fail-fast: false matrix: diff --git a/app/macos/Podfile b/app/macos/Podfile index 699ea8e..aa8955d 100644 --- a/app/macos/Podfile +++ b/app/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj index da79ba8..c7d6e43 100644 --- a/app/macos/Runner.xcodeproj/project.pbxproj +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -557,7 +557,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -639,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -689,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; From 49f1ec108b72de653b7906e08f37bab94e4c4fbd Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 22:47:43 +1000 Subject: [PATCH 07/11] build-macos: make notarization optional (notarize input, default true) Lets us produce signed-but-un-notarized DMGs for local testing when the Apple notary agreement is unavailable. Default stays true for real releases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-macos.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index f5add37..4b46c1c 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -21,6 +21,11 @@ on: - arm64 - x64 default: universal + notarize: + description: 'Notarize with Apple (needs valid developer agreements; off = signed-only, fine for local testing)' + required: false + type: boolean + default: true jobs: # Expand the arch choice into a matrix the build job fans out over. @@ -225,7 +230,9 @@ jobs: NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }} NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} run: | - ./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch ${{ matrix.arch }} --skip-build --notarize + ARGS=(--version "${{ inputs.version }}" --arch ${{ matrix.arch }} --skip-build) + if [ "${{ inputs.notarize }}" = "true" ]; then ARGS+=(--notarize); fi + ./Scripts/package-macos.sh "${ARGS[@]}" - name: Clean up signing keychain if: always() From 8d11cc502221ed578879c14d2dd35dc3e7e6ddd8 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Mon, 29 Jun 2026 23:38:08 +1000 Subject: [PATCH 08/11] deps: extend Monterey source-build treatment to arm64 (issue #39 arm64 test) Un-gate the deployment-target pin, vspipe to_chars patch, source-built support libs, override copies, and minos guard to run on arm64 as well as x64. arm64 now also targets macOS 12.0 and bundles source-built zimg/fftw/dvdread/xz/boost. Recon step: the arm64 prebuilt plugins (yuygfgg neo-f3kdb/dfttest/ttempsmooth/ fft3dfilter/vivtc, Stefan-Olt tmedian/bestsource) are NOT yet source-built, and arm64 nnedi3cl/knlmeanscl still link Homebrew boost. The guard runs in warning mode on arm64 (STRICT_MIN_OS unset for that job) to report what remains >12. Co-Authored-By: Claude Opus 4.8 (1M context) --- Scripts/download-deps-macos.sh | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index 2645212..fb2d83e 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -69,16 +69,14 @@ fi # NOTE: this does NOT retarget prebuilt artifacts the script merely copies # (Homebrew bottles, evermeet ffmpeg, python-build-standalone, Stefan-Olt plugins) — # the minos verification guard near the end of this script reports those. -if [ "$ARCH" = "x86_64" ]; then - MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey - export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" - # CMake honors the env var as a fallback, but some plugin CMakeLists set their - # own target; pass it explicitly on the cmake lines too (see neo-f3kdb). - export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" - echo "Minimum macOS deployment target (x64): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" -else - echo "arm64 build: not pinning a deployment target (Intel-only back-deploy)" -fi +# Both arches now target Monterey 12.0 (arm64 floor is Big Sur 11.0, so 12.0 is +# valid). This is the arm64 source-build test — see the support-lib build below. +MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey +export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" +# CMake honors the env var as a fallback, but some plugin CMakeLists set their +# own target; pass it explicitly on the cmake lines too (see neo-f3kdb). +export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" +echo "Minimum macOS deployment target ($ARCH): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" @@ -159,9 +157,10 @@ done # boost (filesystem,atomic) - ABI-sensitive, so the OpenCL plugins # (nnedi3cl/knlmeanscl) are ALSO compiled against # this build via BOOST_ROOT="$SRCLIB" below. -# arm64 is untouched (no deployment-target pin -> this whole block is skipped). +# Now runs for BOTH arches (arm64 Monterey source-build test). The recipes +# compile natively for whichever arch the runner is, inheriting the 12.0 target. SRCLIB="$BUILD_DIR/srclib" -if [ "$ARCH" = "x86_64" ]; then +if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then echo "" echo "=== Building support libraries from source (target $MACOS_MIN_VERSION) ===" NPROC=$(sysctl -n hw.ncpu) @@ -290,9 +289,9 @@ if [ "$FORCE" = true ] || [ ! -f "$DEPS_DIR/vapoursynth/libvapoursynth.dylib" ]; # ('to_chars is unavailable: introduced in macOS 13.3'); shipped from a 15.0 # build it links a libc++ symbol absent on Monterey -> the "dyld: Symbol not # found" crash in #39. Rewrite it to emit the shortest round-tripping fixed - # string via snprintf, which back-deploys cleanly. x64 only so the arm64 - # bundle stays byte-for-byte unchanged. - if [ "$ARCH" = "x86_64" ]; then + # string via snprintf, which back-deploys cleanly. Both arches now (arm64 + # Monterey test) since both target 12.0 (< 13.3, so to_chars is unavailable). + if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then echo " Patching vspipe doubleToString for Monterey back-deploy (issue #39)..." for vs_src in src/vspipe/vsjson.cpp src/vspipe/vspipe.cpp; do VS_SRC="$vs_src" python3 - <<'PYEOF' @@ -1405,7 +1404,7 @@ fi # Done here -- after all plugin builds, before the repoint passes -- so the # existing @loader_path rewiring and signing apply to them uniformly. The # OpenCL plugins were already compiled against this same boost (BOOST_ROOT). -if [ "$ARCH" = "x86_64" ] && [ -d "$SRCLIB/lib" ]; then +if [ -d "$SRCLIB/lib" ]; then # both arches now (gated on the source build having run) echo "" echo "=== Replacing Homebrew support libs with source builds (target $MACOS_MIN_VERSION) ===" # zimg sits next to libvapoursynth and isn't covered by the repoint passes, @@ -1629,9 +1628,10 @@ fi # ---------------------------------------------------------------------------- # Minimum-OS (minos) verification — x64 only (issue #39) # ---------------------------------------------------------------------------- -# Only the Intel bundle commits to back-deploying (to Monterey); the arm64 bundle -# is built on/for current macOS, so skip the check there to avoid spurious noise. -if [ "$ARCH" = "x86_64" ]; then +# Both arches are checked now (arm64 Monterey test). STRICT_MIN_OS=1 (set per-job +# in the workflow) turns a >target finding into a hard failure; without it the +# pass just reports, which is how the arm64 recon run surfaces what's still >12. +if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then echo "" echo "Minimum-OS (minos) of every bundled Mach-O (target $MACOS_MIN_VERSION, issue #39):" # Print the macOS deployment target baked into a Mach-O, reading whichever load From 1b165356acbd52bb9552a13d72debc02c69f0613 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Tue, 30 Jun 2026 20:24:52 +1000 Subject: [PATCH 09/11] Revert "deps: extend Monterey source-build treatment to arm64 (issue #39 arm64 test)" This reverts commit 8d11cc502221ed578879c14d2dd35dc3e7e6ddd8. --- Scripts/download-deps-macos.sh | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index fb2d83e..2645212 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -69,14 +69,16 @@ fi # NOTE: this does NOT retarget prebuilt artifacts the script merely copies # (Homebrew bottles, evermeet ffmpeg, python-build-standalone, Stefan-Olt plugins) — # the minos verification guard near the end of this script reports those. -# Both arches now target Monterey 12.0 (arm64 floor is Big Sur 11.0, so 12.0 is -# valid). This is the arm64 source-build test — see the support-lib build below. -MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey -export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" -# CMake honors the env var as a fallback, but some plugin CMakeLists set their -# own target; pass it explicitly on the cmake lines too (see neo-f3kdb). -export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" -echo "Minimum macOS deployment target ($ARCH): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" +if [ "$ARCH" = "x86_64" ]; then + MACOS_MIN_VERSION="${MACOS_MIN_VERSION:-12.0}" # Monterey + export MACOSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + # CMake honors the env var as a fallback, but some plugin CMakeLists set their + # own target; pass it explicitly on the cmake lines too (see neo-f3kdb). + export CMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_MIN_VERSION" + echo "Minimum macOS deployment target (x64): $MACOS_MIN_VERSION (override via \$MACOS_MIN_VERSION)" +else + echo "arm64 build: not pinning a deployment target (Intel-only back-deploy)" +fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" @@ -157,10 +159,9 @@ done # boost (filesystem,atomic) - ABI-sensitive, so the OpenCL plugins # (nnedi3cl/knlmeanscl) are ALSO compiled against # this build via BOOST_ROOT="$SRCLIB" below. -# Now runs for BOTH arches (arm64 Monterey source-build test). The recipes -# compile natively for whichever arch the runner is, inheriting the 12.0 target. +# arm64 is untouched (no deployment-target pin -> this whole block is skipped). SRCLIB="$BUILD_DIR/srclib" -if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then +if [ "$ARCH" = "x86_64" ]; then echo "" echo "=== Building support libraries from source (target $MACOS_MIN_VERSION) ===" NPROC=$(sysctl -n hw.ncpu) @@ -289,9 +290,9 @@ if [ "$FORCE" = true ] || [ ! -f "$DEPS_DIR/vapoursynth/libvapoursynth.dylib" ]; # ('to_chars is unavailable: introduced in macOS 13.3'); shipped from a 15.0 # build it links a libc++ symbol absent on Monterey -> the "dyld: Symbol not # found" crash in #39. Rewrite it to emit the shortest round-tripping fixed - # string via snprintf, which back-deploys cleanly. Both arches now (arm64 - # Monterey test) since both target 12.0 (< 13.3, so to_chars is unavailable). - if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then + # string via snprintf, which back-deploys cleanly. x64 only so the arm64 + # bundle stays byte-for-byte unchanged. + if [ "$ARCH" = "x86_64" ]; then echo " Patching vspipe doubleToString for Monterey back-deploy (issue #39)..." for vs_src in src/vspipe/vsjson.cpp src/vspipe/vspipe.cpp; do VS_SRC="$vs_src" python3 - <<'PYEOF' @@ -1404,7 +1405,7 @@ fi # Done here -- after all plugin builds, before the repoint passes -- so the # existing @loader_path rewiring and signing apply to them uniformly. The # OpenCL plugins were already compiled against this same boost (BOOST_ROOT). -if [ -d "$SRCLIB/lib" ]; then # both arches now (gated on the source build having run) +if [ "$ARCH" = "x86_64" ] && [ -d "$SRCLIB/lib" ]; then echo "" echo "=== Replacing Homebrew support libs with source builds (target $MACOS_MIN_VERSION) ===" # zimg sits next to libvapoursynth and isn't covered by the repoint passes, @@ -1628,10 +1629,9 @@ fi # ---------------------------------------------------------------------------- # Minimum-OS (minos) verification — x64 only (issue #39) # ---------------------------------------------------------------------------- -# Both arches are checked now (arm64 Monterey test). STRICT_MIN_OS=1 (set per-job -# in the workflow) turns a >target finding into a hard failure; without it the -# pass just reports, which is how the arm64 recon run surfaces what's still >12. -if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "arm64" ]; then +# Only the Intel bundle commits to back-deploying (to Monterey); the arm64 bundle +# is built on/for current macOS, so skip the check there to avoid spurious noise. +if [ "$ARCH" = "x86_64" ]; then echo "" echo "Minimum-OS (minos) of every bundled Mach-O (target $MACOS_MIN_VERSION, issue #39):" # Print the macOS deployment target baked into a Mach-O, reading whichever load From 03baeeae629f3c03e623f856d2e93b0712eb71fd Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Tue, 30 Jun 2026 20:26:35 +1000 Subject: [PATCH 10/11] nightly: add platform filter input for manual dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets a manual run target just macOS (both arches), Windows, or Linux instead of always fanning out to all four. Scheduled (cron) runs are unaffected — they still run everything. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/nightly.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d1cb2c5..bdf824a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,6 +10,16 @@ on: schedule: - cron: '0 7 * * *' # 07:00 UTC daily workflow_dispatch: + inputs: + platform: + description: 'Which platforms to run (manual dispatch only)' + type: choice + options: + - all + - macos + - windows + - linux + default: all concurrency: group: nightly-${{ github.ref }} @@ -20,6 +30,7 @@ env: jobs: macos: + if: ${{ github.event_name == 'schedule' || inputs.platform == 'all' || inputs.platform == 'macos' }} strategy: fail-fast: false matrix: @@ -93,6 +104,7 @@ jobs: run: cd app && flutter test --tags heavy windows: + if: ${{ github.event_name == 'schedule' || inputs.platform == 'all' || inputs.platform == 'windows' }} runs-on: windows-latest env: VAPOURBOX_DEPS_DIR: ${{ github.workspace }}/deps/windows-x64 @@ -162,6 +174,7 @@ jobs: run: cd app && flutter test --tags heavy linux: + if: ${{ github.event_name == 'schedule' || inputs.platform == 'all' || inputs.platform == 'linux' }} runs-on: ubuntu-22.04 env: VAPOURBOX_DEPS_DIR: ${{ github.workspace }}/deps/linux-x64 From 639a3605f8c7242995506c7207bb7a7982358120 Mon Sep 17 00:00:00 2001 From: Stuart Cameron Date: Tue, 30 Jun 2026 20:43:32 +1000 Subject: [PATCH 11/11] docs: document x64 Monterey deps (source-built support libs, STRICT_MIN_OS) (issue #39) Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0191ced..9309ea9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -565,6 +565,7 @@ The `download-deps-windows.ps1` and `download-deps-macos.sh` scripts apply these - Worker sets: `PYTHONHOME`, `PYTHONPATH`, `VAPOURSYNTH_CONF_PATH`, `DYLD_LIBRARY_PATH` - `vspipe` is a wrapper script that generates config dynamically (needed because `VAPOURSYNTH_PLUGIN_PATH` is additive, not a replacement) - **FFmpeg** is sourced pre-built as a static binary that links only system frameworks: **x64** from evermeet.cx, **arm64** from martin-riedl.de (Homebrew's arm64 ffmpeg is dynamically linked to ~17 Homebrew dylibs and is NOT self-contained, so it can't be bundled). **x64 plugins** build from source under Rosetta, except `tmedian`/`bestsource` which come pre-built from Stefan-Olt/vs-plugin-build. +- **x64 minimum macOS = 12.0 (Monterey), issue #39**: the only hosted Intel runner is `macos-15-intel` (`macos-13` was retired), so Homebrew bottles come out `minos 14/15` and won't load on 12. The x64 build therefore exports `MACOSX_DEPLOYMENT_TARGET=12.0` and **builds the bundled support libs from source** (zimg, fftw, libdvdread, xz, boost) so they target 12; the OpenCL plugins (`nnedi3cl`/`knlmeanscl`) are compiled against that source boost (`BOOST_ROOT="$SRCLIB"`) for ABI match. vspipe's `doubleToString` is patched off `std::to_chars` (needs 13.3+ libc++). A `minos` verification pass at the end fails the build under `STRICT_MIN_OS=1` (set in `build-deps-macos.yml`) if any bundled Mach-O exceeds 12.0. **arm64 is unchanged (still `minos 15`)** — it has no old runner and the prebuilt arm64 plugins are >12; the app/worker deployment target is pinned to 12.0 for both arches (`build-macos.yml`, Runner.xcodeproj, Podfile). - **Code signing**: After `install_name_tool` modifications, binaries must be re-signed: `codesign -s - -f ` (exit code 137 = SIGKILL means invalid signature) - Quarantine removal: `xattr -cr` on deps after download - Show in Folder: `open -R `