Skip to content

Commit 019c50d

Browse files
committed
Properly handle nested directories of Swift Sources in jextract
1 parent c98dd8a commit 019c50d

File tree

10 files changed

+151
-80
lines changed

10 files changed

+151
-80
lines changed

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
9393
$0.pathExtension == "swift"
9494
}
9595

96-
// Output Swift files are just Java filename based converted to Swift files one-to-one
96+
// Output files are flattened filenames of the inputs, with the appended +SwiftJava suffix.
9797
var outputSwiftFiles: [URL] = swiftFiles.compactMap { sourceFileURL in
9898
guard sourceFileURL.isFileURL else {
9999
return nil as URL?
@@ -104,7 +104,6 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
104104
fatalError("Could not get relative path for source file \(sourceFilePath)")
105105
}
106106
let outputURL = outputSwiftDirectory
107-
.appending(path: String(sourceFilePath.dropFirst(sourceDir.count).dropLast(sourceFileURL.lastPathComponent.count + 1)))
108107

109108
let inputFileName = sourceFileURL.deletingPathExtension().lastPathComponent
110109
return outputURL.appending(path: "\(inputFileName)+SwiftJava.swift")

Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
175175

176176
let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'"
177177
log("Prepared: \(displayName)")
178+
179+
for f in outputSwiftFiles {
180+
log("Swift output file: \(f)")
181+
}
178182
commands += [
179183
.buildCommand(
180184
displayName: displayName,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
public final class SwiftTypeInSubDirectory {
16+
public init() {}
17+
18+
public func hello() -> Int {
19+
12
20+
}
21+
}

Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/SwiftTypeInSubDirectoryTest.java

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,47 +24,14 @@
2424

2525
import static org.junit.jupiter.api.Assertions.*;
2626

27-
public class MySwiftClassTest {
28-
29-
void checkPaths(Throwable throwable) {
30-
var paths = SwiftLibraries.getJavaLibraryPath().split(":");
31-
for (var path : paths) {
32-
Stream.of(new File(path).listFiles())
33-
.filter(file -> !file.isDirectory())
34-
.forEach((file) -> {
35-
System.out.println(" - " + file.getPath());
36-
});
37-
}
38-
39-
throw new RuntimeException(throwable);
40-
}
27+
public class SwiftTypeInSubDirectoryTest {
4128

4229
@Test
4330
void test_MySwiftClass_voidMethod() {
44-
try(var arena = AllocatingSwiftArena.ofConfined()) {
45-
MySwiftClass o = MySwiftClass.init(12, 42, arena);
46-
o.voidMethod();
47-
} catch (Throwable throwable) {
48-
checkPaths(throwable);
49-
}
50-
}
51-
52-
@Test
53-
void test_MySwiftClass_makeIntMethod() {
54-
try(var arena = AllocatingSwiftArena.ofConfined()) {
55-
MySwiftClass o = MySwiftClass.init(12, 42, arena);
56-
var got = o.makeIntMethod();
57-
assertEquals(12, got);
58-
}
59-
}
60-
61-
@Test
62-
@Disabled // TODO: Need var mangled names in interfaces
63-
void test_MySwiftClass_property_len() {
64-
try(var arena = AllocatingSwiftArena.ofConfined()) {
65-
MySwiftClass o = MySwiftClass.init(12, 42, arena);
66-
var got = o.getLen();
67-
assertEquals(12, got);
31+
try (var arena = AllocatingSwiftArena.ofConfined()) {
32+
SwiftTypeInSubDirectory o = SwiftTypeInSubDirectory.init(arena);
33+
var num = o.hello();
34+
assertEquals(12, num);
6835
}
6936
}
7037

Sources/JExtractSwiftLib/CodePrinter.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
import Foundation
1616

17-
let PATH_SEPARATOR = "/" // TODO: Windows
17+
#if os(Windows)
18+
let PATH_SEPARATOR = "\\"
19+
#else
20+
let PATH_SEPARATOR = "/"
21+
#endif
1822

1923
public struct CodePrinter {
2024
var contents: String = ""

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,15 @@ extension FFMSwift2JavaGenerator {
2222
}
2323

2424
package func writeSwiftExpectedEmptySources() throws {
25-
let pendingFileCount = self.expectedOutputSwiftFiles.count
25+
let pendingFileCount = self.expectedOutputSwiftFileNames.count
2626
guard pendingFileCount > 0 else {
2727
return // no need to write any empty files, yay
2828
}
2929

30-
print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/")
30+
log.info("[swift-java] Write empty [\(self.expectedOutputSwiftFileNames.count)] 'expected' files in: \(swiftOutputDirectory)/")
3131

32-
for expectedFileName in self.expectedOutputSwiftFiles {
33-
log.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)")
34-
32+
for expectedFileName in self.expectedOutputSwiftFileNames {
33+
log.info("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)")
3534

3635
var printer = CodePrinter()
3736
printer.print("// Empty file generated on purpose")
@@ -55,7 +54,7 @@ extension FFMSwift2JavaGenerator {
5554
javaPackagePath: nil,
5655
filename: moduleFilename) {
5756
log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))")
58-
self.expectedOutputSwiftFiles.remove(moduleFilename)
57+
self.expectedOutputSwiftFileNames.remove(moduleFilename)
5958
}
6059
} catch {
6160
log.warning("Failed to write to Swift thunks: \(moduleFilename)")
@@ -93,7 +92,9 @@ extension FFMSwift2JavaGenerator {
9392
javaPackagePath: nil,
9493
filename: filename) {
9594
log.info("Done writing Swift thunks to: \(outputFile.absoluteString)")
96-
self.expectedOutputSwiftFiles.remove(filename)
95+
// log.info("REMOVE FROM: \(expectedOutputSwiftFileNames)")
96+
// log.info("REMOVE FROM THE: \(filename)")
97+
self.expectedOutputSwiftFileNames.remove(filename)
9798
}
9899
} catch {
99100
log.warning("Failed to write to Swift thunks: \(filename), error: \(error)")

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
3939

4040
/// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet,
4141
/// and write an empty file for those.
42-
var expectedOutputSwiftFiles: Set<String>
42+
///
43+
/// Since Swift files in SwiftPM builds needs to be unique, we use this fact to flatten paths into plain names here.
44+
/// For uniqueness checking "did we write this file already", just checking the name should be sufficient.
45+
var expectedOutputSwiftFileNames: Set<String>
4346

4447
package init(
4548
config: Configuration,
@@ -57,19 +60,26 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
5760
self.javaOutputDirectory = javaOutputDirectory
5861
self.lookupContext = translator.lookupContext
5962

60-
// If we are forced to write empty files, construct the expected outputs
63+
// If we are forced to write empty files, construct the expected outputs.
64+
// It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway.
6165
if translator.config.writeEmptyFiles ?? false {
62-
self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in
63-
guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else {
66+
self.expectedOutputSwiftFileNames = Set(translator.inputs.compactMap { (input) -> String? in
67+
// guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else {
68+
// return nil
69+
// }
70+
// return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
71+
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {
6472
return nil
6573
}
66-
67-
return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
74+
guard fileName.hasSuffix(".swift") else {
75+
return nil
76+
}
77+
return String(fileName.replacing(".swift", with: "+SwiftJava.swift"))
6878
})
69-
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")
70-
self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift")
79+
self.expectedOutputSwiftFileNames.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")
80+
self.expectedOutputSwiftFileNames.insert("Foundation+SwiftJava.swift")
7181
} else {
72-
self.expectedOutputSwiftFiles = []
82+
self.expectedOutputSwiftFileNames = []
7383
}
7484
}
7585

Sources/JExtractSwiftLib/Swift2Java.swift

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SwiftSyntax
1717
import SwiftSyntaxBuilder
1818
import SwiftJavaShared
1919
import SwiftJavaConfigurationShared
20+
import OrderedCollections
2021

2122
public struct SwiftToJava {
2223
let config: Configuration
@@ -34,6 +35,7 @@ public struct SwiftToJava {
3435

3536
let translator = Swift2JavaTranslator(config: config)
3637
translator.log.logLevel = config.logLevel ?? .info
38+
let log = translator.log
3739

3840
if config.javaPackage == nil || config.javaPackage!.isEmpty {
3941
translator.log.warning("Configured java package is '', consider specifying concrete package for generated sources.")
@@ -43,28 +45,17 @@ public struct SwiftToJava {
4345
fatalError("Missing '--swift-input' directory!")
4446
}
4547

46-
translator.log.info("Input swift = \(inputSwift)")
48+
log.info("Input swift = \(inputSwift)")
4749
let inputPaths = inputSwift.split(separator: ",").map { URL(string: String($0))! }
48-
translator.log.info("Input paths = \(inputPaths)")
49-
50-
var allFiles: [URL] = []
51-
let fileManager = FileManager.default
52-
let log = translator.log
50+
log.info("Input paths = \(inputPaths)")
5351

54-
for path in inputPaths {
55-
log.info("Input path: \(path)")
56-
if isDirectory(url: path) {
57-
if let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: nil) {
58-
for case let fileURL as URL in enumerator {
59-
allFiles.append(fileURL)
60-
}
61-
}
62-
} else if path.isFileURL {
63-
allFiles.append(path)
64-
}
52+
let allFiles = collectAllFiles(suffix: ".swift", in: inputPaths, log: translator.log)
53+
for f in allFiles {
54+
log.warning("INPUT FILE: \(f) ->>>")
6555
}
6656

6757
// Register files to the translator.
58+
let fileManager = FileManager.default
6859
for file in allFiles {
6960
guard canExtract(from: file) else {
7061
continue
@@ -137,8 +128,82 @@ public struct SwiftToJava {
137128

138129
}
139130

140-
func isDirectory(url: URL) -> Bool {
141-
var isDirectory: ObjCBool = false
142-
_ = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
143-
return isDirectory.boolValue
131+
extension URL {
132+
var isDirectory: Bool {
133+
var isDir: ObjCBool = false
134+
_ = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
135+
return isDir.boolValue
136+
}
137+
}
138+
139+
/// Collect all files with given 'suffix', will explore directories recursively.
140+
public func collectAllFiles(suffix: String, in inputPaths: [URL], log: Logger) -> OrderedSet<URL> {
141+
guard !inputPaths.isEmpty else {
142+
return []
143+
}
144+
145+
let fileManager = FileManager.default
146+
var allFiles: OrderedSet<URL> = []
147+
allFiles.reserveCapacity(32) // rough guesstimate
148+
149+
let resourceKeys: [URLResourceKey] = [
150+
.isRegularFileKey,
151+
.isDirectoryKey,
152+
.nameKey
153+
]
154+
155+
for path in inputPaths {
156+
do {
157+
try collectFilesFromPath(
158+
path,
159+
suffix: suffix,
160+
fileManager: fileManager,
161+
resourceKeys: resourceKeys,
162+
into: &allFiles,
163+
log: log
164+
)
165+
} catch {
166+
log.trace("Failed to collect paths in: \(path), skipping.")
167+
}
168+
}
169+
170+
return allFiles
171+
}
172+
173+
private func collectFilesFromPath(
174+
_ path: URL,
175+
suffix: String,
176+
fileManager: FileManager,
177+
resourceKeys: [URLResourceKey],
178+
into allFiles: inout OrderedSet<URL>,
179+
log: Logger
180+
) throws {
181+
guard fileManager.fileExists(atPath: path.path) else {
182+
return
183+
}
184+
185+
if path.isDirectory {
186+
let enumerator = fileManager.enumerator(
187+
at: path,
188+
includingPropertiesForKeys: resourceKeys,
189+
options: [.skipsHiddenFiles],
190+
errorHandler: { url, error in
191+
return true
192+
})
193+
guard let enumerator else {
194+
return
195+
}
196+
197+
for case let fileURL as URL in enumerator {
198+
try? collectFilesFromPath(fileURL, suffix: suffix, fileManager: fileManager, resourceKeys: resourceKeys, into: &allFiles, log: log)
199+
}
200+
}
201+
202+
guard path.isFileURL else {
203+
return
204+
}
205+
guard path.lastPathComponent.hasSuffix(suffix) else {
206+
return
207+
}
208+
allFiles.append(path)
144209
}

Sources/JExtractSwiftLib/Swift2JavaTranslator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ extension Swift2JavaTranslator {
7878
}
7979

8080
package func add(filePath: String, text: String) {
81-
log.trace("Adding: \(filePath)")
81+
log.info("Adding: \(filePath)")
8282
let sourceFileSyntax = Parser.parse(source: text)
8383
self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath))
8484
}

Sources/SwiftJavaConfigurationShared/Configuration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable {
309309

310310
extension LogLevel {
311311
public init(from decoder: any Decoder) throws {
312-
var container = try decoder.singleValueContainer()
312+
let container = try decoder.singleValueContainer()
313313
let string = try container.decode(String.self)
314314
switch string {
315315
case "trace": self = .trace

0 commit comments

Comments
 (0)