From 7c9b92e07c1469fced1c8e74dbb7b3dc4b325de6 Mon Sep 17 00:00:00 2001 From: bowen628 Date: Sun, 22 Mar 2026 11:03:30 +0800 Subject: [PATCH] fix: Windows OpenSSL build and remote SSH workspace recovery - fix(build): Update openssl build method on Windows - fix(remote-ssh): update remote workspace recovery logic --- .github/workflows/ci.yml | 5 + .github/workflows/desktop-package.yml | 5 + .github/workflows/nightly.yml | 5 + CONTRIBUTING.md | 19 +-- CONTRIBUTING_CN.md | 19 +-- Cargo.toml | 6 +- README.md | 10 +- README.zh-CN.md | 10 +- package.json | 22 +-- scripts/ci/setup-openssl-windows.ps1 | 33 ++++ scripts/desktop-tauri-build.mjs | 50 ++++++ scripts/dev.cjs | 16 +- scripts/ensure-openssl-windows.mjs | 154 ++++++++++++++++++ src/crates/core/Cargo.toml | 5 + .../core/src/service/workspace/manager.rs | 19 ++- .../core/src/service/workspace/service.rs | 14 +- 16 files changed, 327 insertions(+), 65 deletions(-) create mode 100644 scripts/ci/setup-openssl-windows.ps1 create mode 100644 scripts/desktop-tauri-build.mjs create mode 100644 scripts/ensure-openssl-windows.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 896723db..554169be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup OpenSSL (Windows, prebuilt) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/ci/setup-openssl-windows.ps1 + - name: Download frontend build artifacts uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 2588cda5..376a5ba9 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -87,6 +87,11 @@ jobs: with: ref: ${{ needs.prepare.outputs.release_tag }} + - name: Setup OpenSSL (Windows, prebuilt) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/ci/setup-openssl-windows.ps1 + - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 016d7c91..a5e35a1d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -88,6 +88,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup OpenSSL (Windows, prebuilt) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/ci/setup-openssl-windows.ps1 + - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58a7b833..d0ebdff9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,21 +19,12 @@ Be respectful, kind, and constructive. We welcome contributors of all background #### Windows: OpenSSL Setup -The desktop app includes SSH remote support, which requires OpenSSL. On Windows, you need to provide pre-built OpenSSL binaries and set environment variables before building. +The desktop app includes SSH remote support, which pulls in OpenSSL. On Windows the workspace **does not use vendored OpenSSL**; link against **pre-built** binaries (no Perl/NASM/OpenSSL source build). -1. Download the [FireDaemon OpenSSL 3.5.5 LTS ZIP (x86+x64+ARM64)](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip) -2. Extract to a directory, e.g. `C:\Users\\openssl` -3. Set the following **user environment variables** (persist across terminal sessions): - -```powershell -[System.Environment]::SetEnvironmentVariable("OPENSSL_DIR", "C:\Users\\openssl\x64", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_NO_VENDOR", "1", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_STATIC", "1", "User") -``` - -4. Restart your terminal (or IDE) for the variables to take effect. - -> **Alternative**: Install [Strawberry Perl](https://strawberryperl.com/) to let Cargo build OpenSSL from source automatically — no environment variables needed. +- **Default**: `pnpm run desktop:dev` calls `ensure-openssl-windows.mjs` on Windows. Every `desktop:build*` script runs via `scripts/desktop-tauri-build.mjs`, which does the same before `tauri build` (first run downloads FireDaemon OpenSSL 3.5.5 into `.bitfun/cache/`; later runs reuse the cache). Extra args: `pnpm run desktop:build -- `. +- **Manual / CI**: Download the [FireDaemon OpenSSL 3.5.5 LTS ZIP](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip), extract, set `OPENSSL_DIR` to the `x64` folder, `OPENSSL_STATIC=1`, or run `scripts/ci/setup-openssl-windows.ps1`. +- **Opt out of auto-download**: `BITFUN_SKIP_OPENSSL_BOOTSTRAP=1` and configure `OPENSSL_DIR` yourself. +- **`desktop:dev:raw`** skips the dev script (no OpenSSL bootstrap); set `OPENSSL_DIR` yourself, run `scripts/ci/setup-openssl-windows.ps1`, or `node scripts/ensure-openssl-windows.mjs` (warms `.bitfun/cache/` and prints PowerShell `OPENSSL_*` lines to paste). ### Install dependencies diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index d3f04986..8a14f7c2 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -19,21 +19,12 @@ #### Windows:OpenSSL 配置 -桌面端包含 SSH 远程功能,该功能依赖 OpenSSL。在 Windows 上需要提供预编译的 OpenSSL 并设置环境变量,才能正常编译。 +桌面端包含 SSH 远程功能,会链接 OpenSSL。Windows 上**不使用 OpenSSL 源码编译(vendored)**,需使用**预编译**库。 -1. 下载 [FireDaemon OpenSSL 3.5.5 LTS ZIP(x86+x64+ARM64)](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip) -2. 解压到任意目录,例如 `C:\Users\<用户名>\openssl` -3. 在 PowerShell 中执行以下命令,将环境变量**永久写入用户环境**: - -```powershell -[System.Environment]::SetEnvironmentVariable("OPENSSL_DIR", "C:\Users\<用户名>\openssl\x64", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_NO_VENDOR", "1", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_STATIC", "1", "User") -``` - -4. 重启终端(或 IDE)使环境变量生效。 - -> **备选方案**:安装 [Strawberry Perl](https://strawberryperl.com/),Cargo 会自动从源码编译 OpenSSL,无需配置环境变量。 +- **默认**:Windows 下 `pnpm run desktop:dev` 会调用 `ensure-openssl-windows.mjs`;所有 `desktop:build*` 均通过 `scripts/desktop-tauri-build.mjs` 执行,在 `tauri build` 前做相同引导(首次下载到 `.bitfun/cache/`,之后走缓存)。额外参数:`pnpm run desktop:build -- `。 +- **手动 / CI**:下载 [FireDaemon ZIP](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip),解压后将 `OPENSSL_DIR` 指向 `x64`,并设 `OPENSSL_STATIC=1`,或运行 `scripts/ci/setup-openssl-windows.ps1`。 +- **关闭自动下载**:设置 `BITFUN_SKIP_OPENSSL_BOOTSTRAP=1` 并自行配置 `OPENSSL_DIR`。 +- **`desktop:dev:raw`** 不经过 `dev.cjs`(无 OpenSSL 引导);请自行设置 `OPENSSL_DIR`、运行 `scripts/ci/setup-openssl-windows.ps1`,或执行 `node scripts/ensure-openssl-windows.mjs`(会预热 `.bitfun/cache/` 并打印可在 PowerShell 中粘贴的 `OPENSSL_*` 命令)。 ### 安装依赖 diff --git a/Cargo.toml b/Cargo.toml index e6992fd6..feb0ccb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,10 +78,10 @@ flate2 = "1.0" toml = "0.8" # Git -git2 = { version = "0.18", default-features = false, features = ["https", "vendored-libgit2", "vendored-openssl"] } +git2 = { version = "0.18", default-features = false, features = ["https", "vendored-libgit2"] } -# OpenSSL — vendored so no system OpenSSL is needed (required by russh-keys on Windows) -openssl = { version = "0.10", features = ["vendored"] } +# OpenSSL — Linux/macOS: `bitfun-core` adds `vendored` via target cfg. Windows: link prebuilt (OPENSSL_DIR); see README. +openssl = { version = "0.10" } # Terminal portable-pty = "0.8" diff --git a/README.md b/README.md index a6d3166a..e0903237 100644 --- a/README.md +++ b/README.md @@ -111,15 +111,7 @@ Make sure you have the following prerequisites installed: - Rust toolchain (install via [rustup](https://rustup.rs/)) - [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for desktop development -**Windows only**: The desktop app includes SSH remote support which requires OpenSSL. Before building, download the [FireDaemon OpenSSL 3.5.5 LTS ZIP](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip), extract it, and set these environment variables in PowerShell: - -```powershell -[System.Environment]::SetEnvironmentVariable("OPENSSL_DIR", "C:\path\to\openssl\x64", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_NO_VENDOR", "1", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_STATIC", "1", "User") -``` - -Then restart your terminal before running the build commands. +**Windows only**: The desktop build links against a **prebuilt** OpenSSL (no OpenSSL source compile). `desktop:dev` and all `desktop:build*` scripts use `ensure-openssl-windows.mjs` (via `desktop-tauri-build.mjs` for builds): the first time OpenSSL is needed, it downloads [FireDaemon OpenSSL 3.5.5](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip) into `.bitfun/cache/`; later runs reuse that cache. Override with `OPENSSL_DIR` pointing at the **`x64`** folder from the ZIP, or `BITFUN_SKIP_OPENSSL_BOOTSTRAP=1` and your own `OPENSSL_*`. ```bash # Install dependencies diff --git a/README.zh-CN.md b/README.zh-CN.md index 8837ffb8..ccc47cff 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -114,15 +114,7 @@ Mini Apps 从对话中涌现,Skills 在社区里更新,Agent 在协作中进 - [Rust 工具链](https://rustup.rs/) - [Tauri 前置依赖](https://v2.tauri.app/start/prerequisites/)(桌面端开发需要) -**Windows 特别说明**:桌面端包含 SSH 远程功能,依赖 OpenSSL。构建前请下载 [FireDaemon OpenSSL 3.5.5 LTS ZIP](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip),解压后在 PowerShell 中执行: - -```powershell -[System.Environment]::SetEnvironmentVariable("OPENSSL_DIR", "C:\解压路径\openssl\x64", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_NO_VENDOR", "1", "User") -[System.Environment]::SetEnvironmentVariable("OPENSSL_STATIC", "1", "User") -``` - -执行后重启终端再运行构建命令。 +**Windows 特别说明**:桌面使用**预编译 OpenSSL**(不编译 OpenSSL 源码)。`desktop:dev` 与全部 `desktop:build*` 会通过 `ensure-openssl-windows.mjs`(构建走 `desktop-tauri-build.mjs`)自动准备:首次需要时下载 [FireDaemon OpenSSL 3.5.5](https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-3.5.5.zip) 到 `.bitfun/cache/`,之后复用缓存。可自行设置 `OPENSSL_DIR` 为 ZIP 内 **`x64`** 目录,或 `BITFUN_SKIP_OPENSSL_BOOTSTRAP=1` 并自行配置 `OPENSSL_*`。 ```bash # 安装依赖 diff --git a/package.json b/package.json index 4e259be4..9fcae716 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,17 @@ "preview": "pnpm --dir src/web-ui preview", "desktop:dev": "node scripts/dev.cjs desktop", "desktop:dev:raw": "cross-env-shell CI=true \"cd src/apps/desktop && tauri dev\"", - "desktop:build": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build\"", - "desktop:build:fast": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --debug --no-bundle\"", - "desktop:build:release-fast": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --no-bundle -- --profile release-fast\"", - "desktop:build:exe": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --no-bundle\"", - "desktop:build:nsis": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --bundles nsis\"", - "desktop:build:arm64": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --target aarch64-apple-darwin --bundles dmg\"", - "desktop:build:x86_64": "cross-env-shell CI=true \"cd src/apps/desktop && tauri build --target x86_64-apple-darwin --bundles dmg\"", - "desktop:build:linux": "cross-env-shell CI=true 'cd src/apps/desktop && tauri build'", - "desktop:build:linux:deb": "cross-env-shell CI=true 'cd src/apps/desktop && tauri build --bundles deb'", - "desktop:build:linux:rpm": "cross-env-shell CI=true 'cd src/apps/desktop && tauri build --bundles rpm'", - "desktop:build:linux:appimage": "cross-env-shell CI=true 'cd src/apps/desktop && tauri build --bundles appimage'", + "desktop:build": "node scripts/desktop-tauri-build.mjs", + "desktop:build:fast": "node scripts/desktop-tauri-build.mjs --debug --no-bundle", + "desktop:build:release-fast": "node scripts/desktop-tauri-build.mjs --no-bundle -- --profile release-fast", + "desktop:build:exe": "node scripts/desktop-tauri-build.mjs --no-bundle", + "desktop:build:nsis": "node scripts/desktop-tauri-build.mjs --bundles nsis", + "desktop:build:arm64": "node scripts/desktop-tauri-build.mjs --target aarch64-apple-darwin --bundles dmg", + "desktop:build:x86_64": "node scripts/desktop-tauri-build.mjs --target x86_64-apple-darwin --bundles dmg", + "desktop:build:linux": "node scripts/desktop-tauri-build.mjs", + "desktop:build:linux:deb": "node scripts/desktop-tauri-build.mjs --bundles deb", + "desktop:build:linux:rpm": "node scripts/desktop-tauri-build.mjs --bundles rpm", + "desktop:build:linux:appimage": "node scripts/desktop-tauri-build.mjs --bundles appimage", "installer:build": "pnpm --dir BitFun-Installer run installer:build", "installer:build:fast": "pnpm --dir BitFun-Installer run installer:build:fast", "installer:build:only": "pnpm --dir BitFun-Installer run installer:build:only", diff --git a/scripts/ci/setup-openssl-windows.ps1 b/scripts/ci/setup-openssl-windows.ps1 new file mode 100644 index 00000000..8c10cbc1 --- /dev/null +++ b/scripts/ci/setup-openssl-windows.ps1 @@ -0,0 +1,33 @@ +# Downloads FireDaemon OpenSSL (prebuilt) and sets OPENSSL_* for cargo/openssl-sys on Windows. +# Used by GitHub Actions; developers may run locally if paths are not already set. +# Keep $Version in sync with OPENSSL_VERSION in scripts/ensure-openssl-windows.mjs. +$ErrorActionPreference = "Stop" +$Version = "3.5.5" +$Url = "https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-$Version.zip" +$TempRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } +$Root = Join-Path $TempRoot "firedaemon-openssl" +$Zip = Join-Path $TempRoot "openssl-$Version.zip" + +New-Item -ItemType Directory -Force -Path $Root | Out-Null +if (-not (Test-Path $Zip)) { + Invoke-WebRequest -Uri $Url -OutFile $Zip +} +Expand-Archive -Path $Zip -DestinationPath $Root -Force + +$X64 = Join-Path $Root "x64" +$LibCrypto = Join-Path $X64 "lib\libcrypto.lib" +if (-not (Test-Path $LibCrypto)) { + throw "Expected prebuilt OpenSSL at $LibCrypto (extract layout changed?)" +} + +$Dir = (Resolve-Path $X64).Path +if ($env:GITHUB_ENV) { + Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_DIR=$Dir" + Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_LIB_DIR=$Dir\lib" + Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_STATIC=1" +} else { + $env:OPENSSL_DIR = $Dir + $env:OPENSSL_LIB_DIR = "$Dir\lib" + $env:OPENSSL_STATIC = "1" + Write-Host "Set OPENSSL_DIR=$Dir (current session only)" +} diff --git a/scripts/desktop-tauri-build.mjs b/scripts/desktop-tauri-build.mjs new file mode 100644 index 00000000..fdd9e673 --- /dev/null +++ b/scripts/desktop-tauri-build.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * Runs `tauri build` from src/apps/desktop with CI=true. + * On Windows: shared OpenSSL bootstrap (see ensure-openssl-windows.mjs). + */ +import { spawnSync } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import { ensureOpenSslWindows } from './ensure-openssl-windows.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, '..'); + +function tauriBuildArgsFromArgv() { + const args = process.argv.slice(2); + // `node script.mjs -- --foo` leaves a leading `--`; strip so `tauri build` sees the same argv as before. + let i = 0; + while (i < args.length && args[i] === '--') { + i += 1; + } + return args.slice(i); +} + +async function main() { + const forward = tauriBuildArgsFromArgv(); + + await ensureOpenSslWindows(); + + const desktopDir = join(ROOT, 'src', 'apps', 'desktop'); + // Tauri CLI reads CI and rejects numeric "1" (common in CI providers). + process.env.CI = 'true'; + + const r = spawnSync('pnpm', ['exec', 'tauri', 'build', ...forward], { + cwd: desktopDir, + env: process.env, + stdio: 'inherit', + shell: true, + }); + + if (r.error) { + console.error(r.error); + process.exit(1); + } + process.exit(r.status ?? 1); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 8fce888e..c5174cb8 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -7,6 +7,7 @@ const { execSync, spawn } = require('child_process'); const path = require('path'); +const { pathToFileURL } = require('url'); const { printHeader, printSuccess, @@ -177,7 +178,20 @@ async function main() { try { if (mode === 'desktop') { - await runCommand('npx tauri dev', path.join(ROOT_DIR, 'src/apps/desktop')); + if (process.platform === 'win32') { + printInfo('Windows: ensuring prebuilt OpenSSL (cached under .bitfun/cache/)'); + try { + const { ensureOpenSslWindows } = await import( + pathToFileURL(path.join(__dirname, 'ensure-openssl-windows.mjs')).href + ); + await ensureOpenSslWindows(); + } catch (error) { + printError('OpenSSL bootstrap failed'); + printError(error.message || String(error)); + process.exit(1); + } + } + await runCommand('pnpm exec tauri dev', path.join(ROOT_DIR, 'src/apps/desktop')); } else { await runCommand('pnpm exec vite', path.join(ROOT_DIR, 'src/web-ui')); } diff --git a/scripts/ensure-openssl-windows.mjs b/scripts/ensure-openssl-windows.mjs new file mode 100644 index 00000000..0dc8919f --- /dev/null +++ b/scripts/ensure-openssl-windows.mjs @@ -0,0 +1,154 @@ +/** + * Windows: ensure FireDaemon prebuilt OpenSSL for Cargo (russh / libgit2). + * - Cached under .bitfun/cache/firedaemon-openssl-/x64 (gitignored). + * - Skips download if OPENSSL_DIR already points at a valid tree, or cache hit, or BITFUN_SKIP_OPENSSL_BOOTSTRAP=1. + * Mutates `process.env` by default so child processes (tauri, cargo) inherit OPENSSL_*. + */ +import { spawnSync } from 'child_process'; +import { createWriteStream, existsSync, mkdirSync, realpathSync } from 'fs'; +import { pipeline } from 'stream/promises'; +import { Readable } from 'stream'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, '..'); +const LOG = '[bitfun-openssl]'; + +// Keep in sync with $Version in scripts/ci/setup-openssl-windows.ps1. +export const OPENSSL_VERSION = '3.5.5'; +const OPENSSL_URL = `https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-${OPENSSL_VERSION}.zip`; +export const CACHE_ROOT = join(ROOT, '.bitfun', 'cache', `firedaemon-openssl-${OPENSSL_VERSION}`); + +function libcryptoPath(opensslDir) { + return join(opensslDir, 'lib', 'libcrypto.lib'); +} + +function opensslDirLooksValid(dir) { + return Boolean(dir && existsSync(libcryptoPath(dir))); +} + +async function downloadToFile(url, filePath) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`OpenSSL download failed: HTTP ${res.status} ${res.statusText}`); + } + if (!res.body) { + throw new Error('OpenSSL download failed: empty body'); + } + await pipeline(Readable.fromWeb(res.body), createWriteStream(filePath)); +} + +function extractZipWindows(zipPath, destDir) { + const esc = (p) => p.replace(/'/g, "''"); + const ps = `Expand-Archive -LiteralPath '${esc(zipPath)}' -DestinationPath '${esc(destDir)}' -Force`; + const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { + stdio: 'inherit', + cwd: ROOT, + }); + if (r.error) { + throw r.error; + } + if (r.status !== 0) { + throw new Error('Expand-Archive failed (PowerShell)'); + } +} + +/** + * No-op on non-Windows. On Windows, sets process.env OPENSSL_DIR / OPENSSL_LIB_DIR / OPENSSL_STATIC when needed. + */ +export async function ensureOpenSslWindows() { + if (process.platform !== 'win32') { + return; + } + + if (process.env.BITFUN_SKIP_OPENSSL_BOOTSTRAP === '1') { + console.log(`${LOG} BITFUN_SKIP_OPENSSL_BOOTSTRAP=1, skipping bootstrap`); + return; + } + + if (opensslDirLooksValid(process.env.OPENSSL_DIR)) { + const dir = process.env.OPENSSL_DIR; + if (!process.env.OPENSSL_LIB_DIR) { + process.env.OPENSSL_LIB_DIR = join(dir, 'lib'); + } + if (!process.env.OPENSSL_STATIC) { + process.env.OPENSSL_STATIC = '1'; + } + console.log(`${LOG} Using existing OPENSSL_DIR:`, dir); + return; + } + + mkdirSync(CACHE_ROOT, { recursive: true }); + const x64 = join(CACHE_ROOT, 'x64'); + + if (existsSync(libcryptoPath(x64))) { + process.env.OPENSSL_DIR = x64; + process.env.OPENSSL_LIB_DIR = join(x64, 'lib'); + process.env.OPENSSL_STATIC = '1'; + console.log(`${LOG} Using cached OpenSSL:`, x64); + return; + } + + const zipFile = join(CACHE_ROOT, 'dist.zip'); + if (!existsSync(zipFile)) { + console.log(`${LOG} Downloading prebuilt OpenSSL (cached for future builds)...`); + await downloadToFile(OPENSSL_URL, zipFile); + } else { + console.log(`${LOG} Re-using cached dist.zip, extracting...`); + } + extractZipWindows(zipFile, CACHE_ROOT); + + if (!existsSync(libcryptoPath(x64))) { + throw new Error( + `${LOG} Unexpected layout after extract (missing ${libcryptoPath(x64)}). Delete ${CACHE_ROOT} and retry.`, + ); + } + + process.env.OPENSSL_DIR = x64; + process.env.OPENSSL_LIB_DIR = join(x64, 'lib'); + process.env.OPENSSL_STATIC = '1'; + console.log(`${LOG} OpenSSL ready:`, x64); +} + +function isExecutedAsCli() { + const entry = process.argv[1]; + if (!entry) return false; + try { + const selfPath = realpathSync(fileURLToPath(import.meta.url)); + const entryPath = realpathSync(entry); + return selfPath === entryPath; + } catch { + return false; + } +} + +function printShellEnvHint() { + const dir = process.env.OPENSSL_DIR; + const lib = process.env.OPENSSL_LIB_DIR; + if (!dir || !lib) { + console.log( + `${LOG} No OPENSSL_DIR set in this process (skipped or use your own install). Raw cargo needs these in the shell.`, + ); + return; + } + console.log(`${LOG} For shells that do not inherit Node env (e.g. raw cargo), run in PowerShell before build:`); + console.log(` $env:OPENSSL_DIR="${dir}"`); + console.log(` $env:OPENSSL_LIB_DIR="${lib}"`); + console.log(` $env:OPENSSL_STATIC="1"`); +} + +if (isExecutedAsCli()) { + ensureOpenSslWindows() + .then(() => { + if (process.platform !== 'win32') { + console.log(`${LOG} Not Windows; nothing to do.`); + return; + } + printShellEnvHint(); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); +} diff --git a/src/crates/core/Cargo.toml b/src/crates/core/Cargo.toml index 9a0ea9b1..a479bea8 100644 --- a/src/crates/core/Cargo.toml +++ b/src/crates/core/Cargo.toml @@ -127,6 +127,11 @@ bitfun-transport = { path = "../transport" } # Tauri dependency (optional, enabled only when needed) tauri = { workspace = true, optional = true } +# Non-Windows: vendored OpenSSL (no system install). Windows: prebuilt OpenSSL via OPENSSL_DIR (see README). +[target.'cfg(not(windows))'.dependencies] +git2 = { workspace = true, features = ["vendored-openssl"] } +openssl = { workspace = true, optional = true, features = ["vendored"] } + [target.'cfg(windows)'.dependencies] win32job = { workspace = true } diff --git a/src/crates/core/src/service/workspace/manager.rs b/src/crates/core/src/service/workspace/manager.rs index 067390fe..55b760aa 100644 --- a/src/crates/core/src/service/workspace/manager.rs +++ b/src/crates/core/src/service/workspace/manager.rs @@ -978,7 +978,8 @@ impl WorkspaceManager { } fn find_next_workspace_id_after_close(&self, preferred_kind: &WorkspaceKind) -> Option { - self.opened_workspace_ids + let same_kind = self + .opened_workspace_ids .iter() .find(|id| { self.workspaces @@ -986,8 +987,20 @@ impl WorkspaceManager { .map(|workspace| &workspace.workspace_kind == preferred_kind) .unwrap_or(false) }) - .cloned() - .or_else(|| self.opened_workspace_ids.first().cloned()) + .cloned(); + + if same_kind.is_some() { + return same_kind; + } + + // Closing the last remote workspace (e.g. SSH password session could not auto-reconnect) + // must not activate an unrelated local project; leave current unset until the user picks + // a workspace or reconnects. + if *preferred_kind == WorkspaceKind::Remote { + return None; + } + + self.opened_workspace_ids.first().cloned() } /// Ensures a workspace stays in the opened list. diff --git a/src/crates/core/src/service/workspace/service.rs b/src/crates/core/src/service/workspace/service.rs index d333d022..6ce0c5cc 100644 --- a/src/crates/core/src/service/workspace/service.rs +++ b/src/crates/core/src/service/workspace/service.rs @@ -1379,9 +1379,21 @@ impl WorkspaceService { async fn ensure_assistant_workspaces(&self) -> BitFunResult<()> { let descriptors = self.discover_assistant_workspaces().await?; let mut has_current_workspace = self.get_current_workspace().await.is_some(); + let has_opened_remote = { + let manager = self.manager.read().await; + manager + .get_opened_workspace_infos() + .iter() + .any(|w| w.workspace_kind == WorkspaceKind::Remote) + }; for descriptor in descriptors { - let should_activate = !has_current_workspace && descriptor.assistant_id.is_none(); + // If a remote workspace tab exists but nothing is current yet (e.g. pending SSH + // reconnect), do not auto-activate the default assistant workspace — that would look + // like a spurious new local workspace. + let should_activate = !has_current_workspace + && !has_opened_remote + && descriptor.assistant_id.is_none(); let options = WorkspaceCreateOptions { auto_set_current: should_activate, add_to_recent: false,