diff --git a/modules/mono/config.py b/modules/mono/config.py index 9846d60c335c..859d77b262c9 100644 --- a/modules/mono/config.py +++ b/modules/mono/config.py @@ -1,6 +1,6 @@ # Prior to .NET Core, we supported these: ["windows", "macos", "linuxbsd", "android", "web", "ios"] # Eventually support for each them should be added back. -supported_platforms = ["windows", "macos", "linuxbsd", "android"] +supported_platforms = ["windows", "macos", "linuxbsd", "android", "ios"] def can_build(env, platform): diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index 663eb14f075a..ad3a10ba4937 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -29,5 +29,7 @@ Sdk\SdkPackageVersions.props + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index b35cec64f3db..b6c72bce9dfb 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -59,6 +59,18 @@ + + ios + android + web + + linuxbsd + linuxbsd + macos + windows + + + linuxbsd linuxbsd @@ -97,4 +109,6 @@ $(GodotDefineConstants);$(DefineConstants) + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets index 4dcc96a1f67a..29ef76a5e82f 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -20,4 +20,8 @@ + + + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.props new file mode 100644 index 000000000000..e3c953ccaca4 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.props @@ -0,0 +1,8 @@ + + + true + true + true + false + + diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets new file mode 100644 index 000000000000..d8129a66527e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets @@ -0,0 +1,58 @@ + + + + + + + + + true + true + /Applications/Xcode.app/Contents/Developer + $([MSBuild]::EnsureTrailingSlash('$(XCodePath)')) + + + + + + + + + + + + + + $(XcodeSelect) + $([MSBuild]::EnsureTrailingSlash('$(XCodePath)')) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index f3c8e89dfff9..1e5d7c901ee5 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -25,6 +25,9 @@ namespace GodotTools.ProjectEditor mainGroup.AddProperty("TargetFramework", "net6.0"); mainGroup.AddProperty("EnableDynamicLoading", "true"); + var net8 = mainGroup.AddProperty("TargetFramework", "net8.0"); + net8.Condition = " '$(GodotTargetPlatform)' == 'ios' "; + string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); // If the name is not a valid namespace, manually set RootNamespace to a sanitized one. diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 9bb4fd153b28..2a6090eb6d60 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; @@ -67,7 +68,7 @@ namespace GodotTools.Build { BuildStarted?.Invoke(buildInfo); - // Required in order to update the build tasks list + // Required in order to update the build tasks list. Internal.GodotMainIteration(); try @@ -162,7 +163,7 @@ namespace GodotTools.Build { BuildStarted?.Invoke(buildInfo); - // Required in order to update the build tasks list + // Required in order to update the build tasks list. Internal.GodotMainIteration(); try @@ -317,6 +318,45 @@ namespace GodotTools.Build ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, platform, runtimeIdentifier, publishOutputDir, includeDebugSymbols)); + public static bool GenerateXCFrameworkBlocking( + List outputPaths, + string xcFrameworkPath) + { + using var pr = new EditorProgress("generate_xcframework", "Generating XCFramework...", 1); + + pr.Step("Running xcodebuild -create-xcframework", 0); + + if (!GenerateXCFramework(outputPaths, xcFrameworkPath)) + { + ShowBuildErrorDialog("Failed to generate XCFramework"); + return false; + } + + return true; + } + + private static bool GenerateXCFramework(List outputPaths, string xcFrameworkPath) + { + // Required in order to update the build tasks list. + Internal.GodotMainIteration(); + + try + { + int exitCode = BuildSystem.GenerateXCFramework(outputPaths, xcFrameworkPath, StdOutputReceived, StdErrorReceived); + + if (exitCode != 0) + PrintVerbose( + $"xcodebuild create-xcframework exited with code: {exitCode}."); + + return exitCode == 0; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return false; + } + } + public static bool EditorBuildCallback() { if (!File.Exists(GodotSharpDirs.ProjectCsProjPath)) diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 8a292fd73a0d..57b5598a787d 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -9,7 +9,9 @@ using System.Text; using System.Threading.Tasks; using Godot; using GodotTools.BuildLogger; +using GodotTools.Internals; using GodotTools.Utils; +using Directory = GodotTools.Utils.Directory; namespace GodotTools.Build { @@ -293,5 +295,81 @@ namespace GodotTools.Build foreach (string env in platformEnvironmentVariables) environmentVariables.Remove(env); } + + private static Process DoGenerateXCFramework(List outputPaths, string xcFrameworkPath, + Action stdOutHandler, Action stdErrHandler) + { + if (Directory.Exists(xcFrameworkPath)) + { + Directory.Delete(xcFrameworkPath, true); + } + + var startInfo = new ProcessStartInfo("xcrun"); + + BuildXCFrameworkArguments(outputPaths, xcFrameworkPath, startInfo.ArgumentList); + + string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Packaging: ")).ToString(); + stdOutHandler?.Invoke(launchMessage); + if (Godot.OS.IsStdOutVerbose()) + Console.WriteLine(launchMessage); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + + if (OperatingSystem.IsWindows()) + { + startInfo.StandardOutputEncoding = Encoding.UTF8; + startInfo.StandardErrorEncoding = Encoding.UTF8; + } + + // Needed when running from Developer Command Prompt for VS. + RemovePlatformVariable(startInfo.EnvironmentVariables); + + var process = new Process { StartInfo = startInfo }; + + if (stdOutHandler != null) + process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data); + if (stdErrHandler != null) + process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data); + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + return process; + } + + public static int GenerateXCFramework(List outputPaths, string xcFrameworkPath, Action stdOutHandler, Action stdErrHandler) + { + using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler)) + { + process.WaitForExit(); + + return process.ExitCode; + } + } + + private static void BuildXCFrameworkArguments(List outputPaths, + string xcFrameworkPath, Collection arguments) + { + var baseDylib = $"{GodotSharpDirs.ProjectAssemblyName}.dylib"; + var baseSym = $"{GodotSharpDirs.ProjectAssemblyName}.framework.dSYM"; + + arguments.Add("xcodebuild"); + arguments.Add("-create-xcframework"); + + foreach (var outputPath in outputPaths) + { + arguments.Add("-library"); + arguments.Add(Path.Combine(outputPath, baseDylib)); + arguments.Add("-debug-symbols"); + arguments.Add(Path.Combine(outputPath, baseSym)); + } + + arguments.Add("-output"); + arguments.Add(xcFrameworkPath); + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index b98df190ca69..595c9a126841 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -6,9 +6,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using GodotTools.Build; -using GodotTools.Core; using GodotTools.Internals; -using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; @@ -77,7 +75,7 @@ namespace GodotTools.Export $"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path)); - // TODO What if the source file is not part of the game's C# project + // TODO: What if the source file is not part of the game's C# project? bool includeScriptsContent = (bool)GetOption("dotnet/include_scripts_content"); @@ -89,7 +87,7 @@ namespace GodotTools.Export // Because of this, we add a file which contains a line break. AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false); - // Tell the Godot exporter that we already took care of the file + // Tell the Godot exporter that we already took care of the file. Skip(); } } @@ -119,7 +117,7 @@ namespace GodotTools.Export private void _ExportBeginImpl(string[] features, bool isDebug, string path, long flags) { - _ = flags; // Unused + _ = flags; // Unused. if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; @@ -127,115 +125,261 @@ namespace GodotTools.Export if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported."); - if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android } + if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS } .Contains(platform)) { throw new NotImplementedException("Target platform not yet implemented."); } - string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; + PublishConfig publishConfig = new() + { + BuildConfig = isDebug ? "ExportDebug" : "ExportRelease", + IncludeDebugSymbols = (bool)GetOption("dotnet/include_debug_symbols"), + RidOS = DetermineRuntimeIdentifierOS(platform), + Archs = new List(), + UseTempDir = platform != OS.Platforms.iOS, // xcode project links directly to files in the publish dir, so use one that sticks around. + BundleOutputs = true, + }; - bool includeDebugSymbols = (bool)GetOption("dotnet/include_debug_symbols"); - - var archs = new List(); if (features.Contains("x86_64")) { - archs.Add("x86_64"); + publishConfig.Archs.Add("x86_64"); } + if (features.Contains("x86_32")) { - archs.Add("x86_32"); + publishConfig.Archs.Add("x86_32"); } + if (features.Contains("arm64")) { - archs.Add("arm64"); + publishConfig.Archs.Add("arm64"); } + if (features.Contains("arm32")) { - archs.Add("arm32"); + publishConfig.Archs.Add("arm32"); } + if (features.Contains("universal")) { if (platform == OS.Platforms.MacOS) { - archs.Add("x86_64"); - archs.Add("arm64"); + publishConfig.Archs.Add("x86_64"); + publishConfig.Archs.Add("arm64"); } } + var targets = new List { publishConfig }; + + if (platform == OS.Platforms.iOS) + { + targets.Add(new PublishConfig + { + BuildConfig = publishConfig.BuildConfig, + Archs = new List { "arm64", "x86_64" }, + BundleOutputs = false, + IncludeDebugSymbols = publishConfig.IncludeDebugSymbols, + RidOS = OS.DotNetOS.iOSSimulator, + UseTempDir = true, + }); + } + + List outputPaths = new(); + bool embedBuildResults = (bool)GetOption("dotnet/embed_build_outputs") || features.Contains("android"); - foreach (var arch in archs) + foreach (PublishConfig config in targets) { - string ridOS = DetermineRuntimeIdentifierOS(platform); - string ridArch = DetermineRuntimeIdentifierArch(arch); - string runtimeIdentifier = $"{ridOS}-{ridArch}"; - string projectDataDirName = $"data_{GodotSharpDirs.CSharpProjectName}_{platform}_{arch}"; - if (platform == OS.Platforms.MacOS) + string ridOS = config.RidOS; + string buildConfig = config.BuildConfig; + bool includeDebugSymbols = config.IncludeDebugSymbols; + + foreach (string arch in config.Archs) { - projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName); - } - - // Create temporary publish output directory - - string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", - $"{System.Environment.ProcessId}-{buildConfig}-{runtimeIdentifier}"); - - _tempFolders.Add(publishOutputTempDir); - - if (!Directory.Exists(publishOutputTempDir)) - Directory.CreateDirectory(publishOutputTempDir); - - // Execute dotnet publish - - if (!BuildManager.PublishProjectBlocking(buildConfig, platform, - runtimeIdentifier, publishOutputTempDir, includeDebugSymbols)) - { - throw new InvalidOperationException("Failed to build project."); - } - - string soExt = ridOS switch - { - OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", - OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", - _ => "so" - }; - - if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll")) - // NativeAOT shared library output - && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"))) - { - throw new NotSupportedException( - "Publish succeeded but project assembly not found in the output directory"); - } - - var manifest = new StringBuilder(); - - // Add to the exported project shared object list or packed resources. - foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) - { - if (embedBuildResults) + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; + string projectDataDirName = $"data_{GodotSharpDirs.CSharpProjectName}_{platform}_{arch}"; + if (platform == OS.Platforms.MacOS) { - var filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputTempDir, file)); - var fileData = File.ReadAllBytes(file); - var hash = Convert.ToBase64String(SHA512.HashData(fileData)); + projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName); + } - manifest.Append($"{filePath}\t{hash}\n"); + // Create temporary publish output directory. + string publishOutputDir; - AddFile($"res://.godot/mono/publish/{arch}/{filePath}", fileData, false); + if (config.UseTempDir) + { + publishOutputDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{System.Environment.ProcessId}-{buildConfig}-{runtimeIdentifier}"); + _tempFolders.Add(publishOutputDir); } else { - AddSharedObject(file, tags: null, - Path.Join(projectDataDirName, - Path.GetRelativePath(publishOutputTempDir, Path.GetDirectoryName(file)))); + publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet", + $"{buildConfig}-{runtimeIdentifier}"); + + } + + outputPaths.Add(publishOutputDir); + + if (!Directory.Exists(publishOutputDir)) + Directory.CreateDirectory(publishOutputDir); + + // Execute dotnet publish. + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputDir, includeDebugSymbols)) + { + throw new InvalidOperationException("Failed to build project."); + } + + string soExt = ridOS switch + { + OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", + OS.DotNetOS.OSX or OS.DotNetOS.iOS or OS.DotNetOS.iOSSimulator => "dylib", + _ => "so" + }; + + string assemblyPath = Path.Combine(publishOutputDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll"); + string nativeAotPath = Path.Combine(publishOutputDir, + $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"); + + if (!File.Exists(assemblyPath) && !File.Exists(nativeAotPath)) + { + throw new NotSupportedException( + $"Publish succeeded but project assembly not found at '{assemblyPath}' or '{nativeAotPath}'."); + } + + // For ios simulator builds, skip packaging the build outputs. + if (!config.BundleOutputs) + continue; + + var manifest = new StringBuilder(); + + // Add to the exported project shared object list or packed resources. + RecursePublishContents(publishOutputDir, + filterDir: dir => + { + if (platform == OS.Platforms.iOS) + { + // Exclude dsym folders. + return !dir.EndsWith(".dsym", StringComparison.InvariantCultureIgnoreCase); + } + + return true; + }, + filterFile: file => + { + if (platform == OS.Platforms.iOS) + { + // Exclude the dylib artifact, since it's included separately as an xcframework. + return Path.GetFileName(file) != $"{GodotSharpDirs.ProjectAssemblyName}.dylib"; + } + + return true; + }, + recurseDir: dir => + { + if (platform == OS.Platforms.iOS) + { + // Don't recurse into dsym folders. + return !dir.EndsWith(".dsym", StringComparison.InvariantCultureIgnoreCase); + } + + return true; + }, + addEntry: (path, isFile) => + { + // We get called back for both directories and files, but we only package files for now. + if (isFile) + { + if (embedBuildResults) + { + string filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputDir, path)); + byte[] fileData = File.ReadAllBytes(path); + string hash = Convert.ToBase64String(SHA512.HashData(fileData)); + + manifest.Append($"{filePath}\t{hash}\n"); + + AddFile($"res://.godot/mono/publish/{arch}/{filePath}", fileData, false); + } + else + { + if (platform == OS.Platforms.iOS && path.EndsWith(".dat")) + { + AddIosBundleFile(path); + } + else + { + AddSharedObject(path, tags: null, + Path.Join(projectDataDirName, + Path.GetRelativePath(publishOutputDir, + Path.GetDirectoryName(path)))); + } + } + } + }); + + if (embedBuildResults) + { + byte[] fileData = Encoding.Default.GetBytes(manifest.ToString()); + AddFile($"res://.godot/mono/publish/{arch}/.dotnet-publish-manifest", fileData, false); } } + } - if (embedBuildResults) + if (platform == OS.Platforms.iOS) + { + if (outputPaths.Count > 2) { - var fileData = Encoding.Default.GetBytes(manifest.ToString()); - AddFile($"res://.godot/mono/publish/{arch}/.dotnet-publish-manifest", fileData, false); + // lipo the simulator binaries together + // TODO: Move this to the native lipo implementation we have in the macos export plugin. + var lipoArgs = new List(); + lipoArgs.Add("-create"); + lipoArgs.AddRange(outputPaths.Skip(1).Select(x => Path.Combine(x, $"{GodotSharpDirs.ProjectAssemblyName}.dylib"))); + lipoArgs.Add("-output"); + lipoArgs.Add(Path.Combine(outputPaths[1], $"{GodotSharpDirs.ProjectAssemblyName}.dylib")); + + int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs); + if (lipoExitCode != 0) + throw new InvalidOperationException($"Command 'lipo' exited with code: {lipoExitCode}."); + + outputPaths.RemoveRange(2, outputPaths.Count - 2); + } + + var xcFrameworkPath = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig, + $"{GodotSharpDirs.ProjectAssemblyName}.xcframework"); + if (!BuildManager.GenerateXCFrameworkBlocking(outputPaths, + Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig, xcFrameworkPath))) + { + throw new InvalidOperationException("Failed to generate xcframework."); + } + + AddIosEmbeddedFramework(xcFrameworkPath); + } + } + + private static void RecursePublishContents(string path, Func filterDir, + Func filterFile, Func recurseDir, + Action addEntry) + { + foreach (string file in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly)) + { + if (filterFile(file)) + { + addEntry(file, true); + } + } + + foreach (string dir in Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly)) + { + if (filterDir(dir)) + { + addEntry(dir, false); + } + else if (recurseDir(dir)) + { + RecursePublishContents(dir, filterDir, filterFile, recurseDir, addEntry); } } } @@ -304,5 +448,15 @@ namespace GodotTools.Export platform = null; return false; } + + private struct PublishConfig + { + public bool UseTempDir; + public bool BundleOutputs; + public string RidOS; + public List Archs; + public string BuildConfig; + public bool IncludeDebugSymbols; + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index 55b413453d52..67891a0594bf 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -118,6 +118,16 @@ namespace GodotTools.Internals } } + public static string ProjectBaseOutputPath + { + get + { + if (_projectCsProjPath == null) + DetermineProjectLocation(); + return Path.Combine(Path.GetDirectoryName(_projectCsProjPath)!, ".godot", "mono", "temp", "bin"); + } + } + public static string LogsDirPathFor(string solution, string configuration) => Path.Combine(BuildLogsDirs, $"{solution.Md5Text()}_{configuration}"); diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index bff0c0df7c9b..c24b730c89a8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -56,6 +56,7 @@ namespace GodotTools.Utils public const string Win10 = "win10"; public const string Android = "android"; public const string iOS = "ios"; + public const string iOSSimulator = "iossimulator"; public const string Browser = "browser"; } diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 247968e25194..23f2f2ff13e9 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -322,7 +322,7 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) #if defined(WINDOWS_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dll"); -#elif defined(MACOS_ENABLED) +#elif defined(MACOS_ENABLED) || defined(IOS_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dylib"); #elif defined(UNIX_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".so"); @@ -330,23 +330,19 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) #error "Platform not supported (yet?)" #endif - if (FileAccess::exists(native_aot_so_path)) { - Error err = OS::get_singleton()->open_dynamic_library(native_aot_so_path, r_aot_dll_handle); + Error err = OS::get_singleton()->open_dynamic_library(native_aot_so_path, r_aot_dll_handle); - if (err != OK) { - return nullptr; - } - - void *lib = r_aot_dll_handle; - - void *symbol = nullptr; - - err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "godotsharp_game_main_init", symbol); - ERR_FAIL_COND_V(err != OK, nullptr); - return (godot_plugins_initialize_fn)symbol; + if (err != OK) { + return nullptr; } - return nullptr; + void *lib = r_aot_dll_handle; + + void *symbol = nullptr; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "godotsharp_game_main_init", symbol); + ERR_FAIL_COND_V(err != OK, nullptr); + return (godot_plugins_initialize_fn)symbol; } #endif @@ -376,11 +372,13 @@ void GDMono::initialize() { godot_plugins_initialize_fn godot_plugins_initialize = nullptr; +#if !defined(IOS_ENABLED) // Check that the .NET assemblies directory exists before trying to use it. if (!DirAccess::exists(GodotSharpDirs::get_api_assemblies_dir())) { OS::get_singleton()->alert(vformat(RTR("Unable to find the .NET assemblies directory.\nMake sure the '%s' directory exists and contains the .NET assemblies."), GodotSharpDirs::get_api_assemblies_dir()), RTR(".NET assemblies not found")); ERR_FAIL_MSG(".NET: Assemblies not found"); } +#endif if (!load_hostfxr(hostfxr_dll_handle)) { #if !defined(TOOLS_ENABLED) diff --git a/modules/mono/mono_gd/support/ios_support.h b/modules/mono/mono_gd/support/ios_support.h deleted file mode 100644 index cb397c8b46b4..000000000000 --- a/modules/mono/mono_gd/support/ios_support.h +++ /dev/null @@ -1,50 +0,0 @@ -/**************************************************************************/ -/* ios_support.h */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#ifndef IOS_SUPPORT_H -#define IOS_SUPPORT_H - -#if defined(IOS_ENABLED) - -#include "core/string/ustring.h" - -namespace gdmono { -namespace ios { -namespace support { - -void initialize(); -void cleanup(); -} // namespace support -} // namespace ios -} // namespace gdmono - -#endif // IOS_ENABLED - -#endif // IOS_SUPPORT_H diff --git a/modules/mono/mono_gd/support/ios_support.mm b/modules/mono/mono_gd/support/ios_support.mm deleted file mode 100644 index df8b3e2626d4..000000000000 --- a/modules/mono/mono_gd/support/ios_support.mm +++ /dev/null @@ -1,150 +0,0 @@ -/**************************************************************************/ -/* ios_support.mm */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#include "ios_support.h" - -#if defined(IOS_ENABLED) - -#include "../gd_mono_marshal.h" - -#include "core/ustring.h" - -#import -#include - -// Implemented mostly following: https://github.com/mono/mono/blob/master/sdks/ios/app/runtime.m - -// Definition generated by the Godot exporter -extern "C" void gd_mono_setup_aot(); - -namespace gdmono { -namespace ios { -namespace support { - -void ios_mono_log_callback(const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data) { - os_log_info(OS_LOG_DEFAULT, "(%s %s) %s", log_domain, log_level, message); - if (fatal) { - os_log_info(OS_LOG_DEFAULT, "Exit code: %d.", 1); - exit(1); - } -} - -void initialize() { - mono_dllmap_insert(nullptr, "System.Native", nullptr, "__Internal", nullptr); - mono_dllmap_insert(nullptr, "System.IO.Compression.Native", nullptr, "__Internal", nullptr); - mono_dllmap_insert(nullptr, "System.Security.Cryptography.Native.Apple", nullptr, "__Internal", nullptr); - -#ifdef IOS_DEVICE - // This function is defined in an auto-generated source file - gd_mono_setup_aot(); -#endif - - mono_set_signal_chaining(true); - mono_set_crash_chaining(true); -} - -void cleanup() { -} -} // namespace support -} // namespace ios -} // namespace gdmono - -// The following are P/Invoke functions required by the monotouch profile of the BCL. -// These are P/Invoke functions and not internal calls, hence why they use -// 'mono_bool' and 'const char*' instead of 'MonoBoolean' and 'MonoString*'. - -#define GD_PINVOKE_EXPORT extern "C" __attribute__((visibility("default"))) - -GD_PINVOKE_EXPORT const char *xamarin_get_locale_country_code() { - NSLocale *locale = [NSLocale currentLocale]; - NSString *countryCode = [locale objectForKey:NSLocaleCountryCode]; - if (countryCode == nullptr) { - return strdup("US"); - } - return strdup([countryCode UTF8String]); -} - -GD_PINVOKE_EXPORT void xamarin_log(const uint16_t *p_unicode_message) { - int length = 0; - const uint16_t *ptr = p_unicode_message; - while (*ptr++) { - length += sizeof(uint16_t); - } - NSString *msg = [[NSString alloc] initWithBytes:p_unicode_message length:length encoding:NSUTF16LittleEndianStringEncoding]; - - os_log_info(OS_LOG_DEFAULT, "%{public}@", msg); -} - -GD_PINVOKE_EXPORT const char *xamarin_GetFolderPath(int p_folder) { - NSSearchPathDirectory dd = (NSSearchPathDirectory)p_folder; - NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:dd inDomains:NSUserDomainMask] lastObject]; - NSString *path = [url path]; - return strdup([path UTF8String]); -} - -GD_PINVOKE_EXPORT char *xamarin_timezone_get_local_name() { - NSTimeZone *tz = nil; - tz = [NSTimeZone localTimeZone]; - NSString *name = [tz name]; - return (name != nil) ? strdup([name UTF8String]) : strdup("Local"); -} - -GD_PINVOKE_EXPORT char **xamarin_timezone_get_names(uint32_t *p_count) { - NSArray *array = [NSTimeZone knownTimeZoneNames]; - *p_count = array.count; - char **result = (char **)malloc(sizeof(char *) * (*p_count)); - for (uint32_t i = 0; i < *p_count; i++) { - NSString *s = [array objectAtIndex:i]; - result[i] = strdup(s.UTF8String); - } - return result; -} - -GD_PINVOKE_EXPORT void *xamarin_timezone_get_data(const char *p_name, uint32_t *p_size) { // FIXME: uint32_t since Dec 2019, unsigned long before - NSTimeZone *tz = nil; - if (p_name) { - NSString *n = [[NSString alloc] initWithUTF8String:p_name]; - tz = [[NSTimeZone alloc] initWithName:n]; - } else { - tz = [NSTimeZone localTimeZone]; - } - NSData *data = [tz data]; - *p_size = [data length]; - void *result = malloc(*p_size); - memcpy(result, data.bytes, *p_size); - return result; -} - -GD_PINVOKE_EXPORT void xamarin_start_wwan(const char *p_uri) { - // FIXME: What's this for? No idea how to implement. - os_log_error(OS_LOG_DEFAULT, "Not implemented: 'xamarin_start_wwan'"); -} - -#endif // IOS_ENABLED diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index a8596c30a65b..ed92cac59334 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -1928,11 +1928,15 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { #ifdef MODULE_MONO_ENABLED - // Don't check for additional errors, as this particular error cannot be resolved. - r_error += TTR("Exporting to iOS is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target iOS with C#/Mono instead.") + "\n"; - r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n"; - return false; +#ifdef MACOS_ENABLED + // iOS export is still a work in progress, keep a message as a warning. + r_error += TTR("Exporting to iOS when using C#/.NET is experimental.") + "\n"; #else + // TODO: Remove this restriction when we don't rely on macOS tools to package up the native libraries anymore. + r_error += TTR("Exporting to iOS when using C#/.NET is experimental and requires macOS.") + "\n"; + return false; +#endif +#endif String err; bool valid = false; @@ -1963,7 +1967,6 @@ bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref &p_preset, String &r_error) const {