diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 004051f5b19..8dcf826595d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,8 @@ jobs: scripts/test-mobile-release-candidate-publisher.sh - name: Mobile worktree identity contract run: scripts/test-mobile-worktree-overrides.sh + - name: Desktop dev command contract + run: scripts/test-desktop-dev-command-contract.sh - name: Codex security review contract run: just security-review-check - name: Rust cache contract diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 05dc5553397..fb1c42a8ac4 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "xyz.block.buzz.app", "build": { "beforeDevCommand": { - "script": "exec ./node_modules/.bin/vite", + "script": "pnpm exec vite", "cwd": "..", "wait": false }, diff --git a/scripts/instance-env.sh b/scripts/instance-env.sh index bc185d98f18..f67b2b0791c 100755 --- a/scripts/instance-env.sh +++ b/scripts/instance-env.sh @@ -25,7 +25,16 @@ if [[ "${BUZZ_RESET_WEBVIEW_STATE:-0}" == "1" ]]; then DEV_URL="${DEV_URL}?resetDevState=1" fi -BUZZ_TAURI_CONFIG="{\"build\":{\"devUrl\":\"${DEV_URL}\",\"beforeDevCommand\":\"exec ./node_modules/.bin/vite --port ${BUZZ_VITE_PORT} --strictPort\"},\"identifier\":\"xyz.block.buzz.app.dev\",\"productName\":\"Buzz Dev\"}" +# Tauri runs this through `cmd /C` on Windows, which has no `exec` and cannot +# execute the extensionless `.bin/vite` shim. `pnpm exec` resolves the local +# Vite binary on every platform; elsewhere `exec` still hands the shell's +# process slot to pnpm so Tauri's Ctrl+C reaches the launched command. +case "${BUZZ_TEST_PLATFORM:-$(uname -s)}" in + MINGW*|MSYS*|CYGWIN*) VITE_LAUNCH="pnpm exec vite" ;; + *) VITE_LAUNCH="exec pnpm exec vite" ;; +esac + +BUZZ_TAURI_CONFIG="{\"build\":{\"devUrl\":\"${DEV_URL}\",\"beforeDevCommand\":{\"script\":\"${VITE_LAUNCH} --port ${BUZZ_VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}},\"identifier\":\"xyz.block.buzz.app.dev\",\"productName\":\"Buzz Dev\"}" unset VITE_DEV_BRANCH # In worktrees, extract a label from the branch name and derive a unique app @@ -86,10 +95,10 @@ if git rev-parse --is-inside-work-tree &>/dev/null; then GENERATE_DEV_ICON="$WORKTREE_ROOT/scripts/generate-dev-icon.swift" BASE_ICON="$WORKTREE_ROOT/desktop/src-tauri/icons/icon.icns" - if swift "$GENERATE_DEV_ICON" "$BASE_ICON" "$DEV_ICON" "$BUZZ_WORKTREE_LABEL"; then + if [[ "$(uname -s)" == Darwin ]] && command -v swift &>/dev/null && swift "$GENERATE_DEV_ICON" "$BASE_ICON" "$DEV_ICON" "$BUZZ_WORKTREE_LABEL"; then echo "🌳 Worktree: ${BUZZ_WORKTREE_LABEL}" export VITE_DEV_BRANCH="$BUZZ_WORKTREE_LABEL" - BUZZ_TAURI_CONFIG="{\"build\":{\"devUrl\":\"${DEV_URL}\",\"beforeDevCommand\":\"exec ./node_modules/.bin/vite --port ${BUZZ_VITE_PORT} --strictPort\"},\"identifier\":\"xyz.block.buzz.app.dev.${BUZZ_INSTANCE_SLUG}\",\"productName\":\"Buzz Dev (${BUZZ_WORKTREE_LABEL})\",\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}" + BUZZ_TAURI_CONFIG="{\"build\":{\"devUrl\":\"${DEV_URL}\",\"beforeDevCommand\":{\"script\":\"${VITE_LAUNCH} --port ${BUZZ_VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}},\"identifier\":\"xyz.block.buzz.app.dev.${BUZZ_INSTANCE_SLUG}\",\"productName\":\"Buzz Dev (${BUZZ_WORKTREE_LABEL})\",\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}" fi fi fi diff --git a/scripts/test-desktop-dev-command-contract.sh b/scripts/test-desktop-dev-command-contract.sh new file mode 100755 index 00000000000..fa5922b523b --- /dev/null +++ b/scripts/test-desktop-dev-command-contract.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + +assert_dev_command() { + node -e ' + const config = JSON.parse(process.argv[1]); + const expectedScript = process.argv[2]; + const command = config.build.beforeDevCommand; + if ( + typeof command !== "object" || + command.script !== expectedScript || + command.cwd !== ".." || + command.wait !== false + ) { + console.error("unexpected beforeDevCommand:", JSON.stringify(command)); + process.exit(1); + } + ' "$1" "$2" +} + +config_for_platform() { + local platform=$1 + ( + cd "$repo_root/desktop" + export BUZZ_TEST_PLATFORM="$platform" + source ../scripts/instance-env.sh >/dev/null 2>&1 + printf '%s' "$BUZZ_TAURI_CONFIG" + ) +} + +port_from_json() { + node -e ' + const config = JSON.parse(process.argv[1]); + process.stdout.write(new URL(config.build.devUrl).port); + ' "$1" +} + +base_config=$(cat "$repo_root/desktop/src-tauri/tauri.conf.json") +assert_dev_command "$base_config" "pnpm exec vite" + +windows_config=$(config_for_platform MINGW64_NT-10.0) +windows_port=$(port_from_json "$windows_config") +assert_dev_command "$windows_config" "pnpm exec vite --port ${windows_port} --strictPort" + +unix_config=$(config_for_platform Darwin) +unix_port=$(port_from_json "$unix_config") +assert_dev_command "$unix_config" "exec pnpm exec vite --port ${unix_port} --strictPort" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/bin" +cat > "$tmp/bin/git" <<'MOCK_GIT' +#!/usr/bin/env bash +case "$*" in + "rev-parse --show-toplevel") printf '%s\n' "$TEST_REPO_ROOT" ;; + "rev-parse --is-inside-work-tree") printf '%s\n' true ;; + "rev-parse --git-dir") printf '%s\n' "$TEST_REPO_ROOT/.git/worktrees/windows-test" ;; + "rev-parse --git-common-dir") printf '%s\n' "$TEST_REPO_ROOT/.git" ;; + "rev-parse --abbrev-ref HEAD") printf '%s\n' feature/windows-test ;; + *) exit 1 ;; +esac +MOCK_GIT +cat > "$tmp/bin/uname" <<'MOCK_UNAME' +#!/usr/bin/env bash +printf '%s\n' MINGW64_NT-10.0 +MOCK_UNAME +cat > "$tmp/bin/swift" <<'MOCK_SWIFT' +#!/usr/bin/env bash +touch "$SWIFT_CALLED_MARKER" +exit 1 +MOCK_SWIFT +chmod +x "$tmp/bin/git" "$tmp/bin/uname" "$tmp/bin/swift" + +export TEST_REPO_ROOT="$repo_root" +export SWIFT_CALLED_MARKER="$tmp/swift-called" +( + export PATH="$tmp/bin:$PATH" + cd "$repo_root/desktop" + source ../scripts/instance-env.sh >/dev/null 2>&1 +) +if [[ -e "$SWIFT_CALLED_MARKER" ]]; then + echo "expected Windows desktop setup to skip the macOS Swift icon generator" >&2 + exit 1 +fi + +echo "desktop dev command contract test passed"