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