| // 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)" |
| } |
| } |
| |
| /// Defines an object that can populate a PBXProject based on RuleEntry's. |
| protocol PBXTargetGeneratorProtocol: class { |
| 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(bazelURL: URL, |
| bazelBinPath: String, |
| project: PBXProject, |
| buildScriptPath: String, |
| stubInfoPlistPaths: StubInfoPlistPaths, |
| 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) |
| |
| /// 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. |
| /// Throws if one of the RuleEntry instances is for an unsupported Bazel target type. |
| func generateBuildTargetsForRuleEntries(_ entries: Set<RuleEntry>, |
| ruleEntryMap: RuleEntryMap) throws |
| } |
| |
| 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) |
| } |
| |
| /// 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" |
| |
| /// Location of the bazel binary. |
| let bazelURL: URL |
| |
| /// 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") |
| }() |
| |
| /// The path to the Tulsi generated outputs root. For more information see tulsi_aspects.bzl |
| let tulsiIncludesPath = "_tulsi-includes/x/x" |
| |
| let project: PBXProject |
| let buildScriptPath: String |
| let stubInfoPlistPaths: StubInfoPlistPaths |
| 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(bazelURL: URL, |
| bazelBinPath: String, |
| project: PBXProject, |
| buildScriptPath: String, |
| stubInfoPlistPaths: StubInfoPlistPaths, |
| tulsiVersion: String, |
| options: TulsiOptionSet, |
| localizedMessageLogger: LocalizedMessageLogger, |
| workspaceRootURL: URL, |
| suppressCompilerDefines: Bool = false, |
| redactWorkspaceSymlink: Bool = false) { |
| self.bazelURL = bazelURL |
| self.bazelBinPath = bazelBinPath |
| self.project = project |
| self.buildScriptPath = buildScriptPath |
| self.stubInfoPlistPaths = stubInfoPlistPaths |
| 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]) |
| } |
| |
| // 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 |
| } |
| var 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.defines { |
| 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 has no source files (there's nothing to index!) |
| // - if the target is a test bundle (we generate proper targets for these). |
| if (sourceFileInfos.isEmpty && |
| nonARCSourceFileInfos.isEmpty && |
| frameworkFileInfos.isEmpty && |
| nonSourceVersionedFileInfos.isEmpty) || ruleEntry.pbxTargetType?.isTest ?? false { |
| return (frameworkSearchPaths) |
| } |
| |
| var localPreprocessorDefines = defines |
| let localIncludes = includes.mutableCopy() as! NSMutableOrderedSet |
| let otherCFlags = NSMutableOrderedSet() |
| let swiftIncludePaths = NSMutableOrderedSet() |
| let otherSwiftFlags = NSMutableOrderedSet() |
| addLocalSettings(ruleEntry, localDefines: &localPreprocessorDefines, localIncludes: localIncludes, |
| otherCFlags: otherCFlags, swiftIncludePaths: swiftIncludePaths, otherSwiftFlags: otherSwiftFlags) |
| |
| addOtherSwiftFlags(ruleEntry, toSet: 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) |
| |
| var buildPhaseReferences: [PBXReference] |
| if nonSourceVersionedFileInfos.isEmpty { |
| buildPhaseReferences = [PBXReference]() |
| } else { |
| let versionedFileReferences = createReferencesForVersionedFileTargets(nonSourceVersionedFileInfos) |
| buildPhaseReferences = versionedFileReferences as [PBXReference] |
| } |
| buildPhaseReferences.append(contentsOf: fileReferences as [PBXReference]) |
| |
| let buildPhase = createBuildPhaseForReferences(buildPhaseReferences, |
| withPerFileSettings: nonARCSettings) |
| |
| if !buildPhase.files.isEmpty { |
| // Bazel appends a built-in tools/cpp/gcc3 path in CppHelper.java but that path is not |
| // exposed to Skylark. For now Tulsi hardcodes it here to allow proper indexer behavior. |
| // NOTE: this requires tools/cpp/gcc3 to be available from the workspace root, which may |
| // require symlinking on the part of the user. This requirement should go away when it is |
| // retrieved via the aspect (which should resolve the Bazel tool path correctly). |
| var resolvedIncludes = localIncludes.array as! [String] |
| resolvedIncludes.append("$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/tools/cpp/gcc3") |
| |
| 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.array as! [String], |
| otherSwiftFlags: otherSwiftFlags.array as! [String], |
| includes: resolvedIncludes, |
| frameworkSearchPaths: frameworkSearchPaths.array as! [String], |
| swiftIncludePaths: swiftIncludePaths.array as! [String], |
| deploymentTarget: deploymentTarget, |
| buildPhase: buildPhase, |
| pchFile: pchFile, |
| bridgingHeader: bridgingHeader, |
| enableModules: enableModules) |
| if (ruleEntry.type == "swift_library") { |
| 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 = "") { |
| assert(bazelCleanScriptTarget == nil, "generateBazelCleanTarget may only be called once") |
| |
| let bazelPath = bazelURL.path |
| bazelCleanScriptTarget = project.createLegacyTarget(PBXTargetGenerator.BazelCleanTarget, |
| deploymentTarget: nil, |
| buildToolPath: "\(scriptPath)", |
| buildArguments: "\"\(bazelPath)\" \"\(bazelBinPath)\"", |
| 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"] = "" |
| |
| // 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"; |
| |
| 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. |
| buildSettings["\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName)"] = |
| "${\(PBXTargetGenerator.WorkspaceRootVarName)}/\(PBXTargetGenerator.TulsiWorkspacePath)" |
| |
| buildSettings["TULSI_VERSION"] = tulsiVersion |
| |
| // 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))/\(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: " ") |
| |
| createBuildConfigurationsForList(project.buildConfigurationList, buildSettings: buildSettings) |
| addTestRunnerBuildConfigurationToBuildConfigurationList(project.buildConfigurationList) |
| } |
| |
| /// Generates build targets for the given rule entries. |
| func generateBuildTargetsForRuleEntries(_ ruleEntries: Set<RuleEntry>, |
| ruleEntryMap: RuleEntryMap) throws { |
| let namedRuleEntries = generateUniqueNamesForRuleEntries(ruleEntries) |
| var testTargetLinkages = [(PBXNativeTarget, BuildLabel?, RuleEntry)]() |
| let progressNotifier = ProgressNotifier(name: GeneratingBuildTargets, |
| maxValue: namedRuleEntries.count) |
| var watchAppTargets = [String: (PBXNativeTarget, RuleEntry)]() |
| var watchExtensionsByEntry = [RuleEntry: PBXNativeTarget]() |
| for (name, entry) in namedRuleEntries { |
| progressNotifier.incrementValue() |
| let target = try createBuildTargetForRuleEntry(entry, |
| named: name, |
| ruleEntryMap: ruleEntryMap) |
| |
| 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 = projectTargetForLabel(hostTargetLabel) as? PBXNativeTarget |
| 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) |
| } |
| } |
| |
| // 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 { |
| 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]) -> [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 } |
| |
| 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]) -> ([PBXFileReference], [PBXFileReference: [String: String]]) { |
| let nonARCFileReferences = generateFileReferencesForFileInfos(infos) |
| var settings = [PBXFileReference: [String: String]]() |
| let disableARCSetting = ["COMPILER_FLAGS": "-fno-objc-arc"] |
| nonARCFileReferences.forEach() { |
| settings[$0] = disableARCSetting |
| } |
| return (nonARCFileReferences, settings) |
| } |
| |
| private func generateUniqueNamesForRuleEntries(_ ruleEntries: Set<RuleEntry>) -> [String: RuleEntry] { |
| // Build unique names for the target rules. |
| var collidingRuleEntries = [String: [RuleEntry]]() |
| for entry: RuleEntry in ruleEntries { |
| let shortName = entry.label.targetName! |
| if var existingRules = collidingRuleEntries[shortName] { |
| existingRules.append(entry) |
| collidingRuleEntries[shortName] = existingRules |
| } else { |
| collidingRuleEntries[shortName] = [entry] |
| } |
| } |
| |
| var namedRuleEntries = [String: RuleEntry]() |
| for (name, entries) in collidingRuleEntries { |
| guard entries.count > 1 else { |
| namedRuleEntries[name] = entries.first! |
| continue |
| } |
| |
| for entry in entries { |
| let fullName = entry.label.asFullPBXTargetName! |
| namedRuleEntries[fullName] = entry |
| } |
| } |
| |
| return namedRuleEntries |
| } |
| |
| /// Adds the given file targets to a versioned group. |
| private func createReferencesForVersionedFileTargets(_ fileInfos: [BazelFileInfo]) -> [XCVersionGroup] { |
| 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) |
| } |
| return Array(groups.values) |
| } |
| |
| // 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 is expected to be a pre-quoted set of defines (e.g., if "key" has |
| // spaces it would be the string: key="value with spaces"). |
| 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 |
| if !data.preprocessorDefines.isEmpty { |
| allOtherCFlags.append(contentsOf: data.preprocessorDefines.sorted().map({"-D\($0)"})) |
| } |
| |
| 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) { |
| // 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) |
| } |
| |
| /// 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) { |
| let testSourceFileInfos = ruleEntry.sourceFiles |
| let testNonArcSourceFileInfos = ruleEntry.nonARCSourceFiles |
| 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 containsSwift { |
| let testBuildPhase = createGenerateSwiftDummyFilesTestBuildPhase() |
| target.buildPhases.append(testBuildPhase) |
| } |
| if !testSourceFileInfos.isEmpty || !testNonArcSourceFileInfos.isEmpty { |
| var fileReferences = generateFileReferencesForFileInfos(testSourceFileInfos) |
| let (nonARCFiles, nonARCSettings) = |
| generateFileReferencesAndSettingsForNonARCFileInfos(testNonArcSourceFileInfos) |
| 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 |
| let rootedPath = "$(\(PBXTargetGenerator.WorkspaceRootVarName))/\(path)" |
| if recursive { |
| return "\(rootedPath)/**" |
| } |
| return rootedPath |
| } |
| includes.addObjects(from: rootedPaths) |
| |
| /// Some targets that generate sources also provide header search paths into non-generated |
| /// sources. Using workspace root is needed for the former, but the latter has to be |
| /// included via the Bazel workspace root. |
| /// TODO(tulsi-team): See if we can merge the two locations to just Bazel workspace. |
| let bazelWorkspaceRootedPaths: [String] = includePaths.map() { (path, recursive) in |
| let rootedPath = "$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(path)" |
| if recursive { |
| return "\(rootedPath)/**" |
| } |
| return rootedPath |
| } |
| includes.addObjects(from: bazelWorkspaceRootedPaths) |
| } |
| |
| // TODO(rdar://36107040): Once Xcode supports indexing with multiple -fmodule-map-file |
| // arguments, remove this in favor of "-Xcc -fmodule-map-file=<>" below. |
| // |
| // Include the ObjC modules in HEADER_SEARCH_PATHS in order to fix issues regarding |
| // explicitly passing them via -fmodule-map-file: Xcode 8 seems to ignore the |
| // -fmodule-map-file flag when using SourceKit for CMD+click and Xcode 9 seems to only |
| // pass in the last -fmodule-map-file flag given. |
| for moduleMap in ruleEntry.objCModuleMaps { |
| let fullPath = (moduleMap.fullPath as NSString).deletingLastPathComponent |
| includes.add("$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/\(fullPath)") |
| } |
| } |
| |
| /// 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, toSet swiftFlags: NSMutableOrderedSet) { |
| // TODO(rdar://36107040): Once Xcode supports indexing with multiple -fmodule-map-file |
| // arguments, use "-Xcc -fmodule-map-file=<>" here instead of including paths in the includes |
| // above. |
| } |
| |
| /// 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: NSMutableOrderedSet, |
| swiftIncludePaths: NSMutableOrderedSet, |
| otherSwiftFlags: NSMutableOrderedSet) { |
| 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 = NSMutableOrderedSet() |
| |
| includes.add("$(\(PBXTargetGenerator.BazelWorkspaceSymlinkVarName))/tools/cpp/gcc3") |
| addIncludes(ruleEntry, toSet: includes) |
| addLocalSettings(ruleEntry, localDefines: &defines, localIncludes: includes, |
| otherCFlags: NSMutableOrderedSet(), swiftIncludePaths: NSMutableOrderedSet(), |
| otherSwiftFlags: NSMutableOrderedSet()) |
| addSwiftIncludes(ruleEntry, toSet: swiftIncludePaths) |
| addOtherSwiftFlags(ruleEntry, toSet: otherSwiftFlags) |
| |
| let includesArr = includes.array as! [String] |
| 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.array as? [String], !otherSwiftFlagsArr.isEmpty { |
| testSettings["OTHER_SWIFT_FLAGS"] = "$(inherited) " + otherSwiftFlagsArr.joined(separator: " ") |
| } |
| |
| return testSettings |
| } |
| |
| // Resolves a BuildLabel to an existing PBXTarget, handling target name collisions. |
| private func projectTargetForLabel(_ label: BuildLabel) -> PBXTarget? { |
| guard let targetName = label.targetName else { return nil } |
| if let target = project.targetByName[targetName] { |
| return target |
| } |
| |
| guard let fullTargetName = label.asFullPBXTargetName else { return nil } |
| return project.targetByName[fullTargetName] |
| } |
| |
| // 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 -help" 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"] = "-help" |
| runTestTargetBuildSettings["OTHER_SWIFT_FLAGS"] = "-help" |
| // 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"] = "-help" |
| |
| // 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) |
| |
| if !indexerSettingsOnly { |
| // Enable dSYM generation for release builds. |
| config.buildSettings["TULSI_MUST_USE_DSYM"] = "YES" |
| } |
| } |
| } |
| } |
| |
| 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) |
| } |
| 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! |
| |
| |
| buildSettings["PRODUCT_NAME"] = entry.bundleName ?? 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" |
| } |
| |
| // Tell our build script when a Swift dependency has been found. This could be discovered |
| // through the aspect instead, but we keep this information here for the sake of our debug info |
| // remapping story (for now). |
| // |
| // TODO(b/77916902): See if we can base this directly on if we have any Swift files indexed. |
| // The state of has_swift_dependency is currently computed through our aspect. |
| let swiftDependency = entry.attributes[.has_swift_dependency] as? Bool ?? false |
| buildSettings["TULSI_SWIFT_DEPENDENCY"] = swiftDependency ? "YES" : "NO" |
| |
| // 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 = createBuildPhaseForRuleEntry(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" + |
| " 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") |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "SwiftDummy" |
| return buildPhase |
| } |
| |
| private func createBuildPhaseForRuleEntry(_ entry: RuleEntry) -> PBXShellScriptBuildPhase? { |
| let buildLabel = entry.label.value |
| let commandLine = buildScriptCommandlineForBuildLabels(buildLabel, |
| withOptionsForTargetLabel: entry.label) |
| 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)" |
| |
| let buildPhase = PBXShellScriptBuildPhase(shellScript: shellScript, shellPath: "/bin/bash") |
| buildPhase.showEnvVarsInLog = true |
| buildPhase.mnemonic = "BazelBuild" |
| 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) with user options set for the given |
| /// optionsTarget. |
| private func buildScriptCommandlineForBuildLabels(_ buildLabels: String, |
| withOptionsForTargetLabel target: BuildLabel) -> String { |
| var commandLine = "\"\(buildScriptPath)\" " + |
| "\(buildLabels) " + |
| "--bazel \"\(bazelURL.path)\" " + |
| "--bazel_bin_path \"\(bazelBinPath)\" " + |
| "--verbose " |
| |
| func addPerConfigValuesForOptions(_ optionKeys: [TulsiOptionKey], |
| additionalFlags: String = "", |
| optionFlag: String) { |
| // Get the value for each config and test to see if they are all identical and may be |
| // collapsed. |
| var configValues = [TulsiOptionKey: String?]() |
| var firstValue: String? = nil |
| var valuesDiffer = false |
| for key in optionKeys { |
| let value = options[key, target.value] |
| if configValues.isEmpty { |
| firstValue = value |
| } else if value != firstValue { |
| valuesDiffer = true |
| } |
| configValues[key] = value |
| } |
| |
| if !valuesDiffer { |
| // Return early if nothing was set. |
| guard let concreteValue = firstValue else { return } |
| commandLine += "\(optionFlag) \(concreteValue) " |
| if !additionalFlags.isEmpty { |
| commandLine += "\(additionalFlags) " |
| } |
| commandLine += "-- " |
| |
| return |
| } |
| |
| // Emit a filtered option (--optionName[configName]) for each config. |
| for (optionKey, value) in configValues { |
| guard let concreteValue = value else { continue } |
| let rawName = optionKey.rawValue |
| var configKey: String? |
| for key in PBXTargetGenerator.buildConfigNames { |
| if rawName.hasSuffix(key) { |
| configKey = key |
| break |
| } |
| } |
| if configKey == nil { |
| assertionFailure("Failed to map option key \(optionKey) to a build config.") |
| configKey = "Debug" |
| } |
| commandLine += "\(optionFlag)[\(configKey!)] \(concreteValue) " |
| commandLine += additionalFlags.isEmpty ? "-- " : "\(additionalFlags) -- " |
| } |
| } |
| |
| let additionalFlags: String |
| if let shouldContinueBuildingAfterError = options[.BazelContinueBuildingAfterError].commonValueAsBool, |
| shouldContinueBuildingAfterError { |
| additionalFlags = "--keep_going" |
| } else { |
| additionalFlags = "" |
| } |
| |
| addPerConfigValuesForOptions([.BazelBuildOptionsDebug, |
| .BazelBuildOptionsRelease, |
| ], |
| additionalFlags: additionalFlags, |
| optionFlag: "--bazel_options") |
| |
| addPerConfigValuesForOptions([.BazelBuildStartupOptionsDebug, |
| .BazelBuildStartupOptionsRelease |
| ], |
| optionFlag: "--bazel_startup_options") |
| |
| return commandLine |
| } |
| } |