// Assets/Editor/BuildArtifactsCleaner.cs using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Collections.Generic; using UnityEditor; using UnityEngine; public static class BuildArtifactsCleaner { private static readonly string[] TargetFolderNames = { "obj", "bin" }; // Attributs d'assembly fréquents générés automatiquement par les projets SDK (.NET) private static readonly string[] AssemblyAttrs = { "AssemblyCompany", "AssemblyConfiguration", "AssemblyDescription", "AssemblyFileVersion", "AssemblyInformationalVersion", "AssemblyProduct", "AssemblyTitle", "AssemblyTrademark", "AssemblyVersion", }; // Menu: nettoyer uniquement [MenuItem("Window/Clean obj & bin (Assets)")] public static void CleanOnly() { int removed = CleanObjBinUnderAssets(); EditorUtility.DisplayDialog("Clean Complete", $"Dossiers supprimés : {removed}", "OK"); } // Menu: scanner uniquement [MenuItem("Window/Scan AssemblyInfo Duplicates")] public static void ScanOnly() { var report = ScanManualAssemblyInfoDuplicates(); ShowScanReport(report); } // Menu: tout faire [MenuItem("Window/Clean & Scan (All)")] public static void CleanAndScanAll() { int removed = CleanObjBinUnderAssets(); var report = ScanManualAssemblyInfoDuplicates(); ShowScanReport(report, prefix:$"Dossiers supprimés : {removed}\n\n"); } /// /// Supprime tous les dossiers obj/bin sous Assets/, proprement (avec .meta). /// private static int CleanObjBinUnderAssets() { string assetsPath = Application.dataPath.Replace('\\','/'); var toDelete = new List(); foreach (var target in TargetFolderNames) { // On cherche des dossiers nommés exactement "obj" ou "bin" var found = Directory.GetDirectories(assetsPath, target, SearchOption.AllDirectories); foreach (var path in found) { // ignore “obj/bin” internes de paquets éventuels si besoin ? ici on supprime tout sous Assets/ toDelete.Add(path.Replace('\\','/')); } } // Supprimer int count = 0; foreach (var absPath in toDelete.Distinct()) { // Convertit en chemin relatif Unity (Assets/...) string relative = "Assets" + absPath.Substring(assetsPath.Length); try { // Supprime via AssetDatabase s'il est sous Assets if (AssetDatabase.IsValidFolder(relative)) { // AssetDatabase.DeleteAsset fonctionne dossier par dossier if (AssetDatabase.DeleteAsset(relative)) { count++; continue; } } // Fallback bas niveau FileUtil.DeleteFileOrDirectory(absPath); var meta = absPath + ".meta"; if (File.Exists(meta)) FileUtil.DeleteFileOrDirectory(meta); count++; } catch (Exception ex) { Debug.LogWarning($"Impossible de supprimer: {relative}\n{ex}"); } } AssetDatabase.Refresh(); return count; } /// /// Scan des fichiers AssemblyInfo.cs manuels (hors obj/bin) et détection d'attributs à risque de doublon. /// private static ScanReport ScanManualAssemblyInfoDuplicates() { string assetsPath = Application.dataPath.Replace('\\','/'); var report = new ScanReport(); // Tous les .cs sous Assets (hors obj/bin) var allCs = Directory.GetFiles(assetsPath, "*.cs", SearchOption.AllDirectories) .Select(p => p.Replace('\\','/')) .Where(p => !p.Contains("/obj/") && !p.Contains("/bin/")) .ToArray(); // Filtrer AssemblyInfo.cs var assemblyInfos = allCs.Where(p => Path.GetFileName(p).Equals("AssemblyInfo.cs", StringComparison.OrdinalIgnoreCase)) .ToArray(); // Regex pour lignes du type [assembly: AssemblyXxx("...")] // Autorise espaces, qualifieurs System.Reflection, etc. var rx = new Regex(@"\[\s*assembly\s*:\s*(?:System\.Reflection\.)?(?[A-Za-z_][A-Za-z0-9_]*)\s*\(", RegexOptions.Compiled); foreach (var absPath in assemblyInfos) { string text = SafeReadAllText(absPath); if (text == null) continue; var matches = rx.Matches(text); var names = matches.Cast() .Select(m => m.Groups["name"].Value) .ToList(); var hits = names.Where(n => AssemblyAttrs.Contains(n)).Distinct().ToList(); if (hits.Count > 0) { string relative = "Assets" + absPath.Substring(assetsPath.Length); report.Offenders.Add(new Offender { Path = relative, Attributes = hits }); } } return report; } private static void ShowScanReport(ScanReport report, string prefix = "") { if (report.Offenders.Count == 0) { EditorUtility.DisplayDialog("Scan Complete", prefix + "Aucun AssemblyInfo.cs manuel problématique détecté.", "OK"); return; } // Compose un message concis + log détaillé dans la Console var msg = prefix + "Fichiers AssemblyInfo potentiellement en conflit :\n\n"; foreach (var off in report.Offenders) { msg += $"• {off.Path}\n Attributs: {string.Join(", ", off.Attributes)}\n"; Debug.LogWarning($"[AssemblyInfo Duplicate Risk]\nPath: {off.Path}\nAttributes: {string.Join(", ", off.Attributes)}\n" + "→ Si tu compiles aussi avec un projet .NET SDK générant des infos d’assembly, ces attributs peuvent dupliquer ceux auto-générés.\n" + "Solutions: retirer ces attributs manuels OU désactiver la génération auto (false dans le projet externe)."); } EditorUtility.DisplayDialog("Scan Complete", msg, "OK"); } private static string SafeReadAllText(string path) { try { return File.ReadAllText(path); } catch (Exception ex) { Debug.LogWarning($"Lecture échouée: {path}\n{ex}"); return null; } } // --- Petites structures de rapport --- private class ScanReport { public List Offenders = new List(); } private class Offender { public string Path; public List Attributes; } }