Skip to content

Commit a081b0e

Browse files
committed
[Xcodeproj] Assign stable object IDs to critical objects.
- Objects where can be referenced across projects should have stable IDs, because Xcode will embed the references to these targets by their ID. We want those references to remain stable even as the project evolves under regeneration. - <rdar://problem/31019219> Generated .xcodeproj needs to use stable object IDs
1 parent a6cf331 commit a081b0e

File tree

3 files changed

+49
-13
lines changed

3 files changed

+49
-13
lines changed

Sources/Xcodeproj/XcodeProjectModel.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ public struct Xcode {
7575

7676
/// Creates and adds a new target (which does not initially have any
7777
/// build phases).
78-
public func addTarget(productType: Target.ProductType?, name: String) -> Target {
79-
let target = Target(productType: productType, name: name)
78+
public func addTarget(objectID: String? = nil, productType: Target.ProductType?, name: String) -> Target {
79+
let target = Target(objectID: objectID, productType: productType, name: name)
8080
targets.append(target)
8181
return target
8282
}
@@ -119,10 +119,12 @@ public struct Xcode {
119119

120120
/// A reference to a file system entity (a file, folder, etc).
121121
public class FileReference: Reference {
122+
var objectID: String?
122123
var fileType: String?
123124

124-
init(path: String, pathBase: RefPathBase = .groupDir, name: String? = nil, fileType: String? = nil) {
125+
init(path: String, pathBase: RefPathBase = .groupDir, name: String? = nil, fileType: String? = nil, objectID: String? = nil) {
125126
super.init(path: path, pathBase: pathBase, name: name)
127+
self.objectID = objectID
126128
self.fileType = fileType
127129
}
128130
}
@@ -150,16 +152,18 @@ public struct Xcode {
150152
path: String,
151153
pathBase: RefPathBase = .groupDir,
152154
name: String? = nil,
153-
fileType: String? = nil
155+
fileType: String? = nil,
156+
objectID: String? = nil
154157
) -> FileReference {
155-
let fref = FileReference(path: path, pathBase: pathBase, name: name, fileType: fileType)
158+
let fref = FileReference(path: path, pathBase: pathBase, name: name, fileType: fileType, objectID: objectID)
156159
subitems.append(fref)
157160
return fref
158161
}
159162
}
160163

161164
/// An Xcode target, representing a single entity to build.
162165
public class Target {
166+
var objectID: String?
163167
var name: String
164168
var productName: String
165169
var productType: ProductType?
@@ -176,7 +180,8 @@ public struct Xcode {
176180
case unitTest = "com.apple.product-type.bundle.unit-test"
177181
var asString: String { return rawValue }
178182
}
179-
init(productType: ProductType?, name: String) {
183+
init(objectID: String?, productType: ProductType?, name: String) {
184+
self.objectID = objectID
180185
self.name = name
181186
self.productType = productType
182187
self.productName = name

Sources/Xcodeproj/XcodeProjectModelSerialization.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,19 +470,19 @@ fileprivate class PropertyListSerializer {
470470
/// dictionary is what gets written to the property list.
471471
var idsToDicts = [String: PropertyList]()
472472

473-
/// Returns the identifier for the object, assigning one if needed.
473+
/// Returns the quoted identifier for the object, assigning one if needed.
474474
func id(of object: PropertyListSerializable) -> String {
475475
// We need a "serialized object ref" wrapper for the `objsToIds` map.
476476
let serObjRef = SerializedObjectRef(object)
477477
if let id = objsToIds[serObjRef] {
478-
return id
478+
return "\"\(id)\""
479479
}
480480
// We currently always assign identifiers starting at 1 and going up.
481481
// FIXME: This is a suboptimal format for object identifier strings;
482482
// for debugging purposes they should at least sort in numeric order.
483-
let id = "OBJ_\(objsToIds.count + 1)"
483+
let id = object.objectID ?? "OBJ_\(objsToIds.count + 1)"
484484
objsToIds[serObjRef] = id
485-
return id
485+
return "\"\(id)\""
486486
}
487487

488488
/// Serializes `object` by asking it to construct a plist dictionary and
@@ -546,13 +546,24 @@ fileprivate protocol PropertyListSerializable: class {
546546
/// matters. So this is acceptable for now in the interest of getting it
547547
/// done.
548548

549+
/// A custom ID to use for the instance, if enabled.
550+
///
551+
/// This ID must be unique across the entire serialized graph.
552+
var objectID: String? { get }
553+
549554
/// Should create and return a property list dictionary of the object's
550555
/// attributes. This function may also use the serializer's `serialize()`
551556
/// function to serialize other objects, and may use `id(of:)` to access
552557
/// ids of objects that either have or will be serialized.
553558
func serialize(to serializer: PropertyListSerializer) -> [String: PropertyList]
554559
}
555560

561+
extension PropertyListSerializable {
562+
var objectID: String? {
563+
return nil
564+
}
565+
}
566+
556567
/// A very simple representation of a property list. Note that the `identifier`
557568
/// enum is not strictly necessary, but useful to semantically distinguish the
558569
/// strings that represents object identifiers from those that are just data.

Sources/Xcodeproj/pbxproj().swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ func xcodeProject(
194194
// Determine the list of external package dependencies, if any.
195195
let externalPackages = graph.packages.filter({ !graph.rootPackages.contains($0) })
196196

197+
// Build a backmap of targets and products to packages.
198+
var packagesByTarget = [ResolvedTarget: ResolvedPackage]()
199+
for package in graph.packages {
200+
for target in package.targets {
201+
packagesByTarget[target] = package
202+
}
203+
}
204+
var packagesByProduct = [ResolvedProduct: ResolvedPackage]()
205+
for package in graph.packages {
206+
for product in package.products {
207+
packagesByProduct[product] = package
208+
}
209+
}
210+
197211
// To avoid creating multiple groups for the same path, we keep a mapping
198212
// of the paths we've seen and the corresponding groups we've created.
199213
var srcPathsToGroups: [AbsolutePath: Xcode.Group] = [:]
@@ -351,7 +365,10 @@ func xcodeProject(
351365
}
352366

353367
// Create a Xcode target for the target.
354-
let xcodeTarget = project.addTarget(productType: productType, name: target.name)
368+
let package = packagesByTarget[target]!
369+
let xcodeTarget = project.addTarget(
370+
objectID: "\(package.name)::\(target.name)",
371+
productType: productType, name: target.name)
355372

356373
// Set the product name to the C99-mangled form of the target name.
357374
xcodeTarget.productName = target.c99name
@@ -427,7 +444,7 @@ func xcodeProject(
427444
targetSettings.common.FRAMEWORK_SEARCH_PATHS = ["$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks"]
428445

429446
// Add a file reference for the target's product.
430-
let productRef = productsGroup.addFileReference(path: target.productPath.asString, pathBase: .buildDir)
447+
let productRef = productsGroup.addFileReference(path: target.productPath.asString, pathBase: .buildDir, objectID: "\(package.name)::\(target.name)::Product")
431448

432449
// Set that file reference as the target's product reference.
433450
xcodeTarget.productReference = productRef
@@ -556,7 +573,10 @@ func xcodeProject(
556573
// Go on to next product if we already have a target with the same name.
557574
if targetNames.contains(product.name) { continue }
558575
// Otherwise, create an aggreate target.
559-
let aggregateTarget = project.addTarget(productType: nil, name: product.name)
576+
let package = packagesByProduct[product]!
577+
let aggregateTarget = project.addTarget(
578+
objectID: "\(package.name)::\(product.name)::ProductTarget",
579+
productType: nil, name: product.name)
560580
// Add dependencies on the targets created for each of the dependencies.
561581
for target in product.targets {
562582
// Find the target that corresponds to the target. There might not

0 commit comments

Comments
 (0)