Skip to content

Commit 18a105a

Browse files
committed
Rework Java2Swift to operate entirely on Java2Swift.config files
Instead of having Java2Swift take in a list of classes on the command line, which the plugin synthesizes by reading a configuration file, and then emitting another almost-identical manifest file... rework the command-line interface to Java2Swift to operate entirely on the per-target Java2Swift.config files. This makes the plugin simpler, because it never has to look at build products for its dependencies. Change the `--jar-file` mode of Java2Swift to emit the configuration file but not translate any classes. This should be used as a tool, not a build step. Update the user guide to introduce JavaKit through the build tool.
1 parent 46a05ce commit 18a105a

File tree

13 files changed

+406
-384
lines changed

13 files changed

+406
-384
lines changed

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,19 @@ Java2Swift: $(BUILD_DIR)/debug/Java2Swift
7070

7171
generate-JavaKit: Java2Swift
7272
mkdir -p Sources/JavaKit/generated
73-
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKit -o Sources/JavaKit/generated java.lang.Object=JavaObject java.util.Enumeration java.lang.Throwable java.lang.Exception java.lang.RuntimeException java.lang.Error=JavaError
73+
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKit -o Sources/JavaKit/generated Sources/JavaKit/Java2Swift.config
7474

7575
generate-JavaKitReflection: Java2Swift generate-JavaKit
7676
mkdir -p Sources/JavaKitReflection/generated
77-
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitReflection --manifests Sources/JavaKit/generated/JavaKit.swift2java -o Sources/JavaKitReflection/generated java.lang.reflect.Method java.lang.reflect.Type java.lang.reflect.Constructor java.lang.reflect.Parameter java.lang.reflect.ParameterizedType java.lang.reflect.Executable java.lang.reflect.AnnotatedType java.lang.reflect.TypeVariable java.lang.reflect.WildcardType java.lang.reflect.GenericArrayType java.lang.reflect.AccessibleObject java.lang.annotation.Annotation java.lang.reflect.GenericDeclaration java.lang.reflect.Field
77+
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitReflection --depends-on JavaKit=Sources/JavaKit/Java2Swift.config -o Sources/JavaKitReflection/generated Sources/JavaKitReflection/Java2Swift.config
7878

7979
generate-JavaKitJar: Java2Swift generate-JavaKit
8080
mkdir -p Sources/JavaKitJar/generated
81-
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitJar --manifests Sources/JavaKit/generated/JavaKit.swift2java -o Sources/JavaKitJar/generated java.util.jar.Attributes java.util.jar.JarEntry java.util.jar.JarFile java.util.jar.JarInputStream java.util.jar.JarOutputStream java.util.jar.Manifest
81+
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitJar --depends-on JavaKit=Sources/JavaKit/Java2Swift.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/Java2Swift.config
8282

8383
generate-JavaKitNetwork: Java2Swift generate-JavaKit
8484
mkdir -p Sources/JavaKitNetwork/generated
85-
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitNetwork --manifests Sources/JavaKit/generated/JavaKit.swift2java -o Sources/JavaKitNetwork/generated java.net.URI java.net.URL java.net.URLClassLoader
85+
$(BUILD_DIR)/debug/Java2Swift --module-name JavaKitNetwork --depends-on JavaKit=Sources/JavaKit/Java2Swift.config -o Sources/JavaKitNetwork/generated Sources/JavaKitNetwork/Java2Swift.config
8686

8787
javakit-generate: generate-JavaKit generate-JavaKitReflection generate-JavaKitJar generate-JavaKitNetwork
8888

Plugins/Java2SwiftPlugin/Configuration.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
/// Configuration for the Java2Swift translation tool, provided on a per-target
1616
/// basis.
17+
///
18+
/// Note: there is a copy of this struct in the Java2Swift library. They
19+
/// must be kept in sync.
1720
struct Configuration: Codable {
1821
/// The Java class path that should be passed along to the Java2Swift tool.
1922
var classPath: String? = nil

Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import Foundation
1616
import PackagePlugin
1717

18+
fileprivate let Java2SwiftConfigFileName = "Java2Swift.config"
19+
1820
@main
1921
struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
2022
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
@@ -24,42 +26,39 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
2426
// so we cannot eliminate this deprecation warning.
2527
let sourceDir = target.directory.string
2628

27-
// Read a configuration file JavaKit.config from the target that provides
28-
// information needed to call Java2Swift.
29+
// The name of the configuration file JavaKit.config from the target for
30+
// which we are generating Swift wrappers for Java classes.
2931
let configFile = URL(filePath: sourceDir)
3032
.appending(path: "Java2Swift.config")
3133
let configData = try Data(contentsOf: configFile)
3234
let config = try JSONDecoder().decode(Configuration.self, from: configData)
3335

3436
/// Find the manifest files from other Java2Swift executions in any targets
3537
/// this target depends on.
36-
var manifestFiles: [URL] = []
37-
func searchForManifestFiles(in target: any Target) {
38+
var dependentConfigFiles: [(String, URL)] = []
39+
func searchForConfigFiles(in target: any Target) {
3840
let dependencyURL = URL(filePath: target.directory.string)
3941

40-
// Look for a checked-in manifest file.
41-
let generatedManifestURL = dependencyURL
42-
.appending(path: "generated")
43-
.appending(path: "\(target.name).swift2java")
44-
let generatedManifestString = generatedManifestURL
42+
// Look for a config file within this target.
43+
let dependencyConfigURL = dependencyURL
44+
.appending(path: Java2SwiftConfigFileName)
45+
let dependencyConfigString = dependencyConfigURL
4546
.path(percentEncoded: false)
4647

47-
if FileManager.default.fileExists(atPath: generatedManifestString) {
48-
manifestFiles.append(generatedManifestURL)
48+
if FileManager.default.fileExists(atPath: dependencyConfigString) {
49+
dependentConfigFiles.append((target.name, dependencyConfigURL))
4950
}
50-
51-
// TODO: Look for a manifest file that was built by the plugin itself.
5251
}
5352

5453
// Process direct dependencies of this target.
5554
for dependency in target.dependencies {
5655
switch dependency {
5756
case .target(let target):
58-
searchForManifestFiles(in: target)
57+
searchForConfigFiles(in: target)
5958

6059
case .product(let product):
6160
for target in product.targets {
62-
searchForManifestFiles(in: target)
61+
searchForConfigFiles(in: target)
6362
}
6463

6564
@unknown default:
@@ -69,14 +68,7 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
6968

7069
// Process indirect target dependencies.
7170
for dependency in target.recursiveTargetDependencies {
72-
searchForManifestFiles(in: dependency)
73-
}
74-
75-
/// Determine the list of Java classes that will be translated into Swift,
76-
/// along with the names of the corresponding Swift types. This will be
77-
/// passed along to the Java2Swift tool.
78-
let classes = config.classes.sorted { (lhs, rhs) in
79-
lhs.0 < rhs.0
71+
searchForConfigFiles(in: dependency)
8072
}
8173

8274
let outputDirectory = context.pluginWorkDirectoryURL
@@ -86,27 +78,26 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
8678
"--module-name", sourceModule.name,
8779
"--output-directory", outputDirectory.path(percentEncoded: false),
8880
]
89-
if let classPath = config.classPath {
90-
arguments += ["-cp", classPath]
91-
}
92-
arguments += manifestFiles.flatMap { manifestFile in
93-
[ "--manifests", manifestFile.path(percentEncoded: false) ]
94-
}
95-
arguments += classes.map { (javaClassName, swiftName) in
96-
"\(javaClassName)=\(swiftName)"
81+
arguments += dependentConfigFiles.flatMap { moduleAndConfigFile in
82+
let (moduleName, configFile) = moduleAndConfigFile
83+
return [
84+
"--depends-on",
85+
"\(moduleName)=\(configFile.path(percentEncoded: false))"
86+
]
9787
}
88+
arguments.append(configFile.path(percentEncoded: false))
9889

9990
/// Determine the set of Swift files that will be emitted by the Java2Swift
10091
/// tool.
101-
let outputSwiftFiles = classes.map { (javaClassName, swiftName) in
92+
let outputSwiftFiles = config.classes.map { (javaClassName, swiftName) in
10293
outputDirectory.appending(path: "\(swiftName).swift")
10394
} + [
10495
outputDirectory.appending(path: "\(sourceModule.name).swift2java")
10596
]
10697

10798
return [
10899
.buildCommand(
109-
displayName: "Wrapping \(classes.count) Java classes target \(sourceModule.name) in Swift",
100+
displayName: "Wrapping \(config.classes.count) Java classes target \(sourceModule.name) in Swift",
110101
executable: try context.tool(named: "Java2Swift").url,
111102
arguments: arguments,
112103
inputFiles: [ configFile ],

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 96 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,33 @@ struct JavaToSwift: ParsableCommand {
3131
@Option(help: "The name of the Swift module into which the resulting Swift types will be generated.")
3232
var moduleName: String
3333

34-
@Argument(
35-
help:
36-
"The Java classes to translate into Swift written with their canonical names (e.g., java.lang.Object). If the Swift name of the type should be different from simple name of the type, it can appended to the class name with '=<swift name>'."
37-
)
38-
var classes: [String] = []
39-
4034
@Option(
4135
help:
42-
"The Java-to-Swift module manifest files for any Swift module containing Swift types created to wrap Java classes."
36+
"A Java2Swift configuration file for a given Swift module name on which this module depends, e.g., JavaKitJar=Sources/JavaKitJar/Java2Swift.config. There should be one of these options for each Swift module that this module depends on (transitively) that contains wrapped Java sources."
4337
)
44-
var manifests: [String] = []
38+
var dependsOn: [String] = []
4539

46-
@Option(
40+
@Flag(
4741
help:
48-
"The Jar file from which the set of class names should be loaded, if the classes weren't explicitly listed."
42+
"Specifies that the input is a Jar file whose public classes will be loaded. The output of Java2Swift will be a configuration file (Java2Swift.config) that can be used as input to a subsequent Java2Swift invocation to generate wrappers for those public classes."
4943
)
50-
var jarFile: String? = nil
44+
var jarFile: Bool = false
5145

5246
@Option(
5347
name: [.customLong("cp"), .customLong("classpath")],
5448
help: "Class search path of directories and zip/jar files from which Java classes can be loaded."
5549
)
5650
var classpath: [String] = []
5751

58-
@Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files and manifest.")
52+
@Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the Java2Swift configuration file.")
5953
var outputDirectory: String = "."
6054

55+
@Argument(
56+
help:
57+
"The input file, which is either a Java2Swift configuration file or (if '-jar-file' was specified) a Jar file."
58+
)
59+
var input: String
60+
6161
mutating func run() throws {
6262
var vmOptions: [String] = []
6363
let classpath = classPathWithJarFile
@@ -76,62 +76,39 @@ struct JavaToSwift: ParsableCommand {
7676
environment: environment
7777
)
7878

79-
// Load all of the translation manifests this depends on.
80-
for manifest in manifests {
81-
try translator.loadTranslationManifest(from: URL(filePath: manifest))
82-
}
83-
84-
if jarFile == nil && classes.isEmpty {
85-
throw JavaToSwiftError.noClasses
86-
}
79+
// Load all of the configurations this depends on.
80+
for config in dependsOn {
81+
guard let equalLoc = config.firstIndex(of: "=") else {
82+
throw JavaToSwiftError.badConfigOption(config)
83+
}
8784

88-
// If we have a Jar file but no classes were listed, find all of the
89-
// classes in the Jar file.
90-
if let jarFileName = jarFile, classes.isEmpty {
91-
let jarFile = try JarFile(jarFileName, false, environment: environment)
92-
classes = jarFile.entries()!.compactMap { (entry) -> String? in
93-
guard entry.getName().hasSuffix(".class") else {
94-
return nil
95-
}
85+
let afterEqual = config.index(after: equalLoc)
86+
let swiftModuleName = String(config[..<equalLoc])
87+
let configFileName = String(config[afterEqual...])
9688

97-
// If any of the segments of the Java name start with a number, it's a
98-
// local class that cannot be mapped into Swift.
99-
for segment in entry.getName().split(separator: "$") {
100-
if segment.starts(with: /\d/) {
101-
return nil
102-
}
103-
}
89+
try translator.loadDependentConfiguration(
90+
forSwiftModule: swiftModuleName,
91+
from: URL(filePath: configFileName)
92+
)
93+
}
10494

105-
return String(entry.getName().replacing("/", with: ".")
106-
.dropLast(".class".count))
107-
}
95+
// Jar file mode: read a Jar file and output a configuration.
96+
if jarFile {
97+
return try emitConfiguration(forJarFile: input, environment: environment)
10898
}
10999

100+
// Load the configuration file.
101+
let config = try translator.readConfiguration(from: URL(filePath: input))
102+
110103
// Load all of the requested classes.
111104
let classLoader = URLClassLoader(
112-
try classPathWithJarFile.map { try URL("file://\($0)", environment: environment) },
105+
try classPathWithJarFile.map {
106+
try URL("file://\($0)", environment: environment)
107+
},
113108
environment: environment
114109
)
115110
var javaClasses: [JavaClass<JavaObject>] = []
116-
for javaClassNameOpt in self.classes {
117-
// Determine the Java class name and its resulting Swift name.
118-
let javaClassName: String
119-
let swiftName: String
120-
if let equalLoc = javaClassNameOpt.firstIndex(of: "=") {
121-
let afterEqual = javaClassNameOpt.index(after: equalLoc)
122-
javaClassName = String(javaClassNameOpt[..<equalLoc])
123-
swiftName = String(javaClassNameOpt[afterEqual...])
124-
} else {
125-
if let dotLoc = javaClassNameOpt.lastIndex(of: ".") {
126-
let afterDot = javaClassNameOpt.index(after: dotLoc)
127-
swiftName = String(javaClassNameOpt[afterDot...])
128-
} else {
129-
swiftName = javaClassNameOpt
130-
}
131-
132-
javaClassName = javaClassNameOpt
133-
}
134-
111+
for (javaClassName, swiftName) in config.classes {
135112
guard let javaClass = try classLoader.loadClass(javaClassName) else {
136113
print("warning: could not find Java class '\(javaClassName)'")
137114
continue
@@ -167,18 +144,15 @@ struct JavaToSwift: ParsableCommand {
167144
description: "Java class '\(javaClass.getCanonicalName())' translation"
168145
)
169146
}
170-
171-
// Translation manifest.
172-
let manifestFileName = "\(moduleName).swift2java"
173-
let manifestContents = try translator.encodeTranslationManifest()
174-
try writeContents(manifestContents, to: manifestFileName, description: "translation manifest")
175147
}
176148

177149
/// Return the class path augmented with the Jar file, if there is one.
178150
var classPathWithJarFile: [String] {
179-
guard let jarFile else { return classpath }
151+
if jarFile {
152+
return [input] + classpath
153+
}
180154

181-
return [jarFile] + classpath
155+
return classpath
182156
}
183157

184158
func writeContents(_ contents: String, to filename: String, description: String) throws {
@@ -196,17 +170,71 @@ struct JavaToSwift: ParsableCommand {
196170
)
197171
print(" done.")
198172
}
173+
174+
func emitConfiguration(forJarFile jarFileName: String, environment: JNIEnvironment) throws {
175+
var configuration = Configuration(
176+
classPath: classPathWithJarFile.joined(separator: ":")
177+
)
178+
let jarFile = try JarFile(jarFileName, false, environment: environment)
179+
for entry in jarFile.entries()! {
180+
// We only look at class files in the Jar file.
181+
guard entry.getName().hasSuffix(".class") else {
182+
continue
183+
}
184+
185+
// If any of the segments of the Java name start with a number, it's a
186+
// local class that cannot be mapped into Swift.
187+
for segment in entry.getName().split(separator: "$") {
188+
if segment.starts(with: /\d/) {
189+
continue
190+
}
191+
}
192+
193+
let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
194+
.dropLast(".class".count))
195+
configuration.classes[javaCanonicalName] =
196+
javaCanonicalName.defaultSwiftNameForJavaClass
197+
}
198+
199+
// Encode the configuration.
200+
let encoder = JSONEncoder()
201+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
202+
var contents = String(data: try encoder.encode(configuration), encoding: .utf8)!
203+
contents.append("\n")
204+
205+
// Write the file.
206+
try writeContents(
207+
contents,
208+
to: URL(filePath: outputDirectory)
209+
.appending(path: "Java2Swift.config")
210+
.path(percentEncoded: false),
211+
description: "Java2Swift configuration file"
212+
)
213+
}
199214
}
200215

201216
enum JavaToSwiftError: Error {
202-
case noClasses
217+
case badConfigOption(String)
203218
}
204219

205220
extension JavaToSwiftError: CustomStringConvertible {
206221
var description: String {
207222
switch self {
208-
case .noClasses:
209-
"no classes to translate: either list Java classes or provide a Jar file"
223+
case .badConfigOption(_):
224+
"configuration option must be of the form '<swift module name>=<path to config file>"
210225
}
211226
}
212227
}
228+
229+
extension String {
230+
/// For a String that's of the form java.util.Vector, return the "Vector"
231+
/// part.
232+
fileprivate var defaultSwiftNameForJavaClass: String {
233+
if let dotLoc = lastIndex(of: ".") {
234+
let afterDot = index(after: dotLoc)
235+
return String(self[afterDot...])
236+
}
237+
238+
return self
239+
}
240+
}

0 commit comments

Comments
 (0)