-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBuild.fs
More file actions
431 lines (354 loc) · 18.2 KB
/
Copy pathBuild.fs
File metadata and controls
431 lines (354 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
module Build
open System
open System.Collections.Generic
open Collections
open Serilog
open Terrabuild.PubSub
open Environment
open Errors
[<RequireQualifiedAccess>]
type TaskRequest =
| Restore
| Build
[<RequireQualifiedAccess>]
type TaskStatus =
| Success of completionDate:DateTime
| Failure of completionDate:DateTime * message:string
| Pending
[<RequireQualifiedAccess>]
type NodeInfo = {
Request: TaskRequest
Status: TaskStatus
Project: string
Target: string
ProjectHash: string
TargetHash: string
}
[<RequireQualifiedAccess>]
type Summary = {
Commit: string
BranchOrTag: string
StartedAt: DateTime
EndedAt: DateTime
IsSuccess: bool
Targets: string set
Nodes: Map<string, NodeInfo>
}
type IBuildNotification =
abstract WaitCompletion: unit -> unit
abstract BuildStarted: graph:GraphDef.Graph -> unit
abstract BuildCompleted: summary:Summary -> unit
abstract NodeScheduled: node:GraphDef.Node -> unit
abstract NodeDownloading: node:GraphDef.Node -> unit
abstract NodeBuilding: node:GraphDef.Node -> unit
abstract NodeUploading: node:GraphDef.Node -> unit
abstract NodeCompleted: node:GraphDef.Node -> request:TaskRequest -> success:bool -> unit
let private containerInfos = Concurrent.ConcurrentDictionary<string, string>()
let buildCommands (node: GraphDef.Node) (options: ConfigOptions.Options) projectDirectory homeDir tmpDir =
node.Operations |> List.map (fun operation ->
let metaCommand = operation.MetaCommand
match options.ContainerTool, operation.Container with
| Some cmd, Some container ->
let wsDir = currentDir()
// add platform
let container =
match operation.ContainerPlatform with
| Some platform -> $"--platform={platform} {container}"
| _ -> container
let containerHome =
match containerInfos.TryGetValue(container) with
| true, containerHome ->
Log.Debug("Reusing USER {containerHome} for {container}", containerHome, container)
containerHome
| _ ->
// discover USER
let args = $"run --rm --name {node.TargetHash} --entrypoint sh {container} \"echo -n \\$HOME\""
let containerHome =
Log.Debug("Identifying USER for {container}", container)
match Exec.execCaptureOutput options.Workspace cmd args with
| Exec.Success (containerHome, 0) -> containerHome.Trim()
| _ ->
Log.Debug("USER identification failed for {container}: using root", container)
"/root"
Log.Debug("Using USER {containerHome} for {container}", containerHome, container)
containerInfos.TryAdd(container, containerHome) |> ignore
containerHome
let envs =
operation.ContainerVariables
|> Seq.map (fun var -> $"-e {var}")
|> String.join " "
let args = $"run --rm --net=host --name {node.TargetHash} --pid=host --ipc=host -v /var/run/docker.sock:/var/run/docker.sock -v {homeDir}:{containerHome} -v {tmpDir}:/tmp -v {wsDir}:/terrabuild -w /terrabuild/{projectDirectory} --entrypoint {operation.Command} {envs} {container} {operation.Arguments}"
metaCommand, options.Workspace, cmd, args, operation.Container
| _ -> metaCommand, projectDirectory, operation.Command, operation.Arguments, operation.Container)
let execCommands (node: GraphDef.Node) (cacheEntry: Cache.IEntry) (options: ConfigOptions.Options) projectDirectory homeDir tmpDir =
let stepLogs = List<Cache.OperationSummary>()
let mutable lastStatusCode = 0
let mutable cmdLineIndex = 0
let cmdFirstStartedAt = DateTime.UtcNow
let mutable cmdLastEndedAt = cmdFirstStartedAt
let allCommands = buildCommands node options projectDirectory homeDir tmpDir
while cmdLineIndex < allCommands.Length && lastStatusCode = 0 do
let startedAt =
if cmdLineIndex > 0 then DateTime.UtcNow
else cmdFirstStartedAt
let metaCommand, workDir, cmd, args, container = allCommands[cmdLineIndex]
cmdLineIndex <- cmdLineIndex + 1
Log.Debug("{Hash}: Running '{Command}' with '{Arguments}'", node.TargetHash, cmd, args)
let logFile = cacheEntry.NextLogFile()
let exitCode =
if options.Targets |> Set.contains "serve" then
Exec.execConsole workDir cmd args
else
Exec.execCaptureTimestampedOutput workDir cmd args logFile
cmdLastEndedAt <- DateTime.UtcNow
let endedAt = cmdLastEndedAt
let duration = endedAt - startedAt
let stepLog =
{ Cache.OperationSummary.MetaCommand = metaCommand
Cache.OperationSummary.Command = cmd
Cache.OperationSummary.Arguments = args
Cache.OperationSummary.Container = container
Cache.OperationSummary.StartedAt = startedAt
Cache.OperationSummary.EndedAt = endedAt
Cache.OperationSummary.Duration = duration
Cache.OperationSummary.Log = logFile
Cache.OperationSummary.ExitCode = exitCode }
stepLog |> stepLogs.Add
lastStatusCode <- exitCode
Log.Debug("{Hash}: Execution completed with exit code '{Code}' ({Status})", node.TargetHash, exitCode, lastStatusCode)
lastStatusCode, stepLogs
type Restorable(action: unit -> unit, dependencies: Restorable list) =
let restore = lazy(
dependencies |> List.iter (fun restorable -> restorable.Restore())
action()
)
member _.Restore() = restore.Force()
let run (options: ConfigOptions.Options) (cache: Cache.ICache) (api: Contracts.IApiClient option) (notification: IBuildNotification) (graph: GraphDef.Graph) =
let startedAt = DateTime.UtcNow
$"{Ansi.Emojis.rocket} Processing tasks" |> Terminal.writeLine
notification.BuildStarted graph
api |> Option.iter (fun api -> api.StartBuild())
let allowRemoteCache = options.LocalOnly |> not
let homeDir = Cache.createHome()
let tmpDir = Cache.createTmp()
let retry = options.Retry
let nodeResults = Concurrent.ConcurrentDictionary<string, TaskRequest * TaskStatus>()
let restorables = Concurrent.ConcurrentDictionary<string, Restorable>()
let processNode (maxCompletionChildren: DateTime) (node: GraphDef.Node) =
let cacheEntryId = GraphDef.buildCacheKey node
let projectDirectory =
match node.ProjectDir with
| FS.Directory projectDirectory -> projectDirectory
| FS.File projectFile -> projectFile |> FS.parentDirectory |> Option.get
| _ -> "."
let buildNode() =
let startedAt = DateTime.UtcNow
notification.NodeBuilding node
// restore lazy dependencies
node.Dependencies |> Seq.iter (fun nodeId ->
match restorables.TryGetValue nodeId with
| true, restorable -> restorable.Restore()
| _ -> ())
let beforeFiles =
if node.IsLeaf then IO.Snapshot.Empty
else IO.createSnapshot node.Outputs projectDirectory
let cacheEntry = cache.GetEntry true cacheEntryId
let lastStatusCode, stepLogs = execCommands node cacheEntry options projectDirectory homeDir tmpDir
// keep only new or modified files
let afterFiles = IO.createSnapshot node.Outputs projectDirectory
let newFiles = afterFiles - beforeFiles
let outputs = IO.copyFiles cacheEntry.Outputs projectDirectory newFiles
let successful = lastStatusCode = 0
let endedAt = DateTime.UtcNow
let summary =
{ Cache.TargetSummary.Project = node.ProjectDir
Cache.TargetSummary.Target = node.Target
Cache.TargetSummary.Operations = [ stepLogs |> List.ofSeq ]
Cache.TargetSummary.Outputs = outputs
Cache.TargetSummary.IsSuccessful = successful
Cache.TargetSummary.StartedAt = startedAt
Cache.TargetSummary.EndedAt = endedAt
Cache.TargetSummary.Duration = endedAt - startedAt
Cache.TargetSummary.Cache = node.Cache }
notification.NodeUploading node
// create an archive with new files
Log.Debug("{NodeId}: Building '{Project}/{Target}' with {Hash}", node.Id, node.ProjectDir, node.Target, node.TargetHash)
let files = cacheEntry.Complete summary
api |> Option.iter (fun api -> api.AddArtifact node.ProjectDir node.Target node.ProjectHash node.TargetHash files successful)
match lastStatusCode with
| 0 -> TaskStatus.Success endedAt
| _ -> TaskStatus.Failure (DateTime.UtcNow, $"{node.Id} failed with exit code {lastStatusCode}")
let restoreNode () =
notification.NodeScheduled node
let cacheEntryId = GraphDef.buildCacheKey node
match cache.TryGetSummaryOnly allowRemoteCache cacheEntryId with
| Some (_, summary) ->
let dependencies =
node.Dependencies |> Seq.choose (fun nodeId ->
match restorables.TryGetValue nodeId with
| true, restorable -> Some restorable
| _ -> None)
|> List.ofSeq
let callback() =
notification.NodeDownloading node
match cache.TryGetSummary allowRemoteCache cacheEntryId with
| Some summary ->
Log.Debug("{NodeId} restoring '{Project}/{Target}' from {Hash}", node.Id, node.ProjectDir, node.Target, node.TargetHash)
match summary.Outputs with
| Some outputs ->
let files = IO.enumerateFiles outputs
IO.copyFiles projectDirectory outputs files |> ignore
api |> Option.iter (fun api -> api.UseArtifact node.ProjectHash node.TargetHash)
| _ -> ()
notification.NodeCompleted node TaskRequest.Restore true
| _ ->
notification.NodeCompleted node TaskRequest.Restore false
raiseBugError $"Unable to download build output for {cacheEntryId} for node {node.Id}"
if node.Managed then
let restorable = Restorable(callback, dependencies)
restorables.TryAdd(node.Id, restorable) |> ignore
else
Log.Debug("{NodeId} skipping restore '{Project}/{Target}' from {Hash}", node.Id, node.ProjectDir, node.Target, node.TargetHash)
if summary.IsSuccessful then TaskStatus.Success summary.EndedAt
else TaskStatus.Failure (summary.EndedAt, $"Restored node {node.Id} with a build in failure state")
| _ ->
TaskStatus.Failure (DateTime.UtcNow, $"Unable to download build output for {cacheEntryId} for node {node.Id}")
if node.Rebuild then
Log.Debug("{NodeId} must rebuild because force requested", node.Id)
TaskRequest.Build, buildNode()
elif node.Cache <> Terrabuild.Extensibility.Cacheability.Never then
let cacheEntryId = GraphDef.buildCacheKey node
match cache.TryGetSummaryOnly allowRemoteCache cacheEntryId with
| Some (_, summary) ->
Log.Debug("{NodeId} has existing build summary", node.Id)
// task is failed and retry requested
if retry && not summary.IsSuccessful then
Log.Debug("{NodeId} must rebuild because node is failed and retry requested", node.Id)
TaskRequest.Build, buildNode()
// task is older than children
elif summary.EndedAt <= maxCompletionChildren then
Log.Debug("{NodeId} must rebuild because child is rebuilding", node.Id)
TaskRequest.Build, buildNode()
// task is cached
else
Log.Debug("{NodeId} is marked as used", node.Id)
TaskRequest.Restore, restoreNode()
| _ ->
Log.Debug("{NodeId} must be build since no summary and required", node.Id)
TaskRequest.Build, buildNode()
else
Log.Debug("{NodeId} is not cacheable", node.Id)
TaskRequest.Build, buildNode()
let hub = Hub.Create(options.MaxConcurrency)
let rec schedule nodeId =
if nodeResults.TryAdd(nodeId, (TaskRequest.Build, TaskStatus.Pending)) then
let node = graph.Nodes[nodeId]
let nodeComputed = hub.GetSignal<DateTime> nodeId
// await dependencies
let awaitedDependencies =
node.Dependencies
|> Seq.map (fun awaitedProjectId ->
schedule awaitedProjectId
hub.GetSignal<DateTime> awaitedProjectId)
|> List.ofSeq
let onAllSignaled () =
try
let maxCompletionChildren =
match awaitedDependencies with
| [ ] -> DateTime.MinValue
| _ -> awaitedDependencies |> Seq.maxBy (fun dep -> dep.Value) |> (fun dep -> dep.Value)
let buildRequest, completionStatus = processNode maxCompletionChildren node
Log.Debug("{NodeId} completed request {Request} with status {Status}", node.Id, buildRequest, completionStatus)
let success, completionDate =
match completionStatus with
| TaskStatus.Success completionDate -> true, completionDate
| TaskStatus.Failure (completionDate, _) -> false, completionDate
| _ -> raiseBugError "Unexpected pending state"
nodeResults[node.Id] <- (buildRequest, completionStatus)
notification.NodeCompleted node buildRequest success
if success then nodeComputed.Value <- completionDate
with
exn ->
Log.Fatal(exn, "{NodeId} unexpected failure while building", node.Id)
nodeResults[node.Id] <- (TaskRequest.Build, TaskStatus.Failure (DateTime.UtcNow, exn.Message))
notification.NodeCompleted node TaskRequest.Build false
reraise()
let awaitedSignals = awaitedDependencies |> List.map (fun entry -> entry :> ISignal)
hub.Subscribe nodeId awaitedSignals onAllSignaled
graph.RootNodes |> Seq.iter schedule
let status = hub.WaitCompletion()
match status with
| Status.Ok ->
Log.Debug("Build successful")
| Status.UnfulfilledSubscription (subscription, signals) ->
let unraisedSignals = signals |> String.join ","
Log.Fatal($"Task '{subscription}' has pending operations on '{unraisedSignals}'")
| Status.SubscriptionError exn ->
Log.Fatal(exn, "Build failed with exception")
let headCommit = options.HeadCommit
let branchOrTag = options.BranchOrTag
let nodeStatus =
let getDependencyStatus _ (node: GraphDef.Node) =
match nodeResults.TryGetValue node.Id with
| true, (request, status) ->
{ NodeInfo.Request = request
NodeInfo.Status = status
NodeInfo.Project = node.ProjectDir
NodeInfo.Target = node.Target
NodeInfo.ProjectHash = node.ProjectHash
NodeInfo.TargetHash = node.TargetHash } |> Some
| _ -> None
graph.Nodes
|> Map.choose getDependencyStatus
let isSuccess =
graph.RootNodes |> Set.forall (fun nodeId ->
match nodeStatus |> Map.tryFind nodeId with
| Some info -> info.Status.IsSuccess
| _ -> false)
let buildInfo =
{ Summary.Commit = headCommit.Sha
Summary.BranchOrTag = branchOrTag
Summary.StartedAt = startedAt
Summary.EndedAt = DateTime.UtcNow
Summary.IsSuccess = isSuccess
Summary.Targets = options.Targets
Summary.Nodes = nodeStatus }
notification.BuildCompleted buildInfo
api |> Option.iter (fun api -> api.CompleteBuild buildInfo.IsSuccess)
buildInfo
let loadSummary (options: ConfigOptions.Options) (cache: Cache.ICache) (graph: GraphDef.Graph) =
let startedAt = DateTime.UtcNow
let allowRemoteCache = options.LocalOnly |> not
let nodeStatus =
let getDependencyStatus _ (node: GraphDef.Node) =
let cacheEntryId = GraphDef.buildCacheKey node
match cache.TryGetSummary allowRemoteCache cacheEntryId with
| Some summary ->
let status =
if summary.IsSuccessful then TaskStatus.Success summary.EndedAt
else TaskStatus.Failure (summary.EndedAt, "logs")
{ NodeInfo.Request = TaskRequest.Restore
NodeInfo.Status = status
NodeInfo.Project = node.ProjectDir
NodeInfo.Target = node.Target
NodeInfo.ProjectHash = node.ProjectHash
NodeInfo.TargetHash = node.TargetHash } |> Some
| _ -> None
graph.Nodes |> Map.choose getDependencyStatus
let isSuccess =
graph.RootNodes |> Set.forall (fun nodeId ->
match nodeStatus |> Map.tryFind nodeId with
| Some info -> info.Status.IsSuccess
| _ -> false)
let headCommit = options.HeadCommit
let branchOrTag = options.BranchOrTag
let endedAt = DateTime.UtcNow
let buildInfo =
{ Summary.Commit = headCommit.Sha
Summary.BranchOrTag = branchOrTag
Summary.StartedAt = startedAt
Summary.EndedAt = endedAt
Summary.IsSuccess = isSuccess
Summary.Targets = options.Targets
Summary.Nodes = nodeStatus }
buildInfo