Skip to content

Commit 0018556

Browse files
committed
move the configure command into its own file/subcommand
1 parent 8de85be commit 0018556

File tree

8 files changed

+834
-219
lines changed

8 files changed

+834
-219
lines changed

.swift-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6.1.2

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

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

Sources/SwiftJavaTool/CommonOptions.swift

Whitespace-only changes.

Sources/SwiftJavaTool/SwiftJava+EmitConfiguration.swift

Lines changed: 322 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,339 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
1615
import ArgumentParser
16+
import Foundation
1717
import SwiftJavaLib
18+
import JExtractSwiftLib
1819
import JavaKit
1920
import JavaKitJar
21+
import JavaKitNetwork
22+
import JavaKitReflection
23+
import SwiftSyntax
24+
import SwiftSyntaxBuilder
2025
import JavaKitConfigurationShared
26+
import JavaKitShared
27+
28+
protocol HasCommonOptions {
29+
var commonOptions: SwiftJava.CommonOptions { get set }
30+
}
31+
32+
protocol HasCommonJVMOptions {
33+
var commonJVMOptions: SwiftJava.CommonJVMOptions { get set }
34+
}
35+
36+
extension SwiftJava {
37+
struct CommonOptions: ParsableArguments {
38+
// TODO: clarify this vs outputSwift (history: outputSwift is jextract, and this was java2swift)
39+
@Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the SwiftJava configuration file.")
40+
var outputDirectory: String? = nil
41+
42+
@Option(help: "Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift.")
43+
var inputSwift: String? = nil
44+
45+
@Option(name: .shortAndLong, help: "Configure the level of logs that should be printed")
46+
var logLevel: Logger.Level = .info
47+
}
48+
49+
struct CommonJVMOptions: ParsableArguments {
50+
@Option(
51+
name: [.customLong("cp"), .customLong("classpath")],
52+
help: "Class search path of directories and zip/jar files from which Java classes can be loaded."
53+
)
54+
var classpath: [String] = []
55+
56+
@Option(name: .shortAndLong, help: "While scanning a classpath, inspect only types included in this package")
57+
var filterJavaPackage: String? = nil
58+
}
59+
}
60+
61+
protocol SwiftJavaBaseAsyncParsableCommand: AsyncParsableCommand {
62+
var logLevel: Logger.Level { get set }
63+
64+
var commonOptions: SwiftJava.CommonOptions { get set }
65+
66+
var moduleName: String? { get }
67+
68+
mutating func runSwiftJavaCommand(config: inout Configuration) async throws
69+
70+
}
71+
72+
extension SwiftJavaBaseAsyncParsableCommand {
73+
mutating func writeContents(
74+
_ contents: String,
75+
to filename: String, description: String) throws {
76+
try writeContents(
77+
contents,
78+
outputDirectoryOverride: self.actualOutputDirectory,
79+
to: filename,
80+
description: description)
81+
}
82+
83+
mutating func writeContents(
84+
_ contents: String,
85+
outputDirectoryOverride: Foundation.URL?,
86+
to filename: String,
87+
description: String) throws {
88+
guard let outputDir = (outputDirectoryOverride ?? actualOutputDirectory) else {
89+
print("// \(filename) - \(description)")
90+
print(contents)
91+
return
92+
}
93+
94+
// If we haven't tried to create the output directory yet, do so now before
95+
// we write any files to it.
96+
// if !createdOutputDirectory {
97+
try FileManager.default.createDirectory(
98+
at: outputDir,
99+
withIntermediateDirectories: true
100+
)
101+
// createdOutputDirectory = true
102+
//}
103+
104+
// Write the file:
105+
let file = outputDir.appendingPathComponent(filename)
106+
print("[debug][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "")
107+
try contents.write(to: file, atomically: true, encoding: .utf8)
108+
print("done.".green)
109+
}
110+
}
111+
112+
extension SwiftJavaBaseAsyncParsableCommand {
113+
public mutating func run() async {
114+
print("[info][swift-java] Run: \(CommandLine.arguments.joined(separator: " "))")
115+
print("[info][swift-java] Current work directory: \(URL(fileURLWithPath: "."))")
116+
117+
do {
118+
var config = try readInitialConfiguration(command: self)
119+
try await runSwiftJavaCommand(config: &config)
120+
} catch {
121+
// We fail like this since throwing out of the run often ends up hiding the failure reason when it is executed as SwiftPM plugin (!)
122+
let message = "Failed with error: \(error)"
123+
print("[error][java-swift] \(message)")
124+
fatalError(message)
125+
}
126+
127+
// Just for debugging so it is clear which command has finished
128+
print("[debug][swift-java] " + "Done: ".green + CommandLine.arguments.joined(separator: " ").green)
129+
}
130+
}
131+
132+
extension SwiftJavaBaseAsyncParsableCommand {
133+
var logLevel: Logger.Level {
134+
get {
135+
self.commonOptions.logLevel
136+
}
137+
set {
138+
self.commonOptions.logLevel = newValue
139+
}
140+
}
141+
}
142+
extension SwiftJavaBaseAsyncParsableCommand {
143+
144+
var moduleBaseDir: Foundation.URL? {
145+
if let outputDirectory = commonOptions.outputDirectory {
146+
if outputDirectory == "-" {
147+
return nil
148+
}
149+
150+
print("[debug][swift-java] Module base directory based on outputDirectory!")
151+
return URL(fileURLWithPath: outputDirectory)
152+
}
153+
154+
guard let moduleName else {
155+
return nil
156+
}
157+
158+
// Put the result into Sources/\(moduleName).
159+
let baseDir = URL(fileURLWithPath: ".")
160+
.appendingPathComponent("Sources", isDirectory: true)
161+
.appendingPathComponent(moduleName, isDirectory: true)
162+
163+
return baseDir
164+
}
165+
166+
/// The output directory in which to place the generated files, which will
167+
/// be the specified directory (--output-directory or -o option) if given,
168+
/// or a default directory derived from the other command-line arguments.
169+
///
170+
/// Returns `nil` only when we should emit the files to standard output.
171+
var actualOutputDirectory: Foundation.URL? {
172+
if let outputDirectory = commonOptions.outputDirectory {
173+
if outputDirectory == "-" {
174+
return nil
175+
}
176+
177+
return URL(fileURLWithPath: outputDirectory)
178+
}
179+
180+
guard let moduleName else {
181+
fatalError("--module-name must be set!")
182+
}
183+
184+
// Put the result into Sources/\(moduleName).
185+
let baseDir = URL(fileURLWithPath: ".")
186+
.appendingPathComponent("Sources", isDirectory: true)
187+
.appendingPathComponent(moduleName, isDirectory: true)
188+
189+
// For generated Swift sources, put them into a "generated" subdirectory.
190+
// The configuration file goes at the top level.
191+
let outputDir: Foundation.URL
192+
// if jar {
193+
// precondition(self.input != nil, "-jar mode requires path to jar to be specified as input path")
194+
outputDir = baseDir
195+
// } else {
196+
// outputDir = baseDir
197+
// .appendingPathComponent("generated", isDirectory: true)
198+
// }
199+
200+
return outputDir
201+
}
202+
203+
func readInitialConfiguration(command: some SwiftJavaBaseAsyncParsableCommand) throws -> Configuration {
204+
var earlyConfig: Configuration?
205+
if let moduleBaseDir {
206+
print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)")
207+
earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path)
208+
} else if let inputSwift = commonOptions.inputSwift {
209+
print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)")
210+
earlyConfig = try readConfiguration(sourceDir: inputSwift)
211+
}
212+
var config = earlyConfig ?? Configuration()
213+
// override configuration with options from command line
214+
config.logLevel = command.logLevel
215+
return config
216+
}
217+
}
21218

22219
extension SwiftJava {
220+
struct ConfigureCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions {
221+
static let configuration = CommandConfiguration(
222+
commandName: "configure",
223+
abstract: "Configure and emit a swift-java.config file based on an input dependency or jar file")
224+
225+
// TODO: This should be a "make wrappers" option that just detects when we give it a jar
226+
@Flag(
227+
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."
228+
)
229+
var jar: Bool = false
230+
231+
@Option(
232+
name: .long,
233+
help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration"
234+
)
235+
var existingConfigFile: ExistingConfigFileMode = .overwrite
236+
enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable {
237+
case overwrite
238+
case amend
239+
}
240+
241+
// FIXME: is it used?
242+
@Option(help: "The name of the Swift module into which the resulting Swift types will be generated.")
243+
var moduleName: String? // TODO: rename to --swift-module?
244+
245+
@OptionGroup var commonOptions: SwiftJava.CommonOptions
246+
@OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions
247+
248+
@Argument(
249+
help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file."
250+
)
251+
var input: String = ""
252+
}
253+
}
254+
255+
extension SwiftJava.ConfigureCommand {
256+
mutating func runSwiftJavaCommand(config: inout Configuration) async throws {
257+
// Form a class path from all of our input sources:
258+
// * Command-line option --classpath
259+
let classpathOptionEntries: [String] = self.commonJVMOptions.classpath.flatMap { $0.split(separator: ":").map(String.init) }
260+
let classpathFromEnv = ProcessInfo.processInfo.environment["CLASSPATH"]?.split(separator: ":").map(String.init) ?? []
261+
let classpathFromConfig: [String] = config.classpath?.split(separator: ":").map(String.init) ?? []
262+
print("[debug][swift-java] Base classpath from config: \(classpathFromConfig)")
263+
264+
var classpathEntries: [String] = classpathFromConfig
265+
266+
let swiftJavaCachedModuleClasspath = findSwiftJavaClasspaths(in:
267+
// self.effectiveCacheDirectory ??
268+
FileManager.default.currentDirectoryPath)
269+
print("[debug][swift-java] Classpath from *.swift-java.classpath files: \(swiftJavaCachedModuleClasspath)")
270+
classpathEntries += swiftJavaCachedModuleClasspath
271+
272+
if !classpathOptionEntries.isEmpty {
273+
print("[debug][swift-java] Classpath from options: \(classpathOptionEntries)")
274+
classpathEntries += classpathOptionEntries
275+
} else {
276+
// * Base classpath from CLASSPATH env variable
277+
print("[debug][swift-java] Classpath from environment: \(classpathFromEnv)")
278+
classpathEntries += classpathFromEnv
279+
}
280+
281+
let extraClasspath = input // FIXME: just use the -cp as usual
282+
let extraClasspathEntries = extraClasspath.split(separator: ":").map(String.init)
283+
print("[debug][swift-java] Extra classpath: \(extraClasspathEntries)")
284+
classpathEntries += extraClasspathEntries
285+
286+
// Bring up the Java VM when necessary
287+
288+
if logLevel >= .debug {
289+
let classpathString = classpathEntries.joined(separator: ":")
290+
print("[debug][swift-java] Initialize JVM with classpath: \(classpathString)")
291+
}
292+
let jvm = try JavaVirtualMachine.shared(classpath: classpathEntries)
293+
294+
try emitConfiguration(classpath: self.commonJVMOptions.classpath, environment: jvm.environment())
295+
}
296+
297+
/// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration.
298+
func getBaseConfigurationForWrite() throws -> (Bool, Configuration) {
299+
guard let actualOutputDirectory = self.actualOutputDirectory else {
300+
// If output has no path there's nothing to amend
301+
return (false, .init())
302+
}
303+
304+
switch self.existingConfigFile {
305+
case .overwrite:
306+
// always make up a fresh instance if we're overwriting
307+
return (false, .init())
308+
case .amend:
309+
let configPath = actualOutputDirectory
310+
guard let config = try readConfiguration(sourceDir: configPath.path) else {
311+
return (false, .init())
312+
}
313+
return (true, config)
314+
}
315+
}
23316

24317
// TODO: make this perhaps "emit type mappings"
25318
mutating func emitConfiguration(
26-
classpath: String,
319+
classpath: [String],
27320
environment: JNIEnvironment
28321
) throws {
29-
print("[java-swift] Generate Java->Swift type mappings. Active filter: \(javaPackageFilter)")
30-
print("[java-swift] Classpath: \(classpath)")
322+
if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage {
323+
print("[java-swift][debug] Generate Java->Swift type mappings. Active filter: \(filterJavaPackage)")
324+
}
325+
print("[java-swift][debug] Classpath: \(classpath)")
31326

32327
if classpath.isEmpty {
33-
print("[warning][java-swift] Classpath is empty!")
328+
print("[java-swift][warning] Classpath is empty!")
34329
}
35330

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

43338
// Import types from all the classpath entries;
44339
// Note that we use the package level filtering, so users have some control over what gets imported.
45-
for entry in classpath.split(separator: ":").map(String.init) {
340+
let classpathEntries = classpath.split(separator: ":").map(String.init)
341+
for entry in classpathEntries {
342+
guard fileOrDirectoryExists(at: entry) else {
343+
// We only log specific jars missing, as paths may be empty directories that won't hurt not existing.
344+
print("[debug][swift-java] Classpath entry does not exist: \(entry)")
345+
continue
346+
}
347+
46348
print("[debug][swift-java] Importing classpath entry: \(entry)")
47349
if entry.hasSuffix(".jar") {
48350
let jarFile = try JarFile(entry, false, environment: environment)
@@ -70,10 +372,10 @@ extension SwiftJava {
70372
}
71373

72374
mutating func addJavaToSwiftMappings(
73-
to configuration: inout Configuration,
74-
forJar jarFile: JarFile,
75-
environment: JNIEnvironment
76-
) throws {
375+
to configuration: inout Configuration,
376+
forJar jarFile: JarFile,
377+
environment: JNIEnvironment
378+
) throws {
77379
for entry in jarFile.entries()! {
78380
// We only look at class files in the Jar file.
79381
guard entry.getName().hasSuffix(".class") else {
@@ -99,11 +401,10 @@ extension SwiftJava {
99401
let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
100402
.dropLast(".class".count))
101403

102-
if let javaPackageFilter {
103-
if !javaCanonicalName.hasPrefix(javaPackageFilter) {
104-
// Skip classes which don't match our expected prefix
105-
continue
106-
}
404+
if let filterJavaPackage = self.commonJVMOptions.filterJavaPackage,
405+
!javaCanonicalName.hasPrefix(filterJavaPackage) {
406+
// Skip classes which don't match our expected prefix
407+
continue
107408
}
108409

109410
if configuration.classes?[javaCanonicalName] != nil {
@@ -117,4 +418,9 @@ extension SwiftJava {
117418
}
118419
}
119420

421+
}
422+
423+
package func fileOrDirectoryExists(at path: String) -> Bool {
424+
var isDirectory: ObjCBool = false
425+
return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
120426
}

0 commit comments

Comments
 (0)