/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.PackageManager;
using UnityEngine;
namespace com.IvanMurzak.Unity.MCP.Editor.Utils
{
///
/// Build preprocessor that extracts and merges link.xml files from all packages.
/// This ensures proper code stripping preservation during IL2CPP builds.
///
public class LinkExtractor : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
const string FileName = "link.xml";
const string MergedFolderName = "packages-merged-link";
string MergedFolder => Path.Combine(Application.dataPath, MergedFolderName);
string MergedLinkFilePath => Path.Combine(MergedFolder, FileName);
public int callbackOrder => 0;
public void OnPreprocessBuild(BuildReport report)
{
CreateMergedLinkFromPackages();
}
public void OnPostprocessBuild(BuildReport report)
{
CleanupTemporaryFiles();
}
void CreateMergedLinkFromPackages()
{
var request = Client.List();
// Wait for package manager request to complete
while (!request.IsCompleted)
{
System.Threading.Thread.Sleep(10); // Prevent busy-wait CPU spinning
}
if (request.Status == StatusCode.Success)
{
var linkXmlFiles = CollectLinkXmlFiles(request.Result);
if (linkXmlFiles.Count == 0)
return;
MergeAndSaveLinkFiles(linkXmlFiles);
}
else if (request.Status >= StatusCode.Failure)
{
Debug.LogError($"[LinkExtractor] Failed to list packages: {request.Error.message}");
}
}
List CollectLinkXmlFiles(PackageCollection packages)
{
var xmlPaths = new List();
foreach (var package in packages)
{
if (string.IsNullOrEmpty(package.resolvedPath))
continue;
var packageLinkFiles = Directory.EnumerateFiles(
package.resolvedPath,
FileName,
SearchOption.AllDirectories
);
xmlPaths.AddRange(packageLinkFiles);
}
return xmlPaths;
}
void MergeAndSaveLinkFiles(List xmlPaths)
{
var xmlDocuments = new List();
// Load XML files with error handling
foreach (var path in xmlPaths)
{
try
{
var doc = XDocument.Load(path);
if (doc.Root?.Name.LocalName == "linker")
{
xmlDocuments.Add(doc);
}
else
{
Debug.LogWarning($"[LinkExtractor] Skipping invalid link.xml (no 'linker' root): {path}");
}
}
catch (System.Exception ex)
{
Debug.LogError($"[LinkExtractor] Failed to load {path}: {ex.Message}");
}
}
if (xmlDocuments.Count == 0)
return;
// Create merged document with duplicate detection
var mergedRoot = new XElement("linker");
var seenAssemblies = new HashSet();
foreach (var doc in xmlDocuments)
{
var elements = doc.Root?.Elements();
if (elements == null)
continue;
foreach (var element in elements)
{
// Create a unique key for this element
var elementKey = CreateElementKey(element);
if (seenAssemblies.Add(elementKey))
{
mergedRoot.Add(new XElement(element));
}
}
}
var mergedXml = new XDocument(mergedRoot);
if (!Directory.Exists(MergedFolder))
Directory.CreateDirectory(MergedFolder);
mergedXml.Save(MergedLinkFilePath);
Debug.Log($"[LinkExtractor] Merged {xmlDocuments.Count} link.xml files ({seenAssemblies.Count} unique elements) to {MergedLinkFilePath}");
}
string CreateElementKey(XElement element)
{
// For assembly elements, use fullname attribute
if (element.Name.LocalName == "assembly")
{
var fullname = element.Attribute("fullname")?.Value;
return fullname != null ? $"assembly:{fullname}" : element.ToString();
}
// For other elements (type, method, etc.), use the full element representation
return element.ToString();
}
void CleanupTemporaryFiles()
{
if (Directory.Exists(MergedFolder))
Directory.Delete(MergedFolder, recursive: true);
var metaFilePath = MergedFolder + ".meta";
if (File.Exists(metaFilePath))
File.Delete(metaFilePath);
UnityEditor.AssetDatabase.Refresh();
}
}
}