Skip to content

Commit a2f5f07

Browse files
Capture build graph changes
1 parent 91c6e5e commit a2f5f07

File tree

2 files changed

+84
-21
lines changed

2 files changed

+84
-21
lines changed

Plugins/PackageToJS/MiniMake.swift

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,48 @@ import Foundation
33
/// A simple build system
44
struct MiniMake {
55
/// Attributes of a task
6-
enum TaskAttribute {
6+
enum TaskAttribute: String, Codable {
77
/// Task is phony, meaning it must be built even if its inputs are up to date
88
case phony
99
/// Don't print anything when building this task
1010
case silent
1111
}
12-
/// A task to build
13-
struct Task {
14-
/// Key of the task
15-
let key: TaskKey
16-
/// Display name of the task
17-
let displayName: String
12+
13+
/// Information about a task enough to capture build
14+
/// graph changes
15+
struct TaskInfo: Codable {
1816
/// Input tasks not yet built
19-
var wants: Set<TaskKey>
17+
let wants: [TaskKey]
2018
/// Set of files that must be built before this task
2119
let inputs: [String]
2220
/// Output task name
2321
let output: String
2422
/// Attributes of the task
23+
let attributes: [TaskAttribute]
24+
}
25+
26+
/// A task to build
27+
struct Task {
28+
let info: TaskInfo
29+
/// Input tasks not yet built
30+
let wants: Set<TaskKey>
31+
/// Attributes of the task
2532
let attributes: Set<TaskAttribute>
33+
/// Display name of the task
34+
let displayName: String
35+
/// Key of the task
36+
let key: TaskKey
2637
/// Build operation
2738
let build: (Task) throws -> Void
2839
/// Whether the task is done
2940
var isDone: Bool
41+
42+
var inputs: [String] { self.info.inputs }
43+
var output: String { self.info.output }
3044
}
3145

3246
/// A task key
33-
struct TaskKey: Hashable, Comparable, CustomStringConvertible {
47+
struct TaskKey: Codable, Hashable, Comparable, CustomStringConvertible {
3448
let id: String
3549
var description: String { self.id }
3650

@@ -41,7 +55,9 @@ struct MiniMake {
4155
static func < (lhs: TaskKey, rhs: TaskKey) -> Bool { lhs.id < rhs.id }
4256
}
4357

58+
/// All tasks in the build system
4459
private var tasks: [TaskKey: Task]
60+
/// Whether to explain why tasks are built
4561
private var shouldExplain: Bool
4662
/// Current working directory at the time the build started
4763
private let buildCwd: String
@@ -52,13 +68,26 @@ struct MiniMake {
5268
self.buildCwd = FileManager.default.currentDirectoryPath
5369
}
5470

55-
mutating func addTask(inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String, attributes: Set<TaskAttribute> = [], build: @escaping (Task) throws -> Void) -> TaskKey {
71+
/// Adds a task to the build system
72+
mutating func addTask(inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String, attributes: [TaskAttribute] = [], build: @escaping (Task) throws -> Void) -> TaskKey {
5673
let displayName = output.hasPrefix(self.buildCwd) ? String(output.dropFirst(self.buildCwd.count + 1)) : output
5774
let taskKey = TaskKey(id: output)
58-
self.tasks[taskKey] = Task(key: taskKey, displayName: displayName, wants: Set(inputTasks), inputs: inputFiles, output: output, attributes: attributes, build: build, isDone: false)
75+
let info = TaskInfo(wants: inputTasks, inputs: inputFiles, output: output, attributes: attributes)
76+
self.tasks[taskKey] = Task(info: info, wants: Set(inputTasks), attributes: Set(attributes), displayName: displayName, key: taskKey, build: build, isDone: false)
5977
return taskKey
6078
}
6179

80+
/// Computes a stable fingerprint of the build graph
81+
///
82+
/// This fingerprint must be stable across builds and must change
83+
/// if the build graph changes in any way.
84+
func computeFingerprint(root: TaskKey) throws -> Data {
85+
let encoder = JSONEncoder()
86+
encoder.outputFormatting = .sortedKeys
87+
let tasks = self.tasks.sorted { $0.key < $1.key }.map { $0.value.info }
88+
return try encoder.encode(tasks)
89+
}
90+
6291
private func explain(_ message: @autoclosure () -> String) {
6392
if self.shouldExplain {
6493
print(message())
@@ -100,7 +129,8 @@ struct MiniMake {
100129
}
101130
}
102131

103-
private func computeTotalTasks(task: Task) -> Int {
132+
/// Computes the total number of tasks to build used for progress display
133+
private func computeTotalTasksForDisplay(task: Task) -> Int {
104134
var visited = Set<TaskKey>()
105135
func visit(task: Task) -> Int {
106136
guard !visited.contains(task.key) else { return 0 }
@@ -114,6 +144,14 @@ struct MiniMake {
114144
return visit(task: task)
115145
}
116146

147+
/// Cleans all outputs of all tasks
148+
func cleanEverything() {
149+
for task in self.tasks.values {
150+
try? FileManager.default.removeItem(atPath: task.output)
151+
}
152+
}
153+
154+
/// Starts building
117155
mutating func build(output: TaskKey) throws {
118156
/// Returns true if any of the task's inputs have a modification date later than the task's output
119157
func shouldBuild(task: Task) -> Bool {
@@ -143,7 +181,7 @@ struct MiniMake {
143181
return shouldBuild
144182
}
145183
}
146-
var progressPrinter = ProgressPrinter(total: self.computeTotalTasks(task: self.tasks[output]!))
184+
var progressPrinter = ProgressPrinter(total: self.computeTotalTasksForDisplay(task: self.tasks[output]!))
147185

148186
func runTask(taskKey: TaskKey) throws {
149187
guard var task = self.tasks[taskKey] else {

Plugins/PackageToJS/PackageToJS.swift

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@preconcurrency import Foundation // For "stderr"
1+
@preconcurrency import Foundation // For "stderr"
22
import PackagePlugin
33

44
@main
@@ -104,6 +104,7 @@ struct PackageToJS: CommandPlugin {
104104
let allTask = constructPackagingPlan(
105105
make: &make, options: options, context: context, wasmProductArtifact: productArtifact,
106106
selfPackage: selfPackage, outputDir: outputDir)
107+
cleanIfBuildGraphChanged(root: allTask, make: make, context: context)
107108
print("Packaging...")
108109
try make.build(output: allTask)
109110
print("Packaging finished")
@@ -145,7 +146,11 @@ struct PackageToJS: CommandPlugin {
145146
) -> MiniMake.TaskKey {
146147
let selfPackageURL = selfPackage.directory
147148
let selfPath = String(#filePath)
148-
let outputDirTask = make.addTask(inputFiles: [selfPath], output: outputDir.string) {
149+
150+
// Prepare output directory
151+
let outputDirTask = make.addTask(
152+
inputFiles: [selfPath], output: outputDir.string, attributes: [.silent]
153+
) {
149154
guard !FileManager.default.fileExists(atPath: $0.output) else { return }
150155
try FileManager.default.createDirectory(
151156
atPath: $0.output, withIntermediateDirectories: true, attributes: nil)
@@ -160,23 +165,21 @@ struct PackageToJS: CommandPlugin {
160165
try FileManager.default.copyItem(atPath: from, toPath: to)
161166
}
162167

168+
// Copy the wasm product artifact
163169
let wasmFilename = "main.wasm"
164170
let wasm = make.addTask(
165171
inputFiles: [selfPath, wasmProductArtifact.path.string], inputTasks: [outputDirTask],
166-
output: outputDir.appending(subpath: wasmFilename).string,
167-
// FIXME: This is a hack to ensure that the wasm file is always copied
168-
// even when release/debug configuration is changed.
169-
attributes: [.phony]
172+
output: outputDir.appending(subpath: wasmFilename).string
170173
) {
171174
try syncFile(from: wasmProductArtifact.path.string, to: $0.output)
172175
}
173176
packageInputs.append(wasm)
174177

178+
// Write package.json
175179
let packageJSON = make.addTask(
176180
inputFiles: [selfPath], inputTasks: [outputDirTask],
177181
output: outputDir.appending(subpath: "package.json").string
178182
) {
179-
// Write package.json
180183
let packageJSON = """
181184
{
182185
"name": "\(options.packageName ?? context.package.id.lowercased())",
@@ -195,6 +198,7 @@ struct PackageToJS: CommandPlugin {
195198
}
196199
packageInputs.append(packageJSON)
197200

201+
// Copy the template files
198202
let substitutions = [
199203
"@PACKAGE_TO_JS_MODULE_PATH@": wasmFilename
200204
]
@@ -216,7 +220,28 @@ struct PackageToJS: CommandPlugin {
216220
}
217221
packageInputs.append(copied)
218222
}
219-
return make.addTask(inputTasks: packageInputs, output: "all", attributes: [.phony, .silent]) { _ in }
223+
return make.addTask(
224+
inputTasks: packageInputs, output: "all", attributes: [.phony, .silent]
225+
) { _ in }
226+
}
227+
228+
/// Clean if the build graph of the packaging process has changed
229+
///
230+
/// This is especially important to detect user changes debug/release
231+
/// configurations, which leads to placing the .wasm file in a different
232+
/// path.
233+
private func cleanIfBuildGraphChanged(
234+
root: MiniMake.TaskKey,
235+
make: MiniMake, context: PluginContext
236+
) {
237+
let buildFingerprint = context.pluginWorkDirectoryURL.appending(path: "minimake.json")
238+
let lastBuildFingerprint = try? Data(contentsOf: buildFingerprint)
239+
let currentBuildFingerprint = try? make.computeFingerprint(root: root)
240+
if lastBuildFingerprint != currentBuildFingerprint {
241+
print("Build graph changed, cleaning...")
242+
make.cleanEverything()
243+
}
244+
try? currentBuildFingerprint?.write(to: buildFingerprint)
220245
}
221246
}
222247

0 commit comments

Comments
 (0)