// 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;
}
}