Extension Development
Extensions in your project can be either declared or imported from external sources. They can take the form of C# scripts or assemblies, as long as they are under any "Editor" folder.
Before start, you should have a basic knowledge of Extensions.
Declare an extension
To create a local extension (project level), add a class derived from FlexFramework.FlexCompiler.Integration.LocalExtension
.
using FlexFramework.FlexCompiler.Integration;
public class YourExtension : LocalExtension
{
public override string ID => "com.mycompany.myproduct.yourextension";
public override string Name => "Extension Name";
}
An extension must include the following properties:
ID
: A unique identifier at the project level.Name
: A descriptive name for the extension.
Create a global extension:
using FlexFramework.FlexCompiler.Integration;
public class YourExtension : GlobalExtension
{
public override string ID => "com.mycompany.myproduct.yourextension";
public override string Name => "Extension Name";
}
Extension features
An empty extension does nothing. To interact with various stages of the build process, job hooks need to be added.
Check Extension hooks for better understanding of each hook.
Job hooks are implemented as extension features. An extension can implement one or more of the following features:
Shared features
Feature | Description |
---|---|
IConfigProvider | Read/write config from/to project file |
IGUIProvider | Draw extension GUI |
Local features
Feature | Description |
---|---|
ITaskPatcher | Patch task |
ICompilationPatcher | Patch compilation |
IAssemblyPatcher | Patch assembly |
IPdbPatcher | Patch PDB file |
IXmlDocPatcher | Patch XML document |
ICompilationReporter | Read compilation results |
Global features
Feature | Description |
---|---|
IProjectReader | Read projects in a custom format. |
IProjectWriter | Write projects in a custom format. |
Examples
Logging
Let's create an extension that writes compilation results to a log file.
First, define the extension class:
using FlexFramework.FlexCompiler.Integration;
public class LoggerExtension : Extension
{
public override string ID => "com.defaultcompany.test.logger";
public override string Name => "Logger";
}
Now, open the FlexCompiler project window. You'll see the "Logger" extension in the extensions list. You can toggle it on, but nothing will happen until we define the behavior.
To write compilation results to a log file, update the script:
using System.IO;
using System.Text;
using FlexFramework.FlexCompiler.Integration;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
public class LoggerExtension : Extension, ICompilationReporter
{
public override string ID => "com.defaultcompany.test.logger";
public override string Name => "Logger";
void ICompilationReporter.Report(EmitResult result, CSharpCompilation compilation)
{
var log = new StringBuilder();
foreach (var item in result.Diagnostics)
{
var level = item.WarningLevel switch
{
0 => "Error",
> 0 and <= 2 => "Warn",
_ => "Info"
};
var text = item.ToString();
log.AppendLine($"[{level}] {text}");
}
File.WriteAllText("results.log", log.ToString());
}
}
When you run the compilation with this extension enabled, a file named "results.log" will be written to the project root folder.
[Error] Assets\Editor\LoggerExtension.cs(21,17): error CS8652: The feature 'and pattern' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
[Error] Assets\Editor\LoggerExtension.cs(21,17): error CS8652: The feature 'relational pattern' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
[Error] Assets\Editor\LoggerExtension.cs(21,25): error CS8652: The feature 'relational pattern' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
To specify the log file location manually and ensure persistence, update the script as follows:
using System;
using System.IO;
using System.Text;
using FlexFramework.FlexCompiler.Integration;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using UnityEditor;
using UnityEngine;
public class LoggerExtension : Extension, ICompilationReporter, IGUIProvider
{
public override string ID => "com.defaultcompany.test.logger";
public override string Name => "Logger";
GUIContent IGUIProvider.Icon => null;
string IGUIProvider.Website => null;
private string _file;
void ICompilationReporter.Report(EmitResult result, CSharpCompilation compilation)
{
var log = new StringBuilder();
foreach (var item in result.Diagnostics)
{
var level = item.WarningLevel switch
{
0 => "Error",
> 0 and <= 2 => "Warn",
_ => "Info"
};
var text = item.ToString();
log.AppendLine($"[{level}] {text}");
}
File.WriteAllText("results.log", log.ToString());
}
void IGUIProvider.OnGUI()
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PrefixLabel("Log File");
EditorGUILayout.LabelField(_file, EditorStyles.textField);
if (GUILayout.Button(EditorGUIUtility.IconContent("Save"), EditorStyles.miniButton, GUILayout.ExpandWidth(false)))
{
var file = EditorUtility.SaveFilePanel("Log file", Environment.CurrentDirectory, PlayerSettings.productName, "log");
if (!string.IsNullOrEmpty(file))
{
_file = file;
}
}
}
}
}
You can now designate the location for the log file. However, upon reopening the project window later, you might find that the log file location is absent, indicating that it hasn't been saved. To guarantee the persistence of extension configurations, update the script:
using System;
using System.IO;
using System.Text;
using FlexFramework.FlexCompiler.Integration;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using UnityEditor;
using UnityEngine;
public class LoggerExtension : Extension, ICompilationReporter, IGUIProvider, IConfigProvider
{
public override string ID => "com.defaultcompany.test.logger";
public override string Name => "Logger";
GUIContent IGUIProvider.Icon => null;
string IGUIProvider.Website => null;
[SerializeField]
private string _file;
void ICompilationReporter.Report(EmitResult result, CSharpCompilation compilation)
{
var log = new StringBuilder();
foreach (var item in result.Diagnostics)
{
var level = item.WarningLevel switch
{
0 => "Error",
> 0 and <= 2 => "Warn",
_ => "Info"
};
var text = item.ToString();
log.AppendLine($"[{level}] {text}");
}
File.WriteAllText("results.log", log.ToString());
}
void IGUIProvider.OnGUI()
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PrefixLabel("Log File");
EditorGUILayout.LabelField(_file, EditorStyles.textField);
if (GUILayout.Button(EditorGUIUtility.IconContent("Save"), EditorStyles.miniButton, GUILayout.ExpandWidth(false)))
{
var file = EditorUtility.SaveFilePanel("Log file", Environment.CurrentDirectory, PlayerSettings.productName, "log");
if (!string.IsNullOrEmpty(file))
{
_file = file;
}
}
}
}
void IConfigProvider.ReadConfig(string config)
{
JsonUtility.FromJsonOverwrite(config, this);
}
string IConfigProvider.WriteConfig()
{
return JsonUtility.ToJson(this);
}
}
Additional script
In this example, we'll create an extension that manipulates the compilation process by adding a generated script to each build.
First, create the extension class:
using FlexFramework.FlexCompiler.Integration;
public class AdditionalScriptExtension : Extension
{
public override string ID => "com.defaultcompany.test.additionalscript";
public override string Name => "Additional Script";
}
To interact with the compilation, implement the ICompilationPatcher
feature:
using System.Text;
using FlexFramework.FlexCompiler.Compilation;
using FlexFramework.FlexCompiler.Integration;
using Microsoft.CodeAnalysis.CSharp;
public class AdditionalScriptExtension : Extension, ICompilationPatcher
{
public override string ID => "com.defaultcompany.test.additionalscript";
public override string Name => "Additional Script";
CSharpCompilation ICompilationPatcher.PatchCompilation(CSharpCompilation compilation, BuildTask task)
{
var script = @"
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
void Start()
{
}
void Update()
{
}
}
";
var parseOptions = task.Settings.ToParserOptions();
// we use string.Empty for the virtually generated script file path
return compilation.AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(script, parseOptions, string.Empty, Encoding.Default));
}
}
Now, a component called NewBehaviourScript
will be found in the generated assembly.
For more information about the CSharpCompilation
API, refer to Microsoft Learn↗.
Reference alias
This extension replaces Mono.Cecil.dll
reference with an alias to solve duplicate references conflicts. Same as using XXX=xxx.yyy.zzz;
or extern alias XXX;
. It's also the same as CLI argument /reference:alias=file
.
using System.Linq;
using FlexFramework.FlexCompiler.Compilation;
using FlexFramework.FlexCompiler.Integration;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
public class CecilExtension : LocalExtension, ICompilationPatcher
{
public CecilExtension(Project project) : base(project)
{
}
public override string ID => "com.defaultcompany.test.cecil";
public override string Name => "Cecil Patcher";
public CSharpCompilation PatchCompilation(CSharpCompilation compilation, BuildTask task)
{
var reference = compilation.References.Where(e => e is PortableExecutableReference).Cast<PortableExecutableReference>().FirstOrDefault(e => e.FilePath.EndsWith("Mono.Cecil.dll"));
if (reference != null)
{
return compilation.ReplaceReference(reference, reference.WithAliases(new[] { "Mono.Cecil" }));
}
return compilation;
}
}