2025-05-29 14:01:56 +02:00
using System.Diagnostics ;
2025-05-30 22:39:16 +02:00
using System.Formats.Tar ;
2025-05-29 14:01:56 +02:00
using System.IO.Compression ;
2025-05-30 22:39:16 +02:00
using System.Reflection ;
2025-05-29 14:01:56 +02:00
using System.Text.RegularExpressions ;
2025-05-30 22:39:16 +02:00
using AIStudio.Tools.Metadata ;
2025-05-29 14:01:56 +02:00
using AIStudio.Tools.Services ;
2025-05-30 22:39:16 +02:00
using SharedTools ;
2025-05-29 14:01:56 +02:00
namespace AIStudio.Tools ;
public static partial class Pandoc
{
2025-05-30 22:39:16 +02:00
private static string TB ( string fallbackEN ) => PluginSystem . I18N . I . T ( fallbackEN , typeof ( Pandoc ). Namespace , nameof ( Pandoc ));
2026-01-20 10:34:04 +01:00
2025-05-30 22:39:16 +02:00
private static readonly Assembly ASSEMBLY = Assembly . GetExecutingAssembly ();
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY . GetCustomAttribute < MetaDataArchitectureAttribute >()!;
2026-01-20 10:34:04 +01:00
// Use runtime detection instead of metadata to ensure correct RID on dev machines:
private static readonly RID CPU_ARCHITECTURE = RIDExtensions . GetCurrentRID ();
private static readonly RID METADATA_ARCHITECTURE = META_DATA_ARCH . Architecture . ToRID ();
2025-05-29 14:01:56 +02:00
private const string DOWNLOAD_URL = "https://github.com/jgm/pandoc/releases/download" ;
private const string LATEST_URL = "https://github.com/jgm/pandoc/releases/latest" ;
2026-01-20 10:34:04 +01:00
2025-09-03 21:25:17 +02:00
private static readonly ILogger LOG = Program . LOGGER_FACTORY . CreateLogger ( nameof ( Pandoc ));
2025-05-30 22:39:16 +02:00
private static readonly Version MINIMUM_REQUIRED_VERSION = new ( 3 , 7 , 0 , 2 );
private static readonly Version FALLBACK_VERSION = new ( 3 , 7 , 0 , 2 );
2025-05-29 14:01:56 +02:00
2026-01-20 10:34:04 +01:00
/// <summary>
2026-08-10 16:37:44 +02:00
/// Tracks whether the executable AI Studio checks was already logged.
2026-01-20 10:34:04 +01:00
/// </summary>
2026-08-10 16:37:44 +02:00
/// <remarks>
/// Only informational logs are written once, because they describe a stable state and would
/// otherwise spam the log on repeated calls. Failures are always logged: they are usually
/// transient, e.g. an executable which is temporarily blocked or unreachable. Suppressing
/// repeated failures hid exactly the interesting case, where the check succeeded during
/// startup and started failing later on.
/// </remarks>
private static bool HAS_LOGGED_EXECUTABLE_ONCE ;
/// <summary>
/// Tracks whether a successful availability check was already logged.
/// </summary>
private static bool HAS_LOGGED_SUCCESSFUL_CHECK_ONCE ;
2026-01-20 10:34:04 +01:00
2026-04-10 17:11:05 +02:00
private static readonly HttpClient WEB_CLIENT = new ();
2026-05-16 18:27:16 +02:00
private static readonly SemaphoreSlim INSTALLATION_LOCK = new ( 1 , 1 );
2026-04-10 17:11:05 +02:00
2025-05-29 14:01:56 +02:00
/// <summary>
2025-05-30 22:39:16 +02:00
/// Prepares a Pandoc process by using the Pandoc process builder.
2025-05-29 14:01:56 +02:00
/// </summary>
2025-05-30 22:39:16 +02:00
/// <returns>The Pandoc process builder with default settings.</returns>
2026-05-16 18:27:16 +02:00
private static PandocProcessBuilder PreparePandocProcess () => PandocProcessBuilder . Create ();
2025-05-29 14:01:56 +02:00
2025-05-30 22:39:16 +02:00
/// <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.
/// </summary>
/// <param name="rustService">Global rust service to access file system and data dir.</param>
/// <param name="showMessages">Controls if snackbars are shown to the user.</param>
2025-11-14 13:34:30 +01:00
/// <param name="showSuccessMessage">Controls if a success snackbar is shown to the user.</param>
2025-05-30 22:39:16 +02:00
/// <returns>True, if pandoc is available and the minimum required version is met, else false.</returns>
2025-11-14 13:34:30 +01:00
public static async Task < PandocInstallation > CheckAvailabilityAsync ( RustService rustService , bool showMessages = true , bool showSuccessMessage = true )
2025-05-30 22:39:16 +02:00
{
2025-05-29 14:01:56 +02:00
try
{
2026-01-20 10:34:04 +01:00
//
// Log a warning if the runtime-detected RID differs from the metadata RID.
// This can happen on dev machines where the metadata.txt contains stale values.
// We always use the runtime-detected RID for correct behavior.
//
2026-08-10 16:37:44 +02:00
if (! HAS_LOGGED_EXECUTABLE_ONCE && CPU_ARCHITECTURE != METADATA_ARCHITECTURE )
2026-01-20 10:34:04 +01:00
{
LOG . LogWarning (
"Runtime-detected RID '{RuntimeRID}' differs from metadata RID '{MetadataRID}'. Using runtime-detected RID. This is expected on dev machines where metadata.txt may be outdated." ,
CPU_ARCHITECTURE . ToUserFriendlyName (),
METADATA_ARCHITECTURE . ToUserFriendlyName ());
}
2025-05-30 22:39:16 +02:00
var preparedProcess = await PreparePandocProcess (). AddArgument ( "--version" ). BuildAsync ( rustService );
2026-08-10 16:37:44 +02:00
if (! HAS_LOGGED_EXECUTABLE_ONCE )
{
2026-01-20 10:34:04 +01:00
LOG . LogInformation ( "Checking Pandoc availability using executable: '{Executable}' (IsLocal: {IsLocal})." , preparedProcess . StartInfo . FileName , preparedProcess . IsLocal );
2026-08-10 16:37:44 +02:00
HAS_LOGGED_EXECUTABLE_ONCE = true ;
}
2026-01-20 10:34:04 +01:00
2025-05-30 22:39:16 +02:00
using var process = Process . Start ( preparedProcess . StartInfo );
2025-05-29 14:01:56 +02:00
if ( process == null )
{
if ( showMessages )
2026-01-20 10:34:04 +01:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Help , TB ( "Was not able to check the Pandoc installation." )));
2025-12-10 17:07:39 +01:00
2026-08-10 16:37:44 +02:00
LOG . LogError ( "The Pandoc process was not started, it was null. Executable path: '{Executable}'." , preparedProcess . StartInfo . FileName );
2025-05-30 22:39:16 +02:00
return new ( false , TB ( "Was not able to check the Pandoc installation." ), false , string . Empty , preparedProcess . IsLocal );
2025-05-29 14:01:56 +02:00
}
2025-12-10 17:07:39 +01:00
// Read output streams asynchronously while the process runs (prevents deadlock):
var outputTask = process . StandardOutput . ReadToEndAsync ();
var errorTask = process . StandardError . ReadToEndAsync ();
// Wait for the process to exit AND for streams to be fully read:
2025-05-29 14:01:56 +02:00
await process . WaitForExitAsync ();
2025-12-10 17:07:39 +01:00
var output = await outputTask ;
var error = await errorTask ;
2025-05-29 14:01:56 +02:00
if ( process . ExitCode != 0 )
{
if ( showMessages )
2026-01-20 10:34:04 +01:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "Pandoc is not available on the system or the process had issues." )));
2025-12-10 17:07:39 +01:00
2026-08-10 16:37:44 +02:00
LOG . LogError ( "The Pandoc process exited with code {ProcessExitCode}. Error output: '{ErrorText}'" , process . ExitCode , error );
2025-05-30 22:39:16 +02:00
return new ( false , TB ( "Pandoc is not available on the system or the process had issues." ), false , string . Empty , preparedProcess . IsLocal );
2025-05-29 14:01:56 +02:00
}
var versionMatch = PandocCmdRegex (). Match ( output );
if (! versionMatch . Success )
{
if ( showMessages )
2026-01-20 10:34:04 +01:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Terminal , TB ( "Was not able to validate the Pandoc installation." )));
2026-08-10 16:37:44 +02:00
LOG . LogError ( "Pandoc --version returned an invalid format: '{Output}'." , output );
2025-05-30 22:39:16 +02:00
return new ( false , TB ( "Was not able to validate the Pandoc installation." ), false , string . Empty , preparedProcess . IsLocal );
2025-05-29 14:01:56 +02:00
}
2026-01-20 10:34:04 +01:00
2025-05-29 14:01:56 +02:00
var versions = versionMatch . Groups [ 1 ]. Value ;
var installedVersion = Version . Parse ( versions );
2025-05-30 22:39:16 +02:00
var installedVersionString = installedVersion . ToString ();
2026-01-20 10:34:04 +01:00
2025-05-29 14:01:56 +02:00
if ( installedVersion >= MINIMUM_REQUIRED_VERSION )
{
2025-11-14 13:34:30 +01:00
if ( showMessages && showSuccessMessage )
2025-05-30 22:39:16 +02:00
await MessageBus . INSTANCE . SendSuccess ( new ( Icons . Material . Filled . CheckCircle , string . Format ( TB ( "Pandoc v{0} is installed." ), installedVersionString )));
2026-01-20 10:34:04 +01:00
2026-08-10 16:37:44 +02:00
if (! HAS_LOGGED_SUCCESSFUL_CHECK_ONCE )
{
2026-01-20 10:34:04 +01:00
LOG . LogInformation ( "Pandoc v{0} is installed and matches the required version (v{1})." , installedVersionString , MINIMUM_REQUIRED_VERSION . ToString ());
2026-08-10 16:37:44 +02:00
HAS_LOGGED_SUCCESSFUL_CHECK_ONCE = true ;
}
2026-01-20 10:34:04 +01:00
2025-05-30 22:39:16 +02:00
return new ( true , string . Empty , true , installedVersionString , preparedProcess . IsLocal );
2025-05-29 14:01:56 +02:00
}
2026-01-20 10:34:04 +01:00
2025-05-29 14:01:56 +02:00
if ( showMessages )
2026-01-20 10:34:04 +01:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Build , string . Format ( TB ( "Pandoc v{0} is installed, but it doesn't match the required version (v{1})." ), installedVersionString , MINIMUM_REQUIRED_VERSION . ToString ())));
2026-08-10 16:37:44 +02:00
LOG . LogWarning ( "Pandoc v{0} is installed, but it does not match the required version (v{1})." , installedVersionString , MINIMUM_REQUIRED_VERSION . ToString ());
2025-05-30 22:39:16 +02:00
return new ( true , string . Format ( TB ( "Pandoc v{0} is installed, but it does not match the required version (v{1})." ), installedVersionString , MINIMUM_REQUIRED_VERSION . ToString ()), false , installedVersionString , preparedProcess . IsLocal );
2025-05-29 14:01:56 +02:00
}
catch ( Exception e )
{
if ( showMessages )
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendError ( new ( @Icons . Material . Filled . AppsOutage , TB ( "Pandoc doesn't seem to be installed." )));
2026-01-20 10:34:04 +01:00
2026-08-10 16:37:44 +02:00
LOG . LogError ( e , "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH." );
2026-05-16 18:27:16 +02:00
return new ( false , TB ( "Pandoc doesn't seem to be installed." ), false , string . Empty , false );
2025-05-29 14:01:56 +02:00
}
}
/// <summary>
/// Automatically decompresses the latest pandoc archive into AiStudio's data directory
/// </summary>
/// <param name="rustService">Global rust service to access file system and data dir</param>
/// <returns>None</returns>
public static async Task InstallAsync ( RustService rustService )
{
2026-05-16 18:27:16 +02:00
await INSTALLATION_LOCK . WaitAsync ();
2025-05-30 22:39:16 +02:00
var latestVersion = await FetchLatestVersionAsync ();
2025-05-29 14:01:56 +02:00
var installDir = await GetPandocDataFolder ( rustService );
2026-05-16 18:27:16 +02:00
var installParentDir = Path . GetDirectoryName ( installDir ) ?? Path . GetTempPath ();
var stagingDir = Path . Combine ( installParentDir , $"pandoc-install-{Guid.NewGuid():N}" );
var pandocTempDownloadFile = Path . GetTempFileName ();
2025-05-29 14:01:56 +02:00
2025-05-30 22:39:16 +02:00
LOG . LogInformation ( "Trying to install Pandoc v{0} to '{1}'..." , latestVersion , installDir );
2025-05-29 14:01:56 +02:00
try
{
2026-05-16 18:27:16 +02:00
if (! Directory . Exists ( installParentDir ))
Directory . CreateDirectory ( installParentDir );
2025-05-30 22:39:16 +02:00
//
// Download the latest Pandoc archive from GitHub:
//
2026-05-16 18:27:16 +02:00
var uri = GenerateArchiveUri ( latestVersion );
if ( string . IsNullOrWhiteSpace ( uri ))
{
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "AI Studio couldn't install Pandoc 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 );
2026-04-10 17:11:05 +02:00
if (! response . IsSuccessStatusCode )
2025-05-29 14:01:56 +02:00
{
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "AI Studio couldn't install Pandoc because the archive was not found." )));
2026-04-10 17:11:05 +02:00
LOG . LogError ( "Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'" , response . StatusCode , uri , response . RequestMessage );
return ;
2025-05-29 14:01:56 +02:00
}
2026-04-10 17:11:05 +02:00
// Download the archive to the temporary file:
2026-05-16 18:27:16 +02:00
await using ( var tempFileStream = File . Create ( pandocTempDownloadFile ))
{
await response . Content . CopyToAsync ( tempFileStream );
await tempFileStream . FlushAsync ();
}
2026-04-10 17:11:05 +02:00
2026-05-16 18:27:16 +02:00
Directory . CreateDirectory ( stagingDir );
2025-05-30 22:39:16 +02:00
if ( uri . EndsWith ( ".zip" , StringComparison . OrdinalIgnoreCase ))
2025-05-29 14:01:56 +02:00
{
2026-05-16 18:27:16 +02:00
await RunWithRetriesAsync (
() =>
{
ZipFile . ExtractToDirectory ( pandocTempDownloadFile , stagingDir , true );
return Task . CompletedTask ;
},
"extracting the Pandoc ZIP archive" );
2025-05-29 14:01:56 +02:00
}
2025-05-30 22:39:16 +02:00
else if ( uri . EndsWith ( ".tar.gz" , StringComparison . OrdinalIgnoreCase ))
2025-05-29 14:01:56 +02:00
{
2026-05-16 18:27:16 +02:00
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" );
2025-05-29 14:01:56 +02:00
}
else
{
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "AI Studio couldn't install Pandoc because the archive type is unknown." )));
2025-05-30 22:39:16 +02:00
LOG . LogError ( "Pandoc was not installed, the archive is unknown: url='{0}'" , uri );
2025-05-29 14:01:56 +02:00
return ;
}
2026-05-16 18:27:16 +02:00
var stagedPandocExecutable = FindExecutableInDirectory ( stagingDir , PandocProcessBuilder . PandocExecutableName );
if ( string . IsNullOrWhiteSpace ( stagedPandocExecutable ))
{
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "AI Studio couldn't install Pandoc 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 ;
}
LOG . LogInformation ( "Found Pandoc executable in downloaded archive: '{Executable}'." , stagedPandocExecutable );
await ReplaceInstallationDirectoryAsync ( stagingDir , installDir );
2025-05-30 22:39:16 +02:00
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 );
2025-05-29 14:01:56 +02:00
}
catch ( Exception ex )
{
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Error , TB ( "AI Studio couldn't install Pandoc." )));
2025-05-30 22:39:16 +02:00
LOG . LogError ( ex , "An error occurred while installing Pandoc." );
2025-05-29 14:01:56 +02:00
}
2026-05-16 18:27:16 +02:00
finally
{
TryDeleteFile ( pandocTempDownloadFile );
if ( Directory . Exists ( stagingDir ))
await TryDeleteFolderAsync ( stagingDir );
INSTALLATION_LOCK . Release ();
}
2025-05-29 14:01:56 +02:00
}
2026-05-16 18:27:16 +02:00
private static async Task ReplaceInstallationDirectoryAsync ( string stagingDir , string installDir )
{
var backupDir = $"{installDir}.backup-{Guid.NewGuid():N}" ;
var hasBackup = false ;
var stagingWasMoved = false ;
try
{
if ( Directory . Exists ( installDir ))
{
await MoveDirectoryWithRetriesAsync ( installDir , backupDir , "moving the previous Pandoc installation to backup" );
hasBackup = true ;
}
await MoveDirectoryWithRetriesAsync ( stagingDir , installDir , "moving the new Pandoc installation into place" );
stagingWasMoved = true ;
}
catch ( Exception ex )
{
if ( hasBackup && ! stagingWasMoved && ! Directory . Exists ( installDir ) && Directory . Exists ( backupDir ))
{
try
{
await MoveDirectoryWithRetriesAsync ( backupDir , installDir , "restoring the previous Pandoc installation" );
hasBackup = false ;
}
catch ( Exception rollbackEx )
{
LOG . LogError ( rollbackEx , "Error restoring previous Pandoc installation directory. Keeping backup directory at: '{BackupDir}'." , backupDir );
}
}
LOG . LogError ( ex , "Error replacing pandoc installation directory." );
throw ;
}
finally
{
if ( hasBackup && stagingWasMoved && Directory . Exists ( backupDir ))
await TryDeleteFolderAsync ( backupDir );
}
}
private static string FindExecutableInDirectory ( string rootDirectory , string executableName )
2025-05-29 14:01:56 +02:00
{
2026-05-16 18:27:16 +02:00
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 MoveDirectoryWithRetriesAsync ( string sourceDir , string destinationDir , string operationName )
{
await RunWithRetriesAsync (
() =>
{
Directory . Move ( sourceDir , destinationDir );
return Task . CompletedTask ;
},
operationName ,
maxAttempts : 8 );
}
private static async Task RunWithRetriesAsync ( Func < Task > operation , string operationName , int maxAttempts = 4 )
{
for ( var attempt = 1 ; attempt <= maxAttempts ; attempt ++)
{
try
{
await operation ();
return ;
}
catch ( Exception ex ) when ( attempt < maxAttempts && ex is IOException or UnauthorizedAccessException )
{
LOG . LogWarning ( ex , "Error while {OperationName}; retrying attempt {Attempt}/{MaxAttempts}." , operationName , attempt + 1 , maxAttempts );
await Task . Delay ( TimeSpan . FromMilliseconds ( 250 * attempt ));
}
}
}
private static void TryDeleteFile ( string path )
{
if ( string . IsNullOrWhiteSpace ( path ) || ! File . Exists ( path ))
2025-05-30 22:39:16 +02:00
return ;
2026-05-16 18:27:16 +02:00
2025-05-29 14:01:56 +02:00
try
{
2026-05-16 18:27:16 +02:00
File . Delete ( path );
2025-05-29 14:01:56 +02:00
}
catch ( Exception ex )
{
2026-05-16 18:27:16 +02:00
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}'" ,
maxAttempts : 3 );
}
catch ( Exception ex )
{
LOG . LogWarning ( ex , "Was not able to delete temporary Pandoc directory: '{Path}'." , path );
2025-05-29 14:01:56 +02:00
}
}
/// <summary>
/// Asynchronously fetch the content from Pandoc's latest release page and extract the latest version number
/// </summary>
/// <remarks>Version numbers can have the following formats: x.x, x.x.x or x.x.x.x</remarks>
/// <returns>Latest Pandoc version number</returns>
public static async Task < string > FetchLatestVersionAsync () {
2026-04-10 17:11:05 +02:00
var response = await WEB_CLIENT . GetAsync ( LATEST_URL );
2025-05-29 14:01:56 +02:00
if (! response . IsSuccessStatusCode )
{
2025-05-30 22:39:16 +02:00
LOG . LogError ( "Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}" , response . StatusCode , response . RequestMessage );
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendWarning ( new ( Icons . Material . Filled . Warning , string . Format ( TB ( "AI Studio couldn't find the latest Pandoc version and will install version {0} instead." ), FALLBACK_VERSION . ToString ())));
2025-05-29 14:01:56 +02:00
return FALLBACK_VERSION . ToString ();
}
var htmlContent = await response . Content . ReadAsStringAsync ();
var versionMatch = LatestVersionRegex (). Match ( htmlContent );
if (! versionMatch . Success )
{
2025-05-30 22:39:16 +02:00
LOG . LogError ( "The latest version regex returned nothing: {0}" , versionMatch . Groups . ToString ());
2026-05-16 18:27:16 +02:00
await MessageBus . INSTANCE . SendWarning ( new ( Icons . Material . Filled . Warning , string . Format ( TB ( "AI Studio couldn't find the latest Pandoc version and will install version {0} instead." ), FALLBACK_VERSION . ToString ())));
2025-05-29 14:01:56 +02:00
return FALLBACK_VERSION . ToString ();
}
var version = versionMatch . Groups [ 1 ]. Value ;
return version ;
}
/// <summary>
2025-05-30 22:39:16 +02:00
/// Reads the systems architecture to find the correct archive.
2025-05-29 14:01:56 +02:00
/// </summary>
2025-05-30 22:39:16 +02:00
/// <returns>Full URI to the right archive in Pandoc's repository.</returns>
public static async Task < string > GenerateArchiveUriAsync ()
2025-05-29 14:01:56 +02:00
{
var version = await FetchLatestVersionAsync ();
2026-05-16 18:27:16 +02:00
return GenerateArchiveUri ( version );
}
private static string GenerateArchiveUri ( string version )
{
2025-05-29 14:01:56 +02:00
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-" ;
return CPU_ARCHITECTURE switch
{
2025-05-30 22:39:16 +02:00
//
// Unfortunately, pandoc is not yet available for ARM64 Windows systems,
// so we have to use the x86_64 version for now. ARM Windows contains
// an x86_64 emulation layer, so it should work fine for now.
//
// Pandoc would be available for ARM64 Windows, but the Haskell compiler
// does not support ARM64 Windows yet. Here are the related issues:
//
// - Haskell compiler: https://gitlab.haskell.org/ghc/ghc/-/issues/24603
// - Haskell ARM MR: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/13856
// - Pandoc ARM64: https://github.com/jgm/pandoc/issues/10095
//
RID . WIN_X64 or RID . WIN_ARM64 => $"{baseUri}windows-x86_64.zip" ,
RID . OSX_X64 => $"{baseUri}x86_64-macOS.zip" ,
RID . OSX_ARM64 => $"{baseUri}arm64-macOS.zip" ,
RID . LINUX_X64 => $"{baseUri}linux-amd64.tar.gz" ,
RID . LINUX_ARM64 => $"{baseUri}linux-arm64.tar.gz" ,
2025-05-29 14:01:56 +02:00
_ => string . Empty ,
};
}
/// <summary>
/// Reads the systems architecture to find the correct Pandoc installer
/// </summary>
/// <returns>Full URI to the right installer in Pandoc's repo</returns>
public static async Task < string > GenerateInstallerUriAsync ()
{
var version = await FetchLatestVersionAsync ();
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-" ;
switch ( CPU_ARCHITECTURE )
{
2025-05-30 22:39:16 +02:00
//
// Unfortunately, pandoc is not yet available for ARM64 Windows systems,
// so we have to use the x86_64 version for now. ARM Windows contains
// an x86_64 emulation layer, so it should work fine for now.
//
// Pandoc would be available for ARM64 Windows, but the Haskell compiler
// does not support ARM64 Windows yet. Here are the related issues:
//
// - Haskell compiler: https://gitlab.haskell.org/ghc/ghc/-/issues/24603
// - Haskell ARM MR: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/13856
// - Pandoc ARM64: https://github.com/jgm/pandoc/issues/10095
//
case RID . WIN_X64 or RID . WIN_ARM64 :
2025-05-29 14:01:56 +02:00
return $"{baseUri}windows-x86_64.msi" ;
2025-05-30 22:39:16 +02:00
case RID . OSX_X64 :
2025-05-29 14:01:56 +02:00
return $"{baseUri}x86_64-macOS.pkg" ;
2025-05-30 22:39:16 +02:00
case RID . OSX_ARM64 :
return $"{baseUri}arm64-macOS.pkg" ;
2025-05-29 14:01:56 +02:00
default :
2025-05-30 22:39:16 +02:00
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Terminal , string . Format ( TB ( "Installers are not available on {0} systems." ), CPU_ARCHITECTURE . ToUserFriendlyName ())));
2025-05-29 14:01:56 +02:00
return string . Empty ;
}
}
2025-05-30 22:39:16 +02:00
public static async Task < string > GetPandocDataFolder ( RustService rustService ) => Path . Join ( await rustService . GetDataDirectory (), "pandoc" );
2025-05-29 14:01:56 +02:00
[GeneratedRegex(@"pandoc(?:\.exe)?\s*([0-9] + \ .[ 0 - 9 ]+(?: \ .[ 0 - 9 ]+)?(?: \ .[ 0 - 9 ]+)?) ")]
private static partial Regex PandocCmdRegex ();
[GeneratedRegex(@"pandoc(?:\.exe)?\s*([0-9] + \ .[ 0 - 9 ]+(?: \ .[ 0 - 9 ]+)?(?: \ .[ 0 - 9 ]+)?) ")]
private static partial Regex LatestVersionRegex ();
}