@@ -4,8 +4,6 @@ import PackagePlugin
4
4
@main
5
5
struct PackageToJS : CommandPlugin {
6
6
struct Options {
7
- /// Product to build (default: executable target if there's only one)
8
- var product : String ?
9
7
/// Path to the output directory
10
8
var outputPath : String ?
11
9
/// Name of the package (default: lowercased Package.swift name)
@@ -14,34 +12,82 @@ struct PackageToJS: CommandPlugin {
14
12
var explain : Bool = false
15
13
16
14
static func parse( from extractor: inout ArgumentExtractor ) -> Options {
17
- let product = extractor. extractOption ( named: " product " ) . last
18
15
let outputPath = extractor. extractOption ( named: " output " ) . last
19
16
let packageName = extractor. extractOption ( named: " package-name " ) . last
20
17
let explain = extractor. extractFlag ( named: " explain " )
21
18
return Options (
22
- product: product, outputPath: outputPath, packageName: packageName,
23
- explain: explain != 0
19
+ outputPath: outputPath, packageName: packageName, explain: explain != 0
24
20
)
25
21
}
22
+ }
23
+
24
+ struct BuildOptions {
25
+ /// Product to build (default: executable target if there's only one)
26
+ var product : String ?
27
+ var options : Options
28
+
29
+ static func parse( from extractor: inout ArgumentExtractor ) -> BuildOptions {
30
+ let product = extractor. extractOption ( named: " product " ) . last
31
+ let options = Options . parse ( from: & extractor)
32
+ return BuildOptions ( product: product, options: options)
33
+ }
26
34
27
35
static func help( ) -> String {
28
36
return """
29
- Usage: swift package --swift-sdk <swift-sdk> [swift-package options] plugin run PackageToJS [options]
37
+ OVERVIEW: Builds a JavaScript module from a Swift package.
30
38
31
- Options:
39
+ USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS [options] [subcommand]
40
+
41
+ OPTIONS:
32
42
--product <product> Product to build (default: executable target if there's only one)
33
43
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
34
44
--package-name <name> Name of the package (default: lowercased Package.swift name)
35
45
--explain Whether to explain the build plan
36
46
37
- Examples:
47
+ SUBCOMMANDS:
48
+ test Builds and runs tests
49
+
50
+ EXAMPLES:
38
51
$ swift package --swift-sdk wasm32-unknown-wasi plugin js
52
+ # Build a specific product
39
53
$ swift package --swift-sdk wasm32-unknown-wasi plugin js --product Example
54
+ # Build in release configuration
40
55
$ swift package --swift-sdk wasm32-unknown-wasi -c release plugin js
56
+
57
+ # Run tests
58
+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test
41
59
"""
42
60
}
43
61
}
44
62
63
+ struct TestOptions {
64
+ /// Whether to only build tests, don't run them
65
+ var buildOnly : Bool = false
66
+ var options : Options
67
+
68
+ static func parse( from extractor: inout ArgumentExtractor ) -> TestOptions {
69
+ let buildOnly = extractor. extractFlag ( named: " build-only " )
70
+ let options = Options . parse ( from: & extractor)
71
+ return TestOptions ( buildOnly: buildOnly != 0 , options: options)
72
+ }
73
+
74
+ static func help( ) -> String {
75
+ return """
76
+ OVERVIEW: Builds and runs tests
77
+
78
+ USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS test [options]
79
+
80
+ OPTIONS:
81
+ --build-only Whether to build only (default: false)
82
+
83
+ EXAMPLES:
84
+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test
85
+ # Just build tests, don't run them
86
+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test --build-only
87
+ """
88
+ }
89
+ }
90
+
45
91
static let friendlyBuildDiagnostics :
46
92
[ @Sendable ( _ build: PackageManager . BuildResult , _ arguments: [ String ] ) -> String ? ] = [
47
93
(
@@ -83,58 +129,129 @@ struct PackageToJS: CommandPlugin {
83
129
"""
84
130
} ) ,
85
131
]
132
+ static private func reportBuildFailure( _ build: PackageManager . BuildResult , _ arguments: [ String ] ) {
133
+ for diagnostic in Self . friendlyBuildDiagnostics {
134
+ if let message = diagnostic ( build, arguments) {
135
+ printStderr ( " \n " + message)
136
+ }
137
+ }
138
+ }
86
139
87
140
func performCommand( context: PluginContext , arguments: [ String ] ) throws {
88
141
if arguments. contains ( where: { [ " -h " , " --help " ] . contains ( $0) } ) {
89
- printStderr ( Options . help ( ) )
142
+ printStderr ( BuildOptions . help ( ) )
90
143
return
91
144
}
92
145
146
+ if arguments. first == " test " {
147
+ return try performTestCommand ( context: context, arguments: Array ( arguments. dropFirst ( ) ) )
148
+ }
149
+
150
+ return try performBuildCommand ( context: context, arguments: arguments)
151
+ }
152
+
153
+ static let JAVASCRIPTKIT_PACKAGE_ID : Package . ID = " javascriptkit "
154
+
155
+ func performBuildCommand( context: PluginContext , arguments: [ String ] ) throws {
93
156
var extractor = ArgumentExtractor ( arguments)
94
- let options = Options . parse ( from: & extractor)
157
+ let buildOptions = BuildOptions . parse ( from: & extractor)
95
158
96
159
if extractor. remainingArguments. count > 0 {
97
160
printStderr (
98
161
" Unexpected arguments: \( extractor. remainingArguments. joined ( separator: " " ) ) " )
99
- printStderr ( Options . help ( ) )
162
+ printStderr ( BuildOptions . help ( ) )
100
163
exit ( 1 )
101
164
}
102
165
103
166
// Build products
104
- let ( productArtifact, build) = try buildWasm ( options: options, context: context)
105
- guard let productArtifact = productArtifact else {
106
- for diagnostic in Self . friendlyBuildDiagnostics {
107
- if let message = diagnostic ( build, arguments) {
108
- printStderr ( " \n " + message)
109
- }
110
- }
167
+ let productName = try buildOptions. product ?? deriveDefaultProduct ( package : context. package )
168
+ let build = try buildWasm ( productName: productName, context: context)
169
+ guard build. succeeded else {
170
+ Self . reportBuildFailure ( build, arguments)
111
171
exit ( 1 )
112
172
}
173
+ let productArtifact = try build. findWasmArtifact ( for: productName)
113
174
let outputDir =
114
- if let outputPath = options. outputPath {
175
+ if let outputPath = buildOptions . options. outputPath {
115
176
URL ( fileURLWithPath: outputPath)
116
177
} else {
117
178
context. pluginWorkDirectoryURL. appending ( path: " Package " )
118
179
}
119
180
guard
120
181
let selfPackage = findPackageInDependencies (
121
- package : context. package , id: " javascriptkit " )
182
+ package : context. package , id: Self . JAVASCRIPTKIT_PACKAGE_ID )
122
183
else {
123
184
throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
124
185
}
125
- var make = MiniMake ( explain: options. explain)
126
- let allTask = constructPackagingPlan (
127
- make: & make, options: options, context: context, wasmProductArtifact: productArtifact,
128
- selfPackage: selfPackage, outputDir: outputDir)
129
- cleanIfBuildGraphChanged ( root: allTask, make: make, context: context)
186
+ var make = MiniMake ( explain: buildOptions. options. explain)
187
+ let planner = PackagingPlanner (
188
+ options: buildOptions. options, context: context, selfPackage: selfPackage, outputDir: outputDir)
189
+ let rootTask = planner. planBuild (
190
+ make: & make, wasmProductArtifact: productArtifact)
191
+ cleanIfBuildGraphChanged ( root: rootTask, make: make, context: context)
130
192
print ( " Packaging... " )
131
- try make. build ( output: allTask )
193
+ try make. build ( output: rootTask )
132
194
print ( " Packaging finished " )
133
195
}
134
196
135
- private func buildWasm( options: Options , context: PluginContext ) throws -> (
136
- productArtifact: URL ? , build: PackageManager . BuildResult
137
- ) {
197
+ func performTestCommand( context: PluginContext , arguments: [ String ] ) throws {
198
+ var extractor = ArgumentExtractor ( arguments)
199
+ let testOptions = TestOptions . parse ( from: & extractor)
200
+
201
+ if extractor. remainingArguments. count > 0 {
202
+ printStderr ( " Unexpected arguments: \( extractor. remainingArguments. joined ( separator: " " ) ) " )
203
+ printStderr ( TestOptions . help ( ) )
204
+ exit ( 1 )
205
+ }
206
+
207
+ let productName = " \( context. package . displayName) PackageTests "
208
+ let build = try buildWasm ( productName: productName, context: context)
209
+ guard build. succeeded else {
210
+ Self . reportBuildFailure ( build, arguments)
211
+ exit ( 1 )
212
+ }
213
+
214
+ // NOTE: Find the product artifact from the default build directory
215
+ // because PackageManager.BuildResult doesn't include the
216
+ // product artifact for tests.
217
+ // This doesn't work when `--scratch-path` is used but
218
+ // we don't have a way to guess the correct path. (we can find
219
+ // the path by building a dummy executable product but it's
220
+ // not worth the overhead)
221
+ var productArtifact : URL ?
222
+ for fileExtension in [ " wasm " , " xctest " ] {
223
+ let path = " .build/debug/ \( productName) . \( fileExtension) "
224
+ if FileManager . default. fileExists ( atPath: path) {
225
+ productArtifact = URL ( fileURLWithPath: path)
226
+ break
227
+ }
228
+ }
229
+ guard let productArtifact = productArtifact else {
230
+ throw PackageToJSError ( " Failed to find ' \( productName) .wasm' or ' \( productName) .xctest' " )
231
+ }
232
+ let outputDir = if let outputPath = testOptions. options. outputPath {
233
+ URL ( fileURLWithPath: outputPath)
234
+ } else {
235
+ context. pluginWorkDirectoryURL. appending ( path: " PackageTests " )
236
+ }
237
+ guard
238
+ let selfPackage = findPackageInDependencies (
239
+ package : context. package , id: Self . JAVASCRIPTKIT_PACKAGE_ID)
240
+ else {
241
+ throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
242
+ }
243
+ var make = MiniMake ( explain: testOptions. options. explain)
244
+ let planner = PackagingPlanner (
245
+ options: testOptions. options, context: context, selfPackage: selfPackage, outputDir: outputDir)
246
+ let rootTask = planner. planTestBuild (
247
+ make: & make, wasmProductArtifact: productArtifact)
248
+ cleanIfBuildGraphChanged ( root: rootTask, make: make, context: context)
249
+ print ( " Packaging tests... " )
250
+ try make. build ( output: rootTask)
251
+ print ( " Packaging tests finished " )
252
+ }
253
+
254
+ private func buildWasm( productName: String , context: PluginContext ) throws -> PackageManager . BuildResult {
138
255
var parameters = PackageManager . BuildParameters (
139
256
configuration: . inherit,
140
257
logging: . concise
@@ -154,118 +271,7 @@ struct PackageToJS: CommandPlugin {
154
271
" --export-if-defined=__main_argc_argv "
155
272
]
156
273
}
157
- let productName = try options. product ?? deriveDefaultProduct ( package : context. package )
158
- let build = try self . packageManager. build ( . product( productName) , parameters: parameters)
159
-
160
- var productArtifact : URL ?
161
- if build. succeeded {
162
- let testProductName = " \( context. package . displayName) PackageTests "
163
- if productName == testProductName {
164
- for fileExtension in [ " wasm " , " xctest " ] {
165
- let path = " .build/debug/ \( testProductName) . \( fileExtension) "
166
- if FileManager . default. fileExists ( atPath: path) {
167
- productArtifact = URL ( fileURLWithPath: path)
168
- break
169
- }
170
- }
171
- } else {
172
- productArtifact = try build. findWasmArtifact ( for: productName)
173
- }
174
- }
175
-
176
- return ( productArtifact, build)
177
- }
178
-
179
- /// Construct the build plan and return the root task key
180
- private func constructPackagingPlan(
181
- make: inout MiniMake ,
182
- options: Options ,
183
- context: PluginContext ,
184
- wasmProductArtifact: URL ,
185
- selfPackage: Package ,
186
- outputDir: URL
187
- ) -> MiniMake . TaskKey {
188
- let selfPackageURL = selfPackage. directoryURL
189
- let selfPath = String ( #filePath)
190
-
191
- // Prepare output directory
192
- let outputDirTask = make. addTask (
193
- inputFiles: [ selfPath] , output: outputDir. path, attributes: [ . silent]
194
- ) {
195
- guard !FileManager. default. fileExists ( atPath: $0. output) else { return }
196
- try FileManager . default. createDirectory (
197
- atPath: $0. output, withIntermediateDirectories: true , attributes: nil )
198
- }
199
-
200
- var packageInputs : [ MiniMake . TaskKey ] = [ ]
201
-
202
- func syncFile( from: String , to: String ) throws {
203
- if FileManager . default. fileExists ( atPath: to) {
204
- try FileManager . default. removeItem ( atPath: to)
205
- }
206
- try FileManager . default. copyItem ( atPath: from, toPath: to)
207
- }
208
-
209
- // Copy the wasm product artifact
210
- let wasmFilename = " main.wasm "
211
- let wasm = make. addTask (
212
- inputFiles: [ selfPath, wasmProductArtifact. path] , inputTasks: [ outputDirTask] ,
213
- output: outputDir. appending ( path: wasmFilename) . path
214
- ) {
215
- try syncFile ( from: wasmProductArtifact. path, to: $0. output)
216
- }
217
- packageInputs. append ( wasm)
218
-
219
- // Write package.json
220
- let packageJSON = make. addTask (
221
- inputFiles: [ selfPath] , inputTasks: [ outputDirTask] ,
222
- output: outputDir. appending ( path: " package.json " ) . path
223
- ) {
224
- let packageJSON = """
225
- {
226
- " name " : " \( options. packageName ?? context. package . id. lowercased ( ) ) " ,
227
- " version " : " 0.0.0 " ,
228
- " type " : " module " ,
229
- " exports " : {
230
- " . " : " ./index.js " ,
231
- " ./wasm " : " ./ \( wasmFilename) "
232
- },
233
- " dependencies " : {
234
- " @bjorn3/browser_wasi_shim " : " ^0.4.1 "
235
- }
236
- }
237
- """
238
- try packageJSON. write ( toFile: $0. output, atomically: true , encoding: . utf8)
239
- }
240
- packageInputs. append ( packageJSON)
241
-
242
- // Copy the template files
243
- let substitutions = [
244
- " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename
245
- ]
246
- for (file, output) in [
247
- ( " Plugins/PackageToJS/Templates/index.js " , " index.js " ) ,
248
- ( " Plugins/PackageToJS/Templates/index.d.ts " , " index.d.ts " ) ,
249
- ( " Plugins/PackageToJS/Templates/instantiate.js " , " instantiate.js " ) ,
250
- ( " Plugins/PackageToJS/Templates/instantiate.d.ts " , " instantiate.d.ts " ) ,
251
- ( " Sources/JavaScriptKit/Runtime/index.mjs " , " runtime.js " ) ,
252
- ] {
253
- let inputPath = selfPackageURL. appending ( path: file)
254
- let copied = make. addTask (
255
- inputFiles: [ selfPath, inputPath. path] , inputTasks: [ outputDirTask] ,
256
- output: outputDir. appending ( path: output) . path
257
- ) {
258
- var content = try String ( contentsOf: inputPath, encoding: . utf8)
259
- for (key, value) in substitutions {
260
- content = content. replacingOccurrences ( of: key, with: value)
261
- }
262
- try content. write ( toFile: $0. output, atomically: true , encoding: . utf8)
263
- }
264
- packageInputs. append ( copied)
265
- }
266
- return make. addTask (
267
- inputTasks: packageInputs, output: " all " , attributes: [ . phony, . silent]
268
- ) { _ in }
274
+ return try self . packageManager. build ( . product( productName) , parameters: parameters)
269
275
}
270
276
271
277
/// Clean if the build graph of the packaging process has changed
0 commit comments