diff --git a/AffinityHook/AffinityHook.csproj b/AffinityHook/AffinityHook.csproj index 624cd91..10b0508 100644 --- a/AffinityHook/AffinityHook.csproj +++ b/AffinityHook/AffinityHook.csproj @@ -8,9 +8,11 @@ x64 - AffinityHook + AffinityHook Launch wrapper to hook Affinity and inject AffinityBootstrap 0.2.0 - Noah Curoe & AffinityHook Contributors + Noah Curoe & AffinityHook Contributors + + false diff --git a/AffinityPluginLoader/AffinityPluginLoader.csproj b/AffinityPluginLoader/AffinityPluginLoader.csproj index 508f206..cf19e69 100644 --- a/AffinityPluginLoader/AffinityPluginLoader.csproj +++ b/AffinityPluginLoader/AffinityPluginLoader.csproj @@ -7,10 +7,12 @@ x64 - AffinityHook - Plugin loader for Affinity by Canva. + Affinity Plugin Loader + Plugin loader for 'Affinity by Canva'. 0.2.0 - Noah Curoe & AffinityPluginLoader Contributors + Noah Curoe & APL Contributors + + false diff --git a/AffinityPluginLoader/Core/Logger.cs b/AffinityPluginLoader/Core/Logger.cs new file mode 100644 index 0000000..1e442f7 --- /dev/null +++ b/AffinityPluginLoader/Core/Logger.cs @@ -0,0 +1,350 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using HarmonyLib; + +namespace AffinityPluginLoader.Core +{ + /// + /// Logging API for APL and plugins. + /// Supports console and file output with log levels and rotation. + /// + public static class Logger + { + public enum LogLevel + { + DEBUG = 0, + INFO = 1, + WARNING = 2, + ERROR = 3, + NONE = 4 // Disables all logging + } + + private static LogLevel _minimumLevel = LogLevel.INFO; + private static bool _fileLoggingEnabled = false; + private static string _logFilePath = null; + private static readonly object _lockObj = new object(); + private static bool _initialized = false; + private static StreamWriter _fileWriter = null; + private static bool _hasConsole = false; + + // P/Invoke for console attachment + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AttachConsole(int dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetConsoleWindow(); + + private const int ATTACH_PARENT_PROCESS = -1; + + /// + /// Initialize the logger. Call this once at startup. + /// + public static void Initialize() + { + lock (_lockObj) + { + if (_initialized) + return; + + // Try to attach to parent console for output visibility + AttachToConsole(); + + // Parse APL_LOGGING environment variable + string aplLogging = Environment.GetEnvironmentVariable("APL_LOGGING"); + if (!string.IsNullOrEmpty(aplLogging)) + { + // Parse log level + if (Enum.TryParse(aplLogging.ToUpperInvariant(), out LogLevel level)) + { + _minimumLevel = level; + _fileLoggingEnabled = true; + } + else + { + // Invalid log level, default to DEBUG and warn + _minimumLevel = LogLevel.DEBUG; + _fileLoggingEnabled = true; + Console.WriteLine($"[WARNING] Invalid APL_LOGGING value '{aplLogging}'. Using DEBUG. Valid values: DEBUG, INFO, WARNING, ERROR, NONE"); + } + } + + // Setup file logging if enabled + if (_fileLoggingEnabled) + { + SetupFileLogging(); + } + + _initialized = true; + + // Log startup message with timezone info + var now = DateTime.Now; + var timezone = TimeZoneInfo.Local; + Info($"APL logging initialized"); + Info($"Local timezone: {timezone.DisplayName} (UTC{(timezone.BaseUtcOffset.TotalHours >= 0 ? "+" : "")}{timezone.BaseUtcOffset.TotalHours:0.##})"); + Info($"Log level: {_minimumLevel}"); + if (_fileLoggingEnabled) + { + Info($"File logging enabled: {_logFilePath}"); + } + } + } + + private static void AttachToConsole() + { + try + { + // Check if we already have a console + if (GetConsoleWindow() != IntPtr.Zero) + { + _hasConsole = true; + return; + } + + // Try to attach to parent process console (AffinityHook) + if (AttachConsole(ATTACH_PARENT_PROCESS)) + { + _hasConsole = true; + // Reopen standard output to the console + try + { + var stdOut = Console.OpenStandardOutput(); + Console.SetOut(new StreamWriter(stdOut, Console.OutputEncoding) { AutoFlush = true }); + var stdErr = Console.OpenStandardError(); + Console.SetError(new StreamWriter(stdErr, Console.OutputEncoding) { AutoFlush = true }); + } + catch + { + // If reopening fails, we still have the console attached + } + return; + } + + // If attaching to parent failed, we won't allocate a new console + // (Affinity is a GUI app and allocating a new console creates a popup window) + _hasConsole = false; + } + catch + { + _hasConsole = false; + } + } + + private static void SetupFileLogging() + { + try + { + // Determine log file path (plugins/logs/apl.latest.log) + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string pluginsDir = Path.Combine(assemblyDir, "plugins"); + string logsDir = Path.Combine(pluginsDir, "logs"); + + // Create plugins/logs directory if it doesn't exist + if (!Directory.Exists(logsDir)) + { + Directory.CreateDirectory(logsDir); + } + + _logFilePath = Path.Combine(logsDir, "apl.latest.log"); + + // Rotate existing log files + RotateLogFiles(_logFilePath); + + // Open log file for writing + _fileWriter = new StreamWriter(_logFilePath, append: false, Encoding.UTF8) + { + AutoFlush = true + }; + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to setup file logging: {ex.Message}"); + _fileLoggingEnabled = false; + } + } + + private static void RotateLogFiles(string logFilePath) + { + try + { + // If current log file doesn't exist, nothing to rotate + if (!File.Exists(logFilePath)) + return; + + // Get directory and base name (e.g., "apl.latest.log" -> "apl", ".log") + string logDir = Path.GetDirectoryName(logFilePath); + + // Delete apl.5.log if it exists + string log5 = Path.Combine(logDir, "apl.5.log"); + if (File.Exists(log5)) + { + File.Delete(log5); + } + + // Cycle logs: apl.4.log -> apl.5.log, apl.3.log -> apl.4.log, etc. + for (int i = 4; i >= 1; i--) + { + string oldLog = Path.Combine(logDir, $"apl.{i}.log"); + string newLog = Path.Combine(logDir, $"apl.{i + 1}.log"); + if (File.Exists(oldLog)) + { + File.Move(oldLog, newLog); + } + } + + // Move current log to apl.1.log + File.Move(logFilePath, Path.Combine(logDir, "apl.1.log")); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to rotate log files: {ex.Message}"); + } + } + + private static void Log(LogLevel level, string message) + { + if (!_initialized) + { + // Auto-initialize on first log call + Initialize(); + } + + // Filter by minimum level + if (level < _minimumLevel) + return; + + // Get the plugin name from the calling assembly + string pluginName = GetCallingPluginName(); + + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + string levelStr = level.ToString(); + string logLine = $"[{timestamp}] [{levelStr}] [APL/{pluginName}] {message}"; + + lock (_lockObj) + { + // Write to console if we have one attached + if (_hasConsole) + { + try + { + Console.WriteLine(logLine); + } + catch + { + // Console write failed, disable it + _hasConsole = false; + } + } + + // Write to file if enabled + if (_fileLoggingEnabled && _fileWriter != null) + { + try + { + _fileWriter.WriteLine(logLine); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to write to log file: {ex.Message}"); + } + } + } + } + + public static void Debug(string message) + { + Log(LogLevel.DEBUG, message); + } + + public static void Info(string message) + { + Log(LogLevel.INFO, message); + } + + public static void Warning(string message) + { + Log(LogLevel.WARNING, message); + } + + public static void Error(string message) + { + Log(LogLevel.ERROR, message); + } + + public static void Error(string message, Exception ex) + { + Error($"{message}: {ex.Message}"); + if (ex.StackTrace != null) + { + Error($"Stack trace:\n{ex.StackTrace}"); + } + } + + /// + /// Get the name of the plugin/assembly that called the logger + /// + private static string GetCallingPluginName() + { + try + { + // Walk up the stack to find the first assembly that isn't this one + var stackTrace = new System.Diagnostics.StackTrace(); + var frames = stackTrace.GetFrames(); + + if (frames != null) + { + var loggerAssembly = typeof(Logger).Assembly; + + foreach (var frame in frames) + { + var method = frame.GetMethod(); + if (method != null) + { + var declaringType = method.DeclaringType; + if (declaringType != null) + { + var assembly = declaringType.Assembly; + + // Skip our own assembly + if (assembly != loggerAssembly) + { + // Get the plugin name using the same approach as PluginManager + var nameAttr = assembly.GetCustomAttribute(); + return nameAttr?.Title ?? assembly.GetName().Name; + } + } + } + } + } + + // Fallback to "APL" if we can't determine the caller + return "Core"; + } + catch + { + return "Core"; + } + } + + /// + /// Cleanup resources. Call this on shutdown if needed. + /// + public static void Shutdown() + { + lock (_lockObj) + { + if (_fileWriter != null) + { + _fileWriter.Flush(); + _fileWriter.Close(); + _fileWriter = null; + } + } + } + } +} diff --git a/AffinityPluginLoader/Core/PluginManager.cs b/AffinityPluginLoader/Core/PluginManager.cs index bf4fbc0..d0fc765 100644 --- a/AffinityPluginLoader/Core/PluginManager.cs +++ b/AffinityPluginLoader/Core/PluginManager.cs @@ -22,34 +22,34 @@ public static void Initialize(Harmony harmony) if (_initialized) return; - FileLog.Log($"PluginManager initializing...\n"); + Logger.Info($"PluginManager initializing..."); - // Add AffinityPluginLoader itself as the first plugin + // Add APL itself as the first plugin var loaderAssembly = Assembly.GetExecutingAssembly(); - var loaderNameAttr = loaderAssembly.GetCustomAttribute(); - var loaderVersionAttr = loaderAssembly.GetCustomAttribute(); + var loaderProductAttr = loaderAssembly.GetCustomAttribute(); + var loaderVersionAttr = loaderAssembly.GetCustomAttribute(); var loaderCompanyAttr = loaderAssembly.GetCustomAttribute(); var loaderDescAttr = loaderAssembly.GetCustomAttribute(); - + var loaderInfo = new PluginInfo { - Name = loaderNameAttr?.Title ?? "AffinityPluginLoader", - Version = loaderVersionAttr?.Version ?? loaderAssembly.GetName().Version?.ToString() ?? "0.1.0.1", - Author = loaderCompanyAttr?.Company ?? "AffinityPluginLoader", + Name = loaderProductAttr?.Product ?? loaderAssembly.GetName().Name, + Version = FormatVersion(loaderVersionAttr?.InformationalVersion, loaderAssembly.GetName().Version), + Author = loaderCompanyAttr?.Company ?? "Unknown", AssemblyName = loaderAssembly.FullName, - Description = loaderDescAttr.Description + Description = loaderDescAttr?.Description ?? "" }; _loadedPlugins.Add(loaderInfo); - FileLog.Log($"Added AffinityPluginLoader to plugin list: {loaderInfo.Name} v{loaderInfo.Version}\n"); + Logger.Info($"Added APL to plugin list: {loaderInfo.Name} v{loaderInfo.Version}"); // Apply loader's own patches (version strings, preferences tab) - Patches.LoaderPatches.ApplyPatches(harmony); + Patches.LoaderPatches.ApplyPatches(harmony, loaderInfo); // Load plugins from ./plugins/ directory LoadPlugins(harmony); _initialized = true; - FileLog.Log($"PluginManager initialized with {_loadedPlugins.Count} plugins\n"); + Logger.Info($"PluginManager initialized with {_loadedPlugins.Count} plugins"); } private static void LoadPlugins(Harmony harmony) @@ -61,18 +61,18 @@ private static void LoadPlugins(Harmony harmony) string loaderDir = Path.GetDirectoryName(loaderPath); string pluginsDir = Path.Combine(loaderDir, "plugins"); - FileLog.Log($"Looking for plugins in: {pluginsDir}\n"); + Logger.Debug($"Looking for plugins in: {pluginsDir}"); if (!Directory.Exists(pluginsDir)) { - FileLog.Log($"Plugins directory not found, creating it...\n"); + Logger.Info($"Plugins directory not found, creating it..."); Directory.CreateDirectory(pluginsDir); return; } // Load all DLLs in the plugins directory var pluginFiles = Directory.GetFiles(pluginsDir, "*.dll"); - FileLog.Log($"Found {pluginFiles.Length} DLL files in plugins directory\n"); + Logger.Debug($"Found {pluginFiles.Length} DLL files in plugins directory"); foreach (var pluginFile in pluginFiles) { @@ -82,36 +82,34 @@ private static void LoadPlugins(Harmony harmony) } catch (Exception ex) { - FileLog.Log($"Failed to load plugin {Path.GetFileName(pluginFile)}: {ex.Message}\n"); + Logger.Error($"Failed to load plugin {Path.GetFileName(pluginFile)}: {ex.Message}"); } } } catch (Exception ex) { - FileLog.Log($"Error loading plugins: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error loading plugins", ex); } } private static void LoadPlugin(string pluginPath, Harmony harmony) { - FileLog.Log($"Loading plugin: {Path.GetFileName(pluginPath)}\n"); + Logger.Debug($"Loading plugin: {Path.GetFileName(pluginPath)}"); // Load the assembly var assembly = Assembly.LoadFrom(pluginPath); - - // Get plugin metadata from assembly attributes - var nameAttr = assembly.GetCustomAttribute(); - var versionAttr = assembly.GetCustomAttribute(); + var productAttr = assembly.GetCustomAttribute(); + var versionAttr = assembly.GetCustomAttribute(); var companyAttr = assembly.GetCustomAttribute(); var descAttr = assembly.GetCustomAttribute(); var pluginInfo = new PluginInfo { - Name = nameAttr?.Title ?? assembly.GetName().Name, - Version = versionAttr?.Version ?? assembly.GetName().Version?.ToString() ?? "1.0.0", + Name = productAttr?.Product ?? assembly.GetName().Name, + Version = FormatVersion(versionAttr?.InformationalVersion, assembly.GetName().Version), Author = companyAttr?.Company ?? "Unknown", AssemblyName = assembly.FullName, - Description = descAttr.Description ?? "" + Description = descAttr?.Description ?? "" }; // Look for IAffinityPlugin interface implementation @@ -127,21 +125,53 @@ private static void LoadPlugin(string pluginPath, Harmony harmony) { var plugin = Activator.CreateInstance(pluginType) as IAffinityPlugin; plugin?.Initialize(harmony); - FileLog.Log($" Initialized plugin: {pluginType.Name}\n"); + Logger.Info($"Initialized plugin: {pluginType.Name}"); } catch (Exception ex) { - FileLog.Log($" Failed to initialize {pluginType.Name}: {ex.Message}\n"); + Logger.Error($"Failed to initialize {pluginType.Name}: {ex.Message}"); } } } else { - FileLog.Log($" No IAffinityPlugin implementation found, plugin loaded but not initialized\n"); + Logger.Info($"No IAffinityPlugin implementation found, plugin loaded but not initialized"); } _loadedPlugins.Add(pluginInfo); - FileLog.Log($" Plugin loaded: {pluginInfo.Name} v{pluginInfo.Version} by {pluginInfo.Author}\n"); + Logger.Info($"Plugin loaded: {pluginInfo.Name} v{pluginInfo.Version} by {pluginInfo.Author}"); + } + + /// + /// Format version string, truncating git hash to 8 chars if present + /// + private static string FormatVersion(string informationalVersion, Version assemblyVersion) + { + // If we have an informational version, process it + if (!string.IsNullOrEmpty(informationalVersion)) + { + // Check if it contains a git hash (format: "version+hash") + int plusIndex = informationalVersion.IndexOf('+'); + if (plusIndex > 0 && plusIndex < informationalVersion.Length - 1) + { + string version = informationalVersion.Substring(0, plusIndex); + string hash = informationalVersion.Substring(plusIndex + 1); + + // Truncate hash to 8 chars if longer + if (hash.Length > 8) + { + hash = hash.Substring(0, 8); + } + + return $"{version}+{hash}"; + } + + // No git hash, return as-is + return informationalVersion; + } + + // Fallback to assembly version + return assemblyVersion?.ToString() ?? "0.0.0"; } } diff --git a/AffinityPluginLoader/EntryPoint.cs b/AffinityPluginLoader/EntryPoint.cs index ef49cc1..bc541ac 100644 --- a/AffinityPluginLoader/EntryPoint.cs +++ b/AffinityPluginLoader/EntryPoint.cs @@ -2,6 +2,7 @@ using System.IO; using System.Reflection; using HarmonyLib; +using AffinityPluginLoader.Core; namespace AffinityPluginLoader { @@ -24,7 +25,7 @@ public static int Initialize(string args) } catch (Exception ex) { - FileLog.Log($"Static Initialize error: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Static Initialize error", ex); return 1; } } @@ -39,12 +40,15 @@ private void InitializeInternal() { if (_initialized) return; - + _initialized = true; } - - FileLog.Log($"AffinityPluginLoader initializing... {DateTime.UtcNow}\n"); - FileLog.Log($"Current AppDomain: {AppDomain.CurrentDomain.FriendlyName}\n"); + + // Initialize logger first + Logger.Initialize(); + + Logger.Info($"APL initializing... {DateTime.UtcNow}"); + Logger.Debug($"Current AppDomain: {AppDomain.CurrentDomain.FriendlyName}"); try { @@ -53,29 +57,29 @@ private void InitializeInternal() if (defaultDomain != null && defaultDomain != AppDomain.CurrentDomain) { - FileLog.Log($"Switching to default AppDomain: {defaultDomain.FriendlyName}\n"); - + Logger.Info($"Switching to default AppDomain: {defaultDomain.FriendlyName}"); + // Since AffinityPluginLoader.dll is now in Affinity's folder, // the default domain can find it naturally var patcherType = typeof(DefaultDomainPatcher); var patcher = (DefaultDomainPatcher)defaultDomain.CreateInstanceAndUnwrap( patcherType.Assembly.FullName, patcherType.FullName); - + patcher.Initialize(); - FileLog.Log($"AffinityPluginLoader initialized in default AppDomain\n"); + Logger.Info($"APL initialized in default AppDomain"); } else { // Fallback: run in current domain - FileLog.Log($"Running in current AppDomain\n"); + Logger.Info($"Running in current AppDomain"); var harmony = new Harmony("dev.ncuroe.affinitypluginloader"); Core.PluginManager.Initialize(harmony); } } catch (Exception ex) { - FileLog.Log($"Error: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error during initialization", ex); } } @@ -92,7 +96,7 @@ private AppDomain GetDefaultAppDomain() } catch (Exception ex) { - FileLog.Log($"Error getting default AppDomain: {ex.Message}\n"); + Logger.Error($"Error getting default AppDomain: {ex.Message}"); } return null; @@ -106,14 +110,14 @@ public void Initialize() { try { - HarmonyLib.FileLog.Log($"DefaultDomainPatcher running in AppDomain: {AppDomain.CurrentDomain.FriendlyName}\n"); - + Logger.Debug($"DefaultDomainPatcher running in AppDomain: {AppDomain.CurrentDomain.FriendlyName}"); + var harmony = new Harmony("dev.ncuroe.affinitypluginloader"); Core.PluginManager.Initialize(harmony); } catch (Exception ex) { - HarmonyLib.FileLog.Log($"Error in DefaultDomainPatcher: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error in DefaultDomainPatcher", ex); } } } diff --git a/AffinityPluginLoader/Patches/LoaderPatches.cs b/AffinityPluginLoader/Patches/LoaderPatches.cs index a51b074..0443e93 100644 --- a/AffinityPluginLoader/Patches/LoaderPatches.cs +++ b/AffinityPluginLoader/Patches/LoaderPatches.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using HarmonyLib; +using AffinityPluginLoader.Core; namespace AffinityPluginLoader.Patches { @@ -12,16 +13,19 @@ public static class LoaderPatches { private static Harmony _harmony; private static bool _patchesApplied = false; + private static string _assemblyVersion = ""; - public static void ApplyPatches(Harmony harmony) + public static void ApplyPatches(Harmony harmony, PluginInfo plugin) { _harmony = harmony; - - FileLog.Log($"Applying AffinityPluginLoader patches...\n"); - + + Logger.Info($"Applying Affinity Plugin Loader patches..."); + + _assemblyVersion = plugin.Version ?? "not found"; + // Apply version string patches ApplyVersionPatches(); - + // Apply preferences dialog patches PreferencesPatches.ApplyPatches(harmony); } @@ -36,20 +40,20 @@ private static void ApplyVersionPatches() // Find the Serif.Affinity assembly var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); - + if (serifAssembly == null) { - FileLog.Log($"ERROR: Serif.Affinity assembly not found\n"); + Logger.Error($"ERROR: Serif.Affinity assembly not found"); return; } - - FileLog.Log($"Found Serif.Affinity assembly: {serifAssembly.GetName().Version}\n"); - + + Logger.Info($"Found Serif.Affinity assembly: {serifAssembly.GetName().Version}"); + // Get the Application type var applicationType = serifAssembly.GetType("Serif.Affinity.Application"); if (applicationType == null) { - FileLog.Log($"ERROR: Application type not found\n"); + Logger.Error($"ERROR: Application type not found"); return; } @@ -59,23 +63,22 @@ private static void ApplyVersionPatches() { var postfix = typeof(LoaderPatches).GetMethod(nameof(GetVerboseVersionString_Postfix), BindingFlags.Static | BindingFlags.Public); _harmony.Patch(getVerboseVersionString, postfix: new HarmonyMethod(postfix)); - FileLog.Log($"Patched GetCurrentVerboseVersionString\n"); + Logger.Info($"Patched GetCurrentVerboseVersionString"); } - + _patchesApplied = true; - FileLog.Log($"Version patches applied successfully!\n"); + Logger.Info($"Version patches applied successfully!"); } catch (Exception ex) { - FileLog.Log($"Failed to apply version patches: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Failed to apply version patches", ex); } } // Postfix for GetCurrentVerboseVersionString (splash screen) public static void GetVerboseVersionString_Postfix(ref string __result) { - var version = Assembly.GetExecutingAssembly().GetName().Version; - __result = __result + $" (AffinityPluginLoader {version})"; + __result = __result + $" (APL {_assemblyVersion})"; } } } diff --git a/AffinityPluginLoader/Patches/PreferencesPatches.cs b/AffinityPluginLoader/Patches/PreferencesPatches.cs index 31024c6..2c5daa0 100644 --- a/AffinityPluginLoader/Patches/PreferencesPatches.cs +++ b/AffinityPluginLoader/Patches/PreferencesPatches.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using HarmonyLib; +using AffinityPluginLoader.Core; using AffinityPluginLoader.UI; namespace AffinityPluginLoader.Patches @@ -15,23 +16,23 @@ public static void ApplyPatches(Harmony harmony) { try { - FileLog.Log($"Applying PreferencesDialog patches\n"); - + Logger.Info($"Applying PreferencesDialog patches"); + // Find the Serif.Affinity assembly var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); - + if (serifAssembly == null) { - FileLog.Log($"ERROR: Serif.Affinity assembly not found for preferences patch\n"); + Logger.Error($"ERROR: Serif.Affinity assembly not found for preferences patch"); return; } - + // Get the PreferencesDialog type var preferencesDialogType = serifAssembly.GetType("Serif.Affinity.UI.Dialogs.Preferences.PreferencesDialog"); if (preferencesDialogType == null) { - FileLog.Log($"ERROR: PreferencesDialog type not found\n"); + Logger.Error($"ERROR: PreferencesDialog type not found"); return; } @@ -44,19 +45,19 @@ public static void ApplyPatches(Harmony harmony) if (constructor != null) { - FileLog.Log($"Found PreferencesDialog constructor\n"); + Logger.Info($"Found PreferencesDialog constructor"); var postfix = typeof(PreferencesPatches).GetMethod(nameof(PreferencesDialog_Constructor_Postfix), BindingFlags.Static | BindingFlags.Public); harmony.Patch(constructor, postfix: new HarmonyMethod(postfix)); - FileLog.Log($"Patched PreferencesDialog constructor\n"); + Logger.Info($"Patched PreferencesDialog constructor"); } else { - FileLog.Log($"ERROR: PreferencesDialog constructor not found\n"); + Logger.Error($"ERROR: PreferencesDialog constructor not found"); } } catch (Exception ex) { - FileLog.Log($"Failed to apply preferences patches: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Failed to apply preferences patches", ex); } } @@ -65,22 +66,22 @@ public static void PreferencesDialog_Constructor_Postfix(object __instance) { try { - FileLog.Log($"PreferencesDialog constructor postfix called\n"); - + Logger.Debug($"PreferencesDialog constructor postfix called"); + // Get the type of the dialog var dialogType = __instance.GetType(); - + // Find the property that holds the pages (it's called "Pages") var pagesProperty = dialogType.GetProperty("Pages", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - + if (pagesProperty != null) { var pages = pagesProperty.GetValue(__instance); if (pages is System.Collections.IList pageList) { - FileLog.Log($"Found Pages property with {pageList.Count} existing pages\n"); + Logger.Debug($"Found Pages property with {pageList.Count} existing pages"); - // Add a separator before the AffinityPluginLoader tab + // Add a separator before the Affinity Plugin Loader tab var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); @@ -97,13 +98,13 @@ public static void PreferencesDialog_Constructor_Postfix(object __instance) { indexProperty.SetValue(separator, pageList.Count); } - + pageList.Add(separator); - FileLog.Log($"Added separator to preferences dialog\n"); + Logger.Debug($"Added separator to preferences dialog"); } else { - FileLog.Log($"PreferencesPageSeparator type not found, skipping separator\n"); + Logger.Debug($"PreferencesPageSeparator type not found, skipping separator"); } } @@ -118,24 +119,24 @@ public static void PreferencesDialog_Constructor_Postfix(object __instance) { indexProperty.SetValue(pluginsPage, pageList.Count); } - + pageList.Add(pluginsPage); - FileLog.Log($"Added AffinityPluginLoader tab to preferences dialog\n"); + Logger.Info($"Added Affinity Plugin Loader tab to preferences dialog"); } } else { - FileLog.Log($"Pages property is not IList: {pages?.GetType()?.FullName}\n"); + Logger.Debug($"Pages property is not IList: {pages?.GetType()?.FullName}"); } } else { - FileLog.Log($"Could not find Pages property in PreferencesDialog\n"); + Logger.Debug($"Could not find Pages property in PreferencesDialog"); } } catch (Exception ex) { - FileLog.Log($"Error in PreferencesDialog postfix: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error in PreferencesDialog postfix", ex); } } } diff --git a/AffinityPluginLoader/UI/PluginsPreferencesPage.cs b/AffinityPluginLoader/UI/PluginsPreferencesPage.cs index 75d7cd5..e3fadb2 100644 --- a/AffinityPluginLoader/UI/PluginsPreferencesPage.cs +++ b/AffinityPluginLoader/UI/PluginsPreferencesPage.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Markup; +using AffinityPluginLoader.Core; namespace AffinityPluginLoader.UI { @@ -28,23 +29,23 @@ public static object CreatePage() { try { - HarmonyLib.FileLog.Log($"Creating PluginsPreferencesPage\n"); - + Logger.Debug($"Creating PluginsPreferencesPage"); + // Find Serif.Affinity assembly (it's already loaded in the Affinity process) var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); - + if (serifAssembly == null) { - HarmonyLib.FileLog.Log($"ERROR: Serif.Affinity not found\n"); + Logger.Error($"ERROR: Serif.Affinity not found"); return null; } - + // Get PreferencesPage base type var preferencesPageType = serifAssembly.GetType("Serif.Affinity.UI.Dialogs.Preferences.PreferencesPage"); if (preferencesPageType == null) { - HarmonyLib.FileLog.Log($"ERROR: PreferencesPage type not found\n"); + Logger.Error($"ERROR: PreferencesPage type not found"); return null; } @@ -67,7 +68,7 @@ public static object CreatePage() if (content == null) { - HarmonyLib.FileLog.Log($"ERROR: Could not load XAML\n"); + Logger.Error($"ERROR: Could not load XAML"); return null; } @@ -102,15 +103,15 @@ public static object CreatePage() var pageNameProperty = preferencesPageType.GetProperty("PageName"); if (pageNameProperty != null) { - pageNameProperty.SetValue(grid, "AffinityPluginLoader"); + pageNameProperty.SetValue(grid, "Affinity Plugin Loader"); } - - HarmonyLib.FileLog.Log($"PluginsPreferencesPage created successfully\n"); + + Logger.Debug($"PluginsPreferencesPage created successfully"); return grid; } catch (Exception ex) { - HarmonyLib.FileLog.Log($"Error creating PluginsPreferencesPage: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error creating PluginsPreferencesPage", ex); return null; } } diff --git a/WineFix/Patches/MainWindowLoadedPatch.cs b/WineFix/Patches/MainWindowLoadedPatch.cs index 1a27d8f..c4f0326 100644 --- a/WineFix/Patches/MainWindowLoadedPatch.cs +++ b/WineFix/Patches/MainWindowLoadedPatch.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Reflection.Emit; using HarmonyLib; +using AffinityPluginLoader.Core; namespace WineFix.Patches { @@ -18,23 +19,23 @@ public static void ApplyPatches(Harmony harmony) { try { - HarmonyLib.FileLog.Log($"Applying MainWindowLoaded patch (Wine fix)...\n"); - + Logger.Info($"Applying MainWindowLoaded patch (Wine fix)..."); + // Find the Serif.Affinity assembly var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); - + if (serifAssembly == null) { - HarmonyLib.FileLog.Log($"ERROR: Serif.Affinity assembly not found\n"); + Logger.Error($"ERROR: Serif.Affinity assembly not found"); return; } - + // Get the Application type var applicationType = serifAssembly.GetType("Serif.Affinity.Application"); if (applicationType == null) { - HarmonyLib.FileLog.Log($"ERROR: Application type not found\n"); + Logger.Error($"ERROR: Application type not found"); return; } @@ -45,19 +46,19 @@ public static void ApplyPatches(Harmony harmony) if (onMainWindowLoaded != null) { // Use transpiler to modify the IL - var transpiler = typeof(MainWindowLoadedPatch).GetMethod(nameof(OnMainWindowLoaded_Transpiler), + var transpiler = typeof(MainWindowLoadedPatch).GetMethod(nameof(OnMainWindowLoaded_Transpiler), BindingFlags.Static | BindingFlags.Public); harmony.Patch(onMainWindowLoaded, transpiler: new HarmonyMethod(transpiler)); - HarmonyLib.FileLog.Log($"Patched OnMainWindowLoaded to skip HasPreviousPackageInstalled call\n"); + Logger.Info($"Patched OnMainWindowLoaded to skip HasPreviousPackageInstalled call"); } else { - HarmonyLib.FileLog.Log($"ERROR: OnMainWindowLoaded method not found\n"); + Logger.Error($"ERROR: OnMainWindowLoaded method not found"); } } catch (Exception ex) { - HarmonyLib.FileLog.Log($"Failed to apply MainWindowLoaded patch: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Failed to apply MainWindowLoaded patch", ex); } } @@ -75,13 +76,13 @@ public static IEnumerable OnMainWindowLoaded_Transpiler(IEnumer // Look for: call instance bool Serif.Affinity.Application::HasPreviousPackageInstalled() // (Index 1 in the IL - this is a non-virtual call, not callvirt!) - if (!patchedPackageCheck && + if (!patchedPackageCheck && (instruction.opcode == OpCodes.Call || instruction.opcode == OpCodes.Callvirt) && instruction.operand is MethodInfo method && method.Name == "HasPreviousPackageInstalled") { - HarmonyLib.FileLog.Log($"Found HasPreviousPackageInstalled call at instruction {i}\n"); - + Logger.Debug($"Found HasPreviousPackageInstalled call at instruction {i}"); + // Replace: ldarg.0 + call HasPreviousPackageInstalled // With: ldc.i4.0 (just load false, skip the ldarg.0 before it) // Check if previous instruction is ldarg.0 @@ -91,19 +92,19 @@ instruction.operand is MethodInfo method && var newLoadFalse = new CodeInstruction(OpCodes.Ldc_I4_0); newLoadFalse.labels.AddRange(codes[i - 1].labels); // Transfer labels! codes[i - 1] = newLoadFalse; - + var newNop = new CodeInstruction(OpCodes.Nop); newNop.labels.AddRange(codes[i].labels); // Transfer labels! codes[i] = newNop; - - HarmonyLib.FileLog.Log($"Replaced ldarg.0 + HasPreviousPackageInstalled with 'ldc.i4.0' (false)\n"); + + Logger.Debug($"Replaced ldarg.0 + HasPreviousPackageInstalled with 'ldc.i4.0' (false)"); } else { // Fallback: just replace the call codes[i] = new CodeInstruction(OpCodes.Pop); codes.Insert(i + 1, new CodeInstruction(OpCodes.Ldc_I4_0)); - HarmonyLib.FileLog.Log($"Replaced HasPreviousPackageInstalled call with 'false' (fallback)\n"); + Logger.Debug($"Replaced HasPreviousPackageInstalled call with 'false' (fallback)"); } patchedPackageCheck = true; @@ -120,36 +121,36 @@ instruction.operand is MethodInfo baseMethod && (baseMethod.DeclaringType.FullName == "Serif.Interop.Persona.Application" || baseMethod.DeclaringType.FullName == "System.Windows.Application")) { - HarmonyLib.FileLog.Log($"Found base.OnMainWindowLoaded call at instruction {i}\n"); - + Logger.Debug($"Found base.OnMainWindowLoaded call at instruction {i}"); + // The IL sequence is (indices 20-22): // ldarg.0 ; load 'this' // ldarg.1 ; load 'mainWindow' // call base.OnMainWindowLoaded // Replace all three with NOPs, preserving labels - if (i >= 2 && - codes[i - 2].opcode == OpCodes.Ldarg_0 && + if (i >= 2 && + codes[i - 2].opcode == OpCodes.Ldarg_0 && codes[i - 1].opcode == OpCodes.Ldarg_1) { // Create NOPs but preserve labels var nop1 = new CodeInstruction(OpCodes.Nop); nop1.labels.AddRange(codes[i - 2].labels); codes[i - 2] = nop1; - + var nop2 = new CodeInstruction(OpCodes.Nop); nop2.labels.AddRange(codes[i - 1].labels); codes[i - 1] = nop2; - + var nop3 = new CodeInstruction(OpCodes.Nop); nop3.labels.AddRange(codes[i].labels); codes[i] = nop3; - - HarmonyLib.FileLog.Log($"Removed base.OnMainWindowLoaded call (indices {i-2} to {i})\n"); + + Logger.Debug($"Removed base.OnMainWindowLoaded call (indices {i-2} to {i})"); patchedBaseCall = true; } else { - HarmonyLib.FileLog.Log($"WARNING: Found base.OnMainWindowLoaded but preceding instructions don't match expected pattern\n"); + Logger.Warning($"WARNING: Found base.OnMainWindowLoaded but preceding instructions don't match expected pattern"); var nop = new CodeInstruction(OpCodes.Nop); nop.labels.AddRange(codes[i].labels); codes[i] = nop; @@ -161,12 +162,12 @@ instruction.operand is MethodInfo baseMethod && if (!patchedPackageCheck) { - HarmonyLib.FileLog.Log($"WARNING: Could not find HasPreviousPackageInstalled call to patch\n"); + Logger.Warning($"WARNING: Could not find HasPreviousPackageInstalled call to patch"); } - + if (!patchedBaseCall) { - HarmonyLib.FileLog.Log($"WARNING: Could not find base.OnMainWindowLoaded call to patch\n"); + Logger.Warning($"WARNING: Could not find base.OnMainWindowLoaded call to patch"); } return codes.AsEnumerable(); diff --git a/WineFix/WineFix.csproj b/WineFix/WineFix.csproj index 4a702c4..c64b1c6 100644 --- a/WineFix/WineFix.csproj +++ b/WineFix/WineFix.csproj @@ -7,10 +7,12 @@ x64 - WineFix + WineFix Plugin to fix Wine compatibility issues in Affinity applications 0.2.0 - Noah Curoe & WineFix Contributors + Noah Curoe & WineFix Contributors + + false diff --git a/WineFix/WineFixPlugin.cs b/WineFix/WineFixPlugin.cs index a0a1c6d..8ece872 100644 --- a/WineFix/WineFixPlugin.cs +++ b/WineFix/WineFixPlugin.cs @@ -1,5 +1,6 @@ using System; using HarmonyLib; +using AffinityPluginLoader.Core; namespace WineFix { @@ -12,16 +13,16 @@ public void Initialize(Harmony harmony) { try { - FileLog.Log($"WineFix plugin initializing...\n"); - + Logger.Info($"WineFix plugin initializing..."); + // Apply Wine compatibility patches Patches.MainWindowLoadedPatch.ApplyPatches(harmony); - - FileLog.Log($"WineFix plugin initialized successfully\n"); + + Logger.Info($"WineFix plugin initialized successfully"); } catch (Exception ex) { - FileLog.Log($"Error initializing WineFix: {ex.Message}\n{ex.StackTrace}\n"); + Logger.Error("Error initializing WineFix", ex); } } } diff --git a/build.bat b/build.bat index d39593a..b1f3f05 100644 --- a/build.bat +++ b/build.bat @@ -4,6 +4,9 @@ REM Builds both .NET assemblies and native AffinityBootstrap.dll setlocal enabledelayedexpansion +set CONFIGURATION=%1 +if "%CONFIGURATION%"=="" set CONFIGURATION=Release + echo ======================================== echo Building AffinityPluginLoader echo ======================================== @@ -11,7 +14,7 @@ echo. REM Build .NET projects echo [1/2] Building .NET projects... -dotnet build -c Release +dotnet build -c %CONFIGURATION% if %errorlevel% neq 0 ( echo Error: .NET build failed exit /b 1 diff --git a/build.sh b/build.sh index 86323c3..c0f720a 100755 --- a/build.sh +++ b/build.sh @@ -3,6 +3,8 @@ # Builds both .NET assemblies and native AffinityBootstrap.dll set -e +CONFIGURATION="${1:-Release}" + ROOTDIR=$(dirname "$(readlink -f "$0")") pushd "$ROOTDIR" @@ -14,7 +16,7 @@ echo # Build .NET projects echo "[1/2] Building .NET projects..." -dotnet build -c Release +dotnet build -c "$CONFIGURATION" echo # Build AffinityBootstrap diff --git a/package-release.ps1 b/package-release.ps1 index d02c6f2..bb35fc6 100644 --- a/package-release.ps1 +++ b/package-release.ps1 @@ -3,14 +3,18 @@ # Creates release archives for distribution param( - [switch]$SkipBuild + [switch]$SkipBuild, + [switch]$Debug ) $ErrorActionPreference = "Stop" +$Configuration = if ($Debug) { "Debug" } else { "Release" } + Write-Host "========================================" -ForegroundColor Cyan Write-Host "AffinityPluginLoader Release Packaging" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Configuration: $Configuration" -ForegroundColor Green Write-Host # Function to parse version from .csproj file @@ -30,7 +34,11 @@ function Get-ProjectVersion { # Build everything if not skipping if (-not $SkipBuild) { Write-Host "[1/4] Building all projects..." -ForegroundColor Yellow - & .\build.bat + if ($Debug) { + & .\build.bat Debug + } else { + & .\build.bat + } if ($LASTEXITCODE -ne 0) { throw "Build failed" } @@ -61,10 +69,10 @@ $apl_temp = "releases\apl_temp" New-Item -ItemType Directory -Path $apl_temp | Out-Null # Copy files for AffinityPluginLoader package -Copy-Item "AffinityPluginLoader\bin\Release\net48\win-x64\0Harmony.dll" $apl_temp +Copy-Item "AffinityPluginLoader\bin\$Configuration\net48\win-x64\0Harmony.dll" $apl_temp Copy-Item "AffinityBootstrap\build\AffinityBootstrap.dll" $apl_temp -Copy-Item "AffinityHook\bin\Release\net48\win-x64\AffinityHook.exe" $apl_temp -Copy-Item "AffinityPluginLoader\bin\Release\net48\win-x64\AffinityPluginLoader.dll" $apl_temp +Copy-Item "AffinityHook\bin\$Configuration\net48\win-x64\AffinityHook.exe" $apl_temp +Copy-Item "AffinityPluginLoader\bin\$Configuration\net48\win-x64\AffinityPluginLoader.dll" $apl_temp Copy-Item "README.md" $apl_temp Copy-Item "AffinityPluginLoader\LICENSE" $apl_temp @@ -83,7 +91,7 @@ New-Item -ItemType Directory -Path "$winefix_temp\plugins" | Out-Null # Copy files for WineFix package Copy-Item "README.md" $winefix_temp Copy-Item "WineFix\LICENSE" $winefix_temp -Copy-Item "WineFix\bin\Release\net48\win-x64\WineFix.dll" "$winefix_temp\plugins\" +Copy-Item "WineFix\bin\$Configuration\net48\win-x64\WineFix.dll" "$winefix_temp\plugins\" # Create zip Compress-Archive -Path "$winefix_temp\*" -DestinationPath "releases\winefix-v$winefix_version.zip" -Force @@ -98,11 +106,11 @@ New-Item -ItemType Directory -Path $combined_temp | Out-Null New-Item -ItemType Directory -Path "$combined_temp\plugins" | Out-Null # Copy files for combined package -Copy-Item "AffinityPluginLoader\bin\Release\net48\win-x64\0Harmony.dll" $combined_temp +Copy-Item "AffinityPluginLoader\bin\$Configuration\net48\win-x64\0Harmony.dll" $combined_temp Copy-Item "AffinityBootstrap\build\AffinityBootstrap.dll" $combined_temp -Copy-Item "AffinityHook\bin\Release\net48\win-x64\AffinityHook.exe" $combined_temp -Copy-Item "AffinityPluginLoader\bin\Release\net48\win-x64\AffinityPluginLoader.dll" $combined_temp -Copy-Item "WineFix\bin\Release\net48\win-x64\WineFix.dll" "$combined_temp\plugins\" +Copy-Item "AffinityHook\bin\$Configuration\net48\win-x64\AffinityHook.exe" $combined_temp +Copy-Item "AffinityPluginLoader\bin\$Configuration\net48\win-x64\AffinityPluginLoader.dll" $combined_temp +Copy-Item "WineFix\bin\$Configuration\net48\win-x64\WineFix.dll" "$combined_temp\plugins\" # Create tar.xz (requires tar command, available in Windows 10+) $tar_path = "releases\affinitypluginloader-plus-winefix.tar" diff --git a/package-release.sh b/package-release.sh index 311f9e6..3570295 100755 --- a/package-release.sh +++ b/package-release.sh @@ -5,13 +5,29 @@ set -e SKIP_BUILD=false -if [ "$1" = "--skip-build" ]; then - SKIP_BUILD=true -fi +CONFIGURATION="Release" + +# Parse arguments +for arg in "$@"; do + case $arg in + --skip-build) + SKIP_BUILD=true + ;; + --debug) + CONFIGURATION="Debug" + ;; + *) + echo "Unknown argument: $arg" + echo "Usage: $0 [--skip-build] [--debug]" + exit 1 + ;; + esac +done echo "========================================" echo "AffinityPluginLoader Release Packaging" echo "========================================" +echo "Configuration: $CONFIGURATION" echo # Function to parse version from .csproj file @@ -30,7 +46,11 @@ get_project_version() { # Build everything if not skipping if [ "$SKIP_BUILD" = false ]; then echo "[1/4] Building all projects..." - bash build.sh + if [ "$CONFIGURATION" = "Debug" ]; then + bash build.sh Debug + else + bash build.sh + fi echo else echo "[1/4] Skipping build (using existing binaries)..." @@ -58,10 +78,10 @@ APL_TEMP="$OUTPUT_DIR/apl_temp" mkdir -p "$APL_TEMP" # Copy files for AffinityPluginLoader package -cp "AffinityPluginLoader/bin/x64/Release/net48/win-x64/0Harmony.dll" "$APL_TEMP/" +cp "AffinityPluginLoader/bin/x64/$CONFIGURATION/net48/win-x64/0Harmony.dll" "$APL_TEMP/" cp "AffinityBootstrap/build/AffinityBootstrap.dll" "$APL_TEMP/" -cp "AffinityHook/bin/x64/Release/net48/win-x64/AffinityHook.exe" "$APL_TEMP/" -cp "AffinityPluginLoader/bin/x64/Release/net48/win-x64/AffinityPluginLoader.dll" "$APL_TEMP/" +cp "AffinityHook/bin/x64/$CONFIGURATION/net48/win-x64/AffinityHook.exe" "$APL_TEMP/" +cp "AffinityPluginLoader/bin/x64/$CONFIGURATION/net48/win-x64/AffinityPluginLoader.dll" "$APL_TEMP/" cp "README.md" "$APL_TEMP/" cp "AffinityPluginLoader/LICENSE" "$APL_TEMP/" @@ -79,7 +99,7 @@ mkdir -p "$WINEFIX_TEMP/plugins" # Copy files for WineFix package cp "README.md" "$WINEFIX_TEMP/" cp "WineFix/LICENSE" "$WINEFIX_TEMP/" -cp "WineFix/bin/x64/Release/net48/win-x64/WineFix.dll" "$WINEFIX_TEMP/plugins/" +cp "WineFix/bin/x64/$CONFIGURATION/net48/win-x64/WineFix.dll" "$WINEFIX_TEMP/plugins/" # Create zip (cd "$WINEFIX_TEMP" && zip -q -r "../winefix-v$WINEFIX_VERSION.zip" *) @@ -93,11 +113,11 @@ COMBINED_TEMP="$OUTPUT_DIR/combined_temp" mkdir -p "$COMBINED_TEMP/plugins" # Copy files for combined package -cp "AffinityPluginLoader/bin/x64/Release/net48/win-x64/0Harmony.dll" "$COMBINED_TEMP/" +cp "AffinityPluginLoader/bin/x64/$CONFIGURATION/net48/win-x64/0Harmony.dll" "$COMBINED_TEMP/" cp "AffinityBootstrap/build/AffinityBootstrap.dll" "$COMBINED_TEMP/" -cp "AffinityHook/bin/x64/Release/net48/win-x64/AffinityHook.exe" "$COMBINED_TEMP/" -cp "AffinityPluginLoader/bin/x64/Release/net48/win-x64/AffinityPluginLoader.dll" "$COMBINED_TEMP/" -cp "WineFix/bin/x64/Release/net48/win-x64/WineFix.dll" "$COMBINED_TEMP/plugins/" +cp "AffinityHook/bin/x64/$CONFIGURATION/net48/win-x64/AffinityHook.exe" "$COMBINED_TEMP/" +cp "AffinityPluginLoader/bin/x64/$CONFIGURATION/net48/win-x64/AffinityPluginLoader.dll" "$COMBINED_TEMP/" +cp "WineFix/bin/x64/$CONFIGURATION/net48/win-x64/WineFix.dll" "$COMBINED_TEMP/plugins/" # Create tar.xz tar -C "$COMBINED_TEMP" -cJf "$OUTPUT_DIR/affinitypluginloader-plus-winefix.tar.xz" .