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

FeatureDescription
IConfigProviderRead/write config from/to project file
IGUIProviderDraw extension GUI

Local features

FeatureDescription
ITaskPatcherPatch task
ICompilationPatcherPatch compilation
IAssemblyPatcherPatch assembly
IPdbPatcherPatch PDB file
IXmlDocPatcherPatch XML document
ICompilationReporterRead compilation results

Global features

FeatureDescription
IProjectReaderRead projects in a custom format.
IProjectWriterWrite 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;
    }
}
Previous
Programmatic Usage