1- import PackagePlugin
21import 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
113
124@main
135struct PackageToJS : CommandPlugin {
146 struct Options {
7+ /// Product to build (default: executable target if there's only one)
158 var product : String ?
9+ /// Name of the package (default: lowercased Package.swift name)
1610 var packageName : String ?
11+ /// Whether to explain the build plan
1712 var explain : Bool = false
1813
1914 static func parse( from extractor: inout ArgumentExtractor ) -> Options {
@@ -22,61 +17,124 @@ struct PackageToJS: CommandPlugin {
2217 let explain = extractor. extractFlag ( named: " explain " )
2318 return Options ( product: product, packageName: packageName, explain: explain != 0 )
2419 }
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+ }
2531 }
2632
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+
2775 func performCommand( context: PluginContext , arguments: [ String ] ) throws {
76+ if arguments. contains ( where: { [ " -h " , " --help " ] . contains ( $0) } ) {
77+ print ( Options . help ( ) )
78+ return
79+ }
80+
2881 var extractor = ArgumentExtractor ( arguments)
2982 let options = Options . parse ( from: & extractor)
3083
31- let productName = try options. product ?? deriveDefaultProduct ( package : context. package )
3284 // 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+ ) {
33114 var parameters = PackageManager . BuildParameters (
34115 configuration: . inherit,
35116 logging: . concise
36117 )
37118 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
39122 if !buildingForEmbedded {
40123 // NOTE: We only support static linking for now, and the new SwiftDriver
41124 // does not infer `-static-stdlib` for WebAssembly targets intentionally
42125 // 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+ ]
44129 parameters. otherLinkerFlags = [ " --export-if-defined=__main_argc_argv " ]
45130 }
46-
131+ let productName = try options . product ?? deriveDefaultProduct ( package : context . package )
47132 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)
76134 }
77135
78136 /// Construct the build plan and return the root task key
79- private func constructBuild (
137+ private func constructPackagingPlan (
80138 make: inout MiniMake ,
81139 options: Options ,
82140 context: PluginContext ,
@@ -88,7 +146,8 @@ struct PackageToJS: CommandPlugin {
88146 let selfPath = String ( #filePath)
89147 let outputDirTask = make. addTask ( inputFiles: [ selfPath] , output: outputDir. string) {
90148 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 )
92151 }
93152
94153 var packageInputs : [ MiniMake . TaskKey ] = [ ]
@@ -118,25 +177,25 @@ struct PackageToJS: CommandPlugin {
118177 ) {
119178 // Write package.json
120179 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+ }
131191 }
132- }
133- """
192+ """
134193 try packageJSON. write ( toFile: $0. output, atomically: true , encoding: . utf8)
135194 }
136195 packageInputs. append ( packageJSON)
137196
138197 let substitutions = [
139- " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename,
198+ " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename
140199 ]
141200 for (file, output) in [
142201 ( " Plugins/PackageToJS/Templates/index.js " , " index.js " ) ,
@@ -161,68 +220,43 @@ struct PackageToJS: CommandPlugin {
161220}
162221
163222/// 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
164225internal 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+ )
190235
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
204238}
205239
206-
207240extension 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
223259 }
224- return executable
225- }
226260}
227261
228262private func findPackageInDependencies( package : Package , id: Package . ID ) -> Package ? {
@@ -245,3 +279,11 @@ private func findPackageInDependencies(package: Package, id: Package.ID) -> Pack
245279 }
246280 return visit ( package : package )
247281}
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