Skip to content

Commit 7421a7b

Browse files
mkschulzeclaude
andcommitted
feat(ci): macOS universal build (arm64 + x86_64) via lipo'd ffmpeg tree
cmake/ffmpeg.cmake now detects multi-arch CMAKE_OSX_ARCHITECTURES and lipo's libs/ffmpeg/macos-{arm64,x86_64}/ dylibs into a universal tree at ${CMAKE_BINARY_DIR}/ffmpeg-universal/ at configure time. Universal dylibs live in the build tree (not committed) β€” regenerated whenever the build dir is wiped. Symlink aliases (libavcodec.61.dylib β†’ libavcodec.61.19.101.dylib etc.) are recreated by reading the arm64 tree's symlinks (structure identical to x86_64 by construction). Previously cmake/ffmpeg.cmake lines 26-29 emitted a WARNING and fell back to the host arch when CMAKE_OSX_ARCHITECTURES was a list β€” that was a Phase-14.3-era TODO ("Universal-binary stitching is a Phase 14.3 follow-up") that has now been done. CI workflow changes: - Restored -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" in the configure step (was reduced to arm64-only by commit 4463542 during Phase 14.3 deferral because cmake/ffmpeg.cmake couldn't handle universal). Output is now a fat .app/.vst3/.au/.clap that runs natively on both Apple Silicon and Intel Macs. - Removed the "Vendor LGPL ffmpeg" rebuild-from-source step entirely. Both libs/ffmpeg/macos-arm64/ and libs/ffmpeg/macos-x86_64/ are now bundled in the repo (a51bb04, prior commit), so the 15-25 min CI rebuild per push became a no-op git checkout. The "Verify LGPL discipline" gate still runs against the committed trees, ensuring no libx264 contamination. - Removed the one-shot arm64-ffmpeg-tree upload-artifact step added in commit b58c97c β€” it served its purpose (capturing arm64 ffmpeg for the bundling commit) and is no longer needed. scripts/build_ffmpeg_lgpl.sh stays in the repo for: (a) bumping ffmpeg or openh264 version (re-run, commit refreshed trees), (b) initial vendoring of new platforms (linux-x86_64 + windows-x86_64 are still missing β€” those CI lanes are next iteration), (c) reproducibility verification by downstream consumers. Local-dev workflow unchanged: Intel-only macs continue using libs/ffmpeg/macos-x86_64/ directly via the single-arch CMake path. Universal builds are a CI-only concern (as the user requested). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent afe1642 commit 7421a7b

2 files changed

Lines changed: 114 additions & 44 deletions

File tree

β€Ž.github/workflows/juce-build.ymlβ€Ž

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -47,44 +47,32 @@ jobs:
4747
-output "$GITHUB_WORKSPACE/openssl-universal/lib/libssl.a"
4848
lipo -info "$GITHUB_WORKSPACE/openssl-universal/lib/libcrypto.a"
4949
50-
- name: Vendor LGPL ffmpeg
51-
# Phase 14.3-01: produce libs/ffmpeg/<platform>/ on the CI runner via
52-
# scripts/build_ffmpeg_lgpl.sh. macos-14 = arm64 runner; the script's
53-
# OS+ARCH dispatch picks the Darwin-arm64 leg (Cisco openh264 from
54-
# source β€” MPEG-LA royalty deviation documented in script header).
55-
run: bash scripts/build_ffmpeg_lgpl.sh
56-
57-
- name: Verify LGPL discipline
58-
# Phase 14.3-01: iterate every populated libs/ffmpeg/<platform>/ tree
59-
# and assert no libx264 strings + clean otool/ldd linkage. Exits 1
60-
# on any contamination.
50+
- name: Verify LGPL discipline (vendored trees in libs/ffmpeg/)
51+
# Iterate every populated libs/ffmpeg/<platform>/ tree (both arches
52+
# of macOS are now committed) and assert no libx264 strings + clean
53+
# otool/ldd linkage. Exits 1 on any contamination.
54+
# The "Vendor LGPL ffmpeg" rebuild-from-source step that used to run
55+
# here was removed 2026-05-19 β€” libs/ffmpeg/macos-arm64/ and
56+
# libs/ffmpeg/macos-x86_64/ are both bundled in the repo, so a
57+
# 15-25 min CI rebuild on every push became a no-op git checkout.
58+
# Run scripts/build_ffmpeg_lgpl.sh manually when bumping ffmpeg or
59+
# openh264 version and commit the refreshed trees.
6160
run: bash scripts/verify_ffmpeg_lgpl.sh
6261

63-
- name: Upload vendored macos-arm64 ffmpeg tree (one-shot for bundling)
64-
# Phase 23 (regression-fix scope) iteration: capture the arm64 ffmpeg
65-
# vendored tree built on the macos-14 runner so we can commit it to
66-
# libs/ffmpeg/macos-arm64/ locally, then skip the from-source rebuild
67-
# entirely on subsequent CI runs and unlock universal-mac builds. This
68-
# step is intentionally one-shot β€” once libs/ffmpeg/macos-arm64/ lands
69-
# in the repo, both this upload step AND the Vendor/Verify steps above
70-
# can be deleted (replaced by a no-op git checkout).
71-
uses: actions/upload-artifact@v4
72-
with:
73-
name: libs-ffmpeg-macos-arm64
74-
path: libs/ffmpeg/macos-arm64/
75-
retention-days: 7
76-
77-
- name: Configure CMake
78-
# Production-target-only config for v1.3 betas β€” matches beta.20.5
79-
# surface. JAMWIDE_BUILD_TESTS + JAMWIDE_VIDEO_SPIKE pulled the test
80-
# executables (incl. test_flac_codec which has a pre-existing
81-
# WDL_VORBIS_INTERFACE_ONLY guard gap in wdl/vorbisencdec.h that
82-
# caused the compile to fail). Phase 23+ will re-enable the test
83-
# surface once the test-target wiring is cleaned up.
62+
- name: Configure CMake (universal β€” arm64 + x86_64)
63+
# Universal mac build: cmake/ffmpeg.cmake detects the multi-arch
64+
# CMAKE_OSX_ARCHITECTURES and lipo's libs/ffmpeg/macos-{arm64,x86_64}/
65+
# dylibs into a universal tree at ${CMAKE_BINARY_DIR}/ffmpeg-universal/
66+
# at configure time. JUCE/CMake compiles each .o twice + the linker
67+
# produces fat artifacts. Output runs natively on Apple Silicon AND
68+
# Intel Macs.
69+
# JAMWIDE_BUILD_TESTS + JAMWIDE_VIDEO_SPIKE pulled the test executables
70+
# (incl. test_flac_codec which has a pre-existing WDL_VORBIS_INTERFACE_ONLY
71+
# guard gap in wdl/vorbisencdec.h that caused the compile to fail).
8472
run: |
8573
cmake -B build \
8674
-DCMAKE_BUILD_TYPE=Release \
87-
-DCMAKE_OSX_ARCHITECTURES="arm64" \
75+
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
8876
-DJAMWIDE_BUILD_JUCE=ON \
8977
-DJAMWIDE_BUILD_CLAP=OFF \
9078
-DJAMWIDE_DEV_BUILD=OFF \

β€Žcmake/ffmpeg.cmakeβ€Ž

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,111 @@ endif()
2020

2121
if(APPLE)
2222
# Detect arch from CMAKE_HOST_SYSTEM_PROCESSOR or CMAKE_OSX_ARCHITECTURES.
23+
set(_universal_build FALSE)
2324
if(CMAKE_OSX_ARCHITECTURES AND NOT "${CMAKE_OSX_ARCHITECTURES}" STREQUAL "")
2425
list(LENGTH CMAKE_OSX_ARCHITECTURES _arch_count)
2526
if(_arch_count GREATER 1)
26-
message(WARNING "ffmpeg::lgpl: requested universal build (${CMAKE_OSX_ARCHITECTURES}) "
27-
"but vendored trees are per-arch β€” falling back to host arch. "
28-
"Universal-binary stitching is a Phase 14.3 follow-up.")
29-
set(_ffmpeg_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}")
27+
set(_universal_build TRUE)
3028
else()
3129
set(_ffmpeg_arch "${CMAKE_OSX_ARCHITECTURES}")
3230
endif()
3331
else()
3432
set(_ffmpeg_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}")
3533
endif()
3634

37-
if(_ffmpeg_arch STREQUAL "arm64")
38-
set(_ffmpeg_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-arm64")
39-
elseif(_ffmpeg_arch STREQUAL "x86_64" OR _ffmpeg_arch STREQUAL "AMD64")
40-
set(_ffmpeg_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-x86_64")
35+
if(_universal_build)
36+
# ───── Universal mac build ─────
37+
# CMAKE_OSX_ARCHITECTURES is a list (typically "arm64;x86_64") so the
38+
# JUCE/CMake build will compile each .o twice and the linker will
39+
# produce fat binaries. ffmpeg/openh264 dylibs must also be fat β€” lipo
40+
# the per-arch canonical files into universal dylibs in the build
41+
# tree, then point ffmpeg::lgpl at that build-tree directory.
42+
#
43+
# We DON'T commit the lipo'd universal tree to libs/ffmpeg/ β€”
44+
# CMAKE_BINARY_DIR is the right home for arch-combined artifacts that
45+
# the per-arch trees imply. Regenerated whenever the build dir is
46+
# wiped or whenever per-arch sources mtime newer than universal.
47+
set(_arm64_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-arm64")
48+
set(_x86_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-x86_64")
49+
set(_ffmpeg_dir "${CMAKE_BINARY_DIR}/ffmpeg-universal")
50+
51+
if(NOT EXISTS "${_arm64_dir}/lib" OR NOT EXISTS "${_x86_dir}/lib")
52+
message(FATAL_ERROR
53+
"ffmpeg::lgpl: universal build (${CMAKE_OSX_ARCHITECTURES}) "
54+
"requires BOTH libs/ffmpeg/macos-arm64/ and "
55+
"libs/ffmpeg/macos-x86_64/ to be vendored. Found:\n"
56+
" arm64: ${_arm64_dir}/lib (exists: $<IF:EXISTS,YES,NO>)\n"
57+
" x86_64: ${_x86_dir}/lib (exists: $<IF:EXISTS,YES,NO>)")
58+
endif()
59+
60+
if(NOT EXISTS "${_ffmpeg_dir}/lib/libavcodec.dylib")
61+
message(STATUS "ffmpeg::lgpl: building universal tree at ${_ffmpeg_dir}")
62+
file(MAKE_DIRECTORY "${_ffmpeg_dir}/lib")
63+
# Headers are identical between arches β€” copy arm64's set.
64+
file(COPY "${_arm64_dir}/include" DESTINATION "${_ffmpeg_dir}")
65+
66+
# lipo each canonical versioned dylib (skip symlinks; we recreate
67+
# them after).
68+
file(GLOB _src_libs "${_arm64_dir}/lib/lib*.dylib")
69+
foreach(_arm64_lib ${_src_libs})
70+
if(IS_SYMLINK "${_arm64_lib}")
71+
continue()
72+
endif()
73+
get_filename_component(_libname "${_arm64_lib}" NAME)
74+
set(_x86_lib "${_x86_dir}/lib/${_libname}")
75+
set(_uni_lib "${_ffmpeg_dir}/lib/${_libname}")
76+
if(NOT EXISTS "${_x86_lib}")
77+
message(FATAL_ERROR
78+
"ffmpeg::lgpl: macos-arm64 has ${_libname} but "
79+
"macos-x86_64 doesn't β€” version mismatch between trees")
80+
endif()
81+
execute_process(
82+
COMMAND lipo -create "${_arm64_lib}" "${_x86_lib}"
83+
-output "${_uni_lib}"
84+
RESULT_VARIABLE _lipo_rc
85+
ERROR_VARIABLE _lipo_err
86+
)
87+
if(NOT _lipo_rc EQUAL 0)
88+
message(FATAL_ERROR
89+
"lipo failed for ${_libname}: ${_lipo_err}")
90+
endif()
91+
message(STATUS " lipo'd ${_libname}")
92+
endforeach()
93+
94+
# Recreate the symlink aliases (e.g. libavcodec.61.dylib β†’
95+
# libavcodec.61.19.101.dylib) so @rpath/libavcodec.61.dylib
96+
# resolves at runtime. Read symlink targets from the arm64 tree
97+
# (the structure is identical to x86_64 by construction).
98+
file(GLOB _all_libs "${_arm64_dir}/lib/lib*.dylib")
99+
foreach(_src ${_all_libs})
100+
if(IS_SYMLINK "${_src}")
101+
get_filename_component(_linkname "${_src}" NAME)
102+
execute_process(
103+
COMMAND readlink "${_src}"
104+
OUTPUT_VARIABLE _target
105+
OUTPUT_STRIP_TRAILING_WHITESPACE
106+
)
107+
execute_process(
108+
COMMAND ln -sf "${_target}"
109+
"${_ffmpeg_dir}/lib/${_linkname}"
110+
)
111+
endif()
112+
endforeach()
113+
endif()
114+
set(_lib_glob "*.dylib")
115+
set(_have_symlinks TRUE)
41116
else()
42-
message(FATAL_ERROR "ffmpeg::lgpl: unsupported macOS arch '${_ffmpeg_arch}' (expected arm64 or x86_64)")
117+
# ───── Single-arch mac build (host or explicit) ─────
118+
if(_ffmpeg_arch STREQUAL "arm64")
119+
set(_ffmpeg_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-arm64")
120+
elseif(_ffmpeg_arch STREQUAL "x86_64" OR _ffmpeg_arch STREQUAL "AMD64")
121+
set(_ffmpeg_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/macos-x86_64")
122+
else()
123+
message(FATAL_ERROR "ffmpeg::lgpl: unsupported macOS arch '${_ffmpeg_arch}' (expected arm64 or x86_64)")
124+
endif()
125+
set(_lib_glob "*.dylib")
126+
set(_have_symlinks TRUE)
43127
endif()
44-
set(_lib_glob "*.dylib")
45-
set(_have_symlinks TRUE)
46128
elseif(UNIX AND NOT APPLE)
47129
set(_ffmpeg_dir "${CMAKE_CURRENT_LIST_DIR}/../libs/ffmpeg/linux-x86_64")
48130
set(_lib_glob "*.so*")

0 commit comments

Comments
Β (0)