Skip to content

Commit 4b18559

Browse files
authored
Add support for extension products, allowing use of extensions across packages. (#3298)
This completes the PackageDescription interface changes proposed in the extensible build tools pitch. Note that actual use of this is still guarded by a feature flag, since the proposal has not been accepted yet and there may be changes needed based on review.
1 parent 8c9069d commit 4b18559

File tree

23 files changed

+221
-31
lines changed

23 files changed

+221
-31
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version: 999.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MySourceGenClient",
6+
dependencies: [
7+
.package(path: "../MySourceGenExtension")
8+
],
9+
targets: [
10+
// A tool that uses an extension.
11+
.executableTarget(
12+
name: "MyTool",
13+
dependencies: [
14+
.product(name: "MySourceGenExt", package: "MySourceGenExtension")
15+
]
16+
),
17+
// A unit that uses the extension.
18+
.testTarget(
19+
name: "MyTests",
20+
dependencies: [
21+
.product(name: "MySourceGenExt", package: "MySourceGenExtension")
22+
]
23+
)
24+
]
25+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello Extension Product!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Generated string: '\(generatedString)'")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import XCTest
2+
import class Foundation.Bundle
3+
4+
final class SwiftyProtobufTests: XCTestCase {
5+
func testExample() throws {
6+
// This is an example of a functional test case.
7+
// Use XCTAssert and related functions to verify your tests produce the correct
8+
// results.
9+
10+
// Some of the APIs that we use below are available in macOS 10.13 and above.
11+
guard #available(macOS 10.13, *) else {
12+
return
13+
}
14+
15+
// Mac Catalyst won't have `Process`, but it is supported for executables.
16+
#if !targetEnvironment(macCatalyst)
17+
18+
let fooBinary = productsDirectory.appendingPathComponent("MySourceGenTool")
19+
20+
let process = Process()
21+
process.executableURL = fooBinary
22+
23+
let pipe = Pipe()
24+
process.standardOutput = pipe
25+
26+
try process.run()
27+
process.waitUntilExit()
28+
29+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
30+
let output = String(data: data, encoding: .utf8)
31+
32+
XCTAssertEqual(output, "Hello, world!\n")
33+
#endif
34+
}
35+
36+
/// Returns path to the built products directory.
37+
var productsDirectory: URL {
38+
#if os(macOS)
39+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
40+
return bundle.bundleURL.deletingLastPathComponent()
41+
}
42+
fatalError("couldn't find the products directory")
43+
#else
44+
return Bundle.main.bundleURL
45+
#endif
46+
}
47+
}

Fixtures/Miscellaneous/Extensions/MySourceGenExtension/Package.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ let package = Package(
55
name: "MySourceGenExtension",
66
products: [
77
// The product that vends MySourceGenExt to client packages.
8-
// .extension(
9-
// name: "MySourceGenExt",
10-
// target: "MySourceGenExt"
11-
// )
8+
.extension(
9+
name: "MySourceGenExt",
10+
targets: ["MySourceGenExt"]
11+
),
12+
.executable(
13+
name: "MySourceGenTool",
14+
targets: ["MySourceGenTool"]
15+
)
1216
],
1317
targets: [
1418
// A local tool that uses an extension.
1519
.executableTarget(
1620
name: "MyLocalTool",
1721
dependencies: [
1822
"MySourceGenExt",
19-
"MySourceGenTool"
2023
]
2124
),
2225
// The target that implements the extension and generates commands to invoke MySourceGenTool.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Hello Extension!
1+
Hello Extension Target!
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
print("Exec: \(data)")
1+
print("Generated string: '\(generatedString)'")

Fixtures/Miscellaneous/Extensions/MySourceGenExtension/Sources/MySourceGenExt/extension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ for inputPath in targetBuildContext.otherFiles {
66
let outputPath = targetBuildContext.outputDir.appending(outputName)
77
commandConstructor.createCommand(
88
displayName:
9-
"MySourceGenTooling \(inputPath)",
9+
"Generating \(outputName) from \(inputPath.filename)",
1010
executable:
1111
try targetBuildContext.lookupTool(named: "MySourceGenTool"),
1212
arguments: [

Fixtures/Miscellaneous/Extensions/MySourceGenExtension/Sources/MySourceGenTool/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ let outputFile = ProcessInfo.processInfo.arguments[2]
1111

1212
let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
1313
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
14-
let outputString = "public var data = \(dataAsHex.quotedForSourceCode)\n"
14+
let outputString = "public var generatedString = \(dataAsHex.quotedForSourceCode)\n"
1515
let outputData = outputString.data(using: .utf8)
1616
FileManager.default.createFile(atPath: outputFile, contents: outputData)

Sources/Build/BuildPlan.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,8 @@ public final class ProductBuildDescription {
11381138
}
11391139
}
11401140
args += ["-emit-executable"]
1141+
case .extension:
1142+
throw InternalError("unexpectedly asked to generate linker arguments for an extension product")
11411143
}
11421144

11431145
// Set rpath such that dynamic libraries are looked up
@@ -1153,7 +1155,7 @@ public final class ProductBuildDescription {
11531155
// Embed the swift stdlib library path inside tests and executables on Darwin.
11541156
if containsSwiftTargets {
11551157
switch product.type {
1156-
case .library: break
1158+
case .library, .extension: break
11571159
case .test, .executable:
11581160
if buildParameters.triple.isDarwin() {
11591161
let stdlib = buildParameters.toolchain.macosSwiftStdlib
@@ -1424,8 +1426,8 @@ public class BuildPlan {
14241426

14251427
var productMap: [ResolvedProduct: ProductBuildDescription] = [:]
14261428
// Create product description for each product we have in the package graph except
1427-
// for automatic libraries because they don't produce any output.
1428-
for product in graph.allProducts where product.type != .library(.automatic) {
1429+
// for automatic libraries and extension because they don't produce any output.
1430+
for product in graph.allProducts where product.type != .library(.automatic) && product.type != .extension {
14291431
productMap[product] = ProductBuildDescription(
14301432
product: product, buildParameters: buildParameters,
14311433
fs: fileSystem,
@@ -1588,10 +1590,10 @@ public class BuildPlan {
15881590
return target.dependencies.filter { $0.satisfies(self.buildEnvironment) }
15891591

15901592
// For a product dependency, we only include its content only if we
1591-
// need to statically link it.
1593+
// need to statically link it or if it's an extension.
15921594
case .product(let product, _):
15931595
switch product.type {
1594-
case .library(.automatic), .library(.static):
1596+
case .library(.automatic), .library(.static), .extension:
15951597
return product.targets.map { .target($0, conditions: []) }
15961598
case .library(.dynamic), .test, .executable:
15971599
return []

Sources/Build/ManifestBuilder.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,8 @@ extension LLBuildManifestBuilder {
554554
// Establish a dependency on binary of the product.
555555
inputs.append(file: planProduct.binary)
556556

557-
// For automatic and static libraries, add their targets as static input.
558-
case .library(.automatic), .library(.static):
557+
// For automatic and static libraries, and extensions, add their targets as static input.
558+
case .library(.automatic), .library(.static), .extension:
559559
for target in product.targets {
560560
try addStaticTargetInputs(target)
561561
}
@@ -674,7 +674,7 @@ extension LLBuildManifestBuilder {
674674
let binary = planProduct.binary
675675
inputs.append(file: binary)
676676

677-
case .library(.automatic), .library(.static):
677+
case .library(.automatic), .library(.static), .extension:
678678
for target in product.targets {
679679
addStaticTargetInputs(target)
680680
}
@@ -847,6 +847,8 @@ extension ResolvedProduct {
847847
throw InternalError("automatic library not supported")
848848
case .executable:
849849
return "\(name)-\(config).exe"
850+
case .extension:
851+
throw InternalError("unexpectedly asked for the llbuild target name of an extension product")
850852
}
851853
}
852854

Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ extension PackageModel.ProductType {
342342
self = .library(.init(from: libraryType))
343343
case .executable:
344344
self = .executable
345+
case .extension:
346+
self = .extension
345347
case .test:
346348
self = .test
347349
}

Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ extension PackageCollectionModel.V1 {
324324

325325
/// An executable product.
326326
case executable
327+
328+
/// An extension product.
329+
case `extension`
327330

328331
/// A test product.
329332
case test
@@ -332,7 +335,7 @@ extension PackageCollectionModel.V1 {
332335

333336
extension PackageCollectionModel.V1.ProductType: Codable {
334337
private enum CodingKeys: String, CodingKey {
335-
case library, executable, test
338+
case library, executable, `extension`, test
336339
}
337340

338341
public func encode(to encoder: Encoder) throws {
@@ -343,6 +346,8 @@ extension PackageCollectionModel.V1.ProductType: Codable {
343346
try unkeyedContainer.encode(a1)
344347
case .executable:
345348
try container.encodeNil(forKey: .executable)
349+
case .extension:
350+
try container.encodeNil(forKey: .extension)
346351
case .test:
347352
try container.encodeNil(forKey: .test)
348353
}
@@ -362,6 +367,8 @@ extension PackageCollectionModel.V1.ProductType: Codable {
362367
self = .test
363368
case .executable:
364369
self = .executable
370+
case .extension:
371+
self = .extension
365372
}
366373
}
367374
}

Sources/PackageDescription/Product.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2018 Apple Inc. and the Swift project authors
4+
Copyright (c) 2018 - 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -128,6 +128,29 @@ public class Product: Encodable {
128128
}
129129
}
130130

131+
/// The extension product of a Swift package.
132+
public final class Extension: Product {
133+
private enum ExtensionCodingKeys: CodingKey {
134+
case targets
135+
}
136+
137+
/// The name of the extension target to vend as a product.
138+
public let targets: [String]
139+
140+
init(name: String, targets: [String]) {
141+
self.targets = targets
142+
super.init(name: name)
143+
}
144+
145+
public override func encode(to encoder: Encoder) throws {
146+
try super.encode(to: encoder)
147+
var productContainer = encoder.container(keyedBy: ProductCodingKeys.self)
148+
try productContainer.encode("extension", forKey: .type)
149+
var extensionContainer = encoder.container(keyedBy: ExtensionCodingKeys.self)
150+
try extensionContainer.encode(targets, forKey: .targets)
151+
}
152+
}
153+
131154
/// Creates a library product to allow clients that declare a dependency on this package
132155
/// to use the package's functionality.
133156
///
@@ -161,6 +184,19 @@ public class Product: Encodable {
161184
) -> Product {
162185
return Executable(name: name, targets: targets)
163186
}
187+
188+
/// Creates an extension package product.
189+
///
190+
/// - Parameters:
191+
/// - name: The name of the extension product.
192+
/// - targets: The extension targets to vend as a product.
193+
@available(_PackageDescription, introduced: 999.0)
194+
public static func `extension`(
195+
name: String,
196+
targets: [String]
197+
) -> Product {
198+
return Extension(name: name, targets: targets)
199+
}
164200

165201
public func encode(to encoder: Encoder) throws {
166202
var container = encoder.container(keyedBy: ProductCodingKeys.self)

Sources/PackageLoading/Diagnostics.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ extension Diagnostic.Message {
3434
switch product.type {
3535
case .library(.automatic):
3636
typeString = ""
37-
case .executable, .test,
37+
case .executable, .extension, .test,
3838
.library(.dynamic), .library(.static):
3939
typeString = " (\(product.type))"
4040
}
@@ -76,6 +76,14 @@ extension Diagnostic.Message {
7676
.error("executable product '\(product)' should not have more than one executable target")
7777
}
7878

79+
static func extensionProductWithNoTargets(product: String) -> Diagnostic.Message {
80+
.error("extension product '\(product)' should have at least one extension target")
81+
}
82+
83+
static func extensionProductWithNonExtensionTargets(product: String, otherTargets: [String]) -> Diagnostic.Message {
84+
.error("extension product '\(product)' should have only extension targets (it has \(otherTargets.map{ "'\($0)'" }.joined(separator: ", ")))")
85+
}
86+
7987
static var noLibraryTargetsForREPL: Diagnostic.Message {
8088
.error("unable to synthesize a REPL product as there are no library targets in the package")
8189
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,10 @@ public final class PackageBuilder {
11921192
guard self.validateExecutableProduct(product, with: targets) else {
11931193
continue
11941194
}
1195+
case .extension:
1196+
guard self.validateExtensionProduct(product, with: targets) else {
1197+
continue
1198+
}
11951199
}
11961200

11971201
append(Product(name: product.name, type: product.type, targets: targets))
@@ -1205,7 +1209,7 @@ public final class PackageBuilder {
12051209
// for them.
12061210
let explicitProductsTargets = Set(self.manifest.products.flatMap{ product -> [String] in
12071211
switch product.type {
1208-
case .library, .test:
1212+
case .library, .extension, .test:
12091213
return []
12101214
case .executable:
12111215
return product.targets
@@ -1284,6 +1288,25 @@ public final class PackageBuilder {
12841288

12851289
return true
12861290
}
1291+
1292+
private func validateExtensionProduct(_ product: ProductDescription, with targets: [Target]) -> Bool {
1293+
let nonExtensionTargets = targets.filter{ $0.type != .extension }
1294+
guard nonExtensionTargets.isEmpty else {
1295+
diagnostics.emit(
1296+
.extensionProductWithNonExtensionTargets(product: product.name, otherTargets: nonExtensionTargets.map{ $0.name }),
1297+
location: diagnosticLocation()
1298+
)
1299+
return false
1300+
}
1301+
guard !targets.isEmpty else {
1302+
diagnostics.emit(
1303+
.extensionProductWithNoTargets(product: product.name),
1304+
location: diagnosticLocation()
1305+
)
1306+
return false
1307+
}
1308+
return true
1309+
}
12871310
}
12881311

12891312
/// We create this structure after scanning the filesystem for potential targets.

Sources/PackageLoading/PackageDescription4Loader.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ extension PackageModel.ProductType {
269269
}
270270

271271
self = .library(libraryType)
272+
273+
case "extension":
274+
self = .extension
272275

273276
default:
274277
throw InternalError("unexpected product type: \(json)")

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ fileprivate extension SourceCodeFragment {
156156
self.init(enum: "library", subnodes: params, multiline: true)
157157
case .executable:
158158
self.init(enum: "executable", subnodes: params, multiline: true)
159+
case .extension:
160+
self.init(enum: "extension", subnodes: params, multiline: true)
159161
case .test:
160162
self.init(enum: "test", subnodes: params, multiline: true)
161163
}

0 commit comments

Comments
 (0)