From fe109107a6153bf6dd82d951bcd373e280315d6a Mon Sep 17 00:00:00 2001 From: Egor Zhdan Date: Thu, 16 Mar 2023 18:46:08 +0000 Subject: [PATCH] Add build setting for enabling C++ interop (#6276) This adds a build setting to the package manifest that enables Swift/C++ Interoperability for a given Swift target: ``` .interoperabilityMode(.Cxx, version: "swift-5.9") ``` This relies on the new Swift driver flag for versioned C++ interop (see https://github.com/apple/swift/pull/64088). rdar://106756067 --- .../PackageDescription/BuildSettings.swift | 35 +++++++++++++++++++ .../PackageLoading/ManifestJSONParser.swift | 17 +++++++++ Sources/PackageLoading/PackageBuilder.swift | 25 +++++++++++++ .../TargetBuildSettingDescription.swift | 9 ++++- .../ManifestSourceGeneration.swift | 8 +++++ Tests/BuildTests/BuildPlanTests.swift | 7 ++-- 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/Sources/PackageDescription/BuildSettings.swift b/Sources/PackageDescription/BuildSettings.swift index fc5b6bd7e9e..f1190884f0c 100644 --- a/Sources/PackageDescription/BuildSettings.swift +++ b/Sources/PackageDescription/BuildSettings.swift @@ -352,6 +352,41 @@ public struct SwiftSetting { return SwiftSetting( name: "enableExperimentalFeature", value: [name], condition: condition) } + + public enum InteroperabilityMode: String { + case C + case Cxx + } + + /// Enable Swift interoperability with a given language. + /// + /// This is useful for enabling Swift/C++ interoperability for a given + /// target. + /// + /// Enabling C++ interoperability mode might alter the way some existing + /// C/Objective-C APIs are imported. + /// + /// - Since: First available in PackageDescription 5.9. + /// + /// - Parameters: + /// - mode: The language mode, either C or Cxx. + /// - version: If Cxx language mode is used, the version of Swift/C++ + /// interoperability, otherwise `nil`. + /// - condition: A condition that restricts the application of the build + /// setting. + @available(_PackageDescription, introduced: 5.9) + public static func interoperabilityMode( + _ mode: InteroperabilityMode, + version: String? = nil, + _ condition: BuildSettingCondition? = nil + ) -> SwiftSetting { + var values: [String] = [mode.rawValue] + if let version = version { + values.append(version) + } + return SwiftSetting( + name: "interoperabilityMode", value: values, condition: condition) + } } /// A linker build setting. diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index df4d7b13ad5..02832cd4077 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -675,6 +675,23 @@ extension TargetBuildSettingDescription.Kind { throw InternalError("invalid (empty) build settings value") } return .linkedFramework(value) + case "interoperabilityMode": + guard let rawLang = values.first else { + throw InternalError("invalid (empty) build settings value") + } + guard let lang = TargetBuildSettingDescription.InteroperabilityMode(rawValue: rawLang) else { + throw InternalError("unknown interoperability mode: \(rawLang)") + } + if values.count > 2 { + throw InternalError("invalid build settings value") + } + let version: String? + if values.count == 2 { + version = values[1] + } else { + version = nil + } + return .interoperabilityMode(lang, version) case "enableUpcomingFeature": guard let value = values.first else { throw InternalError("invalid (empty) build settings value") diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index a5b1daecf6a..bbd6bce0578 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -977,6 +977,31 @@ public final class PackageBuilder { decl = .LINK_FRAMEWORKS } + case .interoperabilityMode(let lang, let version): + switch setting.tool { + case .c, .cxx, .linker: + throw InternalError("only Swift supports interoperability") + + case .swift: + decl = .OTHER_SWIFT_FLAGS + } + + if lang == .Cxx { + // `version` is the compatibility version of Swift/C++ interop, + // which is meant to preserve source compatibility for + // user projects while Swift/C++ interop is evolving. + // At the moment the only supported interop version is + // `swift-5.9` which is aligned with the version of + // Swift itself, but this might not always be the case + // in the future. + guard let version else { + throw InternalError("C++ interoperability requires a version (e.g. 'swift-5.9')") + } + values = ["-cxx-interoperability-mode=\(version)"] + } else { + values = [] + } + case .unsafeFlags(let _values): values = _values diff --git a/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift b/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift index a622d7eedfb..e7300cd3b42 100644 --- a/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift +++ b/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift @@ -21,6 +21,11 @@ public enum TargetBuildSettingDescription { case linker } + public enum InteroperabilityMode: String, Codable, Equatable, Sendable { + case C + case Cxx + } + /// The kind of the build setting, with associate configuration public enum Kind: Codable, Equatable, Sendable { case headerSearchPath(String) @@ -28,6 +33,8 @@ public enum TargetBuildSettingDescription { case linkedLibrary(String) case linkedFramework(String) + case interoperabilityMode(InteroperabilityMode, String?) + case enableUpcomingFeature(String) case enableExperimentalFeature(String) @@ -38,7 +45,7 @@ public enum TargetBuildSettingDescription { case .unsafeFlags(let flags): // If `.unsafeFlags` is used, but doesn't specify any flags, we treat it the same way as not specifying it. return !flags.isEmpty - case .headerSearchPath, .define, .linkedLibrary, .linkedFramework, .enableUpcomingFeature, .enableExperimentalFeature: + case .headerSearchPath, .define, .linkedLibrary, .linkedFramework, .interoperabilityMode, .enableUpcomingFeature, .enableExperimentalFeature: return false } } diff --git a/Sources/PackageModel/ManifestSourceGeneration.swift b/Sources/PackageModel/ManifestSourceGeneration.swift index 508893ab018..ac6123dc9fd 100644 --- a/Sources/PackageModel/ManifestSourceGeneration.swift +++ b/Sources/PackageModel/ManifestSourceGeneration.swift @@ -514,6 +514,12 @@ fileprivate extension SourceCodeFragment { params.append(SourceCodeFragment(from: condition)) } self.init(enum: setting.kind.name, subnodes: params) + case .interoperabilityMode(let lang, let version): + params.append(SourceCodeFragment(enum: lang.rawValue)) + if let version = version { + params.append(SourceCodeFragment(key: "version", string: version)) + } + self.init(enum: setting.kind.name, subnodes: params) case .unsafeFlags(let values): params.append(SourceCodeFragment(strings: values)) if let condition = setting.condition { @@ -666,6 +672,8 @@ extension TargetBuildSettingDescription.Kind { return "linkedFramework" case .unsafeFlags: return "unsafeFlags" + case .interoperabilityMode: + return "interoperabilityMode" case .enableUpcomingFeature: return "enableUpcomingFeature" case .enableExperimentalFeature: diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index eb367f19b3e..6991d141c41 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -3242,6 +3242,8 @@ final class BuildPlanTests: XCTestCase { .init(tool: .swift, kind: .define("RLINUX"), condition: .init(platformNames: ["linux"], config: "release")), .init(tool: .swift, kind: .define("DMACOS"), condition: .init(platformNames: ["macos"], config: "debug")), .init(tool: .swift, kind: .unsafeFlags(["-Isfoo", "-L", "sbar"])), + .init(tool: .swift, kind: .interoperabilityMode(.Cxx, "swift-5.9"), condition: .init(platformNames: ["linux"])), + .init(tool: .swift, kind: .interoperabilityMode(.Cxx, "swift-6.0"), condition: .init(platformNames: ["macos"])), .init(tool: .swift, kind: .enableUpcomingFeature("BestFeature")), .init(tool: .swift, kind: .enableUpcomingFeature("WorstFeature"), condition: .init(platformNames: ["macos"], config: "debug")) ] @@ -3250,6 +3252,7 @@ final class BuildPlanTests: XCTestCase { name: "exe", dependencies: ["bar"], settings: [ .init(tool: .swift, kind: .define("FOO")), + .init(tool: .swift, kind: .interoperabilityMode(.C, nil)), .init(tool: .linker, kind: .linkedLibrary("sqlite3")), .init(tool: .linker, kind: .linkedFramework("CoreData"), condition: .init(platformNames: ["macos"])), .init(tool: .linker, kind: .unsafeFlags(["-Ilfoo", "-L", "lbar"])), @@ -3308,7 +3311,7 @@ final class BuildPlanTests: XCTestCase { XCTAssertMatch(cbar, [.anySequence, "-DCCC=2", "-I\(A.appending(components: "Sources", "cbar", "Sources", "headers"))", "-I\(A.appending(components: "Sources", "cbar", "Sources", "cppheaders"))", "-Icfoo", "-L", "cbar", "-Icxxfoo", "-L", "cxxbar", .end]) let bar = try result.target(for: "bar").swiftTarget().compileArguments() - XCTAssertMatch(bar, [.anySequence, "-DLINUX", "-Isfoo", "-L", "sbar", "-enable-upcoming-feature", "BestFeature", .end]) + XCTAssertMatch(bar, [.anySequence, "-DLINUX", "-Isfoo", "-L", "sbar", "-cxx-interoperability-mode=swift-5.9", "-enable-upcoming-feature", "BestFeature", .end]) let exe = try result.target(for: "exe").swiftTarget().compileArguments() XCTAssertMatch(exe, [.anySequence, "-DFOO", .end]) @@ -3324,7 +3327,7 @@ final class BuildPlanTests: XCTestCase { XCTAssertMatch(cbar, [.anySequence, "-DCCC=2", "-I\(A.appending(components: "Sources", "cbar", "Sources", "headers"))", "-I\(A.appending(components: "Sources", "cbar", "Sources", "cppheaders"))", "-Icfoo", "-L", "cbar", "-Icxxfoo", "-L", "cxxbar", .end]) let bar = try result.target(for: "bar").swiftTarget().compileArguments() - XCTAssertMatch(bar, [.anySequence, "-DDMACOS", "-Isfoo", "-L", "sbar", "-enable-upcoming-feature", "BestFeature", "-enable-upcoming-feature", "WorstFeature", .end]) + XCTAssertMatch(bar, [.anySequence, "-DDMACOS", "-Isfoo", "-L", "sbar", "-cxx-interoperability-mode=swift-6.0", "-enable-upcoming-feature", "BestFeature", "-enable-upcoming-feature", "WorstFeature", .end]) let exe = try result.target(for: "exe").swiftTarget().compileArguments() XCTAssertMatch(exe, [.anySequence, "-DFOO", .end])