diff --git a/.github/workflows/cd-swift-lume.yml b/.github/workflows/cd-swift-lume.yml
index de57a860..571851cf 100644
--- a/.github/workflows/cd-swift-lume.yml
+++ b/.github/workflows/cd-swift-lume.yml
@@ -31,6 +31,8 @@ on:
required: true
DEVELOPER_NAME:
required: true
+ PROVISIONING_PROFILE_BASE64:
+ required: true
permissions:
contents: write
@@ -43,6 +45,7 @@ env:
TEAM_ID: ${{ secrets.TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
DEVELOPER_NAME: ${{ secrets.DEVELOPER_NAME }}
+ PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
jobs:
notarize:
@@ -137,6 +140,14 @@ jobs:
# Clean up certificate files
rm application.p12 installer.p12
+ - name: Install Provisioning Profile
+ env:
+ PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
+ run: |
+ echo "Installing provisioning profile..."
+ echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > libs/lume/resources/embedded.provisionprofile
+ echo "Provisioning profile installed successfully"
+
- name: Build and Notarize
id: build_notarize
env:
diff --git a/libs/lume/resources/Info.plist b/libs/lume/resources/Info.plist
new file mode 100644
index 00000000..e67e7d36
--- /dev/null
+++ b/libs/lume/resources/Info.plist
@@ -0,0 +1,25 @@
+
+
+
+
+ CFBundleIdentifier
+ com.trycua.lume
+ CFBundleExecutable
+ lume
+ CFBundleName
+ Lume
+ CFBundleVersion
+ __VERSION__
+ CFBundleShortVersionString
+ __VERSION__
+ CFBundlePackageType
+ APPL
+ CFBundleInfoDictionaryVersion
+ 6.0
+ LSMinimumSystemVersion
+ 14.0
+ LSUIElement
+
+
+
diff --git a/libs/lume/resources/lume.entitlements b/libs/lume/resources/lume.entitlements
index d7d0d6e8..dccbe21c 100644
--- a/libs/lume/resources/lume.entitlements
+++ b/libs/lume/resources/lume.entitlements
@@ -4,5 +4,7 @@
com.apple.security.virtualization
+ com.apple.vm.networking
+
diff --git a/libs/lume/scripts/build/build-release-notarized.sh b/libs/lume/scripts/build/build-release-notarized.sh
index 59ab5040..4a7e3a3d 100755
--- a/libs/lume/scripts/build/build-release-notarized.sh
+++ b/libs/lume/scripts/build/build-release-notarized.sh
@@ -64,23 +64,61 @@ log "normal" "Ensuring .release directory exists and is accessible"
log "essential" "Building release version..."
swift build -c release --product lume > /dev/null
-# Sign the binary with hardened runtime entitlements
-log "essential" "Signing binary with entitlements..."
+# --- Assemble .app bundle ---
+log "essential" "Assembling .app bundle..."
+
+APP_BUNDLE=".release/lume.app"
+rm -rf "$APP_BUNDLE"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+
+# Copy the binary into the bundle
+cp -f .build/release/lume "$APP_BUNDLE/Contents/MacOS/lume"
+
+# Copy resource bundle (contains unattended presets) alongside the executable
+# so SPM's Bundle.module resolves correctly
+BUILD_BUNDLE=".build/release/lume_lume.bundle"
+if [ -d "$BUILD_BUNDLE" ]; then
+ cp -rf "$BUILD_BUNDLE" "$APP_BUNDLE/Contents/MacOS/"
+fi
+
+# Stamp and copy Info.plist
+sed "s/__VERSION__/$VERSION/g" "./resources/Info.plist" > "$APP_BUNDLE/Contents/Info.plist"
+
+# Embed the provisioning profile
+PROVISION_PROFILE="./resources/embedded.provisionprofile"
+if [ -f "$PROVISION_PROFILE" ]; then
+ cp "$PROVISION_PROFILE" "$APP_BUNDLE/Contents/embedded.provisionprofile"
+else
+ log "error" "Error: embedded.provisionprofile not found at $PROVISION_PROFILE"
+ log "error" "The provisioning profile is required for the com.apple.vm.networking entitlement."
+ log "error" "Obtain one from the Apple Developer portal tied to bundle ID com.trycua.lume."
+ exit 1
+fi
+
+# --- Sign the .app bundle ---
+log "essential" "Signing .app bundle..."
+
+# Sign the binary inside the bundle first (with entitlements)
codesign --force --options runtime \
--entitlement ./resources/lume.entitlements \
--sign "$CERT_APPLICATION_NAME" \
- .build/release/lume 2> /dev/null
+ "$APP_BUNDLE/Contents/MacOS/lume" 2> /dev/null
-# Create a temporary directory for packaging
-TEMP_ROOT=$(mktemp -d)
-mkdir -p "$TEMP_ROOT/usr/local/bin"
-cp -f .build/release/lume "$TEMP_ROOT/usr/local/bin/"
+# Sign the outer bundle
+codesign --force --options runtime \
+ --sign "$CERT_APPLICATION_NAME" \
+ "$APP_BUNDLE" 2> /dev/null
-# Build the installer package
+# --- Package as .pkg installer ---
log "essential" "Building installer package..."
+
+TEMP_ROOT=$(mktemp -d)
+mkdir -p "$TEMP_ROOT/usr/local/share/lume"
+cp -R "$APP_BUNDLE" "$TEMP_ROOT/usr/local/share/lume/"
+
if ! pkgbuild --root "$TEMP_ROOT" \
--identifier "com.trycua.lume" \
- --version "1.0" \
+ --version "$VERSION" \
--install-location "/" \
--sign "$CERT_INSTALLER_NAME" \
./.release/lume.pkg; then
@@ -96,7 +134,7 @@ fi
log "essential" "Package created successfully"
-# Submit for notarization using stored credentials
+# --- Notarize ---
log "essential" "Submitting for notarization..."
if [ "$LOG_LEVEL" = "minimal" ] || [ "$LOG_LEVEL" = "none" ]; then
# Minimal output - capture ID but hide details
@@ -127,82 +165,45 @@ else
fi
fi
-# Staple the notarization ticket
-log "essential" "Stapling notarization ticket..."
+# Staple the notarization ticket to the .pkg
+log "essential" "Stapling notarization ticket to .pkg..."
if ! xcrun stapler staple ./.release/lume.pkg > /dev/null 2>&1; then
- log "error" "Failed to staple notarization ticket"
+ log "error" "Failed to staple notarization ticket to .pkg"
exit 1
fi
-# Create temporary directory for package extraction
-EXTRACT_ROOT=$(mktemp -d)
-PKG_PATH="$(pwd)/.release/lume.pkg"
-
-# Extract the pkg using xar
-cd "$EXTRACT_ROOT"
-xar -xf "$PKG_PATH" > /dev/null 2>&1
-
-# Verify Payload exists before proceeding
-if [ ! -f "Payload" ]; then
- log "error" "Error: Payload file not found after xar extraction"
- exit 1
+# Staple the notarization ticket to the .app bundle
+log "essential" "Stapling notarization ticket to .app bundle..."
+if ! xcrun stapler staple "$APP_BUNDLE" > /dev/null 2>&1; then
+ log "normal" "Note: Could not staple .app bundle directly (this is expected when notarizing via .pkg)"
fi
-# Create a directory for the extracted contents
-mkdir -p extracted
-cd extracted
-
-# Extract the Payload
-cat ../Payload | gunzip -dc | cpio -i > /dev/null 2>&1
-
-# Verify the binary exists
-if [ ! -f "usr/local/bin/lume" ]; then
- log "error" "Error: lume binary not found in expected location"
- exit 1
-fi
-
-# Get the release directory absolute path
-RELEASE_DIR="$(realpath "$(dirname "$PKG_PATH")")"
-log "normal" "Using release directory: $RELEASE_DIR"
-
-# Copy extracted lume to the release directory
-cp -f usr/local/bin/lume "$RELEASE_DIR/lume"
-
-# Copy the resource bundle (contains unattended presets) from the build directory
-BUILD_BUNDLE="$LUME_DIR/.build/release/lume_lume.bundle"
-if [ -d "$BUILD_BUNDLE" ]; then
- cp -rf "$BUILD_BUNDLE" "$RELEASE_DIR/"
-fi
-
-# Install to user-local bin directory (standard location)
-USER_BIN="$HOME/.local/bin"
-mkdir -p "$USER_BIN"
-cp -f "$RELEASE_DIR/lume" "$USER_BIN/lume"
-
-# Advise user to add to PATH if not present
-if ! echo "$PATH" | grep -q "$USER_BIN"; then
- log "normal" "[lume build] Note: $USER_BIN is not in your PATH. Add 'export PATH=\"$USER_BIN:\$PATH\"' to your shell profile."
-fi
+# --- Create release archives ---
# Get architecture and create OS identifier
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"
+RELEASE_DIR="$(cd .release && pwd)"
-# Create versioned archives of the package with OS identifier in the name
log "essential" "Creating archives in $RELEASE_DIR..."
cd "$RELEASE_DIR"
# Clean up any existing artifacts first to avoid conflicts
rm -f lume-*.tar.gz lume-*.pkg.tar.gz
+# Create a backward-compatible wrapper script at the tarball root
+cat > lume <<'WRAPPER_EOF'
+#!/bin/sh
+exec "$(dirname "$0")/lume.app/Contents/MacOS/lume" "$@"
+WRAPPER_EOF
+chmod +x lume
+
# Create version-specific archives
log "essential" "Creating version-specific archives (${VERSION})..."
-# Package the binary and resource bundle
-if [ -d "lume_lume.bundle" ]; then
- tar -czf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" lume lume_lume.bundle > /dev/null 2>&1
-else
- tar -czf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" lume > /dev/null 2>&1
-fi
+
+# Package the .app bundle and wrapper script
+tar -czf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" lume lume.app > /dev/null 2>&1
+
# Package the installer
tar -czf "lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" lume.pkg > /dev/null 2>&1
@@ -220,6 +221,5 @@ chmod 644 "$RELEASE_DIR"/*.tar.gz "$RELEASE_DIR"/*.pkg.tar.gz "$RELEASE_DIR"/che
# Clean up
rm -rf "$TEMP_ROOT"
-rm -rf "$EXTRACT_ROOT"
log "essential" "Build and packaging completed successfully."
diff --git a/libs/lume/scripts/build/build-release.sh b/libs/lume/scripts/build/build-release.sh
index 0f5d3f37..d7ed0e41 100755
--- a/libs/lume/scripts/build/build-release.sh
+++ b/libs/lume/scripts/build/build-release.sh
@@ -9,25 +9,56 @@ LUME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$LUME_DIR"
swift build -c release --product lume
-codesign --force --entitlement ./resources/lume.entitlements --sign - .build/release/lume
-mkdir -p ./.release
-cp -f .build/release/lume ./.release/lume
+# Assemble .app bundle
+APP_BUNDLE=".release/lume.app"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
-# Copy the resource bundle (contains unattended presets)
+cp -f .build/release/lume "$APP_BUNDLE/Contents/MacOS/lume"
+
+# Copy resource bundle alongside the executable for Bundle.module resolution
if [ -d ".build/release/lume_lume.bundle" ]; then
- cp -rf .build/release/lume_lume.bundle ./.release/
+ cp -rf .build/release/lume_lume.bundle "$APP_BUNDLE/Contents/MacOS/"
fi
+# Stamp Info.plist with version from VERSION file
+VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0")
+sed "s/__VERSION__/$VERSION/g" "./resources/Info.plist" > "$APP_BUNDLE/Contents/Info.plist"
+
+# Embed provisioning profile if available
+if [ -f "./resources/embedded.provisionprofile" ]; then
+ cp "./resources/embedded.provisionprofile" "$APP_BUNDLE/Contents/embedded.provisionprofile"
+fi
+
+# Ad-hoc sign the bundle
+codesign --force --entitlement ./resources/lume.entitlements --sign - "$APP_BUNDLE/Contents/MacOS/lume"
+codesign --force --sign - "$APP_BUNDLE"
+
+# Create wrapper script
+mkdir -p .release
+cat > .release/lume <<'WRAPPER_EOF'
+#!/bin/sh
+exec "$(dirname "$0")/lume.app/Contents/MacOS/lume" "$@"
+WRAPPER_EOF
+chmod +x .release/lume
+
# Install to user-local bin directory (standard location)
USER_BIN="$HOME/.local/bin"
-mkdir -p "$USER_BIN"
-cp -f ./.release/lume "$USER_BIN/lume"
+APP_INSTALL_DIR="$HOME/.local/share/lume"
-# Install the resource bundle alongside the binary
-if [ -d "./.release/lume_lume.bundle" ]; then
- cp -rf ./.release/lume_lume.bundle "$USER_BIN/"
-fi
+mkdir -p "$USER_BIN"
+mkdir -p "$APP_INSTALL_DIR"
+
+# Install .app bundle
+rm -rf "$APP_INSTALL_DIR/lume.app"
+cp -R ".release/lume.app" "$APP_INSTALL_DIR/"
+
+# Create wrapper script in bin directory
+cat > "$USER_BIN/lume" </dev/null 2>&1; then
- if [ "$USE_BRIDGED_ENTITLEMENT" = true ]; then
- echo "${YELLOW}Warning: binary did not launch with bridged entitlement; falling back to local-safe entitlements.${NORMAL}"
+ APP_BUNDLE="$BUILD_PATH/lume.app"
+ rm -rf "$APP_BUNDLE"
+ mkdir -p "$APP_BUNDLE/Contents/MacOS"
+
+ cp -f "$BUILD_PATH/lume" "$APP_BUNDLE/Contents/MacOS/lume"
+
+ # Copy resource bundle alongside the executable
+ if [ -d "$BUILD_PATH/lume_lume.bundle" ]; then
+ cp -rf "$BUILD_PATH/lume_lume.bundle" "$APP_BUNDLE/Contents/MacOS/"
+ fi
+
+ # Stamp Info.plist with version
+ CURRENT_VERSION=$("$BUILD_PATH/lume" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "0.0.0")
+ sed "s/__VERSION__/$CURRENT_VERSION/g" "$LUME_DIR/resources/Info.plist" > "$APP_BUNDLE/Contents/Info.plist"
+
+ # Embed provisioning profile if available
+ if [ -f "$LUME_DIR/resources/embedded.provisionprofile" ]; then
+ cp "$LUME_DIR/resources/embedded.provisionprofile" "$APP_BUNDLE/Contents/embedded.provisionprofile"
+ else
+ echo "${YELLOW}Warning: No provisioning profile found at $LUME_DIR/resources/embedded.provisionprofile${NORMAL}"
+ echo "${YELLOW}Bridged networking requires a provisioning profile from Apple Developer portal.${NORMAL}"
+ fi
+
+ # Sign the bundle
+ codesign --force --entitlements "$ENTITLEMENTS_FILE" --sign - "$APP_BUNDLE/Contents/MacOS/lume"
+ codesign --force --sign - "$APP_BUNDLE"
+
+ # Verify the signed binary can launch from the bundle
+ if "$APP_BUNDLE/Contents/MacOS/lume" --version >/dev/null 2>&1; then
+ USE_APP_BUNDLE=true
+ else
+ echo "${YELLOW}Warning: binary did not launch from .app bundle with bridged entitlement; falling back to standalone binary.${NORMAL}"
ENTITLEMENTS_FILE="$LUME_DIR/resources/lume.local.entitlements"
codesign --force --entitlements "$ENTITLEMENTS_FILE" --sign - "$BUILD_PATH/lume"
+ USE_APP_BUNDLE=false
fi
+ else
+ # Standard standalone binary (no .app bundle needed)
+ codesign --force --entitlements "$ENTITLEMENTS_FILE" --sign - "$BUILD_PATH/lume"
+
+ # Verify the signed binary can launch
+ if ! "$BUILD_PATH/lume" --version >/dev/null 2>&1; then
+ echo "${YELLOW}Warning: binary did not launch; this may indicate a signing issue.${NORMAL}"
+ fi
+
+ USE_APP_BUNDLE=false
fi
echo "${GREEN}Build complete!${NORMAL}"
@@ -149,19 +194,43 @@ install_binary() {
# Create install directory if it doesn't exist
mkdir -p "$INSTALL_DIR"
- # Copy the binary
- cp -f "$BUILD_PATH/lume" "$INSTALL_DIR/lume"
- chmod +x "$INSTALL_DIR/lume"
+ if [ "$USE_APP_BUNDLE" = true ]; then
+ # Install as .app bundle with wrapper script
+ mkdir -p "$APP_INSTALL_DIR"
+ rm -rf "$APP_INSTALL_DIR/lume.app"
+ cp -R "$BUILD_PATH/lume.app" "$APP_INSTALL_DIR/"
- # Copy the resource bundle if it exists (contains unattended presets)
- if [ -d "$BUILD_PATH/lume_lume.bundle" ]; then
+ # Remove old standalone binary if it's a Mach-O file (migration)
+ if [ -f "$INSTALL_DIR/lume" ] && file "$INSTALL_DIR/lume" | grep -q "Mach-O"; then
+ rm -f "$INSTALL_DIR/lume"
+ fi
rm -rf "$INSTALL_DIR/lume_lume.bundle"
- cp -rf "$BUILD_PATH/lume_lume.bundle" "$INSTALL_DIR/"
- echo "Resource bundle installed to ${BOLD}$INSTALL_DIR/lume_lume.bundle${NORMAL}"
- fi
- echo "${GREEN}Installation complete!${NORMAL}"
- echo "Lume has been installed to ${BOLD}$INSTALL_DIR/lume${NORMAL}"
+ # Create wrapper script
+ cat > "$INSTALL_DIR/lume" < "$INSTALL_DIR/lume" </dev/null || echo "$HOME/.local/bin/lume")
INSTALL_DIR=$(dirname "$LUME_BIN")
+APP_INSTALL_DIR="$HOME/.local/share/lume"
if [ ! -x "$LUME_BIN" ]; then
log "ERROR: lume binary not found at $LUME_BIN"
@@ -405,22 +438,41 @@ apply_update() {
# Stop the daemon before updating
launchctl unload "$HOME/Library/LaunchAgents/com.trycua.lume_daemon.plist" 2>/dev/null || true
- # Install new binary
- mv "$TEMP_DIR/lume" "$INSTALL_DIR/"
- chmod +x "$INSTALL_DIR/lume"
+ if [ -d "$TEMP_DIR/lume.app" ]; then
+ # New .app bundle format
+ mkdir -p "$APP_INSTALL_DIR"
+ rm -rf "$APP_INSTALL_DIR/lume.app"
+ mv "$TEMP_DIR/lume.app" "$APP_INSTALL_DIR/"
- # Install resource bundle if it exists (contains unattended presets)
- if [ -d "$TEMP_DIR/lume_lume.bundle" ]; then
+ # Ensure wrapper script exists
+ mkdir -p "$INSTALL_DIR"
+ cat > "$INSTALL_DIR/lume" </dev/null || true
- log "Successfully updated lume to version $LATEST_VERSION"
-
# Show macOS notification
osascript -e "display notification \"Updated to version $LATEST_VERSION\" with title \"Lume Updated\"" 2>/dev/null || true
else
@@ -532,8 +584,8 @@ main() {
rm -f "$WRAPPER_SCRIPT"
fi
- # Create the plist file - runs signed lume binary directly (no wrapper)
- # This ensures proper code signing identity shows in Login Items
+ # Create the plist file - runs lume via the wrapper script
+ # The wrapper delegates to lume.app/Contents/MacOS/lume
cat < "$PLIST_PATH"
diff --git a/libs/lume/scripts/uninstall.sh b/libs/lume/scripts/uninstall.sh
index 9f280bbc..e3c6302f 100755
--- a/libs/lume/scripts/uninstall.sh
+++ b/libs/lume/scripts/uninstall.sh
@@ -142,10 +142,25 @@ if [ -n "$LUME_BIN" ] && [ -f "$LUME_BIN" ]; then
rm -f "$INSTALL_DIR/lume-daemon"
echo " ${GREEN}Removed $INSTALL_DIR/lume-daemon${NORMAL}"
fi
+
+ # Remove legacy resource bundle if exists
+ if [ -d "$INSTALL_DIR/lume_lume.bundle" ]; then
+ rm -rf "$INSTALL_DIR/lume_lume.bundle"
+ echo " ${GREEN}Removed $INSTALL_DIR/lume_lume.bundle${NORMAL}"
+ fi
else
echo " ${YELLOW}Lume binary not found (skipped)${NORMAL}"
fi
+# Remove .app bundle if installed (new format)
+APP_INSTALL_DIR="$HOME/.local/share/lume"
+if [ -d "$APP_INSTALL_DIR/lume.app" ]; then
+ rm -rf "$APP_INSTALL_DIR/lume.app"
+ echo " ${GREEN}Removed $APP_INSTALL_DIR/lume.app${NORMAL}"
+ # Remove the share directory if empty
+ rmdir "$APP_INSTALL_DIR" 2>/dev/null && echo " ${GREEN}Removed $APP_INSTALL_DIR${NORMAL}" || true
+fi
+
# Remove log files
echo ""
echo "${BOLD}Removing log files...${NORMAL}"