Setting define symbols within a custom package before the rest of the package compiles

Hello Unity community,

I’m currently building a custom Unity package hosted on GitHub that has dependencies on other GitHub-based packages. Since Unity doesn’t support adding direct GitHub package links as dependencies in the package.json, I’ve been working on a solution to dynamically install these packages whenever the main package loads.

To avoid compilation errors due to missing namespaces from those dependencies, I’ve wrapped the dependent parts of my code in custom define symbols (like #if MY_PACKAGE_INSTALLED). However, I’m running into an issue where these define symbols are not recognized during the initial compilation, causing Unity to throw namespace and assembly errors.

The issue:

  • I can’t find a way to make Unity set these define symbols before the rest of the package compiles.
  • When the package loads, Unity compiles everything at once, including the code inside #if MY_PACKAGE_INSTALLED blocks, even though the define symbols haven’t been set yet. This causes the compilation to fail because the package dependencies aren’t available, and the directives are unknown at compile time.

What I’ve Tried:

I’ve tried using a PackageDefineChecker script to check if the necessary dependencies are installed (by inspecting the manifest.json file) and then dynamically set the define symbols. Here’s an example of what I tried using a single package as an example:

using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;

namespace MyPackage.Editor
{
    [InitializeOnLoad]
    public class PackageDefineChecker
    {
        static PackageDefineChecker()
        {
            CheckAndSetDefines();
        }

        private static void CheckAndSetDefines()
        {
            string manifestPath = Path.Combine(Application.dataPath, "../Packages/manifest.json");
            if (!File.Exists(manifestPath))
            {
                Debug.LogError("Manifest file not found.");
                return;
            }

            string manifest = File.ReadAllText(manifestPath);
            bool hasMyDependency = manifest.Contains("com.mygithub.package");

            SetDefineSymbol("MY_PACKAGE_INSTALLED", hasMyDependency);
        }

        private static void SetDefineSymbol(string symbol, bool shouldSet)
        {
            var currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
            var definesList = new List<string>(currentDefines.Split(';'));

            if (shouldSet && !definesList.Contains(symbol))
            {
                definesList.Add(symbol);
            }
            else if (!shouldSet && definesList.Contains(symbol))
            {
                definesList.Remove(symbol);
            }

            PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, string.Join(";", definesList.ToArray()));
        }
    }
}

In my package, I conditionally wrap the code with:

#if MY_PACKAGE_INSTALLED
using MyPackageNamespace;

namespace MyPackage.Editor
{
    public class MyCustomEditor : Editor
    {
        // Code dependent on the package
    }
}
#endif

The problem is that Unity tries to compile everything at once, including the code inside the #if MY_PACKAGE_INSTALLED block, before the PackageDefineChecker runs and sets the define symbol. As a result, Unity throws namespace errors because the dependent package hasn’t been loaded yet.

Question:
Is there a way to set custom define symbols before the rest of the package is compiled, or is there another way to handle package dependencies dynamically without running into these compilation errors?

Any advice or alternative approaches would be greatly appreciated. Thanks in advance!

The appropriate way to conditionally compile code based on the presence of another package is to use version defines in the assembly definition for which the code is being compiled.

3 Likes

This sounds like an awful lot of effort that requires large swaths of code to be enclosed in preprocessor symbols - means: you always have to test at least two code paths, I bet you’ll often get compile errors when you toggle the symbol.

Not sure how much you researched this. There are simpler, possibly more workable alternatives to your approach:

  1. a guideline how to set up project and package repositories, possibly providing a scripted installation of said packages via a menu command that runs the appropriate git commands if this is meant to support a team
  2. setting up your own package registry, users only need to add that registry to their Package Manager settings
  3. using one of those repositories that resolve git url package dependencies like this one - these rely on catching errors however so the user may see errors in the Console and repeated recompile attempts until full resolution
  4. create a simple installer package that, when installed, will install all other packages that it reads from a text file (included or via URL). That package may uninstall itself at the end.

I’d love to see a clean way of resolving git dependencies. If you manage to make this work to satisfaction please post about this! :wink:

PS on #1: The idea here is to set up projects as follows:

../ProjectWorkspaceFolder/
../ProjectWorkspaceFolder/Projects/
../ProjectWorkspaceFolder/Packages/

Ie you may have:

../ProjectWorkspaceFolder/Projects/YetAnotherGame/
../ProjectWorkspaceFolder/Packages/AwesomePackageA/
../ProjectWorkspaceFolder/Packages/AwesomePackageB/

You add each locally cloned package via Package Manager “from disk”. Then you edit manifest.json to make the paths relative like "../../Packages/AwesomePackageA/package.json"

You could use the absolute paths if you know everyone has their project on the same path and drive. (not recommended)

Commit the changes and everyone working on the project first pulls the packages, then the project, and they can start working.

Alternative: you pull those package into the project under /Packages via git subtree/submodule.

Note: packages “added from disk” are NOT read-only! Same with embedded packages located under the project’s Packages folder.

Thanks for your insightful answer, this indeed seems like the right way to go, but I think I need a little more than that.

Since I want to check the presence of a package coming from github (installed on the manifest using the github url to the repo), I don’t seem to find any expression that would fit. It seems the only accepted values in the expression field are versions, but setting something generic like 1.0.0 (which outcomes to x >= 1.0.0) isn’t working if the dependency is not installed, probably because it is not defined by its version but github url (?).

Extra things I tried
Before writing this answer, I went and tried a couple things using the provided documentation, but those did not resolve the issue.

  • Setting expression to the name of my package
  • Setting a wildcard in the expression

I think the key thing to keep in mind here is the specificity that we’re working with packages coming from github, that are not stored using version defines in the manifest but github urls ( when installed ).

I might be missing something but I went through the docs you provided and there doesn’t seem to be a solution for that use case.

“Resource” is where you select the package ID like com.example.mypackage, and “expression” is where you express versions. Both these properties should be specified in your UPM package’s package.json file. There isn’t anything out of the ordinary about your case here, just use those values.

For example, if your optional package had a package.json like

{
  "name": "com.mycompany.mypackage",
  "version": "1.0.0"
}

The asmdef version defines section would look like:

    "versionDefines": [
        {
            "name": "com.mycompany.mypackage",
            "expression": "1.0.0",
            "define": "MY_SYMBOL_TO_DEFINE"
        }
    ]

I forgot, there is another alternative:

  • create a package that, when installed, will install all the packages of the actual target package

Basically you’d redirect users to add the “installer” package to the project, which then gets compiled and has a InitializeOnLoadMethod which uses Package Manager API to install the provided list of git packages in order.

You’d only have to manage the list of packages (git urls) to install in the installer package every time you add a new dependency or remove one, or when the git url changes.

1 Like

Yeah, this is the case in my current setup.

I think the issue is less about the setup in the asmdef file but more in the order in which I’m trying to make things work.

Let’s run through the current setup:

My main package.json

{
   "name": "com.mycompany.mypackage",
   "version": "1.1.0",
   // ... other fields
}

The package.json does not define the dependencies directly, since we’re working with github packages.

My dependency package.json

{
   "name": "com.mycompany.mydependency",
   "version": "1.0.0",
   // ... other fields
}

Asmdef of main package

{
    "name": "MyCompany.MyPackage.Editor",
    "rootNamespace": "MyCompany.MyPackage.Editor",
    "references": [
        "GUID:some_guid_for_dependency",
    ],
    "includePlatforms": [
        "Editor"
    ],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [
        "MY_DEPENDENCY_INSTALLED"
    ],
    "versionDefines": [
        {
            "name": "com.mycompany.mydependency",
            "expression": "1.0.0",
            "define": "MY_DEPENDENCY_INSTALLED"
        },
    ],
    "noEngineReferences": false
}

What happens during installation

  1. When I install my main package from git url it installs fine
  2. Unity notices missing dependencies (i suppose) and throws assembly errors for missing directives for code that is within the #if MY_DEPENDENCY_INSTALLED block.

So the define symbol isn’t working / recognized (?), and my guess is that it is because the dependencies are not installed at all yet so the contraint and version defines do not work because they can’t find the com.mycompany.mydependency package. I was hoping it could avoid errors if the presence of the package is not there but I don’t think that’s the way those asmdef constraints and version defines was meant to work.

I think I might be overcomplicating things here. @CodeSmile 's last suggestion might actually be a much simpler way to go about this. But I am still curious if this remains possible with the asmdef configuration.

The C# compiler only compiles code for which the #if condition is met, it doesn’t willy nilly decide to compile things inside the conditional block otherwise. Either you’ve mistakenly left MY_DEPENDENCY_INSTALLED enabled in the project settings, or there’s a bug in the editor (like the package is detected in the project when it’s not really there for some reason), in which case the editor version would be useful information to share. A full repro case would also be helpful for a bug report and/or for others to evaluate.

1 Like

Oops, I had not realized that my first approach might’ve left some settings set inside the player project settings. You were totally right, the same Scripting Define Symbols were set in there & in the asmdef, which probably caused unnecessary conflicts and the compilation errors.

Removed those from the Player settings and it ran fine !

Thanks for your help, learned quite a lot. :grinning: