From 9d7278c332c7b5eac484e89d4b290758331179cc Mon Sep 17 00:00:00 2001 From: Ankit Aggarwal Date: Thu, 16 Aug 2018 13:22:53 -0700 Subject: [PATCH] Implement SE-0219 Package Manager Dependency Mirroring [SR-8328]: Implement SE-0219 Package Manager Dependency Mirroring https://bugs.swift.org/browse/SR-8328 --- Sources/Commands/SwiftPackageTool.swift | 141 ++++++++++++++++++ Sources/Commands/SwiftTestTool.swift | 10 +- Sources/Commands/SwiftTool.swift | 12 ++ Sources/PackageGraph/PackageGraphLoader.swift | 12 +- .../PackageGraph/RawPackageConstraints.swift | 12 +- .../RepositoryPackageContainerProvider.swift | 17 ++- Sources/SourceControl/SwiftPMConfig.swift | 123 +++++++++++++++ Sources/TestSupport/TestWorkspace.swift | 3 + Sources/Workspace/Workspace.swift | 35 ++++- Tests/CommandsTests/PackageToolTests.swift | 59 +++++++- Tests/CommandsTests/XCTestManifests.swift | 1 + .../DependencyResolverPerfTests.swift | 10 +- Tests/WorkspaceTests/WorkspaceTests.swift | 89 +++++++++++ Tests/WorkspaceTests/XCTestManifests.swift | 1 + 14 files changed, 498 insertions(+), 27 deletions(-) create mode 100644 Sources/SourceControl/SwiftPMConfig.swift diff --git a/Sources/Commands/SwiftPackageTool.swift b/Sources/Commands/SwiftPackageTool.swift index f1446ef2753..ed526dc6a50 100644 --- a/Sources/Commands/SwiftPackageTool.swift +++ b/Sources/Commands/SwiftPackageTool.swift @@ -30,6 +30,30 @@ struct FetchDeprecatedDiagnostic: DiagnosticData { ) } +struct RequiredArgumentDiagnostic: DiagnosticData { + static let id = DiagnosticID( + type: RequiredArgumentDiagnostic.self, + name: "org.swift.diags.required-argument", + defaultBehavior: .error, + description: { + $0 <<< "missing required argument" <<< { "\($0.argument)" } + } + ) + + let argument: String +} + +struct RequiredSubcommandDiagnostic: DiagnosticData { + static let id = DiagnosticID( + type: RequiredSubcommandDiagnostic.self, + name: "org.swift.diags.required-subcommand", + defaultBehavior: .error, + description: { + $0 <<< "missing required subcommand; use --help to list available subcommands" + } + ) +} + /// swift-package tool namespace public class SwiftPackageTool: SwiftTool { @@ -48,6 +72,51 @@ public class SwiftPackageTool: SwiftTool { case .version: print(Versioning.currentVersion.completeDisplayString) + case .config: + guard let configMode = options.configMode else { + diagnostics.emit(data: RequiredSubcommandDiagnostic()) + return + } + + let config = try getSwiftPMConfig() + try config.load() + + switch configMode { + case .getMirror: + guard let packageURL = options.configOptions.packageURL else { + diagnostics.emit(data: RequiredArgumentDiagnostic(argument: "--package-url")) + return + } + + if let mirror = config.getMirror(forURL: packageURL) { + print(mirror) + } else { + stderrStream <<< "not found\n" + stderrStream.flush() + executionStatus = .failure + } + + case .unsetMirror: + guard let packageOrMirror = options.configOptions.packageURL ?? options.configOptions.mirrorURL else { + diagnostics.emit(data: RequiredArgumentDiagnostic(argument: "--package-url or --mirror-url")) + return + } + + try config.unset(packageOrMirrorURL: packageOrMirror) + + case .setMirror: + guard let packageURL = options.configOptions.packageURL else { + diagnostics.emit(data: RequiredArgumentDiagnostic(argument: "--package-url")) + return + } + guard let mirrorURL = options.configOptions.mirrorURL else { + diagnostics.emit(data: RequiredArgumentDiagnostic(argument: "--mirror-url")) + return + } + + try config.set(mirrorURL: mirrorURL, forPackageURL: packageURL) + } + case .initPackage: // FIXME: Error handling. let cwd = localFileSystem.currentWorkingDirectory! @@ -347,6 +416,58 @@ public class SwiftPackageTool: SwiftTool { usage: "Set tools version of package to the current tools version in use"), to: { if $1 { $0.toolsVersionMode = .setCurrent } }) + // SwiftPM config subcommand. + let configParser = parser.add( + subparser: PackageMode.config.rawValue, + overview: "Manipulate configuration of the package") + binder.bind(parser: configParser, + to: { $0.configMode = PackageToolOptions.ConfigMode(rawValue: $1)! }) + + let setMirrorParser = configParser.add( + subparser: PackageToolOptions.ConfigMode.setMirror.rawValue, + overview: "Set a mirror for a dependency") + + binder.bind( + setMirrorParser.add( + option: "--package-url", kind: String.self, + usage: "The package dependency url"), + setMirrorParser.add( + option: "--mirror-url", kind: String.self, + usage: "The mirror url"), + to: { + $0.configOptions.packageURL = $1 + $0.configOptions.mirrorURL = $2 + } + ) + + let unsetMirrorParser = configParser.add( + subparser: PackageToolOptions.ConfigMode.unsetMirror.rawValue, + overview: "Remove an existing mirror") + binder.bind( + unsetMirrorParser.add( + option: "--package-url", kind: String.self, + usage: "The package dependency url"), + unsetMirrorParser.add( + option: "--mirror-url", kind: String.self, + usage: "The mirror url"), + to: { + $0.configOptions.packageURL = $1 + $0.configOptions.mirrorURL = $2 + } + ) + + let getMirrorParser = configParser.add( + subparser: PackageToolOptions.ConfigMode.getMirror.rawValue, + overview: "Print mirror configuration for the given package dependency") + binder.bind( + option: getMirrorParser.add( + option: "--package-url", kind: String.self, usage: "The package dependency url"), + to: { + $0.configOptions.packageURL = $1 + } + ) + + // Xcode project generation. let generateXcodeParser = parser.add( subparser: PackageMode.generateXcodeproj.rawValue, overview: "Generates an Xcode project") @@ -482,10 +603,24 @@ public class PackageToolOptions: ToolOptions { case setCurrent } var toolsVersionMode: ToolsVersionMode = .display + + enum ConfigMode: String { + case setMirror = "set-mirror" + case unsetMirror = "unset-mirror" + case getMirror = "get-mirror" + } + var configMode: ConfigMode? + + struct ConfigOptions { + var packageURL: String? + var mirrorURL: String? + } + var configOptions = ConfigOptions() } public enum PackageMode: String, StringEnumArgument { case clean + case config case describe case dumpPackage = "dump-package" case edit @@ -548,6 +683,12 @@ extension PackageToolOptions.CompletionToolMode: StringEnumArgument { } } +extension PackageToolOptions.ConfigMode: StringEnumArgument { + static var completion: ShellCompletion { + return .none + } +} + extension SwiftPackageTool: ToolName { static var toolName: String { return "swift package" diff --git a/Sources/Commands/SwiftTestTool.swift b/Sources/Commands/SwiftTestTool.swift index aca313ea3e7..42b0db3820a 100644 --- a/Sources/Commands/SwiftTestTool.swift +++ b/Sources/Commands/SwiftTestTool.swift @@ -54,11 +54,11 @@ struct NoMatchingTestsWarning: DiagnosticData { ) } -/// Diagnostic error when a command is run without its requeried command. -struct RequiredArgumentDiagnostic: DiagnosticData { +/// Diagnostic error when a command is run without its dependent command. +struct DependentArgumentDiagnostic: DiagnosticData { static let id = DiagnosticID( - type: RequiredArgumentDiagnostic.self, - name: "org.swift.diags.required-argument", + type: DependentArgumentDiagnostic.self, + name: "org.swift.diags.dependent-argument", defaultBehavior: .error, description: { $0 <<< { "\($0.dependentArgument)" } <<< "must be used with" <<< { "\($0.requiredArgument)" } @@ -423,7 +423,7 @@ public class SwiftTestTool: SwiftTool { // The --num-worker option should be called with --parallel. guard options.mode == .runParallel else { diagnostics.emit( - data: RequiredArgumentDiagnostic(requiredArgument: "--parallel", dependentArgument: "--num-workers")) + data: DependentArgumentDiagnostic(requiredArgument: "--parallel", dependentArgument: "--num-workers")) throw Diagnostics.fatalError } diff --git a/Sources/Commands/SwiftTool.swift b/Sources/Commands/SwiftTool.swift index 098cd5b1013..e6d8186a736 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/Commands/SwiftTool.swift @@ -417,6 +417,17 @@ public class SwiftTool { return try getPackageRoot().appending(component: "Package.resolved") } + func configFilePath() throws -> AbsolutePath { + return try getPackageRoot().appending(components: ".swiftpm", "config") + } + + func getSwiftPMConfig() throws -> SwiftPMConfig { + return try _swiftpmConfig.dematerialize() + } + private lazy var _swiftpmConfig: Result = { + return Result(anyError: { SwiftPMConfig(path: try configFilePath()) }) + }() + /// Holds the currently active workspace. /// /// It is not initialized in init() because for some of the commands like package init , usage etc, @@ -439,6 +450,7 @@ public class SwiftTool { manifestLoader: try getManifestLoader(), toolsVersionLoader: ToolsVersionLoader(), delegate: delegate, + config: try getSwiftPMConfig(), repositoryProvider: provider, isResolverPrefetchingEnabled: options.shouldEnableResolverPrefetching, skipUpdate: options.skipDependencyUpdate diff --git a/Sources/PackageGraph/PackageGraphLoader.swift b/Sources/PackageGraph/PackageGraphLoader.swift index f6409c5757f..a9d7476eb4c 100644 --- a/Sources/PackageGraph/PackageGraphLoader.swift +++ b/Sources/PackageGraph/PackageGraphLoader.swift @@ -9,6 +9,7 @@ */ import Basic +import SourceControl import PackageLoading import PackageModel import Utility @@ -73,6 +74,7 @@ public struct PackageGraphLoader { /// Load the package graph for the given package path. public func load( root: PackageGraphRoot, + config: SwiftPMConfig = SwiftPMConfig(), externalManifests: [Manifest], diagnostics: DiagnosticsEngine, fileSystem: FileSystem = localFileSystem, @@ -90,8 +92,9 @@ public struct PackageGraphLoader { externalManifests.map({ (PackageReference.computeIdentity(packageURL: $0.url), $0) }) let manifestMap = Dictionary(uniqueKeysWithValues: manifestMapSequence) let successors: (Manifest) -> [Manifest] = { manifest in - manifest.dependencies.compactMap({ - manifestMap[PackageReference.computeIdentity(packageURL: $0.url)] + manifest.dependencies.compactMap({ + let url = config.mirroredURL(forURL: $0.url) + return manifestMap[PackageReference.computeIdentity(packageURL: url)] }) } @@ -150,6 +153,7 @@ public struct PackageGraphLoader { // Resolve dependencies and create resolved packages. let resolvedPackages = createResolvedPackages( allManifests: allManifests, + config: config, manifestToPackage: manifestToPackage, rootManifestSet: rootManifestSet, diagnostics: diagnostics @@ -205,6 +209,7 @@ private func checkAllDependenciesAreUsed(_ rootPackages: [ResolvedPackage], _ di /// Create resolved packages from the loaded packages. private func createResolvedPackages( allManifests: [Manifest], + config: SwiftPMConfig, manifestToPackage: [Manifest: Package], // FIXME: This shouldn't be needed once is fixed. rootManifestSet: Set, @@ -232,7 +237,8 @@ private func createResolvedPackages( // Establish the manifest-declared package dependencies. packageBuilder.dependencies = package.manifest.dependencies.compactMap({ - packageMap[PackageReference.computeIdentity(packageURL: $0.url)] + let url = config.mirroredURL(forURL: $0.url) + return packageMap[PackageReference.computeIdentity(packageURL: url)] }) // Create target builders for each target in the package. diff --git a/Sources/PackageGraph/RawPackageConstraints.swift b/Sources/PackageGraph/RawPackageConstraints.swift index 11273056073..226eb615b2e 100644 --- a/Sources/PackageGraph/RawPackageConstraints.swift +++ b/Sources/PackageGraph/RawPackageConstraints.swift @@ -9,13 +9,15 @@ */ import PackageModel +import SourceControl extension PackageDependencyDescription { /// Create the package reference object for the dependency. - public func createPackageRef() -> PackageReference { + public func createPackageRef(config: SwiftPMConfig) -> PackageReference { + let effectiveURL = config.mirroredURL(forURL: self.url) return PackageReference( - identity: PackageReference.computeIdentity(packageURL: url), - path: url, + identity: PackageReference.computeIdentity(packageURL: effectiveURL), + path: effectiveURL, isLocal: (requirement == .localPackage) ) } @@ -24,10 +26,10 @@ extension PackageDependencyDescription { extension Manifest { /// Constructs constraints of the dependencies in the raw package. - public func dependencyConstraints() -> [RepositoryPackageConstraint] { + public func dependencyConstraints(config: SwiftPMConfig) -> [RepositoryPackageConstraint] { return dependencies.map({ return RepositoryPackageConstraint( - container: $0.createPackageRef(), + container: $0.createPackageRef(config: config), requirement: $0.requirement.toConstraintRequirement()) }) } diff --git a/Sources/PackageGraph/RepositoryPackageContainerProvider.swift b/Sources/PackageGraph/RepositoryPackageContainerProvider.swift index 8b480b05474..6df8f091bd3 100644 --- a/Sources/PackageGraph/RepositoryPackageContainerProvider.swift +++ b/Sources/PackageGraph/RepositoryPackageContainerProvider.swift @@ -25,6 +25,7 @@ public class RepositoryPackageContainerProvider: PackageContainerProvider { let repositoryManager: RepositoryManager let manifestLoader: ManifestLoaderProtocol + let config: SwiftPMConfig /// The tools version currently in use. Only the container versions less than and equal to this will be provided by /// the container. @@ -45,11 +46,13 @@ public class RepositoryPackageContainerProvider: PackageContainerProvider { /// - toolsVersionLoader: The tools version loader. public init( repositoryManager: RepositoryManager, + config: SwiftPMConfig = SwiftPMConfig(), manifestLoader: ManifestLoaderProtocol, currentToolsVersion: ToolsVersion = ToolsVersion.currentToolsVersion, toolsVersionLoader: ToolsVersionLoaderProtocol = ToolsVersionLoader() ) { self.repositoryManager = repositoryManager + self.config = config self.manifestLoader = manifestLoader self.currentToolsVersion = currentToolsVersion self.toolsVersionLoader = toolsVersionLoader @@ -64,6 +67,7 @@ public class RepositoryPackageContainerProvider: PackageContainerProvider { if identifier.isLocal { callbacksQueue.async { let container = LocalPackageContainer(identifier, + config: self.config, manifestLoader: self.manifestLoader, toolsVersionLoader: self.toolsVersionLoader, currentToolsVersion: self.currentToolsVersion) @@ -82,6 +86,7 @@ public class RepositoryPackageContainerProvider: PackageContainerProvider { let repository = try handle.open() return RepositoryPackageContainer( identifier: identifier, + config: self.config, repository: repository, manifestLoader: self.manifestLoader, toolsVersionLoader: self.toolsVersionLoader, @@ -116,6 +121,8 @@ public class BasePackageContainer: PackageContainer { public let identifier: Identifier + let config: SwiftPMConfig + /// The manifest loader. let manifestLoader: ManifestLoaderProtocol @@ -147,11 +154,13 @@ public class BasePackageContainer: PackageContainer { fileprivate init( _ identifier: Identifier, + config: SwiftPMConfig, manifestLoader: ManifestLoaderProtocol, toolsVersionLoader: ToolsVersionLoaderProtocol, currentToolsVersion: ToolsVersion ) { self.identifier = identifier + self.config = config self.manifestLoader = manifestLoader self.toolsVersionLoader = toolsVersionLoader self.currentToolsVersion = currentToolsVersion @@ -195,7 +204,7 @@ public class LocalPackageContainer: BasePackageContainer, CustomStringConvertibl } public override func getUnversionedDependencies() throws -> [PackageContainerConstraint] { - return try loadManifest().dependencyConstraints() + return try loadManifest().dependencyConstraints(config: config) } public override func getUpdatedIdentifier(at boundVersion: BoundVersion) throws -> Identifier { @@ -206,6 +215,7 @@ public class LocalPackageContainer: BasePackageContainer, CustomStringConvertibl public init( _ identifier: Identifier, + config: SwiftPMConfig, manifestLoader: ManifestLoaderProtocol, toolsVersionLoader: ToolsVersionLoaderProtocol, currentToolsVersion: ToolsVersion, @@ -215,6 +225,7 @@ public class LocalPackageContainer: BasePackageContainer, CustomStringConvertibl self.fs = fs super.init( identifier, + config: config, manifestLoader: manifestLoader, toolsVersionLoader: toolsVersionLoader, currentToolsVersion: currentToolsVersion @@ -279,6 +290,7 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve init( identifier: PackageReference, + config: SwiftPMConfig, repository: Repository, manifestLoader: ManifestLoaderProtocol, toolsVersionLoader: ToolsVersionLoaderProtocol, @@ -304,6 +316,7 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve self.reversedVersions = [Version](knownVersions.keys).sorted().reversed() super.init( identifier, + config: config, manifestLoader: manifestLoader, toolsVersionLoader: toolsVersionLoader, currentToolsVersion: currentToolsVersion @@ -394,7 +407,7 @@ public class RepositoryPackageContainer: BasePackageContainer, CustomStringConve manifestVersion: toolsVersion.manifestVersion, fileSystem: fs) - return (manifest, manifest.dependencyConstraints()) + return (manifest, manifest.dependencyConstraints(config: config)) } public override func getUnversionedDependencies() throws -> [PackageContainerConstraint] { diff --git a/Sources/SourceControl/SwiftPMConfig.swift b/Sources/SourceControl/SwiftPMConfig.swift new file mode 100644 index 00000000000..fb8011ac5e5 --- /dev/null +++ b/Sources/SourceControl/SwiftPMConfig.swift @@ -0,0 +1,123 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2018 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +import Basic +import Utility + +/// Manages a package's configuration. +/// +// FIXME: We may want to move this class to some other layer once we start +// supporting more things than just mirrors. +public final class SwiftPMConfig { + + enum Error: Swift.Error, CustomStringConvertible { + case mirrorNotFound + + var description: String { + return "mirror not found" + } + } + + /// Persistence support. + let persistence: SimplePersistence? + + /// The schema version of the config file. + /// + /// * 1: Initial version. + static let schemaVersion: Int = 1 + + /// The mirrors. + private var mirrors: [String: Mirror] + + public init(path: AbsolutePath, fs: FileSystem = localFileSystem) { + self.mirrors = [:] + self.persistence = SimplePersistence( + fileSystem: fs, schemaVersion: SwiftPMConfig.schemaVersion, + statePath: path, prettyPrint: true + ) + } + + public init() { + self.mirrors = [:] + self.persistence = nil + } + + /// Set a mirror URL for the given package URL. + public func set(mirrorURL: String, forPackageURL packageURL: String) throws { + mirrors[packageURL] = Mirror(original: packageURL, mirror: mirrorURL) + try saveState() + } + + /// Unset a mirror for the given package or mirror URL. + /// + /// This method will throw if there is no mirror for the given input. + public func unset(packageOrMirrorURL: String) throws { + if mirrors.keys.contains(packageOrMirrorURL) { + mirrors[packageOrMirrorURL] = nil + } else if let mirror = mirrors.first(where: { $0.value.mirror == packageOrMirrorURL }) { + mirrors[mirror.key] = nil + } else { + throw Error.mirrorNotFound + } + try saveState() + } + + /// Returns the mirror for the given specificer. + public func getMirror(forURL url: String) -> String? { + return mirrors[url]?.mirror + } + + /// Returns the tr + public func mirroredURL(forURL url: String) -> String { + return getMirror(forURL: url) ?? url + } + + /// Load the configuration from disk. + public func load() throws { + _ = try self.persistence?.restoreState(self) + } +} + +// MARK: - Persistence. +extension SwiftPMConfig: SimplePersistanceProtocol { + + public func saveState() throws { + try self.persistence?.saveState(self) + } + + public func restore(from json: JSON) throws { + // FIXME: Find a way to avoid encode-decode dance here. + let data = Data(bytes: json.toBytes().contents) + let mirrorsData = try JSONDecoder().decode([Mirror].self, from: data) + self.mirrors = Dictionary(mirrorsData.map({ ($0.original, $0) }), uniquingKeysWith: { first, _ in first }) + } + + public func toJSON() -> JSON { + // FIXME: Find a way to avoid encode-decode dance here. + let jsonData = try! JSONEncoder().encode(mirrors.values.sorted(by: { $0.original < $1.mirror })) + return try! JSON(data: jsonData) + } +} + +/// An individual repository mirror. +public struct Mirror: Codable { + /// The original repository path. + public let original: String + + /// The mirrored repository path. + public let mirror: String + + public init(original: String, mirror: String) { + self.original = original + self.mirror = mirror + } +} diff --git a/Sources/TestSupport/TestWorkspace.swift b/Sources/TestSupport/TestWorkspace.swift index aadb4fc98c3..a856c5f75a1 100644 --- a/Sources/TestSupport/TestWorkspace.swift +++ b/Sources/TestSupport/TestWorkspace.swift @@ -23,6 +23,7 @@ public final class TestWorkspace { let fs: FileSystem let roots: [TestPackage] let packages: [TestPackage] + public let config: SwiftPMConfig public var manifestLoader: MockManifestLoader public var repoProvider: InMemoryGitRepositoryProvider public let delegate = TestWorkspaceDelegate() @@ -40,6 +41,7 @@ public final class TestWorkspace { precondition(Set(roots.map({$0.name})).count == roots.count, "Root packages should be unique") self.sandbox = sandbox self.fs = fs + self.config = SwiftPMConfig(path: sandbox.appending(component: "swiftpm"), fs: fs) self.roots = roots self.packages = packages @@ -133,6 +135,7 @@ public final class TestWorkspace { currentToolsVersion: toolsVersion, toolsVersionLoader: ToolsVersionLoader(), delegate: delegate, + config: config, fileSystem: fs, repositoryProvider: repoProvider, skipUpdate: skipUpdate diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 7ce1f4c7ae9..51685a90dbe 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -161,15 +161,19 @@ public class Workspace { var requiredIdentities = transitiveClosure(inputIdentities) { identity in guard let manifest = manifestsMap[identity.identity] else { return [] } return manifest.dependencies.map({ - let identity = PackageReference.computeIdentity(packageURL: $0.url) - return PackageReference(identity: identity, path: $0.url) + let url = workspace.config.mirroredURL(forURL: $0.url) + let identity = PackageReference.computeIdentity(packageURL: url) + return PackageReference(identity: identity, path: url) }) } requiredIdentities.formUnion(inputIdentities) - let availableIdentities: Set = Set(manifestsMap.map({ PackageReference(identity: $0.key, path: $0.value.url) })) + let availableIdentities: Set = Set(manifestsMap.map({ + let url = workspace.config.mirroredURL(forURL: $0.1.url) + return PackageReference(identity: $0.key, path: url) + })) // We should never have loaded a manifest we don't need. - assert(availableIdentities.isSubset(of: requiredIdentities)) + assert(availableIdentities.isSubset(of: requiredIdentities), "\(availableIdentities) | \(requiredIdentities)") // These are the missing package identities. let missingIdentities = requiredIdentities.subtracting(availableIdentities) @@ -199,7 +203,7 @@ public class Workspace { case .checkout, .local: // For checkouts, add all the constraints in the manifest. - allConstraints += externalManifest.dependencyConstraints() + allConstraints += externalManifest.dependencyConstraints(config: workspace.config) } } return allConstraints @@ -236,6 +240,9 @@ public class Workspace { /// The path of the workspace data. public let dataPath: AbsolutePath + /// The swiftpm config. + fileprivate let config: SwiftPMConfig + /// The current state of managed dependencies. public let managedDependencies: ManagedDependencies @@ -297,6 +304,7 @@ public class Workspace { currentToolsVersion: ToolsVersion = ToolsVersion.currentToolsVersion, toolsVersionLoader: ToolsVersionLoaderProtocol = ToolsVersionLoader(), delegate: WorkspaceDelegate? = nil, + config: SwiftPMConfig = SwiftPMConfig(), fileSystem: FileSystem = localFileSystem, repositoryProvider: RepositoryProvider = GitRepositoryProvider(), isResolverPrefetchingEnabled: Bool = false, @@ -304,6 +312,7 @@ public class Workspace { ) { self.delegate = delegate self.dataPath = dataPath + self.config = config self.editablesPath = editablesPath self.manifestLoader = manifestLoader self.currentToolsVersion = currentToolsVersion @@ -320,8 +329,10 @@ public class Workspace { self.checkoutsPath = self.dataPath.appending(component: "checkouts") self.containerProvider = RepositoryPackageContainerProvider( repositoryManager: repositoryManager, + config: self.config, manifestLoader: manifestLoader, - toolsVersionLoader: toolsVersionLoader) + toolsVersionLoader: toolsVersionLoader + ) self.fileSystem = fileSystem self.pinsStore = LoadableResult { @@ -516,6 +527,9 @@ extension Workspace { // Create cache directories. createCacheDirectories(with: diagnostics) + // Load the config. + diagnostics.wrap { try config.load() } + // Load the root manifests and currently checked out manifests. let rootManifests = loadRootManifests(packages: root.packages, diagnostics: diagnostics) @@ -584,6 +598,7 @@ extension Workspace { // Load the graph. return PackageGraphLoader().load( root: manifests.root, + config: config, externalManifests: externalManifests, diagnostics: diagnostics, fileSystem: fileSystem, @@ -955,7 +970,7 @@ extension Workspace { // Compute the transitive closure of available dependencies. let allManifests = try! topologicalSort(inputManifests.map({ KeyedPair($0, key: $0.name) })) { node in return node.item.dependencies.compactMap({ dependency in - let url = dependency.url + let url = config.mirroredURL(forURL: dependency.url) let manifest = loadedManifests[url] ?? loadManifest(forURL: url, diagnostics: diagnostics) loadedManifests[url] = manifest return manifest.flatMap({ KeyedPair($0, key: $0.name) }) @@ -1060,6 +1075,9 @@ extension Workspace { // Ensure the cache path exists and validate that edited dependencies. createCacheDirectories(with: diagnostics) + // Load the config. + diagnostics.wrap { try config.load() } + // Load the root manifests and currently checked out manifests. let rootManifests = loadRootManifests(packages: root.packages, diagnostics: diagnostics) @@ -1087,7 +1105,7 @@ extension Workspace { let dependencies = graphRoot.constraints + // Include constraints from the manifests in the graph root. - graphRoot.manifests.flatMap({ $0.dependencyConstraints() }) + + graphRoot.manifests.flatMap({ $0.dependencyConstraints(config: config) }) + currentManifests.dependencyConstraints() + extraConstraints @@ -1610,6 +1628,7 @@ extension Workspace { // Check out the given revision. let workingRepo = try repositoryManager.provider.openCheckout(at: path) + // Inform the delegate. delegate?.checkingOut(repository: package.repository.url, atReference: checkoutState.description, to: path) diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 2fe4a6c3cfa..48b64ee5d2a 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -22,8 +22,9 @@ import Workspace @testable import class Workspace.PinsStore final class PackageToolTests: XCTestCase { + @discardableResult private func execute(_ args: [String], packagePath: AbsolutePath? = nil) throws -> String { - return try SwiftPMProduct.SwiftPackage.execute(args, packagePath: packagePath) + return try SwiftPMProduct.SwiftPackage.execute(args, packagePath: packagePath).chomp() } func testUsage() throws { @@ -529,4 +530,60 @@ final class PackageToolTests: XCTestCase { """) } } + + func testMirrorConfig() { + mktmpdir { prefix in + let fs = localFileSystem + let packageRoot = prefix.appending(component: "Foo") + let configFile = packageRoot.appending(components: ".swiftpm", "config") + + fs.createEmptyFiles(at: packageRoot, files: + "/Sources/Foo/Foo.swift", + "/Tests/FooTests/FooTests.swift", + "/Package.swift", + "anchor" + ) + + // Test writing. + try execute(["config", "set-mirror", "--package-url", "https://github.com/foo/bar", "--mirror-url", "https://mygithub.com/foo/bar"], packagePath: packageRoot) + try execute(["config", "set-mirror", "--package-url", "git@github.com:apple/swift-package-manager.git", "--mirror-url", "git@mygithub.com:foo/swift-package-manager.git"], packagePath: packageRoot) + XCTAssertTrue(fs.isFile(configFile)) + + // Test reading. + XCTAssertEqual(try execute(["config", "get-mirror", "--package-url", "https://github.com/foo/bar"], packagePath: packageRoot), + "https://mygithub.com/foo/bar") + XCTAssertEqual(try execute(["config", "get-mirror", "--package-url", "git@github.com:apple/swift-package-manager.git"], packagePath: packageRoot), + "git@mygithub.com:foo/swift-package-manager.git") + + func check(stderr: String, _ block: () throws -> ()) { + do { + try block() + XCTFail() + } catch SwiftPMProductError.executionFailure(_, _, let stderrOutput) { + XCTAssertEqual(stderrOutput, stderr) + } catch { + XCTFail("unexpected error: \(error)") + } + } + + check(stderr: "not found\n") { + try execute(["config", "get-mirror", "--package-url", "foo"], packagePath: packageRoot) + } + + // Test deletion. + try execute(["config", "unset-mirror", "--package-url", "https://github.com/foo/bar"], packagePath: packageRoot) + try execute(["config", "unset-mirror", "--mirror-url", "git@mygithub.com:foo/swift-package-manager.git"], packagePath: packageRoot) + + check(stderr: "not found\n") { + try execute(["config", "get-mirror", "--package-url", "https://github.com/foo/bar"], packagePath: packageRoot) + } + check(stderr: "not found\n") { + try execute(["config", "get-mirror", "--package-url", "git@github.com:apple/swift-package-manager.git"], packagePath: packageRoot) + } + + check(stderr: "error: mirror not found\n") { + try execute(["config", "unset-mirror", "--package-url", "foo"], packagePath: packageRoot) + } + } + } } diff --git a/Tests/CommandsTests/XCTestManifests.swift b/Tests/CommandsTests/XCTestManifests.swift index 43eefd13c23..2f0dd7b1206 100644 --- a/Tests/CommandsTests/XCTestManifests.swift +++ b/Tests/CommandsTests/XCTestManifests.swift @@ -26,6 +26,7 @@ extension PackageToolTests { ("testInitEmpty", testInitEmpty), ("testInitExecutable", testInitExecutable), ("testInitLibrary", testInitLibrary), + ("testMirrorConfig", testMirrorConfig), ("testPackageClean", testPackageClean), ("testPackageEditAndUnedit", testPackageEditAndUnedit), ("testPackageReset", testPackageReset), diff --git a/Tests/PackageGraphPerformanceTests/DependencyResolverPerfTests.swift b/Tests/PackageGraphPerformanceTests/DependencyResolverPerfTests.swift index 0cc773aea7a..d2d4770c2ec 100644 --- a/Tests/PackageGraphPerformanceTests/DependencyResolverPerfTests.swift +++ b/Tests/PackageGraphPerformanceTests/DependencyResolverPerfTests.swift @@ -96,7 +96,8 @@ class DependencyResolverPerfTests: XCTestCasePerf { ) let containerProvider = RepositoryPackageContainerProvider( - repositoryManager: repositoryManager, manifestLoader: ManifestLoader(resources: Resources.default, isManifestCachingEnabled: false, cacheDir: path)) + repositoryManager: repositoryManager, + manifestLoader: ManifestLoader(resources: Resources.default, isManifestCachingEnabled: false, cacheDir: path)) let resolver = DependencyResolver(containerProvider, GitRepositoryResolutionHelper.DummyResolverDelegate()) let container = PackageReference(identity: "dep", path: dep.asString) @@ -315,14 +316,17 @@ struct GitRepositoryResolutionHelper { } var constraints: [RepositoryPackageConstraint] { - return manifestGraph.rootManifest.dependencyConstraints() + return manifestGraph.rootManifest.dependencyConstraints(config: SwiftPMConfig()) } func resolve(prefetchingEnabled: Bool = false) -> [(container: PackageReference, binding: BoundVersion)] { let repositoriesPath = path.appending(component: "repositories") _ = try? systemQuietly(["rm", "-r", repositoriesPath.asString]) let repositoryManager = RepositoryManager(path: repositoriesPath, provider: GitRepositoryProvider(), delegate: DummyRepositoryManagerDelegate()) - let containerProvider = RepositoryPackageContainerProvider(repositoryManager: repositoryManager, manifestLoader: manifestGraph.manifestLoader) + let containerProvider = RepositoryPackageContainerProvider( + repositoryManager: repositoryManager, + manifestLoader: manifestGraph.manifestLoader + ) let resolver = DependencyResolver(containerProvider, DummyResolverDelegate(), isPrefetchingEnabled: prefetchingEnabled) let result = try! resolver.resolve(constraints: constraints) return result diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index 1fbef7a2db9..5a32b961aa9 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -2371,6 +2371,95 @@ final class WorkspaceTests: XCTestCase { result.check(notPresent: "foo") } } + + func testPackageMirror() throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try TestWorkspace( + sandbox: sandbox, + fs: fs, + roots: [ + TestPackage( + name: "Foo", + targets: [ + TestTarget(name: "Foo", dependencies: ["Dep"]), + ], + products: [ + TestProduct(name: "Foo", targets: ["Foo"]), + ], + dependencies: [ + TestDependency(name: "Dep", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + TestPackage( + name: "Dep", + targets: [ + TestTarget(name: "Dep", dependencies: ["Bar"]), + ], + products: [ + TestProduct(name: "Dep", targets: ["Dep"]), + ], + dependencies: [ + TestDependency(name: "Bar", requirement: .upToNextMajor(from: "1.0.0")), + ], + versions: ["1.0.0", "1.5.0"] + ), + TestPackage( + name: "Bar", + targets: [ + TestTarget(name: "Bar"), + ], + products: [ + TestProduct(name: "Bar", targets: ["Bar"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + TestPackage( + name: "Baz", + targets: [ + TestTarget(name: "Baz"), + ], + products: [ + TestProduct(name: "Bar", targets: ["Baz"]), + ], + versions: ["1.0.0", "1.4.0"] + ), + ] + ) + + workspace.checkPackageGraph(roots: ["Foo"]) { (graph, diagnostics) in + PackageGraphTester(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Foo", "Dep", "Bar") + result.check(targets: "Foo", "Dep", "Bar") + } + XCTAssertNoDiagnostics(diagnostics) + } + workspace.checkManagedDependencies() { result in + result.check(dependency: "Dep", at: .checkout(.version("1.5.0"))) + result.check(dependency: "Bar", at: .checkout(.version("1.5.0"))) + result.check(notPresent: "Baz") + } + + try workspace.config.set(mirrorURL: workspace.packagesDir.appending(component: "Baz").asString, forPackageURL: workspace.packagesDir.appending(component: "Bar").asString) + + workspace.checkPackageGraph(roots: ["Foo"]) { (graph, diagnostics) in + PackageGraphTester(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Foo", "Dep", "Baz") + result.check(targets: "Foo", "Dep", "Baz") + } + XCTAssertNoDiagnostics(diagnostics) + } + workspace.checkManagedDependencies() { result in + result.check(dependency: "Dep", at: .checkout(.version("1.5.0"))) + result.check(dependency: "Baz", at: .checkout(.version("1.4.0"))) + result.check(notPresent: "Bar") + } + } } extension PackageGraph { diff --git a/Tests/WorkspaceTests/XCTestManifests.swift b/Tests/WorkspaceTests/XCTestManifests.swift index 980d3cc0112..8f4cf254d91 100644 --- a/Tests/WorkspaceTests/XCTestManifests.swift +++ b/Tests/WorkspaceTests/XCTestManifests.swift @@ -65,6 +65,7 @@ extension WorkspaceTests { ("testLocalVersionSwitch", testLocalVersionSwitch), ("testMissingEditCanRestoreOriginalCheckout", testMissingEditCanRestoreOriginalCheckout), ("testMultipleRootPackages", testMultipleRootPackages), + ("testPackageMirror", testPackageMirror), ("testResolutionFailureWithEditedDependency", testResolutionFailureWithEditedDependency), ("testResolve", testResolve), ("testResolvedFileUpdate", testResolvedFileUpdate),