Skip to content

Extract configure into its own subcommand (first of such extractions) #268

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 5 commits into from
Jun 16, 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final class JExtractSwiftCommandPlugin: SwiftJavaPluginProtocol, BuildToolPlugin

var arguments: [String] = [
"--input-swift", sourceDir,
"--module-name", sourceModule.name,
"--swift-module", sourceModule.name,
"--output-java", context.outputJavaDirectory.path(percentEncoded: false),
"--output-swift", context.outputSwiftDirectory.path(percentEncoded: false),
// TODO: "--build-cache-directory", ...
Expand Down
2 changes: 1 addition & 1 deletion Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {

var arguments: [String] = [
"--input-swift", sourceDir,
"--module-name", sourceModule.name,
"--swift-module", sourceModule.name,
"--output-java", outputJavaDirectory.path(percentEncoded: false),
"--output-swift", outputSwiftDirectory.path(percentEncoded: false),
// TODO: "--build-cache-directory", ...
Expand Down
4 changes: 2 additions & 2 deletions Plugins/PluginsShared/PluginUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ extension PluginContext {
.appending(path: "Sources")
}

func cachedClasspathFile(moduleName: String) -> URL {
func cachedClasspathFile(swiftModule: String) -> URL {
self.pluginWorkDirectoryURL
.appending(path: "\(moduleName)", directoryHint: .notDirectory)
.appending(path: "\(swiftModule)", directoryHint: .notDirectory)
}
}
15 changes: 8 additions & 7 deletions Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
displayName: displayName,
executable: executable,
arguments: [
// FIXME: change to 'resolve' subcommand
"--fetch", configFile.path(percentEncoded: false),
"--module-name", sourceModule.name,
"--swift-module", sourceModule.name,
"--output-directory", outputDirectory(context: context, generated: false).path(percentEncoded: false)
],
environment: [:],
Expand All @@ -180,29 +181,29 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
}

if !outputSwiftFiles.isEmpty {
let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'"
log("Prepared: \(displayName)")
commands += [
.buildCommand(
displayName: "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'",
displayName: displayName,
executable: executable,
arguments: arguments,
inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [
configFile
],
inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [ configFile ],
outputFiles: outputSwiftFiles
)
]
} else {
log("No Swift output files, skip wrapping")
}

return commands
}
}

extension SwiftJavaBuildToolPlugin {
func argumentsModuleName(sourceModule: Target) -> [String] {
return [
"--module-name", sourceModule.name
"--swift-module", sourceModule.name
]
}

Expand Down
429 changes: 429 additions & 0 deletions Samples/JavaDependencySampleApp/Sources/Test/swift-java.config

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Samples/JavaSieve/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ let package = Package(
.product(name: "JavaKit", package: "swift-java"),
.product(name: "JavaKitJar", package: "swift-java"),
],
exclude: ["swift-java.config"],
swiftSettings: [
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
],
Expand All @@ -71,6 +72,7 @@ let package = Package(
.product(name: "JavaKit", package: "swift-java"),
.product(name: "JavaKitCollection", package: "swift-java"),
],
exclude: ["swift-java.config"],
swiftSettings: [
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
],
Expand Down
7 changes: 2 additions & 5 deletions Samples/JavaSieve/Sources/JavaSieve/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@
import JavaKit
import JavaMath

let jvm = try JavaVirtualMachine.shared(classpath: [
"quadratic-sieve-Java/build/libs/QuadraticSieve-1.0.jar",
".",
])
let jvm = try JavaVirtualMachine.shared()

do {
let sieveClass = try JavaClass<SieveOfEratosthenes>(environment: jvm.environment())
for prime in sieveClass.findPrimes(100)! {
print("Found prime: \(prime.intValue())")
}

try JavaClass<RoundingMode>().HALF_UP
_ = try JavaClass<RoundingMode>().HALF_UP // can import a Java enum value
} catch {
print("Failure: \(error)")
}
1 change: 1 addition & 0 deletions Samples/JavaSieve/Sources/JavaSieve/swift-java.config
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
"com.gazman.quadratic_sieve.wheel.Wheel" : "Wheel"
}
}

4 changes: 2 additions & 2 deletions Sources/JavaKitConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ public func readConfiguration(configPath: URL, file: String = #fileID, line: UIn
}
}

public func findSwiftJavaClasspaths(moduleName: String) -> [String] {
public func findSwiftJavaClasspaths(swiftModule: String) -> [String] {
let basePath: String = FileManager.default.currentDirectoryPath
let pluginOutputsDir = URL(fileURLWithPath: basePath)
.appendingPathComponent(".build", isDirectory: true)
.appendingPathComponent("plugins", isDirectory: true)
.appendingPathComponent("outputs", isDirectory: true)
.appendingPathComponent(moduleName, isDirectory: true)
.appendingPathComponent(swiftModule, isDirectory: true)

return findSwiftJavaClasspaths(in: pluginOutputsDir.path)
}
Expand Down
238 changes: 238 additions & 0 deletions Sources/SwiftJavaTool/Commands/ConfigureCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//===----------------------------------------------------------------------===//
//
// 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 ArgumentParser
import Foundation
import SwiftJavaLib
import JExtractSwiftLib
import JavaKit
import JavaKitJar
import JavaKitNetwork
import JavaKitReflection
import SwiftSyntax
import SwiftSyntaxBuilder
import JavaKitConfigurationShared
import JavaKitShared

extension SwiftJava {
struct ConfigureCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions {
static let configuration = CommandConfiguration(
commandName: "configure",
abstract: "Configure and emit a swift-java.config file based on an input dependency or jar file")

@OptionGroup var commonOptions: SwiftJava.CommonOptions
@OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions

// TODO: This should be a "make wrappers" option that just detects when we give it a jar
@Flag(
help: "Specifies that the input is a *.jar file whose public classes will be loaded. The output of swift-java will be a configuration file (swift-java.config) that can be used as input to a subsequent swift-java invocation to generate wrappers for those public classes."
)
var jar: Bool = false

@Option(
name: .long,
help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration"
)
var existingConfigFile: ExistingConfigFileMode = .overwrite
enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable {
case overwrite
case amend
}

@Option(help: "The name of the Swift module into which the resulting Swift types will be generated.")
var swiftModule: String

var effectiveSwiftModule: String {
swiftModule
}

@Argument(
help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file."
)
var input: String?
}
}

extension SwiftJava.ConfigureCommand {
mutating func runSwiftJavaCommand(config: inout Configuration) async throws {
// Form a class path from all of our input sources:
// * Command-line option --classpath
let classpathOptionEntries: [String] = self.commonJVMOptions.classpath.flatMap { $0.split(separator: ":").map(String.init) }
let classpathFromEnv = ProcessInfo.processInfo.environment["CLASSPATH"]?.split(separator: ":").map(String.init) ?? []
let classpathFromConfig: [String] = config.classpath?.split(separator: ":").map(String.init) ?? []
print("[debug][swift-java] Base classpath from config: \(classpathFromConfig)")

var classpathEntries: [String] = classpathFromConfig

let swiftJavaCachedModuleClasspath = findSwiftJavaClasspaths(in:
// self.effectiveCacheDirectory ??
FileManager.default.currentDirectoryPath)
print("[debug][swift-java] Classpath from *.swift-java.classpath files: \(swiftJavaCachedModuleClasspath)")
classpathEntries += swiftJavaCachedModuleClasspath

if !classpathOptionEntries.isEmpty {
print("[debug][swift-java] Classpath from options: \(classpathOptionEntries)")
classpathEntries += classpathOptionEntries
} else {
// * Base classpath from CLASSPATH env variable
print("[debug][swift-java] Classpath from environment: \(classpathFromEnv)")
classpathEntries += classpathFromEnv
}

let extraClasspath = input ?? "" // FIXME: just use the -cp as usual
let extraClasspathEntries = extraClasspath.split(separator: ":").map(String.init)
print("[debug][swift-java] Extra classpath: \(extraClasspathEntries)")
classpathEntries += extraClasspathEntries

// Bring up the Java VM when necessary

if logLevel >= .debug {
let classpathString = classpathEntries.joined(separator: ":")
print("[debug][swift-java] Initialize JVM with classpath: \(classpathString)")
}
let jvm = try JavaVirtualMachine.shared(classpath: classpathEntries)

try emitConfiguration(classpath: self.commonJVMOptions.classpath, environment: jvm.environment())
}

/// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration.
func getBaseConfigurationForWrite() throws -> (Bool, Configuration) {
guard let actualOutputDirectory = self.actualOutputDirectory else {
// If output has no path there's nothing to amend
return (false, .init())
}

switch self.existingConfigFile {
case .overwrite:
// always make up a fresh instance if we're overwriting
return (false, .init())
case .amend:
let configPath = actualOutputDirectory
guard let config = try readConfiguration(sourceDir: configPath.path) else {
return (false, .init())
}
return (true, config)
}
}

// TODO: make this perhaps "emit type mappings"
mutating func emitConfiguration(
classpath: [String],
environment: JNIEnvironment
) throws {
if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage {
print("[java-swift][debug] Generate Java->Swift type mappings. Active filter: \(filterJavaPackage)")
}
print("[java-swift][debug] Classpath: \(classpath)")

if classpath.isEmpty {
print("[java-swift][warning] Classpath is empty!")
}

// Get a fresh or existing configuration we'll amend
var (amendExistingConfig, configuration) = try getBaseConfigurationForWrite()
if amendExistingConfig {
print("[swift-java] Amend existing swift-java.config file...")
}
configuration.classpath = classpath.joined(separator: ":") // TODO: is this correct?

// Import types from all the classpath entries;
// Note that we use the package level filtering, so users have some control over what gets imported.
let classpathEntries = classpath.split(separator: ":").map(String.init)
for entry in classpathEntries {
guard fileOrDirectoryExists(at: entry) else {
// We only log specific jars missing, as paths may be empty directories that won't hurt not existing.
print("[debug][swift-java] Classpath entry does not exist: \(entry)")
continue
}

print("[debug][swift-java] Importing classpath entry: \(entry)")
if entry.hasSuffix(".jar") {
let jarFile = try JarFile(entry, false, environment: environment)
try addJavaToSwiftMappings(
to: &configuration,
forJar: jarFile,
environment: environment
)
} else if FileManager.default.fileExists(atPath: entry) {
print("[warning][swift-java] Currently unable handle directory classpath entries for config generation! Skipping: \(entry)")
} else {
print("[warning][swift-java] Classpath entry does not exist, skipping: \(entry)")
}
}

// Encode the configuration.
let contents = try configuration.renderJSON()

// Write the file.
try writeContents(
contents,
to: "swift-java.config",
description: "swift-java configuration file"
)
}

mutating func addJavaToSwiftMappings(
to configuration: inout Configuration,
forJar jarFile: JarFile,
environment: JNIEnvironment
) throws {
for entry in jarFile.entries()! {
// We only look at class files in the Jar file.
guard entry.getName().hasSuffix(".class") else {
continue
}

// Skip some "common" files we know that would be duplicated in every jar
guard !entry.getName().hasPrefix("META-INF") else {
continue
}
guard !entry.getName().hasSuffix("package-info") else {
continue
}
guard !entry.getName().hasSuffix("package-info.class") else {
continue
}

// If this is a local class, it cannot be mapped into Swift.
if entry.getName().isLocalJavaClass {
continue
}

let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
.dropLast(".class".count))

if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage,
!javaCanonicalName.hasPrefix(filterJavaPackage) {
// Skip classes which don't match our expected prefix
continue
}

if configuration.classes?[javaCanonicalName] != nil {
// We never overwrite an existing class mapping configuration.
// E.g. the user may have configured a custom name for a type.
continue
}

configuration.classes?[javaCanonicalName] =
javaCanonicalName.defaultSwiftNameForJavaClass
}
}

}

package func fileOrDirectoryExists(at path: String) -> Bool {
var isDirectory: ObjCBool = false
return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
}
Loading