diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-20 18:02:24 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-20 18:02:24 +0800 |
| commit | a7136abc6ceb37d3eb354e7647ec4ce25e6d7e01 (patch) | |
| tree | 1b45630e5525704a15fdcd572058ddcbc7601335 /scripts/macos-package.sh | |
| parent | 0b4306938f75430ed1f44c5eb3bf4585cdfa9a9b (diff) | |
| download | living-village-a7136abc6ceb37d3eb354e7647ec4ce25e6d7e01.tar.gz | |
chore(macos): add self-contained packaging script
[Change Nature]
This commit adds release packaging maintenance for the macOS test build.
[Maintenance Work]
- Publish osx-arm64 and osx-x64 as self-contained applications.
- Bundle launchers, Info.plist, native dependencies, verification evidence, and ZIP checksums.
- Document unsigned, unnotarized usage and per-application Gatekeeper handling.
[Implementation]
- Reject missing apphosts or incompatible Mach-O/native dylib architectures.
- Generate reproducible timestamped release directories without overwriting prior output.
- Create DMGs only when hdiutil is available on the build host.
[Impact]
- Adds scripts/macos-package.sh only.
- Does not modify game source, M3 acceptance state, logs, or existing worktree changes.
Diffstat (limited to 'scripts/macos-package.sh')
| -rwxr-xr-x | scripts/macos-package.sh | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/scripts/macos-package.sh b/scripts/macos-package.sh new file mode 100755 index 0000000..10c80b8 --- /dev/null +++ b/scripts/macos-package.sh @@ -0,0 +1,339 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +export LC_ALL=C + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" +PROJECT="$REPO_ROOT/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj" +STAMP="${MACOS_TIMESTAMP:-$(date -u +%Y%m%dT%H%M%SZ)}" +OUTPUT_ROOT="$REPO_ROOT/artifacts/releases/macos/$STAMP" +VERIFICATION_ROOT="$OUTPUT_ROOT/verification" +SOURCE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || printf 'unknown')" + +if [[ -e "$OUTPUT_ROOT" ]]; then + printf 'Refusing to overwrite existing macOS release directory: %s\n' "$OUTPUT_ROOT" >&2 + exit 2 +fi + +for command_name in dotnet file zip unzip; do + if ! command -v "$command_name" >/dev/null 2>&1; then + printf 'Required command is missing: %s\n' "$command_name" >&2 + exit 127 + fi +done + +if [[ ! -f "$PROJECT" ]]; then + printf 'Desktop project not found: %s\n' "$PROJECT" >&2 + exit 2 +fi + +mkdir -p "$VERIFICATION_ROOT" + +file_size() { + if stat -c '%s' "$1" >/dev/null 2>&1; then + stat -c '%s' "$1" + else + stat -f '%z' "$1" + fi +} + +sha256() { + local output + if command -v sha256sum >/dev/null 2>&1; then + output="$(sha256sum "$1")" + else + output="$(shasum -a 256 "$1")" + fi + printf '%s\n' "${output%% *}" +} + +write_launcher() { + local launcher_path="$1" + cat > "$launcher_path" <<'EOF' +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +BUNDLE_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" + +if [ -z "${LV_SAVE_PATH:-}" ]; then + if [ -n "${HOME:-}" ]; then + SAVE_DIR="$HOME/Library/Application Support/Living Village" + mkdir -p "$SAVE_DIR" + export LV_SAVE_PATH="$SAVE_DIR/living-village.save" + else + export LV_SAVE_PATH="$BUNDLE_DIR/living-village.save" + fi +elif [ "${LV_SAVE_PATH#/}" = "$LV_SAVE_PATH" ]; then + export LV_SAVE_PATH="$BUNDLE_DIR/$LV_SAVE_PATH" +fi + +export DYLD_LIBRARY_PATH="$SCRIPT_DIR${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" +cd "$SCRIPT_DIR" +exec "$SCRIPT_DIR/LivingVillage.Desktop" "$@" +EOF + chmod +x "$launcher_path" +} + +write_info_plist() { + local plist_path="$1" + local rid="$2" + local bundle_name="$3" + local architecture="$4" + cat > "$plist_path" <<EOF +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDisplayName</key> + <string>Living Village 测试版</string> + <key>CFBundleExecutable</key> + <string>LivingVillage</string> + <key>CFBundleIdentifier</key> + <string>com.livingvillage.desktop.test</string> + <key>CFBundleName</key> + <string>Living Village</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>0.1.0-test</string> + <key>CFBundleVersion</key> + <string>$STAMP</string> + <key>CFBundleSupportedPlatforms</key> + <array> + <string>MacOSX</string> + </array> + <key>LSMinimumSystemVersion</key> + <string>11.0</string> + <key>LSArchitecturePriority</key> + <array> + <string>$architecture</string> + </array> + <key>NSHighResolutionCapable</key> + <true/> +</dict> +</plist> +EOF + printf '%s\n' "bundle=$bundle_name" "rid=$rid" > /dev/null +} + +write_package_info() { + local info_path="$1" + local rid="$2" + local bundle_name="$3" + cat > "$info_path" <<EOF +package=Living Village macOS test build +created_utc=$STAMP +source_commit=$SOURCE_COMMIT +runtime_mode=self-contained +rid=$rid +entrypoint=$bundle_name/Contents/MacOS/LivingVillage +native_desktopgl_dependencies=bundled SDL2 and OpenAL universal dylibs +signed=false +notarized=false +game_acceptance=long-term acceptance incomplete; test build only +EOF +} + +write_readme() { + cat > "$OUTPUT_ROOT/README.md" <<EOF +# Living Village macOS 测试版 + +本目录是从 Linux 主机交叉生成的 macOS DesktopGL self-contained 测试版,不是最终游戏验收结论。M3 长期批验仍独立进行;本包没有在 macOS 实机启动验证,也没有声称签名或公证。 + +## 选择架构 + +- Apple Silicon(M1/M2/M3/M4 等):使用 LivingVillage-osx-arm64.zip。 +- Intel Mac:使用 LivingVillage-osx-x64.zip。 +- 每个 ZIP 内含一个可拖入 /Applications 的 .app。包内包含 .NET runtime、MonoGame SDL2/OpenAL native dylib,不依赖用户另装 dotnet。 + +## 首次打开 + +包未签名、未公证。解压后将对应 .app 拖入“应用程序”,首次打开时在 Finder 中右键该单个应用并选择“打开”,再确认“打开”;若系统显示“无法打开”,先尝试启动一次,再到“系统设置 → 隐私与安全性”对该应用选择“仍要打开”。 + +不要关闭 Gatekeeper,也不要使用全局禁用安全检查的命令。 + +## 存档与结构 + +- 启动器位于 Contents/MacOS/LivingVillage,会定位自身目录后启动同目录的 self-contained apphost。 +- 默认存档位于 ~/Library/Application Support/Living Village/living-village.save;可用绝对路径覆盖 LV_SAVE_PATH,相对路径相对 app bundle 根目录解析。 +- Contents/Info.plist、Contents/MacOS、Contents/Resources 均包含在包中;SDL2/OpenAL dylib 会随发布文件放在可执行文件同目录。 + +## 证据 + +- PACKAGE-INFO.txt:source commit、RID、self-contained、签名/公证状态。 +- SHA256SUMS.txt:ZIP 的 SHA-256 与字节数。 +- verification/:dotnet publish 日志、Mach-O 架构检查和 ZIP 完整性检查。 +- Linux 主机没有 hdiutil 时不生成 DMG;若生成,DMG 也不代表签名或公证。 + +构建时间:$STAMP +源 HEAD:$SOURCE_COMMIT +EOF +} + +write_readme +cp "$REPO_ROOT/docs/assets-and-licenses.md" "$OUTPUT_ROOT/ASSETS-AND-LICENSES.md" + +declare -a ZIP_PATHS=() +declare -a DMG_STATUS=() + +for rid in osx-arm64 osx-x64; do + case "$rid" in + osx-arm64) + architecture="arm64" + ;; + osx-x64) + architecture="x86_64" + ;; + *) + printf 'Unsupported RID: %s\n' "$rid" >&2 + exit 2 + ;; + esac + + app_name="LivingVillage-$rid.app" + app_dir="$OUTPUT_ROOT/$app_name" + work_dir="$OUTPUT_ROOT/.work/$rid" + publish_dir="$work_dir/publish" + publish_log="$VERIFICATION_ROOT/publish-$rid.log" + mach_o_report="$VERIFICATION_ROOT/mach-o-$rid.txt" + zip_path="$OUTPUT_ROOT/LivingVillage-$rid.zip" + + mkdir -p "$publish_dir" + printf 'Publishing %s self-contained...\n' "$rid" + if ! dotnet publish "$PROJECT" \ + --configuration Release \ + --runtime "$rid" \ + --self-contained true \ + --output "$publish_dir" \ + --nologo \ + -p:UseAppHost=true \ + -p:DebugType=None \ + -p:DebugSymbols=false \ + -p:PublishSingleFile=false \ + > "$publish_log" 2>&1; then + printf 'BLOCKED: dotnet publish failed for %s; see %s\n' "$rid" "$publish_log" >&2 + exit 1 + fi + + executable="$publish_dir/LivingVillage.Desktop" + if [[ ! -f "$executable" ]]; then + printf 'BLOCKED: self-contained apphost missing for %s: %s\n' "$rid" "$executable" >&2 + exit 1 + fi + chmod +x "$executable" + + executable_info="$(file -b "$executable")" + printf 'executable=%s\n' "$executable_info" > "$mach_o_report" + if [[ "$executable_info" != *"Mach-O"* || "$executable_info" != *"$architecture"* ]]; then + printf 'BLOCKED: apphost for %s is not the expected Mach-O architecture: %s\n' "$rid" "$executable_info" >&2 + exit 1 + fi + + for native_name in libSDL2-2.0.0.dylib libopenal.dylib; do + native_path="$publish_dir/$native_name" + if [[ ! -f "$native_path" ]]; then + printf 'BLOCKED: required MonoGame native dependency is missing for %s: %s\n' "$rid" "$native_name" >&2 + exit 1 + fi + native_info="$(file -b "$native_path")" + printf '%s=%s\n' "$native_name" "$native_info" >> "$mach_o_report" + if [[ "$native_info" != *"Mach-O"* || "$native_info" != *"arm64"* || "$native_info" != *"x86_64"* ]]; then + printf 'BLOCKED: MonoGame native dependency lacks universal arm64+x86_64 Mach-O support: %s (%s)\n' "$native_path" "$native_info" >&2 + exit 1 + fi + done + + shopt -s nullglob + dylibs=("$publish_dir"/*.dylib) + shopt -u nullglob + if [[ "${#dylibs[@]}" -eq 0 ]]; then + printf 'BLOCKED: no macOS native dylib was published for %s\n' "$rid" >&2 + exit 1 + fi + for native_path in "${dylibs[@]}"; do + native_info="$(file -b "$native_path")" + printf 'native:%s=%s\n' "$(basename "$native_path")" "$native_info" >> "$mach_o_report" + if [[ "$native_info" != *"Mach-O"* || "$native_info" != *"$architecture"* ]]; then + printf 'BLOCKED: published native dylib has the wrong architecture for %s: %s (%s)\n' "$rid" "$native_path" "$native_info" >&2 + exit 1 + fi + done + + mkdir -p "$app_dir/Contents/MacOS" "$app_dir/Contents/Resources" + cp -R "$publish_dir"/. "$app_dir/Contents/MacOS/" + write_launcher "$app_dir/Contents/MacOS/LivingVillage" + write_info_plist "$app_dir/Contents/Info.plist" "$rid" "$app_name" "$architecture" + write_package_info "$app_dir/Contents/Resources/PACKAGE-INFO.txt" "$rid" "$app_name" + cp "$OUTPUT_ROOT/README.md" "$app_dir/Contents/Resources/README.md" + cp "$REPO_ROOT/docs/assets-and-licenses.md" "$app_dir/Contents/Resources/ASSETS-AND-LICENSES.md" + chmod +x "$app_dir/Contents/MacOS/LivingVillage" "$app_dir/Contents/MacOS/LivingVillage.Desktop" + + if ! file -b "$app_dir/Contents/MacOS/LivingVillage.Desktop" >> "$mach_o_report"; then + printf 'BLOCKED: failed to inspect bundled apphost for %s\n' "$rid" >&2 + exit 1 + fi + + package_info="$OUTPUT_ROOT/$app_name/Contents/Resources/PACKAGE-INFO.txt" + cp "$package_info" "$OUTPUT_ROOT/PACKAGE-INFO-$rid.txt" + + printf 'Zipping %s...\n' "$app_name" + ( + cd "$OUTPUT_ROOT" + zip -q -r -X -y "$zip_path" "$app_name" + ) + if ! unzip -tqq "$zip_path" > "$VERIFICATION_ROOT/zip-$rid.txt" 2>&1; then + printf 'BLOCKED: ZIP integrity check failed: %s\n' "$zip_path" >&2 + exit 1 + fi + ZIP_PATHS+=("$zip_path") + + dmg_path="$OUTPUT_ROOT/LivingVillage-$rid.dmg" + if command -v hdiutil >/dev/null 2>&1; then + if hdiutil create -quiet -volname "Living Village $rid" -srcfolder "$app_dir" -ov -format UDZO "$dmg_path"; then + DMG_STATUS+=("$rid=created:$dmg_path") + else + rm -f "$dmg_path" + DMG_STATUS+=("$rid=not-created:hdiutil-failed") + fi + else + DMG_STATUS+=("$rid=not-created:hdiutil-unavailable-on-build-host") + fi +done + +{ + printf 'path bytes sha256\n' + for zip_path in "${ZIP_PATHS[@]}"; do + printf '%s %s %s\n' \ + "${zip_path#"$OUTPUT_ROOT"/}" \ + "$(file_size "$zip_path")" \ + "$(sha256 "$zip_path")" + done +} > "$OUTPUT_ROOT/SHA256SUMS.txt" + +{ + printf 'release_root=%s\n' "$OUTPUT_ROOT" + printf 'source_commit=%s\n' "$SOURCE_COMMIT" + printf 'runtime_mode=self-contained\n' + printf 'signed=false\n' + printf 'notarized=false\n' + printf 'macos_hardware_verification=false\n' + for status in "${DMG_STATUS[@]}"; do + printf 'dmg_%s\n' "$status" + done +} > "$OUTPUT_ROOT/PACKAGE-INFO.txt" + +rm -rf "$OUTPUT_ROOT/.work" + +printf 'MACOS_RELEASE_ROOT=%s\n' "$OUTPUT_ROOT" +printf 'MACOS_SHA256_FILE=%s\n' "$OUTPUT_ROOT/SHA256SUMS.txt" +for zip_path in "${ZIP_PATHS[@]}"; do + printf 'MACOS_ZIP=%s bytes=%s sha256=%s\n' \ + "$zip_path" \ + "$(file_size "$zip_path")" \ + "$(sha256 "$zip_path")" +done +for status in "${DMG_STATUS[@]}"; do + printf 'MACOS_DMG=%s\n' "$status" +done |
