diff --git a/.licenseignore b/.licenseignore index 86afc57b..cb3be0cf 100644 --- a/.licenseignore +++ b/.licenseignore @@ -5,6 +5,7 @@ .swift-format .github/* *.md +**/*.config CONTRIBUTORS.txt LICENSE.txt NOTICE.txt diff --git a/Package.swift b/Package.swift index a581e842..0d6aa048 100644 --- a/Package.swift +++ b/Package.swift @@ -56,6 +56,11 @@ let package = Package( targets: ["JavaKit"] ), + .library( + name: "JavaRuntime", + targets: ["JavaRuntime"] + ), + .library( name: "JavaKitJar", targets: ["JavaKitReflection"] @@ -83,7 +88,7 @@ let package = Package( .executable( name: "Java2Swift", - targets: ["Java2SwiftTool"] + targets: ["Java2Swift"] ), // ==== Plugin for building Java code @@ -94,6 +99,14 @@ let package = Package( ] ), + // ==== Plugin for wrapping Java classes in Swift + .plugin( + name: "Java2SwiftPlugin", + targets: [ + "Java2SwiftPlugin" + ] + ), + // ==== jextract-swift (extract Java accessors from Swift interface files) .executable( @@ -206,6 +219,14 @@ let package = Package( capability: .buildTool() ), + .plugin( + name: "Java2SwiftPlugin", + capability: .buildTool(), + dependencies: [ + "Java2Swift" + ] + ), + .target( name: "ExampleSwiftLibrary", dependencies: [], @@ -251,7 +272,7 @@ let package = Package( ), .executableTarget( - name: "Java2SwiftTool", + name: "Java2Swift", dependencies: [ .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), diff --git a/Plugins/Java2SwiftPlugin/Configuration.swift b/Plugins/Java2SwiftPlugin/Configuration.swift new file mode 100644 index 00000000..e3e1b25a --- /dev/null +++ b/Plugins/Java2SwiftPlugin/Configuration.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Configuration for the Java2Swift translation tool, provided on a per-target +/// basis. +struct Configuration: Codable { + /// The Java class path that should be passed along to the Java2Swift tool. + var classPath: String? = nil + + /// The Java classes that should be translated to Swift. The keys are + /// canonical Java class names (e.g., java.util.Vector) and the values are + /// the corresponding Swift names (e.g., JavaVector). If the value is `nil`, + /// then the Java class name will be used for the Swift name, too. + var classes: [String: String?] = [:] +} diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift new file mode 100644 index 00000000..e483b716 --- /dev/null +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// 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 Java2SwiftBuildToolPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + guard let sourceModule = target.sourceModule else { return [] } + + // Note: Target doesn't have a directoryURL counterpart to directory, + // so we cannot eliminate this deprecation warning. + let sourceDir = target.directory.string + + // Read a configuration file JavaKit.config from the target that provides + // information needed to call Java2Swift. + let configFile = URL(filePath: sourceDir) + .appending(path: "Java2Swift.config") + let configData = try Data(contentsOf: configFile) + let config = try JSONDecoder().decode(Configuration.self, from: configData) + + /// Find the manifest files from other Java2Swift executions in any targets + /// this target depends on. + var manifestFiles: [URL] = [] + func searchForManifestFiles(in target: any Target) { + let dependencyURL = URL(filePath: target.directory.string) + + // Look for a checked-in manifest file. + let generatedManifestURL = dependencyURL + .appending(path: "generated") + .appending(path: "\(target.name).swift2java") + let generatedManifestString = generatedManifestURL + .path(percentEncoded: false) + + if FileManager.default.fileExists(atPath: generatedManifestString) { + manifestFiles.append(generatedManifestURL) + } + + // TODO: Look for a manifest file that was built by the plugin itself. + } + + // Process direct dependencies of this target. + for dependency in target.dependencies { + switch dependency { + case .target(let target): + searchForManifestFiles(in: target) + + case .product(let product): + for target in product.targets { + searchForManifestFiles(in: target) + } + + @unknown default: + break + } + } + + // Process indirect target dependencies. + for dependency in target.recursiveTargetDependencies { + searchForManifestFiles(in: dependency) + } + + /// Determine the list of Java classes that will be translated into Swift, + /// along with the names of the corresponding Swift types. This will be + /// passed along to the Java2Swift tool. + let classes = config.classes.map { (javaClassName, swiftName) in + (javaClassName, swiftName ?? javaClassName.defaultSwiftNameForJavaClass) + }.sorted { (lhs, rhs) in + lhs.0 < rhs.0 + } + + let outputDirectory = context.pluginWorkDirectoryURL + .appending(path: "generated") + + var arguments: [String] = [ + "--module-name", sourceModule.name, + "--output-directory", outputDirectory.path(percentEncoded: false), + ] + if let classPath = config.classPath { + arguments += ["-cp", classPath] + } + arguments += manifestFiles.flatMap { manifestFile in + [ "--manifests", manifestFile.path(percentEncoded: false) ] + } + arguments += classes.map { (javaClassName, swiftName) in + "\(javaClassName)=\(swiftName)" + } + + /// Determine the set of Swift files that will be emitted by the Java2Swift + /// tool. + let outputSwiftFiles = classes.map { (javaClassName, swiftName) in + outputDirectory.appending(path: "\(swiftName).swift") + } + [ + outputDirectory.appending(path: "\(sourceModule.name).swift2java") + ] + + return [ + .buildCommand( + displayName: "Wrapping \(classes.count) Java classes target \(sourceModule.name) in Swift", + executable: try context.tool(named: "Java2Swift").url, + arguments: arguments, + inputFiles: [ configFile ], + outputFiles: outputSwiftFiles + ) + ] + } +} + +// 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.") +} + +extension String { + /// For a String that's of the form java.util.Vector, return the "Vector" + /// part. + fileprivate var defaultSwiftNameForJavaClass: String { + if let dotLoc = lastIndex(of: ".") { + let afterDot = index(after: dotLoc) + return String(self[afterDot...]) + } + + return self + } +} diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift index 92f41d14..df2c65f1 100644 --- a/Samples/JavaKitSampleApp/Package.swift +++ b/Samples/JavaKitSampleApp/Package.swift @@ -30,14 +30,16 @@ let package = Package( .target( name: "JavaKitExample", dependencies: [ - .product(name: "JavaKit", package: "swift-java") + .product(name: "JavaKit", package: "swift-java"), + .product(name: "JavaKitJar", package: "swift-java"), ], swiftSettings: [ .swiftLanguageMode(.v5) ], plugins: [ + .plugin(name: "Java2SwiftPlugin", package: "swift-java"), .plugin(name: "JavaCompilerPlugin", package: "swift-java") ] ), ] -) \ No newline at end of file +) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config b/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config new file mode 100644 index 00000000..ec19d82b --- /dev/null +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config @@ -0,0 +1,5 @@ +{ + "classes" : { + "java.util.ArrayList" : "ArrayList" + } +} diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index d6eba19d..090c4356 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import JavaKit -import JavaRuntime enum SwiftWrappedError: Error { case message(String) @@ -118,3 +117,10 @@ struct HelloSubclass { @JavaMethod init(greeting: String, environment: JNIEnvironment) } + + +func removeLast(arrayList: ArrayList>) { + if let lastObject = arrayList.getLast() { + _ = arrayList.remove(lastObject) + } +} diff --git a/Sources/Java2SwiftTool/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift similarity index 100% rename from Sources/Java2SwiftTool/JavaToSwift.swift rename to Sources/Java2Swift/JavaToSwift.swift