diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ab26f21fb..46423f9f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,19 +59,11 @@ jobs: buildtype: release, args: -Db_pch=false } - - name: macOS x86_64 Debug - os: macos-15-intel - buildtype: debugoptimized - args: &macos_debug_args >- - -Dmacos_deployment_target=15.0 - -Dbuild_osx_bundle=false - -Dfftw3=enabled - -Dlibpulse=disabled - -Dopenal=enabled - name: macOS x86_64 Release - os: macos-15-intel + os: macos-15 + cross_args: --cross-file tools/macos-release.ini --cross-file tools/macos-x86_64.ini buildtype: release - args: &macos_release_args >- + args: &macos_source_args >- -Ddefault_library=static -Dbuild_osx_bundle=true -Dfftw3=disabled @@ -102,11 +94,17 @@ jobs: - name: macOS arm64 Debug os: macos-15 buildtype: debugoptimized - args: *macos_debug_args + args: >- + -Dmacos_deployment_target=15.0 + -Dbuild_osx_bundle=false + -Dfftw3=enabled + -Dlibpulse=disabled + -Dopenal=enabled - name: macOS arm64 Release os: macos-15 + native_args: --native-file tools/macos-release.ini buildtype: release - args: *macos_release_args + args: *macos_source_args steps: - uses: actions/checkout@v6 @@ -159,17 +157,34 @@ jobs: echo "PKG_CONFIG_PATH=$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix zlib)/lib/pkgconfig" >> "${GITHUB_ENV}" - - name: Install dependencies (macOS Release) + - name: Install build tools (macOS source dependencies) if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' run: | brew install ninja nasm + - name: Check Rosetta (macOS cross builds) + if: matrix.config.cross_args + run: | + if ! /usr/bin/arch -x86_64 /usr/bin/true; then + sudo softwareupdate --install-rosetta --agree-to-license + fi + /usr/bin/arch -x86_64 /usr/bin/true + - name: Install test dependencies (macOS) if: startsWith(matrix.config.os, 'macos-') + env: + CROSS_ARGS: ${{ matrix.config.cross_args }} run: | # LuaJIT is Lua 5.1 to luarocks. brew install luajit luarocks - luarocks --lua-dir="$(brew --prefix luajit)" --lua-version=5.1 install busted + if test -n "${CROSS_ARGS}"; then + # Busted's native modules are loaded by the Intel test executable. + luarocks --lua-dir="$(brew --prefix luajit)" --lua-version=5.1 install busted \ + CC='/usr/bin/clang -arch x86_64' \ + LD='/usr/bin/clang -arch x86_64' + else + luarocks --lua-dir="$(brew --prefix luajit)" --lua-version=5.1 install busted + fi - name: Install dependencies (Linux) if: startsWith(matrix.config.os, 'ubuntu-') @@ -184,7 +199,7 @@ jobs: - name: Configure run: | ${{ matrix.config.devenv }} - meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} + meson setup build ${{ matrix.config.native_args }} ${{ matrix.config.cross_args }} ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} - name: Build FFTW (MacOS) if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' @@ -199,6 +214,14 @@ jobs: ${{ matrix.config.devenv }} meson compile -C build + - name: Verify Intel cross-build architecture + if: matrix.config.cross_args + run: | + test "$(lipo -archs build/aegisub)" = x86_64 + test "$(lipo -archs build/tests/gtest-main)" = x86_64 + test "$(lipo -archs build/tests/luajit-52)" = x86_64 + test "$(lipo -archs build/automation/tests/aegisub-lua)" = x86_64 + # The automation suite is registered at configure time only if the # busted probe succeeds, so make sure it can't silently drop out of CI - name: Check automation tests are registered @@ -211,6 +234,18 @@ jobs: ${{ matrix.config.devenv }} meson test -C build --verbose --suite Aegisub + - name: Check cross-built tests executed + if: matrix.config.cross_args + run: | + python3 - <<'PY' + import json + from pathlib import Path + + results = [json.loads(line) for line in Path('build/meson-logs/testlog.json').read_text().splitlines()] + if not results or any(result['result'] != 'OK' for result in results): + raise SystemExit('Cross-built tests must pass under Rosetta, not be skipped') + PY + # Windows artifacts - name: Generate Windows installer if: matrix.config.os == 'windows-latest' diff --git a/docs/developer_docs.md b/docs/developer_docs.md index 734106987a..b34b4abf54 100644 --- a/docs/developer_docs.md +++ b/docs/developer_docs.md @@ -109,6 +109,32 @@ the notary service and stapled. This follows Apple's nested-container guidance; the app and every nested Mach-O file are still Developer ID-signed before the DMG is created. +## Intel macOS cross builds + +The Intel Release CI lane uses an Apple Silicon runner with +`--cross-file tools/macos-release.ini --cross-file tools/macos-x86_64.ini`. +Compilation uses native Apple Silicon tools targeting x86_64; Rosetta runs +the resulting Intel test executables. +This checks the Intel code path, but does not replace testing a release on +real Intel hardware. + +The Intel lane builds its target dependencies from source using the shared +macOS release dependency options in `.github/workflows/ci.yml`. Both Release +lanes use `tools/macos-release.ini` to restrict pkg-config and CMake dependency +discovery away from Homebrew; arm64 loads it with `--native-file`. Homebrew is +still used for build tools. The native arm64 Debug lane still uses +Homebrew libraries and is tested but not packaged. Intel coverage comes from +the Release lane, including tests under Rosetta. + +For a local cross build, use the same dependency options and cross files as CI. +After Meson setup, run `tools/macos-build-fftw.sh BUILD_DIR`, then reconfigure +with `-Dpkg_config_path="$PWD/BUILD_DIR/fftw-prefix/lib/pkgconfig"` +and `-Dfftw3=enabled`. The bootstrap reads the target architecture from Meson +and only passes Autoconf's `--host` option when it differs from the build CPU. +Automation tests also need Intel builds of Busted's native Lua modules; the +workflow shows the LuaRocks compiler overrides. Use a separate LuaRocks tree +if native arm64 tests need to run on the same machine. + ## Running Doxygen You can run Doxygen with the following command: diff --git a/tools/macos-build-fftw.sh b/tools/macos-build-fftw.sh index cbcf8b74e9..890c3ac025 100755 --- a/tools/macos-build-fftw.sh +++ b/tools/macos-build-fftw.sh @@ -29,8 +29,17 @@ FFTW_SHA256="5630c24cdeb33b131612f7eb4b1a9934234754f9f388ff8617458d0be6f239a1" FFTW_URL="https://fftw.org/fftw-${FFTW_VERSION}.tar.gz" FFTW_PREFIX="${BUILD_DIR}/fftw-prefix" FFTW_STAMP="${FFTW_PREFIX}/.aegisub-build" -ARCH="$(uname -m)" -EXPECTED_STAMP="fftw=${FFTW_VERSION} arch=${ARCH} macos=${DEPLOYMENT_TARGET}" +ARCH="$(meson introspect --machines "${BUILD_DIR}" | python3 -c ' +import json +import sys + +machine = json.load(sys.stdin)["host"] +if machine["system"] != "darwin": + raise SystemExit("FFTW macOS bootstrap requires a Darwin host") +print({"aarch64": "arm64", "x86_64": "x86_64"}.get(machine["cpu_family"], machine["cpu_family"])) +')" +BUILD_ARCH="$(uname -m)" +EXPECTED_STAMP="fftw=${FFTW_VERSION} arch=${ARCH} build=${BUILD_ARCH} macos=${DEPLOYMENT_TARGET}" if test -f "${FFTW_STAMP}" && test "$(cat "${FFTW_STAMP}")" = "${EXPECTED_STAMP}"; then echo "Using existing FFTW build: ${EXPECTED_STAMP}" @@ -62,12 +71,19 @@ rm -rf "${FFTW_PREFIX}" mkdir -p "${FFTW_PREFIX}" cd "${WORK_DIR}/fftw-${FFTW_VERSION}" -export CFLAGS="-O3 -mmacosx-version-min=${DEPLOYMENT_TARGET}" -export LDFLAGS="-mmacosx-version-min=${DEPLOYMENT_TARGET}" +export CC=/usr/bin/clang +export CFLAGS="-O3 -arch ${ARCH} -mmacosx-version-min=${DEPLOYMENT_TARGET}" +export LDFLAGS="-arch ${ARCH} -mmacosx-version-min=${DEPLOYMENT_TARGET}" + +# Keep runtime probes enabled for native builds. +set -- "--build=$(./config.guess)" +if test "${ARCH}" != "${BUILD_ARCH}"; then + set -- "$@" "--host=${ARCH}-apple-darwin" +fi # Aegisub only uses FFTW's single-threaded, double-precision API. # shellcheck disable=SC2086 -./configure \ +./configure "$@" \ --prefix="${FFTW_PREFIX}" \ --disable-shared \ --enable-static \ @@ -80,6 +96,10 @@ export LDFLAGS="-mmacosx-version-min=${DEPLOYMENT_TARGET}" make -s -j"$(sysctl -n hw.logicalcpu)" make -s install +if test "$(lipo -archs "${FFTW_PREFIX}/lib/libfftw3.a")" != "${ARCH}"; then + echo "FFTW library architecture does not match Meson's host: ${ARCH}" >&2 + exit 1 +fi printf '%s\n' "${EXPECTED_STAMP}" > "${FFTW_STAMP}" echo "Built FFTW ${FFTW_VERSION} for macOS ${DEPLOYMENT_TARGET} (${ARCH})" diff --git a/tools/macos-release.ini b/tools/macos-release.ini new file mode 100644 index 0000000000..969f512863 --- /dev/null +++ b/tools/macos-release.ini @@ -0,0 +1,9 @@ +# Use Homebrew build tools, but exclude its target libraries from discovery. +[properties] +pkg_config_libdir = ['/usr/lib/pkgconfig'] + +[built-in options] +pkg_config_path = [] + +[cmake] +CMAKE_IGNORE_PREFIX_PATH = ['/opt/homebrew', '/usr/local'] diff --git a/tools/macos-x86_64.ini b/tools/macos-x86_64.ini new file mode 100644 index 0000000000..e4233c7eee --- /dev/null +++ b/tools/macos-x86_64.ini @@ -0,0 +1,24 @@ +# Build Intel binaries with native Apple Silicon tools; run tests via Rosetta. +# Combine with macos-release.ini for target dependency search paths. +[binaries] +c = ['/usr/bin/clang', '-arch', 'x86_64'] +cpp = ['/usr/bin/clang++', '-arch', 'x86_64'] +objc = ['/usr/bin/clang', '-arch', 'x86_64'] +objcpp = ['/usr/bin/clang++', '-arch', 'x86_64'] +ar = '/usr/bin/ar' +strip = '/usr/bin/strip' +pkg-config = 'pkg-config' +cmake = 'cmake' +exe_wrapper = ['/usr/bin/arch', '-x86_64'] + +[host_machine] +system = 'darwin' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' + +[properties] +needs_exe_wrapper = true + +[cmake] +CMAKE_OSX_ARCHITECTURES = 'x86_64'