Adding Git Dependencies to Unity Packages
As part of my Unity workflow, I create custom packages to help make some of my functions more reusable in the future or to help make setup smoother. While managing these packages, I looked for a better way to import dependencies into the project automatically.
For some background into this package system, I currently use a local Verdaccio server to manage dependencies between these custom packages. Still, I needed a way to automatically import external packages (specifically from GitHub). I created a function to import GitHub unity packages by automatically overriding the package manifest file in the projects folder during installation.
As an example, I will add the unity-toolbar-extender package from the following tutorial for this example. This tutorial script will add a dropdown bar to the right of the play button in the editor to allow for switching scenes.
The first method we need to create in the package is to install the package into the project. I have mine set up as a static method, as it will be reusable in a static class. I also passed through the package name and URL, but this is optional if you plan to use this method only once. If that’s the case, replace the variable names with the string.
public static void AddPackage(string packageName, string gitURL)
The first lines of the method get the local path to the package manifest and save all the file content to a string called jsonString. The path used here assumes that this code resides within a package in the project packages folder, so the path must be updated if the code is elsewhere.
var path = Path.Combine(Application.dataPath, "../Packages/manifest.json");
var jsonString = File.ReadAllText(path);
Next, we parse through the file output and look for the last instance of the “}” character. This should give us roughly the previous package in the manifest. With that index, we want to take the substring from the beginning to this point. From that substring, we then want to take the index of the last ” character, which will give us the index of the end of the previous package in the JSON. Like the methods above, we want to save the substring.
int indexOfLastBracket = jsonString.IndexOf("}");
string dependenciesSubstring = jsonString.Substring(0, indexOfLastBracket);
var endOfLastPackage = dependenciesSubstring.LastIndexOf("\"");
We then take the original package string and add a substring at the index of the last package. This overrides the original string with the formatted substring.
jsonString = jsonString.Insert(endOfLastPackage + 1,
$", \n \"{packageName}\": \"{gitURL}\"");
Finally, we take the original path and write the modified JSON file back to it. We want to call the package manager to resolve any modified packages, which will install the package to the project.
File.WriteAllText(path, jsonString);
UnityEditor.PackageManager.Client.Resolve();
The final method should look something like this:
/// <summary>
/// Adds a package to the Unity package manifest file.
/// </summary>
/// <param name="packageName">The name of the package to add.</param>
/// <param name="gitURL">The git URL of the package.</param>
public static void AddPackage(string packageName, string gitURL)
{
var path = Path.Combine(Application.dataPath, "../Packages/manifest.json");
var jsonString = File.ReadAllText(path);
int indexOfLastBracket = jsonString.IndexOf("}");
string dependenciesSubstring = jsonString.Substring(0, indexOfLastBracket);
var endOfLastPackage = dependenciesSubstring.LastIndexOf("\"");
string oldValue = jsonString.Substring(endOfLastPackage, indexOfLastBracket - endOfLastPackage);
jsonString = jsonString.Insert(endOfLastPackage + 1,
$", \n \"{packageName}\": \"{gitURL}\"");
File.WriteAllText(path, jsonString);
UnityEditor.PackageManager.Client.Resolve();
}
In addition to the add package function, we want to add a method to check if the package is installed. Like the add method above, this will be done by reading the manifest JSON file to see if the package name is included.
/// <summary>
/// Checks if a package is installed in the Unity package manifest file.
/// </summary>
/// <param name="packageName">The name of the package to check.</param>
/// <returns>True if the package is installed, otherwise false.</returns>
public static bool CheckPackageInstalled(string packageName)
{
var path = Path.Combine(Application.dataPath, "../Packages/manifest.json");
var jsonString = File.ReadAllText(path);
return jsonString.Contains(packageName);
}
In my projects, I have added both methods to a static class within a utility package to reuse this code for multiplGitHubub dependencies in other projects. However, as long as this code is accessible from the package, it should work fine.
Now, within the package, we wanted to add code similar to the following:
[InitializeOnLoadMethod]
public static void Add()
{
bool installed = CheckPackageInstalled("com.marijnzwemmer.unity-toolbar-extender");
if (!installed)
{
AddPackage("com.marijnzwemmer.unity-toolbar-extender",
"git+https://github.com/marijnz/unity-toolbar-extender.git");
}
}
Using the InitializeOnLoadMethod attribute, this class will load and check for the package. As long as the package is installed, the project manifest won’t be updated (if the package gets added a second time, the engine will throw an error).
If you have followed the tutorial and used the same package, there is a new issue. We have the code to import a package, but the package doesn’t compile because we’re missing the reference. The auto-import function won’t fire until the dependency is taken care of, which ultimately defeats the purpose of this workaround. This may not be the case for all packages, but in this instance, using the directive for the UnityToolbarExtender causes compiler errors.
Assuming the code is already set up as a custom package, you must ensure the assembly definition file (.asmdef) is correctly set up. First, ensure that the GitHub package is referenced in the assembly.
In the same assembly definition file, we also want to add the following version defines:
While the defined name is up to you, the important part is ensuring that the package and version are set correctly. This allows the compiler to check the script's preprocessor directive (TOOLBAR) and conditionally compile the code around that. It can also be used to set code to compile with specific versions.
In the toolbar tutorial code, the preprocessor directive should be added to the using statement:
#if TOOLBAR
using UnityToolbarExtender;
#endif
As well as the reference to the toolbar extender:
static SceneSelectionToolbar()
{
LoadFromPlayerPrefs();
#if TOOLBAR
ToolbarExtender.LeftToolbarGUI.Add(OnToolbarGUI);
#endif
EditorSceneManager.sceneOpened += HandleSceneOpened;
}
That should be it! The code should now be working, and if you import the package, the dependencies should also be installed.
While this may not be the most optimized code, I am currently running this within the Editor section of my package, which won’t impact any gameplay performance.
I hope this helps! I was looking for a method similar to this to manage my packages, so this resource will let you do the same.