using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; namespace Topon_Adapter.Editor { [Serializable] public sealed class ToponBuildSettings { public bool enableDebuggerUI = false; public bool forceVerbtoUtilVersion = true; public string verbtoUtilDependency = ToponBuildSettingsStore.DefaultVerbtoUtilDependency; public bool stripResolvedDebuggerArtifacts = true; public bool enableIOSDebuggerUI = false; public string iosDebuggerPodVersion = ToponBuildSettingsStore.DefaultIOSDebuggerPodVersion; } internal static class ToponBuildSettingsStore { public const string DefaultVerbtoUtilDependency = "com.verbto.tools:util:1.1.3"; public const string IOSDebuggerPodName = "AnyThinkDebugUISDK"; public const string DefaultIOSDebuggerPodVersion = "1.0.7"; private const string ActiveBuildSessionKey = "Commercialization.Topon.ActiveBuildSettings"; private const string SettingsFileSuffix = "_topon_build_settings.json"; private const string BuildConfigsFolder = "BuildConfigs"; private const string DebuggerPackageMarker = "com.anythink.sdk:debugger-ui"; private const string VerbtoPackageMarker = "com.verbto.tools:util"; [Serializable] private sealed class ActiveBuildSettings { public ToponBuildSettings settings; public long utcTicks; } public static ToponBuildSettings CreateDefault() { return new ToponBuildSettings(); } public static string GetVerbtoUtilDependency(ToponBuildSettings settings) { if (settings == null || string.IsNullOrWhiteSpace(settings.verbtoUtilDependency)) { return DefaultVerbtoUtilDependency; } return settings.verbtoUtilDependency.Trim(); } public static string GetIOSDebuggerPodVersion(ToponBuildSettings settings) { if (settings == null || string.IsNullOrWhiteSpace(settings.iosDebuggerPodVersion)) { return DefaultIOSDebuggerPodVersion; } return settings.iosDebuggerPodVersion.Trim(); } public static ToponBuildSettings LoadForProfileName(string profileName, string repositoryRoot) { var settings = CreateDefault(); var path = GetSettingsPath(profileName, repositoryRoot); if (string.IsNullOrEmpty(path) || !File.Exists(path)) { return settings; } try { JsonUtility.FromJsonOverwrite(File.ReadAllText(path), settings); Normalize(settings); } catch (Exception exception) { Debug.LogWarning($"[TopOn Build] Failed to read build settings: {exception.Message}"); } return settings; } public static void SaveForProfileName(string profileName, string repositoryRoot, ToponBuildSettings settings) { if (settings == null) { settings = CreateDefault(); } else { Normalize(settings); } var path = GetSettingsPath(profileName, repositoryRoot); if (string.IsNullOrEmpty(path)) { return; } try { var directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(path, JsonUtility.ToJson(settings, true)); } catch (Exception exception) { Debug.LogWarning($"[TopOn Build] Failed to save build settings: {exception.Message}"); } } public static void SetActiveForCurrentBuild(ToponBuildSettings settings) { var activeSettings = new ActiveBuildSettings { settings = Clone(settings), utcTicks = DateTime.UtcNow.Ticks }; SessionState.SetString(ActiveBuildSessionKey, JsonUtility.ToJson(activeSettings)); } public static ToponBuildSettings GetActiveForCurrentBuild() { var json = SessionState.GetString(ActiveBuildSessionKey, string.Empty); if (string.IsNullOrWhiteSpace(json)) { return CreateDefault(); } try { var activeSettings = JsonUtility.FromJson(json); if (activeSettings == null || activeSettings.settings == null) { return CreateDefault(); } Normalize(activeSettings.settings); var activatedAt = new DateTime(activeSettings.utcTicks, DateTimeKind.Utc); if (DateTime.UtcNow - activatedAt > TimeSpan.FromHours(6)) { ClearActiveBuildSettings(); return CreateDefault(); } return Clone(activeSettings.settings); } catch (Exception exception) { Debug.LogWarning($"[TopOn Build] Failed to read active build settings: {exception.Message}"); return CreateDefault(); } } public static void ClearActiveBuildSettings() { SessionState.EraseString(ActiveBuildSessionKey); } public static bool HasStaleDebuggerResolverOutput(string repositoryRoot) { foreach (var path in GetResolverOutputPaths(repositoryRoot)) { if (!File.Exists(path)) { continue; } var content = File.ReadAllText(path); if (content.Contains(DebuggerPackageMarker)) { return true; } } return false; } public static bool HasUnforcedVerbtoUtilOutput(string repositoryRoot, string expectedDependency) { expectedDependency = NormalizeDependency(expectedDependency); foreach (var path in GetResolverOutputPaths(repositoryRoot)) { if (!File.Exists(path)) { continue; } var content = File.ReadAllText(path); if (content.Contains(VerbtoPackageMarker) && !content.Contains(expectedDependency)) { return true; } } foreach (var path in FindResolvedVerbtoUtilArtifacts(repositoryRoot)) { if (!IsExpectedVerbtoUtilArtifactFileName(Path.GetFileName(path), expectedDependency)) { return true; } } return false; } public static IReadOnlyList FindResolvedDebuggerArtifacts(string repositoryRoot) { var result = new List(); var root = ResolveRepositoryRoot(repositoryRoot); if (string.IsNullOrEmpty(root)) { return result; } var androidPluginPath = Path.Combine(root, "Assets", "Plugins", "Android"); if (!Directory.Exists(androidPluginPath)) { return result; } foreach (var path in Directory.GetFiles(androidPluginPath, "*", SearchOption.AllDirectories)) { if (IsDebuggerArtifactFileName(Path.GetFileName(path))) { result.Add(path); } } return result; } public static IReadOnlyList FindResolvedVerbtoUtilArtifacts(string repositoryRoot) { var result = new List(); var root = ResolveRepositoryRoot(repositoryRoot); if (string.IsNullOrEmpty(root)) { return result; } var androidPluginPath = Path.Combine(root, "Assets", "Plugins", "Android"); if (!Directory.Exists(androidPluginPath)) { return result; } foreach (var path in Directory.GetFiles(androidPluginPath, "*", SearchOption.AllDirectories)) { if (IsVerbtoUtilArtifactFileName(Path.GetFileName(path))) { result.Add(path); } } return result; } internal static bool IsDebuggerArtifactFileName(string fileName) { if (string.IsNullOrEmpty(fileName)) { return false; } return fileName.StartsWith("com.anythink.sdk.debugger-ui-", StringComparison.OrdinalIgnoreCase); } internal static bool IsVerbtoUtilArtifactFileName(string fileName) { if (string.IsNullOrEmpty(fileName)) { return false; } return fileName.StartsWith("com.verbto.tools.util-", StringComparison.OrdinalIgnoreCase); } internal static bool IsExpectedVerbtoUtilArtifactFileName(string fileName, string expectedDependency) { if (!IsVerbtoUtilArtifactFileName(fileName)) { return false; } var version = GetVersionFromDependency(expectedDependency); if (string.IsNullOrEmpty(version)) { return false; } return fileName.StartsWith($"com.verbto.tools.util-{version}", StringComparison.OrdinalIgnoreCase); } private static ToponBuildSettings Clone(ToponBuildSettings settings) { if (settings == null) { return CreateDefault(); } return new ToponBuildSettings { enableDebuggerUI = settings.enableDebuggerUI, forceVerbtoUtilVersion = settings.forceVerbtoUtilVersion, verbtoUtilDependency = GetVerbtoUtilDependency(settings), stripResolvedDebuggerArtifacts = settings.stripResolvedDebuggerArtifacts, enableIOSDebuggerUI = settings.enableIOSDebuggerUI, iosDebuggerPodVersion = GetIOSDebuggerPodVersion(settings) }; } private static void Normalize(ToponBuildSettings settings) { if (settings == null) { return; } settings.verbtoUtilDependency = NormalizeDependency(settings.verbtoUtilDependency); settings.iosDebuggerPodVersion = NormalizeIOSDebuggerPodVersion(settings.iosDebuggerPodVersion); } private static string NormalizeDependency(string dependency) { if (string.IsNullOrWhiteSpace(dependency)) { return DefaultVerbtoUtilDependency; } return dependency.Trim(); } private static string NormalizeIOSDebuggerPodVersion(string version) { if (string.IsNullOrWhiteSpace(version)) { return DefaultIOSDebuggerPodVersion; } return version.Trim(); } private static string GetVersionFromDependency(string dependency) { dependency = NormalizeDependency(dependency); var parts = dependency.Split(':'); return parts.Length >= 3 ? parts[parts.Length - 1] : string.Empty; } private static IEnumerable GetResolverOutputPaths(string repositoryRoot) { var root = ResolveRepositoryRoot(repositoryRoot); if (string.IsNullOrEmpty(root)) { yield break; } yield return Path.Combine(root, "ProjectSettings", "AndroidResolverDependencies.xml"); yield return Path.Combine(root, "Assets", "Plugins", "Android", "mainTemplate.gradle"); yield return Path.Combine(root, "Assets", "Plugins", "Android", "mainTemplate.gradle.backup"); yield return Path.Combine(root, "Assets", "Plugins", "Android", "settingsTemplate.gradle"); } private static string GetSettingsPath(string profileName, string repositoryRoot) { var root = ResolveRepositoryRoot(repositoryRoot); if (string.IsNullOrEmpty(root)) { return string.Empty; } return Path.Combine(root, BuildConfigsFolder, $"{SanitizeProfileName(profileName)}{SettingsFileSuffix}"); } private static string ResolveRepositoryRoot(string repositoryRoot) { if (!string.IsNullOrWhiteSpace(repositoryRoot)) { return Path.GetFullPath(repositoryRoot); } var dataPath = Application.dataPath; var parent = Directory.GetParent(dataPath); return parent == null ? string.Empty : parent.FullName; } private static string SanitizeProfileName(string profileName) { if (string.IsNullOrWhiteSpace(profileName)) { return "default"; } var invalidChars = Path.GetInvalidFileNameChars(); var chars = profileName.ToCharArray(); for (var i = 0; i < chars.Length; i++) { if (Array.IndexOf(invalidChars, chars[i]) >= 0) { chars[i] = '_'; } } return new string(chars); } } #if UNITY_IOS || UNITY_IPHONE internal static class ToponIOSDebuggerDependencyPostProcessor { private const string Tag = "[TopOn Build]"; [PostProcessBuild(int.MaxValue)] public static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath) { if (buildTarget != BuildTarget.iOS) { return; } var settings = ToponBuildSettingsStore.GetActiveForCurrentBuild(); try { ProcessExportedProject(buildPath, settings); } finally { ToponBuildSettingsStore.ClearActiveBuildSettings(); } } public static void ProcessExportedProject(string buildPath, ToponBuildSettings settings) { if (settings == null) { settings = ToponBuildSettingsStore.CreateDefault(); } var podfilePath = Path.Combine(buildPath, "Podfile"); if (!File.Exists(podfilePath)) { if (settings.enableIOSDebuggerUI) { Debug.LogWarning($"{Tag} Podfile not found, cannot add {ToponBuildSettingsStore.IOSDebuggerPodName}: {podfilePath}"); } return; } var content = File.ReadAllText(podfilePath); var updated = RewritePodfile(content, settings); if (!string.Equals(content, updated, StringComparison.Ordinal)) { File.WriteAllText(podfilePath, updated); } if (settings.enableIOSDebuggerUI) { Debug.Log($"{Tag} iOS DebugUI pod enabled: {ToponBuildSettingsStore.IOSDebuggerPodName} {ToponBuildSettingsStore.GetIOSDebuggerPodVersion(settings)}."); } else { Debug.Log($"{Tag} iOS DebugUI pod disabled for this build."); } } private static string RewritePodfile(string content, ToponBuildSettings settings) { var newline = content.Contains("\r\n") ? "\r\n" : "\n"; var lines = new List(content.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n')); for (var i = lines.Count - 1; i >= 0; i--) { if (IsDebuggerPodLine(lines[i])) { lines.RemoveAt(i); } } if (settings.enableIOSDebuggerUI) { InsertDebuggerPod(lines, ToponBuildSettingsStore.GetIOSDebuggerPodVersion(settings)); } return string.Join(newline, lines); } private static bool IsDebuggerPodLine(string line) { return Regex.IsMatch(line ?? string.Empty, @"^\s*pod\s+['""]" + ToponBuildSettingsStore.IOSDebuggerPodName + @"['""]", RegexOptions.CultureInvariant); } private static void InsertDebuggerPod(List lines, string version) { var targetRange = FindTargetRange(lines, "UnityFramework"); if (targetRange.start < 0) { targetRange = FindTargetRange(lines, "Unity-iPhone"); } if (targetRange.start >= 0) { var indent = ResolvePodIndent(lines, targetRange); var podLine = $"{indent}pod '{ToponBuildSettingsStore.IOSDebuggerPodName}', '{version}'"; lines.Insert(ResolvePodInsertIndex(lines, targetRange), podLine); return; } if (lines.Count > 0 && !string.IsNullOrWhiteSpace(lines[lines.Count - 1])) { lines.Add(string.Empty); } lines.Add("target 'UnityFramework' do"); lines.Add($" pod '{ToponBuildSettingsStore.IOSDebuggerPodName}', '{version}'"); lines.Add("end"); } private static (int start, int end) FindTargetRange(List lines, string targetName) { var targetPattern = new Regex(@"^\s*target\s+['""]" + Regex.Escape(targetName) + @"['""]\s+do\b", RegexOptions.CultureInvariant); for (var i = 0; i < lines.Count; i++) { if (!targetPattern.IsMatch(lines[i] ?? string.Empty)) { continue; } var depth = 0; for (var j = i; j < lines.Count; j++) { var line = StripComment(lines[j]); if (Regex.IsMatch(line, @"\bdo\b", RegexOptions.CultureInvariant)) { depth++; } if (Regex.IsMatch(line, @"^\s*end\s*$", RegexOptions.CultureInvariant)) { depth--; if (depth <= 0) { return (i, j); } } } return (i, lines.Count); } return (-1, -1); } private static int ResolvePodInsertIndex(List lines, (int start, int end) targetRange) { var insertIndex = targetRange.start + 1; for (var i = targetRange.start + 1; i < targetRange.end && i < lines.Count; i++) { if (Regex.IsMatch(lines[i] ?? string.Empty, @"^\s*pod\s+", RegexOptions.CultureInvariant)) { insertIndex = i + 1; } } return insertIndex; } private static string ResolvePodIndent(List lines, (int start, int end) targetRange) { for (var i = targetRange.start + 1; i < targetRange.end && i < lines.Count; i++) { var match = Regex.Match(lines[i] ?? string.Empty, @"^(\s*)pod\s+", RegexOptions.CultureInvariant); if (match.Success) { return match.Groups[1].Value; } } var targetIndent = Regex.Match(lines[targetRange.start] ?? string.Empty, @"^(\s*)", RegexOptions.CultureInvariant).Value; return targetIndent + " "; } private static string StripComment(string line) { if (string.IsNullOrEmpty(line)) { return string.Empty; } var commentIndex = line.IndexOf('#'); return commentIndex >= 0 ? line.Substring(0, commentIndex) : line; } } #endif }