Search Unity

Replace scripts by Assemblies

Discussion in 'Editor & General Support' started by Florian-Nouviale, Nov 5, 2018.

  1. Florian-Nouviale

    Florian-Nouviale

    Joined:
    Mar 14, 2013
    Posts:
    54
    Hi !

    I'm currently trying to get our project (a tool that contains both editor classes and mono behaviours) to output a package with assemblies instead of scripts. We have a CI system so I need to get this to work in batchmode but that should not be an issue.
    So far I think the best way to approach this is to use amdef files to have Unity generate the assemblies from the scripts (are these assemblies in debug or release ?). I was wondering if there is some automated way or method to have unity then replace the scripts with the assemblies they generated?
    If so, how can I trigger it?
    If not, what is the best way to do it ?
    I was thinking, something along the lines of
    1. recover the assembly files from Library/ScriptAssemblies
    2. temporarily move script files associated with said assemblies
    3. move the assemblies in plugins folders
    4. refresh the assets
    5. export the unity package
    Thanks
     
  2. Florian-Nouviale

    Florian-Nouviale

    Joined:
    Mar 14, 2013
    Posts:
    54
    Well I'm running into another issue. I managed to copy the assembly file generated by unity an remove the scripts but the classes in the assembly cannot be added as monobehaviors in the scene through the editor (athough they can be added through script) because the editor expects to have script files corresponding to the classes
     
  3. HECer

    HECer

    Joined:
    Mar 17, 2013
    Posts:
    46
    same problem here. Is there somehow a solution to it?
     
  4. Florian-Nouviale

    Florian-Nouviale

    Joined:
    Mar 14, 2013
    Posts:
    54
    I wrote a script that does the following (warning : it's complicated !) :
    Register to CompilationPipeline.assemblyCompilationFinished
    force an asset refresh if needed ( AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);)

    in the method registered to assemblyCompilationFinished:
    Find if the assembly name correspond to the assembly definition file I want to replace
    save a map of gui/classname of all the scripts inside the assembly definition file
    save the assembly definition file GUID
    Move all the scripts and their .meta files in an external folder (FileUtil.GetUniqueTempPathInProject();)
    Move the assembly definition file in an external folder
    rename the assembly definition file meta (I added tmp at the end of the extension)
    Move the assembly from the Library folder (use CompilationPipeline.GetAssemblies()) to the place where the assembly definition file was
    rename the assembly definition file meta back (removed tmp at the end of the extension)

    register to the end of compilation : CompilationPipeline.compilationFinished
    replaces the guids/fileids in the asset files (including scenes and prefabs) using the ids of the newly compiled monoscripts

    Here is the complete script

    Code (CSharp):
    1. using Hybrid.Tools;
    2. using Hybrid.Tools.Utils;
    3.  
    4. using System.Collections.Generic;
    5. using System.IO;
    6. using System.Linq;
    7. using System.Text;
    8.  
    9. using UnityEditor;
    10. using UnityEditor.Compilation;
    11.  
    12. using UnityEngine;
    13.  
    14. public class ReplaceAssemblies : ScriptableSingleton<ReplaceAssemblies>
    15. {
    16.     #region Nested
    17.  
    18.     protected class GuidClassNameMap : SerializableDictionary<string, string>
    19.     {
    20.     }
    21.  
    22.     protected class GuidFilenameMap : SerializableDictionary<string, string>
    23.     {
    24.     }
    25.  
    26.     #endregion
    27.  
    28.     #region Statics
    29.  
    30.     public static string ASSEMBLY_EXTENSION = ".dll";
    31.     public static string ASSEMBLY_DEFINITION_EXTENSION = ".asmdef";
    32.  
    33.     private static readonly string[] fileListPath = { "*.prefab", "*.unity", "*.asset" };
    34.  
    35.     #endregion
    36.  
    37.     #region Fields
    38.  
    39.     [SerializeField] private readonly List<string> assembliesFilesToReplace = new List<string>();
    40.  
    41.     [SerializeField] private readonly GuidClassNameMap oldGUIDToClassNameMap = new GuidClassNameMap();
    42.  
    43.     [SerializeField] private readonly GuidFilenameMap filenameToGuidMap = new GuidFilenameMap();
    44.  
    45.     [SerializeField] private List<string> pathsOfAssemblyFilesCreatedByUnity = new List<string>();
    46.  
    47.     [SerializeField] private List<string> pathsOfAssemblyFilesInAssetFolder = new List<string>();
    48.  
    49.     [SerializeField] private string tempSourceFilePath;
    50.  
    51.     #endregion
    52.  
    53.     #region Properties
    54.  
    55.     public string TempSourceFilePath
    56.     {
    57.         get
    58.         {
    59.             if (string.IsNullOrEmpty(tempSourceFilePath))
    60.             {
    61.                 tempSourceFilePath = FileUtil.GetUniqueTempPathInProject();
    62.             }
    63.  
    64.             return tempSourceFilePath;
    65.         }
    66.     }
    67.  
    68.     #endregion
    69.  
    70.     #region Methods
    71.  
    72.     [MenuItem("Tools/Replace Assembly")]
    73.     public static void ReplaceAssemblyMenu()
    74.     {
    75.         string assemblyDefinitionFilePath = EditorUtility.OpenFilePanel(
    76.             "Select Assembly Definition File",
    77.             Application.dataPath,
    78.             ASSEMBLY_DEFINITION_EXTENSION.Substring(1));
    79.         if (assemblyDefinitionFilePath.Length == 0)
    80.             return;
    81.  
    82.         instance.ReplaceAssembly(assemblyDefinitionFilePath);
    83.     }
    84.  
    85.     [MenuItem("Tools/Revert Replace all Assemblies")]
    86.     public static void RevertReplaceAssembliesMenu()
    87.     {
    88.         instance.RevertReplaceAssemblies();
    89.     }
    90.  
    91.     public void ReplaceAssembly(string assemblyPath, CompilerMessage[] messages)
    92.     {
    93.         string assemblyFileName = assembliesFilesToReplace.Find(assembly => assemblyPath.EndsWith(assembly));
    94.         // is this one of the assemblies we want to replace ?
    95.         if (!string.IsNullOrEmpty(assemblyFileName))
    96.         {
    97.             Debug.LogFormat("Found assembly to replace : {0}", assemblyPath);
    98.             string[] assemblyDefinitionFilePaths = Directory.GetFiles(".", Path.GetFileNameWithoutExtension(assemblyFileName) + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
    99.             if (assemblyDefinitionFilePaths.Length == 0)
    100.                 assemblyDefinitionFilePaths = Directory.GetFiles(".", assemblyFileName.Split('.')[0] + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
    101.             if (assemblyDefinitionFilePaths.Length == 0)
    102.                 assemblyDefinitionFilePaths = Directory.GetFiles(".", assemblyFileName + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
    103.             if (assemblyDefinitionFilePaths.Length > 0)
    104.             {
    105.                 string assemblyDefinitionFilePath = assemblyDefinitionFilePaths[0];
    106.                 ReplaceAssembly(assemblyDefinitionFilePath);
    107.             }
    108.             else
    109.             {
    110.                 Debug.LogErrorFormat("Could not find assembly definition file for assembly {0}. Tried : \n" +
    111.                                      "{1}\n" +
    112.                                      "{2}\n" +
    113.                                      "{3}" + assemblyFileName, Path.GetFileNameWithoutExtension(assemblyFileName) + ASSEMBLY_DEFINITION_EXTENSION,
    114.                     assemblyFileName.Split('.')[0] + ASSEMBLY_DEFINITION_EXTENSION, assemblyFileName + ASSEMBLY_DEFINITION_EXTENSION);
    115.             }
    116.         }
    117.     }
    118.  
    119.     public void AddAssemblyFileToReplace(string assemblyFile)
    120.     {
    121.         assembliesFilesToReplace.Add(assemblyFile);
    122.     }
    123.  
    124.     /// <summary>
    125.     /// Replace the GUID of an asset
    126.     /// </summary>
    127.     /// <param name="assetPath">The asset path</param>
    128.     /// <param name="newGuid">The new GUID to set</param>
    129.     private static void ReplaceIdOfAsset(string assetPath, string newGuid)
    130.     {
    131.         string[] fileLines = File.ReadAllLines(assetPath + ".meta");
    132.         for (int line = 0; line < fileLines.Length; line++)
    133.         {
    134.             //find all instances of the string "guid: " and grab the next 32 characters as the old GUID
    135.             if (fileLines[line].Contains("guid: "))
    136.             {
    137.                 int index = fileLines[line].IndexOf("guid: ") + 6;
    138.                 string oldGUID = fileLines[line].Substring(index, 32); // GUID has 32 characters.
    139.                 if (oldGUID == newGuid)
    140.                 {
    141.                     Debug.Log($"Asset at {assetPath} was already {newGuid}");
    142.                     return;
    143.                 }
    144.                 fileLines[line] = fileLines[line].Replace(oldGUID, newGuid);
    145.                 Debug.Log($"Replaced asset at {assetPath} GUID from {oldGUID} to {newGuid}");
    146.             }
    147.         }
    148.         //Write the lines back to the file
    149.         File.WriteAllLines(assetPath + ".meta", fileLines);
    150.     }
    151.  
    152.     /// <summary>
    153.     /// Get the GUID of an asset
    154.     /// </summary>
    155.     /// <param name="assetPath">The asset path</param>
    156.     private static GUID GUIDFromAssetPath(string assetPath)
    157.     {
    158.         string[] fileLines = File.ReadAllLines(assetPath + ".meta");
    159.         for (int line = 0; line < fileLines.Length; line++)
    160.         {
    161.             //find all instances of the string "guid: " and grab the next 32 characters as the old GUID
    162.             if (fileLines[line].Contains("guid: "))
    163.             {
    164.                 int index = fileLines[line].IndexOf("guid: ") + 6;
    165.                 string guid = fileLines[line].Substring(index, 32); // GUID has 32 characters.
    166.                 {
    167.                     Debug.Log($"Asset at {assetPath} has GUID {guid}");
    168.                     return new GUID(guid);
    169.                 }
    170.             }
    171.         }
    172.         return new GUID();
    173.     }
    174.  
    175.     /// <summary>
    176.     /// Replace ids in all asset files using the given maps
    177.     /// </summary>
    178.     /// <param name="oldGUIDToClassNameMap">Maps GUID to be replaced => FullClassName</param>
    179.     /// <param name="newMonoScriptToIDsMap">Maps FullClassName => new GUID, new FileID</param>
    180.     private static void ReplaceIdsInAssets(Dictionary<string, string> oldGUIDToClassNameMap, Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap)
    181.     {
    182.         StringBuilder output = new StringBuilder("Report of replaced ids : \n");
    183.         // list all the potential files that might need guid and fileID update
    184.         List<string> fileList = new List<string>();
    185.         foreach (string extension in fileListPath)
    186.         {
    187.             fileList.AddRange(Directory.GetFiles(Application.dataPath, extension, SearchOption.AllDirectories));
    188.             fileList.AddRange(Directory.GetFiles(Path.GetFullPath("Packages"), extension, SearchOption.AllDirectories));
    189.         }
    190.  
    191.         foreach (string file in fileList)
    192.         {
    193.             string[] fileLines = File.ReadAllLines(file);
    194.  
    195.             for (int line = 0; line < fileLines.Length; line++)
    196.             {
    197.                 //find all instances of the string "guid: " and grab the next 32 characters as the old GUID
    198.                 if (fileLines[line].Contains("guid: "))
    199.                 {
    200.                     int index = fileLines[line].IndexOf("guid: ") + 6;
    201.                     string oldGUID = fileLines[line].Substring(index, 32); // GUID has 32 characters.
    202.                     if (oldGUIDToClassNameMap.ContainsKey(oldGUID) && newMonoScriptToIDsMap.ContainsKey(oldGUIDToClassNameMap[oldGUID]))
    203.                     {
    204.                         fileLines[line] = fileLines[line].Replace(oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
    205.                         output.AppendFormat("File {0} : Found GUID {1} of class {2}. Replaced with new GUID {3}.", file, oldGUID, oldGUIDToClassNameMap[oldGUID],
    206.                             newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
    207.                         if (fileLines[line].Contains("fileID: "))
    208.                         {
    209.                             index = fileLines[line].IndexOf("fileID: ") + 8;
    210.                             int index2 = fileLines[line].IndexOf(",", index);
    211.                             string oldFileID = fileLines[line].Substring(index, index2 - index); // GUID has 32 characters.
    212.                             fileLines[line] = fileLines[line].Replace(oldFileID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
    213.                             output.AppendFormat("Replaced fileID {0} with {1}", oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
    214.                         }
    215.  
    216.                         output.Append("\n");
    217.                     }
    218.                 }
    219.             }
    220.  
    221.             //Write the lines back to the file
    222.             File.WriteAllLines(file, fileLines);
    223.         }
    224.  
    225.         Debug.Log(output.ToString());
    226.     }
    227.  
    228.     private void ReplaceAssembly(string assemblyDefinitionFilePath)
    229.     {
    230.         Debug.LogFormat("Replacing scripts for assembly definition file {0}", assemblyDefinitionFilePath);
    231.         string asmdefDirectory = Path.GetDirectoryName(assemblyDefinitionFilePath);
    232.         string assemblyName = Path.GetFileNameWithoutExtension(assemblyDefinitionFilePath);
    233.         Assembly assemblyToReplace = CompilationPipeline.GetAssemblies().ToList().Find(assembly => assembly.name.ToLower().Equals(assemblyName.ToLower()));
    234.         string assemblyPath = assemblyToReplace.outputPath;
    235.         string assemblyFileName = Path.GetFileName(assemblyPath);
    236.         string[] assemblyFilePathInAssets = Directory.GetFiles("./Assets", assemblyFileName, SearchOption.AllDirectories);
    237.  
    238.         // save the guid/classname correspondence of the scripts that we will remove
    239.         Dictionary<string, int> oldClassNameToScriptOrderToMap = new Dictionary<string, int>();
    240.         if (assemblyFilePathInAssets.Length <= 0)
    241.         {
    242.             // Move all script files outside the asset folder
    243.             foreach (string sourceFile in assemblyToReplace.sourceFiles)
    244.             {
    245.                 string tempScriptPath = Path.Combine(TempSourceFilePath, sourceFile);
    246.                 Directory.CreateDirectory(Path.GetDirectoryName(tempScriptPath));
    247.                 if (!File.Exists(sourceFile))
    248.                     Debug.LogErrorFormat("File {0} does not exist while the assembly {1} references it.", sourceFile, assemblyToReplace.name);
    249.                 Debug.Log("will move " + sourceFile + " to " + tempScriptPath);
    250.                 // save the guid of the file because we may need to replace it later
    251.                 MonoScript monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(sourceFile);
    252.                 if (monoScript != null && monoScript.GetClass() != null)
    253.                 {
    254.                     oldGUIDToClassNameMap.Add(AssetDatabase.AssetPathToGUID(sourceFile), monoScript.GetClass().FullName);
    255.                     oldClassNameToScriptOrderToMap.Add(monoScript.GetClass().FullName, MonoImporter.GetExecutionOrder(monoScript));
    256.                 }
    257.  
    258.                 FileUtil.MoveFileOrDirectory(sourceFile, tempScriptPath);
    259.             }
    260.  
    261.             Debug.Log("Map of GUID/Class : \n" + string.Join("\n", oldGUIDToClassNameMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
    262.             Debug.Log("Map of Class/Order : \n" + string.Join("\n", oldClassNameToScriptOrderToMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
    263.  
    264.             // prepare the final path of the dll file
    265.             string finalAssemblyPath = Path.Combine(asmdefDirectory, assemblyFileName);
    266.  
    267.             string asmdefGuid = AssetDatabase.AssetPathToGUID(assemblyDefinitionFilePath.Replace(".\\", ""));
    268.  
    269.             filenameToGuidMap.Add(finalAssemblyPath.Replace(".\\", ""), asmdefGuid);
    270.             Debug.Log("Map of Filename/GUID Updated : \n" + string.Join("\n", filenameToGuidMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
    271.  
    272.             string tempAsmdefPath = Path.Combine(TempSourceFilePath, Path.GetFileName(assemblyDefinitionFilePath));
    273.             // Rename the asmdef meta file to the dll meta file so that the dll guid stays the same. Do it in two steps so that unity cache is update properly
    274.             Debug.Log("will copy " + assemblyDefinitionFilePath + ".meta" + " to " + finalAssemblyPath + ".metatmp");
    275.             FileUtil.CopyFileOrDirectory(assemblyDefinitionFilePath + ".meta", finalAssemblyPath + ".metatmp");
    276.             Debug.Log("will move " + assemblyDefinitionFilePath + " to " + tempAsmdefPath);
    277.             FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath, tempAsmdefPath);
    278.  
    279.             Debug.Log("will move " + assemblyPath + " to " + finalAssemblyPath);
    280.             FileUtil.MoveFileOrDirectory(assemblyPath, finalAssemblyPath);
    281.             Debug.Log("will move " + finalAssemblyPath + ".metatmp" + " to " + finalAssemblyPath + ".meta");
    282.             FileUtil.MoveFileOrDirectory(finalAssemblyPath + ".metatmp", finalAssemblyPath + ".meta");
    283.  
    284.             pathsOfAssemblyFilesInAssetFolder.Add(finalAssemblyPath);
    285.             pathsOfAssemblyFilesCreatedByUnity.Add(assemblyPath);
    286.  
    287.             // We need to remove .\ when using LoadAllAssetsAtPath
    288.             string cleanFinalAssemblyPath = finalAssemblyPath.Replace(".\\", "");
    289.  
    290.             // We need for the compilation to be over before continuing so we hook to this event
    291.             CompilationPipeline.compilationFinished += sender =>
    292.             {
    293.                 if (assembliesFilesToReplace.Count == 0)
    294.                     return;
    295.  
    296.                 // remove empty directories
    297.                 foreach (string sourceFile in assemblyToReplace.sourceFiles)
    298.                     RemoveEmptyDirectories(Path.GetDirectoryName(sourceFile));
    299.  
    300.                 Object[] assetsInAssembly = AssetDatabase.LoadAllAssetsAtPath(cleanFinalAssemblyPath);
    301.                 MonoScript[] assemblyObjects = assetsInAssembly.OfType<MonoScript>().ToArray();
    302.  
    303.                 Debug.LogFormat("Imported assembly {0} contains {1} mono scripts.", cleanFinalAssemblyPath, assemblyObjects.Length);
    304.  
    305.                 ReplaceIdOfAsset(cleanFinalAssemblyPath, filenameToGuidMap[cleanFinalAssemblyPath]);
    306.                 AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
    307.  
    308.                 // save the new GUID and file ID for the MonoScript in the new assembly
    309.                 Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap = new Dictionary<string, KeyValuePair<string, long>>();
    310.                 // for each component, replace the guid and fileID file
    311.                 for (int i = 0; i < assemblyObjects.Length; i++)
    312.                 {
    313.                     if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(assemblyObjects[i], out string dllGuid, out long dllFileId))
    314.                     {
    315.                         string fullClassName = assemblyObjects[i].GetClass().FullName;
    316.                         newMonoScriptToIDsMap.Add(fullClassName, new KeyValuePair<string, long>(dllGuid, dllFileId));
    317.                     }
    318.                 }
    319.  
    320.                 Debug.Log("Map of GUID/Class : \n" + string.Join("\n", oldGUIDToClassNameMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));
    321.                 Debug.Log("Map of Class/GUId:FileId : \n" + string.Join("\n", newMonoScriptToIDsMap.Select(pair => pair.Key + " : " + pair.Value.Key + " - " + pair.Value.Value).ToArray()));
    322.  
    323.                 ReplaceIdsInAssets(oldGUIDToClassNameMap, newMonoScriptToIDsMap);
    324.                 AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
    325.  
    326.                 // Replace execution orders
    327.                 if (assemblyObjects.Length > 0)
    328.                     ChangeScriptExecutionOrder.SetExecutionOrders(oldClassNameToScriptOrderToMap, false);
    329.  
    330.                 if (assemblyObjects.Length > 0)
    331.                 {
    332.                     Debug.LogFormat("Removing assembly {0} from the list of assemblies to replace", assemblyPath);
    333.                     assembliesFilesToReplace.Remove(assemblyFileName);
    334.                 }
    335.             };
    336.  
    337.             AssetDatabase.ImportAsset(cleanFinalAssemblyPath, ImportAssetOptions.ForceUpdate);
    338.         }
    339.         else
    340.         {
    341.             Debug.Log("Already found an assembly file named " + assemblyFileName + " in asset folder");
    342.         }
    343.     }
    344.  
    345.     /// <summary>
    346.     /// Recursively remove empty directories
    347.     /// </summary>
    348.     /// <param name="directory"></param>
    349.     private void RemoveEmptyDirectories(string directory)
    350.     {
    351.         if (Directory.Exists(directory) &&
    352.             Directory.GetFiles(directory).Length == 0 &&
    353.            Directory.GetDirectories(directory).Length == 0)
    354.         {
    355.             Debug.Log("Removing empty directory : " + directory);
    356.             Directory.Delete(directory, true);
    357.             string parentDirectory = Directory.GetParent(directory).FullName;
    358.             // delete directory meta file
    359.             File.Delete(directory + ".meta");
    360.             RemoveEmptyDirectories(parentDirectory);
    361.         }
    362.     }
    363.  
    364.     private void RevertReplaceAssemblies()
    365.     {
    366.         Debug.Log(pathsOfAssemblyFilesInAssetFolder.Count);
    367.         for (int i = 0; i < pathsOfAssemblyFilesInAssetFolder.Count; ++i)
    368.         {
    369.             Debug.Log("will move " + pathsOfAssemblyFilesInAssetFolder[i] + " back to " + pathsOfAssemblyFilesCreatedByUnity[i]);
    370.             FileUtil.MoveFileOrDirectory(pathsOfAssemblyFilesInAssetFolder[i], pathsOfAssemblyFilesCreatedByUnity[i]);
    371.         }
    372.  
    373.         if (Directory.Exists(TempSourceFilePath))
    374.         {
    375.             string[] scriptFilesInTempDir = Directory.GetFiles(TempSourceFilePath, "*", SearchOption.AllDirectories);
    376.             foreach (string scriptFileInTempDir in scriptFilesInTempDir)
    377.             {
    378.                 // remove the temp directories prefix and the directory separator character
    379.                 string originalScriptFilePath = scriptFileInTempDir.Substring(TempSourceFilePath.Length + 1);
    380.                 Debug.Log("will move " + scriptFileInTempDir + " back to " + originalScriptFilePath);
    381.                 FileUtil.MoveFileOrDirectory(scriptFileInTempDir, originalScriptFilePath);
    382.             }
    383.         }
    384.  
    385.         pathsOfAssemblyFilesInAssetFolder = new List<string>();
    386.         pathsOfAssemblyFilesCreatedByUnity = new List<string>();
    387.     }
    388.  
    389.     #endregion
    390. }
    391.