Files
BABA_YAGA/Packages/app.rive.rive-unity/Editor/WebGLBuildPreprocessor.cs
2026-05-19 17:39:03 +07:00

210 lines
8.5 KiB
C#

#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<true>::__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<string>(
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<string>(
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