12
12
//
13
13
//===----------------------------------------------------------------------===//
14
14
15
- import Foundation
16
15
import ArgumentParser
16
+ import Foundation
17
17
import SwiftJavaLib
18
+ import JExtractSwiftLib
18
19
import JavaKit
19
20
import JavaKitJar
21
+ import JavaKitNetwork
22
+ import JavaKitReflection
23
+ import SwiftSyntax
24
+ import SwiftSyntaxBuilder
20
25
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
+ }
21
218
22
219
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
+ }
23
316
24
317
// TODO: make this perhaps "emit type mappings"
25
318
mutating func emitConfiguration(
26
- classpath: String ,
319
+ classpath: [ String ] ,
27
320
environment: JNIEnvironment
28
321
) 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) " )
31
326
32
327
if classpath. isEmpty {
33
- print ( " [warning][ java-swift] Classpath is empty! " )
328
+ print ( " [java-swift][warning ] Classpath is empty! " )
34
329
}
35
330
36
331
// Get a fresh or existing configuration we'll amend
37
332
var ( amendExistingConfig, configuration) = try getBaseConfigurationForWrite ( )
38
333
if amendExistingConfig {
39
334
print ( " [swift-java] Amend existing swift-java.config file... " )
40
335
}
41
- configuration. classpath = classpath // TODO: is this correct?
336
+ configuration. classpath = classpath. joined ( separator : " : " ) // TODO: is this correct?
42
337
43
338
// Import types from all the classpath entries;
44
339
// 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
+
46
348
print ( " [debug][swift-java] Importing classpath entry: \( entry) " )
47
349
if entry. hasSuffix ( " .jar " ) {
48
350
let jarFile = try JarFile ( entry, false , environment: environment)
@@ -70,10 +372,10 @@ extension SwiftJava {
70
372
}
71
373
72
374
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 {
77
379
for entry in jarFile. entries ( ) ! {
78
380
// We only look at class files in the Jar file.
79
381
guard entry. getName ( ) . hasSuffix ( " .class " ) else {
@@ -99,11 +401,10 @@ extension SwiftJava {
99
401
let javaCanonicalName = String ( entry. getName ( ) . replacing ( " / " , with: " . " )
100
402
. dropLast ( " .class " . count) )
101
403
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
107
408
}
108
409
109
410
if configuration. classes ? [ javaCanonicalName] != nil {
@@ -117,4 +418,9 @@ extension SwiftJava {
117
418
}
118
419
}
119
420
421
+ }
422
+
423
+ package func fileOrDirectoryExists( at path: String ) -> Bool {
424
+ var isDirectory : ObjCBool = false
425
+ return FileManager . default. fileExists ( atPath: path, isDirectory: & isDirectory)
120
426
}
0 commit comments