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}"