Skip to content

Commit 37efa0b

Browse files
authored
Merge pull request #75 from DougGregor/swiftpm-java-compile
Introduce a SwiftPM plugin to compile Java sources
2 parents b8ee12c + 4eb6cc8 commit 37efa0b

File tree

13 files changed

+382
-75
lines changed

13 files changed

+382
-75
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ all:
6363
$(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/Java2Swift:
6464
swift build
6565

66-
javakit-run: $(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/libExampleSwiftLibrary.$(LIB_SUFFIX)
67-
./gradlew Samples:JavaKitSampleApp:run
66+
javakit-run:
67+
cd Samples/JavaKitSampleApp && swift build && java -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java -Djava.library.path=.build/debug com.example.swift.JavaKitSampleMain
6868

6969
Java2Swift: $(BUILD_DIR)/debug/Java2Swift
7070

Package.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ let package = Package(
8686
targets: ["Java2SwiftTool"]
8787
),
8888

89+
// ==== Plugin for building Java code
90+
.plugin(
91+
name: "JavaCompilerPlugin",
92+
targets: [
93+
"JavaCompilerPlugin"
94+
]
95+
),
96+
8997
// ==== jextract-swift (extract Java accessors from Swift interface files)
9098

9199
.executable(
@@ -107,11 +115,6 @@ let package = Package(
107115

108116
// ==== Examples
109117

110-
.library(
111-
name: "JavaKitExample",
112-
type: .dynamic,
113-
targets: ["JavaKitExample"]
114-
),
115118
.library(
116119
name: "ExampleSwiftLibrary",
117120
type: .dynamic,
@@ -197,14 +200,12 @@ let package = Package(
197200
.linkedLibrary("jvm"),
198201
]
199202
),
200-
.target(
201-
name: "JavaKitExample",
202-
dependencies: ["JavaKit"],
203-
swiftSettings: [
204-
.swiftLanguageMode(.v5),
205-
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
206-
]
203+
204+
.plugin(
205+
name: "JavaCompilerPlugin",
206+
capability: .buildTool()
207207
),
208+
208209
.target(
209210
name: "ExampleSwiftLibrary",
210211
dependencies: [],
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 JavaCompilerBuildToolPlugin: BuildToolPlugin {
20+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
21+
guard let sourceModule = target.sourceModule else { return [] }
22+
23+
// Collect all of the Java source files within this target's sources.
24+
let javaFiles = sourceModule.sourceFiles.map { $0.url }.filter {
25+
$0.pathExtension == "java"
26+
}
27+
if javaFiles.isEmpty {
28+
return []
29+
}
30+
31+
// Note: Target doesn't have a directoryURL counterpart to directory,
32+
// so we cannot eliminate this deprecation warning.
33+
let sourceDir = target.directory.string
34+
35+
// The class files themselves will be generated into the build directory
36+
// for this target.
37+
let classFiles = javaFiles.map { sourceFileURL in
38+
let sourceFilePath = sourceFileURL.path
39+
guard sourceFilePath.starts(with: sourceDir) else {
40+
fatalError("Could not get relative path for source file \(sourceFilePath)")
41+
}
42+
43+
return URL(
44+
filePath: context.pluginWorkDirectoryURL.path
45+
).appending(path: "Java")
46+
.appending(path: String(sourceFilePath.dropFirst(sourceDir.count)))
47+
.deletingPathExtension()
48+
.appendingPathExtension("class")
49+
}
50+
51+
let javaHome = URL(filePath: findJavaHome())
52+
let javaClassFileURL = context.pluginWorkDirectoryURL
53+
.appending(path: "Java")
54+
return [
55+
.buildCommand(
56+
displayName: "Compiling \(javaFiles.count) Java files for target \(sourceModule.name) to \(javaClassFileURL)",
57+
executable: javaHome
58+
.appending(path: "bin")
59+
.appending(path: "javac"),
60+
arguments: javaFiles.map { $0.path(percentEncoded: false) } + [
61+
"-d", javaClassFileURL.path()
62+
],
63+
inputFiles: javaFiles,
64+
outputFiles: classFiles
65+
)
66+
]
67+
}
68+
}
69+
70+
// Note: the JAVA_HOME environment variable must be set to point to where
71+
// Java is installed, e.g.,
72+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
73+
func findJavaHome() -> String {
74+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
75+
return home
76+
}
77+
78+
// This is a workaround for envs (some IDEs) which have trouble with
79+
// picking up env variables during the build process
80+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
81+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
82+
if let lastChar = home.last, lastChar.isNewline {
83+
return String(home.dropLast())
84+
}
85+
86+
return home
87+
}
88+
89+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
90+
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ Sample apps are located in the `Samples/` directory, and they showcase full "rou
9595
To run a simple app showcasing a Swift process calling into a Java library you can run:
9696

9797
```bash
98-
./gradlew Samples:JavaKitSampleApp:run
98+
cd Samples/JavaKitSampleApp
99+
swift build
100+
java -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java -Djava.library.path=.build/debug com.example.swift.JavaKitSampleMain
99101
```
100102

101103
#### jextract (Java -> Swift)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import CompilerPluginSupport
5+
import PackageDescription
6+
7+
let package = Package(
8+
name: "JavaKitSampleApp",
9+
platforms: [
10+
.macOS(.v13),
11+
.iOS(.v13),
12+
.tvOS(.v13),
13+
.watchOS(.v6),
14+
.macCatalyst(.v13),
15+
],
16+
17+
products: [
18+
.library(
19+
name: "JavaKitExample",
20+
type: .dynamic,
21+
targets: ["JavaKitExample"]
22+
),
23+
],
24+
25+
dependencies: [
26+
.package(name: "swift-java", path: "../../")
27+
],
28+
29+
targets: [
30+
.target(
31+
name: "JavaKitExample",
32+
dependencies: [
33+
.product(name: "JavaKit", package: "swift-java")
34+
],
35+
swiftSettings: [
36+
.swiftLanguageMode(.v5)
37+
],
38+
plugins: [
39+
.plugin(name: "JavaCompilerPlugin", package: "swift-java")
40+
]
41+
),
42+
]
43+
)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 JavaKit
16+
import JavaRuntime
17+
18+
enum SwiftWrappedError: Error {
19+
case message(String)
20+
}
21+
22+
@JavaClass("com.example.swift.HelloSwift")
23+
struct HelloSwift {
24+
@JavaMethod
25+
init(environment: JNIEnvironment)
26+
27+
@JavaMethod
28+
func sayHelloBack(_ i: Int32) -> Double
29+
30+
@JavaMethod
31+
func greet(_ name: String)
32+
33+
@JavaMethod
34+
func doublesToStrings(doubles: [Double]) -> [String]
35+
36+
@JavaMethod
37+
func throwMessage(message: String) throws
38+
39+
@JavaField
40+
var value: Double
41+
42+
@JavaField
43+
var name: String
44+
45+
@ImplementsJava
46+
func sayHello(i: Int32, _ j: Int32) -> Int32 {
47+
print("Hello from Swift!")
48+
let answer = self.sayHelloBack(i + j)
49+
print("Swift got back \(answer) from Java")
50+
51+
print("We expect the above value to be the initial value, \(self.javaClass.initialValue)")
52+
53+
print("Updating Java field value to something different")
54+
self.value = 2.71828
55+
56+
let newAnswer = self.sayHelloBack(17)
57+
print("Swift got back updated \(newAnswer) from Java")
58+
59+
let newHello = HelloSwift(environment: javaEnvironment)
60+
print("Swift created a new Java instance with the value \(newHello.value)")
61+
62+
let name = newHello.name
63+
print("Hello to \(name)")
64+
newHello.greet("Swift 👋🏽 How's it going")
65+
66+
self.name = "a 🗑️-collected language"
67+
_ = self.sayHelloBack(42)
68+
69+
let strings = doublesToStrings(doubles: [3.14159, 2.71828])
70+
print("Converting doubles to strings: \(strings)")
71+
72+
// Try downcasting
73+
if let helloSub = self.as(HelloSubclass.self) {
74+
print("Hello from the subclass!")
75+
helloSub.greetMe()
76+
77+
assert(helloSub.super.value == 2.71828)
78+
} else {
79+
fatalError("Expected subclass here")
80+
}
81+
82+
// Check "is" behavior
83+
assert(newHello.is(HelloSwift.self))
84+
assert(!newHello.is(HelloSubclass.self))
85+
86+
// Create a new instance.
87+
let helloSubFromSwift = HelloSubclass(greeting: "Hello from Swift", environment: javaEnvironment)
88+
helloSubFromSwift.greetMe()
89+
90+
do {
91+
try throwMessage(message: "I am an error")
92+
} catch {
93+
print("Caught Java error: \(error)")
94+
}
95+
96+
return i * j
97+
}
98+
99+
@ImplementsJava
100+
func throwMessageFromSwift(message: String) throws -> String {
101+
throw SwiftWrappedError.message(message)
102+
}
103+
}
104+
105+
extension JavaClass<HelloSwift> {
106+
@JavaStaticField
107+
var initialValue: Double
108+
}
109+
110+
@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self)
111+
struct HelloSubclass {
112+
@JavaField
113+
var greeting: String
114+
115+
@JavaMethod
116+
func greetMe()
117+
118+
@JavaMethod
119+
init(greeting: String, environment: JNIEnvironment)
120+
}

Samples/JavaKitSampleApp/build.gradle

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)