From 2013e31b045f7a9959f43d133e5bd72612fc5442 Mon Sep 17 00:00:00 2001 From: jracek Date: Tue, 15 Apr 2025 13:17:30 +0200 Subject: [PATCH] Added automatic toolchain, gcc and make installation --- Editor/DependencyCheckInitializer.cs | 21 ++ Editor/DependencyCheckInitializer.cs.meta | 2 + Editor/DependencyInstallerWindow.cs | 212 +++++++++++++ Editor/DependencyInstallerWindow.cs.meta | 2 + Editor/QuantizedPreviewWindow.cs | 2 +- Editor/ToolchainChecker.cs | 65 ++++ Editor/ToolchainChecker.cs.meta | 2 + Editor/ToolchainInstaller.cs | 346 ++++++++++++++++++++++ Editor/ToolchainInstaller.cs.meta | 2 + Editor/VramEditorWindow.cs | 2 +- Runtime/PSXData.cs | 2 + 11 files changed, 656 insertions(+), 2 deletions(-) create mode 100644 Editor/DependencyCheckInitializer.cs create mode 100644 Editor/DependencyCheckInitializer.cs.meta create mode 100644 Editor/DependencyInstallerWindow.cs create mode 100644 Editor/DependencyInstallerWindow.cs.meta create mode 100644 Editor/ToolchainChecker.cs create mode 100644 Editor/ToolchainChecker.cs.meta create mode 100644 Editor/ToolchainInstaller.cs create mode 100644 Editor/ToolchainInstaller.cs.meta diff --git a/Editor/DependencyCheckInitializer.cs b/Editor/DependencyCheckInitializer.cs new file mode 100644 index 0000000..75b13fc --- /dev/null +++ b/Editor/DependencyCheckInitializer.cs @@ -0,0 +1,21 @@ +using UnityEditor; +using UnityEditor.Callbacks; + +[InitializeOnLoad] +public static class DependencyCheckInitializer +{ + static DependencyCheckInitializer() + { + EditorApplication.update += OpenInstallerOnStart; + } + + private static void OpenInstallerOnStart() + { + EditorApplication.update -= OpenInstallerOnStart; + if (!SessionState.GetBool("InstallerWindowOpened", false)) + { + InstallerWindow.ShowWindow(); + SessionState.SetBool("InstallerWindowOpened", true); // only once per session + } + } +} \ No newline at end of file diff --git a/Editor/DependencyCheckInitializer.cs.meta b/Editor/DependencyCheckInitializer.cs.meta new file mode 100644 index 0000000..bc4a4e9 --- /dev/null +++ b/Editor/DependencyCheckInitializer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 044a70d388c22fd2aa69bb757eb4c071 \ No newline at end of file diff --git a/Editor/DependencyInstallerWindow.cs b/Editor/DependencyInstallerWindow.cs new file mode 100644 index 0000000..8385b6f --- /dev/null +++ b/Editor/DependencyInstallerWindow.cs @@ -0,0 +1,212 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using SplashEdit.RuntimeCode; + +public class InstallerWindow : EditorWindow +{ + // Cached status for MIPS toolchain binaries. + private Dictionary mipsToolStatus = new Dictionary(); + + // Cached status for optional tools. + private bool makeInstalled; + private bool gdbInstalled; + private string pcsxReduxPath; + + private bool isInstalling = false; + + [MenuItem("PSX/Toolchain & Build Tools Installer")] + public static void ShowWindow() + { + InstallerWindow window = GetWindow("Toolchain Installer"); + window.RefreshToolStatus(); + window.pcsxReduxPath = DataStorage.LoadData().PCSXReduxPath; + } + + /// + /// Refresh the cached statuses for all tools. + /// + private void RefreshToolStatus() + { + mipsToolStatus.Clear(); + foreach (var tool in ToolchainChecker.requiredTools) + { + mipsToolStatus[tool] = ToolchainChecker.IsToolAvailable(tool); + } + + makeInstalled = ToolchainChecker.IsToolAvailable("make"); + gdbInstalled = ToolchainChecker.IsToolAvailable("gdb-multiarch"); + } + + private void OnGUI() + { + GUILayout.Label("Toolchain & Build Tools Installer", EditorStyles.boldLabel); + GUILayout.Space(5); + + if (GUILayout.Button("Refresh Status")) + { + RefreshToolStatus(); + } + GUILayout.Space(10); + + EditorGUILayout.BeginHorizontal(); + DrawToolchainColumn(); + DrawAdditionalToolsColumn(); + EditorGUILayout.EndHorizontal(); + } + + private void DrawToolchainColumn() + { + EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 2 - 10)); + GUILayout.Label("MIPS Toolchain", EditorStyles.boldLabel); + GUILayout.Space(5); + + // Display cached status for each required MIPS tool. + foreach (var kvp in mipsToolStatus) + { + GUI.color = kvp.Value ? Color.green : Color.red; + GUILayout.Label($"{kvp.Key}: {(kvp.Value ? "Found" : "Missing")}"); + } + GUI.color = Color.white; + GUILayout.Space(5); + + if (GUILayout.Button("Install MIPS Toolchain")) + { + if (!isInstalling) + InstallMipsToolchainAsync(); + } + EditorGUILayout.EndVertical(); + } + + private void DrawAdditionalToolsColumn() + { + EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 2 - 10)); + GUILayout.Label("Optional Tools", EditorStyles.boldLabel); + GUILayout.Space(5); + + // GNU Make status (required). + GUI.color = makeInstalled ? Color.green : Color.red; + GUILayout.Label($"GNU Make: {(makeInstalled ? "Found" : "Missing")} (Required)"); + GUI.color = Color.white; + GUILayout.Space(5); + if (GUILayout.Button("Install GNU Make")) + { + if (!isInstalling) + InstallMakeAsync(); + } + + GUILayout.Space(10); + + // GDB status (optional). + GUI.color = gdbInstalled ? Color.green : Color.red; + GUILayout.Label($"GDB: {(gdbInstalled ? "Found" : "Missing")} (Optional)"); + GUI.color = Color.white; + GUILayout.Space(5); + if (GUILayout.Button("Install GDB")) + { + if (!isInstalling) + InstallGDBAsync(); + } + + GUILayout.Space(10); + + // PCSX-Redux (manual install) + GUI.color = string.IsNullOrEmpty(pcsxReduxPath) ? Color.red : Color.green; + GUILayout.Label($"PCSX-Redux: {(string.IsNullOrEmpty(pcsxReduxPath) ? "Not Configured" : "Configured")} (Optional)"); + GUI.color = Color.white; + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Browse for PCSX-Redux")) + { + string selectedPath = EditorUtility.OpenFilePanel("Select PCSX-Redux Executable", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + pcsxReduxPath = selectedPath; + PSXData data = DataStorage.LoadData(); + data.PCSXReduxPath = pcsxReduxPath; + DataStorage.StoreData(data); + } + } + if (!string.IsNullOrEmpty(pcsxReduxPath)) + { + if (GUILayout.Button("Clear", GUILayout.Width(60))) + { + pcsxReduxPath = ""; + PSXData data = DataStorage.LoadData(); + data.PCSXReduxPath = pcsxReduxPath; + DataStorage.StoreData(data); + } + } + GUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private async void InstallMipsToolchainAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing MIPS Toolchain", + "Please wait while the MIPS toolchain is being installed...", 0f); + await ToolchainInstaller.InstallToolchain(); + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Complete", "MIPS toolchain installed successfully.", "OK"); + RefreshToolStatus(); // Update cached statuses after installation + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } + } + + private async void InstallMakeAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing GNU Make", + "Please wait while GNU Make is being installed...", 0f); + await ToolchainInstaller.InstallMake(); + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Complete", "GNU Make installed successfully.", "OK"); + RefreshToolStatus(); + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } + } + + private async void InstallGDBAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing GDB", + "Please wait while GDB is being installed...", 0f); + await ToolchainInstaller.InstallGDB(); + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Complete", "GDB installed successfully.", "OK"); + RefreshToolStatus(); + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } + } +} diff --git a/Editor/DependencyInstallerWindow.cs.meta b/Editor/DependencyInstallerWindow.cs.meta new file mode 100644 index 0000000..a32883e --- /dev/null +++ b/Editor/DependencyInstallerWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 72dec7ea237a8497abc6150ea907b3e2 \ No newline at end of file diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs index e5fb939..ca98f9d 100644 --- a/Editor/QuantizedPreviewWindow.cs +++ b/Editor/QuantizedPreviewWindow.cs @@ -17,7 +17,7 @@ namespace SplashEdit.EditorCode private PSXBPP bpp = PSXBPP.TEX_4BIT; private readonly int previewSize = 256; - [MenuItem("Window/Quantized Preview")] + [MenuItem("PSX/Quantized Preview")] public static void ShowWindow() { // Creates and displays the window diff --git a/Editor/ToolchainChecker.cs b/Editor/ToolchainChecker.cs new file mode 100644 index 0000000..9190c8f --- /dev/null +++ b/Editor/ToolchainChecker.cs @@ -0,0 +1,65 @@ +using UnityEngine; +using System.Diagnostics; +using System.Linq; + +public static class ToolchainChecker +{ + public static readonly string[] requiredTools = new[] + { + "mipsel-linux-gnu-addr2line", + "mipsel-linux-gnu-ar", + "mipsel-linux-gnu-as", + "mipsel-linux-gnu-cpp", + "mipsel-linux-gnu-elfedit", + "mipsel-linux-gnu-g++", + "mipsel-linux-gnu-gcc", + "mipsel-linux-gnu-gcc-ar", + "mipsel-linux-gnu-gcc-nm", + "mipsel-linux-gnu-gcc-ranlib", + "mipsel-linux-gnu-gcov", + "mipsel-linux-gnu-ld", + "mipsel-linux-gnu-nm", + "mipsel-linux-gnu-objcopy", + "mipsel-linux-gnu-objdump", + "mipsel-linux-gnu-ranlib", + "mipsel-linux-gnu-readelf", + "mipsel-linux-gnu-size", + "mipsel-linux-gnu-strings", + "mipsel-linux-gnu-strip" + }; + + /// + /// Checks for the availability of a given tool by using a system command. + /// "where" is used on Windows and "which" on other platforms. + /// + public static bool IsToolAvailable(string toolName) + { + string command = Application.platform == RuntimePlatform.WindowsEditor ? "where" : "which"; + + try + { + Process process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = toolName, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + return !string.IsNullOrEmpty(output); + } + catch + { + return false; + } + } +} diff --git a/Editor/ToolchainChecker.cs.meta b/Editor/ToolchainChecker.cs.meta new file mode 100644 index 0000000..3a276af --- /dev/null +++ b/Editor/ToolchainChecker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 326c6443947d4e5e783e90882b641ce8 \ No newline at end of file diff --git a/Editor/ToolchainInstaller.cs b/Editor/ToolchainInstaller.cs new file mode 100644 index 0000000..557121d --- /dev/null +++ b/Editor/ToolchainInstaller.cs @@ -0,0 +1,346 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using UnityEngine; +using UnityEditor; + +public static class ToolchainInstaller +{ + // Flags to prevent duplicate installations. + private static bool mipsInstalling = false; + private static bool win32MipsToolsInstalling = false; + + // The version string used by the installer command. + public static string mipsVersion = "14.2.0"; + + /// + /// Executes an external process asynchronously. + /// Throws an exception if the process returns a nonzero exit code. + /// + public static async Task RunCommandAsync(string fileName, string arguments, string workingDirectory = "") + { + var tcs = new TaskCompletionSource(); + + Process process = new Process(); + process.StartInfo.FileName = fileName; + process.StartInfo.Arguments = arguments; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.UseShellExecute = false; + if (!string.IsNullOrEmpty(workingDirectory)) + process.StartInfo.WorkingDirectory = workingDirectory; + + process.EnableRaisingEvents = true; + process.Exited += (sender, args) => + { + tcs.SetResult(process.ExitCode); + process.Dispose(); + }; + + try + { + process.Start(); + } + catch (Exception ex) + { + throw new Exception($"Failed to start process {fileName}: {ex.Message}"); + } + + int exitCode = await tcs.Task; + if (exitCode != 0) + throw new Exception($"Process '{fileName} {arguments}' exited with code {exitCode}"); + } + + #region MIPS Toolchain Installation + + /// + /// Installs the MIPS toolchain on Windows using a PowerShell script. + /// (On Windows this installer bundles GNU Make as part of the toolchain.) + /// + public static async Task InstallMips() + { + if (mipsInstalling) return; + mipsInstalling = true; + try + { + // Download and run the installer script via PowerShell. + await RunCommandAsync("powershell", + "-c \"& { iwr -UseBasicParsing https://raw.githubusercontent.com/grumpycoders/pcsx-redux/main/mips.ps1 | iex }\""); + EditorUtility.DisplayDialog("Reboot Required", + "Installing the MIPS toolchain requires a reboot. Please reboot your computer before proceeding further.", + "OK"); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing the MIPS toolchain. Please install it manually.", "OK"); + throw ex; + } + } + + /// + /// Installs the MIPS toolchain based on the current platform. + /// Uses pkexec on Linux to request graphical elevation. + /// + public static async Task InstallToolchain() + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + try + { + if (!ToolchainChecker.IsToolAvailable("mips")) + { + await InstallMips(); + } + else + { + if (win32MipsToolsInstalling) return; + win32MipsToolsInstalling = true; + await RunCommandAsync("mips", $"install {mipsVersion}"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing the MIPS toolchain. Please install it manually.", "OK"); + throw ex; + } + break; + + case RuntimePlatform.LinuxEditor: + try + { + if (ToolchainChecker.IsToolAvailable("apt")) + { + await RunCommandAsync("pkexec", "apt install g++-mipsel-linux-gnu -y"); + } + else if (ToolchainChecker.IsToolAvailable("trizen")) + { + await RunCommandAsync("trizen", "-S cross-mipsel-linux-gnu-binutils cross-mipsel-linux-gnu-gcc"); + } + else if (ToolchainChecker.IsToolAvailable("brew")) + { + string binutilsScriptPath = Application.dataPath + "/Scripts/mipsel-none-elf-binutils.rb"; + string gccScriptPath = Application.dataPath + "/Scripts/mipsel-none-elf-gcc.rb"; + await RunCommandAsync("brew", $"install --formula \"{binutilsScriptPath}\" \"{gccScriptPath}\""); + } + else + { + EditorUtility.DisplayDialog("Error", + "Your Linux distribution is not supported. Please install the MIPS toolchain manually.", "OK"); + throw new Exception("Unsupported Linux distribution"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing the MIPS toolchain. Please install it manually.", "OK"); + throw ex; + } + break; + + case RuntimePlatform.OSXEditor: + try + { + if (ToolchainChecker.IsToolAvailable("brew")) + { + string binutilsScriptPath = Application.dataPath + "/Scripts/mipsel-none-elf-binutils.rb"; + string gccScriptPath = Application.dataPath + "/Scripts/mipsel-none-elf-gcc.rb"; + await RunCommandAsync("brew", $"install --formula \"{binutilsScriptPath}\" \"{gccScriptPath}\""); + } + else + { + await RunCommandAsync("/bin/bash", "-c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""); + EditorUtility.DisplayDialog("Reboot Required", + "Installing Homebrew requires a reboot. Please reboot your computer before proceeding further.", "OK"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing the MIPS toolchain. Please install it manually.", "OK"); + throw ex; + } + break; + + default: + EditorUtility.DisplayDialog("Error", + "Your platform is not supported by this extension. Please install the MIPS toolchain manually.", "OK"); + throw new Exception("Unsupported platform"); + } + } + + #endregion + + #region GNU Make Installation + + /// + /// Installs GNU Make. + /// On Linux/macOS it installs GNU Make normally. + /// On Windows, GNU Make is bundled with the MIPS toolchain—so the user is warned before proceeding. + /// + public static async Task InstallMake() + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + // Inform the user that GNU Make is bundled with the MIPS toolchain. + bool proceed = EditorUtility.DisplayDialog( + "Install GNU Make", + "On Windows, GNU Make is installed as part of the MIPS toolchain installer. Would you like to install the full toolchain?", + "Yes", + "No" + ); + if (proceed) + { + await InstallToolchain(); + } + break; + + case RuntimePlatform.LinuxEditor: + try + { + if (ToolchainChecker.IsToolAvailable("apt")) + { + await RunCommandAsync("pkexec", "apt install build-essential -y"); + } + else + { + EditorUtility.DisplayDialog("Error", + "Your Linux distribution is not supported. Please install GNU Make manually.", "OK"); + throw new Exception("Unsupported Linux distribution"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing GNU Make. Please install it manually.", "OK"); + throw ex; + } + break; + + case RuntimePlatform.OSXEditor: + try + { + if (ToolchainChecker.IsToolAvailable("brew")) + { + await RunCommandAsync("brew", "install make"); + } + else + { + EditorUtility.DisplayDialog("Error", + "Homebrew is not installed. Please install GNU Make manually.", "OK"); + throw new Exception("Brew not installed"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing GNU Make. Please install it manually.", "OK"); + throw ex; + } + break; + + default: + EditorUtility.DisplayDialog("Error", + "Your platform is not supported. Please install GNU Make manually.", "OK"); + throw new Exception("Unsupported platform"); + } + } + + #endregion + + #region GDB Installation (Optional) + + /// + /// Installs GDB Multiarch (or GDB on macOS) + /// + public static async Task InstallGDB() + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + try + { + if (!ToolchainChecker.IsToolAvailable("mips")) + { + await InstallMips(); + } + else + { + if (win32MipsToolsInstalling) return; + win32MipsToolsInstalling = true; + await RunCommandAsync("mips", $"install {mipsVersion}"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing GDB Multiarch. Please install it manually.", "OK"); + throw ex; + } + break; + + case RuntimePlatform.LinuxEditor: + try + { + if (ToolchainChecker.IsToolAvailable("apt")) + { + await RunCommandAsync("pkexec", "apt install gdb-multiarch -y"); + } + else if (ToolchainChecker.IsToolAvailable("trizen")) + { + await RunCommandAsync("trizen", "-S gdb-multiarch"); + } + else if (ToolchainChecker.IsToolAvailable("brew")) + { + await RunCommandAsync("brew", "install gdb-multiarch"); + } + else + { + EditorUtility.DisplayDialog("Error", + "Your Linux distribution is not supported. Please install GDB Multiarch manually.", "OK"); + throw new Exception("Unsupported Linux distribution"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing GDB Multiarch. Please install it manually.", "OK"); + throw ex; + } + break; + + case RuntimePlatform.OSXEditor: + try + { + if (ToolchainChecker.IsToolAvailable("brew")) + { + await RunCommandAsync("brew", "install gdb"); + } + else + { + await RunCommandAsync("/bin/bash", "-c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""); + EditorUtility.DisplayDialog("Reboot Required", + "Installing Homebrew requires a reboot. Please reboot your computer before proceeding further.", "OK"); + } + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Error", + "An error occurred while installing GDB Multiarch. Please install it manually.", "OK"); + throw ex; + } + break; + + default: + EditorUtility.DisplayDialog("Error", + "Your platform is not supported. Please install GDB Multiarch manually.", "OK"); + throw new Exception("Unsupported platform"); + } + } + + #endregion + + +} diff --git a/Editor/ToolchainInstaller.cs.meta b/Editor/ToolchainInstaller.cs.meta new file mode 100644 index 0000000..f931e6a --- /dev/null +++ b/Editor/ToolchainInstaller.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 93df68ab9518c5bf6a962c185fe742fe \ No newline at end of file diff --git a/Editor/VramEditorWindow.cs b/Editor/VramEditorWindow.cs index 7581825..20e68cb 100644 --- a/Editor/VramEditorWindow.cs +++ b/Editor/VramEditorWindow.cs @@ -37,7 +37,7 @@ namespace SplashEdit.EditorCode }; private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray(); - [MenuItem("Window/VRAM Editor")] + [MenuItem("PSX/VRAM Editor")] public static void ShowWindow() { VRAMEditorWindow window = GetWindow("VRAM Editor"); diff --git a/Runtime/PSXData.cs b/Runtime/PSXData.cs index 0cc83ab..c8075a0 100644 --- a/Runtime/PSXData.cs +++ b/Runtime/PSXData.cs @@ -10,5 +10,7 @@ namespace SplashEdit.RuntimeCode public bool DualBuffering = true; public bool VerticalBuffering = true; public List ProhibitedAreas = new List(); + + public string PCSXReduxPath = ""; } } \ No newline at end of file