diff --git a/.github/workflows/cd-swift-lume.yml b/.github/workflows/cd-swift-lume.yml
index de57a86017..571851cf82 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/.gitignore b/.gitignore
index e1d11b8ced..082570d565 100644
--- a/.gitignore
+++ b/.gitignore
@@ -190,6 +190,8 @@ fastlane/test_output
ignore
# .release
.release/
+# Provisioning profiles (generated from CI secrets)
+*.provisionprofile
# Shared folder
shared
# Trajectories
diff --git a/libs/lume/resources/Info.plist b/libs/lume/resources/Info.plist
new file mode 100644
index 0000000000..e67e7d36a6
--- /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 d7d0d6e8b6..dccbe21c27 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 59ab5040fa..ab056d7611 100755
--- a/libs/lume/scripts/build/build-release-notarized.sh
+++ b/libs/lume/scripts/build/build-release-notarized.sh
@@ -64,23 +64,77 @@ 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..."
-codesign --force --options runtime \
- --entitlement ./resources/lume.entitlements \
+# --- Assemble .app bundle ---
+log "essential" "Assembling .app bundle..."
+
+APP_BUNDLE=".release/lume.app"
+rm -rf "$APP_BUNDLE"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+mkdir -p "$APP_BUNDLE/Contents/Resources"
+
+# Copy the binary into the bundle
+cp -f .build/release/lume "$APP_BUNDLE/Contents/MacOS/lume"
+
+# Copy resource bundle to Contents/Resources/.
+# It CANNOT go in Contents/MacOS/ (breaks codesign: "bundle format unrecognized")
+# and CANNOT go at the .app root (breaks codesign: "unsealed contents").
+# The Swift code uses Bundle.lumeResources which checks resourceURL first.
+BUILD_BUNDLE=".build/release/lume_lume.bundle"
+if [ -d "$BUILD_BUNDLE" ]; then
+ cp -rf "$BUILD_BUNDLE" "$APP_BUNDLE/Contents/Resources/"
+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..."
+log "essential" "Using signing identity: $CERT_APPLICATION_NAME"
+
+# Ensure build.keychain is in the search list for codesign
+KEYCHAIN_PATH="$HOME/Library/Keychains/build.keychain-db"
+if [ -f "$KEYCHAIN_PATH" ]; then
+ log "essential" "Adding build keychain to search list..."
+ security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
+ security list-keychains
+fi
+
+# Sign the .app bundle
+log "essential" "Signing .app bundle with Developer ID..."
+codesign --force --options runtime --timestamp \
+ --entitlements ./resources/lume.entitlements \
--sign "$CERT_APPLICATION_NAME" \
- .build/release/lume 2> /dev/null
+ --keychain "$KEYCHAIN_PATH" \
+ "$APP_BUNDLE"
-# 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/"
+# Verify the final bundle signature
+log "essential" "Verifying bundle signature..."
+codesign -dvv "$APP_BUNDLE" 2>&1
+codesign --verify --strict --deep "$APP_BUNDLE" 2>&1 || { log "error" "Bundle signature verification FAILED"; exit 1; }
+log "essential" "Signature verified successfully."
-# 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"
+# Use ditto to preserve code signatures and extended attributes
+ditto "$APP_BUNDLE" "$TEMP_ROOT/usr/local/share/lume/lume.app"
+
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 +150,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
@@ -113,6 +167,20 @@ if [ "$LOG_LEVEL" = "minimal" ] || [ "$LOG_LEVEL" = "none" ]; then
log "error" "Notarization failed. Please check logs."
log "error" "Notarization output:"
echo "$NOTARY_OUTPUT"
+ # Extract submission ID and fetch detailed log
+ SUBMISSION_ID=$(echo "$NOTARY_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
+ if [ -n "$SUBMISSION_ID" ]; then
+ log "error" "Fetching notarization log for submission $SUBMISSION_ID..."
+ xcrun notarytool log "$SUBMISSION_ID" \
+ --apple-id "${APPLE_ID}" \
+ --team-id "${TEAM_ID}" \
+ --password "${APP_SPECIFIC_PASSWORD}" \
+ developer_log.json 2>&1 || true
+ if [ -f developer_log.json ]; then
+ log "error" "Notarization log:"
+ cat developer_log.json
+ fi
+ fi
exit 1
fi
else
@@ -123,86 +191,66 @@ else
--password "${APP_SPECIFIC_PASSWORD}" \
--wait; then
log "error" "Notarization failed"
+ # Try to fetch the log for the last submission
+ LAST_ID=$(xcrun notarytool history \
+ --apple-id "${APPLE_ID}" \
+ --team-id "${TEAM_ID}" \
+ --password "${APP_SPECIFIC_PASSWORD}" 2>&1 | grep "id:" | head -1 | awk '{print $2}')
+ if [ -n "$LAST_ID" ]; then
+ log "error" "Fetching notarization log for submission $LAST_ID..."
+ xcrun notarytool log "$LAST_ID" \
+ --apple-id "${APPLE_ID}" \
+ --team-id "${TEAM_ID}" \
+ --password "${APP_SPECIFIC_PASSWORD}" \
+ developer_log.json 2>&1 || true
+ if [ -f developer_log.json ]; then
+ log "error" "Notarization log:"
+ cat developer_log.json
+ fi
+ fi
exit 1
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
-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/"
+# 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
-# 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 +268,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 0f5d3f373e..8a5e7b5b1d 100755
--- a/libs/lume/scripts/build/build-release.sh
+++ b/libs/lume/scripts/build/build-release.sh
@@ -9,25 +9,60 @@ 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 to Contents/Resources/.
+# It CANNOT go in Contents/MacOS/ (breaks codesign: "bundle format unrecognized")
+# and CANNOT go at the .app root (breaks codesign: "unsealed contents").
+# The Swift code uses Bundle.lumeResources which checks resourceURL first.
+mkdir -p "$APP_BUNDLE/Contents/Resources"
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/Resources/"
+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 --entitlements ./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"
+APP_INSTALL_DIR="$HOME/.local/share/lume"
+
mkdir -p "$USER_BIN"
-cp -f ./.release/lume "$USER_BIN/lume"
+mkdir -p "$APP_INSTALL_DIR"
-# Install the resource bundle alongside the binary
-if [ -d "./.release/lume_lume.bundle" ]; then
- cp -rf ./.release/lume_lume.bundle "$USER_BIN/"
-fi
+# 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 | 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"
- # Verify the signed binary can launch.
- if ! "$BUILD_PATH/lume" --version >/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}"
+ # 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 +198,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"
-
- # Install resource bundle if it exists (contains unattended presets)
- if [ -d "$TEMP_DIR/lume_lume.bundle" ]; then
+ 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/"
+
+ # 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 9f280bbc92..e3c6302f0d 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}"
diff --git a/libs/lume/src/Unattended/ResourceBundle.swift b/libs/lume/src/Unattended/ResourceBundle.swift
new file mode 100644
index 0000000000..2ce87dd339
--- /dev/null
+++ b/libs/lume/src/Unattended/ResourceBundle.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+extension Bundle {
+ /// Custom resource bundle accessor that works both for standalone binaries
+ /// (where SPM places the bundle next to the executable) and for .app bundles
+ /// (where the bundle lives in Contents/Resources/).
+ ///
+ /// SPM's auto-generated `Bundle.module` only checks `Bundle.main.bundleURL`
+ /// (the .app root), which doesn't match `Contents/Resources/` in a .app bundle.
+ /// This accessor checks `resourceURL` first, then `bundleURL`, then the build path.
+ static let lumeResources: Bundle = {
+ let bundleName = "lume_lume.bundle"
+
+ // 1. .app bundle: Contents/Resources/
+ if let resourceURL = Bundle.main.resourceURL {
+ let path = resourceURL.appendingPathComponent(bundleName).path
+ if let bundle = Bundle(path: path) {
+ return bundle
+ }
+ }
+
+ // 2. Standalone binary: next to the executable
+ let mainPath = Bundle.main.bundleURL.appendingPathComponent(bundleName).path
+ if let bundle = Bundle(path: mainPath) {
+ return bundle
+ }
+
+ // 3. Development fallback: SPM build directory
+ #if DEBUG
+ // During development, try the build directory
+ let buildPath = Bundle.main.bundleURL.appendingPathComponent(bundleName).path
+ if let bundle = Bundle(path: buildPath) {
+ return bundle
+ }
+ #endif
+
+ fatalError("Could not load resource bundle '\(bundleName)' from resourceURL or bundleURL")
+ }()
+}
diff --git a/libs/lume/src/Unattended/UnattendedConfig.swift b/libs/lume/src/Unattended/UnattendedConfig.swift
index ffd0605e3c..fcd98c088c 100644
--- a/libs/lume/src/Unattended/UnattendedConfig.swift
+++ b/libs/lume/src/Unattended/UnattendedConfig.swift
@@ -87,7 +87,7 @@ struct UnattendedConfig: Codable, Sendable {
/// Load a built-in preset by name
static func loadPreset(name: String) throws -> UnattendedConfig {
- guard let url = Bundle.module.url(
+ guard let url = Bundle.lumeResources.url(
forResource: name,
withExtension: "yml",
subdirectory: "unattended-presets"
@@ -101,7 +101,7 @@ struct UnattendedConfig: Codable, Sendable {
/// Check if a name is a known preset
static func isPreset(name: String) -> Bool {
- return Bundle.module.url(
+ return Bundle.lumeResources.url(
forResource: name,
withExtension: "yml",
subdirectory: "unattended-presets"
@@ -110,7 +110,7 @@ struct UnattendedConfig: Codable, Sendable {
/// List all available preset names
static func availablePresets() -> [String] {
- guard let resourceURL = Bundle.module.url(forResource: "unattended-presets", withExtension: nil),
+ guard let resourceURL = Bundle.lumeResources.url(forResource: "unattended-presets", withExtension: nil),
let contents = try? FileManager.default.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
else {
return []
diff --git a/libs/lume/tests/Mocks/MockVM.swift b/libs/lume/tests/Mocks/MockVM.swift
index ea21fb6ded..1e24d08da9 100644
--- a/libs/lume/tests/Mocks/MockVM.swift
+++ b/libs/lume/tests/Mocks/MockVM.swift
@@ -23,13 +23,15 @@ class MockVM: VM {
override func run(
noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0,
- recoveryMode: Bool = false, usbMassStoragePaths: [Path]? = nil
+ recoveryMode: Bool = false, usbMassStoragePaths: [Path]? = nil,
+ networkMode: NetworkMode? = nil, clipboard: Bool = false
) async throws {
mockIsRunning = true
try await super.run(
noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount,
vncPort: vncPort, recoveryMode: recoveryMode,
- usbMassStoragePaths: usbMassStoragePaths
+ usbMassStoragePaths: usbMassStoragePaths,
+ networkMode: networkMode, clipboard: clipboard
)
}
diff --git a/libs/lume/tests/VM/VMDetailsPrinterTests.swift b/libs/lume/tests/VM/VMDetailsPrinterTests.swift
index d4acaf187d..b13725eb42 100644
--- a/libs/lume/tests/VM/VMDetailsPrinterTests.swift
+++ b/libs/lume/tests/VM/VMDetailsPrinterTests.swift
@@ -76,12 +76,12 @@ struct VMDetailsPrinterTests {
let headerParts = printedLines[0].split(whereSeparator: \.isWhitespace)
#expect(
headerParts == [
- "name", "os", "cpu", "memory", "disk", "display", "status", "storage", "shared_dirs", "ip", "ssh", "vnc",
+ "name", "os", "cpu", "memory", "disk", "display", "status", "network", "storage", "shared_dirs", "ip", "ssh", "vnc",
])
#expect(
printedLines[1].split(whereSeparator: \.isWhitespace).map(String.init) == [
- "name", "os", "2", "0.00G", "24.0B/30.0B", "1024x768", "status", "mockLocation",
+ "name", "os", "2", "0.00G", "24.0B/30.0B", "1024x768", "status", "nat", "mockLocation",
"-",
"0.0.0.0",
"-",