diff --git a/Package.swift b/Package.swift index ce340154a..73ac97446 100644 --- a/Package.swift +++ b/Package.swift @@ -118,12 +118,12 @@ let package = Package( ], dependencies: [ swiftJavaJNICoreDep, - .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax", from: "603.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), .package(url: "https://github.com/apple/swift-log", from: "1.2.0"), .package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), // primarily for ordered collections - .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.2.1", traits: ["SubprocessFoundation"]), + .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0", traits: ["SubprocessFoundation"]), // Benchmarking .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), @@ -320,6 +320,7 @@ let package = Package( dependencies: [ .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftLexicalLookup", package: "swift-syntax"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), @@ -331,6 +332,25 @@ let package = Package( ], swiftSettings: [ .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "_StaticBuildConfigPlugin") + ] + ), + + .executableTarget( + name: "StaticBuildConfigPluginExecutable", + dependencies: [ + .product(name: "Subprocess", package: "swift-subprocess"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), + ] + ), + + .plugin( + name: "_StaticBuildConfigPlugin", + capability: .buildTool(), + dependencies: [ + "StaticBuildConfigPluginExecutable" ] ), diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 9c2f48a5c..dd5d2f2b7 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -46,7 +46,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // The name of the configuration file SwiftJava.config from the target for // which we are generating Swift wrappers for Java classes. let configFile = sourceDir.appending(path: "swift-java.config") - let configuration = try readConfiguration(sourceDir: sourceDir) + let configuration = try readConfiguration(configPath: configFile) // We use the the usual maven-style structure of "src/[generated|main|test]/java/..." // that is common in JVM ecosystem @@ -71,6 +71,13 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc. ] + if let staticBuildConfig = configuration?.staticBuildConfigurationFile { + guard let resolvedURL = URL(string: staticBuildConfig, relativeTo: configFile) else { + fatalError("Could not resolve 'staticBuildConfigurationFile' url: \(staticBuildConfig)") + } + arguments += ["--static-build-config", resolvedURL.absoluteURL.path(percentEncoded: false)] + } + let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in let (moduleName, configFile) = moduleAndConfigFile return [ diff --git a/Plugins/_StaticBuildConfigPlugin/_StaticBuildConfigPlugin.swift b/Plugins/_StaticBuildConfigPlugin/_StaticBuildConfigPlugin.swift new file mode 100644 index 000000000..4816f59c6 --- /dev/null +++ b/Plugins/_StaticBuildConfigPlugin/_StaticBuildConfigPlugin.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +struct _StaticBuildConfigPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: any Target) async throws -> [Command] { + let outSwift = context.pluginWorkDirectoryURL.appending(path: "StaticBuildConfiguration+embedded.swift") + return [ + .buildCommand( + displayName: "Run -print-static-build-config", + executable: try context.tool(named: "StaticBuildConfigPluginExecutable").url, + arguments: [outSwift.absoluteURL.path(percentEncoded: false)], + environment: [:], + inputFiles: [], + outputFiles: [outSwift.absoluteURL] + ) + ] + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index 86ed42115..4b05971ec 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -229,8 +229,10 @@ extension DeclSyntaxProtocol { } else { "var" } + case .unexpectedCodeDecl(let node): + node.trimmedDescription case .usingDecl(let node): - node.nameForDebug + node.trimmedDescription } } diff --git a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift b/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift new file mode 100644 index 000000000..cdca43554 --- /dev/null +++ b/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftIfConfig +import SwiftSyntax + +/// A default, fixed build configuration during static analysis for interface extraction. +struct JExtractDefaultBuildConfiguration: BuildConfiguration { + static let shared = JExtractDefaultBuildConfiguration() + + private var base: StaticBuildConfiguration + + init() { + let decoder = JSONDecoder() + do { + base = try decoder.decode(StaticBuildConfiguration.self, from: StaticBuildConfiguration.embedded) + } catch { + fatalError("Embedded StaticBuildConfiguration is broken! data: \(String(data: StaticBuildConfiguration.embedded, encoding: .utf8) ?? "")") + } + } + + func isCustomConditionSet(name: String) throws -> Bool { + base.isCustomConditionSet(name: name) + } + + func hasFeature(name: String) throws -> Bool { + base.hasFeature(name: name) + } + + func hasAttribute(name: String) throws -> Bool { + base.hasAttribute(name: name) + } + + func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + try base.canImport(importPath: importPath, version: version) + } + + func isActiveTargetOS(name: String) throws -> Bool { + true + } + + func isActiveTargetArchitecture(name: String) throws -> Bool { + true + } + + func isActiveTargetEnvironment(name: String) throws -> Bool { + true + } + + func isActiveTargetRuntime(name: String) throws -> Bool { + true + } + + func isActiveTargetPointerAuthentication(name: String) throws -> Bool { + true + } + + func isActiveTargetObjectFormat(name: String) throws -> Bool { + true + } + + var targetPointerBitWidth: Int { + base.targetPointerBitWidth + } + + var targetAtomicBitWidths: [Int] { + base.targetAtomicBitWidths + } + + var endianness: Endianness { + base.endianness + } + + var languageVersion: VersionTuple { + base.languageVersion + } + + var compilerVersion: VersionTuple { + base.compilerVersion + } +} + +extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration { + static var jextractDefault: JExtractDefaultBuildConfiguration { + .shared + } +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 5a1b2da24..909b671f4 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -34,7 +34,6 @@ public struct SwiftToJava { } let translator = Swift2JavaTranslator(config: config) - translator.log.logLevel = config.logLevel ?? .info let log = translator.log if config.javaPackage == nil || config.javaPackage!.isEmpty { diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index a5d96a1af..106247a48 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -14,6 +14,7 @@ import Foundation import SwiftBasicFormat +import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftParser @@ -27,6 +28,9 @@ public final class Swift2JavaTranslator { let config: Configuration + /// The build configuration used to resolve #if conditional compilation blocks. + let buildConfig: any BuildConfiguration + /// The name of the Swift module being translated. let swiftModuleName: String @@ -70,6 +74,19 @@ public final class Swift2JavaTranslator { self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) self.config = config self.swiftModuleName = swiftModule + + if let staticBuildConfigPath = config.staticBuildConfigurationFile { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath)) + let decoder = JSONDecoder() + self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)") + } catch { + fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") + } + } else { + self.buildConfig = .jextractDefault + } } } @@ -152,6 +169,7 @@ extension Swift2JavaTranslator { moduleName: self.swiftModuleName, inputs + [dependenciesSource], config: self.config, + buildConfig: self.buildConfig, log: self.log, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 1a583b1d5..c6390bc14 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -70,7 +71,8 @@ final class Swift2JavaVisitor { self.visit(subscriptDecl: node, in: parent) case .enumCaseDecl(let node): self.visit(enumCaseDecl: node, in: parent) - + case .ifConfigDecl(let node): + self.visit(ifConfigDecl: node, in: parent, sourceFilePath: sourceFilePath) default: break } @@ -366,6 +368,30 @@ final class Swift2JavaVisitor { } } + private func visit( + ifConfigDecl node: IfConfigDeclSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { + let (clause, _) = node.activeClause(in: translator.buildConfig) + if let clause, let elements = clause.elements { + switch elements { + case .statements(let codeBlock): + for codeItem in codeBlock { + if let declNode = codeItem.item.as(DeclSyntax.self) { + self.visit(decl: declNode, in: parent, sourceFilePath: sourceFilePath) + } + } + case .decls(let memberBlock): + for memberItem in memberBlock { + self.visit(decl: memberItem.decl, in: parent, sourceFilePath: sourceFilePath) + } + default: + break + } + } + } + private func importAccessor( from node: DeclSyntax, in typeContext: ImportedNominalType?, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 895a0a1b8..f8e5927c9 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftIfConfig import SwiftSyntax struct SwiftParsedModuleSymbolTableBuilder { @@ -23,6 +24,9 @@ struct SwiftParsedModuleSymbolTableBuilder { /// Imported modules to resolve type syntax. let importedModules: [String: SwiftModuleSymbolTable] + /// The build configuration used to resolve #if conditional compilation blocks. + let buildConfig: any BuildConfiguration + /// Extension decls their extended type hasn't been resolved. var unresolvedExtensions: [ExtensionDeclSyntax] @@ -31,6 +35,7 @@ struct SwiftParsedModuleSymbolTableBuilder { requiredAvailablityOfModuleWithName: String? = nil, alternativeModules: SwiftModuleSymbolTable.AlternativeModuleNamesData? = nil, importedModules: [String: SwiftModuleSymbolTable], + buildConfig: any BuildConfiguration = .jextractDefault, log: Logger? = nil ) { self.log = log @@ -40,6 +45,7 @@ struct SwiftParsedModuleSymbolTableBuilder { alternativeModules: alternativeModules ) self.importedModules = importedModules + self.buildConfig = buildConfig self.unresolvedExtensions = [] } @@ -56,17 +62,25 @@ extension SwiftParsedModuleSymbolTableBuilder { ) { // Find top-level type declarations. for statement in sourceFile.statements { - // We only care about declarations. - guard case .decl(let decl) = statement.item else { - continue - } + self.handle(codeBlockItem: statement.item, sourceFilePath: sourceFilePath) + } + } - if let nominalTypeNode = decl.asNominal { - self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalTypeNode, parent: nil) - } - if let extensionNode = decl.as(ExtensionDeclSyntax.self) { - self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath) - } + mutating func handle( + codeBlockItem node: CodeBlockItemSyntax.Item, + sourceFilePath: String + ) { + // We only care about declarations. + guard case .decl(let decl) = node else { + return + } + + if let nominalTypeNode = decl.asNominal { + self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalTypeNode, parent: nil) + } else if let extensionNode = decl.as(ExtensionDeclSyntax.self) { + self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath) + } else if let ifConfigNode = decl.as(IfConfigDeclSyntax.self) { + self.handle(ifConfig: ifConfigNode, sourceFilePath: sourceFilePath) } } @@ -152,6 +166,23 @@ extension SwiftParsedModuleSymbolTableBuilder { return true } + mutating func handle( + ifConfig node: IfConfigDeclSyntax, + sourceFilePath: String + ) { + let (clause, _) = node.activeClause(in: buildConfig) + if let clause, let elements = clause.elements { + switch elements { + case .statements(let codeBlock): + for codeItem in codeBlock { + self.handle(codeBlockItem: codeItem.item, sourceFilePath: sourceFilePath) + } + default: + break + } + } + } + /// Finalize the symbol table and return it. mutating func finalize() -> SwiftModuleSymbolTable { // Handle the unresolved extensions. diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift index d1a5d9e01..5ba824c49 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -69,6 +70,7 @@ extension SwiftSymbolTable { moduleName: String, _ inputFiles: some Collection, config: Configuration?, + buildConfig: any BuildConfiguration = .jextractDefault, log: Logger, ) -> SwiftSymbolTable { @@ -104,6 +106,7 @@ extension SwiftSymbolTable { var stubBuilder = SwiftParsedModuleSymbolTableBuilder( moduleName: stubModuleName, importedModules: ["Swift": importedModules["Swift"]!], + buildConfig: buildConfig, ) stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift") let stubModule = stubBuilder.finalize() @@ -122,6 +125,7 @@ extension SwiftSymbolTable { var builder = SwiftParsedModuleSymbolTableBuilder( moduleName: moduleName, importedModules: importedModules, + buildConfig: buildConfig, log: log, ) // First, register top-level and nested nominal types to the symbol table. diff --git a/Sources/StaticBuildConfigPluginExecutable/StaticBuildConfigPluginExecutable.swift b/Sources/StaticBuildConfigPluginExecutable/StaticBuildConfigPluginExecutable.swift new file mode 100644 index 000000000..3c7479653 --- /dev/null +++ b/Sources/StaticBuildConfigPluginExecutable/StaticBuildConfigPluginExecutable.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Subprocess +import SwiftIfConfig + +@main struct StaticBuildConfigPluginExecutable { + static func main() async throws { + let args = CommandLine.arguments + guard args.count > 1 else { + print("Usage: \(args[0]) ") + return + } + let dst = URL(fileURLWithPath: args[1]) + + let data = try await loadStaticBuildConfig() + let template = #""" + import Foundation + import SwiftIfConfig + + extension StaticBuildConfiguration { + static var embedded: Data { + Data(#"\#(data)"#.utf8) + } + } + """# + try template.write(to: dst, atomically: true, encoding: .utf8) + } + + static func loadStaticBuildConfig() async throws -> String { + #if compiler(>=6.3) + let result = try await run( + .name("swift"), + arguments: ["frontend", "-print-static-build-config", "-target", "aarch64-unknown-linux-gnu"], + output: .string(limit: 65536), + error: .string(limit: 65536) + ) + if let error = result.standardError, !error.isEmpty { + fatalError(error) + } + return result.standardOutput ?? "" + #else + #if compiler(>=6.2) + let configuration = StaticBuildConfiguration( + languageVersion: .init(components: [5, 10]), + compilerVersion: .init(components: [6, 2]) + ) + #else + let configuration = StaticBuildConfiguration( + languageVersion: .init(components: [5, 10]), + compilerVersion: .init(components: [6, 1]) + ) + #endif + let encoder = JSONEncoder() + return String(data: try encoder.encode(configuration), encoding: .utf8) ?? "" + #endif + } +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 502996c47..24beabaa2 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -132,6 +132,10 @@ public struct Configuration: Codable { /// ``` public var specialize: [String: SpecializationConfigEntry]? + /// If set, use this JSON file as the static build configuration for jextract. + /// This allows users to provide a custom StaticBuildConfiguration for #if resolution. + public var staticBuildConfigurationFile: String? + // ==== wrap-java --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index c320bedc6..4a681d866 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -464,3 +464,40 @@ public final class FishBox ... { > NOTE: Currently no helpers are available to convert between unspecialized types to specialized ones, but this can be offered > as additional `box.as(FishBox.class)` conversion methods in the future. + +### Evaluating `#if` + +In jextract, `#if` branches are evaluated using [SwiftIfConfig](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md). +The evaluation parameters are fixed; for example, the `os` expression always evaluates to true, so in the following case the value of the variable will be `Linux`. + +```swift +#if os(Linux) +let os = "Linux" +#elseif os(Android) +let os = "Android" +#else +let os = "Other" +#endif +``` + +If you want the above situation to be evaluated as `Android`, you can override the evaluation parameters. +First, obtain a [StaticBuildConfiguration](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/StaticBuildConfiguration.swift) with the following command and save it to a file. +(Adjust `-target` to match the environment you want to build for. This command is available from Swift 6.3.) + +```sh +swift frontend -print-static-build-config -target aarch64-unknown-linux-android28 > static-build-config.json +``` + +Then pass the path to that file when running jextract. + +- When using the jextract command: `--static-build-config ` +- When configuring via `swift-java.config`: + ```json + { + ... + "staticBuildConfigurationFile": "" // Relative path from `swift-java.config` + } + ``` + +As a result, jextract will evaluate `os` as `Android`. + diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 93dc860fc..b1789cc38 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -133,6 +133,12 @@ extension SwiftJava { @Option(help: "If specified, only generate bindings for this single Swift type name") var singleType: String? + + @Option( + help: + "Path to a JSON file containing a StaticBuildConfiguration. Used to resolve #if conditional compilation blocks." + ) + var staticBuildConfig: String? } } @@ -155,6 +161,7 @@ extension SwiftJava.JExtractCommand { configure(&config.swiftFilterInclude, append: self.filterInclude) configure(&config.swiftFilterExclude, append: self.filterExclude) configure(&config.singleType, overrideWith: self.singleType) + configure(&config.staticBuildConfigurationFile, overrideWith: self.staticBuildConfig) try checkModeCompatibility(config: config) diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index f0e1e72a6..1f10b2aad 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -87,9 +87,17 @@ func assertOutput( let sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column) for notExpectedChunk in notExpectedChunks { + let outputNotContainsNotExpectedChunk = !output.contains(notExpectedChunk) #expect( - !output.contains(notExpectedChunk), - "Output must not contain:\n\(notExpectedChunk)\n\nGot output:\n\(output)", + outputNotContainsNotExpectedChunk, + """ + \("error: Output must not contain not expected chunk!".red) + ==== Not Expected output ----------------------------------------------- + \(notExpectedChunk.yellow) + ==== Got output ---------------------------------------------------- + \(output) + ==== --------------------------------------------------------------- + """, sourceLocation: sourceLocation ) } diff --git a/Tests/JExtractSwiftTests/IfConfigTests.swift b/Tests/JExtractSwiftTests/IfConfigTests.swift new file mode 100644 index 000000000..6eddc08bf --- /dev/null +++ b/Tests/JExtractSwiftTests/IfConfigTests.swift @@ -0,0 +1,177 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +#if compiler(>=6.3) +private let has6_3compilerFeature = true +#else +private let has6_3compilerFeature = false +#endif + +@Suite +struct IfConfigTests { + @Test + func evaluateIfConfigs() throws { + try assertOutput( + input: """ + #if os(Android) + public func androidFunc() + #else + public func notAndroidFunc() + #endif + + #if canImport(Foundation) + public struct CanImport { + } + #else + public struct CannotImport { + } + #endif + + #if DEBUG + public struct IsDebug { + } + #else + public struct IsNotDebug { + #if os(Linux) + public var linuxVar: Int + #elseif os(iOS) || os(macOS) + public var iOSorMacOSVar: Int + #else + #error("unsupported platform") + #endif + } + #endif + """, + .ffm, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public static void androidFunc()", + "public final class CannotImport", + "public final class IsNotDebug", + "public long getLinuxVar()", + ], + notExpectedChunks: [ + "public static void notAndroidFunc()", + "public final class CanImport", + "public final class IsDebug", + "public long getIOSorMacOSVar() ", + ] + ) + } + + @Test + func overrideWithStaticBuildConfigurationFile() throws { + try withTemporaryFile( + suffix: "json", + contents: """ + { + "attributes": [], + "compilerVersion": { + "components": [6, 3] + }, + "customConditions": [ + "DEBUG" + ], + "endianness": "little", + "features": [], + "languageMode": { + "components": [5, 10] + }, + "targetArchitectures": [], + "targetAtomicBitWidths": [], + "targetEnvironments": [], + "targetOSs": [], + "targetObjectFileFormats": [], + "targetPointerAuthenticationSchemes": [], + "targetPointerBitWidth": 64, + "targetRuntimes": [] + } + """ + ) { staticBuildConfigFile in + var config = Configuration() + config.staticBuildConfigurationFile = staticBuildConfigFile.absoluteURL.path(percentEncoded: false) + + try assertOutput( + input: """ + #if DEBUG + public struct IsDebug {} + #else + public struct IsNotDebug {} + #endif + """, + config: config, + .ffm, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class IsDebug" + ], + notExpectedChunks: [ + "public final class IsNotDebug" + ] + ) + } + } + + @Test(.enabled(if: has6_3compilerFeature)) + func swiftinterfaceCommonPattern() throws { + try assertOutput( + input: """ + public enum Foundation { + public struct URL {} + } + + public struct AppStore : Swift.Sendable, Swift.Codable { + public var storeURL: Foundation.URL? + #if compiler(>=5.3) && $NonescapableTypes + public init(storeURL: Foundation.URL?) + #endif + public func encode(to encoder: any Swift.Encoder) throws + public init(from decoder: any Swift.Decoder) throws + } + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_AppStore__00024init__J") + public func Java_com_example_swift_AppStore__00024init__J(environment: UnsafeMutablePointer!, thisClass: jclass, storeURL: jlong) -> jlong + """ + ] + ) + } +} + +private func withTemporaryFile( + suffix: String, + contents: String = "", + in tempDirectory: URL = FileManager.default.temporaryDirectory, + _ perform: (URL) throws -> Void +) throws { + let tempFileName = "tmp_\(UUID().uuidString).\(suffix)" + let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) + + try contents.write(to: tempFileURL, atomically: true, encoding: .utf8) + defer { + try? FileManager.default.removeItem(at: tempFileURL) + } + try perform(tempFileURL) +}