diff --git a/Sources/Commands/PackageCommands/Config.swift b/Sources/Commands/PackageCommands/Config.swift index ef2720f2682..a26a488e179 100644 --- a/Sources/Commands/PackageCommands/Config.swift +++ b/Sources/Commands/PackageCommands/Config.swift @@ -46,6 +46,9 @@ extension SwiftPackageCommand.Config { @Option(name: .customLong("mirror-url"), help: .hidden) var _deprecate_mirrorURL: String? + @Flag(help: "Apply settings to all projects for this user.") + var global: Bool = false + @Option(help: "The original url or identity.") var original: String? @@ -53,8 +56,6 @@ extension SwiftPackageCommand.Config { var mirror: String? func run(_ swiftCommandState: SwiftCommandState) throws { - let config = try getMirrorsConfig(swiftCommandState) - if self._deprecate_packageURL != nil { swiftCommandState.observabilityScope.emit( warning: "'--package-url' option is deprecated; use '--original' instead" @@ -81,8 +82,15 @@ extension SwiftPackageCommand.Config { throw ExitCode.failure } - try config.applyLocal { mirrors in - try mirrors.set(mirror: mirror, for: original) + let config = try getMirrorsConfig(swiftCommandState, global: self.global) + if self.global { + try config.applyShared { mirrors in + try mirrors.set(mirror: mirror, for: original) + } + } else { + try config.applyLocal { mirrors in + try mirrors.set(mirror: mirror, for: original) + } } } } @@ -104,6 +112,9 @@ extension SwiftPackageCommand.Config { @Option(name: .customLong("mirror-url"), help: .hidden) var _deprecate_mirrorURL: String? + @Flag(help: "Apply settings to all projects for this user.") + var global: Bool = false + @Option(help: "The original url or identity.") var original: String? @@ -111,8 +122,6 @@ extension SwiftPackageCommand.Config { var mirror: String? func run(_ swiftCommandState: SwiftCommandState) throws { - let config = try getMirrorsConfig(swiftCommandState) - if self._deprecate_packageURL != nil { swiftCommandState.observabilityScope.emit( warning: "'--package-url' option is deprecated; use '--original' instead" @@ -136,8 +145,15 @@ extension SwiftPackageCommand.Config { throw ExitCode.failure } - try config.applyLocal { mirrors in - try mirrors.unset(originalOrMirror: originalOrMirror) + let config = try getMirrorsConfig(swiftCommandState, global: self.global) + if self.global { + try config.applyShared { mirrors in + try mirrors.unset(originalOrMirror: originalOrMirror) + } + } else { + try config.applyLocal { mirrors in + try mirrors.unset(originalOrMirror: originalOrMirror) + } } } } @@ -155,12 +171,13 @@ extension SwiftPackageCommand.Config { @Option(name: .customLong("original-url"), help: .hidden) var _deprecate_originalURL: String? + @Flag(help: "Read only settings applied to all projects for this user.") + var global: Bool = false + @Option(help: "The original url or identity.") var original: String? func run(_ swiftCommandState: SwiftCommandState) throws { - let config = try getMirrorsConfig(swiftCommandState) - if self._deprecate_packageURL != nil { swiftCommandState.observabilityScope.emit( warning: "'--package-url' option is deprecated; use '--original' instead" @@ -177,6 +194,7 @@ extension SwiftPackageCommand.Config { throw ExitCode.failure } + let config = try getMirrorsConfig(swiftCommandState, global: self.global) if let mirror = config.mirrors.mirror(for: original) { print(mirror) } else { @@ -187,7 +205,18 @@ extension SwiftPackageCommand.Config { } } - static func getMirrorsConfig(_ swiftCommandState: SwiftCommandState) throws -> Workspace.Configuration.Mirrors { + static func getMirrorsConfig(_ swiftCommandState: SwiftCommandState, global: Bool) throws -> Workspace.Configuration.Mirrors { + if global { + let sharedMirrorsFile = Workspace.DefaultLocations.mirrorsConfigurationFile( + at: swiftCommandState.sharedConfigurationDirectory + ) + // Workspace not needed when working with user-level mirrors config + return try .init( + fileSystem: swiftCommandState.fileSystem, + localMirrorsFile: .none, + sharedMirrorsFile: sharedMirrorsFile + ) + } let workspace = try swiftCommandState.getActiveWorkspace() return try .init( fileSystem: swiftCommandState.fileSystem, diff --git a/Sources/Workspace/Workspace+Configuration.swift b/Sources/Workspace/Workspace+Configuration.swift index d560d4c4f4a..7abfaed1f04 100644 --- a/Sources/Workspace/Workspace+Configuration.swift +++ b/Sources/Workspace/Workspace+Configuration.swift @@ -469,7 +469,7 @@ extension Workspace.Configuration { extension Workspace.Configuration { public struct Mirrors { - private let localMirrors: MirrorsStorage + private let localMirrors: MirrorsStorage? private let sharedMirrors: MirrorsStorage? private let fileSystem: FileSystem @@ -511,10 +511,16 @@ extension Workspace.Configuration { /// - sharedMirrorsFile: Path to the shared mirrors configuration file, defaults to the standard location. public init( fileSystem: FileSystem, - localMirrorsFile: AbsolutePath, + localMirrorsFile: AbsolutePath?, sharedMirrorsFile: AbsolutePath? ) throws { - self.localMirrors = .init(path: localMirrorsFile, fileSystem: fileSystem, deleteWhenEmpty: true) + // At least one of local or shared is required + if localMirrorsFile == nil, sharedMirrorsFile == nil { + throw StringError("No mirrors configuration provided") + } + + self.localMirrors = localMirrorsFile + .map { .init(path: $0, fileSystem: fileSystem, deleteWhenEmpty: true) } self.sharedMirrors = sharedMirrorsFile .map { .init(path: $0, fileSystem: fileSystem, deleteWhenEmpty: false) } self.fileSystem = fileSystem @@ -525,7 +531,10 @@ extension Workspace.Configuration { @discardableResult public func applyLocal(handler: (inout DependencyMirrors) throws -> Void) throws -> DependencyMirrors { - try self.localMirrors.apply(handler: handler) + guard let localMirrors else { + throw InternalError("local mirrors not configured") + } + try localMirrors.apply(handler: handler) try self.computeMirrors() return self.mirrors } @@ -547,8 +556,7 @@ extension Workspace.Configuration { self._mirrors.removeAll() // prefer local mirrors to shared ones - let local = try self.localMirrors.get() - if !local.isEmpty { + if let local = try self.localMirrors?.get(), !local.isEmpty { try self._mirrors.append(contentsOf: local) return } diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index fa445dd3b1c..9e00485bdaf 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -3747,9 +3747,12 @@ struct PackageCommandTests { let fs = localFileSystem let packageRoot = fixturePath.appending("Foo") let configOverride = fixturePath.appending("configoverride") - let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile( + let localConfigFile = Workspace.DefaultLocations.mirrorsConfigurationFile( forRootPackage: packageRoot ) + let sharedConfigFile = Workspace.DefaultLocations.mirrorsConfigurationFile( + at: try fs.swiftPMConfigurationDirectory + ) fs.createEmptyFiles( at: packageRoot, @@ -3780,7 +3783,19 @@ struct PackageCommandTests { configuration: config, buildSystem: buildSystem, ) - #expect(fs.isFile(configFile)) + #expect(fs.isFile(localConfigFile)) + + // Test writing. + try await execute( + [ + "config", "set-mirror", "--global", "--original", "https://github.com/foo/bar", "--mirror", + "https://globalgithub.com/foo/bar", + ], + packagePath: packageRoot, + configuration: config, + buildSystem: buildSystem, + ) + #expect(fs.isFile(sharedConfigFile)) // Test env override. try await execute( @@ -3805,6 +3820,13 @@ struct PackageCommandTests { buildSystem: buildSystem, ) #expect(stdout.spm_chomp() == "https://mygithub.com/foo/bar") + (stdout, _) = try await execute( + ["config", "get-mirror", "--global", "--original", "https://github.com/foo/bar"], + packagePath: packageRoot, + configuration: config, + buildSystem: buildSystem, + ) + #expect(stdout.spm_chomp() == "https://globalgithub.com/foo/bar") (stdout, _) = try await execute( [ "config", "get-mirror", "--original", @@ -3830,6 +3852,14 @@ struct PackageCommandTests { buildSystem: buildSystem, ) } + await check(stderr: "not found\n") { + try await execute( + ["config", "get-mirror", "--global", "--original", "git@github.com:swiftlang/swift-package-manager.git"], + packagePath: packageRoot, + configuration: config, + buildSystem: buildSystem, + ) + } // Test deletion. try await execute( @@ -3848,14 +3878,15 @@ struct PackageCommandTests { buildSystem: buildSystem, ) - await check(stderr: "not found\n") { - try await execute( - ["config", "get-mirror", "--original", "https://github.com/foo/bar"], - packagePath: packageRoot, - configuration: config, - buildSystem: buildSystem, - ) - } + // Still found via global + (stdout, _) = try await execute( + ["config", "get-mirror", "--original", "https://github.com/foo/bar"], + packagePath: packageRoot, + configuration: config, + buildSystem: buildSystem, + ) + #expect(stdout.spm_chomp() == "https://globalgithub.com/foo/bar") + await check(stderr: "not found\n") { try await execute( [ diff --git a/Tests/WorkspaceTests/MirrorsConfigurationTests.swift b/Tests/WorkspaceTests/MirrorsConfigurationTests.swift index 7d584bcab17..20c5a9fce6f 100644 --- a/Tests/WorkspaceTests/MirrorsConfigurationTests.swift +++ b/Tests/WorkspaceTests/MirrorsConfigurationTests.swift @@ -110,6 +110,19 @@ fileprivate struct MirrorsConfigurationTests { #expect(try config.get().isEmpty) } + @Test + func throwsWhenBothLocalAndSharedAreNil() throws { + let fs = InMemoryFileSystem() + + #expect(throws: StringError("No mirrors configuration provided")) { + try Workspace.Configuration.Mirrors( + fileSystem: fs, + localMirrorsFile: nil, + sharedMirrorsFile: nil + ) + } + } + @Test func localAndShared() throws { let fs = InMemoryFileSystem() @@ -152,4 +165,40 @@ fileprivate struct MirrorsConfigurationTests { #expect(config.mirrors.mirror(for: original1URL) == nil) #expect(config.mirrors.original(for: mirror1URL) == nil) } + + @Test + func onlyShared() throws { + let fs = InMemoryFileSystem() + let sharedConfigFile = AbsolutePath("/config/shared-mirrors.json") + + let config = try Workspace.Configuration.Mirrors( + fileSystem: fs, + localMirrorsFile: nil, + sharedMirrorsFile: sharedConfigFile + ) + + // can write to shared location + + let original1URL = "https://github.com/apple/swift-argument-parser.git" + let mirror1URL = "https://github.com/mona/swift-argument-parser.git" + + try config.applyShared { mirrors in + try mirrors.set(mirror: mirror1URL, for: original1URL) + } + + // cannot write to local location + + let original2URL = "https://github.com/apple/swift-nio.git" + let mirror2URL = "https://github.com/mona/swift-nio.git" + + #expect(throws: (any Error).self) { + try config.applyLocal { mirrors in + try mirrors.set(mirror: mirror2URL, for: original2URL) + } + } + + #expect(config.mirrors.count == 1) + #expect(config.mirrors.mirror(for: original1URL) == mirror1URL) + #expect(config.mirrors.original(for: mirror1URL) == original1URL) + } }