// 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 { /// /// Xcode project file generator implementation /// class XcodeProjectFileGenerator : ProjectFileGenerator { // always seed the random number the same, so multiple runs of the generator will generate the same project static Random Rand = new Random(0); /// /// Mark for distribution builds /// bool bForDistribution = false; /// /// Override BundleID /// string BundleIdentifier = ""; /// /// Override AppName /// string AppName = ""; /// /// Whether or not to use the new .xcconfig file /// private bool bUseXcconfig = false; public XcodeProjectFileGenerator(FileReference? InOnlyGameProject, CommandLineArguments CommandLine) : base(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.HasOption("-xcconfig")) { bUseXcconfig = true; } } /// /// 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) { DirectoryReference PrimaryProjDeleteFilename = DirectoryReference.Combine(InPrimaryProjectDirectory, InPrimaryProjectName + ".xcworkspace"); if (DirectoryReference.Exists(PrimaryProjDeleteFilename)) { DirectoryReference.Delete(PrimaryProjDeleteFilename, 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) { if (bUseXcconfig) { return new XcodeProjectXcconfig.XcodeProjectFile(InitFilePath, BaseDir, bForDistribution, BundleIdentifier, AppName); } else { return new XcodeProjectLegacy.XcodeProjectFile(InitFilePath, BaseDir, bForDistribution, BundleIdentifier, AppName); } } 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); 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); WorkspaceSettingsContent.Append("\tDisableBuildSystemDeprecationWarning" + ProjectFileGenerator.NewLine); WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine); WorkspaceSettingsContent.Append("\tDisableBuildSystemDeprecationDiagnostic" + ProjectFileGenerator.NewLine); WorkspaceSettingsContent.Append("\t" + ProjectFileGenerator.NewLine); WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine); WorkspaceSettingsContent.Append("" + ProjectFileGenerator.NewLine); return WriteFileIfChanged(Path, WorkspaceSettingsContent.ToString(), Logger, new UTF8Encoding()); } private bool WriteXcodeWorkspace(ILogger Logger) { bool bSuccess = true; StringBuilder WorkspaceDataContent = new StringBuilder(); WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine); WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine); List BuildableProjects = new List(); System.Action< List /* 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) || P.GetType() == typeof(XcodeProjectLegacy.XcodeProjectFile)) .Where(P => XcodeProjectXcconfig.UnrealData.ShouldIncludeProjectInWorkspace(P, Logger)) .OrderBy(P => P.ProjectFilePath.GetFileName()); foreach (ProjectFile XcodeProject in SupportedProjects) { 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 (ProjectFile XcodeProject in BuildableProjects) { FileReference SchemeManagementFile = XcodeProject.ProjectFilePath + "/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 = PrimaryProjectName; if (ProjectFilePlatform != XcodeProjectFilePlatform.All) { ProjectName += ProjectFilePlatform == XcodeProjectFilePlatform.Mac ? "_Mac" : (ProjectFilePlatform == XcodeProjectFilePlatform.iOS ? "_IOS" : "_TVOS"); } string WorkspaceDataFilePath = PrimaryProjectPath + "/" + ProjectName + ".xcworkspace/contents.xcworkspacedata"; 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); } return bSuccess; } protected override bool WritePrimaryProjectFile(ProjectFile? UBTProject, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { return WriteXcodeWorkspace(Logger); } [Flags] public enum XcodeProjectFilePlatform { Mac = 1 << 0, iOS = 1 << 1, tvOS = 1 << 2, All = Mac | iOS | tvOS } /// Which platforms we should generate targets for static public XcodeProjectFilePlatform ProjectFilePlatform = XcodeProjectFilePlatform.All; /// Should we generate a special project to use for iOS signing instead of a normal one static public bool bGeneratingRunIOSProject = false; /// Should we generate a special project to use for tvOS signing instead of a normal one static public bool bGeneratingRunTVOSProject = false; /// protected override void ConfigureProjectFileGeneration(string[] Arguments, ref bool IncludeAllPlatforms, ILogger Logger) { // Call parent implementation first base.ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms, Logger); ProjectFilePlatform = IncludeAllPlatforms ? XcodeProjectFilePlatform.All : XcodeProjectFilePlatform.Mac; foreach (string CurArgument in Arguments) { if (CurArgument.StartsWith("-iOSDeployOnly", StringComparison.InvariantCultureIgnoreCase)) { bGeneratingRunIOSProject = true; break; } if (CurArgument.StartsWith("-tvOSDeployOnly", StringComparison.InvariantCultureIgnoreCase)) { bGeneratingRunTVOSProject = true; break; } } if (bGeneratingGameProjectFiles) { if (bGeneratingRunIOSProject || bGeneratingRunTVOSProject || UnrealBuildBase.Unreal.IsEngineInstalled()) { // an Engine target is required in order to be able to get Xcode to sign blueprint projects // always include the engine target for installed builds. bIncludeEnginePrograms = true; } bIncludeEngineSource = true; } } } }