prole/prole-app/build.sh
chrisfu 64455fa886 ### Milestone: Remove IRCKit, migrate to swift-nio IRC, enable RoyalVNC via SwiftPM, and fix runtime embedding
- Replaced all IRCKit (0.16) usage with NozeIO `swift-nio-irc-client` (SwiftPM).
- Enabled IRC window with new client: connects and joins `#prole` (plain TCP for now; SSL checkbox displays a notice).
- Enabled RoyalVNC and removed all optional/flag-based handling. Integrated RoyalVNC via SwiftPM (`royalvnc` on `main`).
- Updated VNC UI to current RoyalVNC API: `VNCConnection` + `VNCCAFramebufferView` with a strong delegate reference.
- Simplified build to use SwiftPM for both IRC and RoyalVNC.
- Fixed runtime embedding and loader issues:
  - Copy SwiftPM-built `.dylib` products (e.g., `libRoyalVNCKit.dylib`) into `Contents/Frameworks` and codesign them.
  - Added `@executable_path/../Frameworks` to app rpaths with `install_name_tool`.
- Removed manual RoyalVNCKit `.xcframework` building/embedding and any IRCKit traces.

#### Key changes
- `prole-app/build.sh`:
  - Build app via SwiftPM; removed IRCKit and manual RoyalVNC build logic.
  - Embed SwiftPM `.dylib` outputs into `Contents/Frameworks` and set app rpath.
  - Cleaned usage text; dependencies now handled by SwiftPM.
- `prole-app/Package.swift`:
  - Add `swift-nio-irc-client` (NozeIO) dependency.
  - Add RoyalVNC via SwiftPM: `https://github.com/royalapplications/royalvnc` on `main`.
- `prole-app/Sources/IRCWindowController.swift`:
  - Migrate to NozeIO `IRC` package API; re-enable Connect; join `#prole`.
  - Import AppKit; provide convenience init for transcript view.
- `prole-app/Sources/WorkstationWindowController.swift`:
  - Import `RoyalVNCKit`; use `VNCConnection` + `VNCCAFramebufferView`.
  - Implement `VNCConnectionDelegate` and keep a strong reference to the delegate.
- `prole-app/debug.sh`:
  - Removed IRCKit logs section; kept RoyalVNC logs earlier; then removed manual RoyalVNC altogether.

This build now launches successfully (no dyld errors), opens the Workstation window (RoyalVNC), and the IRC window connects via the new client.

### Suggested follow-ups
- If TLS is required for IRC, add `NIOSSL` integration and wire SSL checkbox to TLS connection.
- Optionally remove leftover Vendor references if any local cache remains.
2025-12-05 23:37:54 -08:00

622 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Prole build script — builds a macOS .app bundle without launching Xcode
# Requirements: Xcode Command Line Tools (swiftc, codesign, plutil, lipo, xcodebuild)
APP_NAME="Prole"
BUNDLE_ID="org.prole.${APP_NAME}"
MIN_MACOS="12.0"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
SRC_DIR="$ROOT_DIR/Sources"
BUILD_DIR="$ROOT_DIR/.build-cli"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/${APP_NAME}.app"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
INFO_PLIST="$CONTENTS_DIR/Info.plist"
PARENT_DIR="$(cd "$ROOT_DIR/.." && pwd)"
LOG_DIR="$BUILD_DIR/logs"
ARCH_CURRENT="$(uname -m)" # arm64 or x86_64
# Mandatory dependency sources (auto-fetched)
URL_ROYALVNC="https://github.com/royalapplications/royalvnc/archive/refs/tags/1.0.1.tar.gz"
# Deps staging directories
DEPS_DIR="$BUILD_DIR/deps"
DEPS_SRC_DIR="$DEPS_DIR/src"
DEPS_OUT_DIR="$DEPS_DIR/out"
DEPS_LOGS_DIR="$DEPS_OUT_DIR/logs"
usage() {
cat <<EOF
Usage: $(basename "$0") [command] [options]
Commands:
build Build ${APP_NAME}.app for the current architecture (default)
build-universal Build a universal (arm64+x86_64) ${APP_NAME}.app
clean Remove build artifacts
run Build (if needed) and run the app
debug Build (if needed) and run in foreground with verbose logs
package Zip the built app into dist/${APP_NAME}.zip
Options (for build):
--arch <arch> Build for a specific arch (arm64 or x86_64). Defaults to host arch.
Examples:
./build.sh build
./build.sh build --arch arm64
./build.sh build-universal
./build.sh run
./build.sh package
Dependencies:
IRC and RoyalVNC are built via Swift Package Manager.
Environment overrides (optional):
ROYALVNCKIT_XCFRAMEWORK If set, use this RoyalVNCKit.xcframework instead of building
KEEP_DEPS=1 Keep the deps staging directory after the build (for debugging)
EOF
}
ensure_dirs() {
mkdir -p "$BUILD_DIR" "$DIST_DIR" "$MACOS_DIR" "$RESOURCES_DIR" "$CONTENTS_DIR/Frameworks" "$DEPS_SRC_DIR" "$DEPS_OUT_DIR" "$LOG_DIR" "$DEPS_LOGS_DIR"
}
# Read a key from prole.properties (very simple parser)
prop_get() {
local key="$1"
local file="$ROOT_DIR/prole.properties"
if [[ -f "$file" ]]; then
local line
line=$(grep -E "^${key}=" "$file" | tail -n1 || true)
if [[ -n "$line" ]]; then
echo "${line#*=}"
return 0
fi
fi
return 1
}
gen_statusbar_icon_png() {
# Generates a small monochrome template PNG for the status bar (18x18)
local out_png="$RESOURCES_DIR/statusIcon.png"
# Render a simple 'P' using AppKit to avoid extra deps
/usr/bin/env xcrun swift -F /System/Library/PrivateFrameworks - <<'SWIFT' "$out_png"
import AppKit
import Foundation
let args = CommandLine.arguments
guard args.count > 1 else { exit(2) }
let path = args[1]
let size = NSSize(width: 18, height: 18)
let img = NSImage(size: size)
img.lockFocus()
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedSystemFont(ofSize: 14, weight: .bold),
.foregroundColor: NSColor.labelColor,
.paragraphStyle: paragraph
]
let s = NSString(string: "P")
let rect = NSRect(x: 0, y: -2, width: size.width, height: size.height)
s.draw(in: rect, withAttributes: attrs)
img.unlockFocus()
guard let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:]) else {
exit(3)
}
try! png.write(to: URL(fileURLWithPath: path))
SWIFT
# Mark as template via extended attribute for system tinting (optional)
}
gen_app_icns() {
# Create an .icns for the app. Prefer a configured source image (ui.icon),
# otherwise fall back to generating a bold 'P'.
local icon_name="${APP_NAME}"
local iconset_dir="$BUILD_DIR/${icon_name}.iconset"
rm -rf "$iconset_dir"
mkdir -p "$iconset_dir"
local base_png="$BUILD_DIR/${icon_name}_1024.png"
# Try configured ui.icon relative to repo root
local ui_icon
ui_icon="$(prop_get ui.icon || true)"
if [[ -n "$ui_icon" && -f "$PARENT_DIR/$ui_icon" ]]; then
# Use the provided icon image as base; if not already 1024x1024, sips will resize for each size
cp "$PARENT_DIR/$ui_icon" "$base_png"
else
# Generate a 1024 base PNG using AppKit so we don't depend on ImageMagick/Pillow
/usr/bin/env xcrun swift -F /System/Library/PrivateFrameworks - <<'SWIFT' "$base_png"
import AppKit
import Foundation
let args = CommandLine.arguments
guard args.count > 1 else { exit(2) }
let outPath = args[1]
let size = NSSize(width: 1024, height: 1024)
let img = NSImage(size: size)
img.lockFocus()
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
// Draw a rounded rect background to make it look like an app icon
let bgRect = NSRect(x: 0, y: 0, width: 1024, height: 1024)
let radius: CGFloat = 220
let roundRectPath = NSBezierPath(roundedRect: bgRect, xRadius: radius, yRadius: radius)
NSColor.windowBackgroundColor.setFill()
roundRectPath.fill()
// Draw the letter 'P' centered
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedSystemFont(ofSize: 720, weight: .black),
.foregroundColor: NSColor.labelColor,
.paragraphStyle: paragraph
]
let s = NSString(string: "P")
let rect = NSRect(x: 0, y: 70, width: 1024, height: 820)
s.draw(in: rect, withAttributes: attrs)
img.unlockFocus()
guard let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:]) else {
exit(3)
}
try! png.write(to: URL(fileURLWithPath: outPath))
SWIFT
fi
# Create all iconset sizes from the base image
for s in 16 32 128 256 512; do
/usr/bin/sips -z "$s" "$s" "$base_png" --out "$iconset_dir/icon_${s}x${s}.png" >/dev/null
/usr/bin/sips -z "$((s*2))" "$((s*2))" "$base_png" --out "$iconset_dir/icon_${s}x${s}@2x.png" >/dev/null
done
# Build icns
/usr/bin/iconutil -c icns "$iconset_dir" -o "$RESOURCES_DIR/${icon_name}.icns"
}
gen_plist() {
cat > "$INFO_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleExecutable</key><string>${APP_NAME}</string>
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>${APP_NAME}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>${MIN_MACOS}</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSPrincipalClass</key><string>NSApplication</string>
<key>LSApplicationCategoryType</key><string>public.app-category.developer-tools</string>
<!-- UIElement must be false so the app can present a normal window and app menu when in Application Window mode. -->
<key>LSUIElement</key><false/>
<key>CFBundleIconFile</key><string>${APP_NAME}.icns</string>
</dict>
</plist>
PLIST
}
copy_extra_resources() {
# Copy default properties file if present
local props_src="$ROOT_DIR/prole.properties"
if [[ -f "$props_src" ]]; then
cp "$props_src" "$RESOURCES_DIR/prole.properties"
echo "[resources] Copied prole.properties into Resources"
else
echo "[resources] prole.properties not found at $props_src (skipping)"
fi
}
# --- Dependency preparation (RoyalVNCKit) ---
have_cmd() { command -v "$1" >/dev/null 2>&1; }
fetch_tarball() {
local url="$1"; local out_tar="$2"
echo "[deps] Fetching: $url"
if have_cmd wget; then
wget -q -O "$out_tar" "$url"
else
curl -LsSf -o "$out_tar" "$url"
fi
}
extract_tarball() {
local tarpath="$1"; local destdir="$2"
mkdir -p "$destdir"
tar -xzf "$tarpath" -C "$destdir"
}
# Run a command and tee stdout/stderr to a logfile, returning the command's exit status
log_exec() {
local logfile="$1"; shift
mkdir -p "$(dirname "$logfile")"
echo "[log] → $logfile"
"$@" 2>&1 | tee "$logfile"
return ${PIPESTATUS[0]}
}
xc_archive_one() {
local proj_dir="$1"; local scheme="$2"; local arch="$3"; local outdir="$4"
local archive_path="$outdir/${scheme}-macos-${arch}.xcarchive"
echo "[xcodebuild] archive scheme=$scheme arch=$arch"
local proj_opt=()
# If an explicit Xcode project matching the scheme exists, use it (rare for SwiftPM)
if [[ -d "$proj_dir/${scheme}.xcodeproj" ]]; then
proj_opt=( -project "${scheme}.xcodeproj" )
fi
local logfile="$LOG_DIR/xcode-archive-${scheme}-${arch}.log"
if [[ "$scheme" == "RoyalVNCKit" ]]; then logfile="$DEPS_LOGS_DIR/royalvnc-archive-${arch}.log"; fi
pushd "$proj_dir" >/dev/null || return 1
# Per-scheme extra flags / xcconfig
local extra_args=()
if [[ "$scheme" == "RoyalVNCKit" ]]; then
# Enable distribution interfaces so SPM can import the Swift module, but disable verification to avoid toolchain issues
local xcflags_file="$outdir/royalvnc-archive-overrides.xcconfig"
cat > "$xcflags_file" <<'XCCONFIG'
BUILD_LIBRARY_FOR_DISTRIBUTION = YES
SWIFT_EMIT_MODULE_INTERFACE = YES
SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
OTHER_SWIFT_FLAGS = $(inherited) -no-verify-emitted-module-interface
XCCONFIG
extra_args+=( -xcconfig "$xcflags_file" )
fi
log_exec "$logfile" xcodebuild archive \
-scheme "$scheme" \
-destination 'generic/platform=macOS' \
-configuration Release \
-archivePath "$archive_path" \
-sdk macosx \
-disableAutomaticPackageResolution \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES ARCHS="$arch" ONLY_ACTIVE_ARCH=NO \
SWIFT_VERSION=5.0 \
SWIFT_STRICT_CONCURRENCY=minimal \
MACOSX_DEPLOYMENT_TARGET="${MIN_MACOS}" \
${extra_args[@]:-} \
${proj_opt[@]:-} \
|| { popd >/dev/null; return 1; }
popd >/dev/null
}
# Build one framework w/ xcodebuild (not archive), outputting a .framework into outdir
xc_build_framework_one() {
local proj_dir="$1"; local scheme="$2"; local arch="$3"; local outdir="$4"; local product_name="$5"
local build_dir="$outdir/${scheme}-macos-${arch}-build"
mkdir -p "$build_dir"
local logfile="$DEPS_LOGS_DIR/${scheme}-build-${arch}.log"
pushd "$proj_dir" >/dev/null || return 1
# Prepare xcconfig to disable interface emission/verification
local xcflags_file="$outdir/${scheme}-build-overrides.${arch}.xcconfig"
cat > "$xcflags_file" <<'XCCONFIG'
// Emit module interfaces suitable for distribution so the Swift module can be imported by SPM
BUILD_LIBRARY_FOR_DISTRIBUTION = YES
SWIFT_EMIT_MODULE_INTERFACE = YES
SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
// But do not verify emitted module interfaces to avoid toolchain-specific failures
OTHER_SWIFT_FLAGS = $(inherited) -no-verify-emitted-module-interface
XCCONFIG
# Important: write all xcodebuild output to the log to keep stdout clean (we echo only the path below)
xcodebuild build \
-scheme "$scheme" \
-destination 'generic/platform=macOS' \
-configuration Release \
-sdk macosx \
ARCHS="$arch" ONLY_ACTIVE_ARCH=NO \
SWIFT_VERSION=5.0 \
SWIFT_STRICT_CONCURRENCY=minimal \
MACOSX_DEPLOYMENT_TARGET="${MIN_MACOS}" \
CONFIGURATION_BUILD_DIR="$build_dir" \
-xcconfig "$xcflags_file" \
>"$logfile" 2>&1 || { popd >/dev/null; return 1; }
popd >/dev/null
# Accept both plain build dir and SwiftPM PackageFrameworks location
if [[ -d "$build_dir/${product_name}.framework" ]]; then
echo "$build_dir/${product_name}.framework"
return 0
fi
if [[ -d "$build_dir/PackageFrameworks/${product_name}.framework" ]]; then
echo "$build_dir/PackageFrameworks/${product_name}.framework"
return 0
fi
return 1
}
build_royalvnc_xcframework() {
if [[ -n ${ROYALVNCKIT_XCFRAMEWORK:-} && -d ${ROYALVNCKIT_XCFRAMEWORK} ]]; then
echo "[deps] Using provided RoyalVNCKit.xcframework: ${ROYALVNCKIT_XCFRAMEWORK}"
return 0
fi
local work="$DEPS_SRC_DIR/RoyalVNC-src"
local tar="$DEPS_DIR/royalvnc.tar.gz"
rm -rf "$work"; mkdir -p "$work"
fetch_tarball "$URL_ROYALVNC" "$tar"
extract_tarball "$tar" "$work"
local src_root
src_root="$(find "$work" -maxdepth 1 -type d -name 'royalvnc-*' | head -n1)"
if [[ -z "$src_root" ]]; then
echo "[deps] error: RoyalVNC source not found after extract" >&2
exit 1
fi
pushd "$src_root" >/dev/null
local build_tmp="$DEPS_OUT_DIR/RoyalVNC-build"
rm -rf "$build_tmp"; mkdir -p "$build_tmp"
local have_arm=0; local have_x86=0
local fw_arm=""; local fw_x86=""
local want_arches=(arm64 x86_64)
if [[ "${PROLE_BUILD_ARCH:-}" == "arm64" ]]; then
want_arches=(arm64)
elif [[ "${PROLE_BUILD_ARCH:-}" == "x86_64" ]]; then
want_arches=(x86_64)
fi
for dep_arch in "${want_arches[@]}"; do
if [[ "$dep_arch" == "arm64" ]]; then
# Prefer archive to produce proper Swift module interfaces; fall back to build if archive fails
if xc_archive_one "$PWD" RoyalVNCKit arm64 "$build_tmp"; then
fw_arm="$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework"
have_arm=1
elif fw_arm=$(xc_build_framework_one "$PWD" RoyalVNCKit arm64 "$build_tmp" RoyalVNCKit); then
have_arm=1
fi
else
if xc_archive_one "$PWD" RoyalVNCKit x86_64 "$build_tmp"; then
fw_x86="$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework"
have_x86=1
elif fw_x86=$(xc_build_framework_one "$PWD" RoyalVNCKit x86_64 "$build_tmp" RoyalVNCKit); then
have_x86=1
fi
fi
done
local out_xc="$DEPS_OUT_DIR/RoyalVNCKit.xcframework"
rm -rf "$out_xc"
if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$fw_arm" \
-framework "$fw_x86" \
-output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$fw_arm" \
-output "$out_xc"
elif [[ $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$fw_x86" \
-output "$out_xc"
else
echo "[deps] error: failed to build RoyalVNCKit for any macOS arch" >&2
exit 1
fi
export ROYALVNCKIT_XCFRAMEWORK="$out_xc"
echo "[deps] Built RoyalVNCKit.xcframework at $ROYALVNCKIT_XCFRAMEWORK"
# Provide a stable path within the package for SwiftPM binaryTarget resolution
local vendor_dir="$ROOT_DIR/Vendor"
mkdir -p "$vendor_dir"
rm -rf "$vendor_dir/RoyalVNCKit.xcframework"
cp -R "$ROYALVNCKIT_XCFRAMEWORK" "$vendor_dir/"
echo "[deps] Mirrored RoyalVNCKit.xcframework to $vendor_dir"
popd >/dev/null
}
prepare_dependencies() {
ensure_dirs
echo "[deps] (deprecated) RoyalVNCKit manual build not required; managed by SwiftPM"
}
cleanup_dependencies() {
if [[ "${KEEP_DEPS:-0}" = "1" ]]; then
echo "[deps] KEEP_DEPS=1 set; preserving $DEPS_DIR"
else
rm -rf "$DEPS_DIR"
echo "[deps] Cleaned deps staging directory"
fi
}
# Build via Swift Package Manager and return path to built binary on stdout
spm_build_binary() {
local arch="$1"
echo "[spm] Building (Release) for arch=${arch}" >&2
# ROOT_DIR already points to prole-app; build from there so Package.swift is visible.
pushd "$ROOT_DIR" >/dev/null || return 1
mkdir -p "$LOG_DIR"
# Build via SwiftPM (Package.swift in prole-app). All dependencies managed by SPM.
local args=( build -c release --arch "$arch" \
-Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \
)
# Run swift build quietly; write all output to the log to avoid contaminating stdout
swift "${args[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; }
popd >/dev/null
local candidate1="$ROOT_DIR/.build/${arch}-apple-macosx/release/${APP_NAME}"
local candidate2="$ROOT_DIR/.build/release/${APP_NAME}"
if [[ -x "$candidate1" ]]; then
echo "$candidate1"; return 0
fi
if [[ -x "$candidate2" ]]; then
echo "$candidate2"; return 0
fi
return 1
}
codesign_app() {
echo "[codesign] Ad-hoc signing ${APP_DIR}"
xcrun codesign --force --deep -s - "$APP_DIR"
}
# Copy SwiftPM-produced dynamic libraries (e.g., libRoyalVNCKit.dylib) into the app bundle
embed_spm_dylibs() {
local arch="$1"
local spm_lib_dir="$ROOT_DIR/.build/${arch}-apple-macosx/release"
if [[ ! -d "$spm_lib_dir" ]]; then
spm_lib_dir="$ROOT_DIR/.build/release"
fi
mkdir -p "$CONTENTS_DIR/Frameworks"
local found=0
if compgen -G "$spm_lib_dir/*.dylib" > /dev/null; then
for dyl in "$spm_lib_dir"/*.dylib; do
found=1
echo "[embed] Copying $(basename "$dyl") into Frameworks"
cp -f "$dyl" "$CONTENTS_DIR/Frameworks/"
done
fi
if [[ $found -eq 1 ]]; then
echo "[embed] Codesigning embedded dylibs"
# Sign all copied dylibs
find "$CONTENTS_DIR/Frameworks" -name "*.dylib" -print0 | while IFS= read -r -d '' f; do
xcrun codesign --force -s - "$f"
done
fi
}
build_one_arch() {
local arch="$1"
# Default clean at the start of every build
clean || true
# Constrain dependency builds to the requested architecture to avoid toolchain incompatibilities
export PROLE_BUILD_ARCH="$arch"
prepare_dependencies
ensure_dirs
gen_plist
gen_app_icns
gen_statusbar_icon_png
copy_extra_resources
local built_bin
built_bin=$(spm_build_binary "$arch") || { echo "[spm] build failed" >&2; exit 1; }
mkdir -p "$MACOS_DIR"
cp "$built_bin" "$MACOS_DIR/${APP_NAME}"
# Embed any SwiftPM dynamic libs (e.g., RoyalVNCKit) into the app bundle
embed_spm_dylibs "$arch"
# Ensure the app binary can locate embedded dylibs in Contents/Frameworks at runtime
echo "[rpath] Adding @executable_path/../Frameworks to app binary rpaths"
xcrun install_name_tool -add_rpath "@executable_path/../Frameworks" "$MACOS_DIR/${APP_NAME}" 2>/dev/null || true
codesign_app
echo "Built: $APP_DIR"
echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)"
echo "[summary] Embedded frameworks (.framework):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies
}
build_universal() {
# Default clean at the start of every universal build
clean || true
# Build dependencies for both arches in universal build
unset PROLE_BUILD_ARCH || true
prepare_dependencies
ensure_dirs
gen_plist
local built_arm64; built_arm64=$(spm_build_binary arm64) || { echo "[spm] arm64 build failed" >&2; exit 1; }
local built_x86; built_x86=$(spm_build_binary x86_64) || { echo "[spm] x86_64 build failed" >&2; exit 1; }
mkdir -p "$MACOS_DIR"
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$built_arm64" "$built_x86"
codesign_app
echo "Built universal: $APP_DIR"
echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)"
echo "[summary] Embedded frameworks (.framework):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies
}
clean() {
rm -rf "$BUILD_DIR" "$DIST_DIR"
echo "Cleaned build artifacts."
}
run_app() {
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
echo "[run] Launching ${APP_NAME}.app"
# Launch app in background without activating it; return immediately.
# Then confirm it is running and print its PID.
open -gn "$APP_DIR"
# Wait briefly for the app to start and obtain PID
for i in {1..20}; do
PID=$(pgrep -x "$APP_NAME" || true)
if [ -n "${PID:-}" ]; then
echo "[run] ${APP_NAME} is running (pid: $PID)."
return 0
fi
sleep 0.1
done
echo "[run] Warning: could not confirm ${APP_NAME} is running. Check Console/Activity Monitor." >&2
}
package_app() {
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
local zip="$DIST_DIR/${APP_NAME}.zip"
(cd "$DIST_DIR" && /usr/bin/zip -qry "$(basename "$zip")" "$(basename "$APP_DIR")")
echo "Packaged: $zip"
}
cmd="${1:-build}"
shift || true
case "$cmd" in
-h|--help|help)
usage ;;
clean)
clean ;;
build)
arch="${ARCH_CURRENT}"
while [ $# -gt 0 ]; do
case "$1" in
--arch) arch="$2"; shift 2 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
build_one_arch "$arch" ;;
build-universal)
build_universal ;;
run)
run_app ;;
debug)
# Build if needed, then run the binary directly in the foreground
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
BIN="$MACOS_DIR/${APP_NAME}"
if [ ! -x "$BIN" ]; then
echo "[debug] error: binary not found at $BIN" >&2
exit 1
fi
echo "[debug] Running ${APP_NAME} in foreground with verbose logs"
echo "[debug] Press Ctrl+C to terminate. To quit from UI, use the context menu or Cmd+Q."
export PROLESTATUS_DEBUG=1
"$BIN"
status=$?
echo "[debug] ${APP_NAME} exited with status ${status}"
exit $status
;;
package)
package_app ;;
*)
echo "Unknown command: $cmd"; usage; exit 1 ;;
esac