Skip to content

Commit bbc5538

Browse files
authored
Add support for creating a macro package (#6250)
* Add support for creating a macro package Introduce `swift package init --type macro` to create a new package that defines a macro. It contains: * A macro implementation target * A library that provides the macro declaration for use by clients * A "client" executable that imports the library and uses the macro * A test target for debugging the macro implementation Note that the test target runs into linking issues because the macro implementation target is an executable, not a library. I believe this is a known issue. Tracked by rdar://106468626. * Address review feedback
1 parent 2f3facb commit bbc5538

File tree

2 files changed

+212
-3
lines changed

2 files changed

+212
-3
lines changed

Sources/Commands/PackageTools/Init.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension SwiftPackageTool {
3131
tool - A package with an executable that uses
3232
Swift Argument Parser. Use this template if you
3333
plan to have a rich set of command-line arguments.
34+
macro - A package that vends a macro.
3435
empty - An empty package with a Package.swift manifest.
3536
"""))
3637
var initMode: InitPackage.PackageType = .library

Sources/Workspace/InitPackage.swift

Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public final class InitPackage {
4545
case executable = "executable"
4646
case tool = "tool"
4747
case `extension` = "extension"
48+
case macro = "macro"
4849

4950
public var description: String {
5051
return rawValue
@@ -135,6 +136,16 @@ public final class InitPackage {
135136
136137
import PackageDescription
137138
139+
"""
140+
141+
if packageType == .macro {
142+
stream <<< """
143+
import CompilerPluginSupport
144+
145+
"""
146+
}
147+
148+
stream <<< """
138149
let package = Package(
139150
140151
"""
@@ -144,8 +155,29 @@ public final class InitPackage {
144155
name: "\(pkgname)"
145156
""")
146157

158+
var platforms = options.platforms
159+
160+
// Macros require macOS 10.15, iOS 13, etc.
161+
if packageType == .macro {
162+
func addIfMissing(_ newPlatform: SupportedPlatform) {
163+
if platforms.contains(where: { platform in
164+
platform.platform == newPlatform.platform
165+
}) {
166+
return
167+
}
168+
169+
platforms.append(newPlatform)
170+
}
171+
172+
addIfMissing(.init(platform: .macOS, version: .init("10.15")))
173+
addIfMissing(.init(platform: .iOS, version: .init("13")))
174+
addIfMissing(.init(platform: .tvOS, version: .init("13")))
175+
addIfMissing(.init(platform: .watchOS, version: .init("6")))
176+
addIfMissing(.init(platform: .macCatalyst, version: .init("13")))
177+
}
178+
147179
var platformsParams = [String]()
148-
for supportedPlatform in options.platforms {
180+
for supportedPlatform in platforms {
149181
let version = supportedPlatform.version
150182
let platform = supportedPlatform.platform
151183

@@ -165,7 +197,7 @@ public final class InitPackage {
165197
}
166198

167199
// Package platforms
168-
if !options.platforms.isEmpty {
200+
if !platforms.isEmpty {
169201
pkgParams.append("""
170202
platforms: [\(platformsParams.joined(separator: ", "))]
171203
""")
@@ -181,6 +213,20 @@ public final class InitPackage {
181213
targets: ["\(pkgname)"]),
182214
]
183215
""")
216+
} else if packageType == .macro {
217+
pkgParams.append("""
218+
products: [
219+
// Products define the executables and libraries a package produces, making them visible to other packages.
220+
.library(
221+
name: "\(pkgname)",
222+
targets: ["\(pkgname)"]),
223+
.executable(
224+
name: "\(pkgname)Client",
225+
targets: ["\(pkgname)Client"]
226+
),
227+
]
228+
""")
229+
184230
}
185231

186232
// Package dependencies
@@ -190,6 +236,12 @@ public final class InitPackage {
190236
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
191237
]
192238
""")
239+
} else if packageType == .macro {
240+
pkgParams.append("""
241+
dependencies: [
242+
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"),
243+
]
244+
""")
193245
}
194246

195247
// Package targets
@@ -219,6 +271,32 @@ public final class InitPackage {
219271
path: "Sources"),
220272
]
221273
"""
274+
} else if packageType == .macro {
275+
param += """
276+
// Macro implementation, only built for the host and never part of a client program.
277+
.macro(name: "\(pkgname)Macros",
278+
dependencies: [
279+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
280+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
281+
]
282+
),
283+
284+
// Library that exposes a macro as part of its API, which is used in client programs.
285+
.target(name: "\(pkgname)", dependencies: ["\(pkgname)Macros"]),
286+
287+
// A client of the library, which is able to use the macro in its
288+
// own code.
289+
.executableTarget(name: "\(pkgname)Client", dependencies: ["\(pkgname)"]),
290+
291+
// A test target used to develop the macro implementation.
292+
.testTarget(
293+
name: "\(pkgname)Tests",
294+
dependencies: [
295+
"\(pkgname)Macros",
296+
]
297+
),
298+
]
299+
"""
222300
} else {
223301
param += """
224302
.target(
@@ -239,7 +317,8 @@ public final class InitPackage {
239317
// Create a tools version with current version but with patch set to zero.
240318
// We do this to avoid adding unnecessary constraints to patch versions, if
241319
// the package really needs it, they should add it manually.
242-
let version = InitPackage.newPackageToolsVersion.zeroedPatch
320+
let version = packageType == .macro ? ToolsVersion.vNext
321+
: InitPackage.newPackageToolsVersion.zeroedPatch
243322

244323
// Write the current tools version.
245324
try ToolsVersionSpecificationWriter.rewriteSpecification(
@@ -331,13 +410,33 @@ public final class InitPackage {
331410
}
332411
}
333412
"""
413+
case .macro:
414+
content = """
415+
// The Swift Programming Language
416+
// https://docs.swift.org/swift-book
417+
418+
// A macro that produces both a value and a string containing the
419+
// source code that generated the value. For example,
420+
//
421+
// #stringify(x + y)
422+
//
423+
// produces a tuple `(x + y, "x + y")`.
424+
@freestanding(expression)
425+
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "\(moduleName)Macros", type: "StringifyMacro")
426+
"""
427+
334428
case .empty, .`extension`:
335429
throw InternalError("invalid packageType \(packageType)")
336430
}
337431

338432
try writePackageFile(sourceFile) { stream in
339433
stream.write(content)
340434
}
435+
436+
if packageType == .macro {
437+
try writeMacroPluginSources(sources.appending("\(pkgname)Macros"))
438+
try writeMacroClientSources(sources.appending("\(pkgname)Client"))
439+
}
341440
}
342441

343442
private func writeTests() throws {
@@ -374,6 +473,113 @@ public final class InitPackage {
374473
}
375474
}
376475

476+
private func writeMacroTestsFile(_ path: AbsolutePath) throws {
477+
try writePackageFile(path) { stream in
478+
stream <<< ##"""
479+
import SwiftSyntax
480+
import SwiftSyntaxBuilder
481+
import SwiftSyntaxMacros
482+
import XCTest
483+
import \##(moduleName)Macros
484+
485+
var testMacros: [String: Macro.Type] = [
486+
"stringify" : StringifyMacro.self,
487+
]
488+
489+
final class \##(moduleName)Tests: XCTestCase {
490+
func testMacro() {
491+
// XCTest Documentation
492+
// https://developer.apple.com/documentation/xctest
493+
494+
// Test input is a source file containing uses of the macro.
495+
let sf: SourceFileSyntax =
496+
#"""
497+
let a = #stringify(x + y)
498+
let b = #stringify("Hello, \(name)")
499+
"""#
500+
let context = BasicMacroExpansionContext.init(
501+
sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")]
502+
)
503+
504+
// Expand the macro to produce a new source file with the
505+
// result of the expansion, and ensure that it has the
506+
// expected source code.
507+
let transformedSF = sf.expand(macros: testMacros, in: context)
508+
XCTAssertEqual(
509+
transformedSF.description,
510+
#"""
511+
let a = (x + y, "x + y")
512+
let b = ("Hello, \(name)", #""Hello, \(name)""#)
513+
"""#
514+
)
515+
}
516+
}
517+
518+
"""##
519+
}
520+
}
521+
522+
private func writeMacroPluginSources(_ path: AbsolutePath) throws {
523+
try makeDirectories(path)
524+
525+
try writePackageFile(path.appending("\(moduleName)Macro.swift")) { stream in
526+
stream <<< ##"""
527+
import SwiftCompilerPlugin
528+
import SwiftSyntax
529+
import SwiftSyntaxBuilder
530+
import SwiftSyntaxMacros
531+
532+
/// Implementation of the `stringify` macro, which takes an expression
533+
/// of any type and produces a tuple containing the value of that expression
534+
/// and the source code that produced the value. For example
535+
///
536+
/// #stringify(x + y)
537+
///
538+
/// will expand to
539+
///
540+
/// (x + y, "x + y")
541+
public struct StringifyMacro: ExpressionMacro {
542+
public static func expansion(
543+
of node: some FreestandingMacroExpansionSyntax,
544+
in context: some MacroExpansionContext
545+
) -> ExprSyntax {
546+
guard let argument = node.argumentList.first?.expression else {
547+
fatalError("compiler bug: the macro does not have any arguments")
548+
}
549+
550+
return "(\(argument), \(literal: argument.description))"
551+
}
552+
}
553+
554+
@main
555+
struct \##(moduleName)Plugin: CompilerPlugin {
556+
let providingMacros: [Macro.Type] = [
557+
StringifyMacro.self,
558+
]
559+
}
560+
561+
"""##
562+
}
563+
}
564+
565+
private func writeMacroClientSources(_ path: AbsolutePath) throws {
566+
try makeDirectories(path)
567+
568+
try writePackageFile(path.appending("main.swift")) { stream in
569+
stream <<< ##"""
570+
import \##(moduleName)
571+
572+
let a = 17
573+
let b = 25
574+
575+
let (result, code) = #stringify(a + b)
576+
577+
print("The value \(result) was produced by the code \"\(code)\"")
578+
579+
"""##
580+
}
581+
}
582+
377583
private func writeTestFileStubs(testsPath: AbsolutePath) throws {
378584
let testModule = try AbsolutePath(validating: pkgname + Target.testModuleNameSuffix, relativeTo: testsPath)
379585
progressReporter?("Creating \(testModule.relative(to: destinationPath))/")
@@ -384,6 +590,8 @@ public final class InitPackage {
384590
case .empty, .`extension`, .executable, .tool: break
385591
case .library:
386592
try writeLibraryTestsFile(testClassFile)
593+
case .macro:
594+
try writeMacroTestsFile(testClassFile)
387595
}
388596
}
389597
}

0 commit comments

Comments
 (0)