diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46b9a06..b0cc4ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] jobs: build: @@ -45,6 +44,14 @@ jobs: ${{ runner.os }}-xcode-${{ steps.toolchain.outputs.xcode_version }}-spm- ${{ runner.os }}-spm- + - name: Cache CEF runtime + uses: actions/cache@v5 + with: + path: macos/vendor/cef + key: ${{ runner.os }}-cef-${{ hashFiles('scripts/fetch-cef.sh') }} + restore-keys: | + ${{ runner.os }}-cef- + - name: Verify Ghostty archive run: | set -euo pipefail @@ -53,10 +60,20 @@ jobs: file "$ARCHIVE" ar -t "$ARCHIVE" >/dev/null + - name: Fetch CEF runtime + run: bash scripts/fetch-cef.sh + - name: Build run: | swift build + - name: Install XcodeGen + run: | + set -euo pipefail + if ! command -v xcodegen >/dev/null 2>&1; then + brew install xcodegen + fi + - name: Run SwiftLint run: | set -euo pipefail @@ -93,3 +110,16 @@ jobs: - name: Run smoke test run: bash scripts/smoke-cli.sh + + - name: Generate Xcode project + run: | + cd macos + xcodegen generate + + - name: Build macOS app target + run: | + xcodebuild \ + -project /Users/runner/work/bugbook/bugbook/macos/Bugbook.xcodeproj \ + -scheme BugbookApp \ + -configuration Debug \ + build diff --git a/.gitignore b/.gitignore index 31439af..5071bab 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ macos/build/ xcuserdata/ DerivedData/ .DS_Store +macos/vendor/cef/ # Local coding-agent/tooling folders .agent/ diff --git a/Package.swift b/Package.swift index a317742..3742e4b 100644 --- a/Package.swift +++ b/Package.swift @@ -66,6 +66,9 @@ let package = Package( ], path: "Sources/Bugbook", exclude: ["MCP"], + swiftSettings: [ + .define("BUGBOOK_BROWSER_WEBKIT") + ], linkerSettings: [ .linkedFramework("AppKit"), .linkedFramework("Carbon"), diff --git a/README.md b/README.md index 9fe6294..c1874cc 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,14 @@ swift run BugbookCLI install --force ## Build and run ```bash -# macOS app +# macOS app (SwiftPM/WebKit fallback path) swift run Bugbook +# macOS app bundle (Xcode/Chromium path) +bash scripts/fetch-cef.sh +cd macos && xcodegen generate +xcodebuild -project /Users/maxforsey/Code/bugbook/macos/Bugbook.xcodeproj -scheme BugbookApp -configuration Debug build + # CLI swift build && swift run BugbookCLI --help diff --git a/scripts/fetch-cef.sh b/scripts/fetch-cef.sh new file mode 100755 index 0000000..040be6b --- /dev/null +++ b/scripts/fetch-cef.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +INDEX_URL="https://cef-builds.spotifycdn.com/index.json" +PINNED_CEF_VERSION="${CEF_VERSION:-139.0.40+g465474a+chromium-139.0.7258.139}" +PINNED_CHANNEL="${CEF_CHANNEL:-stable}" +PINNED_FILE_TYPE="${CEF_FILE_TYPE:-standard}" + +case "$(uname -m)" in + arm64) + PLATFORM="macosarm64" + ;; + x86_64) + PLATFORM="macosx64" + ;; + *) + echo "Unsupported macOS architecture: $(uname -m)" >&2 + exit 1 + ;; +esac + +read -r FILE_NAME FILE_SHA1 <<<"$(python3 - <<'PY' "$INDEX_URL" "$PLATFORM" "$PINNED_CEF_VERSION" "$PINNED_CHANNEL" "$PINNED_FILE_TYPE" +import json +import sys +import urllib.request + +index_url, platform, cef_version, channel, file_type = sys.argv[1:6] + +with urllib.request.urlopen(index_url, timeout=30) as response: + payload = json.load(response) + +versions = payload.get(platform, {}).get("versions", []) +for build in versions: + if build.get("cef_version") != cef_version: + continue + if build.get("channel") != channel: + continue + for file_info in build.get("files", []): + if file_info.get("type") == file_type: + print(file_info["name"], file_info["sha1"]) + raise SystemExit(0) + +raise SystemExit(f"No {file_type} build found for {platform} at {cef_version} ({channel}).") +PY +)" + +DOWNLOAD_DIR="$REPO_ROOT/macos/vendor/cef/downloads/$PLATFORM" +EXTRACT_ROOT="$REPO_ROOT/macos/vendor/cef/$PLATFORM" +DEST_DIR="$EXTRACT_ROOT/$PINNED_CEF_VERSION" +CURRENT_LINK="$REPO_ROOT/macos/vendor/cef/current" +ARCHIVE_PATH="$DOWNLOAD_DIR/$FILE_NAME" +DOWNLOAD_URL="https://cef-builds.spotifycdn.com/$FILE_NAME" + +mkdir -p "$DOWNLOAD_DIR" "$EXTRACT_ROOT" + +if [ ! -f "$ARCHIVE_PATH" ]; then + echo "-- Downloading $FILE_NAME" + curl -L "$DOWNLOAD_URL" -o "$ARCHIVE_PATH.part" + mv "$ARCHIVE_PATH.part" "$ARCHIVE_PATH" +fi + +ACTUAL_SHA1="$(shasum -a 1 "$ARCHIVE_PATH" | awk '{print $1}')" +if [ "$ACTUAL_SHA1" != "$FILE_SHA1" ]; then + echo "SHA1 mismatch for $ARCHIVE_PATH" >&2 + echo "Expected: $FILE_SHA1" >&2 + echo "Actual: $ACTUAL_SHA1" >&2 + exit 1 +fi + +if [ ! -d "$DEST_DIR" ]; then + echo "-- Extracting $FILE_NAME" + TMP_DIR="$EXTRACT_ROOT/.tmp-$PINNED_CEF_VERSION" + rm -rf "$TMP_DIR" + mkdir -p "$TMP_DIR" + tar -xjf "$ARCHIVE_PATH" -C "$TMP_DIR" + EXTRACTED_DIR="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + if [ -z "$EXTRACTED_DIR" ]; then + echo "Failed to locate extracted CEF directory" >&2 + exit 1 + fi + mv "$EXTRACTED_DIR" "$DEST_DIR" + rm -rf "$TMP_DIR" +fi + +ln -sfn "$PLATFORM/$PINNED_CEF_VERSION" "$CURRENT_LINK" + +echo "-- CEF ready at $DEST_DIR" +echo "-- Current link: $CURRENT_LINK" diff --git a/scripts/release.sh b/scripts/release.sh old mode 100644 new mode 100755 index 0ded8d7..ecb047b --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,12 +1,4 @@ #!/usr/bin/env bash -# release.sh — Build Bugbook.app and install to ~/Applications -# -# Usage: ./scripts/release.sh -# -# Builds using swift build (Release config), creates a proper .app bundle, -# and copies it to ~/Applications/Bugbook.app. The release build uses a -# different bundle identifier (com.bugbook.Bugbook) so it can run alongside -# the Xcode dev build (com.maxforsey.Bugbook.dev). set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -16,225 +8,57 @@ INSTALL_DIR="$HOME/Applications" APP_NAME="Bugbook.app" APP_PATH="$INSTALL_DIR/$APP_NAME" BUNDLE_ID="com.bugbook.Bugbook" +DERIVED_DATA="$REPO_ROOT/.build/release-derived-data" -contains_item() { - local needle="$1" - shift - - local item - for item in "$@"; do - if [ "$item" = "$needle" ]; then - return 0 - fi - done - - return 1 -} - -copy_swift_runtime_libraries() { - local queue=() - local copied=() - local search_dirs=() - local library="" - local dependency="" - local search_dir="" - local source_path="" - local i=0 - - while IFS= read -r library; do - [ -n "$library" ] && queue+=("$library") - done < <(otool -L "$MACOS_DIR/Bugbook" | awk '$1 ~ /^@rpath\/libswift.*\.dylib$/ { sub("^@rpath/", "", $1); print $1 }') - - while IFS= read -r search_dir; do - [ -d "$search_dir" ] && search_dirs+=("$search_dir") - done < <(otool -l "$MACOS_DIR/Bugbook" | awk '$1 == "path" && $2 ~ /^\// { print $2 }') - - for ((i = 0; i < ${#queue[@]}; i++)); do - library="${queue[$i]}" - - if [ ${#copied[@]} -gt 0 ] && contains_item "$library" "${copied[@]}"; then - continue - fi - - source_path="" - for search_dir in "${search_dirs[@]}"; do - if [ -f "$search_dir/$library" ]; then - source_path="$search_dir/$library" - break - fi - done - - if [ -z "$source_path" ]; then - echo "ERROR: Swift runtime library not found: $library" - exit 1 - fi - - cp -R "$source_path" "$FRAMEWORKS_DIR/" - copied+=("$library") - - while IFS= read -r dependency; do - [ -n "$dependency" ] && queue+=("$dependency") - done < <(otool -L "$source_path" | awk '$1 ~ /^@rpath\/libswift.*\.dylib$/ { sub("^@rpath/", "", $1); print $1 }') - done -} - -# --- Version from git --- VERSION="0.$(git rev-list --count HEAD 2>/dev/null || echo 1)" BUILD_NUMBER="$(git rev-list --count HEAD 2>/dev/null || echo 1)" GIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" echo "-- Building Bugbook $VERSION (build $BUILD_NUMBER, $GIT_SHA)" -# --- Build release binary --- -echo "-- swift build --configuration release --product Bugbook" -swift build --configuration release --product Bugbook 2>&1 | tail -5 - -BIN_DIR="$(swift build -c release --show-bin-path)" -BINARY="$BIN_DIR/Bugbook" -if [ ! -f "$BINARY" ]; then - echo "ERROR: Binary not found at $BINARY" +if ! command -v xcodegen >/dev/null 2>&1; then + echo "xcodegen is required for release builds" >&2 exit 1 fi -# --- Construct .app bundle --- -echo "-- Assembling $APP_NAME bundle" -STAGE_DIR="$REPO_ROOT/.build/release-app" -rm -rf "$STAGE_DIR" - -CONTENTS="$STAGE_DIR/$APP_NAME/Contents" -MACOS_DIR="$CONTENTS/MacOS" -RESOURCES="$CONTENTS/Resources" -FRAMEWORKS_DIR="$CONTENTS/Frameworks" - -mkdir -p "$MACOS_DIR" "$RESOURCES" "$FRAMEWORKS_DIR" - -# Copy binary -cp "$BINARY" "$MACOS_DIR/Bugbook" - -# Teach the SwiftPM-built executable to resolve bundled frameworks. -if ! otool -l "$MACOS_DIR/Bugbook" | grep -Fq "@executable_path/../Frameworks"; then - install_name_tool -add_rpath "@executable_path/../Frameworks" "$MACOS_DIR/Bugbook" +if [ ! -d "$REPO_ROOT/macos/vendor/cef/current/Release/Chromium Embedded Framework.framework" ]; then + bash "$REPO_ROOT/scripts/fetch-cef.sh" fi -# Copy runtime frameworks, bundles, and dylibs emitted by SwiftPM. -shopt -s nullglob -for framework in "$BIN_DIR"/*.framework; do - cp -R "$framework" "$FRAMEWORKS_DIR/" -done -for library in "$BIN_DIR"/*.dylib; do - cp -R "$library" "$FRAMEWORKS_DIR/" -done -for bundle in "$BIN_DIR"/*.bundle; do - cp -R "$bundle" "$RESOURCES/" -done -shopt -u nullglob - -# Bundle non-system Swift runtime libraries referenced through @rpath. -copy_swift_runtime_libraries - -# Compile asset catalog if actool is available, otherwise skip -XCASSETS="$REPO_ROOT/macos/App/Assets.xcassets" -if command -v actool &>/dev/null && [ -d "$XCASSETS" ]; then - echo "-- Compiling asset catalog" - actool "$XCASSETS" \ - --compile "$RESOURCES" \ - --platform macosx \ - --minimum-deployment-target 14.0 \ - --app-icon AppIcon \ - --accent-color AccentColor \ - --output-partial-info-plist /dev/null 2>/dev/null || true -else - # Copy icon PNG as a fallback - ICON_SRC="$REPO_ROOT/macos/App/Assets.xcassets/AppIcon.appiconset/icon_512x512.png" - if [ -f "$ICON_SRC" ]; then - cp "$ICON_SRC" "$RESOURCES/AppIcon.png" - fi +echo "-- Generating Xcode project" +( + cd "$REPO_ROOT/macos" + xcodegen generate +) + +echo "-- Building Xcode app target" +rm -rf "$DERIVED_DATA" +xcodebuild \ + -project "$REPO_ROOT/macos/Bugbook.xcodeproj" \ + -scheme BugbookApp \ + -configuration Release \ + -derivedDataPath "$DERIVED_DATA" \ + PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ + MARKETING_VERSION="$VERSION" \ + CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ + build + +BUILT_APP="$DERIVED_DATA/Build/Products/Release/Bugbook.app" +if [ ! -d "$BUILT_APP" ]; then + echo "ERROR: Built app not found at $BUILT_APP" >&2 + exit 1 fi -# Generate Info.plist -cat > "$CONTENTS/Info.plist" < - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - Bugbook - CFBundleExecutable - Bugbook - CFBundleIdentifier - ${BUNDLE_ID} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Bugbook - CFBundlePackageType - APPL - CFBundleShortVersionString - ${VERSION} - CFBundleVersion - ${BUILD_NUMBER} - CFBundleIconFile - AppIcon - LSMinimumSystemVersion - 14.0 - NSHighResolutionCapable - - NSMicrophoneUsageDescription - Bugbook needs microphone access to record meeting audio for live transcription. - NSSpeechRecognitionUsageDescription - Bugbook uses speech recognition to transcribe meeting recordings in real-time. - NSSupportsAutomaticGraphicsSwitching - - BugbookGitSHA - ${GIT_SHA} - - -PLIST - -# Write entitlements -cat > "$STAGE_DIR/Bugbook.entitlements" < - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation - - com.apple.security.network.client - - com.apple.security.device.audio-input - - - -ENTITLEMENTS - -# Ad-hoc codesign for local use echo "-- Codesigning (ad-hoc)" -codesign --force --deep \ - --sign - \ - --entitlements "$STAGE_DIR/Bugbook.entitlements" \ - "$STAGE_DIR/$APP_NAME" +codesign --force --deep --sign - "$BUILT_APP" -# --- Install --- echo "-- Installing to $APP_PATH" mkdir -p "$INSTALL_DIR" - -# Kill running release Bugbook if present (ignore errors) pkill -f "$APP_PATH/Contents/MacOS/Bugbook" 2>/dev/null || true sleep 0.5 rm -rf "$APP_PATH" -cp -R "$STAGE_DIR/$APP_NAME" "$APP_PATH" - -# Clean up staging -rm -rf "$STAGE_DIR" +cp -R "$BUILT_APP" "$APP_PATH" echo "" echo "Done! Bugbook $VERSION installed to $APP_PATH"