// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using EpicGames.Core;
using System.Linq;
using Microsoft.Extensions.Logging;
namespace UnrealBuildTool
{
enum XcodePerPlatformMode
{
OneWorkspacePerPlatform,
OneTargetPerPlatform,
}
///
/// Xcode project file generator implementation
///
class XcodeProjectFileGenerator : ProjectFileGenerator
{
///
/// Xcode makes a project per target, for modern to be able to pull in Cooked data
///
protected override bool bMakeProjectPerTarget => true;
public DirectoryReference? XCWorkspace;
// always seed the random number the same, so multiple runs of the generator will generate the same project
static Random Rand = new Random(0);
// how to handle multiple platforms
public static XcodePerPlatformMode PerPlatformMode = XcodePerPlatformMode.OneWorkspacePerPlatform;
///
/// Mark for distribution builds
///
bool bForDistribution = false;
///
/// Override BundleID
///
string BundleIdentifier = "";
///
/// Override AppName
///
string AppName = "";
///
/// Store the single game project (when using -game -project=...) to a place that XcodeProjectLegacy can easily retrieve it
///
public static FileReference? SingleGameProject = null;
///
/// If set, only write out one target/project
///
string? SingleTargetName = null;
public XcodeProjectFileGenerator(FileReference? InOnlyGameProject, CommandLineArguments CommandLine)
: base(InOnlyGameProject)
{
SingleGameProject = InOnlyGameProject;
if (CommandLine.HasOption("-distribution"))
{
bForDistribution = true;
}
if (CommandLine.HasValue("-bundleID="))
{
BundleIdentifier = CommandLine.GetString("-bundleID=");
}
if (CommandLine.HasValue("-appname="))
{
AppName = CommandLine.GetString("-appname=");
}
if (CommandLine.HasValue("-SingleTarget="))
{
SingleTargetName = CommandLine.GetString("-SingleTarget=");
}
}
///
/// Make a random Guid string usable by Xcode (24 characters exactly)
///
public static string MakeXcodeGuid()
{
string Guid = "";
byte[] Randoms = new byte[12];
Rand.NextBytes(Randoms);
for (int Index = 0; Index < 12; Index++)
{
Guid += Randoms[Index].ToString("X2");
}
return Guid;
}
/// File extension for project files we'll be generating (e.g. ".vcxproj")
override public string ProjectFileExtension
{
get
{
return ".xcodeproj";
}
}
///
///
public override void CleanProjectFiles(DirectoryReference InPrimaryProjectDirectory, string InPrimaryProjectName, DirectoryReference InIntermediateProjectFilesPath, ILogger Logger)
{
foreach (DirectoryReference ProjectFile in DirectoryReference.EnumerateDirectories(InPrimaryProjectDirectory, $"{InPrimaryProjectName}*.xcworkspace"))
{
DirectoryReference.Delete(ProjectFile, true);
}
// Delete the project files folder
if (DirectoryReference.Exists(InIntermediateProjectFilesPath))
{
try
{
DirectoryReference.Delete(InIntermediateProjectFilesPath, true);
}
catch (Exception Ex)
{
Logger.LogInformation("Error while trying to clean project files path {InIntermediateProjectFilesPath}. Ignored.", InIntermediateProjectFilesPath);
Logger.LogInformation("\t{Ex}", Ex.Message);
}
}
}
///
/// Allocates a generator-specific project file object
///
/// Path to the project file
/// The base directory for files within this project
/// The newly allocated project file object
protected override ProjectFile AllocateProjectFile(FileReference InitFilePath, DirectoryReference BaseDir)
{
// this may internally (later) make a Legacy project object if the unreal project wants old behavior
// unfortunately, we can't read the project configs now because we don't have enough information to
// find the .uproject file for that would make this project (we could change the high level to pass it
// down but it would touch all project generators - not worth it if we end up removing the legacy)
return new XcodeProjectXcconfig.XcodeProjectFile(InitFilePath, BaseDir, bForDistribution, BundleIdentifier, AppName, bMakeProjectPerTarget, SingleTargetName);
}
private bool WriteWorkspaceSettingsFile(string Path, ILogger Logger)
{
StringBuilder WorkspaceSettingsContent = new StringBuilder();
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
// @todo when we move to xcode 14, we remove these next 4 keys
WorkspaceSettingsContent.Append("\tBuildSystemType" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tOriginal" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tBuildLocationStyle" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tUseTargetSettings" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tCustomBuildLocationType" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tRelativeToDerivedData" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tDerivedDataLocationStyle" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tDefault" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tIssueFilterStyle" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tShowAll" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tLiveSourceIssuesEnabled" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tSnapshotAutomaticallyBeforeSignificantChanges" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tSnapshotLocationStyle" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tDefault" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
return WriteFileIfChanged(Path, WorkspaceSettingsContent.ToString(), Logger, new UTF8Encoding());
}
private bool WriteWorkspaceSharedSettingsFile(string Path, ILogger Logger)
{
StringBuilder WorkspaceSettingsContent = new StringBuilder();
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
// @todo when we move to xcode 14, we remove these next 2 keys
WorkspaceSettingsContent.Append("\tDisableBuildSystemDeprecationWarning" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tDisableBuildSystemDeprecationDiagnostic" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\tIDEWorkspaceSharedSettings_AutocreateContextsIfNeeded" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine);
return WriteFileIfChanged(Path, WorkspaceSettingsContent.ToString(), Logger, new UTF8Encoding());
}
private string PrimaryProjectNameForPlatform(UnrealTargetPlatform? Platform)
{
return Platform == null ? PrimaryProjectName : $"{PrimaryProjectName} ({Platform})";
}
private bool WriteXcodeWorkspace(ILogger Logger)
{
bool bSuccess = true;
// loop opver all projects to see if at least one is modern (if not, we don't bother splitting up by platform)
bool bHasModernProjects = false;
Action>? FindModern = null;
FindModern = (FolderList) =>
{
foreach (PrimaryProjectFolder CurFolder in FolderList)
{
var Modern = CurFolder.ChildProjects.FirstOrDefault(P =>
P.GetType() == typeof(XcodeProjectXcconfig.XcodeProjectFile) &&
!((XcodeProjectXcconfig.XcodeProjectFile)P).bHasLegacyProject);
if (Modern != null)
{
//Logger.LogWarning($"Project {Modern.ProjectFilePath} is modern");
bHasModernProjects = true;
}
if (!bHasModernProjects)
{
FindModern!(CurFolder.SubFolders);
}
}
};
FindModern(RootFolder.SubFolders);
// if we want one workspace with multiple platforms, and we have at least one modern project, then process each platform individually
// otherwise use null as Platform which means to merge all platforms
List PlatformsToProcess = bHasModernProjects ? WorkspacePlatforms : NullPlatformList;
foreach (UnrealTargetPlatform? Platform in PlatformsToProcess)
{
StringBuilder WorkspaceDataContent = new();
WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine);
WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine);
List BuildableProjects = new();
System.Action /* Folders */, string /* Ident */ >? AddProjectsFunction = null;
AddProjectsFunction = (FolderList, Ident) =>
{
foreach (PrimaryProjectFolder CurFolder in FolderList)
{
WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine);
AddProjectsFunction!(CurFolder.SubFolders, Ident + " ");
// Filter out anything that isn't an XC project, and that shouldn't be in the workspace
IEnumerable SupportedProjects =
CurFolder.ChildProjects.Where(P => P.GetType() == typeof(XcodeProjectXcconfig.XcodeProjectFile))
.Select(P => (XcodeProjectXcconfig.XcodeProjectFile)P)
.Where(P => XcodeProjectXcconfig.XcodeUtils.ShouldIncludeProjectInWorkspace(P, Logger))
// @todo - still need to handle legacy project getting split up?
.Where(P => P.RootProjects.Count == 0 || P.RootProjects.ContainsValue(Platform))
.OrderBy(P => P.ProjectFilePath.GetFileName());
foreach (XcodeProjectXcconfig.XcodeProjectFile XcodeProject in SupportedProjects)
{
// if we are only generating a single target project, skip any others now
if (!string.IsNullOrEmpty(SingleTargetName) && !XcodeProject.ProjectTargets.Any(x => x.TargetRules?.Name == SingleTargetName))
{
continue;
}
// we have to re-check for each project - if it's a legacy project, even if we wanted it split, it won't be, so always point to
// the shared legacy project
FileReference PathToProject = XcodeProject.ProjectFilePath;
if (!XcodeProject.bHasLegacyProject && PerPlatformMode == XcodePerPlatformMode.OneWorkspacePerPlatform)
{
PathToProject = XcodeProject.ProjectFilePathForPlatform(Platform);
}
WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine);
WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine);
}
BuildableProjects.AddRange(SupportedProjects);
WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine);
}
};
AddProjectsFunction(RootFolder.SubFolders, "");
WorkspaceDataContent.Append("
" + ProjectFileGenerator.NewLine);
// Also, update project's schemes index so that the schemes are in a sensible order
// (Game, Editor, Client, Server, Programs)
int SchemeIndex = 0;
BuildableProjects.Sort((ProjA, ProjB) =>
{
ProjectTarget TargetA = ProjA.ProjectTargets.OfType().OrderBy(T => T.TargetRules!.Type).First();
ProjectTarget TargetB = ProjB.ProjectTargets.OfType().OrderBy(T => T.TargetRules!.Type).First();
TargetType TypeA = TargetA.TargetRules!.Type;
TargetType TypeB = TargetB.TargetRules!.Type;
if (TypeA != TypeB)
{
return TypeA.CompareTo(TypeB);
}
return TargetA.Name.CompareTo(TargetB.Name);
});
foreach (XcodeProjectXcconfig.XcodeProjectFile XcodeProject in BuildableProjects)
{
FileReference SchemeManagementFile = XcodeProject.ProjectFilePathForPlatform(Platform) + "/xcuserdata/" + Environment.UserName + ".xcuserdatad/xcschemes/xcschememanagement.plist";
if (FileReference.Exists(SchemeManagementFile))
{
string SchemeManagementContent = FileReference.ReadAllText(SchemeManagementFile);
SchemeManagementContent = SchemeManagementContent.Replace("orderHint\n\t\t\t1", "orderHint\n\t\t\t" + SchemeIndex.ToString() + "");
FileReference.WriteAllText(SchemeManagementFile, SchemeManagementContent);
SchemeIndex++;
}
}
string ProjectName = PrimaryProjectNameForPlatform(Platform);
string WorkspaceDataFilePath = PrimaryProjectPath + "/" + ProjectName + ".xcworkspace/contents.xcworkspacedata";
Logger.LogInformation($"Writing xcode workspace {Path.GetDirectoryName(WorkspaceDataFilePath)}");
bSuccess = WriteFileIfChanged(WorkspaceDataFilePath, WorkspaceDataContent.ToString(), Logger, new UTF8Encoding());
if (bSuccess)
{
string WorkspaceSettingsFilePath = PrimaryProjectPath + "/" + ProjectName + ".xcworkspace/xcuserdata/" + Environment.UserName + ".xcuserdatad/WorkspaceSettings.xcsettings";
bSuccess = WriteWorkspaceSettingsFile(WorkspaceSettingsFilePath, Logger);
string WorkspaceSharedSettingsFilePath = PrimaryProjectPath + "/" + ProjectName + ".xcworkspace/xcshareddata/WorkspaceSettings.xcsettings";
bSuccess = WriteWorkspaceSharedSettingsFile(WorkspaceSharedSettingsFilePath, Logger);
// cache the location of the workspace, for users of this to know where the final workspace is
XCWorkspace = new FileReference(WorkspaceDataFilePath).Directory;
}
}
return bSuccess;
}
protected override bool WritePrimaryProjectFile(ProjectFile? UBTProject, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger)
{
return WriteXcodeWorkspace(Logger);
}
///
/// A static copy of ProjectPlatforms from the base class
///
static public List XcodePlatforms = new();
static public List WorkspacePlatforms = new();
static public List RunTargetPlatforms = new();
static public List NullPlatformList = new() { null };
///
/// Should we generate only a run project (no build/index targets)
///
static public bool bGenerateRunOnlyProject = false;
///
protected override void ConfigureProjectFileGeneration(string[] Arguments, ref bool IncludeAllPlatforms, ILogger Logger)
{
// Call parent implementation first
base.ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms, Logger);
if (ProjectPlatforms.Count > 0)
{
XcodePlatforms.AddRange(ProjectPlatforms);
}
else
{
// add platforms that have synced platform support
if (InstalledPlatformInfo.IsValidPlatform(UnrealTargetPlatform.Mac, EProjectType.Code))
{
XcodePlatforms.Add(UnrealTargetPlatform.Mac);
}
if (InstalledPlatformInfo.IsValidPlatform(UnrealTargetPlatform.IOS, EProjectType.Code))
{
XcodePlatforms.Add(UnrealTargetPlatform.IOS);
}
if (InstalledPlatformInfo.IsValidPlatform(UnrealTargetPlatform.TVOS, EProjectType.Code))
{
XcodePlatforms.Add(UnrealTargetPlatform.TVOS);
}
}
if (PerPlatformMode == XcodePerPlatformMode.OneWorkspacePerPlatform)
{
WorkspacePlatforms = XcodePlatforms.Select(x => x).ToList();
}
else
{
WorkspacePlatforms = NullPlatformList;
}
foreach (string CurArgument in Arguments)
{
if (CurArgument.Contains("-iOSDeployOnly", StringComparison.InvariantCultureIgnoreCase) ||
CurArgument.Contains("-tvOSDeployOnly", StringComparison.InvariantCultureIgnoreCase) ||
CurArgument.Contains("-DeployOnly", StringComparison.InvariantCultureIgnoreCase))
{
bGenerateRunOnlyProject = true;
break;
}
}
if (bGenerateRunOnlyProject)
{
// if we have a single target, allow all targets so we can pick it out (helps with programs)
if (SingleTargetName != null)
{
bIncludeEnginePrograms = false;
bIncludeTemplateFiles = false;
}
bIncludeConfigFiles = false;
bIncludeDocumentation = false;
bIncludeShaderSource = false;
// generate just the engine project
if (OnlyGameProject == null)
{
bIncludeEngineSource = true;
}
// generate just the game project
else
{
// content only, project-only projects need engine to get the UnrealGame, etc targets (checking for Root and engine catches UnrealGame and content only projects, respectively)
bIncludeEngineSource = PrimaryProjectPath == UnrealBuildBase.Unreal.RootDirectory || PrimaryProjectPath == UnrealBuildBase.Unreal.EngineDirectory;
bGeneratingGameProjectFiles = true;
}
}
}
internal static Dictionary, IEnumerable> TargetFrameworks = new();
internal static Dictionary, IEnumerable> TargetBundles = new();
protected override void AddAdditionalNativeTargetInformation(PlatformProjectGeneratorCollection PlatformProjectGenerators, List> Targets, ILogger Logger)
{
DateTime MainStart = DateTime.UtcNow;
foreach (var TargetPair in Targets)
{
// don't bother if we aren't interested in this target
if (SingleTargetName != null && !TargetPair.Item2.Name.Equals(SingleTargetName, StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
ProjectFile TargetProjectFile = TargetPair.Item1;
// don't do this for legacy projets, for speed
((XcodeProjectXcconfig.XcodeProjectFile)TargetProjectFile).ConditionalCreateLegacyProject();
if (((XcodeProjectXcconfig.XcodeProjectFile)TargetProjectFile).bHasLegacyProject)
{
continue;
}
ProjectTarget CurTarget = TargetPair.Item2;
UnrealTargetPlatform[] PlatformsToGenerate = { UnrealTargetPlatform.Mac, UnrealTargetPlatform.IOS };
foreach (UnrealTargetPlatform Platform in PlatformsToGenerate)
{
UnrealArch Arch = UnrealArch.Arm64;
if (!CurTarget.SupportedPlatforms.Any(x => x == Platform))
{
continue;
}
TargetDescriptor TargetDesc = new TargetDescriptor(CurTarget.UnrealProjectFilePath, CurTarget.Name, Platform, UnrealTargetConfiguration.Development,
new UnrealArchitectures(Arch), new CommandLineArguments(new string[] { "-skipclangvalidation" }));
DateTime Start = DateTime.UtcNow;
try
{
// Create the target
UEBuildTarget Target = UEBuildTarget.Create(TargetDesc, true, false, bUsePrecompiled, Logger);
List Frameworks = new();
List Bundles = new();
// Generate a compile environment for each module in the binary
CppCompileEnvironment GlobalCompileEnvironment = Target.CreateCompileEnvironmentForProjectFiles(Logger);
foreach (UEBuildBinary Binary in Target.Binaries)
{
CppCompileEnvironment BinaryCompileEnvironment = Binary.CreateBinaryCompileEnvironment(GlobalCompileEnvironment);
foreach (UEBuildModuleCPP Module in Binary.Modules.OfType())
{
CppCompileEnvironment CompileEnvironment = Module.CreateModuleCompileEnvironment(Target.Rules, BinaryCompileEnvironment, Logger);
Frameworks.AddRange(CompileEnvironment.AdditionalFrameworks);
}
}
// track frameworks if we found any
if (Frameworks.Count > 0)
{
lock (TargetFrameworks)
{
TargetFrameworks.Add(Tuple.Create(TargetProjectFile, Platform), Frameworks.Distinct());
}
}
if (Bundles.Count > 0)
{
lock (TargetBundles)
{
TargetBundles.Add(Tuple.Create(TargetProjectFile, Platform), Bundles.Distinct());
}
}
}
catch (Exception)
{
}
}
}
Logger.LogInformation("GettingNativeInfo {TimeMs}ms overall", (DateTime.UtcNow - MainStart).TotalMilliseconds);
}
}
}