diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 462aa69f..85c1933d 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -75,7 +75,8 @@ the same on every PC. An external engine is looked for in this order: 1. a copy the user picked in Settings → *Engines*; 2. Filee's own copy (bundled or downloaded); 3. a copy already on the computer: on `PATH`, in Homebrew's and the usual Unix folders, or where the LibreOffice, - calibre, Ghostscript and Tesseract installers put it. Settings → *Engines* shows it as "Found on this PC", with a note when + calibre, Ghostscript and Tesseract installers put it. On Windows, FFmpeg, Tesseract and Whisper are also looked for in + the native environments under the system drive's `msys64` folder, after these locations. Settings → *Engines* shows it as "Found on this PC", with a note when its version is older than the one Filee downloads. Where Filee has no download of an engine (FFmpeg, Ghostscript and Tesseract on macOS, LibreOffice, Ghostscript and diff --git a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs index baf5eaa2..ffb6f84b 100644 --- a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs +++ b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs @@ -256,12 +256,21 @@ private static IEnumerable SystemFolders(string id) break; } } + // MSYS2 is an optional fallback after PATH and the usual installer locations, never ahead of Filee's copies. + if (OperatingSystem.IsWindows() && Path.GetPathRoot(Environment.SystemDirectory) is { Length: > 0 } drive) + folders.AddRange(Msys2Folders(id, Path.Combine(drive, "msys64"))); // Never Filee's own folders: those are its own copy, found before. var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; return folders.Distinct(OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal) .Where(f => !IsWithin(f, DownloadRoot, comparison) && !IsWithin(f, AppContext.BaseDirectory, comparison)); } + /// Native MSYS2 environments under a standard installation; do not search MSYS's own Unix tools. + internal static IEnumerable Msys2Folders(string id, string root) => + id is "ffmpeg" or "tesseract" or "whisper" + ? new[] { "ucrt64", "clang64", "mingw64", "clangarm64", "mingw32" }.Select(env => Path.Combine(root, env, "bin")) + : []; + /// Where LibreOffice's Windows installer records its program folder (the key's default value). internal const string LibreOfficeInstallPathKey = @"SOFTWARE\LibreOffice\UNO\InstallPath"; diff --git a/tests/Filee.Engines.Tests/PortableEngineTests.cs b/tests/Filee.Engines.Tests/PortableEngineTests.cs index 5a3b3e5c..ed8b9845 100644 --- a/tests/Filee.Engines.Tests/PortableEngineTests.cs +++ b/tests/Filee.Engines.Tests/PortableEngineTests.cs @@ -91,6 +91,27 @@ public void Unix_FFmpeg_requires_both_tools_in_the_same_folder() Assert.Equal(_root, EngineEnvironment.FolderWithPrograms("ffmpeg", _root, windows: false)); } + [Theory] + [InlineData("ffmpeg", "mingw64", "ffmpeg.exe", "ffprobe.exe")] + [InlineData("tesseract", "clang64", "tesseract.exe", null)] + [InlineData("whisper", "ucrt64", "whisper-cli.exe", null)] + public void Msys2_native_engines_are_found_only_when_the_required_programs_exist( + string id, string environment, string program, string? second) + { + var bin = Path.Combine(_root, environment, "bin"); + Directory.CreateDirectory(bin); + File.WriteAllText(Path.Combine(bin, program), "binary"); + var folders = EngineEnvironment.Msys2Folders(id, _root).ToList(); + if (second is not null) + { + Assert.Null(folders.Select(path => EngineEnvironment.FolderWithPrograms(id, path, windows: true)).FirstOrDefault(path => path is not null)); + File.WriteAllText(Path.Combine(bin, second), "binary"); + } + Assert.Equal(bin, folders.Select(path => EngineEnvironment.FolderWithPrograms(id, path, windows: true)).FirstOrDefault(path => path is not null)); + Assert.DoesNotContain(Path.Combine(_root, "usr", "bin"), folders); + Assert.Empty(EngineEnvironment.Msys2Folders("pandoc", _root)); + } + [Fact] public async Task Tar_archives_preserve_program_layout_contents_and_Unix_executable_mode() {