@@ -14,13 +14,13 @@ struct MiniMake {
1414
1515 /// Information about a task enough to capture build
1616 /// graph changes
17- struct TaskInfo : Codable {
17+ struct TaskInfo : Encodable {
1818 /// Input tasks not yet built
1919 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
2424 /// Attributes of the task
2525 let attributes : [ TaskAttribute ]
2626 /// Salt for the task, used to differentiate between otherwise identical tasks
@@ -30,41 +30,69 @@ struct MiniMake {
3030 /// A task to build
3131 struct Task {
3232 let info : TaskInfo
33- /// Input tasks not yet built
33+ /// Input tasks (files and phony tasks) not yet built
3434 let wants : Set < TaskKey >
3535 /// Attributes of the task
3636 let attributes : Set < TaskAttribute >
37- /// Display name of the task
38- let displayName : String
3937 /// Key of the task
4038 let key : TaskKey
4139 /// Build operation
42- let build : ( Task ) throws -> Void
40+ let build : ( _ task : Task , _ scope : VariableScope ) throws -> Void
4341 /// Whether the task is done
4442 var isDone : Bool
4543
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 }
4846 }
4947
5048 /// A task key
51- struct TaskKey : Codable , Hashable , Comparable , CustomStringConvertible {
49+ struct TaskKey : Encodable , Hashable , Comparable , CustomStringConvertible {
5250 let id : String
5351 var description : String { self . id }
5452
5553 fileprivate init ( id: String ) {
5654 self . id = id
5755 }
5856
57+ func encode( to encoder: any Encoder ) throws {
58+ var container = encoder. singleValueContainer ( )
59+ try container. encode ( self . id)
60+ }
61+
5962 static func < ( lhs: TaskKey , rhs: TaskKey ) -> Bool { lhs. id < rhs. id }
6063 }
6164
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+
6292 /// All tasks in the build system
6393 private var tasks : [ TaskKey : Task ]
6494 /// Whether to explain why tasks are built
6595 private var shouldExplain : Bool
66- /// Current working directory at the time the build started
67- private let buildCwd : String
6896 /// Prints progress of the build
6997 private var printProgress : ProgressPrinter . PrintProgress
7098
@@ -74,20 +102,16 @@ struct MiniMake {
74102 ) {
75103 self . tasks = [ : ]
76104 self . shouldExplain = explain
77- self . buildCwd = FileManager . default. currentDirectoryPath
78105 self . printProgress = printProgress
79106 }
80107
81108 /// Adds a task to the build system
82109 mutating func addTask(
83- inputFiles: [ String ] = [ ] , inputTasks: [ TaskKey ] = [ ] , output: String ,
110+ inputFiles: [ BuildPath ] = [ ] , inputTasks: [ TaskKey ] = [ ] , output: BuildPath ,
84111 attributes: [ TaskAttribute ] = [ ] , salt: ( any Encodable ) ? = nil ,
85- build: @escaping ( Task ) throws -> Void
112+ build: @escaping ( _ task : Task , _ scope : VariableScope ) throws -> Void
86113 ) -> 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)
91115 let saltData = try ! salt. map {
92116 let encoder = JSONEncoder ( )
93117 encoder. outputFormatting = . sortedKeys
@@ -99,17 +123,20 @@ struct MiniMake {
99123 )
100124 self . tasks [ taskKey] = Task (
101125 info: info, wants: Set ( inputTasks) , attributes: Set ( attributes) ,
102- displayName : displayName , key: taskKey, build: build, isDone: false )
126+ key: taskKey, build: build, isDone: false )
103127 return taskKey
104128 }
105129
106130 /// Computes a stable fingerprint of the build graph
107131 ///
108132 /// This fingerprint must be stable across builds and must change
109133 /// 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 {
111135 let encoder = JSONEncoder ( )
112136 encoder. outputFormatting = . sortedKeys
137+ if prettyPrint {
138+ encoder. outputFormatting. insert ( . prettyPrinted)
139+ }
113140 let tasks = self . tasks. sorted { $0. key < $1. key } . map { $0. value. info }
114141 return try encoder. encode ( tasks)
115142 }
@@ -126,7 +153,13 @@ struct MiniMake {
126153
127154 /// Prints progress of the build
128155 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
130163
131164 /// Total number of tasks to build
132165 let total : Int
@@ -145,17 +178,17 @@ struct MiniMake {
145178 private static var yellow : String { " \u{001B} [33m " }
146179 private static var reset : String { " \u{001B} [0m " }
147180
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) " )
150183 }
151184
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) " )
154187 }
155188
156- private mutating func print( _ task: Task , _ message: @autoclosure ( ) -> String ) {
189+ private mutating func print( _ task: Task , _ scope : VariableScope , _ message: @autoclosure ( ) -> String ) {
157190 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 ( ) )
159192 self . built += 1
160193 }
161194 }
@@ -176,32 +209,32 @@ struct MiniMake {
176209 }
177210
178211 /// Cleans all outputs of all tasks
179- func cleanEverything( ) {
212+ func cleanEverything( scope : VariableScope ) {
180213 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) )
182215 }
183216 }
184217
185218 /// Starts building
186- func build( output: TaskKey ) throws {
219+ func build( output: TaskKey , scope : VariableScope ) throws {
187220 /// Returns true if any of the task's inputs have a modification date later than the task's output
188221 func shouldBuild( task: Task ) -> Bool {
189222 if task. attributes. contains ( . phony) {
190223 return true
191224 }
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 ) {
194227 explain ( " Task \( task. output) should be built because it doesn't exist " )
195228 return true
196229 }
197230 let outputMtime = try ? outputURL. resourceValues ( forKeys: [ . contentModificationDateKey] )
198231 . contentModificationDate
199232 return task. inputs. contains { input in
200- let inputURL = URL ( fileURLWithPath : input)
233+ let inputURL = scope . resolve ( path : input)
201234 // Ignore directory modification times
202235 var isDirectory : ObjCBool = false
203236 let fileExists = FileManager . default. fileExists (
204- atPath: input , isDirectory: & isDirectory)
237+ atPath: inputURL . path , isDirectory: & isDirectory)
205238 if fileExists && isDirectory. boolValue {
206239 return false
207240 }
@@ -238,14 +271,56 @@ struct MiniMake {
238271 }
239272
240273 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 )
243276 } else {
244- progressPrinter. skipped ( task)
277+ progressPrinter. skipped ( task, scope : scope )
245278 }
246279 task. isDone = true
247280 tasks [ taskKey] = task
248281 }
249282 try runTask ( taskKey: output)
250283 }
251284}
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