Skip to content

Commit bb2158d

Browse files
authored
Optionally use a per-package filesystem for PackageBuilder (swiftlang#3827)
Currently, we have the ability to provide a custom file system to the whole workspace and we partially use git-backed file system during package resolution. This change aims to allow using a custom file system when running `PackageBuilder` as well which e.g. allows to load a graph without having full checkouts of every package. To utilize this feature, clients of `Workspace` can pass a custom `PackageContainerProvider` and use the new `CustomPackageContainer` protocol.
1 parent 7b8a90a commit bb2158d

14 files changed

+292
-47
lines changed

Sources/PackageGraph/GraphLoadingNode.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,30 @@ public struct GraphLoadingNode: Equatable, Hashable {
2828
/// The product filter applied to the package.
2929
public let productFilter: ProductFilter
3030

31-
public init(identity: PackageIdentity, manifest: Manifest, productFilter: ProductFilter) {
31+
/// The file system to use for loading the given package.
32+
public let fileSystem: FileSystem
33+
34+
public init(identity: PackageIdentity, manifest: Manifest, productFilter: ProductFilter, fileSystem: FileSystem) {
3235
self.identity = identity
3336
self.manifest = manifest
3437
self.productFilter = productFilter
38+
self.fileSystem = fileSystem
3539
}
3640

3741
/// Returns the dependencies required by this node.
3842
internal func requiredDependencies() -> [PackageDependency] {
3943
return manifest.dependenciesRequired(for: productFilter)
4044
}
45+
46+
public func hash(into hasher: inout Hasher) {
47+
hasher.combine(identity)
48+
hasher.combine(manifest)
49+
hasher.combine(productFilter)
50+
}
51+
52+
public static func == (lhs: GraphLoadingNode, rhs: GraphLoadingNode) -> Bool {
53+
return lhs.identity == rhs.identity && lhs.manifest == rhs.manifest && lhs.productFilter == rhs.productFilter
54+
}
4155
}
4256

4357
extension GraphLoadingNode: CustomStringConvertible {

Sources/PackageGraph/PackageContainer.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import Basics
1212
import Dispatch
1313
import PackageModel
14+
import struct TSCBasic.AbsolutePath
15+
import protocol TSCBasic.FileSystem
1416
import struct TSCUtility.Version
1517

1618
/// A container of packages.
@@ -103,6 +105,24 @@ extension PackageContainer {
103105
}
104106
}
105107

108+
public protocol CustomPackageContainer: PackageContainer {
109+
/// Retrieve the package using this package container.
110+
func retrieve(
111+
at version: Version,
112+
progressHandler: ((_ bytesReceived: Int64, _ totalBytes: Int64?) -> Void)?,
113+
observabilityScope: ObservabilityScope
114+
) throws -> AbsolutePath
115+
116+
/// Get the custom file system for this package container.
117+
func getFileSystem() throws -> FileSystem?
118+
}
119+
120+
public extension CustomPackageContainer {
121+
func retrieve(at version: Version, observabilityScope: ObservabilityScope) throws -> AbsolutePath {
122+
return try self.retrieve(at: version, progressHandler: .none, observabilityScope: observabilityScope)
123+
}
124+
}
125+
106126
// MARK: - PackageContainerConstraint
107127

108128
/// An individual constraint onto a container.

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extension PackageGraph {
2020
root: PackageGraphRoot,
2121
identityResolver: IdentityResolver,
2222
additionalFileRules: [FileRuleDescription] = [],
23-
externalManifests: OrderedDictionary<PackageIdentity, Manifest>,
23+
externalManifests: OrderedDictionary<PackageIdentity, (manifest: Manifest, fs: FileSystem)>,
2424
requiredDependencies: Set<PackageReference> = [],
2525
unsafeAllowedPackages: Set<PackageReference> = [],
2626
binaryArtifacts: [BinaryArtifact] = [],
@@ -37,28 +37,28 @@ extension PackageGraph {
3737
var manifestMap = externalManifests
3838
// prefer roots
3939
root.manifests.forEach {
40-
manifestMap[$0.key] = $0.value
40+
manifestMap[$0.key] = ($0.value, fileSystem)
4141
}
4242

4343
let successors: (GraphLoadingNode) -> [GraphLoadingNode] = { node in
4444
node.requiredDependencies().compactMap{ dependency in
45-
return manifestMap[dependency.identity].map { manifest in
46-
GraphLoadingNode(identity: dependency.identity, manifest: manifest, productFilter: dependency.productFilter)
45+
return manifestMap[dependency.identity].map { (manifest, fileSystem) in
46+
GraphLoadingNode(identity: dependency.identity, manifest: manifest, productFilter: dependency.productFilter, fileSystem: fileSystem)
4747
}
4848
}
4949
}
5050

5151
// Construct the root manifest and root dependencies set.
5252
let rootManifestSet = Set(root.manifests.values)
5353
let rootDependencies = Set(root.dependencies.compactMap{
54-
manifestMap[$0.identity]
54+
manifestMap[$0.identity]?.manifest
5555
})
5656
let rootManifestNodes = root.packages.map { identity, package in
57-
GraphLoadingNode(identity: identity, manifest: package.manifest, productFilter: .everything)
57+
GraphLoadingNode(identity: identity, manifest: package.manifest, productFilter: .everything, fileSystem: fileSystem)
5858
}
5959
let rootDependencyNodes = root.dependencies.lazy.compactMap { (dependency: PackageDependency) -> GraphLoadingNode? in
6060
manifestMap[dependency.identity].map {
61-
GraphLoadingNode(identity: dependency.identity, manifest: $0, productFilter: dependency.productFilter)
61+
GraphLoadingNode(identity: dependency.identity, manifest: $0.manifest, productFilter: dependency.productFilter, fileSystem: $0.fs)
6262
}
6363
}
6464
let inputManifests = rootManifestNodes + rootDependencyNodes
@@ -82,7 +82,8 @@ extension PackageGraph {
8282
let merged = GraphLoadingNode(
8383
identity: node.identity,
8484
manifest: node.manifest,
85-
productFilter: existing.productFilter.union(node.productFilter)
85+
productFilter: existing.productFilter.union(node.productFilter),
86+
fileSystem: node.fileSystem
8687
)
8788
flattenedManifests[node.identity] = merged
8889
} else {
@@ -117,7 +118,7 @@ extension PackageGraph {
117118
xcTestMinimumDeploymentTargets: xcTestMinimumDeploymentTargets,
118119
shouldCreateMultipleTestProducts: shouldCreateMultipleTestProducts,
119120
createREPLProduct: manifest.packageKind.isRoot ? createREPLProduct : false,
120-
fileSystem: fileSystem,
121+
fileSystem: node.fileSystem,
121122
observabilityScope: nodeObservabilityScope
122123
)
123124
let package = try builder.construct()

Sources/SPMTestSupport/MockPackageContainer.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import XCTest
1818

1919
import struct TSCUtility.Version
2020

21-
public class MockPackageContainer: PackageContainer {
21+
public class MockPackageContainer: CustomPackageContainer {
2222
public typealias Constraint = PackageContainerConstraint
2323

2424
public typealias Dependency = (container: PackageReference, requirement: PackageRequirement)
@@ -28,6 +28,8 @@ public class MockPackageContainer: PackageContainer {
2828
let dependencies: [String: [Dependency]]
2929
let filteredMode: Bool
3030
let filteredDependencies: [ProductFilter: [Dependency]]
31+
let fileSystem: FileSystem?
32+
let customRetrievalPath: AbsolutePath?
3133

3234
public var unversionedDeps: [MockPackageContainer.Constraint] = []
3335

@@ -81,6 +83,18 @@ public class MockPackageContainer: PackageContainer {
8183
return true
8284
}
8385

86+
public func retrieve(at version: Version, progressHandler: ((Int64, Int64?) -> Void)?, observabilityScope: ObservabilityScope) throws -> AbsolutePath {
87+
if let path = customRetrievalPath {
88+
return path
89+
} else {
90+
throw StringError("no path configured for mock package container")
91+
}
92+
}
93+
94+
public func getFileSystem() throws -> FileSystem? {
95+
return fileSystem
96+
}
97+
8498
public convenience init(
8599
name: String,
86100
dependenciesByVersion: [Version: [(container: String, versionRequirement: VersionSetSpecifier)]]
@@ -100,13 +114,18 @@ public class MockPackageContainer: PackageContainer {
100114

101115
public init(
102116
package: PackageReference,
103-
dependencies: [String: [Dependency]] = [:]
117+
dependencies: [String: [Dependency]] = [:],
118+
fileSystem: FileSystem? = nil,
119+
customRetrievalPath: AbsolutePath? = nil
104120
) {
105121
self.package = package
106122
self._versions = dependencies.keys.compactMap(Version.init(_:)).sorted()
107123
self.dependencies = dependencies
108124
self.filteredMode = false
109125
self.filteredDependencies = [:]
126+
127+
self.fileSystem = fileSystem
128+
self.customRetrievalPath = customRetrievalPath
110129
}
111130

112131
public init(
@@ -128,6 +147,9 @@ public class MockPackageContainer: PackageContainer {
128147
self.dependencies = [:]
129148
self.filteredMode = true
130149
self.filteredDependencies = dependencies
150+
151+
self.fileSystem = nil
152+
self.customRetrievalPath = nil
131153
}
132154
}
133155

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public final class MockWorkspace {
2828
public let archiver: MockArchiver
2929
public let checksumAlgorithm: MockHashAlgorithm
3030
public let fingerprintStorage: MockPackageFingerprintStorage
31+
public let customPackageContainerProvider: MockPackageContainerProvider?
3132
let roots: [MockPackage]
3233
let packages: [MockPackage]
3334
public let mirrors: DependencyMirrors
@@ -51,6 +52,7 @@ public final class MockWorkspace {
5152
customBinaryArchiver: MockArchiver? = .none,
5253
customChecksumAlgorithm: MockHashAlgorithm? = .none,
5354
customFingerprintStorage: MockPackageFingerprintStorage? = .none,
55+
customPackageContainerProvider: MockPackageContainerProvider? = .none,
5456
resolverUpdateEnabled: Bool = true
5557
) throws {
5658
let archiver = customBinaryArchiver ?? MockArchiver()
@@ -64,6 +66,7 @@ public final class MockWorkspace {
6466
self.fingerprintStorage = customFingerprintStorage ?? MockPackageFingerprintStorage()
6567
self.mirrors = mirrors ?? DependencyMirrors()
6668
self.identityResolver = DefaultIdentityResolver(locationMapper: self.mirrors.effectiveURL(for:))
69+
self.customPackageContainerProvider = customPackageContainerProvider
6770
self.roots = roots
6871
self.packages = packages
6972

@@ -115,7 +118,17 @@ public final class MockWorkspace {
115118
case .fileSystem(let path):
116119
packagePath = basePath.appending(path)
117120
case .sourceControl(let url):
118-
packagePath = basePath.appending(components: "sourceControl", url.absoluteString.spm_mangledToC99ExtendedIdentifier())
121+
if let containerProvider = customPackageContainerProvider {
122+
let observability = ObservabilitySystem.makeForTesting()
123+
let packageRef = PackageReference(identity: PackageIdentity(url: url), kind: .remoteSourceControl(url))
124+
let container = try temp_await { containerProvider.getContainer(for: packageRef, skipUpdate: true, observabilityScope: observability.topScope, on: .sharedConcurrent, completion: $0) }
125+
guard let customContainer = container as? CustomPackageContainer else {
126+
throw StringError("invalid custom container: \(container)")
127+
}
128+
packagePath = try customContainer.retrieve(at: try Version(versionString: package.versions.first!!), observabilityScope: observability.topScope)
129+
} else {
130+
packagePath = basePath.appending(components: "sourceControl", url.absoluteString.spm_mangledToC99ExtendedIdentifier())
131+
}
119132
case .registry(let identity):
120133
packagePath = basePath.appending(components: "registry", identity.description.spm_mangledToC99ExtendedIdentifier())
121134
}
@@ -233,6 +246,7 @@ public final class MockWorkspace {
233246
mirrors: self.mirrors,
234247
customToolsVersion: self.toolsVersion,
235248
customManifestLoader: self.manifestLoader,
249+
customPackageContainerProvider: self.customPackageContainerProvider,
236250
customRepositoryProvider: self.repositoryProvider,
237251
customRegistryClient: self.registryClient,
238252
customIdentityResolver: self.identityResolver,
@@ -510,6 +524,7 @@ public final class MockWorkspace {
510524
case registryDownload(TSCUtility.Version)
511525
case edited(AbsolutePath?)
512526
case local
527+
case custom(TSCUtility.Version, AbsolutePath)
513528
}
514529

515530
public struct ManagedDependencyResult {
@@ -568,6 +583,11 @@ public final class MockWorkspace {
568583
XCTFail("Expected local dependency", file: file, line: line)
569584
return
570585
}
586+
case .custom(let currentVersion, let currentPath):
587+
guard case .custom(let version, let path) = dependency.state else {
588+
return XCTFail("invalid dependency state \(dependency.state)", file: file, line: line)
589+
}
590+
XCTAssertTrue(currentVersion == version && currentPath == path, file: file, line: line)
571591
}
572592
}
573593
}
@@ -694,7 +714,7 @@ public final class MockWorkspace {
694714
return XCTFail("invalid pin state \(pin.state)", file: file, line: line)
695715
}
696716
XCTAssertEqual(pinVersion, downloadVersion, file: file, line: line)
697-
case .edited, .local:
717+
case .edited, .local, .custom:
698718
XCTFail("Unimplemented", file: file, line: line)
699719
}
700720
}

Sources/SPMTestSupport/misc.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ public func loadPackageGraph(
231231
observabilityScope: ObservabilityScope
232232
) throws -> PackageGraph {
233233
let rootManifests = manifests.filter { $0.packageKind.isRoot }.spm_createDictionary{ ($0.path, $0) }
234-
let externalManifests = try manifests.filter { !$0.packageKind.isRoot }.reduce(into: OrderedDictionary<PackageIdentity, Manifest>()) { partial, item in
235-
partial[try identityResolver.resolveIdentity(for: item.packageKind)] = item
234+
let externalManifests = try manifests.filter { !$0.packageKind.isRoot }.reduce(into: OrderedDictionary<PackageIdentity, (manifest: Manifest, fs: FileSystem)>()) { partial, item in
235+
partial[try identityResolver.resolveIdentity(for: item.packageKind)] = (item, fs)
236236
}
237237

238238
let packages = Array(rootManifests.keys)

Sources/Workspace/Diagnostics.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ extension Basics.Diagnostic {
127127
.warning("dependency '\(packageName)' is missing; downloading again")
128128
}
129129

130+
static func customDependencyMissing(packageName: String) -> Self {
131+
.warning("dependency '\(packageName)' is missing; retrieving again")
132+
}
133+
130134
static func artifactChecksumChanged(targetName: String) -> Self {
131135
.error("artifact of binary target '\(targetName)' has changed checksum; this is a potential security risk so the new artifact won't be downloaded")
132136
}

Sources/Workspace/FileSystemPackageContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import TSCBasic
2121
/// There is no need to perform any git operations on such packages and they
2222
/// should be used as-is. In fact, they might not even have a git repository.
2323
/// Examples: Root packages, local dependencies, edited packages.
24-
internal struct FileSystemPackageContainer: PackageContainer {
24+
public struct FileSystemPackageContainer: PackageContainer {
2525
public let package: PackageReference
2626
private let identityResolver: IdentityResolver
2727
private let manifestLoader: ManifestLoaderProtocol

Sources/Workspace/ManagedDependency.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ extension Workspace {
3939
/// for top of the tree style development.
4040
case edited(basedOn: ManagedDependency?, unmanagedPath: AbsolutePath?)
4141

42+
case custom(version: Version, path: AbsolutePath)
43+
4244
public var description: String {
4345
switch self {
4446
case .fileSystem(let path):
@@ -49,6 +51,8 @@ extension Workspace {
4951
return "registryDownload (\(version))"
5052
case .edited:
5153
return "edited"
54+
case .custom:
55+
return "custom"
5256
}
5357
}
5458
}

Sources/Workspace/ResolverPrecomputationProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ struct ResolverPrecomputationProvider: PackageContainerProvider {
6161
) {
6262
queue.async {
6363
// Start by searching manifests from the Workspace's resolved dependencies.
64-
if let manifest = self.dependencyManifests.dependencies.first(where: { _, managed, _ in managed.packageRef == package }) {
64+
if let manifest = self.dependencyManifests.dependencies.first(where: { _, managed, _, _ in managed.packageRef == package }) {
6565
let container = LocalPackageContainer(
6666
package: package,
6767
manifest: manifest.manifest,

0 commit comments

Comments
 (0)