Skip to content

Commit fefd4ce

Browse files
mkschulzeclaude
andcommitted
fix(macos): bundle ffmpeg dylibs into Contents/Frameworks (beta.20.8 crash fix)
v1.1-beta.20.8 crashed on launch on every macOS user's machine. The crash log showed: Library not loaded: @rpath/libavcodec.61.19.101.dylib Reason: tried '/Users/runner/work/JamWide/JamWide/build/ffmpeg-universal/lib/...' Root cause: cmake/ffmpeg.cmake emitted `target_link_options(... LINKER:-rpath,${_ffmpeg_dir}/lib)`, baking the build-time absolute path as the binary's only LC_RPATH. The ffmpeg dylibs were never copied into <bundle>/Contents/Frameworks/, so on user machines that path didn't exist and dyld aborted. CMake also auto-adds the same rpath whenever you link against a full-path @rpath/... dylib, so stripping just the explicit emission isn't enough — the POST_BUILD script handles both cases. Fix: - cmake/bundle_ffmpeg_macos.cmake (new): POST_BUILD script copies dylibs (preserving symlinks like libavcodec.61.dylib -> libavcodec.61.19.101.dylib that libavformat depends on), strips absolute-path rpaths, adds @loader_path/../Frameworks as the bundle-relative rpath. - cmake/ffmpeg.cmake: build-tree rpath emission scoped to Linux only (Linux distribution still deferred). Added JAMWIDE_FFMPEG_LIB_DIR target property + jamwide_bundle_ffmpeg_apple() helper. - CMakeLists.txt: foreach over JUCE plugin formats invoking the helper. Runs before the existing codesign foreach so `codesign --deep` covers the newly-bundled dylibs in one pass. Verified: copied built .app to /tmp, renamed libs/ffmpeg/macos-x86_64 to hide the local path, launched — dyld resolved all 5 ffmpeg dylibs from /private/tmp/.../JamWide.app/Contents/Frameworks/, app reached its own init code, exit 0. codesign --verify --deep --strict passes. No CI workflow changes needed: the bundled dylibs ride along when CI packages from build/JamWideJuce_artefacts/... and `notarize.sh` already uses `codesign --deep --options runtime` which signs nested code with the same Dev ID — hardened runtime accepts this. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9a202b2 commit fefd4ce

5 files changed

Lines changed: 201 additions & 15 deletions

File tree

CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,23 @@ if(JAMWIDE_BUILD_JUCE)
368368
CLAP_FEATURES audio-effect utility mixing
369369
)
370370

371+
# Bundle ffmpeg dylibs into each plugin format's Contents/Frameworks/
372+
# and rewrite the binary's rpath to @loader_path/../Frameworks. This
373+
# replaces the build-tree-absolute -rpath that cmake/ffmpeg.cmake used
374+
# to emit, which baked the CI runner's path into shipping binaries
375+
# and caused beta.20.8 crash-on-launch (dyld: Library not loaded:
376+
# @rpath/libavcodec.61.19.101.dylib — searched only the CI runner's
377+
# /Users/runner/work/JamWide/JamWide/build/ffmpeg-universal/lib).
378+
# Runs before the codesign POST_BUILD below so `codesign --deep`
379+
# covers the newly-bundled dylibs in one pass.
380+
if(APPLE)
381+
foreach(_fmt Standalone VST3 AU CLAP)
382+
if(TARGET JamWideJuce_${_fmt})
383+
jamwide_bundle_ffmpeg_apple(JamWideJuce_${_fmt})
384+
endif()
385+
endforeach()
386+
endif()
387+
371388
# macOS code signing
372389
# - Dev builds: ad-hoc signing (default, no certificate needed)
373390
# - Release builds: Developer ID signing with hardened runtime

cmake/bundle_ffmpeg_macos.cmake

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# cmake/bundle_ffmpeg_macos.cmake
2+
# Phase 23 follow-up — bundle the vendored LGPL ffmpeg dylibs into a JUCE
3+
# plugin bundle's Contents/Frameworks/ and rewrite the main binary's rpath so
4+
# dyld finds them via @loader_path/../Frameworks instead of an absolute build-
5+
# tree path baked in by `target_link_options(... LINKER:-rpath,...)`.
6+
#
7+
# Run as a POST_BUILD step:
8+
# cmake -DSRC_DIR=<ffmpeg-lib-dir>
9+
# -DBUNDLE=<bundle-dir> # e.g. .../JamWide.app or .../JamWide.vst3
10+
# -DBINARY=<bundle>/Contents/MacOS/<name>
11+
# -P cmake/bundle_ffmpeg_macos.cmake
12+
#
13+
# Background (root cause of beta.20.8 crash-on-launch):
14+
# `otool -L JamWide.app/Contents/MacOS/JamWide` showed each ffmpeg dylib
15+
# referenced as `@rpath/libavcodec.61.19.101.dylib`, but the only LC_RPATH
16+
# on the binary was the GitHub Actions runner's absolute build path
17+
# (`/Users/runner/work/JamWide/JamWide/build/ffmpeg-universal/lib`). That
18+
# path doesn't exist on user machines → dyld aborts with
19+
# "Library not loaded: @rpath/libavcodec.61.19.101.dylib". Distribution
20+
# binaries need a bundle-relative rpath PLUS the dylibs physically present
21+
# inside Contents/Frameworks/. This script does both.
22+
23+
if(NOT SRC_DIR OR NOT BUNDLE OR NOT BINARY)
24+
message(FATAL_ERROR
25+
"bundle_ffmpeg_macos.cmake requires -DSRC_DIR -DBUNDLE -DBINARY")
26+
endif()
27+
28+
if(NOT EXISTS "${SRC_DIR}")
29+
message(FATAL_ERROR "SRC_DIR does not exist: ${SRC_DIR}")
30+
endif()
31+
if(NOT EXISTS "${BINARY}")
32+
message(FATAL_ERROR "BINARY does not exist: ${BINARY}")
33+
endif()
34+
35+
set(_dst "${BUNDLE}/Contents/Frameworks")
36+
file(MAKE_DIRECTORY "${_dst}")
37+
38+
# Copy every *.dylib in SRC_DIR into Frameworks/, preserving symlinks.
39+
# CMake's `-E copy` follows symlinks (dereferences them) so we handle
40+
# symlinks separately via create_symlink. The short-name symlinks (e.g.
41+
# libavcodec.61.dylib → libavcodec.61.19.101.dylib) MUST survive because
42+
# libavformat's LC_LOAD_DYLIB references @rpath/libavcodec.61.dylib (the
43+
# short alias), not the versioned canonical name.
44+
file(GLOB _items "${SRC_DIR}/*.dylib")
45+
foreach(_item ${_items})
46+
get_filename_component(_name "${_item}" NAME)
47+
set(_d "${_dst}/${_name}")
48+
if(IS_SYMLINK "${_item}")
49+
file(READ_SYMLINK "${_item}" _link_target)
50+
file(REMOVE "${_d}")
51+
execute_process(
52+
COMMAND ${CMAKE_COMMAND} -E create_symlink "${_link_target}" "${_d}"
53+
RESULT_VARIABLE _rc
54+
)
55+
if(NOT _rc EQUAL 0)
56+
message(FATAL_ERROR
57+
"Failed to create symlink ${_d} -> ${_link_target}")
58+
endif()
59+
else()
60+
# configure_file with COPYONLY does timestamp-based skip, so
61+
# incremental rebuilds don't re-copy ~50MB of dylibs.
62+
configure_file("${_item}" "${_d}" COPYONLY)
63+
endif()
64+
endforeach()
65+
66+
# Rewrite rpaths on the main binary.
67+
# 1. Strip any LC_RPATH whose path looks like a build-tree absolute path —
68+
# either the SRC_DIR itself or anything matching /build/ffmpeg-universal/.
69+
# install_name_tool -delete_rpath requires an exact match; we discover
70+
# what's currently there via `otool -l` and only delete what exists.
71+
# 2. Add @loader_path/../Frameworks if missing.
72+
execute_process(
73+
COMMAND otool -l "${BINARY}"
74+
OUTPUT_VARIABLE _otool_out
75+
RESULT_VARIABLE _otool_rc
76+
)
77+
if(NOT _otool_rc EQUAL 0)
78+
message(FATAL_ERROR "otool -l failed on ${BINARY}")
79+
endif()
80+
81+
# Parse LC_RPATH paths. Each load command appears as:
82+
# cmd LC_RPATH
83+
# cmdsize N
84+
# path /actual/path (offset 12)
85+
string(REGEX MATCHALL "LC_RPATH[^\n]*\n[^\n]*\n[^\n]*path [^ ]+ " _rpath_blocks "${_otool_out}")
86+
set(_existing_rpaths "")
87+
foreach(_block ${_rpath_blocks})
88+
if(_block MATCHES "path ([^ ]+) ")
89+
list(APPEND _existing_rpaths "${CMAKE_MATCH_1}")
90+
endif()
91+
endforeach()
92+
93+
set(_target_rpath "@loader_path/../Frameworks")
94+
95+
foreach(_rp ${_existing_rpaths})
96+
# Drop ANY rpath that's an absolute filesystem path under a build dir
97+
# (matches both local dev builds and CI runner builds). We keep
98+
# @loader_path/... or @executable_path/... rpaths untouched.
99+
if(_rp MATCHES "^/")
100+
execute_process(
101+
COMMAND install_name_tool -delete_rpath "${_rp}" "${BINARY}"
102+
RESULT_VARIABLE _rc
103+
ERROR_VARIABLE _err
104+
OUTPUT_QUIET
105+
)
106+
if(NOT _rc EQUAL 0)
107+
message(WARNING
108+
"install_name_tool -delete_rpath '${_rp}' failed: ${_err}")
109+
else()
110+
message(STATUS " stripped build-tree rpath: ${_rp}")
111+
endif()
112+
endif()
113+
endforeach()
114+
115+
# Add the bundle-relative rpath if it isn't already there.
116+
list(FIND _existing_rpaths "${_target_rpath}" _has_target_rpath)
117+
if(_has_target_rpath EQUAL -1)
118+
execute_process(
119+
COMMAND install_name_tool -add_rpath "${_target_rpath}" "${BINARY}"
120+
RESULT_VARIABLE _rc
121+
ERROR_VARIABLE _err
122+
)
123+
if(NOT _rc EQUAL 0)
124+
message(FATAL_ERROR
125+
"install_name_tool -add_rpath '${_target_rpath}' failed: ${_err}")
126+
endif()
127+
message(STATUS " added bundle rpath: ${_target_rpath}")
128+
endif()

cmake/ffmpeg.cmake

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,22 +229,63 @@ target_link_libraries(ffmpeg::lgpl INTERFACE
229229
"${_avutil_lib}"
230230
"${_openh264_lib}"
231231
)
232-
# Stash the include path on the target as a custom property so the
233-
# jamwide_use_ffmpeg() macro can read it back without re-computing the
234-
# per-platform logic.
232+
# Stash include + lib dirs on the target as custom properties so the
233+
# jamwide_use_ffmpeg() / jamwide_bundle_ffmpeg_apple() helpers can read
234+
# them back without re-computing the per-platform logic.
235235
set_target_properties(ffmpeg::lgpl PROPERTIES
236236
JAMWIDE_FFMPEG_INCLUDE_DIR "${_ffmpeg_dir}/include"
237+
JAMWIDE_FFMPEG_LIB_DIR "${_ffmpeg_dir}/lib"
237238
)
238239

239-
# POSIX runtime discovery: emit -rpath pointing at the vendored dir so the
240-
# executable finds the shared libraries at runtime. On Windows (.dll
241-
# discovery via consumer-adjacent path or %PATH%) this is a no-op.
242-
if(NOT WIN32)
240+
# POSIX runtime discovery:
241+
# * Linux: emit -rpath pointing at the vendored dir so the executable
242+
# finds the shared libraries at runtime. Linux distribution still
243+
# deferred (per memory `project_jamtaba_video_port`: receive-only v1,
244+
# CameraDevice conditional pending), so the build-tree rpath is fine
245+
# for the dev/CI workflow we have today.
246+
# * Apple: do NOT bake in the build-tree path. Apple distribution
247+
# requires dylibs to live inside <bundle>/Contents/Frameworks/ with
248+
# the binary's rpath set to @loader_path/../Frameworks. That's done
249+
# post-build by jamwide_bundle_ffmpeg_apple() — see below + the
250+
# companion cmake/bundle_ffmpeg_macos.cmake script. The old
251+
# target_link_options(LINKER:-rpath,...) emission baked the CI
252+
# runner's absolute build path into shipping binaries and caused the
253+
# v1.1-beta.20.8 crash-on-launch on user machines.
254+
# * Windows: .dll discovery via consumer-adjacent path or %PATH%.
255+
if(UNIX AND NOT APPLE)
243256
target_link_options(ffmpeg::lgpl INTERFACE
244257
"LINKER:-rpath,${_ffmpeg_dir}/lib"
245258
)
246259
endif()
247260

261+
# Apple-only POST_BUILD bundling helper. Capture the cmake/ dir at
262+
# definition time so the function call site doesn't need to know where
263+
# the script lives.
264+
set(_JAMWIDE_FFMPEG_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}")
265+
function(jamwide_bundle_ffmpeg_apple target)
266+
if(NOT APPLE)
267+
return()
268+
endif()
269+
if(NOT TARGET ${target})
270+
message(FATAL_ERROR "jamwide_bundle_ffmpeg_apple: target '${target}' does not exist")
271+
endif()
272+
get_target_property(_ff_lib_dir ffmpeg::lgpl JAMWIDE_FFMPEG_LIB_DIR)
273+
if(NOT _ff_lib_dir)
274+
message(FATAL_ERROR
275+
"jamwide_bundle_ffmpeg_apple(${target}): ffmpeg::lgpl missing "
276+
"JAMWIDE_FFMPEG_LIB_DIR property — vendored tree probably not built.")
277+
endif()
278+
add_custom_command(TARGET ${target} POST_BUILD
279+
COMMAND ${CMAKE_COMMAND}
280+
-DSRC_DIR=${_ff_lib_dir}
281+
-DBUNDLE=$<TARGET_BUNDLE_DIR:${target}>
282+
-DBINARY=$<TARGET_FILE:${target}>
283+
-P "${_JAMWIDE_FFMPEG_CMAKE_DIR}/bundle_ffmpeg_macos.cmake"
284+
COMMENT "Bundling ffmpeg dylibs into ${target}"
285+
VERBATIM
286+
)
287+
endfunction()
288+
248289
message(STATUS "ffmpeg::lgpl resolved → ${_ffmpeg_dir}")
249290
message(STATUS " avcodec = ${_avcodec_lib}")
250291
message(STATUS " avformat = ${_avformat_lib}")

docs/download.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ title: Download
77

88
---
99

10-
## Latest Beta — v1.1-beta.20.8 (macOS Universal + Windows x64)
10+
## Latest Beta — v1.1-beta.20.9 (macOS Universal + Windows x64)
1111

12-
**First cross-platform beta of the native H.264 video stack.** macOS Universal (Apple Silicon + Intel) plus Windows x64 — running the same NinjamZap-compatible wire format, so a macOS user and a Windows user can join the same room and broadcast/receive each other's video. Linux deferred to the next tag (camera-code conditional in flight).
12+
**Critical fix release.** v1.1-beta.20.8 crashed on launch on every macOS user's machine because the ffmpeg dylibs weren't bundled inside the `.app` / `.vst3` / `.component` / `.clap` the binaries pointed at the GitHub Actions runner's build path. v1.1-beta.20.9 ships dylibs inside each bundle's `Contents/Frameworks/` and uses `@loader_path` so they resolve anywhere.
1313

14-
**Highlights:**
14+
**Highlights (carried over from beta.20.8):**
1515
- Native webcam capture + H.264 broadcast/receive in standalone and DAW-hosted plugin
1616
- Per-user video tile grid + detachable popout windows (multi-monitor)
1717
- HD broadcast preset (1280×720 @ 30fps); Medium/Low presets for bandwidth-constrained sessions
1818
- iOS NinjamZap-mobile receiver compat (cross-AI encoder review by Javier @ NinjamZap)
19-
- First Windows x64 build of the native video stack
19+
- Windows x64 build of the native video stack
2020

2121
**This is beta software** — report issues on [GitHub](https://github.com/mkschulze/JamWide/issues).
2222

2323
<div class="download-section">
24-
<a href="https://github.com/mkschulze/JamWide/releases/tag/v1.1-beta.20.8" class="btn btn-primary btn-large">
25-
Download v1.1-beta.20.8 (macOS + Windows)
24+
<a href="https://github.com/mkschulze/JamWide/releases/tag/v1.1-beta.20.9" class="btn btn-primary btn-large">
25+
Download v1.1-beta.20.9 (macOS + Windows)
2626
</a>
2727
<p class="version-info">macOS Universal (Apple Silicon + Intel) / Windows x64 — Linux next beta</p>
2828
</div>
@@ -33,7 +33,7 @@ title: Download
3333

3434
## Linux — use v1.1-beta.20.5 for now
3535

36-
If you're on Linux, use the older cross-platform beta until the next tag lands the camera-code conditional. v1.1-beta.20.8 ships macOS + Windows only.
36+
If you're on Linux, use the older cross-platform beta until the next tag lands the camera-code conditional. v1.1-beta.20.9 ships macOS + Windows only.
3737

3838
<div class="download-section">
3939
<a href="https://github.com/mkschulze/JamWide/releases/tag/v1.1-beta.20.5" class="btn btn-large">

src/build_number.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#pragma once
2-
#define JAMWIDE_BUILD_NUMBER 364
2+
#define JAMWIDE_BUILD_NUMBER 365

0 commit comments

Comments
 (0)