diff --git a/Sources/CoreCommands/SwiftToolObservabilityHandler.swift b/Sources/CoreCommands/SwiftToolObservabilityHandler.swift index 1bbbadd4010..f8958b45e1e 100644 --- a/Sources/CoreCommands/SwiftToolObservabilityHandler.swift +++ b/Sources/CoreCommands/SwiftToolObservabilityHandler.swift @@ -53,7 +53,7 @@ public struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider { self.outputHandler.prompt(message: message, completion: completion) } - func wait(timeout: DispatchTime) { + public func wait(timeout: DispatchTime) { self.outputHandler.wait(timeout: timeout) } @@ -212,12 +212,3 @@ extension Basics.Diagnostic.Severity { return self <= .info } } - -extension ObservabilitySystem { - public static func swiftTool( - outputStream: OutputByteStream = stdoutStream, - logLevel: Basics.Diagnostic.Severity = .warning - ) -> ObservabilitySystem { - .init(SwiftToolObservabilityHandler(outputStream: stdoutStream, logLevel: logLevel)) - } -} diff --git a/Sources/CrossCompilationDestinationsTool/CMakeLists.txt b/Sources/CrossCompilationDestinationsTool/CMakeLists.txt index 7352b02e833..a15558c575b 100644 --- a/Sources/CrossCompilationDestinationsTool/CMakeLists.txt +++ b/Sources/CrossCompilationDestinationsTool/CMakeLists.txt @@ -7,6 +7,9 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(CrossCompilationDestinationsTool + Configuration/ConfigureDestination.swift + Configuration/ResetConfiguration.swift + Configuration/ShowConfiguration.swift DestinationCommand.swift InstallDestination.swift ListDestinations.swift diff --git a/Sources/CrossCompilationDestinationsTool/Configuration/ConfigureDestination.swift b/Sources/CrossCompilationDestinationsTool/Configuration/ConfigureDestination.swift new file mode 100644 index 00000000000..abdabc996ac --- /dev/null +++ b/Sources/CrossCompilationDestinationsTool/Configuration/ConfigureDestination.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 + +struct ConfigureDestination: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "configuration", + abstract: """ + Manages configuration options for installed cross-compilation destinations. + """, + subcommands: [ + ResetConfiguration.self, + ShowConfiguration.self, + ] + ) +} diff --git a/Sources/CrossCompilationDestinationsTool/Configuration/ResetConfiguration.swift b/Sources/CrossCompilationDestinationsTool/Configuration/ResetConfiguration.swift new file mode 100644 index 00000000000..c3c750bb758 --- /dev/null +++ b/Sources/CrossCompilationDestinationsTool/Configuration/ResetConfiguration.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 CoreCommands +import PackageModel + +import struct TSCBasic.AbsolutePath + +struct ResetConfiguration: DestinationCommand { + static let configuration = CommandConfiguration( + commandName: "reset", + abstract: """ + Resets configuration properties currently applied to a given destination and run-time triple. If no specific \ + property is specified, all of them are reset for the destination. + """ + ) + + @OptionGroup(visibility: .hidden) + var locations: LocationOptions + + @Flag(help: "Reset custom configuration for a path to a directory containing the SDK root.") + var sdkRootPath = false + + @Flag(help: "Reset custom configuration for a path to a directory containing Swift resources for dynamic linking.") + var swiftResourcesPath = false + + @Flag(help: "Reset custom configuration for a path to a directory containing Swift resources for static linking.") + var swiftStaticResourcesPath = false + + @Flag(help: "Reset custom configuration for a path to a directory containing headers.") + var includeSearchPath = false + + @Flag(help: "Reset custom configuration for a path to a directory containing libraries.") + var librarySearchPath = false + + @Flag(help: "Reset custom configuration for a path to a toolset file.") + var toolsetPath = false + + @Argument( + help: """ + An identifier of an already installed destination. Use the `list` subcommand to see all available \ + identifiers. + """ + ) + var destinationID: String + + @Argument(help: "The run-time triple of the destination to configure.") + var runTimeTriple: String + + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws { + let configurationStore = try DestinationConfigurationStore( + buildTimeTriple: buildTimeTriple, + destinationsDirectoryPath: destinationsDirectory, + fileSystem: fileSystem, + observabilityScope: observabilityScope + ) + + let triple = try Triple(runTimeTriple) + + guard var destination = try configurationStore.readConfiguration( + destinationID: destinationID, + runTimeTriple: triple + ) else { + throw DestinationError.destinationNotFound( + artifactID: destinationID, + builtTimeTriple: buildTimeTriple, + runTimeTriple: triple + ) + } + + var configuration = destination.pathsConfiguration + var shouldResetAll = true + + if sdkRootPath { + configuration.sdkRootPath = nil + shouldResetAll = false + } + + if swiftResourcesPath { + configuration.swiftResourcesPath = nil + shouldResetAll = false + } + + if swiftStaticResourcesPath { + configuration.swiftResourcesPath = nil + shouldResetAll = false + } + + if includeSearchPath { + configuration.includeSearchPaths = nil + shouldResetAll = false + } + + if librarySearchPath { + configuration.librarySearchPaths = nil + shouldResetAll = false + } + + if toolsetPath { + configuration.toolsetPaths = nil + shouldResetAll = false + } + + if shouldResetAll { + if try !configurationStore.resetConfiguration(destinationID: destinationID, runTimeTriple: triple) { + observabilityScope.emit( + warning: "No configuration for destination \(destinationID)" + ) + } + } else { + try configurationStore.updateConfiguration(destinationID: destinationID, destination: destination) + } + } +} diff --git a/Sources/CrossCompilationDestinationsTool/Configuration/ShowConfiguration.swift b/Sources/CrossCompilationDestinationsTool/Configuration/ShowConfiguration.swift new file mode 100644 index 00000000000..f59cb2d9ee7 --- /dev/null +++ b/Sources/CrossCompilationDestinationsTool/Configuration/ShowConfiguration.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 CoreCommands +import PackageModel + +import struct TSCBasic.AbsolutePath + +struct ShowConfiguration: DestinationCommand { + static let configuration = CommandConfiguration( + commandName: "show", + abstract: """ + Prints all configuration properties currently applied to a given destination and run-time triple. + """ + ) + + @OptionGroup(visibility: .hidden) + var locations: LocationOptions + + @Argument( + help: """ + An identifier of an already installed destination. Use the `list` subcommand to see all available \ + identifiers. + """ + ) + var destinationID: String + + @Argument(help: "The run-time triple of the destination to configure.") + var runTimeTriple: String + + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws { + let configurationStore = try DestinationConfigurationStore( + buildTimeTriple: buildTimeTriple, + destinationsDirectoryPath: destinationsDirectory, + fileSystem: fileSystem, + observabilityScope: observabilityScope + ) + + let triple = try Triple(runTimeTriple) + + guard let configuration = try configurationStore.readConfiguration( + destinationID: destinationID, + runTimeTriple: triple + )?.pathsConfiguration else { + throw DestinationError.destinationNotFound( + artifactID: destinationID, + builtTimeTriple: buildTimeTriple, + runTimeTriple: triple + ) + } + + print(configuration) + } +} + +extension Destination.PathsConfiguration: CustomStringConvertible { + public var description: String { + """ + sdkRootPath: \(sdkRootPath.configurationString) + swiftResourcesPath: \(swiftResourcesPath.configurationString) + swiftStaticResourcesPath: \(swiftStaticResourcesPath.configurationString) + includeSearchPaths: \(includeSearchPaths.configurationString) + librarySearchPaths: \(librarySearchPaths.configurationString) + toolsetPaths: \(toolsetPaths.configurationString) + """ + } +} + +extension Optional where Wrapped == AbsolutePath { + fileprivate var configurationString: String { + self?.pathString ?? "not set" + } +} + +extension Optional where Wrapped == [AbsolutePath] { + fileprivate var configurationString: String { + self?.map(\.pathString).description ?? "not set" + } +} diff --git a/Sources/CrossCompilationDestinationsTool/DestinationCommand.swift b/Sources/CrossCompilationDestinationsTool/DestinationCommand.swift index 212be0f3e2c..5451927a5cf 100644 --- a/Sources/CrossCompilationDestinationsTool/DestinationCommand.swift +++ b/Sources/CrossCompilationDestinationsTool/DestinationCommand.swift @@ -11,13 +11,31 @@ //===----------------------------------------------------------------------===// import ArgumentParser +import Basics import CoreCommands -import TSCBasic +import Dispatch +import PackageModel + +import struct TSCBasic.AbsolutePath +import protocol TSCBasic.FileSystem +import var TSCBasic.localFileSystem +import var TSCBasic.stdoutStream /// A protocol for functions and properties common to all destination subcommands. protocol DestinationCommand: ParsableCommand { /// Common locations options provided by ArgumentParser. var locations: LocationOptions { get } + + /// Run a command operating on cross-compilation destinations, passing it required configuration values. + /// - Parameters: + /// - buildTimeTriple: triple of the machine this command is running on. + /// - destinationsDirectory: directory containing destination artifact bundles and their configuration. + /// - observabilityScope: observability scope used for logging. + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws } extension DestinationCommand { @@ -43,4 +61,31 @@ extension DestinationCommand { return destinationsDirectory } + + public func run() throws { + let observabilityHandler = SwiftToolObservabilityHandler(outputStream: stdoutStream, logLevel: .info) + let observabilitySystem = ObservabilitySystem(observabilityHandler) + let observabilityScope = observabilitySystem.topScope + let destinationsDirectory = try self.getOrCreateDestinationsDirectory() + + let hostToolchain = try UserToolchain(destination: Destination.hostDestination()) + let triple = Triple.getHostTriple(usingSwiftCompiler: hostToolchain.swiftCompilerPath) + + var commandError: Error? = nil + do { + try self.run(buildTimeTriple: triple, destinationsDirectory, observabilityScope) + if observabilityScope.errorsReported { + throw ExitCode.failure + } + } catch { + commandError = error + } + + // wait for all observability items to process + observabilityHandler.wait(timeout: .now() + 5) + + if let error = commandError { + throw error + } + } } diff --git a/Sources/CrossCompilationDestinationsTool/InstallDestination.swift b/Sources/CrossCompilationDestinationsTool/InstallDestination.swift index 797622b4a8c..ec3fe5378f5 100644 --- a/Sources/CrossCompilationDestinationsTool/InstallDestination.swift +++ b/Sources/CrossCompilationDestinationsTool/InstallDestination.swift @@ -35,11 +35,11 @@ struct InstallDestination: DestinationCommand { @Argument(help: "A local filesystem path or a URL of an artifact bundle to install.") var bundlePathOrURL: String - func run() throws { - let observabilitySystem = ObservabilitySystem.swiftTool() - let observabilityScope = observabilitySystem.topScope - let destinationsDirectory = try self.getOrCreateDestinationsDirectory() - + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws { if let bundleURL = URL(string: bundlePathOrURL), let scheme = bundleURL.scheme, @@ -78,6 +78,6 @@ struct InstallDestination: DestinationCommand { throw StringError("Argument `\(bundlePathOrURL)` is neither a valid filesystem path nor a URL.") } - print("Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.") + observabilityScope.emit(info: "Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.") } } diff --git a/Sources/CrossCompilationDestinationsTool/ListDestinations.swift b/Sources/CrossCompilationDestinationsTool/ListDestinations.swift index 6accc860a92..cbf5e91d18f 100644 --- a/Sources/CrossCompilationDestinationsTool/ListDestinations.swift +++ b/Sources/CrossCompilationDestinationsTool/ListDestinations.swift @@ -15,9 +15,10 @@ import Basics import CoreCommands import PackageModel import SPMBuildCore -import TSCBasic -public struct ListDestinations: DestinationCommand { +import struct TSCBasic.AbsolutePath + +struct ListDestinations: DestinationCommand { public static let configuration = CommandConfiguration( commandName: "list", abstract: @@ -29,13 +30,11 @@ public struct ListDestinations: DestinationCommand { @OptionGroup() var locations: LocationOptions - public init() {} - - public func run() throws { - let observabilitySystem = ObservabilitySystem.swiftTool() - let observabilityScope = observabilitySystem.topScope - let destinationsDirectory = try self.getOrCreateDestinationsDirectory() - + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws { let validBundles = try DestinationBundle.getAllValidBundles( destinationsDirectory: destinationsDirectory, fileSystem: fileSystem, @@ -43,7 +42,7 @@ public struct ListDestinations: DestinationCommand { ) guard !validBundles.isEmpty else { - print("No cross-compilation destinations are currently installed.") + observabilityScope.emit(info: "No cross-compilation destinations are currently installed.") return } diff --git a/Sources/CrossCompilationDestinationsTool/RemoveDestination.swift b/Sources/CrossCompilationDestinationsTool/RemoveDestination.swift index e6a250ba495..cf3aa60072a 100644 --- a/Sources/CrossCompilationDestinationsTool/RemoveDestination.swift +++ b/Sources/CrossCompilationDestinationsTool/RemoveDestination.swift @@ -14,6 +14,8 @@ import ArgumentParser import Basics import CoreCommands +import struct TSCBasic.AbsolutePath + struct RemoveDestination: DestinationCommand { static let configuration = CommandConfiguration( commandName: "remove", @@ -28,7 +30,11 @@ struct RemoveDestination: DestinationCommand { @Argument(help: "Name of the destination artifact bundle to remove from the filesystem.") var bundleName: String - func run() throws { + func run( + buildTimeTriple: Triple, + _ destinationsDirectory: AbsolutePath, + _ observabilityScope: ObservabilityScope + ) throws { let destinationsDirectory = try self.getOrCreateDestinationsDirectory() let artifactBundleDirectory = destinationsDirectory.appending(component: self.bundleName) diff --git a/Sources/CrossCompilationDestinationsTool/SwiftDestinationTool.swift b/Sources/CrossCompilationDestinationsTool/SwiftDestinationTool.swift index 464bb1bba51..c54ed44c899 100644 --- a/Sources/CrossCompilationDestinationsTool/SwiftDestinationTool.swift +++ b/Sources/CrossCompilationDestinationsTool/SwiftDestinationTool.swift @@ -20,6 +20,7 @@ public struct SwiftDestinationTool: ParsableCommand { abstract: "Perform operations on Swift cross-compilation destinations.", version: SwiftVersion.current.completeDisplayString, subcommands: [ + ConfigureDestination.self, InstallDestination.self, ListDestinations.self, RemoveDestination.self, diff --git a/Sources/PackageModel/Destination.swift b/Sources/PackageModel/Destination.swift index 01677e7da32..36040cba781 100644 --- a/Sources/PackageModel/Destination.swift +++ b/Sources/PackageModel/Destination.swift @@ -176,19 +176,91 @@ public struct Destination: Equatable { public var sdkRootPath: AbsolutePath? /// Path containing Swift resources for dynamic linking. - private(set) var swiftResourcesPath: AbsolutePath? + public var swiftResourcesPath: AbsolutePath? /// Path containing Swift resources for static linking. - private(set) var swiftStaticResourcesPath: AbsolutePath? + public var swiftStaticResourcesPath: AbsolutePath? /// Array of paths containing headers. - private(set) var includeSearchPaths: [AbsolutePath]? + public var includeSearchPaths: [AbsolutePath]? /// Array of paths containing libraries. - private(set) var librarySearchPaths: [AbsolutePath]? + public var librarySearchPaths: [AbsolutePath]? /// Array of paths containing toolset files. - private(set) var toolsetPaths: [AbsolutePath]? + public var toolsetPaths: [AbsolutePath]? + + /// Initialize paths configuration from values deserialized using v3 schema. + /// - Parameters: + /// - properties: properties of the destination for the given triple. + /// - destinationDirectory: directory used for converting relative paths in `properties` to absolute paths. + init(_ properties: SerializedDestinationV3.TripleProperties, destinationDirectory: AbsolutePath? = nil) throws { + if let destinationDirectory = destinationDirectory { + self.init( + sdkRootPath: try AbsolutePath(validating: properties.sdkRootPath, relativeTo: destinationDirectory), + swiftResourcesPath: try properties.swiftResourcesPath.map { + try AbsolutePath(validating: $0, relativeTo: destinationDirectory) + }, + swiftStaticResourcesPath: try properties.swiftStaticResourcesPath.map { + try AbsolutePath(validating: $0, relativeTo: destinationDirectory) + }, + includeSearchPaths: try properties.includeSearchPaths?.map { + try AbsolutePath(validating: $0, relativeTo: destinationDirectory) + }, + librarySearchPaths: try properties.librarySearchPaths?.map { + try AbsolutePath(validating: $0, relativeTo: destinationDirectory) + }, + toolsetPaths: try properties.toolsetPaths?.map { + try AbsolutePath(validating: $0, relativeTo: destinationDirectory) + } + ) + } else { + self.init( + sdkRootPath: try AbsolutePath(validating: properties.sdkRootPath), + swiftResourcesPath: try properties.swiftResourcesPath.map { + try AbsolutePath(validating: $0) + }, + swiftStaticResourcesPath: try properties.swiftStaticResourcesPath.map { + try AbsolutePath(validating: $0) + }, + includeSearchPaths: try properties.includeSearchPaths?.map { + try AbsolutePath(validating: $0) + }, + librarySearchPaths: try properties.librarySearchPaths?.map { + try AbsolutePath(validating: $0) + }, + toolsetPaths: try properties.toolsetPaths?.map { + try AbsolutePath(validating: $0) + } + ) + } + } + + public mutating func merge(with newConfiguration: Self) { + if let sdkRootPath = newConfiguration.sdkRootPath { + self.sdkRootPath = sdkRootPath + } + + if let swiftResourcesPath = newConfiguration.swiftResourcesPath { + self.swiftResourcesPath = swiftResourcesPath + } + + if let swiftStaticResourcesPath = newConfiguration.swiftStaticResourcesPath { + self.swiftStaticResourcesPath = swiftStaticResourcesPath + } + + if let includeSearchPaths = newConfiguration.includeSearchPaths { + self.includeSearchPaths = includeSearchPaths + } + + if let librarySearchPaths = newConfiguration.librarySearchPaths { + self.librarySearchPaths = librarySearchPaths + } + + if let toolsetPaths = newConfiguration.toolsetPaths { + self.toolsetPaths = toolsetPaths + } + } } /// Configuration of file system paths used by this destination when building. @@ -469,53 +541,11 @@ extension Destination { toolset: Toolset = .init(), destinationDirectory: AbsolutePath? = nil ) throws { - if let destinationDirectory = destinationDirectory { - self.init( - targetTriple: runTimeTriple, - toolset: toolset, - pathsConfiguration: .init( - sdkRootPath: try AbsolutePath(validating: properties.sdkRootPath, relativeTo: destinationDirectory), - swiftResourcesPath: try properties.swiftResourcesPath.map { - try AbsolutePath(validating: $0, relativeTo: destinationDirectory) - }, - swiftStaticResourcesPath: try properties.swiftStaticResourcesPath.map { - try AbsolutePath(validating: $0, relativeTo: destinationDirectory) - }, - includeSearchPaths: try properties.includeSearchPaths?.map { - try AbsolutePath(validating: $0, relativeTo: destinationDirectory) - }, - librarySearchPaths: try properties.librarySearchPaths?.map { - try AbsolutePath(validating: $0, relativeTo: destinationDirectory) - }, - toolsetPaths: try properties.toolsetPaths?.map { - try AbsolutePath(validating: $0, relativeTo: destinationDirectory) - } - ) - ) - } else { - self.init( - targetTriple: runTimeTriple, - toolset: toolset, - pathsConfiguration: .init( - sdkRootPath: try AbsolutePath(validating: properties.sdkRootPath), - swiftResourcesPath: try properties.swiftResourcesPath.map { - try AbsolutePath(validating: $0) - }, - swiftStaticResourcesPath: try properties.swiftStaticResourcesPath.map { - try AbsolutePath(validating: $0) - }, - includeSearchPaths: try properties.includeSearchPaths?.map { - try AbsolutePath(validating: $0) - }, - librarySearchPaths: try properties.librarySearchPaths?.map { - try AbsolutePath(validating: $0) - }, - toolsetPaths: try properties.toolsetPaths?.map { - try AbsolutePath(validating: $0) - } - ) - ) - } + self.init( + targetTriple: runTimeTriple, + toolset: toolset, + pathsConfiguration: try .init(properties, destinationDirectory: destinationDirectory) + ) } /// Load a ``Destination`` description from a legacy JSON representation from disk. diff --git a/Sources/PackageModel/DestinationConfigurationStore.swift b/Sources/PackageModel/DestinationConfigurationStore.swift index 68407a8cfa4..9794e220b24 100644 --- a/Sources/PackageModel/DestinationConfigurationStore.swift +++ b/Sources/PackageModel/DestinationConfigurationStore.swift @@ -94,6 +94,20 @@ public final class DestinationConfigurationStore { component: "\(destinationID)_\(triple.tripleString).json" ) + let destinationBundles = try DestinationBundle.getAllValidBundles( + destinationsDirectory: destinationsDirectoryPath, + fileSystem: fileSystem, + observabilityScope: observabilityScope + ) + + guard var destination = destinationBundles.selectDestination( + id: destinationID, + hostTriple: buildTimeTriple, + targetTriple: triple + ) else { + return nil + } + if fileSystem.isFile(configurationPath) { let properties = try decoder.decode( path: configurationPath, @@ -101,23 +115,15 @@ public final class DestinationConfigurationStore { as: SerializedDestinationV3.TripleProperties.self ) - return try Destination( - runTimeTriple: triple, - properties: properties - ) - } else { - let destinationBundles = try DestinationBundle.getAllValidBundles( - destinationsDirectory: destinationsDirectoryPath, - fileSystem: fileSystem, - observabilityScope: observabilityScope - ) - - return destinationBundles.selectDestination( - id: destinationID, - hostTriple: buildTimeTriple, - targetTriple: triple + destination.pathsConfiguration.merge( + with: try Destination( + runTimeTriple: triple, + properties: properties + ).pathsConfiguration ) } + + return destination } /// Resets configuration for identified destination triple.