#if UNITY_EDITOR && UNITY_WEBGL using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using System.Collections.Generic; using System.Linq; namespace Rive.EditorTools { /// Handles WebGL native plugin selection based on Unity version. /// Different Unity versions require different Emscripten-compiled libraries: /// - Unity 2022.x and earlier use Emscripten 3.1.8 /// - Unity 2023.x (Unity 6) uses Emscripten 3.1.38 /// If we don't match the emscripten library Unity uses, the build will fail with an error like: /// - Building Library\Bee\artifacts\WebGL\build\debug_WebGL_wasm\build.js failed with output: /// - wasm-ld: error: Library/PackageCache/app.rive.rive-unity/Runtime/Libraries/WebGL/librive_wasm.a(artboard.o): undefined symbol: std::__2::__vector_base_common::__throw_length_error() const // - emcc: error: 'C:/6000.0.26f1/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Emscripten/llvm\wasm-ld.exe @C:\Users\AppData\Local\Temp\emscripten_7f06ey06.rsp.utf-8' failed (returned 1) /// /// The ideal way to do this would've been to use BuildUtilities.RegisterShouldIncludeInBuildCallback, but that is only called for managed plugins and not native plugins: https://docs.unity3d.com/ScriptReference/PackageManager.BuildUtilities.RegisterShouldIncludeInBuildCallback.html /// The other ideal way would've been to use `Define Constraints`, but that also doesn't work for native plugins: https://discussions.unity.com/t/define-constraints-are-not-filtering-plugins-pluginimporter-defineconstraints-also-has-no-effect/873361/5 /// /// This preprocessor ensures the correct library is included during WebGL builds by temporarily copying the appropriate libraries to the project's Plugin folder during the build internal class WebGLBuildPreprocessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport { private const string PACKAGE_NAME = PackageInfo.PACKAGE_NAME; private const string TEMP_PLUGINS_PATH = "Assets/Plugins/WebGL/Rive"; private const string CREATED_FOLDERS_PREF = "RiveCreatedPluginFolders"; public int callbackOrder => 0; private static BuildReport currentBuildReport; // We use this to cleanup the plugin files in case of build failure // This is necessary because the IPostprocessBuildWithReport callback is not called when the build fails, only when it succeeds private static void OnEditorUpdate() { if (currentBuildReport != null && (currentBuildReport.summary.result == BuildResult.Failed || currentBuildReport.summary.result == BuildResult.Cancelled)) { // Unsubscribe first to prevent any potential multiple calls EditorApplication.update -= OnEditorUpdate; CleanupPluginFiles(); currentBuildReport = null; } } private void TrackCreatedFolder(string path) { var createdFolders = new HashSet( SessionState.GetString(CREATED_FOLDERS_PREF, "").Split( new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries) ); createdFolders.Add(path); SessionState.SetString(CREATED_FOLDERS_PREF, string.Join("|", createdFolders)); } private static bool WasCreatedByUs(string path) { var createdFolders = SessionState.GetString(CREATED_FOLDERS_PREF, "").Split( new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries ); return System.Array.IndexOf(createdFolders, path) != -1; } private static void ClearFolderTracking(string path) { var createdFolders = new HashSet( SessionState.GetString(CREATED_FOLDERS_PREF, "").Split( new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries) ); createdFolders.Remove(path); SessionState.SetString(CREATED_FOLDERS_PREF, string.Join("|", createdFolders)); } private static void CleanupBuildPrefs() { SessionState.EraseString(CREATED_FOLDERS_PREF); } public void OnPreprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.WebGL) return; // Store the build report so we can cleanup the plugin files in case of build failure currentBuildReport = report; EditorApplication.update += OnEditorUpdate; // Clear any leftover prefs from previous builds that might have failed CleanupBuildPrefs(); bool isUnity6OrNewer = UnityEngine.Application.unityVersion.StartsWith("6000") || UnityEngine.Application.unityVersion.StartsWith("2023"); string emscriptenVersion = isUnity6OrNewer ? "3.1.38" : "3.1.8"; string sourcePath = System.IO.Path.Combine("Packages", PACKAGE_NAME, "Runtime/Libraries/WebGL", $"emscripten_{emscriptenVersion}"); if (!System.IO.Directory.Exists(sourcePath)) { throw new BuildFailedException($"Rive: Could not find WebGL libraries at {sourcePath}"); } // Create and track directories we need so we can clean them up later string[] folders = { "Assets/Plugins", "Assets/Plugins/WebGL", TEMP_PLUGINS_PATH }; foreach (string folder in folders) { if (!AssetDatabase.IsValidFolder(folder)) { System.IO.Directory.CreateDirectory(folder); TrackCreatedFolder(folder); } } // Copy all .a files and configure them for WebGL foreach (string file in System.IO.Directory.GetFiles(sourcePath, "*.a")) { string fileName = System.IO.Path.GetFileName(file); string destFile = System.IO.Path.Combine(TEMP_PLUGINS_PATH, fileName); System.IO.File.Copy(file, destFile, true); AssetDatabase.ImportAsset(destFile); var importer = AssetImporter.GetAtPath(destFile) as PluginImporter; if (importer != null) { importer.SetCompatibleWithAnyPlatform(false); importer.SetCompatibleWithPlatform(BuildTarget.WebGL, true); importer.SaveAndReimport(); } } AssetDatabase.Refresh(); } private static bool IsDirectoryEmpty(string path) { return !AssetDatabase.FindAssets(string.Empty, new[] { path }).Any(); } private static void DeleteAssetPath(string path) { if (AssetDatabase.DeleteAsset(path)) { ClearFolderTracking(path); } } public void OnPostprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.WebGL) return; try { CleanupPluginFiles(); } finally { // Unsubscribe from editor update since we're handling the cleanup here EditorApplication.update -= OnEditorUpdate; currentBuildReport = null; } } private static void CleanupPluginFiles() { try { if (AssetDatabase.IsValidFolder(TEMP_PLUGINS_PATH)) { DeleteAssetPath(TEMP_PLUGINS_PATH); } // Check and clean up parent directories if empty and created by us string webglPath = "Assets/Plugins/WebGL"; if (AssetDatabase.IsValidFolder(webglPath) && IsDirectoryEmpty(webglPath) && WasCreatedByUs(webglPath)) { DeleteAssetPath(webglPath); string pluginsPath = "Assets/Plugins"; if (AssetDatabase.IsValidFolder(pluginsPath) && IsDirectoryEmpty(pluginsPath) && WasCreatedByUs(pluginsPath)) { DeleteAssetPath(pluginsPath); } } AssetDatabase.Refresh(); } finally { // Cleanup prefs to avoid stale data on next build CleanupBuildPrefs(); } } } } #endif