Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwiftTool: introduce --experimental-destination-selector option #5922

Merged
merged 16 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ public struct BuildOptions: ParsableArguments {
shouldDisplay: false))
public var archs: [String] = []

/// Path to the compilation destination describing JSON file.
@Option(name: .customLong("experimental-destination-selector"), help: .hidden)
public var crossCompilationDestinationSelector: String?

/// Which compile-time sanitizers should be enabled.
@Option(name: .customLong("sanitize"),
help: "Turn on runtime checks for erroneous behavior, possible values: \(Sanitizer.formattedValues)",
Expand Down
26 changes: 19 additions & 7 deletions Sources/CoreCommands/SwiftTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import class TSCUtility.MultiLineNinjaProgressAnimation
import class TSCUtility.NinjaProgressAnimation
import class TSCUtility.PercentProgressAnimation
import protocol TSCUtility.ProgressAnimationProtocol
import struct TSCUtility.Triple
import var TSCUtility.verbosity

typealias Diagnostic = Basics.Diagnostic
Expand Down Expand Up @@ -620,13 +621,24 @@ public final class SwiftTool {
var destination: Destination
let hostDestination: Destination
do {
hostDestination = try self._hostToolchain.get().destination
let hostToolchain = try _hostToolchain.get()
hostDestination = hostToolchain.destination
let hostTriple = Triple.getHostTriple(usingSwiftCompiler: hostToolchain.swiftCompilerPath)

// Create custom toolchain if present.
if let customDestination = self.options.locations.customCompileDestination {
destination = try Destination(fromFile: customDestination, fileSystem: self.fileSystem)
} else if let target = self.options.build.customCompileTriple,
if let customDestination = options.locations.customCompileDestination {
destination = try Destination(fromFile: customDestination, fileSystem: fileSystem)
} else if let target = options.build.customCompileTriple,
let targetDestination = Destination.defaultDestination(for: target, host: hostDestination) {
destination = targetDestination
} else if let destinationSelector = options.build.crossCompilationDestinationSelector {
destination = try DestinationsBundle.selectDestination(
fromBundlesAt: sharedCrossCompilationDestinationsDirectory,
fileSystem: fileSystem,
matching: destinationSelector,
hostTriple: hostTriple,
observabilityScope: observabilityScope
)
} else {
// Otherwise use the host toolchain.
destination = hostDestination
Expand All @@ -635,13 +647,13 @@ public final class SwiftTool {
return .failure(error)
}
// Apply any manual overrides.
if let triple = self.options.build.customCompileTriple {
if let triple = options.build.customCompileTriple {
destination.targetTriple = triple
}
if let binDir = self.options.build.customCompileToolchain {
if let binDir = options.build.customCompileToolchain {
destination.toolchainBinDir = binDir.appending(components: "usr", "bin")
}
if let sdk = self.options.build.customCompileSDK {
if let sdk = options.build.customCompileSDK {
destination.sdkRootDir = sdk
}
destination.archs = options.build.archs
Expand Down
30 changes: 7 additions & 23 deletions Sources/CrossCompilationDestinationsTool/ListDestinations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,14 @@ struct ListDestinations: ParsableCommand {
destinationsDirectory = try fileSystem.getOrCreateSwiftPMCrossCompilationDestinationsDirectory()
}

// Get absolute paths to available destination bundles.
let destinationBundles = try fileSystem.getDirectoryContents(destinationsDirectory).filter {
$0.hasSuffix(BinaryTarget.Kind.artifactsArchive.fileExtension)
}.map {
destinationsDirectory.appending(components: [$0])
}

// Enumerate available bundles and parse manifests for each of them, then validate supplied destinations.
for bundlePath in destinationBundles {
do {
let destinationsBundle = try DestinationsBundle.parseAndValidate(
bundlePath: bundlePath,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
let validBundles = try DestinationsBundle.getAllValidBundles(
destinationsDirectory: destinationsDirectory,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)

destinationsBundle.artifacts.keys.forEach { print($0) }
} catch {
observabilityScope.emit(
.warning(
"Couldn't parse `info.json` manifest of a destination bundle at \(bundlePath): \(error)"
)
)
}
for bundle in validBundles {
bundle.artifacts.keys.forEach { print($0) }
}
}
}
2 changes: 1 addition & 1 deletion Sources/PackageModel/Destination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public struct Destination: Encodable, Equatable {
}

/// Additional flags to be passed to the build tools.
public let extraFlags: BuildFlags
public var extraFlags: BuildFlags
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it need to be mutated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it happens on https://github.com/apple/swift-package-manager/pull/5922/files#diff-ba866b666cdc2795c59e6713e0817015f0076f5447541e8c477db717cbb6baf3R114

Since it's a struct, I think making this var is safe, as any mutations are local and don't have side effects?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


/// Creates a compilation destination with the specified properties.
@available(*, deprecated, message: "use `init(targetTriple:sdkRootDir:toolchainBinDir:extraFlags)` instead")
Expand Down
7 changes: 4 additions & 3 deletions Sources/SPMBuildCore/ArtifactsArchiveMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ public struct ArtifactsArchiveMetadata: Equatable {
}
}

// In the future we are likely to extend the ArtifactsArchive file format to carry other types of artifacts beyond executables.
// Additional fields may be required to support these new artifact types e.g. headers path for libraries.
// This can also support resource-only artifacts as well. For example, 3d models along with associated textures, or fonts, etc.
// In the future we are likely to extend the ArtifactsArchive file format to carry other types of artifacts beyond
// executables and cross-compilation destinations. Additional fields may be required to support these new artifact
// types e.g. headers path for libraries. This can also support resource-only artifacts as well. For example,
// 3d models along with associated textures, or fonts, etc.
public enum ArtifactType: String, RawRepresentable, Decodable {
case executable
case crossCompilationDestination
Expand Down
173 changes: 171 additions & 2 deletions Sources/SPMBuildCore/DestinationsBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
import Basics
import PackageModel
import TSCBasic
import TSCUtility

/// Represents an `.artifactbundle` on the filesystem that contains cross-compilation destinations.
public struct DestinationsBundle {
public struct Variant {
public struct Variant: Equatable {
let metadata: ArtifactsArchiveMetadata.Variant
let destination: Destination
}
Expand All @@ -26,6 +27,103 @@ public struct DestinationsBundle {
/// Mapping of artifact IDs to variants available for a corresponding artifact.
public fileprivate(set) var artifacts = [String: [Variant]]()

/// Lists all valid cross-compilation destination bundles in a given directory.
/// - Parameters:
/// - destinationsDirectory: the directory to scan for destination bundles.
/// - fileSystem: the filesystem the directory is located on.
/// - observabilityScope: observability scope to report bundle validation errors.
/// - Returns: an array of valid destination bundles.
public static func getAllValidBundles(
destinationsDirectory: AbsolutePath,
fileSystem: FileSystem,
observabilityScope: ObservabilityScope
) throws -> [Self] {
// Get absolute paths to available destination bundles.
try fileSystem.getDirectoryContents(destinationsDirectory).filter {
$0.hasSuffix(BinaryTarget.Kind.artifactsArchive.fileExtension)
}.map {
destinationsDirectory.appending(components: [$0])
}.compactMap {
do {
// Enumerate available bundles and parse manifests for each of them, then validate supplied destinations.
return try Self.parseAndValidate(
bundlePath: $0,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)
} catch {
observabilityScope.emit(
.warning(
"Couldn't parse `info.json` manifest of a destination bundle at \($0): \(error)"
)
)
return nil
}
}
}

/// Select destinations matching a given query and host triple from all destinations available in a directory.
/// - Parameters:
/// - destinationsDirectory: the directory to scan for destination bundles.
/// - fileSystem: the filesystem the directory is located on.
/// - query: either an artifact ID or target triple to filter with.
/// - hostTriple: triple of the host building with these destinations.
/// - observabilityScope: observability scope to log warnings about multiple matches.
/// - Returns: `Destination` value matching `query` either by artifact ID or target triple, `nil` if none found.
public static func selectDestination(
fromBundlesAt destinationsDirectory: AbsolutePath?,
fileSystem: FileSystem,
matching selector: String,
hostTriple: Triple,
observabilityScope: ObservabilityScope
) throws -> Destination {
guard let destinationsDirectory = destinationsDirectory else {
throw StringError(
"""
No cross-compilation destinations directory found, specify one
with `experimental-destinations-path` option.
""")
}

let validBundles = try DestinationsBundle.getAllValidBundles(
destinationsDirectory: destinationsDirectory,
fileSystem: fileSystem,
observabilityScope: observabilityScope
)

guard !validBundles.isEmpty else {
throw StringError(
"No valid cross-compilation destination bundles found at \(destinationsDirectory)."
)
}

guard var selectedDestination = validBundles.selectDestination(
matching: selector,
hostTriple: hostTriple,
observabilityScope: observabilityScope
) else {
throw StringError(
"""
No cross-compilation destination found matching query `\(selector)` and host triple
`\(hostTriple.tripleString)`. Use `swift package experimental-destination list` command to see
available destinations.
"""
)
}

selectedDestination.extraFlags.swiftCompilerFlags += [
"-tools-directory", selectedDestination.toolchainBinDir.pathString,
]

if let sdkDirPath = selectedDestination.sdkRootDir?.pathString {
selectedDestination.extraFlags.swiftCompilerFlags += [
"-sdk", sdkDirPath,
]
}

return selectedDestination
}

/// Parses metadata of an `.artifactbundle` and validates it as a bundle containing
/// cross-compilation destinations.
/// - Parameters:
Expand All @@ -34,7 +132,7 @@ public struct DestinationsBundle {
/// - observabilityScope: observability scope to log validation warnings.
/// - Returns: Validated `DestinationsBundle` containing validated `Destination` values for
/// each artifact and its variants.
public static func parseAndValidate(
private static func parseAndValidate(
bundlePath: AbsolutePath,
fileSystem: FileSystem,
observabilityScope: ObservabilityScope
Expand Down Expand Up @@ -104,3 +202,74 @@ private extension ArtifactsArchiveMetadata {
return result
}
}

extension Array where Element == DestinationsBundle {
/// Select destinations matching a given query and host triple from a `self` array of available destinations.
/// - Parameters:
/// - query: either an artifact ID or target triple to filter with.
/// - hostTriple: triple of the host building with these destinations.
/// - observabilityScope: observability scope to log warnings about multiple matches.
/// - Returns: `Destination` value matching `query` either by artifact ID or target triple, `nil` if none found.
public func selectDestination(
matching selector: String,
hostTriple: Triple,
observabilityScope: ObservabilityScope
) -> Destination? {
var matchedByID: (path: AbsolutePath, variant: DestinationsBundle.Variant)?
var matchedByTriple: (path: AbsolutePath, variant: DestinationsBundle.Variant)?

for bundle in self {
for (artifactID, variants) in bundle.artifacts {
for variant in variants {
guard variant.metadata.supportedTriples.contains(hostTriple) else {
continue
}

if artifactID == selector {
if let matchedByID = matchedByID {
observabilityScope.emit(warning:
"""
multiple destinations match ID `\(artifactID)` and host triple \(
hostTriple.tripleString
), selected one at \(
matchedByID.path.appending(component: matchedByID.variant.metadata.path)
)
"""
)
} else {
matchedByID = (bundle.path, variant)
}
}

if variant.destination.targetTriple?.tripleString == selector {
if let matchedByTriple = matchedByTriple {
observabilityScope.emit(warning:
"""
multiple destinations match target triple `\(selector)` and host triple \(
hostTriple.tripleString
), selected one at \(
matchedByTriple.path.appending(component: matchedByTriple.variant.metadata.path)
)
"""
)
} else {
matchedByTriple = (bundle.path, variant)
}
}
}
}
}

if let matchedByID = matchedByID, let matchedByTriple = matchedByTriple, matchedByID != matchedByTriple {
observabilityScope.emit(warning:
"""
multiple destinations match the query `\(selector)` and host triple \(
hostTriple.tripleString
), selected one at \(matchedByID.path.appending(component: matchedByID.variant.metadata.path))
"""
)
}

return matchedByID?.variant.destination ?? matchedByTriple?.variant.destination
}
}
2 changes: 1 addition & 1 deletion Sources/SPMTestSupport/Observability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public struct TestingObservability {
self.collector.hasWarnings
}

struct Collector: ObservabilityHandlerProvider, DiagnosticsHandler, CustomStringConvertible {
final class Collector: ObservabilityHandlerProvider, DiagnosticsHandler, CustomStringConvertible {
Copy link
Contributor Author

@MaxDesiatov MaxDesiatov Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm using ObservabilityScope in this PR, I'm taking an opportunity to make this a class, because diagnostics: ThreadSafeArrayStore is a class instance, so there's no actual value semantics here even with a struct.

var diagnosticsHandler: DiagnosticsHandler { return self }

let diagnostics: ThreadSafeArrayStore<Basics.Diagnostic>
Expand Down
13 changes: 8 additions & 5 deletions Sources/Workspace/Workspace+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,22 @@ extension Workspace {
/// Path to the Package.resolved file.
public var resolvedVersionsFile: AbsolutePath

/// Path to the local configuration directory
/// Path to the local configuration directory.
public var localConfigurationDirectory: AbsolutePath

/// Path to the shared configuration directory
/// Path to the shared configuration directory.
public var sharedConfigurationDirectory: AbsolutePath?

/// Path to the shared security directory
/// Path to the shared security directory.
public var sharedSecurityDirectory: AbsolutePath?

/// Path to the shared cache directory
/// Path to the shared cache directory.
public var sharedCacheDirectory: AbsolutePath?

/// Whether or not to emit a warning about the existence of deprecated configuration files
/// Path to the shared cross-compilation destinations directory.
public var sharedCrossCompilationDestinationsDirectory: AbsolutePath?

/// Whether or not to emit a warning about the existence of deprecated configuration files.
public var emitDeprecatedConfigurationWarning: Bool

// working directories
Expand Down
Loading