diff --git a/src/ui/Features/Video/SpeechToText/MissingSharedLibrary.cs b/src/ui/Features/Video/SpeechToText/MissingSharedLibrary.cs
index 2cc037b70ec..9e3dec8bfeb 100644
--- a/src/ui/Features/Video/SpeechToText/MissingSharedLibrary.cs
+++ b/src/ui/Features/Video/SpeechToText/MissingSharedLibrary.cs
@@ -45,6 +45,46 @@ public static class MissingSharedLibrary
return name.Length == 0 ? null : name;
}
+ ///
+ /// True for libraries that are part of an engine download rather than something the user
+ /// installs from their distro. A missing one means the engine folder is incomplete - which
+ /// is what happened to the whisper.cpp Linux archives in issue #13680, where whisper-cli
+ /// shipped without the libwhisper.so.1 and libggml.so.0 it links against.
+ ///
+ public static bool IsBundledWithEngine(string? libraryName)
+ {
+ if (string.IsNullOrEmpty(libraryName))
+ {
+ return false;
+ }
+
+ // dyld reports the full install name, e.g. "@rpath/libwhisper.1.dylib"
+ var name = libraryName;
+ var slash = name.LastIndexOfAny(new[] { '/', '\\' });
+ if (slash >= 0)
+ {
+ name = name.Substring(slash + 1);
+ }
+
+ foreach (var prefix in BundledLibraryPrefixes)
+ {
+ if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static readonly string[] BundledLibraryPrefixes =
+ {
+ "libwhisper", // whisper.cpp
+ "libggml", // whisper.cpp / qwen3-asr.cpp / chatllm.cpp - the ggml core and its backends
+ "libllama", // llama.cpp based engines
+ "libmtmd", // llama.cpp multimodal helper
+ };
+
private static string? GetAfterMarker(string line, string marker)
{
var index = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs
index dee36de04ac..19de080f3aa 100644
--- a/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs
+++ b/src/ui/Features/Video/SpeechToText/SpeechToTextViewModel.cs
@@ -946,6 +946,18 @@ private void ProcessChatLlmTranscription(SeAudioToText settings, ChatLlmCppEngin
///
private static string GetMissingSharedLibraryMessage(string libraryName)
{
+ // Libraries that ship inside the engine download itself. Telling the user to install
+ // these with their package manager is a dead end - no distro packages them, and the
+ // real cause is a bad or incomplete engine folder (issue #13680).
+ if (MissingSharedLibrary.IsBundledWithEngine(libraryName))
+ {
+ return
+ $"The speech to text engine could not start - the shared library \"{libraryName}\" is missing.{Environment.NewLine}{Environment.NewLine}" +
+ "This library is part of the engine download, so the installed engine is incomplete." +
+ $"{Environment.NewLine}{Environment.NewLine}" +
+ "Re-download the engine (Download button next to the engine) and try again.";
+ }
+
var message =
$"The speech to text engine could not start - the shared library \"{libraryName}\" is missing.{Environment.NewLine}{Environment.NewLine}" +
"Install it with your package manager and try again.";
@@ -3674,6 +3686,26 @@ private static Process StartEngineProcess(string executable, string arguments, D
return p;
}
+ ///
+ /// Puts the engine folder on the dynamic loader's search path, so an engine that ships its
+ /// own shared libraries next to the executable can find them.
+ ///
+ /// Windows resolves DLLs from the executable's own directory, so this is a Linux/macOS-only
+ /// concern - and there setting WorkingDirectory is not enough, because the loader does not
+ /// search the working directory. The libraries are supposed to carry an $ORIGIN/@loader_path
+ /// RPATH that makes this unnecessary, but whisper.cpp archives shipped for four releases
+ /// without one (issue #13680), so set it as a belt-and-braces measure.
+ ///
+ private static void AddEngineFolderToLibrarySearchPath(ProcessStartInfo startInfo, string engineFolder)
+ {
+ var variable = OperatingSystem.IsMacOS() ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH";
+ var existing = startInfo.EnvironmentVariables[variable];
+
+ startInfo.EnvironmentVariables[variable] = string.IsNullOrEmpty(existing)
+ ? engineFolder
+ : engineFolder + Path.PathSeparator + existing;
+ }
+
private Process GetWhisperProcess(
ISpeechToTextEngine engine,
string waveFileName,
@@ -3911,6 +3943,10 @@ or WhisperChoice.CppVulkan or WhisperChoice.CppCuBlasLib or WhisperChoice.ConstM
process.StartInfo.EnvironmentVariables["Path"]?.TrimEnd(';') + ";" + whisperFolder;
}
}
+ else if (!string.IsNullOrEmpty(whisperFolder))
+ {
+ AddEngineFolderToLibrarySearchPath(process.StartInfo, whisperFolder);
+ }
if (settings.WhisperChoice != WhisperChoice.Cpp &&
settings.WhisperChoice != WhisperChoice.CppCuBlas &&
diff --git a/src/ui/Logic/Download/DownloadHashManager.cs b/src/ui/Logic/Download/DownloadHashManager.cs
index f94a35722bb..506fa60ede1 100644
--- a/src/ui/Logic/Download/DownloadHashManager.cs
+++ b/src/ui/Logic/Download/DownloadHashManager.cs
@@ -297,12 +297,17 @@ public static class WhisperCpp
// Hashes of the unpacked main executable (whisper-cli / whisper-cli.exe) — used to detect
// the installed version when no sidecar is present (e.g. installs from older SE builds).
- // Linux Vulkan and Linux CUDA produce identical whisper-cli binaries (the backend lives in
- // libggml-*.so), so executable-hash fallback is intentionally Windows + Mac only.
+ // The Linux Vulkan and CUDA builds shipped an identical whisper-cli up to v1.8.4 (the
+ // backend lives in libggml-*.so), and they have differed since v1.8.5. Either way the
+ // lookup is unambiguous: the key comes from the engine choice being asked about, and each
+ // backend has its own install folder, so a hash shared by both lists is simply listed
+ // under both.
public const string WindowsBlasExecutable = "WhisperCpp.Windows.Blas.Executable";
public const string WindowsCuBlasExecutable = "WhisperCpp.Windows.CuBlas.Executable";
public const string WindowsVulkanExecutable = "WhisperCpp.Windows.Vulkan.Executable";
public const string MacOsExecutable = "WhisperCpp.MacOs.Executable";
+ public const string LinuxVulkanExecutable = "WhisperCpp.Linux.Vulkan.Executable";
+ public const string LinuxCudaExecutable = "WhisperCpp.Linux.Cuda.Executable";
}
// For each key, hashes are ordered newest-first. Index 0 is the latest known release.
@@ -1818,19 +1823,25 @@ public static class WhisperCpp
"49ef4acfaef0b4989885c258f22eb1355592c5f343897508899a2b598cd683bf", // whispercpp-185 / v1.8.5
"81dbd530f21a6daf10b1c9cece61d7e56d774e3bcb3d21af4d17299caa532a4d", // whispercpp-184 / v1.8.4
},
+ // whispercpp-184 through -191 shipped whisper-cli without the libwhisper.so.1 /
+ // libggml.so.0 it links against, so every one of those installs is broken and the
+ // user needs the -r2 rebuild (issue #13680). They stay listed so an existing install
+ // is still identified rather than reported as unknown.
[WhisperCpp.LinuxVulkan] = new[]
{
- "7969c5a0ba912d0b0d8aaa2bdf911ca7896ef97a89e293d2596a96022c839e80", // whispercpp-191 / v1.9.1 (current download URL)
- "10aed3a2b28e5ad40fee8267d554f0824943e50e75bfe7bceb7c16b6e3fe8a45", // whispercpp-186 / v1.8.6
- "c385a01228f85764cfd9b6072e078d03e44f151d71ab145e912425e5cc9a7c8e", // whispercpp-185 / v1.8.5
- "7a7d131b5fbb605fef6a8d39b6f2480480d8a26ec8a1dbec3b3740485023158d", // whispercpp-184 / v1.8.4
+ "0943f97f58ca98aafa26659e7c4d8d87f474c74f966f0d9ae91491989a32cf2b", // whispercpp-191-r2 / v1.9.1 (current download URL)
+ "7969c5a0ba912d0b0d8aaa2bdf911ca7896ef97a89e293d2596a96022c839e80", // whispercpp-191 / v1.9.1 (missing shared libraries)
+ "10aed3a2b28e5ad40fee8267d554f0824943e50e75bfe7bceb7c16b6e3fe8a45", // whispercpp-186 / v1.8.6 (missing shared libraries)
+ "c385a01228f85764cfd9b6072e078d03e44f151d71ab145e912425e5cc9a7c8e", // whispercpp-185 / v1.8.5 (missing shared libraries)
+ "7a7d131b5fbb605fef6a8d39b6f2480480d8a26ec8a1dbec3b3740485023158d", // whispercpp-184 / v1.8.4 (missing shared libraries)
},
[WhisperCpp.LinuxCuda] = new[]
{
- "19a232255838c77c9bcddf220292d96dfb62b9a8da1e66ed402961f6a41b1661", // whispercpp-191 / v1.9.1 (current download URL)
- "b8922f7fc25ff4f602c887655882c4114c005177017335f39181cc0417f0cb03", // whispercpp-186 / v1.8.6
- "1aee5fddee30f8486275d3b2bd1d568e92615e7b1b063d46c635cb9f44d7d13b", // whispercpp-185 / v1.8.5
- "708ea1c502ac5082d4eb9afc86c8adb9d67d76da25e70fd81cb6fae2cfcf00ce", // whispercpp-184 / v1.8.4
+ "02129bac653d00d85ef110c24c37eb56d5470d4fee432a09f55ac9fbc63ff768", // whispercpp-191-r2 / v1.9.1 (current download URL)
+ "19a232255838c77c9bcddf220292d96dfb62b9a8da1e66ed402961f6a41b1661", // whispercpp-191 / v1.9.1 (missing shared libraries)
+ "b8922f7fc25ff4f602c887655882c4114c005177017335f39181cc0417f0cb03", // whispercpp-186 / v1.8.6 (missing shared libraries)
+ "1aee5fddee30f8486275d3b2bd1d568e92615e7b1b063d46c635cb9f44d7d13b", // whispercpp-185 / v1.8.5 (missing shared libraries)
+ "708ea1c502ac5082d4eb9afc86c8adb9d67d76da25e70fd81cb6fae2cfcf00ce", // whispercpp-184 / v1.8.4 (missing shared libraries)
},
// SHA-256 of whisper-cli / whisper-cli.exe extracted from each archive above.
@@ -1862,6 +1873,25 @@ public static class WhisperCpp
"0fd752e0384484eb3a72ce644135f20963879e80624b23f7f739eda187a23359", // whispercpp-185 / v1.8.5
"11d902af004d1e79538f8f801b4a42ac3a21094370c9063f67d885d04dccdd96", // whispercpp-184 / v1.8.4
},
+
+ // The v1.8.4 hash is the same in both Linux lists on purpose - that release shipped a
+ // single whisper-cli for both backends. See the note on LinuxVulkanExecutable.
+ [WhisperCpp.LinuxVulkanExecutable] = new[]
+ {
+ "da61c0c1910c103cf8dea855855f072249689ee2310f375e2b79615cbd012c05", // whispercpp-191-r2 / v1.9.1 (current download URL)
+ "782fac61b9bcfe8f6db22564bb5a2cda2c22550b9ef38064ec5f188bc86dfe79", // whispercpp-191 / v1.9.1 (missing shared libraries)
+ "5a2343777fe57327c8956d836d1515ac422d2e7f9fd33ce4e7e62cfe4cd33cbd", // whispercpp-186 / v1.8.6 (missing shared libraries)
+ "e9d5318d513f9dc23ae9fe35cd2783e8def5c66642079dc6a5074b9e5f0cd61c", // whispercpp-185 / v1.8.5 (missing shared libraries)
+ "315f46514fc09a4fefe2f7b6ae95cfac5441a03b8be04eed1b5f567d5ccc38de", // whispercpp-184 / v1.8.4 (missing shared libraries)
+ },
+ [WhisperCpp.LinuxCudaExecutable] = new[]
+ {
+ "3a4d717745c2d8cf19ca7a954c29c0349a7f158d93a6597bb86e155f0735474a", // whispercpp-191-r2 / v1.9.1 (current download URL)
+ "16a838ae67e248020b9bc65b8584fcc113cf18167c9cfc0a2d98e194fa52cf95", // whispercpp-191 / v1.9.1 (missing shared libraries)
+ "330ff60cbabd8e77137000d05905cabca90e299c455a3282948c9edddf28bcd7", // whispercpp-186 / v1.8.6 (missing shared libraries)
+ "33bad46e07f0d9bb64fc03ca8c7047b212482c1f45536290f80bb0fe716c9775", // whispercpp-185 / v1.8.5 (missing shared libraries)
+ "315f46514fc09a4fefe2f7b6ae95cfac5441a03b8be04eed1b5f567d5ccc38de", // whispercpp-184 / v1.8.4 (missing shared libraries)
+ },
};
///
@@ -2432,9 +2462,10 @@ public static bool IsQwen3AsrCppVulkanInstall(string installFolder)
///
/// Resolves the WhisperCpp executable-hash key for the given engine choice on the current OS.
- /// Used as a fallback when no sidecar hash exists alongside the install.
- /// Returns null on Linux: the same whisper-cli binary ships in both the Vulkan and CUDA archives,
- /// so executable hashing cannot disambiguate.
+ /// Used as a fallback when no sidecar hash exists alongside the install - without it a Linux
+ /// install predating the sidecar reports Unknown, so the user is never told their engine is
+ /// outdated. That mattered for issue #13680, where every Linux build from v1.8.4 to v1.9.1 is
+ /// broken and the update prompt is how affected users learn to re-download.
///
public static string? ResolveWhisperCppExecutableKey(string? whisperChoice)
{
@@ -2459,6 +2490,18 @@ public static bool IsQwen3AsrCppVulkanInstall(string installFolder)
return whisperChoice == WhisperChoice.Cpp ? WhisperCpp.MacOsExecutable : null;
}
+ if (OperatingSystem.IsLinux())
+ {
+ // Same mapping as ResolveWhisperCppKey: the default Cpp backend on Linux is the
+ // SE-packaged Vulkan build.
+ return whisperChoice switch
+ {
+ WhisperChoice.Cpp => WhisperCpp.LinuxVulkanExecutable,
+ WhisperChoice.CppCuBlas => WhisperCpp.LinuxCudaExecutable,
+ _ => null,
+ };
+ }
+
return null;
}
diff --git a/src/ui/Logic/Download/WhisperDownloadService.cs b/src/ui/Logic/Download/WhisperDownloadService.cs
index 1921e59e456..6755ff59851 100644
--- a/src/ui/Logic/Download/WhisperDownloadService.cs
+++ b/src/ui/Logic/Download/WhisperDownloadService.cs
@@ -25,11 +25,17 @@ public class WhisperDownloadService : IWhisperDownloadService
private const string WindowsUrl = "https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.1/whisper-blas-bin-x64.zip";
private const string MacArmUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-mac.zip";
private const string MacX64Url = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-mac.zip";
- private const string LinuxUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-vulkan-linux64.zip";
+ // The Linux archives point at whispercpp-191-r2, not whispercpp-191: the original v1.9.1
+ // Linux zips (like v1.8.4 and v1.8.6 before them) shipped whisper-cli without the
+ // libwhisper.so.1 / libggml.so.0 it links against, so the engine died on startup with
+ // "error while loading shared libraries" (issue #13680). The -r2 rebuild is the same
+ // whisper.cpp v1.9.1, repackaged with the libraries under their SONAMEs and $ORIGIN
+ // RPATHs. Windows and macOS were unaffected and still use whispercpp-191.
+ private const string LinuxUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191-r2/whisper-vulkan-linux64.zip";
private const string WindowsUrlCuBlass = "https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.1/whisper-cublas-12.4.0-bin-x64.zip";
private const string WindowsUrlCppVulkan = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-vulkan-x64.zip";
- private const string LinuxUrlCuBlass = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191/whisper-cuda-linux64.zip";
+ private const string LinuxUrlCuBlass = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-191-r2/whisper-cuda-linux64.zip";
private const string DownloadUrlConstMe = "https://github.com/Const-me/Whisper/releases/download/1.12.0/cli.zip";
private const string SileroVadUrl = "https://github.com/SubtitleEdit/support-files/releases/download/whispercpp-184/ggml-silero-v6.2.0.zip";
diff --git a/tests/UI/Features/Video/SpeechToText/MissingSharedLibraryTests.cs b/tests/UI/Features/Video/SpeechToText/MissingSharedLibraryTests.cs
index e077d0f884a..9c1f7baa687 100644
--- a/tests/UI/Features/Video/SpeechToText/MissingSharedLibraryTests.cs
+++ b/tests/UI/Features/Video/SpeechToText/MissingSharedLibraryTests.cs
@@ -32,4 +32,38 @@ public void GetName_UnrelatedOutput_ReturnsNull(string? line)
{
Assert.Null(MissingSharedLibrary.GetName(line));
}
+
+ [Fact]
+ public void GetName_WhisperCppLoaderError_ReturnsLibraryName()
+ {
+ // The exact line from issue #13680 (EndeavourOS, whisper.cpp)
+ const string line = "/home/user/.config/Subtitle Edit/SpeechToText/Cpp/whisper-cli: " +
+ "error while loading shared libraries: libwhisper.so.1: " +
+ "cannot open shared object file: No such file or directory";
+
+ Assert.Equal("libwhisper.so.1", MissingSharedLibrary.GetName(line));
+ }
+
+ [Theory]
+ [InlineData("libwhisper.so.1")]
+ [InlineData("libggml.so.0")]
+ [InlineData("libggml-base.so.0")]
+ [InlineData("libllama.so")]
+ [InlineData("@rpath/libwhisper.1.dylib")]
+ public void IsBundledWithEngine_EngineOwnedLibrary_ReturnsTrue(string libraryName)
+ {
+ Assert.True(MissingSharedLibrary.IsBundledWithEngine(libraryName));
+ }
+
+ [Theory]
+ [InlineData("libopenblas.so.0")]
+ [InlineData("libvulkan.so.1")]
+ [InlineData("libcudart.so.12")]
+ [InlineData("libstdc++.so.6")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void IsBundledWithEngine_SystemLibrary_ReturnsFalse(string? libraryName)
+ {
+ Assert.False(MissingSharedLibrary.IsBundledWithEngine(libraryName));
+ }
}
diff --git a/tests/UI/Logic/Download/WhisperCppLinuxExecutableHashTests.cs b/tests/UI/Logic/Download/WhisperCppLinuxExecutableHashTests.cs
new file mode 100644
index 00000000000..550de92556e
--- /dev/null
+++ b/tests/UI/Logic/Download/WhisperCppLinuxExecutableHashTests.cs
@@ -0,0 +1,90 @@
+using System;
+using Nikse.SubtitleEdit.Logic.Download;
+using Nikse.SubtitleEdit.UiLogic.AudioToText;
+
+namespace UITests.Logic.Download;
+
+///
+/// Every Linux whisper.cpp build from v1.8.4 to v1.9.1 shipped whisper-cli without the
+/// libwhisper.so.1 it links against, so the engine could not start at all (issue #13680). The
+/// rebuilt whispercpp-191-r2 archives fix that, and these tests pin the path by which an affected
+/// user finds out: an install with no .installed.sha256 sidecar is identified by hashing
+/// whisper-cli, and a recognised-but-superseded hash is what turns the engine dot amber and
+/// raises the update prompt. If the executable-hash fallback stops resolving on Linux, those
+/// users silently keep a broken engine.
+///
+public class WhisperCppLinuxExecutableHashTests
+{
+ // whisper-cli extracted from whisper-vulkan-linux64.zip / whisper-cuda-linux64.zip
+ private const string VulkanR2 = "da61c0c1910c103cf8dea855855f072249689ee2310f375e2b79615cbd012c05";
+ private const string VulkanBroken191 = "782fac61b9bcfe8f6db22564bb5a2cda2c22550b9ef38064ec5f188bc86dfe79";
+ private const string CudaR2 = "3a4d717745c2d8cf19ca7a954c29c0349a7f158d93a6597bb86e155f0735474a";
+ private const string CudaBroken191 = "16a838ae67e248020b9bc65b8584fcc113cf18167c9cfc0a2d98e194fa52cf95";
+ private const string Shared184 = "315f46514fc09a4fefe2f7b6ae95cfac5441a03b8be04eed1b5f567d5ccc38de";
+
+ [Theory]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxVulkanExecutable, VulkanR2)]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxCudaExecutable, CudaR2)]
+ public void GetStatus_RebuiltArchive_IsUpToDate(string key, string hash)
+ {
+ Assert.Equal(DownloadHashManager.UpdateStatus.UpToDate, DownloadHashManager.GetStatus(key, hash));
+ }
+
+ [Theory]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxVulkanExecutable, VulkanBroken191)]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxCudaExecutable, CudaBroken191)]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxVulkanExecutable, Shared184)]
+ [InlineData(DownloadHashManager.WhisperCpp.LinuxCudaExecutable, Shared184)]
+ public void GetStatus_BuildWithMissingSharedLibraries_OffersUpdate(string key, string hash)
+ {
+ Assert.Equal(DownloadHashManager.UpdateStatus.UpdateAvailable, DownloadHashManager.GetStatus(key, hash));
+ }
+
+ [Fact]
+ public void GetStatus_VulkanAndCudaHashesAreNotInterchangeable()
+ {
+ // v1.8.5 onwards the two backends produce different whisper-cli binaries, so a CUDA hash
+ // must not validate a Vulkan install (v1.8.4 is the documented exception, covered above).
+ Assert.Equal(
+ DownloadHashManager.UpdateStatus.Unknown,
+ DownloadHashManager.GetStatus(DownloadHashManager.WhisperCpp.LinuxVulkanExecutable, CudaR2));
+ Assert.Equal(
+ DownloadHashManager.UpdateStatus.Unknown,
+ DownloadHashManager.GetStatus(DownloadHashManager.WhisperCpp.LinuxCudaExecutable, VulkanR2));
+ }
+
+ [Fact]
+ public void ResolveWhisperCppExecutableKey_OnLinux_ResolvesBothBackends()
+ {
+ if (!OperatingSystem.IsLinux())
+ {
+ return;
+ }
+
+ Assert.Equal(
+ DownloadHashManager.WhisperCpp.LinuxVulkanExecutable,
+ DownloadHashManager.ResolveWhisperCppExecutableKey(WhisperChoice.Cpp));
+ Assert.Equal(
+ DownloadHashManager.WhisperCpp.LinuxCudaExecutable,
+ DownloadHashManager.ResolveWhisperCppExecutableKey(WhisperChoice.CppCuBlas));
+ }
+
+ [Fact]
+ public void ResolveWhisperCppExecutableKey_MatchesArchiveKeyBackend()
+ {
+ // The executable key and the archive key must name the same backend for a given choice,
+ // or the sidecar and no-sidecar paths would disagree about which install is present.
+ foreach (var choice in new[] { WhisperChoice.Cpp, WhisperChoice.CppCuBlas, WhisperChoice.CppVulkan })
+ {
+ var executableKey = DownloadHashManager.ResolveWhisperCppExecutableKey(choice);
+ if (executableKey == null)
+ {
+ continue;
+ }
+
+ var archiveKey = DownloadHashManager.ResolveWhisperCppKey(choice);
+ Assert.NotNull(archiveKey);
+ Assert.Equal(archiveKey + ".Executable", executableKey);
+ }
+ }
+}