Skip to content

Commit 61e93a9

Browse files
Add initial packager plugin
This is very much a work in progress. It's just a proof of concept at this point and just works for very simple examples. The plugin invocation is as follows: ``` swift package --swift-sdk wasm32-unknown-wasi js ```
1 parent a732a0c commit 61e93a9

File tree

5 files changed

+547
-0
lines changed

5 files changed

+547
-0
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let package = Package(
1212
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
1313
.library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]),
1414
.library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]),
15+
.plugin(name: "PackageToJS", targets: ["PackageToJS"]),
1516
],
1617
targets: [
1718
.target(
@@ -71,5 +72,11 @@ let package = Package(
7172
"JavaScriptEventLoopTestSupport"
7273
]
7374
),
75+
.plugin(
76+
name: "PackageToJS",
77+
capability: .command(
78+
intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package")
79+
)
80+
),
7481
]
7582
)

Plugins/PackageToJS/MiniMake.swift

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import Foundation
2+
3+
/// A simple build system
4+
struct MiniMake {
5+
/// Attributes of a task
6+
enum TaskAttribute {
7+
/// Task is phony, meaning it must be built even if its inputs are up to date
8+
case phony
9+
}
10+
/// A task to build
11+
struct Task {
12+
/// Key of the task
13+
let key: TaskKey
14+
/// Display name of the task
15+
let displayName: String
16+
/// Input tasks not yet built
17+
var wants: Set<TaskKey>
18+
/// Set of files that must be built before this task
19+
let inputs: [String]
20+
/// Output task name
21+
let output: String
22+
/// Attributes of the task
23+
let attributes: Set<TaskAttribute>
24+
/// Build operation
25+
let build: (Task) throws -> Void
26+
/// Whether the task is done
27+
var isDone: Bool
28+
}
29+
30+
/// A task key
31+
struct TaskKey: Hashable, Comparable, CustomStringConvertible {
32+
let id: String
33+
var description: String { self.id }
34+
35+
fileprivate init(id: String) {
36+
self.id = id
37+
}
38+
39+
static func < (lhs: TaskKey, rhs: TaskKey) -> Bool { lhs.id < rhs.id }
40+
}
41+
42+
private var tasks: [TaskKey: Task]
43+
private var shouldExplain: Bool
44+
/// Current working directory at the time the build started
45+
private let buildCwd: String
46+
47+
init(explain: Bool = false) {
48+
self.tasks = [:]
49+
self.shouldExplain = explain
50+
self.buildCwd = FileManager.default.currentDirectoryPath
51+
}
52+
53+
mutating func addTask(inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String, attributes: Set<TaskAttribute> = [], build: @escaping (Task) throws -> Void) -> TaskKey {
54+
let displayName = output.hasPrefix(self.buildCwd) ? String(output.dropFirst(self.buildCwd.count + 1)) : output
55+
let taskKey = TaskKey(id: output)
56+
self.tasks[taskKey] = Task(key: taskKey, displayName: displayName, wants: Set(inputTasks), inputs: inputFiles, output: output, attributes: attributes, build: build, isDone: false)
57+
return taskKey
58+
}
59+
60+
private func explain(_ message: @autoclosure () -> String) {
61+
if self.shouldExplain {
62+
print(message())
63+
}
64+
}
65+
66+
private func violated(_ message: @autoclosure () -> String) {
67+
print(message())
68+
}
69+
70+
/// Prints progress of the build
71+
struct ProgressPrinter {
72+
/// Total number of tasks to build
73+
let total: Int
74+
/// Number of tasks built so far
75+
var built: Int
76+
77+
init(total: Int) {
78+
self.total = total
79+
self.built = 0
80+
}
81+
82+
private static var green: String { "\u{001B}[32m" }
83+
private static var yellow: String { "\u{001B}[33m" }
84+
private static var reset: String { "\u{001B}[0m" }
85+
86+
mutating func started(_ task: Task) {
87+
self.print(task.displayName, "\(Self.green)building\(Self.reset)")
88+
}
89+
90+
mutating func skipped(_ task: Task) {
91+
self.print(task.displayName, "\(Self.yellow)skipped\(Self.reset)")
92+
}
93+
94+
private mutating func print(_ subjectPath: String, _ message: @autoclosure () -> String) {
95+
Swift.print("[\(self.built + 1)/\(self.total)] \(subjectPath): \(message())")
96+
self.built += 1
97+
}
98+
}
99+
100+
private func computeTotalTasks(task: Task) -> Int {
101+
var visited = Set<TaskKey>()
102+
func visit(task: Task) -> Int {
103+
guard !visited.contains(task.key) else { return 0 }
104+
visited.insert(task.key)
105+
var total = 1
106+
for want in task.wants {
107+
total += visit(task: self.tasks[want]!)
108+
}
109+
return total
110+
}
111+
return visit(task: task)
112+
}
113+
114+
mutating func build(output: TaskKey) throws {
115+
/// Returns true if any of the task's inputs have a modification date later than the task's output
116+
func shouldBuild(task: Task) -> Bool {
117+
if task.attributes.contains(.phony) {
118+
return true
119+
}
120+
let outputURL = URL(fileURLWithPath: task.output)
121+
if !FileManager.default.fileExists(atPath: task.output) {
122+
explain("Task \(task.output) should be built because it doesn't exist")
123+
return true
124+
}
125+
let outputMtime = try? outputURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
126+
return task.inputs.contains { input in
127+
let inputURL = URL(fileURLWithPath: input)
128+
// Ignore directory modification times
129+
var isDirectory: ObjCBool = false
130+
let fileExists = FileManager.default.fileExists(atPath: input, isDirectory: &isDirectory)
131+
if fileExists && isDirectory.boolValue {
132+
return false
133+
}
134+
135+
let inputMtime = try? inputURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
136+
let shouldBuild = outputMtime == nil || inputMtime == nil || outputMtime! < inputMtime!
137+
if shouldBuild {
138+
explain("Task \(task.output) should be re-built because \(input) is newer: \(outputMtime?.timeIntervalSince1970 ?? 0) < \(inputMtime?.timeIntervalSince1970 ?? 0)")
139+
}
140+
return shouldBuild
141+
}
142+
}
143+
var progressPrinter = ProgressPrinter(total: self.computeTotalTasks(task: self.tasks[output]!))
144+
145+
func runTask(taskKey: TaskKey) throws {
146+
guard var task = self.tasks[taskKey] else {
147+
violated("Task \(taskKey) not found")
148+
return
149+
}
150+
guard !task.isDone else { return }
151+
152+
// Build dependencies first
153+
for want in task.wants {
154+
try runTask(taskKey: want)
155+
}
156+
157+
if shouldBuild(task: task) {
158+
progressPrinter.started(task)
159+
try task.build(task)
160+
} else {
161+
progressPrinter.skipped(task)
162+
}
163+
task.isDone = true
164+
self.tasks[taskKey] = task
165+
}
166+
try runTask(taskKey: output)
167+
}
168+
}

0 commit comments

Comments
 (0)