瀏覽代碼

C#: Fallback to CoreCLR/MonoVM hosting APIs when hostfxr/NativeAOT fails

Some platforms don't support hostfxr but we can use the coreclr/monosgen library directly to initialize the runtime.

Android exports now use the `android` runtime identifier instead of `linux-bionic`, this removes the restrictions we previously had:
- Adds support for all Android architectures (arm32, arm64, x32, and x64), previously only the 64-bit architectures were supported.
- Loads `System.Security.Cryptography.Native.Android` (the .NET library that binds to the Android OS crypto functions).
Raul Santos 1 年之前
父節點
當前提交
0aa46e19c5

+ 4 - 0
editor/export/editor_export_plugin.cpp

@@ -229,6 +229,10 @@ bool EditorExportPlugin::supports_platform(const Ref<EditorExportPlatform> &p_ex
 	return ret;
 }
 
+PackedStringArray EditorExportPlugin::get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const {
+	return _get_export_features(p_export_platform, p_debug);
+}
+
 PackedStringArray EditorExportPlugin::get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const {
 	PackedStringArray ret;
 	GDVIRTUAL_CALL(_get_android_dependencies, p_export_platform, p_debug, ret);

+ 1 - 0
editor/export/editor_export_plugin.h

@@ -166,6 +166,7 @@ public:
 	virtual String get_name() const;
 
 	virtual bool supports_platform(const Ref<EditorExportPlatform> &p_export_platform) const;
+	PackedStringArray get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;
 
 	virtual PackedStringArray get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;
 	virtual PackedStringArray get_android_dependencies_maven_repos(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;

+ 1 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj

@@ -30,6 +30,7 @@
     <None Include="$(GodotSdkPackageVersionsFilePath)" Pack="true" PackagePath="Sdk">
       <Link>Sdk\SdkPackageVersions.props</Link>
     </None>
+    <None Include="Sdk\Android.props" Pack="true" PackagePath="Sdk" />
     <None Include="Sdk\iOSNativeAOT.props" Pack="true" PackagePath="Sdk" />
     <None Include="Sdk\iOSNativeAOT.targets" Pack="true" PackagePath="Sdk" />
   </ItemGroup>

+ 5 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props

@@ -0,0 +1,5 @@
+<Project>
+  <PropertyGroup>
+    <UseMonoRuntime Condition=" '$(UseMonoRuntime)' == '' and '$(PublishAot)' != 'true' ">true</UseMonoRuntime>
+  </PropertyGroup>
+</Project>

+ 1 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props

@@ -112,5 +112,6 @@
     <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
   </PropertyGroup>
 
+  <Import Project="$(MSBuildThisFileDirectory)\Android.props" Condition=" '$(GodotTargetPlatform)' == 'android' " />
   <Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " />
 </Project>

+ 24 - 1
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -245,7 +245,6 @@ namespace GodotTools.Export
                     {
                         publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet",
                             $"{buildConfig}-{runtimeIdentifier}");
-
                     }
 
                     outputPaths.Add(publishOutputDir);
@@ -322,6 +321,30 @@ namespace GodotTools.Export
                             {
                                 if (embedBuildResults)
                                 {
+                                    if (platform == OS.Platforms.Android)
+                                    {
+                                        if (IsSharedObject(Path.GetFileName(path)))
+                                        {
+                                            AddSharedObject(path, tags: new string[] { arch },
+                                                Path.Join(projectDataDirName,
+                                                    Path.GetRelativePath(publishOutputDir,
+                                                        Path.GetDirectoryName(path)!)));
+
+                                            return;
+                                        }
+
+                                        static bool IsSharedObject(string fileName)
+                                        {
+                                            if (fileName.EndsWith(".so") || fileName.EndsWith(".a")
+                                             || fileName.EndsWith(".jar") || fileName.EndsWith(".dex"))
+                                            {
+                                                return true;
+                                            }
+
+                                            return false;
+                                        }
+                                    }
+
                                     string filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputDir, path));
                                     byte[] fileData = File.ReadAllBytes(path);
                                     string hash = Convert.ToBase64String(SHA512.HashData(fileData));

+ 165 - 11
modules/mono/mono_gd/gd_mono.cpp

@@ -61,6 +61,14 @@ hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config =
 hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr;
 hostfxr_close_fn hostfxr_close = nullptr;
 
+#ifndef TOOLS_ENABLED
+typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_create_delegate_fn)(void *hostHandle, unsigned int domainId, const char *entryPointAssemblyName, const char *entryPointTypeName, const char *entryPointMethodName, void **delegate);
+typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_initialize_fn)(const char *exePath, const char *appDomainFriendlyName, int propertyCount, const char **propertyKeys, const char **propertyValues, void **hostHandle, unsigned int *domainId);
+
+coreclr_create_delegate_fn coreclr_create_delegate = nullptr;
+coreclr_initialize_fn coreclr_initialize = nullptr;
+#endif
+
 #ifdef _WIN32
 static_assert(sizeof(char_t) == sizeof(char16_t));
 using HostFxrCharString = Char16String;
@@ -142,6 +150,56 @@ String find_hostfxr() {
 #endif
 }
 
+#ifndef TOOLS_ENABLED
+String find_monosgen() {
+#if defined(ANDROID_ENABLED)
+	// Android includes all native libraries in the libs directory of the APK
+	// so we assume it exists and use only the name to dlopen it.
+	return "libmonosgen-2.0.so";
+#else
+#if defined(WINDOWS_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("monosgen-2.0.dll");
+#elif defined(MACOS_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("libmonosgen-2.0.dylib");
+#elif defined(UNIX_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("libmonosgen-2.0.so");
+#else
+#error "Platform not supported (yet?)"
+#endif
+
+	if (FileAccess::exists(probe_path)) {
+		return probe_path;
+	}
+
+	return String();
+#endif
+}
+
+String find_coreclr() {
+#if defined(WINDOWS_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("coreclr.dll");
+#elif defined(MACOS_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("libcoreclr.dylib");
+#elif defined(UNIX_ENABLED)
+	String probe_path = GodotSharpDirs::get_api_assemblies_dir()
+								.path_join("libcoreclr.so");
+#else
+#error "Platform not supported (yet?)"
+#endif
+
+	if (FileAccess::exists(probe_path)) {
+		return probe_path;
+	}
+
+	return String();
+}
+#endif
+
 bool load_hostfxr(void *&r_hostfxr_dll_handle) {
 	String hostfxr_path = find_hostfxr();
 
@@ -182,6 +240,47 @@ bool load_hostfxr(void *&r_hostfxr_dll_handle) {
 			hostfxr_close);
 }
 
+#ifndef TOOLS_ENABLED
+bool load_coreclr(void *&r_coreclr_dll_handle) {
+	String coreclr_path = find_coreclr();
+
+	bool is_monovm = false;
+	if (coreclr_path.is_empty()) {
+		// Fallback to MonoVM (should have the same API as CoreCLR).
+		coreclr_path = find_monosgen();
+		is_monovm = true;
+	}
+
+	if (coreclr_path.is_empty()) {
+		return false;
+	}
+
+	const String coreclr_name = is_monovm ? "monosgen" : "coreclr";
+	print_verbose("Found " + coreclr_name + ": " + coreclr_path);
+
+	Error err = OS::get_singleton()->open_dynamic_library(coreclr_path, r_coreclr_dll_handle);
+
+	if (err != OK) {
+		return false;
+	}
+
+	void *lib = r_coreclr_dll_handle;
+
+	void *symbol = nullptr;
+
+	err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_initialize", symbol);
+	ERR_FAIL_COND_V(err != OK, false);
+	coreclr_initialize = (coreclr_initialize_fn)symbol;
+
+	err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_create_delegate", symbol);
+	ERR_FAIL_COND_V(err != OK, false);
+	coreclr_create_delegate = (coreclr_create_delegate_fn)symbol;
+
+	return (coreclr_initialize &&
+			coreclr_create_delegate);
+}
+#endif
+
 #ifdef TOOLS_ENABLED
 load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) {
 	hostfxr_handle cxt = nullptr;
@@ -339,6 +438,56 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle)
 }
 #endif
 
+#ifndef TOOLS_ENABLED
+String make_tpa_list() {
+	String tpa_list;
+
+#if defined(WINDOWS_ENABLED)
+	String separator = ";";
+#else
+	String separator = ":";
+#endif
+
+	String assemblies_dir = GodotSharpDirs::get_api_assemblies_dir();
+	PackedStringArray files = DirAccess::get_files_at(assemblies_dir);
+	for (const String &file : files) {
+		tpa_list += assemblies_dir.path_join(file);
+		tpa_list += separator;
+	}
+
+	return tpa_list;
+}
+
+godot_plugins_initialize_fn initialize_coreclr_and_godot_plugins(bool &r_runtime_initialized) {
+	godot_plugins_initialize_fn godot_plugins_initialize = nullptr;
+
+	String assembly_name = path::get_csharp_project_name();
+
+	String tpa_list = make_tpa_list();
+	const char *prop_keys[] = { HOSTFXR_STR("TRUSTED_PLATFORM_ASSEMBLIES") };
+	const char *prop_values[] = { get_data(str_to_hostfxr(tpa_list)) };
+	int nprops = sizeof(prop_keys) / sizeof(prop_keys[0]);
+
+	void *coreclr_handle = nullptr;
+	unsigned int domain_id = 0;
+	int rc = coreclr_initialize(nullptr, nullptr, nprops, (const char **)&prop_keys, (const char **)&prop_values, &coreclr_handle, &domain_id);
+	ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to initialize CoreCLR.");
+
+	r_runtime_initialized = true;
+
+	print_verbose(".NET: CoreCLR initialized");
+
+	coreclr_create_delegate(coreclr_handle, domain_id,
+			get_data(str_to_hostfxr(assembly_name)),
+			HOSTFXR_STR("GodotPlugins.Game.Main"),
+			HOSTFXR_STR("InitializeFromGameProject"),
+			(void **)&godot_plugins_initialize);
+	ERR_FAIL_NULL_V_MSG(godot_plugins_initialize, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer");
+
+	return godot_plugins_initialize;
+}
+#endif
+
 } // namespace
 
 bool GDMono::should_initialize() {
@@ -382,14 +531,21 @@ void GDMono::initialize() {
 	}
 #endif
 
-	if (!load_hostfxr(hostfxr_dll_handle)) {
+	if (load_hostfxr(hostfxr_dll_handle)) {
+		godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized);
+		ERR_FAIL_NULL(godot_plugins_initialize);
+	} else {
 #if !defined(TOOLS_ENABLED)
-		godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle);
-
-		if (godot_plugins_initialize != nullptr) {
-			is_native_aot = true;
-			runtime_initialized = true;
+		if (load_coreclr(coreclr_dll_handle)) {
+			godot_plugins_initialize = initialize_coreclr_and_godot_plugins(runtime_initialized);
 		} else {
+			godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle);
+			if (godot_plugins_initialize != nullptr) {
+				runtime_initialized = true;
+			}
+		}
+
+		if (godot_plugins_initialize == nullptr) {
 			ERR_FAIL_MSG(".NET: Failed to load hostfxr");
 		}
 #else
@@ -400,11 +556,6 @@ void GDMono::initialize() {
 #endif
 	}
 
-	if (!is_native_aot) {
-		godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized);
-		ERR_FAIL_NULL(godot_plugins_initialize);
-	}
-
 	int32_t interop_funcs_size = 0;
 	const void **interop_funcs = godotsharp::get_runtime_interop_funcs(interop_funcs_size);
 
@@ -553,6 +704,9 @@ GDMono::~GDMono() {
 	if (hostfxr_dll_handle) {
 		OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle);
 	}
+	if (coreclr_dll_handle) {
+		OS::get_singleton()->close_dynamic_library(coreclr_dll_handle);
+	}
 
 	finalizing_scripts_domain = false;
 	runtime_initialized = false;

+ 1 - 1
modules/mono/mono_gd/gd_mono.h

@@ -64,7 +64,7 @@ class GDMono {
 	bool finalizing_scripts_domain = false;
 
 	void *hostfxr_dll_handle = nullptr;
-	bool is_native_aot = false;
+	void *coreclr_dll_handle = nullptr;
 
 	String project_assembly_path;
 	uint64_t project_assembly_modified_time = 0;

二進制
modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar


+ 10 - 14
platform/android/export/export_plugin.cpp

@@ -2381,19 +2381,6 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 #ifdef MODULE_MONO_ENABLED
 	// Android export is still a work in progress, keep a message as a warning.
 	err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n";
-
-	bool unsupported_arch = false;
-	Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
-	for (ABI abi : enabled_abis) {
-		if (abi.arch != "arm64" && abi.arch != "x86_64") {
-			err += vformat(TTR("Android architecture %s not supported in C# projects."), abi.arch) + "\n";
-			unsupported_arch = true;
-		}
-	}
-	if (unsupported_arch) {
-		r_error = err;
-		return false;
-	}
 #endif
 
 	// Look for export templates (first official, and if defined custom templates).
@@ -3201,6 +3188,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 		PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);
 #endif // DISABLE_DEPRECATED
 
+		bool has_dotnet_project = false;
 		Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
 		for (int i = 0; i < export_plugins.size(); i++) {
 			if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
@@ -3218,6 +3206,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 				PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);
 				android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);
 			}
+
+			PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug);
+			if (features.has("dotnet")) {
+				has_dotnet_project = true;
+			}
 		}
 
 		bool clean_build_required = _is_clean_build_required(p_preset);
@@ -3231,12 +3224,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 			cmdline.push_back("clean");
 		}
 
+		String edition = has_dotnet_project ? "Mono" : "Standard";
 		String build_type = p_debug ? "Debug" : "Release";
 		if (export_format == EXPORT_FORMAT_AAB) {
 			String bundle_build_command = vformat("bundle%s", build_type);
 			cmdline.push_back(bundle_build_command);
 		} else if (export_format == EXPORT_FORMAT_APK) {
-			String apk_build_command = vformat("assemble%s", build_type);
+			String apk_build_command = vformat("assemble%s%s", edition, build_type);
 			cmdline.push_back(apk_build_command);
 		}
 
@@ -3319,6 +3313,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 		copy_args.push_back("-p"); // argument to specify the start directory.
 		copy_args.push_back(build_path); // start directory.
 
+		copy_args.push_back("-Pexport_edition=" + edition.to_lower());
+
 		copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());
 
 		String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";

+ 34 - 6
platform/android/java/app/build.gradle

@@ -29,6 +29,8 @@ allprojects {
 configurations {
     // Initializes a placeholder for the devImplementation dependency configuration.
     devImplementation {}
+    // Initializes a placeholder for the monoImplementation dependency configuration.
+    monoImplementation {}
 }
 
 dependencies {
@@ -42,9 +44,9 @@ dependencies {
     } else {
         // Godot gradle build mode. In this scenario this project is the only one around and the Godot
         // library is available through the pre-generated godot-lib.*.aar android archive files.
-        debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar'])
-        devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar'])
-        releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
+        debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
+        devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar'])
+        releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
     }
 
     // Godot user plugins remote dependencies
@@ -60,6 +62,12 @@ dependencies {
     if (pluginsBinaries != null && pluginsBinaries.size() > 0) {
         implementation files(pluginsBinaries)
     }
+
+    // .NET dependencies
+    String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar'
+    if (file(jar).exists()) {
+        monoImplementation files(jar)
+    }
 }
 
 android {
@@ -155,6 +163,10 @@ android {
         }
     }
 
+    buildFeatures {
+        buildConfig = true
+    }
+
     buildTypes {
 
         debug {
@@ -192,6 +204,13 @@ android {
         }
     }
 
+    flavorDimensions 'edition'
+
+    productFlavors {
+        standard {}
+        mono {}
+    }
+
     sourceSets {
         main {
             manifest.srcFile 'AndroidManifest.xml'
@@ -207,7 +226,8 @@ android {
 
     applicationVariants.all { variant ->
         variant.outputs.all { output ->
-            output.outputFileName = "android_${variant.name}.apk"
+            String filenameSuffix = variant.flavorName == "mono" ? variant.name : variant.buildType.name
+            output.outputFileName = "android_${filenameSuffix}.apk"
         }
     }
 }
@@ -220,12 +240,20 @@ task copyAndRenameBinary(type: Copy) {
 
     String exportPath = getExportPath()
     String exportFilename = getExportFilename()
+    String exportEdition = getExportEdition()
     String exportBuildType = getExportBuildType()
+    String exportBuildTypeCapitalized = exportBuildType.capitalize()
     String exportFormat = getExportFormat()
 
     boolean isAab = exportFormat == "aab"
-    String sourceFilepath = isAab ? "$buildDir/outputs/bundle/$exportBuildType/build-${exportBuildType}.aab" : "$buildDir/outputs/apk/$exportBuildType/android_${exportBuildType}.apk"
-    String sourceFilename = isAab ? "build-${exportBuildType}.aab" : "android_${exportBuildType}.apk"
+    boolean isMono = exportEdition == "mono"
+    String filenameSuffix = exportBuildType
+    if (isMono) {
+        filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : "${exportEdition}${exportBuildTypeCapitalized}"
+    }
+
+    String sourceFilename = isAab ? "build-${filenameSuffix}.aab" : "android_${filenameSuffix}.apk"
+    String sourceFilepath = isAab ? "$buildDir/outputs/bundle/${exportEdition}${exportBuildTypeCapitalized}/$sourceFilename" : "$buildDir/outputs/apk/$exportEdition/$exportBuildType/$sourceFilename"
 
     from sourceFilepath
     into exportPath

+ 8 - 0
platform/android/java/app/config.gradle

@@ -224,6 +224,14 @@ ext.getExportFilename = {
     return exportFilename
 }
 
+ext.getExportEdition = {
+    String exportEdition = project.hasProperty("export_edition") ? project.property("export_edition") : ""
+    if (exportEdition == null || exportEdition.isEmpty()) {
+        exportEdition = "standard"
+    }
+    return exportEdition
+}
+
 ext.getExportBuildType = {
     String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : ""
     if (exportBuildType == null || exportBuildType.isEmpty()) {

+ 15 - 0
platform/android/java/app/src/com/godot/game/GodotApp.java

@@ -33,14 +33,29 @@ package com.godot.game;
 import org.godotengine.godot.GodotActivity;
 
 import android.os.Bundle;
+import android.util.Log;
 
 import androidx.core.splashscreen.SplashScreen;
 
+import com.godot.game.BuildConfig;
+
 /**
  * Template activity for Godot Android builds.
  * Feel free to extend and modify this class for your custom logic.
  */
 public class GodotApp extends GodotActivity {
+	static {
+		// .NET libraries.
+		if (BuildConfig.FLAVOR.equals("mono")) {
+			try {
+				Log.v("GODOT", "Loading System.Security.Cryptography.Native.Android library");
+				System.loadLibrary("System.Security.Cryptography.Native.Android");
+			} catch (UnsatisfiedLinkError e) {
+				Log.e("GODOT", "Unable to load System.Security.Cryptography.Native.Android library");
+			}
+		}
+	}
+
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		SplashScreen.installSplashScreen(this);

+ 32 - 8
platform/android/java/build.gradle

@@ -30,6 +30,7 @@ ext {
         "editor": ["dev", "debug", "release"],
         "template": ["dev", "debug", "release"]
     ]
+    supportedEditions = ["standard", "mono"]
 
     // Used by gradle to specify which architecture to build for by default when running
     // `./gradlew build` (this command is usually used by Android Studio).
@@ -53,7 +54,7 @@ def getSconsTaskName(String flavor, String buildType, String abi) {
  * The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
  */
 task zipGradleBuild(type: Zip) {
-    onlyIf { generateGodotTemplates.state.executed || generateDevTemplate.state.executed }
+    onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed || generateDevTemplate.state.executed }
     doFirst {
         logger.lifecycle("Generating Godot gradle build template")
     }
@@ -94,15 +95,22 @@ def templateExcludedBuildTask() {
 /**
  * Generates the build tasks for the given flavor
  * @param flavor Must be one of the supported flavors ('template' / 'editor')
+ * @param edition Must be one of the supported editions ('standard' / 'mono')
  * @param androidDistro Must be one of the supported Android distributions ('android' / 'horizonos')
  */
-def generateBuildTasks(String flavor = "template", String androidDistro = "android") {
+def generateBuildTasks(String flavor = "template", String edition = "standard", String androidDistro = "android") {
     if (!supportedFlavors.contains(flavor)) {
         throw new GradleException("Invalid build flavor: $flavor")
     }
     if (!supportedAndroidDistributions.contains(androidDistro)) {
         throw new GradleException("Invalid Android distribution: $androidDistro")
     }
+    if (!supportedEditions.contains(edition)) {
+        throw new GradleException("Invalid build edition: $edition")
+    }
+    if (edition == "mono" && flavor != "template") {
+        throw new GradleException("'mono' edition only supports the 'template' flavor.")
+    }
 
     String capitalizedAndroidDistro = androidDistro.capitalize()
     def buildTasks = []
@@ -126,6 +134,7 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro
             && targetLibs.listFiles().length > 0)) {
 
             String capitalizedTarget = target.capitalize()
+            String capitalizedEdition = edition.capitalize()
             if (isTemplate) {
                 // Copy the Godot android library archive file into the app module libs directory.
                 // Depends on the library build task to ensure the AAR file is generated prior to copying.
@@ -157,15 +166,16 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro
 
                 // Copy the generated binary template into the Godot bin directory.
                 // Depends on the app build task to ensure the binary is generated prior to copying.
-                String copyBinaryTaskName = "copy${capitalizedTarget}BinaryToBin"
+                String copyBinaryTaskName = "copy${capitalizedEdition}${capitalizedTarget}BinaryToBin"
                 if (tasks.findByName(copyBinaryTaskName) != null) {
                     buildTasks += tasks.getByName(copyBinaryTaskName)
                 } else {
                     buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) {
-                        dependsOn ":app:assemble${capitalizedTarget}"
-                        from("app/build/outputs/apk/${target}")
+                        String filenameSuffix = edition == "mono" ? "${edition}${capitalizedTarget}" : target
+                        dependsOn ":app:assemble${capitalizedEdition}${capitalizedTarget}"
+                        from("app/build/outputs/apk/${edition}/${target}")
                         into(binDir)
-                        include("android_${target}.apk")
+                        include("android_${filenameSuffix}.apk")
                     }
                 }
             } else {
@@ -212,7 +222,7 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro
  */
 task generateGodotEditor {
     gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
-    dependsOn = generateBuildTasks("editor", "android")
+    dependsOn = generateBuildTasks("editor", "standard", "android")
 }
 
 /**
@@ -224,7 +234,7 @@ task generateGodotEditor {
  */
 task generateGodotHorizonOSEditor {
     gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
-    dependsOn = generateBuildTasks("editor", "horizonos")
+    dependsOn = generateBuildTasks("editor", "standard", "horizonos")
 }
 
 /**
@@ -237,6 +247,17 @@ task generateGodotTemplates {
     finalizedBy 'zipGradleBuild'
 }
 
+/**
+ * Master task used to coordinate the tasks defined above to generate the set of Godot templates
+ * for the 'mono' edition of the engine.
+ */
+task generateGodotMonoTemplates {
+    gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
+    dependsOn = generateBuildTasks("template", "mono")
+
+    finalizedBy 'zipGradleBuild'
+}
+
 /**
  * Generates the same output as generateGodotTemplates but with dev symbols
  */
@@ -295,6 +316,9 @@ task cleanGodotTemplates(type: Delete) {
     delete("$binDir/android_debug.apk")
     delete("$binDir/android_dev.apk")
     delete("$binDir/android_release.apk")
+    delete("$binDir/android_monoDebug.apk")
+    delete("$binDir/android_monoDev.apk")
+    delete("$binDir/android_monoRelease.apk")
     delete("$binDir/android_source.zip")
     delete("$binDir/godot-lib.template_debug.aar")
     delete("$binDir/godot-lib.template_debug.dev.aar")