diff --git a/Sources/Commands/APIDigester.swift b/Sources/Commands/APIDigester.swift index 59ae02c7c5e..7df8a65c44e 100644 --- a/Sources/Commands/APIDigester.swift +++ b/Sources/Commands/APIDigester.swift @@ -33,12 +33,6 @@ struct APIDigesterBaselineDumper { /// The input build parameters. let inputBuildParameters: BuildParameters - /// The manifest loader. - let manifestLoader: ManifestLoaderProtocol - - /// The repository manager. - let repositoryManager: RepositoryManager - /// The API digester tool. let apiDigesterTool: SwiftAPIDigester @@ -49,16 +43,12 @@ struct APIDigesterBaselineDumper { baselineRevision: Revision, packageRoot: AbsolutePath, buildParameters: BuildParameters, - manifestLoader: ManifestLoaderProtocol, - repositoryManager: RepositoryManager, apiDigesterTool: SwiftAPIDigester, diags: DiagnosticsEngine ) { self.baselineRevision = baselineRevision self.packageRoot = packageRoot self.inputBuildParameters = buildParameters - self.manifestLoader = manifestLoader - self.repositoryManager = repositoryManager self.apiDigesterTool = apiDigesterTool self.diags = diags } @@ -93,8 +83,9 @@ struct APIDigesterBaselineDumper { } // Clone the current package in a sandbox and checkout the baseline revision. + let repositoryProvider = GitRepositoryProvider() let specifier = RepositorySpecifier(url: baselinePackageRoot.pathString) - let workingCopy = try repositoryManager.provider.createWorkingCopy( + let workingCopy = try repositoryProvider.createWorkingCopy( repository: specifier, sourcePath: packageRoot, at: baselinePackageRoot, @@ -105,9 +96,7 @@ struct APIDigesterBaselineDumper { // Create the workspace for this package. let workspace = try Workspace( - forRootPackage: baselinePackageRoot, - manifestLoader: manifestLoader, - repositoryManager: repositoryManager + forRootPackage: baselinePackageRoot ) let graph = try workspace.loadPackageGraph( @@ -123,7 +112,7 @@ struct APIDigesterBaselineDumper { // Update the data path input build parameters so it's built in the sandbox. var buildParameters = inputBuildParameters - buildParameters.dataPath = workspace.dataPath + buildParameters.dataPath = workspace.location.workingDirectory // Build the baseline module. let buildOp = BuildOperation( diff --git a/Sources/Commands/SwiftPackageTool.swift b/Sources/Commands/SwiftPackageTool.swift index e363ca94302..b4c7fbd2db1 100644 --- a/Sources/Commands/SwiftPackageTool.swift +++ b/Sources/Commands/SwiftPackageTool.swift @@ -364,13 +364,10 @@ extension SwiftPackageTool { try buildOp.build() // Dump JSON for the baseline package. - let workspace = try swiftTool.getActiveWorkspace() let baselineDumper = try APIDigesterBaselineDumper( baselineRevision: baselineRevision, packageRoot: swiftTool.getPackageRoot(), buildParameters: buildOp.buildParameters, - manifestLoader: workspace.manifestLoader, - repositoryManager: workspace.repositoryManager, apiDigesterTool: apiDigesterTool, diags: swiftTool.diagnostics ) @@ -886,7 +883,7 @@ extension SwiftPackageTool.Config { var mirrorURL: String func run(_ swiftTool: SwiftTool) throws { - let config = try swiftTool.getSwiftPMConfig() + let config = try swiftTool.getMirrorsConfig() if packageURL != nil { swiftTool.diagnostics.emit( @@ -920,7 +917,7 @@ extension SwiftPackageTool.Config { var mirrorURL: String? func run(_ swiftTool: SwiftTool) throws { - let config = try swiftTool.getSwiftPMConfig() + let config = try swiftTool.getMirrorsConfig() if packageURL != nil { swiftTool.diagnostics.emit( @@ -951,7 +948,7 @@ extension SwiftPackageTool.Config { var originalURL: String? func run(_ swiftTool: SwiftTool) throws { - let config = try swiftTool.getSwiftPMConfig() + let config = try swiftTool.getMirrorsConfig() if packageURL != nil { swiftTool.diagnostics.emit( diff --git a/Sources/Commands/SwiftTool.swift b/Sources/Commands/SwiftTool.swift index 906e757a626..f82c32e4ba3 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/Commands/SwiftTool.swift @@ -459,14 +459,14 @@ public class SwiftTool { return try getPackageRoot().appending(component: "Packages") } - func resolvedFilePath() throws -> AbsolutePath { + func resolvedVersionsFilePath() throws -> AbsolutePath { if let multiRootPackageDataFile = options.multirootPackageDataFile { return multiRootPackageDataFile.appending(components: "xcshareddata", "swiftpm", "Package.resolved") } return try getPackageRoot().appending(component: "Package.resolved") } - func configFilePath() throws -> AbsolutePath { + func mirrorsConfigFilePath() throws -> AbsolutePath { // Look for the override in the environment. if let envPath = ProcessEnv.vars["SWIFTPM_MIRROR_CONFIG"] { return try AbsolutePath(validating: envPath) @@ -479,15 +479,15 @@ public class SwiftTool { return try getPackageRoot().appending(components: ".swiftpm", "config") } - func getSwiftPMConfig() throws -> Workspace.Configuration { - return try _swiftpmConfig.get() + func getMirrorsConfig() throws -> Workspace.Configuration { + return try _mirrorsConfig.get() } - private lazy var _swiftpmConfig: Result = { - return Result(catching: { try Workspace.Configuration(path: try configFilePath()) }) + private lazy var _mirrorsConfig: Result = { + return Result(catching: { try Workspace.Configuration(path: try mirrorsConfigFilePath(), fileSystem: localFileSystem) }) }() - - func resolvedNetrcFilePath() throws -> AbsolutePath? { + + func netrcFilePath() throws -> AbsolutePath? { guard options.netrc || options.netrcFilePath != nil || options.netrcOptional else { return nil } @@ -551,21 +551,23 @@ public class SwiftTool { let cachePath = self.options.useRepositoriesCache ? try self.getCachePath() : .none _ = try self.getConfigPath() // TODO: actually use this in the workspace let isXcodeBuildSystemEnabled = self.options.buildSystem == .xcode - let workspace = Workspace( - dataPath: buildPath, - editablesPath: try editablesPath(), - pinsFile: try resolvedFilePath(), - manifestLoader: try getManifestLoader(), - toolsVersionLoader: ToolsVersionLoader(), - delegate: delegate, - config: try getSwiftPMConfig(), - repositoryProvider: provider, - netrcFilePath: try resolvedNetrcFilePath(), + let workspace = try Workspace( + fileSystem: localFileSystem, + location: .init( + workingDirectory: buildPath, + editsDirectory: try editablesPath(), + resolvedVersionsFilePath: try resolvedVersionsFilePath(), + sharedCacheDirectory: cachePath + ), + netrcFilePath: try netrcFilePath(), + mirrors: self.getMirrorsConfig().mirrors, + customManifestLoader: try getManifestLoader(), // FIXME: doe we really need to customize it? + customRepositoryProvider: provider, // FIXME: doe we really need to customize it? additionalFileRules: isXcodeBuildSystemEnabled ? FileRuleDescription.xcbuildFileTypes : FileRuleDescription.swiftpmFileTypes, - isResolverPrefetchingEnabled: options.shouldEnableResolverPrefetching, - skipUpdate: options.skipDependencyUpdate, - enableResolverTrace: options.enableResolverTrace, - cachePath: cachePath + resolverUpdateEnabled: !options.skipDependencyUpdate, + resolverPrefetchingEnabled: options.shouldEnableResolverPrefetching, + resolverTracingEnabled: options.enableResolverTrace, + delegate: delegate ) _workspace = workspace _workspaceDelegate = delegate @@ -638,7 +640,7 @@ public class SwiftTool { // The `plugins` directory is inside the workspace's main data directory, and contains all temporary // files related to all plugins in the workspace. let buildEnvironment = try buildParameters().buildEnvironment - let dataDir = try self.getActiveWorkspace().dataPath + let dataDir = try self.getActiveWorkspace().location.workingDirectory let pluginsDir = dataDir.appending(component: "plugins") // The `cache` directory is in the plugins directory and is where the plugin script runner caches diff --git a/Sources/PackageGraph/DependencyMirrors.swift b/Sources/PackageGraph/DependencyMirrors.swift index 716e857129e..47e5f60a5b6 100644 --- a/Sources/PackageGraph/DependencyMirrors.swift +++ b/Sources/PackageGraph/DependencyMirrors.swift @@ -24,6 +24,7 @@ public final class DependencyMirrors { private var index: [String: String] private var reverseIndex: [String: String] + private let lock = Lock() private init(_ mirrors: [Mirror]) { self.index = Dictionary(mirrors.map({ ($0.original, $0.mirror) }), uniquingKeysWith: { first, _ in first }) @@ -32,22 +33,26 @@ public final class DependencyMirrors { /// Sets a mirror URL for the given URL. public func set(mirrorURL: String, forURL url: String) { - self.index[url] = mirrorURL - self.reverseIndex[mirrorURL] = url + self.lock.withLock { + self.index[url] = mirrorURL + self.reverseIndex[mirrorURL] = url + } } /// Unsets a mirror for the given URL. /// - Parameter originalOrMirrorURL: The original URL or the mirrored URL /// - Throws: `Error.mirrorNotFound` if no mirror exists for the provided URL. public func unset(originalOrMirrorURL: String) throws { - if let value = self.index[originalOrMirrorURL] { - self.index[originalOrMirrorURL] = nil - self.reverseIndex[value] = nil - } else if let mirror = self.index.first(where: { $0.value == originalOrMirrorURL }) { - self.index[mirror.key] = nil - self.reverseIndex[originalOrMirrorURL] = nil - } else { - throw Error.mirrorNotFound + try self.lock.withLock { + if let value = self.index[originalOrMirrorURL] { + self.index[originalOrMirrorURL] = nil + self.reverseIndex[value] = nil + } else if let mirror = self.index.first(where: { $0.value == originalOrMirrorURL }) { + self.index[mirror.key] = nil + self.reverseIndex[originalOrMirrorURL] = nil + } else { + throw Error.mirrorNotFound + } } } @@ -55,7 +60,9 @@ public final class DependencyMirrors { /// - Parameter url: The original URL /// - Returns: The mirrored URL, if one exists. public func mirrorURL(for url: String) -> String? { - return self.index[url] + self.lock.withLock { + return self.index[url] + } } /// Returns the effective URL for a package dependency URL. @@ -69,9 +76,10 @@ public final class DependencyMirrors { /// - Parameter url: The mirror URL /// - Returns: The original URL, if one exists. public func originalURL(for url: String) -> String? { - return self.reverseIndex[url] + self.lock.withLock { + return self.reverseIndex[url] + } } - } extension DependencyMirrors: Collection { @@ -79,19 +87,27 @@ extension DependencyMirrors: Collection { public typealias Element = String public var startIndex: Index { - self.index.startIndex + self.lock.withLock { + self.index.startIndex + } } public var endIndex: Index { - self.index.endIndex + self.lock.withLock { + self.index.endIndex + } } public subscript(index: Index) -> Element { - self.index[index].value + self.lock.withLock { + self.index[index].value + } } public func index(after index: Index) -> Index { - self.index.index(after: index) + self.lock.withLock { + self.index.index(after: index) + } } } diff --git a/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift b/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift index 57478dd4e1b..0996b4d5f41 100644 --- a/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift +++ b/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift @@ -102,14 +102,14 @@ public struct PubgrubDependencyResolver { /// The container provider used to load package containers. private let provider: ContainerProvider - /// Skip updating containers while fetching them. - private let skipUpdate: Bool - /// Reference to the package container provider. private let packageContainerProvider: PackageContainerProvider /// Should resolver prefetch the containers. - private let isPrefetchingEnabled: Bool + private let prefetchingEnabled: Bool + + /// Update containers while fetching them. + private let updateEnabled: Bool /// Resolver delegate private let delegate: DependencyResolverDelegate? @@ -117,15 +117,15 @@ public struct PubgrubDependencyResolver { public init( provider: PackageContainerProvider, pinsMap: PinsStore.PinsMap = [:], - isPrefetchingEnabled: Bool = false, - skipUpdate: Bool = false, + updateEnabled: Bool = true, + prefetchingEnabled: Bool = false, delegate: DependencyResolverDelegate? = nil ) { self.packageContainerProvider = provider self.pinsMap = pinsMap - self.isPrefetchingEnabled = isPrefetchingEnabled - self.skipUpdate = skipUpdate - self.provider = ContainerProvider(provider: self.packageContainerProvider, skipUpdate: self.skipUpdate, pinsMap: self.pinsMap) + self.updateEnabled = updateEnabled + self.prefetchingEnabled = prefetchingEnabled + self.provider = ContainerProvider(provider: self.packageContainerProvider, updateEnabled: self.updateEnabled, pinsMap: self.pinsMap) self.delegate = delegate } @@ -170,7 +170,7 @@ public struct PubgrubDependencyResolver { let inputs = try self.processInputs(root: root, with: constraints) // Prefetch the containers if prefetching is enabled. - if self.isPrefetchingEnabled { + if self.prefetchingEnabled { // We avoid prefetching packages that are overridden since // otherwise we'll end up creating a repository container // for them. @@ -1357,8 +1357,8 @@ private final class ContainerProvider { /// The actual package container provider. private let underlying: PackageContainerProvider - /// Wheather to perform update (git fetch) on existing cloned repositories or not. - private let skipUpdate: Bool + /// Whether to perform update (git fetch) on existing cloned repositories or not. + private let updateEnabled: Bool /// Reference to the pins store. private let pinsMap: PinsStore.PinsMap @@ -1369,9 +1369,9 @@ private final class ContainerProvider { //// Store prefetches synchronization private var prefetches = ThreadSafeKeyValueStore() - init(provider underlying: PackageContainerProvider, skipUpdate: Bool, pinsMap: PinsStore.PinsMap) { + init(provider underlying: PackageContainerProvider, updateEnabled: Bool, pinsMap: PinsStore.PinsMap) { self.underlying = underlying - self.skipUpdate = skipUpdate + self.updateEnabled = updateEnabled self.pinsMap = pinsMap } @@ -1404,7 +1404,7 @@ private final class ContainerProvider { } } else { // Otherwise, fetch the container from the provider - self.underlying.getContainer(for: package, skipUpdate: skipUpdate, on: .sharedConcurrent) { result in + self.underlying.getContainer(for: package, skipUpdate: !self.updateEnabled, on: .sharedConcurrent) { result in let result = result.tryMap { container -> PubGrubPackageContainer in let pubGrubContainer = PubGrubPackageContainer(underlying: container, pinsMap: self.pinsMap) // only cache positive results @@ -1428,7 +1428,7 @@ private final class ContainerProvider { return group } if needsFetching { - self.underlying.getContainer(for: identifier, skipUpdate: skipUpdate, on: .sharedConcurrent) { result in + self.underlying.getContainer(for: identifier, skipUpdate: !self.updateEnabled, on: .sharedConcurrent) { result in defer { self.prefetches[identifier]?.leave() } // only cache positive results if case .success(let container) = result { diff --git a/Sources/SPMTestSupport/MockWorkspace.swift b/Sources/SPMTestSupport/MockWorkspace.swift index 47a5b0946ac..d5affc2fa94 100644 --- a/Sources/SPMTestSupport/MockWorkspace.swift +++ b/Sources/SPMTestSupport/MockWorkspace.swift @@ -31,8 +31,7 @@ public final class MockWorkspace { public var repoProvider: InMemoryGitRepositoryProvider public let delegate = MockWorkspaceDelegate() let toolsVersion: ToolsVersion - let skipUpdate: Bool - let enablePubGrub: Bool + let resolverUpdateEnabled: Bool public init( sandbox: AbsolutePath, @@ -44,15 +43,14 @@ public final class MockWorkspace { roots: [MockPackage], packages: [MockPackage], toolsVersion: ToolsVersion = ToolsVersion.currentToolsVersion, - skipUpdate: Bool = false, - enablePubGrub: Bool = true + resolverUpdateEnabled: Bool = true ) throws { self.sandbox = sandbox self.fs = fs self.httpClient = httpClient ?? HTTPClient.mock(fileSystem: fs) self.archiver = archiver self.checksumAlgorithm = checksumAlgorithm - self.config = try config ?? Workspace.Configuration(path: sandbox.appending(component: "swiftpm"), fs: fs) + self.config = try config ?? Workspace.Configuration(path: sandbox.appending(component: "swiftpm"), fileSystem: fs) self.identityResolver = DefaultIdentityResolver(locationMapper: self.config.mirrors.effectiveURL(for:)) self.roots = roots self.packages = packages @@ -60,8 +58,7 @@ public final class MockWorkspace { self.manifestLoader = MockManifestLoader(manifests: [:]) self.repoProvider = InMemoryGitRepositoryProvider() self.toolsVersion = toolsVersion - self.skipUpdate = skipUpdate - self.enablePubGrub = enablePubGrub + self.resolverUpdateEnabled = resolverUpdateEnabled try self.create() } @@ -154,32 +151,35 @@ public final class MockWorkspace { self.manifestLoader = MockManifestLoader(manifests: manifests) } - public func getOrCreateWorkspace() -> Workspace { - if let workspace = _workspace { + public func getOrCreateWorkspace() throws -> Workspace { + if let workspace = self._workspace { return workspace } - self._workspace = Workspace( - dataPath: self.sandbox.appending(component: ".build"), - editablesPath: self.sandbox.appending(component: "edits"), - pinsFile: self.sandbox.appending(component: "Package.resolved"), - manifestLoader: self.manifestLoader, - currentToolsVersion: self.toolsVersion, - toolsVersionLoader: ToolsVersionLoader(), - delegate: self.delegate, - config: self.config, + let workspace = try Workspace( fileSystem: self.fs, - repositoryProvider: self.repoProvider, - identityResolver: self.identityResolver, - httpClient: self.httpClient, - archiver: self.archiver, - checksumAlgorithm: self.checksumAlgorithm, - isResolverPrefetchingEnabled: true, - enablePubgrubResolver: self.enablePubGrub, - skipUpdate: self.skipUpdate, - cachePath: localFileSystem.swiftPMCacheDirectory.appending(component: "repositories") + location: .init ( + workingDirectory: self.sandbox.appending(component: ".build"), + editsDirectory: self.sandbox.appending(component: "edits"), + resolvedVersionsFilePath: self.sandbox.appending(component: "Package.resolved"), + sharedCacheDirectory: self.fs.swiftPMCacheDirectory + ), + mirrors: self.config.mirrors, + customToolsVersion: self.toolsVersion, + customManifestLoader: self.manifestLoader, + customRepositoryProvider: self.repoProvider, + customIdentityResolver: self.identityResolver, + customHTTPClient: self.httpClient, + customArchiver: self.archiver, + customChecksumAlgorithm: self.checksumAlgorithm, + resolverUpdateEnabled: self.resolverUpdateEnabled, + resolverPrefetchingEnabled: true, + delegate: self.delegate ) - return self._workspace! + + self._workspace = workspace + + return workspace } private var _workspace: Workspace? @@ -199,15 +199,17 @@ public final class MockWorkspace { checkoutBranch: String? = nil, _ result: (DiagnosticsEngine) -> Void ) { - let ws = self.getOrCreateWorkspace() let diagnostics = DiagnosticsEngine() - ws.edit( - packageName: packageName, - path: path, - revision: revision, - checkoutBranch: checkoutBranch, - diagnostics: diagnostics - ) + diagnostics.wrap { + let ws = try self.getOrCreateWorkspace() + ws.edit( + packageName: packageName, + path: path, + revision: revision, + checkoutBranch: checkoutBranch, + diagnostics: diagnostics + ) + } result(diagnostics) } @@ -217,10 +219,10 @@ public final class MockWorkspace { forceRemove: Bool = false, _ result: (DiagnosticsEngine) -> Void ) { - let ws = self.getOrCreateWorkspace() let diagnostics = DiagnosticsEngine() let rootInput = PackageGraphRootInput(packages: rootPaths(for: roots)) diagnostics.wrap { + let ws = try self.getOrCreateWorkspace() try ws.unedit(packageName: packageName, forceRemove: forceRemove, root: rootInput, diagnostics: diagnostics) } result(diagnostics) @@ -228,9 +230,9 @@ public final class MockWorkspace { public func checkResolve(pkg: String, roots: [String], version: TSCUtility.Version, _ result: (DiagnosticsEngine) -> Void) { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() let rootInput = PackageGraphRootInput(packages: rootPaths(for: roots)) diagnostics.wrap { + let workspace = try self.getOrCreateWorkspace() try workspace.resolve(packageName: pkg, root: rootInput, version: version, branch: nil, revision: nil, diagnostics: diagnostics) } result(diagnostics) @@ -238,15 +240,19 @@ public final class MockWorkspace { public func checkClean(_ result: (DiagnosticsEngine) -> Void) { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() - workspace.clean(with: diagnostics) + diagnostics.wrap { + let workspace = try self.getOrCreateWorkspace() + workspace.clean(with: diagnostics) + } result(diagnostics) } public func checkReset(_ result: (DiagnosticsEngine) -> Void) { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() - workspace.reset(with: diagnostics) + diagnostics.wrap { + let workspace = try self.getOrCreateWorkspace() + workspace.reset(with: diagnostics) + } result(diagnostics) } @@ -256,13 +262,13 @@ public final class MockWorkspace { packages: [String] = [], _ result: (DiagnosticsEngine) -> Void ) { - let dependencies = deps.map { $0.convert(baseURL: packagesDir, identityResolver: self.identityResolver) } let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() - let rootInput = PackageGraphRootInput( - packages: rootPaths(for: roots), dependencies: dependencies - ) - _ = diagnostics.wrap { + diagnostics.wrap { + let dependencies = deps.map { $0.convert(baseURL: packagesDir, identityResolver: self.identityResolver) } + let rootInput = PackageGraphRootInput( + packages: rootPaths(for: roots), dependencies: dependencies + ) + let workspace = try self.getOrCreateWorkspace() try workspace.updateDependencies(root: rootInput, packages: packages, diagnostics: diagnostics) } result(diagnostics) @@ -274,13 +280,13 @@ public final class MockWorkspace { _ result: ([(PackageReference, Workspace.PackageStateChange)]?, DiagnosticsEngine) -> Void ) { let dependencies = deps.map { $0.convert(baseURL: packagesDir, identityResolver: self.identityResolver) } - let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() let rootInput = PackageGraphRootInput( packages: rootPaths(for: roots), dependencies: dependencies ) - let changes = diagnostics.wrap { - try workspace.updateDependencies(root: rootInput, diagnostics: diagnostics, dryRun: true) + let diagnostics = DiagnosticsEngine() + let changes = diagnostics.wrap { () -> [(PackageReference, Workspace.PackageStateChange)]? in + let workspace = try self.getOrCreateWorkspace() + return try workspace.updateDependencies(root: rootInput, diagnostics: diagnostics, dryRun: true) } ?? nil result(changes, diagnostics) } @@ -301,11 +307,11 @@ public final class MockWorkspace { _ result: (PackageGraph, DiagnosticsEngine) -> Void ) { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() let rootInput = PackageGraphRootInput( packages: rootPaths(for: roots), dependencies: dependencies ) do { + let workspace = try self.getOrCreateWorkspace() let graph = try workspace.loadPackageGraph( rootInput: rootInput, forceResolvedVersions: forceResolvedVersions, diagnostics: diagnostics ) @@ -331,11 +337,11 @@ public final class MockWorkspace { _ result: (DiagnosticsEngine) -> Void ) { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() let rootInput = PackageGraphRootInput( packages: rootPaths(for: roots), dependencies: dependencies ) _ = diagnostics.wrap { + let workspace = try self.getOrCreateWorkspace() try workspace.loadPackageGraph( rootInput: rootInput, forceResolvedVersions: forceResolvedVersions, diagnostics: diagnostics ) @@ -350,7 +356,7 @@ public final class MockWorkspace { public func checkPrecomputeResolution(_ check: (ResolutionPrecomputationResult) -> Void) throws { let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() let pinsStore = try workspace.pinsStore.load() let rootInput = PackageGraphRootInput(packages: rootPaths(for: roots.map { $0.name }), dependencies: []) @@ -374,7 +380,7 @@ public final class MockWorkspace { managedDependencies: [ManagedDependency] = [], managedArtifacts: [ManagedArtifact] = [] ) throws { - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() let pinsStore = try workspace.pinsStore.load() for (ref, state) in pins { @@ -396,7 +402,7 @@ public final class MockWorkspace { } public func resetState() throws { - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() try workspace.resetState() } @@ -508,7 +514,7 @@ public final class MockWorkspace { ) throws { let dependencies = deps.map { $0.convert(baseURL: packagesDir, identityResolver: self.identityResolver) } let diagnostics = DiagnosticsEngine() - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() let rootInput = PackageGraphRootInput( packages: rootPaths(for: roots), dependencies: dependencies ) @@ -520,7 +526,7 @@ public final class MockWorkspace { public func checkManagedDependencies(file: StaticString = #file, line: UInt = #line, _ result: (ManagedDependencyResult) throws -> Void) { do { - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() try result(ManagedDependencyResult(workspace.state.dependencies)) } catch { XCTFail("Failed with error \(error)", file: file, line: line) @@ -529,7 +535,7 @@ public final class MockWorkspace { public func checkManagedArtifacts(file: StaticString = #file, line: UInt = #line, _ result: (ManagedArtifactResult) throws -> Void) { do { - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() try result(ManagedArtifactResult(workspace.state.artifacts)) } catch { XCTFail("Failed with error \(error)", file: file, line: line) @@ -579,7 +585,7 @@ public final class MockWorkspace { public func checkResolved(file: StaticString = #file, line: UInt = #line, _ result: (ResolvedResult) throws -> Void) { do { - let workspace = self.getOrCreateWorkspace() + let workspace = try self.getOrCreateWorkspace() try result(ResolvedResult(workspace.pinsStore.load())) } catch { XCTFail("Failed with error \(error)", file: file, line: line) diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index dc6d3283fc1..96c85182b9a 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -158,49 +158,41 @@ private struct WorkspaceDependencyResolverDelegate: DependencyResolverDelegate { /// This class does *not* support concurrent operations. public class Workspace { /// The delegate interface. - public weak var delegate: WorkspaceDelegate? + fileprivate weak var delegate: WorkspaceDelegate? - /// The path of the workspace data. - public let dataPath: AbsolutePath + /// The workspace location. + public let location: Location - /// The swiftpm config. - fileprivate let config: Workspace.Configuration + /// The mirrors config. + fileprivate let mirrors: DependencyMirrors /// The current persisted state of the workspace. + // public visibility for testing public let state: WorkspaceState /// The Pins store. The pins file will be created when first pin is added to pins store. + // public visibility for testing public let pinsStore: LoadableResult - /// The path to the Package.resolved file for this workspace. - public let resolvedFile: AbsolutePath - - /// The path for working repository clones (checkouts). - public let checkoutsPath: AbsolutePath - - /// The path for downloaded binary artifacts. - public let artifactsPath: AbsolutePath - - /// The path where packages which are put in edit mode are checked out. - public let editablesPath: AbsolutePath - /// The file system on which the workspace will operate. - fileprivate var fileSystem: FileSystem + fileprivate let fileSystem: FileSystem /// The manifest loader to use. - public let manifestLoader: ManifestLoaderProtocol + fileprivate let manifestLoader: ManifestLoaderProtocol /// The tools version currently in use. fileprivate let currentToolsVersion: ToolsVersion /// The manifest loader to use. - fileprivate let toolsVersionLoader: ToolsVersionLoaderProtocol + fileprivate var toolsVersionLoader: ToolsVersionLoaderProtocol /// The repository manager. - public let repositoryManager: RepositoryManager + // var for backwards compatibility with deprecated initializers, remove with them + fileprivate var repositoryManager: RepositoryManager /// Utility to resolve package identifiers - public let identityResolver: IdentityResolver + // var for backwards compatibility with deprecated initializers, remove with them + fileprivate var identityResolver: IdentityResolver /// The package container provider. fileprivate let containerProvider: RepositoryPackageContainerProvider @@ -217,111 +209,110 @@ public class Workspace { fileprivate let checksumAlgorithm: HashAlgorithm /// Enable prefetching containers in resolver. - fileprivate let isResolverPrefetchingEnabled: Bool + fileprivate let resolverPrefetchingEnabled: Bool + + /// Update containers while fetching them. + fileprivate let resolverUpdateEnabled: Bool + + /// Write dependency resolver trace to a file. + fileprivate let resolverTracingEnabled: Bool - /// Skip updating containers while fetching them. - fileprivate let skipUpdate: Bool + fileprivate let additionalFileRules: [FileRuleDescription] + + // state /// The active package resolver. This is set during a dependency resolution operation. fileprivate var activeResolver: PubgrubDependencyResolver? - /// Write dependency resolver trace to a file. - fileprivate let enableResolverTrace: Bool - fileprivate var resolvedFileWatcher: ResolvedFileWatcher? - fileprivate let additionalFileRules: [FileRuleDescription] - /// Create a new package workspace. /// + /// This initializer is designed for use cases when the workspace needs to be highly customized such as testing. + /// In other cases, use the other, more straight forward, initializers + /// /// This will automatically load the persisted state for the package, if /// present. If the state isn't present then a default state will be /// constructed. /// /// - Parameters: - /// - dataPath: The path for the workspace data files. - /// - editablesPath: The path where editable packages should be placed. - /// - pinsFile: The path to pins file. If pins file is not present, it will be created. - /// - manifestLoader: The manifest loader. - /// - fileSystem: The file system to operate on. - /// - repositoryProvider: The repository provider to use in repository manager. - /// - Throws: If the state was present, but could not be loaded. + /// - fileSystem: The file system to use. + /// - location: Workspace location configuration. + /// - netrcFilePath: Path tot he netrc file. + /// - mirrors: Dependencies mirrors. + /// - customToolsVersion: A custom tools version. + /// - customManifestLoader: A custom manifest loader. + /// - customRepositoryManager: A custom repository manager. + /// - customRepositoryProvider: A custom repository provider. + /// - customIdentityResolver: A custom identity resolver. + /// - customHTTPClient: A custom http client. + /// - customArchiver: A custom archiver. + /// - customChecksumAlgorithm: A custom checksum algorithm. + /// - additionalFileRules: File rules to determine resource handling behavior. + /// - resolverUpdateEnabled: Enables the dependencies resolver automatic version update check, relying only on the resolved version file + /// - resolverPrefetchingEnabled: Enables the dependencies resolver prefetching based on the resolved version file + /// - resolverTracingEnabled:Enables the dependencies resolver tracing + /// - delegate: Delegate for workspace events public init( - dataPath: AbsolutePath, - editablesPath: AbsolutePath, - pinsFile: AbsolutePath, - manifestLoader: ManifestLoaderProtocol, - repositoryManager: RepositoryManager? = nil, - currentToolsVersion: ToolsVersion? = nil, - toolsVersionLoader: ToolsVersionLoaderProtocol? = nil, - delegate: WorkspaceDelegate? = nil, - config: Workspace.Configuration? = nil, - fileSystem: FileSystem? = nil, - repositoryProvider: RepositoryProvider? = nil, - identityResolver: IdentityResolver? = nil, - httpClient: HTTPClient? = nil, - netrcFilePath: AbsolutePath? = nil, - archiver: Archiver? = nil, - checksumAlgorithm: HashAlgorithm? = nil, - additionalFileRules: [FileRuleDescription]? = nil, - isResolverPrefetchingEnabled: Bool? = nil, - enablePubgrubResolver: Bool? = nil, - skipUpdate: Bool? = nil, - enableResolverTrace: Bool? = nil, - cachePath: AbsolutePath? = nil - ) { + fileSystem: FileSystem, + location: Location, + netrcFilePath: AbsolutePath? = .none, + mirrors: DependencyMirrors? = .none, + customToolsVersion: ToolsVersion? = .none, + customManifestLoader: ManifestLoaderProtocol? = .none, + customRepositoryManager: RepositoryManager? = .none, + customRepositoryProvider: RepositoryProvider? = .none, + customIdentityResolver: IdentityResolver? = .none, + customHTTPClient: HTTPClient? = .none, + customArchiver: Archiver? = .none, + customChecksumAlgorithm: HashAlgorithm? = .none, + additionalFileRules: [FileRuleDescription]? = .none, + resolverUpdateEnabled: Bool? = .none, + resolverPrefetchingEnabled: Bool? = .none, + resolverTracingEnabled: Bool? = .none, + delegate: WorkspaceDelegate? = .none + ) throws { // defaults - let currentToolsVersion = currentToolsVersion ?? ToolsVersion.currentToolsVersion - let toolsVersionLoader = toolsVersionLoader ?? ToolsVersionLoader() - let config = config ?? Workspace.Configuration() - let fileSystem = fileSystem ?? localFileSystem - let repositoryProvider = repositoryProvider ?? GitRepositoryProvider() - let httpClient = httpClient ?? HTTPClient() - let archiver = archiver ?? ZipArchiver() - var checksumAlgorithm = checksumAlgorithm ?? SHA256() + let currentToolsVersion = customToolsVersion ?? ToolsVersion.currentToolsVersion + let toolsVersionLoader = ToolsVersionLoader() + let manifestLoader = try customManifestLoader ?? ManifestLoader(toolchain: UserToolchain(destination: .hostDestination()).configuration) + let repositoryProvider = customRepositoryProvider ?? GitRepositoryProvider() + let repositoryManager = customRepositoryManager ?? RepositoryManager( + path: location.repositoriesDirectory, + provider: repositoryProvider, + delegate: delegate.map(WorkspaceRepositoryManagerDelegate.init(workspaceDelegate:)), + fileSystem: fileSystem, + cachePath: location.repositoriesSharedCacheDirectory) + let httpClient = customHTTPClient ?? HTTPClient() + let archiver = customArchiver ?? ZipArchiver() + let mirrors = mirrors ?? DependencyMirrors() + let identityResolver = customIdentityResolver ?? DefaultIdentityResolver(locationMapper: mirrors.effectiveURL(for:)) + var checksumAlgorithm = customChecksumAlgorithm ?? SHA256() #if canImport(CryptoKit) if checksumAlgorithm is SHA256, #available(macOS 10.15, *) { checksumAlgorithm = CryptoKitSHA256() } #endif + let additionalFileRules = additionalFileRules ?? [] - let isResolverPrefetchingEnabled = isResolverPrefetchingEnabled ?? false - let skipUpdate = skipUpdate ?? false - let enableResolverTrace = enableResolverTrace ?? false + let resolverUpdateEnabled = resolverUpdateEnabled ?? true + let resolverPrefetchingEnabled = resolverPrefetchingEnabled ?? false + let resolverTracingEnabled = resolverTracingEnabled ?? false // initialize + self.fileSystem = fileSystem + self.location = location self.delegate = delegate - self.dataPath = dataPath - self.config = config - self.editablesPath = editablesPath + self.mirrors = mirrors + self.netrcFilePath = netrcFilePath self.manifestLoader = manifestLoader self.currentToolsVersion = currentToolsVersion self.toolsVersionLoader = toolsVersionLoader self.httpClient = httpClient - self.netrcFilePath = netrcFilePath self.archiver = archiver - - self.checksumAlgorithm = checksumAlgorithm - self.isResolverPrefetchingEnabled = isResolverPrefetchingEnabled - self.skipUpdate = skipUpdate - self.enableResolverTrace = enableResolverTrace - self.resolvedFile = pinsFile - self.additionalFileRules = additionalFileRules - - let repositoriesPath = self.dataPath.appending(component: "repositories") - let repositoriesCachePath = cachePath.map { $0.appending(component: "repositories") } - let repositoryManager = repositoryManager ?? RepositoryManager( - path: repositoriesPath, - provider: repositoryProvider, - delegate: delegate.map(WorkspaceRepositoryManagerDelegate.init(workspaceDelegate:)), - fileSystem: fileSystem, - cachePath: repositoriesCachePath) self.repositoryManager = repositoryManager - - self.checkoutsPath = self.dataPath.appending(component: "checkouts") - self.artifactsPath = self.dataPath.appending(component: "artifacts") - - self.identityResolver = identityResolver ?? DefaultIdentityResolver(locationMapper: config.mirrors.effectiveURL(for:)) + self.identityResolver = identityResolver + self.checksumAlgorithm = checksumAlgorithm self.containerProvider = RepositoryPackageContainerProvider( repositoryManager: repositoryManager, @@ -330,12 +321,74 @@ public class Workspace { currentToolsVersion: currentToolsVersion, toolsVersionLoader: toolsVersionLoader ) - self.fileSystem = fileSystem self.pinsStore = LoadableResult { - try PinsStore(pinsFile: pinsFile, fileSystem: fileSystem, mirrors: config.mirrors) + try PinsStore(pinsFile: location.resolvedVersionsFilePath, fileSystem: fileSystem, mirrors: mirrors) + } + + self.additionalFileRules = additionalFileRules + self.resolverUpdateEnabled = resolverUpdateEnabled + self.resolverPrefetchingEnabled = resolverPrefetchingEnabled + self.resolverTracingEnabled = resolverTracingEnabled + + self.state = WorkspaceState(dataPath: self.location.workingDirectory, fileSystem: fileSystem) + } + + // deprecated 8/2021 + @available(*, deprecated, message: "use non-deprecated initializer instead") + public convenience init( + dataPath: AbsolutePath, + editablesPath: AbsolutePath, + pinsFile: AbsolutePath, + manifestLoader: ManifestLoaderProtocol, + repositoryManager: RepositoryManager? = nil, + currentToolsVersion: ToolsVersion? = nil, + toolsVersionLoader: ToolsVersionLoaderProtocol? = nil, + delegate: WorkspaceDelegate? = nil, + config: Workspace.Configuration? = nil, + fileSystem: FileSystem? = nil, + repositoryProvider: RepositoryProvider? = nil, + identityResolver: IdentityResolver? = nil, + httpClient: HTTPClient? = nil, + netrcFilePath: AbsolutePath? = nil, + archiver: Archiver? = nil, + checksumAlgorithm: HashAlgorithm? = nil, + additionalFileRules: [FileRuleDescription]? = nil, + isResolverPrefetchingEnabled: Bool? = nil, + enablePubgrubResolver: Bool? = nil, + skipUpdate: Bool? = nil, + enableResolverTrace: Bool? = nil, + cachePath: AbsolutePath? = nil + ) { + // try! safe in this case since the new initializer will only throw when creating a manifest loader + // which is passed explicitly in this case. this initializer will go away soon in any case. + let fileSystem = fileSystem ?? localFileSystem + try! self.init( + fileSystem: fileSystem, + location: .init( + workingDirectory: dataPath, + editsDirectory: editablesPath, + resolvedVersionsFilePath: pinsFile, + sharedCacheDirectory: cachePath + ), + netrcFilePath: netrcFilePath, + mirrors: config?.mirrors, + customToolsVersion: currentToolsVersion, + customManifestLoader: manifestLoader, + customRepositoryManager: repositoryManager, + customRepositoryProvider: repositoryProvider, + customIdentityResolver: identityResolver, + customHTTPClient: httpClient, + customArchiver: archiver, + customChecksumAlgorithm: checksumAlgorithm, + additionalFileRules: additionalFileRules, + resolverUpdateEnabled: skipUpdate.map{ !$0 }, + resolverPrefetchingEnabled: isResolverPrefetchingEnabled, + resolverTracingEnabled: enableResolverTrace + ) + if let toolsVersionLoader = toolsVersionLoader { + self.toolsVersionLoader = toolsVersionLoader } - self.state = WorkspaceState(dataPath: dataPath, fileSystem: fileSystem) } /// A convenience method for creating a workspace for the given root @@ -345,23 +398,22 @@ public class Workspace { /// default paths. /// /// - Parameters: + /// - fileSystem: The file system to use, defaults to local file system. /// - forRootPackage: The path for the root package. - /// - toolchain: A custom toolchain. - /// - repositoryManager: A custom repository manager. + /// - customToolchain: A custom toolchain. /// - delegate: Delegate for workspace events public convenience init( + fileSystem: FileSystem? = .none, forRootPackage packagePath: AbsolutePath, - toolchain: UserToolchain? = nil, - repositoryManager: RepositoryManager? = nil, - delegate: WorkspaceDelegate? = nil + customToolchain: UserToolchain, + delegate: WorkspaceDelegate? = .none ) throws { - let toolchain = try toolchain ?? UserToolchain(destination: .hostDestination()) - let manifestLoader = ManifestLoader(toolchain: toolchain.configuration) - + let fileSystem = fileSystem ?? localFileSystem + let manifestLoader = ManifestLoader(toolchain: customToolchain.configuration) try self.init( + fileSystem: fileSystem, forRootPackage: packagePath, - manifestLoader: manifestLoader, - repositoryManager: repositoryManager, + customManifestLoader: manifestLoader, delegate: delegate ) } @@ -373,23 +425,23 @@ public class Workspace { /// default paths. /// /// - Parameters: + /// - fileSystem: The file system to use, defaults to local file system. /// - forRootPackage: The path for the root package. - /// - manifestLoader: A custom manifest loader. - /// - repositoryManager: A custom repository manager. + /// - customManifestLoader: A custom manifest loader. /// - delegate: Delegate for workspace events public convenience init( + fileSystem: FileSystem? = .none, forRootPackage packagePath: AbsolutePath, - manifestLoader: ManifestLoaderProtocol, - repositoryManager: RepositoryManager? = nil, - delegate: WorkspaceDelegate? = nil + customManifestLoader: ManifestLoaderProtocol? = .none, + delegate: WorkspaceDelegate? = .none ) throws { - - self .init( - dataPath: packagePath.appending(component: ".build"), - editablesPath: packagePath.appending(component: "Packages"), - pinsFile: packagePath.appending(component: "Package.resolved"), - manifestLoader: manifestLoader, - repositoryManager: repositoryManager, + let fileSystem = fileSystem ?? localFileSystem + let mirrorsPath = packagePath.appending(components: ".swiftpm", "config") + try self .init( + fileSystem: fileSystem, + location: .init(forRootPackage: packagePath), + mirrors: try Workspace.Configuration(path: mirrorsPath, fileSystem: fileSystem).mirrors, + customManifestLoader: customManifestLoader, delegate: delegate ) } @@ -401,7 +453,7 @@ public class Workspace { /// default paths. // FIXME: this one is kind of messy to backwards support, hopefully we can remove quickly // deprecated 8/2021 - @available(*, deprecated, message: "use constructor instead") + @available(*, deprecated, message: "use initializer instead") public static func create( forRootPackage packagePath: AbsolutePath, manifestLoader: ManifestLoaderProtocol, @@ -409,12 +461,18 @@ public class Workspace { delegate: WorkspaceDelegate? = nil, identityResolver: IdentityResolver? = nil ) -> Workspace { - return try! .init(forRootPackage: packagePath, - manifestLoader: manifestLoader, - repositoryManager: repositoryManager, - delegate: delegate//, - //identityResolver: identityResolver + let workspace = try! Workspace(forRootPackage: packagePath, + customManifestLoader: manifestLoader, + //repositoryManager: repositoryManager, + delegate: delegate ) + if let repositoryManager = repositoryManager { + workspace.repositoryManager = repositoryManager + } + if let identityResolver = identityResolver { + workspace.identityResolver = identityResolver + } + return workspace } } @@ -538,28 +596,28 @@ extension Workspace { // These are the things we don't want to remove while cleaning. let protectedAssets = [ repositoryManager.path, - checkoutsPath, - artifactsPath, + self.location.repositoriesCheckoutsDirectory, + self.location.artifactsDirectory, state.path, ].map({ path -> String in // Assert that these are present inside data directory. - assert(path.parentDirectory == dataPath) + assert(path.parentDirectory == self.location.workingDirectory) return path.basename }) // If we have no data yet, we're done. - guard fileSystem.exists(dataPath) else { + guard fileSystem.exists(self.location.workingDirectory) else { return } - guard let contents = diagnostics.wrap({ try fileSystem.getDirectoryContents(dataPath) }) else { + guard let contents = diagnostics.wrap({ try fileSystem.getDirectoryContents(self.location.workingDirectory) }) else { return } // Remove all but protected paths. let contentsToRemove = Set(contents).subtracting(protectedAssets) for name in contentsToRemove { - try? fileSystem.removeFileTree(dataPath.appending(RelativePath(name))) + try? fileSystem.removeFileTree(self.location.workingDirectory.appending(RelativePath(name))) } } @@ -582,7 +640,7 @@ extension Workspace { /// and notes. public func reset(with diagnostics: DiagnosticsEngine) { let removed = diagnostics.wrap { - try fileSystem.chmod(.userWritable, path: checkoutsPath, options: [.recursive, .onlyFiles]) + try fileSystem.chmod(.userWritable, path: self.location.repositoriesCheckoutsDirectory, options: [.recursive, .onlyFiles]) // Reset state. try self.resetState() } @@ -590,7 +648,7 @@ extension Workspace { guard removed else { return } repositoryManager.reset() try? manifestLoader.resetCache() - try? fileSystem.removeFileTree(dataPath) + try? fileSystem.removeFileTree(self.location.workingDirectory) } // FIXME: @testable internal @@ -930,7 +988,7 @@ extension Workspace { // If a path is provided then we use it as destination. If not, we // use the folder with packageName inside editablesPath. - let destination = path ?? editablesPath.appending(component: packageName) + let destination = path ?? self.location.editsDirectory.appending(component: packageName) // If there is something present at the destination, we confirm it has // a valid manifest with name same as the package we are trying to edit. @@ -989,10 +1047,10 @@ extension Workspace { // For unmanaged dependencies, create the symlink under editables dir. if let path = path { - try fileSystem.createDirectory(editablesPath) + try fileSystem.createDirectory(self.location.editsDirectory) // FIXME: We need this to work with InMem file system too. if !(fileSystem is InMemoryFileSystem) { - let symLinkPath = editablesPath.appending(component: packageName) + let symLinkPath = self.location.editsDirectory.appending(component: packageName) // Cleanup any existing symlink. if fileSystem.isSymlink(symLinkPath) { @@ -1008,7 +1066,7 @@ extension Workspace { // Remove the existing checkout. do { - let oldCheckoutPath = checkoutsPath.appending(dependency.subpath) + let oldCheckoutPath = self.location.repositoriesCheckoutsDirectory.appending(dependency.subpath) try fileSystem.chmod(.userWritable, path: oldCheckoutPath, options: [.recursive, .onlyFiles]) try fileSystem.removeFileTree(oldCheckoutPath) } @@ -1044,7 +1102,7 @@ extension Workspace { } // Form the edit working repo path. - let path = editablesPath.appending(dependency.subpath) + let path = self.location.editsDirectory.appending(dependency.subpath) // Check for uncommited and unpushed changes if force removal is off. if !forceRemove { let workingCopy = try repositoryManager.provider.openWorkingCopy(at: path) @@ -1060,8 +1118,8 @@ extension Workspace { try fileSystem.removeFileTree(path) } // If this was the last editable dependency, remove the editables directory too. - if fileSystem.exists(editablesPath), try fileSystem.getDirectoryContents(editablesPath).isEmpty { - try fileSystem.removeFileTree(editablesPath) + if fileSystem.exists(self.location.editsDirectory), try fileSystem.getDirectoryContents(self.location.editsDirectory).isEmpty { + try fileSystem.removeFileTree(self.location.editsDirectory) } if let checkoutState = dependency.basedOn?.checkoutState { @@ -1224,7 +1282,7 @@ extension Workspace { requiredIdentities = inputIdentities.union(requiredIdentities) let availableIdentities: Set = Set(manifestsMap.map { - let url = workspace.config.mirrors.effectiveURL(for: $0.1.packageLocation) + let url = workspace.mirrors.effectiveURL(for: $0.1.packageLocation) return PackageReference(identity: $0.key, kind: $0.1.packageKind, location: url) }) // We should never have loaded a manifest we don't need. @@ -1297,7 +1355,7 @@ extension Workspace { public func watchResolvedFile() throws { // Return if we're already watching it. guard self.resolvedFileWatcher == nil else { return } - self.resolvedFileWatcher = try ResolvedFileWatcher(resolvedFile: self.resolvedFile) { [weak self] in + self.resolvedFileWatcher = try ResolvedFileWatcher(resolvedFile: self.location.resolvedVersionsFilePath) { [weak self] in self?.delegate?.resolvedFileChanged() } } @@ -1306,8 +1364,8 @@ extension Workspace { fileprivate func createCacheDirectories(with diagnostics: DiagnosticsEngine) { do { try fileSystem.createDirectory(repositoryManager.path, recursive: true) - try fileSystem.createDirectory(checkoutsPath, recursive: true) - try fileSystem.createDirectory(artifactsPath, recursive: true) + try fileSystem.createDirectory(self.location.repositoriesCheckoutsDirectory, recursive: true) + try fileSystem.createDirectory(self.location.artifactsDirectory, recursive: true) } catch { diagnostics.emit(error) } @@ -1321,9 +1379,9 @@ extension Workspace { public func path(to dependency: ManagedDependency) -> AbsolutePath { switch dependency.state { case .checkout: - return checkoutsPath.appending(dependency.subpath) + return self.location.repositoriesCheckoutsDirectory.appending(dependency.subpath) case .edited(let path): - return path ?? editablesPath.appending(dependency.subpath) + return path ?? self.location.editsDirectory.appending(dependency.subpath) case .local: return AbsolutePath(dependency.packageRef.location) } @@ -1621,8 +1679,8 @@ extension Workspace { } } - for directory in try fileSystem.getDirectoryContents(artifactsPath) { - let directoryPath = artifactsPath.appending(component: directory) + for directory in try fileSystem.getDirectoryContents(self.location.artifactsDirectory) { + let directoryPath = self.location.artifactsDirectory.appending(component: directory) if try fileSystem.isDirectory(directoryPath) && fileSystem.getDirectoryContents(directoryPath).isEmpty { try fileSystem.removeFileTree(directoryPath) } @@ -1762,8 +1820,8 @@ extension Workspace { group.enter() defer { group.leave() } - let parentDirectory = self.artifactsPath.appending(component: artifact.packageRef.name) - let tempExtractionDirectory = self.artifactsPath.appending(components: "extract", artifact.targetName) + let parentDirectory = self.location.artifactsDirectory.appending(component: artifact.packageRef.name) + let tempExtractionDirectory = self.location.artifactsDirectory.appending(components: "extract", artifact.targetName) do { try fileSystem.createDirectory(parentDirectory, recursive: true) @@ -1956,10 +2014,10 @@ extension Workspace { ) if precomputationResult.isRequired { - if !fileSystem.exists(resolvedFile) { - diagnostics.emit(error: "a resolved file is required when automatic dependency resolution is disabled and should be placed at \(resolvedFile.pathString)") + if !fileSystem.exists(self.location.resolvedVersionsFilePath) { + diagnostics.emit(error: "a resolved file is required when automatic dependency resolution is disabled and should be placed at \(self.location.resolvedVersionsFilePath.pathString)") } else { - diagnostics.emit(error: "an out-of-date resolved file was detected at \(resolvedFile.pathString), which is not allowed when automatic dependency resolution is disabled; please make sure to update the file to reflect the changes in dependencies") + diagnostics.emit(error: "an out-of-date resolved file was detected at \(self.location.resolvedVersionsFilePath.pathString), which is not allowed when automatic dependency resolution is disabled; please make sure to update the file to reflect the changes in dependencies") } } @@ -2408,16 +2466,16 @@ extension Workspace { if let workspaceDelegate = self.delegate { delegates.append(WorkspaceDependencyResolverDelegate(workspaceDelegate)) } - if self.enableResolverTrace { - delegates.append(try TracingDependencyResolverDelegate(path: self.dataPath.appending(components: "resolver.trace"))) + if self.resolverTracingEnabled { + delegates.append(try TracingDependencyResolverDelegate(path: self.location.workingDirectory.appending(components: "resolver.trace"))) } let delegate = !delegates.isEmpty ? MultiplexResolverDelegate(delegates) : nil return PubgrubDependencyResolver( provider: containerProvider, pinsMap: pinsMap, - isPrefetchingEnabled: isResolverPrefetchingEnabled, - skipUpdate: skipUpdate, + updateEnabled: self.resolverUpdateEnabled, + prefetchingEnabled: self.resolverPrefetchingEnabled, delegate: delegate ) } @@ -2444,7 +2502,7 @@ extension Workspace { } /// Validates that all the edited dependencies are still present in the file system. - /// If some checkout dependency is reomved form the file system, clone it again. + /// If some checkout dependency is removed form the file system, clone it again. /// If some edited dependency is removed from the file system, mark it as unedited and /// fallback on the original checkout. fileprivate func fixManagedDependencies(with diagnostics: DiagnosticsEngine) { @@ -2556,7 +2614,7 @@ extension Workspace { private func fetch(package: PackageReference) throws -> AbsolutePath { // If we already have it, fetch to update the repo from its remote. if let dependency = state.dependencies[forURL: package.location] { - let path = checkoutsPath.appending(dependency.subpath) + let path = self.location.repositoriesCheckoutsDirectory.appending(dependency.subpath) // Make sure the directory is not missing (we will have to clone again // if not). @@ -2588,7 +2646,7 @@ extension Workspace { } // Clone the repository into the checkouts. - let path = checkoutsPath.appending(component: package.repository.basename) + let path = self.location.repositoriesCheckoutsDirectory.appending(component: package.repository.basename) try fileSystem.chmod(.userWritable, path: path, options: [.recursive, .onlyFiles]) try fileSystem.removeFileTree(path) @@ -2632,7 +2690,7 @@ extension Workspace { // Write the state record. state.dependencies.add(ManagedDependency( packageRef: package, - subpath: path.relative(to: checkoutsPath), + subpath: path.relative(to: self.location.repositoriesCheckoutsDirectory), checkoutState: checkoutState)) try state.saveState() @@ -2715,7 +2773,7 @@ extension Workspace { } // Remove the checkout. - let dependencyPath = checkoutsPath.appending(dependencyToRemove.subpath) + let dependencyPath = self.location.repositoriesCheckoutsDirectory.appending(dependencyToRemove.subpath) let workingCopy = try repositoryManager.provider.openWorkingCopy(at: dependencyPath) guard !workingCopy.hasUncommittedChanges() else { throw WorkspaceDiagnostics.UncommitedChanges(repositoryPath: dependencyPath) diff --git a/Sources/Workspace/WorkspaceConfiguration.swift b/Sources/Workspace/WorkspaceConfiguration.swift index 708aebc35bc..4480256b7ab 100644 --- a/Sources/Workspace/WorkspaceConfiguration.swift +++ b/Sources/Workspace/WorkspaceConfiguration.swift @@ -16,6 +16,72 @@ import TSCUtility import PackageGraph extension Workspace { + /// Workspace location configuration + public struct Location { + /// Path to working directory for this workspace. + public var workingDirectory: AbsolutePath + + /// Path to store the editable versions of dependencies. + public var editsDirectory: AbsolutePath + + /// Path to the Package.resolved file. + public var resolvedVersionsFilePath: AbsolutePath + + /// Path to the shared cache + public var sharedCacheDirectory: AbsolutePath? + + /// Path to the repositories shared cache. + public var repositoriesSharedCacheDirectory: AbsolutePath? { + self.sharedCacheDirectory.map { $0.appending(component: "repositories") } + } + + /// Path to the repositories clones. + public var repositoriesDirectory: AbsolutePath { + self.workingDirectory.appending(component: "repositories") + } + + /// Path to the repository checkouts. + public var repositoriesCheckoutsDirectory: AbsolutePath { + self.workingDirectory.appending(component: "checkouts") + } + + /// Path to the downloaded binary artifacts. + public var artifactsDirectory: AbsolutePath { + self.workingDirectory.appending(component: "artifacts") + } + + /// Create a new workspace location. + /// + /// - Parameters: + /// - workingDirectory: Path to working directory for this workspace. + /// - editsDirectory: Path to store the editable versions of dependencies. + /// - resolvedVersionsFile: Path to the Package.resolved file. + /// - sharedCachePath: Path to the sharedCache + public init( + workingDirectory: AbsolutePath, + editsDirectory: AbsolutePath, + resolvedVersionsFilePath: AbsolutePath, + sharedCacheDirectory: AbsolutePath? = .none + ) { + self.workingDirectory = workingDirectory + self.editsDirectory = editsDirectory + self.resolvedVersionsFilePath = resolvedVersionsFilePath + self.sharedCacheDirectory = sharedCacheDirectory + } + + /// Create a new workspace location. + /// + /// - Parameters: + /// - rootPath: Path to the root of the package, from which other locations can be derived. + public init(forRootPackage rootPath: AbsolutePath) { + self.init( + workingDirectory: rootPath.appending(component: ".build"), + editsDirectory: rootPath.appending(component: "Packages"), + resolvedVersionsFilePath: rootPath.appending(component: "Package.resolved") + ) + } + } + /// Manages a package workspace's configuration. public final class Configuration { /// The path to the mirrors file. @@ -35,16 +101,22 @@ extension Workspace { /// The mirrors. public private(set) var mirrors: DependencyMirrors = DependencyMirrors() + + @available(*, deprecated) + public convenience init(path: AbsolutePath, fs: FileSystem = localFileSystem) throws { + try self.init(path: path, fileSystem: fs) + } + /// Creates a new, persisted package configuration with a configuration file. /// - Parameters: /// - path: A path to the configuration file. - /// - fs: The filesystem on which the configuration file is located. + /// - fileSystem: The filesystem on which the configuration file is located. /// - Throws: `StringError` if the configuration file is corrupted or malformed. - public init(path: AbsolutePath, fs: FileSystem = localFileSystem) throws { + public init(path: AbsolutePath, fileSystem: FileSystem) throws { self.configFile = path - self.fileSystem = fs + self.fileSystem = fileSystem let persistence = SimplePersistence( - fileSystem: fs, + fileSystem: fileSystem, schemaVersion: Self.schemaVersion, statePath: path, prettyPrint: true @@ -58,13 +130,6 @@ extension Workspace { } } - /// Initializes a new, ephemeral package configuration. - public init() { - self.configFile = nil - self.fileSystem = nil - self.persistence = nil - } - /// Load the configuration from disk. public func restoreState() throws { _ = try self.persistence?.restoreState(self) diff --git a/Tests/WorkspaceTests/WorkspaceConfigurationTests.swift b/Tests/WorkspaceTests/WorkspaceConfigurationTests.swift index 9d538bb1dbd..d019fba1b5f 100644 --- a/Tests/WorkspaceTests/WorkspaceConfigurationTests.swift +++ b/Tests/WorkspaceTests/WorkspaceConfigurationTests.swift @@ -38,7 +38,7 @@ final class WorkspaceConfigurationTests: XCTestCase { """ } - let config = try Workspace.Configuration(path: configFile, fs: fs) + let config = try Workspace.Configuration(path: configFile, fileSystem: fs) XCTAssertEqual(config.mirrors.mirrorURL(for: "https://github.com/apple/swift-argument-parser.git"), "https://github.com/mona/swift-argument-parser.git") XCTAssertEqual(config.mirrors.originalURL(for: "https://github.com/mona/swift-argument-parser.git"), "https://github.com/apple/swift-argument-parser.git") @@ -47,7 +47,7 @@ final class WorkspaceConfigurationTests: XCTestCase { func testThrowsMirrorNotFound() throws { let fs = InMemoryFileSystem() let configFile = AbsolutePath("/.swiftpm/config") - let config = try Workspace.Configuration(path: configFile, fs: fs) + let config = try Workspace.Configuration(path: configFile, fileSystem: fs) XCTAssertThrows(DependencyMirrors.Error.mirrorNotFound) { try config.mirrors.unset(originalOrMirrorURL: "https://github.com/apple/swift-argument-parser.git") @@ -57,7 +57,7 @@ final class WorkspaceConfigurationTests: XCTestCase { func testEmptyMirrors() throws { let fs = InMemoryFileSystem() let configFile = AbsolutePath("/.swiftpm/config") - let config = try Workspace.Configuration(path: configFile, fs: fs) + let config = try Workspace.Configuration(path: configFile, fileSystem: fs) try config.saveState() XCTAssertFalse(fs.exists(configFile)) diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index d9d65625656..d805ef69d54 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -17,7 +17,7 @@ import SourceControl import SPMBuildCore import TSCBasic import TSCUtility -import Workspace +@testable import Workspace import Basics import SPMTestSupport @@ -107,7 +107,7 @@ final class WorkspaceTests: XCTestCase { result.check(dependency: "quix", at: .checkout(.version("1.2.0"))) } - let stateFile = workspace.getOrCreateWorkspace().state.path + let stateFile = try workspace.getOrCreateWorkspace().state.path // Remove state file and check we can get the state back automatically. try fs.removeFileTree(stateFile) @@ -136,13 +136,16 @@ final class WorkspaceTests: XCTestCase { let manifestLoader = ManifestLoader(toolchain: ToolchainConfiguration.default) let sandbox = path.appending(component: "ws") - return Workspace( - dataPath: sandbox.appending(component: ".build"), - editablesPath: sandbox.appending(component: "edits"), - pinsFile: sandbox.appending(component: "Package.resolved"), - manifestLoader: manifestLoader, - delegate: MockWorkspaceDelegate(), - cachePath: fs.swiftPMCacheDirectory.appending(component: "repositories") + return try Workspace( + fileSystem: fs, + location: .init( + workingDirectory: sandbox.appending(component: ".build"), + editsDirectory: sandbox.appending(component: "edits"), + resolvedVersionsFilePath: sandbox.appending(component: "Package.resolved"), + sharedCacheDirectory: fs.swiftPMCacheDirectory.appending(component: "repositories") + ), + customManifestLoader: manifestLoader, + delegate: MockWorkspaceDelegate() ) } @@ -1697,20 +1700,20 @@ final class WorkspaceTests: XCTestCase { } // Drop a build artifact in data directory. - let ws = workspace.getOrCreateWorkspace() - let buildArtifact = ws.dataPath.appending(component: "test.o") + let ws = try workspace.getOrCreateWorkspace() + let buildArtifact = ws.location.workingDirectory.appending(component: "test.o") try fs.writeFileContents(buildArtifact, bytes: "Hi") // Sanity checks. XCTAssert(fs.exists(buildArtifact)) - XCTAssert(fs.exists(ws.checkoutsPath)) + XCTAssert(fs.exists(ws.location.repositoriesCheckoutsDirectory)) // Check clean. workspace.checkClean { diagnostics in // Only the build artifact should be removed. XCTAssertFalse(fs.exists(buildArtifact)) - XCTAssert(fs.exists(ws.checkoutsPath)) - XCTAssert(fs.exists(ws.dataPath)) + XCTAssert(fs.exists(ws.location.repositoriesCheckoutsDirectory)) + XCTAssert(fs.exists(ws.location.workingDirectory)) XCTAssertNoDiagnostics(diagnostics) } @@ -1725,8 +1728,8 @@ final class WorkspaceTests: XCTestCase { workspace.checkReset { diagnostics in // Only the build artifact should be removed. XCTAssertFalse(fs.exists(buildArtifact)) - XCTAssertFalse(fs.exists(ws.checkoutsPath)) - XCTAssertFalse(fs.exists(ws.dataPath)) + XCTAssertFalse(fs.exists(ws.location.repositoriesCheckoutsDirectory)) + XCTAssertFalse(fs.exists(ws.location.workingDirectory)) XCTAssertNoDiagnostics(diagnostics) } @@ -2041,7 +2044,7 @@ final class WorkspaceTests: XCTestCase { XCTAssertNoDiagnostics(diagnostics) } - try fs.removeFileTree(workspace.getOrCreateWorkspace().checkoutsPath) + try fs.removeFileTree(workspace.getOrCreateWorkspace().location.repositoriesCheckoutsDirectory) workspace.checkPackageGraph(roots: ["Root"]) { graph, diagnostics in PackageGraphTester(graph) { result in @@ -2211,7 +2214,7 @@ final class WorkspaceTests: XCTestCase { } // Edit foo. - let fooPath = workspace.getOrCreateWorkspace().editablesPath.appending(component: "Foo") + let fooPath = try workspace.getOrCreateWorkspace().location.editsDirectory.appending(component: "Foo") workspace.checkEdit(packageName: "Foo") { diagnostics in XCTAssertNoDiagnostics(diagnostics) } @@ -2302,7 +2305,7 @@ final class WorkspaceTests: XCTestCase { workspace.checkPackageGraph(roots: ["Root"]) { _, _ in } // Edit foo. - let fooPath = workspace.getOrCreateWorkspace().editablesPath.appending(component: "Foo") + let fooPath = try workspace.getOrCreateWorkspace().location.editsDirectory.appending(component: "Foo") workspace.checkEdit(packageName: "Foo") { diagnostics in XCTAssertNoDiagnostics(diagnostics) } @@ -2348,7 +2351,7 @@ final class WorkspaceTests: XCTestCase { let deps: [MockDependency] = [ .scm(path: "./Foo", requirement: .upToNextMajor(from: "1.0.0"), products: .specific(["Foo"])), ] - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() // Load the graph and edit foo. workspace.checkPackageGraph(deps: deps) { graph, diagnostics in @@ -2793,7 +2796,7 @@ final class WorkspaceTests: XCTestCase { versions: ["1.5.0"] ), ], - skipUpdate: true + resolverUpdateEnabled: false ) // Run update and remove all events. @@ -3314,7 +3317,7 @@ final class WorkspaceTests: XCTestCase { result.check(dependency: "foo", at: .local) } do { - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() XCTAssertNotNil(ws.state.dependencies[forURL: "/tmp/ws/pkgs/Foo"]) } @@ -3328,7 +3331,7 @@ final class WorkspaceTests: XCTestCase { result.check(dependency: "foo", at: .local) } do { - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() XCTAssertNotNil(ws.state.dependencies[forURL: "/tmp/ws/pkgs/Nested/Foo"]) } } @@ -3392,7 +3395,7 @@ final class WorkspaceTests: XCTestCase { let sandbox = AbsolutePath("/tmp/ws/") let fs = InMemoryFileSystem() - let config = try Workspace.Configuration(path: sandbox.appending(component: "swiftpm"), fs: fs) + let config = try Workspace.Configuration(path: sandbox.appending(component: "swiftpm"), fileSystem: fs) config.mirrors.set(mirrorURL: sandbox.appending(components: "pkgs", "Baz").pathString, forURL: sandbox.appending(components: "pkgs", "Bar").pathString) config.mirrors.set(mirrorURL: sandbox.appending(components: "pkgs", "Baz").pathString, forURL: sandbox.appending(components: "pkgs", "Bam").pathString) try config.saveState() @@ -3584,7 +3587,7 @@ final class WorkspaceTests: XCTestCase { } do { - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() XCTAssertNotNil(ws.state.dependencies[forURL: "/tmp/ws/pkgs/Foo"]) } @@ -3608,7 +3611,7 @@ final class WorkspaceTests: XCTestCase { } do { - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() XCTAssertNotNil(ws.state.dependencies[forURL: "/tmp/ws/pkgs/Nested/Foo"]) } } @@ -3675,7 +3678,7 @@ final class WorkspaceTests: XCTestCase { // Change pin of foo to something else. do { - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() let pinsStore = try ws.pinsStore.load() let fooPin = pinsStore.pins.first(where: { $0.packageRef.identity.description == "foo" })! @@ -3827,7 +3830,7 @@ final class WorkspaceTests: XCTestCase { let packagePath = AbsolutePath(#file).parentDirectory.parentDirectory.parentDirectory let diagnostics = DiagnosticsEngine() - let workspace = try Workspace(forRootPackage: packagePath, toolchain: UserToolchain.default) + let workspace = try Workspace(forRootPackage: packagePath, customToolchain: UserToolchain.default) // From here the API should be simple and straightforward: let manifest = try tsc_await { @@ -4094,7 +4097,7 @@ final class WorkspaceTests: XCTestCase { } // Edit foo. - let fooPath = workspace.getOrCreateWorkspace().editablesPath.appending(component: "Foo") + let fooPath = try workspace.getOrCreateWorkspace().location.editsDirectory.appending(component: "Foo") workspace.checkEdit(packageName: "Foo") { diagnostics in XCTAssertNoDiagnostics(diagnostics) } @@ -4211,8 +4214,7 @@ final class WorkspaceTests: XCTestCase { toolsVersion: .v5_2 ), ], - toolsVersion: .v5_2, - enablePubGrub: true + toolsVersion: .v5_2 ) // Load the graph. @@ -4248,7 +4250,7 @@ final class WorkspaceTests: XCTestCase { packages: [] ) - let ws = workspace.getOrCreateWorkspace() + let ws = try workspace.getOrCreateWorkspace() // Checks the valid case. do {