diff --git a/Editor/Attributes.cs b/Editor/Attributes.cs index 9608f79..f0346c7 100644 --- a/Editor/Attributes.cs +++ b/Editor/Attributes.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace SatorImaging.UnitySourceGenerator { @@ -6,13 +7,17 @@ namespace SatorImaging.UnitySourceGenerator [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; } } diff --git a/Editor/USGEngine.cs b/Editor/USGEngine.cs index 969bb4c..0f58715 100644 --- a/Editor/USGEngine.cs +++ b/Editor/USGEngine.cs @@ -15,19 +15,22 @@ namespace SatorImaging.UnitySourceGenerator { public class USGEngine : AssetPostprocessor { - ///This will be disabled after Unity Editor import event automatically. + ///This will be disabled automatically after Unity Editor import event. 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() { @@ -36,76 +39,67 @@ static USGEngine() s_projectDirPath = s_projectDirPath.Substring(0, s_projectDirPath.Length - ASSETS_DIR_NAME.Length); } - readonly static HashSet 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 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; } @@ -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; } @@ -176,6 +169,7 @@ public static bool ProcessFile(string assetsRelPath) outputPath = Path.Combine(outputPath, info.OutputFileName); + // do it. var context = new USGContext { TargetClass = info.TargetClass, @@ -183,8 +177,6 @@ public static bool ProcessFile(string assetsRelPath) OutputPath = outputPath.Replace('\\', '/'), }; - - // do it. var sb = new StringBuilder(); sb.AppendLine($"// {generatorCls.Name}"); @@ -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 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; } diff --git a/Editor/USGUtility.cs b/Editor/USGUtility.cs index 5f6df57..9965db4 100644 --- a/Editor/USGUtility.cs +++ b/Editor/USGUtility.cs @@ -26,9 +26,10 @@ static void ForceGenerateSelectedScripts() } - public static void ForceGenerate(string clsName, bool showInProjectPanel = true) + ///UNSAFE on use in build event due to this method calls fancy UI methods and fire import event. Use `GetAssetPathByName()` instead. + public static void ForceGenerateInEditor(string clsName, bool showInProjectPanel = true) { - var path = GetScriptFileByName(clsName); + var path = GetAssetPathByName(clsName); if (path == null) return; if (showInProjectPanel) @@ -38,7 +39,8 @@ public static void ForceGenerate(string clsName, bool showInProjectPanel = true) } - internal static string GetScriptFileByName(string clsName) + ///Returns "Assets/" rooted path of the script file. + public static string GetAssetPathByName(string clsName) { var GUIDs = AssetDatabase.FindAssets(clsName); foreach (var GUID in GUIDs) diff --git a/README.md b/README.md index 1a91d12..67e6292 100644 --- a/README.md +++ b/README.md @@ -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)

@@ -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. @@ -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); ``` @@ -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...? diff --git a/package.json b/package.json index ab57c20..464b1e7 100644 --- a/package.json +++ b/package.json @@ -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": {