Skip to content

Commit 8a8ea16

Browse files
committed
Build windows aarch64
1 parent e73e8ca commit 8a8ea16

File tree

3 files changed

+123
-24
lines changed

3 files changed

+123
-24
lines changed

.github/workflows/build-ffmpeg.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,19 @@ jobs:
3737
shell: 'msys2 {0}'
3838
msys_prefix: mingw-w64-x86_64
3939
msys_system: MINGW64
40+
- os: windows-11-arm
41+
arch: arm64
42+
shell: 'msys2 {0}'
43+
msys_prefix: mingw-w64-clang-aarch64
44+
msys_system: CLANGARM64
4045
defaults:
4146
run:
4247
shell: ${{ matrix.shell }}
4348
steps:
4449
- uses: actions/checkout@v6
4550
- uses: actions/setup-python@v6
4651
with:
47-
python-version: "3.13"
52+
python-version: "3.14"
4853
- name: Set deployment target
4954
if: runner.os == 'macOS'
5055
run: |
@@ -62,14 +67,18 @@ jobs:
6267
brew install yasm
6368
fi
6469
- uses: msys2/setup-msys2@v2
65-
if: matrix.os == 'windows-latest'
70+
if: runner.os == 'Windows'
6671
with:
67-
install: base-devel openssl-devel ${{ matrix.msys_prefix }}-gcc ${{ matrix.msys_prefix }}-nasm
72+
install: >-
73+
base-devel
74+
openssl-devel
75+
${{ matrix.msys_prefix }}-pkgconf
76+
${{ matrix.msys_system == 'CLANGARM64' && format('{0}-clang', matrix.msys_prefix) || format('{0}-gcc {0}-nasm', matrix.msys_prefix) }}
6877
msystem: ${{ matrix.msys_system }}
6978
path-type: inherit
7079
- name: Build FFmpeg
7180
env:
72-
CIBW_ARCHS: ${{ matrix.msys_prefix && 'AMD64' || matrix.arch }}
81+
CIBW_ARCHS: ${{ matrix.msys_system == 'CLANGARM64' && 'ARM64' || (matrix.msys_prefix && 'AMD64' || matrix.arch) }}
7382
CIBW_BEFORE_BUILD: python scripts/build-ffmpeg.py /tmp/vendor
7483
CIBW_BEFORE_BUILD_WINDOWS: python scripts\build-ffmpeg.py C:\cibw\vendor
7584
CIBW_BUILD: cp311-*

scripts/build-ffmpeg.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,17 @@ def main():
278278
args = parser.parse_args()
279279
dest_dir = os.path.abspath(args.destination)
280280

281+
machine = platform.machine().lower()
282+
is_arm = machine in {"arm64", "aarch64"}
283+
281284
use_alsa = plat == "Linux"
282-
use_cuda = plat in {"Linux", "Windows"}
283-
use_amf = plat in {"Linux", "Windows"}
285+
# CUDA, AMF, and Intel VPL are not available on ARM64 Windows
286+
use_cuda = plat in {"Linux", "Windows"} and not is_arm
287+
use_amf = plat in {"Linux", "Windows"} and not is_arm
284288

285289
# Use Intel VPL (Video Processing Library) if supported to enable Intel QSV (Quick Sync Video)
286290
# hardware encoders/decoders on modern integrated and discrete Intel GPUs.
287-
use_libvpl = plat in {"Linux", "Windows"}
291+
use_libvpl = plat in {"Linux", "Windows"} and not is_arm
288292

289293
# Use GnuTLS only on Linux, FFmpeg has native TLS backends for macOS and Windows.
290294
use_gnutls = plat == "Linux"
@@ -303,19 +307,25 @@ def main():
303307
# install packages
304308
available_tools = set()
305309
if plat == "Windows":
306-
available_tools.update(["nasm"])
310+
if not is_arm:
311+
available_tools.update(["nasm"])
307312

308313
# print tool locations
309314
print("PATH", os.environ["PATH"])
310-
for tool in ["gcc", "g++", "curl", "ld", "nasm", "pkg-config"]:
315+
if is_arm:
316+
# CLANGARM64 uses clang instead of gcc
317+
tools = ["clang", "clang++", "curl", "ld", "pkg-config"]
318+
else:
319+
tools = ["gcc", "g++", "curl", "ld", "nasm", "pkg-config"]
320+
for tool in tools:
311321
run(["where", tool])
312322

313323
with log_group("install python packages"):
314324
run(["pip", "install", "cmake==3.31.10", "meson", "ninja"])
315325

316326
# build tools
317327
build_tools = []
318-
if "nasm" not in available_tools and platform.machine() not in {"arm64", "aarch64"}:
328+
if "nasm" not in available_tools and platform.machine().lower() not in {"arm64", "aarch64"}:
319329
build_tools.append(
320330
Package(
321331
name="nasm",
@@ -388,6 +398,15 @@ def main():
388398
]
389399
)
390400

401+
if plat == "Windows" and is_arm:
402+
ffmpeg_package.build_arguments.extend(
403+
[
404+
"--cc=clang",
405+
"--cxx=clang++",
406+
"--arch=aarch64",
407+
]
408+
)
409+
391410
ffmpeg_package.build_arguments.extend(
392411
[
393412
"--disable-encoder=avui,dca,mlp,opus,s302m,sonic,sonic_ls,truehd,vorbis",
@@ -412,6 +431,14 @@ def main():
412431
packages += codec_group
413432
packages += [ffmpeg_package]
414433

434+
# Disable runtime CPU detection for opus on Windows ARM64
435+
# (no CPU detection method available for this platform)
436+
if plat == "Windows" and is_arm:
437+
for pkg in packages:
438+
if pkg.name == "opus":
439+
pkg.build_arguments.append("--disable-rtcd")
440+
break
441+
415442
download_tars(build_tools + packages)
416443
for tool in build_tools:
417444
builder.build(tool, for_builder=True)
@@ -437,19 +464,33 @@ def main():
437464
)
438465

439466
# copy some libraries provided by mingw
467+
machine = platform.machine().lower()
468+
is_arm64 = machine in {"arm64", "aarch64"}
469+
compiler = "clang" if is_arm64 else "gcc"
440470
mingw_bindir = os.path.dirname(
441-
subprocess.run(["where", "gcc"], check=True, stdout=subprocess.PIPE)
471+
subprocess.run(["where", compiler], check=True, stdout=subprocess.PIPE)
442472
.stdout.decode()
443473
.splitlines()[0]
444474
.strip()
445475
)
446-
for name in (
447-
"libgcc_s_seh-1.dll",
448-
"libiconv-2.dll",
449-
"libstdc++-6.dll",
450-
"libwinpthread-1.dll",
451-
"zlib1.dll",
452-
):
476+
if is_arm64:
477+
# CLANGARM64 uses clang/libc++ instead of gcc/libstdc++
478+
dll_names = (
479+
"libc++.dll",
480+
"libiconv-2.dll",
481+
"libunwind.dll",
482+
"libwinpthread-1.dll",
483+
"zlib1.dll",
484+
)
485+
else:
486+
dll_names = (
487+
"libgcc_s_seh-1.dll",
488+
"libiconv-2.dll",
489+
"libstdc++-6.dll",
490+
"libwinpthread-1.dll",
491+
"zlib1.dll",
492+
)
493+
for name in dll_names:
453494
shutil.copy(os.path.join(mingw_bindir, name), os.path.join(dest_dir, "bin"))
454495

455496
# find libraries

scripts/cibuildpkg.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def run(cmd: list[str], env=None) -> None:
7474
subprocess.run(cmd, check=True, env=env, stderr=subprocess.PIPE, text=True)
7575
except subprocess.CalledProcessError as e:
7676
print(f"stderr: {e.stderr}")
77+
# Print config.log tail if it exists (for ffmpeg configure debugging)
78+
config_log = os.path.join(os.getcwd(), "ffbuild", "config.log")
79+
if os.path.exists(config_log):
80+
print(f"\n=== Tail of {config_log} ===")
81+
with open(config_log, "r") as f:
82+
lines = f.readlines()
83+
print("".join(lines[-100:]))
7784
raise e
7885

7986

@@ -204,20 +211,31 @@ def _build_with_autoconf(self, package: Package, for_builder: bool) -> None:
204211
env = self._environment(for_builder=for_builder)
205212
prefix = self._prefix(for_builder=for_builder)
206213
configure_args = [
207-
"--disable-static",
208214
"--enable-shared",
209215
"--libdir=" + self._mangle_path(os.path.join(prefix, "lib")),
210216
"--prefix=" + self._mangle_path(prefix),
211217
]
212218

219+
if package.name == "x264":
220+
# Disable asm on Windows ARM64 (no nasm available)
221+
if platform.system() == "Windows" and platform.machine().lower() in {"arm64", "aarch64"}:
222+
configure_args.append("--disable-asm")
223+
# Specify host to ensure correct resource compiler target
224+
configure_args.append("--host=aarch64-w64-mingw32")
225+
213226
if package.name == "vpx":
214227
if platform.system() == "Darwin":
215228
if platform.machine() == "arm64":
216229
configure_args += ["--target=arm64-darwin20-gcc"]
217230
elif platform.machine() == "x86_64":
218231
configure_args += ["--target=x86_64-darwin20-gcc"]
219232
elif platform.system() == "Windows":
220-
configure_args += ["--target=x86_64-win64-gcc"]
233+
if platform.machine().lower() in {"arm64", "aarch64"}:
234+
configure_args += ["--target=arm64-win64-gcc"]
235+
# Link pthread for ARM64 Windows
236+
prepend_env(env, "LDFLAGS", "-lpthread")
237+
else:
238+
configure_args += ["--target=x86_64-win64-gcc"]
221239
elif platform.system() == "Linux":
222240
if "RUNNER_ARCH" in os.environ:
223241
prepend_env(env, "CFLAGS", "-pthread")
@@ -229,9 +247,24 @@ def _build_with_autoconf(self, package: Package, for_builder: bool) -> None:
229247
prepend_env(
230248
env,
231249
"PKG_CONFIG_PATH",
232-
"/c/msys64/usr/lib/pkgconfig",
233-
separator=":",
250+
"C:/msys64/usr/lib/pkgconfig",
251+
separator=";",
252+
)
253+
# Debug: print pkg-config info
254+
print(f"PKG_CONFIG_PATH: {env.get('PKG_CONFIG_PATH')}")
255+
print(f"PKG_CONFIG: {env.get('PKG_CONFIG')}")
256+
import glob
257+
pc_files = glob.glob(os.path.join(prefix, "lib", "pkgconfig", "*.pc"))
258+
print(f"PC files in {prefix}/lib/pkgconfig: {pc_files}")
259+
# Test pkgconf directly
260+
import subprocess
261+
result = subprocess.run(
262+
["pkgconf", "--modversion", "dav1d"],
263+
env=env,
264+
capture_output=True,
265+
text=True
234266
)
267+
print(f"pkgconf dav1d test: returncode={result.returncode}, stdout={result.stdout}, stderr={result.stderr}")
235268

236269
# build package
237270
os.makedirs(package_build_path, exist_ok=True)
@@ -419,18 +452,34 @@ def _environment(self, *, for_builder: bool) -> dict[str, str]:
419452
prepend_env(
420453
env, "LDFLAGS", "-L" + self._mangle_path(os.path.join(prefix, "lib"))
421454
)
455+
# Use ; as separator on Windows, : on Unix
456+
# Don't mangle PKG_CONFIG_PATH on Windows - pkgconf expects native paths
457+
pkg_config_sep = ";" if platform.system() == "Windows" else ":"
458+
pkg_config_path = os.path.join(prefix, "lib", "pkgconfig")
459+
if platform.system() != "Windows":
460+
pkg_config_path = self._mangle_path(pkg_config_path)
422461
prepend_env(
423462
env,
424463
"PKG_CONFIG_PATH",
425-
self._mangle_path(os.path.join(prefix, "lib", "pkgconfig")),
426-
separator=":",
464+
pkg_config_path,
465+
separator=pkg_config_sep,
427466
)
428467

429468
if platform.system() == "Darwin" and not for_builder:
430469
arch_flags = os.environ["ARCHFLAGS"]
431470
for var in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
432471
prepend_env(env, var, arch_flags)
433472

473+
# Use clang on Windows ARM64 (CLANGARM64 environment)
474+
if platform.system() == "Windows" and platform.machine().lower() in {"arm64", "aarch64"}:
475+
env["CC"] = "clang"
476+
env["CXX"] = "clang++"
477+
# Use LLVM windres to compile Windows resources for ARM64
478+
env["RC"] = "llvm-windres"
479+
env["WINDRES"] = "llvm-windres"
480+
# Use MinGW pkgconf instead of MSYS pkg-config
481+
env["PKG_CONFIG"] = "pkgconf"
482+
434483
return env
435484

436485
def _mangle_path(self, path: str) -> str:

0 commit comments

Comments
 (0)