From 0b1c9700029c383c6a50109bd47917563dbcc3d7 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Tue, 4 Aug 2026 14:04:04 -0700 Subject: [PATCH 01/14] build: add local macOS release signing Keep Developer ID and notarization credentials off CI by uploading an ad-hoc-signed app handoff for tags and manual runs. Add local signing, DMG, and notarization targets and architecture-specific package names. --- .github/workflows/ci.yml | 19 ++++++- docs/developer_docs.md | 7 ++- packages/meson.build | 13 ++++- packages/osx_bundle/aegisub.entitlements | 9 +++ packages/win_installer/fragment_setupbase.iss | 6 +- .../portable/create-portable.ps1 | 18 +++++- tools/osx-bundle.sh | 31 ++++------ tools/osx-dmg.sh | 57 ++++++++++++------- tools/osx-notarize.sh | 42 ++++++++++++++ tools/osx-package-name.sh | 31 ++++++++++ tools/osx-sign.sh | 52 +++++++++++++++++ tools/version.ps1 | 6 +- tools/version.sh | 2 + 13 files changed, 244 insertions(+), 49 deletions(-) create mode 100644 packages/osx_bundle/aegisub.entitlements create mode 100755 tools/osx-notarize.sh create mode 100755 tools/osx-package-name.sh create mode 100755 tools/osx-sign.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de06dc0b34..cf913416e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,7 +181,8 @@ jobs: if: matrix.config.os == 'windows-latest' with: name: ${{ matrix.config.name }} - portable - path: build/aegisub-portable-64.zip + path: build/Aegisub-*-portable.zip + if-no-files-found: error # macOS artifacts - name: Generate macOS installer @@ -201,11 +202,25 @@ jobs: i=$((i+1)) done + - name: Archive macOS app for local signing + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' && (github.ref_type == 'tag' || github.event_name == 'workflow_dispatch') + run: | + package_name="$(tools/osx-package-name.sh build/Aegisub.app)" + ditto -c -k --sequesterRsrc --keepParent build/Aegisub.app "build/${package_name}-signing-input.zip" + + - name: Upload macOS local-signing input + uses: actions/upload-artifact@v6 + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' && (github.ref_type == 'tag' || github.event_name == 'workflow_dispatch') + with: + name: ${{ matrix.config.name }} - local signing input + path: build/Aegisub-*-signing-input.zip + if-no-files-found: error + - name: Upload artifacts - macOS dmg uses: actions/upload-artifact@v6 if: startsWith(matrix.config.os, 'macos-') with: - name: ${{ matrix.config.name }} - installer + name: ${{ matrix.config.name }} - ad-hoc installer path: build/Aegisub-*.dmg if-no-files-found: error diff --git a/docs/developer_docs.md b/docs/developer_docs.md index bf314f15d5..f8dc1af6c3 100644 --- a/docs/developer_docs.md +++ b/docs/developer_docs.md @@ -18,6 +18,12 @@ Follow the following steps to release a new Aegisub version: - Add the release to the update checking server - If needed, create a support branch for backports, keeping the master branch free for larger changes +### macOS release signing + +Tagged and manually dispatched builds upload an ad-hoc-signed +`-signing-input.zip`. Run `osx-sign.sh`, `osx-dmg.sh`, and `osx-notarize.sh` +locally; publish only the resulting notarized DMG. + ## Running Doxygen You can run Doxygen with the following command: @@ -38,4 +44,3 @@ Open the newly created `bin/moonscript.lua`, and within it make the following ch 3. Within the function at `package.preload['moonscript']`, remove the line `_with_0.insert_loader()`. The file is now ready for use, to be placed in `automation/include` within the Aegisub repo. - diff --git a/packages/meson.build b/packages/meson.build index 49de536885..ea581fd572 100644 --- a/packages/meson.build +++ b/packages/meson.build @@ -1,6 +1,7 @@ conf_pkg = configuration_data() if host_machine.system() == 'windows' + package_arch = host_machine.cpu_family() == 'aarch64' ? 'arm64' : 'x64' installer_setup = [powershell_exe, '-ExecutionPolicy', 'Bypass', '-File', meson.project_source_root() / 'tools/win-installer-setup.ps1'] @@ -10,7 +11,7 @@ if host_machine.system() == 'windows' portable_setup = [powershell_exe, '-ExecutionPolicy', 'Bypass', '-File', meson.project_source_root() / 'packages/win_installer/portable/create-portable.ps1'] - run_target('win-portable', command: [portable_setup, meson.project_build_root(), meson.project_source_root()]) + run_target('win-portable', command: [portable_setup, meson.project_build_root(), meson.project_source_root(), package_arch]) elif host_machine.system() == 'darwin' fontconfig_conf = run_command('pkg-config', '--variable=confdir', 'fontconfig').stdout().strip() bundle_app_sh = find_program(meson.project_source_root() / 'tools/osx-bundle.sh') @@ -18,9 +19,17 @@ elif host_machine.system() == 'darwin' command: [bundle_app_sh, meson.project_source_root(), meson.project_build_root(), 'wx-config', fontconfig_conf, '', get_option('build_osx_bundle') ? 'TRUE' : 'FALSE']) + sign_app_sh = find_program(meson.project_source_root() / 'tools/osx-sign.sh') + run_target('osx-sign', + command: [sign_app_sh, meson.project_source_root(), meson.project_build_root() / 'Aegisub.app']) + build_dmg_sh = find_program(meson.project_source_root() / 'tools/osx-dmg.sh') run_target('osx-build-dmg', - command: [build_dmg_sh, meson.project_source_root(), meson.project_build_root(), meson.project_version()]) + command: [build_dmg_sh, meson.project_source_root(), meson.project_build_root()]) + + notarize_sh = find_program(meson.project_source_root() / 'tools/osx-notarize.sh') + run_target('osx-notarize', + command: [notarize_sh, meson.project_source_root(), meson.project_build_root()]) else aegisub_rdns_developer = 'org.aegisub' aegisub_rdns_name = aegisub_rdns_developer + '.Aegisub' diff --git a/packages/osx_bundle/aegisub.entitlements b/packages/osx_bundle/aegisub.entitlements new file mode 100644 index 0000000000..a5c29d45cc --- /dev/null +++ b/packages/osx_bundle/aegisub.entitlements @@ -0,0 +1,9 @@ + + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/packages/win_installer/fragment_setupbase.iss b/packages/win_installer/fragment_setupbase.iss index af8ca018ed..130fadf8d4 100644 --- a/packages/win_installer/fragment_setupbase.iss +++ b/packages/win_installer/fragment_setupbase.iss @@ -35,7 +35,11 @@ UninstallDisplayIcon={app}\aegisub.exe WizardImageFile={#INSTALLER_DIR}\welcome-large.bmp WizardSmallImageFile={#INSTALLER_DIR}\aegisub-large.bmp -OutputBaseFilename=Aegisub-{#BUILD_GIT_VERSION_STRING} +#ifdef ARM64 +OutputBaseFilename=Aegisub-{#BUILD_GIT_VERSION_STRING}-arm64 +#else +OutputBaseFilename=Aegisub-{#BUILD_GIT_VERSION_STRING}-x64 +#endif VersionInfoDescription=Aegisub {#BUILD_GIT_VERSION_STRING} [Languages] diff --git a/packages/win_installer/portable/create-portable.ps1 b/packages/win_installer/portable/create-portable.ps1 index 387fb1aa57..6e23a74e3e 100644 --- a/packages/win_installer/portable/create-portable.ps1 +++ b/packages/win_installer/portable/create-portable.ps1 @@ -4,7 +4,10 @@ param ( [Parameter(Position = 0)] [string]$BuildRoot, [Parameter(Position = 1)] - [string]$SourceRoot + [string]$SourceRoot, + [Parameter(Position = 2)] + [ValidateSet('x64', 'arm64')] + [string]$Architecture = 'x64' ) function Copy-New-Item { @@ -85,5 +88,14 @@ Copy-New-Item $SourceRoot\packages\win_installer\portable\config.json $Portable Write-Output 'Creating portable zip' -Remove-Item aegisub-portable-64.zip -7z a aegisub-portable-64.zip aegisub-portable\ +$GitVersionHeader = Join-Path $BuildRoot 'git_version.h' +$GitVersionMatch = Select-String -Path $GitVersionHeader -Pattern '^#define BUILD_GIT_VERSION_STRING "(.+)"$' | Select-Object -First 1 +if (-not $GitVersionMatch) { + throw "Could not read BUILD_GIT_VERSION_STRING from $GitVersionHeader" +} +$GitVersion = $GitVersionMatch.Matches[0].Groups[1].Value +$PortableArchive = "Aegisub-$GitVersion-$Architecture-portable.zip" +if (Test-Path $PortableArchive) { + Remove-Item $PortableArchive +} +7z a $PortableArchive aegisub-portable\ diff --git a/tools/osx-bundle.sh b/tools/osx-bundle.sh index cea6cf9248..a19dd0e3e2 100755 --- a/tools/osx-bundle.sh +++ b/tools/osx-bundle.sh @@ -4,8 +4,6 @@ set -e SRC_DIR="${1}" BUILD_DIR="${2}" -WX_PREFIX="" -FONTCONFIG_CONF_DIR="${4}" DICT_DIR="${5}" MESON_BUILD_OSX_BUNDLE="${6}" @@ -41,25 +39,25 @@ if ! test -f "${BUILD_DIR}/osx-bundle.sed"; then fi # used by osx-bundle.sed -find "${SRC_DIR}/po" -name *.po | sed 's/.*\/\(.*\)\.po/ \1<\/string>/; s/RS/YU/' > "${BUILD_DIR}/languages" +find "${SRC_DIR}/po" -name '*.po' | sed 's/.*\/\(.*\)\.po/ \1<\/string>/; s/RS/YU/' > "${BUILD_DIR}/languages" #find "${SKEL_DIR}" -type f -not -regex ".*.svn.*" -cp -v ${SKEL_DIR}/Contents/Resources/*.icns "${PKG_DIR}/Contents/Resources" -cat "${SKEL_DIR}/Contents/Info.plist" | sed -f "${BUILD_DIR}/osx-bundle.sed" > "${PKG_DIR}/Contents/Info.plist" +cp -v "${SKEL_DIR}"/Contents/Resources/*.icns "${PKG_DIR}/Contents/Resources" +sed -f "${BUILD_DIR}/osx-bundle.sed" "${SKEL_DIR}/Contents/Info.plist" > "${PKG_DIR}/Contents/Info.plist" rm "${BUILD_DIR}/languages" echo echo "---- Installing files ----" -CURRENT_DIR=`pwd` -cd ${BUILD_DIR} -meson install --skip-subprojects luajit -cd ${CURRENT_DIR} +( + cd "${BUILD_DIR}" + meson install --skip-subprojects luajit +) echo echo "---- Copying dictionaries ----" -if test -f "${DICT_DIR}"; then - cp -v "${DICT_DIR}/*" "${PKG_DIR}/Contents/SharedSupport/dictionaries" +if test -d "${DICT_DIR}"; then + cp -v "${DICT_DIR}"/* "${PKG_DIR}/Contents/SharedSupport/dictionaries" else mkdir -p "${BUILD_DIR}/dictionaries" if ! test -f "${BUILD_DIR}/dictionaries/en_US.aff"; then @@ -103,18 +101,13 @@ mkdir -vp "${PKG_DIR}/Contents/Resources/en.lproj" echo echo "---- Fixing libraries ----" -sudo python3 "${SRC_DIR}/tools/osx-fix-libs.py" "${PKG_DIR}/Contents/MacOS/aegisub" || exit $? +python3 "${SRC_DIR}/tools/osx-fix-libs.py" "${PKG_DIR}/Contents/MacOS/aegisub" echo -echo "---- Signing ----" +echo "---- Ad-hoc signing ----" # Even if the binaries were already ad-hoc signed during compilation, # they need to be resigned after bundling and rewriting dylib paths. -if codesign -d "${PKG_DIR}/Contents/MacOS/aegisub"; then - for fname in "${PKG_DIR}/Contents/MacOS/"*; do - codesign -s ${AEGISUB_BUNDLE_SIGNATURE:--} -vf "${fname}" - done - codesign -s ${AEGISUB_BUNDLE_SIGNATURE:--} -vf "${PKG_DIR}/" -fi +AEGISUB_BUNDLE_SIGNATURE=- "${SRC_DIR}/tools/osx-sign.sh" "${SRC_DIR}" "${PKG_DIR}" echo echo "Done creating \"${PKG_DIR}\"" diff --git a/tools/osx-dmg.sh b/tools/osx-dmg.sh index 4c431a3c3e..3ca1127c2c 100755 --- a/tools/osx-dmg.sh +++ b/tools/osx-dmg.sh @@ -1,5 +1,5 @@ #!/bin/sh -# USAGE: osx-dmg.sh [Bundle Directory] "[Package Name]" +# USAGE: osx-dmg.sh [Source Directory] [Build Directory] [Version Override] # # Amar Takhar # @@ -17,28 +17,27 @@ set -e SRC_DIR="${1}" BUILD_DIR="${2}" -AEGI_VER="${3}" - -PKG_NAME="Aegisub-${AEGI_VER}" -PKG_NAME_VOLUME="${PKG_NAME}" PKG_DIR="${BUILD_DIR}/Aegisub.app" -DMG_TMP_DIR="${BUILD_DIR}/temp_dmg" -DMG_PATH="${BUILD_DIR}/${PKG_NAME}.dmg" -DMG_RW_PATH="${BUILD_DIR}/${PKG_NAME}_rw.dmg" - if ! test -d "${PKG_DIR}"; then - echo "\"${PKG_DIR}\" does not exist, please run 'make osx-bundle'" + echo "\"${PKG_DIR}\" does not exist, please run 'meson compile osx-bundle'" exit 1; fi +PKG_NAME="$("${SRC_DIR}/tools/osx-package-name.sh" "${PKG_DIR}" "${3:-}")" +PKG_NAME_VOLUME="${PKG_NAME}" + +DMG_TMP_DIR="${BUILD_DIR}/temp_dmg" +DMG_PATH="${BUILD_DIR}/${PKG_NAME}.dmg" +DMG_RW_PATH="${BUILD_DIR}/${PKG_NAME}_rw.dmg" + echo echo "---- Removing old \"${DMG_TMP_DIR}\", \"${DMG_PATH}\", \"${DMG_RW_PATH}\" ----" rm -rf "${DMG_TMP_DIR}" "${DMG_PATH}" "${DMG_RW_PATH}" mkdir -v "${DMG_TMP_DIR}" echo -echo "---- Copying ${AEGI_VER} into ${DMG_TMP_DIR}/ ----" +echo "---- Copying ${PKG_NAME} into ${DMG_TMP_DIR}/ ----" cp -R "${PKG_DIR}" "${DMG_TMP_DIR}" echo @@ -54,7 +53,7 @@ echo "---- Creating image ----" echo echo "---- Mounting image ----" -DEV_NAME=`/usr/bin/hdiutil attach -readwrite -noverify -noautoopen "${DMG_RW_PATH}" |awk '/GUID_partition_scheme/ {print $1}'` +DEV_NAME="$(/usr/bin/hdiutil attach -readwrite -noverify -noautoopen "${DMG_RW_PATH}" | awk '/GUID_partition_scheme/ {print $1}')" echo "Device name: ${DEV_NAME}" echo @@ -64,13 +63,13 @@ SetFile -a C "/Volumes/${PKG_NAME_VOLUME}" echo if test -n "${SET_STYLE}"; then echo "---- Running AppleScript to set style ----" - SCRIPT_TMP=`mktemp /tmp/aegisub_dmg_as.XXX` + SCRIPT_TMP="$(mktemp /tmp/aegisub_dmg_as.XXX)" - sed -f "${SRC_DIR}/scripts/osx-bundle.sed" "${SRC_DIR}/packages/osx_dmg/dmg_set_style.applescript" > ${SCRIPT_TMP} + sed -f "${SRC_DIR}/scripts/osx-bundle.sed" "${SRC_DIR}/packages/osx_dmg/dmg_set_style.applescript" > "${SCRIPT_TMP}" - /usr/bin/osacompile -o ${SCRIPT_TMP}.scpt ${SCRIPT_TMP} + /usr/bin/osacompile -o "${SCRIPT_TMP}.scpt" "${SCRIPT_TMP}" - /usr/bin/osascript ${SCRIPT_TMP}.scpt + /usr/bin/osascript "${SCRIPT_TMP}.scpt" open "/Volumes/${PKG_NAME_VOLUME}" echo "********************************************************" @@ -80,18 +79,18 @@ if test -n "${SET_STYLE}"; then echo echo "PRESS ENTER WHEN DONE" open "/Volumes/${PKG_NAME_VOLUME}" - read -e DUMB + read -r _ hdiutil detach "${DEV_NAME}" - DEV_NAME=`/usr/bin/hdiutil attach -readwrite -noverify -noautoopen "${DMG_RW_PATH}" |awk '/GUID_partition_scheme/ {print $1}'` + DEV_NAME="$(/usr/bin/hdiutil attach -readwrite -noverify -noautoopen "${DMG_RW_PATH}" | awk '/GUID_partition_scheme/ {print $1}')" echo "Device name: ${DEV_NAME}" cp -v "/Volumes/${PKG_NAME_VOLUME}/.DS_Store" "${SRC_DIR}/packages/osx_dmg/DS_Store" SetFile -a v "${SRC_DIR}/packages/osx_dmg/DS_Store" hdiutil detach "${DEV_NAME}" - rm -rf "${DMG_TMP_DIR}" "${DMG_RW_PATH}" ${SCRIPT_TMP}.scpt ${SCRIPT_TMP} + rm -rf "${DMG_TMP_DIR}" "${DMG_RW_PATH}" "${SCRIPT_TMP}.scpt" "${SCRIPT_TMP}" exit 0 else echo "---- Installing DS_Store ----" @@ -110,5 +109,23 @@ echo "---- Compressing ----" echo "---- Removing temp dmg \"${DMG_RW_PATH}\" ----" rm -rf "${DMG_RW_PATH}" +SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE:--}" +SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" + +if test "${SIGN_IDENTITY}" != "-"; then + echo + echo "---- Signing image ----" + if test -n "${SIGN_KEYCHAIN}"; then + codesign --force --timestamp --keychain "${SIGN_KEYCHAIN}" --sign "${SIGN_IDENTITY}" "${DMG_PATH}" + else + codesign --force --timestamp --sign "${SIGN_IDENTITY}" "${DMG_PATH}" + fi + codesign --verify --strict --verbose=2 "${DMG_PATH}" +fi + +echo +echo "---- Verifying image ----" +/usr/bin/hdiutil verify "${DMG_PATH}" + echo -echo "Done!" +echo "Done creating \"${DMG_PATH}\"" diff --git a/tools/osx-notarize.sh b/tools/osx-notarize.sh new file mode 100755 index 0000000000..88d429cb8f --- /dev/null +++ b/tools/osx-notarize.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -e + +SRC_DIR="${1}" +BUILD_DIR="${2}" +VERSION_OVERRIDE="${3:-}" + +PKG_DIR="${BUILD_DIR}/Aegisub.app" +PKG_NAME="$("${SRC_DIR}/tools/osx-package-name.sh" "${PKG_DIR}" "${VERSION_OVERRIDE}")" +DMG_PATH="${BUILD_DIR}/${PKG_NAME}.dmg" +NOTARY_PROFILE="${AEGISUB_NOTARY_PROFILE:-}" + +if test -z "${NOTARY_PROFILE}"; then + echo "AEGISUB_NOTARY_PROFILE must name a notarytool Keychain profile" + exit 1 +fi + +if ! test -f "${DMG_PATH}"; then + echo "\"${DMG_PATH}\" does not exist, please run 'meson compile osx-build-dmg'" + exit 1 +fi + +codesign --verify --deep --strict --verbose=2 "${PKG_DIR}" +codesign --verify --strict --verbose=2 "${DMG_PATH}" + +echo +echo "---- Submitting image for notarization ----" +if test -n "${AEGISUB_NOTARY_KEYCHAIN:-}"; then + xcrun notarytool submit "${DMG_PATH}" --keychain-profile "${NOTARY_PROFILE}" --keychain "${AEGISUB_NOTARY_KEYCHAIN}" --wait --timeout "${AEGISUB_NOTARY_TIMEOUT:-30m}" +else + xcrun notarytool submit "${DMG_PATH}" --keychain-profile "${NOTARY_PROFILE}" --wait --timeout "${AEGISUB_NOTARY_TIMEOUT:-30m}" +fi + +echo +echo "---- Stapling notarization ticket ----" +xcrun stapler staple "${DMG_PATH}" +xcrun stapler validate "${DMG_PATH}" +spctl --assess --type open --context context:primary-signature --verbose=2 "${DMG_PATH}" + +echo +echo "Notarized \"${DMG_PATH}\"" diff --git a/tools/osx-package-name.sh b/tools/osx-package-name.sh new file mode 100755 index 0000000000..8c97682a2a --- /dev/null +++ b/tools/osx-package-name.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +PKG_DIR="${1}" +AEGI_VER="${2:-}" + +if ! test -d "${PKG_DIR}"; then + echo "\"${PKG_DIR}\" does not exist" >&2 + exit 1 +fi + +if test -z "${AEGI_VER}"; then + AEGI_VER="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${PKG_DIR}/Contents/Info.plist")" +fi + +# Git branch names may contain characters which are unsafe in a filename. +SAFE_AEGI_VER="$(printf '%s' "${AEGI_VER}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '-' | sed 's/--*/-/g; s/^-//; s/-$//')" +if test -z "${SAFE_AEGI_VER}"; then + echo "Could not derive a package version from \"${AEGI_VER}\"" >&2 + exit 1 +fi + +APP_ARCHS="$(lipo -archs "${PKG_DIR}/Contents/MacOS/aegisub")" +case "${APP_ARCHS}" in + 'arm64 x86_64'|'x86_64 arm64') PKG_ARCH="universal2" ;; + 'x86_64') PKG_ARCH="x64" ;; + *) PKG_ARCH="$(printf '%s' "${APP_ARCHS}" | tr ' ' '-')" ;; +esac + +printf 'Aegisub-%s-%s\n' "${SAFE_AEGI_VER}" "${PKG_ARCH}" diff --git a/tools/osx-sign.sh b/tools/osx-sign.sh new file mode 100755 index 0000000000..ba5227c307 --- /dev/null +++ b/tools/osx-sign.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +set -e + +SRC_DIR="${1}" +PKG_DIR="${2}" + +if ! test -d "${PKG_DIR}"; then + echo "\"${PKG_DIR}\" does not exist" >&2 + exit 1 +fi + +SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE:--}" +SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" +ENTITLEMENTS="${AEGISUB_BUNDLE_ENTITLEMENTS:-${SRC_DIR}/packages/osx_bundle/aegisub.entitlements}" + +sign_file() { + if test "${SIGN_IDENTITY}" = "-"; then + codesign --force --sign - "${1}" + elif test -n "${SIGN_KEYCHAIN}"; then + codesign --force --options runtime --timestamp --keychain "${SIGN_KEYCHAIN}" --sign "${SIGN_IDENTITY}" "${1}" + else + codesign --force --options runtime --timestamp --sign "${SIGN_IDENTITY}" "${1}" + fi +} + +echo +echo "---- Signing app bundle ----" + +# Sign each real Mach-O file once. Library aliases are symlinks to these files +# and do not need (or want) their own signatures. +find "${PKG_DIR}/Contents/MacOS" -type f -print | while IFS= read -r fname; do + case "$(file -b "${fname}")" in + Mach-O*) sign_file "${fname}" ;; + esac +done + +if test "${SIGN_IDENTITY}" = "-"; then + codesign --force --sign - "${PKG_DIR}" +elif test -n "${SIGN_KEYCHAIN}"; then + codesign --force --options runtime --timestamp --keychain "${SIGN_KEYCHAIN}" --entitlements "${ENTITLEMENTS}" --sign "${SIGN_IDENTITY}" "${PKG_DIR}" +else + codesign --force --options runtime --timestamp --entitlements "${ENTITLEMENTS}" --sign "${SIGN_IDENTITY}" "${PKG_DIR}" +fi + +codesign --verify --deep --strict --verbose=2 "${PKG_DIR}" + +if test "${SIGN_IDENTITY}" = "-"; then + echo "Ad-hoc signed \"${PKG_DIR}\"" +else + echo "Developer ID signed \"${PKG_DIR}\" with ${SIGN_IDENTITY}" +fi diff --git a/tools/version.ps1 b/tools/version.ps1 index db64b0c68e..4be5ffca9e 100644 --- a/tools/version.ps1 +++ b/tools/version.ps1 @@ -44,7 +44,11 @@ if (Test-Path $gitVersionHeaderPath) { } $gitRevision = $lastSvnRevision + ((git -C $repositoryRootPath log --pretty=oneline "$($lastSvnHash)..HEAD" 2>$null | Measure-Object).Count) -$gitBranch = git -C $repositoryRootPath symbolic-ref --short HEAD 2>$null +$gitBranch = [string](git -C $repositoryRootPath symbolic-ref --short HEAD 2>$null) +$gitBranch = ($gitBranch -replace '[^A-Za-z0-9._-]', '-').Trim('-') +if ([string]::IsNullOrEmpty($gitBranch)) { + $gitBranch = 'unnamed-branch' +} $gitHash = git -C $repositoryRootPath rev-parse --short HEAD 2>$null $gitVersionString = $gitRevision, $gitBranch, $gitHash -join '-' $exactGitTag = git -C $repositoryRootPath describe --exact-match --tags 2>$null diff --git a/tools/version.sh b/tools/version.sh index df7e48f6a9..c4b3ec123b 100755 --- a/tools/version.sh +++ b/tools/version.sh @@ -65,6 +65,8 @@ if test x$git_version_str != x; then else git_branch="$(git symbolic-ref HEAD 2> /dev/null)" || git_branch="(unnamed branch)" git_branch="${git_branch##refs/heads/}" + # Keep the version safe for Info.plist's generated sed script and package filenames. + git_branch="$(printf '%s' "${git_branch}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '-')" git_hash=$(git rev-parse --short HEAD) git_version_str="${git_revision}-${git_branch}-${git_hash}" From b0b4df2cf66d4d6b0dfd23a5a124eb4a39047e02 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Tue, 4 Aug 2026 15:21:06 -0700 Subject: [PATCH 02/14] build: target macOS 13 in release builds --- .github/workflows/ci.yml | 80 +++++++++++++------- README.md | 2 +- meson.build | 48 ++++++++++-- meson_options.txt | 2 +- packages/meson.build | 2 +- packages/osx_bundle/Contents/Info.plist | 2 + tools/macos-build-fftw.sh | 86 +++++++++++++++++++++ tools/macos-verify-deployment-target.py | 99 +++++++++++++++++++++++++ tools/osx-bundle.sh | 10 ++- 9 files changed, 290 insertions(+), 41 deletions(-) create mode 100755 tools/macos-build-fftw.sh create mode 100755 tools/macos-verify-deployment-target.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf913416e3..537adb5c7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,30 +55,49 @@ jobs: buildtype: release, args: -Db_pch=false } - - { - name: macOS x86_64 Debug, - os: macos-15-intel, - buildtype: debugoptimized, - args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true --force-fallback-for=ffms2 - } - - { - name: macOS x86_64 Release, - os: macos-15-intel, - buildtype: release, - args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true --force-fallback-for=ffms2 - } - - { - name: macOS arm64 Debug, - os: macos-15, - buildtype: debugoptimized, - args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true --force-fallback-for=ffms2 - } - - { - name: macOS arm64 Release, - os: macos-15, - buildtype: release, - args: -Ddefault_library=static -Dbuild_osx_bundle=true -Dlocal_boost=true --force-fallback-for=ffms2 - } + - name: macOS x86_64 Debug + os: macos-15-intel + buildtype: debugoptimized + args: &macos_args >- + -Ddefault_library=static + -Dbuild_osx_bundle=true + -Dfftw3=disabled + -Dlibpulse=disabled + -Dopenal=enabled + -Dlibass:fontconfig=disabled + -Dlibass:libunibreak=disabled + -Dlibass:test=disabled + -Dfreetype2:bzip2=disabled + -Dfreetype2:brotli=disabled + -Dfreetype2:hvf=disabled + -Dfribidi:bin=false + -Dfribidi:docs=false + -Dfribidi:tests=false + -Dharfbuzz:gpu=disabled + -Dharfbuzz:raster=disabled + -Dharfbuzz:subset=disabled + -Dharfbuzz:tests=disabled + -Dharfbuzz:utilities=disabled + -Dharfbuzz:vector=disabled + -Dffmpeg:bzlib=disabled + -Dffmpeg:programs=disabled + -Dffmpeg:tests=disabled + -Ddav1d:enable_tests=false + -Ddav1d:enable_tools=false + -Dzlib:tests=disabled + --force-fallback-for=boost,wxWidgets,libass,zlib,freetype2,fribidi,harfbuzz,libpng,ffms2,libavformat,libavcodec,libswscale,libavutil,libswresample,dav1d,hunspell,uchardet,icu-uc,icu-i18n,libcurl + - name: macOS x86_64 Release + os: macos-15-intel + buildtype: release + args: *macos_args + - name: macOS arm64 Debug + os: macos-15 + buildtype: debugoptimized + args: *macos_args + - name: macOS arm64 Release + os: macos-15 + buildtype: release + args: *macos_args steps: - uses: actions/checkout@v6 @@ -127,10 +146,7 @@ jobs: - name: Install dependencies (MacOS) if: startsWith(matrix.config.os, 'macos-') run: | - brew update - brew install ninja - brew install libass zlib ffms2 fftw hunspell - brew install pulseaudio # NO OpenAL in github CI + brew install ninja nasm - name: Install dependencies (Linux) if: startsWith(matrix.config.os, 'ubuntu-') @@ -143,6 +159,14 @@ jobs: ${{ matrix.config.devenv }} meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} + - name: Build FFTW (MacOS) + if: startsWith(matrix.config.os, 'macos-') + run: | + tools/macos-build-fftw.sh build + meson setup --reconfigure --clearcache build \ + -Dpkg_config_path="$PWD/build/fftw-prefix/lib/pkgconfig" \ + -Dfftw3=enabled + - name: Build run: | ${{ matrix.config.devenv }} diff --git a/README.md b/README.md index 053f04eb17..055489c38b 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Once the dependencies are installed, build Aegisub with `meson build && meson co #### Build dmg ```bash -meson build_static -Ddefault_library=static -Dbuildtype=debugoptimized -Dbuild_osx_bundle=true -Dlocal_boost=true +meson build_static -Ddefault_library=static -Dbuildtype=debugoptimized -Dbuild_osx_bundle=true --force-fallback-for=boost meson compile -C build_static meson test -C build_static --verbose meson compile osx-bundle -C build_static diff --git a/meson.build b/meson.build index af1c68547f..3c299da358 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,21 @@ project('Aegisub', ['c', 'cpp'], version: '3.5.0') cmake = import('cmake') +force_fallback_for = get_option('force_fallback_for') + +if host_machine.system() == 'darwin' + macos_deployment_target = get_option('macos_deployment_target') + if macos_deployment_target == '' + error('macos_deployment_target must not be empty') + endif + + add_languages('objc', 'objcpp', native: false) + macos_deployment_flag = '-mmacosx-version-min=' + macos_deployment_target + foreach language : ['c', 'cpp', 'objc', 'objcpp'] + add_global_arguments(macos_deployment_flag, language: language) + add_global_link_arguments(macos_deployment_flag, language: language) + endforeach +endif if host_machine.system() == 'windows' add_project_arguments('-DUNICODE', '-DNOMINMAX', '-D_WIN32_WINNT=0x0601', language: 'cpp') @@ -87,7 +102,6 @@ endif deps = [] if host_machine.system() == 'darwin' - add_languages('objc', 'objcpp') add_project_arguments('-DGL_SILENCE_DEPRECATION', language: 'cpp') # meson neither supports objcpp_std nor inherits cpp_std https://github.com/mesonbuild/meson/issues/5495 add_project_arguments('-std=c++20', language: 'objcpp') @@ -113,7 +127,9 @@ if boost_static boost_modules += 'thread' # static boost dependencies do not include transitive dependencies https://github.com/mesonbuild/meson/issues/15457 endif -if not get_option('local_boost') +use_bundled_boost = force_fallback_for.contains('boost') + +if not use_bundled_boost boost_dep = dependency('boost', version: '>=1.70.0', modules: boost_modules, required: false, @@ -129,7 +145,7 @@ if not get_option('local_boost') endif endif -if get_option('local_boost') or not boost_dep.found() +if use_bundled_boost or not boost_dep.found() boost_dep = [] boost = subproject('boost') foreach module: (boost_modules + ['regex']) @@ -151,9 +167,13 @@ endif deps += dependency('zlib') -wx_dep = dependency('wxWidgets', version: '>=' + get_option('wx_version'), - required: false, - modules: ['std', 'stc', 'gl']) +if force_fallback_for.contains('wxWidgets') + wx_dep = dependency('', required: false) +else + wx_dep = dependency('wxWidgets', version: '>=' + get_option('wx_version'), + required: false, + modules: ['std', 'stc', 'gl']) +endif if wx_dep.found() deps += wx_dep @@ -183,6 +203,12 @@ else 'wxBUILD_MONOLITHIC': true # otherwise breaks project generation w/ meson }) + if host_machine.system() == 'darwin' + opt_var.add_cmake_defines({ + 'CMAKE_OSX_DEPLOYMENT_TARGET': macos_deployment_target, + }) + endif + wx = cmake.subproject('wxWidgets', options: opt_var) deps += wx.dependency('wxmono') @@ -324,7 +350,13 @@ conf_meson = configuration_data() conf_meson.set('DEFAULT_PLAYER_AUDIO', def_audio) if get_option('enable_update_checker') - curl_disable = ['tool', 'dict', 'file', 'ftp', 'gopher', 'imap', 'ldap', 'ldaps', 'mqtt', 'pop3', 'rtmp', 'rtsp', 'smb', 'smtp', 'telnet', 'tftp'] + curl_disable = [ + 'tool', 'tests', 'unittests', + 'dict', 'file', 'ftp', 'gopher', 'imap', 'ipfs', 'ldap', 'ldaps', + 'mqtt', 'pop3', 'rtmp', 'rtsp', 'smb', 'smtp', 'telnet', 'tftp', + 'brotli', 'gss-api', 'http2', 'idn', 'kerberos-auth', 'negotiate-auth', + 'psl', 'ssh', 'tls-srp', 'websockets', 'zstd', + ] curl_options = [] foreach opt : curl_disable curl_options += opt + '=disabled' @@ -333,7 +365,7 @@ if get_option('enable_update_checker') if host_machine.system() == 'windows' curl_options += 'schannel=enabled' elif host_machine.system() == 'darwin' - curl_options += 'secure-transport=enabled' + curl_options += ['openssl=disabled', 'secure-transport=enabled'] else curl_options += 'openssl=enabled' endif diff --git a/meson_options.txt b/meson_options.txt index 6db96ab232..a6c6287e98 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,7 +15,6 @@ option('libportal', type: 'feature', description: 'XDG Desktop Portal support th option('csri', type: 'feature', description: 'CSRI support') option('system_luajit', type: 'boolean', value: false, description: 'Force using system luajit') -option('local_boost', type: 'boolean', value: false, description: 'Force using locally compiled Boost') option('fallback_nasm', type: 'boolean', value: false, description: 'Force using fallback nasm') option('wx_version', type: 'string', value: '3.2.2', description: 'The minimum wxWidgets version to use') @@ -28,5 +27,6 @@ option('update_server', type: 'string', value: 'https://aegisub-updates.redvice. option('update_url', type: 'string', value: '/trunk', description: 'Base path to use for the update checker') option('build_osx_bundle', type: 'boolean', value: false, description: 'Package Aegisub.app on OSX') +option('macos_deployment_target', type: 'string', value: '13.0', description: 'Minimum supported macOS version') option('tests', type: 'boolean', value: true, description: 'Build tests') diff --git a/packages/meson.build b/packages/meson.build index ea581fd572..cd4656ebe3 100644 --- a/packages/meson.build +++ b/packages/meson.build @@ -17,7 +17,7 @@ elif host_machine.system() == 'darwin' bundle_app_sh = find_program(meson.project_source_root() / 'tools/osx-bundle.sh') run_target('osx-bundle', command: [bundle_app_sh, meson.project_source_root(), meson.project_build_root(), 'wx-config', fontconfig_conf, '', - get_option('build_osx_bundle') ? 'TRUE' : 'FALSE']) + get_option('build_osx_bundle') ? 'TRUE' : 'FALSE', get_option('macos_deployment_target')]) sign_app_sh = find_program(meson.project_source_root() / 'tools/osx-sign.sh') run_target('osx-sign', diff --git a/packages/osx_bundle/Contents/Info.plist b/packages/osx_bundle/Contents/Info.plist index 4894bf1b53..2b5c3a61fa 100644 --- a/packages/osx_bundle/Contents/Info.plist +++ b/packages/osx_bundle/Contents/Info.plist @@ -40,6 +40,8 @@ MacOSX + LSMinimumSystemVersion + @MACOS_DEPLOYMENT_TARGET@ CFBundleGetInfoString diff --git a/tools/macos-build-fftw.sh b/tools/macos-build-fftw.sh new file mode 100755 index 0000000000..cbcf8b74e9 --- /dev/null +++ b/tools/macos-build-fftw.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +set -eu + +if test "$#" -ne 1; then + echo "Usage: $0 MESON_BUILD_DIR" >&2 + exit 2 +fi + +if test "$(uname -s)" != "Darwin"; then + echo "FFTW macOS bootstrap must run on macOS" >&2 + exit 2 +fi + +BUILD_DIR="$(cd "$1" && pwd)" +DEPLOYMENT_TARGET="$(meson introspect --buildoptions "${BUILD_DIR}" | python3 -c ' +import json +import sys + +options = json.load(sys.stdin) +matches = [option["value"] for option in options if option["name"] == "macos_deployment_target"] +if len(matches) != 1: + raise SystemExit("macos_deployment_target is missing from the Meson build") +print(matches[0]) +')" + +FFTW_VERSION="3.3.11" +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}" + +if test -f "${FFTW_STAMP}" && test "$(cat "${FFTW_STAMP}")" = "${EXPECTED_STAMP}"; then + echo "Using existing FFTW build: ${EXPECTED_STAMP}" + exit 0 +fi + +case "${ARCH}" in + x86_64) + ARCH_ARGS="--enable-sse2 --enable-avx --enable-avx2" + ;; + arm64) + ARCH_ARGS="--enable-armv8-cntvct-el0" + ;; + *) + echo "Unsupported macOS architecture: ${ARCH}" >&2 + exit 2 + ;; +esac + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/aegisub-fftw.XXXXXX")" +trap 'rm -rf "${WORK_DIR}"' EXIT HUP INT TERM + +ARCHIVE="${WORK_DIR}/fftw.tar.gz" +curl --fail --location --retry 3 --output "${ARCHIVE}" "${FFTW_URL}" +echo "${FFTW_SHA256} ${ARCHIVE}" | shasum -a 256 --check +tar -xzf "${ARCHIVE}" -C "${WORK_DIR}" + +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}" + +# Aegisub only uses FFTW's single-threaded, double-precision API. +# shellcheck disable=SC2086 +./configure \ + --prefix="${FFTW_PREFIX}" \ + --disable-shared \ + --enable-static \ + --disable-fortran \ + --disable-doc \ + --disable-mpi \ + --disable-openmp \ + --disable-threads \ + ${ARCH_ARGS} + +make -s -j"$(sysctl -n hw.logicalcpu)" +make -s install +printf '%s\n' "${EXPECTED_STAMP}" > "${FFTW_STAMP}" + +echo "Built FFTW ${FFTW_VERSION} for macOS ${DEPLOYMENT_TARGET} (${ARCH})" +echo "pkg-config path: ${FFTW_PREFIX}/lib/pkgconfig" diff --git a/tools/macos-verify-deployment-target.py b/tools/macos-verify-deployment-target.py new file mode 100755 index 0000000000..f21a40e8d0 --- /dev/null +++ b/tools/macos-verify-deployment-target.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import plistlib +import re +import subprocess +import sys +from pathlib import Path + + +MACHO_MAGICS = { + b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca", + b"\xca\xfe\xba\xbf", + b"\xbf\xba\xfe\xca", + b"\xce\xfa\xed\xfe", + b"\xfe\xed\xfa\xce", + b"\xcf\xfa\xed\xfe", + b"\xfe\xed\xfa\xcf", +} + + +def version_tuple(version: str) -> tuple[int, ...]: + if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*", version): + raise ValueError(f"invalid macOS version: {version}") + parts = [int(part) for part in version.split(".")] + while len(parts) > 1 and parts[-1] == 0: + parts.pop() + return tuple(parts) + + +def minimum_versions(binary: Path) -> list[str]: + output = subprocess.run( + ["otool", "-l", binary], + check=True, + capture_output=True, + text=True, + ).stdout + + versions = [] + command = None + for line in output.splitlines(): + fields = line.split() + if fields[:1] == ["cmd"]: + command = fields[1] if len(fields) > 1 else None + elif command == "LC_BUILD_VERSION" and fields[:1] == ["minos"]: + versions.append(fields[1]) + elif command == "LC_VERSION_MIN_MACOSX" and fields[:1] == ["version"]: + versions.append(fields[1]) + return versions + + +def is_macho(path: Path) -> bool: + if not path.is_file(): + return False + with path.open("rb") as file: + return file.read(4) in MACHO_MAGICS + + +def main() -> int: + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} AEGISUB_APP", file=sys.stderr) + return 2 + + app = Path(sys.argv[1]) + with (app / "Contents" / "Info.plist").open("rb") as file: + info = plistlib.load(file) + + declared = info["LSMinimumSystemVersion"] + declared_tuple = version_tuple(declared) + executable = app / "Contents" / "MacOS" / info["CFBundleExecutable"] + binaries = [path for path in app.rglob("*") if is_macho(path)] + + errors = [] + for binary in binaries: + versions = minimum_versions(binary) + if not versions: + errors.append(f"{binary}: no macOS deployment target") + continue + for version in versions: + if version_tuple(version) > declared_tuple: + errors.append(f"{binary}: targets macOS {version}, above declared {declared}") + + executable_versions = minimum_versions(executable) + if not executable_versions or any(version_tuple(version) != declared_tuple for version in executable_versions): + found = ", ".join(executable_versions) if executable_versions else "none" + errors.append(f"{executable}: expected deployment target {declared}, found {found}") + + if errors: + print("macOS deployment-target verification failed:", file=sys.stderr) + for error in errors: + print(f" {error}", file=sys.stderr) + return 1 + + print(f"Verified {len(binaries)} Mach-O files target macOS {declared} or older") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/osx-bundle.sh b/tools/osx-bundle.sh index a19dd0e3e2..634258ffe0 100755 --- a/tools/osx-bundle.sh +++ b/tools/osx-bundle.sh @@ -6,6 +6,7 @@ SRC_DIR="${1}" BUILD_DIR="${2}" DICT_DIR="${5}" MESON_BUILD_OSX_BUNDLE="${6}" +MACOS_DEPLOYMENT_TARGET="${7}" if [ "${MESON_BUILD_OSX_BUNDLE}" != "TRUE" ]; then echo "Project not built with \`build_osx_bundle\`" @@ -43,7 +44,8 @@ find "${SRC_DIR}/po" -name '*.po' | sed 's/.*\/\(.*\)\.po/ \1<\/s #find "${SKEL_DIR}" -type f -not -regex ".*.svn.*" cp -v "${SKEL_DIR}"/Contents/Resources/*.icns "${PKG_DIR}/Contents/Resources" -sed -f "${BUILD_DIR}/osx-bundle.sed" "${SKEL_DIR}/Contents/Info.plist" > "${PKG_DIR}/Contents/Info.plist" +sed -e "s/@MACOS_DEPLOYMENT_TARGET@/${MACOS_DEPLOYMENT_TARGET}/g" \ + -f "${BUILD_DIR}/osx-bundle.sed" "${SKEL_DIR}/Contents/Info.plist" > "${PKG_DIR}/Contents/Info.plist" rm "${BUILD_DIR}/languages" @@ -51,7 +53,7 @@ echo echo "---- Installing files ----" ( cd "${BUILD_DIR}" - meson install --skip-subprojects luajit + meson install --skip-subprojects ) echo @@ -103,6 +105,10 @@ echo echo "---- Fixing libraries ----" python3 "${SRC_DIR}/tools/osx-fix-libs.py" "${PKG_DIR}/Contents/MacOS/aegisub" +echo +echo "---- Verifying deployment target ----" +python3 "${SRC_DIR}/tools/macos-verify-deployment-target.py" "${PKG_DIR}" + echo echo "---- Ad-hoc signing ----" # Even if the binaries were already ad-hoc signed during compilation, From c5723d022e1af999d4237017ac68a38692ea0c8d Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Wed, 5 Aug 2026 04:43:16 +0000 Subject: [PATCH 03/14] ci: disable FFmpeg LZMA on Intel macOS --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 537adb5c7c..54d622017a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,7 @@ jobs: - name: macOS x86_64 Debug os: macos-15-intel buildtype: debugoptimized + arch_args: -Dffmpeg:lzma=disabled args: &macos_args >- -Ddefault_library=static -Dbuild_osx_bundle=true @@ -89,14 +90,17 @@ jobs: - name: macOS x86_64 Release os: macos-15-intel buildtype: release + arch_args: -Dffmpeg:lzma=disabled args: *macos_args - name: macOS arm64 Debug os: macos-15 buildtype: debugoptimized + arch_args: '' args: *macos_args - name: macOS arm64 Release os: macos-15 buildtype: release + arch_args: '' args: *macos_args steps: @@ -157,7 +161,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.args }} ${{ matrix.config.arch_args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} - name: Build FFTW (MacOS) if: startsWith(matrix.config.os, 'macos-') From de950d783d9e9f1d3bff579a5fc1c7e68561423b Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 11:42:48 -0700 Subject: [PATCH 04/14] build: share libpng and zlib with wxWidgets --- meson.build | 57 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/meson.build b/meson.build index 3c299da358..4720730ee8 100644 --- a/meson.build +++ b/meson.build @@ -165,7 +165,8 @@ if host_machine.system() == 'windows' conf.set('BOOST_USE_WINDOWS_H', 1) endif -deps += dependency('zlib') +zlib_dep = dependency('zlib') +deps += zlib_dep if force_fallback_for.contains('wxWidgets') wx_dep = dependency('', required: false) @@ -179,6 +180,13 @@ if wx_dep.found() deps += wx_dep wx_defines_dep = wx_dep else + # wxWidgets, FreeType, and the other bundled dependencies must all use the + # same libpng implementation. In particular, wx's bundled libpng has + # partially-prefixed private symbols which can collide with another static + # libpng on ARM. + libpng_dep = dependency('libpng') + deps += libpng_dep + build_shared = true if get_option('default_library') == 'static' build_shared = false @@ -198,11 +206,44 @@ else 'wxBUILD_SHARED': build_shared, 'wxUSE_WEBVIEW': false, # breaks build on linux + 'wxUSE_LIBPNG': 'sys', # supplied by Meson below + 'wxUSE_ZLIB': 'sys', # supplied by Meson below 'CMAKE_BUILD_TYPE': build_type, 'wxUSE_IMAGE': true, 'wxBUILD_MONOLITHIC': true # otherwise breaks project generation w/ meson }) + # CMake can't consume Meson dependency objects directly. When these are + # fallback dependencies, give wx the exact libraries and headers Meson is + # already building instead of allowing find_package() to select a second + # implementation. + if zlib_dep.type_name() == 'internal' + zlib = subproject('zlib').get_variable('zlib') + zlib_source_dir = meson.project_source_root() / 'subprojects/zlib-1.3.1' + opt_var.add_cmake_defines({ + 'ZLIB_INCLUDE_DIR': zlib_source_dir, + 'ZLIB_LIBRARY': zlib.full_path(), + }) + opt_var.append_compile_args('c', '-I' + zlib_source_dir) + opt_var.append_compile_args('cpp', '-I' + zlib_source_dir) + endif + + if libpng_dep.type_name() == 'internal' + libpng = subproject('libpng').get_variable('libpng') + libpng_source_dir = meson.project_source_root() / 'subprojects/libpng-1.6.55' + libpng_build_dir = meson.project_build_root() / 'subprojects/libpng-1.6.55' + opt_var.add_cmake_defines({ + 'PNG_LIBRARY': libpng.full_path(), + 'PNG_PNG_INCLUDE_DIR': libpng_source_dir, + }) + # pnglibconf.h is generated in the subproject build directory, while + # FindPNG only exposes a single include directory to wx. Meson's CMake + # converter also ignores include paths outside the CMake subproject, + # so pass both directories explicitly as compiler arguments. + opt_var.append_compile_args('c', '-I' + libpng_source_dir, '-I' + libpng_build_dir) + opt_var.append_compile_args('cpp', '-I' + libpng_source_dir, '-I' + libpng_build_dir) + endif + if host_machine.system() == 'darwin' opt_var.add_cmake_defines({ 'CMAKE_OSX_DEPLOYMENT_TARGET': macos_deployment_target, @@ -211,6 +252,13 @@ else wx = cmake.subproject('wxWidgets', options: opt_var) + foreach bundled_dependency: ['wxpng', 'wxzlib'] + if wx.target_list().contains(bundled_dependency) + error('wxWidgets unexpectedly built ' + bundled_dependency + + ' instead of using the dependency supplied by Meson') + endif + endforeach + deps += wx.dependency('wxmono') foreach cmake_target: ['wxregex', 'wxscintilla', 'wxlexilla'] if wx.target_list().contains(cmake_target) @@ -222,15 +270,8 @@ else # but allows it if you use partial_dependency() to get only includes and compile args. wx_defines_dep = wx.dependency('wxmono').partial_dependency(compile_args: true, includes: true) - if host_machine.system() == 'windows' or host_machine.system() == 'darwin' - deps += [ - wx.dependency('wxpng'), - ] - endif - if host_machine.system() == 'windows' deps += [ - wx.dependency('wxzlib'), wx.dependency('wxexpat'), ] From dd8ceb42fbe21879cc52969f2401675a4a43f2da Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 12:14:01 -0700 Subject: [PATCH 05/14] build: make wx dependency bridge portable --- meson.build | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 4720730ee8..0ecd6c6377 100644 --- a/meson.build +++ b/meson.build @@ -216,32 +216,49 @@ else # CMake can't consume Meson dependency objects directly. When these are # fallback dependencies, give wx the exact libraries and headers Meson is # already building instead of allowing find_package() to select a second - # implementation. + # implementation. Meson does not yet provide a supported bridge for this: + # https://github.com/mesonbuild/meson/issues/8089 + # https://github.com/mesonbuild/meson/issues/12451 + # + # Use CMake-style paths even on Windows. add_cmake_defines() writes these + # values into a CMake preload file, where native backslashes can be parsed + # as escapes (for example, D:\a contains \a). + c_include_flag = cc.get_argument_syntax() == 'msvc' ? '/I' : '-I' + cpp_include_flag = cxx.get_argument_syntax() == 'msvc' ? '/I' : '-I' + if zlib_dep.type_name() == 'internal' zlib = subproject('zlib').get_variable('zlib') - zlib_source_dir = meson.project_source_root() / 'subprojects/zlib-1.3.1' + zlib_source_dir = (meson.project_source_root() / 'subprojects/zlib-1.3.1').replace('\\', '/') + zlib_library = zlib.full_path().replace('\\', '/') opt_var.add_cmake_defines({ 'ZLIB_INCLUDE_DIR': zlib_source_dir, - 'ZLIB_LIBRARY': zlib.full_path(), + 'ZLIB_LIBRARY': zlib_library, }) - opt_var.append_compile_args('c', '-I' + zlib_source_dir) - opt_var.append_compile_args('cpp', '-I' + zlib_source_dir) + opt_var.append_compile_args('c', c_include_flag + zlib_source_dir, + target: 'wxmono') + opt_var.append_compile_args('cpp', cpp_include_flag + zlib_source_dir, + target: 'wxmono') endif if libpng_dep.type_name() == 'internal' libpng = subproject('libpng').get_variable('libpng') - libpng_source_dir = meson.project_source_root() / 'subprojects/libpng-1.6.55' - libpng_build_dir = meson.project_build_root() / 'subprojects/libpng-1.6.55' + libpng_source_dir = (meson.project_source_root() / 'subprojects/libpng-1.6.55').replace('\\', '/') + libpng_build_dir = (meson.project_build_root() / 'subprojects/libpng-1.6.55').replace('\\', '/') + libpng_library = libpng.full_path().replace('\\', '/') opt_var.add_cmake_defines({ - 'PNG_LIBRARY': libpng.full_path(), + 'PNG_LIBRARY': libpng_library, 'PNG_PNG_INCLUDE_DIR': libpng_source_dir, }) # pnglibconf.h is generated in the subproject build directory, while # FindPNG only exposes a single include directory to wx. Meson's CMake # converter also ignores include paths outside the CMake subproject, # so pass both directories explicitly as compiler arguments. - opt_var.append_compile_args('c', '-I' + libpng_source_dir, '-I' + libpng_build_dir) - opt_var.append_compile_args('cpp', '-I' + libpng_source_dir, '-I' + libpng_build_dir) + opt_var.append_compile_args('c', c_include_flag + libpng_source_dir, + c_include_flag + libpng_build_dir, + target: 'wxmono') + opt_var.append_compile_args('cpp', cpp_include_flag + libpng_source_dir, + cpp_include_flag + libpng_build_dir, + target: 'wxmono') endif if host_machine.system() == 'darwin' From 113707c89342957a2b80cbd995e7d1cb63ff4e4d Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 14:58:40 -0700 Subject: [PATCH 06/14] build: harden macOS release signing --- .github/workflows/ci.yml | 4 ++ docs/developer_docs.md | 62 +++++++++++++++++++++++- packages/osx_bundle/aegisub.entitlements | 2 + tools/osx-dmg.sh | 11 +++-- tools/osx-notarize.sh | 54 ++++++++++++++++++++- tools/osx-package-name.sh | 3 +- tools/osx-sign.sh | 9 +++- tools/version.sh | 5 +- 8 files changed, 139 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d622017a..1b8979612a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,6 +215,10 @@ jobs: # macOS artifacts - name: Generate macOS installer if: startsWith(matrix.config.os, 'macos-') + env: + # CI artifacts are intentionally ad-hoc signed. Release managers must + # explicitly replace this with a Developer ID signature before release. + AEGISUB_BUNDLE_SIGNATURE: '-' run: | meson compile osx-bundle -C build diff --git a/docs/developer_docs.md b/docs/developer_docs.md index f8dc1af6c3..9111be740a 100644 --- a/docs/developer_docs.md +++ b/docs/developer_docs.md @@ -21,8 +21,66 @@ Follow the following steps to release a new Aegisub version: ### macOS release signing Tagged and manually dispatched builds upload an ad-hoc-signed -`-signing-input.zip`. Run `osx-sign.sh`, `osx-dmg.sh`, and `osx-notarize.sh` -locally; publish only the resulting notarized DMG. +`-signing-input.zip`. This is an input to the release process, not a +distributable release artifact. Publish only the Developer ID-signed and +notarized DMG produced below. + +Create a `notarytool` profile once. Add `--keychain PATH` if the profile +should be stored in a keychain other than the login keychain: + +```bash +export AEGISUB_NOTARY_PROFILE=aegisub-release +xcrun notarytool store-credentials "${AEGISUB_NOTARY_PROFILE}" +``` + +Check out the exact commit which produced the CI artifact, configure a build +directory, and extract the artifact into it. The resulting path must be +`build/Aegisub.app`: + +```bash +meson setup build -Dbuild_osx_bundle=true -Ddefault_library=static +ditto -x -k /path/to/Aegisub-*-signing-input.zip build +``` + +Export the release identity and notary profile, then run the three release +targets in order: + +```bash +export AEGISUB_BUNDLE_SIGNATURE='Developer ID Application: Example (TEAMID)' +export AEGISUB_NOTARY_PROFILE=aegisub-release + +# Optional when using non-default keychains: +export AEGISUB_SIGNING_KEYCHAIN=/path/to/signing.keychain-db +export AEGISUB_NOTARY_KEYCHAIN=/path/to/notary.keychain-db + +meson compile -C build osx-sign +meson compile -C build osx-build-dmg +meson compile -C build osx-notarize +``` + +The targets are wrappers around these direct script interfaces: + +```text +tools/osx-sign.sh SOURCE_DIR AEGISUB_APP +tools/osx-dmg.sh SOURCE_DIR BUILD_DIR [VERSION_OVERRIDE] +tools/osx-notarize.sh SOURCE_DIR BUILD_DIR [VERSION_OVERRIDE] +``` + +`AEGISUB_BUNDLE_SIGNATURE` is mandatory. Set it to `-` only for an explicitly +ad-hoc CI or development build; such an image cannot be notarized. +`AEGISUB_BUNDLE_ENTITLEMENTS` optionally replaces the default entitlements +file, and `AEGISUB_NOTARY_TIMEOUT` optionally replaces the default `30m` +submission timeout. + +Hardened-runtime library validation deliberately remains enabled. Bundled and +third-party native Automation modules must therefore be Apple-signed or signed +with the same Team ID as Aegisub. Rebuild and sign controlled modules rather +than disabling library validation for the whole application. + +The DMG is the outermost distributed container, so it is the item submitted to +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. ## Running Doxygen diff --git a/packages/osx_bundle/aegisub.entitlements b/packages/osx_bundle/aegisub.entitlements index a5c29d45cc..3d85368ec2 100644 --- a/packages/osx_bundle/aegisub.entitlements +++ b/packages/osx_bundle/aegisub.entitlements @@ -5,5 +5,7 @@ com.apple.security.cs.allow-unsigned-executable-memory + diff --git a/tools/osx-dmg.sh b/tools/osx-dmg.sh index 3ca1127c2c..b35df819a9 100755 --- a/tools/osx-dmg.sh +++ b/tools/osx-dmg.sh @@ -25,6 +25,14 @@ if ! test -d "${PKG_DIR}"; then exit 1; fi +if test -z "${AEGISUB_BUNDLE_SIGNATURE+x}" || test -z "${AEGISUB_BUNDLE_SIGNATURE}"; then + echo "AEGISUB_BUNDLE_SIGNATURE must be set to a Developer ID identity or '-' for an unsigned image" >&2 + exit 1 +fi + +SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE}" +SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" + PKG_NAME="$("${SRC_DIR}/tools/osx-package-name.sh" "${PKG_DIR}" "${3:-}")" PKG_NAME_VOLUME="${PKG_NAME}" @@ -109,9 +117,6 @@ echo "---- Compressing ----" echo "---- Removing temp dmg \"${DMG_RW_PATH}\" ----" rm -rf "${DMG_RW_PATH}" -SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE:--}" -SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" - if test "${SIGN_IDENTITY}" != "-"; then echo echo "---- Signing image ----" diff --git a/tools/osx-notarize.sh b/tools/osx-notarize.sh index 88d429cb8f..10606c026d 100755 --- a/tools/osx-notarize.sh +++ b/tools/osx-notarize.sh @@ -10,6 +10,33 @@ PKG_DIR="${BUILD_DIR}/Aegisub.app" PKG_NAME="$("${SRC_DIR}/tools/osx-package-name.sh" "${PKG_DIR}" "${VERSION_OVERRIDE}")" DMG_PATH="${BUILD_DIR}/${PKG_NAME}.dmg" NOTARY_PROFILE="${AEGISUB_NOTARY_PROFILE:-}" +DEVELOPER_ID_REQUIREMENT='anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.13] exists' + +verify_timestamp() { + if ! codesign --display --verbose=4 "${1}" 2>&1 | grep -q '^Timestamp='; then + echo "\"${1}\" does not have a secure signing timestamp" >&2 + exit 1 + fi +} + +verify_app() { + codesign --verify --deep --strict --verbose=2 -R="${DEVELOPER_ID_REQUIREMENT}" "${1}" + if ! codesign --display --verbose=4 "${1}" 2>&1 | grep -q '^CodeDirectory .*flags=.*runtime'; then + echo "\"${1}\" does not have the hardened runtime enabled" >&2 + exit 1 + fi + if codesign --display --xml --entitlements - "${1}" 2>/dev/null | + grep -Fq 'com.apple.security.cs.disable-library-validation'; then + echo "\"${1}\" disables hardened-runtime library validation" >&2 + exit 1 + fi + verify_timestamp "${1}" +} + +verify_image() { + codesign --verify --strict --verbose=2 -R="${DEVELOPER_ID_REQUIREMENT}" "${1}" + verify_timestamp "${1}" +} if test -z "${NOTARY_PROFILE}"; then echo "AEGISUB_NOTARY_PROFILE must name a notarytool Keychain profile" @@ -21,8 +48,28 @@ if ! test -f "${DMG_PATH}"; then exit 1 fi -codesign --verify --deep --strict --verbose=2 "${PKG_DIR}" -codesign --verify --strict --verbose=2 "${DMG_PATH}" +verify_app "${PKG_DIR}" +verify_image "${DMG_PATH}" + +# Verify the exact app being submitted, rather than assuming the build-tree +# copy has not changed since the image was made. +VERIFY_MOUNT="$(mktemp -d "${TMPDIR:-/tmp}/aegisub-notary.XXXXXX")" +DMG_MOUNTED=false +cleanup() { + if test "${DMG_MOUNTED}" = true; then + hdiutil detach "${VERIFY_MOUNT}" >/dev/null 2>&1 || true + fi + rmdir "${VERIFY_MOUNT}" >/dev/null 2>&1 || true +} +trap cleanup EXIT HUP INT TERM + +hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "${VERIFY_MOUNT}" "${DMG_PATH}" >/dev/null +DMG_MOUNTED=true +verify_app "${VERIFY_MOUNT}/Aegisub.app" +hdiutil detach "${VERIFY_MOUNT}" >/dev/null +DMG_MOUNTED=false +rmdir "${VERIFY_MOUNT}" +trap - EXIT HUP INT TERM echo echo "---- Submitting image for notarization ----" @@ -34,6 +81,9 @@ fi echo echo "---- Stapling notarization ticket ----" +# Apple recommends notarizing and stapling only the outermost container when +# distributing nested software such as an app inside a disk image: +# https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution xcrun stapler staple "${DMG_PATH}" xcrun stapler validate "${DMG_PATH}" spctl --assess --type open --context context:primary-signature --verbose=2 "${DMG_PATH}" diff --git a/tools/osx-package-name.sh b/tools/osx-package-name.sh index 8c97682a2a..c2f4d535dc 100755 --- a/tools/osx-package-name.sh +++ b/tools/osx-package-name.sh @@ -21,7 +21,8 @@ if test -z "${SAFE_AEGI_VER}"; then exit 1 fi -APP_ARCHS="$(lipo -archs "${PKG_DIR}/Contents/MacOS/aegisub")" +APP_EXECUTABLE="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "${PKG_DIR}/Contents/Info.plist")" +APP_ARCHS="$(lipo -archs "${PKG_DIR}/Contents/MacOS/${APP_EXECUTABLE}")" case "${APP_ARCHS}" in 'arm64 x86_64'|'x86_64 arm64') PKG_ARCH="universal2" ;; 'x86_64') PKG_ARCH="x64" ;; diff --git a/tools/osx-sign.sh b/tools/osx-sign.sh index ba5227c307..0dee35bb89 100755 --- a/tools/osx-sign.sh +++ b/tools/osx-sign.sh @@ -10,7 +10,12 @@ if ! test -d "${PKG_DIR}"; then exit 1 fi -SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE:--}" +if test -z "${AEGISUB_BUNDLE_SIGNATURE+x}" || test -z "${AEGISUB_BUNDLE_SIGNATURE}"; then + echo "AEGISUB_BUNDLE_SIGNATURE must be set to a Developer ID identity or '-' for an ad-hoc build" >&2 + exit 1 +fi + +SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE}" SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" ENTITLEMENTS="${AEGISUB_BUNDLE_ENTITLEMENTS:-${SRC_DIR}/packages/osx_bundle/aegisub.entitlements}" @@ -29,7 +34,7 @@ echo "---- Signing app bundle ----" # Sign each real Mach-O file once. Library aliases are symlinks to these files # and do not need (or want) their own signatures. -find "${PKG_DIR}/Contents/MacOS" -type f -print | while IFS= read -r fname; do +find "${PKG_DIR}/Contents" -type f -print | while IFS= read -r fname; do case "$(file -b "${fname}")" in Mach-O*) sign_file "${fname}" ;; esac diff --git a/tools/version.sh b/tools/version.sh index c4b3ec123b..eeb9854bc4 100755 --- a/tools/version.sh +++ b/tools/version.sh @@ -66,7 +66,10 @@ else git_branch="$(git symbolic-ref HEAD 2> /dev/null)" || git_branch="(unnamed branch)" git_branch="${git_branch##refs/heads/}" # Keep the version safe for Info.plist's generated sed script and package filenames. - git_branch="$(printf '%s' "${git_branch}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '-')" + git_branch="$(printf '%s' "${git_branch}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '-' | sed 's/--*/-/g; s/^-//; s/-$//')" + if test -z "${git_branch}"; then + git_branch="unnamed-branch" + fi git_hash=$(git rev-parse --short HEAD) git_version_str="${git_revision}-${git_branch}-${git_hash}" From f1670e05b297a85f2c5bd51ae56671a2c2a0e7eb Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 14:59:25 -0700 Subject: [PATCH 07/14] build: complete Windows package architecture plumbing --- packages/meson.build | 2 +- packages/win_installer/aegisub3.iss | 14 +++++++++- packages/win_installer/fragment_runtimes.iss | 4 +-- .../portable/create-portable.ps1 | 8 +++--- tools/win-installer-setup.ps1 | 26 ++++++++++++++----- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/meson.build b/packages/meson.build index cd4656ebe3..07e21b6230 100644 --- a/packages/meson.build +++ b/packages/meson.build @@ -6,7 +6,7 @@ if host_machine.system() == 'windows' '-ExecutionPolicy', 'Bypass', '-File', meson.project_source_root() / 'tools/win-installer-setup.ps1'] run_target('win-installer', - command: [installer_setup, meson.project_build_root(), meson.project_source_root()]) + command: [installer_setup, meson.project_build_root(), meson.project_source_root(), package_arch]) portable_setup = [powershell_exe, '-ExecutionPolicy', 'Bypass', diff --git a/packages/win_installer/aegisub3.iss b/packages/win_installer/aegisub3.iss index de510ed6c0..b1f84ce4ff 100644 --- a/packages/win_installer/aegisub3.iss +++ b/packages/win_installer/aegisub3.iss @@ -1,4 +1,8 @@ -#define ARCH 64 +#ifdef ARM64 +#define ARCH "arm64" +#else +#define ARCH "x64" +#endif #include "fragment_setupbase.iss" #include "fragment_strings.iss" @@ -7,12 +11,20 @@ AppID={{24BC8B57-716C-444F-B46B-A3349B9164C5} DefaultDirName={commonpf}\Aegisub PrivilegesRequired=poweruser +#ifdef ARM64 +ArchitecturesInstallIn64BitMode=arm64 +ArchitecturesAllowed=arm64 +#else ArchitecturesInstallIn64BitMode=x64compatible ArchitecturesAllowed=x64compatible +#endif #include "fragment_mainprogram.iss" #include "fragment_associations.iss" +#ifndef ARM64 +; The bundled VSFilter binary is x64-only. #include "fragment_codecs.iss" +#endif #include "fragment_automation.iss" #include "fragment_translations.iss" #include "fragment_spelling.iss" diff --git a/packages/win_installer/fragment_runtimes.iss b/packages/win_installer/fragment_runtimes.iss index c7c69762fb..3db18cfe3f 100644 --- a/packages/win_installer/fragment_runtimes.iss +++ b/packages/win_installer/fragment_runtimes.iss @@ -1,7 +1,7 @@ ; This file implements checking for and installing runtime libraries for Aegisub [Files] -DestDir: {tmp}; Source: "{#DEPS_DIR}\VC_redist\VC_redist.x{#ARCH}.exe"; Flags: nocompression deleteafterinstall +DestDir: {tmp}; Source: "{#DEPS_DIR}\VC_redist\VC_redist.{#ARCH}.exe"; Flags: nocompression deleteafterinstall [Run] -Filename: {tmp}\VC_redist.x{#ARCH}.exe; StatusMsg: {cm:InstallRuntime}; Parameters: "/install /quiet /norestart" +Filename: {tmp}\VC_redist.{#ARCH}.exe; StatusMsg: {cm:InstallRuntime}; Parameters: "/install /quiet /norestart" diff --git a/packages/win_installer/portable/create-portable.ps1 b/packages/win_installer/portable/create-portable.ps1 index 6e23a74e3e..e3b672a160 100644 --- a/packages/win_installer/portable/create-portable.ps1 +++ b/packages/win_installer/portable/create-portable.ps1 @@ -62,10 +62,12 @@ Write-Output 'Copying - codecs' # Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\Output\system\DevIL.dll $PortableOutputDir # Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\Output\AviSynth.dll $PortableOutputDir # Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x64\Output\plugins\DirectShowSource.dll $PortableOutputDir -Write-Output 'Copying - codecs\VSFilter' -Copy-New-Item $InstallerDepsDir\VSFilter\x64\VSFilter.dll $PortableOutputDir\csri +if ($Architecture -eq 'x64') { + Write-Output 'Copying - codecs\VSFilter' + Copy-New-Item $InstallerDepsDir\VSFilter\x64\VSFilter.dll $PortableOutputDir\csri +} Write-Output 'Copying - runtimes\MS-CRT' -Copy-New-Item $InstallerDepsDir\VC_redist\VC_redist.x64.exe $PortableOutputDir\Microsoft.CRT +Copy-New-Item "$InstallerDepsDir\VC_redist\VC_redist.$Architecture.exe" $PortableOutputDir\Microsoft.CRT Write-Output 'Copying - automation' Copy-New-Items "$InstallerDir\share\aegisub\automation\*" "$PortableOutputDir\automation\" -Recurse diff --git a/tools/win-installer-setup.ps1 b/tools/win-installer-setup.ps1 index 85b1f6cd8f..9aa7c1060c 100644 --- a/tools/win-installer-setup.ps1 +++ b/tools/win-installer-setup.ps1 @@ -4,7 +4,10 @@ param ( [Parameter(Position = 0)] [string]$BuildRoot, [Parameter(Position = 1)] - [string]$SourceRoot + [string]$SourceRoot, + [Parameter(Position = 2)] + [ValidateSet('x64', 'arm64')] + [string]$Architecture = 'x64' ) $InstallerDir = Join-Path $SourceRoot "packages\win_installer" | Resolve-Path @@ -51,8 +54,8 @@ if (!(Test-Path luajson)) { # Remove-Item AviSynthPlus.7z # } -# VSFilter -if (!(Test-Path VSFilter)) { +# VSFilter has no ARM64 build and must not be put in an ARM64 process. +if ($Architecture -eq 'x64' -and !(Test-Path VSFilter)) { $vsFilterDir = New-Item -ItemType Directory VSFilter Set-Location $vsFilterDir $vsFilterReleases = Invoke-WebRequest "https://api.github.com/repos/pinterf/xy-VSFilter/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json @@ -76,9 +79,14 @@ if (!(Test-Path ffi-experiments)) { } # VC++ redistributable -if (!(Test-Path VC_redist)) { - $redistDir = New-Item -ItemType Directory VC_redist - Invoke-WebRequest https://aka.ms/vs/17/release/VC_redist.x64.exe -OutFile "$redistDir\VC_redist.x64.exe" -UseBasicParsing +$redistDir = Join-Path $DepsDir 'VC_redist' +$redistName = "VC_redist.$Architecture.exe" +$redistPath = Join-Path $redistDir $redistName +if (!(Test-Path $redistPath)) { + if (!(Test-Path $redistDir)) { + New-Item -ItemType Directory -Path $redistDir + } + Invoke-WebRequest "https://aka.ms/vs/17/release/$redistName" -OutFile $redistPath -UseBasicParsing } # Dictionaries @@ -108,5 +116,9 @@ if(!$?) { Exit $LASTEXITCODE } # Invoke InnoSetup $IssUrl = Join-Path $InstallerDir "aegisub_depctrl.iss" -iscc $IssUrl +if ($Architecture -eq 'arm64') { + iscc /DARM64 $IssUrl +} else { + iscc $IssUrl +} if(!$?) { Exit $LASTEXITCODE } From 3753d35780c29eeba0f2502bf418413b0ff1d12c Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 14:59:28 -0700 Subject: [PATCH 08/14] build: guard unsupported shared wx fallbacks --- meson.build | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 0ecd6c6377..f911c9c95d 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ project('Aegisub', ['c', 'cpp'], version: '3.5.0') cmake = import('cmake') +fs = import('fs') force_fallback_for = get_option('force_fallback_for') if host_machine.system() == 'darwin' @@ -191,6 +192,12 @@ else if get_option('default_library') == 'static' build_shared = false endif + + if build_shared and (zlib_dep.type_name() == 'internal' or libpng_dep.type_name() == 'internal') + error('Shared fallback wxWidgets cannot consume Meson-built zlib/libpng safely: ' + + 'Meson converts them to raw paths without Ninja dependency edges. ' + + 'Use -Ddefault_library=static or provide system zlib and libpng.') + endif build_type = 'Release' if get_option('buildtype') == 'debug' build_type = 'Debug' @@ -228,8 +235,9 @@ else if zlib_dep.type_name() == 'internal' zlib = subproject('zlib').get_variable('zlib') - zlib_source_dir = (meson.project_source_root() / 'subprojects/zlib-1.3.1').replace('\\', '/') zlib_library = zlib.full_path().replace('\\', '/') + zlib_build_dir = fs.parent(zlib_library).replace('\\', '/') + zlib_source_dir = (meson.project_source_root() / 'subprojects' / fs.name(zlib_build_dir)).replace('\\', '/') opt_var.add_cmake_defines({ 'ZLIB_INCLUDE_DIR': zlib_source_dir, 'ZLIB_LIBRARY': zlib_library, @@ -242,9 +250,9 @@ else if libpng_dep.type_name() == 'internal' libpng = subproject('libpng').get_variable('libpng') - libpng_source_dir = (meson.project_source_root() / 'subprojects/libpng-1.6.55').replace('\\', '/') - libpng_build_dir = (meson.project_build_root() / 'subprojects/libpng-1.6.55').replace('\\', '/') libpng_library = libpng.full_path().replace('\\', '/') + libpng_build_dir = fs.parent(libpng_library).replace('\\', '/') + libpng_source_dir = (meson.project_source_root() / 'subprojects' / fs.name(libpng_build_dir)).replace('\\', '/') opt_var.add_cmake_defines({ 'PNG_LIBRARY': libpng_library, 'PNG_PNG_INCLUDE_DIR': libpng_source_dir, From 7937409530dd82ff1004b34ad418371b7c59daa6 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 15:41:06 -0700 Subject: [PATCH 09/14] build: polish macOS packaging workflows --- .github/workflows/ci.yml | 13 +++++++++++++ README.md | 19 ++++++++++++++++--- packages/meson.build | 3 +-- tools/osx-bundle.sh | 6 +++--- tools/osx-notarize.sh | 4 ++-- tools/osx-sign.sh | 18 ++++++++++++++++-- tools/version.ps1 | 5 ++++- tools/version.sh | 5 +++-- 8 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b8979612a..d113580697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ jobs: build: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} + env: + # Pull request checkouts use a detached merge commit. Preserve the source + # branch in generated non-release version and artifact names. + AEGISUB_BUILD_BRANCH: ${{ github.head_ref || github.ref_name }} strategy: fail-fast: false @@ -163,6 +167,15 @@ jobs: ${{ matrix.config.devenv }} meson setup build ${{ matrix.config.args }} ${{ matrix.config.arch_args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} + - name: Cache FFTW (MacOS) + if: startsWith(matrix.config.os, 'macos-') + uses: actions/cache@v6 + with: + path: build/fftw-prefix + # The script hash covers the FFTW version and build flags; the Meson + # options hash covers the configured deployment-target default. + key: ${{ runner.os }}-${{ runner.arch }}-fftw-${{ hashFiles('tools/macos-build-fftw.sh', 'meson_options.txt') }} + - name: Build FFTW (MacOS) if: startsWith(matrix.config.os, 'macos-') run: | diff --git a/README.md b/README.md index 055489c38b..c47a926ebf 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,27 @@ When compiling on Apple Silicon, replace `/usr/local` with `/opt/homebrew`. Once the dependencies are installed, build Aegisub with `meson build && meson compile -C build`. -#### Build dmg +#### Build a local DMG + +Homebrew bottles target the macOS release they were built for, which may be +newer than Aegisub's default deployment target. For a local package, target +the current macOS major release and explicitly request an ad-hoc signature. +The resulting DMG is for development and personal use on that macOS release; +release packages use CI's source-built dependencies and the +[macOS release-signing process](docs/developer_docs.md#macos-release-signing). ```bash -meson build_static -Ddefault_library=static -Dbuildtype=debugoptimized -Dbuild_osx_bundle=true --force-fallback-for=boost +deployment_target="$(sw_vers -productVersion)" +meson setup build_static \ + -Ddefault_library=static \ + -Dbuildtype=debugoptimized \ + -Dbuild_osx_bundle=true \ + -Dmacos_deployment_target="${deployment_target}" \ + --force-fallback-for=boost meson compile -C build_static meson test -C build_static --verbose meson compile osx-bundle -C build_static -meson compile osx-build-dmg -C build_static +AEGISUB_BUNDLE_SIGNATURE=- meson compile osx-build-dmg -C build_static ``` ### Linux or other diff --git a/packages/meson.build b/packages/meson.build index 07e21b6230..969a441940 100644 --- a/packages/meson.build +++ b/packages/meson.build @@ -13,10 +13,9 @@ if host_machine.system() == 'windows' '-File', meson.project_source_root() / 'packages/win_installer/portable/create-portable.ps1'] run_target('win-portable', command: [portable_setup, meson.project_build_root(), meson.project_source_root(), package_arch]) elif host_machine.system() == 'darwin' - fontconfig_conf = run_command('pkg-config', '--variable=confdir', 'fontconfig').stdout().strip() bundle_app_sh = find_program(meson.project_source_root() / 'tools/osx-bundle.sh') run_target('osx-bundle', - command: [bundle_app_sh, meson.project_source_root(), meson.project_build_root(), 'wx-config', fontconfig_conf, '', + command: [bundle_app_sh, meson.project_source_root(), meson.project_build_root(), '', get_option('build_osx_bundle') ? 'TRUE' : 'FALSE', get_option('macos_deployment_target')]) sign_app_sh = find_program(meson.project_source_root() / 'tools/osx-sign.sh') diff --git a/tools/osx-bundle.sh b/tools/osx-bundle.sh index 634258ffe0..d7b27d66b2 100755 --- a/tools/osx-bundle.sh +++ b/tools/osx-bundle.sh @@ -4,9 +4,9 @@ set -e SRC_DIR="${1}" BUILD_DIR="${2}" -DICT_DIR="${5}" -MESON_BUILD_OSX_BUNDLE="${6}" -MACOS_DEPLOYMENT_TARGET="${7}" +DICT_DIR="${3}" +MESON_BUILD_OSX_BUNDLE="${4}" +MACOS_DEPLOYMENT_TARGET="${5}" if [ "${MESON_BUILD_OSX_BUNDLE}" != "TRUE" ]; then echo "Project not built with \`build_osx_bundle\`" diff --git a/tools/osx-notarize.sh b/tools/osx-notarize.sh index 10606c026d..90dfc38637 100755 --- a/tools/osx-notarize.sh +++ b/tools/osx-notarize.sh @@ -39,12 +39,12 @@ verify_image() { } if test -z "${NOTARY_PROFILE}"; then - echo "AEGISUB_NOTARY_PROFILE must name a notarytool Keychain profile" + echo "AEGISUB_NOTARY_PROFILE must name a notarytool Keychain profile" >&2 exit 1 fi if ! test -f "${DMG_PATH}"; then - echo "\"${DMG_PATH}\" does not exist, please run 'meson compile osx-build-dmg'" + echo "\"${DMG_PATH}\" does not exist, please run 'meson compile osx-build-dmg'" >&2 exit 1 fi diff --git a/tools/osx-sign.sh b/tools/osx-sign.sh index 0dee35bb89..3e4ad2c460 100755 --- a/tools/osx-sign.sh +++ b/tools/osx-sign.sh @@ -34,11 +34,25 @@ echo "---- Signing app bundle ----" # Sign each real Mach-O file once. Library aliases are symlinks to these files # and do not need (or want) their own signatures. -find "${PKG_DIR}/Contents" -type f -print | while IFS= read -r fname; do +SIGN_FILE_LIST="$(mktemp "${TMPDIR:-/tmp}/aegisub-sign.XXXXXX")" +cleanup() { + rm -f "${SIGN_FILE_LIST}" +} +trap cleanup EXIT HUP INT TERM + +if ! find "${PKG_DIR}/Contents" -type f -print > "${SIGN_FILE_LIST}"; then + echo "Could not enumerate files in \"${PKG_DIR}\"" >&2 + exit 1 +fi + +while IFS= read -r fname; do case "$(file -b "${fname}")" in Mach-O*) sign_file "${fname}" ;; esac -done +done < "${SIGN_FILE_LIST}" + +rm -f "${SIGN_FILE_LIST}" +trap - EXIT HUP INT TERM if test "${SIGN_IDENTITY}" = "-"; then codesign --force --sign - "${PKG_DIR}" diff --git a/tools/version.ps1 b/tools/version.ps1 index 4be5ffca9e..1293ae6a49 100644 --- a/tools/version.ps1 +++ b/tools/version.ps1 @@ -45,7 +45,10 @@ if (Test-Path $gitVersionHeaderPath) { $gitRevision = $lastSvnRevision + ((git -C $repositoryRootPath log --pretty=oneline "$($lastSvnHash)..HEAD" 2>$null | Measure-Object).Count) $gitBranch = [string](git -C $repositoryRootPath symbolic-ref --short HEAD 2>$null) -$gitBranch = ($gitBranch -replace '[^A-Za-z0-9._-]', '-').Trim('-') +if ([string]::IsNullOrEmpty($gitBranch)) { + $gitBranch = [Environment]::GetEnvironmentVariable('AEGISUB_BUILD_BRANCH') +} +$gitBranch = (($gitBranch -replace '[^A-Za-z0-9._-]', '-') -replace '-+', '-').Trim('-') if ([string]::IsNullOrEmpty($gitBranch)) { $gitBranch = 'unnamed-branch' } diff --git a/tools/version.sh b/tools/version.sh index eeb9854bc4..e7f32c0472 100755 --- a/tools/version.sh +++ b/tools/version.sh @@ -63,8 +63,9 @@ if test x$git_version_str != x; then resource_version=$(echo $git_version_str | sed 's/\./, /g') fi else - git_branch="$(git symbolic-ref HEAD 2> /dev/null)" || git_branch="(unnamed branch)" - git_branch="${git_branch##refs/heads/}" + if ! git_branch="$(git symbolic-ref --short HEAD 2> /dev/null)"; then + git_branch="${AEGISUB_BUILD_BRANCH:-}" + fi # Keep the version safe for Info.plist's generated sed script and package filenames. git_branch="$(printf '%s' "${git_branch}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '-' | sed 's/--*/-/g; s/^-//; s/-$//')" if test -z "${git_branch}"; then From e2a99995e9064c6a5decd3cb2a071b11806219b4 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 16:07:56 -0700 Subject: [PATCH 10/14] build: tighten packaging dependency policy --- .github/workflows/ci.yml | 7 ++----- docs/developer_docs.md | 27 +++++++++++++++++++-------- meson.build | 5 +++-- tools/osx-bundle.sh | 11 +++++++++-- tools/osx-dmg.sh | 15 +++++++++++++-- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d113580697..071324e92e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,6 @@ jobs: - name: macOS x86_64 Debug os: macos-15-intel buildtype: debugoptimized - arch_args: -Dffmpeg:lzma=disabled args: &macos_args >- -Ddefault_library=static -Dbuild_osx_bundle=true @@ -85,6 +84,7 @@ jobs: -Dharfbuzz:utilities=disabled -Dharfbuzz:vector=disabled -Dffmpeg:bzlib=disabled + -Dffmpeg:lzma=disabled -Dffmpeg:programs=disabled -Dffmpeg:tests=disabled -Ddav1d:enable_tests=false @@ -94,17 +94,14 @@ jobs: - name: macOS x86_64 Release os: macos-15-intel buildtype: release - arch_args: -Dffmpeg:lzma=disabled args: *macos_args - name: macOS arm64 Debug os: macos-15 buildtype: debugoptimized - arch_args: '' args: *macos_args - name: macOS arm64 Release os: macos-15 buildtype: release - arch_args: '' args: *macos_args steps: @@ -165,7 +162,7 @@ jobs: - name: Configure run: | ${{ matrix.config.devenv }} - meson setup build ${{ matrix.config.args }} ${{ matrix.config.arch_args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} + meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} - name: Cache FFTW (MacOS) if: startsWith(matrix.config.os, 'macos-') diff --git a/docs/developer_docs.md b/docs/developer_docs.md index 9111be740a..3f15b23cb5 100644 --- a/docs/developer_docs.md +++ b/docs/developer_docs.md @@ -33,17 +33,21 @@ export AEGISUB_NOTARY_PROFILE=aegisub-release xcrun notarytool store-credentials "${AEGISUB_NOTARY_PROFILE}" ``` -Check out the exact commit which produced the CI artifact, configure a build +Check out the exact commit which produced the CI artifact, create a staging directory, and extract the artifact into it. The resulting path must be `build/Aegisub.app`: ```bash -meson setup build -Dbuild_osx_bundle=true -Ddefault_library=static +mkdir -p build ditto -x -k /path/to/Aegisub-*-signing-input.zip build ``` -Export the release identity and notary profile, then run the three release -targets in order: +This release-manager step does not rebuild or resolve Aegisub's dependencies. +It requires the matching source checkout, Xcode command-line tools, the +Developer ID certificate, the stored notary profile, and the extracted app; +Meson, CMake, Ninja, Homebrew, and network access to the wrap sources are not +required. Export the release identity and notary profile, then run the three +scripts in order from the repository root: ```bash export AEGISUB_BUNDLE_SIGNATURE='Developer ID Application: Example (TEAMID)' @@ -53,12 +57,15 @@ export AEGISUB_NOTARY_PROFILE=aegisub-release export AEGISUB_SIGNING_KEYCHAIN=/path/to/signing.keychain-db export AEGISUB_NOTARY_KEYCHAIN=/path/to/notary.keychain-db -meson compile -C build osx-sign -meson compile -C build osx-build-dmg -meson compile -C build osx-notarize +tools/osx-sign.sh "$PWD" "$PWD/build/Aegisub.app" +tools/osx-dmg.sh "$PWD" "$PWD/build" +tools/osx-notarize.sh "$PWD" "$PWD/build" ``` -The targets are wrappers around these direct script interfaces: +The final notarization submission requires internet access to Apple's notary +service. + +The complete script interfaces are: ```text tools/osx-sign.sh SOURCE_DIR AEGISUB_APP @@ -66,6 +73,10 @@ tools/osx-dmg.sh SOURCE_DIR BUILD_DIR [VERSION_OVERRIDE] tools/osx-notarize.sh SOURCE_DIR BUILD_DIR [VERSION_OVERRIDE] ``` +An existing, fully configured Meson build offers equivalent `osx-sign`, +`osx-build-dmg`, and `osx-notarize` targets, but setting up a fresh build only +to obtain these wrappers unnecessarily resolves the full dependency tree. + `AEGISUB_BUNDLE_SIGNATURE` is mandatory. Set it to `-` only for an explicitly ad-hoc CI or development build; such an image cannot be notarized. `AEGISUB_BUNDLE_ENTITLEMENTS` optionally replaces the default entitlements diff --git a/meson.build b/meson.build index f911c9c95d..39e1d83b2a 100644 --- a/meson.build +++ b/meson.build @@ -19,6 +19,7 @@ project('Aegisub', ['c', 'cpp'], cmake = import('cmake') fs = import('fs') force_fallback_for = get_option('force_fallback_for') +force_all_fallbacks = get_option('wrap_mode') == 'forcefallback' if host_machine.system() == 'darwin' macos_deployment_target = get_option('macos_deployment_target') @@ -128,7 +129,7 @@ if boost_static boost_modules += 'thread' # static boost dependencies do not include transitive dependencies https://github.com/mesonbuild/meson/issues/15457 endif -use_bundled_boost = force_fallback_for.contains('boost') +use_bundled_boost = force_all_fallbacks or force_fallback_for.contains('boost') if not use_bundled_boost boost_dep = dependency('boost', version: '>=1.70.0', @@ -169,7 +170,7 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep -if force_fallback_for.contains('wxWidgets') +if force_all_fallbacks or force_fallback_for.contains('wxWidgets') wx_dep = dependency('', required: false) else wx_dep = dependency('wxWidgets', version: '>=' + get_option('wx_version'), diff --git a/tools/osx-bundle.sh b/tools/osx-bundle.sh index d7b27d66b2..357cc2a272 100755 --- a/tools/osx-bundle.sh +++ b/tools/osx-bundle.sh @@ -110,10 +110,17 @@ echo "---- Verifying deployment target ----" python3 "${SRC_DIR}/tools/macos-verify-deployment-target.py" "${PKG_DIR}" echo -echo "---- Ad-hoc signing ----" +echo "---- Signing ----" # Even if the binaries were already ad-hoc signed during compilation, # they need to be resigned after bundling and rewriting dylib paths. -AEGISUB_BUNDLE_SIGNATURE=- "${SRC_DIR}/tools/osx-sign.sh" "${SRC_DIR}" "${PKG_DIR}" +# Preserve the historical ability to request a Developer ID-signed bundle, +# while treating an unset identity as a development/ad-hoc operation. An +# explicitly set but empty identity is still rejected by osx-sign.sh. +if test -n "${AEGISUB_BUNDLE_SIGNATURE+x}"; then + "${SRC_DIR}/tools/osx-sign.sh" "${SRC_DIR}" "${PKG_DIR}" +else + AEGISUB_BUNDLE_SIGNATURE=- "${SRC_DIR}/tools/osx-sign.sh" "${SRC_DIR}" "${PKG_DIR}" +fi echo echo "Done creating \"${PKG_DIR}\"" diff --git a/tools/osx-dmg.sh b/tools/osx-dmg.sh index b35df819a9..c718c94611 100755 --- a/tools/osx-dmg.sh +++ b/tools/osx-dmg.sh @@ -21,8 +21,8 @@ BUILD_DIR="${2}" PKG_DIR="${BUILD_DIR}/Aegisub.app" if ! test -d "${PKG_DIR}"; then - echo "\"${PKG_DIR}\" does not exist, please run 'meson compile osx-bundle'" - exit 1; + echo "\"${PKG_DIR}\" does not exist, please run 'meson compile osx-bundle'" >&2 + exit 1 fi if test -z "${AEGISUB_BUNDLE_SIGNATURE+x}" || test -z "${AEGISUB_BUNDLE_SIGNATURE}"; then @@ -32,6 +32,17 @@ fi SIGN_IDENTITY="${AEGISUB_BUNDLE_SIGNATURE}" SIGN_KEYCHAIN="${AEGISUB_SIGNING_KEYCHAIN:-}" +DEVELOPER_ID_REQUIREMENT='anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.13] exists' + +if test "${SIGN_IDENTITY}" != "-"; then + # Do not create an apparently release-signed image around an ad-hoc app. + # The app must be signed separately because changing its signature after + # image creation would invalidate the image contents. + if ! codesign --verify --deep --strict --verbose=2 -R="${DEVELOPER_ID_REQUIREMENT}" "${PKG_DIR}"; then + echo "\"${PKG_DIR}\" is not Developer ID-signed; sign the app with tools/osx-sign.sh or the osx-sign Meson target first" >&2 + exit 1 + fi +fi PKG_NAME="$("${SRC_DIR}/tools/osx-package-name.sh" "${PKG_DIR}" "${3:-}")" PKG_NAME_VOLUME="${PKG_NAME}" From d3c654008538add0c8a74a33af61e5135016750d Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 16:59:28 -0700 Subject: [PATCH 11/14] ci: defer FFTW caching --- .github/workflows/ci.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 071324e92e..9e34f144eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,15 +164,6 @@ jobs: ${{ matrix.config.devenv }} meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} - - name: Cache FFTW (MacOS) - if: startsWith(matrix.config.os, 'macos-') - uses: actions/cache@v6 - with: - path: build/fftw-prefix - # The script hash covers the FFTW version and build flags; the Meson - # options hash covers the configured deployment-target default. - key: ${{ runner.os }}-${{ runner.arch }}-fftw-${{ hashFiles('tools/macos-build-fftw.sh', 'meson_options.txt') }} - - name: Build FFTW (MacOS) if: startsWith(matrix.config.os, 'macos-') run: | From 21c5f751e8c1cc0d2c31d7d86d20c4f9740aa536 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 17:30:57 -0700 Subject: [PATCH 12/14] build: disable unused wx TIFF support --- docs/developer_docs.md | 24 ++++++++++++++++++++---- meson.build | 7 +++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/developer_docs.md b/docs/developer_docs.md index 3f15b23cb5..734106987a 100644 --- a/docs/developer_docs.md +++ b/docs/developer_docs.md @@ -33,13 +33,29 @@ export AEGISUB_NOTARY_PROFILE=aegisub-release xcrun notarytool store-credentials "${AEGISUB_NOTARY_PROFILE}" ``` -Check out the exact commit which produced the CI artifact, create a staging -directory, and extract the artifact into it. The resulting path must be -`build/Aegisub.app`: +Check out the exact commit which produced the CI artifact. Downloads from the +GitHub web UI are wrapper ZIPs containing the uploaded `-signing-input.zip`. +Extract that wrapper first: + +```bash +mkdir -p signing-artifact +ditto -x -k /path/to/github-artifact-download.zip signing-artifact +``` + +Alternatively, GitHub CLI performs that outer extraction while downloading: + +```bash +gh run download RUN_ID \ + --name 'macOS arm64 Release - local signing input' \ + --dir signing-artifact +``` + +Then extract the signing input into the staging directory. The resulting path +must be `build/Aegisub.app`: ```bash mkdir -p build -ditto -x -k /path/to/Aegisub-*-signing-input.zip build +ditto -x -k signing-artifact/Aegisub-*-signing-input.zip build ``` This release-manager step does not rebuild or resolve Aegisub's dependencies. diff --git a/meson.build b/meson.build index 39e1d83b2a..ad1b5649c1 100644 --- a/meson.build +++ b/meson.build @@ -215,6 +215,9 @@ else 'wxUSE_WEBVIEW': false, # breaks build on linux 'wxUSE_LIBPNG': 'sys', # supplied by Meson below + # Aegisub registers only wxPNGHandler. Avoid building the unused TIFF + # codec and its additional zlib/JPEG dependency plumbing. + 'wxUSE_LIBTIFF': 'OFF', 'wxUSE_ZLIB': 'sys', # supplied by Meson below 'CMAKE_BUILD_TYPE': build_type, 'wxUSE_IMAGE': true, @@ -278,6 +281,10 @@ else wx = cmake.subproject('wxWidgets', options: opt_var) + if wx.target_list().contains('wxtiff') + error('wxWidgets unexpectedly built wxtiff despite wxUSE_LIBTIFF=OFF') + endif + foreach bundled_dependency: ['wxpng', 'wxzlib'] if wx.target_list().contains(bundled_dependency) error('wxWidgets unexpectedly built ' + bundled_dependency + From e190bcc3fbaf00eb37587886e266cf56d809623f Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 21:20:10 -0700 Subject: [PATCH 13/14] ci: keep macOS debug builds lightweight --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e34f144eb..8f09ef37ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,16 @@ jobs: - name: macOS x86_64 Debug os: macos-15-intel buildtype: debugoptimized - args: &macos_args >- + 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 + buildtype: release + args: &macos_release_args >- -Ddefault_library=static -Dbuild_osx_bundle=true -Dfftw3=disabled @@ -91,18 +100,14 @@ jobs: -Ddav1d:enable_tools=false -Dzlib:tests=disabled --force-fallback-for=boost,wxWidgets,libass,zlib,freetype2,fribidi,harfbuzz,libpng,ffms2,libavformat,libavcodec,libswscale,libavutil,libswresample,dav1d,hunspell,uchardet,icu-uc,icu-i18n,libcurl - - name: macOS x86_64 Release - os: macos-15-intel - buildtype: release - args: *macos_args - name: macOS arm64 Debug os: macos-15 buildtype: debugoptimized - args: *macos_args + args: *macos_debug_args - name: macOS arm64 Release os: macos-15 buildtype: release - args: *macos_args + args: *macos_release_args steps: - uses: actions/checkout@v6 @@ -148,8 +153,15 @@ jobs: Get-Location | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append popd - - name: Install dependencies (MacOS) - if: startsWith(matrix.config.os, 'macos-') + - name: Install dependencies (macOS Debug) + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'debugoptimized' + run: | + brew install ninja pkg-config libass boost zlib libpng ffms2 fftw hunspell uchardet icu4c wxwidgets + + echo "PKG_CONFIG_PATH=$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix zlib)/lib/pkgconfig" >> "${GITHUB_ENV}" + + - name: Install dependencies (macOS Release) + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' run: | brew install ninja nasm @@ -165,7 +177,7 @@ jobs: meson setup build ${{ matrix.config.args }} -Dbuildtype=${{ matrix.config.buildtype }} ${{ github.ref_type == 'tag' && '-Dofficial_release=true' || '' }} - name: Build FFTW (MacOS) - if: startsWith(matrix.config.os, 'macos-') + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' run: | tools/macos-build-fftw.sh build meson setup --reconfigure --clearcache build \ @@ -215,7 +227,7 @@ jobs: # macOS artifacts - name: Generate macOS installer - if: startsWith(matrix.config.os, 'macos-') + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' env: # CI artifacts are intentionally ad-hoc signed. Release managers must # explicitly replace this with a Developer ID signature before release. @@ -251,7 +263,7 @@ jobs: - name: Upload artifacts - macOS dmg uses: actions/upload-artifact@v6 - if: startsWith(matrix.config.os, 'macos-') + if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release' with: name: ${{ matrix.config.name }} - ad-hoc installer path: build/Aegisub-*.dmg From 42c4ac0638c30c414ac14a75e320e65376e6b5c1 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 12 Aug 2026 21:25:50 -0700 Subject: [PATCH 14/14] build: clarify wx fallback selection --- meson.build | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index ad1b5649c1..733820d0a7 100644 --- a/meson.build +++ b/meson.build @@ -170,15 +170,15 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep -if force_all_fallbacks or force_fallback_for.contains('wxWidgets') - wx_dep = dependency('', required: false) -else +use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') +if not use_bundled_wx wx_dep = dependency('wxWidgets', version: '>=' + get_option('wx_version'), required: false, modules: ['std', 'stc', 'gl']) + use_bundled_wx = not wx_dep.found() endif -if wx_dep.found() +if not use_bundled_wx deps += wx_dep wx_defines_dep = wx_dep else