@@ -19,11 +19,17 @@ import PackageModel
19
19
import SwiftParser
20
20
@_spi ( PackageRefactor) import SwiftRefactor
21
21
import SwiftSyntax
22
- import TSCBasic
23
- import TSCUtility
22
+ import SwiftSyntaxBuilder
24
23
import Workspace
25
24
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 ) ]
27
33
28
34
extension SwiftPackageCommand {
29
35
struct AddTarget : AsyncSwiftCommand {
@@ -36,7 +42,8 @@ extension SwiftPackageCommand {
36
42
}
37
43
38
44
package static let configuration = CommandConfiguration (
39
- abstract: " Add a new target to the manifest. " )
45
+ abstract: " Add a new target to the manifest. "
46
+ )
40
47
41
48
@OptionGroup ( visibility: . hidden)
42
49
var globalOptions : GlobalOptions
@@ -62,7 +69,9 @@ extension SwiftPackageCommand {
62
69
@Option ( help: " The checksum for a remote binary target. " )
63
70
var checksum : String ?
64
71
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
+ )
66
75
var testingLibrary : AddPackageTarget . TestHarness = . default
67
76
68
77
func run( _ swiftCommandState: SwiftCommandState ) async throws {
@@ -92,46 +101,55 @@ extension SwiftPackageCommand {
92
101
}
93
102
94
103
// Move sources into their own folder if they're directly in `./Sources`.
95
- try await moveSingleTargetSources (
104
+ try await self . moveSingleTargetSources (
96
105
workspace: workspace,
97
106
packagePath: packagePath,
98
- verbose: !globalOptions. logging. quiet,
107
+ verbose: !self . globalOptions. logging. quiet,
99
108
observabilityScope: swiftCommandState. observabilityScope
100
109
)
101
110
102
111
// Map the target type.
103
112
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
108
117
}
109
118
110
119
// Map dependencies
111
120
let dependencies : [ PackageTarget . Dependency ] = self . dependencies. map {
112
121
. byName( name: $0)
113
122
}
114
123
124
+ let target = PackageTarget (
125
+ name: name,
126
+ type: type,
127
+ dependencies: dependencies,
128
+ path: path,
129
+ url: url,
130
+ checksum: checksum
131
+ )
115
132
let editResult = try AddPackageTarget . manifestRefactor (
116
133
syntax: manifestSyntax,
117
134
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
127
137
)
128
138
)
129
139
130
140
try editResult. applyEdits (
131
141
to: fileSystem,
132
142
manifest: manifestSyntax,
133
143
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
135
153
)
136
154
}
137
155
@@ -140,7 +158,7 @@ extension SwiftPackageCommand {
140
158
// the target before adding a new target.
141
159
private func moveSingleTargetSources(
142
160
workspace: Workspace ,
143
- packagePath: Basics . AbsolutePath ,
161
+ packagePath: AbsolutePath ,
144
162
verbose: Bool = false ,
145
163
observabilityScope: ObservabilityScope
146
164
) async throws {
@@ -185,6 +203,218 @@ extension SwiftPackageCommand {
185
203
}
186
204
}
187
205
}
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
+ }
188
409
}
189
410
}
190
411
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