1
- import PackagePlugin
2
1
import Foundation
3
-
4
- struct PackageToJSError : Swift . Error , CustomStringConvertible {
5
- let description : String
6
-
7
- init ( _ message: String ) {
8
- self . description = " Error: " + message
9
- }
10
- }
2
+ import PackagePlugin
11
3
12
4
@main
13
5
struct PackageToJS : CommandPlugin {
14
6
struct Options {
7
+ /// Product to build (default: executable target if there's only one)
15
8
var product : String ?
9
+ /// Name of the package (default: lowercased Package.swift name)
16
10
var packageName : String ?
11
+ /// Whether to explain the build plan
17
12
var explain : Bool = false
18
13
19
14
static func parse( from extractor: inout ArgumentExtractor ) -> Options {
@@ -22,61 +17,124 @@ struct PackageToJS: CommandPlugin {
22
17
let explain = extractor. extractFlag ( named: " explain " )
23
18
return Options ( product: product, packageName: packageName, explain: explain != 0 )
24
19
}
20
+
21
+ static func help( ) -> String {
22
+ return """
23
+ Usage: swift package --swift-sdk <swift-sdk> plugin run PackageToJS [options]
24
+
25
+ Options:
26
+ --product <product> Product to build (default: executable target if there's only one)
27
+ --package-name <name> Name of the package (default: lowercased Package.swift name)
28
+ --explain Whether to explain the build plan
29
+ """
30
+ }
25
31
}
26
32
33
+ static let friendlyBuildDiagnostics :
34
+ [ ( _ build: PackageManager . BuildResult , _ arguments: [ String ] ) -> String ? ] = [
35
+ (
36
+ // In case user misses the `--swift-sdk` option
37
+ { build, arguments in
38
+ guard
39
+ build. logText. contains (
40
+ " ld.gold: --export-if-defined=__main_argc_argv: unknown option " )
41
+ else { return nil }
42
+ let didYouMean =
43
+ [
44
+ " swift " , " package " , " --swift-sdk " , " wasm32-unknown-wasi " , " js " ,
45
+ ] + arguments
46
+ return """
47
+ Please pass the `--swift-sdk` option to the " swift package " command.
48
+
49
+ Did you mean:
50
+ \( didYouMean. joined ( separator: " " ) )
51
+ """
52
+ } ) ,
53
+ (
54
+ // In case selected Swift SDK version is not compatible with the Swift compiler version
55
+ { build, arguments in
56
+ let regex =
57
+ #/module compiled with Swift (?<swiftSDKVersion>\d+\.\d+(?:\.\d+)?) cannot be imported by the Swift (?<compilerVersion>\d+\.\d+(?:\.\d+)?) compiler/#
58
+ guard let match = build. logText. firstMatch ( of: regex) else { return nil }
59
+ let swiftSDKVersion = match. swiftSDKVersion
60
+ let compilerVersion = match. compilerVersion
61
+ return """
62
+ Swift versions mismatch:
63
+ - Swift SDK version: \( swiftSDKVersion)
64
+ - Swift compiler version: \( compilerVersion)
65
+
66
+ Please ensure you are using matching versions of the Swift SDK and Swift compiler.
67
+
68
+ 1. Use 'swift --version' to check your Swift compiler version
69
+ 2. Use 'swift sdk list' to check available Swift SDKs
70
+ 3. Select a matching SDK version with --swift-sdk option
71
+ """
72
+ } ) ,
73
+ ]
74
+
27
75
func performCommand( context: PluginContext , arguments: [ String ] ) throws {
76
+ if arguments. contains ( where: { [ " -h " , " --help " ] . contains ( $0) } ) {
77
+ print ( Options . help ( ) )
78
+ return
79
+ }
80
+
28
81
var extractor = ArgumentExtractor ( arguments)
29
82
let options = Options . parse ( from: & extractor)
30
83
31
- let productName = try options. product ?? deriveDefaultProduct ( package : context. package )
32
84
// Build products
85
+ let ( build, productName) = try buildWasm ( options: options, context: context)
86
+ guard build. succeeded else {
87
+ for diagnostic in Self . friendlyBuildDiagnostics {
88
+ if let message = diagnostic ( build, arguments) {
89
+ fputs ( " \n " + message + " \n " , stderr)
90
+ }
91
+ }
92
+ exit ( 1 )
93
+ }
94
+
95
+ let productArtifact = try build. findWasmArtifact ( for: productName)
96
+ let outputDir = context. pluginWorkDirectory. appending ( subpath: " Package " )
97
+ guard
98
+ let selfPackage = findPackageInDependencies (
99
+ package : context. package , id: " javascriptkit " )
100
+ else {
101
+ throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
102
+ }
103
+ var make = MiniMake ( explain: options. explain)
104
+ let allTask = constructPackagingPlan (
105
+ make: & make, options: options, context: context, wasmProductArtifact: productArtifact,
106
+ selfPackage: selfPackage, outputDir: outputDir)
107
+ try make. build ( output: allTask)
108
+ print ( " Packaging finished " )
109
+ }
110
+
111
+ private func buildWasm( options: Options , context: PluginContext ) throws -> (
112
+ build: PackageManager . BuildResult , productName: String
113
+ ) {
33
114
var parameters = PackageManager . BuildParameters (
34
115
configuration: . inherit,
35
116
logging: . concise
36
117
)
37
118
parameters. echoLogs = true
38
- let buildingForEmbedded = ProcessInfo . processInfo. environment [ " JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM " ] . flatMap ( Bool . init) ?? false
119
+ let buildingForEmbedded =
120
+ ProcessInfo . processInfo. environment [ " JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM " ] . flatMap (
121
+ Bool . init) ?? false
39
122
if !buildingForEmbedded {
40
123
// NOTE: We only support static linking for now, and the new SwiftDriver
41
124
// does not infer `-static-stdlib` for WebAssembly targets intentionally
42
125
// for future dynamic linking support.
43
- parameters. otherSwiftcFlags = [ " -static-stdlib " , " -Xclang-linker " , " -mexec-model=reactor " ]
126
+ parameters. otherSwiftcFlags = [
127
+ " -static-stdlib " , " -Xclang-linker " , " -mexec-model=reactor " ,
128
+ ]
44
129
parameters. otherLinkerFlags = [ " --export-if-defined=__main_argc_argv " ]
45
130
}
46
-
131
+ let productName = try options . product ?? deriveDefaultProduct ( package : context . package )
47
132
let build = try self . packageManager. build ( . product( productName) , parameters: parameters)
48
-
49
- guard build. succeeded else {
50
- print ( build. logText)
51
- exit ( 1 )
52
- }
53
-
54
- guard let product = try context. package . products ( named: [ productName] ) . first else {
55
- throw PackageToJSError ( " Failed to find product named \" \( productName) \" " )
56
- }
57
- guard let executableProduct = product as? ExecutableProduct else {
58
- throw PackageToJSError ( " Product type of \" \( productName) \" is not supported. Only executable products are supported. " )
59
- }
60
-
61
- let productArtifact = try build. findWasmArtifact ( for: productName)
62
- let resourcesPaths = deriveResourcesPaths (
63
- productArtifactPath: productArtifact. path,
64
- sourceTargets: executableProduct. targets,
65
- package : context. package
66
- )
67
-
68
- let outputDir = context. pluginWorkDirectory. appending ( subpath: " Package " )
69
- guard let selfPackage = findPackageInDependencies ( package : context. package , id: " javascriptkit " ) else {
70
- throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
71
- }
72
- var make = MiniMake ( explain: options. explain)
73
- let allTask = constructBuild ( make: & make, options: options, context: context, wasmProductArtifact: productArtifact, selfPackage: selfPackage, outputDir: outputDir)
74
- try make. build ( output: allTask)
75
- print ( " Build finished " )
133
+ return ( build, productName)
76
134
}
77
135
78
136
/// Construct the build plan and return the root task key
79
- private func constructBuild (
137
+ private func constructPackagingPlan (
80
138
make: inout MiniMake ,
81
139
options: Options ,
82
140
context: PluginContext ,
@@ -88,7 +146,8 @@ struct PackageToJS: CommandPlugin {
88
146
let selfPath = String ( #filePath)
89
147
let outputDirTask = make. addTask ( inputFiles: [ selfPath] , output: outputDir. string) {
90
148
guard !FileManager. default. fileExists ( atPath: $0. output) else { return }
91
- try FileManager . default. createDirectory ( atPath: $0. output, withIntermediateDirectories: true , attributes: nil )
149
+ try FileManager . default. createDirectory (
150
+ atPath: $0. output, withIntermediateDirectories: true , attributes: nil )
92
151
}
93
152
94
153
var packageInputs : [ MiniMake . TaskKey ] = [ ]
@@ -118,25 +177,25 @@ struct PackageToJS: CommandPlugin {
118
177
) {
119
178
// Write package.json
120
179
let packageJSON = """
121
- {
122
- " name " : " \( options. packageName ?? context. package . id. lowercased ( ) ) " ,
123
- " version " : " 0.0.0 " ,
124
- " type " : " module " ,
125
- " exports " : {
126
- " . " : " ./index.js " ,
127
- " ./wasm " : " ./ \( wasmFilename) "
128
- },
129
- " dependencies " : {
130
- " @bjorn3/browser_wasi_shim " : " ^0.4.1 "
180
+ {
181
+ " name " : " \( options. packageName ?? context. package . id. lowercased ( ) ) " ,
182
+ " version " : " 0.0.0 " ,
183
+ " type " : " module " ,
184
+ " exports " : {
185
+ " . " : " ./index.js " ,
186
+ " ./wasm " : " ./ \( wasmFilename) "
187
+ },
188
+ " dependencies " : {
189
+ " @bjorn3/browser_wasi_shim " : " ^0.4.1 "
190
+ }
131
191
}
132
- }
133
- """
192
+ """
134
193
try packageJSON. write ( toFile: $0. output, atomically: true , encoding: . utf8)
135
194
}
136
195
packageInputs. append ( packageJSON)
137
196
138
197
let substitutions = [
139
- " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename,
198
+ " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename
140
199
]
141
200
for (file, output) in [
142
201
( " Plugins/PackageToJS/Templates/index.js " , " index.js " ) ,
@@ -161,68 +220,43 @@ struct PackageToJS: CommandPlugin {
161
220
}
162
221
163
222
/// Derive default product from the package
223
+ /// - Returns: The name of the product to build
224
+ /// - Throws: `PackageToJSError` if there's no executable product or if there's more than one
164
225
internal func deriveDefaultProduct( package : Package ) throws -> String {
165
- let executableProducts = package . products ( ofType: ExecutableProduct . self)
166
- guard !executableProducts. isEmpty else {
167
- throw PackageToJSError (
168
- " Make sure there's at least one executable product in your Package.swift " )
169
- }
170
- guard executableProducts. count == 1 else {
171
- throw PackageToJSError (
172
- " Failed to disambiguate the product. Pass one of \( executableProducts. map ( \. name) . joined ( separator: " , " ) ) to the --product option "
173
- )
174
-
175
- }
176
- return executableProducts [ 0 ] . name
177
- }
178
-
179
- /// Returns the list of resource bundle paths for the given targets
180
- internal func deriveResourcesPaths(
181
- productArtifactPath: Path ,
182
- sourceTargets: [ any PackagePlugin . Target ] ,
183
- package : Package
184
- ) -> [ Path ] {
185
- return deriveResourcesPaths (
186
- buildDirectory: productArtifactPath. removingLastComponent ( ) ,
187
- sourceTargets: sourceTargets, package : package
188
- )
189
- }
226
+ let executableProducts = package . products ( ofType: ExecutableProduct . self)
227
+ guard !executableProducts. isEmpty else {
228
+ throw PackageToJSError (
229
+ " Make sure there's at least one executable product in your Package.swift " )
230
+ }
231
+ guard executableProducts. count == 1 else {
232
+ throw PackageToJSError (
233
+ " Failed to disambiguate the product. Pass one of \( executableProducts. map ( \. name) . joined ( separator: " , " ) ) to the --product option "
234
+ )
190
235
191
- internal func deriveResourcesPaths(
192
- buildDirectory: Path ,
193
- sourceTargets: [ any PackagePlugin . Target ] ,
194
- package : Package
195
- ) -> [ Path ] {
196
- sourceTargets. compactMap { target -> Path ? in
197
- // NOTE: The resource bundle file name is constructed from `displayName` instead of `id` for some reason
198
- // https://github.com/apple/swift-package-manager/blob/swift-5.9.2-RELEASE/Sources/PackageLoading/PackageBuilder.swift#L908
199
- let bundleName = package . displayName + " _ " + target. name + " .resources "
200
- let resourcesPath = buildDirectory. appending ( subpath: bundleName)
201
- guard FileManager . default. fileExists ( atPath: resourcesPath. string) else { return nil }
202
- return resourcesPath
203
- }
236
+ }
237
+ return executableProducts [ 0 ] . name
204
238
}
205
239
206
-
207
240
extension PackageManager . BuildResult {
208
- /// Find `.wasm` executable artifact
209
- internal func findWasmArtifact( for product: String ) throws
210
- -> PackageManager . BuildResult . BuiltArtifact
211
- {
212
- let executables = self . builtArtifacts. filter {
213
- $0. kind == . executable && $0. path. lastComponent == " \( product) .wasm "
214
- }
215
- guard !executables. isEmpty else {
216
- throw PackageToJSError (
217
- " Failed to find ' \( product) .wasm' from executable artifacts of product ' \( product) ' " )
218
- }
219
- guard executables. count == 1 , let executable = executables. first else {
220
- throw PackageToJSError (
221
- " Failed to disambiguate executable product artifacts from \( executables. map ( \. path. string) . joined ( separator: " , " ) ) "
222
- )
241
+ /// Find `.wasm` executable artifact
242
+ internal func findWasmArtifact( for product: String ) throws
243
+ -> PackageManager . BuildResult . BuiltArtifact
244
+ {
245
+ let executables = self . builtArtifacts. filter {
246
+ $0. kind == . executable && $0. path. lastComponent == " \( product) .wasm "
247
+ }
248
+ guard !executables. isEmpty else {
249
+ throw PackageToJSError (
250
+ " Failed to find ' \( product) .wasm' from executable artifacts of product ' \( product) ' "
251
+ )
252
+ }
253
+ guard executables. count == 1 , let executable = executables. first else {
254
+ throw PackageToJSError (
255
+ " Failed to disambiguate executable product artifacts from \( executables. map ( \. path. string) . joined ( separator: " , " ) ) "
256
+ )
257
+ }
258
+ return executable
223
259
}
224
- return executable
225
- }
226
260
}
227
261
228
262
private func findPackageInDependencies( package : Package , id: Package . ID ) -> Package ? {
@@ -245,3 +279,11 @@ private func findPackageInDependencies(package: Package, id: Package.ID) -> Pack
245
279
}
246
280
return visit ( package : package )
247
281
}
282
+
283
+ private struct PackageToJSError : Swift . Error , CustomStringConvertible {
284
+ let description : String
285
+
286
+ init ( _ message: String ) {
287
+ self . description = " Error: " + message
288
+ }
289
+ }
0 commit comments