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 }