From 3c76cd8adbe9e33c64452c0d57631fc7db001130 Mon Sep 17 00:00:00 2001 From: David Hart Date: Thu, 28 Nov 2019 21:40:01 +0100 Subject: [PATCH] Implementation of the conditional target dependency proposal --- Sources/Build/BuildParameters.swift | 5 + Sources/Build/BuildPlan.swift | 53 +-- Sources/Build/ManifestBuilder.swift | 13 +- Sources/PackageGraph/PackageGraph.swift | 47 ++- Sources/PackageGraph/PackageGraphLoader.swift | 55 +-- .../PackageLoading/ModuleMapGenerator.swift | 2 +- Sources/PackageLoading/PackageBuilder.swift | 97 ++--- Sources/PackageModel/Platform.swift | 1 + Sources/PackageModel/ResolvedModels.swift | 91 +++-- Sources/PackageModel/Target.swift | 74 +++- .../SPMTestSupport/PackageGraphTester.swift | 93 ++++- Sources/Xcodeproj/pbxproj.swift | 4 +- Tests/BuildTests/BuildPlanTests.swift | 360 +++++++++++++++++- .../PackageGraphTests/PackageGraphTests.swift | 89 ++++- .../PackageBuilderTests.swift | 140 ++++++- Tests/PackageModelTests/TargetTests.swift | 53 ++- Tests/WorkspaceTests/WorkspaceTests.swift | 32 +- 17 files changed, 964 insertions(+), 245 deletions(-) diff --git a/Sources/Build/BuildParameters.swift b/Sources/Build/BuildParameters.swift index 044d1c3c1bd..d241b9a3ac1 100644 --- a/Sources/Build/BuildParameters.swift +++ b/Sources/Build/BuildParameters.swift @@ -81,6 +81,11 @@ public struct BuildParameters: Encodable { /// module to finish building. public var emitSwiftModuleSeparately: Bool + /// The current build environment. + public var buildEnvironment: BuildEnvironment { + BuildEnvironment(platform: currentPlatform, configuration: configuration) + } + public init( dataPath: AbsolutePath, configuration: BuildConfiguration, diff --git a/Sources/Build/BuildPlan.swift b/Sources/Build/BuildPlan.swift index 6e9008a29bc..7c1d87448b8 100644 --- a/Sources/Build/BuildPlan.swift +++ b/Sources/Build/BuildPlan.swift @@ -76,7 +76,7 @@ extension BuildParameters { } /// The current platform we're building for. - fileprivate var currentPlatform: PackageModel.Platform { + var currentPlatform: PackageModel.Platform { if self.triple.isDarwin() { return .macOS } else if self.triple.isAndroid() { @@ -88,10 +88,7 @@ extension BuildParameters { /// Returns the scoped view of build settings for a given target. fileprivate func createScope(for target: ResolvedTarget) -> BuildSettings.Scope { - return BuildSettings.Scope( - target.underlyingTarget.buildSettings, - environment: BuildEnvironment(platform: currentPlatform, configuration: configuration) - ) + return BuildSettings.Scope(target.underlyingTarget.buildSettings, environment: buildEnvironment) } } @@ -148,6 +145,10 @@ public final class ClangTargetBuildDescription { /// The build parameters. let buildParameters: BuildParameters + var buildEnvironment: BuildEnvironment { + buildParameters.buildEnvironment + } + /// Path to the bundle generated for this module (if any). var bundlePath: AbsolutePath? { buildParameters.bundlePath(for: target) @@ -1039,6 +1040,10 @@ public class BuildPlan { /// The build parameters. public let buildParameters: BuildParameters + private var buildEnvironment: BuildEnvironment { + buildParameters.buildEnvironment + } + /// The package graph. public let graph: PackageGraph @@ -1107,10 +1112,10 @@ public class BuildPlan { let swiftTarget = SwiftTarget( testDiscoverySrc: src, name: testProduct.name, - dependencies: testProduct.underlyingProduct.targets) + dependencies: testProduct.underlyingProduct.targets.map({ .target($0, conditions: []) })) let linuxMainTarget = ResolvedTarget( target: swiftTarget, - dependencies: testProduct.targets.map(ResolvedTarget.Dependency.target) + dependencies: testProduct.targets.map({ .target($0, conditions: []) }) ) let target = try SwiftTargetBuildDescription( @@ -1145,7 +1150,7 @@ public class BuildPlan { for dependency in target.dependencies { switch dependency { case .target: break - case .product(let product): + case .product(let product, _): if buildParameters.triple.isDarwin() { BuildPlan.validateDeploymentVersionOfProductDependency( product, forTarget: target, diagnostics: diagnostics) @@ -1320,19 +1325,19 @@ public class BuildPlan { ) { // Sort the product targets in topological order. - let nodes = product.targets.map(ResolvedTarget.Dependency.target) + let nodes: [ResolvedTarget.Dependency] = product.targets.map({ .target($0, conditions: []) }) let allTargets = try! topologicalSort(nodes, successors: { dependency in switch dependency { // Include all the depenencies of a target. - case .target(let target): - return target.dependencies + case .target(let target, _): + return target.dependencies.filter({ $0.satisfies(self.buildEnvironment) }) // For a product dependency, we only include its content only if we // need to statically link it. - case .product(let product): + case .product(let product, _): switch product.type { case .library(.automatic), .library(.static): - return product.targets.map(ResolvedTarget.Dependency.target) + return product.targets.map({ .target($0, conditions: []) }) case .library(.dynamic), .test, .executable: return [] } @@ -1346,7 +1351,7 @@ public class BuildPlan { for dependency in allTargets { switch dependency { - case .target(let target): + case .target(let target, _): switch target.type { // Include executable and tests only if they're top level contents // of the product. Otherwise they are just build time dependency. @@ -1362,7 +1367,7 @@ public class BuildPlan { systemModules.append(target) } - case .product(let product): + case .product(let product, _): // Add the dynamic products to array of libraries to link. if product.type == .library(.dynamic) { linkLibraries.append(product) @@ -1381,10 +1386,11 @@ public class BuildPlan { /// Plan a Clang target. private func plan(clangTarget: ClangTargetBuildDescription) { - for dependency in clangTarget.target.recursiveDependencies() { - switch dependency.underlyingTarget { + let recursiveBuildTargets = clangTarget.target.recursiveBuildTargetDependencies(in: buildEnvironment) + for targetDependency in recursiveBuildTargets { + switch targetDependency.underlyingTarget { case is SwiftTarget: - if case let .swift(dependencyTargetDescription)? = targetMap[dependency] { + if case let .swift(dependencyTargetDescription)? = targetMap[targetDependency] { if let moduleMap = dependencyTargetDescription.moduleMap { clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] } @@ -1395,7 +1401,7 @@ public class BuildPlan { clangTarget.additionalFlags += ["-I", target.includeDir.pathString] // Add the modulemap of the dependency if it has one. - if case let .clang(dependencyTargetDescription)? = targetMap[dependency] { + if case let .clang(dependencyTargetDescription)? = targetMap[targetDependency] { if let moduleMap = dependencyTargetDescription.moduleMap { clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] } @@ -1412,10 +1418,13 @@ public class BuildPlan { private func plan(swiftTarget: SwiftTargetBuildDescription) throws { // We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target // depends on. - for dependency in swiftTarget.target.recursiveDependencies() { - switch dependency.underlyingTarget { + let recursiveBuildTargets = swiftTarget.target + .recursiveBuildDependencies(in: buildEnvironment) + .compactMap({ $0.target }) + for targetDependency in recursiveBuildTargets { + switch targetDependency.underlyingTarget { case let underlyingTarget as ClangTarget where underlyingTarget.type == .library: - guard case let .clang(target)? = targetMap[dependency] else { + guard case let .clang(target)? = targetMap[targetDependency] else { fatalError("unexpected clang target \(underlyingTarget)") } // Add the path to modulemap of the dependency. Currently we require that all Clang targets have a diff --git a/Sources/Build/ManifestBuilder.swift b/Sources/Build/ManifestBuilder.swift index a12efa02fea..f9ca4639264 100644 --- a/Sources/Build/ManifestBuilder.swift +++ b/Sources/Build/ManifestBuilder.swift @@ -36,6 +36,7 @@ public class LLBuildManifestBuilder { var buildConfig: String { buildParameters.configuration.dirname } var buildParameters: BuildParameters { plan.buildParameters } + var buildEnvironment: BuildEnvironment { buildParameters.buildEnvironment } /// Create a new builder with a build plan. public init(_ plan: BuildPlan) { @@ -259,12 +260,12 @@ extension LLBuildManifestBuilder { } } - for dependency in target.target.dependencies { + for dependency in target.target.buildDependencies(in: buildEnvironment) { switch dependency { - case .target(let target): + case .target(let target, _): addStaticTargetInputs(target) - case .product(let product): + case .product(let product, _): switch product.type { case .executable, .library(.dynamic): // Establish a dependency on binary of the product. @@ -350,12 +351,12 @@ extension LLBuildManifestBuilder { } } - for dependency in target.target.dependencies { + for dependency in target.target.buildDependencies(in: buildEnvironment) { switch dependency { - case .target(let target): + case .target(let target, _): addStaticTargetInputs(target) - case .product(let product): + case .product(let product, _): switch product.type { case .executable, .library(.dynamic): // Establish a dependency on binary of the product. diff --git a/Sources/PackageGraph/PackageGraph.swift b/Sources/PackageGraph/PackageGraph.swift index 1afb573c2c4..82478a1ae25 100644 --- a/Sources/PackageGraph/PackageGraph.swift +++ b/Sources/PackageGraph/PackageGraph.swift @@ -49,6 +49,8 @@ public struct PackageGraph { return rootPackages.contains(package) } + private let inputPackages: [ResolvedPackage] + /// Construct a package graph directly. public init( rootPackages: [ResolvedPackage], @@ -57,7 +59,7 @@ public struct PackageGraph { ) { self.rootPackages = rootPackages self.requiredDependencies = requiredDependencies - let inputPackages = rootPackages + rootDependencies + self.inputPackages = rootPackages + rootDependencies self.packages = try! topologicalSort(inputPackages, successors: { $0.dependencies }) allTargets = Set(packages.flatMap({ package -> [ResolvedTarget] in @@ -80,26 +82,29 @@ public struct PackageGraph { } })) - // Compute the input targets. - let inputTargets = inputPackages.flatMap({ $0.targets }).map(ResolvedTarget.Dependency.target) - // Find all the dependencies of the root targets. - let dependencies = try! topologicalSort(inputTargets, successors: { $0.dependencies }) - - // Separate out the products and targets but maintain their topological order. - var reachableTargets: Set = [] - var reachableProducts = Set(inputPackages.flatMap({ $0.products })) - - for dependency in dependencies { - switch dependency { - case .target(let target): - reachableTargets.insert(target) - case .product(let product): - reachableProducts.insert(product) - } - } + // Compute the reachable targets and products. + let inputTargets = inputPackages.lazy.flatMap({ $0.targets }) + let inputProducts = inputPackages.lazy.flatMap({ $0.products }) + let recursiveDependencies = inputTargets.flatMap({ $0.recursiveDependencies() }) + + self.reachableTargets = Set(inputTargets).union(recursiveDependencies.compactMap({ $0.target })) + self.reachableProducts = Set(inputProducts).union(recursiveDependencies.compactMap({ $0.product })) + } + + public func reachableBuildTargets(in environment: BuildEnvironment) -> Set { + let inputTargets = inputPackages.lazy.flatMap({ $0.targets }) + let recursiveBuildTargetDependencies = inputTargets + .flatMap({ $0.recursiveBuildTargetDependencies(in: environment) }) + return Set(inputTargets).union(recursiveBuildTargetDependencies) + } - self.reachableTargets = reachableTargets - self.reachableProducts = reachableProducts + public func reachableBuildProducts(in environment: BuildEnvironment) -> Set { + let recursiveBuildProductDependencies = inputPackages + .lazy + .flatMap({ $0.targets }) + .flatMap({ $0.recursiveBuildDependencies(in: environment) }) + .compactMap({ $0.product }) + return Set(inputPackages.flatMap({ $0.products })).union(recursiveBuildProductDependencies) } /// Computes a map from each executable target in any of the root packages to the corresponding test targets. @@ -119,7 +124,7 @@ public struct PackageGraph { for target in rootTargets where target.type == .executable { // Find all dependencies of this target within its package. let dependencies = try! topologicalSort(target.dependencies, successors: { - $0.dependencies.compactMap({ $0.target }).map(ResolvedTarget.Dependency.target) + $0.dependencies.compactMap({ $0.target }).map({ .target($0, conditions: []) }) }).compactMap({ $0.target }) // Include the test targets whose dependencies intersect with the diff --git a/Sources/PackageGraph/PackageGraphLoader.swift b/Sources/PackageGraph/PackageGraphLoader.swift index ee6f8446de7..d5bf49c5c35 100644 --- a/Sources/PackageGraph/PackageGraphLoader.swift +++ b/Sources/PackageGraph/PackageGraphLoader.swift @@ -168,7 +168,7 @@ private func checkAllDependenciesAreUsed(_ rootPackages: [ResolvedPackage], _ di let productDependencies: Set = Set(package.targets.flatMap({ target in return target.dependencies.compactMap({ targetDependency in switch targetDependency { - case .product(let product): + case .product(let product, _): return product case .target: return nil @@ -242,7 +242,13 @@ private func createResolvedPackages( // Establish dependencies between the targets. A target can only depend on another target present in the same package. let targetMap = targetBuilders.spm_createDictionary({ ($0.target, $0) }) for targetBuilder in targetBuilders { - targetBuilder.dependencies += targetBuilder.target.dependencies.map({ targetMap[$0]! }) + targetBuilder.dependencies += targetBuilder.target.dependencies.compactMap({ dependency in + if case .target(let target, let conditions) = dependency { + return .target(targetMap[target]!, conditions: conditions) + } else { + return nil + } + }) } // Create product builders for each product in the package. A product can only contain a target present in the same package. @@ -308,10 +314,10 @@ private func createResolvedPackages( foundDuplicateTarget = foundDuplicateTarget || !allTargetNames.insert(targetBuilder.target.name).inserted // Directly add all the system module dependencies. - targetBuilder.dependencies += implicitSystemTargetDeps + targetBuilder.dependencies += implicitSystemTargetDeps.map({ .target($0, conditions: []) }) // Establish product dependencies. - for productRef in targetBuilder.target.productDependencies { + for case .product(let productRef, let conditions) in targetBuilder.target.dependencies { // Find the product in this package's dependency products. guard let product = productDependencyMap[productRef.name] else { // Only emit a diagnostic if there are no other diagnostics. @@ -337,7 +343,7 @@ private func createResolvedPackages( } } - targetBuilder.productDeps.append(product) + targetBuilder.dependencies.append(.product(product, conditions: conditions)) } } } @@ -410,14 +416,16 @@ private final class ResolvedProductBuilder: ResolvedBuilder { /// Builder for resolved target. private final class ResolvedTargetBuilder: ResolvedBuilder { + enum Dependency { + case target(_ target: ResolvedTargetBuilder, conditions: [ManifestCondition]) + case product(_ product: ResolvedProductBuilder, conditions: [ManifestCondition]) + } + /// The target reference. let target: Target /// The target dependencies of this target. - var dependencies: [ResolvedTargetBuilder] = [] - - /// The product dependencies of this target. - var productDeps: [ResolvedProductBuilder] = [] + var dependencies: [Dependency] = [] /// The diagnostics engine. let diagnostics: DiagnosticsEngine @@ -441,24 +449,19 @@ private final class ResolvedTargetBuilder: ResolvedBuilder { } override func constructImpl() -> ResolvedTarget { - var deps: [ResolvedTarget.Dependency] = [] - for dependency in dependencies { - deps.append(.target(dependency.construct())) - } - for dependency in productDeps { - let product = dependency.construct() - - if !dependency.packageBuilder.isAllowedToVendUnsafeProducts { - diagnoseInvalidUseOfUnsafeFlags(product) + let dependencies = self.dependencies.map({ dependency -> ResolvedTarget.Dependency in + switch dependency { + case .target(let targetBuilder, let conditions): + return .target(targetBuilder.construct(), conditions: conditions) + case .product(let productBuilder, let conditions): + let product = productBuilder.construct() + if !productBuilder.packageBuilder.isAllowedToVendUnsafeProducts { + diagnoseInvalidUseOfUnsafeFlags(product) + } + return .product(product, conditions: conditions) } - - deps.append(.product(product)) - } - - return ResolvedTarget( - target: target, - dependencies: deps - ) + }) + return ResolvedTarget(target: target, dependencies: dependencies) } } diff --git a/Sources/PackageLoading/ModuleMapGenerator.swift b/Sources/PackageLoading/ModuleMapGenerator.swift index da587e648c4..9177511409d 100644 --- a/Sources/PackageLoading/ModuleMapGenerator.swift +++ b/Sources/PackageLoading/ModuleMapGenerator.swift @@ -173,7 +173,7 @@ public struct ModuleMapGenerator { // If the file exists with the identical contents, we don't need to rewrite it. // Otherwise, compiler will recompile even if nothing else has changed. - if let contents = try? localFileSystem.readFileContents(file), contents == stream.bytes { + if let contents = try? fileSystem.readFileContents(file), contents == stream.bytes { return } try fileSystem.writeFileContents(file, bytes: stream.bytes) diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 9c3c136e60a..16656b2e1a2 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -555,48 +555,49 @@ public final class PackageBuilder { for potentialModule in potentialModules.lazy.reversed() { // Validate the target name. This function will throw an error if it detects a problem. try validateModuleName(potentialModule.path, potentialModule.name, isTest: potentialModule.isTest) - // Get the intra-package dependencies of this target. - let deps: [Target] = targetMap[potentialModule.name].map({ - $0.dependencies.compactMap({ - switch $0 { - case .target(let name, _): + + // Get the target from the manifest. + let manifestTarget = targetMap[potentialModule.name] + + // Get the dependencies of this target. + let dependencies: [Target.Dependency] = manifestTarget.map({ + $0.dependencies.compactMap({ dependency in + switch dependency { + case .target(let name, let condition): // We don't create an object for targets which have no sources. if emptyModules.contains(name) { return nil } - return targets[name]! + guard let target = targets[name] else { return nil } + return .target(target, conditions: buildConditions(from: condition)) - case .byName(let name, _): + case .product(let name, let package, let condition): + return .product( + .init(name: name, package: package), + conditions: buildConditions(from: condition) + ) + + case .byName(let name, let condition): // We don't create an object for targets which have no sources. if emptyModules.contains(name) { return nil } - return targets[name] - - case .product: return nil + if let target = targets[name] { + return .target(target, conditions: buildConditions(from: condition)) + } else if potentialModuleMap[name] == nil { + return .product( + .init(name: name, package: nil), + conditions: buildConditions(from: condition) + ) + } else { + return nil + } } }) }) ?? [] - // Get the target from the manifest. - let manifestTarget = targetMap[potentialModule.name] - - // Figure out the product dependencies. - let productDeps: [(String, String?)] - productDeps = manifestTarget?.dependencies.compactMap({ - switch $0 { - case .target: - return nil - case .byName(let name, _): - // If this dependency was not found locally, it is a product dependency. - return potentialModuleMap[name] == nil ? (name, nil) : nil - case .product(let name, let package, _): - return (name, package) - } - }) ?? [] - // Create the target. let target = try createTarget( potentialModule: potentialModule, manifestTarget: manifestTarget, - moduleDependencies: deps, - productDeps: productDeps) + dependencies: dependencies + ) // Add the created target to the map or print no sources warning. if let createdTarget = target { targets[createdTarget.name] = createdTarget @@ -622,8 +623,7 @@ public final class PackageBuilder { private func createTarget( potentialModule: PotentialModule, manifestTarget: TargetDescription?, - moduleDependencies: [Target], - productDeps: [(name: String, package: String?)] + dependencies: [Target.Dependency] ) throws -> Target? { guard let manifestTarget = manifestTarget else { return nil } @@ -644,7 +644,7 @@ public final class PackageBuilder { } // Check for duplicate target dependencies by name - let combinedDependencyNames = moduleDependencies.map { $0.name } + productDeps.map { $0.0 } + let combinedDependencyNames = dependencies.map({ $0.target?.name ?? $0.product!.name }) combinedDependencyNames.spm_findDuplicates().forEach { diagnostics.emit(.duplicateTargetDependency(dependency: $0, target: potentialModule.name)) } @@ -695,8 +695,7 @@ public final class PackageBuilder { isTest: potentialModule.isTest, sources: sources, resources: resources, - dependencies: moduleDependencies, - productDependencies: productDeps, + dependencies: dependencies, swiftVersion: try swiftVersion(), buildSettings: buildSettings ) @@ -711,8 +710,7 @@ public final class PackageBuilder { isTest: potentialModule.isTest, sources: sources, resources: resources, - dependencies: moduleDependencies, - productDependencies: productDeps, + dependencies: dependencies, buildSettings: buildSettings ) } @@ -786,16 +784,7 @@ public final class PackageBuilder { // Create an assignment for this setting. var assignment = BuildSettings.Assignment() assignment.value = setting.value - - if let config = setting.condition?.config.map({ BuildConfiguration(rawValue: $0)! }) { - let condition = ConfigurationCondition(configuration: config) - assignment.conditions.append(condition) - } - - if let platforms = setting.condition?.platformNames.map({ platformRegistry.platformByName[$0]! }), !platforms.isEmpty { - let condition = PlatformsCondition(platforms: platforms) - assignment.conditions.append(condition) - } + assignment.conditions = buildConditions(from: setting.condition) // Finally, add the assignment to the assignment table. table.add(assignment, for: decl) @@ -804,6 +793,22 @@ public final class PackageBuilder { return table } + func buildConditions(from condition: ManifestConditionDescription?) -> [ManifestCondition] { + var conditions: [ManifestCondition] = [] + + if let config = condition?.config.map({ BuildConfiguration(rawValue: $0)! }) { + let condition = ConfigurationCondition(configuration: config) + conditions.append(condition) + } + + if let platforms = condition?.platformNames.map({ platformRegistry.platformByName[$0]! }), !platforms.isEmpty { + let condition = PlatformsCondition(platforms: platforms) + conditions.append(condition) + } + + return conditions + } + /// Returns the list of platforms supported by the manifest. func platforms() -> [SupportedPlatform] { if let platforms = _platforms { diff --git a/Sources/PackageModel/Platform.swift b/Sources/PackageModel/Platform.swift index 66d1432141a..df328c6a7a5 100644 --- a/Sources/PackageModel/Platform.swift +++ b/Sources/PackageModel/Platform.swift @@ -56,6 +56,7 @@ public struct Platform: Equatable, Hashable { public static let watchOS: Platform = Platform(name: "watchos", oldestSupportedVersion: "2.0") public static let linux: Platform = Platform(name: "linux", oldestSupportedVersion: .unknown) public static let android: Platform = Platform(name: "android", oldestSupportedVersion: .unknown) + public static let windows: Platform = Platform(name: "windows", oldestSupportedVersion: .unknown) } /// Represents a platform version. diff --git a/Sources/PackageModel/ResolvedModels.swift b/Sources/PackageModel/ResolvedModels.swift index 36cee383d77..d6cac6339c5 100644 --- a/Sources/PackageModel/ResolvedModels.swift +++ b/Sources/PackageModel/ResolvedModels.swift @@ -15,16 +15,26 @@ public final class ResolvedTarget: CustomStringConvertible, ObjectIdentifierProt /// Represents dependency of a resolved target. public enum Dependency: Hashable { + public static func == (lhs: ResolvedTarget.Dependency, rhs: ResolvedTarget.Dependency) -> Bool { + switch (lhs, rhs) { + case (.target(let lhsTarget, _), .target(let rhsTarget, _)): + return lhsTarget == rhsTarget + case (.product(let lhsProduct, _), .product(let rhsProduct, _)): + return lhsProduct == rhsProduct + default: + return false + } + } /// Direct dependency of the target. This target is in the same package and should be statically linked. - case target(ResolvedTarget) + case target(_ target: ResolvedTarget, conditions: [ManifestCondition]) /// The target depends on this product. - case product(ResolvedProduct) + case product(_ product: ResolvedProduct, conditions: [ManifestCondition]) public var target: ResolvedTarget? { switch self { - case .target(let target): return target + case .target(let target, _): return target case .product: return nil } } @@ -32,9 +42,29 @@ public final class ResolvedTarget: CustomStringConvertible, ObjectIdentifierProt public var product: ResolvedProduct? { switch self { case .target: return nil - case .product(let product): return product + case .product(let product, _): return product + } + } + + public var conditions: [ManifestCondition] { + switch self { + case .target(_, let conditions): return conditions + case .product(_, let conditions): return conditions } } + + public func hash(into hasher: inout Hasher) { + switch self { + case .target(let target, _): + hasher.combine(target) + case .product(let product, _): + hasher.combine(product) + } + } + + public func satisfies(_ environment: BuildEnvironment) -> Bool { + conditions.allSatisfy({ $0.satisfies(environment) }) + } } /// The underlying target represented in this resolved target. @@ -48,16 +78,33 @@ public final class ResolvedTarget: CustomStringConvertible, ObjectIdentifierProt /// The dependencies of this target. public let dependencies: [Dependency] - /// Returns the recursive dependencies filtered by the given platform, if present. - public func recursiveDependencies() -> [ResolvedTarget] { - return try! topologicalSort(self.dependencies, successors: { - switch $0 { - case .target(let target): - return target.dependencies - case .product(let product): - return product.targets.map(ResolvedTarget.Dependency.target) - } - }).compactMap({ $0.target }) + /// Returns the recursive dependencies. + public func recursiveDependencies() -> [Dependency] { + return try! topologicalSort(self.dependencies, successors: { $0.dependencies }) + } + + /// Returns the recursive target dependencies. + public func recursiveTargetDependencies() -> [ResolvedTarget] { + return recursiveDependencies().compactMap({ $0.target }) + } + + /// Returns dependencies which satisfy the input build environment, based on their conditions. + /// - Parameters: + /// - environment: The build environmen to use to filter dependencies on. + public func buildDependencies(in environment: BuildEnvironment) -> [Dependency] { + return dependencies.filter({ $0.satisfies(environment) }) + } + + /// Returns the recursive dependencies which satisfy the input build environment, based on their conditions. + public func recursiveBuildDependencies(in environment: BuildEnvironment) -> [Dependency] { + return try! topologicalSort(buildDependencies(in: environment), successors: { dependency in + return dependency.dependencies.filter({ $0.satisfies(environment) }) + }) + } + + /// Returns the recursive target dependencies which satisfy the input build environment, based on their conditions. + public func recursiveBuildTargetDependencies(in environment: BuildEnvironment) -> [ResolvedTarget] { + return recursiveBuildDependencies(in: environment).compactMap({ $0.target }) } /// The language-level target name. @@ -169,9 +216,9 @@ public final class ResolvedProduct: ObjectIdentifierProtocol, CustomStringConver self.linuxMainTarget = underlyingProduct.linuxMain.map({ linuxMain in // Create an exectutable resolved target with the linux main, adding product's targets as dependencies. - let swiftTarget = SwiftTarget( - linuxMain: linuxMain, name: product.name, dependencies: product.targets) - return ResolvedTarget(target: swiftTarget, dependencies: targets.map(ResolvedTarget.Dependency.target)) + let dependencies: [Target.Dependency] = product.targets.map({ .target($0, conditions: []) }) + let swiftTarget = SwiftTarget(linuxMain: linuxMain, name: product.name, dependencies: dependencies) + return ResolvedTarget(target: swiftTarget, dependencies: targets.map({ .target($0, conditions: []) })) }) } @@ -197,10 +244,10 @@ extension ResolvedTarget.Dependency: CustomStringConvertible { /// Returns the dependencies of the underlying dependency. public var dependencies: [ResolvedTarget.Dependency] { switch self { - case .target(let target): + case .target(let target, _): return target.dependencies - case .product(let product): - return product.targets.map(ResolvedTarget.Dependency.target) + case .product(let product, _): + return product.targets.map({ .target($0, conditions: []) }) } } @@ -209,9 +256,9 @@ extension ResolvedTarget.Dependency: CustomStringConvertible { public var description: String { var str = " Void + ) { + guard let target = find(target: name) else { + return XCTFail("Module \(name) not found", file: file, line: line) + } + body(ResolvedTargetResult(target)) + } + public func check(testModules: String..., file: StaticString = #file, line: UInt = #line) { XCTAssertEqual( graph.allTargets @@ -51,21 +95,58 @@ public final class PackageGraphResult { public func find(target: String) -> ResolvedTarget? { return graph.allTargets.first(where: { $0.name == target }) } +} - public func check(dependencies: String..., target name: String, file: StaticString = #file, line: UInt = #line) { - guard let target = find(target: name) else { - return XCTFail("Module \(name) not found", file: file, line: line) +public final class ResolvedTargetResult { + private let target: ResolvedTarget + + init(_ target: ResolvedTarget) { + self.target = target + } + + public func check(dependencies: String..., file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(Set(dependencies), Set(target.dependencies.map({ $0.name })), file: file, line: line) + } + + public func checkDependency( + _ name: String, + file: StaticString = #file, + line: UInt = #line, + body: (ResolvedTargetDependencyResult) -> Void + ) { + guard let dependency = target.dependencies.first(where: { $0.name == name }) else { + return XCTFail("Dependency \(name) not found", file: file, line: line) } - XCTAssertEqual(dependencies.sorted(), target.dependencies.map{$0.name}.sorted(), file: file, line: line) + body(ResolvedTargetDependencyResult(dependency)) + } +} + +public final class ResolvedTargetDependencyResult { + private let dependency: ResolvedTarget.Dependency + + init(_ dependency: ResolvedTarget.Dependency) { + self.dependency = dependency + } + + public func checkConditions(satisfy environment: BuildEnvironment, file: StaticString = #file, line: UInt = #line) { + XCTAssert(dependency.conditions.allSatisfy({ $0.satisfies(environment) }), file: file, line: line) + } + + public func checkConditions( + dontSatisfy environment: BuildEnvironment, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssert(!dependency.conditions.allSatisfy({ $0.satisfies(environment) }), file: file, line: line) } } extension ResolvedTarget.Dependency { public var name: String { switch self { - case .target(let target): + case .target(let target, _): return target.name - case .product(let product): + case .product(let product, _): return product.name } } diff --git a/Sources/Xcodeproj/pbxproj.swift b/Sources/Xcodeproj/pbxproj.swift index b47b9cf2379..e096f131115 100644 --- a/Sources/Xcodeproj/pbxproj.swift +++ b/Sources/Xcodeproj/pbxproj.swift @@ -490,7 +490,7 @@ func xcodeProject( // Add header search paths for any C target on which we depend. var hdrInclPaths = ["$(inherited)"] - for depModule in [target] + target.recursiveDependencies() { + for depModule in [target] + target.recursiveTargetDependencies() { // FIXME: Possibly factor this out into a separate protocol; the // idea would be that we would ask the target how it contributes // to the overall build environment for client targets, which can @@ -640,7 +640,7 @@ func xcodeProject( // For each target on which this one depends, add a target dependency // and also link against the target's product. - for dependency in target.recursiveDependencies() { + for dependency in target.recursiveTargetDependencies() { // We should never find ourself in the list of dependencies. assert(dependency != target) diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 2982dacd434..de20cf9b6ac 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -72,6 +72,24 @@ final class BuildPlanTests: XCTestCase { ) } + func mockBuildParameters(environment: BuildEnvironment) -> BuildParameters { + let triple: Triple + switch environment.platform { + case .macOS: + triple = Triple.macOS + case .linux: + triple = Triple.arm64Linux + case .android: + triple = Triple.arm64Android + case .windows: + triple = Triple.windows + default: + fatalError("unsupported platform in tests") + } + + return mockBuildParameters(config: environment.configuration, destinationTriple: triple) + } + func testBasicSwiftPackage() throws { let fs = InMemoryFileSystem(emptyFiles: "/Pkg/Sources/exe/main.swift", @@ -141,6 +159,111 @@ final class BuildPlanTests: XCTestCase { #endif } + func testSwiftConditionalDependency() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Pkg/Sources/exe/main.swift", + "/Pkg/Sources/PkgLib/lib.swift", + "/ExtPkg/Sources/ExtLib/lib.swift" + ) + + let diagnostics = DiagnosticsEngine() + let graph = loadPackageGraph(root: "/Pkg", fs: fs, diagnostics: diagnostics, + manifests: [ + Manifest.createV4Manifest( + name: "Pkg", + path: "/Pkg", + url: "/Pkg", + dependencies: [ + PackageDependencyDescription(url: "/ExtPkg", requirement: .upToNextMajor(from: "1.0.0")), + ], + targets: [ + TargetDescription(name: "exe", dependencies: [ + .target(name: "PkgLib", condition: ManifestConditionDescription( + platformNames: ["linux", "android"], + config: nil + )) + ]), + TargetDescription(name: "PkgLib", dependencies: [ + .product(name: "ExtLib", package: "ExtPkg", condition: ManifestConditionDescription( + platformNames: [], + config: "debug" + )) + ]), + ] + ), + Manifest.createV4Manifest( + name: "ExtPkg", + path: "/ExtPkg", + url: "/ExtPkg", + products: [ + ProductDescription(name: "ExtLib", targets: ["ExtLib"]), + ], + targets: [ + TargetDescription(name: "ExtLib", dependencies: []), + ] + ), + ] + ) + + XCTAssertNoDiagnostics(diagnostics) + + do { + let plan = try BuildPlan( + buildParameters: mockBuildParameters(environment: BuildEnvironment( + platform: .linux, + configuration: .release + )), + graph: graph, + diagnostics: diagnostics, + fileSystem: fs + ) + + let linkedFileList = try fs.readFileContents(AbsolutePath("/path/to/build/release/exe.product/Objects.LinkFileList")) + XCTAssertMatch(linkedFileList.description, .contains("PkgLib")) + XCTAssertNoMatch(linkedFileList.description, .contains("ExtLib")) + + mktmpdir { path in + let yaml = path.appending(component: "release.yaml") + let llbuild = LLBuildManifestBuilder(plan) + try llbuild.generateManifest(at: yaml) + let contents = try localFileSystem.readFileContents(yaml).description + XCTAssertMatch(contents, .contains(""" + "C.exe-release.module": + tool: swift-compiler + inputs: ["/Pkg/Sources/exe/main.swift","/path/to/build/release/PkgLib.swiftmodule"] + """)) + } + } + + do { + let plan = try BuildPlan( + buildParameters: mockBuildParameters(environment: BuildEnvironment( + platform: .macOS, + configuration: .debug + )), + graph: graph, + diagnostics: diagnostics, + fileSystem: fs + ) + + let linkedFileList = try fs.readFileContents(AbsolutePath("/path/to/build/debug/exe.product/Objects.LinkFileList")) + XCTAssertNoMatch(linkedFileList.description, .contains("PkgLib")) + XCTAssertNoMatch(linkedFileList.description, .contains("ExtLib")) + + mktmpdir { path in + let yaml = path.appending(component: "debug.yaml") + let llbuild = LLBuildManifestBuilder(plan) + try llbuild.generateManifest(at: yaml) + let contents = try localFileSystem.readFileContents(yaml).description + XCTAssertMatch(contents, .contains(""" + "C.exe-debug.module": + tool: swift-compiler + inputs: ["/Pkg/Sources/exe/main.swift"] + """)) + } + } + } + func testBasicExtPackages() throws { let fileSystem = InMemoryFileSystem(emptyFiles: "/A/Sources/ATarget/foo.swift", @@ -354,6 +477,96 @@ final class BuildPlanTests: XCTestCase { """) } + func testClangConditionalDependency() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Pkg/Sources/exe/main.c", + "/Pkg/Sources/PkgLib/lib.c", + "/Pkg/Sources/PkgLib/lib.S", + "/Pkg/Sources/PkgLib/include/lib.h", + "/ExtPkg/Sources/ExtLib/extlib.c", + "/ExtPkg/Sources/ExtLib/include/ext.h" + ) + + let diagnostics = DiagnosticsEngine() + let graph = loadPackageGraph( + root: "/Pkg", + fs: fs, + diagnostics: diagnostics, + manifests: [ + Manifest.createV4Manifest( + name: "Pkg", + path: "/Pkg", + url: "/Pkg", + dependencies: [ + PackageDependencyDescription(url: "/ExtPkg", requirement: .upToNextMajor(from: "1.0.0")), + ], + targets: [ + TargetDescription(name: "exe", dependencies: [ + .target(name: "PkgLib", condition: ManifestConditionDescription( + platformNames: ["linux", "android"], + config: nil + )) + ]), + TargetDescription(name: "PkgLib", dependencies: [ + .product(name: "ExtPkg", package: "ExtPkg", condition: ManifestConditionDescription( + platformNames: [], + config: "debug" + )) + ]), + ]), + Manifest.createV4Manifest( + name: "ExtPkg", + path: "/ExtPkg", + url: "/ExtPkg", + products: [ + ProductDescription(name: "ExtPkg", targets: ["ExtLib"]), + ], + targets: [ + TargetDescription(name: "ExtLib", dependencies: []), + ]), + ] + ) + + XCTAssertNoDiagnostics(diagnostics) + + do { + let result = BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(environment: BuildEnvironment( + platform: .linux, + configuration: .release + )), + graph: graph, + diagnostics: diagnostics, + fileSystem: fs + )) + + let exeArguments = try result.target(for: "exe").clangTarget().basicArguments() + XCTAssert(exeArguments.contains(where: { $0.contains("PkgLib") })) + XCTAssert(exeArguments.allSatisfy({ !$0.contains("ExtLib") })) + + let libArguments = try result.target(for: "PkgLib").clangTarget().basicArguments() + XCTAssert(libArguments.allSatisfy({ !$0.contains("ExtLib") })) + } + + do { + let result = BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(environment: BuildEnvironment( + platform: .macOS, + configuration: .debug + )), + graph: graph, + diagnostics: diagnostics, + fileSystem: fs + )) + + let arguments = try result.target(for: "exe").clangTarget().basicArguments() + XCTAssert(arguments.allSatisfy({ !$0.contains("PkgLib") && !$0.contains("ExtLib") })) + + let libArguments = try result.target(for: "PkgLib").clangTarget().basicArguments() + XCTAssert(libArguments.contains(where: { $0.contains("ExtLib") })) + } + } + func testCLanguageStandard() throws { let fs = InMemoryFileSystem(emptyFiles: "/Pkg/Sources/exe/main.cpp", @@ -980,19 +1193,146 @@ final class BuildPlanTests: XCTestCase { ] ) XCTAssertNoDiagnostics(diagnostics) + let graphResult = PackageGraphResult(graph) + graphResult.check(reachableProducts: "aexec", "BLibrary") + graphResult.check(reachableTargets: "ATarget", "BTarget1") + graphResult.check(products: "aexec", "BLibrary", "bexec", "cexec") + graphResult.check(targets: "ATarget", "BTarget1", "BTarget2", "CTarget") - XCTAssertEqual(Set(graph.reachableProducts.map({ $0.name })), ["aexec", "BLibrary"]) - XCTAssertEqual(Set(graph.reachableTargets.map({ $0.name })), ["ATarget", "BTarget1"]) - XCTAssertEqual(Set(graph.allProducts.map({ $0.name })), ["aexec", "BLibrary", "bexec", "cexec"]) - XCTAssertEqual(Set(graph.allTargets.map({ $0.name })), ["ATarget", "BTarget1", "BTarget2", "CTarget"]) - - let result = BuildPlanResult(plan: try BuildPlan( + let planResult = BuildPlanResult(plan: try BuildPlan( buildParameters: mockBuildParameters(), - graph: graph, diagnostics: diagnostics, - fileSystem: fileSystem)) + graph: graph, + diagnostics: diagnostics, + fileSystem: fileSystem + )) + + planResult.checkProductsCount(4) + planResult.checkTargetsCount(4) + } - XCTAssertEqual(Set(result.productMap.keys), ["aexec", "BLibrary", "bexec", "cexec"]) - XCTAssertEqual(Set(result.targetMap.keys), ["ATarget", "BTarget1", "BTarget2", "CTarget"]) + func testReachableBuildProductsAndTargets() throws { + let fileSystem = InMemoryFileSystem(emptyFiles: + "/A/Sources/ATarget/main.swift", + "/B/Sources/BTarget1/source.swift", + "/B/Sources/BTarget2/source.swift", + "/B/Sources/BTarget3/source.swift", + "/C/Sources/CTarget/source.swift" + ) + + let diagnostics = DiagnosticsEngine() + let graph = loadPackageGraph( + root: "/A", + fs: fileSystem, + diagnostics: diagnostics, + manifests: [ + Manifest.createV4Manifest( + name: "A", + path: "/A", + url: "/A", + dependencies: [ + PackageDependencyDescription(url: "/B", requirement: .upToNextMajor(from: "1.0.0")), + PackageDependencyDescription(url: "/C", requirement: .upToNextMajor(from: "1.0.0")), + ], + products: [ + ProductDescription(name: "aexec", type: .executable, targets: ["ATarget"]), + ], + targets: [ + TargetDescription(name: "ATarget", dependencies: [ + .product(name: "BLibrary1", package: "B", condition: ManifestConditionDescription( + platformNames: ["linux"], + config: nil + )), + .product(name: "BLibrary2", package: "B", condition: ManifestConditionDescription( + platformNames: [], + config: "debug" + )), + .product(name: "CLibrary", package: "C", condition: ManifestConditionDescription( + platformNames: ["android"], + config: "release" + )), + ]) + ] + ), + Manifest.createV4Manifest( + name: "B", + path: "/B", + url: "/B", + products: [ + ProductDescription(name: "BLibrary1", type: .library(.static), targets: ["BTarget1"]), + ProductDescription(name: "BLibrary2", type: .library(.static), targets: ["BTarget2"]), + ], + targets: [ + TargetDescription(name: "BTarget1", dependencies: []), + TargetDescription(name: "BTarget2", dependencies: [ + .target(name: "BTarget3", condition: ManifestConditionDescription( + platformNames: ["macos"], + config: nil + )), + ]), + TargetDescription(name: "BTarget3", dependencies: []), + ] + ), + Manifest.createV4Manifest( + name: "C", + path: "/C", + url: "/C", + products: [ + ProductDescription(name: "CLibrary", type: .library(.static), targets: ["CTarget"]) + ], + targets: [ + TargetDescription(name: "CTarget", dependencies: []), + ] + ), + ] + ) + + XCTAssertNoDiagnostics(diagnostics) + let graphResult = PackageGraphResult(graph) + + do { + let linuxDebug = BuildEnvironment(platform: .linux, configuration: .debug) + graphResult.check(reachableBuildProducts: "aexec", "BLibrary1", "BLibrary2", in: linuxDebug) + graphResult.check(reachableBuildTargets: "ATarget", "BTarget1", "BTarget2", in: linuxDebug) + + let planResult = BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(environment: linuxDebug), + graph: graph, + diagnostics: diagnostics, + fileSystem: fileSystem + )) + planResult.checkProductsCount(4) + planResult.checkTargetsCount(5) + } + + do { + let macosDebug = BuildEnvironment(platform: .macOS, configuration: .debug) + graphResult.check(reachableBuildProducts: "aexec", "BLibrary2", in: macosDebug) + graphResult.check(reachableBuildTargets: "ATarget", "BTarget2", "BTarget3", in: macosDebug) + + let planResult = BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(environment: macosDebug), + graph: graph, + diagnostics: diagnostics, + fileSystem: fileSystem + )) + planResult.checkProductsCount(4) + planResult.checkTargetsCount(5) + } + + do { + let androidRelease = BuildEnvironment(platform: .android, configuration: .release) + graphResult.check(reachableBuildProducts: "aexec", "CLibrary", in: androidRelease) + graphResult.check(reachableBuildTargets: "ATarget", "CTarget", in: androidRelease) + + let planResult = BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(environment: androidRelease), + graph: graph, + diagnostics: diagnostics, + fileSystem: fileSystem + )) + planResult.checkProductsCount(4) + planResult.checkTargetsCount(5) + } } func testSystemPackageBuildPlan() throws { diff --git a/Tests/PackageGraphTests/PackageGraphTests.swift b/Tests/PackageGraphTests/PackageGraphTests.swift index 980466fb908..83972048c6e 100644 --- a/Tests/PackageGraphTests/PackageGraphTests.swift +++ b/Tests/PackageGraphTests/PackageGraphTests.swift @@ -73,9 +73,9 @@ class PackageGraphTests: XCTestCase { result.check(packages: "Bar", "Foo", "Baz") result.check(targets: "Bar", "Foo", "Baz", "FooDep") result.check(testModules: "BazTests") - result.check(dependencies: "FooDep", target: "Foo") - result.check(dependencies: "Foo", target: "Bar") - result.check(dependencies: "Bar", target: "Baz") + result.checkTarget("Foo") { result in result.check(dependencies: "FooDep") } + result.checkTarget("Bar") { result in result.check(dependencies: "Foo") } + result.checkTarget("Baz") { result in result.check(dependencies: "Bar") } } } @@ -118,8 +118,8 @@ class PackageGraphTests: XCTestCase { PackageGraphTester(g) { result in result.check(packages: "Bar", "Foo") result.check(targets: "Bar", "CBar", "Foo") - result.check(dependencies: "Bar", "CBar", target: "Foo") - result.check(dependencies: "CBar", target: "Bar") + result.checkTarget("Foo") { result in result.check(dependencies: "Bar", "CBar") } + result.checkTarget("Bar") { result in result.check(dependencies: "CBar") } } } @@ -776,4 +776,83 @@ class PackageGraphTests: XCTestCase { result.check(diagnostic: .contains("the target 'Bar2' in product 'Bar' contains unsafe build flags"), behavior: .error) } } + + func testConditionalTargetDependency() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Foo/Sources/Foo/source.swift", + "/Foo/Sources/Bar/source.swift", + "/Foo/Sources/Baz/source.swift", + "/Biz/Sources/Biz/source.swift" + ) + + let diagnostics = DiagnosticsEngine() + let graph = loadPackageGraph( + root: "/Foo", + fs: fs, + diagnostics: diagnostics, + manifests: [ + Manifest.createV4Manifest( + name: "Foo", + path: "/Foo", + url: "/Foo", + dependencies: [ + PackageDependencyDescription(url: "/Biz", requirement: .localPackage), + ], + targets: [ + TargetDescription(name: "Foo", dependencies: [ + .target(name: "Bar", condition: ManifestConditionDescription( + platformNames: ["linux"], + config: nil + )), + .byName(name: "Baz", condition: ManifestConditionDescription( + platformNames: [], + config: "debug" + )), + .product(name: "Biz", package: "Biz", condition: ManifestConditionDescription( + platformNames: ["watchos", "ios"], + config: "release" + )) + ]), + TargetDescription(name: "Bar"), + TargetDescription(name: "Baz"), + ] + ), + Manifest.createV4Manifest( + name: "Biz", + path: "/Biz", + url: "/Biz", + products: [ + ProductDescription(name: "Biz", targets: ["Biz"]) + ], + targets: [ + TargetDescription(name: "Biz"), + ] + ), + ] + ) + + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTester(graph) { result in + result.check(targets: "Foo", "Bar", "Baz", "Biz") + result.checkTarget("Foo") { result in + result.check(dependencies: "Bar", "Baz", "Biz") + result.checkDependency("Bar") { result in + result.checkConditions(satisfy: .init(platform: .linux, configuration: .debug)) + result.checkConditions(satisfy: .init(platform: .linux, configuration: .release)) + result.checkConditions(dontSatisfy: .init(platform: .macOS, configuration: .release)) + } + result.checkDependency("Baz") { result in + result.checkConditions(satisfy: .init(platform: .watchOS, configuration: .debug)) + result.checkConditions(satisfy: .init(platform: .tvOS, configuration: .debug)) + result.checkConditions(dontSatisfy: .init(platform: .tvOS, configuration: .release)) + } + result.checkDependency("Biz") { result in + result.checkConditions(satisfy: .init(platform: .watchOS, configuration: .release)) + result.checkConditions(satisfy: .init(platform: .iOS, configuration: .release)) + result.checkConditions(dontSatisfy: .init(platform: .iOS, configuration: .debug)) + result.checkConditions(dontSatisfy: .init(platform: .macOS, configuration: .release)) + } + } + } + } } diff --git a/Tests/PackageLoadingTests/PackageBuilderTests.swift b/Tests/PackageLoadingTests/PackageBuilderTests.swift index 1d8935c91d0..072ae714c39 100644 --- a/Tests/PackageLoadingTests/PackageBuilderTests.swift +++ b/Tests/PackageLoadingTests/PackageBuilderTests.swift @@ -545,19 +545,19 @@ class PackageBuilderTests: XCTestCase { result.checkModule("TheTestOfA") { moduleResult in moduleResult.check(c99name: "TheTestOfA", type: .test) moduleResult.checkSources(root: "/Tests/TheTestOfA", paths: "Foo.swift") - moduleResult.check(dependencies: ["A"]) + moduleResult.check(targetDependencies: ["A"]) } result.checkModule("B") { moduleResult in moduleResult.check(c99name: "B", type: .test) moduleResult.checkSources(root: "/Tests/B", paths: "Foo.swift") - moduleResult.check(dependencies: []) + moduleResult.check(targetDependencies: []) } result.checkModule("ATests") { moduleResult in moduleResult.check(c99name: "ATests", type: .test) moduleResult.checkSources(root: "/Tests/ATests", paths: "Foo.swift") - moduleResult.check(dependencies: []) + moduleResult.check(targetDependencies: []) } result.checkProduct("FooPackageTests") { _ in } @@ -622,7 +622,7 @@ class PackageBuilderTests: XCTestCase { result.checkModule("Foo") { moduleResult in moduleResult.check(c99name: "Foo", type: .library) moduleResult.checkSources(root: "/Sources/Foo", paths: "Foo.swift") - moduleResult.check(dependencies: ["Bar"]) + moduleResult.check(targetDependencies: ["Bar"]) } for target in ["Bar", "Baz"] { @@ -646,13 +646,13 @@ class PackageBuilderTests: XCTestCase { result.checkModule("Foo") { moduleResult in moduleResult.check(c99name: "Foo", type: .library) moduleResult.checkSources(root: "/Sources/Foo", paths: "Foo.swift") - moduleResult.check(dependencies: ["Bar"]) + moduleResult.check(targetDependencies: ["Bar"]) } result.checkModule("Bar") { moduleResult in moduleResult.check(c99name: "Bar", type: .library) moduleResult.checkSources(root: "/Sources/Bar", paths: "Bar.swift") - moduleResult.check(dependencies: ["Baz"]) + moduleResult.check(targetDependencies: ["Baz"]) } result.checkModule("Baz") { moduleResult in @@ -707,8 +707,8 @@ class PackageBuilderTests: XCTestCase { result.checkModule("Foo") { moduleResult in moduleResult.check(c99name: "Foo", type: .library) moduleResult.checkSources(root: "/Sources/Foo", paths: "Foo.swift") - moduleResult.check(dependencies: ["Bar", "Baz"]) - moduleResult.check(productDeps: [(name: "Bam", package: nil)]) + moduleResult.check(targetDependencies: ["Bar", "Baz"]) + moduleResult.check(productDependencies: [.init(name: "Bam", package: nil)]) } for target in ["Bar", "Baz"] { @@ -1262,7 +1262,7 @@ class PackageBuilderTests: XCTestCase { result.checkModule("bar") { moduleResult in moduleResult.check(c99name: "bar", type: .library) moduleResult.checkSources(root: "/Sources/bar", paths: "bar.swift") - moduleResult.check(dependencies: ["foo"]) + moduleResult.check(targetDependencies: ["foo"]) } result.checkProduct("foo") { productResult in productResult.check(type: .library(.automatic), targets: ["foo"]) @@ -1662,6 +1662,71 @@ class PackageBuilderTests: XCTestCase { result.checkDiagnostic("invalid duplicate target dependency declaration 'Foo2' in target 'Foo'") } } + + func testConditionalDependencies() { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/Foo/main.swift", + "/Sources/Bar/bar.swift", + "/Sources/Baz/baz.swift" + ) + + let manifest = Manifest.createManifest( + name: "Foo", + v: .v5_3, + dependencies: [ + PackageDependencyDescription(url: "/Biz", requirement: .localPackage), + ], + targets: [ + TargetDescription( + name: "Foo", + dependencies: [ + .target(name: "Bar", condition: ManifestConditionDescription( + platformNames: ["macos"], + config: nil + )), + .byName(name: "Baz", condition: ManifestConditionDescription( + platformNames: [], + config: "debug" + )), + .product(name: "Biz", package: "Biz", condition: ManifestConditionDescription( + platformNames: ["watchos", "ios"], + config: "release" + )), + ] + ), + TargetDescription(name: "Bar"), + TargetDescription(name: "Baz"), + ] + ) + + PackageBuilderTester(manifest, in: fs) { result in + result.checkProduct("Foo") + result.checkModule("Bar") + result.checkModule("Baz") + result.checkModule("Foo") { result in + result.check(dependencies: ["Bar", "Baz", "Biz"]) + + result.checkDependency("Bar") { result in + result.checkConditions(satisfy: .init(platform: .macOS, configuration: .debug)) + result.checkConditions(satisfy: .init(platform: .macOS, configuration: .release)) + result.checkConditions(dontSatisfy: .init(platform: .watchOS, configuration: .release)) + } + + result.checkDependency("Baz") { result in + result.checkConditions(satisfy: .init(platform: .macOS, configuration: .debug)) + result.checkConditions(satisfy: .init(platform: .linux, configuration: .debug)) + result.checkConditions(dontSatisfy: .init(platform: .linux, configuration: .release)) + } + + result.checkDependency("Biz") { result in + result.checkConditions(satisfy: .init(platform: .watchOS, configuration: .release)) + result.checkConditions(satisfy: .init(platform: .iOS, configuration: .release)) + result.checkConditions(dontSatisfy: .init(platform: .linux, configuration: .release)) + result.checkConditions(dontSatisfy: .init(platform: .iOS, configuration: .debug)) + } + } + } + } } extension PackageModel.Product: ObjectIdentifierProtocol {} @@ -1822,22 +1887,49 @@ final class PackageBuilderTester { checkSources(root: root, sources: paths, file: file, line: line) } - func check(dependencies depsToCheck: [String], file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(Set(depsToCheck), Set(target.dependencies.map{$0.name}), "unexpected dependencies in \(target.name)", file: file, line: line) + func check(targetDependencies depsToCheck: [String], file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(Set(depsToCheck), Set(target.dependencies.compactMap({ $0.target?.name })), "unexpected dependencies in \(target.name)", file: file, line: line) } - func check(productDeps depsToCheck: [(name: String, package: String?)], file: StaticString = #file, line: UInt = #line) { - guard depsToCheck.count == target.productDependencies.count else { + func check( + productDependencies depsToCheck: [Target.ProductReference], + file: StaticString = #file, + line: UInt = #line + ) { + let productDependencies = target.dependencies.compactMap({ $0.product }) + guard depsToCheck.count == productDependencies.count else { return XCTFail("Incorrect product dependencies", file: file, line: line) } for (idx, element) in depsToCheck.enumerated() { - let rhs = target.productDependencies[idx] + let rhs = productDependencies[idx] guard element.name == rhs.name && element.package == rhs.package else { return XCTFail("Incorrect product dependencies", file: file, line: line) } } } + func check(dependencies: [String], file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual( + Set(dependencies), + Set(target.dependencies.map({ $0.name })), + "unexpected dependencies in \(target.name)", + file: file, + line: line + ) + } + + func checkDependency( + _ name: String, + file: StaticString = #file, + line: UInt = #line, + _ body: (ModuleDependencyResult) -> Void + ) { + guard let dependency = target.dependencies.first(where: { $0.name == name }) else { + return XCTFail("Module: \(name) not found", file: file, line: line) + } + body(ModuleDependencyResult(dependency)) + } + func check(swiftVersion: String, file: StaticString = #file, line: UInt = #line) { guard case let swiftTarget as SwiftTarget = target else { return XCTFail("\(target) is not a swift target", file: file, line: line) @@ -1850,4 +1942,24 @@ final class PackageBuilderTester { XCTAssertEqual(platforms, targetPlatforms, file: file, line: line) } } + + final class ModuleDependencyResult { + let dependency: PackageModel.Target.Dependency + + fileprivate init(_ dependency: PackageModel.Target.Dependency) { + self.dependency = dependency + } + + func checkConditions(satisfy environment: BuildEnvironment, file: StaticString = #file, line: UInt = #line) { + XCTAssert(dependency.conditions.allSatisfy({ $0.satisfies(environment) }), file: file, line: line) + } + + func checkConditions( + dontSatisfy environment: BuildEnvironment, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssert(!dependency.conditions.allSatisfy({ $0.satisfies(environment) }), file: file, line: line) + } + } } diff --git a/Tests/PackageModelTests/TargetTests.swift b/Tests/PackageModelTests/TargetTests.swift index 548aeedf03b..f2c68cf2224 100644 --- a/Tests/PackageModelTests/TargetTests.swift +++ b/Tests/PackageModelTests/TargetTests.swift @@ -19,7 +19,7 @@ private extension ResolvedTarget { target: SwiftTarget( name: name, isTest: false, sources: Sources(paths: [], root: AbsolutePath("/")), dependencies: [], swiftVersion: .v4), - dependencies: deps.map(ResolvedTarget.Dependency.target)) + dependencies: deps.map({ .target($0, conditions: []) })) } } @@ -39,8 +39,8 @@ class TargetDependencyTests: XCTestCase { let t2 = ResolvedTarget(name: "t2", deps: t1) let t3 = ResolvedTarget(name: "t3", deps: t2) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } @@ -51,9 +51,9 @@ class TargetDependencyTests: XCTestCase { let t3 = ResolvedTarget(name: "t3", deps: t2, t1) let t4 = ResolvedTarget(name: "t4", deps: t2, t3, t1) - XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1]) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t4.recursiveTargetDependencies(), [t3, t2, t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } @@ -64,9 +64,9 @@ class TargetDependencyTests: XCTestCase { let t3 = ResolvedTarget(name: "t3", deps: t2, t1) let t4 = ResolvedTarget(name: "t4", deps: t1, t2, t3) - XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1]) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t4.recursiveTargetDependencies(), [t3, t2, t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } @@ -77,9 +77,9 @@ class TargetDependencyTests: XCTestCase { let t3 = ResolvedTarget(name: "t3", deps: t2) let t4 = ResolvedTarget(name: "t4", deps: t3) - XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1]) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t4.recursiveTargetDependencies(), [t3, t2, t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } @@ -93,17 +93,17 @@ class TargetDependencyTests: XCTestCase { let t6 = ResolvedTarget(name: "t6", deps: t5, t4) // precise order is not important, but it is important that the following are true - let t6rd = t6.recursiveDeps + let t6rd = t6.recursiveTargetDependencies() XCTAssertEqual(t6rd.firstIndex(of: t3)!, t6rd.index(after: t6rd.firstIndex(of: t4)!)) XCTAssert(t6rd.firstIndex(of: t5)! < t6rd.firstIndex(of: t2)!) XCTAssert(t6rd.firstIndex(of: t5)! < t6rd.firstIndex(of: t1)!) XCTAssert(t6rd.firstIndex(of: t2)! < t6rd.firstIndex(of: t1)!) XCTAssert(t6rd.firstIndex(of: t3)! < t6rd.firstIndex(of: t2)!) - XCTAssertEqual(t5.recursiveDeps, [t2, t1]) - XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1]) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t5.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t4.recursiveTargetDependencies(), [t3, t2, t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } @@ -117,26 +117,17 @@ class TargetDependencyTests: XCTestCase { let t6 = ResolvedTarget(name: "t6", deps: t4, t5) // same as above, but these two swapped // precise order is not important, but it is important that the following are true - let t6rd = t6.recursiveDeps + let t6rd = t6.recursiveTargetDependencies() XCTAssertEqual(t6rd.firstIndex(of: t3)!, t6rd.index(after: t6rd.firstIndex(of: t4)!)) XCTAssert(t6rd.firstIndex(of: t5)! < t6rd.firstIndex(of: t2)!) XCTAssert(t6rd.firstIndex(of: t5)! < t6rd.firstIndex(of: t1)!) XCTAssert(t6rd.firstIndex(of: t2)! < t6rd.firstIndex(of: t1)!) XCTAssert(t6rd.firstIndex(of: t3)! < t6rd.firstIndex(of: t2)!) - XCTAssertEqual(t5.recursiveDeps, [t2, t1]) - XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1]) - XCTAssertEqual(t3.recursiveDeps, [t2, t1]) - XCTAssertEqual(t2.recursiveDeps, [t1]) + XCTAssertEqual(t5.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t4.recursiveTargetDependencies(), [t3, t2, t1]) + XCTAssertEqual(t3.recursiveTargetDependencies(), [t2, t1]) + XCTAssertEqual(t2.recursiveTargetDependencies(), [t1]) } } } - -private extension ResolvedTarget { - var recursiveDeps: [ResolvedTarget] { - return try! topologicalSort(self.dependencies, successors: { $0.dependencies }).compactMap({ - guard case .target(let target) = $0 else { return nil } - return target - }) - } -} diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index 342f57a3dc2..8a97b464eab 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -79,9 +79,9 @@ final class WorkspaceTests: XCTestCase { result.check(packages: "Baz", "Foo", "Quix") result.check(targets: "Bar", "Baz", "Foo", "Quix") result.check(testModules: "BarTests") - result.check(dependencies: "Bar", target: "Foo") - result.check(dependencies: "Baz", target: "Bar") - result.check(dependencies: "Bar", target: "BarTests") + result.checkTarget("Foo") { result in result.check(dependencies: "Bar") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + result.checkTarget("BarTests") { result in result.check(dependencies: "Bar") } } XCTAssertNoDiagnostics(diagnostics) } @@ -215,8 +215,8 @@ final class WorkspaceTests: XCTestCase { PackageGraphTester(graph) { result in result.check(roots: "Bar", "Foo") result.check(packages: "Bar", "Baz", "Foo") - result.check(dependencies: "Baz", target: "Foo") - result.check(dependencies: "Baz", target: "Bar") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } } XCTAssertNoDiagnostics(diagnostics) } @@ -280,7 +280,7 @@ final class WorkspaceTests: XCTestCase { PackageGraphTester(graph) { result in result.check(roots: "Bar", "Foo", "Baz") result.check(packages: "Bar", "Baz", "Foo") - result.check(dependencies: "Baz", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } } XCTAssertNoDiagnostics(diagnostics) } @@ -349,7 +349,7 @@ final class WorkspaceTests: XCTestCase { PackageGraphTester(graph) { result in result.check(packages: "Bar", "Foo") result.check(targets: "Bar", "Foo") - result.check(dependencies: "Bar", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Bar") } } XCTAssertNoDiagnostics(diagnostics) } @@ -449,7 +449,7 @@ final class WorkspaceTests: XCTestCase { result.check(roots: "Baz", "Foo") result.check(packages: "Baz", "Foo") result.check(targets: "BazA", "BazB", "Foo") - result.check(dependencies: "BazAB", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "BazAB") } } XCTAssertNoDiagnostics(diagnostics) } @@ -510,7 +510,7 @@ final class WorkspaceTests: XCTestCase { result.check(roots: "Foo") result.check(packages: "Baz", "Foo") result.check(targets: "Baz", "Foo") - result.check(dependencies: "Baz", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } } XCTAssertNoDiagnostics(diagnostics) } @@ -527,7 +527,7 @@ final class WorkspaceTests: XCTestCase { result.check(roots: "Baz", "Foo") result.check(packages: "Baz", "Foo") result.check(targets: "BazA", "BazB", "Foo") - result.check(dependencies: "Baz", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } } XCTAssertNoDiagnostics(diagnostics) } @@ -591,7 +591,7 @@ final class WorkspaceTests: XCTestCase { PackageGraphTester(graph) { result in result.check(packages: "Bar", "Foo") result.check(targets: "Bar", "Foo") - result.check(dependencies: "Bar", target: "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Bar") } } XCTAssertNoDiagnostics(diagnostics) } @@ -654,7 +654,7 @@ final class WorkspaceTests: XCTestCase { PackageGraphTester(graph) { result in result.check(packages: "A", "AA") result.check(targets: "A", "AA") - result.check(dependencies: "AA", target: "A") + result.checkTarget("A") { result in result.check(dependencies: "AA") } } XCTAssertNoDiagnostics(diagnostics) } @@ -675,7 +675,7 @@ final class WorkspaceTests: XCTestCase { ] workspace.checkPackageGraph(deps: deps) { (graph, diagnostics) in PackageGraphTester(graph) { result in - result.check(dependencies: "AA", target: "A") + result.checkTarget("A") { result in result.check(dependencies: "AA") } } XCTAssertNoDiagnostics(diagnostics) } @@ -2178,9 +2178,9 @@ final class WorkspaceTests: XCTestCase { result.check(packages: "Bar", "Baz", "Foo") result.check(targets: "Bar", "Baz", "Foo") result.check(testModules: "FooTests") - result.check(dependencies: "Bar", target: "Baz") - result.check(dependencies: "Baz", "Bar", target: "Foo") - result.check(dependencies: "Foo", target: "FooTests") + result.checkTarget("Baz") { result in result.check(dependencies: "Bar") } + result.checkTarget("Foo") { result in result.check(dependencies: "Baz", "Bar") } + result.checkTarget("FooTests") { result in result.check(dependencies: "Foo") } } XCTAssertNoDiagnostics(diagnostics) }