prole/prole-app/build.sh
chrisfu ace9587611 feat(build): milestone alpha build — conditional deps; IRCKit and RoyalVNCKit disabled by default
This marks a milestone alpha build of Prole.

Key changes
- build.sh now supports conditional third‑party frameworks via env flags:
  - PROLE_ENABLE_IRCKit (default: 0)
  - PROLE_ENABLE_RoyalVNCKit (default: 0)
- When disabled, IRCKit/RoyalVNCKit are not fetched, linked, or embedded.
- Swift compile receives convenience defines for conditional compilation:
  - -D PROLE_ENABLE_IRCKit or -D PROLE_ENABLE_IRCKit_DISABLED
  - -D PROLE_ENABLE_RoyalVNCKit or -D PROLE_ENABLE_RoyalVNCKit_DISABLED
- Archive flow stabilized; optional SSL-less path for IRCKit remains available via PROLE_DISABLE_SSL=1.
- App bundle generation unchanged (plist, icons, resources, codesign).

Why
- Restore a clean build on current toolchains and allow incremental re‑enablement of optional integrations.
- Make the build resilient: dependencies can be toggled on CI or locally without modifying sources.

Usage examples
- Default (alpha baseline; both off):
  ./build.sh build --arch arm64
- Enable IRCKit only:
  PROLE_ENABLE_IRCKit=1 ./build.sh build --arch arm64
- Enable RoyalVNCKit only:
  PROLE_ENABLE_RoyalVNCKit=1 ./build.sh build --arch arm64
- Enable both:
  PROLE_ENABLE_IRCKit=1 PROLE_ENABLE_RoyalVNCKit=1 ./build.sh build --arch arm64

Notes
- IRCKit SSL-less switch remains available if needed:
  PROLE_DISABLE_SSL=1 PROLE_ENABLE_IRCKit=1 ./build.sh build --arch arm64
- Current alpha defaults keep both IRCKit and RoyalVNCKit disabled.

Build: 2025-12-05 14:21 local
2025-12-05 14:24:22 -08:00

826 lines
31 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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_IRCKIT="https://github.com/FuelRats/IRCKit/archive/refs/tags/0.16.0.tar.gz"
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:
This script can download, build, link, and embed IRCKit (0.16.0)
and RoyalVNCKit (1.0.1). These are optional and controlled by flags below.
Environment overrides (optional):
IRCKIT_XCFRAMEWORK If set, use this IRCKit.xcframework instead of building
ROYALVNCKIT_XCFRAMEWORK If set, use this RoyalVNCKit.xcframework instead of building
KEEP_DEPS=1 Keep the deps staging directory after the build (for debugging)
PROLE_DISABLE_SSL=1 Build IRCKit with SSL/TLS disabled (compilation condition); may also update deps to Swift‑6‑compatible NIO
PROLE_ENABLE_IRCKit=0 Enable building/linking IRCKit (0/1). Default: 0 (disabled)
PROLE_ENABLE_RoyalVNCKit=0 Enable building/linking RoyalVNCKit (0/1). Default: 0 (disabled)
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 animated tip GIF into app resources if it exists in repo
local gif_src="$PARENT_DIR/www/images/prole-type.gif"
if [[ -f "$gif_src" ]]; then
mkdir -p "$RESOURCES_DIR"
cp "$gif_src" "$RESOURCES_DIR/prole-type.gif"
echo "[resources] Copied prole-type.gif into Resources"
else
echo "[resources] prole-type.gif not found at $gif_src (skipping)"
fi
# Copy configured background image (ui.background) into Resources, keeping basename
local ui_bg
ui_bg="$(prop_get ui.background || true)"
if [[ -n "$ui_bg" && -f "$PARENT_DIR/$ui_bg" ]]; then
mkdir -p "$RESOURCES_DIR"
local base
base="$(basename "$ui_bg")"
cp "$PARENT_DIR/$ui_bg" "$RESOURCES_DIR/$base"
echo "[resources] Copied background image ($base) into Resources"
else
echo "[resources] ui.background not set or file missing (skipping)"
fi
# 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 (mandatory IRCKit & 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" == "IRCKit" ]]; then logfile="$DEPS_LOGS_DIR/irckit-archive-${arch}.log"; fi
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" == "IRCKit" && -n ${PROLE_DISABLE_SSL:-} ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: building IRCKit with PROLE_DISABLE_SSL compilation condition"
# Use a temporary xcconfig to avoid shell/argument parsing issues on the command line
local xcflags_file="$outdir/irckit-prole-disable-ssl.xcconfig"
# IMPORTANT: Keep this file strictly to valid xcconfig key/value lines (no comments or extra text)
cat > "$xcflags_file" <<'XCCONFIG'
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PROLE_DISABLE_SSL
OTHER_SWIFT_FLAGS = $(inherited) -DPROLE_DISABLE_SSL
SWIFT_VERSION = 5.0
SWIFT_STRICT_CONCURRENCY = minimal
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_irckit_xcframework() {
# If external override exists, trust it
if [[ -n ${IRCKIT_XCFRAMEWORK:-} && -d ${IRCKIT_XCFRAMEWORK} ]]; then
echo "[deps] Using provided IRCKit.xcframework: ${IRCKIT_XCFRAMEWORK}"
return 0
fi
local work="$DEPS_SRC_DIR/IRCKit-src"
local tar="$DEPS_DIR/irckit.tar.gz"
rm -rf "$work"; mkdir -p "$work"
fetch_tarball "$URL_IRCKIT" "$tar"
extract_tarball "$tar" "$work"
# Detect package or project root robustly
local src_root=""
# 1) If a Package.swift exists directly under work
if [[ -f "$work/Package.swift" ]]; then
src_root="$work"
fi
# 2) Else pick the first child directory that contains a Package.swift
if [[ -z "$src_root" ]]; then
for d in "$work"/*; do
[[ -d "$d" ]] || continue
if [[ -f "$d/Package.swift" ]]; then src_root="$d"; break; fi
done
fi
# 3) Else fall back to the first child directory (common for GitHub tarballs)
if [[ -z "$src_root" ]]; then
for d in "$work"/*; do
[[ -d "$d" ]] || continue
src_root="$d"; break
done
fi
if [[ -z "$src_root" ]]; then
echo "[deps] error: IRCKit source not found after extract" >&2
exit 1
fi
echo "[deps] IRCKit source root: $src_root"
pushd "$src_root" >/dev/null
# Ensure SwiftPM uses Swift 5 toolchain mode (avoid Swift 6 defaults under Xcode 16.x)
if have_cmd swift; then
echo "[deps] Forcing SwiftPM tools-version to 5.9 for IRCKit"
log_exec "$DEPS_LOGS_DIR/irckit-spm-tools-version.log" swift package tools-version --set 5.9 || true
fi
# Under SSL-less builds, strip NIOSSL from the graph and pin swift-nio to a Swift‑5–compatible release
if [[ -n ${PROLE_DISABLE_SSL:-} && -f "Package.swift" ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: patching IRCKit Package.swift to remove NIOSSL and pin swift-nio to 2.40.0 (exact)"
cp -f "Package.swift" "$DEPS_LOGS_DIR/irckit-Package.swift.before" 2>/dev/null || true
# 1) Remove swift-nio-ssl package declaration lines entirely
perl -0777 -pe 's|\s*\.package\(\s*url:\s*"https://github\.com/apple/swift-nio-ssl"\s*,\s*[^\)]*\),?\n||g' -i "Package.swift" 2>/dev/null || true
# 2) Remove NIOSSL from any target dependency arrays (handles product and target name forms)
perl -0777 -pe 's|("|\[)NIOSSL(\]|"),?\s*||g; s|\[\s*,\s*\]|[]|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\s*\.product\(name:\s*"NIOSSL"[^\)]*\),?\n||g' -i "Package.swift" 2>/dev/null || true
# 3) Pin swift-nio to an exact, Swift‑5–compatible version to avoid Swift‑6‑only codepaths
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*from:\s*"[^"]+"\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*\.upToNextMajor\(from:\s*"[^"]+"\)\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*\.exact\([^\)]*\)\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
# 3b) Optionally pin swift-atomics to a Swift‑5–compatible release to help NIOConcurrencyHelpers
if ! grep -q "swift-atomics" Package.swift 2>/dev/null; then
echo "[deps] PROLE_DISABLE_SSL=1: adding swift-atomics .exact(\"1.0.2\") to Package.swift to stabilize NIOConcurrencyHelpers"
perl -0777 -pe 's|(dependencies\s*:\s*\[)|$1\n .package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2")),|s' -i "Package.swift" 2>/dev/null || true
else
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-atomics"\s*,\s*from:\s*"[^"]+"\s*\)|.package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-atomics"\s*,\s*\.upToNextMajor\(from:\s*"[^"]+"\)\s*\)|.package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2"))|g' -i "Package.swift" 2>/dev/null || true
fi
# 4) Ensure the package explicitly targets Swift 5 language mode if not already present
if ! grep -q "swiftLanguageVersions" Package.swift 2>/dev/null; then
echo "[deps] PROLE_DISABLE_SSL=1: injecting swiftLanguageVersions = [.v5] into Package.swift"
# Try to insert after the targets array before the closing paren of Package initializer
perl -0777 -pe 's|(targets\s*:\s*\[[\s\S]*?\])\s*\)\s*$|$1,\n swiftLanguageVersions: [.v5]\n)\n|s' -i "Package.swift" 2>/dev/null || true
fi
cp -f "Package.swift" "$DEPS_LOGS_DIR/irckit-Package.swift.after" 2>/dev/null || true
fi
# If SSL is disabled, avoid updating to latest; just resolve with our pinned/stripped graph
if have_cmd swift; then
if [[ -n ${PROLE_DISABLE_SSL:-} ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: resolving IRCKit SwiftPM dependencies (no update)"
log_exec "$DEPS_LOGS_DIR/irckit-spm-resolve.log" swift package resolve || true
else
echo "[deps] Resolving IRCKit SwiftPM dependencies as pinned (no update)"
log_exec "$DEPS_LOGS_DIR/irckit-spm-resolve.log" swift package resolve || true
fi
# Best-effort diagnostics of the resolved graph
log_exec "$DEPS_LOGS_DIR/irckit-spm-show-deps.log" swift package show-dependencies || true
if [[ -f .swiftpm/resolved/Package.resolved ]]; then
cp -f .swiftpm/resolved/Package.resolved "$DEPS_LOGS_DIR/irckit-Package.resolved" || true
fi
fi
# Note: Avoid surgical source patches; rely on version pinning per user request
local build_tmp="$DEPS_OUT_DIR/IRCKit-build"
rm -rf "$build_tmp"; mkdir -p "$build_tmp"
local have_arm=0; local have_x86=0
# Determine which architectures to build for dependencies
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
if xc_archive_one "$PWD" IRCKit arm64 "$build_tmp"; then have_arm=1; fi
else
if xc_archive_one "$PWD" IRCKit x86_64 "$build_tmp"; then have_x86=1; fi
fi
done
local out_xc="$DEPS_OUT_DIR/IRCKit.xcframework"
rm -rf "$out_xc"
if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-arm64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-framework "$build_tmp/IRCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-arm64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
elif [[ $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
else
echo "[deps] error: failed to archive IRCKit for any macOS arch" >&2
exit 1
fi
export IRCKIT_XCFRAMEWORK="$out_xc"
echo "[deps] Built IRCKit.xcframework at $IRCKIT_XCFRAMEWORK"
popd >/dev/null
}
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 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
if xc_archive_one "$PWD" RoyalVNCKit arm64 "$build_tmp"; then have_arm=1; fi
else
if xc_archive_one "$PWD" RoyalVNCKit x86_64 "$build_tmp"; 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 "$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \
-framework "$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \
-output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \
-output "$out_xc"
elif [[ $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \
-output "$out_xc"
else
echo "[deps] error: failed to archive RoyalVNCKit for any macOS arch" >&2
exit 1
fi
export ROYALVNCKIT_XCFRAMEWORK="$out_xc"
echo "[deps] Built RoyalVNCKit.xcframework at $ROYALVNCKIT_XCFRAMEWORK"
popd >/dev/null
}
prepare_dependencies() {
ensure_dirs
local want_irc="${PROLE_ENABLE_IRCKit:-0}"
local want_vnc="${PROLE_ENABLE_RoyalVNCKit:-0}"
echo "[deps] Preparing frameworks — IRCKit: ${want_irc} (0=off,1=on), RoyalVNCKit: ${want_vnc} (0=off,1=on)"
if [[ "$want_irc" = "1" ]]; then
build_irckit_xcframework
if [[ ! -d "${IRCKIT_XCFRAMEWORK:-}" ]]; then
echo "[deps] error: IRCKit.xcframework is missing" >&2; exit 1; fi
else
echo "[deps] IRCKit disabled (PROLE_ENABLE_IRCKit=0); skipping build"
IRCKIT_XCFRAMEWORK=""
fi
if [[ "$want_vnc" = "1" ]]; then
build_royalvnc_xcframework
if [[ ! -d "${ROYALVNCKIT_XCFRAMEWORK:-}" ]]; then
echo "[deps] error: RoyalVNCKit.xcframework is missing" >&2; exit 1; fi
else
echo "[deps] RoyalVNCKit disabled (PROLE_ENABLE_RoyalVNCKit=0); skipping build"
ROYALVNCKIT_XCFRAMEWORK=""
fi
}
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
}
swiftc_compile() {
local arch="$1"; shift
local out="$1"; shift
# Map arch to -target
local target="${arch}-apple-macosx${MIN_MACOS}"
echo "[swiftc] Building for ${arch} -> ${out}"
# Optional linking of frameworks based on flags
local want_irc="${PROLE_ENABLE_IRCKit:-0}"
local want_vnc="${PROLE_ENABLE_RoyalVNCKit:-0}"
local irc_flags=()
local vnc_flags=()
local swift_cfg_flags=()
if [[ "$want_irc" = "1" && -n "${IRCKIT_XCFRAMEWORK:-}" && -d "${IRCKIT_XCFRAMEWORK}" ]]; then
irc_flags=( -F "${IRCKIT_XCFRAMEWORK}" -framework IRCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" )
swift_cfg_flags+=( -D PROLE_ENABLE_IRCKit )
echo "[swiftc] Linking IRCKit from ${IRCKIT_XCFRAMEWORK}"
else
echo "[swiftc] IRCKit disabled or not present; not linking"
# Define a convenience symbol for conditional compilation if app code uses it
swift_cfg_flags+=( -D PROLE_ENABLE_IRCKit_DISABLED )
fi
if [[ "$want_vnc" = "1" && -n "${ROYALVNCKIT_XCFRAMEWORK:-}" && -d "${ROYALVNCKIT_XCFRAMEWORK}" ]]; then
vnc_flags=( -F "${ROYALVNCKIT_XCFRAMEWORK}" -framework RoyalVNCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" )
swift_cfg_flags+=( -D PROLE_ENABLE_RoyalVNCKit )
echo "[swiftc] Linking RoyalVNCKit from ${ROYALVNCKIT_XCFRAMEWORK}"
else
echo "[swiftc] RoyalVNCKit disabled or not present; not linking"
swift_cfg_flags+=( -D PROLE_ENABLE_RoyalVNCKit_DISABLED )
fi
# shellcheck disable=SC2046
log_exec "$LOG_DIR/swiftc-${arch}.log" xcrun swiftc \
-target "$target" \
-O \
-sdk "$(xcrun --show-sdk-path --sdk macosx)" \
-framework AppKit \
-framework Carbon \
-framework Network \
${swift_cfg_flags[@]:-} \
${irc_flags[@]:-} \
${vnc_flags[@]:-} \
$(/usr/bin/find "$SRC_DIR" -name "*.swift" | sort) \
-o "$out"
}
codesign_app() {
echo "[codesign] Ad-hoc signing ${APP_DIR}"
xcrun codesign --force --deep -s - "$APP_DIR"
}
embed_irckit_framework() {
if [[ "${PROLE_ENABLE_IRCKit:-0}" != "1" ]]; then
return 0
fi
# Embed IRCKit.xcframework slice into Contents/Frameworks if available
if [[ -z "${IRCKIT_XCFRAMEWORK:-}" ]]; then
return 0
fi
if [[ ! -d "${IRCKIT_XCFRAMEWORK}" ]]; then
echo "[embed] IRCKIT_XCFRAMEWORK path not found: ${IRCKIT_XCFRAMEWORK}"
return 0
fi
local slice=""
# Prefer universal slice if available; otherwise match host arch
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework"
else
case "$ARCH_CURRENT" in
arm64)
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-arm64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-arm64/IRCKit.framework"
fi
;;
x86_64)
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-x86_64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-x86_64/IRCKit.framework"
fi
;;
esac
fi
if [[ -z "$slice" ]]; then
echo "[embed] Could not find matching IRCKit.framework slice in xcframework"
return 0
fi
local dest="$CONTENTS_DIR/Frameworks/IRCKit.framework"
rm -rf "$dest"
echo "[embed] Embedding IRCKit.framework slice: $slice"
cp -R "$slice" "$dest"
# Sign the embedded framework
xcrun codesign --force -s - "$dest"
}
embed_royalvnckit_framework() {
if [[ "${PROLE_ENABLE_RoyalVNCKit:-0}" != "1" ]]; then
return 0
fi
# Embed RoyalVNCKit.xcframework slice into Contents/Frameworks if available
if [[ -z "${ROYALVNCKIT_XCFRAMEWORK:-}" ]]; then
return 0
fi
if [[ ! -d "${ROYALVNCKIT_XCFRAMEWORK}" ]]; then
echo "[embed] ROYALVNCKIT_XCFRAMEWORK path not found: ${ROYALVNCKIT_XCFRAMEWORK}"
return 0
fi
local slice=""
# Prefer universal slice if present; otherwise pick matching arch
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64_x86_64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64_x86_64/RoyalVNCKit.framework"
else
case "$ARCH_CURRENT" in
arm64)
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64/RoyalVNCKit.framework"
fi
;;
x86_64)
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-x86_64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-x86_64/RoyalVNCKit.framework"
fi
;;
esac
fi
if [[ -z "$slice" ]]; then
echo "[embed] Could not find matching RoyalVNCKit.framework slice in xcframework"
return 0
fi
local dest="$CONTENTS_DIR/Frameworks/RoyalVNCKit.framework"
rm -rf "$dest"
echo "[embed] Embedding RoyalVNCKit.framework slice: $slice"
cp -R "$slice" "$dest"
# Sign the embedded framework
xcrun codesign --force -s - "$dest"
}
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 bin="$BUILD_DIR/${APP_NAME}-${arch}"
swiftc_compile "$arch" "$bin"
mkdir -p "$MACOS_DIR"
cp "$bin" "$MACOS_DIR/${APP_NAME}"
embed_irckit_framework
embed_royalvnckit_framework
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:"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -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 bin_arm64="$BUILD_DIR/${APP_NAME}-arm64"
local bin_x86="$BUILD_DIR/${APP_NAME}-x86_64"
swiftc_compile arm64 "$bin_arm64"
swiftc_compile x86_64 "$bin_x86"
mkdir -p "$MACOS_DIR"
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$bin_arm64" "$bin_x86"
embed_irckit_framework
embed_royalvnckit_framework
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:"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -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