using System; using System.Collections.Generic; using System.Diagnostics; using UnityEditor; using UnityEngine; public class BuildGeneratorEditorWindow : EditorWindow { private VersionDefinition editorVersion = default; private VersionDefinition gitVersion = default; [MenuItem("Window/Build Generator")] private static void ShowWindow() { BuildGeneratorEditorWindow window = GetWindow(); window.titleContent = new GUIContent("Build Generator"); window.editorVersion = new(PlayerSettings.bundleVersion); try { window.gitVersion = GitProcess.GetLatestBuildVersion(); } catch { } window.Show(); } private void OnGUI() { if (GUILayout.Button($"Re-evaluate versions")) { editorVersion = new(PlayerSettings.bundleVersion); gitVersion = GitProcess.GetLatestBuildVersion(); } EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginVertical(); if (GUILayout.Button($"Create Release")) CommitVersion(GitProcess.GetUpcomingReleaseVersion()); if (GUILayout.Button($"Create Release Candidate")) CommitVersion(GitProcess.GetUpcomingReleaseCandidateVersion()); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField($"Editor Version: {editorVersion} ({editorVersion.BuildNumber})"); EditorGUILayout.LabelField($"Git Version: {editorVersion} ({editorVersion.BuildNumber})"); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (GUILayout.Button($"Push All")) { GitProcess.Push(); GitProcess.PushTags(); } EditorGUILayout.Space(); EditorGUILayout.LabelField("Force New Version"); EditorGUILayout.BeginHorizontal(); if (IncrementButton("Major", versionDefinition.IncreaseMajor())) CommitVersion(versionDefinition.IncreaseMajor()); if (IncrementButton("Minor", versionDefinition.IncreaseMinor())) CommitVersion(versionDefinition.IncreaseMinor()); if (IncrementButton("Patch", versionDefinition.IncreasePatch())) CommitVersion(versionDefinition.IncreasePatch()); if (IncrementButton("Release Candidate", versionDefinition.IncreaseReleaseCandidate())) CommitVersion(versionDefinition.IncreaseReleaseCandidate()); EditorGUILayout.EndHorizontal(); } private void CommitVersion(VersionDefinition versionDefinition) { if (this.editorVersion >= versionDefinition) { EditorUtility.DisplayDialog( "Release Conflict", $"You can't create a new version because the old version({this.editorVersion}) is either higher or the same as the new version({versionDefinition}). Please make new commits to create new releases.", "Return"); return; } if (EditorUtility.DisplayDialog( "Release Confirmation", $"Are you sure to commit a new incremental version of {versionDefinition}?", "Yes", "Cancel") ) return; ApplyVersion(versionDefinition); GitProcess.Add("ProjectSettings\\ProjectSettings.asset"); GitProcess.Add("Assets\\Settings\\Build Profiles\\**"); GitProcess.Commit($"chore: Bump Version to {versionDefinition}"); GitProcess.CreateTag(versionDefinition); GitProcess.PushTag(versionDefinition); } private bool IncrementButton(string fieldName, VersionDefinition resultDefinition) { EditorGUILayout.BeginVertical(); bool isButtonPressed = GUILayout.Button($"Add Incremental {fieldName}"); EditorGUILayout.LabelField($"{editorVersion} -> {resultDefinition}", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter }); EditorGUILayout.EndVertical(); return isButtonPressed && EditorUtility.DisplayDialog("Increment Confirmation", $"Are you sure to add an incremental {fieldName} version?", "Yes", "Cancel"); } private void ApplyVersion(VersionDefinition versionDefinition) { this.editorVersion = versionDefinition; PlayerSettings.Android.bundleVersionCode = versionDefinition.BuildNumber; PlayerSettings.iOS.buildNumber = versionDefinition.BuildNumber.ToString(); PlayerSettings.bundleVersion = versionDefinition.ToString(); AssetDatabase.SaveAssets(); } private struct VersionDefinition { public const int MAX_VALUE = 99; public const string BUILD_NUMBER_FORMAT = "00"; public const int DEFAULT_MAJOR = 0; public const int DEFAULT_MINOR = 0; public const int DEFAULT_PATCH = 0; public const int DEFAULT_RELEASE_CANDIDATE = 1; public const int RELEASE_RC_VALUE = 99; private readonly int _major; private readonly int _minor; private readonly int _patch; private readonly int _releaseCandidate; public readonly int Major => _major > MAX_VALUE ? MAX_VALUE : _major; public readonly int Minor => _minor > MAX_VALUE ? MAX_VALUE : _minor; public readonly int Patch => _patch > MAX_VALUE ? MAX_VALUE : _patch; public readonly int ReleaseCandidate => _releaseCandidate > MAX_VALUE ? MAX_VALUE : _releaseCandidate; public readonly bool IsRelease => ReleaseCandidate == RELEASE_RC_VALUE; public VersionDefinition(int? major = null, int? minor = null, int? patch = null, int? releaseCandidate = null) { _major = major ?? DEFAULT_MAJOR; _minor = minor ?? DEFAULT_MINOR; _patch = patch ?? DEFAULT_PATCH; _releaseCandidate = releaseCandidate ?? DEFAULT_RELEASE_CANDIDATE; if (_major == 0 && _minor == 0) _minor = _releaseCandidate = 1; } public VersionDefinition(string versionString) { if (versionString.StartsWith('v')) versionString = versionString[1..]; _major = DEFAULT_MAJOR; _minor = DEFAULT_MINOR; _patch = DEFAULT_PATCH; _releaseCandidate = RELEASE_RC_VALUE; string[] releaseCandidateVersionStrings = versionString.Split("-rc"); string[] versionNumbers = releaseCandidateVersionStrings[0].Split('.'); if (versionNumbers.Length > 0 && int.TryParse(versionNumbers[0], out int major)) _major = major; if (versionNumbers.Length > 1 && int.TryParse(versionNumbers[1], out int minor)) _minor = minor; if (versionNumbers.Length > 2 && int.TryParse(versionNumbers[2], out int patch)) _patch = patch; if (releaseCandidateVersionStrings.Length > 1 && int.TryParse(releaseCandidateVersionStrings[1], out int releaseCandidate)) _releaseCandidate = releaseCandidate; if (_major == 0 && _minor == 0) _minor = _releaseCandidate = 1; } public readonly VersionDefinition IncreaseMajor() => new(Major + 1, DEFAULT_MINOR, DEFAULT_PATCH, DEFAULT_RELEASE_CANDIDATE); public readonly VersionDefinition IncreaseMinor() => new(Major, Minor + 1, DEFAULT_PATCH, DEFAULT_RELEASE_CANDIDATE); public readonly VersionDefinition IncreasePatch() => new(Major, Minor, Patch + 1, DEFAULT_RELEASE_CANDIDATE); public readonly VersionDefinition IncreaseReleaseCandidate() => new(Major, Minor, Patch, ReleaseCandidate + 1); public readonly VersionDefinition ToReleaseVersion() => new(Major, Minor, Patch, RELEASE_RC_VALUE); public readonly int BuildNumber => int.Parse($"{Major.ToString(BUILD_NUMBER_FORMAT)}{Minor.ToString(BUILD_NUMBER_FORMAT)}{Patch.ToString(BUILD_NUMBER_FORMAT)}{ReleaseCandidate.ToString(BUILD_NUMBER_FORMAT)}"); public override readonly string ToString() { if (IsRelease) return $"{Major}.{Minor}.{Patch}"; return $"{Major}.{Minor}.{Patch}-rc{ReleaseCandidate}"; } public static bool operator >(VersionDefinition left, VersionDefinition right) => left.BuildNumber > right.BuildNumber; public static bool operator <(VersionDefinition left, VersionDefinition right) => left.BuildNumber < right.BuildNumber; public static bool operator >=(VersionDefinition left, VersionDefinition right) => left.BuildNumber >= right.BuildNumber; public static bool operator <=(VersionDefinition left, VersionDefinition right) => left.BuildNumber <= right.BuildNumber; public static bool operator ==(VersionDefinition left, VersionDefinition right) => left.BuildNumber == right.BuildNumber; public static bool operator !=(VersionDefinition left, VersionDefinition right) => left.BuildNumber != right.BuildNumber; public override readonly bool Equals(object obj) => obj is VersionDefinition versionDefinition && BuildNumber == versionDefinition.BuildNumber; public override readonly int GetHashCode() => HashCode.Combine(Major, Minor, Patch, ReleaseCandidate); } private static class GitProcess { public static VersionDefinition GetLatestBuildVersion() => new(GetLatestBuildVersionString()); public static VersionDefinition GetUpcomingReleaseVersion() => GetUpcomingReleaseCandidateVersion().ToReleaseVersion(); public static VersionDefinition GetUpcomingReleaseCandidateVersion() { VersionDefinition latestVersionDefinition = GetLatestBuildVersion(); string[] commits = GetCommitsSinceLastTag(); return ApplySemVer(latestVersionDefinition, commits); } public static VersionDefinition GetVersion(string version, IList gitLogs) => ApplySemVer(new(version), gitLogs); public static VersionDefinition ApplySemVer(VersionDefinition latestVersionDefinition, IList commits) { VersionDefinition result = latestVersionDefinition.IsRelease ? latestVersionDefinition.IncreasePatch() : latestVersionDefinition; for (int i = commits.Count - 1; i >= 0; i--) { string line = commits[i]; if (string.IsNullOrWhiteSpace(line)) continue; string commitTitle = line.Length > 9 ? line[9..] : line; if (commitTitle.StartsWith("breaking change", StringComparison.InvariantCultureIgnoreCase)) { result = result.IncreaseMajor(); break; } if (latestVersionDefinition.Minor != result.Minor) continue; if (commitTitle.StartsWith("feat", StringComparison.OrdinalIgnoreCase)) { result = result.IncreaseMinor(); continue; } if (latestVersionDefinition.Patch != result.Patch) continue; if (commitTitle.StartsWith("fix", StringComparison.OrdinalIgnoreCase)) { result = result.IncreasePatch(); continue; } if (latestVersionDefinition.ReleaseCandidate == result.ReleaseCandidate) result = result.IncreaseReleaseCandidate(); } return result; } public static string GetLatestBuildVersionString() => RunGitCommand($"describe --tags --abbrev=0"); public static string[] GetCommits() => RunGitCommand("log --oneline").Replace("\r", "").Split('\n'); public static string[] GetCommitsSinceLastTag() => RunGitCommand($"log {GetLatestBuildVersionString()}..HEAD --oneline").Replace("\r", "").Split('\n'); public static string CreateTag(VersionDefinition versionDefinition) => CreateTag($"v{versionDefinition}", $"{(versionDefinition.IsRelease ? "Release" : "Build")} {versionDefinition} with build number: {versionDefinition.BuildNumber}\""); public static string Push() => RunGitCommand($"push"); public static string Add(IList files) => RunGitCommand($"add \"{string.Join("\" \"", files)}\""); public static string Add(string file) => RunGitCommand($"add \"{file}\""); public static string Commit(string message) => RunGitCommand($"commit -m \"{message}\""); public static string PushTags() => RunGitCommand($"push --tags"); public static string PushTag(VersionDefinition versionDefinition, string remote = "origin") => RunGitCommand($"push {remote} tag v{versionDefinition}"); public static string CreateTag(string title, string message) => RunGitCommand($"tag -a {title} -m \"{message}"); public static string Unstage(IList files) => RunGitCommand($"reset \"{string.Join("\" \"", files)}\""); public static string Unstage(string file) => RunGitCommand($"reset \"{file}\""); public static string Unstage() => RunGitCommand($"reset"); private static string RunGitCommand(string gitArgs) { ProcessStartInfo processStartInfo = new() { FileName = "git", Arguments = gitArgs, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; try { Process process = Process.Start(processStartInfo); string output = process.StandardOutput.ReadToEnd(); if (output.EndsWith('\n')) output = output[0..^1]; if (output.EndsWith('\r')) output = output[0..^1]; process.WaitForExit(); return output; } catch (Exception ex) { Console.WriteLine("An error occurred: " + ex.Message); throw; } } } }