diff --git a/docs/LOCAL_WEBKIT_PREVIEW.md b/docs/LOCAL_WEBKIT_PREVIEW.md new file mode 100644 index 00000000..c6446313 --- /dev/null +++ b/docs/LOCAL_WEBKIT_PREVIEW.md @@ -0,0 +1,16 @@ +# Local WebKit preview + +Pasted can run against a locally built WebKit on macOS without changing its ordinary build, release artifacts, or dependency graph. This path is intended for WebKit development and before/after demonstrations; it is not a distributable Pasted build. + +Build WebKit, install Pasted's locked dependencies, then pass the WebKit build-products directory to the preview launcher: + +```sh +npm ci +npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release +``` + +The launcher builds Pasted's normal debug executable and CLI, starts Vite on loopback, and launches the executable directly with framework and XPC loader paths pointed at the supplied build. It uses `vmmap` to verify that the Pasted process actually loaded that `WebKit.framework`; a successful application launch without that verification is treated as a failure. + +For recording safety, the launcher creates and seeds a temporary demonstration database, starts the preview with clipboard capture paused, and deletes that temporary database when the preview exits. It never opens or modifies the ordinary Pasted library. Quit Pasted to stop the preview and its Vite server. Set `PASTED_LOCAL_WEBKIT_SKIP_BUILD=1` to reuse the existing debug executables during repeated recording sessions. + +The launcher does not bundle WebKit, modify Pasted's release configuration, replace the system framework, or alter the release build's database selection. The database override is compiled only into debug builds. diff --git a/package.json b/package.json index 88001d1f..e7ebd874 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:local-webkit": "bash scripts/run-with-local-webkit.sh", "build": "tsc && vite build", "preview": "vite preview", "test": "cargo test --manifest-path src-tauri/Cargo.toml", @@ -29,7 +30,7 @@ "test:transformations": "node --experimental-strip-types scripts/test-manual-transform-step-model.ts && node scripts/audit-transformations.js", "test:activity": "node scripts/audit-activity-events.js", "test:backup": "node --experimental-strip-types scripts/test-backup-client-state.js && node --experimental-strip-types scripts/test-app-ui-state.ts && node scripts/audit-full-backup.js", - "test:platform": "node scripts/audit-platform-chrome.js", + "test:platform": "node scripts/test-local-webkit-preview.js && node scripts/audit-platform-chrome.js", "test:feedback": "node scripts/audit-capture-feedback.js", "test:app-lock": "node --experimental-strip-types scripts/test-app-lock-idle.ts", "bench:analysis": "cargo run --release --manifest-path src-tauri/Cargo.toml --example analysis_baseline --", diff --git a/scripts/run-with-local-webkit.sh b/scripts/run-with-local-webkit.sh new file mode 100755 index 00000000..d166f5c5 --- /dev/null +++ b/scripts/run-with-local-webkit.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release + +Builds the Pasted debug application, starts its Vite frontend, and launches the +application directly with a local WebKit build selected for both the app and its +XPC services. The script exits unless vmmap confirms that Pasted loaded that +WebKit.framework instead of the system framework. It uses a temporary, seeded +database and pauses clipboard capture so private Pasted history cannot appear. + +Set PASTED_LOCAL_WEBKIT_SKIP_BUILD=1 to reuse an existing debug application. +EOF +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "A local WebKit preview can only run on macOS." >&2 + exit 1 +fi + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 1 +fi + +webkit_build="$1" +if [[ "$webkit_build" != /* ]]; then + echo "Pass an absolute WebKit build directory." >&2 + exit 1 +fi + +webkit_binary="$webkit_build/WebKit.framework/Versions/A/WebKit" +if [[ ! -f "$webkit_binary" ]]; then + echo "WebKit.framework was not found at $webkit_build." >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +app_binary="$repo_root/src-tauri/target/debug/pasted-app" +cli_binary="$repo_root/src-tauri/target/debug/pasted" +preview_parent="${TMPDIR:-/tmp}" +preview_parent="${preview_parent%/}" +vite_pid="" +app_pid="" +preview_root="" + +cleanup() { + local status=$? + trap - EXIT INT TERM HUP + if [[ -n "$app_pid" ]] && kill -0 "$app_pid" 2>/dev/null; then + kill "$app_pid" 2>/dev/null || true + wait "$app_pid" 2>/dev/null || true + fi + if [[ -n "$vite_pid" ]] && kill -0 "$vite_pid" 2>/dev/null; then + kill "$vite_pid" 2>/dev/null || true + wait "$vite_pid" 2>/dev/null || true + fi + if [[ -n "$preview_root" && "$preview_root" == "$preview_parent/pasted-local-webkit."* ]]; then + rm -rf -- "$preview_root" + fi + exit "$status" +} +trap cleanup EXIT INT TERM HUP + +cd "$repo_root" + +if [[ "${PASTED_LOCAL_WEBKIT_SKIP_BUILD:-0}" != "1" ]]; then + if [[ ! -d node_modules ]]; then + echo "Install the locked frontend dependencies with npm ci first." >&2 + exit 1 + fi + cargo build --locked --manifest-path src-tauri/Cargo.toml --bin pasted-app + cargo build --locked --manifest-path src-tauri/Cargo.toml --no-default-features --features cli --bin pasted +fi + +if [[ ! -x "$app_binary" || ! -x "$cli_binary" ]]; then + echo "Pasted's debug application and CLI were not found in src-tauri/target/debug." >&2 + exit 1 +fi + +preview_root="$(mktemp -d "$preview_parent/pasted-local-webkit.XXXXXX")" +preview_database="$preview_root/pasted.db" +preview_seed="$preview_root/demo-clips.csv" +printf '%s\n' \ + 'id,content_type,source,is_pinned,created_at,name,text_content' \ + '1,"text","Pasted Demo",true,"2026-09-01T18:00:00Z","Release engineering","Ship it, but make the pixels behave first."' \ + '2,"text","Pasted Demo",false,"2026-09-01T18:01:00Z","WebKit field notes","Yellow and cyan have agreed to remain perfectly still."' \ + '3,"text","Pasted Demo",false,"2026-09-01T18:02:00Z","Important research","Can a window corner outrun a layout viewport? Not anymore."' \ + '4,"text","Pasted Demo",false,"2026-09-01T18:03:00Z","Extremely official memo","This clipboard contains zero secrets and several excellent rectangles."' \ + > "$preview_seed" +PASTED_DATABASE_PATH="$preview_database" \ + "$cli_binary" clip import "$preview_seed" --format csv >/dev/null + +npm run dev -- --host 127.0.0.1 & +vite_pid=$! + +vite_ready=false +for _ in {1..100}; do + if curl --silent --fail --output /dev/null http://127.0.0.1:1420/; then + vite_ready=true + break + fi + if ! kill -0 "$vite_pid" 2>/dev/null; then + wait "$vite_pid" + fi + sleep 0.1 +done + +if [[ "$vite_ready" != "true" ]]; then + echo "Vite did not become ready at http://127.0.0.1:1420/." >&2 + exit 1 +fi + +env \ + DYLD_FRAMEWORK_PATH="$webkit_build" \ + __XPC_DYLD_FRAMEWORK_PATH="$webkit_build" \ + DYLD_LIBRARY_PATH="$webkit_build" \ + __XPC_DYLD_LIBRARY_PATH="$webkit_build" \ + PASTED_PREVIEW_DATABASE_PATH="$preview_database" \ + "$app_binary" --skip-welcome & +app_pid=$! + +verified_path="" +alternate_build="${webkit_build#/private}" +for _ in {1..100}; do + if ! kill -0 "$app_pid" 2>/dev/null; then + wait "$app_pid" + fi + loaded_webkit="$(vmmap "$app_pid" 2>/dev/null | sed -n '/\/WebKit\.framework\/.*\/WebKit$/p' || true)" + if [[ "$loaded_webkit" == *"$webkit_build/WebKit.framework/"* \ + || "$loaded_webkit" == *"$alternate_build/WebKit.framework/"* ]]; then + verified_path="$loaded_webkit" + break + fi + sleep 0.1 +done + +if [[ -z "$verified_path" ]]; then + echo "Pasted launched, but vmmap did not confirm the requested WebKit.framework." >&2 + exit 1 +fi + +echo "Verified local WebKit.framework:" +echo "$verified_path" +echo "Quit Pasted to stop the preview and its Vite server." + +wait "$app_pid" +app_pid="" diff --git a/scripts/test-local-webkit-preview.js b/scripts/test-local-webkit-preview.js new file mode 100644 index 00000000..dfd69e9c --- /dev/null +++ b/scripts/test-local-webkit-preview.js @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { spawnSync } from 'node:child_process'; + +const scriptPath = 'scripts/run-with-local-webkit.sh'; +const source = fs.readFileSync(scriptPath, 'utf8'); + +const syntax = spawnSync('bash', ['-n', scriptPath], { encoding: 'utf8' }); +assert.equal(syntax.status, 0, syntax.stderr); + +const help = spawnSync('bash', [scriptPath, '--help'], { encoding: 'utf8' }); +assert.equal(help.status, 0, help.stderr); +assert.match(help.stdout, /WebKitBuild\/Release/); +assert.match(help.stdout, /vmmap confirms/); +assert.match(help.stdout, /temporary, seeded/); + +for (const variable of [ + 'DYLD_FRAMEWORK_PATH', + '__XPC_DYLD_FRAMEWORK_PATH', + 'DYLD_LIBRARY_PATH', + '__XPC_DYLD_LIBRARY_PATH', +]) { + assert.match(source, new RegExp(`\\b${variable}=`), `${variable} must remain in the preview process environment`); +} + +assert.match(source, /cargo build --locked/, 'The preview must build from the reviewed Rust lockfile'); +assert.match(source, /--no-default-features --features cli --bin pasted/, 'The preview must build the CLI used to seed its isolated database'); +assert.match(source, /npm run dev -- --host 127\.0\.0\.1/, 'The preview must bind Vite to loopback'); +assert.match(source, /vmmap "\$app_pid"/, 'The preview must inspect the launched Pasted process'); +assert.match(source, /mktemp -d/, 'The preview must isolate its demonstration database in a temporary directory'); +assert.match(source, /PASTED_PREVIEW_DATABASE_PATH="\$preview_database"/, 'The GUI must use the isolated preview database'); +assert.match(source, /PASTED_DATABASE_PATH="\$preview_database"/, 'The CLI must seed the same isolated preview database'); +assert.match(source, /rm -rf -- "\$preview_root"/, 'The temporary preview database must be removed during cleanup'); +assert.match(source, /kill "\$app_pid"/, 'Interrupted previews must stop the launched application'); +assert.match(source, /kill "\$vite_pid"/, 'Interrupted previews must stop the Vite server'); + +console.log('Local WebKit preview launcher checks passed.'); diff --git a/src-tauri/src/app_runtime.rs b/src-tauri/src/app_runtime.rs index 13624710..250a0473 100644 --- a/src-tauri/src/app_runtime.rs +++ b/src-tauri/src/app_runtime.rs @@ -6,6 +6,16 @@ use tauri::{Emitter, Manager}; static EXIT_REQUESTED: AtomicBool = AtomicBool::new(false); +#[cfg(debug_assertions)] +fn preview_database_path() -> Option { + std::env::var_os("PASTED_PREVIEW_DATABASE_PATH").map(Into::into) +} + +#[cfg(not(debug_assertions))] +fn preview_database_path() -> Option { + None +} + pub(crate) fn exit_requested() -> bool { EXIT_REQUESTED.load(Ordering::SeqCst) } @@ -68,7 +78,10 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box Result<(), Box, seq_state: Arc, ocr_service: Arc, + initially_paused: bool, ) -> MonitorHandle { let running = Arc::new(AtomicBool::new(true)); let running_clone = running.clone(); - let is_manually_paused = Arc::new(AtomicBool::new(false)); + let is_manually_paused = Arc::new(AtomicBool::new(initially_paused)); let is_auto_paused = Arc::new(AtomicBool::new(false)); let is_manually_paused_clone = is_manually_paused.clone();