Skip to content

Commit 9c3162f

Browse files
committed
[Commands] Add initial Workspace support for loading a package graph.
- This is the basic "load and fetch-if-necessary" operation used by all command line tools. - This doesn't yet support interacting with the dependency resolver to fetch packages which aren't checked out yet. - This is also showing a need for some more testing infrastructure around being able to test mock package graphs...
1 parent 9609b77 commit 9c3162f

File tree

2 files changed

+161
-5
lines changed

2 files changed

+161
-5
lines changed

Sources/Commands/Workspace.swift

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import Basic
1212
import PackageLoading
1313
import PackageModel
14+
import PackageGraph
1415
import SourceControl
1516
import Utility
1617

@@ -20,14 +21,43 @@ public enum WorkspaceOperationError: Swift.Error {
2021
case unavailableRepository
2122
}
2223

24+
/// Convenience initializer for Dictionary.
25+
//
26+
// FIXME: Lift to Basic?
27+
extension Dictionary {
28+
init<S: Sequence>(items: S) where S.Iterator.Element == (Key, Value) {
29+
var result = Dictionary.init()
30+
for (key, value) in items {
31+
result[key] = value
32+
}
33+
self = result
34+
}
35+
}
36+
37+
/// The delegate interface used by the workspace to report status information.
38+
public protocol WorkspaceDelegate: class {
39+
/// The workspace is fetching additional repositories in support of
40+
/// loading a complete package.
41+
func fetchingMissingRepositories(_ urls: Set<String>)
42+
}
43+
2344
/// A workspace represents the state of a working project directory.
2445
///
25-
/// This class is responsible for managing the persistent working state of a
46+
/// The workspace is responsible for managing the persistent working state of a
2647
/// project directory (e.g., the active set of checked out repositories) and for
2748
/// coordinating the changes to that state.
2849
///
50+
/// This class glues together the basic facilities provided by the dependency
51+
/// resolution, source control, and package graph loading subsystems into a
52+
/// cohesive interface for exposing the high-level operations for the package
53+
/// manager to maintain working package directories.
54+
///
2955
/// This class does *not* support concurrent operations.
3056
public class Workspace {
57+
/// An individual managed dependency.
58+
///
59+
/// Each dependency will have a checkout containing the sources at a
60+
/// particular revision, and may have an associated version.
3161
public struct ManagedDependency {
3262
/// The specifier for the dependency.
3363
public let repository: RepositorySpecifier
@@ -98,7 +128,10 @@ public class Workspace {
98128
])
99129
}
100130
}
101-
131+
132+
/// The delegate interface.
133+
public let delegate: WorkspaceDelegate
134+
102135
/// The path of the root package.
103136
public let rootPackagePath: AbsolutePath
104137

@@ -136,8 +169,10 @@ public class Workspace {
136169
public init(
137170
rootPackage path: AbsolutePath,
138171
dataPath: AbsolutePath? = nil,
139-
manifestLoader: ManifestLoaderProtocol
172+
manifestLoader: ManifestLoaderProtocol,
173+
delegate: WorkspaceDelegate
140174
) throws {
175+
self.delegate = delegate
141176
self.rootPackagePath = path
142177
self.dataPath = dataPath ?? path.appending(component: ".build")
143178
self.manifestLoader = manifestLoader
@@ -268,6 +303,57 @@ public class Workspace {
268303
return (root: rootManifest, dependencies: dependencies.map{ $0.item })
269304
}
270305

306+
/// Fetch and load the complete package at the given path.
307+
///
308+
/// This will implicitly cause any dependencies not yet present in the
309+
/// working checkouts to be resolved, cloned, and checked out.
310+
///
311+
/// When fetching additional dependencies, the existing checkout versions
312+
/// will never be re-bound (or even re-fetched) as a result of this
313+
/// operation. This implies that the resulting local state may not match
314+
/// what would be computed from a fresh clone, but this makes for a more
315+
/// consistent command line development experience.
316+
///
317+
/// - Returns: The loaded package graph.
318+
/// - Throws: Rethrows errors from dependency resolution (if required) and package graph loading.
319+
public func loadPackageGraph() throws -> PackageGraph {
320+
// First, load the active manifest sets.
321+
let (rootManifest, currentExternalManifests) = try loadDependencyManifests()
322+
323+
// Check for missing checkouts.
324+
let manifestsMap = Dictionary<String, Manifest>(
325+
items: [(rootManifest.url, rootManifest)] + currentExternalManifests.map{ ($0.url, $0) })
326+
let availableURLs = Set<String>(manifestsMap.keys)
327+
var requiredURLs = transitiveClosure([rootManifest.url]) { url in
328+
guard let manifest = manifestsMap[url] else { return [] }
329+
return manifest.package.dependencies.map{ $0.url }
330+
}
331+
requiredURLs.insert(rootManifest.url)
332+
333+
// We should never have loaded a manifest we don't need.
334+
assert(availableURLs.isSubset(of: requiredURLs))
335+
336+
// If there are have missing URLs, we need to fetch them now.
337+
let missingURLs = requiredURLs.subtracting(availableURLs)
338+
let externalManifests = currentExternalManifests
339+
if !missingURLs.isEmpty {
340+
// Inform the delegate.
341+
delegate.fetchingMissingRepositories(missingURLs)
342+
343+
// Perform dependency resolution using the constraint set induced by the active checkouts.
344+
//
345+
// FIXME: We are going to need to a way to tell the resolution
346+
// algorithm that certain repositories are pinned to the current
347+
// checkout. We might be able to do that simply by overriding the
348+
// view presented by the repository container provider.
349+
350+
fatalError("FIXME: Unimplemented.")
351+
}
352+
353+
// We've loaded the complete set of manifests, load the graph.
354+
return try PackageGraphLoader().load(rootManifest: rootManifest, externalManifests: externalManifests)
355+
}
356+
271357
// MARK: Persistence
272358

273359
// FIXME: A lot of the persistence mechanism here is copied from

Tests/CommandsTests/WorkspaceTests.swift

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ import TestSupport
2626

2727
private let sharedManifestLoader = ManifestLoader(resources: Resources())
2828

29+
private class TestWorkspaceDelegate: WorkspaceDelegate {
30+
func fetchingMissingRepositories(_ urls: Set<String>) {
31+
}
32+
}
33+
2934
extension Workspace {
3035
convenience init(rootPackage path: AbsolutePath) throws {
31-
try self.init(rootPackage: path, manifestLoader: sharedManifestLoader)
36+
try self.init(rootPackage: path, manifestLoader: sharedManifestLoader, delegate: TestWorkspaceDelegate())
3237
}
3338
}
3439

@@ -156,7 +161,7 @@ final class WorkspaceTests: XCTestCase {
156161
])
157162

158163
// Create the workspace.
159-
let workspace = try Workspace(rootPackage: path, manifestLoader: mockManifestLoader)
164+
let workspace = try Workspace(rootPackage: path, manifestLoader: mockManifestLoader, delegate: TestWorkspaceDelegate())
160165

161166
// Ensure we have checkouts for A & AA.
162167
for name in ["A", "AA"] {
@@ -179,8 +184,73 @@ final class WorkspaceTests: XCTestCase {
179184
}
180185
}
181186

187+
/// Check the basic ability to load a graph from the workspace
188+
func testPackageGraphLoadingBasics() {
189+
// We mock up the following dep graph:
190+
//
191+
// Root
192+
// \ A: checked out (@v1)
193+
//
194+
// FIXME: We need better infrastructure for mocking up the things we
195+
// want to test here.
196+
197+
mktmpdir { path in
198+
// Create the test repositories, we don't need them to have actual
199+
// contents (the manifests are mocked).
200+
var repos: [String: RepositorySpecifier] = [:]
201+
for name in ["A"] {
202+
let repoPath = path.appending(component: name)
203+
try makeDirectories(repoPath)
204+
initGitRepo(repoPath, tag: "initial")
205+
repos[name] = RepositorySpecifier(url: repoPath.asString)
206+
}
207+
208+
// Create the mock manifests.
209+
let rootManifest = Manifest(
210+
path: path.appending(component: Manifest.filename),
211+
url: path.asString,
212+
package: PackageDescription.Package(
213+
name: "Root",
214+
dependencies: [
215+
.Package(url: repos["A"]!.url, majorVersion: 1),
216+
]),
217+
products: [],
218+
version: nil
219+
)
220+
let aManifest = Manifest(
221+
path: AbsolutePath(repos["A"]!.url).appending(component: Manifest.filename),
222+
url: repos["A"]!.url,
223+
package: PackageDescription.Package(name: "A"),
224+
products: [],
225+
version: v1
226+
)
227+
let mockManifestLoader = MockManifestLoader(manifests: [
228+
MockManifestLoader.Key(url: path.asString, version: nil): rootManifest,
229+
MockManifestLoader.Key(url: repos["A"]!.url, version: v1): aManifest,
230+
])
231+
232+
// Create the workspace.
233+
let workspace = try Workspace(rootPackage: path, manifestLoader: mockManifestLoader, delegate: TestWorkspaceDelegate())
234+
235+
// Ensure we have a checkout for A.
236+
for name in ["A"] {
237+
let revision = try GitRepository(path: AbsolutePath(repos[name]!.url)).getCurrentRevision()
238+
_ = try workspace.clone(repository: repos[name]!, at: revision, for: v1)
239+
}
240+
241+
// Load the package graph.
242+
let graph = try workspace.loadPackageGraph()
243+
244+
// Validate the graph has the correct basic structure.
245+
XCTAssertEqual(graph.packages.count, 2)
246+
XCTAssertEqual(graph.packages[0].name, "Root")
247+
XCTAssertEqual(graph.packages[1].name, "A")
248+
}
249+
}
250+
182251
static var allTests = [
183252
("testBasics", testBasics),
184253
("testDependencyManifestLoading", testDependencyManifestLoading),
254+
("testPackageGraphLoadingBasics", testPackageGraphLoadingBasics),
185255
]
186256
}

0 commit comments

Comments
 (0)