prole/prole-tools-app/build.sh
chrisfu 0e01595de0 refactor: switch to environment wrapper; standardize env.sh usage and improve $PROLE_HOME management
- Replace direct script invocation with `$PROLE_HOME/env.sh` wrapper for consistent environment setup across all components.
- Update runtime processes to dynamically resolve `$PROLE_HOME` or fallback to `$HOME/.prole`.
- Deprecate `init-port-forwards.sh` script bundling; remove from LaunchAgentManager and build system.
- Overhaul `env.sh` generation to include execution capability, customizable paths, and improved error handling.
- Standardize app name and paths to "Prole Tools" across all files and UI elements.
- Adjust build output to align with new structure, including executable naming and resource packaging.
2025-12-15 22:00:32 -08:00

685 lines
24 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)
# Human-friendly app bundle name (can contain spaces)
APP_NAME="Prole Tools"
# SwiftPM product (executable) name as defined in Package.swift -> products/executable(name: ...)
# This must match the actual built binary filename produced by SwiftPM.
EXECUTABLE_NAME="Prole"
# Bundle identifiers cannot contain spaces — keep a stable reverse-DNS id
BUNDLE_ID="org.prole.ProleTools"
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.
--verbose Print verbose build commands (same as PROLE_VERBOSE=1)
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)
PROLE_VERBOSE=1 Stream verbose output from SwiftPM/xcodebuild and echo commands
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"
}
# Determine verbosity from env/flags
VERBOSE="${PROLE_VERBOSE:-0}"
if [[ "$VERBOSE" = "1" ]]; then
# Enable shell tracing for more insight
set -x
fi
# Resolve a SwiftPM-capable command (prefer xcrun swift on macOS)
resolve_swiftpm() {
if command -v xcrun >/dev/null 2>&1 && xcrun swift --version >/dev/null 2>&1; then
echo "xcrun swift"
return 0
fi
if command -v swift >/dev/null 2>&1; then
if swift build --help >/dev/null 2>&1; then
echo "swift"
return 0
fi
fi
# Fallback to package form via xcrun if available
if command -v xcrun >/dev/null 2>&1 && xcrun swift package --help >/dev/null 2>&1; then
echo "xcrun swift"
return 0
fi
return 1
}
SWIFTPM_CMD=$(resolve_swiftpm || true)
if [[ -z "${SWIFTPM_CMD:-}" ]]; then
echo "[spm] error: Swift Package Manager not found. Install Xcode Command Line Tools: xcode-select --install" >&2
xcodebuild -version 2>/dev/null || true
swift --version 2>/dev/null || true
exit 1
fi
echo "[spm] Using SwiftPM: ${SWIFTPM_CMD}"
# 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.
# Prepare argument arrays to avoid word-splitting issues
local spm_verbose_flag=()
if [[ "$VERBOSE" = "1" ]]; then
spm_verbose_flag=( -v )
fi
local build_flags=( -c release --arch "$arch" \
-Xlinker -rpath -Xlinker "@executable_path/../Frameworks" )
# Turn SWIFTPM_CMD (e.g., "xcrun swift") into an array
local -a SWIFTPM_ARR
# shellcheck disable=SC2206
SWIFTPM_ARR=( $SWIFTPM_CMD )
# If verbose, stream output to stdout and tee to log; otherwise, write only to log
if [[ "$VERBOSE" = "1" ]]; then
echo "[spm] Executing: ${SWIFTPM_CMD} build ${spm_verbose_flag[*]} ${build_flags[*]}"
"${SWIFTPM_ARR[@]}" build "${spm_verbose_flag[@]}" "${build_flags[@]}" 2>&1 | tee "$LOG_DIR/spm-build-${arch}.log"
local rc=${PIPESTATUS[0]}
if [[ $rc -ne 0 ]]; then popd >/dev/null; return $rc; fi
else
"${SWIFTPM_ARR[@]}" build "${build_flags[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; }
fi
popd >/dev/null
# Note: SwiftPM output binary name equals EXECUTABLE_NAME (not APP_NAME)
local candidate1="$ROOT_DIR/.build/${arch}-apple-macosx/release/${EXECUTABLE_NAME}"
local candidate2="$ROOT_DIR/.build/release/${EXECUTABLE_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" | tail -n1) || { 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 | tail -n1) || { echo "[spm] arm64 build failed" >&2; exit 1; }
local built_x86; built_x86=$(spm_build_binary x86_64 | tail -n1) || { 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}"
# Allow per-invocation verbose flag
while [ $# -gt 0 ]; do
case "$1" in
--arch) arch="$2"; shift 2 ;;
--verbose) VERBOSE="1"; shift ;;
*) 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