diff --git a/Package.swift b/Package.swift index 376ed040..24f83236 100644 --- a/Package.swift +++ b/Package.swift @@ -334,7 +334,8 @@ let package = Package( .process("Resources") ], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .enableUpcomingFeature("BareSlashRegexLiterals"), ], plugins: [ .plugin(name: "_StaticBuildConfigPlugin") diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 40cab064..1bc52d6e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -90,7 +90,7 @@ extension FFMSwift2JavaGenerator { .sorted(by: { $0.qualifiedName < $1.qualifiedName }) let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" - let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") // Print file header before all type thunks printer.print( diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index b711f9b7..3c762d10 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -91,10 +91,12 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { return nil } - guard fileName.hasSuffix(".swift") else { - return nil + if fileName.hasSuffix(".swift") { + return String(fileName.replacing(".swift", with: "+SwiftJava.swift")) + } else if fileName.hasSuffix(".swiftinterface") { + return String(fileName.replacing(".swiftinterface", with: "+SwiftJava.swift")) } - return String(fileName.replacing(".swift", with: "+SwiftJava.swift")) + return nil } ) // Also include filtered-out files so SwiftPM gets the empty outputs it expects diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 4f0fb6c3..8ca81f4f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -95,7 +95,7 @@ extension JNISwift2JavaGenerator { .sorted(by: { $0.qualifiedName < $1.qualifiedName }) let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" - let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") for ty in importedTypesForThisFile { logger.info("Printing Swift thunks for type: \(ty.effectiveJavaName.bold)") diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index beffe9fb..7fd4bd7b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -94,10 +94,12 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { return nil } - guard fileName.hasSuffix(".swift") else { - return nil + if fileName.hasSuffix(".swift") { + return String(fileName.replacing(".swift", with: "+SwiftJava.swift")) + } else if fileName.hasSuffix(".swiftinterface") { + return String(fileName.replacing(".swiftinterface", with: "+SwiftJava.swift")) } - return String(fileName.replacing(".swift", with: "+SwiftJava.swift")) + return nil } ) // Also include filtered-out files so SwiftPM gets the empty outputs it expects diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 909b671f..4db5ea22 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -50,7 +50,14 @@ public struct SwiftToJava { let inputPaths = inputSwift.split(separator: ",").map { URL(string: String($0))! } log.info("Input paths = \(inputPaths)") - let allFiles = collectAllFiles(suffix: ".swift", in: inputPaths, log: translator.log) + var allFiles: OrderedSet = [] + for path in inputPaths { + if path.isDirectory { + allFiles.formUnion(collectAllFiles(suffix: ".swift", in: [path], log: translator.log)) + } else { + allFiles.append(path) + } + } let hasFilters = !(config.swiftFilterInclude ?? []).isEmpty || !(config.swiftFilterExclude ?? []).isEmpty diff --git a/Tests/JExtractSwiftTests/IfConfigTests.swift b/Tests/JExtractSwiftTests/IfConfigTests.swift index 6eddc08b..32545c3d 100644 --- a/Tests/JExtractSwiftTests/IfConfigTests.swift +++ b/Tests/JExtractSwiftTests/IfConfigTests.swift @@ -79,7 +79,7 @@ struct IfConfigTests { @Test func overrideWithStaticBuildConfigurationFile() throws { try withTemporaryFile( - suffix: "json", + extension: "json", contents: """ { "attributes": [], @@ -159,19 +159,3 @@ struct IfConfigTests { ) } } - -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) -} diff --git a/Tests/JExtractSwiftTests/InputSwiftTests.swift b/Tests/JExtractSwiftTests/InputSwiftTests.swift new file mode 100644 index 00000000..edbb7ac5 --- /dev/null +++ b/Tests/JExtractSwiftTests/InputSwiftTests.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@Suite +struct InputSwiftTests { + let fileManager = FileManager.default + + @Test(arguments: [JExtractGenerationMode.jni, .ffm]) + func loadSwiftinterface(mode: JExtractGenerationMode) throws { + let tempDirectory: URL = fileManager.temporaryDirectory + .appending(path: "loadSwiftinterface-\(UUID())") + let outSwiftURL = tempDirectory.appending(path: "swift") + let outJavaURL = tempDirectory.appending(path: "java") + + try withTemporaryFile( + fileName: "MyDependent", + extension: "swiftinterface", + contents: """ + public struct Foo {} + """, + in: tempDirectory + ) { swiftInterfaceURL in + var config = Configuration() + config.mode = mode + config.javaPackage = "com.example" + config.inputSwiftDirectory = swiftInterfaceURL.absoluteURL.path() + config.swiftModule = "MySwift" + config.outputSwiftDirectory = outSwiftURL.absoluteURL.path() + config.outputJavaDirectory = outJavaURL.absoluteURL.path() + + try SwiftToJava(config: config, dependentConfigs: []) + .run() + } + + let javaPackageRoot = + outJavaURL + .appending(path: "com") + .appending(path: "example") + let expectedSources: [URL] = [ + outSwiftURL.appending(path: "MySwiftModule+SwiftJava.swift"), + outSwiftURL.appending(path: "MyDependent+SwiftJava.swift"), + javaPackageRoot.appending(path: "Foo.java"), + javaPackageRoot.appending(path: "MySwift.java"), + ] + + for expectedSource in expectedSources { + #expect(fileManager.fileExists(atPath: expectedSource.path())) + } + } + + @Test(arguments: [JExtractGenerationMode.jni, .ffm]) + func loadEmptySwiftinterface(mode: JExtractGenerationMode) throws { + let tempDirectory: URL = fileManager.temporaryDirectory + .appending(path: "loadEmptySwiftinterface-\(UUID())") + let outSwiftURL = tempDirectory.appending(path: "swift") + let outJavaURL = tempDirectory.appending(path: "java") + + try withTemporaryFile( + fileName: "MyDependent", + extension: "swiftinterface", + contents: "", + in: tempDirectory + ) { swiftInterfaceURL in + var config = Configuration() + config.mode = mode + config.writeEmptyFiles = true + config.javaPackage = "com.example" + config.inputSwiftDirectory = swiftInterfaceURL.absoluteURL.path() + config.swiftModule = "MySwift" + config.outputSwiftDirectory = outSwiftURL.absoluteURL.path() + config.outputJavaDirectory = outJavaURL.absoluteURL.path() + + try SwiftToJava(config: config, dependentConfigs: []) + .run() + } + + let javaPackageRoot = + outJavaURL + .appending(path: "com") + .appending(path: "example") + let expectedSources: [URL] = [ + outSwiftURL.appending(path: "MySwiftModule+SwiftJava.swift"), + outSwiftURL.appending(path: "MyDependent+SwiftJava.swift"), + javaPackageRoot.appending(path: "MySwift.java"), + ] + + for expectedSource in expectedSources { + #expect(fileManager.fileExists(atPath: expectedSource.path())) + } + } +} diff --git a/Tests/JExtractSwiftTests/Utils.swift b/Tests/JExtractSwiftTests/Utils.swift new file mode 100644 index 00000000..985e48f8 --- /dev/null +++ b/Tests/JExtractSwiftTests/Utils.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 + +func withTemporaryFile( + fileName: String = "tmp_\(UUID().uuidString)", + extension: String, + contents: String = "", + in tempDirectory: URL = FileManager.default.temporaryDirectory, + _ perform: (URL) throws -> Void +) throws { + let tempFileName = "\(fileName).\(`extension`)" + let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) + + try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) + try contents.write(to: tempFileURL, atomically: true, encoding: .utf8) + defer { + try? FileManager.default.removeItem(at: tempFileURL) + } + try perform(tempFileURL) +}