diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index b679863b..31933cb3 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -37,6 +37,11 @@ jobs: shell: 'msys2 {0}' msys_prefix: mingw-w64-x86_64 msys_system: MINGW64 + - os: windows-11-arm + arch: arm64 + shell: 'msys2 {0}' + msys_prefix: mingw-w64-clang-aarch64 + msys_system: CLANGARM64 defaults: run: shell: ${{ matrix.shell }} @@ -44,7 +49,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Set deployment target if: runner.os == 'macOS' run: | @@ -62,14 +67,18 @@ jobs: brew install yasm fi - uses: msys2/setup-msys2@v2 - if: matrix.os == 'windows-latest' + if: runner.os == 'Windows' with: - install: base-devel openssl-devel ${{ matrix.msys_prefix }}-gcc ${{ matrix.msys_prefix }}-nasm + install: >- + base-devel + openssl-devel + ${{ matrix.msys_prefix }}-pkgconf + ${{ matrix.msys_system == 'CLANGARM64' && format('{0}-clang', matrix.msys_prefix) || format('{0}-gcc {0}-nasm', matrix.msys_prefix) }} msystem: ${{ matrix.msys_system }} path-type: inherit - name: Build FFmpeg env: - CIBW_ARCHS: ${{ matrix.msys_prefix && 'AMD64' || matrix.arch }} + CIBW_ARCHS: ${{ matrix.msys_system == 'CLANGARM64' && 'ARM64' || (matrix.msys_prefix && 'AMD64' || matrix.arch) }} CIBW_BEFORE_BUILD: python scripts/build-ffmpeg.py /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\build-ffmpeg.py C:\cibw\vendor CIBW_BUILD: cp311-* diff --git a/scripts/build-ffmpeg.py b/scripts/build-ffmpeg.py index 14cddd94..f7a6d949 100644 --- a/scripts/build-ffmpeg.py +++ b/scripts/build-ffmpeg.py @@ -278,13 +278,17 @@ def main(): args = parser.parse_args() dest_dir = os.path.abspath(args.destination) + machine = platform.machine().lower() + is_arm = machine in {"arm64", "aarch64"} + use_alsa = plat == "Linux" - use_cuda = plat in {"Linux", "Windows"} - use_amf = plat in {"Linux", "Windows"} + # CUDA, AMF, and Intel VPL are not available on ARM64 Windows + use_cuda = plat in {"Linux", "Windows"} and not is_arm + use_amf = plat in {"Linux", "Windows"} and not is_arm # Use Intel VPL (Video Processing Library) if supported to enable Intel QSV (Quick Sync Video) # hardware encoders/decoders on modern integrated and discrete Intel GPUs. - use_libvpl = plat in {"Linux", "Windows"} + use_libvpl = plat in {"Linux", "Windows"} and not is_arm # Use GnuTLS only on Linux, FFmpeg has native TLS backends for macOS and Windows. use_gnutls = plat == "Linux" @@ -303,11 +307,17 @@ def main(): # install packages available_tools = set() if plat == "Windows": - available_tools.update(["nasm"]) + if not is_arm: + available_tools.update(["nasm"]) # print tool locations print("PATH", os.environ["PATH"]) - for tool in ["gcc", "g++", "curl", "ld", "nasm", "pkg-config"]: + if is_arm: + # CLANGARM64 uses clang instead of gcc + tools = ["clang", "clang++", "curl", "ld", "pkg-config"] + else: + tools = ["gcc", "g++", "curl", "ld", "nasm", "pkg-config"] + for tool in tools: run(["where", tool]) with log_group("install python packages"): @@ -315,7 +325,7 @@ def main(): # build tools build_tools = [] - if "nasm" not in available_tools and platform.machine() not in {"arm64", "aarch64"}: + if "nasm" not in available_tools and platform.machine().lower() not in {"arm64", "aarch64"}: build_tools.append( Package( name="nasm", @@ -388,6 +398,15 @@ def main(): ] ) + if plat == "Windows" and is_arm: + ffmpeg_package.build_arguments.extend( + [ + "--cc=clang", + "--cxx=clang++", + "--arch=aarch64", + ] + ) + ffmpeg_package.build_arguments.extend( [ "--disable-encoder=avui,dca,mlp,opus,s302m,sonic,sonic_ls,truehd,vorbis", @@ -412,6 +431,14 @@ def main(): packages += codec_group packages += [ffmpeg_package] + # Disable runtime CPU detection for opus on Windows ARM64 + # (no CPU detection method available for this platform) + if plat == "Windows" and is_arm: + for pkg in packages: + if pkg.name == "opus": + pkg.build_arguments.append("--disable-rtcd") + break + download_tars(build_tools + packages) for tool in build_tools: builder.build(tool, for_builder=True) @@ -437,19 +464,33 @@ def main(): ) # copy some libraries provided by mingw + machine = platform.machine().lower() + is_arm64 = machine in {"arm64", "aarch64"} + compiler = "clang" if is_arm64 else "gcc" mingw_bindir = os.path.dirname( - subprocess.run(["where", "gcc"], check=True, stdout=subprocess.PIPE) + subprocess.run(["where", compiler], check=True, stdout=subprocess.PIPE) .stdout.decode() .splitlines()[0] .strip() ) - for name in ( - "libgcc_s_seh-1.dll", - "libiconv-2.dll", - "libstdc++-6.dll", - "libwinpthread-1.dll", - "zlib1.dll", - ): + if is_arm64: + # CLANGARM64 uses clang/libc++ instead of gcc/libstdc++ + dll_names = ( + "libc++.dll", + "libiconv-2.dll", + "libunwind.dll", + "libwinpthread-1.dll", + "zlib1.dll", + ) + else: + dll_names = ( + "libgcc_s_seh-1.dll", + "libiconv-2.dll", + "libstdc++-6.dll", + "libwinpthread-1.dll", + "zlib1.dll", + ) + for name in dll_names: shutil.copy(os.path.join(mingw_bindir, name), os.path.join(dest_dir, "bin")) # find libraries diff --git a/scripts/cibuildpkg.py b/scripts/cibuildpkg.py index 01ea20ff..b8af6f9a 100644 --- a/scripts/cibuildpkg.py +++ b/scripts/cibuildpkg.py @@ -74,6 +74,13 @@ def run(cmd: list[str], env=None) -> None: subprocess.run(cmd, check=True, env=env, stderr=subprocess.PIPE, text=True) except subprocess.CalledProcessError as e: print(f"stderr: {e.stderr}") + # Print config.log tail if it exists (for ffmpeg configure debugging) + config_log = os.path.join(os.getcwd(), "ffbuild", "config.log") + if os.path.exists(config_log): + print(f"\n=== Tail of {config_log} ===") + with open(config_log, "r") as f: + lines = f.readlines() + print("".join(lines[-100:])) raise e @@ -204,12 +211,18 @@ def _build_with_autoconf(self, package: Package, for_builder: bool) -> None: env = self._environment(for_builder=for_builder) prefix = self._prefix(for_builder=for_builder) configure_args = [ - "--disable-static", "--enable-shared", "--libdir=" + self._mangle_path(os.path.join(prefix, "lib")), "--prefix=" + self._mangle_path(prefix), ] + if package.name == "x264": + # Disable asm on Windows ARM64 (no nasm available) + if platform.system() == "Windows" and platform.machine().lower() in {"arm64", "aarch64"}: + configure_args.append("--disable-asm") + # Specify host to ensure correct resource compiler target + configure_args.append("--host=aarch64-w64-mingw32") + if package.name == "vpx": if platform.system() == "Darwin": if platform.machine() == "arm64": @@ -217,7 +230,12 @@ def _build_with_autoconf(self, package: Package, for_builder: bool) -> None: elif platform.machine() == "x86_64": configure_args += ["--target=x86_64-darwin20-gcc"] elif platform.system() == "Windows": - configure_args += ["--target=x86_64-win64-gcc"] + if platform.machine().lower() in {"arm64", "aarch64"}: + configure_args += ["--target=arm64-win64-gcc"] + # Link pthread for ARM64 Windows + prepend_env(env, "LDFLAGS", "-lpthread") + else: + configure_args += ["--target=x86_64-win64-gcc"] elif platform.system() == "Linux": if "RUNNER_ARCH" in os.environ: prepend_env(env, "CFLAGS", "-pthread") @@ -229,9 +247,24 @@ def _build_with_autoconf(self, package: Package, for_builder: bool) -> None: prepend_env( env, "PKG_CONFIG_PATH", - "/c/msys64/usr/lib/pkgconfig", - separator=":", + "C:/msys64/usr/lib/pkgconfig", + separator=";", + ) + # Debug: print pkg-config info + print(f"PKG_CONFIG_PATH: {env.get('PKG_CONFIG_PATH')}") + print(f"PKG_CONFIG: {env.get('PKG_CONFIG')}") + import glob + pc_files = glob.glob(os.path.join(prefix, "lib", "pkgconfig", "*.pc")) + print(f"PC files in {prefix}/lib/pkgconfig: {pc_files}") + # Test pkgconf directly + import subprocess + result = subprocess.run( + ["pkgconf", "--modversion", "dav1d"], + env=env, + capture_output=True, + text=True ) + print(f"pkgconf dav1d test: returncode={result.returncode}, stdout={result.stdout}, stderr={result.stderr}") # build package os.makedirs(package_build_path, exist_ok=True) @@ -419,11 +452,17 @@ def _environment(self, *, for_builder: bool) -> dict[str, str]: prepend_env( env, "LDFLAGS", "-L" + self._mangle_path(os.path.join(prefix, "lib")) ) + # Use ; as separator on Windows, : on Unix + # Don't mangle PKG_CONFIG_PATH on Windows - pkgconf expects native paths + pkg_config_sep = ";" if platform.system() == "Windows" else ":" + pkg_config_path = os.path.join(prefix, "lib", "pkgconfig") + if platform.system() != "Windows": + pkg_config_path = self._mangle_path(pkg_config_path) prepend_env( env, "PKG_CONFIG_PATH", - self._mangle_path(os.path.join(prefix, "lib", "pkgconfig")), - separator=":", + pkg_config_path, + separator=pkg_config_sep, ) if platform.system() == "Darwin" and not for_builder: @@ -431,6 +470,12 @@ def _environment(self, *, for_builder: bool) -> dict[str, str]: for var in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]: prepend_env(env, var, arch_flags) + if platform.system() == "Windows" and platform.machine().lower() in {"arm64", "aarch64"}: + env["CC"] = "clang" + env["CXX"] = "clang++" + env["RC"] = "llvm-windres" + env["WINDRES"] = "llvm-windres" + return env def _mangle_path(self, path: str) -> str: