Skip to content

Commit

Permalink
refactor: optimize, performance and the following
Browse files Browse the repository at this point in the history
- remove old logic
- public interface updates
- text encoding option
- follow attribute guideline
- OverwriteIfFileExists is now enabled by default
  • Loading branch information
sator-imaging committed Mar 20, 2023
1 parent fc86d66 commit d2d1b04
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 56 deletions.
11 changes: 8 additions & 3 deletions Editor/Attributes.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using System;
using System.Text;

namespace SatorImaging.UnitySourceGenerator
{
///<summary>NOTE: Implement "IUnitySourceGenerator" (C# 11.0)</summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class UnitySourceGeneratorAttribute : Attribute
{
Type generatorClass;

public UnitySourceGeneratorAttribute(Type generatorClass = null)
{
GeneratorClass = generatorClass;
this.generatorClass = generatorClass;
}

public bool OverwriteIfFileExists { get; set; } = false;
public Type GeneratorClass { get; set; }
public Type GeneratorClass => generatorClass;

public bool OverwriteIfFileExists { get; set; } = true;
public Encoding OutputFileEncoding { get; set; } = Encoding.UTF8;

}
}
97 changes: 54 additions & 43 deletions Editor/USGEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ namespace SatorImaging.UnitySourceGenerator
{
public class USGEngine : AssetPostprocessor
{
///<summary>This will be disabled after Unity Editor import event automatically.</summary>
///<summary>This will be disabled automatically after Unity Editor import event.</summary>
public static bool IgnoreOverwriteSettingByAttribute = false;


const int BUFFER_LENGTH = 61_440;
const int BUFFER_MAX_CHAR_LENGTH = BUFFER_LENGTH / 3; // worst case of UTF-8
const string GENERATOR_PREFIX = ".";
const string GENERATOR_EXT = ".g";
const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. this is used by .EndsWith()
const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. used to determine file is generated one or not.
const string ASSETS_DIR_NAME = "Assets";
const string ASSETS_DIR_SLASH = "Assets/";
const string ASSETS_DIR_SLASH = ASSETS_DIR_NAME + "/";
const string TARGET_FILE_EXT = @".cs";
const string PATH_PREFIX_TO_IGNORE = @"Packages/";
readonly static char[] DIR_SEPARATORS = new char[] { '\\', '/' };

// OPTIMIZE: Avoiding explicit static ctor is best practice for performance???
readonly static string s_projectDirPath;
static USGEngine()
{
Expand All @@ -36,76 +39,67 @@ static USGEngine()
s_projectDirPath = s_projectDirPath.Substring(0, s_projectDirPath.Length - ASSETS_DIR_NAME.Length);
}

readonly static HashSet<string> s_targetFilePaths = new();
static void AddAppropriateTarget(string filePath)
static bool IsAppropriateTarget(string filePath)
{
if (!filePath.EndsWith(TARGET_FILE_EXT) ||
!filePath.StartsWith(ASSETS_DIR_SLASH))
{
return;
return false;
}
s_targetFilePaths.Add(filePath);
}


void OnPreprocessAsset()
{
AddAppropriateTarget(assetPath);
return true;
}


// NOTE: To avoid event invoked twice on file deletion.
static bool s_processingJobQueued = false;

static void OnPostprocessAllAssets(
string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
// NOTE: Do NOT handle deleted assets because Unity tracking changes perfectly.
// Even if delete file while Unity shutted down, asset deletion event happens on next Unity launch.
// As a result, delete/import event loops infinitely and file cannot be deleted.
for (int i = 0; i < importedAssets.Length; i++)
{
AddAppropriateTarget(importedAssets[i]);
}

if (s_processingJobQueued) return;
s_processingJobQueued = true;

// TODO: Unity sometimes reloads updated scripts by Visual Studio in background automatically.
// In this situation, code generation will be done with script data right before saving.
// It cannot be solved on C#, simply restart Unity.
// Using [DidReloadScripts] or EditorApplication.delayCall, It works fine with Reimport
// menu command but OnPostprocessAllAssets event doesn't work as expected.
// (script runs with static field cleared even though .Clear() is only in ProcessingFiles())
// (script runs with static field cleared even though .Clear() is only in ProcessingFiles().
// it's weird that event happens and asset paths retrieved but hashset items gone.)
////EditorApplication.delayCall += () =>
{
ProcessingFiles();
ProcessingFiles(importedAssets);
};
}


readonly static HashSet<string> s_updatedGeneratorNames = new();
static void ProcessingFiles()
static void ProcessingFiles(string[] targetPaths)
{
bool somethingUpdated = false;
foreach (string path in s_targetFilePaths)
for (int i = 0; i < targetPaths.Length; i++)
{
if (ProcessFile(path))
// NOTE: Do NOT early return in this method.
// check path here to allow generator class can be lie outside of Assets/ folder.
if (!IsAppropriateTarget(targetPaths[i])) continue;

if (ProcessFile(targetPaths[i]))
somethingUpdated = true;
}

// TODO: more efficient way to process related targets
var overwriteEnabledByCaller = IgnoreOverwriteSettingByAttribute;
foreach (var generatorName in s_updatedGeneratorNames)
{
foreach (var info in s_typeNameToInfo.Values)
{
if (info.TargetClass == null) continue;
if (info.Attribute.GeneratorClass?.Name != generatorName) continue;
if (info.TargetClass == null ||
info.Attribute.GeneratorClass?.Name != generatorName)
continue;

var path = USGUtility.GetScriptFileByName(info.TargetClass.Name);
if (path != null)
var path = USGUtility.GetAssetPathByName(info.TargetClass.Name);
if (path != null && IsAppropriateTarget(path))
{
IgnoreOverwriteSettingByAttribute = true;
IgnoreOverwriteSettingByAttribute = overwriteEnabledByCaller
|| info.Attribute.OverwriteIfFileExists;
if (ProcessFile(path))
somethingUpdated = true;
}
Expand All @@ -114,11 +108,10 @@ static void ProcessingFiles()

if (somethingUpdated)
AssetDatabase.Refresh();
s_targetFilePaths.Clear();

s_updatedGeneratorNames.Clear();

IgnoreOverwriteSettingByAttribute = false; // always turn it off.
s_processingJobQueued = false;
}


Expand Down Expand Up @@ -176,15 +169,14 @@ public static bool ProcessFile(string assetsRelPath)
outputPath = Path.Combine(outputPath, info.OutputFileName);


// do it.
var context = new USGContext
{
TargetClass = info.TargetClass,
AssetPath = assetsRelPath.Replace('\\', '/'),
OutputPath = outputPath.Replace('\\', '/'),
};


// do it.
var sb = new StringBuilder();
sb.AppendLine($"// <auto-generated>{generatorCls.Name}</auto-generated>");

Expand All @@ -199,25 +191,44 @@ public static bool ProcessFile(string assetsRelPath)
throw;
}

//save??
if (!isSaveFile || sb == null || string.IsNullOrWhiteSpace(context.OutputPath))
return false;

if (File.Exists(context.OutputPath) &&
(!info.Attribute.OverwriteIfFileExists && !IgnoreOverwriteSettingByAttribute)
)
{
return false;
}


var outputDir = Path.GetDirectoryName(context.OutputPath);
if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);

if (File.Exists(context.OutputPath) &&
(!info.Attribute.OverwriteIfFileExists && !IgnoreOverwriteSettingByAttribute)
)
#if UNITY_2021_3_OR_NEWER

// OPTIMIZE: use sb.GetChunks() in future release of Unity. 2021 LTS doesn't support it.
using (var fs = new FileStream(context.OutputPath, FileMode.Create, FileAccess.Write))
{
return false;
Span<byte> buffer = stackalloc byte[BUFFER_LENGTH];
var span = sb.ToString().AsSpan();
for (int i = 0; i < span.Length; i += BUFFER_MAX_CHAR_LENGTH)
{
var len = BUFFER_MAX_CHAR_LENGTH;
if (len + i > span.Length) len = span.Length - i;

int written = info.Attribute.OutputFileEncoding.GetBytes(span.Slice(i, len), buffer);
fs.Write(buffer.Slice(0, written));
}
fs.Flush();
}

#else
File.WriteAllText(context.OutputPath, sb.ToString(), info.Attribute.OutputFileEncoding);
#endif

File.WriteAllText(context.OutputPath, sb.ToString());
Debug.Log($"[{nameof(UnitySourceGenerator)}] Generated: {context.OutputPath}");

return true;
}

Expand Down
8 changes: 5 additions & 3 deletions Editor/USGUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ static void ForceGenerateSelectedScripts()
}


public static void ForceGenerate(string clsName, bool showInProjectPanel = true)
///<summary>UNSAFE on use in build event due to this method calls fancy UI methods and fire import event. Use `GetAssetPathByName()` instead.</summary>
public static void ForceGenerateInEditor(string clsName, bool showInProjectPanel = true)
{
var path = GetScriptFileByName(clsName);
var path = GetAssetPathByName(clsName);
if (path == null) return;

if (showInProjectPanel)
Expand All @@ -38,7 +39,8 @@ public static void ForceGenerate(string clsName, bool showInProjectPanel = true)
}


internal static string GetScriptFileByName(string clsName)
///<summary>Returns "Assets/" rooted path of the script file.</summary>
public static string GetAssetPathByName(string clsName)
{
var GUIDs = AssetDatabase.FindAssets(clsName);
foreach (var GUID in GUIDs)
Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ As you already know, Roslyn's source generator is too sophisticated. This framew
- [Copyright](#copyright)
- [License](#license)
- [Devnote](#devnote)
- [TODO](#todo)
- [Memo](#memo)

<!------- End of Details EN Tag -------></details></p>

Expand Down Expand Up @@ -132,7 +134,7 @@ namespace Sample

Here is target-less generator example.

It is useful to generate static database that cannot be generated on Unity runtime. For example, asset GUIDs database, resource integrity tables, etc.
It is useful to generate static database that cannot be generated on Unity runtime. For example, build asset GUIDs database using `UnityEditor.AssetDatabase`, resource integrity tables, etc.



Expand Down Expand Up @@ -206,12 +208,12 @@ There are utility functions to perform source code generation on build event.


```csharp
// perform by known asset path.
USGEngine.IgnoreOverwriteSettingByAttribute = true; // force overwrite
USGEngine.ProcessFile(pathToGeneratorScriptFile);
// search by class name if you don't know where it is.
var assetPath = USGUtility.GetAssetPathByName(nameof(MinimalGenerator));

// search by class name.
USGUtility.ForceGenerate(nameof(MinimalGenerator));
// perform code generation.
USGEngine.IgnoreOverwriteSettingByAttribute = true; // force overwrite
USGEngine.ProcessFile(assetPath);
```


Expand Down Expand Up @@ -355,4 +357,16 @@ SOFTWARE.

# Devnote


## TODO

- Add new attribute option `UseCustomWriter` to use it's own file writer instead of builtin writer. For the "non-allocation" addicted developers.
- `USGEngine.ProcessingFile()` doesn't care what happens in custom writer. just returns true in this situation.
- Option is for generator class. Referenced generator class doesn't have `UnitySourceGenerator` attribute so that need to retrieve it from target classes. (how handle conflicts?)
- `USGContext.UseCustomWriter` can be used to prevent writing file but `StringBuilder` is built prior to `Emit()` method.



## Memo

Unity doesn't invoke import event if Visual Studio is not launch by current session of Unity...?
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "com.sator-imaging.alt-source-generator",
"displayName": "Alternative Source Generator for Unity",
"version": "1.1.0",
"version": "1.2.0",
"unity": "2021.3",
"description": "Ease-of-Use Source Generator Alternative for Unity.",
"author": {
Expand Down

0 comments on commit d2d1b04

Please sign in to comment.