summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xscripts/macos-package.sh339
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