Skip to content

Commit a9f99ec

Browse files
committed
build(darwin): fix macOS arm64 compilation and enable VideoToolbox
Resolve three blocking issues for building FFmpeg and 20 dependencies on macOS arm64 within Nix development environment: - Force clang compiler via FFmpeg configure flags and build environment - Disable x264 assembly on Apple Silicon (GNU macro incompatibility with LLVM) - Build rav1e without git_version feature to avoid libgit2/libiconv dependency - Configure C++ header search order with -nostdinc++ and explicit libc++ path - Filter NIX_CFLAGS_COMPILE to prevent incorrect -isystem injection - Use Apple's /usr/bin/libtool instead of GNU libtool for library combining - Add VideoToolbox hardware encoder support (H.264, HEVC, ProRes) - Export LIBCXX_INCLUDE from flake.nix for C++ dependency builds Root cause: Nix dev shell provides both GCC and Clang, but CGO_CFLAGS points to Clang's builtin headers containing preprocessor features (__has_feature, __building_module) that GCC doesn't recognise. Verified: All 17 platform-applicable libraries build successfully, combine into lib/darwin_arm64/libffmpeg.a (63MB), tests pass, VideoToolbox encoders confirmed present. Signed-off-by: Martin Wimpress <martin@wimpress.org>
1 parent 9299e3a commit a9f99ec

6 files changed

Lines changed: 218 additions & 10 deletions

File tree

flake.nix

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
cargo-c
5454
rustc
5555
]
56+
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
57+
# C++ standard library headers for building C++ dependencies (zimg, etc.)
58+
# The .dev output contains include/c++/v1/ with <algorithm>, <iostream>, etc.
59+
llvmPackages_18.libcxx
60+
]
5661
++ pkgs.lib.optionals pkgs.stdenv.isLinux [
5762
# Hardware acceleration runtime (Linux only)
5863
vulkan-loader # Required for Vulkan accelerated encoders
@@ -73,6 +78,9 @@
7378
# CGO needs both the SDK path and clang's builtin headers (stdarg.h, stddef.h, etc.)
7479
export CGO_CFLAGS="-isysroot ''${SDKROOT:-$(xcrun --show-sdk-path)} -I${pkgs.llvmPackages_18.libclang.lib}/lib/clang/18/include"
7580
export CPATH="${pkgs.llvmPackages_18.libclang.dev}/include:$CPATH"
81+
# C++ standard library headers path for building C++ dependencies (zimg, etc.)
82+
# The builder uses this to add -I flag for <algorithm>, <iostream>, etc.
83+
export LIBCXX_INCLUDE="${pkgs.llvmPackages_18.libcxx.dev}/include/c++/v1"
7684
# Set deployment target to match ffmpeg-statigo build (macOS 13.0+)
7785
export MACOSX_DEPLOYMENT_TARGET="13.0"
7886
# macOS uses DYLD_LIBRARY_PATH instead of LD_LIBRARY_PATH

internal/builder/buildsystems.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88
"runtime"
9+
"strings"
910
)
1011

1112
// stagingDir derives the staging/install directory from a build directory path.
@@ -63,11 +64,49 @@ func (a *AutoconfBuild) Configure(lib *Library, srcPath, buildDir, installDir st
6364

6465
cflags := fmt.Sprintf("-O3 -I%s", incDir)
6566
cppflags := fmt.Sprintf("-I%s", incDir)
67+
cxxflags := "-O3" // Start with optimization flag
6668
ldflags := fmt.Sprintf("-L%s", libDir)
6769

70+
// On macOS, configure proper SDK and header paths
71+
// This is required for CGO compilation and C++ builds
72+
if runtime.GOOS == "darwin" {
73+
// CGO_CFLAGS format: -isysroot /path/to/sdk -I/nix/store/.../lib/clang/18/include
74+
cgoCflags := os.Getenv("CGO_CFLAGS")
75+
libcxxInclude := os.Getenv("LIBCXX_INCLUDE")
76+
77+
// For C: add SDK path and clang builtins
78+
if cgoCflags != "" {
79+
cflags = fmt.Sprintf("%s %s", cflags, cgoCflags)
80+
}
81+
82+
// For CPPFLAGS: only add -isysroot, NOT the clang builtin include path
83+
// This prevents C++ from finding clang's stddef.h before libc++'s wrapper
84+
if cgoCflags != "" {
85+
// Extract just the -isysroot part from CGO_CFLAGS
86+
// CGO_CFLAGS = "-isysroot /path/to/sdk -I/path/to/clang/include"
87+
// We only want the -isysroot portion for CPPFLAGS
88+
parts := strings.Fields(cgoCflags)
89+
for i, part := range parts {
90+
if part == "-isysroot" && i+1 < len(parts) {
91+
cppflags = fmt.Sprintf("%s -isysroot %s", cppflags, parts[i+1])
92+
break
93+
}
94+
}
95+
}
96+
97+
// For C++: use -nostdinc++ to disable built-in paths, then add libc++ first,
98+
// then clang builtins (for stddef.h, stdarg.h), then SDK path
99+
if libcxxInclude != "" && cgoCflags != "" {
100+
cxxflags = fmt.Sprintf("%s -nostdinc++ -I%s %s", cxxflags, libcxxInclude, cgoCflags)
101+
} else if cgoCflags != "" {
102+
cxxflags = fmt.Sprintf("%s %s", cxxflags, cgoCflags)
103+
}
104+
}
105+
68106
args = append(args,
69107
fmt.Sprintf("CFLAGS=%s", cflags),
70108
fmt.Sprintf("CPPFLAGS=%s", cppflags),
109+
fmt.Sprintf("CXXFLAGS=%s", cxxflags),
71110
fmt.Sprintf("LDFLAGS=%s", ldflags),
72111
)
73112
}

internal/builder/ffmpeg_args.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,5 +393,32 @@ func FFmpegArgsCommon(os string) []string {
393393
)
394394
}
395395

396+
// macOS-specific hardware acceleration (VideoToolbox)
397+
if os == "darwin" {
398+
args = append(args,
399+
// H.264 VideoToolbox encoder and hwaccel
400+
"--enable-encoder=h264_videotoolbox",
401+
"--enable-hwaccel=h264_videotoolbox",
402+
// H.265/HEVC VideoToolbox encoder and hwaccel
403+
"--enable-encoder=hevc_videotoolbox",
404+
"--enable-hwaccel=hevc_videotoolbox",
405+
// ProRes VideoToolbox encoder and hwaccel
406+
"--enable-encoder=prores_videotoolbox",
407+
"--enable-hwaccel=prores_videotoolbox",
408+
// AV1 VideoToolbox hwaccel (M3+ decode support)
409+
"--enable-hwaccel=av1_videotoolbox",
410+
// VP9 VideoToolbox hwaccel
411+
"--enable-hwaccel=vp9_videotoolbox",
412+
// MPEG-2 VideoToolbox hwaccel
413+
"--enable-hwaccel=mpeg2_videotoolbox",
414+
// MPEG-4 VideoToolbox hwaccel
415+
"--enable-hwaccel=mpeg4_videotoolbox",
416+
// H.263 VideoToolbox hwaccel
417+
"--enable-hwaccel=h263_videotoolbox",
418+
// MPEG-1 VideoToolbox hwaccel
419+
"--enable-hwaccel=mpeg1_videotoolbox",
420+
)
421+
}
422+
396423
return args
397424
}

internal/builder/libraries.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ var x264 = &Library{
537537
BuildSystem: &AutoconfBuild{},
538538
SkipAutoFlags: true, // x264 has a custom configure script that rejects CFLAGS/LDFLAGS
539539
ConfigureArgs: func(os string) []string {
540-
return []string{
540+
args := []string{
541541
"--disable-avs",
542542
"--disable-cli",
543543
"--disable-ffms",
@@ -548,6 +548,12 @@ var x264 = &Library{
548548
"--enable-static",
549549
"--enable-strip",
550550
}
551+
// x264's aarch64 assembly uses GNU assembler macros (const, endconst, T())
552+
// that aren't compatible with LLVM's integrated assembler on macOS
553+
if os == "darwin" && runtime.GOARCH == "arm64" {
554+
args = append(args, "--disable-asm")
555+
}
556+
return args
551557
},
552558
PostExtract: func(srcPath string) error {
553559
// x264 needs to find nasm explicitly on x86/x86_64
@@ -605,16 +611,41 @@ var rav1e = &Library{
605611
BuildSystem: &CargoBuild{
606612
InstallFunc: func(srcPath, installDir string) error {
607613
// Set RUSTFLAGS for native CPU optimization
608-
os.Setenv("RUSTFLAGS", "-C target-cpu=native")
614+
rustflags := "-C target-cpu=native"
615+
616+
// On macOS, add SDK library path for any native dependencies
617+
if runtime.GOOS == "darwin" {
618+
cgoCflags := os.Getenv("CGO_CFLAGS")
619+
// Extract SDK path from CGO_CFLAGS (-isysroot <path>)
620+
var sdkPath string
621+
parts := strings.Fields(cgoCflags)
622+
for i, p := range parts {
623+
if p == "-isysroot" && i+1 < len(parts) {
624+
sdkPath = parts[i+1]
625+
break
626+
}
627+
}
628+
629+
if sdkPath != "" {
630+
sdkLibPath := filepath.Join(sdkPath, "usr", "lib")
631+
rustflags += " -C link-arg=-L" + sdkLibPath
632+
}
633+
}
634+
635+
os.Setenv("RUSTFLAGS", rustflags)
609636
os.Setenv("CARGO_PROFILE_RELEASE_DEBUG", "false")
610637

611638
// cargo cinstall for C library installation
639+
// Use --no-default-features to avoid git_version which pulls in libgit2
640+
// Re-enable asm and threading for performance
612641
return runCommand(srcPath, os.Stdout, installDir, "cargo", "cinstall",
613642
fmt.Sprintf("--prefix=%s", installDir),
614643
"--libdir=lib",
615644
"--library-type=staticlib",
616645
"--crt-static",
617-
"--release")
646+
"--release",
647+
"--no-default-features",
648+
"--features=asm,threading")
618649
},
619650
},
620651
LinkLibs: []string{"librav1e"},
@@ -767,7 +798,7 @@ var ffmpeg = &Library{
767798
fmt.Printf("Applied OpenSSL 3.6 compatibility patch to tls_openssl.c\n")
768799
return nil
769800
},
770-
ConfigureArgs: func(os string) []string {
801+
ConfigureArgs: func(targetOS string) []string {
771802
// FFmpeg needs explicit paths to headers and libraries
772803
stagingDir, _ := filepath.Abs(".build/staging")
773804
incDir := filepath.Join(stagingDir, "include")
@@ -782,8 +813,16 @@ var ffmpeg = &Library{
782813
fmt.Sprintf("--extra-ldflags=%s", extraLdflags),
783814
}
784815

816+
// On macOS, force clang as the compiler
817+
// The Nix dev shell includes both gcc and clang, but our CFLAGS include
818+
// paths to Clang's builtin headers (stddef.h, stdarg.h) which use Clang-specific
819+
// features like __has_feature and __building_module that gcc doesn't understand
820+
if targetOS == "darwin" {
821+
args = append(args, "--cc=clang", "--cxx=clang++")
822+
}
823+
785824
// Add common FFmpeg arguments (platform-specific)
786-
args = append(args, FFmpegArgsCommon(os)...)
825+
args = append(args, FFmpegArgsCommon(targetOS)...)
787826

788827
return args
789828
},

internal/builder/library.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,100 @@ func buildEnv(installDir string) []string {
207207
env = append(env, "PATH="+binPath)
208208
}
209209

210+
// On macOS, remove NIX_CFLAGS_COMPILE which interferes with C++ header search order
211+
// The Nix clang wrapper injects -isystem paths that cause libc++ headers to be
212+
// searched after C standard library headers, breaking <cstddef> and similar wrappers
213+
if runtime.GOOS == "darwin" {
214+
filtered := env[:0]
215+
for _, e := range env {
216+
if !strings.HasPrefix(e, "NIX_CFLAGS_COMPILE=") {
217+
filtered = append(filtered, e)
218+
}
219+
}
220+
env = filtered
221+
222+
// Force clang as the compiler on macOS
223+
// The Nix dev shell includes both gcc and clang, but our CFLAGS include
224+
// paths to Clang's builtin headers (stddef.h, stdarg.h) which use Clang-specific
225+
// features like __has_feature and __building_module that gcc doesn't understand
226+
env = append(env, "CC=clang", "CXX=clang++")
227+
}
228+
229+
// On macOS, ensure CFLAGS/CXXFLAGS include SDK path and clang builtin headers
230+
// This is required for both C (stdarg.h, stddef.h) and C++ (<algorithm>, <cstring>) compilation
231+
if runtime.GOOS == "darwin" {
232+
cgoCflags := os.Getenv("CGO_CFLAGS")
233+
if cgoCflags != "" {
234+
// Set CFLAGS with full CGO_CFLAGS (includes -isysroot and -I.../clang/18/include)
235+
updatedCflags := false
236+
for i, e := range env {
237+
if strings.HasPrefix(e, "CFLAGS=") {
238+
existing := strings.TrimPrefix(e, "CFLAGS=")
239+
env[i] = "CFLAGS=" + existing + " " + cgoCflags
240+
updatedCflags = true
241+
break
242+
}
243+
}
244+
if !updatedCflags {
245+
env = append(env, "CFLAGS="+cgoCflags)
246+
}
247+
248+
// Build CXXFLAGS with -nostdinc++ and explicit libcxx include path
249+
// Use -nostdinc++ to disable built-in C++ paths, preventing NIX_CFLAGS_COMPILE
250+
// from interfering with header search order
251+
// Then add libc++ headers before clang builtins
252+
var cxxExtra string
253+
libcxxInclude := os.Getenv("LIBCXX_INCLUDE")
254+
if libcxxInclude != "" {
255+
cxxExtra = "-nostdinc++ -I" + libcxxInclude + " " + cgoCflags
256+
} else {
257+
cxxExtra = cgoCflags
258+
}
259+
260+
// Set CXXFLAGS with same flags for C++ builds
261+
updatedCxxflags := false
262+
for i, e := range env {
263+
if strings.HasPrefix(e, "CXXFLAGS=") {
264+
existing := strings.TrimPrefix(e, "CXXFLAGS=")
265+
env[i] = "CXXFLAGS=" + existing + " " + cxxExtra
266+
updatedCxxflags = true
267+
break
268+
}
269+
}
270+
if !updatedCxxflags {
271+
env = append(env, "CXXFLAGS="+cxxExtra)
272+
}
273+
274+
// Extract SDK path from CGO_CFLAGS (-isysroot <path>) for LDFLAGS
275+
// This ensures cargo/rustc can find SDK libraries like libiconv
276+
var sdkPath string
277+
parts := strings.Fields(cgoCflags)
278+
for i, p := range parts {
279+
if p == "-isysroot" && i+1 < len(parts) {
280+
sdkPath = parts[i+1]
281+
break
282+
}
283+
}
284+
if sdkPath != "" {
285+
ldExtra := "-L" + filepath.Join(sdkPath, "usr", "lib")
286+
updatedLdflags := false
287+
for i, e := range env {
288+
if strings.HasPrefix(e, "LDFLAGS=") {
289+
existing := strings.TrimPrefix(e, "LDFLAGS=")
290+
env[i] = "LDFLAGS=" + existing + " " + ldExtra
291+
updatedLdflags = true
292+
break
293+
}
294+
}
295+
if !updatedLdflags {
296+
env = append(env, "LDFLAGS="+ldExtra)
297+
}
298+
299+
// Also set LIBRARY_PATH for cargo/rustc which may not use LDFLAGS
300+
env = append(env, "LIBRARY_PATH="+filepath.Join(sdkPath, "usr", "lib"))
301+
}
302+
}
303+
}
304+
210305
return env
211306
}

internal/builder/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@ func combineLibraries(libs []*Library, stagingDir, output string) error {
210210
return combineLinux(libFiles, output)
211211
}
212212

213-
// combineMac uses libtool to combine static libraries on macOS
213+
// combineMac uses Apple's libtool to combine static libraries on macOS
214214
// This is more efficient than ar as it doesn't require extracting all object files
215215
func combineMac(libFiles []string, output string) error {
216-
log.Println("Using libtool -static approach (macOS)")
216+
log.Println("Using Apple libtool -static approach (macOS)")
217217

218218
// Ensure output directory exists
219219
outputDir := filepath.Dir(output)
@@ -224,10 +224,10 @@ func combineMac(libFiles []string, output string) error {
224224
// Remove existing output file if present
225225
os.Remove(output)
226226

227-
// Use libtool to combine libraries directly
228-
// libtool -static is Apple's tool specifically designed for this purpose
227+
// Use Apple's libtool (not GNU libtool from Nix) to combine libraries directly
228+
// Apple's libtool -static is specifically designed for this purpose
229229
args := append([]string{"-static", "-o", output}, libFiles...)
230-
libtoolCmd := exec.Command("libtool", args...)
230+
libtoolCmd := exec.Command("/usr/bin/libtool", args...)
231231

232232
// Capture output for debugging
233233
var stdout, stderr bytes.Buffer

0 commit comments

Comments
 (0)