1212//
1313//===----------------------------------------------------------------------===//
1414
15- import Foundation
1615import ArgumentParser
16+ import Foundation
1717import SwiftJavaLib
18+ import JExtractSwiftLib
1819import JavaKit
1920import JavaKitJar
21+ import JavaKitNetwork
22+ import JavaKitReflection
23+ import SwiftSyntax
24+ import SwiftSyntaxBuilder
2025import 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
22219extension 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