Skip to content

Commit 2fb33d8

Browse files
authored
Extract configure into its own subcommand (first of such extractions) (#268)
1 parent ccc87db commit 2fb33d8

File tree

19 files changed

+1213
-549
lines changed

19 files changed

+1213
-549
lines changed

Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class JExtractSwiftCommandPlugin: SwiftJavaPluginProtocol, BuildToolPlugin
7272

7373
var arguments: [String] = [
7474
"--input-swift", sourceDir,
75-
"--module-name", sourceModule.name,
75+
"--swift-module", sourceModule.name,
7676
"--output-java", context.outputJavaDirectory.path(percentEncoded: false),
7777
"--output-swift", context.outputSwiftDirectory.path(percentEncoded: false),
7878
// TODO: "--build-cache-directory", ...

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
5555

5656
var arguments: [String] = [
5757
"--input-swift", sourceDir,
58-
"--module-name", sourceModule.name,
58+
"--swift-module", sourceModule.name,
5959
"--output-java", outputJavaDirectory.path(percentEncoded: false),
6060
"--output-swift", outputSwiftDirectory.path(percentEncoded: false),
6161
// TODO: "--build-cache-directory", ...

Plugins/PluginsShared/PluginUtils.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ extension PluginContext {
7272
.appending(path: "Sources")
7373
}
7474

75-
func cachedClasspathFile(moduleName: String) -> URL {
75+
func cachedClasspathFile(swiftModule: String) -> URL {
7676
self.pluginWorkDirectoryURL
77-
.appending(path: "\(moduleName)", directoryHint: .notDirectory)
77+
.appending(path: "\(swiftModule)", directoryHint: .notDirectory)
7878
}
7979
}

Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
166166
displayName: displayName,
167167
executable: executable,
168168
arguments: [
169+
// FIXME: change to 'resolve' subcommand
169170
"--fetch", configFile.path(percentEncoded: false),
170-
"--module-name", sourceModule.name,
171+
"--swift-module", sourceModule.name,
171172
"--output-directory", outputDirectory(context: context, generated: false).path(percentEncoded: false)
172173
],
173174
environment: [:],
@@ -180,29 +181,29 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
180181
}
181182

182183
if !outputSwiftFiles.isEmpty {
184+
let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'"
185+
log("Prepared: \(displayName)")
183186
commands += [
184187
.buildCommand(
185-
displayName: "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'",
188+
displayName: displayName,
186189
executable: executable,
187190
arguments: arguments,
188-
inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [
189-
configFile
190-
],
191+
inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [ configFile ],
191192
outputFiles: outputSwiftFiles
192193
)
193194
]
194195
} else {
195196
log("No Swift output files, skip wrapping")
196197
}
197-
198+
198199
return commands
199200
}
200201
}
201202

202203
extension SwiftJavaBuildToolPlugin {
203204
func argumentsModuleName(sourceModule: Target) -> [String] {
204205
return [
205-
"--module-name", sourceModule.name
206+
"--swift-module", sourceModule.name
206207
]
207208
}
208209

Samples/JavaDependencySampleApp/Sources/Test/swift-java.config

Lines changed: 429 additions & 0 deletions
Large diffs are not rendered by default.

Samples/JavaSieve/Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ let package = Package(
5454
.product(name: "JavaKit", package: "swift-java"),
5555
.product(name: "JavaKitJar", package: "swift-java"),
5656
],
57+
exclude: ["swift-java.config"],
5758
swiftSettings: [
5859
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
5960
],
@@ -71,6 +72,7 @@ let package = Package(
7172
.product(name: "JavaKit", package: "swift-java"),
7273
.product(name: "JavaKitCollection", package: "swift-java"),
7374
],
75+
exclude: ["swift-java.config"],
7476
swiftSettings: [
7577
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
7678
],

Samples/JavaSieve/Sources/JavaSieve/main.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,15 @@
1515
import JavaKit
1616
import JavaMath
1717

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

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

29-
try JavaClass<RoundingMode>().HALF_UP
26+
_ = try JavaClass<RoundingMode>().HALF_UP // can import a Java enum value
3027
} catch {
3128
print("Failure: \(error)")
3229
}

Samples/JavaSieve/Sources/JavaSieve/swift-java.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@
2929
"com.gazman.quadratic_sieve.wheel.Wheel" : "Wheel"
3030
}
3131
}
32+

Sources/JavaKitConfigurationShared/Configuration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,13 @@ public func readConfiguration(configPath: URL, file: String = #fileID, line: UIn
137137
}
138138
}
139139

140-
public func findSwiftJavaClasspaths(moduleName: String) -> [String] {
140+
public func findSwiftJavaClasspaths(swiftModule: String) -> [String] {
141141
let basePath: String = FileManager.default.currentDirectoryPath
142142
let pluginOutputsDir = URL(fileURLWithPath: basePath)
143143
.appendingPathComponent(".build", isDirectory: true)
144144
.appendingPathComponent("plugins", isDirectory: true)
145145
.appendingPathComponent("outputs", isDirectory: true)
146-
.appendingPathComponent(moduleName, isDirectory: true)
146+
.appendingPathComponent(swiftModule, isDirectory: true)
147147

148148
return findSwiftJavaClasspaths(in: pluginOutputsDir.path)
149149
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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 ArgumentParser
16+
import Foundation
17+
import SwiftJavaLib
18+
import JExtractSwiftLib
19+
import JavaKit
20+
import JavaKitJar
21+
import JavaKitNetwork
22+
import JavaKitReflection
23+
import SwiftSyntax
24+
import SwiftSyntaxBuilder
25+
import JavaKitConfigurationShared
26+
import JavaKitShared
27+
28+
extension SwiftJava {
29+
struct ConfigureCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions {
30+
static let configuration = CommandConfiguration(
31+
commandName: "configure",
32+
abstract: "Configure and emit a swift-java.config file based on an input dependency or jar file")
33+
34+
@OptionGroup var commonOptions: SwiftJava.CommonOptions
35+
@OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions
36+
37+
// TODO: This should be a "make wrappers" option that just detects when we give it a jar
38+
@Flag(
39+
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."
40+
)
41+
var jar: Bool = false
42+
43+
@Option(
44+
name: .long,
45+
help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration"
46+
)
47+
var existingConfigFile: ExistingConfigFileMode = .overwrite
48+
enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable {
49+
case overwrite
50+
case amend
51+
}
52+
53+
@Option(help: "The name of the Swift module into which the resulting Swift types will be generated.")
54+
var swiftModule: String
55+
56+
var effectiveSwiftModule: String {
57+
swiftModule
58+
}
59+
60+
@Argument(
61+
help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file."
62+
)
63+
var input: String?
64+
}
65+
}
66+
67+
extension SwiftJava.ConfigureCommand {
68+
mutating func runSwiftJavaCommand(config: inout Configuration) async throws {
69+
// Form a class path from all of our input sources:
70+
// * Command-line option --classpath
71+
let classpathOptionEntries: [String] = self.commonJVMOptions.classpath.flatMap { $0.split(separator: ":").map(String.init) }
72+
let classpathFromEnv = ProcessInfo.processInfo.environment["CLASSPATH"]?.split(separator: ":").map(String.init) ?? []
73+
let classpathFromConfig: [String] = config.classpath?.split(separator: ":").map(String.init) ?? []
74+
print("[debug][swift-java] Base classpath from config: \(classpathFromConfig)")
75+
76+
var classpathEntries: [String] = classpathFromConfig
77+
78+
let swiftJavaCachedModuleClasspath = findSwiftJavaClasspaths(in:
79+
// self.effectiveCacheDirectory ??
80+
FileManager.default.currentDirectoryPath)
81+
print("[debug][swift-java] Classpath from *.swift-java.classpath files: \(swiftJavaCachedModuleClasspath)")
82+
classpathEntries += swiftJavaCachedModuleClasspath
83+
84+
if !classpathOptionEntries.isEmpty {
85+
print("[debug][swift-java] Classpath from options: \(classpathOptionEntries)")
86+
classpathEntries += classpathOptionEntries
87+
} else {
88+
// * Base classpath from CLASSPATH env variable
89+
print("[debug][swift-java] Classpath from environment: \(classpathFromEnv)")
90+
classpathEntries += classpathFromEnv
91+
}
92+
93+
let extraClasspath = input ?? "" // FIXME: just use the -cp as usual
94+
let extraClasspathEntries = extraClasspath.split(separator: ":").map(String.init)
95+
print("[debug][swift-java] Extra classpath: \(extraClasspathEntries)")
96+
classpathEntries += extraClasspathEntries
97+
98+
// Bring up the Java VM when necessary
99+
100+
if logLevel >= .debug {
101+
let classpathString = classpathEntries.joined(separator: ":")
102+
print("[debug][swift-java] Initialize JVM with classpath: \(classpathString)")
103+
}
104+
let jvm = try JavaVirtualMachine.shared(classpath: classpathEntries)
105+
106+
try emitConfiguration(classpath: self.commonJVMOptions.classpath, environment: jvm.environment())
107+
}
108+
109+
/// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration.
110+
func getBaseConfigurationForWrite() throws -> (Bool, Configuration) {
111+
guard let actualOutputDirectory = self.actualOutputDirectory else {
112+
// If output has no path there's nothing to amend
113+
return (false, .init())
114+
}
115+
116+
switch self.existingConfigFile {
117+
case .overwrite:
118+
// always make up a fresh instance if we're overwriting
119+
return (false, .init())
120+
case .amend:
121+
let configPath = actualOutputDirectory
122+
guard let config = try readConfiguration(sourceDir: configPath.path) else {
123+
return (false, .init())
124+
}
125+
return (true, config)
126+
}
127+
}
128+
129+
// TODO: make this perhaps "emit type mappings"
130+
mutating func emitConfiguration(
131+
classpath: [String],
132+
environment: JNIEnvironment
133+
) throws {
134+
if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage {
135+
print("[java-swift][debug] Generate Java->Swift type mappings. Active filter: \(filterJavaPackage)")
136+
}
137+
print("[java-swift][debug] Classpath: \(classpath)")
138+
139+
if classpath.isEmpty {
140+
print("[java-swift][warning] Classpath is empty!")
141+
}
142+
143+
// Get a fresh or existing configuration we'll amend
144+
var (amendExistingConfig, configuration) = try getBaseConfigurationForWrite()
145+
if amendExistingConfig {
146+
print("[swift-java] Amend existing swift-java.config file...")
147+
}
148+
configuration.classpath = classpath.joined(separator: ":") // TODO: is this correct?
149+
150+
// Import types from all the classpath entries;
151+
// Note that we use the package level filtering, so users have some control over what gets imported.
152+
let classpathEntries = classpath.split(separator: ":").map(String.init)
153+
for entry in classpathEntries {
154+
guard fileOrDirectoryExists(at: entry) else {
155+
// We only log specific jars missing, as paths may be empty directories that won't hurt not existing.
156+
print("[debug][swift-java] Classpath entry does not exist: \(entry)")
157+
continue
158+
}
159+
160+
print("[debug][swift-java] Importing classpath entry: \(entry)")
161+
if entry.hasSuffix(".jar") {
162+
let jarFile = try JarFile(entry, false, environment: environment)
163+
try addJavaToSwiftMappings(
164+
to: &configuration,
165+
forJar: jarFile,
166+
environment: environment
167+
)
168+
} else if FileManager.default.fileExists(atPath: entry) {
169+
print("[warning][swift-java] Currently unable handle directory classpath entries for config generation! Skipping: \(entry)")
170+
} else {
171+
print("[warning][swift-java] Classpath entry does not exist, skipping: \(entry)")
172+
}
173+
}
174+
175+
// Encode the configuration.
176+
let contents = try configuration.renderJSON()
177+
178+
// Write the file.
179+
try writeContents(
180+
contents,
181+
to: "swift-java.config",
182+
description: "swift-java configuration file"
183+
)
184+
}
185+
186+
mutating func addJavaToSwiftMappings(
187+
to configuration: inout Configuration,
188+
forJar jarFile: JarFile,
189+
environment: JNIEnvironment
190+
) throws {
191+
for entry in jarFile.entries()! {
192+
// We only look at class files in the Jar file.
193+
guard entry.getName().hasSuffix(".class") else {
194+
continue
195+
}
196+
197+
// Skip some "common" files we know that would be duplicated in every jar
198+
guard !entry.getName().hasPrefix("META-INF") else {
199+
continue
200+
}
201+
guard !entry.getName().hasSuffix("package-info") else {
202+
continue
203+
}
204+
guard !entry.getName().hasSuffix("package-info.class") else {
205+
continue
206+
}
207+
208+
// If this is a local class, it cannot be mapped into Swift.
209+
if entry.getName().isLocalJavaClass {
210+
continue
211+
}
212+
213+
let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
214+
.dropLast(".class".count))
215+
216+
if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage,
217+
!javaCanonicalName.hasPrefix(filterJavaPackage) {
218+
// Skip classes which don't match our expected prefix
219+
continue
220+
}
221+
222+
if configuration.classes?[javaCanonicalName] != nil {
223+
// We never overwrite an existing class mapping configuration.
224+
// E.g. the user may have configured a custom name for a type.
225+
continue
226+
}
227+
228+
configuration.classes?[javaCanonicalName] =
229+
javaCanonicalName.defaultSwiftNameForJavaClass
230+
}
231+
}
232+
233+
}
234+
235+
package func fileOrDirectoryExists(at path: String) -> Bool {
236+
var isDirectory: ObjCBool = false
237+
return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
238+
}

0 commit comments

Comments
 (0)