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