Skip to content

Converge command line tools into one: swift-java #248

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 8 commits into from
Jun 9, 2025
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
99 changes: 63 additions & 36 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import CompilerPluginSupport
import PackageDescription

import class Foundation.FileManager
import class Foundation.ProcessInfo
import Foundation

// Note: the JAVA_HOME environment variable must be set to point to where
// Java is installed, e.g.,
Expand All @@ -25,9 +24,53 @@ func findJavaHome() -> String {

return home
}

if let home = getJavaHomeFromLibexecJavaHome(),
!home.isEmpty {
return home
}

fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
}

/// On MacOS we can use the java_home tool as a fallback if we can't find JAVA_HOME environment variable.
func getJavaHomeFromLibexecJavaHome() -> String? {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/libexec/java_home")

// Check if the executable exists before trying to run it
guard FileManager.default.fileExists(atPath: task.executableURL!.path) else {
print("/usr/libexec/java_home does not exist")
return nil
}

let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe // Redirect standard error to the same pipe for simplicity

do {
try task.run()
task.waitUntilExit()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)

if task.terminationStatus == 0 {
return output
} else {
print("java_home terminated with status: \(task.terminationStatus)")
// Optionally, log the error output for debugging
if let errorOutput = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) {
print("Error output: \(errorOutput)")
}
return nil
}
} catch {
print("Error running java_home: \(error)")
return nil
}
}

let javaHome = findJavaHome()

let javaIncludePath = "\(javaHome)/include"
Expand Down Expand Up @@ -92,8 +135,8 @@ let package = Package(
),

.executable(
name: "Java2Swift",
targets: ["Java2Swift"]
name: "swift-java",
targets: ["SwiftJavaTool"]
),

// ==== Plugin for building Java code
Expand All @@ -106,19 +149,12 @@ let package = Package(

// ==== Plugin for wrapping Java classes in Swift
.plugin(
name: "Java2SwiftPlugin",
name: "SwiftJavaPlugin",
targets: [
"Java2SwiftPlugin"
"SwiftJavaPlugin"
]
),

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

.executable(
name: "jextract-swift",
targets: ["JExtractSwiftTool"]
),

// Support library written in Swift for SwiftKit "Java"
.library(
name: "SwiftKitSwift",
Expand All @@ -127,8 +163,8 @@ let package = Package(
),

.library(
name: "JExtractSwift",
targets: ["JExtractSwift"]
name: "JExtractSwiftLib",
targets: ["JExtractSwiftLib"]
),

// ==== Plugin for wrapping Java classes in Swift
Expand Down Expand Up @@ -271,10 +307,10 @@ let package = Package(
),

.plugin(
name: "Java2SwiftPlugin",
name: "SwiftJavaPlugin",
capability: .buildTool(),
dependencies: [
"Java2Swift"
"SwiftJavaTool"
]
),

Expand Down Expand Up @@ -312,7 +348,7 @@ let package = Package(
),

.target(
name: "Java2SwiftLib",
name: "SwiftJavaLib",
dependencies: [
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
Expand All @@ -334,7 +370,7 @@ let package = Package(
),

.executableTarget(
name: "Java2Swift",
name: "SwiftJavaTool",
dependencies: [
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
Expand All @@ -343,7 +379,8 @@ let package = Package(
"JavaKit",
"JavaKitJar",
"JavaKitNetwork",
"Java2SwiftLib",
"SwiftJavaLib",
"JExtractSwiftLib",
"JavaKitShared",
],

Expand All @@ -355,7 +392,7 @@ let package = Package(
),

.target(
name: "JExtractSwift",
name: "JExtractSwiftLib",
dependencies: [
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
Expand All @@ -370,21 +407,11 @@ let package = Package(
]
),

.executableTarget(
name: "JExtractSwiftTool",
dependencies: [
"JExtractSwift",
],
swiftSettings: [
.swiftLanguageMode(.v5)
]
),

.plugin(
name: "JExtractSwiftPlugin",
capability: .buildTool(),
dependencies: [
"JExtractSwiftTool"
"SwiftJavaTool"
]
),
.plugin(
Expand All @@ -394,7 +421,7 @@ let package = Package(
permissions: [
]),
dependencies: [
"JExtractSwiftTool"
"SwiftJavaTool"
]
),

Expand Down Expand Up @@ -427,8 +454,8 @@ let package = Package(
),

.testTarget(
name: "Java2SwiftTests",
dependencies: ["Java2SwiftLib"],
name: "SwiftJavaTests",
dependencies: ["SwiftJavaLib"],
swiftSettings: [
.swiftLanguageMode(.v5),
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
Expand All @@ -438,7 +465,7 @@ let package = Package(
.testTarget(
name: "JExtractSwiftTests",
dependencies: [
"JExtractSwift"
"JExtractSwiftLib"
],
swiftSettings: [
.swiftLanguageMode(.v5),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,21 @@ final class JExtractSwiftCommandPlugin: SwiftJavaPluginProtocol, BuildToolPlugin
let sourceDir = target.directory.string

let configuration = try readConfiguration(sourceDir: "\(sourceDir)")
guard let javaPackage = configuration.javaPackage else {
throw SwiftJavaPluginError.missingConfiguration(sourceDir: "\(sourceDir)", key: "javaPackage")
}

var arguments: [String] = [
"--swift-module", sourceModule.name,
"--package-name", javaPackage,
"--output-directory-java", context.outputDirectoryJava.path(percentEncoded: false),
"--output-directory-swift", context.outputDirectorySwift.path(percentEncoded: false),
"--input-swift", sourceDir,
"--module-name", sourceModule.name,
"--output-java", context.outputJavaDirectory.path(percentEncoded: false),
"--output-swift", context.outputSwiftDirectory.path(percentEncoded: false),
// TODO: "--build-cache-directory", ...
// Since plugins cannot depend on libraries we cannot detect what the output files will be,
// as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin.
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
]
arguments.append(sourceDir)
// arguments.append(sourceDir) // TODO: we could do this shape maybe? to have the dirs last?
if let package = configuration?.javaPackage, !package.isEmpty {
["--java-package", package]
}

return arguments
}
Expand Down Expand Up @@ -130,7 +130,7 @@ final class JExtractSwiftCommandPlugin: SwiftJavaPluginProtocol, BuildToolPlugin

func runExtract(context: PluginContext, target: Target, arguments: [String]) throws {
let process = Process()
process.executableURL = try context.tool(named: "JExtractSwiftTool").url
process.executableURL = try context.tool(named: "SwiftJavaTool").url
process.arguments = arguments

do {
Expand Down
23 changes: 13 additions & 10 deletions Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
var verbose: Bool = getEnvironmentBool("SWIFT_JAVA_VERBOSE")

func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
let toolURL = try context.tool(named: "JExtractSwiftTool").url
let toolURL = try context.tool(named: "SwiftJavaTool").url

guard let sourceModule = target.sourceModule else { return [] }

Expand All @@ -42,28 +42,31 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
let sourceDir = target.directory.string
let configuration = try readConfiguration(sourceDir: "\(sourceDir)")

guard let javaPackage = configuration.javaPackage else {
guard let javaPackage = configuration?.javaPackage else {
// throw SwiftJavaPluginError.missingConfiguration(sourceDir: "\(sourceDir)", key: "javaPackage")
log("Skipping jextract step, no 'javaPackage' configuration in \(getSwiftJavaConfigPath(target: target) ?? "")")
return []
}

// We use the the usual maven-style structure of "src/[generated|main|test]/java/..."
// that is common in JVM ecosystem
let outputDirectoryJava = context.outputDirectoryJava
let outputDirectorySwift = context.outputDirectorySwift
let outputJavaDirectory = context.outputJavaDirectory
let outputSwiftDirectory = context.outputSwiftDirectory

var arguments: [String] = [
"--swift-module", sourceModule.name,
"--package-name", javaPackage,
"--output-directory-java", outputDirectoryJava.path(percentEncoded: false),
"--output-directory-swift", outputDirectorySwift.path(percentEncoded: false),
"--input-swift", sourceDir,
"--module-name", sourceModule.name,
"--output-java", outputJavaDirectory.path(percentEncoded: false),
"--output-swift", outputSwiftDirectory.path(percentEncoded: false),
// TODO: "--build-cache-directory", ...
// Since plugins cannot depend on libraries we cannot detect what the output files will be,
// as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin.
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
]
arguments.append(sourceDir)
// arguments.append(sourceDir)
if !javaPackage.isEmpty {
arguments.append(contentsOf: ["--java-package", javaPackage])
}

return [
.prebuildCommand(
Expand All @@ -72,7 +75,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
arguments: arguments,
// inputFiles: [ configFile ] + swiftFiles,
// outputFiles: outputJavaFiles
outputFilesDirectory: outputDirectorySwift
outputFilesDirectory: outputSwiftDirectory
)
]
}
Expand Down
4 changes: 2 additions & 2 deletions Plugins/PluginsShared/PluginUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ func getEnvironmentBool(_ name: String) -> Bool {
}

extension PluginContext {
var outputDirectoryJava: URL {
var outputJavaDirectory: URL {
self.pluginWorkDirectoryURL
.appending(path: "src")
.appending(path: "generated")
.appending(path: "java")
}

var outputDirectorySwift: URL {
var outputSwiftDirectory: URL {
self.pluginWorkDirectoryURL
.appending(path: "Sources")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import PackagePlugin
fileprivate let SwiftJavaConfigFileName = "swift-java.config"

@main
struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {

var pluginName: String = "swift-java"
var verbose: Bool = getEnvironmentBool("SWIFT_JAVA_VERBOSE")
Expand All @@ -27,7 +27,7 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
log("Create build commands for target '\(target.name)'")
guard let sourceModule = target.sourceModule else { return [] }

let executable = try context.tool(named: "Java2Swift").url
let executable = try context.tool(named: "SwiftJavaTool").url
var commands: [Command] = []

// Note: Target doesn't have a directoryURL counterpart to directory,
Expand All @@ -38,7 +38,7 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
// which we are generating Swift wrappers for Java classes.
let configFile = URL(filePath: sourceDir)
.appending(path: SwiftJavaConfigFileName)
let config = try readConfiguration(sourceDir: sourceDir)
let config = try readConfiguration(sourceDir: sourceDir) ?? Configuration()

log("Config on path: \(configFile.path(percentEncoded: false))")
log("Config was: \(config)")
Expand Down Expand Up @@ -199,7 +199,7 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
}
}

extension Java2SwiftBuildToolPlugin {
extension SwiftJavaBuildToolPlugin {
func argumentsModuleName(sourceModule: Target) -> [String] {
return [
"--module-name", sourceModule.name
Expand Down
4 changes: 2 additions & 2 deletions Samples/JavaDependencySampleApp/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ let package = Package(
.swiftLanguageMode(.v5),
],
plugins: [
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
.plugin(name: "SwiftJavaPlugin", package: "swift-java"),
]
),

Expand All @@ -96,7 +96,7 @@ let package = Package(
],
plugins: [
// .plugin(name: "SwiftJavaBootstrapJavaPlugin", package: "swift-java"),
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
.plugin(name: "SwiftJavaPlugin", package: "swift-java"),
]
),

Expand Down
2 changes: 1 addition & 1 deletion Samples/JavaKitSampleApp/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ let package = Package(
plugins: [
.plugin(name: "JavaCompilerPlugin", package: "swift-java"),
.plugin(name: "JExtractSwiftPlugin", package: "swift-java"),
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
.plugin(name: "SwiftJavaPlugin", package: "swift-java"),
]
),
]
Expand Down
2 changes: 1 addition & 1 deletion Samples/JavaProbablyPrime/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ let package = Package(
.swiftLanguageMode(.v5)
],
plugins: [
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
.plugin(name: "SwiftJavaPlugin", package: "swift-java"),
]
),
]
Expand Down
Loading