Skip to content

Commit 23e7c75

Browse files
committed
make package.resolved deterministic in edge cases
motivation: in some cases like two URLs with casing differences, package.resolved could be non-deterministic changes: * make the maanged dependencies location lookup case insensitive * refactor how package.resolved is saved to test for non material differences * adjust call sites that manipulate the pin store to be more efficient about when to save the resolved file * add warning when expected dependency that should be stored in the resolved file is not found * adjust call sites TODO: add tests
1 parent 23c1486 commit 23e7c75

File tree

4 files changed

+216
-48
lines changed

4 files changed

+216
-48
lines changed

Sources/PackageGraph/PinsStore.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ public final class PinsStore {
101101
self._pins[pin.packageRef.identity] = pin
102102
}
103103

104+
/// Remove a pin.
105+
///
106+
/// This will replace any previous pin with same package name.
107+
public func remove(_ pin: Pin) {
108+
self._pins[pin.packageRef.identity] = nil
109+
}
110+
104111
/// Unpin all of the currently pinned dependencies.
105112
///
106113
/// This method does not automatically write to state file.

Sources/PackageModel/PackageReference.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ extension PackageReference: Equatable {
154154

155155
// TODO: consider rolling into Equatable
156156
public func equalsIncludingLocation(_ other: PackageReference) -> Bool {
157-
return self.identity == other.identity && self.kind.locationString == other.kind.locationString
157+
return self.identity == other.identity && self.kind.locationString.caseInsensitiveCompare(other.kind.locationString) == .orderedSame
158158
}
159159
}
160160

Sources/Workspace/Workspace.swift

Lines changed: 136 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ extension Workspace {
815815
let resolver = try self.createResolver(pinsMap: pinsMap, observabilityScope: observabilityScope)
816816
self.activeResolver = resolver
817817

818-
let updateResults = resolveDependencies(
818+
let updateResults = self.resolveDependencies(
819819
resolver: resolver,
820820
constraints: updateConstraints,
821821
observabilityScope: observabilityScope
@@ -836,8 +836,8 @@ extension Workspace {
836836
// Load the updated manifests.
837837
let updatedDependencyManifests = try self.loadDependencyManifests(root: graphRoot, observabilityScope: observabilityScope)
838838

839-
// Update the pins store.
840-
self.pinAll(
839+
// Update the resolved file.
840+
self.saveResolvedFile(
841841
pinsStore: pinsStore,
842842
dependencyManifests: updatedDependencyManifests,
843843
rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion,
@@ -1326,25 +1326,86 @@ extension Workspace {
13261326
// MARK: - Pinning Functions
13271327

13281328
extension Workspace {
1329-
13301329
/// Pins all of the current managed dependencies at their checkout state.
1331-
fileprivate func pinAll(
1330+
fileprivate func saveResolvedFile(
13321331
pinsStore: PinsStore,
13331332
dependencyManifests: DependencyManifests,
13341333
rootManifestsMinimumToolsVersion: ToolsVersion,
13351334
observabilityScope: ObservabilityScope
1336-
) {
1335+
) {
1336+
var dependenciesToPin = [ManagedDependency]()
1337+
let requiredDependencies = dependencyManifests.computePackages().required.filter({ $0.kind.isPinnable })
1338+
for dependency in requiredDependencies {
1339+
if let managedDependency = self.state.dependencies[comparingLocation: dependency] {
1340+
dependenciesToPin.append(managedDependency)
1341+
} else {
1342+
observabilityScope.emit(warning: "required dependency \(dependency.identity) (\(dependency.locationString)) was not found in managed dependencies and will not be recorded in resolved file")
1343+
}
1344+
}
1345+
1346+
// FIXME
1347+
// load the pin store from disk so we can compare for any changes
1348+
// this is needed as we want to avoid re-writing the resolved files
1349+
// unless absolutely required
1350+
guard let storedPinStore = observabilityScope.trap({ try self.pinsStore.load() }) else {
1351+
return
1352+
}
1353+
1354+
// compare for any differences between the existing state and the stored one
1355+
// subtle changes between versions of SwiftPM could treat URLs differently
1356+
// in which case we dont want to cause unnecessary churn
1357+
var needsUpdate = false
1358+
if dependenciesToPin.count != storedPinStore.pinsMap.count {
1359+
needsUpdate = true
1360+
} else {
1361+
for dependency in dependenciesToPin {
1362+
if let pin = storedPinStore.pinsMap.first(where: { $0.value.packageRef.equalsIncludingLocation(dependency.packageRef) }) {
1363+
if pin.value.state != PinsStore.Pin(dependency)?.state {
1364+
needsUpdate = true
1365+
break
1366+
}
1367+
} else {
1368+
needsUpdate = true
1369+
break
1370+
}
1371+
}
1372+
}
1373+
1374+
// exist early is there is nothing to do
1375+
guard needsUpdate else {
1376+
return
1377+
}
1378+
1379+
// reset the pinsStore and start pinning the required dependencies.
1380+
pinsStore.unpinAll()
1381+
for dependency in dependenciesToPin {
1382+
pinsStore.pin(dependency)
1383+
}
1384+
1385+
/*
13371386
// Reset the pinsStore and start pinning the required dependencies.
13381387
pinsStore.unpinAll()
13391388

1389+
let requiredDependencies = dependencyManifests.computePackages().required.filter({ $0.kind.isPinnable })
1390+
for dependency in requiredDependencies {
1391+
if let managedDependency = self.state.dependencies[comparingLocation: dependency] {
1392+
pinsStore.pin(managedDependency)
1393+
} else {
1394+
observabilityScope.emit(warning: "required dependency \(dependency.identity) (\(dependency.locationString)) was not found in managed dependencies and will not be recorded in resolved file")
1395+
}
1396+
}
1397+
*/
1398+
1399+
1400+
/*
13401401
let requiredDependencies = dependencyManifests.computePackages().required
13411402
for dependency in self.state.dependencies {
13421403
if requiredDependencies.contains(where: { $0.equalsIncludingLocation(dependency.packageRef) }) {
13431404
pinsStore.pin(dependency)
13441405
}
1345-
}
1406+
}*/
13461407

1347-
observabilityScope.trap{
1408+
observabilityScope.trap {
13481409
try pinsStore.saveState(toolsVersion: rootManifestsMinimumToolsVersion)
13491410
}
13501411

@@ -1359,30 +1420,38 @@ fileprivate extension PinsStore {
13591420
///
13601421
/// This method does nothing if the dependency is in edited state.
13611422
func pin(_ dependency: Workspace.ManagedDependency) {
1423+
if let pin = PinsStore.Pin(dependency) {
1424+
self.add(pin)
1425+
}
1426+
}
1427+
}
1428+
1429+
fileprivate extension PinsStore.Pin {
1430+
init?(_ dependency: Workspace.ManagedDependency) {
13621431
switch dependency.state {
13631432
case .sourceControlCheckout(.version(let version, let revision)):
1364-
self.pin(
1433+
self.init(
13651434
packageRef: dependency.packageRef,
13661435
state: .version(version, revision: revision.identifier)
13671436
)
13681437
case .sourceControlCheckout(.branch(let branch, let revision)):
1369-
self.pin(
1438+
self.init(
13701439
packageRef: dependency.packageRef,
13711440
state: .branch(name: branch, revision: revision.identifier)
13721441
)
13731442
case .sourceControlCheckout(.revision(let revision)):
1374-
self.pin(
1443+
self.init(
13751444
packageRef: dependency.packageRef,
13761445
state: .revision(revision.identifier)
13771446
)
13781447
case .registryDownload(let version):
1379-
self.pin(
1448+
self.init(
13801449
packageRef: dependency.packageRef,
13811450
state: .version(version, revision: .none)
13821451
)
13831452
case .edited, .fileSystem, .custom:
13841453
// NOOP
1385-
break
1454+
return nil
13861455
}
13871456
}
13881457
}
@@ -1398,7 +1467,7 @@ extension Workspace {
13981467
/// The dependency manifests in the transitive closure of root manifest.
13991468
let dependencies: [(manifest: Manifest, dependency: ManagedDependency, productFilter: ProductFilter, fileSystem: FileSystem)]
14001469

1401-
let workspace: Workspace
1470+
private let workspace: Workspace
14021471

14031472
fileprivate init(
14041473
root: PackageGraphRoot,
@@ -1649,7 +1718,7 @@ extension Workspace {
16491718
})
16501719

16511720
// optimization: preload first level dependencies manifest (in parallel)
1652-
let firstLevelDependencies = topLevelManifests.values.map { $0.dependencies.map{ $0.createPackageRef() } }.flatMap { $0 }
1721+
let firstLevelDependencies = topLevelManifests.values.map { $0.dependencies.map{ $0.createPackageRef() } }.flatMap({ $0 })
16531722
let firstLevelManifests = try temp_await { self.loadManagedManifests(for: firstLevelDependencies, observabilityScope: observabilityScope, completion: $0) } // FIXME: this should not block
16541723

16551724
// Continue to load the rest of the manifest for this graph
@@ -1677,7 +1746,7 @@ extension Workspace {
16771746
// dependencies that have the same identity but from a different location
16781747
// which is an error case we diagnose an report about in the GraphLoading part which
16791748
// is prepared to handle the case where not all manifest are available
1680-
$0.packageLocation == dependency.locationString ?
1749+
$0.packageLocation.caseInsensitiveCompare(dependency.locationString) == .orderedSame ?
16811750
KeyedPair($0, key: Key(identity: dependency.identity, productFilter: dependency.productFilter)) : nil
16821751
}
16831752
}
@@ -2524,10 +2593,18 @@ extension Workspace {
25242593
return currentManifests
25252594
}
25262595

2527-
self.validatePinsStore(dependencyManifests: currentManifests, rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion, observabilityScope: observabilityScope)
2596+
// load and update the pins store with any changes from loading the top level dependencies
2597+
guard let pinsStore = self.loadAndUpdatePinsStore(
2598+
dependencyManifests: currentManifests,
2599+
rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion,
2600+
observabilityScope: observabilityScope
2601+
) else {
2602+
// abort if PinsStore reported any errors.
2603+
return currentManifests
2604+
}
25282605

2529-
// Abort if pinsStore is unloadable or if diagnostics has errors.
2530-
guard !observabilityScope.errorsReported, let pinsStore = observabilityScope.trap({ try pinsStore.load() }) else {
2606+
// abort if PinsStore reported any errors.
2607+
guard !observabilityScope.errorsReported else {
25312608
return currentManifests
25322609
}
25332610

@@ -2551,6 +2628,15 @@ extension Workspace {
25512628

25522629
switch result {
25532630
case .notRequired:
2631+
// since nothing changed we can exit early,
2632+
// but need update resolved file and download an missing binary artifact
2633+
self.saveResolvedFile(
2634+
pinsStore: pinsStore,
2635+
dependencyManifests: currentManifests,
2636+
rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion,
2637+
observabilityScope: observabilityScope
2638+
)
2639+
25542640
try self.updateBinaryArtifacts(
25552641
manifests: currentManifests,
25562642
addedOrUpdatedPackages: [],
@@ -2601,7 +2687,8 @@ extension Workspace {
26012687
return updatedDependencyManifests
26022688
}
26032689

2604-
self.pinAll(
2690+
// Update the resolved file.
2691+
self.saveResolvedFile(
26052692
pinsStore: pinsStore,
26062693
dependencyManifests: updatedDependencyManifests,
26072694
rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion,
@@ -2784,43 +2871,32 @@ extension Workspace {
27842871
}
27852872

27862873
/// Validates that each checked out managed dependency has an entry in pinsStore.
2787-
private func validatePinsStore(
2874+
private func loadAndUpdatePinsStore(
27882875
dependencyManifests: DependencyManifests,
27892876
rootManifestsMinimumToolsVersion: ToolsVersion,
27902877
observabilityScope: ObservabilityScope
2791-
) {
2792-
guard let pinsStore = observabilityScope.trap({ try pinsStore.load() }) else {
2793-
return
2878+
) -> PinsStore? {
2879+
guard let pinsStore = observabilityScope.trap({ try self.pinsStore.load() }) else {
2880+
return nil
27942881
}
27952882

2796-
let pins = pinsStore.pinsMap.keys
2797-
let requiredDependencies = dependencyManifests.computePackages().required
2798-
2799-
for dependency in self.state.dependencies {
2800-
switch dependency.state {
2801-
case .sourceControlCheckout, .registryDownload: break
2802-
case .edited, .fileSystem, .custom: continue
2803-
}
2804-
2883+
let requiredDependencies = dependencyManifests.computePackages().required.filter({ $0.kind.isPinnable })
2884+
for dependency in self.state.dependencies.filter({ $0.packageRef.kind.isPinnable }) {
2885+
// a required dependency that is already loaded (managed) should be represented in the pins store.
28052886
// also comparing location as it may have changed at this point
28062887
if requiredDependencies.contains(where: { $0.equalsIncludingLocation(dependency.packageRef) }) {
2807-
// If required identity contains this dependency, it should be in the pins store.
2808-
// also comparing location as it may have changed at this point
2809-
if let pin = pinsStore.pinsMap[dependency.packageRef.identity], pin.packageRef.equalsIncludingLocation(dependency.packageRef) {
2810-
continue
2888+
let pin = pinsStore.pinsMap[dependency.packageRef.identity]
2889+
// if pin not found, or location is different (it may have changed at this point) pin it
2890+
if !(pin?.packageRef.equalsIncludingLocation(dependency.packageRef) ?? false) {
2891+
pinsStore.pin(dependency)
28112892
}
2812-
} else if !pins.contains(dependency.packageRef.identity) {
2813-
// Otherwise, it should *not* be in the pins store.
2814-
continue
2893+
} else if let pin = pinsStore.pinsMap[dependency.packageRef.identity] {
2894+
// otherwise, it should *not* be in the pins store.
2895+
pinsStore.remove(pin)
28152896
}
2816-
2817-
return self.pinAll(
2818-
pinsStore: pinsStore,
2819-
dependencyManifests: dependencyManifests,
2820-
rootManifestsMinimumToolsVersion: rootManifestsMinimumToolsVersion,
2821-
observabilityScope: observabilityScope
2822-
)
28232897
}
2898+
2899+
return pinsStore
28242900
}
28252901

28262902
/// This enum represents state of an external package.
@@ -3296,6 +3372,8 @@ extension Workspace {
32963372
try workingCopy.checkout(revision: checkoutState.revision)
32973373
try? fileSystem.chmod(.userUnWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
32983374

3375+
print("********************* adding \(package.locationString) to managed deps")
3376+
32993377
// Write the state record.
33003378
self.state.dependencies.add(
33013379
try .sourceControlCheckout(
@@ -3557,6 +3635,17 @@ internal extension PackageReference {
35573635
}
35583636
}
35593637

3638+
fileprivate extension PackageReference.Kind {
3639+
var isPinnable: Bool {
3640+
switch self {
3641+
case .remoteSourceControl, .localSourceControl, .registry:
3642+
return true
3643+
default:
3644+
return false
3645+
}
3646+
}
3647+
}
3648+
35603649
// FIXME: remove this when remove the single call site that uses it
35613650
fileprivate extension PackageDependency {
35623651
var isLocal: Bool {

0 commit comments

Comments
 (0)