| // Copyright 2016 The Tulsi Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import Foundation |
| |
| /// Provides a set of project paths to stub Info.plist files to be used by |
| /// generated targets. |
| struct StubInfoPlistPaths { |
| let resourcesDirectory: String |
| let defaultStub: String |
| let watchOSStub: String |
| let watchOSAppExStub: String |
| |
| func stubPlist(_ entry: RuleEntry) -> String { |
| |
| switch entry.pbxTargetType! { |
| case .Watch1App, .Watch2App: |
| return watchOSStub |
| |
| case .Watch1Extension, .Watch2Extension: |
| return watchOSAppExStub |
| |
| case .MessagesExtension: |
| fallthrough |
| case .MessagesStickerPackExtension: |
| fallthrough |
| case .AppExtension: |
| return stubProjectPath(forRuleEntry: entry) |
| |
| default: |
| return defaultStub |
| } |
| } |
| |
| func plistFilename(forRuleEntry ruleEntry: RuleEntry) -> String { |
| return "Stub_\(ruleEntry.label.asFullPBXTargetName!).plist" |
| } |
| |
| func stubProjectPath(forRuleEntry ruleEntry: RuleEntry) -> String { |
| let fileName = plistFilename(forRuleEntry: ruleEntry) |
| return "\(resourcesDirectory)/\(fileName)" |
| } |
| } |
| |
| /// Provides a set of project paths to stub binary files to use in the generated |
| /// Xcode project. |
| struct StubBinaryPaths { |
| let clang: String |
| let swiftc: String |
| let ld: String |
| } |
| |
| /// Defines an object that can populate a PBXProject based on RuleEntry's. |
| protocol PBXTargetGeneratorProtocol: AnyObject { |
| static func getRunTestTargetBuildConfigPrefix() -> String |
| |
| static func workingDirectoryForPBXGroup(_ group: PBXGroup) -> String |
| |
| /// Returns a new PBXGroup instance appropriate for use as a top level project group. |
| static func mainGroupForOutputFolder(_ outputFolderURL: URL, workspaceRootURL: URL) -> PBXGroup |
| |
| init(bazelPath: String, |
| bazelBinPath: String, |
| project: PBXProject, |
| buildScriptPath: String, |
| resignerScriptPath: String, |
| stubInfoPlistPaths: StubInfoPlistPaths, |
| stubBinaryPaths: StubBinaryPaths, |
| tulsiVersion: String, |
| options: TulsiOptionSet, |
| localizedMessageLogger: LocalizedMessageLogger, |
| workspaceRootURL: URL, |
| suppressCompilerDefines: Bool, |
| redactWorkspaceSymlink: Bool) |
| |
| /// Generates file references for the given file paths in the associated project without adding |
| /// them to an indexer target. The paths must be relative to the workspace root. If pathFilters is |
| /// non-nil, paths that do not match an entry in the pathFilters set will be omitted. |
| func generateFileReferencesForFilePaths(_ paths: [String], pathFilters: Set<String>?) |
| |
| /// Registers the given Bazel rule and its transitive dependencies for inclusion by the Xcode |
| /// indexer, adding source files whose directories are present in pathFilters. The rule will |
| /// only be processed if it hasn't already (and therefore isn't in processedEntries). |
| /// - processedEntries: Map of RuleEntry to cumulative preprocessor framework search paths. |
| func registerRuleEntryForIndexer(_ ruleEntry: RuleEntry, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>, |
| processedEntries: inout [RuleEntry: (NSOrderedSet)]) |
| |
| /// Generates indexer targets for rules that were previously registered through |
| /// registerRuleEntryForIndexer. This method may only be called once, after all rule entries have |
| /// been registered. |
| /// Returns a map of indexer targets, keyed by the name of the indexer. |
| func generateIndexerTargets() -> [String: PBXTarget] |
| |
| /// Generates a legacy target that is added as a dependency of all build targets and invokes |
| /// the given script. The build action may be accessed by the script via the ACTION environment |
| /// variable. |
| func generateBazelCleanTarget(_ scriptPath: String, workingDirectory: String, |
| startupOptions: [String]) |
| |
| /// Generates project-level build configurations. |
| func generateTopLevelBuildConfigurations(_ buildSettingOverrides: [String: String]) |
| |
| /// Generates Xcode build targets that invoke Bazel for the given targets. For test-type rules, |
| /// non-compiling source file linkages are created to facilitate indexing of XCTests. |
| /// |
| /// If `pathFilters` is nil, no path filtering is done for test sources (to keep legacy behavior |
| /// for users who were depending upon it). |
| /// |
| /// Returns a mapping from build label to generated PBXNativeTarget. |
| /// Throws if one of the RuleEntry instances is for an unsupported Bazel target type. |
| func generateBuildTargetsForRuleEntries( |
| _ entries: Set<RuleEntry>, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>? |
| ) throws -> [BuildLabel: PBXNativeTarget] |
| } |
| |
| extension PBXTargetGeneratorProtocol { |
| func generateFileReferencesForFilePaths(_ paths: [String]) { |
| generateFileReferencesForFilePaths(paths, pathFilters: nil) |
| } |
| } |
| |
| |
| /// Concrete PBXProject target generator. |
| final class PBXTargetGenerator: PBXTargetGeneratorProtocol { |
| |
| enum ProjectSerializationError: Error { |
| case buildFileIsNotContainedByProjectRoot |
| case generalFailure(String) |
| case unsupportedTargetType(String, String) |
| } |
| |
| /// Names of Xcode build configurations to generate. |
| // NOTE: Must be kept in sync with the CONFIGURATION environment variable use in the build script. |
| static let buildConfigNames = ["Debug", "Release"] |
| |
| /// Tuples consisting of a test runner config name and the base config name (from |
| /// buildConfigNames) that it inherits from. |
| static let testRunnerEnabledBuildConfigNames = ["Debug", "Release"].map({ |
| (runTestTargetBuildConfigPrefix + $0, $0) |
| }) |
| |
| /// Prefix for special configs used when running XCTests that prevent compilation and linking of |
| /// any source files. This allows XCTest bundles to have associated test sources indexed by Xcode |
| /// but not compiled when testing (as they're compiled by Bazel and the generated project may be |
| /// missing information necessary to compile them anyway). Configs are generated for Debug and |
| /// Release builds. |
| // NOTE: This value needs to be kept in sync with the bazel_build script. |
| static let runTestTargetBuildConfigPrefix = "__TulsiTestRunner_" |
| static func getRunTestTargetBuildConfigPrefix() -> String { |
| return runTestTargetBuildConfigPrefix |
| } |
| |
| /// Name of the static library target that will be used to accumulate all source file dependencies |
| /// in order to make their symbols available to the Xcode indexer. |
| static let IndexerTargetPrefix = "_idx_" |
| |
| /// Rough sanity limit on indexer name length. Names may be slightly longer than this limit. |
| /// Empirically, 255 characters is the observed threshold for problems with file systems. |
| static let MaxIndexerNameLength = 180 |
| |
| // Name prefix for auto-generated nop-app extension targets necessary to get Xcode to debug watch |
| // Apps. |
| private static let watchAppExtensionTargetPrefix = "_tulsi_appex_" |
| |
| /// Name of the legacy target that will be used to communicate with Bazel during Xcode clean |
| /// actions. |
| static let BazelCleanTarget = "_bazel_clean_" |
| |
| /// Xcode variable name used to refer to the workspace root. |
| static let WorkspaceRootVarName = "TULSI_WR" |
| |
| /// Symlink to the Bazel workspace |
| static let TulsiWorkspacePath = "tulsi-workspace" |
| |
| /// Xcode variable name used to refer to the symlink generated by Bazel that contains all |
| /// workspace content. |
| static let BazelWorkspaceSymlinkVarName = "TULSI_BWRS" |
| |
| /// Path to the Bazel executable. |
| let bazelPath: String |
| |
| /// Location of the bazel bin symlink, relative to the workspace root. |
| let bazelBinPath: String |
| private(set) lazy var bazelGenfilesPath: String = { [unowned self] in |
| return self.bazelBinPath.replacingOccurrences(of: "-bin", with: "-genfiles") |
| }() |
| |
| /// Previous path to the Tulsi generated outputs root. We remap any paths of this form to the |
| /// new `tulsiIncludesPath` form automatically for convenience. |
| static let legacyTulsiIncludesPath = "_tulsi-includes/x/x" |
| |
| /// The path to the Tulsi generated outputs root. For more information see tulsi_aspects.bzl |
| static let tulsiIncludesPath = "bazel-tulsi-includes/x/x" |
| |
| let project: PBXProject |
| let buildScriptPath: String |
| let resignerScriptPath: String |
| let stubInfoPlistPaths: StubInfoPlistPaths |
| let stubBinaryPaths: StubBinaryPaths |
| let tulsiVersion: String |
| let options: TulsiOptionSet |
| let localizedMessageLogger: LocalizedMessageLogger |
| let workspaceRootURL: URL |
| let suppressCompilerDefines: Bool |
| let redactWorkspaceSymlink: Bool |
| |
| var bazelCleanScriptTarget: PBXLegacyTarget? = nil |
| |
| /// Stores data about a given RuleEntry to be used in order to generate Xcode indexer targets. |
| private struct IndexerData { |
| /// Provides information about the RuleEntry instances supported by an IndexerData. |
| /// Specifically, NameInfoToken tuples provide the targetName, full target label hash, and |
| /// potentially the target configuration in order to differentiate between rules with the |
| /// same name but different paths and configurations. |
| struct NameInfoToken { |
| let targetName: String |
| let labelHash: Int |
| |
| init(ruleEntry: RuleEntry) { |
| self.init(label: ruleEntry.label) |
| } |
| |
| init(label: BuildLabel) { |
| targetName = label.targetName! |
| labelHash = label.hashValue |
| } |
| } |
| |
| let indexerNameInfo: [NameInfoToken] |
| let dependencies: Set<BuildLabel> |
| let resolvedDependencies: Set<RuleEntry> |
| let preprocessorDefines: Set<String> |
| let otherCFlags: [String] |
| let otherSwiftFlags: [String] |
| let includes: [String] |
| let frameworkSearchPaths: [String] |
| let swiftIncludePaths: [String] |
| let deploymentTarget: DeploymentTarget |
| let buildPhase: PBXSourcesBuildPhase |
| let pchFile: BazelFileInfo? |
| let bridgingHeader: BazelFileInfo? |
| let enableModules: Bool |
| |
| /// Returns the deploymentTarget as a string for an indexerName. |
| static func deploymentTargetLabel(_ deploymentTarget: DeploymentTarget) -> String { |
| return String(format: "%@_min%@", |
| deploymentTarget.platform.rawValue, |
| deploymentTarget.osVersion) |
| } |
| |
| /// Returns the deploymentTarget as a string for the indexerName. |
| var deploymentTargetLabel: String { |
| return IndexerData.deploymentTargetLabel(deploymentTarget) |
| } |
| |
| /// Returns the full name that should be used when generating a target for this indexer. |
| var indexerName: String { |
| var fullName = "" |
| var fullHash = 0 |
| |
| for token in indexerNameInfo { |
| if fullName.isEmpty { |
| fullName = token.targetName |
| } else { |
| fullName += "_\(token.targetName)" |
| } |
| fullHash = fullHash &+ token.labelHash |
| } |
| return PBXTargetGenerator.indexerNameForTargetName(fullName, |
| hash: fullHash, |
| suffix: deploymentTargetLabel) |
| } |
| |
| /// Returns an array of aliases for this indexer data. Each element is the full indexerName of |
| /// an IndexerData instance that has been merged into this IndexerData. |
| var supportedIndexingTargets: [String] { |
| var supportedTargets = [indexerName] |
| if indexerNameInfo.count > 1 { |
| for token in indexerNameInfo { |
| supportedTargets.append(PBXTargetGenerator.indexerNameForTargetName(token.targetName, |
| hash: token.labelHash, |
| suffix: deploymentTargetLabel)) |
| } |
| } |
| return supportedTargets |
| } |
| |
| /// Returns an array of indexing target names that this indexer depends on. |
| var indexerNamesForResolvedDependencies: [String] { |
| let parentDeploymentTargetLabel = self.deploymentTargetLabel |
| return resolvedDependencies.map() { entry in |
| let deploymentTargetLabel: String |
| if let deploymentTarget = entry.deploymentTarget { |
| deploymentTargetLabel = IndexerData.deploymentTargetLabel(deploymentTarget) |
| } else { |
| deploymentTargetLabel = parentDeploymentTargetLabel |
| } |
| return PBXTargetGenerator.indexerNameForTargetName(entry.label.targetName!, |
| hash: entry.label.hashValue, |
| suffix: deploymentTargetLabel) |
| } |
| } |
| |
| /// Indicates whether or not this indexer may be merged with the given indexer. |
| func canMergeWith(_ other: IndexerData) -> Bool { |
| if self.pchFile != other.pchFile || self.bridgingHeader != other.bridgingHeader { |
| return false |
| } |
| |
| if !(preprocessorDefines == other.preprocessorDefines && |
| enableModules == other.enableModules && |
| otherCFlags == other.otherCFlags && |
| otherSwiftFlags == other.otherSwiftFlags && |
| frameworkSearchPaths == other.frameworkSearchPaths && |
| includes == other.includes && |
| swiftIncludePaths == other.swiftIncludePaths && |
| deploymentTarget == other.deploymentTarget) { |
| return false |
| } |
| |
| return true |
| } |
| |
| /// Returns a new IndexerData instance that is the result of merging this indexer with another. |
| func merging(_ other: IndexerData) -> IndexerData { |
| let newDependencies = dependencies.union(other.dependencies) |
| let newResolvedDependencies = resolvedDependencies.union(other.resolvedDependencies) |
| let newName = indexerNameInfo + other.indexerNameInfo |
| let newBuildPhase = PBXSourcesBuildPhase() |
| newBuildPhase.files = buildPhase.files + other.buildPhase.files |
| |
| return IndexerData(indexerNameInfo: newName, |
| dependencies: newDependencies, |
| resolvedDependencies: newResolvedDependencies, |
| preprocessorDefines: preprocessorDefines, |
| otherCFlags: otherCFlags, |
| otherSwiftFlags: otherSwiftFlags, |
| includes: includes, |
| frameworkSearchPaths: frameworkSearchPaths, |
| swiftIncludePaths: swiftIncludePaths, |
| deploymentTarget: deploymentTarget, |
| buildPhase: newBuildPhase, |
| pchFile: pchFile, |
| bridgingHeader: bridgingHeader, |
| enableModules: enableModules) |
| } |
| } |
| |
| /// Registered indexers that will be modeled as static libraries. |
| private var staticIndexers = [String: IndexerData]() |
| /// Registered indexers that will be modeled as dynamic frameworks. |
| private var frameworkIndexers = [String: IndexerData]() |
| |
| /// Maps the names of indexer targets to the generated target instance in the project. Values are |
| /// not guaranteed to be unique, as several targets may be merged into a single target during |
| /// optimization. |
| private var indexerTargetByName = [String: PBXTarget]() |
| |
| static func workingDirectoryForPBXGroup(_ group: PBXGroup) -> String { |
| switch group.sourceTree { |
| case .SourceRoot: |
| if let relativePath = group.path, !relativePath.isEmpty { |
| return "${SRCROOT}/\(relativePath)" |
| } |
| return "" |
| |
| case .Absolute: |
| return group.path! |
| |
| default: |
| assertionFailure("Group has an unexpected sourceTree type \(group.sourceTree)") |
| return "" |
| } |
| } |
| |
| static func mainGroupForOutputFolder(_ outputFolderURL: URL, workspaceRootURL: URL) -> PBXGroup { |
| let outputFolder = outputFolderURL.path |
| let workspaceRoot = workspaceRootURL.path |
| |
| let slashTerminatedOutputFolder = outputFolder + (outputFolder.hasSuffix("/") ? "" : "/") |
| let slashTerminatedWorkspaceRoot = workspaceRoot + (workspaceRoot.hasSuffix("/") ? "" : "/") |
| |
| // If workspaceRoot == outputFolder, return a relative group with no path. |
| if slashTerminatedOutputFolder == slashTerminatedWorkspaceRoot { |
| return PBXGroup(name: "mainGroup", path: nil, sourceTree: .SourceRoot, parent: nil) |
| } |
| |
| // If outputFolder contains workspaceRoot, return a relative group with the path from |
| // outputFolder to workspaceRoot |
| if workspaceRoot.hasPrefix(slashTerminatedOutputFolder) { |
| let index = workspaceRoot.index(workspaceRoot.startIndex, offsetBy: slashTerminatedOutputFolder.count) |
| let relativePath = String(workspaceRoot[index...]) |
| return PBXGroup(name: "mainGroup", |
| path: relativePath, |
| sourceTree: .SourceRoot, |
| parent: nil) |
| } |
| |
| // If workspaceRoot contains outputFolder, return a relative group using .. to walk up to |
| // workspaceRoot from outputFolder. |
| if outputFolder.hasPrefix(slashTerminatedWorkspaceRoot) { |
| let index = outputFolder.index(outputFolder.startIndex, offsetBy: slashTerminatedWorkspaceRoot.count + 1) |
| let pathToWalkBackUp = String(outputFolder[index...]) as NSString |
| let numberOfDirectoriesToWalk = pathToWalkBackUp.pathComponents.count |
| let relativePath = [String](repeating: "..", count: numberOfDirectoriesToWalk).joined(separator: "/") |
| return PBXGroup(name: "mainGroup", |
| path: relativePath, |
| sourceTree: .SourceRoot, |
| parent: nil) |
| } |
| |
| return PBXGroup(name: "mainGroup", |
| path: workspaceRootURL.path, |
| sourceTree: .Absolute, |
| parent: nil) |
| } |
| |
| /// Returns a project-relative path for the given BazelFileInfo. |
| private static func projectRefForBazelFileInfo(_ info: BazelFileInfo) -> String { |
| switch info.targetType { |
| case .generatedFile: |
| return "$(\(WorkspaceRootVarName))/\(info.fullPath)" |
| case .sourceFile: |
| return "$(\(BazelWorkspaceSymlinkVarName))/\(info.fullPath)" |
| } |
| } |
| |
| /// Returns the default Deployment Target (iOS 9). This is just a sensible default |
| /// in the odd case that we didn't get a Deployment Target from the Aspect. |
| private static func defaultDeploymentTarget() -> DeploymentTarget { |
| return DeploymentTarget(platform: .ios, osVersion: "9.0") |
| } |
| |
| /// Computed property to determine if USER_HEADER_SEARCH_PATHS should be set for Objective-C |
| /// targets. |
| var improvedImportAutocompletionFix: Bool { |
| return options[.ImprovedImportAutocompletionFix].commonValueAsBool ?? true |
| } |
| |
| init(bazelPath: String, |
| bazelBinPath: String, |
| project: PBXProject, |
| buildScriptPath: String, |
| resignerScriptPath: String, |
| stubInfoPlistPaths: StubInfoPlistPaths, |
| stubBinaryPaths: StubBinaryPaths, |
| tulsiVersion: String, |
| options: TulsiOptionSet, |
| localizedMessageLogger: LocalizedMessageLogger, |
| workspaceRootURL: URL, |
| suppressCompilerDefines: Bool = false, |
| redactWorkspaceSymlink: Bool = false) { |
| self.bazelPath = bazelPath |
| self.bazelBinPath = bazelBinPath |
| self.project = project |
| self.buildScriptPath = buildScriptPath |
| self.resignerScriptPath = resignerScriptPath |
| self.stubInfoPlistPaths = stubInfoPlistPaths |
| self.stubBinaryPaths = stubBinaryPaths |
| self.tulsiVersion = tulsiVersion |
| self.options = options |
| self.localizedMessageLogger = localizedMessageLogger |
| self.workspaceRootURL = workspaceRootURL |
| self.suppressCompilerDefines = suppressCompilerDefines |
| self.redactWorkspaceSymlink = redactWorkspaceSymlink |
| } |
| |
| func generateFileReferencesForFilePaths(_ paths: [String], pathFilters: Set<String>?) { |
| if let pathFilters = pathFilters { |
| let filteredPaths = paths.filter(pathFilterFunc(pathFilters)) |
| project.getOrCreateGroupsAndFileReferencesForPaths(filteredPaths) |
| } else { |
| project.getOrCreateGroupsAndFileReferencesForPaths(paths) |
| } |
| } |
| |
| /// Registers the given Bazel rule and its transitive dependencies for inclusion by the Xcode |
| /// indexer, adding source files whose directories are present in pathFilters. The rule will |
| /// only be processed if it hasn't already (and therefore isn't in processedEntries). |
| /// - processedEntries: Map of RuleEntry to cumulative preprocessor framework search paths. |
| func registerRuleEntryForIndexer(_ ruleEntry: RuleEntry, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>, |
| processedEntries: inout [RuleEntry: (NSOrderedSet)]) { |
| let includePathInProject = pathFilterFunc(pathFilters) |
| func includeFileInProject(_ info: BazelFileInfo) -> Bool { |
| return includePathInProject(info.fullPath) |
| } |
| |
| func addFileReference(_ info: BazelFileInfo) { |
| let (_, fileReferences) = project.getOrCreateGroupsAndFileReferencesForPaths([info.fullPath]) |
| fileReferences.first!.isInputFile = info.targetType == .sourceFile |
| } |
| |
| func addBuildFileForRule(_ ruleEntry: RuleEntry) { |
| guard let buildFilePath = ruleEntry.buildFilePath, includePathInProject(buildFilePath) else { |
| return |
| } |
| project.getOrCreateGroupsAndFileReferencesForPaths([buildFilePath]) |
| } |
| |
| // Recursively find all targets that are direct dependencies of test targets, and skip adding |
| // indexers for them, because their sources will be added directly to the test target. |
| var ruleEntryLabelsToSkipForIndexing = Set<BuildLabel>() |
| func addTestDepsToSkipList(_ ruleEntry: RuleEntry) { |
| if ruleEntry.pbxTargetType?.isTest ?? false { |
| for dep in ruleEntry.dependencies { |
| ruleEntryLabelsToSkipForIndexing.insert(dep) |
| guard let depEntry = ruleEntryMap.ruleEntry(buildLabel: dep, depender: ruleEntry) else { |
| localizedMessageLogger.warning("UnknownTargetRule", |
| comment: "Failure to look up a Bazel target that was expected to be present. The target label is %1$@", |
| values: dep.value) |
| continue |
| } |
| addTestDepsToSkipList(depEntry) |
| } |
| } |
| } |
| addTestDepsToSkipList(ruleEntry) |
| |
| // TODO(b/63628175): Clean this nested method to also retrieve framework_dir and framework_file |
| // from the ObjcProvider, for both static and dynamic frameworks. |
| @discardableResult |
| func generateIndexerTargetGraphForRuleEntry(_ ruleEntry: RuleEntry) -> (NSOrderedSet) { |
| if let data = processedEntries[ruleEntry] { |
| return data |
| } |
| let frameworkSearchPaths = NSMutableOrderedSet() |
| |
| defer { |
| processedEntries[ruleEntry] = (frameworkSearchPaths) |
| } |
| |
| var resolvedDependecies = [RuleEntry]() |
| for dep in ruleEntry.dependencies { |
| guard let depEntry = ruleEntryMap.ruleEntry(buildLabel: dep, depender: ruleEntry) else { |
| localizedMessageLogger.warning("UnknownTargetRule", |
| comment: "Failure to look up a Bazel target that was expected to be present. The target label is %1$@", |
| values: dep.value) |
| continue |
| } |
| |
| resolvedDependecies.append(depEntry) |
| let inheritedFrameworkSearchPaths = generateIndexerTargetGraphForRuleEntry(depEntry) |
| frameworkSearchPaths.union(inheritedFrameworkSearchPaths) |
| } |
| var defines = Set<String>() |
| if let ruleDefines = ruleEntry.objcDefines { |
| defines.formUnion(ruleDefines) |
| } |
| |
| if !suppressCompilerDefines, |
| let ruleDefines = ruleEntry.attributes[.compiler_defines] as? [String], !ruleDefines.isEmpty { |
| defines.formUnion(ruleDefines) |
| } |
| |
| let includes = NSMutableOrderedSet() |
| addIncludes(ruleEntry, toSet: includes) |
| |
| // Search path entries are added for all framework imports, regardless of whether the |
| // framework bundles are allowed by the include filters. The search path excludes the bundle |
| // itself. |
| ruleEntry.frameworkImports.forEach() { |
| let fullPath = $0.fullPath as NSString |
| let rootedPath = "$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(fullPath.deletingLastPathComponent)" |
| frameworkSearchPaths.add(rootedPath) |
| } |
| let sourceFileInfos = ruleEntry.sourceFiles.filter(includeFileInProject) |
| let nonARCSourceFileInfos = ruleEntry.nonARCSourceFiles.filter(includeFileInProject) |
| let frameworkFileInfos = ruleEntry.frameworkImports.filter(includeFileInProject) |
| let nonSourceVersionedFileInfos = ruleEntry.versionedNonSourceArtifacts.filter(includeFileInProject) |
| |
| for target in ruleEntry.normalNonSourceArtifacts.filter(includeFileInProject) { |
| let path = target.fullPath |
| let (_, ref) = project.createGroupsAndFileReferenceForPath(path, underGroup: project.mainGroup) |
| ref.isInputFile = target.targetType == .sourceFile |
| } |
| |
| // Indexer targets aren't needed: |
| // - if the target is a filegroup (we generate an indexer for what references the filegroup). |
| // - if the target has no source files (there's nothing to index!) |
| // - if the target is a test bundle (we generate proper targets for these). |
| // - if the target is a direct dependency of a test target (these sources are added directly to the test target). |
| if (sourceFileInfos.isEmpty && |
| nonARCSourceFileInfos.isEmpty && |
| frameworkFileInfos.isEmpty && |
| nonSourceVersionedFileInfos.isEmpty) |
| || ruleEntry.pbxTargetType?.isTest ?? false |
| || ruleEntry.type == "filegroup" |
| || ruleEntryLabelsToSkipForIndexing.contains(ruleEntry.label) { |
| addBuildFileForRule(ruleEntry) |
| return (frameworkSearchPaths) |
| } |
| |
| var localPreprocessorDefines = defines |
| let localIncludes = includes.mutableCopy() as! NSMutableOrderedSet |
| let otherCFlags = NSMutableArray() |
| let swiftIncludePaths = NSMutableOrderedSet() |
| let otherSwiftFlags = NSMutableArray() |
| addLocalSettings(ruleEntry, localDefines: &localPreprocessorDefines, localIncludes: localIncludes, |
| otherCFlags: otherCFlags, swiftIncludePaths: swiftIncludePaths, otherSwiftFlags: otherSwiftFlags) |
| |
| addOtherSwiftFlags(ruleEntry, toArray: otherSwiftFlags) |
| addSwiftIncludes(ruleEntry, toSet: swiftIncludePaths) |
| |
| let pchFile = BazelFileInfo(info: ruleEntry.attributes[.pch]) |
| if let pchFile = pchFile, includeFileInProject(pchFile) { |
| addFileReference(pchFile) |
| } |
| |
| let bridgingHeader = BazelFileInfo(info: ruleEntry.attributes[.bridging_header]) |
| if let bridgingHeader = bridgingHeader, includeFileInProject(bridgingHeader) { |
| addFileReference(bridgingHeader) |
| } |
| let enableModules = (ruleEntry.attributes[.enable_modules] as? Bool) == true |
| |
| addBuildFileForRule(ruleEntry) |
| |
| let (nonARCFiles, nonARCSettings) = generateFileReferencesAndSettingsForNonARCFileInfos(nonARCSourceFileInfos) |
| var fileReferences = generateFileReferencesForFileInfos(sourceFileInfos) |
| fileReferences.append(contentsOf: generateFileReferencesForFileInfos(frameworkFileInfos)) |
| fileReferences.append(contentsOf: nonARCFiles) |
| |
| if !nonSourceVersionedFileInfos.isEmpty { |
| createReferencesForVersionedFileTargets(nonSourceVersionedFileInfos) |
| } |
| let buildPhase = createBuildPhaseForReferences(fileReferences, |
| withPerFileSettings: nonARCSettings) |
| |
| if !buildPhase.files.isEmpty { |
| let resolvedIncludes = localIncludes.array as! [String] |
| |
| let deploymentTarget: DeploymentTarget |
| if let ruleDeploymentTarget = ruleEntry.deploymentTarget { |
| deploymentTarget = ruleDeploymentTarget |
| } else { |
| deploymentTarget = PBXTargetGenerator.defaultDeploymentTarget() |
| localizedMessageLogger.warning("NoDeploymentTarget", |
| comment: "Rule Entry for %1$@ has no DeploymentTarget set. Defaulting to iOS 9.", |
| values: ruleEntry.label.value) |
| } |
| |
| let indexerData = IndexerData(indexerNameInfo: [IndexerData.NameInfoToken(ruleEntry: ruleEntry)], |
| dependencies: ruleEntry.dependencies, |
| resolvedDependencies: Set(resolvedDependecies), |
| preprocessorDefines: localPreprocessorDefines, |
| otherCFlags: otherCFlags as! [String], |
| otherSwiftFlags: otherSwiftFlags as! [String], |
| includes: resolvedIncludes, |
| frameworkSearchPaths: frameworkSearchPaths.array as! [String], |
| swiftIncludePaths: swiftIncludePaths.array as! [String], |
| deploymentTarget: deploymentTarget, |
| buildPhase: buildPhase, |
| pchFile: pchFile, |
| bridgingHeader: bridgingHeader, |
| enableModules: enableModules) |
| let isSwiftRule = ruleEntry.attributes[.has_swift_info] as? Bool ?? false |
| if (isSwiftRule) { |
| frameworkIndexers[indexerData.indexerName] = indexerData |
| } else { |
| staticIndexers[indexerData.indexerName] = indexerData |
| } |
| } |
| |
| return (frameworkSearchPaths) |
| } |
| |
| generateIndexerTargetGraphForRuleEntry(ruleEntry) |
| } |
| |
| @discardableResult |
| func generateIndexerTargets() -> [String: PBXTarget] { |
| mergeRegisteredIndexers() |
| |
| func generateIndexer(_ name: String, |
| indexerType: PBXTarget.ProductType, |
| data: IndexerData) { |
| let indexingTarget = project.createNativeTarget(name, |
| deploymentTarget: nil, |
| targetType: indexerType, |
| isIndexerTarget: true) |
| indexingTarget.buildPhases.append(data.buildPhase) |
| addConfigsForIndexingTarget(indexingTarget, data: data) |
| |
| for name in data.supportedIndexingTargets { |
| indexerTargetByName[name] = indexingTarget |
| } |
| } |
| |
| for (name, data) in staticIndexers { |
| generateIndexer(name, indexerType: PBXTarget.ProductType.StaticLibrary, data: data) |
| } |
| |
| for (name, data) in frameworkIndexers { |
| generateIndexer(name, indexerType: PBXTarget.ProductType.Framework, data: data) |
| } |
| |
| func linkDependencies(_ dataMap: [String: IndexerData]) { |
| for (name, data) in dataMap { |
| guard let indexerTarget = indexerTargetByName[name] else { |
| localizedMessageLogger.infoMessage("Unexpectedly failed to resolve indexer \(name)") |
| continue |
| } |
| |
| for depName in data.indexerNamesForResolvedDependencies { |
| guard let indexerDependency = indexerTargetByName[depName], indexerDependency !== indexerTarget else { |
| continue |
| } |
| |
| indexerTarget.createDependencyOn(indexerDependency, |
| proxyType: PBXContainerItemProxy.ProxyType.targetReference, |
| inProject: project) |
| } |
| } |
| } |
| |
| linkDependencies(staticIndexers) |
| linkDependencies(frameworkIndexers) |
| |
| return indexerTargetByName |
| } |
| |
| func generateBazelCleanTarget(_ scriptPath: String, workingDirectory: String = "", |
| startupOptions: [String] = []) { |
| assert(bazelCleanScriptTarget == nil, "generateBazelCleanTarget may only be called once") |
| |
| let allArgs = [bazelPath, bazelBinPath] + startupOptions |
| let buildArgs = allArgs.map { "\"\($0)\""}.joined(separator: " ") |
| |
| bazelCleanScriptTarget = project.createLegacyTarget(PBXTargetGenerator.BazelCleanTarget, |
| deploymentTarget: nil, |
| buildToolPath: "\(scriptPath)", |
| buildArguments: buildArgs, |
| buildWorkingDirectory: workingDirectory) |
| |
| for target: PBXTarget in project.allTargets { |
| if target === bazelCleanScriptTarget { |
| continue |
| } |
| |
| target.createDependencyOn(bazelCleanScriptTarget!, |
| proxyType: PBXContainerItemProxy.ProxyType.targetReference, |
| inProject: project, |
| first: true) |
| } |
| } |
| |
| func generateTopLevelBuildConfigurations(_ buildSettingOverrides: [String: String] = [:]) { |
| var buildSettings = options.commonBuildSettings() |
| |
| for (key, value) in buildSettingOverrides { |
| buildSettings[key] = value |
| } |
| |
| buildSettings["ONLY_ACTIVE_ARCH"] = "YES" |
| // Fixes an Xcode "Upgrade to recommended settings" warning. Technically the warning only |
| // requires this to be added to the Debug build configuration but as code is never compiled |
| // anyway it doesn't hurt anything to set it on all configs. |
| buildSettings["ENABLE_TESTABILITY"] = "YES" |
| |
| // Bazel sources are more or less ARC by default (the user has to use the special non_arc_srcs |
| // attribute for non-ARC) so the project is set to reflect that and per-file flags are used to |
| // override the default. |
| buildSettings["CLANG_ENABLE_OBJC_ARC"] = "YES" |
| |
| // Bazel takes care of signing the generated applications, so Xcode's signing must be disabled. |
| buildSettings["CODE_SIGNING_REQUIRED"] = "NO" |
| buildSettings["CODE_SIGN_IDENTITY"] = "" |
| // This is required to disable code signing with the new build system. |
| if !options.useLegacyBuildSystem { |
| buildSettings["CODE_SIGNING_ALLOWED"] = "NO" |
| } |
| |
| // Explicitly setting the FRAMEWORK_SEARCH_PATHS will allow Xcode to resolve references to the |
| // XCTest framework when performing Live issues analysis. |
| buildSettings["FRAMEWORK_SEARCH_PATHS"] = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; |
| |
| // Prevent Xcode from replacing the Swift StdLib dylibs that Bazel already packaged. |
| buildSettings["DONT_RUN_SWIFT_STDLIB_TOOL"] = "YES" |
| |
| var sourceDirectory = PBXTargetGenerator.workingDirectoryForPBXGroup(project.mainGroup) |
| if sourceDirectory.isEmpty { |
| sourceDirectory = "$(SRCROOT)" |
| } |
| |
| // A variable pointing to the true root is provided for scripts that may need it. |
| buildSettings["\(PBXTargetGenerator.WorkspaceRootVarName)"] = sourceDirectory |
| |
| // Bazel generates a symlink to a directory that collects all of the data needed for this |
| // workspace. While this is often identical to the workspace, it sometimes collects other paths |
| // and is the better option for most Xcode project path references. |
| // This directory is symlinked to `tulsi-workspace` during builds. |
| // The symlink is located inside of the project package as opposed to relative to the workspace |
| // so that it is using the same local file system as the project to maximize performance. |
| // In some cases where the workspace was on a remote volume, jumping through the symlink on the |
| // remote volume that pointed back to local disk was causing performance issues. |
| buildSettings["\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName)"] = |
| "$(PROJECT_FILE_PATH)/.tulsi/\(PBXTargetGenerator.TulsiWorkspacePath)" |
| |
| buildSettings["TULSI_VERSION"] = tulsiVersion |
| if !options.useLegacyBuildSystem { |
| // Mark that this project was generated with new build system support. |
| // We'll check this in bazel_build.py to issue a warning if you try |
| // to use the legacy integration with Xcode 14 or later. |
| buildSettings["TULSI_NEW_BUILD_SYSTEM"] = "YES" |
| } |
| |
| // Set default Python STDOUT encoding of scripts run by Xcode (such as bazel_build.py) to UTF-8. |
| // Otherwise, this would be the Python 2 default of ASCII, causing various encoding errors when |
| // handling UTF-8 output from Bazel BEP in bazel_build.py. |
| buildSettings["PYTHONIOENCODING"] = "utf8" |
| |
| let searchPaths = ["$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))", |
| "$(\(PBXTargetGenerator.WorkspaceRootVarName))/\(bazelBinPath)", |
| "$(\(PBXTargetGenerator.WorkspaceRootVarName))/\(bazelGenfilesPath)", |
| "$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(PBXTargetGenerator.tulsiIncludesPath)" |
| ] |
| // Ideally this would use USER_HEADER_SEARCH_PATHS but some code generation tools (e.g., |
| // protocol buffers) make use of system-style includes. |
| buildSettings["HEADER_SEARCH_PATHS"] = searchPaths.joined(separator: " ") |
| |
| // Configure our binary stubs if we're targetting the new build system. |
| if !options.useLegacyBuildSystem { |
| buildSettings["CC"] = stubBinaryPaths.clang |
| buildSettings["CXX"] = stubBinaryPaths.clang |
| buildSettings["LD"] = stubBinaryPaths.ld |
| buildSettings["LDPLUSPLUS"] = stubBinaryPaths.ld |
| buildSettings["SWIFT_EXEC"] = stubBinaryPaths.swiftc |
| // We must disable the integrated driver in order for Xcode 14 to |
| // use our $SWIFT_EXEC stub. Without this, Xcode 14 uses swift-frontend |
| // with no other way to stub it. |
| buildSettings["SWIFT_USE_INTEGRATED_DRIVER"] = "NO" |
| } |
| |
| createBuildConfigurationsForList(project.buildConfigurationList, buildSettings: buildSettings) |
| addTestRunnerBuildConfigurationToBuildConfigurationList(project.buildConfigurationList) |
| } |
| |
| /// Generates build targets for the given rule entries. |
| func generateBuildTargetsForRuleEntries( |
| _ ruleEntries: Set<RuleEntry>, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>? |
| ) throws -> [BuildLabel: PBXNativeTarget] { |
| let namedRuleEntries = generateUniqueNamesForRuleEntries(ruleEntries) |
| |
| let progressNotifier = ProgressNotifier(name: GeneratingBuildTargets, |
| maxValue: namedRuleEntries.count) |
| |
| var testTargetLinkages = [(PBXNativeTarget, BuildLabel?, RuleEntry)]() |
| var watchAppTargets = [String: (PBXNativeTarget, RuleEntry)]() |
| var watchExtensionsByEntry = [RuleEntry: PBXNativeTarget]() |
| var targetsByLabel = [BuildLabel: PBXNativeTarget]() |
| |
| for (name, entry) in namedRuleEntries { |
| progressNotifier.incrementValue() |
| let target = try createBuildTargetForRuleEntry(entry, |
| named: name, |
| ruleEntryMap: ruleEntryMap) |
| targetsByLabel[entry.label] = target |
| |
| if let script = options[.PreBuildPhaseRunScript, entry.label.value] { |
| let runScript = PBXShellScriptBuildPhase( |
| shellScript: script, |
| shellPath: "/bin/bash", |
| name: "Pre-build Run Script") |
| runScript.showEnvVarsInLog = true |
| target.buildPhases.insert(runScript, at: 0) |
| } |
| |
| if let script = options[.PostBuildPhaseRunScript, entry.label.value] { |
| let runScript = PBXShellScriptBuildPhase( |
| shellScript: script, |
| shellPath: "/bin/bash", |
| name: "Post-build Run Script") |
| runScript.showEnvVarsInLog = true |
| target.buildPhases.append(runScript) |
| } |
| |
| if let hostLabelString = entry.attributes[.test_host] as? String { |
| let hostLabel = BuildLabel(hostLabelString) |
| testTargetLinkages.append((target, hostLabel, entry)) |
| } else if entry.pbxTargetType == .UnitTest { |
| // If there is no host and it's a unit test, assume it doesn't need one, i.e. it's a |
| // library based test. |
| testTargetLinkages.append((target, nil, entry)) |
| } |
| |
| switch entry.pbxTargetType { |
| case .Watch2App?: |
| watchAppTargets[name] = (target, entry) |
| case .Watch2Extension?: |
| watchExtensionsByEntry[entry] = target |
| default: |
| break |
| } |
| } |
| |
| // The watch app target must have an explicit dependency on the watch extension target. |
| for (_, (watchAppTarget, watchRuleEntry)) in watchAppTargets { |
| for ext in watchRuleEntry.extensions { |
| if let extEntry = ruleEntryMap.ruleEntry(buildLabel: ext, depender: watchRuleEntry), |
| extEntry.pbxTargetType == .Watch2Extension { |
| if let watchExtensionTarget = watchExtensionsByEntry[extEntry] { |
| watchAppTarget.createDependencyOn(watchExtensionTarget, proxyType: .targetReference, inProject: project) |
| } else { |
| localizedMessageLogger.warning("FindingWatchExtensionFailed", |
| comment: "Message to show when the watchOS app extension %1$@ could not be found and the resulting project will not be able to launch the watch app.", |
| values: extEntry.label.value) |
| } |
| } |
| } |
| } |
| |
| for (testTarget, testHostLabel, entry) in testTargetLinkages { |
| let testHostTarget: PBXNativeTarget? |
| if let hostTargetLabel = testHostLabel { |
| testHostTarget = targetsByLabel[hostTargetLabel] |
| if testHostTarget == nil { |
| // If the user did not choose to include the host target it won't be available so the |
| // linkage can be skipped. We will still force the generation of this test host target to |
| // avoid issues when running tests as bundle targets in Xcode. |
| localizedMessageLogger.warning("MissingTestHost", |
| comment: "Warning to show when a user has selected an XCTest but not its host application.", |
| values: entry.label.value, hostTargetLabel.value) |
| continue |
| } |
| } else { |
| testHostTarget = nil |
| } |
| updateTestTarget(testTarget, |
| withLinkageToHostTarget: testHostTarget, |
| ruleEntry: entry, |
| ruleEntryMap: ruleEntryMap, |
| pathFilters: pathFilters) |
| } |
| return targetsByLabel |
| } |
| |
| // MARK: - Private methods |
| |
| /// Generates a filter function that may be used to verify that a path string is allowed by the |
| /// given set of pathFilters. |
| private func pathFilterFunc(_ pathFilters: Set<String>?) -> (String) -> Bool { |
| guard let pathFilters = pathFilters else { |
| return { (path: String) -> Bool in |
| return true |
| } |
| } |
| let recursiveFilters = Set<String>(pathFilters.filter({ $0.hasSuffix("/...") }).map() { |
| let index = $0.index($0.endIndex, offsetBy: -3) |
| return String($0[..<index]) |
| }) |
| |
| func includePath(_ path: String) -> Bool { |
| let dir = (path as NSString).deletingLastPathComponent |
| if pathFilters.contains(dir) { return true } |
| let terminatedDir = dir + "/" |
| for filter in recursiveFilters { |
| if terminatedDir.hasPrefix(filter) { return true } |
| } |
| return false |
| } |
| |
| return includePath |
| } |
| |
| /// Attempts to reduce the number of indexers by merging any that have identical settings. |
| private func mergeRegisteredIndexers() { |
| |
| func mergeIndexers<T : Sequence>(_ indexers: T) -> [String: IndexerData] where T.Iterator.Element == IndexerData { |
| var mergedIndexers = [String: IndexerData]() |
| var indexers = Array(indexers).sorted { $0.indexerName < $1.indexerName } |
| |
| while !indexers.isEmpty { |
| var remaining = [IndexerData]() |
| var d1 = indexers.popLast()! |
| for d2 in indexers { |
| if d1.canMergeWith(d2) { |
| d1 = d1.merging(d2) |
| } else { |
| remaining.append(d2) |
| } |
| } |
| |
| mergedIndexers[d1.indexerName] = d1 |
| indexers = remaining |
| } |
| |
| return mergedIndexers |
| } |
| |
| staticIndexers = mergeIndexers(staticIndexers.values) |
| frameworkIndexers = mergeIndexers(frameworkIndexers.values) |
| } |
| |
| private func generateFileReferencesForFileInfos( |
| _ infos: [BazelFileInfo], |
| includeGenfiles: Bool = false |
| ) -> [PBXFileReference] { |
| guard !infos.isEmpty else { return [] } |
| var generatedFilePaths = [String]() |
| var sourceFilePaths = [String]() |
| for info in infos { |
| switch info.targetType { |
| case .generatedFile: |
| generatedFilePaths.append(info.fullPath) |
| case .sourceFile: |
| sourceFilePaths.append(info.fullPath) |
| } |
| } |
| |
| // Add the source paths directly and the generated paths with explicitFileType set. |
| var (_, fileReferences) = project.getOrCreateGroupsAndFileReferencesForPaths(sourceFilePaths) |
| let (_, generatedFileReferences) = project.getOrCreateGroupsAndFileReferencesForPaths(generatedFilePaths) |
| generatedFileReferences.forEach() { $0.isInputFile = false } |
| |
| if includeGenfiles { |
| fileReferences.append(contentsOf: generatedFileReferences) |
| } |
| return fileReferences |
| } |
| |
| /// Generates file references for the given infos, and returns a settings dictionary to be passed |
| /// to createBuildPhaseForReferences:withPerFileSettings:. |
| private func generateFileReferencesAndSettingsForNonARCFileInfos( |
| _ infos: [BazelFileInfo], |
| includeGenfiles: Bool = true |
| ) -> ([PBXFileReference], [PBXFileReference: [String: String]]) { |
| let nonARCFileReferences = generateFileReferencesForFileInfos(infos, includeGenfiles: includeGenfiles) |
| var settings = [PBXFileReference: [String: String]]() |
| let disableARCSetting = ["COMPILER_FLAGS": "-fno-objc-arc"] |
| nonARCFileReferences.forEach() { |
| settings[$0] = disableARCSetting |
| } |
| return (nonARCFileReferences, settings) |
| } |
| |
| /// Find the longest common non-empty strict prefix for the given strings if there is one. |
| private func longestCommonPrefix(_ strings: Set<String>, separator: Character) -> String { |
| // Longest common prefix for 0 or 1 string(s) doesn't make sense. |
| guard strings.count >= 2, var shortestString = strings.first else { return "" } |
| for str in strings { |
| guard str.count < shortestString.count else { continue } |
| shortestString = str |
| } |
| |
| guard !shortestString.isEmpty else { return "" } |
| |
| // Drop the last so we can only get a strict prefix. |
| var components = shortestString.split(separator: separator).dropLast() |
| var potentialPrefix = "\(components.joined(separator: "\(separator)"))\(separator)" |
| |
| for str in strings { |
| while !components.isEmpty && !str.hasPrefix(potentialPrefix) { |
| components = components.dropLast() |
| potentialPrefix = "\(components.joined(separator: "\(separator)"))\(separator)" |
| } |
| } |
| return potentialPrefix |
| } |
| |
| /// Name the given `ruleEntries` using the `namer` function. |
| /// |
| /// `ruleEntries` must be mutually exclusive with the values in `named`. Intended use case: |
| /// call this first with an initial set and `namer`, and then subsequent calls should use the |
| /// results of the previous call (unnamed entries) with a different `namer`. |
| /// |
| /// Only unique names will be inserted into the `named` dictionary. If when naming a |
| /// `RuleEntry`, the name is already in the `named` dictionary, the previously named |
| /// `RuleEntry` will still be valid. |
| /// |
| /// Returns a `Set<RuleEntry>` representing the entries which still need to be named. |
| private func uniqueNames(for ruleEntries: Set<RuleEntry>, |
| named: inout [String: RuleEntry], |
| namer: (_ ruleEntry: RuleEntry) -> String? |
| ) -> Set<RuleEntry> { |
| var unnamed = Set<RuleEntry>() |
| |
| // Group the entries by name. |
| var ruleEntriesByName = [String: [RuleEntry]]() |
| for entry in ruleEntries { |
| guard let name = namer(entry) else { |
| unnamed.insert(entry) |
| continue |
| } |
| ruleEntriesByName[name, default: []].append(entry) |
| } |
| |
| for (name, entries) in ruleEntriesByName { |
| // Name already used or not unique. |
| guard entries.count == 1 && named.index(forKey: name) == nil else { |
| unnamed.formUnion(entries) |
| continue |
| } |
| named[name] = entries.first! |
| } |
| return unnamed |
| } |
| |
| /// Generate unique names for the given rule entries, using the bundle name when it is |
| /// unique. Otherwise, falls back to a name based on the target label. |
| private func generateUniqueNamesForRuleEntries(_ ruleEntries: Set<RuleEntry>) -> [String: RuleEntry] { |
| var named = [String: RuleEntry]() |
| // Try to name using the bundle names first, then the target name. |
| var unnamed = self.uniqueNames(for: ruleEntries, named: &named) { $0.bundleName } |
| unnamed = self.uniqueNames(for: unnamed, named: &named) { |
| $0.label.targetName |
| } |
| |
| // Continue only if we need to de-duplicate. |
| guard !unnamed.isEmpty else { |
| return named |
| } |
| |
| // Special handling for the remaining unnamed entries - use their full target label. |
| let conflictingFullNames = Set(unnamed.map { |
| $0.label.asFullPBXTargetName! |
| }) |
| |
| // Try to strip out a common prefix if we can find one. |
| let commonPrefix = self.longestCommonPrefix(conflictingFullNames, separator: "-") |
| |
| guard !commonPrefix.isEmpty else { |
| for entry in unnamed { |
| let fullName = entry.label.asFullPBXTargetName! |
| named[fullName] = entry |
| } |
| return named |
| } |
| |
| // Found a common prefix, we can strip it as long as we don't cause a new duplicate. |
| let charsToDrop = commonPrefix.count |
| for entry in unnamed { |
| let fullName = entry.label.asFullPBXTargetName! |
| let shortenedFullName = String(fullName.dropFirst(charsToDrop)) |
| guard !shortenedFullName.isEmpty && named.index(forKey: shortenedFullName) == nil else { |
| named[fullName] = entry |
| continue |
| } |
| named[shortenedFullName] = entry |
| } |
| |
| return named |
| } |
| |
| /// Adds the given file targets to a versioned group. |
| private func createReferencesForVersionedFileTargets(_ fileInfos: [BazelFileInfo]) { |
| var groups = [String: XCVersionGroup]() |
| |
| for info in fileInfos { |
| let path = info.fullPath as NSString |
| let versionedGroupPath = path.deletingLastPathComponent |
| let type = info.subPath.pbPathUTI ?? "" |
| let versionedGroup = project.getOrCreateVersionGroupForPath(versionedGroupPath, |
| versionGroupType: type) |
| if groups[versionedGroupPath] == nil { |
| groups[versionedGroupPath] = versionedGroup |
| } |
| let ref = versionedGroup.getOrCreateFileReferenceBySourceTree(.Group, |
| path: path as String) |
| ref.isInputFile = info.targetType == .sourceFile |
| } |
| |
| for (sourcePath, group) in groups { |
| setCurrentVersionForXCVersionGroup(group, atPath: sourcePath) |
| } |
| } |
| |
| // Attempt to read the .xccurrentversion plists in the xcdatamodeld's and sync up the |
| // currentVersion in the XCVersionGroup instances. Failure to specify the currentVersion will |
| // result in Xcode picking an arbitrary version. |
| private func setCurrentVersionForXCVersionGroup(_ group: XCVersionGroup, |
| atPath sourcePath: String) { |
| |
| let versionedBundleURL = workspaceRootURL.appendingPathComponent(sourcePath, |
| isDirectory: true) |
| let currentVersionPlistURL = versionedBundleURL.appendingPathComponent(".xccurrentversion", |
| isDirectory: false) |
| let path = currentVersionPlistURL.path |
| guard let data = FileManager.default.contents(atPath: path) else { |
| self.localizedMessageLogger.warning("LoadingXCCurrentVersionFailed", |
| comment: "Message to show when loading a .xccurrentversion file fails.", |
| values: group.name, "Version file at '\(path)' could not be read") |
| return |
| } |
| |
| do { |
| let plist = try PropertyListSerialization.propertyList(from: data, |
| options: PropertyListSerialization.MutabilityOptions(), |
| format: nil) as! [String: AnyObject] |
| if let currentVersion = plist["_XCCurrentVersionName"] as? String { |
| if !group.setCurrentVersionByName(currentVersion) { |
| self.localizedMessageLogger.warning("LoadingXCCurrentVersionFailed", |
| comment: "Message to show when loading a .xccurrentversion file fails.", |
| values: group.name, "Version '\(currentVersion)' specified by file at '\(path)' was not found") |
| } |
| } |
| } catch let e as NSError { |
| self.localizedMessageLogger.warning("LoadingXCCurrentVersionFailed", |
| comment: "Message to show when loading a .xccurrentversion file fails.", |
| values: group.name, "Version file at '\(path)' is invalid: \(e)") |
| } catch { |
| self.localizedMessageLogger.warning("LoadingXCCurrentVersionFailed", |
| comment: "Message to show when loading a .xccurrentversion file fails.", |
| values: group.name, "Version file at '\(path)' is invalid.") |
| } |
| } |
| |
| // Adds XCBuildConfigurations to the given indexer PBXTarget. |
| // Note that preprocessorDefines may or may not contain values with spaces. If it does contain |
| // spaces, the key will be escaped (e.g. -Dfoo bar becomes -D"foo bar"). |
| private func addConfigsForIndexingTarget(_ target: PBXTarget, data: IndexerData) { |
| |
| var buildSettings = options.buildSettingsForTarget(target.name) |
| buildSettings["PRODUCT_NAME"] = target.productName! |
| |
| if let pchFile = data.pchFile { |
| buildSettings["GCC_PREFIX_HEADER"] = PBXTargetGenerator.projectRefForBazelFileInfo(pchFile) |
| } |
| |
| var allOtherCFlags = data.otherCFlags.filter { !$0.hasPrefix("-W") } |
| // Escape the spaces in the defines by transforming -Dfoo bar into -D"foo bar". |
| if !data.preprocessorDefines.isEmpty { |
| allOtherCFlags.append(contentsOf: data.preprocessorDefines.sorted().map { define in |
| // Need to quote all defines with spaces that are not yet quoted. |
| if define.rangeOfCharacter(from: .whitespaces) != nil && |
| !((define.hasPrefix("\"") && define.hasSuffix("\"")) || |
| (define.hasPrefix("'") && define.hasSuffix("'"))) { |
| return "-D\"\(define)\"" |
| } |
| return "-D\(define)" |
| }) |
| } |
| |
| if !allOtherCFlags.isEmpty { |
| buildSettings["OTHER_CFLAGS"] = allOtherCFlags.joined(separator: " ") |
| } |
| |
| if let bridgingHeader = data.bridgingHeader { |
| buildSettings["SWIFT_OBJC_BRIDGING_HEADER"] = PBXTargetGenerator.projectRefForBazelFileInfo(bridgingHeader) |
| } |
| |
| if data.enableModules { |
| buildSettings["CLANG_ENABLE_MODULES"] = "YES" |
| } |
| |
| if !data.includes.isEmpty { |
| let includes = data.includes.joined(separator: " ") |
| buildSettings["HEADER_SEARCH_PATHS"] = "$(inherited) \(includes) " |
| } |
| |
| if !data.frameworkSearchPaths.isEmpty { |
| buildSettings["FRAMEWORK_SEARCH_PATHS"] = "$(inherited) " + data.frameworkSearchPaths.joined(separator: " ") |
| } |
| |
| if !data.swiftIncludePaths.isEmpty { |
| let paths = data.swiftIncludePaths.joined(separator: " ") |
| buildSettings["SWIFT_INCLUDE_PATHS"] = "$(inherited) \(paths)" |
| } |
| |
| if !data.otherSwiftFlags.isEmpty { |
| buildSettings["OTHER_SWIFT_FLAGS"] = "$(inherited) " + data.otherSwiftFlags.joined(separator: " ") |
| } |
| |
| // Set USER_HEADER_SEARCH_PATHS for non-Swift (meaning static library) indexer targets if the |
| // improved include/import setting is enabled. |
| if self.improvedImportAutocompletionFix, let nativeTarget = target as? PBXNativeTarget, |
| nativeTarget.productType == .StaticLibrary { |
| buildSettings["USER_HEADER_SEARCH_PATHS"] = "$(\(PBXTargetGenerator.WorkspaceRootVarName))" |
| } |
| |
| // Default the SDKROOT to the proper device SDK. |
| // Previously, we would force the indexer targets to the x86_64 simulator. This caused indexing |
| // to fail when building Swift for device, as the arm Swift modules would be discovered via |
| // tulsi-includes but Xcode would only index for x86_64. Note that just setting this is not |
| // enough; research has shown that Xcode needs a scheme for these indexer targets in order to |
| // use the proper ARCH for indexing, so we also generate an `_idx_Scheme` containing all |
| // indexer targets as build targets. |
| let deploymentTarget = data.deploymentTarget |
| let platform = deploymentTarget.platform |
| buildSettings["SDKROOT"] = platform.deviceSDK |
| buildSettings[platform.buildSettingsDeploymentTarget] = deploymentTarget.osVersion |
| |
| createBuildConfigurationsForList(target.buildConfigurationList, |
| buildSettings: buildSettings, |
| indexerSettingsOnly: true) |
| } |
| |
| /// Updates the build settings and optionally adds a "Compile sources" phase for the given test |
| /// bundle target. |
| private func updateTestTarget(_ target: PBXNativeTarget, |
| withLinkageToHostTarget hostTarget: PBXNativeTarget?, |
| ruleEntry: RuleEntry, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>?) { |
| // If the test target has a test host, check that it was included in the Tulsi configuration. |
| if let hostTarget = hostTarget { |
| project.linkTestTarget(target, toHostTarget: hostTarget) |
| } |
| updateTestTargetIndexer(target, ruleEntry: ruleEntry, hostTarget: hostTarget, ruleEntryMap: ruleEntryMap) |
| updateTestTargetBuildPhases(target, ruleEntry: ruleEntry, ruleEntryMap: ruleEntryMap, pathFilters: pathFilters) |
| } |
| |
| /// Updates the test target indexer with test specific values. |
| private func updateTestTargetIndexer(_ target: PBXNativeTarget, |
| ruleEntry: RuleEntry, |
| hostTarget: PBXNativeTarget?, |
| ruleEntryMap: RuleEntryMap) { |
| let testSettings = targetTestSettings(target, hostTarget: hostTarget, ruleEntry: ruleEntry, ruleEntryMap: ruleEntryMap) |
| |
| // Inherit the resolved values from the indexer. |
| let deploymentTarget = ruleEntry.deploymentTarget ?? PBXTargetGenerator.defaultDeploymentTarget() |
| let deploymentTargetLabel = IndexerData.deploymentTargetLabel(deploymentTarget) |
| let indexerName = PBXTargetGenerator.indexerNameForTargetName(ruleEntry.label.targetName!, |
| hash: ruleEntry.label.hashValue, |
| suffix: deploymentTargetLabel) |
| let indexerTarget = indexerTargetByName[indexerName] |
| updateMissingBuildConfigurationsForList(target.buildConfigurationList, |
| withBuildSettings: testSettings, |
| inheritingFromConfigurationList: indexerTarget?.buildConfigurationList, |
| suppressingBuildSettings: ["ARCHS", "VALID_ARCHS"]) |
| } |
| |
| private func updateTestTargetBuildPhases(_ target: PBXNativeTarget, |
| ruleEntry: RuleEntry, |
| ruleEntryMap: RuleEntryMap, |
| pathFilters: Set<String>?) { |
| let needDummyPhases = options.useLegacyBuildSystem |
| let includePathInProject = pathFilterFunc(pathFilters) |
| func includeFileInProject(_ info: BazelFileInfo) -> Bool { |
| return includePathInProject(info.fullPath) |
| } |
| let testSourceFileInfos = ruleEntry.sourceFiles.filter(includeFileInProject) |
| let testNonArcSourceFileInfos = ruleEntry.nonARCSourceFiles.filter(includeFileInProject) |
| let containsSwift = ruleEntry.attributes[.has_swift_dependency] as? Bool ?? false |
| |
| // For the Swift dummy files phase to work, it has to be placed before the Compile Sources build |
| // phase. |
| if needDummyPhases && containsSwift { |
| let testBuildPhase = createGenerateSwiftDummyFilesTestBuildPhase() |
| target.buildPhases.append(testBuildPhase) |
| } |
| if !testSourceFileInfos.isEmpty || !testNonArcSourceFileInfos.isEmpty { |
| if needDummyPhases { |
| // Create dummy dependency files for non-Swift code as Xcode expects Clang to generate them. |
| let allSources = testSourceFileInfos + testNonArcSourceFileInfos |
| let nonSwiftSources = allSources.filter { !$0.subPath.hasSuffix(".swift") } |
| if !nonSwiftSources.isEmpty { |
| let testBuildPhase = createGenerateDummyDependencyFilesTestBuildPhase(nonSwiftSources) |
| target.buildPhases.append(testBuildPhase) |
| } |
| } |
| var fileReferences = generateFileReferencesForFileInfos(testSourceFileInfos, includeGenfiles: false) |
| let (nonARCFiles, nonARCSettings) = |
| generateFileReferencesAndSettingsForNonARCFileInfos(testNonArcSourceFileInfos, includeGenfiles: false) |
| fileReferences.append(contentsOf: nonARCFiles) |
| let buildPhase = createBuildPhaseForReferences(fileReferences, |
| withPerFileSettings: nonARCSettings) |
| target.buildPhases.append(buildPhase) |
| } |
| } |
| |
| /// Adds includes paths from the RuleEntry to the given NSSet. |
| private func addIncludes(_ ruleEntry: RuleEntry, |
| toSet includes: NSMutableOrderedSet) { |
| if let includePaths = ruleEntry.includePaths { |
| let rootedPaths: [String] = includePaths.map() { (path, recursive) in |
| // Any paths of the tulsi-includes form will only be in the bazel workspace symlink since |
| // they refer to generated files from a build. |
| // Otherwise we assume the file exists in the workspace. |
| let prefixVar = path.hasPrefix(PBXTargetGenerator.tulsiIncludesPath) |
| ? PBXTargetGenerator.BazelWorkspaceSymlinkVarName |
| : PBXTargetGenerator.WorkspaceRootVarName |
| let rootedPath = "$(\(prefixVar))/\(path)" |
| if recursive { |
| return "\(rootedPath)/**" |
| } |
| return rootedPath |
| } |
| includes.addObjects(from: rootedPaths) |
| } |
| } |
| |
| /// Adds swift include paths from the RuleEntry to the given NSSet. |
| private func addSwiftIncludes(_ ruleEntry: RuleEntry, |
| toSet swiftIncludes: NSMutableOrderedSet) { |
| for module in ruleEntry.swiftTransitiveModules { |
| let fullPath = module.fullPath as NSString |
| let includePath = fullPath.deletingLastPathComponent |
| swiftIncludes.add("$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(includePath)") |
| } |
| } |
| |
| /// Returns other swift compiler flags for the given target based on the RuleEntry. |
| private func addOtherSwiftFlags(_ ruleEntry: RuleEntry, toArray swiftFlags: NSMutableArray) { |
| // Load module maps explicitly instead of letting Clang discover them on search paths. This |
| // is needed to avoid a case where Clang may load the same header both in modular and |
| // non-modular contexts, leading to duplicate definitions in the same file. |
| // See llvm.org/bugs/show_bug.cgi?id=19501 |
| swiftFlags.addObjects(from: ruleEntry.objCModuleMaps.map() { |
| "-Xcc -fmodule-map-file=$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\($0.fullPath)" |
| }) |
| |
| if let swiftDefines = ruleEntry.swiftDefines { |
| for flag in swiftDefines { |
| swiftFlags.add("-D\(flag)") |
| } |
| } |
| } |
| |
| /// Reads the RuleEntry's copts and puts the arguments into the correct set. |
| private func addLocalSettings(_ ruleEntry: RuleEntry, |
| localDefines: inout Set<String>, |
| localIncludes: NSMutableOrderedSet, |
| otherCFlags: NSMutableArray, |
| swiftIncludePaths: NSMutableOrderedSet, |
| otherSwiftFlags: NSMutableArray) { |
| if let swiftc_opts = ruleEntry.attributes[.swiftc_opts] as? [String], !swiftc_opts.isEmpty { |
| for opt in swiftc_opts { |
| if opt.hasPrefix("-I") { |
| let index = opt.index(opt.startIndex, offsetBy: 2) |
| var path = String(opt[index...]) |
| if !path.hasPrefix("/") { |
| path = "$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(path)" |
| } |
| swiftIncludePaths.add(path) |
| } else { |
| otherSwiftFlags.add(opt) |
| } |
| } |
| } |
| guard let copts = ruleEntry.attributes[.copts] as? [String], !copts.isEmpty else { |
| return |
| } |
| for opt in copts { |
| if opt.hasPrefix("-D") { |
| let index = opt.index(opt.startIndex, offsetBy: 2) |
| localDefines.insert(String(opt[index...])) |
| } else if opt.hasPrefix("-I") { |
| let index = opt.index(opt.startIndex, offsetBy: 2) |
| var path = String(opt[index...]) |
| if !path.hasPrefix("/") { |
| path = "$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(path)" |
| } |
| localIncludes.add(path) |
| } else { |
| otherCFlags.add(opt) |
| } |
| } |
| } |
| |
| /// Returns test specific settings for test targets. |
| private func targetTestSettings(_ target: PBXNativeTarget, |
| hostTarget: PBXNativeTarget?, |
| ruleEntry: RuleEntry, |
| ruleEntryMap: RuleEntryMap) -> [String: String] { |
| var testSettings = ["TULSI_TEST_RUNNER_ONLY": "YES"] |
| // Attempt to update the build configs for the target to include BUNDLE_LOADER and TEST_HOST |
| // values, linking the test target to its host. |
| if let hostTargetPath = hostTarget?.productReference?.path, |
| let hostTargetProductName = hostTarget?.productName, |
| let deploymentTarget = target.deploymentTarget { |
| |
| if target.productType == .UIUnitTest { |
| testSettings["TEST_TARGET_NAME"] = hostTargetProductName |
| } else if let testHostPath = deploymentTarget.platform.testHostPath(hostTargetPath: hostTargetPath, |
| hostTargetProductName: hostTargetProductName) { |
| testSettings["BUNDLE_LOADER"] = "$(TEST_HOST)" |
| testSettings["TEST_HOST"] = testHostPath |
| } |
| } |
| |
| let includes = NSMutableOrderedSet() |
| |
| // We don't use the defines at the moment but the function will add them anyway. We could try |
| // to use the defines but we'd have to do so on a per-file basis as this Test target can contain |
| // files from multiple targets. |
| var defines = Set<String>() |
| let swiftIncludePaths = NSMutableOrderedSet() |
| let otherSwiftFlags = NSMutableArray() |
| |
| addIncludes(ruleEntry, toSet: includes) |
| addLocalSettings(ruleEntry, localDefines: &defines, localIncludes: includes, |
| otherCFlags: NSMutableArray(), swiftIncludePaths: NSMutableOrderedSet(), |
| otherSwiftFlags: NSMutableArray()) |
| addSwiftIncludes(ruleEntry, toSet: swiftIncludePaths) |
| addOtherSwiftFlags(ruleEntry, toArray: otherSwiftFlags) |
| |
| let includesArr = includes.array as! [String] |
| if !includesArr.isEmpty { |
| testSettings["HEADER_SEARCH_PATHS"] = "$(inherited) " + includesArr.joined(separator: " ") |
| } |
| |
| if let swiftIncludes = swiftIncludePaths.array as? [String], !swiftIncludes.isEmpty { |
| testSettings["SWIFT_INCLUDE_PATHS"] = "$(inherited) " + swiftIncludes.joined(separator: " ") |
| } |
| |
| if let otherSwiftFlagsArr = otherSwiftFlags as? [String], !otherSwiftFlagsArr.isEmpty { |
| testSettings["OTHER_SWIFT_FLAGS"] = "$(inherited) " + otherSwiftFlagsArr.joined(separator: " ") |
| } |
| |
| if let moduleName = ruleEntry.moduleName { |
| testSettings["PRODUCT_MODULE_NAME"] = moduleName |
| } |
| |
| return testSettings |
| } |
| |
| // Adds a dummy build configuration to the given list based off of the Debug config that is |
| // used to effectively disable compilation when running XCTests by converting each compile call |
| // into a "clang --version" invocation. |
| private func addTestRunnerBuildConfigurationToBuildConfigurationList(_ list: XCConfigurationList) { |
| |
| func createTestConfigNamed(_ testConfigName: String, |
| forBaseConfigNamed configurationName: String) { |
| let baseConfig = list.getOrCreateBuildConfiguration(configurationName) |
| let config = list.getOrCreateBuildConfiguration(testConfigName) |
| |
| var runTestTargetBuildSettings = baseConfig.buildSettings |
| // Prevent compilation invocations from actually compiling ObjC, C and Swift files. |
| runTestTargetBuildSettings["OTHER_CFLAGS"] = "--version" |
| runTestTargetBuildSettings["OTHER_SWIFT_FLAGS"] = "--version" |
| // Prevents linker invocations from attempting to use the .o files which were never generated |
| // due to compilation being turned into nop's. |
| runTestTargetBuildSettings["OTHER_LDFLAGS"] = "--version" |
| |
| // Force the output of the -emit-objc-header flag to a known value. This should be kept in |
| // sync with the RunScript build phase created in createGenerateSwiftDummyFilesTestBuildPhase. |
| runTestTargetBuildSettings["SWIFT_OBJC_INTERFACE_HEADER_NAME"] = "$(PRODUCT_NAME).h" |
| |
| // Disable the generation of ObjC header files from Swift for test targets. |
| runTestTargetBuildSettings["SWIFT_INSTALL_OBJC_HEADER"] = "NO" |
| |
| // Prevent Xcode from attempting to create a fat binary with lipo from artifacts that were |
| // never generated by the linker nop's. |
| runTestTargetBuildSettings["ONLY_ACTIVE_ARCH"] = "YES" |
| |
| // Wipe out settings that are not useful for test runners |
| // These do nothing and can cause issues due to exceeding environment limits: |
| runTestTargetBuildSettings["FRAMEWORK_SEARCH_PATHS"] = "" |
| runTestTargetBuildSettings["HEADER_SEARCH_PATHS"] = "" |
| |
| config.buildSettings = runTestTargetBuildSettings |
| } |
| |
| for (testConfigName, configName) in PBXTargetGenerator.testRunnerEnabledBuildConfigNames { |
| createTestConfigNamed(testConfigName, forBaseConfigNamed: configName) |
| } |
| } |
| |
| private func createBuildConfigurationsForList(_ buildConfigurationList: XCConfigurationList, |
| buildSettings: Dictionary<String, String>, |
| indexerSettingsOnly: Bool = false) { |
| func addPreprocessorDefine(_ define: String, toConfig config: XCBuildConfiguration) { |
| if let existingDefinitions = config.buildSettings["GCC_PREPROCESSOR_DEFINITIONS"] { |
| // NOTE(abaire): Technically this should probably check first to see if "define" has been |
| // set but in the foreseeable usage it's unlikely that this if condition would |
| // ever trigger at all. |
| config.buildSettings["GCC_PREPROCESSOR_DEFINITIONS"] = existingDefinitions + " \(define)" |
| } else { |
| config.buildSettings["GCC_PREPROCESSOR_DEFINITIONS"] = define |
| } |
| } |
| |
| for configName in PBXTargetGenerator.buildConfigNames { |
| let config = buildConfigurationList.getOrCreateBuildConfiguration(configName) |
| config.buildSettings = buildSettings |
| |
| // Insert any defines that are injected by Bazel's ObjcConfiguration. |
| if configName == "Debug" { |
| addPreprocessorDefine("DEBUG=1", toConfig: config) |
| } else if configName == "Release" { |
| addPreprocessorDefine("NDEBUG=1", toConfig: config) |
| } |
| } |
| } |
| |
| private func updateMissingBuildConfigurationsForList(_ buildConfigurationList: XCConfigurationList, |
| withBuildSettings newSettings: Dictionary<String, String>, |
| inheritingFromConfigurationList baseConfigurationList: XCConfigurationList? = nil, |
| suppressingBuildSettings suppressedKeys: Set<String> = []) { |
| func mergeDictionary(_ old: inout [String: String], |
| withContentsOfDictionary new: [String: String]) { |
| for (key, value) in new { |
| if let _ = old[key] { continue } |
| if suppressedKeys.contains(key) { continue } |
| old.updateValue(value, forKey: key) |
| } |
| } |
| |
| for configName in PBXTargetGenerator.buildConfigNames { |
| let config = buildConfigurationList.getOrCreateBuildConfiguration(configName) |
| mergeDictionary(&config.buildSettings, withContentsOfDictionary: newSettings) |
| |
| if let baseSettings = baseConfigurationList?.getBuildConfiguration(configName)?.buildSettings { |
| mergeDictionary(&config.buildSettings, withContentsOfDictionary: baseSettings) |
| } |
| } |
| |
| for (testRunnerConfigName, configName) in PBXTargetGenerator.testRunnerEnabledBuildConfigNames { |
| let config = buildConfigurationList.getOrCreateBuildConfiguration(testRunnerConfigName) |
| mergeDictionary(&config.buildSettings, withContentsOfDictionary: newSettings) |
| |
| if let baseSettings = baseConfigurationList?.getBuildConfiguration(testRunnerConfigName)?.buildSettings { |
| mergeDictionary(&config.buildSettings, withContentsOfDictionary: baseSettings) |
| } else if let baseSettings = baseConfigurationList?.getBuildConfiguration(configName)?.buildSettings { |
| // Fall back to the base config name if the base configuration list doesn't support a given |
| // test runner. |
| mergeDictionary(&config.buildSettings, withContentsOfDictionary: baseSettings) |
| } |
| } |
| } |
| |
| static func indexerNameForTargetName(_ targetName: String, hash: Int, suffix: String?) -> String { |
| let normalizedTargetName: String |
| if targetName.count > MaxIndexerNameLength { |
| let endIndex = targetName.index(targetName.startIndex, offsetBy: MaxIndexerNameLength - 4) |
| normalizedTargetName = String(targetName[..<endIndex]) + "_etc" |
| } else { |
| normalizedTargetName = targetName |
| } |
| if let suffix = suffix { |
| return String(format: "\(IndexerTargetPrefix)\(normalizedTargetName)_%08X_%@", hash, suffix) |
| } |
| return String(format: "\(IndexerTargetPrefix)\(normalizedTargetName)_%08X", hash) |
| } |
| |
| // Creates a PBXSourcesBuildPhase with the given references, optionally applying the given |
| // per-file settings to each. |
| private func createBuildPhaseForReferences(_ refs: [PBXReference], |
| withPerFileSettings settings: [PBXFileReference: [String: String]]? = nil) -> PBXSourcesBuildPhase { |
| let buildPhase = PBXSourcesBuildPhase() |
| |
| for ref in refs { |
| if let ref = ref as? PBXFileReference { |
| // Do not add header files to the build phase. |
| guard let fileUTI = ref.uti, fileUTI.hasPrefix("sourcecode.") && !fileUTI.hasSuffix(".h") else { |
| continue |
| } |
| buildPhase.files.append(PBXBuildFile(fileRef: ref, settings: settings?[ref])) |
| } else { |
| buildPhase.files.append(PBXBuildFile(fileRef: ref)) |
| } |
| |
| } |
| return buildPhase |
| } |
| |
| /// Creates a PBXNativeTarget for the given rule entry, returning it. |
| private func createBuildTargetForRuleEntry(_ entry: RuleEntry, |
| named name: String, |
| ruleEntryMap: RuleEntryMap) |
| throws -> (PBXNativeTarget) { |
| guard let pbxTargetType = entry.pbxTargetType else { |
| throw ProjectSerializationError.unsupportedTargetType(entry.type, entry.label.value) |
| } |
| let target = project.createNativeTarget(name, |
| deploymentTarget: entry.deploymentTarget, |
| targetType: pbxTargetType) |
| |
| for f in entry.secondaryArtifacts { |
| project.createProductReference(f.fullPath) |
| } |
| |
| var buildSettings = options.buildSettingsForTarget(name) |
| buildSettings["TULSI_BUILD_PATH"] = entry.label.packageName! |
| |
| // iOS test targets need to resign the test dylibs/frameworks that Xcode |
| // injects, otherwise they won't be signed properly for running on device. |
| // |
| // For UI tests with the new build system, we can't resign from within the |
| // same target, so instead we resign from a `PBXAggregateTarget`. To track |
| // which artifacts need to be resigned between the two targets, we use the |
| // resigning manifest below. |
| if !options.useLegacyBuildSystem && entry.pbxTargetType == .UIUnitTest && |
| entry.deploymentTarget?.platform == PlatformType.ios { |
| let manifest = "tulsi_resign_manifest.json" |
| self.createResignerTargetForUITestTarget( |
| target, |
| buildSettings: [ |
| "SDKROOT": PlatformType.ios.deviceSDK, |
| "TULSI_RESIGN_MANIFEST": |
| "$(CONFIGURATION_TEMP_DIR)/\(name).build/\(manifest)", |
| ]) |
| |
| buildSettings["TULSI_RESIGN_MANIFEST"] = "$(TARGET_TEMP_DIR)/\(manifest)" |
| } |
| // For the new build system, we do want to enable code-signing specifically |
| // for simulator test targets. This will let Xcode adhoc codesign the |
| // test runner. |
| if !options.useLegacyBuildSystem && entry.pbxTargetType?.isTest ?? false { |
| buildSettings["CODE_SIGNING_ALLOWED[sdk=iphonesimulator*]"] = "YES" |
| } |
| |
| buildSettings["PRODUCT_NAME"] = name |
| if let bundleID = entry.bundleID { |
| buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = bundleID |
| } |
| if let sdkRoot = entry.XcodeSDKRoot { |
| buildSettings["SDKROOT"] = sdkRoot |
| } |
| |
| // An invalid launch image is set in order to suppress Xcode's warning about missing default |
| // launch images. |
| buildSettings["ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME"] = "Stub Launch Image" |
| buildSettings["INFOPLIST_FILE"] = stubInfoPlistPaths.stubPlist(entry) |
| |
| if let deploymentTarget = entry.deploymentTarget { |
| buildSettings[deploymentTarget.platform.buildSettingsDeploymentTarget] = deploymentTarget.osVersion |
| } |
| |
| // watchOS1 apps require TARGETED_DEVICE_FAMILY to be overridden as they are a specialization of |
| // an iOS target rather than a true standalone (like watchOS2 and later). |
| if pbxTargetType == .Watch1App { |
| buildSettings["TARGETED_DEVICE_FAMILY"] = "4" |
| buildSettings["TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]"] = "1,4" |
| } |
| |
| // App clips are improperly signed by Xcode when using the legacy build system even with |
| // CODE_SIGNING_REQUIRED=NO so disable code signing and let bazel_build.py do the necessary |
| // signing. |
| if pbxTargetType == .AppClip { |
| buildSettings["CODE_SIGNING_ALLOWED"] = "NO" |
| } |
| |
| // bazel_build.py uses this to determine if it needs to pass the --xcode_version flag, as the |
| // flag can have implications for caching even if the user's active Xcode version is the same |
| // as the flag. |
| if let xcodeVersion = entry.xcodeVersion { |
| buildSettings["TULSI_XCODE_VERSION"] = xcodeVersion |
| } |
| |
| // Disable Xcode's attempts at generating dSYM bundles as it conflicts with the operation of the |
| // special test runner build configurations (which have associated sources but don't actually |
| // compile anything). |
| buildSettings["DEBUG_INFORMATION_FORMAT"] = "dwarf" |
| |
| // The following settings are simply passed through the environment for use by build scripts. |
| buildSettings["BAZEL_TARGET"] = entry.label.value |
| |
| createBuildConfigurationsForList(target.buildConfigurationList, buildSettings: buildSettings) |
| addTestRunnerBuildConfigurationToBuildConfigurationList(target.buildConfigurationList) |
| |
| if let buildPhase = createBazelBuildPhaseForRuleEntry(entry) { |
| target.buildPhases.append(buildPhase) |
| } |
| |
| if let legacyTarget = bazelCleanScriptTarget { |
| target.createDependencyOn(legacyTarget, |
| proxyType: PBXContainerItemProxy.ProxyType.targetReference, |
| inProject: project, |
| first: true) |
| } |
| |
| return target |
| } |
| |
| private func createGenerateSwiftDummyFilesTestBuildPhase() -> PBXShellScriptBuildPhase { |
| let shellScript = |
| "# Script to generate specific Swift files Xcode expects when running tests.\n" + |
| "set -eu\n" + |
| "ARCH_ARRAY=($ARCHS)\n" + |
| "SUFFIXES=(swiftdoc swiftmodule)\n" + |
| "for ARCH in \"${ARCH_ARRAY[@]}\"\n" + |
| "do\n" + |
| " mkdir -p \"$OBJECT_FILE_DIR_normal/$ARCH/\"\n" + |
| " touch \"$OBJECT_FILE_DIR_normal/$ARCH/$SWIFT_OBJC_INTERFACE_HEADER_NAME\"\n" + |
| " for SUFFIX in \"${SUFFIXES[@]}\"\n" + |
| " do\n" + |
| " touch \"$OBJECT_FILE_DIR_normal/$ARCH/$PRODUCT_MODULE_NAME.$SUFFIX\"\n" + |
| " done\n" + |
| "done\n" |
| |
| let buildPhase = PBXShellScriptBuildPhase( |
| shellScript: shellScript, |
| shellPath: "/bin/bash", |
| name: "Swift dummy file generation") |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "SwiftDummy" |
| return buildPhase |
| } |
| |
| private func createGenerateDummyDependencyFilesTestBuildPhase(_ sources: [BazelFileInfo]) -> PBXShellScriptBuildPhase { |
| let files = sources.map { ($0.subPath as NSString).deletingPathExtension.pbPathLastComponent } |
| let shellScript = """ |
| # Script to generate dependency files Xcode expects when running tests. |
| set -eu |
| ARCH_ARRAY=($ARCHS) |
| FILES=(\(files.map { $0.escapingForShell }.joined(separator: " "))) |
| for ARCH in "${ARCH_ARRAY[@]}" |
| do |
| mkdir -p "$OBJECT_FILE_DIR_normal/$ARCH/" |
| rm -f "$OBJECT_FILE_DIR_normal/$ARCH/${PRODUCT_NAME}_dependency_info.dat" |
| printf '\\x00\\x31\\x00' >"$OBJECT_FILE_DIR_normal/$ARCH/${PRODUCT_NAME}_dependency_info.dat" |
| for FILE in "${FILES[@]}" |
| do |
| touch "$OBJECT_FILE_DIR_normal/$ARCH/$FILE.d" |
| done |
| done |
| """ |
| let buildPhase = PBXShellScriptBuildPhase( |
| shellScript: shellScript, |
| shellPath: "/bin/bash", |
| name: "Objective-C dummy file generation") |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "ObjcDummy" |
| return buildPhase |
| } |
| |
| private func createBazelBuildPhaseForRuleEntry(_ entry: RuleEntry) |
| -> PBXShellScriptBuildPhase? { |
| let buildLabel = entry.label.value |
| let commandLine = buildScriptCommandlineForBuildLabels(buildLabel) |
| let workingDirectory = PBXTargetGenerator.workingDirectoryForPBXGroup(project.mainGroup) |
| let changeDirectoryAction: String |
| if workingDirectory.isEmpty { |
| changeDirectoryAction = "" |
| } else { |
| changeDirectoryAction = "cd \"\(workingDirectory)\"" |
| } |
| let shellScript = "set -e\n" + |
| "\(changeDirectoryAction)\n" + |
| "exec \(commandLine)" |
| |
| // Using the Info.plist as an input forces Xcode to run this after processing the Info.plist, |
| // allowing our script to safely overwrite the Info.plist after Xcode does its processing. |
| let inputPaths = ["$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)"] |
| let buildPhase = PBXShellScriptBuildPhase( |
| shellScript: shellScript, |
| shellPath: "/bin/bash", |
| name: "build \(entry.label)", |
| inputPaths: inputPaths, |
| alwaysOutOfDate: true |
| ) |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "BazelBuild" |
| return buildPhase |
| } |
| |
| /// Create a `PBXAggregateTarget` which calls the resigner script to resign |
| /// the given UI test target's artifacts so the artifacts are properly signed |
| /// for running on device. |
| /// |
| /// Ideally we could have a regular `PBXShellScriptBuildPhase` for the UI test |
| /// target itself, but due to FB5418543 the new build system does not allow |
| /// run scripts to depend upon test frameworks/to have the script always run |
| /// after the test artifacts are copied over (only for UI tests, regular unit |
| /// tests do allow this behavior in Xcode 13). |
| /// |
| /// To work around this, we need a separate target which can depend upon the |
| /// test frameworks by depending upon the UI test target *and* including this |
| /// new target in all schemes containing the UI test target. |
| private func createResignerTargetForUITestTarget( |
| _ testTarget: PBXNativeTarget, |
| buildSettings: Dictionary<String, String> |
| ) { |
| let aggregate = project.createAggregateTarget( |
| testTarget.name + "_Resigner", deploymentTarget: testTarget.deploymentTarget) |
| // The resigner depends upon the test target since it needs to resign the test bundle. |
| aggregate.createDependencyOn( |
| testTarget, proxyType: .targetReference, inProject: project, first: true) |
| aggregate.buildPhases.append(self.createResignerBuildPhase()) |
| |
| // The test target's scheme needs to depend upon the aggregate resigner |
| // target in order for the resigner to run during the build. |
| testTarget.createSchemeBuildDependencyOn(aggregate) |
| |
| let buildConfigurationList = aggregate.buildConfigurationList |
| for (testName, baseName) in PBXTargetGenerator.testRunnerEnabledBuildConfigNames { |
| let testConfig = buildConfigurationList.getOrCreateBuildConfiguration(testName) |
| testConfig.buildSettings = buildSettings |
| |
| let baseConfig = buildConfigurationList.getOrCreateBuildConfiguration(baseName) |
| baseConfig.buildSettings = buildSettings |
| } |
| } |
| |
| private func createResignerBuildPhase() -> PBXShellScriptBuildPhase { |
| let shellScript = "set -e\nexec \"\(resignerScriptPath)\"" |
| |
| let buildPhase = PBXShellScriptBuildPhase( |
| shellScript: shellScript, |
| shellPath: "/bin/bash", |
| name: "Resign test artifacts", |
| alwaysOutOfDate: true |
| ) |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "Resigner" |
| return buildPhase |
| } |
| |
| /// Constructs a commandline string that will invoke the bazel build script to generate the given |
| /// buildLabels (a space-separated set of Bazel target labels). |
| private func buildScriptCommandlineForBuildLabels(_ buildLabels: String) -> String { |
| return "\"\(buildScriptPath)\" " + |
| "\(buildLabels) " + |
| "--bazel \"\(bazelPath)\" " + |
| "--bazel_bin_path \"\(bazelBinPath)\" " + |
| "--verbose " |
| } |
| } |