Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 94 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ jobs:
- os: macos-latest
label: macOS
preflight: false
- os: windows-latest
# Rust 1.78+ 生成的 Windows 测试运行器会使用 WaitOnAddress。
# 固定 Server 2022,减少 windows-latest/Server 2025 的镜像变量。
- os: windows-2022
label: Windows
preflight: true
- os: ubuntu-latest
Expand Down Expand Up @@ -72,6 +74,21 @@ jobs:
shell: pwsh
run: ./scripts/windows-preflight.ps1 -Toolchain msvc

- name: Check MSVC developer command prompt
if: runner.os == 'Windows'
shell: pwsh
run: |
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
$vsInstall = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $vsInstall) {
throw "Visual Studio C++ toolchain not found"
}
$vsDevCmd = Join-Path $vsInstall "Common7\Tools\VsDevCmd.bat"
if (-not (Test-Path $vsDevCmd)) {
throw "VsDevCmd.bat not found: $vsDevCmd"
}
Write-Host "[ok] VsDevCmd.bat -> $vsDevCmd"

- name: Check PowerShell scripts
if: matrix.preflight
shell: pwsh
Expand All @@ -89,18 +106,89 @@ jobs:
run: npm run build

- name: Check Tauri backend (cargo check)
if: runner.os != 'Windows'
run: cargo check --manifest-path src-tauri/Cargo.toml

- name: Check Tauri backend (cargo check, Windows MSVC)
if: runner.os == 'Windows'
shell: pwsh
run: |
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
$vsInstall = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
$vsDevCmd = Join-Path $vsInstall "Common7\Tools\VsDevCmd.bat"
$command = "call `"$vsDevCmd`" -arch=x64 -host_arch=x64 && cargo check --manifest-path src-tauri/Cargo.toml"
& cmd.exe /d /s /c $command
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

- name: Run Rust backend unit tests
if: runner.os != 'Windows'
run: cargo test --manifest-path src-tauri/Cargo.toml --lib

- name: Compile Rust backend unit tests (Windows)
# Windows runner 能链接 lib test binary,但干净镜像缺少可选 native runtime
# DLL entrypoint 时,进程会在 test harness 启动前退出。这里保留 cfg/link
# 覆盖;共享单测在 macOS / Linux 上实际执行。
- name: Run Rust backend unit tests (Windows)
if: runner.os == 'Windows'
run: cargo test --manifest-path src-tauri/Cargo.toml --lib --no-run
shell: pwsh
run: |
# 产品默认构建仍通过 cargo check 覆盖 Foundry 链接形态,单测门禁
# 用无 Foundry native 链接的构建来真正执行 Windows 后端测试。
# stable test harness 在 runner 上会直接导入
# api-ms-win-core-synch-l1-2-0.dll/WaitOnAddress 并在单测开始前
# loader 失败。先 no-run 生成测试 exe,把该 import 指向更常规
# 的 Kernel32.dll,再直接运行已修补的 harness,避免 cargo 重新链接。
$env:CARGO_TARGET_DIR = Join-Path (Get-Location) "src-tauri\target\windows-unit-tests"
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
$vsInstall = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
$vsDevCmd = Join-Path $vsInstall "Common7\Tools\VsDevCmd.bat"
$targetDir = $env:CARGO_TARGET_DIR
$buildCommand = "call `"$vsDevCmd`" -arch=x64 -host_arch=x64 && set `"CARGO_TARGET_DIR=$targetDir`" && cargo test --manifest-path src-tauri/Cargo.toml --lib --no-default-features --features custom-protocol --no-run"
& cmd.exe /d /s /c $buildCommand
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

$depsDir = Join-Path $env:CARGO_TARGET_DIR "debug\deps"
$needleText = "api-ms-win-core-synch-l1-2-0.dll" + [char]0
$replacementText = "kernel32.dll" + [char]0
$encoding = [System.Text.Encoding]::GetEncoding("iso-8859-1")
$needle = $encoding.GetBytes($needleText)
$replacement = New-Object byte[] $needle.Length
$replacementBytes = $encoding.GetBytes($replacementText)
[Array]::Copy($replacementBytes, $replacement, $replacementBytes.Length)
$patchedExes = @()
Get-ChildItem -Path $depsDir -Filter "openless_lib-*.exe" | ForEach-Object {
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
$text = $encoding.GetString($bytes)
$offset = $text.IndexOf($needleText, [System.StringComparison]::Ordinal)
$patchedThis = $false
while ($offset -ge 0) {
[Array]::Copy($replacement, 0, $bytes, $offset, $replacement.Length)
$patchedThis = $true
$offset = $text.IndexOf($needleText, $offset + $needle.Length, [System.StringComparison]::Ordinal)
}
if ($patchedThis) {
[System.IO.File]::WriteAllBytes($_.FullName, $bytes)
$patchedExes += $_.FullName
Write-Host "[ci] Patched Windows Rust test import to Kernel32.dll: $($_.FullName)"
}
}
if ($patchedExes.Count -eq 0) {
throw "api-ms-win-core-synch-l1-2-0.dll import not found in Windows Rust test executable."
}

foreach ($testExe in $patchedExes) {
$patchedText = $encoding.GetString([System.IO.File]::ReadAllBytes($testExe))
if ($patchedText.Contains($needleText)) {
throw "Windows Rust test executable still imports api-ms-win-core-synch-l1-2-0.dll: $testExe"
}
Write-Host "[ci] Running patched Windows Rust test executable: $testExe"
& $testExe
if ($LASTEXITCODE -ne 0) {
$unsignedExit = $LASTEXITCODE -band 0xffffffff
Write-Host ("[ci] Patched Windows Rust test executable failed with exit 0x{0:X8}" -f $unsignedExit)
exit $LASTEXITCODE
}
}

- name: Verify version sync across all 5 files
# 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。
Expand Down
5 changes: 3 additions & 2 deletions openless-all/app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ objc2-app-kit = "0.2"
libc = "0.2"

[target.'cfg(target_os = "windows")'.dependencies]
foundry-local-sdk = { version = "1.1.0", features = ["winml"] }
foundry-local-sdk = { version = "1.1.0", features = ["winml"], optional = true }
raw-window-handle = "0.6"
windows = { version = "0.58", features = [
"Win32_Foundation",
Expand All @@ -103,5 +103,6 @@ winreg = "0.52"
window-vibrancy = "0.7"

[features]
default = ["custom-protocol"]
default = ["custom-protocol", "foundry-local-runtime"]
custom-protocol = ["tauri/custom-protocol"]
foundry-local-runtime = ["dep:foundry-local-sdk"]
37 changes: 25 additions & 12 deletions openless-all/app/src-tauri/src/asr/local/foundry_runtime.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "foundry-local-runtime"))]
#[allow(dead_code)]
mod imp {
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -579,23 +579,27 @@ mod imp {
}
}

#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "foundry-local-runtime"))]
pub use imp::FoundryLocalRuntime;

#[cfg(not(target_os = "windows"))]
pub struct FoundryLocalRuntime;
#[cfg(any(not(target_os = "windows"), all(target_os = "windows", not(feature = "foundry-local-runtime"))))]
pub struct FoundryLocalRuntime {
cancel_prepare: std::sync::atomic::AtomicBool,
}

#[cfg(not(target_os = "windows"))]
#[cfg(any(not(target_os = "windows"), all(target_os = "windows", not(feature = "foundry-local-runtime"))))]
impl Default for FoundryLocalRuntime {
fn default() -> Self {
Self::new()
}
}

#[cfg(not(target_os = "windows"))]
#[cfg(any(not(target_os = "windows"), all(target_os = "windows", not(feature = "foundry-local-runtime"))))]
impl FoundryLocalRuntime {
pub fn new() -> Self {
Self
Self {
cancel_prepare: std::sync::atomic::AtomicBool::new(false),
}
}

pub async fn status_snapshot(
Expand All @@ -605,7 +609,7 @@ impl FoundryLocalRuntime {
) -> super::foundry::FoundryRuntimeStatus {
let mut status = super::foundry::FoundryRuntimeStatus::unavailable(
active_model.to_string(),
"Foundry Local Whisper is only available on Windows",
"Foundry Local Whisper is unavailable in this build",
);
status.runtime_source = super::foundry_native::normalize_runtime_source_str(runtime_source);
status
Expand All @@ -616,7 +620,7 @@ impl FoundryLocalRuntime {
alias: &str,
_runtime_source: &str,
) -> anyhow::Result<String> {
anyhow::bail!("Foundry Local Whisper is only available on Windows: {alias}");
anyhow::bail!("Foundry Local Whisper is unavailable in this build: {alias}");
}

pub async fn ensure_loaded_with_progress<F>(
Expand All @@ -628,10 +632,19 @@ impl FoundryLocalRuntime {
where
F: Fn(super::foundry::FoundryPrepareProgressPayload) + Send + Sync + 'static,
{
anyhow::bail!("Foundry Local Whisper is only available on Windows: {alias}");
anyhow::bail!("Foundry Local Whisper is unavailable in this build: {alias}");
}

pub fn request_cancel_prepare(&self) {
self.cancel_prepare
.store(true, std::sync::atomic::Ordering::SeqCst);
}

pub fn request_cancel_prepare(&self) {}
#[cfg(test)]
pub(crate) fn cancel_prepare_requested_for_tests(&self) -> bool {
self.cancel_prepare
.load(std::sync::atomic::Ordering::SeqCst)
}

pub async fn catalog_snapshot(
&self,
Expand All @@ -647,7 +660,7 @@ impl FoundryLocalRuntime {
_audio_path: &std::path::Path,
_audio_timeout: std::time::Duration,
) -> anyhow::Result<String> {
anyhow::bail!("Foundry Local Whisper is only available on Windows: {alias}");
anyhow::bail!("Foundry Local Whisper is unavailable in this build: {alias}");
}

pub async fn release_now(&self) -> anyhow::Result<()> {
Expand Down
Loading