diff --git a/Sources/Workspace/Workspace+State.swift b/Sources/Workspace/Workspace+State.swift index e1a0a91cbd0..b311f7ce459 100644 --- a/Sources/Workspace/Workspace+State.swift +++ b/Sources/Workspace/Workspace+State.swift @@ -65,14 +65,22 @@ public final class WorkspaceState { try self.save() } + // marked public for testing public func save() throws { try self.storage.save(dependencies: self.dependencies, artifacts: self.artifacts) } /// Returns true if the state file exists on the filesystem. - public func stateFileExists() -> Bool { + func stateFileExists() -> Bool { return self.storage.fileExists() } + + /// Returns true if the state file exists on the filesystem. + func reload() throws { + let storedState = try self.storage.load() + self.dependencies = storedState.dependencies + self.artifacts = storedState.artifacts + } } // MARK: - Serialization diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 8089c4abb72..46d4799328a 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1069,6 +1069,10 @@ extension Workspace { customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none, observabilityScope: ObservabilityScope ) throws -> PackageGraph { + // reload state in case it was modified externally (eg by another process) before reloading the graph + // long running host processes (ie IDEs) need this in case other SwiftPM processes (ie CLI) made changes to the state + // such hosts processes call loadPackageGraph to make sure the workspace state is correct + try self.state.reload() // Perform dependency resolution, if required. let manifests: DependencyManifests diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index e6a2d91e3e4..169dabb5ff3 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -2757,6 +2757,92 @@ final class WorkspaceTests: XCTestCase { } } + func testStateModified() throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Root", + targets: [ + MockTarget(name: "Root", dependencies: [ + .product(name: "Foo", package: "foo"), + .product(name: "Bar", package: "bar") + ]), + ], + dependencies: [ + .sourceControl(url: "https://scm.com/org/foo", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(url: "https://scm.com/org/bar", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Foo", + url: "https://scm.com/org/foo", + targets: [ + MockTarget(name: "Foo"), + ], + products: [ + MockProduct(name: "Foo", targets: ["Foo"]), + ], + versions: [nil, "1.0.0", "1.1.0"] + ), + MockPackage( + name: "Bar", + url: "https://scm.com/org/bar", + targets: [ + MockTarget(name: "Bar"), + ], + products: [ + MockProduct(name: "Bar", targets: ["Bar"]), + ], + versions: ["1.0.0", "1.1.0", "1.2.0"] + ), + ] + ) + + try workspace.checkPackageGraph(roots: ["Root"], deps: []) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTester(graph) { result in + result.check(packages: "Bar", "Foo", "Root") + } + } + + let underlying = try workspace.getOrCreateWorkspace() + let fooEditPath = sandbox.appending(components: ["edited", "foo"]) + + // mimic external process putting a dependency into edit mode + do { + try fs.createDirectory(fooEditPath, recursive: true) + try fs.writeFileContents(fooEditPath.appending(component: "Package.swift"), bytes: "// swift-tools-version: 5.6") + + let fooState = underlying.state.dependencies[.plain("foo")]! + let externalState = WorkspaceState(fileSystem: fs, storageDirectory: underlying.state.storagePath.parentDirectory, initializationWarningHandler: { _ in }) + externalState.dependencies.remove(fooState.packageRef.identity) + externalState.dependencies.add(try fooState.edited(subpath: .init("foo"), unmanagedPath: fooEditPath)) + try externalState.save() + } + + // reload graph after "external" change + try workspace.checkPackageGraph(roots: ["Root"], deps: []) { graph, diagnostics in + PackageGraphTester(graph) { result in + result.check(packages: "Bar", "Foo", "Root") + } + } + + do { + let fooState = underlying.state.dependencies[.plain("foo")]! + guard case .edited(basedOn: _, unmanagedPath: fooEditPath) = fooState.state else { + XCTFail("'\(fooState.packageRef.identity)' dependency expected to be in edit mode, but was: \(fooState)") + return + } + } + } + func testSkipUpdate() throws { let sandbox = AbsolutePath("/tmp/ws/") let fs = InMemoryFileSystem()