| // 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 | |
| /// Models the label and type of a single supported Bazel target. | |
| /// See http://bazel.build/docs/build-ref.html#targets. | |
| public class RuleInfo: Equatable, Hashable, CustomDebugStringConvertible { | |
| public let label: BuildLabel | |
| public let type: String | |
| /// Set of BuildLabels referencing targets that are required by this RuleInfo. For example, test | |
| /// hosts for XCTest targets. | |
| public let linkedTargetLabels: Set<BuildLabel> | |
| public var hashValue: Int { | |
| return label.hashValue ^ type.hashValue | |
| } | |
| public var debugDescription: String { | |
| return "\(type(of: self))(\(label) \(type))" | |
| } | |
| init(label: BuildLabel, type: String, linkedTargetLabels: Set<BuildLabel>) { | |
| self.label = label | |
| self.type = type | |
| self.linkedTargetLabels = linkedTargetLabels | |
| } | |
| func equals(_ other: RuleInfo) -> Bool { | |
| guard type(of: self) == type(of: other) else { | |
| return false | |
| } | |
| return self.type == other.type && self.label == other.label | |
| } | |
| } | |
| /// Encapsulates data about a file that may be a Bazel input or output. | |
| public class BazelFileInfo: Equatable, Hashable, CustomDebugStringConvertible { | |
| public enum TargetType: Int { | |
| case sourceFile | |
| case generatedFile | |
| } | |
| /// The path to this file relative to rootPath. | |
| public let subPath: String | |
| /// The root of this file's path (typically used to indicate the path to a generated file's root). | |
| public let rootPath: String | |
| /// The type of this file. | |
| public let targetType: TargetType | |
| /// Whether or not this file object is a directory. | |
| public let isDirectory: Bool | |
| public lazy var fullPath: String = { [unowned self] in | |
| return NSString.path(withComponents: [self.rootPath, self.subPath]) | |
| }() | |
| public lazy var uti: String? = { [unowned self] in | |
| return self.subPath.pbPathUTI | |
| }() | |
| public lazy var hashValue: Int = { [unowned self] in | |
| return self.subPath.hashValue &+ | |
| self.rootPath.hashValue &+ | |
| self.targetType.hashValue &+ | |
| self.isDirectory.hashValue | |
| }() | |
| init?(info: AnyObject?) { | |
| guard let info = info as? [String: AnyObject] else { | |
| return nil | |
| } | |
| guard let subPath = info["path"] as? String, | |
| let isSourceFile = info["src"] as? Bool else { | |
| assertionFailure("Aspect provided a file info dictionary but was missing required keys") | |
| return nil | |
| } | |
| self.subPath = subPath | |
| if let rootPath = info["root"] as? String { | |
| // Patch up | |
| self.rootPath = rootPath | |
| } else { | |
| self.rootPath = "" | |
| } | |
| self.targetType = isSourceFile ? .sourceFile : .generatedFile | |
| self.isDirectory = info["is_dir"] as? Bool ?? false | |
| } | |
| init(rootPath: String, subPath: String, isDirectory: Bool, targetType: TargetType) { | |
| self.rootPath = rootPath | |
| self.subPath = subPath | |
| self.isDirectory = isDirectory | |
| self.targetType = targetType | |
| } | |
| // MARK: - CustomDebugStringConvertible | |
| public lazy var debugDescription: String = { [unowned self] in | |
| return "{\(self.fullPath) \(self.isDirectory ? "<DIR> " : "")\(self.targetType)}" | |
| }() | |
| } | |
| public func ==(lhs: BazelFileInfo, rhs: BazelFileInfo) -> Bool { | |
| return lhs.targetType == rhs.targetType && | |
| lhs.rootPath == rhs.rootPath && | |
| lhs.subPath == rhs.subPath && | |
| lhs.isDirectory == rhs.isDirectory | |
| } | |
| /// Models the full metadata of a single supported Bazel target. | |
| /// See http://bazel.build/docs/build-ref.html#targets. | |
| public final class RuleEntry: RuleInfo { | |
| // Include paths are represented by a string and a boolean indicating whether they should be | |
| // searched recursively or not. | |
| public typealias IncludePath = (String, Bool) | |
| /// Mapping of BUILD file type to Xcode Target type. | |
| static let BuildTypeToTargetType = [ | |
| "apple_ui_test": PBXTarget.ProductType.UIUnitTest, | |
| "apple_unit_test": PBXTarget.ProductType.UnitTest, | |
| "cc_binary": PBXTarget.ProductType.Application, | |
| "cc_library": PBXTarget.ProductType.StaticLibrary, | |
| "ios_application": PBXTarget.ProductType.Application, | |
| "ios_extension": PBXTarget.ProductType.AppExtension, | |
| "ios_framework": PBXTarget.ProductType.Framework, | |
| "ios_legacy_test": PBXTarget.ProductType.Application, | |
| "macos_application": PBXTarget.ProductType.Application, | |
| "macos_bundle": PBXTarget.ProductType.Bundle, | |
| "macos_command_line_application": PBXTarget.ProductType.Tool, | |
| "macos_extension": PBXTarget.ProductType.AppExtension, | |
| "objc_binary": PBXTarget.ProductType.Application, | |
| "objc_library": PBXTarget.ProductType.StaticLibrary, | |
| "swift_library": PBXTarget.ProductType.StaticLibrary, | |
| "tvos_application": PBXTarget.ProductType.Application, | |
| "tvos_extension": PBXTarget.ProductType.TVAppExtension, | |
| "watchos_application": PBXTarget.ProductType.Watch2App, | |
| "watchos_extension": PBXTarget.ProductType.Watch2Extension, | |
| ] | |
| /// Keys for a RuleEntry's attributes map. Definitions may be found in the Bazel Build | |
| /// Encyclopedia (see http://bazel.build/docs/be/overview.html). | |
| // Note: This set of must be kept in sync with the tulsi_aspects aspect. | |
| public enum Attribute: String { | |
| case binary | |
| case bridging_header | |
| // Contains defines that were specified by the user on the commandline or are built into | |
| // Bazel itself. | |
| case compiler_defines | |
| case copts | |
| case datamodels | |
| case enable_modules | |
| case has_swift_dependency | |
| case launch_storyboard | |
| case pch | |
| case swift_language_version | |
| case swift_toolchain | |
| // Contains various files that are used as part of the build process but need no special | |
| // handling in the generated Xcode project. For example, asset_catalog, storyboard, and xibs | |
| // attributes all end up as supporting_files. | |
| case supporting_files | |
| // For the apple_unit_test and apple_ui_test rules, contains a label reference to the .xctest | |
| // bundle packaging target. | |
| case test_bundle | |
| // For the apple_unit_test and apple_ui_test rules, contains a label reference to the | |
| // ios_application target to be used as the test host when running the tests. | |
| case test_host | |
| } | |
| /// Bazel attributes for this rule (e.g., "binary": <some label> on an ios_application). | |
| public let attributes: [Attribute: AnyObject] | |
| /// Artifacts produced by Bazel when this rule is built. | |
| public let artifacts: [BazelFileInfo] | |
| /// Defines to be applied to this rule by Bazel. | |
| public let defines: [String]? | |
| /// Source files associated with this rule. | |
| public let sourceFiles: [BazelFileInfo] | |
| /// Non-ARC source files associated with this rule. | |
| public let nonARCSourceFiles: [BazelFileInfo] | |
| /// Paths to directories that will include header files. | |
| public let includePaths: [IncludePath]? | |
| /// Set of the labels that this rule depends on. | |
| public let dependencies: Set<BuildLabel> | |
| /// Set of ios_application extension labels that this rule utilizes. | |
| public let extensions: Set<BuildLabel> | |
| /// .framework bundles provided by this rule. | |
| public let frameworkImports: [BazelFileInfo] | |
| /// List of implicit artifacts that are generated by this rule. | |
| public let secondaryArtifacts: [BazelFileInfo] | |
| /// The Swift language version used by this target. | |
| public let swiftLanguageVersion: String? | |
| /// The swift toolchain argument used by this target. | |
| // TODO(abaire): It is hoped that this may be removed when support for Swift 2.3 is dropped. | |
| public let swiftToolchain: String? | |
| /// List containing the transitive swiftmodules on which this rule depends. | |
| public let swiftTransitiveModules: [BazelFileInfo] | |
| /// List containing the transitive ObjC modulemaps on which this rule depends. | |
| public let objCModuleMaps: [BazelFileInfo] | |
| /// The deployment platform target for this target. | |
| public let deploymentTarget: DeploymentTarget? | |
| /// Set of labels that this rule depends on but does not require. | |
| /// TODO(b/71904309): Remove this once test_suite fetching via Aspect is stable. | |
| // NOTE(abaire): This is a hack used for test_suite rules, where the possible expansions retrieved | |
| // via queries are filtered by the existence of the selected labels extracted via the normal | |
| // aspect path. Ideally the aspect would be able to directly express the relationship between the | |
| // test_suite and the test rules themselves, but that expansion is done prior to the application | |
| // of the aspect. | |
| public var weakDependencies = Set<BuildLabel>() | |
| /// Set of labels that this test_suite depends on. If this target is not a test_suite, returns | |
| /// an empty set. This maps directly to the `tests` attribute of the test_suite. | |
| public var testSuiteDependencies: Set<BuildLabel> { | |
| guard type == "test_suite" else { return Set() } | |
| // Legacy support for expansion of test_suite via a Bazel query. If a Bazel query is used, | |
| // `dependencies` will be empty and `weakDependencies` will contain the test_suite's | |
| // dependencies. Otherwise, `dependencies` will contain the test_suite's dependencies. | |
| guard dependencies.isEmpty else { return dependencies } | |
| return weakDependencies | |
| } | |
| /// The BUILD file that this rule was defined in. | |
| public let buildFilePath: String? | |
| // The CFBundleIdentifier associated with the target for this rule, if any. | |
| public let bundleID: String? | |
| /// The bundle name associated with the target for this rule, if any. | |
| public let bundleName: String? | |
| /// The CFBundleIdentifier of the watchOS extension target associated with this rule, if any. | |
| public let extensionBundleID: String? | |
| /// The NSExtensionPointIdentifier of the extension associated with this rule, if any. | |
| public let extensionType: String? | |
| /// Returns the set of non-versioned artifacts that are not source files. | |
| public var normalNonSourceArtifacts: [BazelFileInfo] { | |
| var artifacts = [BazelFileInfo]() | |
| if let description = attributes[.launch_storyboard] as? [String: AnyObject], | |
| let fileTarget = BazelFileInfo(info: description as AnyObject?) { | |
| artifacts.append(fileTarget) | |
| } | |
| if let fileTargets = parseFileDescriptionListAttribute(.supporting_files) { | |
| artifacts.append(contentsOf: fileTargets) | |
| } | |
| return artifacts | |
| } | |
| /// Returns the set of artifacts for which a versioned group should be created in the generated | |
| /// Xcode project. | |
| public var versionedNonSourceArtifacts: [BazelFileInfo] { | |
| if let fileTargets = parseFileDescriptionListAttribute(.datamodels) { | |
| return fileTargets | |
| } | |
| return [] | |
| } | |
| /// The full set of input and output artifacts for this rule. | |
| public var projectArtifacts: [BazelFileInfo] { | |
| var artifacts = sourceFiles | |
| artifacts.append(contentsOf: nonARCSourceFiles) | |
| artifacts.append(contentsOf: frameworkImports) | |
| artifacts.append(contentsOf: normalNonSourceArtifacts) | |
| artifacts.append(contentsOf: versionedNonSourceArtifacts) | |
| return artifacts | |
| } | |
| private(set) lazy var pbxTargetType: PBXTarget.ProductType? = { [unowned self] in | |
| return BuildTypeToTargetType[self.type] | |
| }() | |
| /// Returns the value to be used as the Xcode SDKROOT for the build target generated for this | |
| /// RuleEntry. | |
| private(set) lazy var XcodeSDKRoot: String? = { [unowned self] in | |
| if let platformType = self.deploymentTarget?.platform { | |
| return platformType.deviceSDK | |
| } | |
| return PlatformType.ios.deviceSDK | |
| }() | |
| init(label: BuildLabel, | |
| type: String, | |
| attributes: [String: AnyObject], | |
| artifacts: [BazelFileInfo] = [], | |
| sourceFiles: [BazelFileInfo] = [], | |
| nonARCSourceFiles: [BazelFileInfo] = [], | |
| dependencies: Set<BuildLabel> = Set(), | |
| frameworkImports: [BazelFileInfo] = [], | |
| secondaryArtifacts: [BazelFileInfo] = [], | |
| weakDependencies: Set<BuildLabel>? = nil, | |
| extensions: Set<BuildLabel>? = nil, | |
| bundleID: String? = nil, | |
| bundleName: String? = nil, | |
| extensionBundleID: String? = nil, | |
| platformType: String? = nil, | |
| osDeploymentTarget: String? = nil, | |
| buildFilePath: String? = nil, | |
| defines: [String]? = nil, | |
| includePaths: [IncludePath]? = nil, | |
| swiftLanguageVersion: String? = nil, | |
| swiftToolchain: String? = nil, | |
| swiftTransitiveModules: [BazelFileInfo] = [], | |
| objCModuleMaps: [BazelFileInfo] = [], | |
| extensionType: String? = nil) { | |
| var checkedAttributes = [Attribute: AnyObject]() | |
| for (key, value) in attributes { | |
| guard let checkedKey = Attribute(rawValue: key) else { | |
| print("Tulsi rule \(label.value) - Ignoring unknown attribute key \(key)") | |
| assertionFailure("Unknown attribute key \(key)") | |
| continue | |
| } | |
| checkedAttributes[checkedKey] = value | |
| } | |
| self.attributes = checkedAttributes | |
| let parsedPlatformType: PlatformType? | |
| if let platformTypeStr = platformType { | |
| parsedPlatformType = PlatformType(rawValue: platformTypeStr) | |
| } else { | |
| parsedPlatformType = nil | |
| } | |
| self.artifacts = artifacts | |
| self.sourceFiles = sourceFiles | |
| self.nonARCSourceFiles = nonARCSourceFiles | |
| self.dependencies = dependencies | |
| self.frameworkImports = frameworkImports | |
| self.secondaryArtifacts = secondaryArtifacts | |
| if let weakDependencies = weakDependencies { | |
| self.weakDependencies = weakDependencies | |
| } | |
| if let extensions = extensions { | |
| self.extensions = extensions | |
| } else { | |
| self.extensions = Set() | |
| } | |
| self.bundleID = bundleID | |
| self.bundleName = bundleName | |
| self.extensionBundleID = extensionBundleID | |
| var deploymentTarget: DeploymentTarget? = nil | |
| if let platform = parsedPlatformType, | |
| let osVersion = osDeploymentTarget { | |
| deploymentTarget = DeploymentTarget(platform: platform, osVersion: osVersion) | |
| } | |
| self.deploymentTarget = deploymentTarget | |
| self.buildFilePath = buildFilePath | |
| self.defines = defines | |
| self.includePaths = includePaths | |
| self.swiftLanguageVersion = swiftLanguageVersion | |
| self.swiftToolchain = swiftToolchain | |
| self.swiftTransitiveModules = swiftTransitiveModules | |
| self.objCModuleMaps = objCModuleMaps | |
| self.extensionType = extensionType | |
| var linkedTargetLabels = Set<BuildLabel>() | |
| if let hostLabelString = self.attributes[.test_host] as? String { | |
| linkedTargetLabels.insert(BuildLabel(hostLabelString)) | |
| } | |
| super.init(label: label, type: type, linkedTargetLabels: linkedTargetLabels) | |
| } | |
| convenience init(label: String, | |
| type: String, | |
| attributes: [String: AnyObject], | |
| artifacts: [BazelFileInfo] = [], | |
| sourceFiles: [BazelFileInfo] = [], | |
| nonARCSourceFiles: [BazelFileInfo] = [], | |
| dependencies: Set<BuildLabel> = Set(), | |
| frameworkImports: [BazelFileInfo] = [], | |
| secondaryArtifacts: [BazelFileInfo] = [], | |
| weakDependencies: Set<BuildLabel>? = nil, | |
| extensions: Set<BuildLabel>? = nil, | |
| bundleID: String? = nil, | |
| bundleName: String? = nil, | |
| extensionBundleID: String? = nil, | |
| platformType: String? = nil, | |
| osDeploymentTarget: String? = nil, | |
| buildFilePath: String? = nil, | |
| defines: [String]? = nil, | |
| includePaths: [IncludePath]? = nil, | |
| swiftLanguageVersion: String? = nil, | |
| swiftToolchain: String? = nil, | |
| swiftTransitiveModules: [BazelFileInfo] = [], | |
| objCModuleMaps: [BazelFileInfo] = [], | |
| extensionType: String? = nil) { | |
| self.init(label: BuildLabel(label), | |
| type: type, | |
| attributes: attributes, | |
| artifacts: artifacts, | |
| sourceFiles: sourceFiles, | |
| nonARCSourceFiles: nonARCSourceFiles, | |
| dependencies: dependencies, | |
| frameworkImports: frameworkImports, | |
| secondaryArtifacts: secondaryArtifacts, | |
| weakDependencies: weakDependencies, | |
| extensions: extensions, | |
| bundleID: bundleID, | |
| bundleName: bundleName, | |
| extensionBundleID: extensionBundleID, | |
| platformType: platformType, | |
| osDeploymentTarget: osDeploymentTarget, | |
| buildFilePath: buildFilePath, | |
| defines: defines, | |
| includePaths: includePaths, | |
| swiftLanguageVersion: swiftLanguageVersion, | |
| swiftToolchain: swiftToolchain, | |
| swiftTransitiveModules: swiftTransitiveModules, | |
| objCModuleMaps: objCModuleMaps, | |
| extensionType: extensionType) | |
| } | |
| // MARK: Private methods | |
| private func parseFileDescriptionListAttribute(_ attribute: RuleEntry.Attribute) -> [BazelFileInfo]? { | |
| guard let descriptions = attributes[attribute] as? [[String: AnyObject]] else { | |
| return nil | |
| } | |
| var fileTargets = [BazelFileInfo]() | |
| for description in descriptions { | |
| guard let target = BazelFileInfo(info: description as AnyObject?) else { | |
| assertionFailure("Failed to resolve file description to a file target") | |
| continue | |
| } | |
| fileTargets.append(target) | |
| } | |
| return fileTargets | |
| } | |
| override func equals(_ other: RuleInfo) -> Bool { | |
| guard super.equals(other), let entry = other as? RuleEntry else { | |
| return false | |
| } | |
| return deploymentTarget == entry.deploymentTarget | |
| } | |
| } | |
| // MARK: - Equatable | |
| public func ==(lhs: RuleInfo, rhs: RuleInfo) -> Bool { | |
| return lhs.equals(rhs) | |
| } |