File: PIFLoader.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (670 lines) | stat: -rw-r--r-- 31,645 bytes parent folder | download
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
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public import SWBUtil
public import SWBProtocol
public import SWBMacro
import Foundation

enum PIFLoadingError: Error {
    /// Indicates a general PIF object decoding error.
    case invalidObject(message: String)

    /// Indicates an invalid top-level PIF object.
    case invalidObjectType(typeName: String)

    /// Indicates that an unloaded PIF object was expected to be available for loading, but was not.
    case unavailableObject(key: PIFObjectReference)

    /// Indicates an object that wasn't needed was sent to the incremental PIF loader.
    case nonRequiredObject(key: PIFObjectReference)

    /// Indicates that a referenced object was not sent to the incremental PIF loader.
    case missingObject(key: PIFObjectReference)

    /// Indicates that multiple referenced objects were not sent to the incremental PIF loader.
    case missingObjects(keys: Set<PIFObjectReference>)

    /// Indicates a duplicate registration error for the specified GUID.
    case guidAlreadyRegistered(type: PIFLoader.Type, guid: String)

    /// Indicates multiple objects of the same type had the same signature.
    case conflictingSignatures(type: PIFObjectType, signature: String)

    /// Indicates that there is a mismatch between the supported PIF format version between the client and service.
    case incompatiblePIFVersion(client: PIFSchemaVersion, service: PIFSchemaVersion)

    /// Indicates there is a problem determining the PIF schema version.
    case missingPIFVersionFromSignature(signature: String)

    /// Indicates there is a problem with the composition of targets and the location of package products.
    case incompatiblePackageTargetProject(target: String)
}

extension PIFLoadingError: CustomStringConvertible {
    var description: String {
        switch self {
        case let .invalidObject(message):
            return message
        case let .invalidObjectType(typeName):
            return "unexpected top-level object type: '\(typeName)'"
        case let .unavailableObject(key):
            return "Expected unloaded PIF object with key '\(key)' to be available for loading"
        case let .nonRequiredObject(key):
            return "object \(key) was added, but not required"
        case let .missingObject(key):
            return "object \(key) was referenced, but is missing"
        case let .missingObjects(keys):
            return "objects \(keys.map { String(describing: $0) }.sorted().joined(separator: ", ")) was referenced, but is missing"
        case let .guidAlreadyRegistered(type, guid):
            return "\(type): GUID '\(guid)' has already been registered"
        case let .conflictingSignatures(type, signature):
            return "multiple \(type.rawValue)s with identical signature: \(signature)"
        case let .incompatiblePIFVersion(client, service):
            return "mismatch between client and service versions: \(client) vs. \(service)"
        case let .missingPIFVersionFromSignature(signature):
            return "no PIF version found in signature: \(signature)"
        case let .incompatiblePackageTargetProject(target):
            return "package target '\(target)' must be contained within a package project"
        }
    }
}

/// A PIF value, in one of the two supported encodings.
//
// FIXME: Eliminate the legacy encoding so we can get rid of this.
public enum EncodedPIFValue {
    case json(ProjectModelItemPIF)
    case binary(ByteString)

    var isBinary: Bool {
        switch self {
        case .json: return false
        case .binary: return true
        }
    }
}

/// A reference to a PIFObject.
struct PIFObjectReference: Hashable {
    /// The type of a PIF object signature.
    typealias Signature = String

    /// The signature of the object.
    let signature: Signature

    /// The type of the object.
    let type: PIFObjectType
}

/// Based on the PIF signature, the encoded PIF schema version is parsed out.
@_spi(Testing) public func parsePIFVersion(_ signature: String) throws -> PIFSchemaVersion {
    // The version is derived from the signature, which starts with: "WORKSPACE@v%d_". The "WORKSPACE" string should be ignored as there are other variants!
    guard let vers = Int(signature.split("_").0.split("v").1) else {
        // NOTE: Once rdar://54261777 is implemented, this can be a more stringent test. Until then, assume the latest version .
        // throw PIFLoadingError.missingPIFVersionFromSignature(signature: signature)
        return SWBProtocol.PIFObject.supportedPIFEncodingSchemaVersion
    }
    return vers
}


/// A top-level object which can be represented independently in the PIF format.
///
/// These objects represent the granularity at which the PIF can be incrementally loaded or replaced.
protocol PIFObject: Sendable {
    typealias Signature = PIFObjectReference.Signature

    /// The PIF object type.
    static var pifType: PIFObjectType { get }

    /// The signature of the object.
    ///
    /// The signature represents a unique signature which is changed any time the object is modified with respect to its containing object. That is to say, a different containing object is allowed to have an object of the same type with the same signature, but different values.
    ///
    /// The caveats above are not ideal (we would like to have completely unique signatures), but allow us to more efficiently implement the transfer mechanism for objects for which we do not have an easy way to performantly generate a unique signature. See the Xcode implementation of target signatures to understand why this is an important distinction.
    var signature: Signature { get }

    /// Extract the list of PIF objects referenced from the given PIF data.
    ///
    /// - Returns: The list of referenced objects by signature and type.
    static func referencedObjects(for data: EncodedPIFValue) throws -> [PIFObjectReference]

    /// Create an object from the given data.
    static func construct(from data: EncodedPIFValue, signature: PIFObjectReference.Signature, loader: PIFLoader) throws -> Self
}

/// A protocol for providing loaded PIF objects.
///
/// This may be backed by previously loaded instances, or it may always load the items via the provided `loader`.
protocol PIFObjectProvider {
    /// Fetch the object with the given type and signature.
    func fetchObject<T: PIFObject>(signature: PIFObject.Signature, type: T.Type, loader: PIFLoader) throws -> T
}

extension PIFObjectType {
    /// The set of known PIF object types.
    var objectType: any PIFObject.Type {
        switch self {
        case .workspace:
            return Workspace.self
        case .project:
            return Project.self
        case .target:
            return Target.self
        }
    }
}

/// Helper class for loading PIF data into complete objects.
public final class PIFLoader {
    /// The object provider.
    let provider: any PIFObjectProvider

    /// The namespace for user build settings.  A new one is created for each `PIFLoader`, and ownership is transferred to the loaded `Workspace` during loading.
    let userNamespace: MacroNamespace

    /// A mapping of GUIDs to `ProjectModelItem`s built up as the PIF is loaded.  Once all objects in the PIF have been loaded, a copy of this dictionary will be transferred to the loaded `Workspace`.
    private var knownReferences: [String: Reference] = [:]

    /// The map of loaded objects which are available, used to satisfy indirect references.
    fileprivate var loadedObjects: [PIFObjectReference: any PIFObject] = [:]

    /// Create a loader from a raw PIF object.
    ///
    /// This object is expected to be a list of top-level PIF objects.
    convenience public init(data: PropertyListItem, namespace: MacroNamespace) {
        let objects = Result<[PIFObjectReference: EncodedPIFValue], any Swift.Error> { () -> [PIFObjectReference: EncodedPIFValue] in
            var objects = [PIFObjectReference: EncodedPIFValue]()
            guard case let .plArray(pifObjects) = data else {
                throw PIFLoadingError.invalidObject(message: "top level object must be an array")
            }
            for (index, data) in pifObjects.enumerated() {
                let (pifType, signature, objectContents) = try pifObject(data: data, at: index)
                let ref = PIFObjectReference(signature: signature, type: pifType)
                if objects.contains(ref) {
                    throw PIFLoadingError.conflictingSignatures(type: pifType, signature: signature)
                }
                objects[ref] = .json(objectContents)
            }
            return objects
        }

        self.init(objects: objects, namespace: namespace)
    }

    /// Create a new loader.
    ///
    /// The set of all possible references *must* be supplied to the loader.
    ///
    /// - parameter objects: The map of referenceable objects.
    /// - parameter namespace: The namespace to use as the parent of the namespace that loading will use to parse build settings in the PIF.
    //
    // FIXME: This API is somewhat cumbersome because of how incremental loading was retrofitted in. We should clean it up.
    convenience private init(objects: Result<[PIFObjectReference: EncodedPIFValue], any Error>, namespace: MacroNamespace) {
        struct StaticObjectProvider: PIFObjectProvider {
            let objects: Result<[PIFObjectReference: EncodedPIFValue], any Error>

            func fetchObject<T: PIFObject>(signature: String, type: T.Type, loader: PIFLoader) throws -> T {
                let ref = PIFObjectReference(signature: signature, type: type.pifType)
                guard let data = try objects.get()[ref] else {
                    throw PIFLoadingError.missingObject(key: ref)
                }
                return try type.construct(from: data, signature: signature, loader: loader)
            }
        }

        // When using the convenience API, we always load into a new namespace.
        let userNamespace = MacroNamespace(parent: namespace, debugDescription: "workspace")

        self.init(provider: StaticObjectProvider(objects: objects), userNamespace: userNamespace)
    }

    /// Create a loader from a PIF object provider.
    ///
    /// This object is expected to be a list of top-level PIF objects.
    init(provider: any PIFObjectProvider, userNamespace: MacroNamespace) {
        self.userNamespace = userNamespace
        self.provider = provider
    }

    /// Load the workspace from the PIF.
    public func load(workspaceSignature: String) throws -> Workspace {
        return try MacroNamespace.withExpressionInterningEnabled {
            try loadReference(signature: workspaceSignature, type: Workspace.self)
        }
    }

    /// Load a referenced object of the given type.
    ///
    /// It is a fatal error to attempt to load a reference which is unavailable.
    //
    // FIXME: This shouldn't be fatal, the service should never die on malformed data.
    func loadReference<T: PIFObject>(signature: PIFObjectReference.Signature, type: T.Type) throws -> T {
        let key = PIFObjectReference(signature: signature, type: type.pifType)
        if let object = loadedObjects[key] {
            return object as! T
        }

        let object = try provider.fetchObject(signature: signature, type: type, loader: self)
        loadedObjects[key] = object
        return object
    }

    /// Register a ProjectModelItem by GUID so references to them elsewhere in the PIF can be resolved. It is an error to register the same GUID twice.
    ///
    /// Only items which could actually be referred to by other items need to be registered.  This isn't intended to be a generic "make sure the same GUID is never used twice in the PIF" mechanism.
    func registerReference(_ item: Reference, for guid: String) throws {
        guard knownReferences[guid] == nil else {
            throw PIFLoadingError.guidAlreadyRegistered(type: type(of: self), guid: guid)
        }
        knownReferences[guid] = item
    }

    // Return the ProjectModelItem for the requested GUID.  If one cannot be returned, that's an internal error.
    func lookupReference(for guid: String ) -> Reference? {
        return knownReferences[guid]
    }

    /// Extract the workspace signature from a raw PIF object.
    ///
    /// This object is expected to be a list of top-level PIF objects, the first of which is the workspace.
    public static func extractWorkspaceSignature(objects data: PropertyListItem) throws -> String {
        guard case .plArray(let pifObjects) = data else {
            throw PIFLoadingError.invalidObject(message: "top level object must be an array")
        }
        guard case .plDict(let workspaceObject)? = pifObjects.first else {
            throw PIFLoadingError.invalidObject(message: "first item in the top level array must be a dictionary")
        }
        guard case .plString(let type)? = workspaceObject["type"], type == "workspace" else {
            throw PIFLoadingError.invalidObject(message: "'type' property of the first item in the top level array must be 'workspace'")
        }
        guard case .plString(let signature)? = workspaceObject["signature"] else {
            throw PIFLoadingError.invalidObject(message: "missing signature in workspace object")
        }
        return signature
    }
}


/// An incremental PIF loader.
///
/// This is a PIF loader implementation which keeps track of available objects and dynamically negotiates the minimal set of objects which need to be transferred to load the complete PIF.
///
/// If enabled, this class can also manage persistent storage of PIF objects to a `FileSystem`. When this is enabled, objects which are transferred will be stored in the cache so that later transfer requests do not require the client to recompute and retransfer the PIF data.
///
/// This class is *not* thread safe.
//
// FIXME: This API doesn't feel right, it seems bogus that both the incremental loader and the regular loader are both maintaining maps of objects keyed by the same thing. The separation currently is between the incremental loader, which maintains a set of objects across multiple load requests and can communicate back to the client (via `LoadingSession`) about what new objects are required, and the bare `PIFLoader` which is only responsible for loading a single, complete graph.
public final class IncrementalPIFLoader {
    @_spi(Testing) public static let loadsRequested = Statistic("IncrementalPIFLoader.loadsRequested",
        "The total number of top-level objects requested.")
    static let objectsRequested = Statistic("IncrementalPIFLoader.objectsRequested",
        "The number of PIF objects which were requested.")
    @_spi(Testing) public static let objectsLoaded = Statistic("IncrementalPIFLoader.objectsLoaded",
        "The number of PIF objects which were loaded, in any form.")
    static let objectsCachedInMemory = Statistic("IncrementalPIFLoader.objectsCachedInMemory",
        "The number of PIF objects which hit the in-memory cache.")
    static let objectsCachedOnDisk = Statistic("IncrementalPIFLoader.objectsCachedOnDisk",
        "The number of PIF objects which hit the on-disk cache.")
    @_spi(Testing) public static let objectsTransferred = Statistic("IncrementalPIFLoader.objectsTransferred",
        "The number of PIF objects which were transferred.")

    /// A session for incrementally loading an individual object.
    ///
    /// NOTE: The loader expects that only one loading session is ever in use at a time. This is an unchecked condition.
    public final class LoadingSession {
        private struct ObjectProviderAdaptor: PIFObjectProvider {
            let session: LoadingSession

            func fetchObject<T: PIFObject>(signature: PIFObject.Signature, type: T.Type, loader: PIFLoader) throws -> T {
                let key = PIFObjectReference(signature: signature, type: type.pifType)

                // Check if we have already loaded this object.
                if let object = session.incrementalLoader.loadedObjects[key] {
                    return object as! T
                }

                // If not, load it.
                guard let data = session.availableDataObjects[key] else {
                    throw PIFLoadingError.unavailableObject(key: key)
                }

                return try type.construct(from: data, signature: signature, loader: loader)
            }
        }

        /// The incremental loader.
        let incrementalLoader: IncrementalPIFLoader

        /// The workspace signature being loaded.
        let workspaceSignature: String

        /// The available, but unloaded object data.
        var availableDataObjects: [PIFObjectReference: EncodedPIFValue] = [:]

        /// Indicates whether the PIF cache was read at least once.
        public private(set) var didUseCache = false

        /// The complete set of missing objects for the unloaded data.
        public var missingObjects: AnySequence<(signature: String, type: PIFObjectType)> {
            return AnySequence(_missingObjects.map{ (signature: $0.signature, type: $0.type) })
        }
        var _missingObjects: Set<PIFObjectReference> = []

        /// Create a workspace loading session.
        init(workspaceSignature: String, _ incrementalLoader: IncrementalPIFLoader) {
            self.workspaceSignature = workspaceSignature
            self.incrementalLoader = incrementalLoader

            let key = PIFObjectReference(signature: workspaceSignature, type: .workspace)
            addRequiredObject(key)
        }

        /// Load the named workspace.
        ///
        /// This may only be called when `missingObjects` is empty.
        public func load() throws -> Workspace {
            IncrementalPIFLoader.loadsRequested.increment()

            if !_missingObjects.isEmpty {
                throw PIFLoadingError.missingObjects(keys: _missingObjects)
            }

            // Create the loader for this instance and load the object.
            let loader = PIFLoader(provider: ObjectProviderAdaptor(session: self), userNamespace: incrementalLoader.userNamespace)
            let result = try MacroNamespace.withExpressionInterningEnabled{ try loader.loadReference(signature: workspaceSignature, type: Workspace.self) }

            // Merge back in all the loaded objects.
            for (key, value) in loader.loadedObjects {
                incrementalLoader.loadedObjects[key] = value
            }

            return result
        }

        /// Add the given raw PIF object.
        ///
        /// This will register and scan the object, and then update `missingObjects` to reflect what objects are still required, if any.
        public func add(object data: PropertyListItem) throws {
            let (pifType, signature, objectContents) = try pifObject(data: data, at: nil)

            // Validate the version compatibility. Recall that the PIF is written from Xcode, which acts at the client.
            let pifVersionForClient = try parsePIFVersion(signature)
            guard pifVersionForClient <= SWBProtocol.PIFObject.supportedPIFEncodingSchemaVersion else {
                throw PIFLoadingError.incompatiblePIFVersion(client: pifVersionForClient, service: SWBProtocol.PIFObject.supportedPIFEncodingSchemaVersion)
            }

            // Automatically decapsulate binary data.
            let value: EncodedPIFValue
            if let data = try BuildFile.parseOptionalValueForKeyAsByteString("data", pifDict: objectContents) {
                value = .binary(data)
            } else {
                value = .json(objectContents)
            }

            try add(pifType: pifType, object: value, signature: signature)
        }

        /// Add the given raw PIF object.
        ///
        /// This will register and scan the object, and then update `missingObjects` to reflect what objects are still required, if any.
        public func add(pifType: PIFObjectType, object: EncodedPIFValue, signature: String) throws {
            // It is an error to add a non-required object.
            let key = PIFObjectReference(signature: signature, type: pifType)
            if !_missingObjects.contains(key) {
                throw PIFLoadingError.nonRequiredObject(key: key)
            }

            IncrementalPIFLoader.objectsTransferred.increment()

            try add(contents: object, for: key)
        }

        /// Record a required object reference.
        func addRequiredObject(_ key: PIFObjectReference) {
            IncrementalPIFLoader.objectsRequested.increment()

            // Check if on-disk cache is still intact.
            if incrementalLoader.isOnDiskCacheIntact(for: key) {
                // If the object is available, we don't need to add it.
                if availableDataObjects.contains(key) || incrementalLoader.loadedObjects.contains(key) {
                    return IncrementalPIFLoader.objectsCachedInMemory.increment()
                }
            } else {
                // If on-disk cache was compromised, remove the in memory data for that key.
                availableDataObjects[key] = nil
                incrementalLoader.loadedObjects[key] = nil
            }

            // Check if we have an entry in the on-disk cache, and if so add it.
            if let contents = incrementalLoader.readCacheObject(for: key) {
                IncrementalPIFLoader.objectsCachedOnDisk.increment()
                self.didUseCache = true

                // If we were able to load the data, then try to add it recursively (since we will be bypassing the addition of all referenced objects).
                if let _ = try? add(contents: contents, for: key, writeToCache: false) {
                    // If we were able to add it, use the cached value.
                    return
                }

                // Otherwise, if there was an error loading the cached object then force it to be explicitly transferred (this still might fail when the actual loading is attempted).
            }

            // Otherwise, we need the client to provide the object.
            _missingObjects.insert(key)
        }

        /// Add the `contents` for the object `key`.
        ///
        /// - Parameters:
        ///   - writeToCache: If true, the object should be added to the cache if in use.
        func add(contents: EncodedPIFValue, for key: PIFObjectReference, writeToCache: Bool = true) throws {
            IncrementalPIFLoader.objectsLoaded.increment()

            // Get the list of referenced objects (which can fail), before we mutate any other data.
            let referencedObjects = try key.type.objectType.referencedObjects(for: contents)

            availableDataObjects[key] = contents

            // Record the data in the cache.
            if writeToCache {
                try incrementalLoader.writeCacheObject(contents: contents, for: key)
            }

            // Update the set of missing objects.
            _missingObjects.remove(key)
            for reference in referencedObjects {
                addRequiredObject(reference)
            }
        }
    }

    /// The user namespace to use when loading objects.
    ///
    /// This namespace is currently constant for *all* objects loaded by this interface. Eventually we would like to support actually segregating the user namespaces along workspace or project boundaries, which will require that we lift the namespace to an actual transported object we can integrate with the PIF loading process (right now, we must have a constant one so that different PIF objects always see a consistent namespace view).
    public let userNamespace: MacroNamespace

    /// The loaded objects.
    private let loadedObjects = HeavyCache<PIFObjectReference, any PIFObject>(timeToLive: Tuning.pifCacheTTL)

    @_spi(Testing) public var loadedObjectCount: Int {
        loadedObjects.count
    }

    /// The cache path, if enabled.
    var cachePath: Path?

    /// The file system to use for persistence.
    let fs: any FSProxy

    /// Create an incremental PIF loader.
    ///
    /// - Parameters:
    ///   - internalNamespace: The internal namespace, used to derive a workspace-level namespace to load all objects into.
    ///   - cachePath: If provided, the path to a directory to use for persisting PIF objects, for use in incremental restoration.
    public init(internalNamespace: MacroNamespace, cachePath: Path?, fs: any FSProxy = localFS) {
        self.userNamespace = MacroNamespace(parent: internalNamespace, debugDescription: "workspace")
        self.cachePath = cachePath
        self.fs = fs

        // FIXME: Eventually, we will want a much more sophisticated cache here,
        // with LRU eviction and versioning: <rdar://problem/28190187> Improve PIF data cache

        // If the cache is enabled, ensure the type-keyed directories exist.
        createCacheDirectories()
    }

    /// Initiate a workspace loading session.
    public func startLoading(workspaceSignature: String) -> LoadingSession {
        return LoadingSession(workspaceSignature: workspaceSignature, self)
    }

    /// Clear the persistent cache.
    public func clearCache() {
        guard cachePath != nil else { return }

        for path in cacheDirectories() {
            // FIXME: We need to handle the errors thrown from here.
            try? fs.removeDirectory(path)
        }
        // Recreate the cache directories.
        createCacheDirectories()
    }

    /// Create the persistent cache directories, if used.
    private func createCacheDirectories() {
        for path in cacheDirectories() {
            createCacheDirectory(path)
        }
    }

    /// Create directory and potentially tag it as a build directory
    private func createCacheDirectory(_ cachePath: Path) {
        // Determine the parent of "XCBuildData", because that will be the intermediates directory.
        var path = cachePath
        while path.str.contains("XCBuildData") && !path.isRoot && !path.isEmpty {
            path = path.dirname
        }

        // This is some unfortunate duplication with `CreateBuildDirectoryTaskAction`, but we have to good way to clean it up right now.
        if !fs.exists(path) {
            // FIXME: We need to handle the errors thrown from here.
            try? fs.createDirectory(path, recursive: true)
            do {
                try fs.setCreatedByBuildSystemAttribute(path)
            } catch {
                print("Couldn't set attribute on intermediates directory: \(error.localizedDescription)")
            }
        }

        // FIXME: We need to handle the errors thrown from here.
        try? fs.createDirectory(cachePath, recursive: true)
    }

    /// Returns the directories that we use for on-disk cache.
    private func cacheDirectories() -> [Path] {
        guard let cachePath else { return [] }
        return PIFObjectType.allCases.map({ cachePath.join($0.rawValue) })
    }

    /// Returns true if the on-disk cache is intact for the given key.
    ///
    /// This always returns true if on-disk cache is not active.
    private func isOnDiskCacheIntact(for key: PIFObjectReference) -> Bool {
        // Return true if we're not using on-disk cache.
        if cachePath == nil {
            return true
        }

        // Check if the on-disk cache is present.
        for isBinary in [true, false] {
            let path = cacheEntryPath(for: key, isBinary: isBinary)!
            if fs.exists(path) {
                return true
            }
        }
        return false
    }

    // MARK: Persistence

    /// Get the cache entry path for `key`.
    ///
    /// - Returns: The entry path, if the cache is in use.
    private func cacheEntryPath(for key: PIFObjectReference, isBinary: Bool) -> Path? {
        return cachePath?.join(key.type.rawValue).join(key.signature + (isBinary ? "-binary" : "-json"))
    }

    /// Cache the given object, if enabled.
    private func writeCacheObject(contents: EncodedPIFValue, for key: PIFObjectReference) throws {
        if let path = cacheEntryPath(for: key, isBinary: contents.isBinary) {
            if !fs.isDirectory(path.dirname) {
                createCacheDirectory(path.dirname)
            }
            do {
                // FIXME: Use a more efficient representation: <rdar://problem/28190187> Improve PIF data cache
                let bytes: ByteString
                switch contents {
                case .json(let contents):
                    bytes = try PropertyListItem.plDict(contents).asJSONFragment()
                case .binary(let contents):
                    bytes = contents
                }
                try fs.write(path, contents: bytes, atomically: true)
            }
        }
    }

    /// Read the cached contents for the given key, if available
    private func readCacheObject(for key: PIFObjectReference) -> EncodedPIFValue? {
        // Check if we have a binary cache entry.
        if let path = cacheEntryPath(for: key, isBinary: true), fs.exists(path) {
            // Load from the cache.
            //
            // FIXME: Use a more efficient representation: <rdar://problem/28190187> Improve PIF data cache
            if let data = try? fs.read(path) {
                return .binary(data)
            }
        }
        // Check if we have a JSON cache entry.
        if let path = cacheEntryPath(for: key, isBinary: false), fs.exists(path) {
            // Load from the cache.
            //
            // FIXME: Use a more efficient representation: <rdar://problem/28190187> Improve PIF data cache
            if let data = try? fs.read(path), let plItem = try? PropertyList.fromJSONData(data.bytes), case let .plDict(contents) = plItem {
                return .json(contents)
            }
        }
        return nil
    }
}

fileprivate func pifObject(data pifObject: PropertyListItem, at index: Int?) throws -> (pifType: PIFObjectType, signature: String, objectContents: [String: PropertyListItem]) {
    let indexInfix = index.map { index in "PIF object at index \(index) in the top level array" } ?? "PIF object"
    guard case let .plDict(contents) = pifObject else {
        throw PIFLoadingError.invalidObject(message: "\(indexInfix) must be a dictionary")
    }
    guard case let .plString(type)? = contents["type"] else {
        throw PIFLoadingError.invalidObject(message: "'type' property of \(indexInfix) is missing or is not a string")
    }
    guard case let .plString(signature)? = contents["signature"] else {
        throw PIFLoadingError.invalidObject(message: "'signature' property of '\(type)' \(indexInfix) is missing or is not a string")
    }
    guard case let .plDict(objectContents)? = contents["contents"] else {
        throw PIFLoadingError.invalidObject(message: "'contents' property of '\(type)' \(indexInfix) is missing or is not a dictionary")
    }
    guard let objectType = PIFObjectType(rawValue: type) else {
        throw PIFLoadingError.invalidObjectType(typeName: type)
    }
    return (objectType, signature, objectContents)
}