Revamped psxsplash installer
This commit is contained in:
@@ -60,6 +60,13 @@ namespace SplashEdit.EditorCode
|
||||
private string _nativeInstallStatus = "";
|
||||
private string _manualNativePath = "";
|
||||
|
||||
// ───── Release selector ─────
|
||||
private int _selectedReleaseIndex = 0;
|
||||
private string[] _releaseDisplayNames = new string[0];
|
||||
private bool _isFetchingReleases;
|
||||
private string _currentTag = "";
|
||||
private bool _isSwitchingRelease;
|
||||
|
||||
// PCdrv serial host instance (for real hardware file serving)
|
||||
private static PCdrvSerialHost _pcdrvHost;
|
||||
|
||||
@@ -89,6 +96,8 @@ namespace SplashEdit.EditorCode
|
||||
RefreshToolchainStatus();
|
||||
LoadSceneList();
|
||||
_manualNativePath = SplashSettings.NativeProjectPath;
|
||||
FetchGitHubReleases();
|
||||
RefreshCurrentTag();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
@@ -215,44 +224,106 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("Not found — clone from GitHub or set path manually", EditorStyles.miniLabel);
|
||||
GUILayout.Label("Not found — download from GitHub or set path manually", EditorStyles.miniLabel);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// ── Option 1: Auto-clone from GitHub ──
|
||||
// ── Option 1: Download a release from GitHub ──
|
||||
PSXEditorStyles.DrawSeparator(4, 4);
|
||||
GUILayout.Label("Clone from GitHub", PSXEditorStyles.SectionHeader);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label(PSXSplashInstaller.RepoUrl, EditorStyles.miniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUI.BeginDisabledGroup(_isInstallingNative || PSXSplashInstaller.IsInstalled());
|
||||
if (GUILayout.Button(PSXSplashInstaller.IsInstalled() ? "Installed" : "Clone", GUILayout.Width(80)))
|
||||
GUILayout.Label("Download from GitHub", PSXEditorStyles.SectionHeader);
|
||||
|
||||
// Git availability check
|
||||
if (!PSXSplashInstaller.IsGitAvailable())
|
||||
{
|
||||
CloneNativeProject();
|
||||
EditorGUILayout.HelpBox(
|
||||
"git is required to download the native project (submodules need recursive clone).\n" +
|
||||
"Install git from: https://git-scm.com/downloads",
|
||||
MessageType.Warning);
|
||||
}
|
||||
|
||||
// Release selector
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Release:", GUILayout.Width(55));
|
||||
if (_isFetchingReleases)
|
||||
{
|
||||
GUILayout.Label("Fetching releases...", EditorStyles.miniLabel);
|
||||
}
|
||||
else if (_releaseDisplayNames.Length == 0)
|
||||
{
|
||||
GUILayout.Label("No releases found", EditorStyles.miniLabel);
|
||||
if (GUILayout.Button("Refresh", EditorStyles.miniButton, GUILayout.Width(60)))
|
||||
FetchGitHubReleases();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedReleaseIndex = EditorGUILayout.Popup(_selectedReleaseIndex, _releaseDisplayNames);
|
||||
if (GUILayout.Button("↻", EditorStyles.miniButton, GUILayout.Width(22)))
|
||||
FetchGitHubReleases();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (_isInstallingNative)
|
||||
// Current version display (when installed)
|
||||
if (PSXSplashInstaller.IsInstalled() && !string.IsNullOrEmpty(_currentTag))
|
||||
{
|
||||
GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox);
|
||||
var prevColor = GUI.contentColor;
|
||||
GUI.contentColor = PSXEditorStyles.Success;
|
||||
GUILayout.Label($"Current version: {_currentTag}", EditorStyles.miniLabel);
|
||||
GUI.contentColor = prevColor;
|
||||
}
|
||||
|
||||
// If already cloned, show version management
|
||||
if (PSXSplashInstaller.IsInstalled())
|
||||
// Clone / Switch / Open buttons
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (!PSXSplashInstaller.IsInstalled())
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Fetch Latest", EditorStyles.miniButton, GUILayout.Width(90)))
|
||||
// Not installed yet — show Clone button
|
||||
EditorGUI.BeginDisabledGroup(
|
||||
_isInstallingNative || _releaseDisplayNames.Length == 0 ||
|
||||
!PSXSplashInstaller.IsGitAvailable());
|
||||
if (GUILayout.Button("Download Release", GUILayout.Width(130)))
|
||||
{
|
||||
FetchNativeLatest();
|
||||
CloneNativeProject();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Already installed — show Switch and Open buttons
|
||||
EditorGUI.BeginDisabledGroup(
|
||||
_isSwitchingRelease || _isInstallingNative ||
|
||||
_releaseDisplayNames.Length == 0 || !PSXSplashInstaller.IsGitAvailable());
|
||||
if (GUILayout.Button("Switch Release", EditorStyles.miniButton, GUILayout.Width(100)))
|
||||
{
|
||||
SwitchNativeRelease();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (GUILayout.Button("Open Folder", EditorStyles.miniButton, GUILayout.Width(90)))
|
||||
{
|
||||
EditorUtility.RevealInFinder(PSXSplashInstaller.FullInstallPath);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Progress / status message
|
||||
if (_isInstallingNative || _isSwitchingRelease)
|
||||
{
|
||||
GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox);
|
||||
}
|
||||
|
||||
// Show release notes for selected release
|
||||
if (_releaseDisplayNames.Length > 0 && _selectedReleaseIndex < PSXSplashInstaller.CachedReleases.Count)
|
||||
{
|
||||
var selected = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex];
|
||||
if (!string.IsNullOrEmpty(selected.Body))
|
||||
{
|
||||
EditorGUILayout.Space(2);
|
||||
string trimmedNotes = selected.Body.Length > 200
|
||||
? selected.Body.Substring(0, 200) + "..."
|
||||
: selected.Body;
|
||||
GUILayout.Label(trimmedNotes, EditorStyles.wordWrappedMiniLabel);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
@@ -1840,34 +1911,99 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
}
|
||||
|
||||
// ───── Native Project Clone/Fetch ─────
|
||||
// ───── Release fetching & management ─────
|
||||
|
||||
private async void CloneNativeProject()
|
||||
private async void FetchGitHubReleases()
|
||||
{
|
||||
_isInstallingNative = true;
|
||||
_nativeInstallStatus = "Cloning psxsplash repository (this may take a minute)...";
|
||||
_isFetchingReleases = true;
|
||||
Repaint();
|
||||
|
||||
Log("Cloning psxsplash native project from GitHub...", LogType.Log);
|
||||
|
||||
try
|
||||
{
|
||||
bool success = await PSXSplashInstaller.Install();
|
||||
if (success)
|
||||
var releases = await PSXSplashInstaller.FetchReleasesAsync();
|
||||
if (releases.Count > 0)
|
||||
{
|
||||
Log("psxsplash cloned successfully!", LogType.Log);
|
||||
_nativeInstallStatus = "";
|
||||
RefreshToolchainStatus();
|
||||
_releaseDisplayNames = releases
|
||||
.Select(r =>
|
||||
{
|
||||
string label = r.TagName;
|
||||
if (!string.IsNullOrEmpty(r.Name) && r.Name != r.TagName)
|
||||
label += $" — {r.Name}";
|
||||
if (r.IsPrerelease)
|
||||
label += " (pre-release)";
|
||||
return label;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
// Try to select the currently checked-out tag
|
||||
if (!string.IsNullOrEmpty(_currentTag))
|
||||
{
|
||||
int idx = releases.FindIndex(r => r.TagName == _currentTag);
|
||||
if (idx >= 0) _selectedReleaseIndex = idx;
|
||||
}
|
||||
|
||||
_selectedReleaseIndex = Mathf.Clamp(_selectedReleaseIndex, 0, _releaseDisplayNames.Length - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Clone failed. Check console for errors.", LogType.Error);
|
||||
_nativeInstallStatus = "Clone failed — check console for details.";
|
||||
_releaseDisplayNames = new string[0];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Clone error: {ex.Message}", LogType.Error);
|
||||
Log($"Failed to fetch releases: {ex.Message}", LogType.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isFetchingReleases = false;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCurrentTag()
|
||||
{
|
||||
_currentTag = PSXSplashInstaller.GetCurrentTag() ?? "";
|
||||
}
|
||||
|
||||
// ───── Native Project Clone/Switch ─────
|
||||
|
||||
private async void CloneNativeProject()
|
||||
{
|
||||
if (_selectedReleaseIndex < 0 || _selectedReleaseIndex >= PSXSplashInstaller.CachedReleases.Count)
|
||||
return;
|
||||
|
||||
string tag = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex].TagName;
|
||||
|
||||
_isInstallingNative = true;
|
||||
_nativeInstallStatus = $"Downloading psxsplash {tag} (this may take a minute)...";
|
||||
Repaint();
|
||||
|
||||
Log($"Downloading psxsplash {tag} from GitHub...", LogType.Log);
|
||||
|
||||
try
|
||||
{
|
||||
bool success = await PSXSplashInstaller.InstallRelease(tag, msg =>
|
||||
{
|
||||
_nativeInstallStatus = msg;
|
||||
Repaint();
|
||||
});
|
||||
|
||||
if (success)
|
||||
{
|
||||
Log($"psxsplash {tag} downloaded successfully!", LogType.Log);
|
||||
_nativeInstallStatus = "";
|
||||
RefreshToolchainStatus();
|
||||
RefreshCurrentTag();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Download failed. Check console for errors.", LogType.Error);
|
||||
_nativeInstallStatus = "Download failed — check console for details.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Download error: {ex.Message}", LogType.Error);
|
||||
_nativeInstallStatus = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
@@ -1877,22 +2013,55 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
}
|
||||
|
||||
private async void FetchNativeLatest()
|
||||
private async void SwitchNativeRelease()
|
||||
{
|
||||
Log("Fetching latest changes...", LogType.Log);
|
||||
if (_selectedReleaseIndex < 0 || _selectedReleaseIndex >= PSXSplashInstaller.CachedReleases.Count)
|
||||
return;
|
||||
|
||||
string tag = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex].TagName;
|
||||
|
||||
if (tag == _currentTag)
|
||||
{
|
||||
Log($"Already on {tag}.", LogType.Log);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSwitchingRelease = true;
|
||||
_nativeInstallStatus = $"Switching to {tag}...";
|
||||
Repaint();
|
||||
|
||||
Log($"Switching native project to {tag}...", LogType.Log);
|
||||
|
||||
try
|
||||
{
|
||||
bool success = await PSXSplashInstaller.FetchLatestAsync();
|
||||
bool success = await PSXSplashInstaller.SwitchToReleaseAsync(tag, msg =>
|
||||
{
|
||||
_nativeInstallStatus = msg;
|
||||
Repaint();
|
||||
});
|
||||
|
||||
if (success)
|
||||
Log("Fetch complete. Use 'git pull' to apply updates.", LogType.Log);
|
||||
{
|
||||
Log($"Switched to {tag}.", LogType.Log);
|
||||
_nativeInstallStatus = "";
|
||||
RefreshCurrentTag();
|
||||
}
|
||||
else
|
||||
Log("Fetch failed.", LogType.Warning);
|
||||
{
|
||||
Log($"Failed to switch to {tag}.", LogType.Error);
|
||||
_nativeInstallStatus = "Switch failed — check console for details.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Fetch error: {ex.Message}", LogType.Error);
|
||||
Log($"Switch error: {ex.Message}", LogType.Error);
|
||||
_nativeInstallStatus = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSwitchingRelease = false;
|
||||
Repaint();
|
||||
}
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1,149 +1,307 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Manages downloading and updating the psxsplash native project from GitHub releases.
|
||||
/// Uses the GitHub REST API (HTTP) to list releases and git to clone/checkout
|
||||
/// (required for recursive submodule support).
|
||||
/// </summary>
|
||||
public static class PSXSplashInstaller
|
||||
{
|
||||
// ───── Public config ─────
|
||||
public static readonly string RepoOwner = "psxsplash";
|
||||
public static readonly string RepoName = "psxsplash";
|
||||
public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git";
|
||||
public static readonly string InstallPath = "Assets/psxsplash";
|
||||
public static readonly string FullInstallPath;
|
||||
|
||||
private static readonly string GitHubApiReleasesUrl =
|
||||
$"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases";
|
||||
|
||||
// ───── Cached release list ─────
|
||||
private static List<ReleaseInfo> _cachedReleases = new List<ReleaseInfo>();
|
||||
private static bool _isFetchingReleases;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GitHub release.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ReleaseInfo
|
||||
{
|
||||
public string TagName; // e.g. "v1.2.0"
|
||||
public string Name; // human-readable name
|
||||
public string Body; // release notes (markdown)
|
||||
public string PublishedAt; // ISO 8601 date
|
||||
public bool IsPrerelease;
|
||||
public bool IsDraft;
|
||||
}
|
||||
|
||||
static PSXSplashInstaller()
|
||||
{
|
||||
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Queries
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>Is the native project cloned on disk?</summary>
|
||||
public static bool IsInstalled()
|
||||
{
|
||||
return Directory.Exists(FullInstallPath) &&
|
||||
Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
|
||||
}
|
||||
|
||||
public static async Task<bool> Install()
|
||||
{
|
||||
if (IsInstalled()) return true;
|
||||
/// <summary>Are we currently fetching releases from GitHub?</summary>
|
||||
public static bool IsFetchingReleases => _isFetchingReleases;
|
||||
|
||||
/// <summary>Cached list of releases (call FetchReleasesAsync to populate).</summary>
|
||||
public static IReadOnlyList<ReleaseInfo> CachedReleases => _cachedReleases;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the tag currently checked out, or null if unknown / not a git repo.
|
||||
/// </summary>
|
||||
public static string GetCurrentTag()
|
||||
{
|
||||
if (!IsInstalled()) return null;
|
||||
try
|
||||
{
|
||||
// Create the parent directory if it doesn't exist
|
||||
Directory.CreateDirectory(Application.dataPath);
|
||||
|
||||
// Clone the repository
|
||||
var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath);
|
||||
return !result.Contains("error");
|
||||
string result = RunGitCommandSync("describe --tags --exact-match HEAD", FullInstallPath);
|
||||
return string.IsNullOrWhiteSpace(result) ? null : result.Trim();
|
||||
}
|
||||
catch (Exception e)
|
||||
catch
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}");
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<string, string>> GetBranchesWithLatestCommitsAsync()
|
||||
{
|
||||
if (!IsInstalled()) return new Dictionary<string, string>();
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Fetch Releases (HTTP — no git required)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the list of releases from the GitHub REST API.
|
||||
/// Does NOT require git — uses UnityWebRequest.
|
||||
/// </summary>
|
||||
public static async Task<List<ReleaseInfo>> FetchReleasesAsync()
|
||||
{
|
||||
_isFetchingReleases = true;
|
||||
try
|
||||
{
|
||||
// Fetch all branches and tags
|
||||
await RunGitCommandAsync("fetch --all", FullInstallPath);
|
||||
|
||||
// Get all remote branches
|
||||
var branchesOutput = await RunGitCommandAsync("branch -r", FullInstallPath);
|
||||
var branches = branchesOutput.Split('\n')
|
||||
.Where(b => !string.IsNullOrEmpty(b.Trim()))
|
||||
.Select(b => b.Trim().Replace("origin/", ""))
|
||||
.Where(b => !b.Contains("HEAD"))
|
||||
.ToList();
|
||||
|
||||
var branchesWithCommits = new Dictionary<string, string>();
|
||||
|
||||
// Get the latest commit for each branch
|
||||
foreach (var branch in branches)
|
||||
string json = await HttpGetAsync(GitHubApiReleasesUrl);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath);
|
||||
if (!string.IsNullOrEmpty(commitOutput))
|
||||
{
|
||||
branchesWithCommits[branch] = commitOutput.Trim();
|
||||
}
|
||||
UnityEngine.Debug.LogWarning("[PSXSplashInstaller] Failed to fetch releases from GitHub.");
|
||||
return _cachedReleases;
|
||||
}
|
||||
|
||||
return branchesWithCommits;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}");
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<string>> GetReleasesAsync()
|
||||
{
|
||||
if (!IsInstalled()) return new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
await RunGitCommandAsync("fetch --tags", FullInstallPath);
|
||||
var output = await RunGitCommandAsync("tag -l", FullInstallPath);
|
||||
|
||||
return output.Split('\n')
|
||||
.Where(t => !string.IsNullOrEmpty(t.Trim()))
|
||||
.Select(t => t.Trim())
|
||||
var releases = ParseReleasesJson(json);
|
||||
// Filter out drafts, sort by newest first
|
||||
releases = releases
|
||||
.Where(r => !r.IsDraft)
|
||||
.OrderByDescending(r => r.PublishedAt)
|
||||
.ToList();
|
||||
|
||||
_cachedReleases = releases;
|
||||
return releases;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}");
|
||||
return new List<string>();
|
||||
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Error fetching releases: {ex.Message}");
|
||||
return _cachedReleases;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isFetchingReleases = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> CheckoutVersionAsync(string version)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Install / Clone at a specific release tag
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Clones the repository at the specified release tag with --recursive.
|
||||
/// Uses a shallow clone (--depth 1) for speed.
|
||||
/// Requires git to be installed (submodules cannot be fetched via HTTP archives).
|
||||
/// </summary>
|
||||
/// <param name="tag">The release tag to clone, e.g. "v1.2.0". If null, clones the default branch.</param>
|
||||
/// <param name="onProgress">Optional progress callback.</param>
|
||||
public static async Task<bool> InstallRelease(string tag, Action<string> onProgress = null)
|
||||
{
|
||||
if (!IsInstalled()) return false;
|
||||
if (IsInstalled())
|
||||
{
|
||||
onProgress?.Invoke("Already installed. Use SwitchToRelease to change version.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsGitAvailable())
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[PSXSplashInstaller] git is required for recursive submodule clone but was not found on PATH.\n" +
|
||||
"Please install git: https://git-scm.com/downloads");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If it's a branch name, checkout the branch
|
||||
// If it's a commit hash, checkout the commit
|
||||
var result = await RunGitCommandAsync($"checkout {version}", FullInstallPath);
|
||||
var result2 = await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(FullInstallPath));
|
||||
|
||||
return !result.Contains("error") && !result2.Contains("error");
|
||||
string branchArg = string.IsNullOrEmpty(tag) ? "" : $"--branch {tag}";
|
||||
string cmd = $"clone --recursive --depth 1 {branchArg} {RepoUrl} \"{FullInstallPath}\"";
|
||||
|
||||
onProgress?.Invoke($"Cloning {RepoUrl} at {tag ?? "HEAD"}...");
|
||||
string result = await RunGitCommandAsync(cmd, Application.dataPath, onProgress);
|
||||
|
||||
if (!IsInstalled())
|
||||
{
|
||||
UnityEngine.Debug.LogError("[PSXSplashInstaller] Clone completed but directory is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
onProgress?.Invoke("Clone complete.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Failed to checkout version: {e.Message}");
|
||||
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Clone failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches an existing clone to a different release tag.
|
||||
/// Fetches tags, checks out the tag, and updates submodules recursively.
|
||||
/// </summary>
|
||||
public static async Task<bool> SwitchToReleaseAsync(string tag, Action<string> onProgress = null)
|
||||
{
|
||||
if (!IsInstalled())
|
||||
{
|
||||
UnityEngine.Debug.LogError("[PSXSplashInstaller] Not installed — clone first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsGitAvailable())
|
||||
{
|
||||
UnityEngine.Debug.LogError("[PSXSplashInstaller] git not found on PATH.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
onProgress?.Invoke("Fetching tags...");
|
||||
await RunGitCommandAsync("fetch --tags --depth=1", FullInstallPath, onProgress);
|
||||
await RunGitCommandAsync($"fetch origin tag {tag} --no-tags", FullInstallPath, onProgress);
|
||||
|
||||
onProgress?.Invoke($"Checking out {tag}...");
|
||||
await RunGitCommandAsync($"checkout {tag}", FullInstallPath, onProgress);
|
||||
|
||||
onProgress?.Invoke("Updating submodules...");
|
||||
await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath, onProgress);
|
||||
|
||||
onProgress?.Invoke($"Switched to {tag}.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Switch failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy compatibility: Install without specifying a tag (clones default branch).
|
||||
/// </summary>
|
||||
public static Task<bool> Install()
|
||||
{
|
||||
return InstallRelease(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches latest remote data (tags, branches).
|
||||
/// Requires git.
|
||||
/// </summary>
|
||||
public static async Task<bool> FetchLatestAsync()
|
||||
{
|
||||
if (!IsInstalled()) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await RunGitCommandAsync("fetch --all", FullInstallPath);
|
||||
return !result.Contains("error");
|
||||
await RunGitCommandAsync("fetch --all --tags", FullInstallPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Failed to fetch latest: {e.Message}");
|
||||
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Fetch failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> RunGitCommandAsync(string arguments, string workingDirectory)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Git helpers
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether git is available on the system PATH.
|
||||
/// </summary>
|
||||
public static bool IsGitAvailable()
|
||||
{
|
||||
var processInfo = new ProcessStartInfo
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using (var p = Process.Start(psi))
|
||||
{
|
||||
p.WaitForExit(5000);
|
||||
return p.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string RunGitCommandSync(string arguments, string workingDirectory)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(10000);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> RunGitCommandAsync(
|
||||
string arguments, string workingDirectory, Action<string> onProgress = null)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = arguments,
|
||||
@@ -156,47 +314,136 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo = processInfo;
|
||||
var outputBuilder = new System.Text.StringBuilder();
|
||||
var errorBuilder = new System.Text.StringBuilder();
|
||||
process.StartInfo = psi;
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
var stdout = new System.Text.StringBuilder();
|
||||
var stderr = new System.Text.StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
{
|
||||
stdout.AppendLine(e.Data);
|
||||
onProgress?.Invoke(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
errorBuilder.AppendLine(e.Data);
|
||||
{
|
||||
stderr.AppendLine(e.Data);
|
||||
// git writes progress to stderr (clone progress, etc.)
|
||||
onProgress?.Invoke(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
var tcs = new TaskCompletionSource<int>();
|
||||
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
var timeout = TimeSpan.FromMinutes(10);
|
||||
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
|
||||
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(10));
|
||||
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
process.WaitForExit(); // Ensure all output is processed
|
||||
|
||||
string output = outputBuilder.ToString();
|
||||
string error = errorBuilder.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Git error: {error}");
|
||||
}
|
||||
|
||||
return output;
|
||||
try { process.Kill(); } catch { }
|
||||
throw new TimeoutException("Git command timed out after 10 minutes.");
|
||||
}
|
||||
|
||||
int exitCode = await tcs.Task;
|
||||
process.Dispose();
|
||||
|
||||
string output = stdout.ToString();
|
||||
string error = stderr.ToString();
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"[git {arguments}] exit code {exitCode}\n{error}");
|
||||
}
|
||||
|
||||
return output + error;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// HTTP helpers (no git needed)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
private static Task<string> HttpGetAsync(string url)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
var request = UnityWebRequest.Get(url);
|
||||
request.SetRequestHeader("User-Agent", "SplashEdit-Unity");
|
||||
request.SetRequestHeader("Accept", "application/vnd.github.v3+json");
|
||||
|
||||
var op = request.SendWebRequest();
|
||||
op.completed += _ =>
|
||||
{
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
tcs.TrySetResult(request.downloadHandler.text);
|
||||
else
|
||||
{
|
||||
process.Kill();
|
||||
throw new TimeoutException("Git command timed out");
|
||||
UnityEngine.Debug.LogWarning($"[PSXSplashInstaller] HTTP GET {url} failed: {request.error}");
|
||||
tcs.TrySetResult(null);
|
||||
}
|
||||
request.Dispose();
|
||||
};
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// JSON parsing (minimal, avoids external dependency)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Minimal JSON parser for the GitHub releases API response.
|
||||
/// Uses Unity's JsonUtility via a wrapper since it can't parse top-level arrays.
|
||||
/// </summary>
|
||||
private static List<ReleaseInfo> ParseReleasesJson(string json)
|
||||
{
|
||||
var releases = new List<ReleaseInfo>();
|
||||
|
||||
string wrapped = "{\"items\":" + json + "}";
|
||||
var wrapper = JsonUtility.FromJson<GitHubReleaseArrayWrapper>(wrapped);
|
||||
|
||||
if (wrapper?.items == null) return releases;
|
||||
|
||||
foreach (var item in wrapper.items)
|
||||
{
|
||||
releases.Add(new ReleaseInfo
|
||||
{
|
||||
TagName = item.tag_name ?? "",
|
||||
Name = item.name ?? item.tag_name ?? "",
|
||||
Body = item.body ?? "",
|
||||
PublishedAt = item.published_at ?? "",
|
||||
IsPrerelease = item.prerelease,
|
||||
IsDraft = item.draft
|
||||
});
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class GitHubReleaseArrayWrapper
|
||||
{
|
||||
public GitHubReleaseJson[] items;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class GitHubReleaseJson
|
||||
{
|
||||
public string tag_name;
|
||||
public string name;
|
||||
public string body;
|
||||
public string published_at;
|
||||
public bool prerelease;
|
||||
public bool draft;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user