Skip to content

Introduce a SwiftPM plugin to compile Java sources #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ all:
$(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/Java2Swift:
swift build

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

Java2Swift: $(BUILD_DIR)/debug/Java2Swift

Expand Down
25 changes: 13 additions & 12 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ let package = Package(
targets: ["Java2SwiftTool"]
),

// ==== Plugin for building Java code
.plugin(
name: "JavaCompilerPlugin",
targets: [
"JavaCompilerPlugin"
]
),

// ==== jextract-swift (extract Java accessors from Swift interface files)

.executable(
Expand All @@ -107,11 +115,6 @@ let package = Package(

// ==== Examples

.library(
name: "JavaKitExample",
type: .dynamic,
targets: ["JavaKitExample"]
),
.library(
name: "ExampleSwiftLibrary",
type: .dynamic,
Expand Down Expand Up @@ -197,14 +200,12 @@ let package = Package(
.linkedLibrary("jvm"),
]
),
.target(
name: "JavaKitExample",
dependencies: ["JavaKit"],
swiftSettings: [
.swiftLanguageMode(.v5),
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
]

.plugin(
name: "JavaCompilerPlugin",
capability: .buildTool()
),

.target(
name: "ExampleSwiftLibrary",
dependencies: [],
Expand Down
90 changes: 90 additions & 0 deletions Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import PackagePlugin

@main
struct JavaCompilerBuildToolPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
guard let sourceModule = target.sourceModule else { return [] }

// Collect all of the Java source files within this target's sources.
let javaFiles = sourceModule.sourceFiles.map { $0.url }.filter {
$0.pathExtension == "java"
}
if javaFiles.isEmpty {
return []
}

// Note: Target doesn't have a directoryURL counterpart to directory,
// so we cannot eliminate this deprecation warning.
let sourceDir = target.directory.string

// The class files themselves will be generated into the build directory
// for this target.
let classFiles = javaFiles.map { sourceFileURL in
let sourceFilePath = sourceFileURL.path
guard sourceFilePath.starts(with: sourceDir) else {
fatalError("Could not get relative path for source file \(sourceFilePath)")
}

return URL(
filePath: context.pluginWorkDirectoryURL.path
).appending(path: "Java")
.appending(path: String(sourceFilePath.dropFirst(sourceDir.count)))
.deletingPathExtension()
.appendingPathExtension("class")
}

let javaHome = URL(filePath: findJavaHome())
let javaClassFileURL = context.pluginWorkDirectoryURL
.appending(path: "Java")
return [
.buildCommand(
displayName: "Compiling \(javaFiles.count) Java files for target \(sourceModule.name) to \(javaClassFileURL)",
executable: javaHome
.appending(path: "bin")
.appending(path: "javac"),
arguments: javaFiles.map { $0.path(percentEncoded: false) } + [
"-d", javaClassFileURL.path()
],
inputFiles: javaFiles,
outputFiles: classFiles
)
]
}
}

// Note: the JAVA_HOME environment variable must be set to point to where
// Java is installed, e.g.,
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
func findJavaHome() -> String {
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
return home
}

// This is a workaround for envs (some IDEs) which have trouble with
// picking up env variables during the build process
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
if let lastChar = home.last, lastChar.isNewline {
return String(home.dropLast())
}

return home
}

fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ Sample apps are located in the `Samples/` directory, and they showcase full "rou
To run a simple app showcasing a Swift process calling into a Java library you can run:

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

#### jextract (Java -> Swift)
Expand Down
43 changes: 43 additions & 0 deletions Samples/JavaKitSampleApp/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "JavaKitSampleApp",
platforms: [
.macOS(.v13),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macCatalyst(.v13),
],

products: [
.library(
name: "JavaKitExample",
type: .dynamic,
targets: ["JavaKitExample"]
),
],

dependencies: [
.package(name: "swift-java", path: "../../")
],

targets: [
.target(
name: "JavaKitExample",
dependencies: [
.product(name: "JavaKit", package: "swift-java")
],
swiftSettings: [
.swiftLanguageMode(.v5)
],
plugins: [
.plugin(name: "JavaCompilerPlugin", package: "swift-java")
]
),
]
)
120 changes: 120 additions & 0 deletions Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import JavaKit
import JavaRuntime

enum SwiftWrappedError: Error {
case message(String)
}

@JavaClass("com.example.swift.HelloSwift")
struct HelloSwift {
@JavaMethod
init(environment: JNIEnvironment)

@JavaMethod
func sayHelloBack(_ i: Int32) -> Double

@JavaMethod
func greet(_ name: String)

@JavaMethod
func doublesToStrings(doubles: [Double]) -> [String]

@JavaMethod
func throwMessage(message: String) throws

@JavaField
var value: Double

@JavaField
var name: String

@ImplementsJava
func sayHello(i: Int32, _ j: Int32) -> Int32 {
print("Hello from Swift!")
let answer = self.sayHelloBack(i + j)
print("Swift got back \(answer) from Java")

print("We expect the above value to be the initial value, \(self.javaClass.initialValue)")

print("Updating Java field value to something different")
self.value = 2.71828

let newAnswer = self.sayHelloBack(17)
print("Swift got back updated \(newAnswer) from Java")

let newHello = HelloSwift(environment: javaEnvironment)
print("Swift created a new Java instance with the value \(newHello.value)")

let name = newHello.name
print("Hello to \(name)")
newHello.greet("Swift 👋🏽 How's it going")

self.name = "a 🗑️-collected language"
_ = self.sayHelloBack(42)

let strings = doublesToStrings(doubles: [3.14159, 2.71828])
print("Converting doubles to strings: \(strings)")

// Try downcasting
if let helloSub = self.as(HelloSubclass.self) {
print("Hello from the subclass!")
helloSub.greetMe()

assert(helloSub.super.value == 2.71828)
} else {
fatalError("Expected subclass here")
}

// Check "is" behavior
assert(newHello.is(HelloSwift.self))
assert(!newHello.is(HelloSubclass.self))

// Create a new instance.
let helloSubFromSwift = HelloSubclass(greeting: "Hello from Swift", environment: javaEnvironment)
helloSubFromSwift.greetMe()

do {
try throwMessage(message: "I am an error")
} catch {
print("Caught Java error: \(error)")
}

return i * j
}

@ImplementsJava
func throwMessageFromSwift(message: String) throws -> String {
throw SwiftWrappedError.message(message)
}
}

extension JavaClass<HelloSwift> {
@JavaStaticField
var initialValue: Double
}

@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self)
struct HelloSubclass {
@JavaField
var greeting: String

@JavaMethod
func greetMe()

@JavaMethod
init(greeting: String, environment: JNIEnvironment)
}
60 changes: 0 additions & 60 deletions Samples/JavaKitSampleApp/build.gradle

This file was deleted.

Loading
Loading