Browse Source

Add C# iOS support

This support is experimental and requires .NET 8

Known issues:
- Requires macOS due to use of lipo and xcodebuild
- arm64 simulator templates are not currently included
  in the official packaging
Andreia Gaita 1 year ago
parent
commit
ee9a735c26

+ 1 - 1
modules/mono/config.py

@@ -1,6 +1,6 @@
 # Prior to .NET Core, we supported these: ["windows", "macos", "linuxbsd", "android", "web", "ios"]
 # Prior to .NET Core, we supported these: ["windows", "macos", "linuxbsd", "android", "web", "ios"]
 # Eventually support for each them should be added back.
 # 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):
 def can_build(env, platform):

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

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

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

@@ -59,6 +59,18 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Auto-detect the target Godot platform if it was not specified. -->
   <!-- Auto-detect the target Godot platform if it was not specified. -->
+  <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' ">
+    <GodotTargetPlatform Condition=" $(RuntimeIdentifier.StartsWith('ios')) ">ios</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('android')) ">android</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('browser')) ">web</GodotTargetPlatform>
+
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('linux')) ">linuxbsd</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('freebsd')) ">linuxbsd</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('osx')) ">macos</GodotTargetPlatform>
+    <GodotTargetPlatform Condition=" '$(GodotTargetPlatform)' == '' and $(RuntimeIdentifier.StartsWith('win')) ">windows</GodotTargetPlatform>
+  </PropertyGroup>
+  
+  <!-- Auto-detect the target Godot platform if it was not specified and there's no runtime identifier information. -->
   <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' ">
   <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' ">
     <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">linuxbsd</GodotTargetPlatform>
     <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">linuxbsd</GodotTargetPlatform>
     <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">linuxbsd</GodotTargetPlatform>
     <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">linuxbsd</GodotTargetPlatform>
@@ -97,4 +109,6 @@
 
 
     <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
     <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
+
+  <Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " />
 </Project>
 </Project>

+ 4 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets

@@ -20,4 +20,8 @@
     <PackageReference Include="GodotSharp" Version="$(PackageVersion_GodotSharp)" />
     <PackageReference Include="GodotSharp" Version="$(PackageVersion_GodotSharp)" />
     <PackageReference Include="GodotSharpEditor" Version="$(PackageVersion_GodotSharp)" Condition=" '$(Configuration)' == 'Debug' " />
     <PackageReference Include="GodotSharpEditor" Version="$(PackageVersion_GodotSharp)" Condition=" '$(Configuration)' == 'Debug' " />
   </ItemGroup>
   </ItemGroup>
+
+  <!-- iOS-specific build targets -->
+  <Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.targets" Condition=" '$(GodotTargetPlatform)' == 'ios' " />
+
 </Project>
 </Project>

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

@@ -0,0 +1,8 @@
+<Project>
+  <PropertyGroup>
+    <PublishAot>true</PublishAot>
+    <PublishAotUsingRuntimePack>true</PublishAotUsingRuntimePack>
+    <UseNativeAOTRuntime>true</UseNativeAOTRuntime>
+    <TrimmerSingleWarn>false</TrimmerSingleWarn>
+  </PropertyGroup>
+</Project>

+ 58 - 0
modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets

@@ -0,0 +1,58 @@
+<Project>
+  <ItemGroup>
+    <TrimmerRootAssembly Include="GodotSharp" />
+    <TrimmerRootAssembly Include="$(TargetName)" />
+    <LinkerArg Include="-install_name '@rpath/$(TargetName)$(NativeBinaryExt)'" />
+  </ItemGroup>
+
+  <PropertyGroup>
+    <LinkStandardCPlusPlusLibrary>true</LinkStandardCPlusPlusLibrary>
+    <FindXCode Condition=" '$(XCodePath)' == '' and '$([MSBuild]::IsOsPlatform(OSX))' ">true</FindXCode>
+    <XCodePath Condition=" '$(XCodePath)' == '' ">/Applications/Xcode.app/Contents/Developer</XCodePath>
+    <XCodePath>$([MSBuild]::EnsureTrailingSlash('$(XCodePath)'))</XCodePath>
+  </PropertyGroup>
+  
+  <Target Name="PrepareBeforeIlcCompile"
+          BeforeTargets="IlcCompile">
+    
+    <Copy SourceFiles="%(ResolvedRuntimePack.PackageDirectory)/runtimes/$(RuntimeIdentifier)/native/icudt.dat" DestinationFolder="$(PublishDir)"/>
+
+    <!-- We need to find the path to Xcode so we can set manual linker args to the correct SDKs
+        Once https://github.com/dotnet/runtime/issues/88737 is released, we can take this out
+    -->
+
+    <Exec Command="xcrun xcode-select -p" ConsoleToMSBuild="true" Condition=" '$(FindXCode)' == 'true' ">
+      <Output TaskParameter="ConsoleOutput" PropertyName="XcodeSelect" />
+    </Exec>
+
+    <PropertyGroup Condition=" '$(FindXCode)' == 'true' ">
+      <XCodePath>$(XcodeSelect)</XCodePath>
+      <XCodePath>$([MSBuild]::EnsureTrailingSlash('$(XCodePath)'))</XCodePath>
+    </PropertyGroup>
+    
+    <Message Importance="normal" Text="Found XCode at $(XcodeSelect)"  Condition=" '$(FindXCode)' == 'true' "/>
+    
+    <ItemGroup>
+      <LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk%22"
+                 Condition=" $(RuntimeIdentifier.Contains('simulator')) "/>
+      <LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk%22"
+                 Condition=" !$(RuntimeIdentifier.Contains('simulator')) "/>
+    </ItemGroup>
+
+  </Target>
+
+  <Target Name="FixSymbols"
+          AfterTargets="Publish">
+
+    <RemoveDir Directories="$(PublishDir)$(TargetName).framework.dSYM"/>
+
+    <!-- create-xcframework (called from the export plugin wants the symbol files in a directory
+    with a slightly different name from the one created by dotnet publish, so we copy them over
+    to the correctly-named directory -->
+    <ItemGroup>
+      <SymbolFiles Include="$(NativeBinary).dsym\**\*.*"/>
+    </ItemGroup>
+    <Copy SourceFiles="@(SymbolFiles)" DestinationFolder="$(PublishDir)$(TargetName).framework.dSYM"/>
+  </Target>
+  
+</Project>

+ 3 - 0
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs

@@ -25,6 +25,9 @@ namespace GodotTools.ProjectEditor
             mainGroup.AddProperty("TargetFramework", "net6.0");
             mainGroup.AddProperty("TargetFramework", "net6.0");
             mainGroup.AddProperty("EnableDynamicLoading", "true");
             mainGroup.AddProperty("EnableDynamicLoading", "true");
 
 
+            var net8 = mainGroup.AddProperty("TargetFramework", "net8.0");
+            net8.Condition = " '$(GodotTargetPlatform)' == 'ios' ";
+
             string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true);
             string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true);
 
 
             // If the name is not a valid namespace, manually set RootNamespace to a sanitized one.
             // If the name is not a valid namespace, manually set RootNamespace to a sanitized one.

+ 42 - 2
modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.IO;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -67,7 +68,7 @@ namespace GodotTools.Build
             {
             {
                 BuildStarted?.Invoke(buildInfo);
                 BuildStarted?.Invoke(buildInfo);
 
 
-                // Required in order to update the build tasks list
+                // Required in order to update the build tasks list.
                 Internal.GodotMainIteration();
                 Internal.GodotMainIteration();
 
 
                 try
                 try
@@ -162,7 +163,7 @@ namespace GodotTools.Build
             {
             {
                 BuildStarted?.Invoke(buildInfo);
                 BuildStarted?.Invoke(buildInfo);
 
 
-                // Required in order to update the build tasks list
+                // Required in order to update the build tasks list.
                 Internal.GodotMainIteration();
                 Internal.GodotMainIteration();
 
 
                 try
                 try
@@ -317,6 +318,45 @@ namespace GodotTools.Build
         ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration,
         ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration,
             platform, runtimeIdentifier, publishOutputDir, includeDebugSymbols));
             platform, runtimeIdentifier, publishOutputDir, includeDebugSymbols));
 
 
+        public static bool GenerateXCFrameworkBlocking(
+            List<string> 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<string> 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()
         public static bool EditorBuildCallback()
         {
         {
             if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))
             if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))

+ 78 - 0
modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs

@@ -9,7 +9,9 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Godot;
 using Godot;
 using GodotTools.BuildLogger;
 using GodotTools.BuildLogger;
+using GodotTools.Internals;
 using GodotTools.Utils;
 using GodotTools.Utils;
+using Directory = GodotTools.Utils.Directory;
 
 
 namespace GodotTools.Build
 namespace GodotTools.Build
 {
 {
@@ -293,5 +295,81 @@ namespace GodotTools.Build
             foreach (string env in platformEnvironmentVariables)
             foreach (string env in platformEnvironmentVariables)
                 environmentVariables.Remove(env);
                 environmentVariables.Remove(env);
         }
         }
+
+        private static Process DoGenerateXCFramework(List<string> outputPaths, string xcFrameworkPath,
+            Action<string> stdOutHandler, Action<string> 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<string> outputPaths, string xcFrameworkPath, Action<string> stdOutHandler, Action<string> stdErrHandler)
+        {
+            using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler))
+            {
+                process.WaitForExit();
+
+                return process.ExitCode;
+            }
+        }
+
+        private static void BuildXCFrameworkArguments(List<string> outputPaths,
+            string xcFrameworkPath, Collection<string> 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);
+        }
     }
     }
 }
 }

+ 222 - 68
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -6,9 +6,7 @@ using System.Linq;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
 using System.Text;
 using System.Text;
 using GodotTools.Build;
 using GodotTools.Build;
-using GodotTools.Core;
 using GodotTools.Internals;
 using GodotTools.Internals;
-using static GodotTools.Internals.Globals;
 using Directory = GodotTools.Utils.Directory;
 using Directory = GodotTools.Utils.Directory;
 using File = GodotTools.Utils.File;
 using File = GodotTools.Utils.File;
 using OS = GodotTools.Utils.OS;
 using OS = GodotTools.Utils.OS;
@@ -77,7 +75,7 @@ namespace GodotTools.Export
                     $"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}",
                     $"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}",
                     nameof(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");
             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.
                 // Because of this, we add a file which contains a line break.
                 AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false);
                 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();
                 Skip();
             }
             }
         }
         }
@@ -119,7 +117,7 @@ namespace GodotTools.Export
 
 
         private void _ExportBeginImpl(string[] features, bool isDebug, string path, long flags)
         private void _ExportBeginImpl(string[] features, bool isDebug, string path, long flags)
         {
         {
-            _ = flags; // Unused
+            _ = flags; // Unused.
 
 
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
                 return;
                 return;
@@ -127,115 +125,261 @@ namespace GodotTools.Export
             if (!DeterminePlatformFromFeatures(features, out string platform))
             if (!DeterminePlatformFromFeatures(features, out string platform))
                 throw new NotSupportedException("Target platform not supported.");
                 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))
                     .Contains(platform))
             {
             {
                 throw new NotImplementedException("Target platform not yet implemented.");
                 throw new NotImplementedException("Target platform not yet implemented.");
             }
             }
 
 
-            string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
-
-            bool includeDebugSymbols = (bool)GetOption("dotnet/include_debug_symbols");
+            PublishConfig publishConfig = new()
+            {
+                BuildConfig = isDebug ? "ExportDebug" : "ExportRelease",
+                IncludeDebugSymbols = (bool)GetOption("dotnet/include_debug_symbols"),
+                RidOS = DetermineRuntimeIdentifierOS(platform),
+                Archs = new List<string>(),
+                UseTempDir = platform != OS.Platforms.iOS, // xcode project links directly to files in the publish dir, so use one that sticks around.
+                BundleOutputs = true,
+            };
 
 
-            var archs = new List<string>();
             if (features.Contains("x86_64"))
             if (features.Contains("x86_64"))
             {
             {
-                archs.Add("x86_64");
+                publishConfig.Archs.Add("x86_64");
             }
             }
+
             if (features.Contains("x86_32"))
             if (features.Contains("x86_32"))
             {
             {
-                archs.Add("x86_32");
+                publishConfig.Archs.Add("x86_32");
             }
             }
+
             if (features.Contains("arm64"))
             if (features.Contains("arm64"))
             {
             {
-                archs.Add("arm64");
+                publishConfig.Archs.Add("arm64");
             }
             }
+
             if (features.Contains("arm32"))
             if (features.Contains("arm32"))
             {
             {
-                archs.Add("arm32");
+                publishConfig.Archs.Add("arm32");
             }
             }
+
             if (features.Contains("universal"))
             if (features.Contains("universal"))
             {
             {
                 if (platform == OS.Platforms.MacOS)
                 if (platform == OS.Platforms.MacOS)
                 {
                 {
-                    archs.Add("x86_64");
-                    archs.Add("arm64");
+                    publishConfig.Archs.Add("x86_64");
+                    publishConfig.Archs.Add("arm64");
                 }
                 }
             }
             }
 
 
-            bool embedBuildResults = (bool)GetOption("dotnet/embed_build_outputs") || features.Contains("android");
+            var targets = new List<PublishConfig> { publishConfig };
 
 
-            foreach (var arch in archs)
+            if (platform == OS.Platforms.iOS)
             {
             {
-                string ridOS = DetermineRuntimeIdentifierOS(platform);
-                string ridArch = DetermineRuntimeIdentifierArch(arch);
-                string runtimeIdentifier = $"{ridOS}-{ridArch}";
-                string projectDataDirName = $"data_{GodotSharpDirs.CSharpProjectName}_{platform}_{arch}";
-                if (platform == OS.Platforms.MacOS)
+                targets.Add(new PublishConfig
                 {
                 {
-                    projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName);
-                }
+                    BuildConfig = publishConfig.BuildConfig,
+                    Archs = new List<string> { "arm64", "x86_64" },
+                    BundleOutputs = false,
+                    IncludeDebugSymbols = publishConfig.IncludeDebugSymbols,
+                    RidOS = OS.DotNetOS.iOSSimulator,
+                    UseTempDir = true,
+                });
+            }
 
 
-                // Create temporary publish output directory
+            List<string> outputPaths = new();
 
 
-                string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet",
-                    $"{System.Environment.ProcessId}-{buildConfig}-{runtimeIdentifier}");
+            bool embedBuildResults = (bool)GetOption("dotnet/embed_build_outputs") || features.Contains("android");
 
 
-                _tempFolders.Add(publishOutputTempDir);
+            foreach (PublishConfig config in targets)
+            {
+                string ridOS = config.RidOS;
+                string buildConfig = config.BuildConfig;
+                bool includeDebugSymbols = config.IncludeDebugSymbols;
 
 
-                if (!Directory.Exists(publishOutputTempDir))
-                    Directory.CreateDirectory(publishOutputTempDir);
+                foreach (string arch in config.Archs)
+                {
+                    string ridArch = DetermineRuntimeIdentifierArch(arch);
+                    string runtimeIdentifier = $"{ridOS}-{ridArch}";
+                    string projectDataDirName = $"data_{GodotSharpDirs.CSharpProjectName}_{platform}_{arch}";
+                    if (platform == OS.Platforms.MacOS)
+                    {
+                        projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName);
+                    }
 
 
-                // Execute dotnet publish
+                    // Create temporary publish output directory.
+                    string publishOutputDir;
 
 
-                if (!BuildManager.PublishProjectBlocking(buildConfig, platform,
-                        runtimeIdentifier, publishOutputTempDir, includeDebugSymbols))
-                {
-                    throw new InvalidOperationException("Failed to build project.");
-                }
+                    if (config.UseTempDir)
+                    {
+                        publishOutputDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet",
+                            $"{System.Environment.ProcessId}-{buildConfig}-{runtimeIdentifier}");
+                        _tempFolders.Add(publishOutputDir);
+                    }
+                    else
+                    {
+                        publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet",
+                            $"{buildConfig}-{runtimeIdentifier}");
 
 
-                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();
+                    outputPaths.Add(publishOutputDir);
 
 
-                // Add to the exported project shared object list or packed resources.
-                foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories))
-                {
-                    if (embedBuildResults)
+                    if (!Directory.Exists(publishOutputDir))
+                        Directory.CreateDirectory(publishOutputDir);
+
+                    // Execute dotnet publish.
+                    if (!BuildManager.PublishProjectBlocking(buildConfig, platform,
+                            runtimeIdentifier, publishOutputDir, includeDebugSymbols))
                     {
                     {
-                        var filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputTempDir, file));
-                        var fileData = File.ReadAllBytes(file);
-                        var hash = Convert.ToBase64String(SHA512.HashData(fileData));
+                        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"
+                    };
 
 
-                        manifest.Append($"{filePath}\t{hash}\n");
+                    string assemblyPath = Path.Combine(publishOutputDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll");
+                    string nativeAotPath = Path.Combine(publishOutputDir,
+                        $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}");
 
 
-                        AddFile($"res://.godot/mono/publish/{arch}/{filePath}", fileData, false);
+                    if (!File.Exists(assemblyPath) && !File.Exists(nativeAotPath))
+                    {
+                        throw new NotSupportedException(
+                            $"Publish succeeded but project assembly not found at '{assemblyPath}' or '{nativeAotPath}'.");
                     }
                     }
-                    else
+
+                    // 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)
                     {
                     {
-                        AddSharedObject(file, tags: null,
-                            Path.Join(projectDataDirName,
-                                Path.GetRelativePath(publishOutputTempDir, Path.GetDirectoryName(file))));
+                        byte[] fileData = Encoding.Default.GetBytes(manifest.ToString());
+                        AddFile($"res://.godot/mono/publish/{arch}/.dotnet-publish-manifest", fileData, false);
                     }
                     }
                 }
                 }
+            }
+
+            if (platform == OS.Platforms.iOS)
+            {
+                if (outputPaths.Count > 2)
+                {
+                    // lipo the simulator binaries together
+                    // TODO: Move this to the native lipo implementation we have in the macos export plugin.
+                    var lipoArgs = new List<string>();
+                    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);
+                }
 
 
-                if (embedBuildResults)
+                var xcFrameworkPath = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig,
+                    $"{GodotSharpDirs.ProjectAssemblyName}.xcframework");
+                if (!BuildManager.GenerateXCFrameworkBlocking(outputPaths,
+                        Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig, xcFrameworkPath)))
                 {
                 {
-                    var fileData = Encoding.Default.GetBytes(manifest.ToString());
-                    AddFile($"res://.godot/mono/publish/{arch}/.dotnet-publish-manifest", fileData, false);
+                    throw new InvalidOperationException("Failed to generate xcframework.");
+                }
+
+                AddIosEmbeddedFramework(xcFrameworkPath);
+            }
+        }
+
+        private static void RecursePublishContents(string path, Func<string, bool> filterDir,
+            Func<string, bool> filterFile, Func<string, bool> recurseDir,
+            Action<string, bool> 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;
             platform = null;
             return false;
             return false;
         }
         }
+
+        private struct PublishConfig
+        {
+            public bool UseTempDir;
+            public bool BundleOutputs;
+            public string RidOS;
+            public List<string> Archs;
+            public string BuildConfig;
+            public bool IncludeDebugSymbols;
+        }
     }
     }
 }
 }

+ 10 - 0
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)
         public static string LogsDirPathFor(string solution, string configuration)
             => Path.Combine(BuildLogsDirs, $"{solution.Md5Text()}_{configuration}");
             => Path.Combine(BuildLogsDirs, $"{solution.Md5Text()}_{configuration}");
 
 

+ 1 - 0
modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs

@@ -56,6 +56,7 @@ namespace GodotTools.Utils
             public const string Win10 = "win10";
             public const string Win10 = "win10";
             public const string Android = "android";
             public const string Android = "android";
             public const string iOS = "ios";
             public const string iOS = "ios";
+            public const string iOSSimulator = "iossimulator";
             public const string Browser = "browser";
             public const string Browser = "browser";
         }
         }
 
 

+ 12 - 14
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)
 #if defined(WINDOWS_ENABLED)
 	String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dll");
 	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");
 	String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dylib");
 #elif defined(UNIX_ENABLED)
 #elif defined(UNIX_ENABLED)
 	String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".so");
 	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?)"
 #error "Platform not supported (yet?)"
 #endif
 #endif
 
 
-	if (FileAccess::exists(native_aot_so_path)) {
-		Error err = OS::get_singleton()->open_dynamic_library(native_aot_so_path, r_aot_dll_handle);
-
-		if (err != OK) {
-			return nullptr;
-		}
+	Error err = OS::get_singleton()->open_dynamic_library(native_aot_so_path, r_aot_dll_handle);
 
 
-		void *lib = r_aot_dll_handle;
+	if (err != OK) {
+		return nullptr;
+	}
 
 
-		void *symbol = nullptr;
+	void *lib = r_aot_dll_handle;
 
 
-		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;
-	}
+	void *symbol = nullptr;
 
 
-	return 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
 #endif
 
 
@@ -376,11 +372,13 @@ void GDMono::initialize() {
 
 
 	godot_plugins_initialize_fn godot_plugins_initialize = nullptr;
 	godot_plugins_initialize_fn godot_plugins_initialize = nullptr;
 
 
+#if !defined(IOS_ENABLED)
 	// Check that the .NET assemblies directory exists before trying to use it.
 	// Check that the .NET assemblies directory exists before trying to use it.
 	if (!DirAccess::exists(GodotSharpDirs::get_api_assemblies_dir())) {
 	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"));
 		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");
 		ERR_FAIL_MSG(".NET: Assemblies not found");
 	}
 	}
+#endif
 
 
 	if (!load_hostfxr(hostfxr_dll_handle)) {
 	if (!load_hostfxr(hostfxr_dll_handle)) {
 #if !defined(TOOLS_ENABLED)
 #if !defined(TOOLS_ENABLED)

+ 0 - 50
modules/mono/mono_gd/support/ios_support.h

@@ -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

+ 0 - 150
modules/mono/mono_gd/support/ios_support.mm

@@ -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 <Foundation/Foundation.h>
-#include <os/log.h>
-
-// 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

+ 8 - 5
platform/ios/export/export_plugin.cpp

@@ -1928,11 +1928,15 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres
 
 
 bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
 bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
 #ifdef MODULE_MONO_ENABLED
 #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
 #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;
 	String err;
 	bool valid = false;
 	bool valid = false;
@@ -1963,7 +1967,6 @@ bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExp
 	}
 	}
 
 
 	return valid;
 	return valid;
-#endif // !MODULE_MONO_ENABLED
 }
 }
 
 
 bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
 bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {