Skip to content

Commit b43b85e

Browse files
authored
jextract: Support .swiftinterface files for --input-swift (#695)
1 parent 8b53d41 commit b43b85e

9 files changed

Lines changed: 163 additions & 27 deletions

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ let package = Package(
334334
.process("Resources")
335335
],
336336
swiftSettings: [
337-
.swiftLanguageMode(.v5)
337+
.swiftLanguageMode(.v5),
338+
.enableUpcomingFeature("BareSlashRegexLiterals"),
338339
],
339340
plugins: [
340341
.plugin(name: "_StaticBuildConfigPlugin")

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ extension FFMSwift2JavaGenerator {
9090
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
9191

9292
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
93-
let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift")
93+
let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift")
9494

9595
// Print file header before all type thunks
9696
printer.print(

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
9191
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {
9292
return nil
9393
}
94-
guard fileName.hasSuffix(".swift") else {
95-
return nil
94+
if fileName.hasSuffix(".swift") {
95+
return String(fileName.replacing(".swift", with: "+SwiftJava.swift"))
96+
} else if fileName.hasSuffix(".swiftinterface") {
97+
return String(fileName.replacing(".swiftinterface", with: "+SwiftJava.swift"))
9698
}
97-
return String(fileName.replacing(".swift", with: "+SwiftJava.swift"))
99+
return nil
98100
}
99101
)
100102
// Also include filtered-out files so SwiftPM gets the empty outputs it expects

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extension JNISwift2JavaGenerator {
9595
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
9696

9797
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
98-
let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift")
98+
let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift")
9999

100100
for ty in importedTypesForThisFile {
101101
logger.info("Printing Swift thunks for type: \(ty.effectiveJavaName.bold)")

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,12 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
9494
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {
9595
return nil
9696
}
97-
guard fileName.hasSuffix(".swift") else {
98-
return nil
97+
if fileName.hasSuffix(".swift") {
98+
return String(fileName.replacing(".swift", with: "+SwiftJava.swift"))
99+
} else if fileName.hasSuffix(".swiftinterface") {
100+
return String(fileName.replacing(".swiftinterface", with: "+SwiftJava.swift"))
99101
}
100-
return String(fileName.replacing(".swift", with: "+SwiftJava.swift"))
102+
return nil
101103
}
102104
)
103105
// Also include filtered-out files so SwiftPM gets the empty outputs it expects

Sources/JExtractSwiftLib/Swift2Java.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ public struct SwiftToJava {
5050
let inputPaths = inputSwift.split(separator: ",").map { URL(string: String($0))! }
5151
log.info("Input paths = \(inputPaths)")
5252

53-
let allFiles = collectAllFiles(suffix: ".swift", in: inputPaths, log: translator.log)
53+
var allFiles: OrderedSet<URL> = []
54+
for path in inputPaths {
55+
if path.isDirectory {
56+
allFiles.formUnion(collectAllFiles(suffix: ".swift", in: [path], log: translator.log))
57+
} else {
58+
allFiles.append(path)
59+
}
60+
}
5461

5562
let hasFilters =
5663
!(config.swiftFilterInclude ?? []).isEmpty || !(config.swiftFilterExclude ?? []).isEmpty

Tests/JExtractSwiftTests/IfConfigTests.swift

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ struct IfConfigTests {
7979
@Test
8080
func overrideWithStaticBuildConfigurationFile() throws {
8181
try withTemporaryFile(
82-
suffix: "json",
82+
extension: "json",
8383
contents: """
8484
{
8585
"attributes": [],
@@ -159,19 +159,3 @@ struct IfConfigTests {
159159
)
160160
}
161161
}
162-
163-
private func withTemporaryFile(
164-
suffix: String,
165-
contents: String = "",
166-
in tempDirectory: URL = FileManager.default.temporaryDirectory,
167-
_ perform: (URL) throws -> Void
168-
) throws {
169-
let tempFileName = "tmp_\(UUID().uuidString).\(suffix)"
170-
let tempFileURL = tempDirectory.appendingPathComponent(tempFileName)
171-
172-
try contents.write(to: tempFileURL, atomically: true, encoding: .utf8)
173-
defer {
174-
try? FileManager.default.removeItem(at: tempFileURL)
175-
}
176-
try perform(tempFileURL)
177-
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import JExtractSwiftLib
17+
import SwiftJavaConfigurationShared
18+
import Testing
19+
20+
@Suite
21+
struct InputSwiftTests {
22+
let fileManager = FileManager.default
23+
24+
@Test(arguments: [JExtractGenerationMode.jni, .ffm])
25+
func loadSwiftinterface(mode: JExtractGenerationMode) throws {
26+
let tempDirectory: URL = fileManager.temporaryDirectory
27+
.appending(path: "loadSwiftinterface-\(UUID())")
28+
let outSwiftURL = tempDirectory.appending(path: "swift")
29+
let outJavaURL = tempDirectory.appending(path: "java")
30+
31+
try withTemporaryFile(
32+
fileName: "MyDependent",
33+
extension: "swiftinterface",
34+
contents: """
35+
public struct Foo {}
36+
""",
37+
in: tempDirectory
38+
) { swiftInterfaceURL in
39+
var config = Configuration()
40+
config.mode = mode
41+
config.javaPackage = "com.example"
42+
config.inputSwiftDirectory = swiftInterfaceURL.absoluteURL.path()
43+
config.swiftModule = "MySwift"
44+
config.outputSwiftDirectory = outSwiftURL.absoluteURL.path()
45+
config.outputJavaDirectory = outJavaURL.absoluteURL.path()
46+
47+
try SwiftToJava(config: config, dependentConfigs: [])
48+
.run()
49+
}
50+
51+
let javaPackageRoot =
52+
outJavaURL
53+
.appending(path: "com")
54+
.appending(path: "example")
55+
let expectedSources: [URL] = [
56+
outSwiftURL.appending(path: "MySwiftModule+SwiftJava.swift"),
57+
outSwiftURL.appending(path: "MyDependent+SwiftJava.swift"),
58+
javaPackageRoot.appending(path: "Foo.java"),
59+
javaPackageRoot.appending(path: "MySwift.java"),
60+
]
61+
62+
for expectedSource in expectedSources {
63+
#expect(fileManager.fileExists(atPath: expectedSource.path()))
64+
}
65+
}
66+
67+
@Test(arguments: [JExtractGenerationMode.jni, .ffm])
68+
func loadEmptySwiftinterface(mode: JExtractGenerationMode) throws {
69+
let tempDirectory: URL = fileManager.temporaryDirectory
70+
.appending(path: "loadEmptySwiftinterface-\(UUID())")
71+
let outSwiftURL = tempDirectory.appending(path: "swift")
72+
let outJavaURL = tempDirectory.appending(path: "java")
73+
74+
try withTemporaryFile(
75+
fileName: "MyDependent",
76+
extension: "swiftinterface",
77+
contents: "",
78+
in: tempDirectory
79+
) { swiftInterfaceURL in
80+
var config = Configuration()
81+
config.mode = mode
82+
config.writeEmptyFiles = true
83+
config.javaPackage = "com.example"
84+
config.inputSwiftDirectory = swiftInterfaceURL.absoluteURL.path()
85+
config.swiftModule = "MySwift"
86+
config.outputSwiftDirectory = outSwiftURL.absoluteURL.path()
87+
config.outputJavaDirectory = outJavaURL.absoluteURL.path()
88+
89+
try SwiftToJava(config: config, dependentConfigs: [])
90+
.run()
91+
}
92+
93+
let javaPackageRoot =
94+
outJavaURL
95+
.appending(path: "com")
96+
.appending(path: "example")
97+
let expectedSources: [URL] = [
98+
outSwiftURL.appending(path: "MySwiftModule+SwiftJava.swift"),
99+
outSwiftURL.appending(path: "MyDependent+SwiftJava.swift"),
100+
javaPackageRoot.appending(path: "MySwift.java"),
101+
]
102+
103+
for expectedSource in expectedSources {
104+
#expect(fileManager.fileExists(atPath: expectedSource.path()))
105+
}
106+
}
107+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
func withTemporaryFile(
18+
fileName: String = "tmp_\(UUID().uuidString)",
19+
extension: String,
20+
contents: String = "",
21+
in tempDirectory: URL = FileManager.default.temporaryDirectory,
22+
_ perform: (URL) throws -> Void
23+
) throws {
24+
let tempFileName = "\(fileName).\(`extension`)"
25+
let tempFileURL = tempDirectory.appendingPathComponent(tempFileName)
26+
27+
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
28+
try contents.write(to: tempFileURL, atomically: true, encoding: .utf8)
29+
defer {
30+
try? FileManager.default.removeItem(at: tempFileURL)
31+
}
32+
try perform(tempFileURL)
33+
}

0 commit comments

Comments
 (0)