Skip to content

Commit f041f10

Browse files
authored
Merge pull request #77 from DougGregor/java2swift-plugin
Add a SwiftPM build plugin to generate Swift wrappers for Java classes
2 parents a548b45 + a9fdd61 commit f041f10

File tree

8 files changed

+220
-5
lines changed

8 files changed

+220
-5
lines changed

.licenseignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.swift-format
66
.github/*
77
*.md
8+
**/*.config
89
CONTRIBUTORS.txt
910
LICENSE.txt
1011
NOTICE.txt

Package.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ let package = Package(
5656
targets: ["JavaKit"]
5757
),
5858

59+
.library(
60+
name: "JavaRuntime",
61+
targets: ["JavaRuntime"]
62+
),
63+
5964
.library(
6065
name: "JavaKitJar",
6166
targets: ["JavaKitReflection"]
@@ -83,7 +88,7 @@ let package = Package(
8388

8489
.executable(
8590
name: "Java2Swift",
86-
targets: ["Java2SwiftTool"]
91+
targets: ["Java2Swift"]
8792
),
8893

8994
// ==== Plugin for building Java code
@@ -94,6 +99,14 @@ let package = Package(
9499
]
95100
),
96101

102+
// ==== Plugin for wrapping Java classes in Swift
103+
.plugin(
104+
name: "Java2SwiftPlugin",
105+
targets: [
106+
"Java2SwiftPlugin"
107+
]
108+
),
109+
97110
// ==== jextract-swift (extract Java accessors from Swift interface files)
98111

99112
.executable(
@@ -206,6 +219,14 @@ let package = Package(
206219
capability: .buildTool()
207220
),
208221

222+
.plugin(
223+
name: "Java2SwiftPlugin",
224+
capability: .buildTool(),
225+
dependencies: [
226+
"Java2Swift"
227+
]
228+
),
229+
209230
.target(
210231
name: "ExampleSwiftLibrary",
211232
dependencies: [],
@@ -251,7 +272,7 @@ let package = Package(
251272
),
252273

253274
.executableTarget(
254-
name: "Java2SwiftTool",
275+
name: "Java2Swift",
255276
dependencies: [
256277
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
257278
.product(name: "SwiftSyntax", package: "swift-syntax"),
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
/// Configuration for the Java2Swift translation tool, provided on a per-target
16+
/// basis.
17+
struct Configuration: Codable {
18+
/// The Java class path that should be passed along to the Java2Swift tool.
19+
var classPath: String? = nil
20+
21+
/// The Java classes that should be translated to Swift. The keys are
22+
/// canonical Java class names (e.g., java.util.Vector) and the values are
23+
/// the corresponding Swift names (e.g., JavaVector). If the value is `nil`,
24+
/// then the Java class name will be used for the Swift name, too.
25+
var classes: [String: String?] = [:]
26+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
import Foundation
16+
import PackagePlugin
17+
18+
@main
19+
struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
20+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
21+
guard let sourceModule = target.sourceModule else { return [] }
22+
23+
// Note: Target doesn't have a directoryURL counterpart to directory,
24+
// so we cannot eliminate this deprecation warning.
25+
let sourceDir = target.directory.string
26+
27+
// Read a configuration file JavaKit.config from the target that provides
28+
// information needed to call Java2Swift.
29+
let configFile = URL(filePath: sourceDir)
30+
.appending(path: "Java2Swift.config")
31+
let configData = try Data(contentsOf: configFile)
32+
let config = try JSONDecoder().decode(Configuration.self, from: configData)
33+
34+
/// Find the manifest files from other Java2Swift executions in any targets
35+
/// this target depends on.
36+
var manifestFiles: [URL] = []
37+
func searchForManifestFiles(in target: any Target) {
38+
let dependencyURL = URL(filePath: target.directory.string)
39+
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
45+
.path(percentEncoded: false)
46+
47+
if FileManager.default.fileExists(atPath: generatedManifestString) {
48+
manifestFiles.append(generatedManifestURL)
49+
}
50+
51+
// TODO: Look for a manifest file that was built by the plugin itself.
52+
}
53+
54+
// Process direct dependencies of this target.
55+
for dependency in target.dependencies {
56+
switch dependency {
57+
case .target(let target):
58+
searchForManifestFiles(in: target)
59+
60+
case .product(let product):
61+
for target in product.targets {
62+
searchForManifestFiles(in: target)
63+
}
64+
65+
@unknown default:
66+
break
67+
}
68+
}
69+
70+
// Process indirect target dependencies.
71+
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.map { (javaClassName, swiftName) in
79+
(javaClassName, swiftName ?? javaClassName.defaultSwiftNameForJavaClass)
80+
}.sorted { (lhs, rhs) in
81+
lhs.0 < rhs.0
82+
}
83+
84+
let outputDirectory = context.pluginWorkDirectoryURL
85+
.appending(path: "generated")
86+
87+
var arguments: [String] = [
88+
"--module-name", sourceModule.name,
89+
"--output-directory", outputDirectory.path(percentEncoded: false),
90+
]
91+
if let classPath = config.classPath {
92+
arguments += ["-cp", classPath]
93+
}
94+
arguments += manifestFiles.flatMap { manifestFile in
95+
[ "--manifests", manifestFile.path(percentEncoded: false) ]
96+
}
97+
arguments += classes.map { (javaClassName, swiftName) in
98+
"\(javaClassName)=\(swiftName)"
99+
}
100+
101+
/// Determine the set of Swift files that will be emitted by the Java2Swift
102+
/// tool.
103+
let outputSwiftFiles = classes.map { (javaClassName, swiftName) in
104+
outputDirectory.appending(path: "\(swiftName).swift")
105+
} + [
106+
outputDirectory.appending(path: "\(sourceModule.name).swift2java")
107+
]
108+
109+
return [
110+
.buildCommand(
111+
displayName: "Wrapping \(classes.count) Java classes target \(sourceModule.name) in Swift",
112+
executable: try context.tool(named: "Java2Swift").url,
113+
arguments: arguments,
114+
inputFiles: [ configFile ],
115+
outputFiles: outputSwiftFiles
116+
)
117+
]
118+
}
119+
}
120+
121+
// Note: the JAVA_HOME environment variable must be set to point to where
122+
// Java is installed, e.g.,
123+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
124+
func findJavaHome() -> String {
125+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
126+
return home
127+
}
128+
129+
// This is a workaround for envs (some IDEs) which have trouble with
130+
// picking up env variables during the build process
131+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
132+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
133+
if let lastChar = home.last, lastChar.isNewline {
134+
return String(home.dropLast())
135+
}
136+
137+
return home
138+
}
139+
140+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
141+
}
142+
143+
extension String {
144+
/// For a String that's of the form java.util.Vector, return the "Vector"
145+
/// part.
146+
fileprivate var defaultSwiftNameForJavaClass: String {
147+
if let dotLoc = lastIndex(of: ".") {
148+
let afterDot = index(after: dotLoc)
149+
return String(self[afterDot...])
150+
}
151+
152+
return self
153+
}
154+
}

Samples/JavaKitSampleApp/Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ let package = Package(
3030
.target(
3131
name: "JavaKitExample",
3232
dependencies: [
33-
.product(name: "JavaKit", package: "swift-java")
33+
.product(name: "JavaKit", package: "swift-java"),
34+
.product(name: "JavaKitJar", package: "swift-java"),
3435
],
3536
swiftSettings: [
3637
.swiftLanguageMode(.v5)
3738
],
3839
plugins: [
40+
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
3941
.plugin(name: "JavaCompilerPlugin", package: "swift-java")
4042
]
4143
),
4244
]
43-
)
45+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"classes" : {
3+
"java.util.ArrayList" : "ArrayList"
4+
}
5+
}

Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import JavaKit
16-
import JavaRuntime
1716

1817
enum SwiftWrappedError: Error {
1918
case message(String)
@@ -118,3 +117,10 @@ struct HelloSubclass {
118117
@JavaMethod
119118
init(greeting: String, environment: JNIEnvironment)
120119
}
120+
121+
122+
func removeLast(arrayList: ArrayList<JavaClass<HelloSwift>>) {
123+
if let lastObject = arrayList.getLast() {
124+
_ = arrayList.remove(lastObject)
125+
}
126+
}

0 commit comments

Comments
 (0)