#!/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" < CFBundleDisplayName Living Village 测试版 CFBundleExecutable LivingVillage CFBundleIdentifier com.livingvillage.desktop.test CFBundleName Living Village CFBundlePackageType APPL CFBundleShortVersionString 0.1.0-test CFBundleVersion $STAMP CFBundleSupportedPlatforms MacOSX LSMinimumSystemVersion 11.0 LSArchitecturePriority $architecture NSHighResolutionCapable 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" < "$OUTPUT_ROOT/README.md" <&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" for asset in cjk-glyph-atlas.png cjk-glyphs.txt; do if [[ ! -s "$app_dir/Contents/MacOS/Assets/$asset" ]]; then printf 'BLOCKED: required CJK asset is missing for %s: %s\n' "$rid" "$app_dir/Contents/MacOS/Assets/$asset" >&2 exit 1 fi done mapfile -t cjk_manifest_lines < "$app_dir/Contents/MacOS/Assets/cjk-glyphs.txt" if [[ "${#cjk_manifest_lines[@]}" -ne 188 ]]; then printf 'BLOCKED: packaged CJK manifest for %s contains %s lines; expected 188\n' "$rid" "${#cjk_manifest_lines[@]}" >&2 exit 1 fi for cjk_manifest_line in "${cjk_manifest_lines[@]}"; do if [[ ${#cjk_manifest_line} -ne 1 ]]; then printf 'BLOCKED: packaged CJK manifest for %s contains a line that is not one character\n' "$rid" >&2 exit 1 fi done 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