@@ -14,13 +14,13 @@ struct MiniMake {
14
14
15
15
/// Information about a task enough to capture build
16
16
/// graph changes
17
- struct TaskInfo : Codable {
17
+ struct TaskInfo : Encodable {
18
18
/// Input tasks not yet built
19
19
let wants : [ TaskKey ]
20
- /// Set of files that must be built before this task
21
- let inputs : [ String ]
22
- /// Output task name
23
- let output : String
20
+ /// Set of file paths that must be built before this task
21
+ let inputs : [ BuildPath ]
22
+ /// Output file path
23
+ let output : BuildPath
24
24
/// Attributes of the task
25
25
let attributes : [ TaskAttribute ]
26
26
/// Salt for the task, used to differentiate between otherwise identical tasks
@@ -30,41 +30,69 @@ struct MiniMake {
30
30
/// A task to build
31
31
struct Task {
32
32
let info : TaskInfo
33
- /// Input tasks not yet built
33
+ /// Input tasks (files and phony tasks) not yet built
34
34
let wants : Set < TaskKey >
35
35
/// Attributes of the task
36
36
let attributes : Set < TaskAttribute >
37
- /// Display name of the task
38
- let displayName : String
39
37
/// Key of the task
40
38
let key : TaskKey
41
39
/// Build operation
42
- let build : ( Task ) throws -> Void
40
+ let build : ( _ task : Task , _ scope : VariableScope ) throws -> Void
43
41
/// Whether the task is done
44
42
var isDone : Bool
45
43
46
- var inputs : [ String ] { self . info. inputs }
47
- var output : String { self . info. output }
44
+ var inputs : [ BuildPath ] { self . info. inputs }
45
+ var output : BuildPath { self . info. output }
48
46
}
49
47
50
48
/// A task key
51
- struct TaskKey : Codable , Hashable , Comparable , CustomStringConvertible {
49
+ struct TaskKey : Encodable , Hashable , Comparable , CustomStringConvertible {
52
50
let id : String
53
51
var description : String { self . id }
54
52
55
53
fileprivate init ( id: String ) {
56
54
self . id = id
57
55
}
58
56
57
+ func encode( to encoder: any Encoder ) throws {
58
+ var container = encoder. singleValueContainer ( )
59
+ try container. encode ( self . id)
60
+ }
61
+
59
62
static func < ( lhs: TaskKey , rhs: TaskKey ) -> Bool { lhs. id < rhs. id }
60
63
}
61
64
65
+ struct VariableScope {
66
+ let variables : [ String : String ]
67
+
68
+ func resolve( path: BuildPath ) -> URL {
69
+ var components = [ String] ( )
70
+ for component in path. components {
71
+ switch component {
72
+ case . prefix( let variable) :
73
+ guard let value = variables [ variable] else {
74
+ fatalError ( " Build path variable \" \( variable) \" not defined! " )
75
+ }
76
+ components. append ( value)
77
+ case . constant( let path) :
78
+ components. append ( path)
79
+ }
80
+ }
81
+ guard let first = components. first else {
82
+ fatalError ( " Build path is empty " )
83
+ }
84
+ var url = URL ( fileURLWithPath: first)
85
+ for component in components. dropFirst ( ) {
86
+ url = url. appending ( path: component)
87
+ }
88
+ return url
89
+ }
90
+ }
91
+
62
92
/// All tasks in the build system
63
93
private var tasks : [ TaskKey : Task ]
64
94
/// Whether to explain why tasks are built
65
95
private var shouldExplain : Bool
66
- /// Current working directory at the time the build started
67
- private let buildCwd : String
68
96
/// Prints progress of the build
69
97
private var printProgress : ProgressPrinter . PrintProgress
70
98
@@ -74,20 +102,16 @@ struct MiniMake {
74
102
) {
75
103
self . tasks = [ : ]
76
104
self . shouldExplain = explain
77
- self . buildCwd = FileManager . default. currentDirectoryPath
78
105
self . printProgress = printProgress
79
106
}
80
107
81
108
/// Adds a task to the build system
82
109
mutating func addTask(
83
- inputFiles: [ String ] = [ ] , inputTasks: [ TaskKey ] = [ ] , output: String ,
110
+ inputFiles: [ BuildPath ] = [ ] , inputTasks: [ TaskKey ] = [ ] , output: BuildPath ,
84
111
attributes: [ TaskAttribute ] = [ ] , salt: ( any Encodable ) ? = nil ,
85
- build: @escaping ( Task ) throws -> Void
112
+ build: @escaping ( _ task : Task , _ scope : VariableScope ) throws -> Void
86
113
) -> TaskKey {
87
- let displayName =
88
- output. hasPrefix ( self . buildCwd)
89
- ? String ( output. dropFirst ( self . buildCwd. count + 1 ) ) : output
90
- let taskKey = TaskKey ( id: output)
114
+ let taskKey = TaskKey ( id: output. description)
91
115
let saltData = try ! salt. map {
92
116
let encoder = JSONEncoder ( )
93
117
encoder. outputFormatting = . sortedKeys
@@ -99,17 +123,20 @@ struct MiniMake {
99
123
)
100
124
self . tasks [ taskKey] = Task (
101
125
info: info, wants: Set ( inputTasks) , attributes: Set ( attributes) ,
102
- displayName : displayName , key: taskKey, build: build, isDone: false )
126
+ key: taskKey, build: build, isDone: false )
103
127
return taskKey
104
128
}
105
129
106
130
/// Computes a stable fingerprint of the build graph
107
131
///
108
132
/// This fingerprint must be stable across builds and must change
109
133
/// if the build graph changes in any way.
110
- func computeFingerprint( root: TaskKey ) throws -> Data {
134
+ func computeFingerprint( root: TaskKey , prettyPrint : Bool = false ) throws -> Data {
111
135
let encoder = JSONEncoder ( )
112
136
encoder. outputFormatting = . sortedKeys
137
+ if prettyPrint {
138
+ encoder. outputFormatting. insert ( . prettyPrinted)
139
+ }
113
140
let tasks = self . tasks. sorted { $0. key < $1. key } . map { $0. value. info }
114
141
return try encoder. encode ( tasks)
115
142
}
@@ -126,7 +153,13 @@ struct MiniMake {
126
153
127
154
/// Prints progress of the build
128
155
struct ProgressPrinter {
129
- typealias PrintProgress = ( _ subject: Task , _ total: Int , _ built: Int , _ message: String ) -> Void
156
+ struct Context {
157
+ let subject : Task
158
+ let total : Int
159
+ let built : Int
160
+ let scope : VariableScope
161
+ }
162
+ typealias PrintProgress = ( _ context: Context , _ message: String ) -> Void
130
163
131
164
/// Total number of tasks to build
132
165
let total : Int
@@ -145,17 +178,17 @@ struct MiniMake {
145
178
private static var yellow : String { " \u{001B} [33m " }
146
179
private static var reset : String { " \u{001B} [0m " }
147
180
148
- mutating func started( _ task: Task ) {
149
- self . print ( task, " \( Self . green) building \( Self . reset) " )
181
+ mutating func started( _ task: Task , scope : VariableScope ) {
182
+ self . print ( task, scope , " \( Self . green) building \( Self . reset) " )
150
183
}
151
184
152
- mutating func skipped( _ task: Task ) {
153
- self . print ( task, " \( Self . yellow) skipped \( Self . reset) " )
185
+ mutating func skipped( _ task: Task , scope : VariableScope ) {
186
+ self . print ( task, scope , " \( Self . yellow) skipped \( Self . reset) " )
154
187
}
155
188
156
- private mutating func print( _ task: Task , _ message: @autoclosure ( ) -> String ) {
189
+ private mutating func print( _ task: Task , _ scope : VariableScope , _ message: @autoclosure ( ) -> String ) {
157
190
guard !task. attributes. contains ( . silent) else { return }
158
- self . printProgress ( task, self . total, self . built, message ( ) )
191
+ self . printProgress ( Context ( subject : task, total : self . total, built : self . built, scope : scope ) , message ( ) )
159
192
self . built += 1
160
193
}
161
194
}
@@ -176,32 +209,32 @@ struct MiniMake {
176
209
}
177
210
178
211
/// Cleans all outputs of all tasks
179
- func cleanEverything( ) {
212
+ func cleanEverything( scope : VariableScope ) {
180
213
for task in self . tasks. values {
181
- try ? FileManager . default. removeItem ( atPath : task. output)
214
+ try ? FileManager . default. removeItem ( at : scope . resolve ( path : task. output) )
182
215
}
183
216
}
184
217
185
218
/// Starts building
186
- func build( output: TaskKey ) throws {
219
+ func build( output: TaskKey , scope : VariableScope ) throws {
187
220
/// Returns true if any of the task's inputs have a modification date later than the task's output
188
221
func shouldBuild( task: Task ) -> Bool {
189
222
if task. attributes. contains ( . phony) {
190
223
return true
191
224
}
192
- let outputURL = URL ( fileURLWithPath : task. output)
193
- if !FileManager. default. fileExists ( atPath: task . output ) {
225
+ let outputURL = scope . resolve ( path : task. output)
226
+ if !FileManager. default. fileExists ( atPath: outputURL . path ) {
194
227
explain ( " Task \( task. output) should be built because it doesn't exist " )
195
228
return true
196
229
}
197
230
let outputMtime = try ? outputURL. resourceValues ( forKeys: [ . contentModificationDateKey] )
198
231
. contentModificationDate
199
232
return task. inputs. contains { input in
200
- let inputURL = URL ( fileURLWithPath : input)
233
+ let inputURL = scope . resolve ( path : input)
201
234
// Ignore directory modification times
202
235
var isDirectory : ObjCBool = false
203
236
let fileExists = FileManager . default. fileExists (
204
- atPath: input , isDirectory: & isDirectory)
237
+ atPath: inputURL . path , isDirectory: & isDirectory)
205
238
if fileExists && isDirectory. boolValue {
206
239
return false
207
240
}
@@ -238,14 +271,56 @@ struct MiniMake {
238
271
}
239
272
240
273
if shouldBuild ( task: task) {
241
- progressPrinter. started ( task)
242
- try task. build ( task)
274
+ progressPrinter. started ( task, scope : scope )
275
+ try task. build ( task, scope )
243
276
} else {
244
- progressPrinter. skipped ( task)
277
+ progressPrinter. skipped ( task, scope : scope )
245
278
}
246
279
task. isDone = true
247
280
tasks [ taskKey] = task
248
281
}
249
282
try runTask ( taskKey: output)
250
283
}
251
284
}
285
+
286
+ struct BuildPath : Encodable , Hashable , CustomStringConvertible {
287
+ enum Component : Hashable , CustomStringConvertible {
288
+ case prefix( variable: String )
289
+ case constant( String )
290
+
291
+ var description : String {
292
+ switch self {
293
+ case . prefix( let variable) : return " $ \( variable) "
294
+ case . constant( let path) : return path
295
+ }
296
+ }
297
+ }
298
+ fileprivate let components : [ Component ]
299
+
300
+ var description : String { self . components. map ( \. description) . joined ( separator: " / " ) }
301
+
302
+ init ( phony: String ) {
303
+ self . components = [ . constant( phony) ]
304
+ }
305
+
306
+ init ( prefix: String , _ tail: String ... ) {
307
+ self . components = [ . prefix( variable: prefix) ] + tail. map ( Component . constant)
308
+ }
309
+
310
+ init ( absolute: String ) {
311
+ self . components = [ . constant( absolute) ]
312
+ }
313
+
314
+ private init ( components: [ Component ] ) {
315
+ self . components = components
316
+ }
317
+
318
+ func appending( path: String ) -> BuildPath {
319
+ return BuildPath ( components: self . components + [ . constant( path) ] )
320
+ }
321
+
322
+ func encode( to encoder: any Encoder ) throws {
323
+ var container = encoder. singleValueContainer ( )
324
+ try container. encode ( self . description)
325
+ }
326
+ }
0 commit comments