// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; 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; protected override bool bAllowContentOnlyProjects => true; protected override bool bAllowMultiModuleReference => 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; /// /// Don't use entitlements /// bool bNoEntitlements = 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; /// /// Shared file that the project agnostic projects can point to to get content only projects working nicely in Xcode /// public static FileReference ContentOnlySettingsFile = FileReference.Combine(Unreal.EngineDirectory, "Build/Xcode/ContentOnlySettings.xcconfig"); /// /// A static copy of ProjectPlatforms from the base class /// public static List XcodePlatforms = new(); public static List WorkspacePlatforms = new(); public static List RunTargetPlatforms = new(); public static List NullPlatformList = new() { null }; internal static Dictionary, IEnumerable> TargetFrameworks = new(); internal static Dictionary, IEnumerable> TargetBundles = new(); internal static Dictionary, IEnumerable> TargetRawDylibs = new(); /// /// Should we generate only a run project (no build/index targets) /// public static bool bGenerateRunOnlyProject = false; /// /// Platforms we should generate IntelliSense data for, Xcode projects should index for each Apple platform. /// protected override List GetIntelliSensePlatforms() { if (bMakeProjectPerTarget) { // For now just allow indexing to work on Mac and IOS, enabling too many will make GPF and workspaces even slower return new() { UnrealTargetPlatform.Mac, UnrealTargetPlatform.IOS }; } else { // If sharing projects across platform targets, we are unable to index separately (due to generated headers being platform dependent) // Choose Mac to be the platform for indexing return new() { UnrealTargetPlatform.Mac }; } } public XcodeProjectFileGenerator(FileReference? InOnlyGameProject, CommandLineArguments CommandLine) : base(InOnlyGameProject) { SingleGameProject = InOnlyGameProject; if (CommandLine.HasOption("-distribution")) { bForDistribution = true; } if (CommandLine.HasOption("-noEntitlements")) { bNoEntitlements = true; } if (CommandLine.HasValue("-bundleID=")) { BundleIdentifier = CommandLine.GetString("-bundleID="); } if (CommandLine.HasValue("-appname=")) { AppName = CommandLine.GetString("-appname="); } // make sure only one Target writes the file lock (ContentOnlySettingsFile) { if (OnlyGameProject == null && !FileReference.Exists(ContentOnlySettingsFile)) { DirectoryReference.CreateDirectory(ContentOnlySettingsFile.Directory); File.WriteAllLines(ContentOnlySettingsFile.FullName, new string[] { "// Enter the settings for your active code only project here. Note you will need to cook and stage your project before running.", "// Since we use the variables directly, we cannot take a single path to a uproject file, it must be split over two variables", "", "// Enter the path to the root directory of your project (use the full path - you can get it by selecting the folder in Finder and pressing Option-Command-C, then pasting here:", "UE_CONTENTONLY_PROJECT_DIR=", "", "// Enter the name of the project (which is your uproject filename without the extension)", "UE_CONTENTONLY_PROJECT_NAME=", "", "// Uncomment this line if you want the Editor to launch with your Content Only project above", "// UE_CONTENTONLY_EDITOR_STARTUP_PROJECT=$(UE_CONTENTONLY_PROJECT_DIR)/$(UE_CONTENTONLY_PROJECT_NAME).uproject", "", "", "", "", "", "// This is the default location for your Staged data, only change this if you know that you need to:", "UE_OVERRIDE_STAGE_DIR = $(UE_CONTENTONLY_PROJECT_DIR)/Saved/StagedBuilds/$(UE_TARGET_PLATFORM_NAME)", }); } } } /// /// Make a random Guid string usable by Xcode (24 characters exactly) /// public static string MakeXcodeGuid() { string Guid = ""; byte[] Randoms = new byte[12]; lock(Rand) { 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") public override string ProjectFileExtension => ".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, bNoEntitlements, 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) { string ProjectName = PrimaryProjectName; if (!String.IsNullOrEmpty(SingleTargetName)) { ProjectName = $"{ProjectName}_{SingleTargetName}"; } // if there are projectplatforms, then there is a platform name already in the name if (Platform != null && ProjectPlatforms.Count == 0) { ProjectName = $"{ProjectName} ({Platform})"; } return ProjectName; } 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) { ProjectFile? 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); // add a reference to the file used for launching content only game projects (don't need the file if we are making a project for a single speciic project) if (bHasModernProjects && OnlyGameProject == null && CurFolder.FolderName == "Engine") { WorkspaceDataContent.Append(" " + ProjectFileGenerator.NewLine); WorkspaceDataContent.Append(" " + 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.ProjectFilePath.GetFileNameWithoutAnyExtensions() != 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; } } // delete outdated workspace files, reduce confusion (only for real workspaces, not stub ones) if (!bGenerateRunOnlyProject) { if (bHasModernProjects) { DirectoryReference OutdatedWorkspaceDirectory = new DirectoryReference(PrimaryProjectPath + "/" + PrimaryProjectNameForPlatform(null) + ".xcworkspace"); if (DirectoryReference.Exists(OutdatedWorkspaceDirectory)) { DirectoryReference.Delete(OutdatedWorkspaceDirectory, true); } } else { foreach (UnrealTargetPlatform? Platform in WorkspacePlatforms) { DirectoryReference OutdatedWorkspaceDirectory = new DirectoryReference(PrimaryProjectPath + "/" + PrimaryProjectNameForPlatform(Platform) + ".xcworkspace"); if (DirectoryReference.Exists(OutdatedWorkspaceDirectory)) { DirectoryReference.Delete(OutdatedWorkspaceDirectory, true); } } } } return bSuccess; } protected override bool WritePrimaryProjectFile(ProjectFile? UBTProject, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { return WriteXcodeWorkspace(Logger); } /// protected override void ConfigureProjectFileGeneration(string[] Arguments, ref bool IncludeAllPlatforms, ILogger Logger) { // Call parent implementation first base.ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms, Logger); // reset some statics in case it runs twice XcodeProjectFileGenerator.XcodePlatforms.Clear(); XcodeProjectFileGenerator.WorkspacePlatforms.Clear(); XcodeProjectFileGenerator.RunTargetPlatforms.Clear(); XcodeProjectFileGenerator.TargetFrameworks.Clear(); XcodeProjectFileGenerator.TargetBundles.Clear(); XcodeProjectFileGenerator.TargetRawDylibs.Clear(); XcodeProjectFileGenerator.bGenerateRunOnlyProject = false; if (ProjectPlatforms.Count > 0) { XcodePlatforms.AddRange(ProjectPlatforms); } else { // add platforms that have synced platform support foreach (UnrealTargetPlatform ApplePlatform in UEBuildPlatform.GetPlatformsInGroup(UnrealPlatformGroup.Apple)) { if (InstalledPlatformInfo.IsValidPlatform(ApplePlatform, EProjectType.Code) && UEBuildPlatform.TryGetBuildPlatform(ApplePlatform, out _)) { XcodePlatforms.Add(ApplePlatform); } } } 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) { bIncludeConfigFiles = false; bIncludeDocumentation = false; bIncludeShaderSource = false; bIncludeEngineSource = true; bGeneratingTemporaryProjects = true; // generate just the engine project if (OnlyGameProject != null) { bGeneratingGameProjectFiles = true; } } } protected override void AddAdditionalNativeTargetInformation(PlatformProjectGeneratorCollection PlatformProjectGenerators, List> Targets, ILogger Logger) { using ITimelineEvent Timer = Timeline.ScopeEvent("Adding additional Xcode project info", Logger); DateTime MainStart = DateTime.UtcNow; ParallelOptions Options = new ParallelOptions { //MaxDegreeOfParallelism=1, }; Parallel.ForEach(Targets, Options, TargetPair => { // don't bother if we aren't interested in this target if (SingleTargetName != null && !TargetPair.Item2.Name.Equals(SingleTargetName, StringComparison.InvariantCultureIgnoreCase)) { return; } ProjectFile TargetProjectFile = TargetPair.Item1; if (TargetProjectFile.IsContentOnlyProject) { return; } // don't do this for legacy projects, for speed ((XcodeProjectXcconfig.XcodeProjectFile)TargetProjectFile).ConditionalCreateLegacyProject(); if (((XcodeProjectXcconfig.XcodeProjectFile)TargetProjectFile).bHasLegacyProject) { return; } ProjectTarget CurTarget = TargetPair.Item2; HashSet PlatformsToGenerate = [UnrealTargetPlatform.Mac, UnrealTargetPlatform.IOS]; PlatformsToGenerate.IntersectWith(CurTarget.SupportedPlatforms); foreach (UnrealTargetPlatform Platform in PlatformsToGenerate) { UnrealArch Arch = UnrealArch.Arm64; TargetDescriptor TargetDesc = new TargetDescriptor(CurTarget.UnrealProjectFilePath, CurTarget.Name, Platform, UnrealTargetConfiguration.Development, new UnrealArchitectures(Arch), null); TargetDesc.IntermediateEnvironment = UnrealIntermediateEnvironment.GenerateProjectFiles; DateTime Start = DateTime.UtcNow; try { // Create the target UEBuildTarget Target = UEBuildTarget.Create(TargetDesc, true, false, bUsePrecompiled, Logger); List Frameworks = new(); List Bundles = new(); List Dylibs = new(); // Generate a compile environment for each module in the binary, and cache the one per Target, since that is the slowest one CppCompileEnvironment GlobalCompileEnvironment = Target.CreateCompileEnvironmentForProjectFiles(Logger); GlobalCompileEnvironmentCache.TryAdd(MakeGlobalCompileEnvironmentCacheKey(CurTarget.UnrealProjectFilePath, CurTarget.Name, Platform, UnrealTargetConfiguration.Development, new UnrealArchitectures(Arch)), GlobalCompileEnvironment); 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); } // walk over CPP and External modules looking for dylibs that need to be copied into the .app directly (not in a framework) foreach (UEBuildModule Module in Binary.Modules) { Dylibs.AddRange(Module.Rules.RuntimeDependencies.Inner.Where(x => x.Path.StartsWith("$(BinaryOutputDir)") && Path.GetExtension(x.SourcePath) == ".dylib")); Bundles.AddRange(Module.Rules.AdditionalBundleResources.Select(x => new UEBuildBundleResource(x))); } } // 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()); } } if (Dylibs.Count > 0) { lock (TargetRawDylibs) { TargetRawDylibs.Add(Tuple.Create(TargetProjectFile, Platform), Dylibs); } } } catch (Exception Ex) { Logger.LogDebug("Failed to build target {Target} for {Platform}. Skipping it for GettingNativeInfo. Exception:\n{Exception}", TargetProjectFile.ProjectFilePath.GetFileNameWithoutAnyExtensions(), Platform, Ex.Message); } Logger.LogDebug("GettingNativeInfo [{Project} / {Platform}] {TimeMs}ms", TargetProjectFile.ProjectFilePath.GetFileNameWithoutAnyExtensions(), Platform, (DateTime.UtcNow - Start).TotalMilliseconds); } }); Timer.Finish(); } } }