From: Stefan Reinauer Date: Thu, 4 Jun 2026 14:59:36 +0000 (-0700) Subject: tools: add macOS packaging helpers X-Git-Url: https://git.unchartedbackwaters.co.uk/w/?a=commitdiff_plain;h=bba6a8d38f43069b7e1659c9a01cee7913aaa45b;p=francis%2Fwinuae.git tools: add macOS packaging helpers Add helpers for building private macOS dependencies, bundling the app, building the QEMU-PPC plugin, creating the DMG, and validating output. This keeps the release packaging flow reproducible outside local shell history. --- diff --git a/od-unix/graphics/dmg_background.tiff b/od-unix/graphics/dmg_background.tiff new file mode 100644 index 00000000..4da049aa Binary files /dev/null and b/od-unix/graphics/dmg_background.tiff differ diff --git a/tools/build-qemu-uae.sh b/tools/build-qemu-uae.sh new file mode 100755 index 00000000..1183b862 --- /dev/null +++ b/tools/build-qemu-uae.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <&2 + exit 1 +fi +qemu_source="$(cd "${qemu_source}" && pwd)" + +if [[ "$(uname -s)" == "Darwin" ]]; then + export MACOSX_DEPLOYMENT_TARGET="${WINUAE_MACOS_DEPLOYMENT_TARGET:-${MACOSX_DEPLOYMENT_TARGET:-13.0}}" +fi + +if [[ -n "${deps_prefix}" ]]; then + if [[ ! -d "${deps_prefix}" ]]; then + echo "error: QEMU_UAE_DEPS_PREFIX does not exist: ${deps_prefix}" >&2 + exit 1 + fi + deps_prefix="$(cd "${deps_prefix}" && pwd)" + export PATH="${deps_prefix}/bin:${PATH}" + export PKG_CONFIG_LIBDIR="${deps_prefix}/lib/pkgconfig:${deps_prefix}/share/pkgconfig${PKG_CONFIG_LIBDIR:+:${PKG_CONFIG_LIBDIR}}" + if [[ "$(uname -s)" == "Darwin" ]]; then + export DYLD_LIBRARY_PATH="${deps_prefix}/lib${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}" + fi +fi + +ninja_executable="${QEMU_UAE_NINJA:-$(command -v ninja || true)}" +if [[ -z "${ninja_executable}" && -x "${qemu_source}/build/pyvenv/bin/ninja" ]]; then + ninja_executable="${qemu_source}/build/pyvenv/bin/ninja" +fi +if [[ -z "${ninja_executable}" ]]; then + echo "error: ninja not found; set QEMU_UAE_NINJA" >&2 + exit 1 +fi + +if [[ "${QEMU_UAE_SKIP_CONFIGURE:-0}" != "1" && ( "${QEMU_UAE_FORCE_CONFIGURE:-0}" == "1" || ! -f "${qemu_source}/build/build.ninja" ) ]]; then + (cd "${qemu_source}" && ./configure-qemu-uae --ninja="${ninja_executable}") +fi + +"${ninja_executable}" -C "${qemu_source}/build" qemu-uae.so + +plugin="${qemu_source}/build/qemu-uae.so" +if [[ ! -f "${plugin}" ]]; then + echo "error: qemu-uae.so was not produced" >&2 + exit 1 +fi + +if [[ -n "${output_plugin}" ]]; then + mkdir -p "$(dirname "${output_plugin}")" + cp "${plugin}" "${output_plugin}" + plugin="${output_plugin}" +fi + +echo "${plugin}" diff --git a/tools/macos-build-deps.sh b/tools/macos-build-deps.sh new file mode 100755 index 00000000..283dff6b --- /dev/null +++ b/tools/macos-build-deps.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat </../winuae-macos-deps. + +Environment: + WINUAE_MACOS_DEPLOYMENT_TARGET Minimum macOS version. Defaults to 13.0. + WINUAE_DEPS_BUILD_DIR Build directory. Defaults to /build. + WINUAE_DEPS_JOBS Parallel build jobs. Defaults to hw.ncpu. + WINUAE_SDL3_SOURCE SDL3 source tree. Required unless + WINUAE_SKIP_SDL3=1. + WINUAE_QT_SOURCE Qt 6 source tree, or a qtbase CMake source + tree. Required unless WINUAE_SKIP_QT=1. + WINUAE_LIBPNG_SOURCE libpng source tree. Optional unless + WINUAE_REQUIRE_LIBPNG=1. + WINUAE_FLAC_SOURCE FLAC source tree. Optional unless + WINUAE_REQUIRE_FLAC=1. + WINUAE_LIBMPEG2_SOURCE libmpeg2 source tree. Optional unless + WINUAE_REQUIRE_LIBMPEG2=1. + WINUAE_MT32EMU_SOURCE Munt source tree or mt32emu source tree. + Optional unless WINUAE_REQUIRE_MT32EMU=1. + WINUAE_SKIP_SDL3=1 Do not build SDL3. + WINUAE_SKIP_QT=1 Do not build Qt. + WINUAE_SKIP_LIBPNG=1 Do not build libpng. + WINUAE_SKIP_FLAC=1 Do not build libFLAC. + WINUAE_SKIP_LIBMPEG2=1 Do not build libmpeg2. + WINUAE_SKIP_MT32EMU=1 Do not build libmt32emu. + WINUAE_REQUIRE_LIBPNG=1 Fail if WINUAE_LIBPNG_SOURCE is missing. + WINUAE_REQUIRE_FLAC=1 Fail if WINUAE_FLAC_SOURCE is missing. + WINUAE_REQUIRE_LIBMPEG2=1 Fail if WINUAE_LIBMPEG2_SOURCE is missing. + WINUAE_REQUIRE_MT32EMU=1 Fail if WINUAE_MT32EMU_SOURCE is missing. + WINUAE_SDL3_CMAKE_ARGS Extra arguments passed to SDL3 CMake. + WINUAE_QT_CONFIGURE_ARGS Extra arguments passed to Qt configure, + in addition to the release-safe defaults. + WINUAE_QT_CMAKE_ARGS Extra arguments passed to Qt CMake. + WINUAE_LIBPNG_CMAKE_ARGS Extra arguments passed to libpng CMake. + WINUAE_FLAC_CMAKE_ARGS Extra arguments passed to FLAC CMake. + WINUAE_LIBMPEG2_CONFIGURE_ARGS Extra arguments passed to libmpeg2 configure. + WINUAE_MT32EMU_CMAKE_ARGS Extra arguments passed to mt32emu CMake. + +Extra argument variables are whitespace-separated argv fragments. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source_dir="$(cd "${script_dir}/.." && pwd)" +target="${WINUAE_MACOS_DEPLOYMENT_TARGET:-13.0}" +prefix="${1:-${WINUAE_DEPS_PREFIX:-${source_dir}/../winuae-macos-deps}}" +build_dir="${WINUAE_DEPS_BUILD_DIR:-${prefix}/build}" +sdl_source="${WINUAE_SDL3_SOURCE:-}" +qt_source="${WINUAE_QT_SOURCE:-}" +libpng_source="${WINUAE_LIBPNG_SOURCE:-}" +flac_source="${WINUAE_FLAC_SOURCE:-}" +libmpeg2_source="${WINUAE_LIBMPEG2_SOURCE:-}" +mt32emu_source="${WINUAE_MT32EMU_SOURCE:-}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS dependency builds require Darwin/macOS" >&2 + exit 1 +fi + +jobs="${WINUAE_DEPS_JOBS:-}" +if [[ -z "${jobs}" ]]; then + jobs="$(sysctl -n hw.ncpu 2>/dev/null || echo 8)" +fi +export MACOSX_DEPLOYMENT_TARGET="${target}" + +require_source() { + local name="$1" + local path="$2" + if [[ -z "${path}" ]]; then + echo "error: ${name} source path is required" >&2 + usage >&2 + exit 1 + fi + if [[ ! -d "${path}" ]]; then + echo "error: ${name} source directory not found: ${path}" >&2 + exit 1 + fi +} + +run_cmake_build() { + local src="$1" + local bld="$2" + shift 2 + cmake -S "${src}" -B "${bld}" "$@" + cmake --build "${bld}" -j "${jobs}" + cmake --install "${bld}" +} + +run_autotools_build() { + local src="$1" + local bld="$2" + shift 2 + if [[ -z "${bld}" || "${bld}" == "/" ]]; then + echo "error: refusing unsafe autotools build directory: ${bld}" >&2 + exit 1 + fi + rm -rf "${bld}" + mkdir -p "${bld}/src" + cp -R "${src}/." "${bld}/src/" + ( + cd "${bld}/src" + if [[ -f Makefile ]]; then + make distclean >/dev/null 2>&1 || true + fi + rm -f config.log config.status + env \ + CFLAGS="${CFLAGS:-} -mmacosx-version-min=${target}" \ + CXXFLAGS="${CXXFLAGS:-} -mmacosx-version-min=${target}" \ + LDFLAGS="${LDFLAGS:-} -mmacosx-version-min=${target}" \ + ./configure "$@" + make -j "${jobs}" + make install + ) +} + +patch_qtbase_source() { + local header="${qt_source}/src/corelib/thread/qyieldcpu.h" + if [[ -f "${header}" ]] && grep -q "__yield();" "${header}" && ! grep -q "arm_acle.h" "${header}"; then + perl -0pi -e 's/(#include \n)/$1\n#if defined(__has_include)\n# if __has_include()\n# include \n# endif\n#endif\n/' "${header}" + fi + + local simd="${qt_source}/src/corelib/global/qsimd.cpp" + if [[ -f "${simd}" ]] && grep -q 'sysctlbyname("hw.optional.neon"' "${simd}" && ! grep -q "AArch64 includes Advanced SIMD" "${simd}"; then + perl -0pi -e 's/#elif defined\(Q_OS_DARWIN\) && defined\(Q_PROCESSOR_ARM\)\n unsigned feature;\n size_t len = sizeof\(feature\);\n if \(sysctlbyname\("hw\.optional\.neon", &feature, &len, nullptr, 0\) == 0\)\n features \|= feature \? CpuFeatureNEON : 0;/#elif defined(Q_OS_DARWIN) \&\& defined(Q_PROCESSOR_ARM)\n unsigned feature;\n size_t len = sizeof(feature);\n# if defined(Q_PROCESSOR_ARM_64)\n \/\/ AArch64 includes Advanced SIMD; some macOS versions no longer\n \/\/ expose the legacy hw.optional.neon sysctl that Qt probes here.\n features |= CpuFeatureNEON;\n# else\n if (sysctlbyname("hw.optional.neon", \&feature, \&len, nullptr, 0) == 0)\n features |= feature ? CpuFeatureNEON : 0;\n# endif/' "${simd}" + fi +} + +split_extra_args() { + extra_args=() + if [[ -n "${1:-}" ]]; then + # Extra-arg variables are whitespace-separated argv fragments. + # Split once here and pass the result as an array at call sites. + # shellcheck disable=SC2206 + extra_args=($1) + fi +} + +mkdir -p "${prefix}" "${build_dir}" + +if [[ "${WINUAE_SKIP_LIBPNG:-0}" != "1" ]]; then + if [[ -n "${libpng_source}" ]]; then + require_source "libpng" "${libpng_source}" + libpng_cmake_args=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="${prefix}" + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" + -DPNG_SHARED=ON + -DPNG_STATIC=OFF + -DPNG_TESTS=OFF + -DPNG_TOOLS=OFF + ) + split_extra_args "${WINUAE_LIBPNG_CMAKE_ARGS:-}" + libpng_cmake_args+=(${extra_args[@]+"${extra_args[@]}"}) + run_cmake_build "${libpng_source}" "${build_dir}/libpng" \ + "${libpng_cmake_args[@]}" + elif [[ "${WINUAE_REQUIRE_LIBPNG:-0}" == "1" ]]; then + echo "error: libpng source path is required when WINUAE_REQUIRE_LIBPNG=1" >&2 + usage >&2 + exit 1 + else + echo "note: WINUAE_LIBPNG_SOURCE not set; skipping private libpng build" >&2 + fi +fi + +if [[ "${WINUAE_SKIP_FLAC:-0}" != "1" ]]; then + if [[ -n "${flac_source}" ]]; then + require_source "FLAC" "${flac_source}" + flac_cmake_args=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="${prefix}" + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" + -DBUILD_SHARED_LIBS=ON + -DBUILD_CXXLIBS=OFF + -DBUILD_PROGRAMS=OFF + -DBUILD_EXAMPLES=OFF + -DBUILD_DOCS=OFF + -DBUILD_TESTING=OFF + -DINSTALL_MANPAGES=OFF + -DINSTALL_PKGCONFIG_MODULES=ON + -DWITH_OGG=OFF + ) + split_extra_args "${WINUAE_FLAC_CMAKE_ARGS:-}" + flac_cmake_args+=(${extra_args[@]+"${extra_args[@]}"}) + run_cmake_build "${flac_source}" "${build_dir}/flac" \ + "${flac_cmake_args[@]}" + elif [[ "${WINUAE_REQUIRE_FLAC:-0}" == "1" ]]; then + echo "error: FLAC source path is required when WINUAE_REQUIRE_FLAC=1" >&2 + usage >&2 + exit 1 + else + echo "note: WINUAE_FLAC_SOURCE not set; skipping private libFLAC build" >&2 + fi +fi + +if [[ "${WINUAE_SKIP_LIBMPEG2:-0}" != "1" ]]; then + if [[ -n "${libmpeg2_source}" ]]; then + require_source "libmpeg2" "${libmpeg2_source}" + libmpeg2_configure_args=( + --prefix="${prefix}" + --disable-sdl + --without-x + --enable-shared + --disable-static + --disable-dependency-tracking + ) + if [[ -n "${WINUAE_LIBMPEG2_CONFIGURE_ARGS:-}" ]]; then + split_extra_args "${WINUAE_LIBMPEG2_CONFIGURE_ARGS}" + libmpeg2_configure_args+=(${extra_args[@]+"${extra_args[@]}"}) + fi + run_autotools_build "${libmpeg2_source}" "${build_dir}/libmpeg2" \ + "${libmpeg2_configure_args[@]}" + ( + cd "${build_dir}/libmpeg2/src" + make -C libmpeg2 install + make -C include install + ) + elif [[ "${WINUAE_REQUIRE_LIBMPEG2:-0}" == "1" ]]; then + echo "error: libmpeg2 source path is required when WINUAE_REQUIRE_LIBMPEG2=1" >&2 + usage >&2 + exit 1 + else + echo "note: WINUAE_LIBMPEG2_SOURCE not set; skipping private libmpeg2 build" >&2 + fi +fi + +if [[ "${WINUAE_SKIP_MT32EMU:-0}" != "1" ]]; then + if [[ -n "${mt32emu_source}" ]]; then + require_source "Munt/mt32emu" "${mt32emu_source}" + mt32emu_cmake_source="${mt32emu_source}" + if [[ -f "${mt32emu_source}/mt32emu/CMakeLists.txt" ]]; then + mt32emu_cmake_source="${mt32emu_source}/mt32emu" + fi + if [[ ! -f "${mt32emu_cmake_source}/CMakeLists.txt" ]]; then + echo "error: mt32emu CMakeLists.txt not found under ${mt32emu_source}" >&2 + exit 1 + fi + mt32emu_cmake_args=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="${prefix}" + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" + -DBUILD_TESTING=OFF + -Dlibmt32emu_SHARED=ON + -Dlibmt32emu_C_INTERFACE=ON + -Dlibmt32emu_CPP_INTERFACE=OFF + -Dlibmt32emu_WITH_INTERNAL_RESAMPLER=ON + ) + split_extra_args "${WINUAE_MT32EMU_CMAKE_ARGS:-}" + mt32emu_cmake_args+=(${extra_args[@]+"${extra_args[@]}"}) + run_cmake_build "${mt32emu_cmake_source}" "${build_dir}/mt32emu" \ + "${mt32emu_cmake_args[@]}" + elif [[ "${WINUAE_REQUIRE_MT32EMU:-0}" == "1" ]]; then + echo "error: Munt/mt32emu source path is required when WINUAE_REQUIRE_MT32EMU=1" >&2 + usage >&2 + exit 1 + else + echo "note: WINUAE_MT32EMU_SOURCE not set; skipping private libmt32emu build" >&2 + fi +fi + +if [[ "${WINUAE_SKIP_SDL3:-0}" != "1" ]]; then + require_source "SDL3" "${sdl_source}" + sdl3_cmake_args=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="${prefix}" + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" + -DSDL_SHARED=ON + -DSDL_STATIC=OFF + -DSDL_TESTS=OFF + -DSDL_EXAMPLES=OFF + -DSDL_INSTALL_TESTS=OFF + ) + split_extra_args "${WINUAE_SDL3_CMAKE_ARGS:-}" + sdl3_cmake_args+=(${extra_args[@]+"${extra_args[@]}"}) + run_cmake_build "${sdl_source}" "${build_dir}/sdl3" \ + "${sdl3_cmake_args[@]}" +fi + +if [[ "${WINUAE_SKIP_QT:-0}" != "1" ]]; then + require_source "Qt" "${qt_source}" + qt_build="${build_dir}/qt" + mkdir -p "${qt_build}" + patch_qtbase_source + + qt_configure_args=( + -force-bundled-libs + -no-dbus + -no-openssl + -no-glib + -no-icu + -no-cups + -no-fontconfig + -no-gtk + -qt-doubleconversion + -qt-pcre + -qt-zlib + -qt-libpng + -qt-libjpeg + -qt-freetype + -qt-harfbuzz + ) + if [[ -n "${WINUAE_QT_CONFIGURE_ARGS:-}" ]]; then + split_extra_args "${WINUAE_QT_CONFIGURE_ARGS}" + qt_configure_args+=(${extra_args[@]+"${extra_args[@]}"}) + fi + + qt_cmake_args=() + if ! xcodebuild -version >/dev/null 2>&1 && xcrun --show-sdk-path >/dev/null 2>&1; then + qt_cmake_args+=(-DQT_NO_XCODE_MIN_VERSION_CHECK=ON) + fi + if [[ -n "${WINUAE_QT_CMAKE_ARGS:-}" ]]; then + split_extra_args "${WINUAE_QT_CMAKE_ARGS}" + qt_cmake_args+=(${extra_args[@]+"${extra_args[@]}"}) + fi + + qt_submodule_args=() + if [[ -d "${qt_source}/qtbase" || -f "${qt_source}/init-repository" ]]; then + qt_submodule_args=(-submodules qtbase) + fi + + if [[ -x "${qt_source}/configure" && ! -d "${qt_source}/src/corelib" ]]; then + ( + cd "${qt_build}" + "${qt_source}/configure" \ + -prefix "${prefix}" \ + -release \ + -opensource \ + -confirm-license \ + -nomake examples \ + -nomake tests \ + ${qt_submodule_args[@]+"${qt_submodule_args[@]}"} \ + "${qt_configure_args[@]}" \ + -- \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" \ + ${qt_cmake_args[@]+"${qt_cmake_args[@]}"} + ) + cmake --build "${qt_build}" -j "${jobs}" + cmake --install "${qt_build}" + else + run_cmake_build "${qt_source}" "${qt_build}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${prefix}" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="${target}" \ + -DQT_BUILD_EXAMPLES=OFF \ + -DQT_BUILD_TESTS=OFF \ + -DQT_FEATURE_dbus=OFF \ + -DQT_FEATURE_openssl=OFF \ + -DQT_FEATURE_glib=OFF \ + -DQT_FEATURE_icu=OFF \ + -DQT_FEATURE_cups=OFF \ + -DQT_FEATURE_fontconfig=OFF \ + -DQT_FEATURE_gtk=OFF \ + -DQT_FEATURE_opengl=OFF \ + -DQT_FEATURE_opengles2=OFF \ + ${qt_cmake_args[@]+"${qt_cmake_args[@]}"} + fi +fi + +"${script_dir}/macos-check-deployment-target.sh" "${prefix}" "${target}" + +env_file="${prefix}/winuae-macos-deps-env.sh" +cat > "${env_file}" </../winuae-macos-deps. + +Environment: + WINUAE_MACOS_DEPLOYMENT_TARGET Minimum macOS version. Defaults to 13.0. + WINUAE_QEMU_DEPS_BUILD_DIR Build directory. Defaults to + /build/qemu-deps. + WINUAE_DEPS_JOBS Parallel build jobs. Defaults to hw.ncpu. + WINUAE_GLIB_SOURCE GLib source tree. Required. + WINUAE_MESON meson executable. Defaults to meson in PATH + or ../qemu-uae-v11.0/build/pyvenv/bin/meson. + WINUAE_NINJA ninja executable. Defaults to ninja in PATH. + WINUAE_QEMU_BUILD_TOOLS_DIR Optional tools prefix. If set, + /bin/ninja is used as fallback. + WINUAE_GLIB_MESON_ARGS Extra arguments passed to GLib Meson. + +Extra argument variables are whitespace-separated argv fragments. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source_dir="$(cd "${script_dir}/.." && pwd)" +target="${WINUAE_MACOS_DEPLOYMENT_TARGET:-13.0}" +prefix="${1:-${WINUAE_QEMU_DEPS_PREFIX:-${WINUAE_DEPS_PREFIX:-${source_dir}/../winuae-macos-deps}}}" +build_dir="${WINUAE_QEMU_DEPS_BUILD_DIR:-${prefix}/build/qemu-deps}" +glib_source="${WINUAE_GLIB_SOURCE:-}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS QEMU dependency builds require Darwin/macOS" >&2 + exit 1 +fi + +jobs="${WINUAE_DEPS_JOBS:-}" +if [[ -z "${jobs}" ]]; then + jobs="$(sysctl -n hw.ncpu 2>/dev/null || echo 8)" +fi + +require_source() { + local name="$1" + local path="$2" + if [[ -z "${path}" ]]; then + echo "error: ${name} source path is required" >&2 + usage >&2 + exit 1 + fi + if [[ ! -d "${path}" ]]; then + echo "error: ${name} source directory not found: ${path}" >&2 + exit 1 + fi +} + +find_tool() { + local env_value="$1" + local tool_name="$2" + local fallback="$3" + + if [[ -n "${env_value}" ]]; then + echo "${env_value}" + return + fi + if command -v "${tool_name}" >/dev/null 2>&1; then + command -v "${tool_name}" + return + fi + if [[ -n "${fallback}" && -x "${fallback}" ]]; then + echo "${fallback}" + return + fi + + echo "error: ${tool_name} not found" >&2 + exit 1 +} + +split_extra_args() { + extra_args=() + if [[ -n "${1:-}" ]]; then + # Extra-arg variables are whitespace-separated argv fragments. + # Split once here and pass the result as an array at call sites. + # shellcheck disable=SC2206 + extra_args=($1) + fi +} + +run_meson_build() { + local src="$1" + local bld="$2" + shift 2 + + if [[ -f "${bld}/build.ninja" ]]; then + "${meson_executable}" setup --reconfigure "${bld}" "$@" + else + "${meson_executable}" setup "${bld}" "${src}" "$@" + fi + "${ninja_executable}" -C "${bld}" -j "${jobs}" + "${ninja_executable}" -C "${bld}" install +} + +require_source "GLib" "${glib_source}" + +meson_fallback="${source_dir}/../qemu-uae-v11.0/build/pyvenv/bin/meson" +ninja_fallback="" +if [[ -n "${WINUAE_QEMU_BUILD_TOOLS_DIR:-}" ]]; then + ninja_fallback="${WINUAE_QEMU_BUILD_TOOLS_DIR}/bin/ninja" +fi +meson_executable="$(find_tool "${WINUAE_MESON:-}" meson "${meson_fallback}")" +ninja_executable="$(find_tool "${WINUAE_NINJA:-}" ninja "${ninja_fallback}")" + +mkdir -p "${prefix}" "${build_dir}" + +export MACOSX_DEPLOYMENT_TARGET="${target}" +export PATH="$(dirname "${ninja_executable}"):${prefix}/bin:${PATH}" +export PKG_CONFIG_LIBDIR="${prefix}/lib/pkgconfig:${prefix}/share/pkgconfig" + +common_meson_args=( + --prefix="${prefix}" + --libdir=lib + --buildtype=release + -Ddefault_library=shared +) + +glib_args=( + "${common_meson_args[@]}" + --force-fallback-for=libpcre2-8,libffi,intl + -Dtests=false + -Dinstalled_tests=false + -Dglib_debug=disabled + -Dglib_assert=false + -Dglib_checks=false + -Dman-pages=disabled + -Ddocumentation=false + -Dgtk_doc=false + -Dnls=disabled + -Dselinux=disabled + -Dxattr=false + -Dlibmount=disabled + -Dsysprof=disabled + -Dintrospection=disabled + -Ddtrace=disabled + -Dsystemtap=disabled +) +if [[ -n "${WINUAE_GLIB_MESON_ARGS:-}" ]]; then + split_extra_args "${WINUAE_GLIB_MESON_ARGS}" + glib_args+=(${extra_args[@]+"${extra_args[@]}"}) +fi + +run_meson_build "${glib_source}" "${build_dir}/glib" "${glib_args[@]}" + +"${script_dir}/macos-check-deployment-target.sh" "${prefix}" "${target}" + +env_file="${prefix}/winuae-macos-deps-env.sh" +cat > "${env_file}" </package. + +Environment: + WINUAE_SKIP_MACDEPLOYQT=1 Do not run macdeployqt even if it is available. + WINUAE_SKIP_MACOS_DEPLOYMENT_CHECK=1 + Do not check bundled Mach-O deployment targets. + WINUAE_SKIP_CODESIGN=1 Do not ad-hoc codesign the bundle. + WINUAE_CODESIGN_IDENTITY codesign identity. Defaults to "-" for ad-hoc. + WINUAE_CODESIGN_OPTIONS Extra options passed to codesign, for example + "--options runtime --timestamp". + WINUAE_CODESIGN_ENTITLEMENTS + Optional entitlements plist passed to codesign. + WINUAE_QEMU_UAE_PLUGIN Optional qemu-uae.so path to copy into + Contents/PlugIns. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source_dir="$(cd "${script_dir}/.." && pwd)" +build_dir="${1:-${WINUAE_BUILD_DIR:-$(pwd)}}" +output_dir="${2:-${build_dir}/package}" +executable="${build_dir}/winuae_unix" +app_dir="${output_dir}/WinUAE.app" +contents_dir="${app_dir}/Contents" +macos_dir="${contents_dir}/MacOS" +resources_dir="${contents_dir}/Resources" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS app bundling requires Darwin/macOS" >&2 + exit 1 +fi + +if [[ ! -x "${executable}" ]]; then + echo "error: executable not found: ${executable}" >&2 + echo "hint: build winuae_unix first, or pass the CMake build directory" >&2 + exit 1 +fi + +major="$(awk '/^#define UAEMAJOR / { print $3; exit }' "${source_dir}/include/options.h")" +minor="$(awk '/^#define UAEMINOR / { print $3; exit }' "${source_dir}/include/options.h")" +revision="$(awk '/^#define UAESUBREV / { print $3; exit }' "${source_dir}/include/options.h")" +version="${major:-0}.${minor:-0}.${revision:-0}" +deployment_target="${WINUAE_MACOS_DEPLOYMENT_TARGET:-}" +if [[ -z "${deployment_target}" && -f "${build_dir}/CMakeCache.txt" ]]; then + deployment_target="$(awk -F= '/^CMAKE_OSX_DEPLOYMENT_TARGET:/ { print $2; exit }' "${build_dir}/CMakeCache.txt")" +fi +if [[ -z "${deployment_target}" ]]; then + deployment_target="13.0" +fi + +cmake_cache_value() { + local key="$1" + if [[ -f "${build_dir}/CMakeCache.txt" ]]; then + awk -F= -v key="${key}" '$1 ~ "^" key ":" { print $2; exit }' "${build_dir}/CMakeCache.txt" + fi +} + +append_qt_plugin_candidate() { + local candidate="$1" + if [[ -n "${candidate}" && -f "${candidate}/platforms/libqcocoa.dylib" ]]; then + qt_plugin_candidates+=("${candidate}") + fi +} + +split_extra_args() { + if [[ -n "${1:-}" ]]; then + # Intentionally split user-provided extra flags the same way a shell would. + # shellcheck disable=SC2206 + extra_args=($1) + else + extra_args=() + fi +} + +path_in_list() { + local needle="$1" + shift + local item + for item in "$@"; do + if [[ "${item}" == "${needle}" ]]; then + return 0 + fi + done + return 1 +} + +copy_private_dylib_deps() { + local root_binary="$1" + local install_prefix="$2" + local frameworks_dir="${contents_dir}/Frameworks" + local queue=("${root_binary}") + local visited=() + + mkdir -p "${frameworks_dir}" + while [[ ${#queue[@]} -gt 0 ]]; do + local binary="${queue[0]}" + queue=("${queue[@]:1}") + if path_in_list "${binary}" ${visited[@]+"${visited[@]}"}; then + continue + fi + visited+=("${binary}") + + local dep + while IFS= read -r dep; do + case "${dep}" in + ""|@*|/usr/lib/*|/System/Library/*) + continue + ;; + esac + if [[ ! -f "${dep}" ]]; then + continue + fi + + local name target + name="$(basename "${dep}")" + target="${frameworks_dir}/${name}" + if [[ ! -f "${target}" ]]; then + cp "${dep}" "${target}" + chmod u+w "${target}" 2>/dev/null || true + install_name_tool -id "@rpath/${name}" "${target}" \ + 2>/dev/null || true + fi + install_name_tool -change "${dep}" "${install_prefix}/${name}" \ + "${binary}" 2>/dev/null || true + if ! path_in_list "${target}" ${visited[@]+"${visited[@]}"} \ + && ! path_in_list "${target}" ${queue[@]+"${queue[@]}"}; then + queue+=("${target}") + fi + done < <(otool -L "${binary}" | awk 'NR > 1 { print $1 }') + done +} + +rm -rf "${app_dir}" +mkdir -p "${macos_dir}" "${resources_dir}/od-win32/resources" + +cp "${executable}" "${macos_dir}/WinUAE" +find "${source_dir}/od-win32/resources" -maxdepth 1 -type f \ + ! -name '*.rc' \ + ! -name '*.manifest' \ + ! -name 'resource.h' \ + -exec cp '{}' "${resources_dir}/od-win32/resources/" ';' +cp "${source_dir}/README_unix.md" "${resources_dir}/README_unix.md" + +if [[ -f "${source_dir}/od-win32/resources/winuae.ico" ]]; then + cp "${source_dir}/od-win32/resources/winuae.ico" "${resources_dir}/winuae.ico" + if command -v sips >/dev/null 2>&1; then + sips -s format icns "${source_dir}/od-win32/resources/winuae.ico" --out "${resources_dir}/WinUAE.icns" >/dev/null + fi +fi + +cat > "${contents_dir}/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + WinUAE + CFBundleExecutable + WinUAE + CFBundleIdentifier + net.winuae.unix + CFBundleIconFile + WinUAE.icns + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + WinUAE + CFBundlePackageType + APPL + CFBundleShortVersionString + ${version} + CFBundleVersion + ${version} + LSMinimumSystemVersion + ${deployment_target} + NSHighResolutionCapable + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + uae + + CFBundleTypeName + WinUAE Configuration + CFBundleTypeRole + Editor + + + + +EOF + +qemu_uae_plugin="" +for candidate in \ + "${WINUAE_QEMU_UAE_PLUGIN:-}" \ + "${build_dir}/qemu-uae.so" \ + "${build_dir}/plugins/qemu-uae.so" \ + "${source_dir}/../qemu-uae-v11.0/build/qemu-uae.so" +do + if [[ -n "${candidate}" && -f "${candidate}" ]]; then + qemu_uae_plugin="${candidate}" + break + fi +done + +copy_qemu_uae_plugin() { + if [[ -n "${qemu_uae_plugin}" ]]; then + mkdir -p "${contents_dir}/PlugIns" + cp "${qemu_uae_plugin}" "${contents_dir}/PlugIns/qemu-uae.so" + install_name_tool -add_rpath "@loader_path/../Frameworks" \ + "${contents_dir}/PlugIns/qemu-uae.so" 2>/dev/null || true + copy_private_dylib_deps \ + "${contents_dir}/PlugIns/qemu-uae.so" \ + "@loader_path/../Frameworks" + fi +} + +macdeployqt_executable="${WINUAE_MACDEPLOYQT:-}" +if [[ -z "${macdeployqt_executable}" ]]; then + macdeployqt_executable="$(cmake_cache_value MACDEPLOYQT_EXECUTABLE)" +fi +if [[ -z "${macdeployqt_executable}" ]]; then + macdeployqt_executable="$(command -v macdeployqt || true)" +fi + +if [[ "${WINUAE_SKIP_MACDEPLOYQT:-0}" != "1" && -n "${macdeployqt_executable}" && -x "${macdeployqt_executable}" ]]; then + macdeployqt_args=("${app_dir}" -always-overwrite -no-plugins -verbose=0) + if "${macdeployqt_executable}" -help 2>&1 | grep -q -- "-no-codesign"; then + macdeployqt_args+=(-no-codesign) + fi + "${macdeployqt_executable}" "${macdeployqt_args[@]}" + + qt_plugin_root="" + qt_plugin_candidates=() + append_qt_plugin_candidate "${WINUAE_QT_PLUGIN_ROOT:-}" + + qt6_dir="$(cmake_cache_value Qt6_DIR)" + if [[ -n "${qt6_dir}" && -d "${qt6_dir}" ]]; then + qt_prefix="$(cd "${qt6_dir}/../../.." && pwd)" + append_qt_plugin_candidate "${qt_prefix}/plugins" + append_qt_plugin_candidate "${qt_prefix}/share/qt/plugins" + append_qt_plugin_candidate "${qt_prefix}/share/qt6/plugins" + if [[ -x "${qt_prefix}/bin/qtpaths" ]]; then + append_qt_plugin_candidate "$("${qt_prefix}/bin/qtpaths" --plugin-dir 2>/dev/null || true)" + fi + fi + + macdeployqt_dir="$(cd "$(dirname "${macdeployqt_executable}")" && pwd)" + if [[ -x "${macdeployqt_dir}/qtpaths" ]]; then + append_qt_plugin_candidate "$("${macdeployqt_dir}/qtpaths" --plugin-dir 2>/dev/null || true)" + fi + + for candidate in "${qt_plugin_candidates[@]}" \ + /opt/homebrew/share/qt/plugins \ + /opt/homebrew/opt/qt/share/qt/plugins \ + /opt/homebrew/opt/qt6/share/qt/plugins \ + /opt/homebrew/opt/qt@6/share/qt/plugins \ + /usr/local/share/qt/plugins \ + /usr/local/opt/qt/share/qt/plugins \ + /usr/local/opt/qt6/share/qt/plugins \ + /usr/local/opt/qt@6/share/qt/plugins + do + if [[ -n "${candidate}" && -f "${candidate}/platforms/libqcocoa.dylib" ]]; then + qt_plugin_root="${candidate}" + break + fi + done + + if [[ -n "${qt_plugin_root}" ]]; then + copy_qt_plugin() { + local relative="$1" + local source="${qt_plugin_root}/${relative}" + local target="${contents_dir}/PlugIns/${relative}" + if [[ -f "${source}" ]]; then + mkdir -p "$(dirname "${target}")" + cp "${source}" "${target}" + install_name_tool -add_rpath "@loader_path/../../Frameworks" "${target}" 2>/dev/null || true + fi + } + copy_qt_plugin "platforms/libqcocoa.dylib" + copy_qt_plugin "imageformats/libqico.dylib" + copy_qt_plugin "styles/libqmacstyle.dylib" + fi +fi + +copy_qemu_uae_plugin + +if [[ "${WINUAE_SKIP_MACOS_DEPLOYMENT_CHECK:-0}" != "1" ]]; then + "${script_dir}/macos-check-deployment-target.sh" "${app_dir}" "${deployment_target}" >&2 +fi + +if [[ "${WINUAE_SKIP_CODESIGN:-0}" != "1" ]] && command -v codesign >/dev/null 2>&1; then + codesign_identity="${WINUAE_CODESIGN_IDENTITY:--}" + codesign_args=(--force --deep --sign "${codesign_identity}") + split_extra_args "${WINUAE_CODESIGN_OPTIONS:-}" + codesign_args+=(${extra_args[@]+"${extra_args[@]}"}) + if [[ -n "${WINUAE_CODESIGN_ENTITLEMENTS:-}" ]]; then + codesign_args+=(--entitlements "${WINUAE_CODESIGN_ENTITLEMENTS}") + fi + codesign "${codesign_args[@]}" "${app_dir}" +fi + +echo "${app_dir}" diff --git a/tools/macos-check-deployment-target.sh b/tools/macos-check-deployment-target.sh new file mode 100755 index 00000000..560bbae2 --- /dev/null +++ b/tools/macos-check-deployment-target.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <&2 + exit 1 +fi +if [[ -z "${root}" || -z "${target}" ]]; then + usage >&2 + exit 1 +fi +if [[ ! -e "${root}" ]]; then + echo "error: path not found: ${root}" >&2 + exit 1 +fi + +version_gt() { + local a="$1" + local b="$2" + local ai bi + IFS=. read -r -a av <<< "${a}" + IFS=. read -r -a bv <<< "${b}" + for i in 0 1 2; do + ai="${av[$i]:-0}" + bi="${bv[$i]:-0}" + if ((10#${ai} > 10#${bi})); then + return 0 + fi + if ((10#${ai} < 10#${bi})); then + return 1 + fi + done + return 1 +} + +mach_o_minos() { + otool -l "$1" 2>/dev/null | awk ' + /LC_BUILD_VERSION/ { build = 1; old = 0; next } + build && /minos/ { print $2; exit } + /LC_VERSION_MIN_MACOSX/ { old = 1; build = 0; next } + old && /version/ { print $2; exit } + ' +} + +check_file() { + local file="$1" + local info minos + + info="$(file -b "${file}" 2>/dev/null || true)" + case "${info}" in + *Mach-O*) ;; + *) return 0 ;; + esac + + minos="$(mach_o_minos "${file}")" + if [[ -n "${minos}" ]] && version_gt "${minos}" "${target}"; then + echo "error: ${file} requires macOS ${minos}, newer than deployment target ${target}" >&2 + return 1 + fi + return 0 +} + +failed=0 +if [[ -f "${root}" ]]; then + check_file "${root}" || failed=1 +else + while IFS= read -r -d '' file_path; do + check_file "${file_path}" || failed=1 + done < <(find "${root}" -type f -print0) +fi + +if ((failed)); then + exit 1 +fi +echo "verified macOS deployment target ${target}: ${root}" diff --git a/tools/macos-dmg.sh b/tools/macos-dmg.sh new file mode 100755 index 00000000..b811a4a1 --- /dev/null +++ b/tools/macos-dmg.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat </package. + +Environment: + WINUAE_DMG_CODESIGN_IDENTITY codesign identity for the final DMG. + Defaults to WINUAE_CODESIGN_IDENTITY when set. + WINUAE_DMG_CODESIGN_OPTIONS Extra options passed to codesign for the DMG. + WINUAE_NOTARY_PROFILE notarytool keychain profile. When set, submit + the final DMG and staple the ticket. + WINUAE_SKIP_NOTARIZATION=1 Do not submit/staple even if a profile is set. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source_dir="$(cd "${script_dir}/.." && pwd)" +build_dir="${1:-${WINUAE_BUILD_DIR:-$(pwd)}}" +output_dir="${2:-${build_dir}/package}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS DMG creation requires Darwin/macOS" >&2 + exit 1 +fi + +major="$(awk '/^#define UAEMAJOR / { print $3; exit }' "${source_dir}/include/options.h")" +minor="$(awk '/^#define UAEMINOR / { print $3; exit }' "${source_dir}/include/options.h")" +revision="$(awk '/^#define UAESUBREV / { print $3; exit }' "${source_dir}/include/options.h")" +version="${major:-0}.${minor:-0}.${revision:-0}" + +app_dir="$("${script_dir}/macos-bundle.sh" "${build_dir}" "${output_dir}" | awk 'NF { line = $0 } END { print line }')" +staging_dir="${output_dir}/dmg-root" +volume_name="WinUAE" +rw_dmg="${output_dir}/WinUAE-${version}.rw.dmg" +final_dmg="${output_dir}/WinUAE-${version}.dmg" +mount_dir="" + +cleanup() { + if [[ -n "${mount_dir}" && -d "${mount_dir}" ]]; then + hdiutil detach "${mount_dir}" -quiet >/dev/null 2>&1 || true + rmdir "${mount_dir}" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +split_extra_args() { + if [[ -n "${1:-}" ]]; then + # Intentionally split user-provided extra flags the same way a shell would. + # shellcheck disable=SC2206 + extra_args=($1) + else + extra_args=() + fi +} + +rm -rf "${staging_dir}" "${rw_dmg}" "${final_dmg}" +mkdir -p "${staging_dir}/.background" +cp -R "${app_dir}" "${staging_dir}/WinUAE.app" +ln -s /Applications "${staging_dir}/Applications" + +volume_icon="${app_dir}/Contents/Resources/WinUAE.icns" +apply_volume_icon() { + local target_dir="$1" + + if [[ ! -f "${volume_icon}" ]]; then + return + fi + + cp "${volume_icon}" "${target_dir}/.VolumeIcon.icns" + if command -v SetFile >/dev/null 2>&1; then + SetFile -a C "${target_dir}" || true + SetFile -a V "${target_dir}/.VolumeIcon.icns" || true + fi +} + +apply_volume_icon "${staging_dir}" + +background_tiff="${staging_dir}/.background/background.tiff" +background_source="${source_dir}/od-unix/graphics/dmg_background.tiff" +if [[ ! -f "${background_source}" ]]; then + echo "error: missing DMG background: ${background_source}" >&2 + exit 1 +fi +cp "${background_source}" "${background_tiff}" + +hdiutil create -volname "${volume_name}" -srcfolder "${staging_dir}" -fs HFS+ -format UDRW -ov "${rw_dmg}" >/dev/null +mount_dir="$(hdiutil attach "${rw_dmg}" -readwrite -noverify -noautoopen | awk -F '\t' '/\/Volumes\// { print $NF; exit }')" +if [[ -z "${mount_dir}" || ! -d "${mount_dir}" ]]; then + echo "error: failed to mount ${rw_dmg}" >&2 + exit 1 +fi + +apply_volume_icon "${mount_dir}" + +require_finder_layout_records() { + local ds_store="$1" + local missing=0 + + for record in Iloc bwsp icvp; do + if ! LC_ALL=C grep -aq "${record}" "${ds_store}"; then + echo "error: Finder layout metadata is missing ${record} record in ${ds_store}" >&2 + missing=1 + fi + done + + return "${missing}" +} + +if ! command -v osascript >/dev/null 2>&1; then + echo "error: osascript is required to write the DMG Finder layout" >&2 + exit 1 +fi + +osascript </dev/null 2>&1; then + break + fi + sleep 0.5 +done +if [[ ! -f "${mount_dir}/.DS_Store" ]]; then + echo "error: Finder did not write ${mount_dir}/.DS_Store; DMG background layout was not saved" >&2 + exit 1 +fi +require_finder_layout_records "${mount_dir}/.DS_Store" + +apply_volume_icon "${mount_dir}" +if [[ -f "${volume_icon}" ]] && command -v SetFile >/dev/null 2>&1 && command -v GetFileInfo >/dev/null 2>&1; then + volume_attrs="$(GetFileInfo -a "${mount_dir}" 2>/dev/null || true)" + case "${volume_attrs}" in + *C*) ;; + *) + echo "error: custom volume icon attribute was not set on ${mount_dir}" >&2 + exit 1 + ;; + esac +fi + +sync +hdiutil detach "${mount_dir}" -quiet +mount_dir="" +hdiutil convert "${rw_dmg}" -format UDZO -imagekey zlib-level=9 -o "${final_dmg}" -ov >/dev/null + +dmg_codesign_identity="${WINUAE_DMG_CODESIGN_IDENTITY:-${WINUAE_CODESIGN_IDENTITY:-}}" +if [[ -n "${dmg_codesign_identity}" && "${dmg_codesign_identity}" != "-" ]] && command -v codesign >/dev/null 2>&1; then + dmg_codesign_args=(--force --sign "${dmg_codesign_identity}") + split_extra_args "${WINUAE_DMG_CODESIGN_OPTIONS:-}" + dmg_codesign_args+=(${extra_args[@]+"${extra_args[@]}"}) + codesign "${dmg_codesign_args[@]}" "${final_dmg}" +fi + +hdiutil verify "${final_dmg}" >/dev/null + +if [[ "${WINUAE_SKIP_NOTARIZATION:-0}" != "1" && -n "${WINUAE_NOTARY_PROFILE:-}" ]]; then + if ! command -v xcrun >/dev/null 2>&1; then + echo "error: notarization requires xcrun/notarytool" >&2 + exit 1 + fi + xcrun notarytool submit "${final_dmg}" --keychain-profile "${WINUAE_NOTARY_PROFILE}" --wait + xcrun stapler staple "${final_dmg}" +fi + +"${script_dir}/macos-verify-dmg.sh" "${final_dmg}" >/dev/null +rm -rf "${rw_dmg}" "${staging_dir}" + +echo "${final_dmg}" diff --git a/tools/macos-smoke-app.sh b/tools/macos-smoke-app.sh new file mode 100755 index 00000000..dd945952 --- /dev/null +++ b/tools/macos-smoke-app.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat </dev/null 2>&1 || true +on run argv + tell application id (item 1 of argv) to quit +end run +APPLESCRIPT + fi + if [[ -n "${smoke_home}" && -d "${smoke_home}" ]]; then + rm -rf "${smoke_home}" + fi + if [[ -n "${smoke_log}" && -f "${smoke_log}" ]]; then + rm -f "${smoke_log}" + fi + if [[ -n "${timeout_file}" && -f "${timeout_file}" ]]; then + rm -f "${timeout_file}" + fi +} +trap cleanup EXIT + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS app smoke testing requires Darwin/macOS" >&2 + exit 1 +fi + +if [[ -z "${app_path}" ]]; then + usage >&2 + exit 1 +fi + +if [[ -d "${app_path}/WinUAE.app" ]]; then + app_path="${app_path}/WinUAE.app" +fi + +executable="${app_path}/Contents/MacOS/WinUAE" +if [[ ! -x "${executable}" ]]; then + echo "error: app executable not found: ${executable}" >&2 + exit 1 +fi + +bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${app_path}/Contents/Info.plist" 2>/dev/null || true)" +if [[ -z "${bundle_id}" ]]; then + echo "error: CFBundleIdentifier missing from ${app_path}/Contents/Info.plist" >&2 + exit 1 +fi + +timeout="${WINUAE_MACOS_SMOKE_TIMEOUT:-20}" +smoke_home="$(mktemp -d -t winuae-app-smoke-home.XXXXXX)" +smoke_log="$(mktemp -t winuae-app-smoke-log.XXXXXX)" +timeout_file="$(mktemp -t winuae-app-smoke-timeout.XXXXXX)" +rm -f "${timeout_file}" + +open -n -F -W \ + --env "HOME=${smoke_home}" \ + --env "WINUAE_MACOS_APP_SMOKE=1" \ + -o "${smoke_log}" \ + --stderr "${smoke_log}" \ + "${app_path}" & +open_pid="$!" +launched=1 + +( + sleep "${timeout}" + if kill -0 "${open_pid}" >/dev/null 2>&1; then + : > "${timeout_file}" + osascript - "${bundle_id}" <<'APPLESCRIPT' >/dev/null 2>&1 || true +on run argv + tell application id (item 1 of argv) to quit +end run +APPLESCRIPT + fi +) & +watchdog_pid="$!" + +open_status=0 +wait "${open_pid}" || open_status="$?" +kill "${watchdog_pid}" >/dev/null 2>&1 || true +wait "${watchdog_pid}" >/dev/null 2>&1 || true + +if [[ -f "${timeout_file}" ]]; then + echo "error: timed out waiting for packaged Qt app smoke mode to finish" >&2 + sed -n '1,120p' "${smoke_log}" >&2 + exit 1 +fi +if [[ "${open_status}" != "0" ]]; then + echo "error: packaged app launch failed with status ${open_status}" >&2 + sed -n '1,120p' "${smoke_log}" >&2 + exit 1 +fi +if ! grep -q '^WINUAE_QT_SMOKE_WINDOW_VISIBLE$' "${smoke_log}"; then + echo "error: packaged app did not report a visible Qt configuration window" >&2 + sed -n '1,120p' "${smoke_log}" >&2 + exit 1 +fi + +echo "verified packaged Qt app launch: ${app_path}" diff --git a/tools/macos-verify-dmg.sh b/tools/macos-verify-dmg.sh new file mode 100755 index 00000000..575e5c0f --- /dev/null +++ b/tools/macos-verify-dmg.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat </dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: macOS DMG verification requires Darwin/macOS" >&2 + exit 1 +fi + +if [[ -z "${dmg_path}" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -f "${dmg_path}" ]]; then + echo "error: DMG not found: ${dmg_path}" >&2 + exit 1 +fi + +hdiutil verify "${dmg_path}" >/dev/null +mount_dir="$(hdiutil attach "${dmg_path}" -readonly -noverify -noautoopen | awk -F '\t' '/\/Volumes\// { print $NF; exit }')" +if [[ -z "${mount_dir}" || ! -d "${mount_dir}" ]]; then + echo "error: failed to mount ${dmg_path}" >&2 + exit 1 +fi + +app_dir="${mount_dir}/WinUAE.app" +info_plist="${app_dir}/Contents/Info.plist" + +require_path() { + local path="$1" + local description="$2" + if [[ ! -e "${path}" ]]; then + echo "error: missing ${description}: ${path}" >&2 + exit 1 + fi +} + +require_file() { + local path="$1" + local description="$2" + if [[ ! -f "${path}" ]]; then + echo "error: missing ${description}: ${path}" >&2 + exit 1 + fi +} + +require_path "${app_dir}" "app bundle" +require_file "${info_plist}" "Info.plist" +require_file "${app_dir}/Contents/MacOS/WinUAE" "bundle executable" +if [[ ! -x "${app_dir}/Contents/MacOS/WinUAE" ]]; then + echo "error: bundle executable is not executable: ${app_dir}/Contents/MacOS/WinUAE" >&2 + exit 1 +fi +require_file "${app_dir}/Contents/Resources/WinUAE.icns" "application icon" +require_file "${app_dir}/Contents/Resources/README_unix.md" "bundled README" + +if [[ ! -L "${mount_dir}/Applications" ]]; then + echo "error: missing /Applications symlink" >&2 + exit 1 +fi +if [[ "$(readlink "${mount_dir}/Applications")" != "/Applications" ]]; then + echo "error: Applications symlink does not point to /Applications" >&2 + exit 1 +fi + +require_file "${mount_dir}/.DS_Store" "Finder layout metadata" +require_file "${mount_dir}/.background/background.tiff" "Finder background image" +require_file "${mount_dir}/.VolumeIcon.icns" "volume icon" + +for record in Iloc bwsp icvp; do + if ! LC_ALL=C grep -aq "${record}" "${mount_dir}/.DS_Store"; then + echo "error: Finder layout metadata is missing ${record} record in ${mount_dir}/.DS_Store" >&2 + exit 1 + fi +done + +if command -v GetFileInfo >/dev/null 2>&1; then + volume_attrs="$(GetFileInfo -a "${mount_dir}" 2>/dev/null || true)" + case "${volume_attrs}" in + *C*) ;; + *) + echo "error: custom volume icon attribute is not set on ${mount_dir}" >&2 + exit 1 + ;; + esac +fi + +plist_get() { + /usr/libexec/PlistBuddy -c "Print $1" "${info_plist}" 2>/dev/null || true +} + +if [[ "$(plist_get ':CFBundleExecutable')" != "WinUAE" ]]; then + echo "error: CFBundleExecutable is not WinUAE" >&2 + exit 1 +fi +if [[ "$(plist_get ':CFBundleIconFile')" != "WinUAE.icns" ]]; then + echo "error: CFBundleIconFile is not WinUAE.icns" >&2 + exit 1 +fi +if [[ "$(plist_get ':CFBundleDocumentTypes:0:CFBundleTypeExtensions:0')" != "uae" ]]; then + echo "error: .uae document type is not registered in Info.plist" >&2 + exit 1 +fi + +launch_home="$(mktemp -d -t winuae-dmg-home.XXXXXX)" +launch_log="$(mktemp -t winuae-dmg-launch.XXXXXX)" +if ! HOME="${launch_home}" QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy "${app_dir}/Contents/MacOS/WinUAE" -h >"${launch_log}" 2>&1; then + echo "error: bundled executable did not start successfully from the mounted DMG" >&2 + sed -n '1,120p' "${launch_log}" >&2 + exit 1 +fi + +echo "verified ${dmg_path}"