mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:54:32 +00:00
- Moved prole-tools-app to prole-app at the project root to make it self-contained for transition to its own repository. - Created prole-tools-app/dist/ directory to host build artifacts. - Generated distribution artifacts (Prole Tools.app and Prole Tools.zip) using prole-app/build.sh package. - Checked in the generated artifacts to prole-tools-app/dist/ (bypassing .gitignore for temporary release process). Changes Summary • Renamed directory prole-tools-app/ to prole-app/. • Populated prole-tools-app/dist/ with the latest build output from prole-app/build.sh. • Staged all changes, including the forced addition of ignored artifacts in prole-tools-app/dist/.
685 lines
24 KiB
Bash
Executable File
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
|