diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 537a2c2d..7effcc0a 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -224,3 +224,107 @@ jobs: frontend/src-tauri/target/release/bundle/appimage/*.AppImage frontend/src-tauri/target/release/bundle/deb/*.deb retention-days: 5 + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.2.2 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install sccache + run: | + choco install sccache --no-progress -y + sccache --version + + - name: Cache sccache + uses: actions/cache@v4 + with: + path: ~\AppData\Local\Mozilla\sccache + key: ${{ runner.os }}-sccache-windows-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-sccache-windows- + ${{ runner.os }}-sccache- + + - name: Provide ONNX Runtime (Windows) + shell: bash + run: | + ORT_VERSION=1.22.0 + ORT_ROOT="$RUNNER_TEMP/onnxruntime" + mkdir -p "$ORT_ROOT" + curl -fL --retry 5 --retry-delay 2 --retry-all-errors \ + "https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-win-x64-${ORT_VERSION}.zip" \ + -o "$ORT_ROOT/ort.zip" + unzip -q "$ORT_ROOT/ort.zip" -d "$ORT_ROOT" + echo "ORT_LIB_LOCATION=$ORT_ROOT/onnxruntime-win-x64-${ORT_VERSION}" >> "$GITHUB_ENV" + echo "ORT_SKIP_DOWNLOAD=true" >> "$GITHUB_ENV" + + - name: Install frontend dependencies + working-directory: ./frontend + run: bun install + + - name: Configure sccache + shell: bash + run: | + { + echo "RUSTC_WRAPPER=sccache" + echo "SCCACHE_CACHE_SIZE=2G" + } >> "$GITHUB_ENV" + + # Fork PRs do not receive TAURI_SIGNING_PRIVATE_KEY. When absent, write + # an unsigned config override so tauri does not attempt updater + # artifact signing after producing the NSIS installer. + - name: Detect signing capability + id: signing + env: + KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + shell: bash + run: | + if [ -z "$KEY" ]; then + echo "skip_signing=true" >> "$GITHUB_OUTPUT" + echo "::notice::TAURI_SIGNING_PRIVATE_KEY unavailable (likely fork PR); skipping updater artifact signing" + else + echo "skip_signing=false" >> "$GITHUB_OUTPUT" + fi + + - name: Write unsigned config override + if: steps.signing.outputs.skip_signing == 'true' + shell: bash + run: | + cat > frontend/src-tauri/tauri.unsigned.conf.json <<'EOF' + { + "bundle": { + "createUpdaterArtifacts": false + } + } + EOF + + - name: Build Tauri App (Windows) + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + VITE_OPEN_SECRET_API_URL: ${{ github.event_name == 'pull_request' && 'https://enclave.secretgpt.ai' || 'https://enclave.trymaple.ai' }} + VITE_MAPLE_BILLING_API_URL: ${{ github.event_name == 'pull_request' && 'https://billing-dev.opensecret.cloud' || 'https://billing.opensecret.cloud' }} + VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6 + with: + projectPath: './frontend' + args: --bundles nsis ${{ steps.signing.outputs.skip_signing == 'true' && '--config src-tauri/tauri.unsigned.conf.json' || '' }} + + - name: Show sccache stats + run: sccache --show-stats + + - name: Upload Windows Build + uses: actions/upload-artifact@v4 + with: + name: maple-windows-x64 + path: | + frontend/src-tauri/target/release/bundle/nsis/*.exe + retention-days: 5 diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index 9e75622f..6263b21e 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -11,38 +11,23 @@ "fs:default", { "identifier": "fs:allow-read-file", - "allow": [ - { "path": "$APPCONFIG/**" }, - { "path": "$HOME/.config/maple/**" } - ] + "allow": [{ "path": "$APPCONFIG/**" }] }, { "identifier": "fs:allow-write-file", - "allow": [ - { "path": "$APPCONFIG/**" }, - { "path": "$HOME/.config/maple/**" } - ] + "allow": [{ "path": "$APPCONFIG/**" }] }, { "identifier": "fs:allow-create", - "allow": [ - { "path": "$APPCONFIG/**" }, - { "path": "$HOME/.config/maple/**" } - ] + "allow": [{ "path": "$APPCONFIG/**" }] }, { "identifier": "fs:allow-exists", - "allow": [ - { "path": "$APPCONFIG/**" }, - { "path": "$HOME/.config/maple/**" } - ] + "allow": [{ "path": "$APPCONFIG/**" }] }, { "identifier": "fs:allow-mkdir", - "allow": [ - { "path": "$APPCONFIG" }, - { "path": "$HOME/.config/maple" } - ] + "allow": [{ "path": "$APPCONFIG" }] }, { "identifier": "opener:allow-open-url", diff --git a/frontend/src-tauri/src/proxy.rs b/frontend/src-tauri/src/proxy.rs index 1b205357..d101e297 100644 --- a/frontend/src-tauri/src/proxy.rs +++ b/frontend/src-tauri/src/proxy.rs @@ -64,6 +64,7 @@ impl ProxyState { #[tauri::command] pub async fn start_proxy( + app_handle: AppHandle, state: State<'_, ProxyState>, config: ProxyConfig, ) -> Result { @@ -132,7 +133,7 @@ pub async fn start_proxy( *running = true; // Save config to disk - if let Err(e) = save_proxy_config(&config).await { + if let Err(e) = save_proxy_config(&app_handle, &config).await { log::error!("Failed to save proxy config: {e}"); } @@ -185,15 +186,15 @@ pub async fn get_proxy_status(state: State<'_, ProxyState>) -> Result Result { - load_saved_proxy_config() +pub async fn load_proxy_config(app_handle: AppHandle) -> Result { + load_saved_proxy_config(&app_handle) .await .map_err(|e| format!("Failed to load proxy config: {e}")) } #[tauri::command] -pub async fn save_proxy_settings(config: ProxyConfig) -> Result<(), String> { - save_proxy_config(&config) +pub async fn save_proxy_settings(app_handle: AppHandle, config: ProxyConfig) -> Result<(), String> { + save_proxy_config(&app_handle, &config) .await .map_err(|e| format!("Failed to save proxy config: {e}")) } @@ -214,20 +215,24 @@ pub async fn test_proxy_port(host: String, port: u16) -> Result { } } -// Helper functions for config persistence -async fn get_config_path() -> Result { - // Use a hardcoded app name for the data directory - let app_name = "maple"; - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .map_err(|_| anyhow!("Failed to get home directory"))?; - - // Note: We use ~/.config on all platforms for simplicity. - // This works well on Linux and macOS (our currently supported platforms). - // While macOS traditionally uses ~/Library/Application Support, many modern - // cross-platform tools use ~/.config on macOS as well. - // If Windows support is added in the future, consider using %APPDATA% instead. - let app_dir = PathBuf::from(home_dir).join(".config").join(app_name); +// Helper functions for config persistence. +// Epic 2 (PR 3) will unify all platforms onto app_config_dir() and add atomic +// migration + keyring-backed secret storage. This minimal arm just unblocks +// Windows compile + launch. +async fn get_config_path(app_handle: &AppHandle) -> Result { + let app_dir = if cfg!(target_os = "windows") { + // Resolves to %APPDATA%\cloud.opensecret.maple\ (Roaming). + app_handle + .path() + .app_config_dir() + .map_err(|e| anyhow!("Failed to resolve app config dir: {e}"))? + } else { + // macOS/Linux: ~/.config/maple/ — unchanged for byte-identical behavior. + let app_name = "maple"; + let home_dir = + std::env::var("HOME").map_err(|_| anyhow!("Failed to get home directory"))?; + PathBuf::from(home_dir).join(".config").join(app_name) + }; // Ensure directory exists tokio::fs::create_dir_all(&app_dir).await?; @@ -235,8 +240,8 @@ async fn get_config_path() -> Result { Ok(app_dir.join("proxy_config.json")) } -async fn save_proxy_config(config: &ProxyConfig) -> Result<()> { - let path = get_config_path().await?; +async fn save_proxy_config(app_handle: &AppHandle, config: &ProxyConfig) -> Result<()> { + let path = get_config_path(app_handle).await?; let json = serde_json::to_string_pretty(config)?; // Write the config file @@ -253,8 +258,8 @@ async fn save_proxy_config(config: &ProxyConfig) -> Result<()> { Ok(()) } -async fn load_saved_proxy_config() -> Result { - let path = get_config_path().await?; +async fn load_saved_proxy_config(app_handle: &AppHandle) -> Result { + let path = get_config_path(app_handle).await?; if !path.exists() { return Ok(ProxyConfig::default()); @@ -268,7 +273,7 @@ async fn load_saved_proxy_config() -> Result { // Initialize proxy on app startup if auto_start is enabled pub async fn init_proxy_on_startup_simple(app_handle: AppHandle) -> Result<()> { // Load saved config - let config = load_saved_proxy_config().await?; + let config = load_saved_proxy_config(&app_handle).await?; // Check if auto-start is enabled and we have an API key if config.auto_start && !config.api_key.is_empty() { @@ -278,7 +283,7 @@ pub async fn init_proxy_on_startup_simple(app_handle: AppHandle) -> Result<()> { let proxy_state: tauri::State = app_handle.state(); // Try to start the proxy - match start_proxy(proxy_state, config.clone()).await { + match start_proxy(app_handle.clone(), proxy_state, config.clone()).await { Ok(_) => { log::info!( "Proxy auto-started successfully on {}:{}", diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 819b8bb4..9b0fc8fd 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -63,7 +63,7 @@ }, "bundle": { "active": true, - "targets": "all", + "targets": ["nsis", "deb", "appimage", "rpm", "dmg", "app"], "publisher": "OpenSecret", "icon": [ "icons/32x32.png", @@ -94,7 +94,13 @@ "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", - "timestampUrl": "" + "timestampUrl": "", + "webviewInstallMode": { + "type": "downloadBootstrapper" + }, + "nsis": { + "installMode": "currentUser" + } }, "createUpdaterArtifacts": true }