@@ -19,11 +19,17 @@ import PackageModel
1919import SwiftParser
2020@_spi ( PackageRefactor) import SwiftRefactor
2121import SwiftSyntax
22- import TSCBasic
23- import TSCUtility
22+ import SwiftSyntaxBuilder
2423import Workspace
2524
26- extension AddPackageTarget . TestHarness : @retroactive ExpressibleByArgument { }
25+ import struct TSCBasic. ByteString
26+ import struct TSCBasic. StringError
27+
28+ extension AddPackageTarget . TestHarness : @retroactive ExpressibleByArgument { }
29+
30+ /// The array of auxiliary files that can be added by a package editing
31+ /// operation.
32+ private typealias AuxiliaryFiles = [ ( RelativePath , SourceFileSyntax ) ]
2733
2834extension SwiftPackageCommand {
2935 struct AddTarget : AsyncSwiftCommand {
@@ -36,7 +42,8 @@ extension SwiftPackageCommand {
3642 }
3743
3844 package static let configuration = CommandConfiguration (
39- abstract: " Add a new target to the manifest. " )
45+ abstract: " Add a new target to the manifest. "
46+ )
4047
4148 @OptionGroup ( visibility: . hidden)
4249 var globalOptions : GlobalOptions
@@ -62,7 +69,9 @@ extension SwiftPackageCommand {
6269 @Option ( help: " The checksum for a remote binary target. " )
6370 var checksum : String ?
6471
65- @Option ( help: " The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'. " )
72+ @Option (
73+ help: " The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'. "
74+ )
6675 var testingLibrary : AddPackageTarget . TestHarness = . default
6776
6877 func run( _ swiftCommandState: SwiftCommandState ) async throws {
@@ -92,46 +101,55 @@ extension SwiftPackageCommand {
92101 }
93102
94103 // Move sources into their own folder if they're directly in `./Sources`.
95- try await moveSingleTargetSources (
104+ try await self . moveSingleTargetSources (
96105 workspace: workspace,
97106 packagePath: packagePath,
98- verbose: !globalOptions. logging. quiet,
107+ verbose: !self . globalOptions. logging. quiet,
99108 observabilityScope: swiftCommandState. observabilityScope
100109 )
101110
102111 // Map the target type.
103112 let type : PackageTarget . TargetKind = switch self . type {
104- case . library: . library
105- case . executable: . executable
106- case . test: . test
107- case . macro: . macro
113+ case . library: . library
114+ case . executable: . executable
115+ case . test: . test
116+ case . macro: . macro
108117 }
109118
110119 // Map dependencies
111120 let dependencies : [ PackageTarget . Dependency ] = self . dependencies. map {
112121 . byName( name: $0)
113122 }
114123
124+ let target = PackageTarget (
125+ name: name,
126+ type: type,
127+ dependencies: dependencies,
128+ path: path,
129+ url: url,
130+ checksum: checksum
131+ )
115132 let editResult = try AddPackageTarget . manifestRefactor (
116133 syntax: manifestSyntax,
117134 in: . init(
118- target: . init(
119- name: name,
120- type: type,
121- dependencies: dependencies,
122- path: path,
123- url: url,
124- checksum: checksum
125- ) ,
126- testHarness: testingLibrary
135+ target: target,
136+ testHarness: self . testingLibrary
127137 )
128138 )
129139
130140 try editResult. applyEdits (
131141 to: fileSystem,
132142 manifest: manifestSyntax,
133143 manifestPath: manifestPath,
134- verbose: !globalOptions. logging. quiet
144+ verbose: !self . globalOptions. logging. quiet
145+ )
146+
147+ // Once edits are applied, it's time to create new files for the target.
148+ try self . addAuxiliaryFiles (
149+ target: target,
150+ testHarness: self . testingLibrary,
151+ fileSystem: fileSystem,
152+ rootPath: manifestPath. parentDirectory
135153 )
136154 }
137155
@@ -140,7 +158,7 @@ extension SwiftPackageCommand {
140158 // the target before adding a new target.
141159 private func moveSingleTargetSources(
142160 workspace: Workspace ,
143- packagePath: Basics . AbsolutePath ,
161+ packagePath: AbsolutePath ,
144162 verbose: Bool = false ,
145163 observabilityScope: ObservabilityScope
146164 ) async throws {
@@ -185,6 +203,218 @@ extension SwiftPackageCommand {
185203 }
186204 }
187205 }
206+
207+ private func createAuxiliaryFile(
208+ fileSystem: any FileSystem ,
209+ rootPath: AbsolutePath ,
210+ filePath: RelativePath ,
211+ contents: SourceFileSyntax ,
212+ verbose: Bool = false
213+ ) throws {
214+ // If the file already exists, skip it.
215+ let filePath = rootPath. appending ( filePath)
216+ if fileSystem. exists ( filePath) {
217+ if verbose {
218+ print ( " Skipping \( filePath. relative ( to: rootPath) ) because it already exists. " )
219+ }
220+
221+ return
222+ }
223+
224+ // If the directory does not exist yet, create it.
225+ let fileDir = filePath. parentDirectory
226+ if !fileSystem. exists ( fileDir) {
227+ if verbose {
228+ print ( " Creating directory \( fileDir. relative ( to: rootPath) ) ... " , terminator: " " )
229+ }
230+
231+ try fileSystem. createDirectory ( fileDir, recursive: true )
232+
233+ if verbose {
234+ print ( " done. " )
235+ }
236+ }
237+
238+ // Write the file.
239+ if verbose {
240+ print ( " Writing \( filePath. relative ( to: rootPath) ) ... " , terminator: " " )
241+ }
242+
243+ try fileSystem. writeFileContents (
244+ filePath,
245+ string: contents. description
246+ )
247+
248+ if verbose {
249+ print ( " done. " )
250+ }
251+ }
252+
253+ private func addAuxiliaryFiles(
254+ target: PackageTarget ,
255+ testHarness: AddPackageTarget . TestHarness ,
256+ fileSystem: any FileSystem ,
257+ rootPath: AbsolutePath
258+ ) throws {
259+ let outerDirectory : String ? = switch target. type {
260+ case . binary, . plugin, . system: nil
261+ case . executable, . library, . macro: " Sources "
262+ case . test: " Tests "
263+ }
264+
265+ guard let outerDirectory else {
266+ return
267+ }
268+
269+ let targetDir = try RelativePath ( validating: outerDirectory) . appending ( component: target. name)
270+ let sourceFilePath = targetDir. appending ( component: " \( target. name) .swift " )
271+
272+ // Introduce imports for each of the dependencies that were specified.
273+ var importModuleNames = target. dependencies. map {
274+ switch $0 {
275+ case . byName( let name) ,
276+ . target( let name) ,
277+ . product( let name, package : _) :
278+ name
279+ }
280+ }
281+
282+ // Add appropriate test module dependencies.
283+ if target. type == . test {
284+ switch testHarness {
285+ case . none:
286+ break
287+
288+ case . xctest:
289+ importModuleNames. append ( " XCTest " )
290+
291+ case . swiftTesting:
292+ importModuleNames. append ( " Testing " )
293+ }
294+ }
295+
296+ let importDecls = importModuleNames. lazy. sorted ( ) . map { name in
297+ DeclSyntax ( " import \( raw: name) \n " )
298+ }
299+
300+ let imports = CodeBlockItemListSyntax {
301+ for importDecl in importDecls {
302+ importDecl
303+ }
304+ }
305+
306+ var files : AuxiliaryFiles = [ ]
307+ switch target. type {
308+ case . binary, . plugin, . system:
309+ break
310+
311+ case . macro:
312+ files. addSourceFile (
313+ path: sourceFilePath,
314+ sourceCode: """
315+ \( imports)
316+ struct \( raw: target. sanitizedName) : Macro {
317+ /// TODO: Implement one or more of the protocols that inherit
318+ /// from Macro. The appropriate macro protocol is determined
319+ /// by the " macro " declaration that \( raw: target. sanitizedName) implements.
320+ /// Examples include:
321+ /// @freestanding(expression) macro --> ExpressionMacro
322+ /// @attached(member) macro --> MemberMacro
323+ }
324+ """
325+ )
326+
327+ // Add a file that introduces the main entrypoint and provided macros
328+ // for a macro target.
329+ files. addSourceFile (
330+ path: targetDir. appending ( component: " ProvidedMacros.swift " ) ,
331+ sourceCode: """
332+ import SwiftCompilerPlugin
333+
334+ @main
335+ struct \( raw: target. sanitizedName) Macros: CompilerPlugin {
336+ let providingMacros: [Macro.Type] = [
337+ \( raw: target. sanitizedName) .self,
338+ ]
339+ }
340+ """
341+ )
342+
343+ case . test:
344+ let sourceCode : SourceFileSyntax = switch testHarness {
345+ case . none:
346+ """
347+ \( imports)
348+ // Test code here
349+ """
350+
351+ case . xctest:
352+ """
353+ \( imports)
354+ class \( raw: target. sanitizedName) Tests: XCTestCase {
355+ func test \( raw: target. sanitizedName) () {
356+ XCTAssertEqual(42, 17 + 25)
357+ }
358+ }
359+ """
360+
361+ case . swiftTesting:
362+ """
363+ \( imports)
364+ @Suite
365+ struct \( raw: target. sanitizedName) Tests {
366+ @Test( " \( raw: target. sanitizedName) tests " )
367+ func example() {
368+ #expect(42 == 17 + 25)
369+ }
370+ }
371+ """
372+ }
373+
374+ files. addSourceFile ( path: sourceFilePath, sourceCode: sourceCode)
375+
376+ case . library:
377+ files. addSourceFile (
378+ path: sourceFilePath,
379+ sourceCode: """
380+ \( imports)
381+ """
382+ )
383+
384+ case . executable:
385+ files. addSourceFile (
386+ path: sourceFilePath,
387+ sourceCode: """
388+ \( imports)
389+ @main
390+ struct \( raw: target. sanitizedName) Main {
391+ static func main() {
392+ print( " Hello, world " )
393+ }
394+ }
395+ """
396+ )
397+ }
398+
399+ for (file, sourceCode) in files {
400+ try self . createAuxiliaryFile (
401+ fileSystem: fileSystem,
402+ rootPath: rootPath,
403+ filePath: file,
404+ contents: sourceCode,
405+ verbose: !self . globalOptions. logging. quiet
406+ )
407+ }
408+ }
188409 }
189410}
190411
412+ extension AuxiliaryFiles {
413+ /// Add a source file to the list of auxiliary files.
414+ fileprivate mutating func addSourceFile(
415+ path: RelativePath ,
416+ sourceCode: SourceFileSyntax
417+ ) {
418+ self . append ( ( path, sourceCode) )
419+ }
420+ }
0 commit comments