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
This commit is contained in:
Andreia Gaita 2023-10-02 18:07:20 +02:00
parent 9215b03429
commit ee9a735c26
16 changed files with 471 additions and 298 deletions

View file

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

View file

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

View file

@ -59,6 +59,18 @@
</PropertyGroup>
<!-- 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)' == '' ">
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">linuxbsd</GodotTargetPlatform>
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">linuxbsd</GodotTargetPlatform>
@ -97,4 +109,6 @@
<DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " />
</Project>

View file

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

View file

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

View file

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

View file

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

View file

@ -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<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()
{
if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))

View file

@ -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<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);
}
}
}

View file

@ -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<string>(),
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<string>();
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> { publishConfig };
if (platform == OS.Platforms.iOS)
{
targets.Add(new PublishConfig
{
BuildConfig = publishConfig.BuildConfig,
Archs = new List<string> { "arm64", "x86_64" },
BundleOutputs = false,
IncludeDebugSymbols = publishConfig.IncludeDebugSymbols,
RidOS = OS.DotNetOS.iOSSimulator,
UseTempDir = true,
});
}
List<string> 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<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);
}
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<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;
return false;
}
private struct PublishConfig
{
public bool UseTempDir;
public bool BundleOutputs;
public string RidOS;
public List<string> Archs;
public string BuildConfig;
public bool IncludeDebugSymbols;
}
}
}

View file

@ -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}");

View file

@ -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";
}

View file

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

View file

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

View file

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

View file

@ -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 {
#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<EditorExp
}
return valid;
#endif // !MODULE_MONO_ENABLED
}
bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {