Skip to content

Commit d6ed78f

Browse files
committed
[Commands] Add an option to force resolution to Package.resolved
This adds a new flag `--force-resolved-versions` which will force resolve to versions recorded in the Package.resolved file. The entries in Package.resolved can be considered as "soft" pins. Since the requirements in root manifest and Package.resolved can change anytime, most SwiftPM operations (that load the package graph) first check if the current state of things is still correct. However, it is sometimes useful to force resolve to the dependencies recorded in the resolved file by entirely bypassing the dependency resolution. <rdar://problem/45290071> Add option to not re-resolve, but use Package.resolved explicitly
1 parent 0af02ed commit d6ed78f

File tree

6 files changed

+200
-5
lines changed

6 files changed

+200
-5
lines changed

Sources/Commands/Options.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,8 @@ public class ToolOptions {
6161
/// Whether to enable code coverage.
6262
public var shouldEnableCodeCoverage = false
6363

64+
/// Use Package.resolved file for resolving dependencies.
65+
public var forceResolvedVersions = false
66+
6467
public required init() {}
6568
}

Sources/Commands/SwiftTool.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ public class SwiftTool<Options: ToolOptions> {
351351
usage: "Enable building with the llbuild library"),
352352
to: { $0.shouldEnableLLBuildLibrary = $1 })
353353

354+
binder.bind(
355+
option: parser.add(option: "--force-resolved-versions", kind: Bool.self,
356+
usage: "Force resolve to versions recorded in the Package.resolved file"),
357+
to: { $0.forceResolvedVersions = $1 })
358+
354359
// Let subclasses bind arguments.
355360
type(of: self).defineArguments(parser: parser, binder: binder)
356361

@@ -502,7 +507,13 @@ public class SwiftTool<Options: ToolOptions> {
502507
/// Resolve the dependencies.
503508
func resolve() throws {
504509
let workspace = try getActiveWorkspace()
505-
workspace.resolve(root: try getWorkspaceRoot(), diagnostics: diagnostics)
510+
let root = try getWorkspaceRoot()
511+
512+
if options.forceResolvedVersions {
513+
workspace.resolveToResolvedVersion(root: root, diagnostics: diagnostics)
514+
} else {
515+
workspace.resolve(root: root, diagnostics: diagnostics)
516+
}
506517

507518
// Throw if there were errors when loading the graph.
508519
// The actual errors will be printed before exiting.
@@ -523,6 +534,7 @@ public class SwiftTool<Options: ToolOptions> {
523534
let graph = try workspace.loadPackageGraph(
524535
root: getWorkspaceRoot(),
525536
createREPLProduct: createREPLProduct,
537+
forceResolvedVersions: options.forceResolvedVersions,
526538
diagnostics: diagnostics
527539
)
528540

Sources/TestSupport/TestWorkspace.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,15 @@ public final class TestWorkspace {
254254
public func checkPackageGraph(
255255
roots: [String] = [],
256256
dependencies: [PackageGraphRootInput.PackageDependency] = [],
257+
forceResolvedVersions: Bool = false,
257258
_ result: (PackageGraph, DiagnosticsEngine) -> ()
258259
) {
259260
let diagnostics = DiagnosticsEngine()
260261
let workspace = createWorkspace()
261262
let rootInput = PackageGraphRootInput(
262263
packages: rootPaths(for: roots), dependencies: dependencies)
263-
let graph = workspace.loadPackageGraph(root: rootInput, diagnostics: diagnostics)
264+
let graph = workspace.loadPackageGraph(
265+
root: rootInput, forceResolvedVersions: forceResolvedVersions, diagnostics: diagnostics)
264266
result(graph, diagnostics)
265267
}
266268

@@ -379,8 +381,10 @@ public final class TestWorkspace {
379381
switch state {
380382
case .version(let version):
381383
XCTAssertEqual(pin.state.version, version, file: file, line: line)
382-
case .revision, .branch:
383-
XCTFail("Unimplemented", file: file, line: line)
384+
case .revision(let revision):
385+
XCTAssertEqual(pin.state.revision.identifier, revision, file: file, line: line)
386+
case .branch(let branch):
387+
XCTAssertEqual(pin.state.branch, branch, file: file, line: line)
384388
}
385389
case .edited, .local:
386390
XCTFail("Unimplemented", file: file, line: line)

Sources/Workspace/Workspace.swift

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,11 +588,17 @@ extension Workspace {
588588
root: PackageGraphRootInput,
589589
createMultipleTestProducts: Bool = false,
590590
createREPLProduct: Bool = false,
591+
forceResolvedVersions: Bool = false,
591592
diagnostics: DiagnosticsEngine
592593
) -> PackageGraph {
593594

594595
// Perform dependency resolution, if required.
595-
let manifests = self._resolve(root: root, diagnostics: diagnostics)
596+
let manifests: DependencyManifests
597+
if forceResolvedVersions {
598+
manifests = self._resolveToResolvedVersion(root: root, diagnostics: diagnostics)
599+
} else {
600+
manifests = self._resolve(root: root, diagnostics: diagnostics)
601+
}
596602
let externalManifests = manifests.allManifests()
597603

598604
// Load the graph.
@@ -1059,6 +1065,73 @@ extension Workspace {
10591065

10601066
extension Workspace {
10611067

1068+
/// Resolves the dependencies according to the entries present in the Package.resolved file.
1069+
///
1070+
/// This method bypasses the dependency resolution and resolves dependencies
1071+
/// according to the information in the resolved file.
1072+
public func resolveToResolvedVersion(
1073+
root: PackageGraphRootInput,
1074+
diagnostics: DiagnosticsEngine
1075+
) {
1076+
_resolveToResolvedVersion(root: root, diagnostics: diagnostics)
1077+
}
1078+
1079+
/// Resolves the dependencies according to the entries present in the Package.resolved file.
1080+
///
1081+
/// This method bypasses the dependency resolution and resolves dependencies
1082+
/// according to the information in the resolved file.
1083+
@discardableResult
1084+
fileprivate func _resolveToResolvedVersion(
1085+
root: PackageGraphRootInput,
1086+
diagnostics: DiagnosticsEngine
1087+
) -> DependencyManifests {
1088+
createCacheDirectories(with: diagnostics)
1089+
1090+
let rootManifests = loadRootManifests(packages: root.packages, diagnostics: diagnostics)
1091+
let graphRoot = PackageGraphRoot(input: root, manifests: rootManifests)
1092+
1093+
// Load the pins store or abort now.
1094+
guard let pinsStore = diagnostics.wrap({ try self.pinsStore.load() }), !diagnostics.hasErrors else {
1095+
return loadDependencyManifests(root: graphRoot, diagnostics: diagnostics)
1096+
}
1097+
1098+
// Request all the containers to fetch them in parallel.
1099+
//
1100+
// We just request the packages here, repository manager will
1101+
// automatically manage the parallelism.
1102+
let pins = pinsStore.pins.map({ $0 })
1103+
DispatchQueue.concurrentPerform(iterations: pins.count) { idx in
1104+
_ = try? await {
1105+
containerProvider.getContainer(for: pins[idx].packageRef, skipUpdate: true, completion: $0)
1106+
}
1107+
}
1108+
1109+
// Compute the pins that we need to actually clone.
1110+
//
1111+
// We require cloning if there is no checkout or if the checkout doesn't
1112+
// match with the pin.
1113+
let requiredPins = pins.filter({ pin in
1114+
guard let dependency = managedDependencies[forURL: pin.packageRef.path] else {
1115+
return true
1116+
}
1117+
switch dependency.state {
1118+
case .checkout(let checkoutState):
1119+
return pin.state != checkoutState
1120+
case .edited, .local:
1121+
return true
1122+
}
1123+
})
1124+
1125+
// Clone the required pins.
1126+
for pin in requiredPins {
1127+
diagnostics.wrap {
1128+
_ = try self.clone(package: pin.packageRef, at: pin.state)
1129+
}
1130+
}
1131+
1132+
return loadDependencyManifests(root: graphRoot, diagnostics: diagnostics)
1133+
}
1134+
10621135
/// Implementation of resolve(root:diagnostics:).
10631136
///
10641137
/// The extra constraints will be added to the main requirements.

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,6 +2588,108 @@ final class WorkspaceTests: XCTestCase {
25882588
XCTAssertNotNil(ws.managedDependencies[forURL: "/tmp/ws/pkgs/Nested/Foo"])
25892589
}
25902590
}
2591+
2592+
func testForceResolveToResolvedVersions() throws {
2593+
let sandbox = AbsolutePath("/tmp/ws/")
2594+
let fs = InMemoryFileSystem()
2595+
2596+
let workspace = try TestWorkspace(
2597+
sandbox: sandbox,
2598+
fs: fs,
2599+
roots: [
2600+
TestPackage(
2601+
name: "Root",
2602+
targets: [
2603+
TestTarget(name: "Root", dependencies: ["Foo", "Bar"]),
2604+
],
2605+
products: [],
2606+
dependencies: [
2607+
TestDependency(name: "Foo", requirement: .upToNextMajor(from: "1.0.0")),
2608+
TestDependency(name: "Bar", requirement: .upToNextMajor(from: "1.0.0")),
2609+
]
2610+
),
2611+
],
2612+
packages: [
2613+
TestPackage(
2614+
name: "Foo",
2615+
targets: [
2616+
TestTarget(name: "Foo"),
2617+
],
2618+
products: [
2619+
TestProduct(name: "Foo", targets: ["Foo"]),
2620+
],
2621+
versions: ["1.0.0", "1.2.0", "1.3.2"]
2622+
),
2623+
TestPackage(
2624+
name: "Bar",
2625+
targets: [
2626+
TestTarget(name: "Bar"),
2627+
],
2628+
products: [
2629+
TestProduct(name: "Bar", targets: ["Bar"]),
2630+
],
2631+
versions: ["1.0.0", "develop"]
2632+
),
2633+
]
2634+
)
2635+
2636+
// Load the initial graph.
2637+
let deps: [TestWorkspace.PackageDependency] = [
2638+
.init(name: "Bar", requirement: .revision("develop")),
2639+
]
2640+
workspace.checkPackageGraph(roots: ["Root"], deps: deps) { (graph, diagnostics) in
2641+
XCTAssertNoDiagnostics(diagnostics)
2642+
}
2643+
workspace.checkManagedDependencies() { result in
2644+
result.check(dependency: "foo", at: .checkout(.version("1.3.2")))
2645+
result.check(dependency: "bar", at: .checkout(.branch("develop")))
2646+
}
2647+
workspace.checkResolved { result in
2648+
result.check(dependency: "foo", at: .checkout(.version("1.3.2")))
2649+
result.check(dependency: "bar", at: .checkout(.branch("develop")))
2650+
}
2651+
2652+
// Change pin of foo to something else.
2653+
do {
2654+
let ws = workspace.createWorkspace()
2655+
let pinsStore = try ws.pinsStore.load()
2656+
let fooPin = pinsStore.pins.first(where: { $0.packageRef.identity == "foo" })!
2657+
2658+
let fooRepo = workspace.repoProvider.specifierMap[RepositorySpecifier(url: fooPin.packageRef.path)]!
2659+
let revision = try fooRepo.resolveRevision(tag: "1.0.0")
2660+
let newState = CheckoutState(revision: revision, version: "1.0.0")
2661+
2662+
pinsStore.pin(packageRef: fooPin.packageRef, state: newState )
2663+
try pinsStore.saveState()
2664+
}
2665+
2666+
// Check force resolve. This should still produce bar @ develop even
2667+
// though that requirement is gone.
2668+
workspace.checkPackageGraph(roots: ["Root"], forceResolvedVersions: true) { (graph, diagnostics) in
2669+
XCTAssertNoDiagnostics(diagnostics)
2670+
}
2671+
workspace.checkManagedDependencies() { result in
2672+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
2673+
result.check(dependency: "bar", at: .checkout(.branch("develop")))
2674+
}
2675+
workspace.checkResolved { result in
2676+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
2677+
result.check(dependency: "bar", at: .checkout(.branch("develop")))
2678+
}
2679+
2680+
// A normal resolution.
2681+
workspace.checkPackageGraph(roots: ["Root"]) { (graph, diagnostics) in
2682+
XCTAssertNoDiagnostics(diagnostics)
2683+
}
2684+
workspace.checkManagedDependencies() { result in
2685+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
2686+
result.check(dependency: "bar", at: .checkout(.version("1.0.0")))
2687+
}
2688+
workspace.checkResolved { result in
2689+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
2690+
result.check(dependency: "bar", at: .checkout(.version("1.0.0")))
2691+
}
2692+
}
25912693
}
25922694

25932695
extension PackageGraph {

Tests/WorkspaceTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ extension WorkspaceTests {
5353
("testDependencyResolutionWithEdit", testDependencyResolutionWithEdit),
5454
("testDependencySwitchWithSameIdentity", testDependencySwitchWithSameIdentity),
5555
("testEditDependency", testEditDependency),
56+
("testForceResolveToResolvedVersions", testForceResolveToResolvedVersions),
5657
("testGraphData", testGraphData),
5758
("testGraphRootDependencies", testGraphRootDependencies),
5859
("testInterpreterFlags", testInterpreterFlags),

0 commit comments

Comments
 (0)