I’m currently trying my hand at source generation, but I’m running into a bit of a wall. When trying to reference assemblies like Unity.Entities, I’m getting the following error:
ModifierSourceGenerator\ModifierSourceGenerator.ModifierEnumGenerator\RecalculateModifiers.g.cs(4,58): error CS0234: The type or namespace name 'Entities' does not exist in the namespace 'Unity' (are you missing an assembly reference?)
The source generator dll is located in its own folder inside the Assets folder. I tried setting up a .asmdef file, both inside the generator folder and by having it be generated by the dll but that didn’t really seem to work, as when I used .asmdef I wasn’t even able to use the marker attribute that’s also generated even though I could before. The generated code is just a raw const string atm, with the source generator code looking like this:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace ModifierSourceGenerator
{
[Generator]
public class ModifierEnumGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx => {
ctx.AddSource("ModifiableStatsAttribute.g.cs", SourceText.From(GeneratorHelper.ModifierAttributeSourceText, Encoding.UTF8));
ctx.AddSource("RecalculateModifiers.g.cs", SourceText.From(GeneratorHelper.RecalculateModifiersSourceText, Encoding.UTF8));
});
}
}
}
Obviously the component doesn’t necessarily need to be generated, but later on I also want to generate a system which acts on this component, and if I can’t generate a simple component I doubt I’ll be able to generate a system.
I know for a fact that I have the Entities package installed as I’ve used it in manually written code just fine. I’m guessing it’s something fairly obvious to those in the know, but how do I fix this?
I still need to take a look at your link, but it looks useful regardless!
For now, I’ve uploaded a recreation here: GitHub - TijsP/Source-generator-error-reproduce
For good measure, I used the latest version of Unity (6000.0.32f1) and the Entities package (1.3.8). If you get any weird errors: removing the generator DLL, recompiling the project and adding the DLL back again seems to fix those
Third, you should examine the source code of generators from either my repo or Untiy’s Entities to eradicate errors similar to this:
Path could not be found for script compilation file 'ModifierSourceGenerator/ModifierSourceGenerator.ModifierEnumGenerator/ModifiableStatsAttribute.g.cs'
The source code of my generators locates at Assets/EncosyTower/EncosyTower.Modules/SourceGenerator~.
The source code for Unity Entities generators locates at Library/PackageCache/com.unity.entities/Unity.Entities/SourceGenerators/Source~
Thanks so much for helping out, I never would’ve gotten this far without your help!
I guess I’ll summarise what I did wrong up until now for future reference:
In addition to the RoslynAnalyser label, the generator DLL also needs the RunOnlyOnAssembliesWithReference and SourceGenerator labels
Besides disabling the “Any platform” and related check boxes on the generator DLL, the Auto Reference and Validate References check boxes need to be disabled as well
There needs to be an assembly definition file either in the same or a parent folder as the generator DLL. Besides the default options, all references to Unity assemblies required by the generated code need to be included there. In my case, these are Unity.Burst, Unity.Collections, Unity.Entities and Unity.Entities.Hybrid
The assembly definition file needs an accompanying AssemblyInfo.cs file, even if that file remains empty
Static code like marker attributes should be added to the Unity Assets directly (or potentially as a separate class library referenced by the generator project and included as a DLL in the Unity Assets, but I didn’t test this thoroughly). In other words, avoid using IncrementalGeneratorInitializationContext.RegisterPostInitializationOutput
Make sure the generated code is actually valid…
These changes seem to fix the problems I was having in my original project, where I’m not getting any errors anymore.
However, in the test repo I’m not able to get rid of the error you mentioned. It seems I’m still in way over my head, since I hardly have any idea how to go about addressing the issue. I understand it somehow can’t find the path to the generated files, but I have no idea what I can do about this or what “it” even refers to in this case. At first I thought it had to do with not giving a Root Namespace in the assembly definition, but the error comes back as soon as you recompile any scripts. I also tried looking at the source code of both your project and Unity.Entities, but because I have no idea what to look for, I couldn’t really come up with anything.
Edit: the problem lies with context.RegisterPostInitializationOutput. Use context.RegisterSourceOutput instead.
The one mildly interesting thing I did find is that upon inspecting the generated code, it seems like any changes I make to the source generator don’t actually result in newly generated files? I’m not really sure if that’s because the generator just won’t run because of the errors, there’s some cache that I need to clear manually or whether it’s a result of me generating the code using IncrementalGeneratorInitializationContext.RegisterPostInitializationOutput
Edit: IDE needs to be restarted after the generator DLL is rebuilt
One annoying limitation of source generator: for each new build of the generator dll, you have to restart the IDE. It won’t update automatically for the IDE that is running.
However you could always set up a debugable generator project. That will ease the generator development to a great extent. Unfortunately atm I don’t have a public repo at hand to guide you. But if you can wait a few days I can probably push one onto GitHub.
The culprit is the API you’re using to output generated code. You need to examine how my generators were set up and how I output code into the context. For this part I based on the setup of Entities generators.
I also made sure to output the source code to a separate file. While I’m not getting the path could not be found error anymore, I’m now back to type or namespace name could not be found. So unless you were talking about me using context.RegisterPostInitializationOutput as opposed to context.RegisterSourceOutput, I’m not really sure again how to continue ^^’
I’ve update the reproduction repo with the changes I’ve made
One special thing about source generator is that you don’t have to output the code to an actual physical file on disk. To normal users, generated code just seemingly “magically” exists out of thin air. Outputing to a file might negatively affect your IDE and/or Unity editor. As you can see here, by default, generators won’t output files to disk, unless you define a symbol (I just name it ENCOSY_OUTPUT_SOURCEGEN_FILES):
Code under the “SourceGen.Common” folder was borrowed from Unity.Entities package.
After some quick testing, I guess it’s best to have the marker attribute in a separate class library so it can be used both in the generator and in the Unity project? I wasn’t able to reference the attribute in both when just putting the file in the generator project, I guess on account of the tags added to the DLL.
That would leave the enableable component. I supposed I could include it in the system I will be generating, or in some other generated file. That doesn’t seem that “clean” though, is there some other/better way to output “static” code using the generator?
What do you mean exactly by this? Does that mean you want to do this if (type == typeof(UnityEngine.SerializeField))? It is NOT possible. Period.
Source generators run inside a totally different environment thus it shouldn’t (or generally can’t) reference user code. That’s why you see most source generator only works on SyntaxNode, ISymbol, and hard-coded strings, etc.
For example, I have to write code like if (name.StartsWith("SerializeField"))
because I can’t write this if (name.StartsWith(nameof(UnityEngine.SerializeField))).
To make it clear, source generators are a part of the C# compiler, not the user project, not even the Unity compilation.
That’s why you shouldn’t do this.
Because the C# compiler will need to load source generator .dll to invoke the functionality. What if that source generator references .dll from UnityEngine itself? How can C# compiler understand and run UnityEngine specific functionality while Unity is an engine written mostly in C++? C# compiler will then have to load the whole engine and that will greatly complicate the C# compilation process, as well as affect it negatively performance-wise.
Though if you try really hard, you can achieve it. But it’s not recommended. Just keep source generators simple and fast and domain-agnostic.
Aah, right, I understand now. The generic source generator tutorial I’ve been loosely following uses marker attributes to specify which components should have code generated, and I misremembered how they then use this attribute in the generator code. They do exactly as you say, using if(name == "MarkerAttribute)" as opposed to directly referencing the type itself.
Since I’m not trying to create a library, I’ll just put the code for the marker attribute and enableable component in Unity itself. It’s just that in the tutorial (which isn’t specific to Unity) they’ve been using context.RegisterPostInitializationOutput() to generate these marker attributes, which is why I was using that method in the first place
My generators just follow the setup taken from Entities. The Entities team has invested great amount of work on that and it works perfectly fine for Unity. It really saves me time and research effort.
Alright, I moved the marker attribute outside of the generator project into Unity. After adding a debug project it does seem like the generator is working like it should (it’s not generating anything particularly useful atm, but that’s besides the point).
I think the main thing now that’s holding it back from working is me not properly understanding assemblies. I have the generator dll + marker attribute in one assembly, as well as an IComponentData struct for testing. The generator detects this struct just fine and generates the files it should. However, it remains unaware of the testing struct in its parent and sibling folders. I tried both without and with an assembly definition file in the parent folder and also added a sibling assembly with a test component, but it seems like I’m still doing something wrong, as none of those components got picked up by the generator. I tried comparing the dll+asmdef to your project and the entities generator, but as far as I’m aware I have the same options selected as they do. Is there something specific I should put into the AssemblyInfo file? Right now it’s empty, but I don’t think/am not sure that’s got anything to do with it?
I updated the repo with what I have so far, in case you have the time and interest to take a look
The name AssemblyInfo can change into anything else, the name is not important. The purpose of that file is to put some attributes onto the .dll itself (or the assembly, hence that name is chosen following Microsoft convention). For example:
[assembly: SkipSourceGeneratorsForThisAssembly]
In fact, if you don’t have anything to put on the assembly then you could leave that file empty or even remove the file.
On the matter of your source generator. As you can see in this PR, I tried to make your source generator .csproj as close as possible to my project. I also changed some code to make things more correct according to my experience. Your source generator now works as expected, but I can’t tell confidently which change has made it.
On a side note, you should rethink the namespace and class name for the generated code. The current name will surely cause an ambiguity error between various assemblies.
That’s a good point, I’ll make sure to pay attention to that.
Also, I think the issue has finally been resolved! It seems the last thing that was holding me back was the way I was trying to debug. For some reason, the relative path I was trying to write to would only actually end up being in the Unity project directory when running on its own assembly. When running on any other assembly, it would try to write to Unity\Hub\Editor\6000.0.32f1\Editor\Data\DotNetSdkRoslyn\..., but as access would be declined, the IsSyntaxTargetForGeneration method would fail (more or less) silently, after which code generation would of course simply not take place. So using absolute paths solved this, as would simply not outputting files of course.
I only found this out after going through your commit and removing things until it broke again. If it hadn’t been for your commit I never would’ve realised this was the issue, so a huge thanks for your help!