diff --git a/Package.swift b/Package.swift index ac7d11b962f..d0e7b9bb068 100644 --- a/Package.swift +++ b/Package.swift @@ -342,6 +342,20 @@ let package = Package( // MARK: Commands + .target( + /** Minimal set of commands required for bootstrapping a new SwiftPM */ + name: "CoreCommands", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + "Basics", + "Build", + "PackageFingerprint", + "PackageModel", + "Workspace", + ], + exclude: ["CMakeLists.txt"] + ), + .target( /** High-level commands */ name: "Commands", @@ -349,6 +363,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), "Basics", "Build", + "CoreCommands", "PackageCollections", "PackageFingerprint", "PackageGraph", diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index cd9d43d990c..9cb089fb010 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(SPMSQLite3) add_subdirectory(Basics) add_subdirectory(Build) add_subdirectory(Commands) +add_subdirectory(CoreCommands) add_subdirectory(DriverSupport) add_subdirectory(LLBuildManifest) add_subdirectory(PackageCollections) diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index d1c7acaa0b3..2cf0c14f748 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -7,7 +7,6 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(Commands - Options.swift Snippets/CardEvent.swift Snippets/Cards/SnippetCard.swift Snippets/Cards/SnippetGroupCard.swift @@ -21,7 +20,7 @@ add_library(Commands SwiftPackageTool.swift SwiftRunTool.swift SwiftTestTool.swift - SwiftTool.swift + ToolWorkspaceDelegate.swift Utilities/APIDigester.swift Utilities/DependenciesSerializer.swift Utilities/Describe.swift @@ -35,6 +34,7 @@ target_link_libraries(Commands PUBLIC ArgumentParser Basics Build + CoreCommands PackageCollections PackageFingerprint PackageGraph diff --git a/Sources/Commands/Snippets/CardStack.swift b/Sources/Commands/Snippets/CardStack.swift index adad1221d11..cd4a7c3e607 100644 --- a/Sources/Commands/Snippets/CardStack.swift +++ b/Sources/Commands/Snippets/CardStack.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +import CoreCommands import PackageGraph import PackageModel import TSCBasic diff --git a/Sources/Commands/Snippets/Cards/SnippetCard.swift b/Sources/Commands/Snippets/Cards/SnippetCard.swift index 47dbf4bb24d..378b542ff16 100644 --- a/Sources/Commands/Snippets/Cards/SnippetCard.swift +++ b/Sources/Commands/Snippets/Cards/SnippetCard.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import CoreCommands import PackageModel import TSCBasic diff --git a/Sources/Commands/Snippets/Cards/SnippetGroupCard.swift b/Sources/Commands/Snippets/Cards/SnippetGroupCard.swift index 5c4afd04eee..220246ad438 100644 --- a/Sources/Commands/Snippets/Cards/SnippetGroupCard.swift +++ b/Sources/Commands/Snippets/Cards/SnippetGroupCard.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import CoreCommands import PackageModel /// A card showing the snippets in a ``SnippetGroup``. diff --git a/Sources/Commands/Snippets/Cards/TopCard.swift b/Sources/Commands/Snippets/Cards/TopCard.swift index dd7791089cb..19c2a375c39 100644 --- a/Sources/Commands/Snippets/Cards/TopCard.swift +++ b/Sources/Commands/Snippets/Cards/TopCard.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import CoreCommands import Foundation import PackageModel import PackageGraph diff --git a/Sources/Commands/SwiftBuildTool.swift b/Sources/Commands/SwiftBuildTool.swift index cb2081c1aba..97bb547c435 100644 --- a/Sources/Commands/SwiftBuildTool.swift +++ b/Sources/Commands/SwiftBuildTool.swift @@ -13,9 +13,11 @@ import ArgumentParser import Basics import Build +import CoreCommands import PackageGraph import SPMBuildCore import TSCBasic +import XCBuildSupport import enum TSCUtility.Diagnostics import func TSCUtility.getClangVersion @@ -94,7 +96,7 @@ public struct SwiftBuildTool: SwiftCommand { helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @OptionGroup() - var globalOptions: GlobalOptions + public var globalOptions: GlobalOptions @OptionGroup() var options: BuildToolOptions @@ -157,8 +159,22 @@ public struct SwiftBuildTool: SwiftCommand { public init() {} } -extension Basics.Diagnostic { - static func mutuallyExclusiveArgumentsError(arguments: [String]) -> Self { - .error(arguments.map{ "'\($0)'" }.spm_localizedJoin(type: .conjunction) + " are mutually exclusive") +extension SwiftCommand { + public func buildSystemProvider(_ swiftTool: SwiftTool) throws -> BuildSystemProvider { + return .init(providers: try swiftTool.defaultBuildSystemProvider.providers.merging([ + .xcode: { (explicitProduct: String?, cacheBuildManifest: Bool, customBuildParameters: BuildParameters?, customPackageGraphLoader: (() throws -> PackageGraph)?, customOutputStream: OutputByteStream?, customLogLevel: Basics.Diagnostic.Severity?, customObservabilityScope: ObservabilityScope?) throws -> BuildSystem in + let graphLoader = { try swiftTool.loadPackageGraph(explicitProduct: explicitProduct) } + return try XcodeBuildSystem( + buildParameters: customBuildParameters ?? swiftTool.buildParameters(), + packageGraphLoader: customPackageGraphLoader ?? graphLoader, + outputStream: customOutputStream ?? swiftTool.outputStream, + logLevel: customLogLevel ?? swiftTool.logLevel, + fileSystem: swiftTool.fileSystem, + observabilityScope: customObservabilityScope ?? swiftTool.observabilityScope + ) + }, + ], uniquingKeysWith: { a, b in + return b + })) } } diff --git a/Sources/Commands/SwiftPackageCollectionsTool.swift b/Sources/Commands/SwiftPackageCollectionsTool.swift index cb64a0ade5a..ab91b7a2b75 100644 --- a/Sources/Commands/SwiftPackageCollectionsTool.swift +++ b/Sources/Commands/SwiftPackageCollectionsTool.swift @@ -12,6 +12,7 @@ import ArgumentParser import Basics +import CoreCommands import Foundation import PackageCollections import PackageModel diff --git a/Sources/Commands/SwiftPackageRegistryTool.swift b/Sources/Commands/SwiftPackageRegistryTool.swift index 3c73a672fa8..41b5d36dc1a 100644 --- a/Sources/Commands/SwiftPackageRegistryTool.swift +++ b/Sources/Commands/SwiftPackageRegistryTool.swift @@ -12,6 +12,7 @@ import ArgumentParser import Basics +import CoreCommands import TSCBasic import SPMBuildCore import PackageModel diff --git a/Sources/Commands/SwiftPackageTool.swift b/Sources/Commands/SwiftPackageTool.swift index b6ca7776bd6..a7650abf559 100644 --- a/Sources/Commands/SwiftPackageTool.swift +++ b/Sources/Commands/SwiftPackageTool.swift @@ -12,6 +12,7 @@ import ArgumentParser import Basics +import CoreCommands import TSCBasic import SPMBuildCore import PackageModel diff --git a/Sources/Commands/SwiftRunTool.swift b/Sources/Commands/SwiftRunTool.swift index 8149759e55d..4c9df9fe052 100644 --- a/Sources/Commands/SwiftRunTool.swift +++ b/Sources/Commands/SwiftRunTool.swift @@ -12,6 +12,7 @@ import ArgumentParser import Basics +import CoreCommands import PackageGraph import PackageModel import TSCBasic @@ -96,12 +97,12 @@ public struct SwiftRunTool: SwiftCommand { helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @OptionGroup() - var globalOptions: GlobalOptions + public var globalOptions: GlobalOptions @OptionGroup() var options: RunToolOptions - var toolWorkspaceConfiguration: ToolWorkspaceConfiguration { + public var toolWorkspaceConfiguration: ToolWorkspaceConfiguration { return .init(wantsREPLProduct: options.mode == .repl) } diff --git a/Sources/Commands/SwiftTestTool.swift b/Sources/Commands/SwiftTestTool.swift index b7a43aa0dd2..fe348ac32c5 100644 --- a/Sources/Commands/SwiftTestTool.swift +++ b/Sources/Commands/SwiftTestTool.swift @@ -12,6 +12,7 @@ import ArgumentParser import Basics +import CoreCommands import Dispatch import class Foundation.NSLock import class Foundation.ProcessInfo @@ -150,7 +151,7 @@ public struct SwiftTestTool: SwiftCommand { helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @OptionGroup() - var globalOptions: GlobalOptions + public var globalOptions: GlobalOptions @OptionGroup() var sharedOptions: SharedOptions diff --git a/Sources/Commands/ToolWorkspaceDelegate.swift b/Sources/Commands/ToolWorkspaceDelegate.swift new file mode 100644 index 00000000000..4efb2ba3785 --- /dev/null +++ b/Sources/Commands/ToolWorkspaceDelegate.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2022 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import Build +import CoreCommands +import Dispatch +import class Foundation.NSLock +import OrderedCollections +import PackageGraph +import PackageModel +import SPMBuildCore +import Workspace + +import struct TSCBasic.AbsolutePath +import protocol TSCBasic.OutputByteStream + +class ToolWorkspaceDelegate: WorkspaceDelegate { + private struct DownloadProgress { + let bytesDownloaded: Int64 + let totalBytesToDownload: Int64 + } + + private struct FetchProgress { + let progress: Int64 + let total: Int64 + } + + /// The progress of binary downloads. + private var binaryDownloadProgress = OrderedCollections.OrderedDictionary() + private let binaryDownloadProgressLock = NSLock() + + /// The progress of package fetch operations. + private var fetchProgress = OrderedCollections.OrderedDictionary() + private let fetchProgressLock = NSLock() + + private let observabilityScope: ObservabilityScope + + private let outputHandler: (String, Bool) -> Void + private let progressHandler: (Int64, Int64, String?) -> Void + + init( + observabilityScope: ObservabilityScope, + outputHandler: @escaping (String, Bool) -> Void, + progressHandler: @escaping (Int64, Int64, String?) -> Void + ) { + self.observabilityScope = observabilityScope + self.outputHandler = outputHandler + self.progressHandler = progressHandler + } + + func willFetchPackage(package: PackageIdentity, packageLocation: String?, fetchDetails: PackageFetchDetails) { + self.outputHandler("Fetching \(packageLocation ?? package.description)\(fetchDetails.fromCache ? " from cache" : "")", false) + } + + func didFetchPackage(package: PackageIdentity, packageLocation: String?, result: Result, duration: DispatchTimeInterval) { + guard case .success = result, !self.observabilityScope.errorsReported else { + return + } + + self.fetchProgressLock.withLock { + let progress = self.fetchProgress.values.reduce(0) { $0 + $1.progress } + let total = self.fetchProgress.values.reduce(0) { $0 + $1.total } + + if progress == total && !self.fetchProgress.isEmpty { + self.fetchProgress.removeAll() + } else { + self.fetchProgress[package] = nil + } + } + + self.outputHandler("Fetched \(packageLocation ?? package.description) (\(duration.descriptionInSeconds))", false) + } + + func fetchingPackage(package: PackageIdentity, packageLocation: String?, progress: Int64, total: Int64?) { + let (step, total, packages) = self.fetchProgressLock.withLock { () -> (Int64, Int64, String) in + self.fetchProgress[package] = FetchProgress( + progress: progress, + total: total ?? progress + ) + + let progress = self.fetchProgress.values.reduce(0) { $0 + $1.progress } + let total = self.fetchProgress.values.reduce(0) { $0 + $1.total } + let packages = self.fetchProgress.keys.map { $0.description }.joined(separator: ", ") + return (progress, total, packages) + } + self.progressHandler(step, total, "Fetching \(packages)") + } + + func willUpdateRepository(package: PackageIdentity, repository url: String) { + self.outputHandler("Updating \(url)", false) + } + + func didUpdateRepository(package: PackageIdentity, repository url: String, duration: DispatchTimeInterval) { + self.outputHandler("Updated \(url) (\(duration.descriptionInSeconds))", false) + } + + func dependenciesUpToDate() { + self.outputHandler("Everything is already up-to-date", false) + } + + func willCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) { + self.outputHandler("Creating working copy for \(url)", false) + } + + func didCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) { + self.outputHandler("Working copy of \(url) resolved at \(revision)", false) + } + + func removing(package: PackageIdentity, packageLocation: String?) { + self.outputHandler("Removing \(packageLocation ?? package.description)", false) + } + + func willResolveDependencies(reason: WorkspaceResolveReason) { + self.outputHandler(Workspace.format(workspaceResolveReason: reason), true) + } + + func willComputeVersion(package: PackageIdentity, location: String) { + self.outputHandler("Computing version for \(location)", false) + } + + func didComputeVersion(package: PackageIdentity, location: String, version: String, duration: DispatchTimeInterval) { + self.outputHandler("Computed \(location) at \(version) (\(duration.descriptionInSeconds))", false) + } + + func willDownloadBinaryArtifact(from url: String) { + self.outputHandler("Downloading binary artifact \(url)", false) + } + + func didDownloadBinaryArtifact(from url: String, result: Result, duration: DispatchTimeInterval) { + guard case .success = result, !self.observabilityScope.errorsReported else { + return + } + + self.binaryDownloadProgressLock.withLock { + let progress = self.binaryDownloadProgress.values.reduce(0) { $0 + $1.bytesDownloaded } + let total = self.binaryDownloadProgress.values.reduce(0) { $0 + $1.totalBytesToDownload } + + if progress == total && !self.binaryDownloadProgress.isEmpty { + self.binaryDownloadProgress.removeAll() + } else { + self.binaryDownloadProgress[url] = nil + } + } + + self.outputHandler("Downloaded \(url) (\(duration.descriptionInSeconds))", false) + } + + func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?) { + let (step, total, artifacts) = self.binaryDownloadProgressLock.withLock { () -> (Int64, Int64, String) in + self.binaryDownloadProgress[url] = DownloadProgress( + bytesDownloaded: bytesDownloaded, + totalBytesToDownload: totalBytesToDownload ?? bytesDownloaded + ) + + let step = self.binaryDownloadProgress.values.reduce(0, { $0 + $1.bytesDownloaded }) + let total = self.binaryDownloadProgress.values.reduce(0, { $0 + $1.totalBytesToDownload }) + let artifacts = self.binaryDownloadProgress.keys.joined(separator: ", ") + return (step, total, artifacts) + } + + self.progressHandler(step, total, "Downloading \(artifacts)") + } + + // noop + + func willLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind) {} + func didLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind, manifest: Manifest?, diagnostics: [Basics.Diagnostic]) {} + func willCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) {} + func didCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) {} + func resolvedFileChanged() {} + func didDownloadAllBinaryArtifacts() {} +} + +extension SwiftCommand { + public var workspaceDelegateProvider: WorkspaceDelegateProvider { + return { + ToolWorkspaceDelegate(observabilityScope: $0, outputHandler: $1, progressHandler: $2) + } + } + + public var workspaceLoaderProvider: WorkspaceLoaderProvider { + return { + XcodeWorkspaceLoader(fileSystem: $0, observabilityScope: $1) + } + } +} diff --git a/Sources/Commands/Utilities/APIDigester.swift b/Sources/Commands/Utilities/APIDigester.swift index 4b4c136e2aa..20cadc3a0e6 100644 --- a/Sources/Commands/Utilities/APIDigester.swift +++ b/Sources/Commands/Utilities/APIDigester.swift @@ -16,6 +16,7 @@ import TSCBasic import SPMBuildCore import Basics +import CoreCommands import PackageGraph import PackageModel import SourceControl @@ -62,7 +63,7 @@ struct APIDigesterBaselineDumper { for modulesToDiff: Set, at baselineDir: AbsolutePath?, force: Bool, - logLevel: Diagnostic.Severity, + logLevel: Basics.Diagnostic.Severity, swiftTool: SwiftTool ) throws -> AbsolutePath { var modulesToDiff = modulesToDiff diff --git a/Sources/Commands/Utilities/MultiRootSupport.swift b/Sources/Commands/Utilities/MultiRootSupport.swift index e3bc941e893..507cdff6d4e 100644 --- a/Sources/Commands/Utilities/MultiRootSupport.swift +++ b/Sources/Commands/Utilities/MultiRootSupport.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +import CoreCommands import Foundation #if canImport(FoundationXML) import FoundationXML @@ -21,7 +22,7 @@ import TSCBasic /// A bare minimum loader for Xcode workspaces. /// /// Warning: This is only useful for debugging workspaces that contain Swift packages. -public struct XcodeWorkspaceLoader { +public struct XcodeWorkspaceLoader: WorkspaceLoader { /// The parsed location. private struct Location { diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index 15c1f344c7c..819f5b88c4c 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +import CoreCommands import Foundation import PackageModel import SPMBuildCore @@ -80,7 +81,7 @@ final class PluginDelegate: PluginInvocationDelegate { buildParameters.flags.linkerFlags.append(contentsOf: parameters.otherLinkerFlags) // Configure the verbosity of the output. - let logLevel: Diagnostic.Severity + let logLevel: Basics.Diagnostic.Severity switch parameters.logging { case .concise: logLevel = .warning diff --git a/Sources/Commands/Utilities/TestingSupport.swift b/Sources/Commands/Utilities/TestingSupport.swift index 51dc817fd93..7c6f41fe087 100644 --- a/Sources/Commands/Utilities/TestingSupport.swift +++ b/Sources/Commands/Utilities/TestingSupport.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +import CoreCommands import PackageModel import SPMBuildCore import TSCBasic diff --git a/Sources/CoreCommands/BuildSystemSupport.swift b/Sources/CoreCommands/BuildSystemSupport.swift new file mode 100644 index 00000000000..78a530ff8ff --- /dev/null +++ b/Sources/CoreCommands/BuildSystemSupport.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2022 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Build +import SPMBuildCore + +import class Basics.ObservabilityScope +import struct PackageGraph.PackageGraph +import struct PackageLoading.FileRuleDescription +import protocol TSCBasic.OutputByteStream + +extension SwiftTool { + public var defaultBuildSystemProvider: BuildSystemProvider { + get throws { + return .init(providers: [ + .native: { (explicitProduct: String?, cacheBuildManifest: Bool, customBuildParameters: BuildParameters?, customPackageGraphLoader: (() throws -> PackageGraph)?, customOutputStream: OutputByteStream?, customLogLevel: Diagnostic.Severity?, customObservabilityScope: ObservabilityScope?) throws -> BuildSystem in + let testEntryPointPath = customBuildParameters?.testProductStyle.explicitlySpecifiedEntryPointPath + let graphLoader = { try self.loadPackageGraph(explicitProduct: explicitProduct, testEntryPointPath: testEntryPointPath) } + return try BuildOperation( + buildParameters: customBuildParameters ?? self.buildParameters(), + cacheBuildManifest: cacheBuildManifest && self.canUseCachedBuildManifest(), + packageGraphLoader: customPackageGraphLoader ?? graphLoader, + additionalFileRules: FileRuleDescription.swiftpmFileTypes, + pluginScriptRunner: self.getPluginScriptRunner(), + pluginWorkDirectory: try self.getActiveWorkspace().location.pluginWorkingDirectory, + disableSandboxForPluginCommands: self.options.security.shouldDisableSandbox, + outputStream: customOutputStream ?? self.outputStream, + logLevel: customLogLevel ?? self.logLevel, + fileSystem: self.fileSystem, + observabilityScope: customObservabilityScope ?? self.observabilityScope) }, + ]) + } + } +} diff --git a/Sources/CoreCommands/CMakeLists.txt b/Sources/CoreCommands/CMakeLists.txt new file mode 100644 index 00000000000..2e6a0755641 --- /dev/null +++ b/Sources/CoreCommands/CMakeLists.txt @@ -0,0 +1,35 @@ +# This source file is part of the Swift open source project +# +# Copyright (c) 2022 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 + +add_library(CoreCommands + BuildSystemSupport.swift + MinimalBuildTool.swift + SwiftTool.swift + Options.swift) +target_link_libraries(CoreCommands PUBLIC + ArgumentParser + Basics + Build + PackageGraph + TSCBasic + TSCUtility + Workspace + XCBuildSupport) +target_link_libraries(CoreCommands PRIVATE + DriverSupport + $<$>:FoundationXML>) +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(CoreCommands PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +if(USE_CMAKE_INSTALL) +install(TARGETS CoreCommands + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) +endif() diff --git a/Sources/CoreCommands/MinimalBuildTool.swift b/Sources/CoreCommands/MinimalBuildTool.swift new file mode 100644 index 00000000000..b73a4346484 --- /dev/null +++ b/Sources/CoreCommands/MinimalBuildTool.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2022 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Basics +import Dispatch +import PackageModel +import Workspace + +import struct SPMBuildCore.BuildSystemProvider +import struct TSCBasic.AbsolutePath +import var TSCBasic.stdoutStream +import enum TSCUtility.Diagnostics +import struct TSCUtility.Version + +private class MinimalWorkspaceDelegate: WorkspaceDelegate { + func willLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind) {} + func didLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind, manifest: Manifest?, diagnostics: [Basics.Diagnostic]) {} + + func willFetchPackage(package: PackageIdentity, packageLocation: String?, fetchDetails: PackageFetchDetails) {} + func didFetchPackage(package: PackageIdentity, packageLocation: String?, result: Result, duration: DispatchTimeInterval) {} + func fetchingPackage(package: PackageIdentity, packageLocation: String?, progress: Int64, total: Int64?) {} + + func willUpdateRepository(package: PackageIdentity, repository url: String) {} + func didUpdateRepository(package: PackageIdentity, repository url: String, duration: DispatchTimeInterval) {} + func dependenciesUpToDate() {} + + func willCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) {} + func didCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) {} + func willCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) {} + func didCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) {} + + func removing(package: PackageIdentity, packageLocation: String?) {} + + func willResolveDependencies(reason: WorkspaceResolveReason) {} + func willComputeVersion(package: PackageIdentity, location: String) {} + func didComputeVersion(package: PackageIdentity, location: String, version: String, duration: DispatchTimeInterval) {} + func resolvedFileChanged() {} + + func willDownloadBinaryArtifact(from url: String) {} + func didDownloadBinaryArtifact(from url: String, result: Result, duration: DispatchTimeInterval) {} + func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?) {} + func didDownloadAllBinaryArtifacts() {} +} + +public struct SwiftMinimalBuildTool: SwiftCommand { + @OptionGroup() + public var globalOptions: GlobalOptions + + public func run(_ swiftTool: SwiftTool) throws { + let buildSystem = try swiftTool.createBuildSystem(customOutputStream: TSCBasic.stdoutStream) + + do { + try buildSystem.build(subset: .allExcludingTests) + } catch _ as Diagnostics { + throw ExitCode.failure + } + } + + public init() {} + + public func buildSystemProvider(_ swiftTool: SwiftTool) throws -> BuildSystemProvider { + return try swiftTool.defaultBuildSystemProvider + } + + public var workspaceDelegateProvider: WorkspaceDelegateProvider { + return { _, _ , _ in + MinimalWorkspaceDelegate() + } + } + + public var workspaceLoaderProvider: WorkspaceLoaderProvider { + return { _, _ in + fatalError("minimal build tool does not support loading workspaces") + } + } +} diff --git a/Sources/Commands/Options.swift b/Sources/CoreCommands/Options.swift similarity index 75% rename from Sources/Commands/Options.swift rename to Sources/CoreCommands/Options.swift index b548cf3df9b..80e3f64f29a 100644 --- a/Sources/Commands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -11,59 +11,59 @@ //===----------------------------------------------------------------------===// import ArgumentParser -import TSCBasic -import PackageFingerprint -import PackageModel -import SPMBuildCore +import enum PackageFingerprint.FingerprintCheckingMode +import enum PackageModel.BuildConfiguration +import struct PackageModel.BuildFlags +import struct PackageModel.EnabledSanitizers +import enum PackageModel.Sanitizer + +import struct SPMBuildCore.BuildSystemProvider + +import struct TSCBasic.AbsolutePath +import struct TSCBasic.StringError import struct TSCUtility.Triple +import var TSCBasic.localFileSystem -struct GlobalOptions: ParsableArguments { - init() {} +public struct GlobalOptions: ParsableArguments { + public init() {} @OptionGroup() - var locations: LocationOptions + public var locations: LocationOptions @OptionGroup() - var caching: CachingOptions + public var caching: CachingOptions @OptionGroup() - var logging: LoggingOptions + public var logging: LoggingOptions @OptionGroup() - var security: SecurityOptions + public var security: SecurityOptions @OptionGroup() - var resolver: ResolverOptions + public var resolver: ResolverOptions @OptionGroup() - var build: BuildOptions + public var build: BuildOptions @OptionGroup() - var linker: LinkerOptions + public var linker: LinkerOptions } -struct LocationOptions: ParsableArguments { - init() {} +public struct LocationOptions: ParsableArguments { + public init() {} @Option(name: .customLong("package-path"), help: "Specify the package path to operate on (default current directory). This changes the working directory before any other operation", completion: .directory) - var _packageDirectory: AbsolutePath? + public var packageDirectory: AbsolutePath? @Option(name: .customLong("cache-path"), help: "Specify the shared cache directory path", completion: .directory) - var cacheDirectory: AbsolutePath? + public var cacheDirectory: AbsolutePath? @Option(name: .customLong("config-path"), help: "Specify the shared configuration directory path", completion: .directory) - var configurationDirectory: AbsolutePath? + public var configurationDirectory: AbsolutePath? @Option(name: .customLong("security-path"), help: "Specify the shared security directory path", completion: .directory) - var securityDirectory: AbsolutePath? - - @Option(name: [.long, .customShort("C")], help: .hidden) - var _deprecated_chdir: AbsolutePath? - - var packageDirectory: AbsolutePath? { - self._packageDirectory ?? self._deprecated_chdir - } + public var securityDirectory: AbsolutePath? /// The custom .build directory, if provided. @Option(name: .customLong("scratch-path"), help: "Specify a custom scratch directory path (default .build)", completion: .directory) @@ -78,86 +78,75 @@ struct LocationOptions: ParsableArguments { /// The path to the file containing multiroot package data. This is currently Xcode's workspace file. @Option(name: .customLong("multiroot-data-file"), help: .hidden, completion: .directory) - var multirootPackageDataFile: AbsolutePath? + public var multirootPackageDataFile: AbsolutePath? /// Path to the compilation destination describing JSON file. @Option(name: .customLong("destination"), help: .hidden, completion: .directory) - var customCompileDestination: AbsolutePath? + public var customCompileDestination: AbsolutePath? } -struct CachingOptions: ParsableArguments { +public struct CachingOptions: ParsableArguments { + public init() {} + /// Disables package caching. @Flag(name: .customLong("dependency-cache"), inversion: .prefixedEnableDisable, help: "Use a shared cache when fetching dependencies") - var _useDependenciesCache: Bool = true - - // TODO: simplify when deprecating the older flag - var useDependenciesCache: Bool { - if let value = self._deprecated_useRepositoriesCache { - return value - } else { - return self._useDependenciesCache - } - } + public var useDependenciesCache: Bool = true /// Disables manifest caching. @Flag(name: .customLong("disable-package-manifest-caching"), help: .hidden) - var shouldDisableManifestCaching: Bool = false + public var shouldDisableManifestCaching: Bool = false /// Whether to enable llbuild manifest caching. @Flag(name: .customLong("build-manifest-caching"), inversion: .prefixedEnableDisable) - var cacheBuildManifest: Bool = true + public var cacheBuildManifest: Bool = true /// Disables manifest caching. @Option(name: .customLong("manifest-cache"), help: "Caching mode of Package.swift manifests (shared: shared cache, local: package's build directory, none: disabled") - var manifestCachingMode: ManifestCachingMode = .shared + public var manifestCachingMode: ManifestCachingMode = .shared - enum ManifestCachingMode: String, ExpressibleByArgument { + public enum ManifestCachingMode: String, ExpressibleByArgument { case none case local case shared - init?(argument: String) { + public init?(argument: String) { self.init(rawValue: argument) } } - - /// Disables repository caching. - @Flag(name: .customLong("repository-cache"), inversion: .prefixedEnableDisable, help: .hidden) - var _deprecated_useRepositoriesCache: Bool? } -struct LoggingOptions: ParsableArguments { - init() {} +public struct LoggingOptions: ParsableArguments { + public init() {} /// The verbosity of informational output. @Flag(name: .shortAndLong, help: "Increase verbosity to include informational output") - var verbose: Bool = false + public var verbose: Bool = false /// The verbosity of informational output. @Flag(name: [.long, .customLong("vv")], help: "Increase verbosity to include debug output") - var veryVerbose: Bool = false + public var veryVerbose: Bool = false } -struct SecurityOptions: ParsableArguments { - init() {} +public struct SecurityOptions: ParsableArguments { + public init() {} /// Disables sandboxing when executing subprocesses. @Flag(name: .customLong("disable-sandbox"), help: "Disable using the sandbox when executing subprocesses") - var shouldDisableSandbox: Bool = false + public var shouldDisableSandbox: Bool = false /// Whether to load .netrc files for authenticating with remote servers /// when downloading binary artifacts or communicating with a registry. @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a .netrc file") - var netrc: Bool = true + public var netrc: Bool = true /// The path to the .netrc file used when `netrc` is `true`. @Option( name: .customLong("netrc-file"), help: "Specify the .netrc file path.", completion: .file()) - var netrcFilePath: AbsolutePath? + public var netrcFilePath: AbsolutePath? /// Whether to use keychain for authenticating with remote servers /// when downloading binary artifacts or communicating with a registry. @@ -165,53 +154,43 @@ struct SecurityOptions: ParsableArguments { @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Search credentials in macOS keychain") - var keychain: Bool = true + public var keychain: Bool = true #else @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: .hidden) - var keychain: Bool = false + public var keychain: Bool = false #endif @Option(name: .customLong("resolver-fingerprint-checking")) - var fingerprintCheckingMode: FingerprintCheckingMode = .strict - - @Flag(name: .customLong("netrc"), help: .hidden) - var _deprecated_netrc: Bool = false - - @Flag(name: .customLong("netrc-optional"), help: .hidden) - var _deprecated_netrcOptional: Bool = false + public var fingerprintCheckingMode: FingerprintCheckingMode = .strict } -struct ResolverOptions: ParsableArguments { - init() {} +public struct ResolverOptions: ParsableArguments { + public init() {} /// Enable prefetching in resolver which will kick off parallel git cloning. @Flag(name: .customLong("prefetching"), inversion: .prefixedEnableDisable) - var shouldEnableResolverPrefetching: Bool = true + public var shouldEnableResolverPrefetching: Bool = true /// Use Package.resolved file for resolving dependencies. @Flag(name: [.long, .customLong("disable-automatic-resolution"), .customLong("only-use-versions-from-resolved-file")], help: "Only use versions from the Package.resolved file and fail resolution if it is out-of-date") - var forceResolvedVersions: Bool = false + public var forceResolvedVersions: Bool = false /// Skip updating dependencies from their remote during a resolution. @Flag(name: .customLong("skip-update"), help: "Skip updating dependencies from their remote during a resolution") - var skipDependencyUpdate: Bool = false + public var skipDependencyUpdate: Bool = false @Flag(help: "Define automatic transformation of source control based dependencies to registry based ones") - var sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation = .disabled - - /// Write dependency resolver trace to a file. - @Flag(name: .customLong("trace-resolver"), help: .hidden) - var _deprecated_enableResolverTrace: Bool = false + public var sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation = .disabled - enum SourceControlToRegistryDependencyTransformation: EnumerableFlag { + public enum SourceControlToRegistryDependencyTransformation: EnumerableFlag { case disabled case identity case swizzle - static func name(for value: Self) -> NameSpecification { + public static func name(for value: Self) -> NameSpecification { switch value { case .disabled: return .customLong("disable-scm-to-registry-transformation") @@ -222,7 +201,7 @@ struct ResolverOptions: ParsableArguments { } } - static func help(for value: SourceControlToRegistryDependencyTransformation) -> ArgumentHelp? { + public static func help(for value: SourceControlToRegistryDependencyTransformation) -> ArgumentHelp? { switch value { case .disabled: return "disable source control to registry transformation" @@ -235,12 +214,12 @@ struct ResolverOptions: ParsableArguments { } } -struct BuildOptions: ParsableArguments { - init() {} +public struct BuildOptions: ParsableArguments { + public init() {} /// Build configuration. @Option(name: .shortAndLong, help: "Build with configuration") - var configuration: BuildConfiguration = .debug + public var configuration: BuildConfiguration = .debug @Option(name: .customLong("Xcc", withSingleDash: true), parsing: .unconditionalSingleValue, @@ -267,15 +246,15 @@ struct BuildOptions: ParsableArguments { help: ArgumentHelp( "Pass flag through to the Xcode build system invocations", shouldDisplay: false)) - var xcbuildFlags: [String] = [] + public var xcbuildFlags: [String] = [] @Option(name: .customLong("Xmanifest", withSingleDash: true), parsing: .unconditionalSingleValue, help: ArgumentHelp("Pass flag to the manifest build invocation", shouldDisplay: false)) - var manifestFlags: [String] = [] + public var manifestFlags: [String] = [] - var buildFlags: BuildFlags { + public var buildFlags: BuildFlags { BuildFlags( cCompilerFlags: cCompilerFlags, cxxCompilerFlags: cxxCompilerFlags, @@ -286,15 +265,15 @@ struct BuildOptions: ParsableArguments { /// The compilation destination’s target triple. @Option(name: .customLong("triple"), transform: Triple.init) - var customCompileTriple: Triple? + public var customCompileTriple: Triple? /// Path to the compilation destination’s SDK. @Option(name: .customLong("sdk")) - var customCompileSDK: AbsolutePath? + public var customCompileSDK: AbsolutePath? /// Path to the compilation destination’s toolchain. @Option(name: .customLong("toolchain")) - var customCompileToolchain: AbsolutePath? + public var customCompileToolchain: AbsolutePath? /// The architectures to compile for. @Option( @@ -308,46 +287,46 @@ struct BuildOptions: ParsableArguments { @Option(name: .customLong("sanitize"), help: "Turn on runtime checks for erroneous behavior, possible values: \(Sanitizer.formattedValues)", transform: { try Sanitizer(argument: $0) }) - var sanitizers: [Sanitizer] = [] + public var sanitizers: [Sanitizer] = [] - var enabledSanitizers: EnabledSanitizers { + public var enabledSanitizers: EnabledSanitizers { EnabledSanitizers(Set(sanitizers)) } @Flag(help: "Enable or disable indexing-while-building feature") - var indexStoreMode: StoreMode = .autoIndexStore + public var indexStoreMode: StoreMode = .autoIndexStore /// Whether to enable generation of `.swiftinterface`s alongside `.swiftmodule`s. @Flag(name: .customLong("enable-parseable-module-interfaces")) - var shouldEnableParseableModuleInterfaces: Bool = false + public var shouldEnableParseableModuleInterfaces: Bool = false /// The number of jobs for llbuild to start (aka the number of schedulerLanes) @Option(name: .shortAndLong, help: "The number of jobs to spawn in parallel during the build process") - var jobs: UInt32? + public var jobs: UInt32? /// Emit the Swift module separately from the object files. @Flag() - var emitSwiftModuleSeparately: Bool = false + public var emitSwiftModuleSeparately: Bool = false /// Whether to use the integrated Swift driver rather than shelling out /// to a separate process. @Flag() - var useIntegratedSwiftDriver: Bool = false + public var useIntegratedSwiftDriver: Bool = false /// A flag that inidcates this build should check whether targets only import /// their explicitly-declared dependencies @Option() - var explicitTargetDependencyImportCheck: TargetDependencyImportCheckingMode = .none + public var explicitTargetDependencyImportCheck: TargetDependencyImportCheckingMode = .none /// Whether to use the explicit module build flow (with the integrated driver) @Flag(name: .customLong("experimental-explicit-module-build")) - var useExplicitModuleBuild: Bool = false + public var useExplicitModuleBuild: Bool = false /// The build system to use. @Option(name: .customLong("build-system")) var _buildSystem: BuildSystemProvider.Kind = .native - var buildSystem: BuildSystemProvider.Kind { + public var buildSystem: BuildSystemProvider.Kind { #if os(macOS) // Force the Xcode build system if we want to build more than one arch. return archs.count > 1 ? .xcode : self._buildSystem @@ -359,42 +338,42 @@ struct BuildOptions: ParsableArguments { /// Whether to enable test discovery on platforms without Objective-C runtime. @Flag(help: .hidden) - var enableTestDiscovery: Bool = false + public var enableTestDiscovery: Bool = false /// Path of test entry point file to use, instead of synthesizing one or using `XCTMain.swift` in the package (if present). /// This implies `--enable-test-discovery` @Option(name: .customLong("experimental-test-entry-point-path"), help: .hidden) - var testEntryPointPath: AbsolutePath? + public var testEntryPointPath: AbsolutePath? // @Flag works best when there is a default value present // if true, false aren't enough and a third state is needed // nil should not be the goto. Instead create an enum - enum StoreMode: EnumerableFlag { + public enum StoreMode: EnumerableFlag { case autoIndexStore case enableIndexStore case disableIndexStore } - enum TargetDependencyImportCheckingMode : String, Codable, ExpressibleByArgument { + public enum TargetDependencyImportCheckingMode : String, Codable, ExpressibleByArgument { case none case warn case error } } -struct LinkerOptions: ParsableArguments { - init() {} +public struct LinkerOptions: ParsableArguments { + public init() {} @Flag( name: .customLong("dead-strip"), inversion: .prefixedEnableDisable, help: "Disable/enable dead code stripping by the linker") - var linkerDeadStrip: Bool = true + public var linkerDeadStrip: Bool = true /// If should link the Swift stdlib statically. @Flag(name: .customLong("static-swift-stdlib"), inversion: .prefixedNo, help: "Link Swift stdlib statically") - var shouldLinkStaticSwiftStdlib: Bool = false + public var shouldLinkStaticSwiftStdlib: Bool = false } @@ -436,7 +415,7 @@ extension FingerprintCheckingMode: ExpressibleByArgument { } } -extension Sanitizer { +public extension Sanitizer { init(argument: String) throws { if let sanitizer = Sanitizer(rawValue: argument) { self = sanitizer diff --git a/Sources/Commands/SwiftTool.swift b/Sources/CoreCommands/SwiftTool.swift similarity index 75% rename from Sources/Commands/SwiftTool.swift rename to Sources/CoreCommands/SwiftTool.swift index e72f27a4a80..e99db9d251a 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/CoreCommands/SwiftTool.swift @@ -16,16 +16,10 @@ import Dispatch @_implementationOnly import DriverSupport import class Foundation.NSLock import class Foundation.ProcessInfo -import OrderedCollections import PackageGraph import PackageLoading import PackageModel -import SourceControl import SPMBuildCore -import TSCBasic -import class TSCUtility.MultiLineNinjaProgressAnimation -import class TSCUtility.NinjaProgressAnimation -import protocol TSCUtility.ProgressAnimationProtocol import Workspace #if canImport(WinSDK) @@ -36,188 +30,44 @@ import Darwin import Glibc #endif -import protocol TSCUtility.ProgressAnimationProtocol +import struct TSCBasic.AbsolutePath +import func TSCBasic.exec +import protocol TSCBasic.FileSystem +import var TSCBasic.localFileSystem +import protocol TSCBasic.OutputByteStream +import class TSCBasic.Process +import enum TSCBasic.ProcessEnv +import var TSCBasic.stderrStream +import class TSCBasic.TerminalController +import class TSCBasic.ThreadSafeOutputByteStream + +import class TSCUtility.MultiLineNinjaProgressAnimation import class TSCUtility.NinjaProgressAnimation import class TSCUtility.PercentProgressAnimation +import protocol TSCUtility.ProgressAnimationProtocol import var TSCUtility.verbosity typealias Diagnostic = Basics.Diagnostic -// TODO: Refactor in upcoming PRs -import Build -import XCBuildSupport - -private class ToolWorkspaceDelegate: WorkspaceDelegate { - private struct DownloadProgress { - let bytesDownloaded: Int64 - let totalBytesToDownload: Int64 - } - - private struct FetchProgress { - let progress: Int64 - let total: Int64 - } - - /// The progress of binary downloads. - private var binaryDownloadProgress = OrderedCollections.OrderedDictionary() - private let binaryDownloadProgressLock = NSLock() - - /// The progress of package fetch operations. - private var fetchProgress = OrderedCollections.OrderedDictionary() - private let fetchProgressLock = NSLock() - - private let observabilityScope: ObservabilityScope - - private let outputHandler: (String, Bool) -> Void - private let progressHandler: (Int64, Int64, String?) -> Void - - init( - observabilityScope: ObservabilityScope, - outputHandler: @escaping (String, Bool) -> Void, - progressHandler: @escaping (Int64, Int64, String?) -> Void - ) { - self.observabilityScope = observabilityScope - self.outputHandler = outputHandler - self.progressHandler = progressHandler - } - - func willFetchPackage(package: PackageIdentity, packageLocation: String?, fetchDetails: PackageFetchDetails) { - self.outputHandler("Fetching \(packageLocation ?? package.description)\(fetchDetails.fromCache ? " from cache" : "")", false) - } - - func didFetchPackage(package: PackageIdentity, packageLocation: String?, result: Result, duration: DispatchTimeInterval) { - guard case .success = result, !self.observabilityScope.errorsReported else { - return - } - - self.fetchProgressLock.withLock { - let progress = self.fetchProgress.values.reduce(0) { $0 + $1.progress } - let total = self.fetchProgress.values.reduce(0) { $0 + $1.total } - - if progress == total && !self.fetchProgress.isEmpty { - self.fetchProgress.removeAll() - } else { - self.fetchProgress[package] = nil - } - } - - self.outputHandler("Fetched \(packageLocation ?? package.description) (\(duration.descriptionInSeconds))", false) - } - - func fetchingPackage(package: PackageIdentity, packageLocation: String?, progress: Int64, total: Int64?) { - let (step, total, packages) = self.fetchProgressLock.withLock { () -> (Int64, Int64, String) in - self.fetchProgress[package] = FetchProgress( - progress: progress, - total: total ?? progress - ) - - let progress = self.fetchProgress.values.reduce(0) { $0 + $1.progress } - let total = self.fetchProgress.values.reduce(0) { $0 + $1.total } - let packages = self.fetchProgress.keys.map { $0.description }.joined(separator: ", ") - return (progress, total, packages) - } - self.progressHandler(step, total, "Fetching \(packages)") - } - - func willUpdateRepository(package: PackageIdentity, repository url: String) { - self.outputHandler("Updating \(url)", false) - } - - func didUpdateRepository(package: PackageIdentity, repository url: String, duration: DispatchTimeInterval) { - self.outputHandler("Updated \(url) (\(duration.descriptionInSeconds))", false) - } - - func dependenciesUpToDate() { - self.outputHandler("Everything is already up-to-date", false) - } - - func willCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) { - self.outputHandler("Creating working copy for \(url)", false) - } - - func didCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) { - self.outputHandler("Working copy of \(url) resolved at \(revision)", false) - } - - func removing(package: PackageIdentity, packageLocation: String?) { - self.outputHandler("Removing \(packageLocation ?? package.description)", false) - } - - func willResolveDependencies(reason: WorkspaceResolveReason) { - self.outputHandler(Workspace.format(workspaceResolveReason: reason), true) - } - - func willComputeVersion(package: PackageIdentity, location: String) { - self.outputHandler("Computing version for \(location)", false) - } - - func didComputeVersion(package: PackageIdentity, location: String, version: String, duration: DispatchTimeInterval) { - self.outputHandler("Computed \(location) at \(version) (\(duration.descriptionInSeconds))", false) - } - - func willDownloadBinaryArtifact(from url: String) { - self.outputHandler("Downloading binary artifact \(url)", false) - } - - func didDownloadBinaryArtifact(from url: String, result: Result, duration: DispatchTimeInterval) { - guard case .success = result, !self.observabilityScope.errorsReported else { - return - } - - self.binaryDownloadProgressLock.withLock { - let progress = self.binaryDownloadProgress.values.reduce(0) { $0 + $1.bytesDownloaded } - let total = self.binaryDownloadProgress.values.reduce(0) { $0 + $1.totalBytesToDownload } - - if progress == total && !self.binaryDownloadProgress.isEmpty { - self.binaryDownloadProgress.removeAll() - } else { - self.binaryDownloadProgress[url] = nil - } - } - - self.outputHandler("Downloaded \(url) (\(duration.descriptionInSeconds))", false) - } - - func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?) { - let (step, total, artifacts) = self.binaryDownloadProgressLock.withLock { () -> (Int64, Int64, String) in - self.binaryDownloadProgress[url] = DownloadProgress( - bytesDownloaded: bytesDownloaded, - totalBytesToDownload: totalBytesToDownload ?? bytesDownloaded - ) - - let step = self.binaryDownloadProgress.values.reduce(0, { $0 + $1.bytesDownloaded }) - let total = self.binaryDownloadProgress.values.reduce(0, { $0 + $1.totalBytesToDownload }) - let artifacts = self.binaryDownloadProgress.keys.joined(separator: ", ") - return (step, total, artifacts) - } - - self.progressHandler(step, total, "Downloading \(artifacts)") - } - - // noop - - func willLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind) {} - func didLoadManifest(packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind, manifest: Manifest?, diagnostics: [Basics.Diagnostic]) {} - func willCheckOut(package: PackageIdentity, repository url: String, revision: String, at path: AbsolutePath) {} - func didCreateWorkingCopy(package: PackageIdentity, repository url: String, at path: AbsolutePath) {} - func resolvedFileChanged() {} - func didDownloadAllBinaryArtifacts() {} -} - -struct ToolWorkspaceConfiguration { +public struct ToolWorkspaceConfiguration { let wantsMultipleTestProducts: Bool let wantsREPLProduct: Bool - init(wantsMultipleTestProducts: Bool = false, + public init(wantsMultipleTestProducts: Bool = false, wantsREPLProduct: Bool = false) { self.wantsMultipleTestProducts = wantsMultipleTestProducts self.wantsREPLProduct = wantsREPLProduct } } -protocol SwiftCommand: ParsableCommand { +public typealias WorkspaceDelegateProvider = (_ observabilityScope: ObservabilityScope, _ outputHandler: @escaping (String, Bool) -> Void, _ progressHandler: @escaping (Int64, Int64, String?) -> Void) -> WorkspaceDelegate +public typealias WorkspaceLoaderProvider = (_ fileSystem: FileSystem, _ observabilityScope: ObservabilityScope) -> WorkspaceLoader + +public protocol SwiftCommand: ParsableCommand { var globalOptions: GlobalOptions { get } var toolWorkspaceConfiguration: ToolWorkspaceConfiguration { get } + var workspaceDelegateProvider: WorkspaceDelegateProvider { get } + var workspaceLoaderProvider: WorkspaceLoaderProvider { get } func buildSystemProvider(_ swiftTool: SwiftTool) throws -> BuildSystemProvider func run(_ swiftTool: SwiftTool) throws @@ -225,7 +75,7 @@ protocol SwiftCommand: ParsableCommand { extension SwiftCommand { public func run() throws { - let swiftTool = try SwiftTool(options: globalOptions, toolWorkspaceConfiguration: self.toolWorkspaceConfiguration) + let swiftTool = try SwiftTool(options: globalOptions, toolWorkspaceConfiguration: self.toolWorkspaceConfiguration, workspaceDelegateProvider: self.workspaceDelegateProvider, workspaceLoaderProvider: self.workspaceLoaderProvider) swiftTool.buildSystemProvider = try buildSystemProvider(swiftTool) var toolError: Error? = .none do { @@ -250,38 +100,6 @@ extension SwiftCommand { public var toolWorkspaceConfiguration: ToolWorkspaceConfiguration { return .init() } - - public func buildSystemProvider(_ swiftTool: SwiftTool) throws -> BuildSystemProvider { - return .init(providers: [ - .native: { (explicitProduct: String?, cacheBuildManifest: Bool, customBuildParameters: BuildParameters?, customPackageGraphLoader: (() throws -> PackageGraph)?, customOutputStream: OutputByteStream?, customLogLevel: Diagnostic.Severity?, customObservabilityScope: ObservabilityScope?) throws -> BuildSystem in - let testEntryPointPath = customBuildParameters?.testProductStyle.explicitlySpecifiedEntryPointPath - let graphLoader = { try swiftTool.loadPackageGraph(explicitProduct: explicitProduct, testEntryPointPath: testEntryPointPath) } - return try BuildOperation( - buildParameters: customBuildParameters ?? swiftTool.buildParameters(), - cacheBuildManifest: cacheBuildManifest && swiftTool.canUseCachedBuildManifest(), - packageGraphLoader: customPackageGraphLoader ?? graphLoader, - additionalFileRules: FileRuleDescription.swiftpmFileTypes, - pluginScriptRunner: swiftTool.getPluginScriptRunner(), - pluginWorkDirectory: try swiftTool.getActiveWorkspace().location.pluginWorkingDirectory, - disableSandboxForPluginCommands: swiftTool.options.security.shouldDisableSandbox, - outputStream: customOutputStream ?? swiftTool.outputStream, - logLevel: customLogLevel ?? swiftTool.logLevel, - fileSystem: swiftTool.fileSystem, - observabilityScope: customObservabilityScope ?? swiftTool.observabilityScope) }, - .xcode: { (explicitProduct: String?, cacheBuildManifest: Bool, customBuildParameters: BuildParameters?, customPackageGraphLoader: (() throws -> PackageGraph)?, customOutputStream: OutputByteStream?, customLogLevel: Diagnostic.Severity?, customObservabilityScope: ObservabilityScope?) throws -> BuildSystem in - let graphLoader = { try swiftTool.loadPackageGraph(explicitProduct: explicitProduct) } - // FIXME: Implement the custom build command provider also. - return try XcodeBuildSystem( - buildParameters: customBuildParameters ?? swiftTool.buildParameters(), - packageGraphLoader: customPackageGraphLoader ?? graphLoader, - outputStream: customOutputStream ?? swiftTool.outputStream, - logLevel: customLogLevel ?? swiftTool.logLevel, - fileSystem: swiftTool.fileSystem, - observabilityScope: customObservabilityScope ?? swiftTool.observabilityScope - ) - } - ]) - } } /// A safe wrapper of TSCBasic.exec. @@ -302,16 +120,16 @@ public class SwiftTool { #endif /// The original working directory. - let originalWorkingDirectory: AbsolutePath + public let originalWorkingDirectory: AbsolutePath /// The options of this tool. - let options: GlobalOptions + public let options: GlobalOptions /// Path to the root package directory, nil if manifest is not found. - let packageRoot: AbsolutePath? + private let packageRoot: AbsolutePath? /// Helper function to get package root or throw error if it is not found. - func getPackageRoot() throws -> AbsolutePath { + public func getPackageRoot() throws -> AbsolutePath { guard let packageRoot = packageRoot else { throw StringError("Could not find \(Manifest.filename) in this directory or any of its parent directories.") } @@ -319,11 +137,11 @@ public class SwiftTool { } /// Get the current workspace root object. - func getWorkspaceRoot() throws -> PackageGraphRootInput { + public func getWorkspaceRoot() throws -> PackageGraphRootInput { let packages: [AbsolutePath] if let workspace = options.locations.multirootPackageDataFile { - packages = try XcodeWorkspaceLoader(fileSystem: self.fileSystem, observabilityScope: self.observabilityScope).load(workspace: workspace) + packages = try self.workspaceLoaderProvider(self.fileSystem, self.observabilityScope).load(workspace: workspace) } else { packages = [try getPackageRoot()] } @@ -332,22 +150,22 @@ public class SwiftTool { } /// Scratch space (.build) directory. - let scratchDirectory: AbsolutePath + public let scratchDirectory: AbsolutePath /// Path to the shared security directory - let sharedSecurityDirectory: AbsolutePath? + public let sharedSecurityDirectory: AbsolutePath? /// Path to the shared cache directory - let sharedCacheDirectory: AbsolutePath? + public let sharedCacheDirectory: AbsolutePath? /// Path to the shared configuration directory - let sharedConfigurationDirectory: AbsolutePath? + public let sharedConfigurationDirectory: AbsolutePath? /// Cancellator to handle cancellation of outstanding work when handling SIGINT - let cancellator: Cancellator + public let cancellator: Cancellator /// The execution status of the tool. - var executionStatus: ExecutionStatus = .success + public var executionStatus: ExecutionStatus = .success /// Holds the currently active workspace. /// @@ -355,18 +173,24 @@ public class SwiftTool { /// workspace is not needed, in-fact it would be an error to ask for the workspace object /// for package init because the Manifest file should *not* present. private var _workspace: Workspace? - private var _workspaceDelegate: ToolWorkspaceDelegate? + private var _workspaceDelegate: WorkspaceDelegate? private let observabilityHandler: SwiftToolObservabilityHandler /// The observability scope to emit diagnostics event on - let observabilityScope: ObservabilityScope + public let observabilityScope: ObservabilityScope /// The min severity at which to log diagnostics - let logLevel: Diagnostic.Severity + public let logLevel: Basics.Diagnostic.Severity /// The file system in use - let fileSystem: FileSystem + public let fileSystem: FileSystem + + /// Provider which can create a `WorkspaceDelegate` if needed. + private let workspaceDelegateProvider: WorkspaceDelegateProvider + + /// Provider which can create a `WorkspaceLoader` if needed. + private let workspaceLoaderProvider: WorkspaceLoaderProvider private let toolWorkspaceConfiguration: ToolWorkspaceConfiguration @@ -375,17 +199,17 @@ public class SwiftTool { /// Create an instance of this tool. /// /// - parameter options: The command line options to be passed to this tool. - convenience init(options: GlobalOptions, toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init()) throws { + public convenience init(options: GlobalOptions, toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init(), workspaceDelegateProvider: @escaping WorkspaceDelegateProvider, workspaceLoaderProvider: @escaping WorkspaceLoaderProvider) throws { // output from background activities goes to stderr, this includes diagnostics and output from build operations, // package resolution that take place as part of another action // CLI commands that have user facing output, use stdout directly to emit the final result // this means that the build output from "swift build" goes to stdout // but the build output from "swift test" goes to stderr, while the tests output go to stdout - try self.init(outputStream: TSCBasic.stderrStream, options: options, toolWorkspaceConfiguration: toolWorkspaceConfiguration) + try self.init(outputStream: TSCBasic.stderrStream, options: options, toolWorkspaceConfiguration: toolWorkspaceConfiguration, workspaceDelegateProvider: workspaceDelegateProvider, workspaceLoaderProvider: workspaceLoaderProvider) } // marked internal for testing - internal init(outputStream: OutputByteStream, options: GlobalOptions, toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init()) throws { + internal init(outputStream: OutputByteStream, options: GlobalOptions, toolWorkspaceConfiguration: ToolWorkspaceConfiguration, workspaceDelegateProvider: @escaping WorkspaceDelegateProvider, workspaceLoaderProvider: @escaping WorkspaceLoaderProvider) throws { self.fileSystem = localFileSystem // first, bootstrap the observability system self.logLevel = options.logging.logLevel @@ -393,6 +217,8 @@ public class SwiftTool { let observabilitySystem = ObservabilitySystem(self.observabilityHandler) self.observabilityScope = observabilitySystem.topScope self.toolWorkspaceConfiguration = toolWorkspaceConfiguration + self.workspaceDelegateProvider = workspaceDelegateProvider + self.workspaceLoaderProvider = workspaceLoaderProvider let cancellator = Cancellator(observabilityScope: self.observabilityScope) @@ -487,14 +313,6 @@ public class SwiftTool { } static func postprocessArgParserResult(options: GlobalOptions, observabilityScope: ObservabilityScope) throws { - if options.locations._deprecated_buildPath != nil { - observabilityScope.emit(warning: "'--build-path' option is deprecated; use '--scratch-path' instead") - } - - if options.locations._deprecated_chdir != nil { - observabilityScope.emit(warning: "'--chdir/-C' option is deprecated; use '--package-path' instead") - } - if options.locations.multirootPackageDataFile != nil { observabilityScope.emit(.unsupportedFlag("--multiroot-data-file")) } @@ -521,22 +339,6 @@ public class SwiftTool { if let _ = options.security.netrcFilePath, options.security.netrc == false { observabilityScope.emit(.mutuallyExclusiveArgumentsError(arguments: ["--disable-netrc", "--netrc-file"])) } - - if options.security._deprecated_netrc { - observabilityScope.emit(warning: "'--netrc' option is deprecated; .netrc files are located by default") - } - - if options.security._deprecated_netrcOptional { - observabilityScope.emit(warning: "'--netrc-optional' option is deprecated; .netrc files are located by default") - } - - if options.resolver._deprecated_enableResolverTrace { - observabilityScope.emit(warning: "'--enableResolverTrace' flag is deprecated; use '--verbose' option to log resolver output") - } - - if options.caching._deprecated_useRepositoriesCache != nil { - observabilityScope.emit(warning: "'--disable-repository-cache'/'--enable-repository-cache' flags are deprecated; use '--disable-dependency-cache'/'--enable-dependency-cache' instead") - } } func waitForObservabilityEvents(timeout: DispatchTime) { @@ -544,16 +346,12 @@ public class SwiftTool { } /// Returns the currently active workspace. - func getActiveWorkspace(emitDeprecatedConfigurationWarning: Bool = false) throws -> Workspace { + public func getActiveWorkspace(emitDeprecatedConfigurationWarning: Bool = false) throws -> Workspace { if let workspace = _workspace { return workspace } - let delegate = ToolWorkspaceDelegate( - observabilityScope: self.observabilityScope, - outputHandler: self.observabilityHandler.print, - progressHandler: self.observabilityHandler.progress - ) + let delegate = self.workspaceDelegateProvider(self.observabilityScope, self.observabilityHandler.print, self.observabilityHandler.progress) let isXcodeBuildSystemEnabled = self.options.build.buildSystem == .xcode let workspace = try Workspace( fileSystem: self.fileSystem, @@ -621,7 +419,7 @@ public class SwiftTool { } } - func getAuthorizationProvider() throws -> AuthorizationProvider? { + public func getAuthorizationProvider() throws -> AuthorizationProvider? { var authorization = Workspace.Configuration.Authorization.default if !options.security.netrc { authorization.netrc = .disabled @@ -640,7 +438,7 @@ public class SwiftTool { } /// Resolve the dependencies. - func resolve() throws { + public func resolve() throws { let workspace = try getActiveWorkspace() let root = try getWorkspaceRoot() @@ -662,7 +460,7 @@ public class SwiftTool { /// - Parameters: /// - explicitProduct: The product specified on the command line to a “swift run” or “swift build” command. This allows executables from dependencies to be run directly without having to hook them up to any particular target. @discardableResult - func loadPackageGraph( + public func loadPackageGraph( explicitProduct: String? = nil, testEntryPointPath: AbsolutePath? = nil ) throws -> PackageGraph { @@ -689,7 +487,7 @@ public class SwiftTool { } } - func getPluginScriptRunner(customPluginsDir: AbsolutePath? = .none) throws -> PluginScriptRunner { + public func getPluginScriptRunner(customPluginsDir: AbsolutePath? = .none) throws -> PluginScriptRunner { let pluginsDir = try customPluginsDir ?? self.getActiveWorkspace().location.pluginWorkingDirectory let cacheDir = pluginsDir.appending(component: "cache") let pluginScriptRunner = try DefaultPluginScriptRunner( @@ -705,11 +503,11 @@ public class SwiftTool { } /// Returns the user toolchain to compile the actual product. - func getDestinationToolchain() throws -> UserToolchain { + public func getDestinationToolchain() throws -> UserToolchain { return try _destinationToolchain.get() } - func getHostToolchain() throws -> UserToolchain { + public func getHostToolchain() throws -> UserToolchain { return try _hostToolchain.get() } @@ -717,7 +515,7 @@ public class SwiftTool { return try _manifestLoader.get() } - fileprivate func canUseCachedBuildManifest() throws -> Bool { + public func canUseCachedBuildManifest() throws -> Bool { if !self.options.caching.cacheBuildManifest { return false } @@ -746,14 +544,14 @@ public class SwiftTool { // "customOutputStream" is designed to support build output redirection // but it is only expected to be used when invoking builds from "swift build" command. // in all other cases, the build output should go to the default which is stderr - func createBuildSystem( + public func createBuildSystem( explicitBuildSystem: BuildSystemProvider.Kind? = .none, explicitProduct: String? = .none, cacheBuildManifest: Bool = true, customBuildParameters: BuildParameters? = .none, customPackageGraphLoader: (() throws -> PackageGraph)? = .none, customOutputStream: OutputByteStream? = .none, - customLogLevel: Diagnostic.Severity? = .none, + customLogLevel: Basics.Diagnostic.Severity? = .none, customObservabilityScope: ObservabilityScope? = .none ) throws -> BuildSystem { guard let buildSystemProvider = buildSystemProvider else { @@ -777,7 +575,7 @@ public class SwiftTool { } /// Return the build parameters. - func buildParameters() throws -> BuildParameters { + public func buildParameters() throws -> BuildParameters { return try _buildParameters.get() } @@ -908,7 +706,7 @@ public class SwiftTool { }() /// An enum indicating the execution status of run commands. - enum ExecutionStatus { + public enum ExecutionStatus { case success case failure } @@ -985,6 +783,12 @@ extension Basics.Diagnostic { } } +// MARK: - Support for loading external workspaces + +public protocol WorkspaceLoader { + func load(workspace: AbsolutePath) throws -> [AbsolutePath] +} + // MARK: - Diagnostics private struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider { @@ -1109,7 +913,7 @@ private struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider { extension SwiftTool { // FIXME: deprecate these one we are further along refactoring the call sites that use it /// The stream to print standard output on. - var outputStream: OutputByteStream { + public var outputStream: OutputByteStream { self.observabilityHandler.outputStream } } @@ -1136,7 +940,7 @@ private struct InteractiveWriter { if let term = self.term { term.write(string, inColor: color, bold: bold) } else { - stream <<< string + string.write(to: stream) stream.flush() } } @@ -1225,3 +1029,9 @@ extension Basics.Diagnostic.Severity { return self <= .info } } + +public extension Basics.Diagnostic { + static func mutuallyExclusiveArgumentsError(arguments: [String]) -> Self { + .error(arguments.map{ "'\($0)'" }.spm_localizedJoin(type: .conjunction) + " are mutually exclusive") + } +} diff --git a/Sources/SPMBuildCore/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem.swift index 9f96eeb0b7b..7a50e6ba585 100644 --- a/Sources/SPMBuildCore/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem.swift @@ -103,7 +103,7 @@ public struct BuildSystemProvider { _ customObservabilityScope: ObservabilityScope? ) throws -> BuildSystem - private let providers: [Kind:Provider] + public let providers: [Kind:Provider] public init(providers: [Kind:Provider]) { self.providers = providers diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 1b42ba48898..679f3689557 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +@testable import CoreCommands @testable import Commands import Foundation import PackageGraph @@ -104,17 +105,6 @@ final class PackageToolTests: CommandsTestCase { ) { error in XCTAssertMatch(String(describing: error), .contains("Value to be set with flag '--disable-netrc' had already been set with flag '--enable-netrc'")) } - - // deprecated --netrc flag - let stderr = try self.execute(["resolve", "--netrc"], packagePath: fixturePath).stderr - XCTAssertMatch(stderr, .contains("'--netrc' option is deprecated")) - } - } - - func testNetrcOptional() throws { - try fixture(name: "DependencyResolution/External/XCFramework") { fixturePath in - let stderr = try self.execute(["resolve", "--netrc-optional"], packagePath: fixturePath).stderr - XCTAssertMatch(stderr, .contains("'--netrc-optional' option is deprecated")) } } @@ -206,8 +196,7 @@ final class PackageToolTests: CommandsTestCase { _ = try execute(["reset"], packagePath: packageRoot) try localFileSystem.removeFileTree(cachePath) - let (_, stderr) = try self.execute(["resolve", "--enable-repository-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssertMatch(stderr, .contains("'--disable-repository-cache'/'--enable-repository-cache' flags are deprecated; use '--disable-dependency-cache'/'--enable-dependency-cache' instead")) + let (_, _) = try self.execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) // we have to check for the prefix here since the hash value changes because spm sees the `prefix` // directory `/var/...` as `/private/var/...`. @@ -218,7 +207,7 @@ final class PackageToolTests: CommandsTestCase { _ = try execute(["reset"], packagePath: packageRoot) // Perform another cache this time from the cache - _ = try execute(["resolve", "--enable-repository-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + _ = try execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) // Remove .build and cache folder @@ -226,7 +215,7 @@ final class PackageToolTests: CommandsTestCase { try localFileSystem.removeFileTree(cachePath) // Perform another fetch - _ = try execute(["resolve", "--enable-repository-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + _ = try execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) } @@ -236,8 +225,7 @@ final class PackageToolTests: CommandsTestCase { _ = try execute(["reset"], packagePath: packageRoot) try localFileSystem.removeFileTree(cachePath) - let (_, stderr) = try self.execute(["resolve", "--disable-repository-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssertMatch(stderr, .contains("'--disable-repository-cache'/'--enable-repository-cache' flags are deprecated; use '--disable-dependency-cache'/'--enable-dependency-cache' instead")) + let (_, _) = try self.execute(["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) // we have to check for the prefix here since the hash value changes because spm sees the `prefix` // directory `/var/...` as `/private/var/...`. @@ -479,7 +467,7 @@ final class PackageToolTests: CommandsTestCase { // Returns symbol graph with or without pretty printing. private func symbolGraph(atPath path: AbsolutePath, withPrettyPrinting: Bool, file: StaticString = #file, line: UInt = #line) throws -> Data? { - let tool = try SwiftTool(options: GlobalOptions.parse(["--package-path", path.pathString])) + let tool = try SwiftTool.createSwiftToolForTest(options: GlobalOptions.parse(["--package-path", path.pathString])) let symbolGraphExtractorPath = try tool.getDestinationToolchain().getSymbolGraphExtract() let arguments = withPrettyPrinting ? ["dump-symbol-graph", "--pretty-print"] : ["dump-symbol-graph"] diff --git a/Tests/CommandsTests/SwiftToolTests.swift b/Tests/CommandsTests/SwiftToolTests.swift index 0d35c46a0ff..c7da1ed4c15 100644 --- a/Tests/CommandsTests/SwiftToolTests.swift +++ b/Tests/CommandsTests/SwiftToolTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// @testable import Basics +@testable import CoreCommands @testable import Commands import SPMTestSupport import TSCBasic @@ -23,7 +24,7 @@ final class SwiftToolTests: CommandsTestCase { do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString]) - let tool = try SwiftTool(outputStream: outputStream, options: options) + let tool = try SwiftTool.createSwiftToolForTest(outputStream: outputStream, options: options) XCTAssertEqual(tool.logLevel, .warning) tool.observabilityScope.emit(error: "error") @@ -42,7 +43,7 @@ final class SwiftToolTests: CommandsTestCase { do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--verbose"]) - let tool = try SwiftTool(outputStream: outputStream, options: options) + let tool = try SwiftTool.createSwiftToolForTest(outputStream: outputStream, options: options) XCTAssertEqual(tool.logLevel, .info) tool.observabilityScope.emit(error: "error") @@ -61,7 +62,7 @@ final class SwiftToolTests: CommandsTestCase { do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "-v"]) - let tool = try SwiftTool(outputStream: outputStream, options: options) + let tool = try SwiftTool.createSwiftToolForTest(outputStream: outputStream, options: options) XCTAssertEqual(tool.logLevel, .info) tool.observabilityScope.emit(error: "error") @@ -80,7 +81,7 @@ final class SwiftToolTests: CommandsTestCase { do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--very-verbose"]) - let tool = try SwiftTool(outputStream: outputStream, options: options) + let tool = try SwiftTool.createSwiftToolForTest(outputStream: outputStream, options: options) XCTAssertEqual(tool.logLevel, .debug) tool.observabilityScope.emit(error: "error") @@ -99,7 +100,7 @@ final class SwiftToolTests: CommandsTestCase { do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--vv"]) - let tool = try SwiftTool(outputStream: outputStream, options: options) + let tool = try SwiftTool.createSwiftToolForTest(outputStream: outputStream, options: options) XCTAssertEqual(tool.logLevel, .debug) tool.observabilityScope.emit(error: "error") @@ -130,7 +131,7 @@ final class SwiftToolTests: CommandsTestCase { } let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--netrc-file", customPath.pathString]) - let tool = try SwiftTool(options: options) + let tool = try SwiftTool.createSwiftToolForTest(options: options) let authorizationProvider = try tool.getAuthorizationProvider() as? CompositeAuthorizationProvider let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider } ?? [] @@ -155,7 +156,7 @@ final class SwiftToolTests: CommandsTestCase { } let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString]) - let tool = try SwiftTool(options: options) + let tool = try SwiftTool.createSwiftToolForTest(options: options) let authorizationProvider = try tool.getAuthorizationProvider() as? CompositeAuthorizationProvider let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider } ?? [] @@ -171,3 +172,21 @@ final class SwiftToolTests: CommandsTestCase { } } } + +extension SwiftTool { + static func createSwiftToolForTest( + outputStream: OutputByteStream = stderrStream, + options: GlobalOptions + ) throws -> SwiftTool { + return try SwiftTool( + outputStream: outputStream, + options: options, + toolWorkspaceConfiguration: .init(), + workspaceDelegateProvider: { + ToolWorkspaceDelegate(observabilityScope: $0, outputHandler: $1, progressHandler: $2) + }, + workspaceLoaderProvider: { + XcodeWorkspaceLoader(fileSystem: $0, observabilityScope: $1) + }) + } +}