diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index de06dc0b34..8f09ef37ba 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
@@ -55,30 +59,55 @@ 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_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
+ -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:lzma=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 arm64 Debug
+ os: macos-15
+ buildtype: debugoptimized
+ args: *macos_debug_args
+ - name: macOS arm64 Release
+ os: macos-15
+ buildtype: release
+ args: *macos_release_args
steps:
- uses: actions/checkout@v6
@@ -124,13 +153,17 @@ 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 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 +176,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-') && matrix.config.buildtype == 'release'
+ 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 }}
@@ -181,11 +222,16 @@ 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
- 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.
+ AEGISUB_BUNDLE_SIGNATURE: '-'
run: |
meson compile osx-bundle -C build
@@ -201,11 +247,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-')
+ if: startsWith(matrix.config.os, 'macos-') && matrix.config.buildtype == 'release'
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/README.md b/README.md
index 053f04eb17..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 -Dlocal_boost=true
+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/docs/developer_docs.md b/docs/developer_docs.md
index bf314f15d5..734106987a 100644
--- a/docs/developer_docs.md
+++ b/docs/developer_docs.md
@@ -18,6 +18,97 @@ 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`. 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. 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 signing-artifact/Aegisub-*-signing-input.zip build
+```
+
+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)'
+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
+
+tools/osx-sign.sh "$PWD" "$PWD/build/Aegisub.app"
+tools/osx-dmg.sh "$PWD" "$PWD/build"
+tools/osx-notarize.sh "$PWD" "$PWD/build"
+```
+
+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
+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
+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
You can run Doxygen with the following command:
@@ -38,4 +129,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/meson.build b/meson.build
index af1c68547f..733820d0a7 100644
--- a/meson.build
+++ b/meson.build
@@ -17,6 +17,23 @@ project('Aegisub', ['c', 'cpp'],
version: '3.5.0')
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')
+ 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 +104,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 +129,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_all_fallbacks or 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 +147,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'])
@@ -149,20 +167,38 @@ if host_machine.system() == 'windows'
conf.set('BOOST_USE_WINDOWS_H', 1)
endif
-deps += dependency('zlib')
+zlib_dep = dependency('zlib')
+deps += zlib_dep
-wx_dep = dependency('wxWidgets', version: '>=' + get_option('wx_version'),
- required: false,
- modules: ['std', 'stc', 'gl'])
+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
+ # 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
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'
@@ -178,13 +214,84 @@ else
'wxBUILD_SHARED': build_shared,
'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,
'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. 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_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,
+ })
+ 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_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,
+ })
+ # 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', 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'
+ opt_var.add_cmake_defines({
+ 'CMAKE_OSX_DEPLOYMENT_TARGET': macos_deployment_target,
+ })
+ endif
+
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 +
+ ' 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)
@@ -196,15 +303,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'),
]
@@ -324,7 +424,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 +439,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 49de536885..969a441940 100644
--- a/packages/meson.build
+++ b/packages/meson.build
@@ -1,26 +1,34 @@
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']
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',
'-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')
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'])
+ 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')
+ 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/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/packages/osx_bundle/aegisub.entitlements b/packages/osx_bundle/aegisub.entitlements
new file mode 100644
index 0000000000..3d85368ec2
--- /dev/null
+++ b/packages/osx_bundle/aegisub.entitlements
@@ -0,0 +1,11 @@
+
+
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
+
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/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..e3b672a160 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 {
@@ -59,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
@@ -85,5 +90,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/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 cea6cf9248..357cc2a272 100755
--- a/tools/osx-bundle.sh
+++ b/tools/osx-bundle.sh
@@ -4,10 +4,9 @@ set -e
SRC_DIR="${1}"
BUILD_DIR="${2}"
-WX_PREFIX=""
-FONTCONFIG_CONF_DIR="${4}"
-DICT_DIR="${5}"
-MESON_BUILD_OSX_BUNDLE="${6}"
+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\`"
@@ -41,25 +40,26 @@ 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 -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"
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
+)
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,17 +103,23 @@ 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 "---- Verifying deployment target ----"
+python3 "${SRC_DIR}/tools/macos-verify-deployment-target.py" "${PKG_DIR}"
echo
echo "---- 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}/"
+# 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
diff --git a/tools/osx-dmg.sh b/tools/osx-dmg.sh
index 4c431a3c3e..c718c94611 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,46 @@ set -e
SRC_DIR="${1}"
BUILD_DIR="${2}"
-AEGI_VER="${3}"
-PKG_NAME="Aegisub-${AEGI_VER}"
+PKG_DIR="${BUILD_DIR}/Aegisub.app"
+
+if ! test -d "${PKG_DIR}"; then
+ 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
+ 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:-}"
+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}"
-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'"
- exit 1;
-fi
-
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 +72,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 +82,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 +98,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 +128,20 @@ echo "---- Compressing ----"
echo "---- Removing temp dmg \"${DMG_RW_PATH}\" ----"
rm -rf "${DMG_RW_PATH}"
+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..90dfc38637
--- /dev/null
+++ b/tools/osx-notarize.sh
@@ -0,0 +1,92 @@
+#!/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:-}"
+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" >&2
+ exit 1
+fi
+
+if ! test -f "${DMG_PATH}"; then
+ echo "\"${DMG_PATH}\" does not exist, please run 'meson compile osx-build-dmg'" >&2
+ exit 1
+fi
+
+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 ----"
+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 ----"
+# 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}"
+
+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..c2f4d535dc
--- /dev/null
+++ b/tools/osx-package-name.sh
@@ -0,0 +1,32 @@
+#!/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_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" ;;
+ *) 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..3e4ad2c460
--- /dev/null
+++ b/tools/osx-sign.sh
@@ -0,0 +1,71 @@
+#!/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
+
+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}"
+
+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.
+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 < "${SIGN_FILE_LIST}"
+
+rm -f "${SIGN_FILE_LIST}"
+trap - EXIT HUP INT TERM
+
+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..1293ae6a49 100644
--- a/tools/version.ps1
+++ b/tools/version.ps1
@@ -44,7 +44,14 @@ 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)
+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'
+}
$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..e7f32c0472 100755
--- a/tools/version.sh
+++ b/tools/version.sh
@@ -63,8 +63,14 @@ 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
+ git_branch="unnamed-branch"
+ fi
git_hash=$(git rev-parse --short HEAD)
git_version_str="${git_revision}-${git_branch}-${git_hash}"
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 }