mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-20 14:12:15 +00:00
Fixed Pandoc installation and management
This commit is contained in:
parent
e728290104
commit
352af0004d
@ -6985,6 +6985,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1885555132"] = "Pandoc v{0} was insta
|
||||
-- Was not able to check the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2171727146"] = "Was not able to check the Pandoc installation."
|
||||
|
||||
-- Pandoc was not installed successfully, because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2173645304"] = "Pandoc was not installed successfully, because the executable was not found in the archive."
|
||||
|
||||
-- Pandoc v{0} is installed, but it doesn't match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2299898115"] = "Pandoc v{0} is installed, but it doesn't match the required version (v{1})."
|
||||
|
||||
@ -7003,6 +7006,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not availab
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
||||
|
||||
-- Pandoc was not installed successfully, because the downloaded executable could not be validated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T4214136908"] = "Pandoc was not installed successfully, because the downloaded executable could not be validated."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
||||
|
||||
|
||||
@ -6987,6 +6987,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1885555132"] = "Pandoc v{0} wurde erf
|
||||
-- Was not able to check the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2171727146"] = "Die Pandoc-Installation konnte nicht überprüft werden."
|
||||
|
||||
-- Pandoc was not installed successfully, because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2173645304"] = "Pandoc wurde nicht erfolgreich installiert, da die ausführbare Datei im Archiv nicht gefunden wurde."
|
||||
|
||||
-- Pandoc v{0} is installed, but it doesn't match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2299898115"] = "Pandoc v{0} ist installiert, entspricht jedoch nicht der erforderlichen Version (v{1})."
|
||||
|
||||
@ -7005,6 +7008,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem Sy
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc wurde nicht erfolgreich installiert, da der Archivtyp unbekannt ist."
|
||||
|
||||
-- Pandoc was not installed successfully, because the downloaded executable could not be validated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T4214136908"] = "Pandoc wurde nicht erfolgreich installiert, da die heruntergeladene ausführbare Datei nicht validiert werden konnte."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pandoc nicht installiert ist."
|
||||
|
||||
|
||||
@ -6987,6 +6987,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1885555132"] = "Pandoc v{0} was insta
|
||||
-- Was not able to check the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2171727146"] = "Was not able to check the Pandoc installation."
|
||||
|
||||
-- Pandoc was not installed successfully, because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2173645304"] = "Pandoc was not installed successfully, because the executable was not found in the archive."
|
||||
|
||||
-- Pandoc v{0} is installed, but it doesn't match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2299898115"] = "Pandoc v{0} is installed, but it doesn't match the required version (v{1})."
|
||||
|
||||
@ -7005,6 +7008,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not availab
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
||||
|
||||
-- Pandoc was not installed successfully, because the downloaded executable could not be validated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T4214136908"] = "Pandoc was not installed successfully, because the downloaded executable could not be validated."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
||||
|
||||
|
||||
@ -35,12 +35,13 @@ public static partial class Pandoc
|
||||
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
|
||||
|
||||
private static readonly HttpClient WEB_CLIENT = new();
|
||||
private static readonly SemaphoreSlim INSTALLATION_LOCK = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a Pandoc process by using the Pandoc process builder.
|
||||
/// </summary>
|
||||
/// <returns>The Pandoc process builder with default settings.</returns>
|
||||
public static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
|
||||
private static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir.
|
||||
@ -165,25 +166,33 @@ public static partial class Pandoc
|
||||
/// <returns>None</returns>
|
||||
public static async Task InstallAsync(RustService rustService)
|
||||
{
|
||||
await INSTALLATION_LOCK.WaitAsync();
|
||||
|
||||
var latestVersion = await FetchLatestVersionAsync();
|
||||
var installDir = await GetPandocDataFolder(rustService);
|
||||
ClearFolder(installDir);
|
||||
var installParentDir = Path.GetDirectoryName(installDir) ?? Path.GetTempPath();
|
||||
var stagingDir = Path.Combine(installParentDir, $"pandoc-install-{Guid.NewGuid():N}");
|
||||
var pandocTempDownloadFile = Path.GetTempFileName();
|
||||
|
||||
LOG.LogInformation("Trying to install Pandoc v{0} to '{1}'...", latestVersion, installDir);
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(installDir))
|
||||
Directory.CreateDirectory(installDir);
|
||||
|
||||
// Create a temporary file to download the archive to:
|
||||
var pandocTempDownloadFile = Path.GetTempFileName();
|
||||
if (!Directory.Exists(installParentDir))
|
||||
Directory.CreateDirectory(installParentDir);
|
||||
|
||||
//
|
||||
// Download the latest Pandoc archive from GitHub:
|
||||
//
|
||||
var uri = await GenerateArchiveUriAsync();
|
||||
var response = await WEB_CLIENT.GetAsync(uri);
|
||||
var uri = GenerateArchiveUri(latestVersion);
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive type is unknown.")));
|
||||
LOG.LogError("Pandoc was not installed, no archive is available for architecture '{Architecture}'.", CPU_ARCHITECTURE.ToUserFriendlyName());
|
||||
return;
|
||||
}
|
||||
|
||||
using var response = await WEB_CLIENT.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
|
||||
@ -192,18 +201,33 @@ public static partial class Pandoc
|
||||
}
|
||||
|
||||
// Download the archive to the temporary file:
|
||||
await using var tempFileStream = File.Create(pandocTempDownloadFile);
|
||||
await response.Content.CopyToAsync(tempFileStream);
|
||||
await using (var tempFileStream = File.Create(pandocTempDownloadFile))
|
||||
{
|
||||
await response.Content.CopyToAsync(tempFileStream);
|
||||
await tempFileStream.FlushAsync();
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir);
|
||||
await RunWithRetriesAsync(
|
||||
() =>
|
||||
{
|
||||
ZipFile.ExtractToDirectory(pandocTempDownloadFile, stagingDir, true);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
"extracting the Pandoc ZIP archive");
|
||||
}
|
||||
else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
|
||||
await TarFile.ExtractToDirectoryAsync(uncompressedStream, installDir, true);
|
||||
await RunWithRetriesAsync(
|
||||
async () =>
|
||||
{
|
||||
await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
|
||||
await TarFile.ExtractToDirectoryAsync(uncompressedStream, stagingDir, true);
|
||||
},
|
||||
"extracting the Pandoc TAR archive");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -212,8 +236,22 @@ public static partial class Pandoc
|
||||
return;
|
||||
}
|
||||
|
||||
File.Delete(pandocTempDownloadFile);
|
||||
|
||||
var stagedPandocExecutable = FindExecutableInDirectory(stagingDir, PandocProcessBuilder.PandocExecutableName);
|
||||
if (string.IsNullOrWhiteSpace(stagedPandocExecutable))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the executable was not found in the archive.")));
|
||||
LOG.LogError("Pandoc was not installed, the executable was not found in the extracted archive: '{StagingDir}'.", stagingDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await IsPandocExecutableValidAsync(stagedPandocExecutable))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the downloaded executable could not be validated.")));
|
||||
LOG.LogError("Pandoc was not installed, the downloaded executable could not be validated: '{Executable}'.", stagedPandocExecutable);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplaceInstallationDirectoryAsync(stagingDir, installDir);
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} was installed successfully."), latestVersion)));
|
||||
LOG.LogInformation("Pandoc v{0} was installed successfully.", latestVersion);
|
||||
}
|
||||
@ -221,20 +259,158 @@ public static partial class Pandoc
|
||||
{
|
||||
LOG.LogError(ex, "An error occurred while installing Pandoc.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteFile(pandocTempDownloadFile);
|
||||
|
||||
if (Directory.Exists(stagingDir))
|
||||
await TryDeleteFolderAsync(stagingDir);
|
||||
|
||||
INSTALLATION_LOCK.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearFolder(string path)
|
||||
private static async Task ReplaceInstallationDirectoryAsync(string stagingDir, string installDir)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return;
|
||||
|
||||
var backupDir = $"{installDir}.backup-{Guid.NewGuid():N}";
|
||||
var hasBackup = false;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
if (Directory.Exists(installDir))
|
||||
{
|
||||
Directory.Move(installDir, backupDir);
|
||||
hasBackup = true;
|
||||
}
|
||||
|
||||
Directory.Move(stagingDir, installDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogError(ex, "Error clearing pandoc installation directory.");
|
||||
if (hasBackup && !Directory.Exists(installDir) && Directory.Exists(backupDir))
|
||||
Directory.Move(backupDir, installDir);
|
||||
|
||||
LOG.LogError(ex, "Error replacing pandoc installation directory.");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hasBackup && Directory.Exists(backupDir))
|
||||
await TryDeleteFolderAsync(backupDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> IsPandocExecutableValidAsync(string executable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
startInfo.ArgumentList.Add("--version");
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
return false;
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
var output = await outputTask;
|
||||
var error = await errorTask;
|
||||
|
||||
if (process.ExitCode is not 0)
|
||||
{
|
||||
LOG.LogError("Downloaded Pandoc executable exited with code {ProcessExitCode}. Error output: '{ErrorText}'", process.ExitCode, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionMatch = PandocCmdRegex().Match(output);
|
||||
return versionMatch.Success && Version.Parse(versionMatch.Groups[1].Value) >= MINIMUM_REQUIRED_VERSION;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogError(ex, "Error validating downloaded Pandoc executable.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindExecutableInDirectory(string rootDirectory, string executableName)
|
||||
{
|
||||
if (!Directory.Exists(rootDirectory))
|
||||
return string.Empty;
|
||||
|
||||
var rootExecutablePath = Path.Combine(rootDirectory, executableName);
|
||||
if (File.Exists(rootExecutablePath))
|
||||
return rootExecutablePath;
|
||||
|
||||
foreach (var subdirectory in Directory.GetDirectories(rootDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var pandocPath = Path.Combine(subdirectory, executableName);
|
||||
if (File.Exists(pandocPath))
|
||||
return pandocPath;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static async Task RunWithRetriesAsync(Func<Task> operation, string operationName)
|
||||
{
|
||||
const int MAX_ATTEMPTS = 4;
|
||||
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await operation();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (attempt < MAX_ATTEMPTS && ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
LOG.LogWarning(ex, "Error while {OperationName}; retrying attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, MAX_ATTEMPTS);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogWarning(ex, "Was not able to delete temporary Pandoc archive: '{Path}'.", path);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task TryDeleteFolderAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await RunWithRetriesAsync(
|
||||
() =>
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
$"deleting temporary Pandoc directory '{path}'");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogWarning(ex, "Was not able to delete temporary Pandoc directory: '{Path}'.", path);
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,6 +448,11 @@ public static partial class Pandoc
|
||||
public static async Task<string> GenerateArchiveUriAsync()
|
||||
{
|
||||
var version = await FetchLatestVersionAsync();
|
||||
return GenerateArchiveUri(version);
|
||||
}
|
||||
|
||||
private static string GenerateArchiveUri(string version)
|
||||
{
|
||||
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
|
||||
return CPU_ARCHITECTURE switch
|
||||
{
|
||||
|
||||
@ -220,6 +220,17 @@ public sealed class PandocProcessBuilder
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName))
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
continue;
|
||||
|
||||
if (shouldLog)
|
||||
LOGGER.LogInformation("Found system Pandoc installation at: '{Path}'.", candidate);
|
||||
|
||||
return new(candidate, false);
|
||||
}
|
||||
|
||||
//
|
||||
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
|
||||
//
|
||||
@ -238,4 +249,59 @@ public sealed class PandocProcessBuilder
|
||||
/// Reads the os platform to determine the used executable name.
|
||||
/// </summary>
|
||||
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc";
|
||||
|
||||
private static IEnumerable<string> SystemPandocExecutableCandidates(string executableName)
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
|
||||
switch (CPU_ARCHITECTURE)
|
||||
{
|
||||
case RID.WIN_X64 or RID.WIN_ARM64:
|
||||
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Pandoc", executableName);
|
||||
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Pandoc", executableName);
|
||||
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Pandoc", executableName);
|
||||
break;
|
||||
|
||||
case RID.OSX_X64 or RID.OSX_ARM64:
|
||||
AddCandidate(candidates, "/opt/homebrew/bin", executableName);
|
||||
AddCandidate(candidates, "/usr/local/bin", executableName);
|
||||
AddCandidate(candidates, "/usr/bin", executableName);
|
||||
break;
|
||||
|
||||
case RID.LINUX_X64 or RID.LINUX_ARM64:
|
||||
AddCandidate(candidates, "/usr/local/bin", executableName);
|
||||
AddCandidate(candidates, "/usr/bin", executableName);
|
||||
AddCandidate(candidates, "/snap/bin", executableName);
|
||||
|
||||
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
AddCandidate(candidates, homeDirectory, ".local", "bin", executableName);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var pathDirectory in GetPathDirectories())
|
||||
AddCandidate(candidates, pathDirectory, executableName);
|
||||
|
||||
var comparer = CPU_ARCHITECTURE is RID.WIN_X64 or RID.WIN_ARM64
|
||||
? StringComparer.OrdinalIgnoreCase
|
||||
: StringComparer.Ordinal;
|
||||
return candidates.Distinct(comparer);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetPathDirectories()
|
||||
{
|
||||
var pathValue = Environment.GetEnvironmentVariable("PATH");
|
||||
if (string.IsNullOrWhiteSpace(pathValue))
|
||||
yield break;
|
||||
|
||||
foreach (var pathDirectory in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
yield return pathDirectory;
|
||||
}
|
||||
|
||||
private static void AddCandidate(List<string> candidates, params string[] pathParts)
|
||||
{
|
||||
if (pathParts.Any(string.IsNullOrWhiteSpace))
|
||||
return;
|
||||
|
||||
candidates.Add(Path.Combine(pathParts));
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
# v26.5.5, build 240 (2026-05-xx xx:xx UTC)
|
||||
- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base.
|
||||
- Improved the Pandoc installation, management, and detection process. This stabilizes the Pandoc installation process and makes it more reliable.
|
||||
- Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system.
|
||||
- Upgraded Rust to v1.95.0.
|
||||
- Upgraded .NET to v9.0.16.
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
use tokio::process::Command;
|
||||
use crate::environment::DATA_DIRECTORY;
|
||||
use crate::metadata::META_DATA;
|
||||
|
||||
/// Tracks whether the RID mismatch warning has been logged.
|
||||
static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new();
|
||||
static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new();
|
||||
|
||||
pub struct PandocExecutable {
|
||||
pub executable: String,
|
||||
@ -114,28 +117,43 @@ impl PandocProcessBuilder {
|
||||
// Any local installation should be preferred over the system-wide installation.
|
||||
let data_folder = PathBuf::from(DATA_DIRECTORY.get().unwrap());
|
||||
let local_installation_root_directory = data_folder.join("pandoc");
|
||||
let executable_name = Self::pandoc_executable_name();
|
||||
|
||||
if local_installation_root_directory.exists() {
|
||||
let executable_name = Self::pandoc_executable_name();
|
||||
if let Ok(pandoc_path) = Self::find_executable_in_dir(&local_installation_root_directory, &executable_name) {
|
||||
HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
|
||||
info!(Source = "PandocProcessBuilder"; "Found local Pandoc installation at: '{}'.", pandoc_path.to_string_lossy()
|
||||
);
|
||||
});
|
||||
|
||||
if let Ok(entries) = fs::read_dir(&local_installation_root_directory) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Ok(pandoc_path) = Self::find_executable_in_dir(&path, &executable_name) {
|
||||
return PandocExecutable {
|
||||
executable: pandoc_path.to_string_lossy().to_string(),
|
||||
is_local_installation: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return PandocExecutable {
|
||||
executable: pandoc_path.to_string_lossy().to_string(),
|
||||
is_local_installation: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for candidate in Self::system_pandoc_executable_candidates(&executable_name) {
|
||||
if candidate.exists() && candidate.is_file() {
|
||||
HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
|
||||
info!(Source = "PandocProcessBuilder"; "Found system Pandoc installation at: '{}'.", candidate.to_string_lossy()
|
||||
);
|
||||
});
|
||||
|
||||
return PandocExecutable {
|
||||
executable: candidate.to_string_lossy().to_string(),
|
||||
is_local_installation: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
|
||||
HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
|
||||
warn!(Source = "PandocProcessBuilder"; "Falling back to system PATH for the Pandoc executable: '{}'.", executable_name);
|
||||
});
|
||||
|
||||
PandocExecutable {
|
||||
executable: Self::pandoc_executable_name(),
|
||||
executable: executable_name,
|
||||
is_local_installation: false,
|
||||
}
|
||||
}
|
||||
@ -161,6 +179,56 @@ impl PandocProcessBuilder {
|
||||
Err("Executable not found".into())
|
||||
}
|
||||
|
||||
fn system_pandoc_executable_candidates(executable_name: &str) -> Vec<PathBuf> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
match env::consts::OS {
|
||||
"windows" => {
|
||||
Self::push_env_candidate(&mut candidates, "LOCALAPPDATA", &["Pandoc", executable_name]);
|
||||
Self::push_env_candidate(&mut candidates, "ProgramFiles", &["Pandoc", executable_name]);
|
||||
Self::push_env_candidate(&mut candidates, "ProgramFiles(x86)", &["Pandoc", executable_name]);
|
||||
},
|
||||
"macos" => {
|
||||
candidates.push(PathBuf::from("/opt/homebrew/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/usr/local/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/usr/bin").join(executable_name));
|
||||
},
|
||||
"linux" => {
|
||||
candidates.push(PathBuf::from("/usr/local/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/usr/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/snap/bin").join(executable_name));
|
||||
|
||||
if let Some(home_dir) = env::var_os("HOME") {
|
||||
candidates.push(PathBuf::from(home_dir).join(".local").join("bin").join(executable_name));
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(path_value) = env::var_os("PATH") {
|
||||
for path_dir in env::split_paths(&path_value) {
|
||||
candidates.push(path_dir.join(executable_name));
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|path| seen.insert(path.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn push_env_candidate(candidates: &mut Vec<PathBuf>, env_name: &str, parts: &[&str]) {
|
||||
if let Some(root) = env::var_os(env_name) {
|
||||
let mut path = PathBuf::from(root);
|
||||
|
||||
for part in parts {
|
||||
path.push(part);
|
||||
}
|
||||
|
||||
candidates.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the executable name based on the current OS at runtime.
|
||||
///
|
||||
/// This uses runtime detection instead of metadata to ensure correct behavior
|
||||
|
||||
Loading…
Reference in New Issue
Block a user