Skip to content

Commit f2b17d8

Browse files
Merge PackagingPlanner into PackageToJS.swift
To save compile time
1 parent 4c00e25 commit f2b17d8

File tree

2 files changed

+243
-244
lines changed

2 files changed

+243
-244
lines changed

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,246 @@ struct PackageToJSError: Swift.Error, CustomStringConvertible {
5454
self.description = "Error: " + message
5555
}
5656
}
57+
58+
/// Plans the build for packaging.
59+
struct PackagingPlanner {
60+
/// The options of the plugin
61+
let options: PackageToJS.Options
62+
/// The package ID of the package that this plugin is running on
63+
let packageId: String
64+
/// The directory of the package that contains this plugin
65+
let selfPackageDir: URL
66+
/// The path of this file itself, used to capture changes of planner code
67+
let selfPath: String
68+
/// The directory for the final output
69+
let outputDir: URL
70+
/// The directory for intermediate files
71+
let intermediatesDir: URL
72+
/// The filename of the .wasm file
73+
let wasmFilename = "main.wasm"
74+
75+
init(
76+
options: PackageToJS.Options,
77+
packageId: String,
78+
pluginWorkDirectoryURL: URL,
79+
selfPackageDir: URL,
80+
outputDir: URL
81+
) {
82+
self.options = options
83+
self.packageId = packageId
84+
self.selfPackageDir = selfPackageDir
85+
self.outputDir = outputDir
86+
self.intermediatesDir = pluginWorkDirectoryURL.appending(path: outputDir.lastPathComponent + ".tmp")
87+
self.selfPath = String(#filePath)
88+
}
89+
90+
// MARK: - Primitive build operations
91+
92+
private static func syncFile(from: String, to: String) throws {
93+
if FileManager.default.fileExists(atPath: to) {
94+
try FileManager.default.removeItem(atPath: to)
95+
}
96+
try FileManager.default.copyItem(atPath: from, toPath: to)
97+
}
98+
99+
private static func createDirectory(atPath: String) throws {
100+
guard !FileManager.default.fileExists(atPath: atPath) else { return }
101+
try FileManager.default.createDirectory(
102+
atPath: atPath, withIntermediateDirectories: true, attributes: nil
103+
)
104+
}
105+
106+
private static func runCommand(_ command: URL, _ arguments: [String]) throws {
107+
let task = Process()
108+
task.executableURL = command
109+
task.arguments = arguments
110+
task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
111+
try task.run()
112+
task.waitUntilExit()
113+
guard task.terminationStatus == 0 else {
114+
throw PackageToJSError("Command failed with status \(task.terminationStatus)")
115+
}
116+
}
117+
118+
// MARK: - Build plans
119+
120+
/// Construct the build plan and return the root task key
121+
func planBuild(
122+
make: inout MiniMake,
123+
splitDebug: Bool,
124+
wasmProductArtifact: URL
125+
) throws -> MiniMake.TaskKey {
126+
let (allTasks, _) = try planBuildInternal(
127+
make: &make, splitDebug: splitDebug, wasmProductArtifact: wasmProductArtifact
128+
)
129+
return make.addTask(
130+
inputTasks: allTasks, output: "all", attributes: [.phony, .silent]
131+
) { _ in }
132+
}
133+
134+
private func planBuildInternal(
135+
make: inout MiniMake,
136+
splitDebug: Bool,
137+
wasmProductArtifact: URL
138+
) throws -> (allTasks: [MiniMake.TaskKey], outputDirTask: MiniMake.TaskKey) {
139+
// Prepare output directory
140+
let outputDirTask = make.addTask(
141+
inputFiles: [selfPath], output: outputDir.path, attributes: [.silent]
142+
) {
143+
try Self.createDirectory(atPath: $0.output)
144+
}
145+
146+
var packageInputs: [MiniMake.TaskKey] = []
147+
148+
// Guess the build configuration from the parent directory name of .wasm file
149+
let buildConfiguration = wasmProductArtifact.deletingLastPathComponent().lastPathComponent
150+
let wasm: MiniMake.TaskKey
151+
152+
let shouldOptimize: Bool
153+
let wasmOptPath = try? which("wasm-opt")
154+
if buildConfiguration == "debug" {
155+
shouldOptimize = false
156+
} else {
157+
if wasmOptPath != nil {
158+
shouldOptimize = true
159+
} else {
160+
print("Warning: wasm-opt not found in PATH, skipping optimizations")
161+
shouldOptimize = false
162+
}
163+
}
164+
165+
if let wasmOptPath = wasmOptPath, shouldOptimize {
166+
// Optimize the wasm in release mode
167+
let intermediatesDirTask = make.addTask(
168+
inputFiles: [selfPath], output: intermediatesDir.path, attributes: [.silent]
169+
) {
170+
try Self.createDirectory(atPath: $0.output)
171+
}
172+
// If splitDebug is true, we need to place the DWARF-stripped wasm file (but "name" section remains)
173+
// in the output directory.
174+
let stripWasmPath = (splitDebug ? outputDir : intermediatesDir).appending(path: wasmFilename + ".debug").path
175+
176+
// First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt
177+
let stripWasm = make.addTask(
178+
inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask, intermediatesDirTask],
179+
output: stripWasmPath
180+
) {
181+
print("Stripping DWARF debug info...")
182+
try Self.runCommand(wasmOptPath, [wasmProductArtifact.path, "--strip-dwarf", "--debuginfo", "-o", $0.output])
183+
}
184+
// Then, run wasm-opt with all optimizations
185+
wasm = make.addTask(
186+
inputFiles: [selfPath], inputTasks: [outputDirTask, stripWasm],
187+
output: outputDir.appending(path: wasmFilename).path
188+
) {
189+
print("Optimizing the wasm file...")
190+
try Self.runCommand(wasmOptPath, [stripWasmPath, "-Os", "-o", $0.output])
191+
}
192+
} else {
193+
// Copy the wasm product artifact
194+
wasm = make.addTask(
195+
inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask],
196+
output: outputDir.appending(path: wasmFilename).path
197+
) {
198+
try Self.syncFile(from: wasmProductArtifact.path, to: $0.output)
199+
}
200+
}
201+
packageInputs.append(wasm)
202+
203+
// Write package.json
204+
let packageJSON = make.addTask(
205+
inputFiles: [selfPath], inputTasks: [outputDirTask],
206+
output: outputDir.appending(path: "package.json").path
207+
) {
208+
let packageJSON = """
209+
{
210+
"name": "\(options.packageName ?? packageId.lowercased())",
211+
"version": "0.0.0",
212+
"type": "module",
213+
"exports": {
214+
".": "./index.js",
215+
"./wasm": "./\(wasmFilename)"
216+
},
217+
"dependencies": {
218+
"@bjorn3/browser_wasi_shim": "^0.4.1"
219+
}
220+
}
221+
"""
222+
try packageJSON.write(toFile: $0.output, atomically: true, encoding: .utf8)
223+
}
224+
packageInputs.append(packageJSON)
225+
226+
// Copy the template files
227+
for (file, output) in [
228+
("Plugins/PackageToJS/Templates/index.js", "index.js"),
229+
("Plugins/PackageToJS/Templates/index.d.ts", "index.d.ts"),
230+
("Plugins/PackageToJS/Templates/instantiate.js", "instantiate.js"),
231+
("Plugins/PackageToJS/Templates/instantiate.d.ts", "instantiate.d.ts"),
232+
("Sources/JavaScriptKit/Runtime/index.mjs", "runtime.js"),
233+
] {
234+
packageInputs.append(planCopyTemplateFile(
235+
make: &make, file: file, output: output, outputDirTask: outputDirTask,
236+
inputs: []
237+
))
238+
}
239+
return (packageInputs, outputDirTask)
240+
}
241+
242+
/// Construct the test build plan and return the root task key
243+
func planTestBuild(
244+
make: inout MiniMake,
245+
wasmProductArtifact: URL
246+
) throws -> (rootTask: MiniMake.TaskKey, binDir: URL) {
247+
var (allTasks, outputDirTask) = try planBuildInternal(
248+
make: &make, splitDebug: false, wasmProductArtifact: wasmProductArtifact
249+
)
250+
251+
let binDir = outputDir.appending(path: "bin")
252+
let binDirTask = make.addTask(
253+
inputFiles: [selfPath], inputTasks: [outputDirTask],
254+
output: binDir.path
255+
) {
256+
try Self.createDirectory(atPath: $0.output)
257+
}
258+
allTasks.append(binDirTask)
259+
260+
// Copy the template files
261+
for (file, output) in [
262+
("Plugins/PackageToJS/Templates/test.js", "test.js"),
263+
("Plugins/PackageToJS/Templates/test.d.ts", "test.d.ts"),
264+
("Plugins/PackageToJS/Templates/bin/test.js", "bin/test.js"),
265+
] {
266+
allTasks.append(planCopyTemplateFile(
267+
make: &make, file: file, output: output, outputDirTask: outputDirTask,
268+
inputs: [binDirTask]
269+
))
270+
}
271+
let rootTask = make.addTask(
272+
inputTasks: allTasks, output: "all", attributes: [.phony, .silent]
273+
) { _ in }
274+
return (rootTask, binDir)
275+
}
276+
277+
private func planCopyTemplateFile(
278+
make: inout MiniMake,
279+
file: String,
280+
output: String,
281+
outputDirTask: MiniMake.TaskKey,
282+
inputs: [MiniMake.TaskKey]
283+
) -> MiniMake.TaskKey {
284+
let inputPath = selfPackageDir.appending(path: file)
285+
let substitutions = [
286+
"@PACKAGE_TO_JS_MODULE_PATH@": wasmFilename
287+
]
288+
return make.addTask(
289+
inputFiles: [selfPath, inputPath.path], inputTasks: [outputDirTask] + inputs,
290+
output: outputDir.appending(path: output).path
291+
) {
292+
var content = try String(contentsOf: inputPath, encoding: .utf8)
293+
for (key, value) in substitutions {
294+
content = content.replacingOccurrences(of: key, with: value)
295+
}
296+
try content.write(toFile: $0.output, atomically: true, encoding: .utf8)
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)