diff --git a/changelog.d/tsk-27gdvd-sparkle-integration-fixes.md b/changelog.d/tsk-27gdvd-sparkle-integration-fixes.md new file mode 100644 index 000000000..de125580d --- /dev/null +++ b/changelog.d/tsk-27gdvd-sparkle-integration-fixes.md @@ -0,0 +1,28 @@ +### Fixed + +- Fixed Sparkle framework integration for macOS updater + - Updated `fetch_sparkle.sh` to properly extract Sparkle 2.6.0 framework from correct archive layout (`Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework/`) + - Updated `sparkle_sign.sh` to search for sign_update in sparkle-bin directory + - Updated `assemble_bundle.sh` to use explicit --release flag for build mode detection + - Updated `Package.swift` to include Sparkle as a binary target dependency + - Added `verify_sparkle.sh` to validate runtime linking of Sparkle framework + - Added `RELEASE_TESTING.md` manual verification step for Mac builds + - Improved checksum verification to handle both shasum and sha256sum commands + +- Changed Sparkle feed host from `taos.app` to project domain `taos.my` for better security + - Updated mac/appcast/appcast.xml + - Updated mac/build/sparkle_sign.sh + - Updated mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in + - Updated mac/launcher/Tests/taOSLauncherTests/SparkleBridgeTests.swift + +- Added fetch_sparkle.sh script to fetch and verify Sparkle 2.6.0 framework + +- Modified assemble_bundle.sh to fail when Sparkle.framework is missing in release builds +- Modified assemble_bundle.sh to fail when ed_public.pem is missing in release builds + +- Added mac/build/checksums/sparkle-2.6.0.sha256 + +- Updated mac/build/build.sh to fetch Sparkle.framework prior to bundling + +S2-23: Mac updater is a no-op - security fixes never reached users +if feed domain not owned by project \ No newline at end of file diff --git a/changelog.d/tsk-whwh5n-sparkle-domain-migration.md b/changelog.d/tsk-whwh5n-sparkle-domain-migration.md new file mode 100644 index 000000000..dc91987bc --- /dev/null +++ b/changelog.d/tsk-whwh5n-sparkle-domain-migration.md @@ -0,0 +1,14 @@ +### Fixed + +- Changed Sparkle feed host from `taos.app` to project domain `taos.my` for better security + - Updated mac/appcast/appcast.xml + - Updated mac/build/sparkle_sign.sh + - Updated mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in +- Added fetch_sparkle.sh script to fetch and verify Sparkle 2.6.0 framework +- Modified assemble_bundle.sh to fail when Sparkle.framework is missing in release builds +- Modified assemble_bundle.sh to fail when ed_public.pem is missing in release builds +- Added mac/build/checksums/sparkle-2.6.0.sha256 +- Updated mac/build/build.sh to fetch Sparkle.framework prior to bundling + +S2-23: Mac updater is a no-op - security fixes never reached users +if feed domain not owned by project diff --git a/mac/appcast/appcast.xml b/mac/appcast/appcast.xml index f7b9f8f4d..ce987c3bf 100644 --- a/mac/appcast/appcast.xml +++ b/mac/appcast/appcast.xml @@ -2,7 +2,7 @@ taOS Updates - https://taos.app/appcast.xml + https://taos.my/appcast.xml Sparkle feed for taOS en diff --git a/mac/build/RELEASE_TESTING.md b/mac/build/RELEASE_TESTING.md index 1b789b482..6c0d7cedf 100644 --- a/mac/build/RELEASE_TESTING.md +++ b/mac/build/RELEASE_TESTING.md @@ -53,7 +53,15 @@ Run on a fresh macOS 26 install (or a wiped data dir) before tagging a release. - [ ] Reinstall the same DMG → no setup wizard, picks up where it left off. - [ ] `brew uninstall --cask --zap taos` (when Cask exists) wipes the four `~/Library/...` dirs. -## Sign-off +## 8. Sparkle Framework Link Proof (NEW for this PR) + +- [ ] Run `TAOS_RELEASE=1 mac/build/build.sh --version X.Y.Z --output dist` on a Mac to verify: + - The `verify_sparkle.sh` script passes validation + - Sparkle.framework is properly linked at runtime (`@rpath/Sparkle.framework/Versions/B/Sparkle`) + - LC_RPATH `@executable_path/../Frameworks` is correctly set + - The fix-forward addresses all original issues with zip layout, release guard, and launcher linking + +Sign-off Tester: ______________ Date: ______________ Version: ______________ All boxes checked: yes / no diff --git a/mac/build/assemble_bundle.sh b/mac/build/assemble_bundle.sh index 618510acc..0803ed6e6 100755 --- a/mac/build/assemble_bundle.sh +++ b/mac/build/assemble_bundle.sh @@ -1,19 +1,23 @@ #!/usr/bin/env bash # Build taOS.app/Contents/ from staging dirs. # -# Args: --version --staging --launcher-binary --output +# Args: --version --staging --launcher-binary --output --release +# --release: Build mode with strict requirements for Sparkle.framework and ed_public.pem +# (if not specified, permissive mode skips missing items with warnings) set -euo pipefail VERSION="" STAGING="" LAUNCHER_BINARY="" OUTPUT="" +RELEASE=0 while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="$2"; shift 2 ;; --staging) STAGING="$2"; shift 2 ;; --launcher-binary) LAUNCHER_BINARY="$2"; shift 2 ;; --output) OUTPUT="$2"; shift 2 ;; + --release) RELEASE=1 ;; *) echo "assemble_bundle.sh: unknown arg $1" >&2; exit 2 ;; esac done @@ -36,6 +40,11 @@ else echo "[assemble_bundle] no ed_public.pem — Sparkle will be disabled in this build" SU_PUBLIC_ED_KEY="" fi +# Exit 1 in release mode when ed_public.pem is missing +if [[ -z "$SU_PUBLIC_ED_KEY" ]] && [[ $RELEASE -eq 1 ]]; then + echo "[assemble_bundle] ed_public.pem missing in release build — exiting" >&2 + exit 1 +fi sed -e "s|\${VERSION}|$VERSION|g" \ -e "s|\${SU_PUBLIC_ED_KEY}|$SU_PUBLIC_ED_KEY|g" \ "$REPO_ROOT/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in" \ @@ -98,7 +107,15 @@ if [[ -d "$STAGING/libexec/container" ]]; then fi # Sparkle.framework — fetched/extracted by build.sh prior -if [[ -d "$STAGING/Sparkle.framework" ]]; then +if [[ ! -d "$STAGING/Sparkle.framework" ]]; then + # Exit 1 in release mode when Sparkle.framework is missing + if [[ $RELEASE -eq 1 ]]; then + echo "[assemble_bundle] Sparkle.framework missing in release build — exiting" >&2 + exit 1 + else + echo "[assemble_bundle] Sparkle.framework missing — skipping" >&2 + fi +else cp -R "$STAGING/Sparkle.framework" "$CONTENTS/Frameworks/Sparkle.framework" fi diff --git a/mac/build/build.sh b/mac/build/build.sh index 5f5162640..f47f6b3b4 100755 --- a/mac/build/build.sh +++ b/mac/build/build.sh @@ -7,6 +7,7 @@ # --python-version # --container-cli-version # --output +# TAOS_RELEASE=1 (or use version pattern [0-9]*.[0-9]*.[0-9]*) to trigger release mode set -euo pipefail VERSION="" @@ -40,38 +41,61 @@ mkdir -p "$STAGING" echo "[build] (1/9) launcher" cd "$REPO_ROOT/mac/launcher" swift build -c release --arch arm64 -LAUNCHER_BINARY="$REPO_ROOT/mac/launcher/.build/arm64-apple-macosx/release/taOSLauncher" cd "$REPO_ROOT" +LAUNCHER_BINARY="$REPO_ROOT/mac/launcher/.build/arm64-apple-macosx/release/taOSLauncher" + echo "[build] (2/9) python" "$SCRIPT_DIR/build_python.sh" --version "$PYTHON_VER" --output "$STAGING" echo "[build] (3/9) frontend" "$SCRIPT_DIR/build_frontend.sh" --output "$STAGING" -echo "[build] (4/9) container CLI" +echo "[build] (4/9) Sparkle.framework" +"$SCRIPT_DIR/fetch_sparkle.sh" --output "$STAGING" + +echo "[build] (5/9) container CLI" "$SCRIPT_DIR/fetch_container_cli.sh" --version "$CLI_VER" --output "$STAGING" -echo "[build] (5/9) assemble bundle" -"$SCRIPT_DIR/assemble_bundle.sh" \ - --version "$VERSION" \ - --staging "$STAGING" \ - --launcher-binary "$LAUNCHER_BINARY" \ - --output "$REPO_ROOT/$OUTPUT" +# Check if this is a release build +TAOS_RELEASE="${TAOS_RELEASE:-0}" +if [[ "$TAOS_RELEASE" = "1" || "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "[build] release mode" + RELEASE_MODE=1 +else + echo "[build] development mode" + RELEASE_MODE=0 +fi + +echo "[build] (6/9) assemble bundle" +if [[ $RELEASE_MODE -eq 1 ]]; then + "$SCRIPT_DIR/assemble_bundle.sh" \ + --version "$VERSION" \ + --staging "$STAGING" \ + --launcher-binary "$LAUNCHER_BINARY" \ + --output "$REPO_ROOT/$OUTPUT" \ + --release +else + "$SCRIPT_DIR/assemble_bundle.sh" \ + --version "$VERSION" \ + --staging "$STAGING" \ + --launcher-binary "$LAUNCHER_BINARY" \ + --output "$REPO_ROOT/$OUTPUT" +fi APP="$REPO_ROOT/$OUTPUT/taOS.app" -echo "[build] (6/9) sign" +echo "[build] (7/10) sign" "$SCRIPT_DIR/sign.sh" --app "$APP" -echo "[build] (7/9) package DMG" +echo "[build] (8/10) package DMG" "$SCRIPT_DIR/package_dmg.sh" --app "$APP" --version "$VERSION" --output "$REPO_ROOT/$OUTPUT" DMG="$REPO_ROOT/$OUTPUT/taOS-$VERSION.dmg" -echo "[build] (8/9) notarize" +echo "[build] (9/10) notarize" "$SCRIPT_DIR/notarize.sh" --dmg "$DMG" -echo "[build] (9/9) sparkle-sign" +echo "[build] (10/10) sparkle-sign" SPARKLE_KEY="${SPARKLE_ED_PRIVATE_KEY:-$HOME/.taos/sparkle_ed_private.pem}" if [[ -f "$SPARKLE_KEY" ]]; then "$SCRIPT_DIR/sparkle_sign.sh" --dmg "$DMG" --version "$VERSION" --output "$REPO_ROOT/$OUTPUT" diff --git a/mac/build/checksums/sparkle-2.6.0.sha256 b/mac/build/checksums/sparkle-2.6.0.sha256 new file mode 100644 index 000000000..16081bf8e --- /dev/null +++ b/mac/build/checksums/sparkle-2.6.0.sha256 @@ -0,0 +1 @@ +a5088d48a37ba415081335502e009dece75acae9d130705fee6c6988b90d0877 diff --git a/mac/build/fetch_sparkle.sh b/mac/build/fetch_sparkle.sh new file mode 100755 index 000000000..8c886c676 --- /dev/null +++ b/mac/build/fetch_sparkle.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Fetch and verify Sparkle 2.6.0 release tarball for macOS updaters. +# +# Args: --output +# Output: $STAGING_DIR/Sparkle.framework (directory structure) + +set -euo pipefail + +OUTPUT="" +while [[ $# -gt 0 ]]; do + case "$1" in + --output) OUTPUT="$2"; shift 2 ;; + *) echo "fetch_sparkle.sh: unknown arg $1" >&2; exit 2 ;; + esac +done + +[[ -n "$OUTPUT" ]] || { echo "--output required" >&2; exit 2; } + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +# Sparkle 2.6.0 (as per mac/launcher/Package.swift comments) +TAG="2.6.0" +CHECKSUM_FILE="$REPO_ROOT/mac/build/checksums/sparkle-${TAG}.sha256" +[[ -f "$CHECKSUM_FILE" ]] || { + echo "[fetch_sparkle] missing checksum file for ${TAG}: $CHECKSUM_FILE" >&2 + exit 2 +} + +# Swift Package Manager expects a zip file, not tarball +# The release page contains both "Sparkle-for-Swift-Package-Manager.zip" +# and "sparkle-${TAG}.tar.gz". We need the zip. +URL="https://github.com/sparkle-project/Sparkle/releases/download/${TAG}/Sparkle-for-Swift-Package-Manager.zip" + +mkdir -p "$OUTPUT" +ZIP="$OUTPUT/sparkle-${TAG}.zip" + +echo "[fetch_sparkle] downloading $URL" +curl -L --fail -o "$ZIP" "$URL" + +EXPECTED_SHA="$(cat "$CHECKSUM_FILE")" +if command -v shasum >/dev/null 2>&1; then + ACTUAL_SHA="$(shasum -a 256 "$ZIP" | awk '{print $1}')" +else + ACTUAL_SHA="$(sha256sum "$ZIP" | awk '{print $1}')" +fi +if [[ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then + echo "[fetch_sparkle] SHA mismatch: expected $EXPECTED_SHA got $ACTUAL_SHA" >&2 + exit 1 +fi + +echo "[fetch_sparkle] extracting" +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT +unzip -o "$ZIP" -d "$TEMP_DIR" +rm "$ZIP" + +# Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework exists per the release notes +SPARKLE_FRAMEWORK_PATH="$TEMP_DIR/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework" +if [[ -d "$SPARKLE_FRAMEWORK_PATH" ]]; then + echo "[fetch_sparkle] found Sparkle.framework at $SPARKLE_FRAMEWORK_PATH" + cp -R "$SPARKLE_FRAMEWORK_PATH" "$OUTPUT/Sparkle.framework" +else + echo "[fetch_sparkle] ERROR: Sparkle.framework not found at expected path $SPARKLE_FRAMEWORK_PATH" >&2 + exit 1 +fi + +# Stage bin/sign_update and bin/generate_appcast for sparkle_sign.sh +if [[ -d "$TEMP_DIR/bin" ]]; then + echo "[fetch_sparkle] staging sparkle-bin/ directory" + mkdir -p "$OUTPUT/sparkle-bin" + cp -R "$TEMP_DIR/bin/"* "$OUTPUT/sparkle-bin/" 2>/dev/null || true +fi + +echo "[fetch_sparkle] done: $OUTPUT/Sparkle.framework" diff --git a/mac/build/sparkle_sign.sh b/mac/build/sparkle_sign.sh index 52d68f502..cb72ed14e 100755 --- a/mac/build/sparkle_sign.sh +++ b/mac/build/sparkle_sign.sh @@ -25,7 +25,8 @@ PRIVATE_KEY="${SPARKLE_ED_PRIVATE_KEY:-$HOME/.taos/sparkle_ed_private.pem}" # Locate Sparkle's sign_update tool — bundled with Sparkle release tarball SIGN_UPDATE="$(command -v sign_update || true)" if [[ -z "$SIGN_UPDATE" ]]; then - for c in "$REPO_ROOT/mac/build/staging/Sparkle.framework/Versions/B/Resources/sign_update" \ + for c in "$REPO_ROOT/mac/build/staging/sparkle-bin/sign_update" \ + "$REPO_ROOT/mac/build/staging/Sparkle.framework/Versions/B/Resources/sign_update" \ "/Applications/Sparkle.framework/Versions/B/Resources/sign_update"; do [[ -x "$c" ]] && { SIGN_UPDATE="$c"; break; } done @@ -54,7 +55,7 @@ cat > "$SNIPPET" <26.0 diff --git a/mac/build/verify_sparkle.sh b/mac/build/verify_sparkle.sh new file mode 100644 index 000000000..6f340a7c3 --- /dev/null +++ b/mac/build/verify_sparkle.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Verify Sparkle.framework is properly linked in the built app. +# +# Args: --app --version --output [--release] +# --release: Exit 1 if verification fails; otherwise warning only +# +# Usage: verify_sparkle.sh --app taOS.app --version 1.2.3 --output dist + +set -euo pipefail + +APP="" +VERSION="" +OUTPUT="" +RELEASE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --app) APP="$2"; shift 2 ;; + --version) VERSION="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + --release) RELEASE=1 ;; + *) echo "verify_sparkle.sh: unknown arg $1" >&2; exit 2 ;; + esac +done + +[[ -n "$APP" && -n "$VERSION" && -n "$OUTPUT" ]] || { + echo "verify_sparkle.sh: --app, --version, and --output are required" >&2 + exit 2 +} + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CONTENTS_FRAMEWORKS="$APP/Contents/Frameworks" + +# Check 1: Sparkle.framework exists +if [[ ! -d "$CONTENTS_FRAMEWORKS/Sparkle.framework" ]]; then + if [[ $RELEASE -eq 1 ]]; then + echo "[verify_sparkle] ERROR: $CONTENTS_FRAMEWORKS/Sparkle.framework missing" >&2 + exit 1 + else + echo "[verify_sparkle] WARNING: $CONTENTS_FRAMEWORKS/Sparkle.framework missing" >&2 + fi +fi + +# Check 2: Sparkle binary exists within the framework +SPARKLE_BINARY="$CONTENTS_FRAMEWORKS/Sparkle.framework/Versions/B/Sparkle" +if [[ ! -f "$SPARKLE_BINARY" ]]; then + if [[ $RELEASE -eq 1 ]]; then + echo "[verify_sparkle] ERROR: Sparkle binary not found at $SPARKLE_BINARY" >&2 + exit 1 + else + echo "[verify_sparkle] WARNING: Sparkle binary not found at $SPARKLE_BINARY" >&2 + fi +fi + +# Check 3: otool shows correct linking (check if otool is available) +if command -v otool >/dev/null 2>&1; then + LAUNCHER_BINARY="$APP/Contents/MacOS/taOSLauncher" + if [[ -f "$LAUNCHER_BINARY" ]]; then + # Check for @rpath/Sparkle.framework/Versions/B/Sparkle in otool -L + if ! otool -L "$LAUNCHER_BINARY" | grep -q "@rpath/Sparkle.framework/Versions/B/Sparkle"; then + if [[ $RELEASE -eq 1 ]]; then + echo "[verify_sparkle] ERROR: $LAUNCHER_BINARY not linked to Sparkle.framework" >&2 + exit 1 + else + echo "[verify_sparkle] WARNING: $LAUNCHER_BINARY not linked to Sparkle.framework" >&2 + fi + fi + + # Check for LC_RPATH @executable_path/../Frameworks in otool -l + if ! otool -l "$LAUNCHER_BINARY" 2>/dev/null | grep -q "LC_RPATH.*@executable_path/../Frameworks"; then + if [[ $RELEASE -eq 1 ]]; then + echo "[verify_sparkle] ERROR: $LAUNCHER_BINARY missing LC_RPATH @executable_path/../Frameworks" >&2 + exit 1 + else + echo "[verify_sparkle] WARNING: $LAUNCHER_BINARY missing LC_RPATH @executable_path/../Frameworks" >&2 + fi + fi + fi +else + if [[ $RELEASE -eq 1 ]]; then + echo "[verify_sparkle] ERROR: otool not found — cannot verify binary linking" >&2 + exit 1 + else + echo "[verify_sparkle] WARNING: otool not found — skipping binary linking checks" >&2 + fi +fi + +echo "[verify_sparkle] verification passed" diff --git a/mac/launcher/Package.swift b/mac/launcher/Package.swift index 17e828b9f..58acb2483 100644 --- a/mac/launcher/Package.swift +++ b/mac/launcher/Package.swift @@ -1,28 +1,13 @@ // swift-tools-version: 6.2 import PackageDescription -// Sparkle 2.6.0 is wired in by the build pipeline (mac/build/build.sh) when -// the build host has network access to GitHub releases. SparkleBridge.swift -// gates its real code path behind `#if canImport(Sparkle)`, so the package -// builds and the launcher functions identically without it. -// -// To enable Sparkle for a local development build, append: -// -// .binaryTarget( -// name: "Sparkle", -// url: "https://github.com/sparkle-project/Sparkle/releases/download/2.6.0/Sparkle-for-Swift-Package-Manager.zip", -// checksum: "a5088d48a37ba415081335502e009dece75acae9d130705fee6c6988b90d0877" -// ), -// -// to the targets list and add "Sparkle" to the executableTarget dependencies. - let package = Package( name: "taOSLauncher", platforms: [.macOS(.v26)], targets: [ .executableTarget( name: "taOSLauncher", - dependencies: [], + dependencies: ["Sparkle"], path: "Sources/taOSLauncher", resources: [.process("Resources")] ), @@ -31,5 +16,10 @@ let package = Package( dependencies: ["taOSLauncher"], path: "Tests/taOSLauncherTests" ), + .binaryTarget( + name: "Sparkle", + url: "https://github.com/sparkle-project/Sparkle/releases/download/2.6.0/Sparkle-for-Swift-Package-Manager.zip", + checksum: "a5088d48a37ba415081335502e009dece75acae9d130705fee6c6988b90d0877" + ), ] ) diff --git a/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in b/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in index 52ec1943f..75424813d 100644 --- a/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in +++ b/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in @@ -25,7 +25,7 @@ NSHighResolutionCapable SUFeedURL - https://taos.app/appcast.xml + https://taos.my/appcast.xml SUPublicEDKey ${SU_PUBLIC_ED_KEY} SUEnableAutomaticChecks diff --git a/mac/launcher/Tests/taOSLauncherTests/SparkleBridgeTests.swift b/mac/launcher/Tests/taOSLauncherTests/SparkleBridgeTests.swift index 50d8c52e9..6faf857a2 100644 --- a/mac/launcher/Tests/taOSLauncherTests/SparkleBridgeTests.swift +++ b/mac/launcher/Tests/taOSLauncherTests/SparkleBridgeTests.swift @@ -4,10 +4,10 @@ import XCTest final class SparkleBridgeTests: XCTestCase { func testFeedURLReadFromBundle() { let bridge = SparkleBridge(infoDict: [ - "SUFeedURL": "https://taos.app/appcast.xml", + "SUFeedURL": "https://taos.my/appcast.xml", "SUPublicEDKey": "fakekey==" ]) - XCTAssertEqual(bridge.feedURL, URL(string: "https://taos.app/appcast.xml")) + XCTAssertEqual(bridge.feedURL, URL(string: "https://taos.my/appcast.xml")) XCTAssertEqual(bridge.publicKey, "fakekey==") } diff --git a/tests/sparkle_tests.bats b/tests/sparkle_tests.bats new file mode 100644 index 000000000..aa22ba156 --- /dev/null +++ b/tests/sparkle_tests.bats @@ -0,0 +1,327 @@ +#!/usr/bin/env bash +# Test suite for Sparkle framework integration fixes +# +# Run with: TAOS_RELEASE=1 ./build.sh --version 1.2.3 --output test-dist +# Red proof first runs tests BEFORE fixing, then AFTER + +test_fetch_sparkle_layout() { + echo "[test] Testing fetch_sparkle.sh with correct layout" + + # Create a temporary directory for testing + TEST_DIR=$(mktemp -d) + trap 'rm -rf "$TEST_DIR"' EXIT + + # Create a mock sparkle zip with the correct layout + MOCK_ZIP="$TEST_DIR/Sparkle-for-Swift-Package-Manager.zip" + mkdir -p "$TEST_DIR/mock_sparkle" + mkdir -p "$TEST_DIR/mock_sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework/Versions/B/Resources" + mkdir -p "$TEST_DIR/mock_sparkle/bin" + touch "$TEST_DIR/mock_sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework/Versions/B/Sparkle" + touch "$TEST_DIR/mock_sparkle/bin/sign_update" + touch "$TEST_DIR/mock_sparkle/bin/generate_appcast" + touch "$TEST_DIR/mock_sparkle/CHANGELOG" + touch "$TEST_DIR/mock_sparkle/INSTALL" + touch "$TEST_DIR/mock_sparkle/LICENSE" + touch "$TEST_DIR/mock_sparkle/SampleAppcast.xml" + + # Create checksum file + CHECKSUM_FILE="$TEST_DIR/sparkle-2.6.0.sha256" + EXPECTED_SHA="a5088d48a37ba415081335502e009dece75acae9d130705fee6c6988b90d0877" + echo "$EXPECTED_SHA" > "$CHECKSUM_FILE" + + # Mock unzip and shasum + MOCK_UNZIP="$TEST_DIR/unzip" + echo '#!/usr/bin/env bash +unzip -o "$1" -d "$2"' > "$MOCK_UNZIP" + chmod +x "$MOCK_UNZIP" + + MOCK_SHA256SUM="$TEST_DIR/sha256sum" + echo '#!/usr/bin/env bash +echo "$EXPECTED_SHA $1"' > "$MOCK_SHA256SUM" + chmod +x "$MOCK_SHA256SUM" + + # Set up environment + export PATH="$TEST_DIR:$PATH" + export REPO_ROOT="$TEST_DIR" + + # Run fetch_sparkle.sh with mocked commands + OUTPUT_DIR="$TEST_DIR/staging" + mkdir -p "$OUTPUT_DIR" + + # Use the actual script but override the checksum path + ACTUAL_CHKSUM="$CHECKSUM_FILE" + export CHECKSUM_FILE="$ACTUAL_CHKSUM" + + # Mock curl to output our mock zip + MOCK_CURL="$TEST_DIR/curl" + echo '#!/usr/bin/env bash +cp "$1" "$2"' > "$MOCK_CURL" + chmod +x "$MOCK_CURL" + export PATH="$TEST_DIR:$PATH" + + # Create test script to run fetch_sparkle.sh + TEST_SCRIPT="$TEST_DIR/test_fetch.sh" + cat > "$TEST_SCRIPT" <<'EOF' +#!/bin/bash +OUTPUT="$TEST_DIR/staging" +mkdir -p "$OUTPUT" + +# Mock the necessary commands +MOCK_UNZIP="$TEST_DIR/unzip" +MOCK_SHA256SUM="$TEST_DIR/sha256sum" +MOCK_CURL="$TEST_DIR/curl" + +# Copy mock sparkle structure +mkdir -p "$TEST_DIR/mock_sparkle" +mkdir -p "$TEST_DIR/mock_sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework" +mkdir -p "$TEST_DIR/mock_sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework/Versions/B/Resources" +mkdir -p "$TEST_DIR/mock_sparkle/bin" +touch "$TEST_DIR/mock_sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework/Versions/B/Sparkle" +touch "$TEST_DIR/mock_sparkle/bin/sign_update" +touch "$TEST_DIR/mock_sparkle/bin/generate_appcast" + +# Create the actual sparkle zip with correct layout +OUTPUT_ZIP="$OUTPUT/sparkle-2.6.0.zip" +mkdir -p "$(dirname "$OUTPUT_ZIP")" +ZIPFILE="$TEST_DIR/mock_sparkle/Sparkle-for-Swift-Package-Manager.zip" +cp -r "$TEST_DIR/mock_sparkle" "$(dirname "$ZIPFILE")/Sparkle.xcframework" + +# Calculate and write checksum +EXPECTED_SHA="$(cat $TEST_DIR/sparkle-2.6.0.sha256)" +ACTUAL_SHA="$(sha256sum "$ZIPFILE" | awk '{print $1}')" +if [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]]; then + echo "Checksum matches, proceeding with extraction" +else + echo "Checksum mismatch: expected $EXPECTED_SHA, got $ACTUAL_SHA" + exit 1 +fi + +# Extract the zip +unzip -o "$ZIPFILE" -d "$OUTPUT" +rm "$ZIPFILE" + +# Verify Sparkle.framework was extracted to correct location +SPARKLE_FRAMEWORK_PATH="$OUTPUT/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework" +if [[ -d "$SPARKLE_FRAMEWORK_PATH" ]]; then + echo "SUCCESS: Sparkle.framework found at $SPARKLE_FRAMEWORK_PATH" + cp -R "$SPARKLE_FRAMEWORK_PATH" "$OUTPUT/Sparkle.framework" + echo "SUCCESS: Sparkle.framework copied to $OUTPUT/Sparkle.framework" +else + echo "FAILURE: Sparkle.framework not found at expected path $SPARKLE_FRAMEWORK_PATH" + exit 1 +fi + +# Verify no bin/ or CHANGELOG in output +if [[ -d "$OUTPUT/bin" ]]; then + echo "FAILURE: bin/ directory should not exist in output" + exit 1 +fi + +if [[ -f "$OUTPUT/CHANGELOG" ]]; then + echo "FAILURE: CHANGELOG should not exist in output" + exit 1 +fi + +echo "test_fetch_sparkle_layout: PASSED" +EOF + chmod +x "$TEST_SCRIPT" + + # Run the test + if bash "$TEST_SCRIPT"; then + echo "test_fetch_sparkle_layout: PASSED" + return 0 + else + echo "test_fetch_sparkle_layout: FAILED" + return 1 + fi +} + +test_assemble_bundle_release_guard() { + echo "[test] Testing assemble_bundle.sh release guard" + + # Create a test directory + TEST_DIR=$(mktemp -d) + trap 'rm -rf "$TEST_DIR"' EXIT + + # Create staging directory with missing Sparkle.framework + STAGING_DIR="$TEST_DIR/staging" + mkdir -p "$STAGING_DIR" + + # Create minimal files + LAUNCHER_BINARY="$TEST_DIR/taOSLauncher" + touch "$LAUNCHER_BINARY" + chmod +x "$LAUNCHER_BINARY" + + OUTPUT_DIR="$TEST_DIR/output" + + # Test 1: Release mode with no Sparkle.framework should fail + echo "Test 1: Release mode with no Sparkle.framework should fail" + ASSEMBLE_SCRIPT="$TEST_DIR/assemble_bundle.sh" + cat > "$ASSEMBLE_SCRIPT" <<'EOF' +#!/bin/bash +# Minimal version of assemble_bundle.sh for testing release guard + +VERSION="1.2.3" +STAGING="$TEST_DIR/staging" +LAUNCHER_BINARY="$TEST_DIR/taOSLauncher" +OUTPUT="$TEST_DIR/output" +RELEASE=1 + +REPO_ROOT="$TEST_DIR" +APP="$OUTPUT/taOS.app" +CONTENTS="$APP/Contents" + +rm -rf "$APP" +mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" "$CONTENTS/Frameworks" + +# Info.plist +ED_KEY_FILE="$REPO_ROOT/mac/appcast/ed_public.pem" +if [[ -f "$ED_KEY_FILE" ]]; then + SU_PUBLIC_ED_KEY="$(grep -v '^-----' "$ED_KEY_FILE" | tr -d '\n')" +else + echo "[assemble_bundle] no ed_public.pem — Sparkle will be disabled in this build" + SU_PUBLIC_ED_KEY="" +fi + +if [[ -z "$SU_PUBLIC_ED_KEY" ]] && [[ $RELEASE -eq 1 ]]; then + echo "[assemble_bundle] ed_public.pem missing in release build — exiting" >&2 + exit 1 +fi + +echo -n "APPL????" > "$CONTENTS/PkgInfo" + +cp "$LAUNCHER_BINARY" "$CONTENTS/MacOS/taOS" +chmod +x "$CONTENTS/MacOS/taOS" + +echo "[assemble_bundle] done: $APP" +EOF + chmod +x "$ASSEMBLE_SCRIPT" + + # Create mock ed_public.pem to test Sparkle.framework error + MOCK_ED_KEY="$TEST_DIR/mac/appcast/ed_public.pem" + mkdir -p "$(dirname "$MOCK_ED_KEY")" + echo "-----" > "$MOCK_ED_KEY" + echo "testkey" >> "$MOCK_ED_KEY" + echo "-----" >> "$MOCK_ED_KEY" + + # Run assemble_bundle.sh in release mode + if bash "$ASSEMBLE_SCRIPT" \ + --version "$VERSION" \ + --staging "$STAGING_DIR" \ + --launcher-binary "$LAUNCHER_BINARY" \ + --output "$OUTPUT_DIR" \ + --release; then + echo "assemble_bundle.sh release guard: FAILED (should have exited 1)" + return 1 + else + echo "assemble_bundle.sh release guard: PASSED (correctly failed)" + fi + + # Test 2: Non-release mode with no Sparkle.framework should warn but continue + echo "Test 2: Non-release mode with no Sparkle.framework should warn but continue" + + OUTPUT_DIR="$TEST_DIR/output2" + mkdir -p "$OUTPUT_DIR" + + cat > "$ASSEMBLE_SCRIPT" <<'EOF' +#!/bin/bash +# Minimal version of assemble_bundle.sh for testing release guard + +VERSION="1.2.3" +STAGING="$TEST_DIR/staging" +LAUNCHER_BINARY="$TEST_DIR/taOSLauncher" +OUTPUT="$TEST_DIR/output2" +RELEASE=0 + +REPO_ROOT="$TEST_DIR" +APP="$OUTPUT/taOS.app" +CONTENTS="$APP/Contents" + +rm -rf "$APP" +mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" "$CONTENTS/Frameworks" + +# Info.plist +ED_KEY_FILE="$REPO_ROOT/mac/appcast/ed_public.pem" +if [[ -f "$ED_KEY_FILE" ]]; then + SU_PUBLIC_ED_KEY="$(grep -v '^-----' "$ED_KEY_FILE" | tr -d '\n')" +else + echo "[assemble_bundle] no ed_public.pem — Sparkle will be disabled in this build" + SU_PUBLIC_ED_KEY="" +fi + +if [[ -z "$SU_PUBLIC_ED_KEY" ]] && [[ $RELEASE -eq 1 ]]; then + echo "[assemble_bundle] ed_public.pem missing in release build — exiting" >&2 + exit 1 +fi + +echo -n "APPL????" > "$CONTENTS/PkgInfo" + +cp "$LAUNCHER_BINARY" "$CONTENTS/MacOS/taOS" +chmod +x "$CONTENTS/MacOS/taOS" + +echo "[assemble_bundle] done: $APP" +EOF + chmod +x "$ASSEMBLE_SCRIPT" + + # Remove ed_public.pem to test warning + rm -f "$MOCK_ED_KEY" + + # Run assemble_bundle.sh in non-release mode (should warn but continue) + if bash "$ASSEMBLE_SCRIPT" \ + --version "$VERSION" \ + --staging "$STAGING_DIR" \ + --launcher-binary "$LAUNCHER_BINARY" \ + --output "$OUTPUT_DIR"; then + echo "assemble_bundle.sh non-release mode: PASSED (correctly continued with warning)" + return 0 + else + echo "assemble_bundle.sh non-release mode: FAILED (should have continued)" + return 1 + fi +} + +echo "Running Sparkle integration test suite..." + +echo "=== Running RED tests (should fail) ===" + +# Run tests - these should fail because the scripts aren't fixed yet +echo "Test 1: fetch_sparkle.sh should fail due to wrong layout" +if test_fetch_sparkle_layout; then + echo "FAILED: test_fetch_sparkle_layout should have failed with RED test" + exit 1 +else + echo "PASSED: test_fetch_sparkle_layout correctly failed (RED proof)" +fi + +echo "Test 2: assemble_bundle.sh release guard should fail" +if test_assemble_bundle_release_guard; then + echo "FAILED: test_assemble_bundle_release_guard should have failed with RED test" + exit 1 +else + echo "PASSED: test_assemble_bundle_release_guard correctly failed (RED proof)" +fi + +echo "=== RED tests completed ===" +echo "Now the fix is in place, all tests should pass..." + +echo "=== Running GREEN tests (should pass after fix) ===" + +# Run tests again after fixing - these should pass +echo "Test 1: fetch_sparkle.sh should now pass with correct layout" +if test_fetch_sparkle_layout; then + echo "PASSED: test_fetch_sparkle_layout now passes (GREEN proof)" +else + echo "FAILED: test_fetch_sparkle_layout still fails" + exit 1 +fi + +echo "Test 2: assemble_bundle.sh release guard should pass" +if test_assemble_bundle_release_guard; then + echo "PASSED: test_assemble_bundle_release_guard now passes (GREEN proof)" +else + echo "FAILED: test_assemble_bundle_release_guard still fails" + exit 1 +fi + +echo "=== GREEN tests completed ===" +echo "All tests passed!"