// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.IO; using Tools.DotNETCommon; namespace UnrealBuildTool { /// /// Represents a folder within the master project (e.g. Visual Studio solution) /// class XcodeProjectFolder : MasterProjectFolder { public XcodeProjectFolder(ProjectFileGenerator InitOwnerProjectFileGenerator, string InitFolderName) : base(InitOwnerProjectFileGenerator, InitFolderName) { } } /// /// 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 = ""; public XcodeProjectFileGenerator(FileReference InOnlyGameProject, CommandLineArguments CommandLine) : base(InOnlyGameProject) { if (CommandLine.HasOption("-distribution")) { bForDistribution = true; } if (CommandLine.HasValue("-bundleID=")) { BundleIdentifier = CommandLine.GetString("-bundleID="); } } /// /// 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 InMasterProjectDirectory, string InMasterProjectName, DirectoryReference InIntermediateProjectFilesPath) { DirectoryReference MasterProjDeleteFilename = DirectoryReference.Combine(InMasterProjectDirectory, InMasterProjectName + ".xcworkspace"); if (DirectoryReference.Exists(MasterProjDeleteFilename)) { DirectoryReference.Delete(MasterProjDeleteFilename, true); } // Delete the project files folder if (DirectoryReference.Exists(InIntermediateProjectFilesPath)) { try { DirectoryReference.Delete(InIntermediateProjectFilesPath, true); } catch (Exception Ex) { Log.TraceInformation("Error while trying to clean project files path {0}. Ignored.", InIntermediateProjectFilesPath); Log.TraceInformation("\t" + Ex.Message); } } } /// /// Allocates a generator-specific project file object /// /// Path to the project file /// The newly allocated project file object protected override ProjectFile AllocateProjectFile(FileReference InitFilePath) { return new XcodeProjectFile(InitFilePath, OnlyGameProject, bForDistribution, BundleIdentifier); } /// ProjectFileGenerator interface public override MasterProjectFolder AllocateMasterProjectFolder(ProjectFileGenerator InitOwnerProjectFileGenerator, string InitFolderName) { return new XcodeProjectFolder(InitOwnerProjectFileGenerator, InitFolderName); } private bool WriteWorkspaceSettingsFile(string Path) { 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(), new UTF8Encoding()); } private bool WriteXcodeWorkspace() { bool bSuccess = true; StringBuilder WorkspaceDataContent = new StringBuilder(); WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine); WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine); System.Action< List /* Folders */, string /* Ident */ > AddProjectsFunction = null; AddProjectsFunction = (FolderList, Ident) => { int SchemeIndex = 0; foreach (XcodeProjectFolder CurFolder in FolderList) { WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine); AddProjectsFunction(CurFolder.SubFolders, Ident + " "); List ChildProjects = new List(CurFolder.ChildProjects); ChildProjects.Sort((ProjectFile A, ProjectFile B) => { return A.ProjectFilePath.GetFileName().CompareTo(B.ProjectFilePath.GetFileName()); }); foreach (ProjectFile CurProject in ChildProjects) { XcodeProjectFile XcodeProject = CurProject as XcodeProjectFile; if (XcodeProject != null) { WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine); WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine); // Also, update project's schemes index so that the schemes list order match projects order in the navigator 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++; } } } WorkspaceDataContent.Append(Ident + " " + ProjectFileGenerator.NewLine); } }; AddProjectsFunction(RootFolder.SubFolders, ""); WorkspaceDataContent.Append("" + ProjectFileGenerator.NewLine); string ProjectName = MasterProjectName; if (ProjectFilePlatform != XcodeProjectFilePlatform.All) { ProjectName += ProjectFilePlatform == XcodeProjectFilePlatform.Mac ? "_Mac" : (ProjectFilePlatform == XcodeProjectFilePlatform.iOS ? "_IOS" : "_TVOS"); } string WorkspaceDataFilePath = MasterProjectPath + "/" + ProjectName + ".xcworkspace/contents.xcworkspacedata"; bSuccess = WriteFileIfChanged(WorkspaceDataFilePath, WorkspaceDataContent.ToString(), new UTF8Encoding()); if (bSuccess) { string WorkspaceSettingsFilePath = MasterProjectPath + "/" + ProjectName + ".xcworkspace/xcuserdata/" + Environment.UserName + ".xcuserdatad/WorkspaceSettings.xcsettings"; bSuccess = WriteWorkspaceSettingsFile(WorkspaceSettingsFilePath); } return bSuccess; } protected override bool WriteMasterProjectFile(ProjectFile UBTProject, PlatformProjectGeneratorCollection PlatformProjectGenerators) { return WriteXcodeWorkspace(); } [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; /// /// Configures project generator based on command-line options /// /// Arguments passed into the program /// True if all platforms should be included protected override void ConfigureProjectFileGeneration(string[] Arguments, ref bool IncludeAllPlatforms) { // Call parent implementation first base.ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms); 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) { bIncludeEngineSource = true; } } } }