Search Unity

[SOLVED] Removing packages in an editor script

Discussion in 'Package Manager' started by nickfourtimes, Jun 18, 2019.

  1. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    I'm looking to remove several of the default packages from the Package Manager via an editor script – they get re-added every time we update Unity, and we're legally required to remove them for a project.

    However, the following doesn't quite work (assume the static method is called from an editor button):
    Code (CSharp):
    1. using UnityEditor.PackageManager;
    2.  
    3. [...]
    4.  
    5. static public void RemovePackages() {
    6.     Client.Remove("com.unity.ads");
    7.     Client.Remove("com.unity.analytics");
    8.     Client.Remove("com.unity.multiplayer-hlapi");
    9.     Client.Remove("com.unity.purchasing");
    10.     Client.Remove("com.unity.xr.legacyinputhelpers");
    11.  
    12.     return;
    13. }
    In this case, it will remove the first package and then immediately start recompiling the project, and therefore either miss or completely stop before running the subsequent Remove() calls. The end result is that I have to press the button (in this case) five times to remove these five packages. Is there any way to maybe halt the immediate reimport process until these five commands are issued?
     
  2. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    Actually, adding in a few Debug.Log() calls between each Remove() method, it looks like it does hit each of the five calls, but it only removes the last package, and the others are unaffected.
     
  3. okcompute_unity

    okcompute_unity

    Unity Technologies

    Joined:
    Jan 16, 2017
    Posts:
    756
    Hi @nickfourtimes ,

    I'm sorry, our documentation does not make it clear that you should not make concurrent calls (we have a task in our backlog to fix the documentation). The behaviour will be random. Each call reads the manifest in memory and rewrites the validated result at the end. The last call to finish will overwrite any previous update. You need to wait for a call to succeeds before proceeding with another one. We have plans to offer a batch method in our API.

    Regards,

    Pascal
     
  4. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    Got it, thanks. So until the batch call is available, I should (for example) just have one button per package to remove, or simply remove them manually in-editor? I suppose I could also write an external script that just edits the manifest.json file and removes any line that it doesn't like...
     
  5. okcompute_unity

    okcompute_unity

    Unity Technologies

    Joined:
    Jan 16, 2017
    Posts:
    756
    Something like this:

    Code (CSharp):
    1. using System;
    2. using UnityEditor;
    3. using UnityEditor.PackageManager.Requests;
    4. using UnityEditor.PackageManager;
    5. using UnityEngine;
    6.  
    7. namespace Unity.Editor.Example {
    8.    static class AddPackageExample
    9.    {
    10.        static RemoveRequest Request;
    11.  
    12.        Queue<string> packages = new Queue<string>();
    13.  
    14.  
    15.        [MenuItem("Window/Remove packages")]
    16.        static void Remove()
    17.        {
    18.            packages.Enqueue("package1");
    19.            packages.Enqueue("package2");
    20.            packages.Enqueue("package3");
    21.            Request = Client.Remove(packages.DeQueue());
    22.            EditorApplication.update += Progress;
    23.        }
    24.  
    25.        static void Progress()
    26.        {
    27.            if (Request.IsCompleted)
    28.            {
    29.                if (Request.Status == StatusCode.Success) {
    30.                    Debug.Log("Removed: " + Request.Result.packageId);
    31.                    if (packages.Count > 0) {
    32.                        Request = Client.Remove(packages.DeQueue());
    33.                    } else {
    34.                        EditorApplication.update -= Progress;
    35.                    }
    36.                }
    37.                else if (Request.Status >= StatusCode.Failure) {
    38.                    Debug.Log(Request.Error.message);
    39.                    EditorApplication.update -= Progress;
    40.                }
    41.            }
    42.        }
    43.    }
    44. }
    45.  
    Note: I modified the example in the documentation to use a Queue instead. I haven't tested the code. Use at your own risk :p. This is just to give you an idea. Also, depending on the size of the project, the number of packages to remove, the operation above could take a bit of time.

    Hope this helps,

    Pascal
     
    crekri and nickfourtimes like this.
  6. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    Ah, thanks for the code. Looks like it still only removes one of the packages, but I'll keep poking at this to see what I can get working.
     
  7. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    Hm. Yeah it seems like now it works on the first two packages, but then there never seems to be a callback when the second one sets Request.IsCompleted.

    (I just had to make some small changes to the code above: making the "packages" variable static; using packages.Dequeue() instead of .DeQueue(); and using Request.PackageIdOrName instead of Request.Result.packageId).
     
  8. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    Okay! With some extra help I've added (Un)LockReloadAssemblies to the code and it works! It removes all the packages first and then does a full recompile. The code I'm using is as follows:

    Code (CSharp):
    1.  
    2.  
    3. static RemoveRequest s_RemRequest;
    4. static Queue<string> s_pkgNameQueue;
    5.  
    6. // this is called via a UI button
    7. static public void StartRemovingBadPackages() {
    8.     s_pkgNameQueue = new Queue<string>();
    9.     s_pkgNameQueue.Enqueue("com.unity.ads");
    10.     s_pkgNameQueue.Enqueue("com.unity.analytics");
    11.     s_pkgNameQueue.Enqueue("com.unity.multiplayer-hlapi");
    12.     s_pkgNameQueue.Enqueue("com.unity.purchasing");
    13.     s_pkgNameQueue.Enqueue("com.unity.xr.legacyinputhelpers");
    14.  
    15.     // callback for every frame in the editor
    16.     EditorApplication.update += PackageRemovalProgress;
    17.     EditorApplication.LockReloadAssemblies();
    18.  
    19.     var nextRequestStr = s_pkgNameQueue.Dequeue();
    20.     s_RemRequest = Client.Remove(nextRequestStr);
    21.  
    22.     return;
    23. }
    24.  
    25.  
    26. static void PackageRemovalProgress() {
    27.     if (s_RemRequest.IsCompleted) {
    28.         switch (s_RemRequest.Status) {
    29.         case StatusCode.Failure:    // couldn't remove package
    30.             Debug.LogError("Couldn't remove package '" + s_RemRequest.PackageIdOrName + "': " + s_RemRequest.Error.message);
    31.             break;
    32.  
    33.         case StatusCode.InProgress:
    34.             break;
    35.  
    36.         case StatusCode.Success:
    37.             Debug.Log("Removed package: " + s_RemRequest.PackageIdOrName);
    38.                 break;
    39.         }
    40.  
    41.         if (s_pkgNameQueue.Count > 0) {
    42.             var nextRequestStr = s_pkgNameQueue.Dequeue();
    43.             Debug.Log("Requesting removal of '" + nextRequestStr + "'.");
    44.             s_RemRequest = Client.Remove(nextRequestStr);
    45.  
    46.         } else {    // no more packages to remove
    47.             EditorApplication.update -= PackageRemovalProgress;
    48.             EditorApplication.UnlockReloadAssemblies();
    49.         }
    50.     }
    51.  
    52.     return;
    53. }
     
  9. okcompute_unity

    okcompute_unity

    Unity Technologies

    Joined:
    Jan 16, 2017
    Posts:
    756
    Nice!

    Though, are you sure you need the
    (Un)LockReloadAssemblies
    ? I'm surprised that you need it. There should be a domain reload triggered after each package is removed.

    Pascal
     
  10. nickfourtimes

    nickfourtimes

    Joined:
    Oct 13, 2010
    Posts:
    219
    In my case, it was necessary – without the lock/unlock, it would:
    1. Start removing the first package
    2. Return with s_remRequest.IsCompleted when it was finished removing the first package
    3. Start removing the second package
    4. Never return with s_remRequest.IsCompleted after removing the second package, and therefore never proceed with the subsequent packages.
    Once I added the lock/unlock, it was able to remove all of the packages as requested. Not sure if that's the intended behaviour but it's what I observed... this is on Unity 2019.1.6f1.
     
  11. okcompute_unity

    okcompute_unity

    Unity Technologies

    Joined:
    Jan 16, 2017
    Posts:
    756
    I believe that you need to wrap the data into a class and make it serializable for it to survive domain reload. Using a combination of
    [Serializable]
    and
     [SerializableField]
    attribute.

    Example:

    Code (CSharp):
    1.     [Serializable]
    2.     public class MyClass
    3.     {
    4.         [SerializeField]
    5.         private RemoveRequest m_RemoveRequestl;
    6.         [SerializeField]
    7.         private Queue<string>  m_Queue;
    8.     }
    9.  
    Again, I haven't tried this code. Just giving you a potential direction from my personal experience scripting in Unity (I'm not an expert) :p

    Regards,

    Pascal
     
  12. o_ourson

    o_ourson

    Joined:
    Jun 17, 2014
    Posts:
    3
    Hi there !
    Any update on the batch method for removing multiples packages ?
     
    ExcaliburGames and KBaker46 like this.
  13. o_ourson

    o_ourson

    Joined:
    Jun 17, 2014
    Posts:
    3
    Also it would be nice to have
    Client.Remove()
    await
    able.
     
    Last edited: Apr 29, 2020
  14. Idle_Splash

    Idle_Splash

    Joined:
    Jun 4, 2022
    Posts:
    5
    As manifest.json is a json file a simpler approach is to use Json.net like this:

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.IO;
    5. using System.Linq;
    6. using Newtonsoft.Json.Linq;
    7. using Scripts.Editor.AndroidManifest;
    8. using UnityEngine;
    9.  
    10. public static class PackageManagerModifier
    11. {
    12.  
    13.     public static void RemovePackage(string key)
    14.     {
    15.         var path =  Application.dataPath.Replace("Assets", "Packages");
    16.         path = Path.Combine(path, "manifest.json");
    17.        
    18.        
    19.        
    20.         JObject jo = JObject.Parse(File.ReadAllText(path));
    21.         JObject header = (JObject)jo.SelectToken("dependencies");
    22.  
    23.         bool found = false;
    24.         foreach (JProperty property in header.Properties())
    25.         {
    26.             if (property.Name.ToLower() == key.ToLower())
    27.             {
    28.                 found = true;
    29.                 break;
    30.             }
    31.         }
    32.        
    33.         if (found)
    34.         {
    35.             header.Property(key).Remove();
    36.             var json = jo.ToString();
    37.        
    38.             //save manifest.json
    39.             File.WriteAllText(path, json);          
    40.             Debug.Log($"package {key} deleted successfully");
    41.         }
    42.         else
    43.         {
    44.             Debug.Log($"package {key} not found in manifest.json");
    45.         }
    46.  
    47.     }
    48.  
    49.     public static void AddPackage(string key, string value)
    50.     {
    51.        
    52.        
    53.         var path =  Application.dataPath.Replace("Assets", "Packages");
    54.         path = Path.Combine(path, "manifest.json");
    55.        
    56.         JObject jo = JObject.Parse(File.ReadAllText(path));
    57.         JObject header = (JObject)jo.SelectToken("dependencies");
    58.  
    59.         bool found = false;
    60.         foreach (JProperty property in header.Properties())
    61.         {
    62.             if (property.Name.ToLower() == key.ToLower())
    63.             {
    64.                 found = true;
    65.                 break;
    66.             }
    67.         }
    68.  
    69.         if (!found)
    70.         {
    71.             header.Add(key, value);
    72.             var json = jo.ToString();
    73.        
    74.             //save manifest.json
    75.             File.WriteAllText(path, json);
    76.        
    77.             Debug.Log($"package {key} added successfully");          
    78.         }
    79.         else
    80.         {
    81.             Debug.Log($"Package {key} was already present in the manifest.json");
    82.         }
    83.  
    84.     }
    85. }
    86.  
    I've tested it and it can simply remove/add 5 packages at once.
     
  15. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    That doesn't immediately trigger an import of the packages, however.

    Also, this is a rather old thread. In 2021.2 and newer versions, you can use PackageManager.Client.AddAndRemove. That will let you add and/or remove any number of dependencies in your project manifest in a single operation. (Unfortunately not available in 2020.3 and older.)
     
    jason_yak likes this.
  16. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,442
    is there anything for older unity versions? (to remove/add packages easily, even if need to use Reflection for something internal)
     
  17. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Did not know this was added, will try this out. This sounds much better.

    I've been using a different solution where I've had to create an elaborate queue to process multiple packages. To achieve it I serialise the array of package name strings using EditorPrefs so the packages names persist through a client recompile. I call Client.Add and force it to compile using CompilationPipeline.RequestScriptCompilation, and then using the callback [UnityEditor.Callbacks.DidReloadScripts] I wait until compiling is done then move onto the next package in the array. It works, but it's ugly and I'm guessing won't work with Unity Cloud compiling (haven't tried it though). But hopefully I can remove this now and use this combo AddAndRemove method.

    For the approach that @Idle_Splash is using, I would have thought calling this should trigger a compile after the manifest file is manually updated:

    CompilationPipeline.RequestScriptCompilation( RequestScriptCompilationOptions.CleanBuildCache );
     
  18. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    I was just wondering if you have any clues on how to wait on the AddAndRemoveRequest, it's not working for me.

    I've tried the following, and to confirm the AddAndRemove method is working for me and adding multiple packages to the manifest, but my compiler is currently broken. The async request is not triggering a completion or the async await is bailing out?? any ideas what I'm doing wrong? thanks.

    Code (CSharp):
    1. // gets called by an editor menu option
    2. private static async void Compile () {
    3.     await AddPlatformPackages( getPackagesArray() ); // <-- returns a list of package paths
    4.     Debug.Log( $"packages added" ); // <--- never called
    5.     // continue with compile actions....
    6. }
    7.  
    8. private static async Task AddPlatformPackages ( string [] packageNames ) {
    9.     Debug.Log( $"AddPlatformPackages" );
    10.     var request = Client.AddAndRemove( packageNames, new string[] { } );
    11.     while ( !request.IsCompleted )
    12.         await Task.Yield();
    13. }
     
  19. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    At the very least the Client.AddAndRemove has allowed me to remove the crazy queue setup I needed to setup to process mulitple packages so thanks for that tip off. But I have no idea how to wait for the request to complete. It's not working out for me.
     
  20. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    Hi @jason_yak,

    I don't exactly know why it's not working for you, your code snippet works for me. Tested on 2021.3.15f1.

    I can venture a guess. When adding packages via the API, there is a step where the AssetDatabase is refreshed with synchronous import (ie blocking), then all scripts in the packages are compiled and this will lead to a Domain Reload, which trashes the currently-executing .NET code and reloads the newly compiled assemblies. That means if you have an async process currently ongoing, it is simply interrupted right there.

    That said, Package Manager requests are serializable, so if you store it in a field of an object instance that will be serialized and restored (e.g. an EditorWindow, a ScriptableObject instance, a MonoBehaviour on your current scene...) it will be restored back after the domain reload. You can use [InitializeOnLoad] to register a callback that monitors the request state and kicks off the remainder of your process. ("// continue with compile actions....")

    I would normally have expected your code to print "packages added" and on the next time you "await", kick off the synchronous import process I mentioned. It must be down to the task context which manages the awaited tasks; maybe it does not resume your task soon enough (compared to the main application loop that ticks very frequently).
     
  21. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Thanks for the reply! That’s good to hear that it works for you. First thought is that the class the code is executed from is most likely static without a serialised instance, I think it’s just a series of static methods running triggered from an editor menu option which would explain the lack of any resumed deserialisation execution. I didn’t know that would happen in some situations. I’ll take a look at the class and change it to create an instance that can be serialised.

    Will report back, thanks!
     
  22. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    Just to clarify, I think the problem is just that when using async/await with
    Task.Yield()
    , it will cause that task to be eventually resumed later, but that "later" is not synchronized/polled on every iteration of the main editor application loop, so your code is missing the opportunity to see `IsCompleted` true before the synchronous Asset database refresh and ensuing compilation and domain reload. That's why breaking down your code in two parts is better. Awaiting the Package Manager request still makes sense to detect and handle an error, should any arise (e.g. no network, etc.)
     
  23. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Hi @maximeb_unity thanks for the added info, but I'm still unable to find a working solution. I've removed using an await/async approach and instead I'm using the [InitializeOnLoadMethod] callback. I have now also changed the class that initiates the call to add the packages to a scriptableobject, I create an instance of it, I call the add packages method and store the returned AddAndRemoveRequest as a serialized field of the scriptableobject class instance, the initialize callback event fires but the instance of the class is null.

    I can not find a way to resume any code after the domain reload. Do you have a example of how you were able to resume code execution after the domain reload? no instances or fields survive the domain reload for me and the only workaround I've been able to use for years is to a hack to persistently store a temporary value in EditorPrefs. I just can't for the life of me work out how it's possible to use an AddAndRemoveRequest because the instance never survives a domain reload for me.
     
  24. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    To provide more info on the sequence of events, a [MenuItem( "Compile" )] option triggers a static method, this creates the scriptableobject class instance called AppCompiler, to try and keep this instance in scope I store the instance in a static field. The AppCompiler instance calls the AddAndRemove packages method, the returned request gets stored as a field on the AppCompiler instance, but app compiler instance doesn't surivive the domain reload. How can I store the AddAndRemoveRequest so it survives the domain reload?
     
  25. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Still trying to solve this, I've read up about how domain reloading works and found that when domain reloading happens all static variables are cleared. I also found that 'maybe' if I use the HideFlag.DontSave that it might help the ScriptableObjecct instance survive a domain reload.

    So I'm setting the hide flag in the OnEnable event of the SO instance, and then went to change how the AppCompiler ScriptableObject is stored so it's not stored in a static variable, but then the InitializeOnLoadMethod only calls back to static methods so there's no way for me to target the SO instance even if it does survive the domain reload.

    If all static variables are cleared in a domain reload how would a static callback method target a non static instance?
     
  26. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Sorry for jamming up the thread everyone, just reporting back that I've had some success with not using InitializeOnLoadMethod, there was no way I could use any callbacks and be able to target an SO instance.

    But after I stopped storing the instance in a static variable, I confirmed the instance itself was suriving the domain reload without having to use hide flags either, and instead of using a static callback to push the compiler through steps, I'm instead using the SO instance's OnEnable event to progress the compiler through steps. So it can now check the AddAndRemoveRequest is completed before moving to the next compiler step.

    edit: it's not really any better than using EditorPrefs to progress a compiler after a domain reload for my situation however, possibly worse because OnEnable doesn't seem to be called on the SO instance unless the Unity Editor has focus, it get's stuck easily.
     
    Last edited: Jan 31, 2023
  27. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    Hi @jason_yak,

    You might have some chance by using a class that derives from ScriptableSingleton<T>, e.g.
    class MyCompilationHelper : ScriptableSingleton<MyCompilationHelper>
    . You can then store data in it (like the request instance) and have access to it from your
    [InitializeOnLoad]
    static constructor/
    [InitializeOnLoadMethod]
    static method. Ideally, you should avoid long-running code executing synchronously in those locations. This doesn't seem to be your case, though, if you use async/await (and don't await in the top call).

    You can also register a callback to be invoked on every application loop iteration (EditorApplication.update(<callback>)) where it could look at whatever state the compilation helper is in and trigger the next step if needed.
     
    jason_yak likes this.
  28. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Oh this is great! Thank you for letting me know about this, this will be so handy for having a formal base class for an app compiler and some editor plugin classes I have that trigger a domain reload, and then need to carry on. Thanks for the tip.
     
    maximeb_unity likes this.
  29. AnandMWio

    AnandMWio

    Joined:
    Sep 11, 2023
    Posts:
    1
    @jason_yak did you managed to ignore plugins by add and remove and get back to the loop to start the build process?
    do you have any updates on you last code snippet?
     
  30. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Yes, so the problem with most approaches is that when you compile it triggers a domain reload, and when this happens it kills all variables that are not serialised. So you can't use static vars to store something, for a time I couldn't use async requests values to check if the package add had completed "because" the way I was storing the returned request value was not being serialised.

    So one of the long running approaches that people have used to get around this is to store values in EditorPrefs before a domain reload, then after the reload recall their data and carry on, but this is a really hacky and average way to solve it, you'd have to convert data types to serialise the data, it's a much uglier way of going about it.

    But the class that @maximeb_unity had brought to my attention ScriptableSingleton<T> is the perfect solution for a compiler script. So I use this class as the base class for my compiler script, any local variables that you store on the compiler instance that are flagged to be serialised will survive a domain reload, like this for example:

    [SerializeField]
    public AddAndRemoveRequest PackagesAddRequest;

    So when you trigger a package add, store the return of Client.AddAndRemove into ^ a variable that is serialised, then after the domain reload happens, do something like this to kick off a way to check that the request has completed:

    Code (CSharp):
    1. [UnityEditor.Callbacks.DidReloadScripts]
    2. private static void OnScriptsReloadBegan () {
    3.     EditorApplication.update += CheckScriptReloadComplete;
    4. }
    5.  
    6. private static void CheckScriptReloadComplete () {
    7.     if ( EditorApplication.isCompiling || EditorApplication.isUpdating ) {
    8.         return;
    9.     }
    10.     var packagesAddRequest = AppCompiler.instance.PackagesAddRequest;
    11.     if ( packagesAddRequest != null ) {
    12.         Debug.Log($" - package add complete: {packagesAddRequest.IsCompleted}");
    13.         if ( !packagesAddRequest.IsCompleted ) {
    14.             return;
    15.         }
    16.         AppCompiler.instance.PackagesAddRequest = null;
    17.         // continue with compiling now that the packages are added....
    18.     } else {
    19.         Debug.Log( $"packagesAddRequest is null, can't verify if packages are added, bailing out" );
    20.     }
    21.     EditorApplication.update -= CheckScriptReloadComplete;
    22. }
    This approach has been a game changer for my compiler scripts.
     
    Last edited: Jan 28, 2024
    WilsonCWong and maximeb_unity like this.
  31. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    (posted answer above, just sending this so you get notified as a reply)